From 383cd2967ea111efcb95fc20cec186aa29070d1a Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:20:40 -0300 Subject: [PATCH] feat(elements): Handle ticket-based sign-in flow (#4746) --- .changeset/itchy-mangos-remember.md | 5 + .../machines/sign-in/router.machine.ts | 33 +++++-- .../machines/sign-in/router.types.ts | 1 + .../machines/sign-in/start.machine.ts | 99 ++++++++++++++----- .../internals/machines/sign-in/start.types.ts | 2 + 5 files changed, 105 insertions(+), 35 deletions(-) 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 0000000000..bf01886a09 --- /dev/null +++ b/.changeset/itchy-mangos-remember.md @@ -0,0 +1,5 @@ +--- +'@clerk/elements': minor +--- + +Handle ticket-based sign in flows such as impersonation 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 2a923afe41..54da5d5fe5 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 6e095f634c..e82de5896c 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.machine.ts b/packages/elements/src/internals/machines/sign-in/start.machine.ts index 1379942055..232df004cf 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,10 +10,14 @@ 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'; +type AttemptParams = { strategy: 'ticket'; ticket: string } | { strategy?: never; ticket?: never }; + export const SignInStartMachine = setup({ actors: { attemptPasskey: fromPromise< @@ -38,26 +42,31 @@ 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 passwordParams = password?.value + ? { + password: password.value, + strategy: 'password', + } + : {}; - return clerk.client.signIn.create({ - identifier: (identifier?.value as string) || '', - ...passwordParams, - }); - }, - ), + return clerk.client.signIn.create({ + ...passwordParams, + ...(params?.ticket + ? params + : { + identifier: (identifier?.value as string) ?? '', + }), + }); + }), }, actions: { sendToNext: ({ context, event }) => { @@ -65,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 }) => { @@ -77,6 +99,7 @@ export const SignInStartMachine = setup({ ), }, guards: { + hasTicket: ({ context }) => Boolean(context.ticket), isExampleMode: ({ context }) => Boolean(context.parent.getSnapshot().context.exampleMode), }, types: {} as SignInStartSchema, @@ -87,9 +110,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,15 +158,28 @@ 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'], + actions: ['setFormDisabledTicketFields', 'sendToNext', 'sendToLoading'], }, onError: { - actions: ['setFormErrors', 'sendToLoading'], + actions: ['setFormDisabledTicketFields', 'setFormErrors', 'sendToLoading'], target: 'Pending', }, }, 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 3e76450b0f..e15b914535 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 ---------------------------------- // @@ -41,6 +42,7 @@ export interface SignInStartContext { formRef: ActorRefFrom; parent: SignInRouterMachineActorRef; loadingStep: 'start'; + ticket?: string | undefined; } // ---------------------------------- Schema ---------------------------------- //