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 { AuthRateLimitProvider } from '@/contexts/AuthRateLimitContext';
import { RuntimeConfigProvider } from '@/contexts/RuntimeConfigContext';
import { PrivacyModal } from '@/components/PrivacyModal';
import { AuthLoader } from '@/components/auth/AuthLoader';
interface ProvidersProps {
@ -38,10 +37,7 @@ export function Providers({ children, authEnabled, authBaseUrl, allowAnonymousAu
>
<ThemeProvider>
<AuthLoader>
<>
{children}
{authEnabled && <PrivacyModal />}
</>
{children}
</AuthLoader>
</ThemeProvider>
</AuthRateLimitProvider>

View file

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

View file

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