Skip to content

fix(passkey): stop reporting a cancelled passkey prompt as an error - #2739

Open
innolope-dev wants to merge 1 commit into
devfrom
fix/passkey-cancel-unhandled
Open

fix(passkey): stop reporting a cancelled passkey prompt as an error#2739
innolope-dev wants to merge 1 commit into
devfrom
fix/passkey-cancel-unhandled

Conversation

@innolope-dev

@innolope-dev innolope-dev commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

The intent that was being undone

useZeroDev.handleLogin already does the right thing. It classifies the raw WebAuthn failure, captures it with full context, and throws a curated PasskeyError carrying user-facing copy. For a plain user cancel it captures nothing on web:

// Cancel saved no state; everything else clears stale state and reports the error to Sentry.
if (code !== 'LOGIN_CANCELED') {
    
    captureException(err, { tags: { error_type: 'login_error' } })
} else if (isCapacitor()) {
    // keep visibility without alerting
    captureException(err, { level: 'warning', tags: { error_type: 'login_canceled_native' } })
}

Four call sites then reported the wrapper anyway, which is why PEANUT-UI-R20"Login was cancelled, or no passkey was found on this device" — exists at error level despite that deliberate silence.

What changed

Site Was Now
InvitesPage void handleLoginClick(), no catch → unhandled rejection on every cancel caught; curated message surfaced like every other login entry point
GuestLoginModal console.error(e)captureConsoleIntegration event dropped; toast unchanged
Landing Sentry.captureException(wrapper) skipped for PasskeyError
JoinWaitlist Sentry.captureException(wrapper) skipped for PasskeyError

The guard is a named helper in webauthn.utils.ts rather than four inline checks, and it's narrow — Landing and JoinWaitlist still report anything that isn't a PasskeyError, so an unexpected failure in the login path is not silenced. PostHog's SIGNUP_LOGIN_ERROR is untouched: a failed login is still a funnel event, just not a crash report.

Note on InvitesPage

That one is a real bug independent of Sentry — a floating promise with no rejection handler. It previously showed the user nothing at all on failure; it now shows the curated message, consistent with Landing, SetupPasskey and JoinWaitlist.

