diff --git a/src/hooks/useAccountSetup.ts b/src/hooks/useAccountSetup.ts index 04a96807d..abb471b21 100644 --- a/src/hooks/useAccountSetup.ts +++ b/src/hooks/useAccountSetup.ts @@ -114,7 +114,7 @@ export const useAccountSetup = () => { setError('Error adding account. Please try refreshing the page.') // clear auth state if account creation fails - clearAuthState(user?.user.userId) + await clearAuthState(user?.user.userId) return false } finally { setIsProcessing(false) diff --git a/src/hooks/useZeroDev.ts b/src/hooks/useZeroDev.ts index 81cb91e98..effa6cbc0 100644 --- a/src/hooks/useZeroDev.ts +++ b/src/hooks/useZeroDev.ts @@ -198,7 +198,7 @@ export const useZeroDev = () => { // Cancel saved no state; everything else clears stale state and reports the error to Sentry. if (code !== 'LOGIN_CANCELED') { console.error('Error logging in', err) - clearAuthState(user?.user.userId) + await clearAuthState(user?.user.userId) captureException(err, { tags: { error_type: 'login_error' } }) } else if (isCapacitor()) { // the native plugin maps ceremony failures (.failed/.notHandled) to the diff --git a/src/utils/__tests__/demo-api.test.ts b/src/utils/__tests__/demo-api.test.ts index 1b2332803..a8bd53711 100644 --- a/src/utils/__tests__/demo-api.test.ts +++ b/src/utils/__tests__/demo-api.test.ts @@ -78,6 +78,16 @@ describe('demoRespond — routing', () => { }) }) +describe('demoRespond — rain card overview', () => { + it('returns a full RainCardOverview shape (useRainCardOverview derefs .status, cardState derefs .cards)', async () => { + const { res, data } = await body('/rain/cards') + expect(res.status).toBe(200) + expect(data.status.hasApplication).toBe(false) + expect(data.balance).toBeNull() + expect(Array.isArray(data.cards)).toBe(true) + }) +}) + describe('demoRespond — shape-aware fallback (never throws on undefined.map)', () => { it('returns [] for an unmatched collection-ish path', async () => { const { res, data } = await body('/something/payments') diff --git a/src/utils/__tests__/webauthn.utils.test.ts b/src/utils/__tests__/webauthn.utils.test.ts index e4c1013cc..8d8286415 100644 --- a/src/utils/__tests__/webauthn.utils.test.ts +++ b/src/utils/__tests__/webauthn.utils.test.ts @@ -64,6 +64,11 @@ describe('classifyPasskeyError', () => { expect(classifyPasskeyError(err).code).toBe('LOGIN_CANCELED') }) + test('maps WebKit "Load failed" fetch errors to NETWORK', () => { + const err = Object.assign(new TypeError('Load failed'), { name: 'TypeError' }) + expect(classifyPasskeyError(err).code).toBe('NETWORK') + }) + test('falls back to LOGIN_ERROR for unknown errors', () => { expect(classifyPasskeyError(new Error('mystery')).code).toBe('LOGIN_ERROR') }) diff --git a/src/utils/auth.utils.ts b/src/utils/auth.utils.ts index ba05b2c0f..267de023a 100644 --- a/src/utils/auth.utils.ts +++ b/src/utils/auth.utils.ts @@ -1,4 +1,5 @@ import { removeFromCookie, updateUserPreferences } from './general.utils' +import { clearAuthToken } from './auth-token' import * as Sentry from '@sentry/nextjs' /** @@ -10,7 +11,7 @@ import * as Sentry from '@sentry/nextjs' * * @param userId - Optional user ID for clearing user-scoped preferences */ -export const clearAuthState = (userId?: string) => { +export const clearAuthState = async (userId?: string) => { try { // Clear user preferences if userId available if (userId) { @@ -20,10 +21,9 @@ export const clearAuthState = (userId?: string) => { // Clear cookies (always do this, even if no userId) removeFromCookie('web-authn-key') - // Clear JWT cookie - if (typeof document !== 'undefined') { - document.cookie = 'jwt-token=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;' - } + // Clear the JWT everywhere it can live — on native that includes the + // plugin-managed stores, not just the cookie this used to expire by hand. + await clearAuthToken() console.log('Cleared auth state', { userId: userId || 'none' }) } catch (error) { diff --git a/src/utils/demo-api.ts b/src/utils/demo-api.ts index d90915246..328ff4d92 100644 --- a/src/utils/demo-api.ts +++ b/src/utils/demo-api.ts @@ -570,6 +570,14 @@ const ROUTES: Array<{ method: string; pattern: string; handler: Handler }> = [ tokenSymbol: PEANUT_WALLET_TOKEN_SYMBOL, }), }, + // useRainCardOverview polls this for every logged-in user; the fallback {} + // has no `status`/`cards` and crashes consumers that deref them + // (PEANUT-UI-RM6). hasApplication:false also stops polling in demo. + { + method: 'GET', + pattern: '/rain/cards', + handler: () => ({ status: { hasApplication: false }, balance: null, cards: [] }), + }, // rhino (crypto deposit / cross-chain) — return a believable deposit address // (the demo wallet) so the "add money → crypto → choose network" screen renders diff --git a/src/utils/webauthn.utils.ts b/src/utils/webauthn.utils.ts index 747e43015..172703065 100644 --- a/src/utils/webauthn.utils.ts +++ b/src/utils/webauthn.utils.ts @@ -44,7 +44,9 @@ const PASSKEY_LOGIN_MESSAGES: Record = { function isNetworkError(error: Error): boolean { if (error.name === 'TypeError' && /fetch|network/i.test(error.message)) return true - return /failed to fetch|network ?error|timeout|connection|offline/i.test(error.message) + // "Load failed" is WebKit's message for a failed fetch (common when the + // request fires right as the app foregrounds from the passkey sheet) + return /failed to fetch|load failed|network ?error|timeout|connection|offline/i.test(error.message) } /**