From 3a2f5c04edb7fe81e1982a2c4e43aa01544e2a20 Mon Sep 17 00:00:00 2001 From: Eli Front Date: Fri, 19 Sep 2025 11:35:52 -0500 Subject: [PATCH 1/5] fix: handle waitlist restricted error & don't clear errors always on exit to prevent early clear --- .../elements/src/internals/constants/index.ts | 1 + .../machines/sign-in/router.machine.ts | 47 +++++++++---------- .../machines/sign-up/router.machine.ts | 45 +++++++++--------- 3 files changed, 44 insertions(+), 49 deletions(-) diff --git a/packages/elements/src/internals/constants/index.ts b/packages/elements/src/internals/constants/index.ts index 135bdfd35aa..1cf45290c01 100644 --- a/packages/elements/src/internals/constants/index.ts +++ b/packages/elements/src/internals/constants/index.ts @@ -48,6 +48,7 @@ export const ERROR_CODES = { FORM_PASSWORD_INCORRECT: 'form_password_incorrect', INVALID_STRATEGY_FOR_USER: 'strategy_for_user_invalid', NOT_ALLOWED_TO_SIGN_UP: 'not_allowed_to_sign_up', + SIGN_UP_RESTRICTED_WAITLIST: 'sign_up_restricted_waitlist', OAUTH_ACCESS_DENIED: 'oauth_access_denied', OAUTH_EMAIL_DOMAIN_RESERVED_BY_SAML: 'oauth_email_domain_reserved_by_saml', NOT_ALLOWED_ACCESS: 'not_allowed_access', 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 d012894beb1..14bca8a1365 100644 --- a/packages/elements/src/internals/machines/sign-in/router.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/router.machine.ts @@ -33,14 +33,14 @@ export type TSignInRouterMachine = typeof SignInRouterMachine; const isCurrentPath = (path: `/${string}`) => - ({ context }: { context: SignInRouterContext }, _params?: NonReducibleUnknown) => { - return context.router?.match(path) ?? false; - }; + ({ context }: { context: SignInRouterContext }, _params?: NonReducibleUnknown) => { + return context.router?.match(path) ?? false; + }; const needsStatus = (status: SignInStatus) => - ({ context, event }: { context: SignInRouterContext; event?: SignInRouterEvents }, _?: NonReducibleUnknown) => - (event as SignInRouterNextEvent)?.resource?.status === status || context.clerk?.client.signIn.status === status; + ({ context, event }: { context: SignInRouterContext; event?: SignInRouterEvents }, _?: NonReducibleUnknown) => + (event as SignInRouterNextEvent)?.resource?.status === status || context.clerk?.client.signIn.status === status; export const SignInRouterMachineId = 'SignInRouter'; @@ -131,6 +131,7 @@ export const SignInRouterMachine = setup({ case ERROR_CODES.ENTERPRISE_SSO_HOSTED_DOMAIN_MISMATCH: case ERROR_CODES.SAML_EMAIL_ADDRESS_DOMAIN_MISMATCH: case ERROR_CODES.ORGANIZATION_MEMBERSHIP_QUOTA_EXCEEDED_FOR_SSO: + case ERROR_CODES.SIGN_UP_RESTRICTED_WAITLIST: error = new ClerkElementsError(errorOrig.code, errorOrig.longMessage || ''); break; default: @@ -196,11 +197,10 @@ export const SignInRouterMachine = setup({ type: 'REDIRECT', params: { strategy: event.strategy, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signInUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, + redirectUrl: `${context.router?.mode === ROUTING.virtual + ? context.clerk.__unstable__environment?.displayConfig.signInUrl + : context.router?.basePath + }${SSO_CALLBACK_PATH_ROUTE}`, redirectUrlComplete: context.clerk.buildAfterSignInUrl({ params: context.router?.searchParams(), }), @@ -213,11 +213,10 @@ export const SignInRouterMachine = setup({ params: { strategy: 'saml', identifier: context.formRef.getSnapshot().context.fields.get('identifier')?.value, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signInUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, + redirectUrl: `${context.router?.mode === ROUTING.virtual + ? context.clerk.__unstable__environment?.displayConfig.signInUrl + : context.router?.basePath + }${SSO_CALLBACK_PATH_ROUTE}`, redirectUrlComplete: context.clerk.buildAfterSignInUrl({ params: context.router?.searchParams(), }), @@ -230,11 +229,10 @@ export const SignInRouterMachine = setup({ params: { strategy: 'enterprise_sso', identifier: context.formRef.getSnapshot().context.fields.get('identifier')?.value, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signInUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, + redirectUrl: `${context.router?.mode === ROUTING.virtual + ? context.clerk.__unstable__environment?.displayConfig.signInUrl + : context.router?.basePath + }${SSO_CALLBACK_PATH_ROUTE}`, redirectUrlComplete: context.clerk.buildAfterSignInUrl({ params: context.router?.searchParams(), }), @@ -371,7 +369,6 @@ export const SignInRouterMachine = setup({ }, Start: { tags: ['step:start'], - exit: 'clearFormErrors', invoke: { id: 'start', src: 'startMachine', @@ -402,21 +399,21 @@ export const SignInRouterMachine = setup({ NEXT: [ { guard: 'isComplete', - actions: 'setActive', + actions: ['setActive', 'clearFormErrors'], }, { guard: 'statusNeedsFirstFactor', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, + actions: ["clearFormErrors", { type: "navigateInternal", params: { path: "/continue" } }], target: 'FirstFactor', }, { guard: 'statusNeedsSecondFactor', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, + actions: ["clearFormErrors", { type: "navigateInternal", params: { path: "/continue" } }], target: 'SecondFactor', }, { guard: 'statusNeedsNewPassword', - actions: { type: 'navigateInternal', params: { path: '/reset-password' } }, + actions: ["clearFormErrors", { type: "navigateInternal", params: { path: "/reset-password" } }], target: 'ResetPassword', }, ], 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 f3dcf09b8cc..2f6b7ca31e8 100644 --- a/packages/elements/src/internals/machines/sign-up/router.machine.ts +++ b/packages/elements/src/internals/machines/sign-up/router.machine.ts @@ -31,13 +31,13 @@ export type TSignUpRouterMachine = typeof SignUpRouterMachine; const isCurrentPath = (path: `/${string}`) => - ({ context }: { context: SignUpRouterContext }, _params?: NonReducibleUnknown) => - context.router?.match(path) ?? false; + ({ context }: { context: SignUpRouterContext }, _params?: NonReducibleUnknown) => + context.router?.match(path) ?? false; const needsStatus = (status: SignUpStatus) => - ({ context, event }: { context: SignUpRouterContext; event?: SignUpRouterEvents }, _?: NonReducibleUnknown) => - (event as SignUpRouterNextEvent)?.resource?.status === status || context.clerk?.client?.signUp?.status === status; + ({ context, event }: { context: SignUpRouterContext; event?: SignUpRouterEvents }, _?: NonReducibleUnknown) => + (event as SignUpRouterNextEvent)?.resource?.status === status || context.clerk?.client?.signUp?.status === status; export const SignUpRouterMachine = setup({ actors: { @@ -116,6 +116,7 @@ export const SignUpRouterMachine = setup({ case ERROR_CODES.ENTERPRISE_SSO_HOSTED_DOMAIN_MISMATCH: case ERROR_CODES.SAML_EMAIL_ADDRESS_DOMAIN_MISMATCH: case ERROR_CODES.ORGANIZATION_MEMBERSHIP_QUOTA_EXCEEDED_FOR_SSO: + case ERROR_CODES.SIGN_UP_RESTRICTED_WAITLIST: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion error = new ClerkElementsError(errorOrig.code, errorOrig.longMessage!); break; @@ -197,11 +198,10 @@ export const SignUpRouterMachine = setup({ type: 'REDIRECT', params: { strategy: event.strategy, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signUpUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, + redirectUrl: `${context.router?.mode === ROUTING.virtual + ? context.clerk.__unstable__environment?.displayConfig.signUpUrl + : context.router?.basePath + }${SSO_CALLBACK_PATH_ROUTE}`, redirectUrlComplete: context.clerk.buildAfterSignUpUrl({ params: context.router?.searchParams(), }), @@ -214,11 +214,10 @@ export const SignUpRouterMachine = setup({ params: { strategy: 'saml', emailAddress: context.formRef.getSnapshot().context.fields.get('emailAddress')?.value, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signUpUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, + redirectUrl: `${context.router?.mode === ROUTING.virtual + ? context.clerk.__unstable__environment?.displayConfig.signUpUrl + : context.router?.basePath + }${SSO_CALLBACK_PATH_ROUTE}`, redirectUrlComplete: context.clerk.buildAfterSignUpUrl({ params: context.router?.searchParams(), }), @@ -231,11 +230,10 @@ export const SignUpRouterMachine = setup({ params: { strategy: 'enterprise_sso', emailAddress: context.formRef.getSnapshot().context.fields.get('emailAddress')?.value, - redirectUrl: `${ - context.router?.mode === ROUTING.virtual - ? context.clerk.__unstable__environment?.displayConfig.signUpUrl - : context.router?.basePath - }${SSO_CALLBACK_PATH_ROUTE}`, + redirectUrl: `${context.router?.mode === ROUTING.virtual + ? context.clerk.__unstable__environment?.displayConfig.signUpUrl + : context.router?.basePath + }${SSO_CALLBACK_PATH_ROUTE}`, redirectUrlComplete: context.clerk.buildAfterSignUpUrl({ params: context.router?.searchParams(), }), @@ -357,7 +355,6 @@ export const SignUpRouterMachine = setup({ }, Start: { tags: ['step:start'], - exit: 'clearFormErrors', invoke: { id: 'start', src: 'startMachine', @@ -381,22 +378,22 @@ export const SignUpRouterMachine = setup({ NEXT: [ { guard: 'isStatusComplete', - actions: ['setActive', 'delayedReset'], + actions: ['setActive', 'delayedReset', 'clearFormErrors'], }, { guard: and(['hasTicket', 'statusNeedsContinue']), - actions: { type: 'navigateInternal', params: { path: '/' } }, + actions: ['clearFormErrors', { type: 'navigateInternal', params: { path: '/' } }], target: 'Start', reenter: true, }, { guard: 'statusNeedsVerification', target: 'Verification', - actions: { type: 'navigateInternal', params: { path: '/verify' } }, + actions: ['clearFormErrors', { type: 'navigateInternal', params: { path: '/verify' } }], }, { guard: 'statusNeedsContinue', - actions: { type: 'navigateInternal', params: { path: '/continue' } }, + actions: ['clearFormErrors', { type: 'navigateInternal', params: { path: '/continue' } }], target: 'Continue', }, ], From 5f42556b0582f3e7a22c7f1a6e46238b4a291611 Mon Sep 17 00:00:00 2001 From: Eli Front Date: Fri, 19 Sep 2025 12:26:50 -0500 Subject: [PATCH 2/5] fix: correctly handle errors case to prevent iterable error --- .../internals/machines/form/form.machine.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/elements/src/internals/machines/form/form.machine.ts b/packages/elements/src/internals/machines/form/form.machine.ts index 71c1cfb5bbc..9f163af8fe2 100644 --- a/packages/elements/src/internals/machines/form/form.machine.ts +++ b/packages/elements/src/internals/machines/form/form.machine.ts @@ -1,6 +1,5 @@ import { isClerkAPIResponseError, isKnownError, isMetamaskError } from '@clerk/shared/error'; import { snakeToCamel } from '@clerk/shared/underscore'; -import type { ClerkAPIError } from '@clerk/types'; import type { MachineContext } from 'xstate'; import { assign, enqueueActions, setup } from 'xstate'; @@ -40,7 +39,7 @@ export type FormMachineEvents = type: 'FIELD.UPDATE'; field: Pick; } - | { type: 'ERRORS.SET'; error: any } + | { type: 'ERRORS.SET'; error: any; errors: any[] } | { type: 'ERRORS.CLEAR' } | { type: 'FIELD.FEEDBACK.SET'; @@ -96,15 +95,22 @@ export const FormMachine = setup({ on: { 'ERRORS.SET': { actions: enqueueActions(({ enqueue, event }) => { - const isClerkAPIError = (err: any): err is ClerkAPIError => 'meta' in err; + const isClerkAPIError = (err: any) => err && typeof err === 'object' && 'meta' in err; if (isKnownError(event.error)) { + const candidate = + (event && event.error && isClerkAPIResponseError(event.error) && event.error.errors) || + (Array.isArray(event?.errors) ? event.errors : undefined) || + event?.error || + []; + + const errors = Array.isArray(candidate) ? candidate : [candidate]; + const fields: Record = {}; - const globalErrors: ClerkElementsError[] = []; - const errors = isClerkAPIResponseError(event.error) ? event.error?.errors : [event.error]; + const globalErrors = []; for (const error of errors) { - const name = isClerkAPIError(error) ? snakeToCamel(error.meta?.paramName) : null; + const name = isClerkAPIError(error) ? snakeToCamel(error?.meta?.paramName) : null; if (!name || isMetamaskError(error)) { globalErrors.push(ClerkElementsError.fromAPIError(error)); @@ -140,6 +146,7 @@ export const FormMachine = setup({ } }), }, + 'ERRORS.CLEAR': { actions: assign({ errors: () => [], From 81b3e8c53d7411e17a532103e4e3d24e477cead2 Mon Sep 17 00:00:00 2001 From: Eli Front Date: Fri, 19 Sep 2025 12:54:15 -0500 Subject: [PATCH 3/5] chore: cleanup --- .../elements/src/internals/machines/form/form.machine.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/elements/src/internals/machines/form/form.machine.ts b/packages/elements/src/internals/machines/form/form.machine.ts index 9f163af8fe2..81a566e6d0b 100644 --- a/packages/elements/src/internals/machines/form/form.machine.ts +++ b/packages/elements/src/internals/machines/form/form.machine.ts @@ -1,5 +1,6 @@ import { isClerkAPIResponseError, isKnownError, isMetamaskError } from '@clerk/shared/error'; import { snakeToCamel } from '@clerk/shared/underscore'; +import type { ClerkAPIError } from '@clerk/types'; import type { MachineContext } from 'xstate'; import { assign, enqueueActions, setup } from 'xstate'; @@ -95,11 +96,11 @@ export const FormMachine = setup({ on: { 'ERRORS.SET': { actions: enqueueActions(({ enqueue, event }) => { - const isClerkAPIError = (err: any) => err && typeof err === 'object' && 'meta' in err; + const isClerkAPIError = (err: any): err is ClerkAPIError => 'meta' in err; if (isKnownError(event.error)) { const candidate = - (event && event.error && isClerkAPIResponseError(event.error) && event.error.errors) || + (isClerkAPIResponseError(event.error) && event.error.errors) || (Array.isArray(event?.errors) ? event.errors : undefined) || event?.error || []; From 2c65c8eda15f724b2e9ac94551361fcbbba72208 Mon Sep 17 00:00:00 2001 From: Eli Front Date: Fri, 19 Sep 2025 13:17:05 -0500 Subject: [PATCH 4/5] fix: edge case --- .../src/internals/machines/form/form.machine.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/elements/src/internals/machines/form/form.machine.ts b/packages/elements/src/internals/machines/form/form.machine.ts index 81a566e6d0b..aae6c9f48d2 100644 --- a/packages/elements/src/internals/machines/form/form.machine.ts +++ b/packages/elements/src/internals/machines/form/form.machine.ts @@ -40,7 +40,7 @@ export type FormMachineEvents = type: 'FIELD.UPDATE'; field: Pick; } - | { type: 'ERRORS.SET'; error: any; errors: any[] } + | { type: 'ERRORS.SET'; error: any } | { type: 'ERRORS.CLEAR' } | { type: 'FIELD.FEEDBACK.SET'; @@ -99,11 +99,9 @@ export const FormMachine = setup({ const isClerkAPIError = (err: any): err is ClerkAPIError => 'meta' in err; if (isKnownError(event.error)) { - const candidate = - (isClerkAPIResponseError(event.error) && event.error.errors) || - (Array.isArray(event?.errors) ? event.errors : undefined) || - event?.error || - []; + const candidate = isClerkAPIResponseError(event.error) + ? event.error?.errors || event.error + : event?.error || []; const errors = Array.isArray(candidate) ? candidate : [candidate]; From 2cfd7501a235199620ac0d53ecdb9cde5f21c7a4 Mon Sep 17 00:00:00 2001 From: Eli Front Date: Fri, 19 Sep 2025 13:18:18 -0500 Subject: [PATCH 5/5] chore: cleanup --- .../elements/src/internals/machines/form/form.machine.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/elements/src/internals/machines/form/form.machine.ts b/packages/elements/src/internals/machines/form/form.machine.ts index aae6c9f48d2..ccdfcb3a875 100644 --- a/packages/elements/src/internals/machines/form/form.machine.ts +++ b/packages/elements/src/internals/machines/form/form.machine.ts @@ -99,15 +99,14 @@ export const FormMachine = setup({ const isClerkAPIError = (err: any): err is ClerkAPIError => 'meta' in err; if (isKnownError(event.error)) { + const fields: Record = {}; + const globalErrors: ClerkElementsError[] = []; const candidate = isClerkAPIResponseError(event.error) ? event.error?.errors || event.error : event?.error || []; const errors = Array.isArray(candidate) ? candidate : [candidate]; - const fields: Record = {}; - const globalErrors = []; - for (const error of errors) { const name = isClerkAPIError(error) ? snakeToCamel(error?.meta?.paramName) : null;