Verification

  • src/components/Invites: 61 passed, 1 failed — identical to the dev baseline. The one failure is badge-campaign-context.test.ts › source-qualifies every published content UTM, which reads @/content/generated/… and fails in any worktree without the src/content submodule initialised. Confirmed pre-existing by re-running with this change reverted.
  • src/components/Setup, GuestLoginModal, webauthn: 8 passed, 0 failed.
  • tsc --noEmit: 231 errors, zero in any touched file — 229 are missing @/assets/* declarations in a fresh worktree, 2 are the same src/content submodule.
  • prettier --check: clean.

InvitesPage.test.tsx gained a useToast mock alongside the existing useLogin one, since the suite renders InvitePageContent outside a ToastProvider.

Related

#2735 adds PasskeyError to the alreadyReported beforeSend list as a backstop, so a future call site can't reintroduce this. This PR removes the reports at source; that one stops them arriving if it happens again.

Correction to the commit message

The commit says "PEANUT-UI-QRW and PEANUT-UI-R20: 19 events yesterday". The accurate figure is 13/day — QRW 9 + R20 4.

The extra 6 was PEANUT-UI-SFV ("No matching passkey was found"), which this PR does not fix. SFV is the raw Android error captured at the throw site in useZeroDev, not the PasskeyError wrapper, so none of the four call-site changes touch it. It's arguably also an expected outcome (no passkey enrolled on this device) and worth reclassifying to LOGIN_CANCELED, but that changes user-facing copy and belongs in its own change.

peanut-ui blocks force-push, so the commit message stands as written.

Summary by CodeRabbit

  • Bug Fixes

    • Improved login failure handling across invitations and setup flows.
    • Login errors now display appropriate localized messages without causing unhandled promise rejections.
    • Prevented duplicate reporting of passkey and WebAuthn errors while preserving user notifications and tracking.
  • Tests

    • Updated invitation flow tests to safely handle toast notifications.

useZeroDev classifies a WebAuthn failure, captures the raw error with full
context, and throws a curated PasskeyError for display. For a plain user
cancel it deliberately captures nothing on web — "Cancel saved no state".

Four call sites undid that:

- InvitesPage fired `void handleLoginClick()` with no catch, so cancelling the
  prompt became an unhandled rejection. Now caught, and the curated message is
  surfaced the way every other login entry point surfaces it.
- GuestLoginModal console.error'd the wrapper, which captureConsoleIntegration
  turns into an event.
- Landing and JoinWaitlist called Sentry.captureException on the wrapper —
  a second, context-free copy of an error already reported at the throw site,
  and the reason LOGIN_CANCELED showed up at error level at all.

Landing and JoinWaitlist still report anything that isn't a PasskeyError, so an
unexpected failure in the login path is not silenced.

PEANUT-UI-QRW and PEANUT-UI-R20: 19 events yesterday, all expected outcomes.
@innolope-dev innolope-dev self-assigned this Aug 18, 2026
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 18, 2026 4:20pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8222ad59-e7fd-42ec-94f7-614595e393e9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 216988fd-25a4-44f1-84a1-45fa61b23f9f

📥 Commits

Reviewing files that changed from the base of the PR and between 062be52 and 8f5a9f5.

📒 Files selected for processing (6)
  • src/components/Global/GuestLoginModal/index.tsx
  • src/components/Invites/InvitesPage.test.tsx
  • src/components/Invites/InvitesPage.tsx
  • src/components/Setup/Views/JoinWaitlist.tsx
  • src/components/Setup/Views/Landing.tsx
  • src/utils/webauthn.utils.ts

Included review availability: 3 reviews are currently available. Based on recent review activity, included reviews refill at 5 per hour.


📝 Walkthrough

Walkthrough

Login failure handling now shows feedback in the guest login and invites flows. Setup views avoid duplicate Sentry reports for PasskeyError instances through a shared WebAuthn utility.

Changes

Login error handling

Layer / File(s) Summary
Passkey error detection and Sentry filtering
src/utils/webauthn.utils.ts, src/components/Setup/Views/JoinWaitlist.tsx, src/components/Setup/Views/Landing.tsx
Added isAlreadyReported for PasskeyError detection. Setup error handlers skip duplicate Sentry exception capture while retaining error-code metadata.
Login rejection feedback
src/components/Global/GuestLoginModal/index.tsx, src/components/Invites/InvitesPage.tsx, src/components/Invites/InvitesPage.test.tsx
Guest login removes console logging from rejected logins. Invite login displays the passkey error or localized fallback through a toast. Invite tests mock useToast methods.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 8f5a9

This change prevents expected passkey cancellations from being reported as errors while preserving user messaging and unexpected-error reporting; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: kushagrasarathe, hugo0

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: preventing cancelled passkey prompts from being reported as errors.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/passkey-cancel-unhandled

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7167.28 → 7168 (+0.72)
Findings: 0 net (+24 new, -24 resolved)

🆕 New findings (24)

  • critical complexity — src/components/Invites/InvitesPage.tsx — CC 87, MI 55.1, SLOC 214
  • high complexity — src/components/Setup/Views/JoinWaitlist.tsx — CC 32, MI 58.55, SLOC 115
  • medium high-mdd — src/components/Invites/InvitesPage.tsx:39 — InvitePageContent: MDD 109.8 (uses across many lines from declarations)
  • medium high-mdd — src/components/Setup/Views/JoinWaitlist.tsx:29 — JoinWaitlist: MDD 50.7 (uses across many lines from declarations)
  • medium high-dlt — src/components/Invites/InvitesPage.tsx:39 — InvitePageContent: DLT 49 (calls 49 distinct functions — high context load)
  • medium high-dlt — src/components/Setup/Views/JoinWaitlist.tsx:29 — JoinWaitlist: DLT 34 (calls 34 distinct functions — high context load)
  • medium hotspot — src/components/Invites/InvitesPage.tsx — 29 commits, +553/-347 lines since 6 months ago
  • medium high-mdd — src/components/Setup/Views/Landing.tsx:23 — LandingStep: MDD 28.4 (uses across many lines from declarations)
  • medium complexity — src/utils/webauthn.utils.ts — CC 28, MI 54.34, SLOC 117
  • medium method-complexity — src/components/Invites/InvitesPage.tsx:39 — InvitePageContent CC 22 SLOC 96
  • medium complexity — src/components/Setup/Views/Landing.tsx — CC 15, MI 62.13, SLOC 56
  • low high-dlt — src/components/Setup/Views/Landing.tsx:23 — LandingStep: DLT 20 (calls 20 distinct functions — high context load)
  • low high-mdd — src/components/Invites/InvitesPage.tsx:144 — : MDD 18.9 (uses across many lines from declarations)
  • low high-mdd — src/components/Global/GuestLoginModal/index.tsx:9 — GuestLoginModal: MDD 17.6 (uses across many lines from declarations)
  • low structural-dup — components/Invites/InvitesPage.test.tsx:182 — 17 duplicate lines / 61 tokens with components/Invites/InvitesPage.test.tsx:403
  • low structural-dup — components/Invites/InvitesPage.test.tsx:475 — 17 duplicate lines / 74 tokens with components/Invites/InvitesPage.test.tsx:615
  • low structural-dup — components/Invites/InvitesPage.test.tsx:180 — 16 duplicate lines / 60 tokens with components/Invites/InvitesPage.test.tsx:211
  • low high-dlt — src/components/Invites/InvitesPage.tsx:144 — : DLT 15 (calls 15 distinct functions — high context load)
  • low high-mdd — src/components/Invites/InvitesPage.tsx:160 — : MDD 12.6 (uses across many lines from declarations)
  • low structural-dup — components/Invites/InvitesPage.test.tsx:427 — 12 duplicate lines / 51 tokens with components/Invites/InvitesPage.test.tsx:663

…and 4 more.

✅ Resolved (24)

  • src/components/Invites/InvitesPage.tsx — CC 84, MI 54.71, SLOC 209
  • src/components/Setup/Views/JoinWaitlist.tsx — CC 31, MI 58.76, SLOC 113
  • src/components/Invites/InvitesPage.tsx:38 — InvitePageContent: MDD 106.0 (uses across many lines from declarations)
  • src/components/Setup/Views/JoinWaitlist.tsx:28 — JoinWaitlist: MDD 49.7 (uses across many lines from declarations)
  • src/components/Invites/InvitesPage.tsx:38 — InvitePageContent: DLT 47 (calls 47 distinct functions — high context load)
  • src/components/Setup/Views/JoinWaitlist.tsx:28 — JoinWaitlist: DLT 33 (calls 33 distinct functions — high context load)
  • src/components/Invites/InvitesPage.tsx — 28 commits, +546/-346 lines since 6 months ago
  • src/components/Setup/Views/Landing.tsx:22 — LandingStep: MDD 27.3 (uses across many lines from declarations)
  • src/utils/webauthn.utils.ts — CC 26, MI 53.01, SLOC 115
  • src/components/Invites/InvitesPage.tsx:38 — InvitePageContent CC 22 SLOC 94
  • src/components/Setup/Views/Landing.tsx — CC 14, MI 62.7, SLOC 54
  • src/components/Invites/InvitesPage.tsx:142 — : MDD 18.9 (uses across many lines from declarations)
  • src/components/Setup/Views/Landing.tsx:22 — LandingStep: DLT 19 (calls 19 distinct functions — high context load)
  • components/Invites/InvitesPage.test.tsx:178 — 17 duplicate lines / 61 tokens with components/Invites/InvitesPage.test.tsx:399
  • components/Invites/InvitesPage.test.tsx:471 — 17 duplicate lines / 74 tokens with components/Invites/InvitesPage.test.tsx:611
  • src/components/Global/GuestLoginModal/index.tsx:9 — GuestLoginModal: MDD 17.0 (uses across many lines from declarations)
  • components/Invites/InvitesPage.test.tsx:176 — 16 duplicate lines / 60 tokens with components/Invites/InvitesPage.test.tsx:207
  • src/components/Invites/InvitesPage.tsx:142 — : DLT 15 (calls 15 distinct functions — high context load)
  • src/components/Invites/InvitesPage.tsx:158 — : MDD 12.6 (uses across many lines from declarations)
  • components/Invites/InvitesPage.test.tsx:423 — 12 duplicate lines / 51 tokens with components/Invites/InvitesPage.test.tsx:659

…and 4 more.

@innolope-dev

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 3172 ran, 0 failed, 0 skipped, 41.8s

📊 Coverage (unit)

metric %
statements 67.4%
branches 52.3%
functions 57.8%
lines 68.2%
⏱ 10 slowest test cases
time test
2.7s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.9s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/hooks/__tests__/useCrispTokenId.test.ts › retries then stays undefined when the endpoint keeps failing (no fallback token)
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/utils/__tests__/auth-token.test.ts › ignores the guarded marker and falls back to the plain token
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.2s src/utils/__tests__/auth-token.test.ts › is none — never guarded — when only the guarded marker is present
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant