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, {