diff --git a/src/components/Send/link/views/Initial.link.send.view.tsx b/src/components/Send/link/views/Initial.link.send.view.tsx index 007b69988..e9cdabf63 100644 --- a/src/components/Send/link/views/Initial.link.send.view.tsx +++ b/src/components/Send/link/views/Initial.link.send.view.tsx @@ -105,27 +105,38 @@ const LinkSendInitialView = () => { return } + // only manage balance related errors here + // dont clear non-balance errors (like PasskeyError from transaction signing like biometric timeout errors) + const isBalanceError = errorState?.errorMessage === 'Insufficient balance' + if (!peanutWalletBalance || !tokenValue) { - // Clear error state when no balance or token value - dispatch( - sendFlowActions.setErrorState({ - showError: false, - errorMessage: '', - }) - ) + // clear balance error when no balance or token value + // but preserve non-balance errors (e.g. PasskeyError) + if (isBalanceError) { + dispatch( + sendFlowActions.setErrorState({ + showError: false, + errorMessage: '', + }) + ) + } return } + if ( parseUnits(peanutWalletBalance, PEANUT_WALLET_TOKEN_DECIMALS) < parseUnits(tokenValue, PEANUT_WALLET_TOKEN_DECIMALS) ) { + // set insufficient balance error dispatch( sendFlowActions.setErrorState({ showError: true, errorMessage: 'Insufficient balance', }) ) - } else { + } else if (isBalanceError) { + // clear balance error only if thats the current error + // dont clear non-balance errors (e.g. PasskeyError) dispatch( sendFlowActions.setErrorState({ showError: false, @@ -133,7 +144,7 @@ const LinkSendInitialView = () => { }) ) } - }, [peanutWalletBalance, tokenValue, dispatch, hasPendingTransactions, isLoading]) + }, [peanutWalletBalance, tokenValue, dispatch, hasPendingTransactions, isLoading, errorState]) return (
diff --git a/src/hooks/useZeroDev.ts b/src/hooks/useZeroDev.ts index c7cb01450..8c8c52756 100644 --- a/src/hooks/useZeroDev.ts +++ b/src/hooks/useZeroDev.ts @@ -6,7 +6,14 @@ import { useAuth } from '@/context/authContext' import { useKernelClient } from '@/context/kernelClient.context' import { useAppDispatch, useSetupStore, useZerodevStore } from '@/redux/hooks' import { zerodevActions } from '@/redux/slices/zerodev-slice' -import { getFromCookie, removeFromCookie, saveToCookie, clearAuthState } from '@/utils' +import { + getFromCookie, + removeFromCookie, + saveToCookie, + clearAuthState, + capturePasskeyDebugInfo, + WebAuthnErrorName, +} from '@/utils' import { toWebAuthnKey, WebAuthnMode } from '@zerodev/passkey-validator' import { useCallback, useContext } from 'react' import type { TransactionReceipt, Hex, Hash } from 'viem' @@ -169,8 +176,36 @@ export const useZeroDev = () => { } catch (error) { console.error('Error sending UserOp:', error) - // Detect stale webAuthnKey errors (AA24, wapk) and provide user feedback - // NOTE: Don't auto-clear here - user is mid-transaction, avoid data loss + // handle NotAllowedError (user canceled, timeout, or biometric failure) + // this commonly occurs when: + // - windows biometric reader takes too long (exceeds webauthn timeout) + // - user cancels the authentication prompt + // - biometric authentication fails + if ((error as Error).name === WebAuthnErrorName.NotAllowed) { + // capture device debug info to help diagnose device-specific issues + const debugInfo = await capturePasskeyDebugInfo('transaction_signing_not_allowed') + + captureException(error, { + tags: { error_type: 'transaction_signing_not_allowed' }, + extra: { + errorMessage: String(error), + context: 'transaction_signing', + userId: user?.user.userId, + // device debug info for troubleshooting + debugInfo, + }, + }) + + dispatch(zerodevActions.setIsSendingUserOp(false)) + + throw new PasskeyError( + 'Biometric verification timed out. Please try again and complete the verification.', + 'SIGNING_CANCELED' + ) + } + + // detect stale webAuthnKey errors (AA24, wapk) and provide user feedback + // NOTE: don't auto-clear here - user is mid-transaction, avoid data loss if (isStaleKeyError(error)) { console.error('Detected stale webAuthnKey error - session is invalid') captureException(error, {