@@ -2379,6 +2379,7 @@ export class Clerk implements ClerkInterface {
23792379 externalAccountErrorCode : externalAccount . error ?. code ,
23802380 externalAccountSessionId : externalAccount . error ?. meta ?. sessionId ,
23812381 sessionId : signUp . createdSessionId ,
2382+ protectCheck : signUp . protectCheck ,
23822383 } ;
23832384
23842385 const si = {
@@ -2387,6 +2388,7 @@ export class Clerk implements ClerkInterface {
23872388 firstFactorVerificationErrorCode : firstFactorVerification . error ?. code ,
23882389 firstFactorVerificationSessionId : firstFactorVerification . error ?. meta ?. sessionId ,
23892390 sessionId : signIn . createdSessionId ,
2391+ protectCheck : signIn . protectCheck ,
23902392 } ;
23912393
23922394 const makeNavigate = ( to : string ) => ( ) => navigate ( to ) ;
@@ -2410,6 +2412,11 @@ export class Clerk implements ClerkInterface {
24102412 buildURL ( { base : displayConfig . signInUrl , hashPath : '/reset-password' } , { stringify : true } ) ,
24112413 ) ;
24122414
2415+ const navigateToSignInProtectCheck = makeNavigate (
2416+ params . signInProtectCheckUrl ||
2417+ buildURL ( { base : displayConfig . signInUrl , hashPath : '/protect-check' } , { stringify : true } ) ,
2418+ ) ;
2419+
24132420 const redirectUrls = new RedirectUrls ( this . #options, params ) ;
24142421
24152422 const navigateToContinueSignUp = makeNavigate (
@@ -2423,7 +2430,19 @@ export class Clerk implements ClerkInterface {
24232430 ) ,
24242431 ) ;
24252432
2433+ const navigateToSignUpProtectCheck = makeNavigate (
2434+ params . signUpProtectCheckUrl ||
2435+ buildURL ( { base : displayConfig . signUpUrl , hashPath : '/protect-check' } , { stringify : true } ) ,
2436+ ) ;
2437+
24262438 const navigateToNextStepSignUp = ( { missingFields } : { missingFields : SignUpField [ ] } ) => {
2439+ // A protect-gated sign-up always carries 'protect_check' in missing_fields, so this gate
2440+ // check must run BEFORE the generic missing-fields short-circuit below — otherwise the
2441+ // OAuth/SAML callback would land on /continue instead of the challenge.
2442+ if ( signUp . protectCheck || missingFields . includes ( 'protect_check' ) ) {
2443+ return navigateToSignUpProtectCheck ( ) ;
2444+ }
2445+
24272446 if ( missingFields . length ) {
24282447 return navigateToContinueSignUp ( ) ;
24292448 }
@@ -2442,6 +2461,9 @@ export class Clerk implements ClerkInterface {
24422461 verifyPhonePath :
24432462 params . verifyPhoneNumberUrl ||
24442463 buildURL ( { base : displayConfig . signUpUrl , hashPath : '/verify-phone-number' } , { stringify : true } ) ,
2464+ protectCheckPath :
2465+ params . signUpProtectCheckUrl ||
2466+ buildURL ( { base : displayConfig . signUpUrl , hashPath : '/protect-check' } , { stringify : true } ) ,
24452467 navigate,
24462468 } ) ;
24472469 } ;
@@ -2492,12 +2514,36 @@ export class Clerk implements ClerkInterface {
24922514 } ) ;
24932515 }
24942516
2517+ // OAuth/SAML callbacks can resolve into a protect_check gate that surfaces on the next
2518+ // /v1/client read, so check for it here before continuing with the transfer logic below.
2519+ // Honor either the explicit `protectCheck` field or the `needs_protect_check` status override.
2520+ //
2521+ // Scope to the callback's intent: an abandoned sign-in keeps serializing its pending
2522+ // `protect_check` on the client for up to a day (and a later sign-up doesn't clear it in
2523+ // multi-session mode), so an unscoped check would route a *sign-up* callback into the stale
2524+ // sign-in's challenge. We only consult `si` here unless this is explicitly a sign-up callback.
2525+ // Transfers are unaffected: the `signIn.create({ transfer })` path below checks its own fresh
2526+ // response for the gate.
2527+ if ( params . reloadResource !== 'signUp' && ( si . protectCheck || si . status === 'needs_protect_check' ) ) {
2528+ return navigateToSignInProtectCheck ( ) ;
2529+ }
2530+
2531+ // The sign-up resource can be gated the same way (e.g. a callback that resolves straight into a
2532+ // gated sign-up). Scope to the sign-up intent for the symmetric reason — a stale sign-up's gate
2533+ // shouldn't hijack a sign-in callback.
2534+ if ( params . reloadResource !== 'signIn' && su . protectCheck ) {
2535+ return navigateToSignUpProtectCheck ( ) ;
2536+ }
2537+
24952538 const userExistsButNeedsToSignIn =
24962539 su . externalAccountStatus === 'transferable' &&
24972540 su . externalAccountErrorCode === ERROR_CODES . EXTERNAL_ACCOUNT_EXISTS ;
24982541
24992542 if ( userExistsButNeedsToSignIn ) {
25002543 const res = await signIn . create ( { transfer : true } ) ;
2544+ if ( res . protectCheck || res . status === 'needs_protect_check' ) {
2545+ return navigateToSignInProtectCheck ( ) ;
2546+ }
25012547 switch ( res . status ) {
25022548 case 'complete' :
25032549 return this . setActive ( {
@@ -2756,6 +2802,8 @@ export class Clerk implements ClerkInterface {
27562802 strategy,
27572803 legalAccepted,
27582804 secondFactorUrl,
2805+ protectCheckUrl,
2806+ signUpProtectCheckUrl,
27592807 walletName,
27602808 } : ClerkAuthenticateWithWeb3Params ) : Promise < void > => {
27612809 if ( ! this . client || ! this . environment ) {
@@ -2798,6 +2846,15 @@ export class Clerk implements ClerkInterface {
27982846 secondFactorUrl || buildURL ( { base : displayConfig . signInUrl , hashPath : '/factor-two' } , { stringify : true } ) ,
27992847 ) ;
28002848
2849+ const navigateToSignInProtectCheck = makeNavigate (
2850+ protectCheckUrl || buildURL ( { base : displayConfig . signInUrl , hashPath : '/protect-check' } , { stringify : true } ) ,
2851+ ) ;
2852+
2853+ const navigateToSignUpProtectCheck = makeNavigate (
2854+ signUpProtectCheckUrl ||
2855+ buildURL ( { base : displayConfig . signUpUrl , hashPath : '/protect-check' } , { stringify : true } ) ,
2856+ ) ;
2857+
28012858 const navigateToContinueSignUp = makeNavigate (
28022859 signUpContinueUrl ||
28032860 buildURL (
@@ -2810,6 +2867,7 @@ export class Clerk implements ClerkInterface {
28102867 ) ;
28112868
28122869 let signInOrSignUp : SignInResource | SignUpResource ;
2870+ let viaSignUp = false ;
28132871 try {
28142872 signInOrSignUp = await this . client . signIn . authenticateWithWeb3 ( {
28152873 identifier,
@@ -2819,6 +2877,7 @@ export class Clerk implements ClerkInterface {
28192877 } ) ;
28202878 } catch ( err ) {
28212879 if ( isError ( err , ERROR_CODES . FORM_IDENTIFIER_NOT_FOUND ) ) {
2880+ viaSignUp = true ;
28222881 signInOrSignUp = await this . client . signUp . authenticateWithWeb3 ( {
28232882 identifier,
28242883 generateSignature,
@@ -2831,7 +2890,10 @@ export class Clerk implements ClerkInterface {
28312890 if (
28322891 signUpContinueUrl &&
28332892 signInOrSignUp . status === 'missing_requirements' &&
2834- signInOrSignUp . verifications . web3Wallet . status === 'verified'
2893+ signInOrSignUp . verifications . web3Wallet . status === 'verified' &&
2894+ // A protect_check gate also surfaces as missing_requirements; don't skip past it into
2895+ // the continue step. The gate is handled by the sign-up protect-check route instead.
2896+ ! signInOrSignUp . protectCheck
28352897 ) {
28362898 await navigateToContinueSignUp ( ) ;
28372899 }
@@ -2852,6 +2914,15 @@ export class Clerk implements ClerkInterface {
28522914 } ) ;
28532915 } ;
28542916
2917+ // A Clerk Protect challenge can gate the inline web3 attempt (no redirect happens, so the
2918+ // centralized _handleRedirectCallback check never runs). Route to the challenge before the
2919+ // status switch below, otherwise the user is stranded on the wallet step. The sign-up fallback
2920+ // gates as `missing_requirements` + `protectCheck`, so it has no status branch below either.
2921+ if ( signInOrSignUp . protectCheck || signInOrSignUp . status === 'needs_protect_check' ) {
2922+ await ( viaSignUp ? navigateToSignUpProtectCheck : navigateToSignInProtectCheck ) ( ) ;
2923+ return ;
2924+ }
2925+
28552926 switch ( signInOrSignUp . status ) {
28562927 case 'needs_second_factor' :
28572928 await navigateToFactorTwo ( ) ;
0 commit comments