From 22f132bbc882159356e870ea28c9c973dc105717 Mon Sep 17 00:00:00 2001 From: Richard R Date: Wed, 27 May 2026 00:40:12 -0600 Subject: [PATCH] hard-cut onboarding modal flow orchestration --- src/app/(app)/app/layout.tsx | 7 +- src/app/(app)/layout.tsx | 2 - src/components/SettingsModal.tsx | 49 +-- src/components/auth/ClaimDataModal.tsx | 76 +---- .../documents/DexieMigrationModal.tsx | 92 ++---- src/contexts/OnboardingFlowContext.tsx | 307 ++++++++++++++++++ src/hooks/useOnboardingCoordinator.ts | 184 ----------- 7 files changed, 371 insertions(+), 346 deletions(-) create mode 100644 src/contexts/OnboardingFlowContext.tsx delete mode 100644 src/hooks/useOnboardingCoordinator.ts diff --git a/src/app/(app)/app/layout.tsx b/src/app/(app)/app/layout.tsx index f8abbbb..947ab4f 100644 --- a/src/app/(app)/app/layout.tsx +++ b/src/app/(app)/app/layout.tsx @@ -4,16 +4,13 @@ import type { ReactNode } from 'react'; import { ConfigProvider } from '@/contexts/ConfigContext'; import { DocumentProvider } from '@/contexts/DocumentContext'; -import { DexieMigrationModal } from '@/components/documents/DexieMigrationModal'; +import { OnboardingFlowProvider } from '@/contexts/OnboardingFlowContext'; export default function AppHomeLayout({ children }: { children: ReactNode }) { return ( - <> - {children} - - + {children} ); diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index fb14c2c..7cb8cb8 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -3,7 +3,6 @@ import type { ReactNode } from 'react'; import { Toaster } from 'react-hot-toast'; import { Providers } from '@/app/providers'; -import ClaimDataPopup from '@/components/auth/ClaimDataModal'; import { getAuthBaseUrl, isAnonymousAuthSessionsEnabled, isAuthEnabled, isGithubAuthEnabled } from '@/lib/server/auth/config'; export const dynamic = 'force-dynamic'; @@ -36,7 +35,6 @@ export default function AppLayout({ children }: { children: ReactNode }) { githubAuthEnabled={githubAuthEnabled} >
- {authEnabled && }
{children}
{ - const appConfig = await getAppConfig(); - const row = appConfig as Record | null; - const privacyKey = ONBOARDING_STATE_REGISTRY.privacyAccepted.localKey; - const firstVisitKey = ONBOARDING_STATE_REGISTRY.firstVisitSettingsOpened.localKey; - - return { - privacyAccepted: privacyKey ? Boolean(row?.[privacyKey]) : false, - firstVisitSettingsOpened: firstVisitKey ? Boolean(row?.[firstVisitKey]) : await getFirstVisit(), + useEffect(() => { + registerSettingsController({ + open: openSettings, + close: closeSettings, + }); + return () => { + registerSettingsController(null); }; - }, []); - - const markFirstVisitSettingsOpened = useCallback(async () => { - await setFirstVisit(true); - }, []); - - const { requestOpenSettings } = useOnboardingCoordinator({ - authEnabled, - isSessionPending, - sessionUserId: session?.user?.id, - appVersion: runtimeConfig.appVersion, - isSettingsOpen: isOpen, - readLocalSnapshot: readLocalOnboardingSnapshot, - markFirstVisitSettingsOpened, - postChangelogVersionCheck: async (currentVersion) => postChangelogVersionCheck(currentVersion), - openSettings, - closeSettings, - }); + }, [closeSettings, openSettings, registerSettingsController]); useEffect(() => { setLocalApiKey(apiKey); @@ -530,7 +509,11 @@ export function SettingsModal({ className = '' }: { className?: string }) { - + void; + onClaimed: () => void; +}; + +export default function ClaimDataModal({ + isOpen, + claimableCounts, + onDismiss, + onClaimed, +}: ClaimDataModalProps) { const router = useRouter(); - const [isOpen, setIsOpen] = useState(false); - const [hasChecked, setHasChecked] = useState(false); const [isClaiming, setIsClaiming] = useState(false); - const [claimableCounts, setClaimableCounts] = useState({ - documents: 0, - audiobooks: 0, - preferences: 0, - progress: 0, - }); - const user = sessionData?.user; - const userId = user?.id; - - const checkClaimableData = useCallback(async () => { - setHasChecked(true); - - try { - const res = await fetch('/api/user/claim', { - method: 'GET', - }); - if (res.ok) { - const data = await res.json(); - const counts = toClaimableCounts(data); - setClaimableCounts(counts); - - if (counts.documents + counts.audiobooks + counts.preferences + counts.progress > 0) { - setIsOpen(true); - } - } - } catch (e) { - console.error(e); - } - }, []); - - useEffect(() => { - // Reset per-user guard so account switches trigger a fresh check. - setHasChecked(false); - }, [userId]); - - useEffect(() => { - // Only check once per authenticated user - if (userId && !user?.isAnonymous && !hasChecked) { - checkClaimableData(); - } - }, [userId, user?.isAnonymous, hasChecked, checkClaimableData]); const handleClaim = async () => { setIsClaiming(true); @@ -93,8 +60,7 @@ export default function ClaimDataModal() { + `${claimed.preferences} preference set(s), and ` + `${claimed.progress} reading progress record(s)!`, ); - - setIsOpen(false); + onClaimed(); router.refresh(); return; } @@ -107,15 +73,9 @@ export default function ClaimDataModal() { } }; - const handleDismiss = () => { - // Close the modal for this session - will reappear on next page load/refresh - setIsOpen(false); - // Keep hasChecked = true so useEffect doesn't re-trigger in this session - }; - return ( - +