diff --git a/.changeset/odd-coins-carry.md b/.changeset/odd-coins-carry.md new file mode 100644 index 00000000000..d2e7f8d811e --- /dev/null +++ b/.changeset/odd-coins-carry.md @@ -0,0 +1,10 @@ +--- +'@clerk/elements': patch +--- + +This release includes various smaller fixes and one dependency update: + +- `xstate` was updated from `5.12.0` to `5.13.0` +- Previously, the contents of the `fallback` prop were sometimes shown even if the user wasn't on the `start` step. This bug is fixed now. +- Upon completion of an sign-in/sign-up attempt, don't immediately return to the `start` step. This fixes the issue of a "flash of content" that could e.g. be seen during sign-in with OAuth providers. +- Some underlying fixes in Clerk Elements' XState logic were applied to make sure that during a sign-in/sign-up attempt the state is properly maintained. For example, if you visit an already completed attempt (some step of that flow) it now properly keeps track of that state. diff --git a/package-lock.json b/package-lock.json index 012dd48c1be..9599a2b80e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38410,7 +38410,7 @@ "@statelyai/inspect": "^0.3.0", "@xstate/react": "^4.1.1", "client-only": "^0.0.1", - "xstate": "^5.12.0" + "xstate": "^5.13.0" }, "devDependencies": { "@clerk/clerk-react": "5.0.4", @@ -38629,9 +38629,9 @@ } }, "packages/elements/node_modules/xstate": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.12.0.tgz", - "integrity": "sha512-4W/Hj553mwVnTLQ1itc3rni/cGtM5OkjyavTjaxCelc0ZZKE/ks6tYllc98KbekIoUrEPm4CJH/wTB5p5pPGEw==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.13.0.tgz", + "integrity": "sha512-Z0om784N5u8sAzUvQJBa32jiTCIGGF/2ZsmKkerQEqeeUktAeOMK20FIHFUMywC4GcAkNksSvaeX7lwoRNXPEQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/xstate" diff --git a/packages/elements/package.json b/packages/elements/package.json index be75485282b..17f3c2d6792 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -74,7 +74,7 @@ "@statelyai/inspect": "^0.3.0", "@xstate/react": "^4.1.1", "client-only": "^0.0.1", - "xstate": "^5.12.0" + "xstate": "^5.13.0" }, "devDependencies": { "@clerk/clerk-react": "5.0.4", diff --git a/packages/elements/src/internals/machines/sign-in/router.machine.ts b/packages/elements/src/internals/machines/sign-in/router.machine.ts index b8ea5b6a15b..8356414e455 100644 --- a/packages/elements/src/internals/machines/sign-in/router.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/router.machine.ts @@ -13,6 +13,7 @@ import { ClerkElementsError, ClerkElementsRuntimeError } from '~/internals/error import { ThirdPartyMachine, ThirdPartyMachineId } from '~/internals/machines/third-party'; import { shouldUseVirtualRouting } from '~/internals/machines/utils/next'; +import { FormMachine } from '../form'; import { SignInResetPasswordMachine } from './reset-password.machine'; import type { SignInRouterContext, @@ -41,6 +42,7 @@ export const SignInRouterMachineId = 'SignInRouter'; export const SignInRouterMachine = setup({ actors: { firstFactorMachine: SignInFirstFactorMachine, + formMachine: FormMachine, resetPasswordMachine: SignInResetPasswordMachine, startMachine: SignInStartMachine, secondFactorMachine: SignInSecondFactorMachine, @@ -60,8 +62,8 @@ export const SignInRouterMachine = setup({ }, navigateExternal: ({ context }, { path }: { path: string }) => context.router?.push(path), raiseNext: raise({ type: 'NEXT' }), - setActive({ context, event }) { - if (context.exampleMode) return; + setActive: enqueueActions(({ enqueue, check, context, event }) => { + if (check('isExampleMode')) return; const lastActiveSessionId = context.clerk.client.lastActiveSessionId; const createdSessionId = ((event as SignInRouterNextEvent)?.resource || context.clerk.client.signIn) @@ -71,7 +73,9 @@ export const SignInRouterMachine = setup({ const beforeEmit = () => context.router?.push(context.clerk.buildAfterSignInUrl()); void context.clerk.setActive({ session, beforeEmit }); - }, + + enqueue.raise({ type: 'RESET' }, { delay: 2000 }); // Reset machine after 2s delay. + }), setError: assign({ error: (_, { error }: { error?: ClerkElementsError }) => { if (error) return error; @@ -182,6 +186,7 @@ export const SignInRouterMachine = setup({ }, })), }, + RESET: '.Idle', }, states: { Idle: { @@ -221,6 +226,10 @@ export const SignInRouterMachine = setup({ guard: 'needsCallback', target: 'Callback', }, + { + guard: 'isComplete', + actions: 'setActive', + }, { guard: 'isLoggedInAndSingleSession', actions: [ @@ -280,7 +289,6 @@ export const SignInRouterMachine = setup({ { guard: 'isComplete', actions: 'setActive', - target: 'Start', }, { guard: 'statusNeedsFirstFactor', @@ -318,7 +326,6 @@ export const SignInRouterMachine = setup({ { guard: 'isComplete', actions: 'setActive', - target: 'Start', }, { guard: 'statusNeedsSecondFactor', @@ -385,7 +392,6 @@ export const SignInRouterMachine = setup({ { guard: 'isComplete', actions: 'setActive', - target: 'Start', }, { guard: 'statusNeedsNewPassword', @@ -413,7 +419,6 @@ export const SignInRouterMachine = setup({ { guard: 'isComplete', actions: 'setActive', - target: 'Start', }, { guard: 'statusNeedsFirstFactor', @@ -439,9 +444,8 @@ export const SignInRouterMachine = setup({ target: 'Start', }, { - guard: or(['isComplete', 'hasAuthenticatedViaClerkJS']), + guard: or(['isLoggedIn', 'isComplete', 'hasAuthenticatedViaClerkJS']), actions: 'setActive', - target: 'Start', }, { guard: 'statusNeedsIdentifier', diff --git a/packages/elements/src/internals/machines/sign-in/router.types.ts b/packages/elements/src/internals/machines/sign-in/router.types.ts index 50a9257d6da..ada057c95c9 100644 --- a/packages/elements/src/internals/machines/sign-in/router.types.ts +++ b/packages/elements/src/internals/machines/sign-in/router.types.ts @@ -10,6 +10,7 @@ import type { BaseRouterNextEvent, BaseRouterPrevEvent, BaseRouterRedirectEvent, + BaseRouterResetEvent, BaseRouterSetClerkEvent, BaseRouterStartEvent, BaseRouterTransferEvent, @@ -54,6 +55,7 @@ export type SignInRouterForgotPasswordEvent = { type: 'NAVIGATE.FORGOT_PASSWORD' export type SignInRouterErrorEvent = BaseRouterErrorEvent; export type SignInRouterTransferEvent = BaseRouterTransferEvent; export type SignInRouterRedirectEvent = BaseRouterRedirectEvent; +export type SignInRouterResetEvent = BaseRouterResetEvent; export type SignInRouterLoadingEvent = BaseRouterLoadingEvent<'start' | 'verifications' | 'reset-password'>; export type SignInRouterSetClerkEvent = BaseRouterSetClerkEvent; export type SignInRouterSubmitEvent = { type: 'SUBMIT' }; @@ -77,6 +79,7 @@ export type SignInRouterEvents = | SignInRouterErrorEvent | SignInRouterTransferEvent | SignInRouterRedirectEvent + | SignInRouterResetEvent | SignInVerificationFactorUpdateEvent | SignInRouterLoadingEvent | SignInRouterSetClerkEvent diff --git a/packages/elements/src/internals/machines/sign-up/router.machine.ts b/packages/elements/src/internals/machines/sign-up/router.machine.ts index 9359ff57862..8659db35265 100644 --- a/packages/elements/src/internals/machines/sign-up/router.machine.ts +++ b/packages/elements/src/internals/machines/sign-up/router.machine.ts @@ -1,7 +1,7 @@ import { joinURL } from '@clerk/shared/url'; import type { SignUpStatus, VerificationStatus } from '@clerk/types'; import type { NonReducibleUnknown } from 'xstate'; -import { and, assign, log, not, or, raise, sendTo, setup, spawnChild } from 'xstate'; +import { and, assign, enqueueActions, log, not, or, raise, sendTo, setup, spawnChild } from 'xstate'; import { ERROR_CODES, @@ -59,15 +59,20 @@ export const SignUpRouterMachine = setup({ }, navigateExternal: ({ context }, { path }: { path: string }) => context.router?.push(path), raiseNext: raise({ type: 'NEXT' }), - setActive({ context, event }, params?: { sessionId?: string; useLastActiveSession?: boolean }) { - const session = - params?.sessionId || - (params?.useLastActiveSession && context.clerk.client.lastActiveSessionId) || - ((event as SignUpRouterNextEvent)?.resource || context.clerk.client.signUp).createdSessionId; + setActive: (_, params?: { sessionId?: string; useLastActiveSession?: boolean }) => + enqueueActions(({ enqueue, check, context, event }) => { + if (check('isExampleMode')) return; - const beforeEmit = () => context.router?.push(context.clerk.buildAfterSignUpUrl()); - void context.clerk.setActive({ session, beforeEmit }); - }, + const session = + params?.sessionId || + (params?.useLastActiveSession && context.clerk.client.lastActiveSessionId) || + ((event as SignUpRouterNextEvent)?.resource || context.clerk.client.signUp).createdSessionId; + + const beforeEmit = () => context.router?.push(context.clerk.buildAfterSignUpUrl()); + void context.clerk.setActive({ session, beforeEmit }); + + enqueue.raise({ type: 'RESET' }, { delay: 2000 }); // Reset machine after 2s delay. + }), setError: assign({ error: (_, { error }: { error?: ClerkElementsError }) => { if (error) return error; @@ -182,6 +187,7 @@ export const SignUpRouterMachine = setup({ }, })), }, + RESET: '.Idle', }, states: { Idle: { diff --git a/packages/elements/src/internals/machines/sign-up/router.types.ts b/packages/elements/src/internals/machines/sign-up/router.types.ts index 8c359f7e7ea..2ea4fd4b0b6 100644 --- a/packages/elements/src/internals/machines/sign-up/router.types.ts +++ b/packages/elements/src/internals/machines/sign-up/router.types.ts @@ -10,6 +10,7 @@ import type { BaseRouterNextEvent, BaseRouterPrevEvent, BaseRouterRedirectEvent, + BaseRouterResetEvent, BaseRouterSetClerkEvent, BaseRouterStartEvent, BaseRouterTransferEvent, @@ -46,6 +47,7 @@ export type SignUpRouterPrevEvent = BaseRouterPrevEvent; export type SignUpRouterErrorEvent = BaseRouterErrorEvent; export type SignUpRouterTransferEvent = BaseRouterTransferEvent; export type SignUpRouterRedirectEvent = BaseRouterRedirectEvent; +export type SignUpRouterResetEvent = BaseRouterResetEvent; export type SignUpRouterLoadingEvent = BaseRouterLoadingEvent<'start' | 'verifications' | 'continue'>; export type SignUpRouterSetClerkEvent = BaseRouterSetClerkEvent; @@ -64,6 +66,7 @@ export type SignUpRouterEvents = | SignUpRouterErrorEvent | SignUpRouterTransferEvent | SignUpRouterRedirectEvent + | SignUpRouterResetEvent | SignUpRouterLoadingEvent | SignUpRouterSetClerkEvent; diff --git a/packages/elements/src/internals/machines/types/router.types.ts b/packages/elements/src/internals/machines/types/router.types.ts index a4d6e7bf489..904fb56dd4e 100644 --- a/packages/elements/src/internals/machines/types/router.types.ts +++ b/packages/elements/src/internals/machines/types/router.types.ts @@ -17,6 +17,7 @@ export type BaseRouterLoadingStep = 'start' | 'verifications' | 'continue' | 're export type BaseRouterNextEvent = { type: 'NEXT'; resource?: T }; export type BaseRouterPrevEvent = { type: 'NAVIGATE.PREVIOUS' }; export type BaseRouterStartEvent = { type: 'NAVIGATE.START' }; +export type BaseRouterResetEvent = { type: 'RESET' }; export type BaseRouterErrorEvent = { type: 'ERROR'; error: Error }; export type BaseRouterTransferEvent = { type: 'TRANSFER' }; export type BaseRouterLoadingEvent = ( diff --git a/packages/elements/src/react/sign-in/root.tsx b/packages/elements/src/react/sign-in/root.tsx index 7453eb7d156..1d12978205a 100644 --- a/packages/elements/src/react/sign-in/root.tsx +++ b/packages/elements/src/react/sign-in/root.tsx @@ -44,7 +44,9 @@ function SignInFlowProvider({ children, exampleMode }: SignInFlowProviderProps) actor.send(evt); } }); - }, [clerk, exampleMode, formRef, router]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [clerk, exampleMode, formRef?.id, !!router]); return {children}; } @@ -94,6 +96,7 @@ export function SignInRoot({ // TODO: eventually we'll rely on the framework SDK to specify its host router, but for now we'll default to Next.js const router = useNextRouter(); + const isRootPath = path === router.pathname(); return ( - -
{fallback}
-
+ {isRootPath ? ( + +
{fallback}
+
+ ) : null} {children}
diff --git a/packages/elements/src/react/sign-in/verifications.tsx b/packages/elements/src/react/sign-in/verifications.tsx index a2682fa2b6e..6b3d647ae5d 100644 --- a/packages/elements/src/react/sign-in/verifications.tsx +++ b/packages/elements/src/react/sign-in/verifications.tsx @@ -80,7 +80,7 @@ export function SignInStrategy({ children, name }: SignInStrategyProps) { } return () => { - if (factorCtx) { + if (factorCtx?.getSnapshot().status === 'active') { factorCtx.send({ type: 'STRATEGY.UNREGISTER', factor: name as unknown as SignInFactor }); } }; diff --git a/packages/elements/src/react/sign-up/root.tsx b/packages/elements/src/react/sign-up/root.tsx index 99380cd0144..bf2c9c198e3 100644 --- a/packages/elements/src/react/sign-up/root.tsx +++ b/packages/elements/src/react/sign-up/root.tsx @@ -46,7 +46,8 @@ function SignUpFlowProvider({ children, exampleMode }: SignUpFlowProviderProps) ref.send(evt); } }); - }, [clerk, exampleMode, formRef, router]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [clerk, exampleMode, formRef?.id, !!router]); return isReady ? {children} : null; } @@ -91,6 +92,7 @@ export function SignUpRoot({ // TODO: eventually we'll rely on the framework SDK to specify its host router, but for now we'll default to Next.js const router = useNextRouter(); + const isRootPath = path === router.pathname(); return ( - -
{fallback}
-
+ {isRootPath ? ( + +
{fallback}
+
+ ) : null} {children}