diff --git a/.changeset/quick-gorillas-grin.md b/.changeset/quick-gorillas-grin.md
new file mode 100644
index 00000000000..a845151cc84
--- /dev/null
+++ b/.changeset/quick-gorillas-grin.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/.github/actions/init/action.yml b/.github/actions/init/action.yml
index e52e50fc912..72411241c94 100644
--- a/.github/actions/init/action.yml
+++ b/.github/actions/init/action.yml
@@ -10,10 +10,6 @@ inputs:
description: 'Enable Playwright?'
required: false
default: 'false'
- cache-suffix:
- description: 'The cache suffix to use'
- required: false
- default: ''
turbo-summarize:
description: 'The token to use for Turbo task summaries'
required: false
@@ -97,23 +93,12 @@ runs:
- name: Setup NodeJS ${{ inputs.node-version }}
uses: actions/setup-node@v4
with:
+ cache: 'npm'
node-version: ${{ inputs.node-version }}
registry-url: ${{ inputs.registry-url }}
- - name: Cache node_modules
- uses: actions/cache@v3
- id: npm-cache
- with:
- path: ./node_modules
- key: ${{ runner.os }}-node-${{ inputs.node-version }}-${{ hashFiles('.github/.cache-version') }}-node-modules-${{ hashFiles('package-lock.json') }}${{ inputs.cache-suffix }}
-
- name: Install NPM Dependencies
- if: steps.npm-cache.outputs.cache-hit != 'true'
- run: npm ci --audit=false --fund=false
- shell: bash
-
- - name: Lint GitHub Actions Workflows
- run: npx eslint .github
+ run: npm ci --audit=false --fund=false --prefer-offline
shell: bash
- name: Get Playwright Version
@@ -128,7 +113,7 @@ runs:
id: playwright-cache
with:
path: ~/.cache/ms-playwright
- key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-${{ hashFiles('.github/.cache-version') }}-playwright-${{ steps.playwright-version.outputs.VERSION }}
+ key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-playwright-${{ steps.playwright-version.outputs.VERSION }}
- name: Install Playwright Browsers
if: inputs.playwright-enabled == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5d0b3ca3411..5d0afad0afa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,11 +39,19 @@ jobs:
turbo-team: ${{ vars.TURBO_TEAM }}
turbo-token: ${{ secrets.TURBO_TOKEN }}
+ - name: List node_modules
+ run: npm ls
+ shell: bash
+
- name: Require Changeset
timeout-minutes: ${{ fromJSON(vars.TIMEOUT_MINUTES_SHORT) }}
if: ${{ !(github.event_name == 'merge_group') }}
run: if [ "${{ github.event.pull_request.user.login }}" = "clerk-cookie" ]; then echo 'Skipping' && exit 0; else npx changeset status --since=origin/main; fi
+ - name: Lint GitHub Actions Workflows
+ run: npx eslint .github
+ shell: bash
+
- name: Check Formatting
timeout-minutes: ${{ fromJSON(vars.TIMEOUT_MINUTES_SHORT) }}
run: npm run format:check
diff --git a/.github/workflows/preview.retheme.yml b/.github/workflows/preview.retheme.yml
index a443321a05c..6e584342631 100644
--- a/.github/workflows/preview.retheme.yml
+++ b/.github/workflows/preview.retheme.yml
@@ -29,7 +29,6 @@ jobs:
id: config
uses: ./.github/actions/init
with:
- cache-suffix: "-retheme"
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
turbo-team: ${{ vars.TURBO_TEAM }}
turbo-token: ${{ secrets.TURBO_TOKEN }}
diff --git a/package-lock.json b/package-lock.json
index 9dd06d27117..b3b97d4b917 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5866,6 +5866,134 @@
"node": ">= 10"
}
},
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "14.0.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz",
+ "integrity": "sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "14.0.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz",
+ "integrity": "sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "14.0.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz",
+ "integrity": "sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "14.0.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz",
+ "integrity": "sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "14.0.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz",
+ "integrity": "sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "14.0.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz",
+ "integrity": "sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "14.0.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz",
+ "integrity": "sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "14.0.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz",
+ "integrity": "sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"license": "MIT",
@@ -32767,9 +32895,10 @@
}
},
"node_modules/xstate": {
- "version": "4.38.2",
+ "version": "4.38.3",
+ "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.38.3.tgz",
+ "integrity": "sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==",
"dev": true,
- "license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/xstate"
@@ -33292,6 +33421,7 @@
"dependencies": {
"@clerk/nextjs": "^5.0.0-alpha-v5.12",
"@radix-ui/react-form": "^0.0.3",
+ "@radix-ui/react-slot": "^1.0.2",
"@xstate/react": "^4.0.1",
"clsx": "^2.0.0",
"react-children-utilities": "^2.9.0",
diff --git a/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx b/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx
index 9b17f599a2b..e598afa5cb9 100644
--- a/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx
+++ b/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx
@@ -1,18 +1,63 @@
-import { SignIn, SignInFactorOne, SignInFactorTwo, SignInSSOCallback, SignInStart } from '@clerk/elements';
+import {
+ Field,
+ Form,
+ Input,
+ Label,
+ SignIn,
+ SignInFactorOne,
+ SignInFactorTwo,
+ SignInSSOCallback,
+ SignInStart,
+ Submit,
+} from '@clerk/elements';
+
+import { Debug } from '@/components/debug';
export default function SignInPage() {
return (
-
- Start child
-
-
- Factor one child
-
-
- Factor two child
-
-
+
+
+ Start
+
+
+
+
+ Factor one child
+
+
+ Factor two child
+
+
+
+
+
);
}
diff --git a/packages/elements/examples/nextjs/components/debug.tsx b/packages/elements/examples/nextjs/components/debug.tsx
new file mode 100644
index 00000000000..d5c56c6320f
--- /dev/null
+++ b/packages/elements/examples/nextjs/components/debug.tsx
@@ -0,0 +1,45 @@
+'use client';
+
+import { SignedIn } from '@clerk/clerk-react';
+import { useSignInFlow } from '@clerk/elements';
+import { SignOutButton } from '@clerk/nextjs';
+
+export function Button(props: React.ComponentProps<'button'>) {
+ return (
+
+ );
+}
+
+export function Debug() {
+ const ref = useSignInFlow();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/elements/package.json b/packages/elements/package.json
index da93239f88c..c166efe1dbd 100644
--- a/packages/elements/package.json
+++ b/packages/elements/package.json
@@ -60,6 +60,7 @@
"dependencies": {
"@clerk/nextjs": "^5.0.0-alpha-v5.12",
"@radix-ui/react-form": "^0.0.3",
+ "@radix-ui/react-slot": "^1.0.2",
"@xstate/react": "^4.0.1",
"clsx": "^2.0.0",
"react-children-utilities": "^2.9.0",
diff --git a/packages/elements/src/common/form.tsx b/packages/elements/src/common/form.tsx
new file mode 100644
index 00000000000..444493a16b7
--- /dev/null
+++ b/packages/elements/src/common/form.tsx
@@ -0,0 +1,33 @@
+import type { FormControlProps, FormProps } from '@radix-ui/react-form';
+import { Control, Field, Form as RadixForm, Label, Submit } from '@radix-ui/react-form';
+import { Slot } from '@radix-ui/react-slot';
+
+import { useForm, useInput } from '../internals/machines/sign-in.context';
+
+function Input({ asChild, ...rest }: FormControlProps) {
+ const { type, value } = rest;
+ const field = useInput({ type, value });
+
+ const Comp = asChild ? Slot : Control;
+
+ return (
+
+ );
+}
+
+function Form({ asChild, ...rest }: FormProps) {
+ const form = useForm();
+
+ const Comp = asChild ? Slot : RadixForm;
+ return (
+
+ );
+}
+
+export { Form, Input, Field, Label, Submit };
diff --git a/packages/elements/src/index.tsx b/packages/elements/src/index.tsx
index a1cc5e46c38..0fec957e5b8 100644
--- a/packages/elements/src/index.tsx
+++ b/packages/elements/src/index.tsx
@@ -2,5 +2,8 @@
import { useNextRouter } from './internals/router';
export * from './sign-in';
+export * from './common/form';
+
+export { useSignInFlow, useSignInFlowSelector } from './internals/machines/sign-in.context';
export { useNextRouter };
diff --git a/packages/elements/src/internals/machines/sign-in.context.ts b/packages/elements/src/internals/machines/sign-in.context.ts
index e3467bfa7c0..54f96d7e1bf 100644
--- a/packages/elements/src/internals/machines/sign-in.context.ts
+++ b/packages/elements/src/internals/machines/sign-in.context.ts
@@ -1,5 +1,94 @@
import { createActorContext } from '@xstate/react';
+import { useCallback, useEffect } from 'react';
+import type { SnapshotFrom } from 'xstate';
import { SignInMachine } from './sign-in.machine';
+import type { FieldDetails } from './sign-in.types';
-export const SignInActor = createActorContext(SignInMachine);
+export const {
+ Provider: SignInFlowProvider,
+ useActorRef: useSignInFlow,
+ useSelector: useSignInFlowSelector,
+} = createActorContext(SignInMachine);
+
+// TODO: Move selectors
+const fieldHasValueSelector = (type: string | undefined) => (state: SnapshotFrom) =>
+ type ? Boolean(state.context.fields.get(type)?.value) : false;
+
+const fieldErrorSelector = (type: string | undefined) => (state: SnapshotFrom) =>
+ type ? Boolean(state.context.fields.get(type)?.error) : undefined;
+
+const globalErrorSelector = (state: SnapshotFrom) => state.context.error;
+
+export const useField = ({ type }: Partial>) => {
+ const hasValue = useSignInFlowSelector(fieldHasValueSelector(type));
+ const error = useSignInFlowSelector(fieldErrorSelector(type));
+
+ const shouldBeHidden = false; // TODO: Implement clerk-js utils
+ const validity = error ? 'invalid' : 'valid';
+
+ return {
+ hasValue,
+ props: {
+ [`data-${validity}`]: true,
+ 'data-hidden': shouldBeHidden ? true : undefined,
+ tabIndex: shouldBeHidden ? -1 : 0,
+ },
+ };
+};
+
+export const useInput = ({ type, value: initialValue }: Partial>) => {
+ const ref = useSignInFlow();
+ const hasValue = useSignInFlowSelector(fieldHasValueSelector(type));
+
+ useEffect(() => {
+ if (!type || ref.getSnapshot().context.fields.get(type)) return;
+
+ ref.send({ type: 'FIELD.ADD', field: { type, value: initialValue } });
+
+ return () => ref.send({ type: 'FIELD.REMOVE', field: { type } });
+ }, [ref]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const onChange = useCallback(
+ (event: React.ChangeEvent) => {
+ if (!type) return;
+ ref.send({ type: 'FIELD.UPDATE', field: { type, value: event.target.value } });
+ },
+ [ref, type],
+ );
+
+ // TODO: Implement clerk-js utils
+ const shouldBeHidden = false;
+
+ return {
+ hasValue,
+ props: {
+ 'data-hidden': shouldBeHidden ? true : undefined,
+ 'data-has-value': hasValue ? true : undefined,
+ onChange,
+ tabIndex: shouldBeHidden ? -1 : 0,
+ },
+ };
+};
+
+export const useForm = () => {
+ const ref = useSignInFlow();
+ const error = useSignInFlowSelector(globalErrorSelector);
+
+ const validity = error ? 'invalid' : 'valid';
+
+ const onSubmit = useCallback(
+ (event: React.FormEvent) => {
+ event.preventDefault();
+ ref.send({ type: 'SUBMIT' });
+ },
+ [ref],
+ );
+
+ return {
+ props: {
+ [`data-${validity}`]: true,
+ onSubmit,
+ },
+ };
+};
diff --git a/packages/elements/src/internals/machines/sign-in.machine.ts b/packages/elements/src/internals/machines/sign-in.machine.ts
index b68cf133033..c3ff6c97e22 100644
--- a/packages/elements/src/internals/machines/sign-in.machine.ts
+++ b/packages/elements/src/internals/machines/sign-in.machine.ts
@@ -11,7 +11,36 @@ import {
prepareFirstFactor,
prepareSecondFactor,
} from './sign-in.actors';
-import type { SignInClient } from './sign-in.types';
+import type { FieldDetails, SignInClient } from './sign-in.types';
+
+export interface SignInMachineContext {
+ client: SignInClient;
+ router: ClerkHostRouter;
+ error?: Error | ClerkAPIResponseError;
+ resource?: SignInResource;
+ fields: Map;
+}
+
+export interface SignInMachineInput {
+ client: SignInClient;
+ router: ClerkHostRouter;
+}
+
+export type SignInMachineEvents =
+ | { type: 'START' }
+ | { type: 'SUBMIT' }
+ | { type: 'NEXT' }
+ | { type: 'RETRY' }
+ | { type: 'FIELD.ADD'; field: Pick }
+ | { type: 'FIELD.REMOVE'; field: Pick }
+ | {
+ type: 'FIELD.UPDATE';
+ field: Pick;
+ }
+ | {
+ type: 'FIELD.ERROR';
+ field: Pick;
+ };
export const STATES = {
Init: 'Init',
@@ -58,7 +87,7 @@ export const SignInMachine = setup({
navigateTo: ({ context }, { path }: { path: string }) => context.router.replace(path),
clearFields: assign({
- fields: {},
+ fields: new Map(),
}),
},
guards: {
@@ -71,27 +100,68 @@ export const SignInMachine = setup({
: false,
},
types: {
- context: {} as {
- client: SignInClient;
- router: ClerkHostRouter;
- error?: Error | ClerkAPIResponseError;
- resource?: SignInResource;
- fields: Record;
- },
- input: {} as {
- client: SignInClient;
- router: ClerkHostRouter;
- },
- events: {} as { type: 'START' } | { type: 'SUBMIT' } | { type: 'NEXT' } | { type: 'RETRY' } | { type: 'ASSIGN' },
+ context: {} as SignInMachineContext,
+ input: {} as SignInMachineInput,
+ events: {} as SignInMachineEvents,
},
}).createMachine({
context: ({ input }) => ({
client: input.client,
router: input.router,
currentFactor: null,
- fields: {},
+ fields: new Map(),
}),
initial: STATES.Init,
+ on: {
+ 'FIELD.ADD': {
+ actions: assign({
+ fields: ({ context, event }) => {
+ if (!event.field.type) throw new Error('Field type is required');
+
+ context.fields.set(event.field.type, event.field);
+ return context.fields;
+ },
+ }),
+ },
+ 'FIELD.UPDATE': {
+ actions: assign({
+ fields: ({ context, event }) => {
+ if (!event.field.type) throw new Error('Field type is required');
+
+ if (context.fields.has(event.field.type)) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ context.fields.get(event.field.type)!.value = event.field.value;
+ }
+
+ return context.fields;
+ },
+ }),
+ },
+ 'FIELD.REMOVE': {
+ actions: assign({
+ fields: ({ context, event }) => {
+ if (!event.field.type) throw new Error('Field type is required');
+
+ context.fields.delete(event.field.type);
+ return context.fields;
+ },
+ }),
+ },
+ 'FIELD.ERROR': {
+ actions: assign({
+ fields: ({ context, event }) => {
+ if (!event.field.type) throw new Error('Field type is required');
+
+ if (context.fields.has(event.field.type)) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ context.fields.get(event.field.type)!.error = event.field.error;
+ }
+
+ return context.fields;
+ },
+ }),
+ },
+ },
states: {
[STATES.Init]: {
always: 'Start',
@@ -110,8 +180,8 @@ export const SignInMachine = setup({
input: ({ context }) => ({
client: context.client,
params: {
- identifier: 'tom@clerk.dev',
- password: 'tom@clerk.dev',
+ identifier: context.fields.get('identifier')?.value as string,
+ password: context.fields.get('identifier')?.value as string,
strategy: 'password',
},
}),
diff --git a/packages/elements/src/internals/machines/sign-in.types.ts b/packages/elements/src/internals/machines/sign-in.types.ts
index 526335aceca..c6f873868f0 100644
--- a/packages/elements/src/internals/machines/sign-in.types.ts
+++ b/packages/elements/src/internals/machines/sign-in.types.ts
@@ -1,5 +1,11 @@
import type { ClientResource } from '@clerk/types';
+export type FieldDetails = {
+ type?: string;
+ value?: string | readonly string[] | number;
+ error?: Error;
+};
+
export type SignInClient = ClientResource;
export type SignInResourceParams = {
diff --git a/packages/elements/src/sign-in/index.tsx b/packages/elements/src/sign-in/index.tsx
index ce8c432f39b..0b3fa247f86 100644
--- a/packages/elements/src/sign-in/index.tsx
+++ b/packages/elements/src/sign-in/index.tsx
@@ -3,7 +3,7 @@
import { useClerk } from '@clerk/nextjs';
import { useEffect } from 'react';
-import { SignInActor } from '../internals/machines/sign-in.context';
+import { SignInFlowProvider, useSignInFlow } from '../internals/machines/sign-in.context';
import { useNextRouter } from '../internals/router';
import { Route, Router } from '../internals/router-react';
@@ -25,43 +25,23 @@ export function SignIn({ children }: { children: React.ReactNode }): JSX.Element
router={router}
basePath='/sign-in'
>
- {children}
+ {children}
);
}
-export function SubmitButton() {
- const ref = SignInActor.useActorRef();
- // const fields = SignInRootMachine.useSelector((state) => state.context.fields['identifier']);
- // console.log(ref.getSnapshot());
-
- return (
-
- );
-}
-
export function SignInStartInner({ children }: { children: React.ReactNode }) {
- const ref = SignInActor.useActorRef();
+ const ref = useSignInFlow();
useEffect(() => ref.send({ type: 'START' }), [ref]);
- return <>{children}>;
+ return children;
}
export function SignInStart({ children }: { children: React.ReactNode }) {
return (
-
- Start
- {children}
-
-
+ {children}
);
}