From e2c8a9a09de9a5f0d1a391fe2dd9f278f7839a11 Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Mon, 27 Jul 2026 13:00:28 +0100 Subject: [PATCH 1/2] fix(demo+passkey): /rain/cards demo fixture, WebKit 'Load failed' classification, full auth clear MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the applicable subset of mobile-release 9cdb8aec0: - demo: add /rain/cards fixture so overview consumers never see the {} fallback (crashes deref of .status/.cards — PEANUT-UI-RM6 on native) - passkey: classify WebKit 'Load failed' fetch errors as NETWORK instead of the LOGIN_ERROR modal (PEANUT-UI-QV6) - auth: clearAuthState delegates to clearAuthToken so native token stores are cleared too, not just the jwt cookie --- src/utils/__tests__/demo-api.test.ts | 10 ++++++++++ src/utils/__tests__/webauthn.utils.test.ts | 5 +++++ src/utils/auth.utils.ts | 8 ++++---- src/utils/demo-api.ts | 8 ++++++++ src/utils/webauthn.utils.ts | 4 +++- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/utils/__tests__/demo-api.test.ts b/src/utils/__tests__/demo-api.test.ts index 1b2332803d..a8bd53711a 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 e4c1013cc0..8d82864150 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 ba05b2c0f9..60c4d810aa 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' /** @@ -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. + void 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 d90915246b..328ff4d925 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 747e430154..1727030657 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) } /** From 69499e8337157691615406e7b5f660e39b879f9b Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Mon, 27 Jul 2026 13:10:50 +0100 Subject: [PATCH 2/2] fix(auth): await native token clear in clearAuthState Review follow-up: clearAuthToken's Preferences/cookie-jar clears are async; make clearAuthState await them so callers can't proceed on a half-cleared session (in-memory clear was already synchronous). --- src/hooks/useAccountSetup.ts | 2 +- src/hooks/useZeroDev.ts | 2 +- src/utils/auth.utils.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useAccountSetup.ts b/src/hooks/useAccountSetup.ts index 04a96807d9..abb471b21b 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 81cb91e988..effa6cbc04 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/auth.utils.ts b/src/utils/auth.utils.ts index 60c4d810aa..267de023ac 100644 --- a/src/utils/auth.utils.ts +++ b/src/utils/auth.utils.ts @@ -11,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) { @@ -23,7 +23,7 @@ export const clearAuthState = (userId?: string) => { // 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. - void clearAuthToken() + await clearAuthToken() console.log('Cleared auth state', { userId: userId || 'none' }) } catch (error) {