Skip to content

Commit 383cd29

Browse files
authored
feat(elements): Handle ticket-based sign-in flow (clerk#4746)
1 parent e4c6c1f commit 383cd29

5 files changed

Lines changed: 105 additions & 35 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/elements': minor
3+
---
4+
5+
Handle ticket-based sign in flows such as impersonation

packages/elements/src/internals/machines/sign-in/router.machine.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
CHOOSE_SESSION_PATH_ROUTE,
99
ERROR_CODES,
1010
ROUTING,
11+
SEARCH_PARAMS,
1112
SIGN_IN_DEFAULT_BASE_PATH,
1213
SIGN_UP_DEFAULT_BASE_PATH,
1314
SSO_CALLBACK_PATH_ROUTE,
@@ -151,6 +152,7 @@ export const SignInRouterMachine = setup({
151152
Boolean(context.clerk.client.signIn.status === null && context.clerk.client.lastActiveSessionId),
152153
hasOAuthError: ({ context }) => Boolean(context.clerk?.client?.signIn?.firstFactorVerification?.error),
153154
hasResource: ({ context }) => Boolean(context.clerk?.client?.signIn?.status),
155+
hasTicket: ({ context }) => Boolean(context.ticket),
154156

155157
isLoggedInAndSingleSession: and(['isLoggedIn', 'isSingleSessionMode', not('isExampleMode')]),
156158
isActivePathRoot: isCurrentPath('/'),
@@ -253,16 +255,21 @@ export const SignInRouterMachine = setup({
253255
},
254256
on: {
255257
INIT: {
256-
actions: assign(({ event }) => ({
257-
clerk: event.clerk,
258-
exampleMode: event.exampleMode || false,
259-
formRef: event.formRef,
260-
loading: {
261-
isLoading: false,
262-
},
263-
router: event.router,
264-
signUpPath: event.signUpPath || SIGN_UP_DEFAULT_BASE_PATH,
265-
})),
258+
actions: assign(({ event }) => {
259+
const searchParams = event.router?.searchParams();
260+
261+
return {
262+
clerk: event.clerk,
263+
exampleMode: event.exampleMode || false,
264+
formRef: event.formRef,
265+
loading: {
266+
isLoading: false,
267+
},
268+
router: event.router,
269+
signUpPath: event.signUpPath || SIGN_UP_DEFAULT_BASE_PATH,
270+
ticket: searchParams?.get(SEARCH_PARAMS.ticket) || undefined,
271+
};
272+
}),
266273
target: 'Init',
267274
},
268275
},
@@ -331,6 +338,11 @@ export const SignInRouterMachine = setup({
331338
actions: { type: 'navigateInternal', params: { force: true, path: '/' } },
332339
target: 'Start',
333340
},
341+
{
342+
guard: 'hasTicket',
343+
actions: { type: 'navigateInternal', params: { force: true, path: '/' } },
344+
target: 'Start',
345+
},
334346
],
335347
},
336348
Start: {
@@ -343,6 +355,7 @@ export const SignInRouterMachine = setup({
343355
basePath: context.router?.basePath,
344356
formRef: context.formRef,
345357
parent: self,
358+
ticket: context.ticket,
346359
}),
347360
onDone: {
348361
actions: 'raiseNext',

packages/elements/src/internals/machines/sign-in/router.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export interface SignInRouterContext extends BaseRouterContext {
112112
loading: SignInRouterLoadingContext;
113113
signUpPath: string;
114114
webAuthnAutofillSupport: boolean;
115+
ticket: string | undefined;
115116
}
116117

117118
// ---------------------------------- Input ---------------------------------- //

packages/elements/src/internals/machines/sign-in/start.machine.ts

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { SignInResource, Web3Strategy } from '@clerk/types';
2-
import { assertEvent, fromPromise, not, sendTo, setup } from 'xstate';
2+
import { assertEvent, enqueueActions, fromPromise, not, sendTo, setup } from 'xstate';
33

44
import { SIGN_IN_DEFAULT_BASE_PATH } from '~/internals/constants';
55
import { ClerkElementsRuntimeError } from '~/internals/errors';
@@ -10,10 +10,14 @@ import { assertActorEventError } from '~/internals/machines/utils/assert';
1010
import type { SignInRouterMachineActorRef } from './router.types';
1111
import type { SignInStartSchema } from './start.types';
1212

13+
const DISABLEABLE_FIELDS = ['emailAddress', 'phoneNumber'] as const;
14+
1315
export type TSignInStartMachine = typeof SignInStartMachine;
1416

1517
export const SignInStartMachineId = 'SignInStart';
1618

19+
type AttemptParams = { strategy: 'ticket'; ticket: string } | { strategy?: never; ticket?: never };
20+
1721
export const SignInStartMachine = setup({
1822
actors: {
1923
attemptPasskey: fromPromise<
@@ -38,33 +42,51 @@ export const SignInStartMachine = setup({
3842
throw new ClerkElementsRuntimeError(`Unsupported Web3 strategy: ${strategy}`);
3943
},
4044
),
41-
attempt: fromPromise<SignInResource, { parent: SignInRouterMachineActorRef; fields: FormFields }>(
42-
({ input: { fields, parent } }) => {
43-
const clerk = parent.getSnapshot().context.clerk;
45+
attempt: fromPromise<
46+
SignInResource,
47+
{ parent: SignInRouterMachineActorRef; fields: FormFields; params?: AttemptParams }
48+
>(({ input: { fields, parent, params } }) => {
49+
const clerk = parent.getSnapshot().context.clerk;
4450

45-
const password = fields.get('password');
46-
const identifier = fields.get('identifier');
51+
const password = fields.get('password');
52+
const identifier = fields.get('identifier');
4753

48-
const passwordParams = password?.value
49-
? {
50-
password: password.value,
51-
strategy: 'password',
52-
}
53-
: {};
54+
const passwordParams = password?.value
55+
? {
56+
password: password.value,
57+
strategy: 'password',
58+
}
59+
: {};
5460

55-
return clerk.client.signIn.create({
56-
identifier: (identifier?.value as string) || '',
57-
...passwordParams,
58-
});
59-
},
60-
),
61+
return clerk.client.signIn.create({
62+
...passwordParams,
63+
...(params?.ticket
64+
? params
65+
: {
66+
identifier: (identifier?.value as string) ?? '',
67+
}),
68+
});
69+
}),
6170
},
6271
actions: {
6372
sendToNext: ({ context, event }) => {
6473
// @ts-expect-error -- We're calling this in onDone, and event.output exists on the actor done event
6574
return context.parent.send({ type: 'NEXT', resource: event?.output });
6675
},
6776
sendToLoading,
77+
setFormDisabledTicketFields: enqueueActions(({ context, enqueue }) => {
78+
if (!context.ticket) {
79+
return;
80+
}
81+
82+
const currentFields = context.formRef.getSnapshot().context.fields;
83+
84+
for (const name of DISABLEABLE_FIELDS) {
85+
if (currentFields.has(name)) {
86+
enqueue.sendTo(context.formRef, { type: 'FIELD.DISABLE', field: { name } });
87+
}
88+
}
89+
}),
6890
setFormErrors: sendTo(
6991
({ context }) => context.formRef,
7092
({ event }) => {
@@ -77,6 +99,7 @@ export const SignInStartMachine = setup({
7799
),
78100
},
79101
guards: {
102+
hasTicket: ({ context }) => Boolean(context.ticket),
80103
isExampleMode: ({ context }) => Boolean(context.parent.getSnapshot().context.exampleMode),
81104
},
82105
types: {} as SignInStartSchema,
@@ -87,9 +110,22 @@ export const SignInStartMachine = setup({
87110
parent: input.parent,
88111
formRef: input.formRef,
89112
loadingStep: 'start',
113+
ticket: input.ticket,
90114
}),
91-
initial: 'Pending',
115+
initial: 'Init',
92116
states: {
117+
Init: {
118+
description: 'Handle ticket, if present; Else, default to Pending state.',
119+
always: [
120+
{
121+
guard: 'hasTicket',
122+
target: 'Attempting',
123+
},
124+
{
125+
target: 'Pending',
126+
},
127+
],
128+
},
93129
Pending: {
94130
tags: ['state:pending'],
95131
description: 'Waiting for user input',
@@ -122,15 +158,28 @@ export const SignInStartMachine = setup({
122158
invoke: {
123159
id: 'attempt',
124160
src: 'attempt',
125-
input: ({ context }) => ({
126-
parent: context.parent,
127-
fields: context.formRef.getSnapshot().context.fields,
128-
}),
161+
input: ({ context }) => {
162+
// Standard fields
163+
const defaultParams = {
164+
fields: context.formRef.getSnapshot().context.fields,
165+
parent: context.parent,
166+
};
167+
168+
// Handle ticket-specific flows
169+
const params: AttemptParams = context.ticket
170+
? {
171+
strategy: 'ticket',
172+
ticket: context.ticket,
173+
}
174+
: {};
175+
176+
return { ...defaultParams, params };
177+
},
129178
onDone: {
130-
actions: ['sendToNext', 'sendToLoading'],
179+
actions: ['setFormDisabledTicketFields', 'sendToNext', 'sendToLoading'],
131180
},
132181
onError: {
133-
actions: ['setFormErrors', 'sendToLoading'],
182+
actions: ['setFormDisabledTicketFields', 'setFormErrors', 'sendToLoading'],
134183
target: 'Pending',
135184
},
136185
},

packages/elements/src/internals/machines/sign-in/start.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type SignInStartInput = {
3131
basePath?: string;
3232
formRef: ActorRefFrom<typeof FormMachine>;
3333
parent: SignInRouterMachineActorRef;
34+
ticket?: string | undefined;
3435
};
3536

3637
// ---------------------------------- Context ---------------------------------- //
@@ -41,6 +42,7 @@ export interface SignInStartContext {
4142
formRef: ActorRefFrom<typeof FormMachine>;
4243
parent: SignInRouterMachineActorRef;
4344
loadingStep: 'start';
45+
ticket?: string | undefined;
4446
}
4547

4648
// ---------------------------------- Schema ---------------------------------- //

0 commit comments

Comments
 (0)