From 0d1caee5753dd120e8f6228b88dadd97178ea7de Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 10 Dec 2024 07:47:16 -0300 Subject: [PATCH 1/4] Update router to handle ticket param --- .../machines/sign-in/router.machine.ts | 33 +++++++++++++------ .../machines/sign-in/router.types.ts | 1 + .../internals/machines/sign-in/start.types.ts | 1 + 3 files changed, 25 insertions(+), 10 deletions(-) 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 2a923afe41b..54da5d5fe57 100644 --- a/packages/elements/src/internals/machines/sign-in/router.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/router.machine.ts @@ -8,6 +8,7 @@ import { CHOOSE_SESSION_PATH_ROUTE, ERROR_CODES, ROUTING, + SEARCH_PARAMS, SIGN_IN_DEFAULT_BASE_PATH, SIGN_UP_DEFAULT_BASE_PATH, SSO_CALLBACK_PATH_ROUTE, @@ -151,6 +152,7 @@ export const SignInRouterMachine = setup({ Boolean(context.clerk.client.signIn.status === null && context.clerk.client.lastActiveSessionId), hasOAuthError: ({ context }) => Boolean(context.clerk?.client?.signIn?.firstFactorVerification?.error), hasResource: ({ context }) => Boolean(context.clerk?.client?.signIn?.status), + hasTicket: ({ context }) => Boolean(context.ticket), isLoggedInAndSingleSession: and(['isLoggedIn', 'isSingleSessionMode', not('isExampleMode')]), isActivePathRoot: isCurrentPath('/'), @@ -253,16 +255,21 @@ export const SignInRouterMachine = setup({ }, on: { INIT: { - actions: assign(({ event }) => ({ - clerk: event.clerk, - exampleMode: event.exampleMode || false, - formRef: event.formRef, - loading: { - isLoading: false, - }, - router: event.router, - signUpPath: event.signUpPath || SIGN_UP_DEFAULT_BASE_PATH, - })), + actions: assign(({ event }) => { + const searchParams = event.router?.searchParams(); + + return { + clerk: event.clerk, + exampleMode: event.exampleMode || false, + formRef: event.formRef, + loading: { + isLoading: false, + }, + router: event.router, + signUpPath: event.signUpPath || SIGN_UP_DEFAULT_BASE_PATH, + ticket: searchParams?.get(SEARCH_PARAMS.ticket) || undefined, + }; + }), target: 'Init', }, }, @@ -331,6 +338,11 @@ export const SignInRouterMachine = setup({ actions: { type: 'navigateInternal', params: { force: true, path: '/' } }, target: 'Start', }, + { + guard: 'hasTicket', + actions: { type: 'navigateInternal', params: { force: true, path: '/' } }, + target: 'Start', + }, ], }, Start: { @@ -343,6 +355,7 @@ export const SignInRouterMachine = setup({ basePath: context.router?.basePath, formRef: context.formRef, parent: self, + ticket: context.ticket, }), onDone: { actions: 'raiseNext', 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 6e095f634c6..e82de5896cd 100644 --- a/packages/elements/src/internals/machines/sign-in/router.types.ts +++ b/packages/elements/src/internals/machines/sign-in/router.types.ts @@ -112,6 +112,7 @@ export interface SignInRouterContext extends BaseRouterContext { loading: SignInRouterLoadingContext; signUpPath: string; webAuthnAutofillSupport: boolean; + ticket: string | undefined; } // ---------------------------------- Input ---------------------------------- // diff --git a/packages/elements/src/internals/machines/sign-in/start.types.ts b/packages/elements/src/internals/machines/sign-in/start.types.ts index 3e76450b0fb..7de9250ccc0 100644 --- a/packages/elements/src/internals/machines/sign-in/start.types.ts +++ b/packages/elements/src/internals/machines/sign-in/start.types.ts @@ -31,6 +31,7 @@ export type SignInStartInput = { basePath?: string; formRef: ActorRefFrom; parent: SignInRouterMachineActorRef; + ticket?: string | undefined; }; // ---------------------------------- Context ---------------------------------- // From ed53131442ee8288172e75aa928298f4e2485e5e Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:01:48 -0300 Subject: [PATCH 2/4] Attempt ticket flow --- .../machines/sign-in/start.machine.ts | 79 +++++++++++++------ .../internals/machines/sign-in/start.types.ts | 1 + 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/packages/elements/src/internals/machines/sign-in/start.machine.ts b/packages/elements/src/internals/machines/sign-in/start.machine.ts index 13799420552..b5c159675ff 100644 --- a/packages/elements/src/internals/machines/sign-in/start.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/start.machine.ts @@ -14,6 +14,8 @@ export type TSignInStartMachine = typeof SignInStartMachine; export const SignInStartMachineId = 'SignInStart'; +type AttemptParams = { strategy: 'ticket'; ticket: string } | { strategy?: never; ticket?: never }; + export const SignInStartMachine = setup({ actors: { attemptPasskey: fromPromise< @@ -38,26 +40,32 @@ export const SignInStartMachine = setup({ throw new ClerkElementsRuntimeError(`Unsupported Web3 strategy: ${strategy}`); }, ), - attempt: fromPromise( - ({ input: { fields, parent } }) => { - const clerk = parent.getSnapshot().context.clerk; + attempt: fromPromise< + SignInResource, + { parent: SignInRouterMachineActorRef; fields: FormFields; params?: AttemptParams } + >(({ input: { fields, parent, params } }) => { + const clerk = parent.getSnapshot().context.clerk; - const password = fields.get('password'); - const identifier = fields.get('identifier'); + const password = fields.get('password'); + const identifier = fields.get('identifier'); - const passwordParams = password?.value - ? { - password: password.value, - strategy: 'password', - } - : {}; + const commonStrategyParams = { + identifier: (identifier?.value as string) || '', + }; - return clerk.client.signIn.create({ - identifier: (identifier?.value as string) || '', - ...passwordParams, - }); - }, - ), + const passwordParams = password?.value + ? { + password: password.value, + strategy: 'password', + } + : {}; + + return clerk.client.signIn.create({ + ...commonStrategyParams, + ...passwordParams, + ...params, + }); + }), }, actions: { sendToNext: ({ context, event }) => { @@ -77,6 +85,7 @@ export const SignInStartMachine = setup({ ), }, guards: { + hasTicket: ({ context }) => Boolean(context.ticket), isExampleMode: ({ context }) => Boolean(context.parent.getSnapshot().context.exampleMode), }, types: {} as SignInStartSchema, @@ -87,9 +96,22 @@ export const SignInStartMachine = setup({ parent: input.parent, formRef: input.formRef, loadingStep: 'start', + ticket: input.ticket, }), - initial: 'Pending', + initial: 'Init', states: { + Init: { + description: 'Handle ticket, if present; Else, default to Pending state.', + always: [ + { + guard: 'hasTicket', + target: 'Attempting', + }, + { + target: 'Pending', + }, + ], + }, Pending: { tags: ['state:pending'], description: 'Waiting for user input', @@ -122,10 +144,23 @@ export const SignInStartMachine = setup({ invoke: { id: 'attempt', src: 'attempt', - input: ({ context }) => ({ - parent: context.parent, - fields: context.formRef.getSnapshot().context.fields, - }), + input: ({ context }) => { + // Standard fields + const defaultParams = { + fields: context.formRef.getSnapshot().context.fields, + parent: context.parent, + }; + + // Handle ticket-specific flows + const params: AttemptParams = context.ticket + ? { + strategy: 'ticket', + ticket: context.ticket, + } + : {}; + + return { ...defaultParams, params }; + }, onDone: { actions: ['sendToNext', 'sendToLoading'], }, diff --git a/packages/elements/src/internals/machines/sign-in/start.types.ts b/packages/elements/src/internals/machines/sign-in/start.types.ts index 7de9250ccc0..e15b9145357 100644 --- a/packages/elements/src/internals/machines/sign-in/start.types.ts +++ b/packages/elements/src/internals/machines/sign-in/start.types.ts @@ -42,6 +42,7 @@ export interface SignInStartContext { formRef: ActorRefFrom; parent: SignInRouterMachineActorRef; loadingStep: 'start'; + ticket?: string | undefined; } // ---------------------------------- Schema ---------------------------------- // From 35d68255724bd566176eb08b4ac8fedffb74a0e3 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:04:53 -0300 Subject: [PATCH 3/4] Disable fields while attempting ticket flow --- .../machines/sign-in/start.machine.ts | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/elements/src/internals/machines/sign-in/start.machine.ts b/packages/elements/src/internals/machines/sign-in/start.machine.ts index b5c159675ff..232df004cfc 100644 --- a/packages/elements/src/internals/machines/sign-in/start.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/start.machine.ts @@ -1,5 +1,5 @@ import type { SignInResource, Web3Strategy } from '@clerk/types'; -import { assertEvent, fromPromise, not, sendTo, setup } from 'xstate'; +import { assertEvent, enqueueActions, fromPromise, not, sendTo, setup } from 'xstate'; import { SIGN_IN_DEFAULT_BASE_PATH } from '~/internals/constants'; import { ClerkElementsRuntimeError } from '~/internals/errors'; @@ -10,6 +10,8 @@ import { assertActorEventError } from '~/internals/machines/utils/assert'; import type { SignInRouterMachineActorRef } from './router.types'; import type { SignInStartSchema } from './start.types'; +const DISABLEABLE_FIELDS = ['emailAddress', 'phoneNumber'] as const; + export type TSignInStartMachine = typeof SignInStartMachine; export const SignInStartMachineId = 'SignInStart'; @@ -49,10 +51,6 @@ export const SignInStartMachine = setup({ const password = fields.get('password'); const identifier = fields.get('identifier'); - const commonStrategyParams = { - identifier: (identifier?.value as string) || '', - }; - const passwordParams = password?.value ? { password: password.value, @@ -61,9 +59,12 @@ export const SignInStartMachine = setup({ : {}; return clerk.client.signIn.create({ - ...commonStrategyParams, ...passwordParams, - ...params, + ...(params?.ticket + ? params + : { + identifier: (identifier?.value as string) ?? '', + }), }); }), }, @@ -73,6 +74,19 @@ export const SignInStartMachine = setup({ return context.parent.send({ type: 'NEXT', resource: event?.output }); }, sendToLoading, + setFormDisabledTicketFields: enqueueActions(({ context, enqueue }) => { + if (!context.ticket) { + return; + } + + const currentFields = context.formRef.getSnapshot().context.fields; + + for (const name of DISABLEABLE_FIELDS) { + if (currentFields.has(name)) { + enqueue.sendTo(context.formRef, { type: 'FIELD.DISABLE', field: { name } }); + } + } + }), setFormErrors: sendTo( ({ context }) => context.formRef, ({ event }) => { @@ -162,10 +176,10 @@ export const SignInStartMachine = setup({ return { ...defaultParams, params }; }, onDone: { - actions: ['sendToNext', 'sendToLoading'], + actions: ['setFormDisabledTicketFields', 'sendToNext', 'sendToLoading'], }, onError: { - actions: ['setFormErrors', 'sendToLoading'], + actions: ['setFormDisabledTicketFields', 'setFormErrors', 'sendToLoading'], target: 'Pending', }, }, From 8a98a3660455544e19d9c9d233b04e1bf0919387 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:23:02 -0300 Subject: [PATCH 4/4] Add changeset --- .changeset/itchy-mangos-remember.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/itchy-mangos-remember.md diff --git a/.changeset/itchy-mangos-remember.md b/.changeset/itchy-mangos-remember.md new file mode 100644 index 00000000000..bf01886a099 --- /dev/null +++ b/.changeset/itchy-mangos-remember.md @@ -0,0 +1,5 @@ +--- +'@clerk/elements': minor +--- + +Handle ticket-based sign in flows such as impersonation