From 5b6310a64cc949d01de7e21416c6410738baa4e9 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Fri, 27 Mar 2026 17:56:02 +0000 Subject: [PATCH 1/2] fix: remove KYC gate from post-signup claim redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New users arriving via claim links were blocked from seeing the claim modal because PostSignupActionManager required KYC verification. KYC is only needed for bank/offramp claims, and the claim page itself handles that gating — the modal-level check was redundant and breaking the flow. Also adds TODO for future WebAuthn conditional mediation to address Samsung dual-provider passkey conflicts. --- .../Global/PostSignupActionManager/index.tsx | 11 ++++------- src/hooks/useZeroDev.ts | 6 +++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/Global/PostSignupActionManager/index.tsx b/src/components/Global/PostSignupActionManager/index.tsx index b58d6143f6..aa7b1f8865 100644 --- a/src/components/Global/PostSignupActionManager/index.tsx +++ b/src/components/Global/PostSignupActionManager/index.tsx @@ -6,8 +6,6 @@ import { useEffect, useState } from 'react' import ActionModal from '../ActionModal' import { POST_SIGNUP_ACTIONS } from './post-signup-action.consts' import { type IconName } from '../Icons/Icon' -import { useAuth } from '@/context/authContext' -import { isUserKycVerified } from '@/constants/kyc.consts' export const PostSignupActionManager = ({ onActionModalVisibilityChange, @@ -23,11 +21,10 @@ export const PostSignupActionManager = ({ action: () => void } | null>(null) const router = useRouter() - const { user } = useAuth() - const checkClaimModalAfterKYC = () => { + const checkForPostSignupAction = () => { const redirectUrl = getRedirectUrl() - if (isUserKycVerified(user?.user) && redirectUrl) { + if (redirectUrl) { const matchedAction = POST_SIGNUP_ACTIONS.find((action) => action.pathPattern.test(redirectUrl)) if (matchedAction) { setActionConfig({ @@ -44,8 +41,8 @@ export const PostSignupActionManager = ({ } useEffect(() => { - checkClaimModalAfterKYC() - }, [router, user]) + checkForPostSignupAction() + }, [router]) useEffect(() => { onActionModalVisibilityChange(showModal) diff --git a/src/hooks/useZeroDev.ts b/src/hooks/useZeroDev.ts index adff7930de..43d922e503 100644 --- a/src/hooks/useZeroDev.ts +++ b/src/hooks/useZeroDev.ts @@ -129,7 +129,11 @@ export const useZeroDev = () => { } } - // login function + // TODO: Consider implementing conditional mediation (WebAuthn autofill) for login. + // Currently, discoverable credential requests cause conflicts on Samsung devices with + // dual passkey providers (Google Credential Manager + Samsung Pass). Conditional mediation + // surfaces passkeys from ALL providers in the autofill UI, bypassing provider priority issues. + // See: https://web.dev/articles/passkey-form-autofill const handleLogin = async () => { dispatch(zerodevActions.setIsLoggingIn(true)) try { From 5f4856207954408aaf746e61989f289342821961 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Fri, 27 Mar 2026 19:00:39 +0000 Subject: [PATCH 2/2] fix: auto-retry passkey login on NotAllowedError for multi-provider devices On Samsung devices with both Google Credential Manager and Samsung Pass, the first provider may intercept the WebAuthn request and report "no passkeys" while the actual passkey lives in the other provider. This adds a single automatic retry so the second provider gets a chance to respond, which typically succeeds. Addresses ~3,900 Sentry events (PEANUT-UI-6T2/A9W/6T7) where many are likely caused by this dual-provider conflict rather than actual missing passkeys. --- src/hooks/useZeroDev.ts | 57 +++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/src/hooks/useZeroDev.ts b/src/hooks/useZeroDev.ts index 43d922e503..c1870c0ecb 100644 --- a/src/hooks/useZeroDev.ts +++ b/src/hooks/useZeroDev.ts @@ -134,34 +134,47 @@ export const useZeroDev = () => { // dual passkey providers (Google Credential Manager + Samsung Pass). Conditional mediation // surfaces passkeys from ALL providers in the autofill UI, bypassing provider priority issues. // See: https://web.dev/articles/passkey-form-autofill - const handleLogin = async () => { - dispatch(zerodevActions.setIsLoggingIn(true)) - try { - const passkeyServerHeaders: Record = {} + const _attemptLogin = async () => { + const passkeyServerHeaders: Record = {} - if (user?.user?.username) { - passkeyServerHeaders['x-username'] = user.user.username - } + if (user?.user?.username) { + passkeyServerHeaders['x-username'] = user.user.username + } - const webAuthnKey = await toWebAuthnKey({ - passkeyName: '[]', - passkeyServerUrl: consts.PASSKEY_SERVER_URL as string, - mode: WebAuthnMode.Login, - passkeyServerHeaders, - rpID: window.location.hostname.replace(/^www\./, ''), - }) + const webAuthnKey = await toWebAuthnKey({ + passkeyName: '[]', + passkeyServerUrl: consts.PASSKEY_SERVER_URL as string, + mode: WebAuthnMode.Login, + passkeyServerHeaders, + rpID: window.location.hostname.replace(/^www\./, ''), + }) - setWebAuthnKey(webAuthnKey) - saveToCookie(WEB_AUTHN_COOKIE_KEY, webAuthnKey, 90) + setWebAuthnKey(webAuthnKey) + saveToCookie(WEB_AUTHN_COOKIE_KEY, webAuthnKey, 90) + } + + const handleLogin = async () => { + dispatch(zerodevActions.setIsLoggingIn(true)) + try { + await _attemptLogin() } catch (e) { const error = e as Error if (error.name === 'NotAllowedError') { - // User cancelled - no state was saved, just let them retry - dispatch(zerodevActions.setIsLoggingIn(false)) - throw new PasskeyError( - 'Login was canceled or no passkey found. Please try again or register.', - 'LOGIN_CANCELED' - ) + // On devices with multiple passkey providers (e.g. Samsung with Google + Samsung Pass), + // the first provider may intercept and report "no passkeys" while the actual passkey + // lives in the other provider. Auto-retry once to give the second provider a chance. + console.warn('[useZeroDev] Login NotAllowedError, retrying once for multi-provider devices') + try { + await _attemptLogin() + return // retry succeeded + } catch (retryError) { + // retry also failed — throw user-facing error + dispatch(zerodevActions.setIsLoggingIn(false)) + throw new PasskeyError( + 'Login was canceled or no passkey found. Please try again or register.', + 'LOGIN_CANCELED' + ) + } } // Other login errors - clear any stale state