From 51d98173a6561bdba3be2795d73802d8457eeb31 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Tue, 17 Mar 2026 22:51:06 +0800 Subject: [PATCH] Phase 3: Post-login passkey prompt toast Show a one-time toast suggesting passkey setup after login when: - User has no passkeys registered - Browser supports WebAuthn - Prompt hasn't been shown this session (sessionStorage gate) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/layout/AppLayout.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index aae73f7..0d9909f 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -1,7 +1,9 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Outlet } from 'react-router-dom'; +import { toast } from 'sonner'; import { Menu } from 'lucide-react'; import { useTheme } from '@/hooks/useTheme'; +import { useAuth } from '@/hooks/useAuth'; import { usePrefetch } from '@/hooks/usePrefetch'; import { AlertsProvider } from '@/hooks/useAlerts'; import { LockProvider, useLock } from '@/hooks/useLock'; @@ -17,7 +19,20 @@ function AppContent({ mobileOpen, setMobileOpen }: { setMobileOpen: (v: boolean) => void; }) { const { isLocked, isLockResolved } = useLock(); + const { hasPasskeys } = useAuth(); usePrefetch(isLockResolved && !isLocked); + + // Post-login passkey prompt — show once per session if user has no passkeys + useEffect(() => { + if ( + isLockResolved && !isLocked && !hasPasskeys && + window.PublicKeyCredential && + !sessionStorage.getItem('passkey-prompt-shown') + ) { + sessionStorage.setItem('passkey-prompt-shown', '1'); + toast.info('Simplify your login \u2014 set up a passkey in Settings', { duration: 8000 }); + } + }, [isLockResolved, isLocked, hasPasskeys]); const [collapsed, setCollapsed] = useState(() => { try { return JSON.parse(localStorage.getItem('umbra-sidebar-collapsed') || 'false'); } catch { return false; }