11import { joinURL } from '@clerk/shared/url' ;
22import { isWebAuthnAutofillSupported } from '@clerk/shared/webauthn' ;
3- import type { LoadedClerk , SignInResource , SignInStatus , SignInStrategy , Web3Strategy } from '@clerk/types' ;
3+ import type {
4+ AttemptFirstFactorParams ,
5+ EmailCodeAttempt ,
6+ LoadedClerk ,
7+ PasswordAttempt ,
8+ PhoneCodeAttempt ,
9+ PrepareFirstFactorParams ,
10+ PrepareSecondFactorParams ,
11+ ResetPasswordEmailCodeAttempt ,
12+ ResetPasswordPhoneCodeAttempt ,
13+ SignInFirstFactor ,
14+ SignInResource ,
15+ SignInSecondFactor ,
16+ SignInStatus ,
17+ SignInStrategy ,
18+ Web3Attempt ,
19+ Web3Strategy ,
20+ } from '@clerk/types' ;
421import type { NonReducibleUnknown } from 'xstate' ;
522import { and , assertEvent , assign , enqueueActions , fromPromise , log , not , or , raise , sendTo , setup } from 'xstate' ;
623
@@ -16,7 +33,7 @@ import { ClerkElementsError, ClerkElementsRuntimeError } from '~/internals/error
1633import { type FormFields , FormMachine } from '~/internals/machines/form' ;
1734import { ThirdPartyMachine , ThirdPartyMachineId } from '~/internals/machines/third-party' ;
1835import type { BaseRouterLoadingStep } from '~/internals/machines/types' ;
19- import { assertActorEventError } from '~/internals/machines/utils/assert' ;
36+ import { assertActorEventError , assertIsDefined } from '~/internals/machines/utils/assert' ;
2037import { shouldUseVirtualRouting } from '~/internals/machines/utils/next' ;
2138
2239import type {
@@ -26,7 +43,7 @@ import type {
2643 SignInRouterSchema ,
2744 SignInRouterSessionSetActiveEvent ,
2845} from './router.types' ;
29- import { SignInFirstFactorMachine , SignInSecondFactorMachine } from './verification.machine ' ;
46+ import { determineStartingSignInFactor , determineStartingSignInSecondFactor } from './utils/starting-factors ' ;
3047
3148export type TSignInRouterMachine = typeof SignInRouterMachine ;
3249
@@ -43,56 +60,265 @@ const needsStatus =
4360
4461export const SignInRouterMachineId = 'SignInRouter' ;
4562
63+ export type PrepareFirstFactorInput = {
64+ clerk : LoadedClerk ;
65+ params : PrepareFirstFactorParams ;
66+ resendable : boolean ;
67+ } ;
68+ export type PrepareSecondFactorInput = {
69+ clerk : LoadedClerk ;
70+ params : PrepareSecondFactorParams ;
71+ resendable : boolean ;
72+ } ;
73+
74+ export type AttemptFirstFactorInput = {
75+ clerk : LoadedClerk ;
76+ currentFactor : SignInFirstFactor | null ;
77+ fields : FormFields ;
78+ } ;
79+
80+ export type AttemptSecondFactorInput = {
81+ clerk : LoadedClerk ;
82+ currentFactor : SignInSecondFactor | null ;
83+ fields : FormFields ;
84+ } ;
85+
86+ export type ResetPasswordAttemptInput = { clerk : LoadedClerk ; fields : FormFields } ;
87+ export type ClerkInput = { clerk : LoadedClerk } ;
88+
89+ export type AttemptPasskeyInput = { clerk : LoadedClerk ; flow : 'autofill' | 'discoverable' | undefined } ;
90+ export type AttemptWeb3Input = { clerk : LoadedClerk ; strategy : Web3Strategy } ;
91+ export type StartAttemptInput = { clerk : LoadedClerk ; fields : FormFields } ;
92+
93+ const isNonPreparableStrategy = ( strategy ?: SignInFirstFactor [ 'strategy' ] | SignInSecondFactor [ 'strategy' ] ) => {
94+ if ( ! strategy ) {
95+ return false ;
96+ }
97+
98+ return [ 'passkey' , 'password' ] . includes ( strategy ) ;
99+ } ;
100+
46101export const SignInRouterMachine = setup ( {
47102 actors : {
48- attemptPasskey : fromPromise < SignInResource , { clerk : LoadedClerk ; flow : 'autofill' | 'discoverable' | undefined } > (
49- ( { input : { clerk, flow } } ) => {
50- return clerk . client . signIn . authenticateWithPasskey ( {
51- flow,
52- } ) ;
53- } ,
54- ) ,
55- attemptWeb3 : fromPromise < SignInResource , { clerk : LoadedClerk ; strategy : Web3Strategy } > (
56- ( { input : { clerk, strategy } } ) => {
57- if ( strategy === 'web3_metamask_signature' ) {
58- return clerk . client . signIn . authenticateWithMetamask ( ) ;
103+ // ----------------
104+ // Global
105+ // ----------------
106+
107+ attemptPasskey : fromPromise < SignInResource , AttemptPasskeyInput > ( ( { input } ) => {
108+ const { clerk, flow } = input ;
109+
110+ return clerk . client . signIn . authenticateWithPasskey ( {
111+ flow,
112+ } ) ;
113+ } ) ,
114+
115+ attemptWeb3 : fromPromise < SignInResource , AttemptWeb3Input > ( ( { input } ) => {
116+ const { clerk, strategy } = input ;
117+
118+ if ( strategy === 'web3_metamask_signature' ) {
119+ return clerk . client . signIn . authenticateWithMetamask ( ) ;
120+ }
121+
122+ if ( strategy === 'web3_coinbase_wallet_signature' ) {
123+ return clerk . client . signIn . authenticateWithCoinbaseWallet ( ) ;
124+ }
125+
126+ throw new ClerkElementsRuntimeError ( `Unsupported Web3 strategy: ${ strategy } ` ) ;
127+ } ) ,
128+
129+ // ----------------
130+ // Start
131+ // ----------------
132+
133+ startAttempt : fromPromise < SignInResource , StartAttemptInput > ( ( { input } ) => {
134+ const { clerk, fields } = input ;
135+
136+ const password = fields . get ( 'password' ) ;
137+ const identifier = fields . get ( 'identifier' ) ;
138+
139+ const passwordParams = password ?. value
140+ ? {
141+ password : password . value ,
142+ strategy : 'password' ,
143+ }
144+ : { } ;
145+
146+ return clerk . client . signIn . create ( {
147+ identifier : ( identifier ?. value as string ) || '' ,
148+ ...passwordParams ,
149+ } ) ;
150+ } ) ,
151+
152+ // ----------------
153+ // First Factor
154+ // ----------------
155+
156+ firstFactorAttempt : fromPromise < SignInResource , AttemptFirstFactorInput > ( async ( { input } ) => {
157+ const { clerk, currentFactor, fields } = input ;
158+ assertIsDefined ( currentFactor , 'Current factor' ) ;
159+
160+ let attemptParams : AttemptFirstFactorParams ;
161+
162+ const strategy = currentFactor . strategy ;
163+ const code = fields . get ( 'code' ) ?. value as string | undefined ;
164+ const password = fields . get ( 'password' ) ?. value as string | undefined ;
165+
166+ switch ( strategy ) {
167+ case 'passkey' : {
168+ return await clerk . client . signIn . authenticateWithPasskey ( ) ;
59169 }
60- if ( strategy === 'web3_coinbase_wallet_signature' ) {
61- return clerk . client . signIn . authenticateWithCoinbaseWallet ( ) ;
170+ case 'password' : {
171+ assertIsDefined ( password , 'Password' ) ;
172+
173+ attemptParams = {
174+ strategy,
175+ password,
176+ } satisfies PasswordAttempt ;
177+
178+ break ;
62179 }
63- throw new ClerkElementsRuntimeError ( `Unsupported Web3 strategy: ${ strategy } ` ) ;
64- } ,
65- ) ,
66- resetPasswordAttempt : fromPromise < SignInResource , { clerk : LoadedClerk ; fields : FormFields } > (
67- ( { input : { clerk, fields } } ) => {
68- const password = ( fields . get ( 'password' ) ?. value as string ) || '' ;
69- const signOutOfOtherSessions = fields . get ( 'signOutOfOtherSessions' ) ?. checked || false ;
70- return clerk . client . signIn . resetPassword ( { password, signOutOfOtherSessions } ) ;
71- } ,
72- ) ,
73- startAttempt : fromPromise < SignInResource , { clerk : LoadedClerk ; fields : FormFields } > (
74- ( { input : { clerk, fields } } ) => {
75- const password = fields . get ( 'password' ) ;
76- const identifier = fields . get ( 'identifier' ) ;
77-
78- const passwordParams = password ?. value
79- ? {
80- password : password . value ,
81- strategy : 'password' ,
82- }
83- : { } ;
84-
85- return clerk . client . signIn . create ( {
86- identifier : ( identifier ?. value as string ) || '' ,
87- ...passwordParams ,
88- } ) ;
89- } ,
180+ case 'reset_password_phone_code' :
181+ case 'reset_password_email_code' : {
182+ assertIsDefined ( code , 'Code for resetting phone/email' ) ;
183+
184+ attemptParams = {
185+ strategy,
186+ code,
187+ password,
188+ } satisfies ResetPasswordPhoneCodeAttempt | ResetPasswordEmailCodeAttempt ;
189+
190+ break ;
191+ }
192+ case 'phone_code' :
193+ case 'email_code' : {
194+ assertIsDefined ( code , 'Code for phone/email' ) ;
195+
196+ attemptParams = {
197+ strategy,
198+ code,
199+ } satisfies PhoneCodeAttempt | EmailCodeAttempt ;
200+
201+ break ;
202+ }
203+ case 'web3_metamask_signature' : {
204+ const signature = fields . get ( 'signature' ) ?. value as string | undefined ;
205+ assertIsDefined ( signature , 'Web3 Metamask signature' ) ;
206+
207+ attemptParams = {
208+ strategy,
209+ signature,
210+ } satisfies Web3Attempt ;
211+
212+ break ;
213+ }
214+ case 'web3_coinbase_wallet_signature' : {
215+ const signature = fields . get ( 'signature' ) ?. value as string | undefined ;
216+ assertIsDefined ( signature , 'Web3 Coinbase Wallet signature' ) ;
217+
218+ attemptParams = {
219+ strategy,
220+ signature,
221+ } satisfies Web3Attempt ;
222+
223+ break ;
224+ }
225+ default :
226+ throw new ClerkElementsRuntimeError ( `Invalid strategy: ${ strategy } ` ) ;
227+ }
228+
229+ return await clerk . client . signIn . attemptFirstFactor ( attemptParams ) ;
230+ } ) ,
231+
232+ firstFactorDetermineStartingFactor : fromPromise < SignInFirstFactor | null , ClerkInput > ( async ( { input } ) => {
233+ return Promise . resolve (
234+ determineStartingSignInFactor (
235+ input . clerk . client . signIn . supportedFirstFactors ,
236+ input . clerk . client . signIn . identifier ,
237+ input . clerk . __unstable__environment ?. displayConfig . preferredSignInStrategy ,
238+ ) ,
239+ ) ;
240+ } ) ,
241+
242+ firstFactorPrepare : fromPromise < SignInResource , PrepareFirstFactorInput > ( async ( { input } ) => {
243+ const { clerk, params, resendable } = input ;
244+
245+ // If a prepare call has already been fired recently, don't re-send
246+ const currentVerificationExpiration = clerk . client . signIn . firstFactorVerification . expireAt ;
247+ const needsPrepare = resendable || ! currentVerificationExpiration || currentVerificationExpiration < new Date ( ) ;
248+
249+ if ( isNonPreparableStrategy ( params ?. strategy ) || ! needsPrepare ) {
250+ return Promise . resolve ( clerk . client . signIn ) ;
251+ }
252+
253+ assertIsDefined ( params , 'First factor params' ) ;
254+ return await clerk . client . signIn . prepareFirstFactor ( params ) ;
255+ } ) ,
256+
257+ // ----------------
258+ // Second Factor
259+ // ----------------
260+
261+ secondFactorAttempt : fromPromise < SignInResource , AttemptSecondFactorInput > ( async ( { input } ) => {
262+ const { clerk, fields, currentFactor } = input ;
263+
264+ const code = fields . get ( 'code' ) ?. value as string ;
265+
266+ assertIsDefined ( currentFactor , 'Current factor' ) ;
267+ assertIsDefined ( code , 'Code' ) ;
268+
269+ return await clerk . client . signIn . attemptSecondFactor ( {
270+ strategy : currentFactor . strategy ,
271+ code,
272+ } ) ;
273+ } ) ,
274+
275+ secondFactorDetermineStartingFactor : fromPromise < SignInSecondFactor | null , ClerkInput > ( async ( { input } ) =>
276+ Promise . resolve ( determineStartingSignInSecondFactor ( input . clerk . client . signIn . supportedSecondFactors ) ) ,
90277 ) ,
91278
92- firstFactorMachine : SignInFirstFactorMachine ,
279+ secondFactorPrepare : fromPromise < SignInResource , PrepareSecondFactorInput > ( async ( { input } ) => {
280+ const { clerk, params, resendable } = input ;
281+
282+ // If a prepare call has already been fired recently, don't re-send
283+ const currentVerificationExpiration = clerk . client . signIn . secondFactorVerification . expireAt ;
284+ const needsPrepare = resendable || ! currentVerificationExpiration || currentVerificationExpiration < new Date ( ) ;
285+
286+ assertIsDefined ( params , 'Second factor params' ) ;
287+
288+ if ( params . strategy !== 'phone_code' || ! needsPrepare ) {
289+ return Promise . resolve ( clerk . client . signIn ) ;
290+ }
291+
292+ return await clerk . client . signIn . prepareSecondFactor ( {
293+ strategy : params . strategy ,
294+ phoneNumberId : params . phoneNumberId ,
295+ } ) ;
296+ } ) ,
297+
298+ // ----------------
299+ // Reset Password
300+ // ----------------
301+
302+ resetPasswordAttempt : fromPromise < SignInResource , ResetPasswordAttemptInput > ( ( { input } ) => {
303+ const { clerk, fields } = input ;
304+
305+ const password = ( fields . get ( 'password' ) ?. value as string ) || '' ;
306+ const signOutOfOtherSessions = fields . get ( 'signOutOfOtherSessions' ) ?. checked || false ;
307+
308+ return clerk . client . signIn . resetPassword ( { password, signOutOfOtherSessions } ) ;
309+ } ) ,
310+
311+ // ----------------
312+ // Shared Machines
313+ // ----------------
314+
93315 formMachine : FormMachine ,
94- secondFactorMachine : SignInSecondFactorMachine ,
95316 thirdPartyMachine : ThirdPartyMachine ,
317+
318+ // ----------------
319+ // Other
320+ // ----------------
321+
96322 webAuthnAutofillSupport : fromPromise ( ( ) => isWebAuthnAutofillSupported ( ) ) ,
97323 } ,
98324 actions : {
0 commit comments