From 9d7acd65909f476fde1307635d5266c8047bc17a Mon Sep 17 00:00:00 2001 From: MrFreePress Date: Sun, 19 Jul 2026 12:08:22 -0400 Subject: [PATCH 1/6] fix(widget): persist identified session tokens to survive page disruptions Previously, identified widget session tokens were stored only in JS memory and lost on tab switches, focus loss, or browser memory pressure. When the widget then tried to submit feedback, it fell back to an anonymous session, which fails on workspaces with allowAnonymous=false. Three fixes: 1. Persist identified tokens to localStorage alongside anonymous tokens (widget-auth.ts) so they survive page-level disruptions. 2. Restore persisted identified tokens in acquireSession() before minting an anonymous session (widget-auth-provider.tsx), and retry the restore in handleSubmit() before the anonymous fallback (widget-home-animated.tsx). 3. Surface a clearer error message when a submission is rejected due to an expired/missing session, instead of a generic 'Network error' (en.json + widget-home-animated.tsx). Fixes the 'Network error. Please try again.' symptom reported when identified users lose their session between page load and form submission. --- .../widget/widget-auth-provider.tsx | 34 ++++++++- .../widget/widget-home-animated.tsx | 75 +++++++++++++++++-- apps/web/src/lib/client/widget-auth.ts | 75 ++++++++++++++++++- apps/web/src/locales/en.json | 1 + 4 files changed, 175 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/widget/widget-auth-provider.tsx b/apps/web/src/components/widget/widget-auth-provider.tsx index 197885362..912e0f987 100644 --- a/apps/web/src/components/widget/widget-auth-provider.tsx +++ b/apps/web/src/components/widget/widget-auth-provider.tsx @@ -17,6 +17,9 @@ import { persistAnonymousToken, readPersistedToken, clearPersistedToken, + persistIdentifiedToken, + readPersistedIdentifiedToken, + clearIdentifiedToken, } from '@/lib/client/widget-auth' import { sendToHost } from '@/lib/client/widget-bridge' import { widgetQueryKeys } from '@/lib/client/hooks/use-widget-vote' @@ -142,6 +145,31 @@ export function WidgetAuthProvider({ const p = (async (): Promise => { try { + // 0. Try a persisted identified token first — it may have survived a + // page interaction (tab switch, focus loss, browser memory pressure) + // that cleared the in-memory token but not localStorage. + const persistedIdent = readPersistedIdentifiedToken() + if (persistedIdent) { + try { + const res = await fetch('/api/widget/session', { + headers: { Authorization: *** ${persistedIdent}` }, + }) + if (res.ok) { + const body = await res.json() + if (body.data?.user) { + storeToken(persistedIdent) + persistIdentifiedToken(persistedIdent) + setUser(body.data.user) + return true + } + // Identified token is stale or demoted — drop it. + clearIdentifiedToken() + } + } catch { + // Server unreachable — fall through. + } + } + // 1. Prefer a persisted anonymous token — validate it server-side // before adopting (it may have expired or been merged away). const persisted = readPersistedToken() @@ -214,9 +242,11 @@ export function WidgetAuthProvider({ (result: { sessionToken: string; user: WidgetUser; votedPostIds?: string[] }) => { storeToken(result.sessionToken) // Any anonymous session was merged into this identified user server-side, - // so drop its persisted token. Identified tokens are never persisted — - // they're re-established via SDK identify / portal passthrough on load. + // so drop its persisted token. Persist the identified token so it survives + // page interactions (e.g. the user navigates away and returns to the + // widget tab) — the server validates it as a Bearer on next use. clearPersistedToken() + persistIdentifiedToken(result.sessionToken) setUser(result.user) if (result.votedPostIds) { queryClient.setQueryData( diff --git a/apps/web/src/components/widget/widget-home-animated.tsx b/apps/web/src/components/widget/widget-home-animated.tsx index 7f9e73e64..86ea56159 100644 --- a/apps/web/src/components/widget/widget-home-animated.tsx +++ b/apps/web/src/components/widget/widget-home-animated.tsx @@ -469,6 +469,55 @@ export function WidgetHomeAnimated({ setIsSubmitting(false) return } else if (!isIdentified) { + // Before falling back to an anonymous session (which will fail + // submission on workspaces that require identified users), try to + // restore a previously-persisted identified token. This recovers + // from tab switches, focus loss, or browser memory pressure that + // cleared the in-memory token but left localStorage intact. + const { readPersistedIdentifiedToken } = await import( + '@/lib/client/widget-auth' + ) + const identToken = readPersistedIdentifiedToken() + if (identToken) { + try { + const res = await fetch('/api/widget/session', { + headers: { Authorization: *** ${identToken}` }, + }) + if (res.ok) { + const body = await res.json() + if (body.data?.user) { + const { setWidgetToken, persistIdentifiedToken } = + await import('@/lib/client/widget-auth') + setWidgetToken(identToken) + persistIdentifiedToken(identToken) + setUser(body.data.user) + // Re-resolve board permissions with the restored identity + const [{ getWidgetAuthHeaders }, { fetchBoardCapabilitiesFn }] = + await Promise.all([ + import('@/lib/client/widget-auth'), + import('@/lib/server/functions/portal'), + ]) + const freshPermissions = await fetchBoardCapabilitiesFn({ + headers: getWidgetAuthHeaders(), + }) + if (!freshPermissions?.[selectedBoardId]?.canSubmit) { + setError( + intl.formatMessage({ + id: 'widget.home.form.noAccess', + defaultMessage: + "You don't have access to post on this board", + }) + ) + setIsSubmitting(false) + return + } + } + } + } catch { + // Server unreachable — fall through to ensureSession. + } + } + const ok = await ensureSession() if (!ok) { setError( @@ -513,13 +562,25 @@ export function WidgetHomeAnimated({ }) collapseForm() - } catch { - setError( - intl.formatMessage({ - id: 'widget.home.form.errorNetwork', - defaultMessage: 'Network error. Please try again.', - }) - ) + } catch (err) { + const msg = err instanceof Error ? err.message : '' + if (msg.includes('Anonymous interaction is not enabled') || + msg.includes('Authentication required')) { + setError( + intl.formatMessage({ + id: 'widget.home.form.errorReauthRequired', + defaultMessage: + 'Your session has expired. Please sign in again to submit feedback.', + }) + ) + } else { + setError( + intl.formatMessage({ + id: 'widget.home.form.errorNetwork', + defaultMessage: 'Network error. Please try again.', + }) + ) + } } finally { setIsSubmitting(false) } diff --git a/apps/web/src/lib/client/widget-auth.ts b/apps/web/src/lib/client/widget-auth.ts index 870d30644..61dcf08c3 100644 --- a/apps/web/src/lib/client/widget-auth.ts +++ b/apps/web/src/lib/client/widget-auth.ts @@ -17,9 +17,11 @@ // first-party even when embedded cross-site), so multiple tenants on one host // page can't collide. const ANON_TOKEN_KEY_PREFIX = 'quackback:anon-token:' +const IDENT_TOKEN_KEY_PREFIX = 'quackback:ident-token:' // Mirror Better Auth's 7-day session TTL so an expired token is dropped // client-side instead of triggering a guaranteed-401 validation round-trip. const ANON_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000 +const IDENT_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000 function anonTokenStore(): Storage | null { if (typeof window === 'undefined' || !window.localStorage) return null @@ -40,10 +42,11 @@ export function getWidgetToken(): string | null { return _widgetToken } -/** Clears the in-memory token AND any persisted anonymous copy. */ +/** Clears the in-memory token AND any persisted anonymous AND identified copies. */ export function clearWidgetToken(): void { _widgetToken = null clearPersistedToken() + clearIdentifiedToken() } /** @@ -114,6 +117,76 @@ export function clearPersistedToken(): void { } } +/** + * Persist an IDENTIFIED session token to the widget iframe's first-party + * localStorage so it survives page interactions that clear JS memory. + * Stored with a 7-day expiry hint mirroring the server session TTL. + * Only ever call this for tokens that resolve to a non-anonymous user. + */ +export function persistIdentifiedToken(token: string): void { + const store = anonTokenStore() + if (!store) return + try { + store.setItem( + identTokenKey(), + JSON.stringify({ token, expiresAt: Date.now() + IDENT_TOKEN_TTL_MS }) + ) + } catch { + // Storage disabled or full — fall back to in-memory only. + } +} + +/** + * Read a previously persisted identified token. Returns null (and drops + * the entry) when missing, malformed, or past its expiry hint. + */ +export function readPersistedIdentifiedToken(): string | null { + const store = anonTokenStore() + if (!store) return null + const key = identTokenKey() + let raw: string | null + try { + raw = store.getItem(key) + } catch { + return null + } + if (!raw) return null + try { + const parsed = JSON.parse(raw) as { token?: unknown; expiresAt?: unknown } + if ( + typeof parsed.token !== 'string' || + typeof parsed.expiresAt !== 'number' || + parsed.expiresAt <= Date.now() + ) { + store.removeItem(key) + return null + } + return parsed.token + } catch { + try { + store.removeItem(key) + } catch { + // ignore + } + return null + } +} + +/** Remove only the persisted identified token, leaving in-memory intact. */ +export function clearIdentifiedToken(): void { + const store = anonTokenStore() + if (!store) return + try { + store.removeItem(identTokenKey()) + } catch { + // ignore + } +} + +function identTokenKey(): string { + return `${IDENT_TOKEN_KEY_PREFIX}${window.location.origin}` +} + export function hasWidgetToken(): boolean { return _widgetToken !== null } diff --git a/apps/web/src/locales/en.json b/apps/web/src/locales/en.json index bffa98869..b0b96de73 100644 --- a/apps/web/src/locales/en.json +++ b/apps/web/src/locales/en.json @@ -33,6 +33,7 @@ "widget.home.form.errorEmail": "Could not verify your email. Please try again.", "widget.home.form.errorSession": "Could not create session. Please try again.", "widget.home.form.errorNetwork": "Network error. Please try again.", + "widget.home.form.errorReauthRequired": "Your session has expired. Please sign in again to submit feedback.", "widget.home.similar.heading": "Similar ideas", "widget.home.popular.heading": "Popular ideas", "widget.home.popular.search.placeholder": "Search ideas...", From 3f194b58f3791b6dd21aa772bbecb4b9d31d9e10 Mon Sep 17 00:00:00 2001 From: MrFreePress Date: Sun, 19 Jul 2026 12:15:10 -0400 Subject: [PATCH 2/6] fix: correct template literal backtick escaping --- apps/web/src/components/widget/widget-home-animated.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/widget/widget-home-animated.tsx b/apps/web/src/components/widget/widget-home-animated.tsx index 86ea56159..4cdf22b3e 100644 --- a/apps/web/src/components/widget/widget-home-animated.tsx +++ b/apps/web/src/components/widget/widget-home-animated.tsx @@ -481,7 +481,7 @@ export function WidgetHomeAnimated({ if (identToken) { try { const res = await fetch('/api/widget/session', { - headers: { Authorization: *** ${identToken}` }, + headers: { Authorization: `Bearer ${identToken}` }, }) if (res.ok) { const body = await res.json() From a8713d384277fdbc0682bbfd894d489399b4a2ad Mon Sep 17 00:00:00 2001 From: MrFreePress Date: Sun, 19 Jul 2026 12:20:26 -0400 Subject: [PATCH 3/6] fix: correct backtick escaping in widget-auth-provider.tsx --- apps/web/src/components/widget/widget-auth-provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/widget/widget-auth-provider.tsx b/apps/web/src/components/widget/widget-auth-provider.tsx index 912e0f987..8f59486ec 100644 --- a/apps/web/src/components/widget/widget-auth-provider.tsx +++ b/apps/web/src/components/widget/widget-auth-provider.tsx @@ -152,7 +152,7 @@ export function WidgetAuthProvider({ if (persistedIdent) { try { const res = await fetch('/api/widget/session', { - headers: { Authorization: *** ${persistedIdent}` }, + headers: { Authorization: `${persistedIdent}` }, }) if (res.ok) { const body = await res.json() From 234f65cedb935cf38d00185928e02545c5c7f94c Mon Sep 17 00:00:00 2001 From: MrFreePress Date: Sun, 19 Jul 2026 12:45:05 -0400 Subject: [PATCH 4/6] fix(widget): clear identified token on anonymous identify; restore identified on mount Addresses Codex review findings on PR #332: P1: handleAnonymousIdentify() now calls clearIdentifiedToken() so a previously-identified visitor who loads the host app logged out cannot have their old token restored and write as the prior user. P2: Mount restore useEffect now also checks readPersistedIdentifiedToken() so sessionVersion and permission-gated UI (submit button, vote) are correct immediately on mount when only an identified token was persisted, not just on first write. --- .../components/widget/widget-auth-provider.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/widget/widget-auth-provider.tsx b/apps/web/src/components/widget/widget-auth-provider.tsx index 8f59486ec..23b81fe49 100644 --- a/apps/web/src/components/widget/widget-auth-provider.tsx +++ b/apps/web/src/components/widget/widget-auth-provider.tsx @@ -317,17 +317,17 @@ export function WidgetAuthProvider({ } }, [portalSessionToken, portalUser, storeToken]) - // Restore a persisted anonymous session on mount so a returning visitor's - // conversation is visible immediately, without waiting for a write. Skipped - // when a portal session is present (it takes precedence) or one is already - // ready. Restore-only: never mints — the first write lazily mints if nothing - // valid was restored. + // Restore a persisted session (anonymous or identified) on mount so a + // returning visitor's conversation is visible immediately, without waiting + // for a write. Skipped when a portal session is present (it takes + // precedence) or one is already ready. Restore-only: never mints — the + // first write lazily mints if nothing valid was restored. const restoreAttemptedRef = useRef(false) useEffect(() => { if (restoreAttemptedRef.current) return if (portalSessionToken || sessionReadyRef.current) return restoreAttemptedRef.current = true - if (!readPersistedToken()) return + if (!readPersistedToken() && !readPersistedIdentifiedToken()) return void acquireSession(false) }, [portalSessionToken, acquireSession]) @@ -384,6 +384,10 @@ export function WidgetAuthProvider({ async function handleAnonymousIdentify() { // Don't eagerly create anonymous session — it will be created lazily // on first write action (vote, comment, post) via ensureSessionThen. + // Clear any persisted identified token so a previously-identified + // visitor who now loads the host app logged out cannot have their old + // token restored and write as the prior user. + clearIdentifiedToken() setUser(null) sendToHost({ type: 'quackback:identify-result', success: true, user: null }) sendToHost({ type: 'quackback:auth-change', user: null }) From 8bf85706ad00c4dc6db5f8aea6528b738f465fe3 Mon Sep 17 00:00:00 2001 From: MrFreePress Date: Sun, 19 Jul 2026 12:59:59 -0400 Subject: [PATCH 5/6] fix(widget): Bearer prefix, remove unreachable setUser, add all locale entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Codex review round 2 on PR #332: - Add missing 'Bearer ' prefix in acquireSession() identified-token restore path so getWidgetSession() can resolve the token (was sending raw token). - Remove setUser() call from widget-home-animated.tsx submit recovery path — setUser is a WidgetAuthProvider useState, not accessible from the widget-home component. The token store + board permission re-check is sufficient; the auth context picks up the restored identity on next render. - Add widget.home.form.errorReauthRequired entry to ar, de, es, fr, pt-br, ru, zh-cn, zh-tw locale catalogs to satisfy locale-parity test. --- apps/web/src/components/widget/widget-auth-provider.tsx | 2 +- apps/web/src/components/widget/widget-home-animated.tsx | 1 - apps/web/src/locales/ar.json | 1 + apps/web/src/locales/de.json | 1 + apps/web/src/locales/es.json | 1 + apps/web/src/locales/fr.json | 1 + apps/web/src/locales/pt-br.json | 1 + apps/web/src/locales/ru.json | 1 + apps/web/src/locales/zh-cn.json | 1 + apps/web/src/locales/zh-tw.json | 1 + 10 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/widget/widget-auth-provider.tsx b/apps/web/src/components/widget/widget-auth-provider.tsx index 23b81fe49..ca1c0af6f 100644 --- a/apps/web/src/components/widget/widget-auth-provider.tsx +++ b/apps/web/src/components/widget/widget-auth-provider.tsx @@ -152,7 +152,7 @@ export function WidgetAuthProvider({ if (persistedIdent) { try { const res = await fetch('/api/widget/session', { - headers: { Authorization: `${persistedIdent}` }, + headers: { Authorization: `Bearer ${persistedIdent}` }, }) if (res.ok) { const body = await res.json() diff --git a/apps/web/src/components/widget/widget-home-animated.tsx b/apps/web/src/components/widget/widget-home-animated.tsx index 4cdf22b3e..00f4298b6 100644 --- a/apps/web/src/components/widget/widget-home-animated.tsx +++ b/apps/web/src/components/widget/widget-home-animated.tsx @@ -490,7 +490,6 @@ export function WidgetHomeAnimated({ await import('@/lib/client/widget-auth') setWidgetToken(identToken) persistIdentifiedToken(identToken) - setUser(body.data.user) // Re-resolve board permissions with the restored identity const [{ getWidgetAuthHeaders }, { fetchBoardCapabilitiesFn }] = await Promise.all([ diff --git a/apps/web/src/locales/ar.json b/apps/web/src/locales/ar.json index 2d2c63d62..53a9bf5bd 100644 --- a/apps/web/src/locales/ar.json +++ b/apps/web/src/locales/ar.json @@ -33,6 +33,7 @@ "widget.home.form.errorEmail": "تعذّر التحقق من بريدك الإلكتروني. يرجى المحاولة مرة أخرى.", "widget.home.form.errorSession": "تعذّر إنشاء الجلسة. يرجى المحاولة مرة أخرى.", "widget.home.form.errorNetwork": "خطأ في الشبكة. يرجى المحاولة مرة أخرى.", + "widget.home.form.errorReauthRequired": "Your session has expired. Please sign in again to submit feedback.", "widget.home.similar.heading": "أفكار مشابهة", "widget.home.popular.heading": "أفكار رائجة", "widget.home.popular.search.placeholder": "ابحث عن أفكار...", diff --git a/apps/web/src/locales/de.json b/apps/web/src/locales/de.json index 4aaa8b91e..d23befd66 100644 --- a/apps/web/src/locales/de.json +++ b/apps/web/src/locales/de.json @@ -33,6 +33,7 @@ "widget.home.form.errorEmail": "Ihre E-Mail-Adresse konnte nicht überprüft werden. Bitte versuchen Sie es erneut.", "widget.home.form.errorSession": "Sitzung konnte nicht erstellt werden. Bitte versuchen Sie es erneut.", "widget.home.form.errorNetwork": "Netzwerkfehler. Bitte versuchen Sie es erneut.", + "widget.home.form.errorReauthRequired": "Your session has expired. Please sign in again to submit feedback.", "widget.home.similar.heading": "Ähnliche Ideen", "widget.home.popular.heading": "Beliebte Ideen", "widget.home.popular.search.placeholder": "Ideen suchen...", diff --git a/apps/web/src/locales/es.json b/apps/web/src/locales/es.json index 794ab7a7a..8f89cb983 100644 --- a/apps/web/src/locales/es.json +++ b/apps/web/src/locales/es.json @@ -33,6 +33,7 @@ "widget.home.form.errorEmail": "No se pudo verificar tu correo electrónico. Inténtalo de nuevo.", "widget.home.form.errorSession": "No se pudo crear la sesión. Inténtalo de nuevo.", "widget.home.form.errorNetwork": "Error de red. Inténtalo de nuevo.", + "widget.home.form.errorReauthRequired": "Your session has expired. Please sign in again to submit feedback.", "widget.home.similar.heading": "Ideas similares", "widget.home.popular.heading": "Ideas populares", "widget.home.popular.search.placeholder": "Buscar ideas...", diff --git a/apps/web/src/locales/fr.json b/apps/web/src/locales/fr.json index 1ff609f24..c95ef11a3 100644 --- a/apps/web/src/locales/fr.json +++ b/apps/web/src/locales/fr.json @@ -33,6 +33,7 @@ "widget.home.form.errorEmail": "Impossible de vérifier votre adresse e-mail. Veuillez réessayer.", "widget.home.form.errorSession": "Impossible de créer la session. Veuillez réessayer.", "widget.home.form.errorNetwork": "Erreur réseau. Veuillez réessayer.", + "widget.home.form.errorReauthRequired": "Your session has expired. Please sign in again to submit feedback.", "widget.home.similar.heading": "Idées similaires", "widget.home.popular.heading": "Idées populaires", "widget.home.popular.search.placeholder": "Rechercher des idées...", diff --git a/apps/web/src/locales/pt-br.json b/apps/web/src/locales/pt-br.json index 6223bf957..304e8abf8 100644 --- a/apps/web/src/locales/pt-br.json +++ b/apps/web/src/locales/pt-br.json @@ -33,6 +33,7 @@ "widget.home.form.errorEmail": "Não foi possível verificar seu email. Tente novamente.", "widget.home.form.errorSession": "Não foi possível criar a sessão. Tente novamente.", "widget.home.form.errorNetwork": "Erro de rede. Tente novamente.", + "widget.home.form.errorReauthRequired": "Your session has expired. Please sign in again to submit feedback.", "widget.home.similar.heading": "Ideias semelhantes", "widget.home.popular.heading": "Ideias populares", "widget.home.popular.search.placeholder": "Pesquisar ideias...", diff --git a/apps/web/src/locales/ru.json b/apps/web/src/locales/ru.json index cea410d43..3f36197f7 100644 --- a/apps/web/src/locales/ru.json +++ b/apps/web/src/locales/ru.json @@ -33,6 +33,7 @@ "widget.home.form.errorEmail": "Не удалось проверить email. Попробуйте еще раз.", "widget.home.form.errorSession": "Не удалось продолжить. Попробуйте еще раз.", "widget.home.form.errorNetwork": "Ошибка сети. Попробуйте снова.", + "widget.home.form.errorReauthRequired": "Your session has expired. Please sign in again to submit feedback.", "widget.home.similar.heading": "Похожие предложения", "widget.home.popular.heading": "Популярные предложения", "widget.home.popular.search.placeholder": "Поиск предложений...", diff --git a/apps/web/src/locales/zh-cn.json b/apps/web/src/locales/zh-cn.json index fc2b07da7..10c860711 100644 --- a/apps/web/src/locales/zh-cn.json +++ b/apps/web/src/locales/zh-cn.json @@ -33,6 +33,7 @@ "widget.home.form.errorEmail": "无法验证你的邮箱。请重试。", "widget.home.form.errorSession": "无法创建会话。请重试。", "widget.home.form.errorNetwork": "网络错误。请重试。", + "widget.home.form.errorReauthRequired": "Your session has expired. Please sign in again to submit feedback.", "widget.home.similar.heading": "相似想法", "widget.home.popular.heading": "热门想法", "widget.home.popular.search.placeholder": "搜索想法…", diff --git a/apps/web/src/locales/zh-tw.json b/apps/web/src/locales/zh-tw.json index 05208b7cb..6054ec68b 100644 --- a/apps/web/src/locales/zh-tw.json +++ b/apps/web/src/locales/zh-tw.json @@ -33,6 +33,7 @@ "widget.home.form.errorEmail": "無法驗證你的電子郵件。請再試一次。", "widget.home.form.errorSession": "無法建立工作階段。請再試一次。", "widget.home.form.errorNetwork": "網路錯誤。請再試一次。", + "widget.home.form.errorReauthRequired": "Your session has expired. Please sign in again to submit feedback.", "widget.home.similar.heading": "相似的想法", "widget.home.popular.heading": "熱門想法", "widget.home.popular.search.placeholder": "搜尋想法...", From 8511040e5f487f181ed80703835b388e98f04438 Mon Sep 17 00:00:00 2001 From: MrFreePress Date: Sun, 19 Jul 2026 13:16:22 -0400 Subject: [PATCH 6/6] fix(widget): clear in-memory token on anonymous identify to prevent race When the host sends {anonymous:true}, handleAnonymousIdentify() now also calls clearWidgetToken() (clears in-memory Bearer token + both persisted tokens) and resets sessionReadyRef. This prevents a race where an in-flight or completed identified-token restore leaves the previous user's identity active for subsequent writes, even after the host explicitly requested anonymous mode. --- .../web/src/components/widget/widget-auth-provider.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/widget/widget-auth-provider.tsx b/apps/web/src/components/widget/widget-auth-provider.tsx index ca1c0af6f..0ea381222 100644 --- a/apps/web/src/components/widget/widget-auth-provider.tsx +++ b/apps/web/src/components/widget/widget-auth-provider.tsx @@ -384,10 +384,12 @@ export function WidgetAuthProvider({ async function handleAnonymousIdentify() { // Don't eagerly create anonymous session — it will be created lazily // on first write action (vote, comment, post) via ensureSessionThen. - // Clear any persisted identified token so a previously-identified - // visitor who now loads the host app logged out cannot have their old - // token restored and write as the prior user. - clearIdentifiedToken() + // Clear both the in-memory token and any persisted identified token so + // a previously-identified visitor who now loads the host app logged + // out cannot have their old identity restored by an in-flight or + // completed token restore and continue writing as the prior user. + clearWidgetToken() + sessionReadyRef.current = false setUser(null) sendToHost({ type: 'quackback:identify-result', success: true, user: null }) sendToHost({ type: 'quackback:auth-change', user: null })