orchestrate privacy modal in onboarding flow

This commit is contained in:
Richard R 2026-05-27 00:50:10 -06:00
parent 22f132bbc8
commit 93ae9f3082
3 changed files with 33 additions and 29 deletions

View file

@ -6,7 +6,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '@/contexts/ThemeContext'; import { ThemeProvider } from '@/contexts/ThemeContext';
import { AuthRateLimitProvider } from '@/contexts/AuthRateLimitContext'; import { AuthRateLimitProvider } from '@/contexts/AuthRateLimitContext';
import { RuntimeConfigProvider } from '@/contexts/RuntimeConfigContext'; import { RuntimeConfigProvider } from '@/contexts/RuntimeConfigContext';
import { PrivacyModal } from '@/components/PrivacyModal';
import { AuthLoader } from '@/components/auth/AuthLoader'; import { AuthLoader } from '@/components/auth/AuthLoader';
interface ProvidersProps { interface ProvidersProps {
@ -38,10 +37,7 @@ export function Providers({ children, authEnabled, authBaseUrl, allowAnonymousAu
> >
<ThemeProvider> <ThemeProvider>
<AuthLoader> <AuthLoader>
<> {children}
{children}
{authEnabled && <PrivacyModal />}
</>
</AuthLoader> </AuthLoader>
</ThemeProvider> </ThemeProvider>
</AuthRateLimitProvider> </AuthRateLimitProvider>

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { Fragment, useState, useEffect, useCallback } from 'react'; import { Fragment, useState, useEffect } from 'react';
import { import {
Dialog, Dialog,
DialogPanel, DialogPanel,
@ -9,10 +9,12 @@ import {
TransitionChild, TransitionChild,
Button, Button,
} from '@headlessui/react'; } from '@headlessui/react';
import { updateAppConfig, getAppConfig } from '@/lib/client/dexie'; import { updateAppConfig } from '@/lib/client/dexie';
interface PrivacyModalProps { interface PrivacyModalProps {
isOpen: boolean;
onAccept?: () => void; onAccept?: () => void;
onDismiss?: () => void;
} }
function PrivacyModalBody({ origin }: { origin: string }) { function PrivacyModalBody({ origin }: { origin: string }) {
@ -38,32 +40,23 @@ function PrivacyModalBody({ origin }: { origin: string }) {
); );
} }
export function PrivacyModal({ onAccept }: PrivacyModalProps) { export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps) {
const [isOpen, setIsOpen] = useState(false);
const [origin, setOrigin] = useState(''); const [origin, setOrigin] = useState('');
const [agreed, setAgreed] = useState(false); const [agreed, setAgreed] = useState(false);
const checkPrivacyAccepted = useCallback(async () => {
const config = await getAppConfig();
if (!config?.privacyAccepted) {
setIsOpen(true);
}
}, []);
useEffect(() => {
checkPrivacyAccepted().catch((err) => {
console.error('Privacy acceptance check failed:', err);
});
}, [checkPrivacyAccepted]);
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
setOrigin(window.location.origin); setOrigin(window.location.origin);
}, []); }, []);
useEffect(() => {
if (isOpen) {
setAgreed(false);
}
}, [isOpen]);
const handleAccept = async () => { const handleAccept = async () => {
await updateAppConfig({ privacyAccepted: true }); await updateAppConfig({ privacyAccepted: true });
setIsOpen(false);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.dispatchEvent(new Event('openreader:privacyAccepted')); window.dispatchEvent(new Event('openreader:privacyAccepted'));
} }
@ -72,7 +65,7 @@ export function PrivacyModal({ onAccept }: PrivacyModalProps) {
return ( return (
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-[60]" onClose={() => { }}> <Dialog as="div" className="relative z-[80]" onClose={onDismiss ?? (() => { })}>
<TransitionChild <TransitionChild
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"

View file

@ -3,6 +3,7 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import ClaimDataModal, { type ClaimableCounts } from '@/components/auth/ClaimDataModal'; import ClaimDataModal, { type ClaimableCounts } from '@/components/auth/ClaimDataModal';
import { DexieMigrationModal } from '@/components/documents/DexieMigrationModal'; import { DexieMigrationModal } from '@/components/documents/DexieMigrationModal';
import { PrivacyModal } from '@/components/PrivacyModal';
import { useAuthConfig } from '@/contexts/AuthRateLimitContext'; import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
import { useAuthSession } from '@/hooks/useAuthSession'; import { useAuthSession } from '@/hooks/useAuthSession';
import { useRuntimeConfig } from '@/contexts/RuntimeConfigContext'; import { useRuntimeConfig } from '@/contexts/RuntimeConfigContext';
@ -120,7 +121,7 @@ export function OnboardingFlowProvider({ children }: { children: ReactNode }) {
const userId = user?.id ?? null; const userId = user?.id ?? null;
const isAnonymous = Boolean(user?.isAnonymous); const isAnonymous = Boolean(user?.isAnonymous);
const [activeBlockingModal, setActiveBlockingModal] = useState<'claim' | 'migration' | null>(null); const [activeBlockingModal, setActiveBlockingModal] = useState<'privacy' | 'claim' | 'migration' | null>(null);
const [claimableCounts, setClaimableCounts] = useState<ClaimableCounts>(EMPTY_CLAIM_COUNTS); const [claimableCounts, setClaimableCounts] = useState<ClaimableCounts>(EMPTY_CLAIM_COUNTS);
const [migrationCounts, setMigrationCounts] = useState<{ localCount: number; missingCount: number }>({ const [migrationCounts, setMigrationCounts] = useState<{ localCount: number; missingCount: number }>({
localCount: 0, localCount: 0,
@ -144,12 +145,14 @@ export function OnboardingFlowProvider({ children }: { children: ReactNode }) {
} }
runningAdvanceRef.current = true; runningAdvanceRef.current = true;
try { try {
if (activeBlockingModal) {
return;
}
const local = await readLocalOnboardingSnapshot(); const local = await readLocalOnboardingSnapshot();
if (authEnabled && !local.privacyAccepted) { if (authEnabled && !local.privacyAccepted) {
setActiveBlockingModal('privacy');
return;
}
if (activeBlockingModal === 'privacy') {
setActiveBlockingModal(null);
} else if (activeBlockingModal) {
return; return;
} }
@ -237,6 +240,11 @@ export function OnboardingFlowProvider({ children }: { children: ReactNode }) {
void advanceFlow(); void advanceFlow();
}, [advanceFlow]); }, [advanceFlow]);
const handlePrivacyAccepted = useCallback(() => {
setActiveBlockingModal(null);
void advanceFlow();
}, [advanceFlow]);
useEffect(() => { useEffect(() => {
void advanceFlow(); void advanceFlow();
}, [advanceFlow, authEnabled, isAnonymous, userId]); }, [advanceFlow, authEnabled, isAnonymous, userId]);
@ -280,6 +288,13 @@ export function OnboardingFlowProvider({ children }: { children: ReactNode }) {
return ( return (
<OnboardingFlowContext.Provider value={contextValue}> <OnboardingFlowContext.Provider value={contextValue}>
{children} {children}
{authEnabled && (
<PrivacyModal
isOpen={activeBlockingModal === 'privacy'}
onAccept={handlePrivacyAccepted}
onDismiss={() => { }}
/>
)}
{authEnabled && ( {authEnabled && (
<ClaimDataModal <ClaimDataModal
isOpen={activeBlockingModal === 'claim'} isOpen={activeBlockingModal === 'claim'}