diff --git a/.changeset/olive-points-own.md b/.changeset/olive-points-own.md new file mode 100644 index 00000000000..fbdb020ee3c --- /dev/null +++ b/.changeset/olive-points-own.md @@ -0,0 +1,5 @@ +--- +'@clerk/elements': patch +--- + +Add appropriate messaging for an invalid instance configuration vs implemented Elements strategies diff --git a/.changeset/violet-lobsters-scream.md b/.changeset/violet-lobsters-scream.md new file mode 100644 index 00000000000..f533bcefc06 --- /dev/null +++ b/.changeset/violet-lobsters-scream.md @@ -0,0 +1,5 @@ +--- +'@clerk/elements': patch +--- + +Fixes a bug where multiple verification codes were sent at once. diff --git a/packages/elements/examples/nextjs/next.config.js b/packages/elements/examples/nextjs/next.config.js index 38d9318fcc9..840f438e060 100644 --- a/packages/elements/examples/nextjs/next.config.js +++ b/packages/elements/examples/nextjs/next.config.js @@ -1,7 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { // Disable React strict mode when using the state machine inspector - reactStrictMode: process.env['NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG'] !== 'true', + reactStrictMode: false, typescript: { ignoreBuildErrors: true, }, diff --git a/packages/elements/src/internals/machines/sign-in/verification.machine.ts b/packages/elements/src/internals/machines/sign-in/verification.machine.ts index 9be31fab59a..9b6aef44ac6 100644 --- a/packages/elements/src/internals/machines/sign-in/verification.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/verification.machine.ts @@ -34,9 +34,11 @@ type Parent = ActorRefFrom; export type PrepareFirstFactorInput = WithParams & { parent: Parent; + resendable: boolean; }; export type PrepareSecondFactorInput = WithParams & { parent: Parent; + resendable: boolean; }; export type AttemptFirstFactorInput = { parent: Parent; fields: FormFields; currentFactor: SignInFirstFactor | null }; @@ -109,11 +111,16 @@ const SignInVerificationMachine = setup({ if ( process.env.NODE_ENV === 'development' && + context.currentFactor?.strategy && !context.registeredStrategies.has(context.currentFactor?.strategy as unknown as SignInFactor) ) { throw new ClerkElementsRuntimeError( `Your sign-in attempt is missing a ${context.currentFactor?.strategy} strategy. Make sure is rendered in your flow. For more information, visit the documentation: https://clerk.com/docs/elements/reference/sign-in#strategy`, ); + } else if (process.env.NODE_ENV === 'development' && !context.currentFactor?.strategy) { + throw new ClerkElementsRuntimeError( + 'Unable to determine an authentication strategy to verify. This means your instance is misconfigured. Visit the Clerk Dashboard and verify that your instance has authentication strategies enabled: https://dashboard.clerk.com/last-active?path=/user-authentication/email-phone-username', + ); } }, sendToNext: ({ context, event }) => @@ -171,6 +178,7 @@ const SignInVerificationMachine = setup({ src: 'prepare', input: ({ context }) => ({ parent: context.parent, + resendable: context.resendable, params: context.currentFactor as SignInFirstFactor | null, }), onDone: { @@ -267,10 +275,14 @@ const SignInVerificationMachine = setup({ export const SignInFirstFactorMachine = SignInVerificationMachine.provide({ actors: { prepare: fromPromise(async ({ input }) => { - const { params, parent } = input; + const { params, parent, resendable } = input; const clerk = parent.getSnapshot().context.clerk; - if (!params?.strategy || params.strategy === 'password') { + // If a prepare call has already been fired recently, don't re-send + const currentVerificationExpiration = clerk.client.signIn.firstFactorVerification.expireAt; + const needsPrepare = resendable || !currentVerificationExpiration || currentVerificationExpiration < new Date(); + + if (!params?.strategy || params.strategy === 'password' || !needsPrepare) { return Promise.resolve(clerk.client.signIn); } @@ -359,12 +371,16 @@ export const SignInFirstFactorMachine = SignInVerificationMachine.provide({ export const SignInSecondFactorMachine = SignInVerificationMachine.provide({ actors: { prepare: fromPromise(({ input }) => { - const { params, parent } = input; + const { params, parent, resendable } = input; const clerk = parent.getSnapshot().context.clerk; + // If a prepare call has already been fired recently, don't re-send + const currentVerificationExpiration = clerk.client.signIn.secondFactorVerification.expireAt; + const needsPrepare = resendable || !currentVerificationExpiration || currentVerificationExpiration < new Date(); + assertIsDefined(params); - if (params.strategy !== 'phone_code') { + if (params.strategy !== 'phone_code' || !needsPrepare) { return Promise.resolve(clerk.client.signIn); }