orchestrate privacy modal in onboarding flow
This commit is contained in:
parent
22f132bbc8
commit
93ae9f3082
3 changed files with 33 additions and 29 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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'}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue