Skip to content
Merged
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
5 changes: 3 additions & 2 deletions src/components/Global/GuestLoginModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ const GuestLoginModal = () => {
onClick={() => {
handleLogin()
.then(closeModal)
.catch((e) => {
console.error(e)
.catch(() => {
// useZeroDev already reported the underlying failure;
// console.error here would capture the wrapper again.
toast.error(t('guestLoginModal.loginError'))
})
}}
Expand Down
4 changes: 4 additions & 0 deletions src/components/Invites/InvitesPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ jest.mock('@/redux/slices/setup-slice', () => ({
},
}))
jest.mock('@/hooks/useLogin', () => ({ useLogin: () => ({ handleLoginClick: mockLogin, isLoggingIn: false }) }))
jest.mock('@/components/0_Bruddle/Toast', () => ({
...jest.requireActual('@/components/0_Bruddle/Toast'),
useToast: () => ({ error: jest.fn(), success: jest.fn() }),
}))
jest.mock('@/hooks/useGuestStoreHandoff', () => ({
useGuestStoreHandoff: (opts: { trackImpressionWhenGuest?: boolean }) => {
mockUseGuestStoreHandoff(opts)
Expand Down
8 changes: 7 additions & 1 deletion src/components/Invites/InvitesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { EInviteType } from '@/services/services.types'
import { getValidRedirectUrl, saveRedirectUrl, saveToCookie } from '@/utils/general.utils'
import { useGuestStoreHandoff } from '@/hooks/useGuestStoreHandoff'
import { useLogin } from '@/hooks/useLogin'
import { useToast } from '@/components/0_Bruddle/Toast'
import UnsupportedBrowserModal from '../Global/UnsupportedBrowserModal'
import posthog from 'posthog-js'
import { useTranslations } from 'next-intl'
Expand All @@ -38,6 +39,7 @@ import { destinationForInviteAcquisition } from '@/services/invite-acquisition'
function InvitePageContent() {
const t = useTranslations('invites')
const tSetup = useTranslations('setup')
const toast = useToast()
const searchParams = useSearchParams()
// trim trailing '?' from invite code to handle qr codes with ? at the end
const inviteCode = searchParams.get('code')?.toLowerCase().replace(/\?+$/, '')
Expand Down Expand Up @@ -292,7 +294,11 @@ function InvitePageContent() {
// normal-app fallback.
saveRedirectUrl()
}
void handleLoginClick()
// PasskeyError carries curated user-facing copy; without this catch a
// cancelled passkey prompt becomes an unhandled rejection and a Sentry event.
handleLoginClick().catch((error: unknown) => {
toast.error((error instanceof Error && error.message) || tSetup('loginFailed'))
})
}

useEffect(() => {
Expand Down
5 changes: 4 additions & 1 deletion src/components/Setup/Views/JoinWaitlist.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { Button } from '@/components/0_Bruddle/Button'
import { isAlreadyReported } from '@/utils/webauthn.utils'
import { useToast } from '@/components/0_Bruddle/Toast'
import ValidatedInput from '@/components/Global/ValidatedInput'
import { useEffect, useState } from 'react'
Expand Down Expand Up @@ -89,7 +90,9 @@ const JoinWaitlist = () => {
? t('waitlist.noPasskey')
: t('waitlist.loginUnexpectedError')
toast.error(errorMessage)
Sentry.captureException(error, { extra: { errorCode } })
if (!isAlreadyReported(error)) {
Sentry.captureException(error, { extra: { errorCode } })
}
}

const _onLoginClick = async () => {
Expand Down
5 changes: 4 additions & 1 deletion src/components/Setup/Views/Landing.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { useToast } from '@/components/0_Bruddle/Toast'
import { isAlreadyReported } from '@/utils/webauthn.utils'
import { useSetupFlow } from '@/hooks/useSetupFlow'
import { useLogin } from '@/hooks/useLogin'
import * as Sentry from '@sentry/nextjs'
Expand Down Expand Up @@ -45,7 +46,9 @@ const LandingStep = () => {
const handleError = (error: unknown) => {
const errorCode = error instanceof Error && 'code' in error ? String(error.code) : undefined
toast.error((error instanceof Error && error.message) || t('loginFailed'))
Sentry.captureException(error, { extra: { errorCode } })
if (!isAlreadyReported(error)) {
Sentry.captureException(error, { extra: { errorCode } })
}
posthog.capture(ANALYTICS_EVENTS.SIGNUP_LOGIN_ERROR, { error_code: errorCode })
}

Expand Down
10 changes: 10 additions & 0 deletions src/utils/webauthn.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ export const PASSKEY_WARNINGS = {
*/
const WEBAUTHN_ERROR_NAMES = new Set<string>(Object.values(WebAuthnErrorName))

/**
* useZeroDev captures the raw WebAuthn failure with full context before throwing
* the curated PasskeyError — and for a plain user cancel it deliberately captures
* nothing on web. Re-reporting the wrapper at a call site adds a context-free
* duplicate and undoes that silence.
*/
export function isAlreadyReported(error: unknown): boolean {
return error instanceof Error && error.name === 'PasskeyError'
}

export function capturePasskeySignFailure(error: unknown, context: string): void {
if (!(error instanceof Error) || !WEBAUTHN_ERROR_NAMES.has(error.name)) return
posthog.capture(ANALYTICS_EVENTS.PASSKEY_SIGN_FAILED, { error_name: error.name, context })
Expand Down
Loading