@@ -21,8 +21,10 @@ describe('SignInFactorOne sign-up-if-missing transfer', () => {
2121 props . setProps ( { withSignUp : true } ) ;
2222
2323 fixtures . signIn . prepareFirstFactor . mockReturnValueOnce ( Promise . resolve ( { } as SignInResource ) ) ;
24+ // The SDK updates firstFactorVerification on the resource *before* throwing
25+ // the API error. This coupling is intentional — the component reads the
26+ // resource status inside the catch block to decide whether to transfer.
2427 fixtures . signIn . attemptFirstFactor . mockImplementationOnce ( ( ) => {
25- // Simulate SDK updating the resource before throwing (backend returns 404 with transferable in meta.client)
2628 fixtures . signIn . firstFactorVerification = { status : 'transferable' } as any ;
2729 return Promise . reject (
2830 new ClerkAPIResponseError ( 'Error' , {
@@ -127,4 +129,61 @@ describe('SignInFactorOne sign-up-if-missing transfer', () => {
127129 expect ( fixtures . signUp . create ) . not . toHaveBeenCalled ( ) ;
128130 } ) ;
129131 } ) ;
132+
133+ it ( 'proceeds to second factor for existing users (no transfer)' , async ( ) => {
134+ const { wrapper, fixtures, props } = await createFixtures ( f => {
135+ f . withEmailAddress ( ) ;
136+ f . withPreferredSignInStrategy ( { strategy : 'otp' } ) ;
137+ f . withEnumerationProtection ( ) ;
138+ f . startSignInWithEmailAddress ( { supportEmailCode : true , supportPassword : false } ) ;
139+ } ) ;
140+ props . setProps ( { withSignUp : true } ) ;
141+
142+ fixtures . signIn . prepareFirstFactor . mockReturnValueOnce ( Promise . resolve ( { } as SignInResource ) ) ;
143+ fixtures . signIn . attemptFirstFactor . mockResolvedValueOnce ( {
144+ status : 'needs_second_factor' ,
145+ firstFactorVerification : { status : 'verified' } ,
146+ } as any ) ;
147+
148+ const { userEvent } = render ( < SignInFactorOne /> , { wrapper } ) ;
149+
150+ await userEvent . type ( screen . getByLabelText ( / E n t e r v e r i f i c a t i o n c o d e / i) , '123456' ) ;
151+ await waitFor ( ( ) => {
152+ expect ( fixtures . router . navigate ) . toHaveBeenCalledWith ( '../factor-two' ) ;
153+ expect ( fixtures . signUp . create ) . not . toHaveBeenCalled ( ) ;
154+ } ) ;
155+ } ) ;
156+
157+ it ( 'surfaces transfer errors instead of leaving the code form loading' , async ( ) => {
158+ const { wrapper, fixtures, props } = await createFixtures ( f => {
159+ f . withEmailAddress ( ) ;
160+ f . withPreferredSignInStrategy ( { strategy : 'otp' } ) ;
161+ f . withEnumerationProtection ( ) ;
162+ f . startSignInWithEmailAddress ( { supportEmailCode : true , supportPassword : false } ) ;
163+ } ) ;
164+ props . setProps ( { withSignUp : true } ) ;
165+
166+ fixtures . signIn . prepareFirstFactor . mockReturnValueOnce ( Promise . resolve ( { } as SignInResource ) ) ;
167+ fixtures . signIn . attemptFirstFactor . mockImplementationOnce ( ( ) => {
168+ fixtures . signIn . firstFactorVerification = { status : 'transferable' } as any ;
169+ return Promise . reject (
170+ new ClerkAPIResponseError ( 'Error' , {
171+ data : [ { code : 'form_identifier_not_found' , long_message : '' , message : '' } ] ,
172+ status : 404 ,
173+ } ) ,
174+ ) ;
175+ } ) ;
176+ fixtures . signUp . create . mockResolvedValueOnce ( { status : 'abandoned' } as any ) ;
177+
178+ const { userEvent } = render ( < SignInFactorOne /> , { wrapper } ) ;
179+ const input = screen . getByLabelText ( / E n t e r v e r i f i c a t i o n c o d e / i) ;
180+
181+ await userEvent . type ( input , '123456' ) ;
182+
183+ await waitFor ( ( ) => {
184+ expect ( fixtures . signUp . create ) . toHaveBeenCalled ( ) ;
185+ expect ( input ) . toHaveValue ( '' ) ;
186+ expect ( input ) . not . toBeDisabled ( ) ;
187+ } ) ;
188+ } ) ;
130189} ) ;
0 commit comments