Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions src/components/Send/link/views/Initial.link.send.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,35 +105,46 @@ 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'
Comment on lines +108 to +110
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

String literal comparison for error detection is fragile.

Using the hardcoded string 'Insufficient balance' to distinguish balance errors from other errors (like PasskeyError) creates a tight coupling that breaks if the error message changes elsewhere. This is a maintainability risk.

🔎 Recommended solution: Use error types or codes instead

Define error types/codes in a shared constant:

// In a shared constants file or at the top of the file
export const ERROR_TYPES = {
  INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE',
  PASSKEY_ERROR: 'PASSKEY_ERROR',
  // ... other error types
} as const

Update the error state interface to include an error type/code field:

// In Redux slice or wherever errorState is defined
interface ErrorState {
  showError: boolean
  errorMessage: string
  errorType?: string  // Add this
}

Then use error types for comparison:

-        const isBalanceError = errorState?.errorMessage === 'Insufficient balance'
+        const isBalanceError = errorState?.errorType === ERROR_TYPES.INSUFFICIENT_BALANCE

And when setting errors:

            dispatch(
                sendFlowActions.setErrorState({
                    showError: true,
                    errorMessage: 'Insufficient balance',
+                   errorType: ERROR_TYPES.INSUFFICIENT_BALANCE,
                })
            )

This would decouple error detection logic from user-facing messages and make the code more maintainable.

Based on learnings: prefer generic error messages for hotfixes until copy can be reviewed, but use typed error codes internally for logic.

Also applies to: 134-134

🤖 Prompt for AI Agents
In src/components/Send/link/views/Initial.link.send.view.tsx around lines
108-110 (and also apply to line 134), the code uses a hardcoded string
comparison ('Insufficient balance') to detect balance errors which is fragile;
replace this by adding and using an errorType/errorCode on the shared error
state (e.g., ERROR_TYPES.INSUFFICIENT_BALANCE) instead of comparing user-facing
messages: update the error state/interface where errors are set to include
errorType, introduce a shared constant for error codes, change the check here to
compare errorState.errorType to that constant, and update all places that set
this error to set the new errorType; keep user-facing messages separate and
unchanged.


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,
errorMessage: '',
})
)
}
}, [peanutWalletBalance, tokenValue, dispatch, hasPendingTransactions, isLoading])
}, [peanutWalletBalance, tokenValue, dispatch, hasPendingTransactions, isLoading, errorState])

return (
<div className="w-full space-y-4">
Expand Down
41 changes: 38 additions & 3 deletions src/hooks/useZeroDev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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, {
Expand Down
Loading