refactor(onboarding): introduce onboarding state coordinator and central registry
Add useOnboardingCoordinator hook and onboarding-state registry to manage onboarding-related state such as privacy acceptance and first-visit tracking. Refactor SettingsModal to utilize the new onboarding state abstractions, removing legacy privacy gating logic and improving maintainability. Includes unit tests for onboarding-state registry to ensure correctness.
This commit is contained in:
parent
df321b8832
commit
e75114a943
4 changed files with 282 additions and 85 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Fragment, useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import { Fragment, useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPanel,
|
DialogPanel,
|
||||||
|
|
@ -34,7 +34,6 @@ import { useRouter } from 'next/navigation';
|
||||||
import { showPrivacyModal } from '@/components/PrivacyModal';
|
import { showPrivacyModal } from '@/components/PrivacyModal';
|
||||||
import { deleteDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client/api/documents';
|
import { deleteDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client/api/documents';
|
||||||
import { postChangelogVersionCheck } from '@/lib/client/api/user-state';
|
import { postChangelogVersionCheck } from '@/lib/client/api/user-state';
|
||||||
import { scheduleChangelogCheck } from '@/lib/client/changelog-check';
|
|
||||||
import { cacheStoredDocumentFromBytes, clearDocumentCache } from '@/lib/client/cache/documents';
|
import { cacheStoredDocumentFromBytes, clearDocumentCache } from '@/lib/client/cache/documents';
|
||||||
import { clearAllDocumentPreviewCaches, clearInMemoryDocumentPreviewCache } from '@/lib/client/cache/previews';
|
import { clearAllDocumentPreviewCaches, clearInMemoryDocumentPreviewCache } from '@/lib/client/cache/previews';
|
||||||
import { resolveTtsSettingsViewModel } from '@/lib/client/settings/tts-settings';
|
import { resolveTtsSettingsViewModel } from '@/lib/client/settings/tts-settings';
|
||||||
|
|
@ -72,6 +71,8 @@ import {
|
||||||
type ChangelogManifestEntry,
|
type ChangelogManifestEntry,
|
||||||
type ChangelogReleaseBody,
|
type ChangelogReleaseBody,
|
||||||
} from '@/lib/shared/changelog';
|
} from '@/lib/shared/changelog';
|
||||||
|
import { useOnboardingCoordinator } from '@/hooks/useOnboardingCoordinator';
|
||||||
|
import { ONBOARDING_STATE_REGISTRY } from '@/lib/shared/onboarding-state';
|
||||||
|
|
||||||
// Hard-coded theme color palettes for the visual theme selector
|
// Hard-coded theme color palettes for the visual theme selector
|
||||||
type ThemeColorSet = { background: string; base: string; offbase: string; accent: string; secondaryAccent: string; foreground: string; muted: string };
|
type ThemeColorSet = { background: string; base: string; offbase: string; accent: string; secondaryAccent: string; foreground: string; muted: string };
|
||||||
|
|
@ -174,8 +175,6 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
||||||
const { progress, setProgress, estimatedTimeRemaining } = useTimeEstimation();
|
const { progress, setProgress, estimatedTimeRemaining } = useTimeEstimation();
|
||||||
const { authEnabled, baseUrl: authBaseUrl } = useAuthConfig();
|
const { authEnabled, baseUrl: authBaseUrl } = useAuthConfig();
|
||||||
const { data: session, isPending: isSessionPending } = useAuthSession();
|
const { data: session, isPending: isSessionPending } = useAuthSession();
|
||||||
const changelogVersionCheckKeyRef = useRef<string | null>(null);
|
|
||||||
const changelogVersionCheckInFlightRef = useRef<string | null>(null);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isBusy = isImportingLibrary;
|
const isBusy = isImportingLibrary;
|
||||||
const {
|
const {
|
||||||
|
|
@ -208,46 +207,44 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
||||||
const isSharedSelected = Boolean(selectedSharedProvider);
|
const isSharedSelected = Boolean(selectedSharedProvider);
|
||||||
const selectedProviderOption = ttsProviders.find((p) => p.id === localProviderRef) ?? ttsProviders[0];
|
const selectedProviderOption = ttsProviders.find((p) => p.id === localProviderRef) ?? ttsProviders[0];
|
||||||
|
|
||||||
const closeSettingsForPrivacyGate = useCallback(() => {
|
const closeSettings = useCallback(() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setIsChangelogOpen(false);
|
setIsChangelogOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const canOpenSettings = useCallback(async () => {
|
const openSettings = useCallback((options?: { changelog?: boolean }) => {
|
||||||
if (!authEnabled) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const appConfig = await getAppConfig();
|
|
||||||
return Boolean(appConfig?.privacyAccepted);
|
|
||||||
}, [authEnabled]);
|
|
||||||
|
|
||||||
const openSettings = useCallback(async (options?: { changelog?: boolean }) => {
|
|
||||||
const allowed = await canOpenSettings();
|
|
||||||
if (!allowed) {
|
|
||||||
closeSettingsForPrivacyGate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
setIsChangelogOpen(Boolean(options?.changelog));
|
setIsChangelogOpen(Boolean(options?.changelog));
|
||||||
}, [canOpenSettings, closeSettingsForPrivacyGate]);
|
}, []);
|
||||||
|
|
||||||
const checkFirstVist = useCallback(async () => {
|
const readLocalOnboardingSnapshot = useCallback(async () => {
|
||||||
const allowed = await canOpenSettings();
|
const appConfig = await getAppConfig();
|
||||||
if (!allowed) {
|
const row = appConfig as Record<string, unknown> | null;
|
||||||
return;
|
const privacyKey = ONBOARDING_STATE_REGISTRY.privacyAccepted.localKey;
|
||||||
}
|
const firstVisitKey = ONBOARDING_STATE_REGISTRY.firstVisitSettingsOpened.localKey;
|
||||||
const firstVisit = await getFirstVisit();
|
|
||||||
if (!firstVisit) {
|
|
||||||
await setFirstVisit(true);
|
|
||||||
setIsOpen(true);
|
|
||||||
}
|
|
||||||
}, [canOpenSettings, setIsOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
return {
|
||||||
checkFirstVist().catch((err) => {
|
privacyAccepted: privacyKey ? Boolean(row?.[privacyKey]) : false,
|
||||||
console.error('First visit check failed:', err);
|
firstVisitSettingsOpened: firstVisitKey ? Boolean(row?.[firstVisitKey]) : await getFirstVisit(),
|
||||||
});
|
};
|
||||||
}, [checkFirstVist]);
|
}, []);
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalApiKey(apiKey);
|
setLocalApiKey(apiKey);
|
||||||
|
|
@ -258,54 +255,6 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
||||||
setLocalTTSInstructions(ttsInstructions);
|
setLocalTTSInstructions(ttsInstructions);
|
||||||
}, [apiKey, baseUrl, providerRef, providerType, ttsModel, ttsInstructions]);
|
}, [apiKey, baseUrl, providerRef, providerType, ttsModel, ttsInstructions]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!authEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const onPrivacyAccepted = () => {
|
|
||||||
checkFirstVist().catch((err) => {
|
|
||||||
console.error('First visit check after privacy acceptance failed:', err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
window.addEventListener('openreader:privacyAccepted', onPrivacyAccepted);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('openreader:privacyAccepted', onPrivacyAccepted);
|
|
||||||
};
|
|
||||||
}, [authEnabled, checkFirstVist]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let cancelled = false;
|
|
||||||
void (async () => {
|
|
||||||
const allowed = await canOpenSettings();
|
|
||||||
if (!allowed && !cancelled) {
|
|
||||||
closeSettingsForPrivacyGate();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [isOpen, canOpenSettings, closeSettingsForPrivacyGate]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return scheduleChangelogCheck({
|
|
||||||
authEnabled,
|
|
||||||
isSessionPending,
|
|
||||||
sessionUserId: session?.user?.id,
|
|
||||||
appVersion: runtimeConfig.appVersion,
|
|
||||||
completedRef: changelogVersionCheckKeyRef,
|
|
||||||
inFlightRef: changelogVersionCheckInFlightRef,
|
|
||||||
postCheck: async (currentVersion) => postChangelogVersionCheck(currentVersion),
|
|
||||||
onShouldOpen: () => {
|
|
||||||
void openSettings({ changelog: true });
|
|
||||||
},
|
|
||||||
delayMs: 120,
|
|
||||||
retryDelayMs: 400,
|
|
||||||
});
|
|
||||||
}, [authEnabled, isSessionPending, session?.user?.id, runtimeConfig.appVersion, openSettings]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ttsModels.some(m => m.id === modelValue) && modelValue !== '') {
|
if (!ttsModels.some(m => m.id === modelValue) && modelValue !== '') {
|
||||||
setCustomModelInput(modelValue);
|
setCustomModelInput(modelValue);
|
||||||
|
|
@ -571,7 +520,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void openSettings();
|
void requestOpenSettings();
|
||||||
}}
|
}}
|
||||||
className={`inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase hover:text-accent transition-transform transition-colors duration-200 ease-out hover:scale-[1.08] ${className}`}
|
className={`inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase hover:text-accent transition-transform transition-colors duration-200 ease-out hover:scale-[1.08] ${className}`}
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
|
|
|
||||||
184
src/hooks/useOnboardingCoordinator.ts
Normal file
184
src/hooks/useOnboardingCoordinator.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
scheduleChangelogCheck,
|
||||||
|
type ChangelogVersionCheckResponse,
|
||||||
|
} from '@/lib/client/changelog-check';
|
||||||
|
|
||||||
|
type OpenSettingsOptions = {
|
||||||
|
changelog?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LocalOnboardingSnapshot = {
|
||||||
|
privacyAccepted: boolean;
|
||||||
|
firstVisitSettingsOpened: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseOnboardingCoordinatorArgs = {
|
||||||
|
authEnabled: boolean;
|
||||||
|
isSessionPending: boolean;
|
||||||
|
sessionUserId: string | null | undefined;
|
||||||
|
appVersion: string | null | undefined;
|
||||||
|
isSettingsOpen: boolean;
|
||||||
|
readLocalSnapshot: () => Promise<LocalOnboardingSnapshot>;
|
||||||
|
markFirstVisitSettingsOpened: () => Promise<void>;
|
||||||
|
postChangelogVersionCheck: (currentVersion: string) => Promise<ChangelogVersionCheckResponse>;
|
||||||
|
openSettings: (options?: OpenSettingsOptions) => void;
|
||||||
|
closeSettings: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseOnboardingCoordinatorResult = {
|
||||||
|
requestOpenSettings: (options?: OpenSettingsOptions) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useOnboardingCoordinator(
|
||||||
|
args: UseOnboardingCoordinatorArgs,
|
||||||
|
): UseOnboardingCoordinatorResult {
|
||||||
|
const {
|
||||||
|
authEnabled,
|
||||||
|
isSessionPending,
|
||||||
|
sessionUserId,
|
||||||
|
appVersion,
|
||||||
|
isSettingsOpen,
|
||||||
|
readLocalSnapshot,
|
||||||
|
markFirstVisitSettingsOpened,
|
||||||
|
postChangelogVersionCheck,
|
||||||
|
openSettings,
|
||||||
|
closeSettings,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
const pendingChangelogOpenRef = useRef(false);
|
||||||
|
const changelogVersionCheckKeyRef = useRef<string | null>(null);
|
||||||
|
const changelogVersionCheckInFlightRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
const canOpenSettingsNow = useCallback(async (): Promise<{
|
||||||
|
allowed: boolean;
|
||||||
|
local: LocalOnboardingSnapshot | null;
|
||||||
|
}> => {
|
||||||
|
if (!authEnabled) {
|
||||||
|
return { allowed: true, local: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const local = await readLocalSnapshot();
|
||||||
|
return {
|
||||||
|
allowed: local.privacyAccepted,
|
||||||
|
local,
|
||||||
|
};
|
||||||
|
}, [authEnabled, readLocalSnapshot]);
|
||||||
|
|
||||||
|
const requestOpenSettings = useCallback(async (options?: OpenSettingsOptions): Promise<boolean> => {
|
||||||
|
const gate = await canOpenSettingsNow();
|
||||||
|
if (!gate.allowed) {
|
||||||
|
if (options?.changelog) {
|
||||||
|
pendingChangelogOpenRef.current = true;
|
||||||
|
}
|
||||||
|
closeSettings();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
openSettings({ changelog: Boolean(options?.changelog) });
|
||||||
|
return true;
|
||||||
|
}, [canOpenSettingsNow, closeSettings, openSettings]);
|
||||||
|
|
||||||
|
const maybeOpenFirstVisitSettings = useCallback(async () => {
|
||||||
|
const gate = await canOpenSettingsNow();
|
||||||
|
if (!gate.allowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstVisitSettingsOpened = gate.local
|
||||||
|
? gate.local.firstVisitSettingsOpened
|
||||||
|
: (await readLocalSnapshot()).firstVisitSettingsOpened;
|
||||||
|
|
||||||
|
if (firstVisitSettingsOpened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await markFirstVisitSettingsOpened();
|
||||||
|
openSettings();
|
||||||
|
}, [canOpenSettingsNow, markFirstVisitSettingsOpened, openSettings, readLocalSnapshot]);
|
||||||
|
|
||||||
|
const replayDeferredChangelogOpen = useCallback(async () => {
|
||||||
|
if (!pendingChangelogOpenRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opened = await requestOpenSettings({ changelog: true });
|
||||||
|
if (opened) {
|
||||||
|
pendingChangelogOpenRef.current = false;
|
||||||
|
}
|
||||||
|
}, [requestOpenSettings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
maybeOpenFirstVisitSettings().catch((err) => {
|
||||||
|
console.error('First visit settings check failed:', err);
|
||||||
|
});
|
||||||
|
}, [maybeOpenFirstVisitSettings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPrivacyAccepted = () => {
|
||||||
|
maybeOpenFirstVisitSettings().catch((err) => {
|
||||||
|
console.error('First visit settings check after privacy acceptance failed:', err);
|
||||||
|
});
|
||||||
|
replayDeferredChangelogOpen().catch((err) => {
|
||||||
|
console.error('Deferred changelog open after privacy acceptance failed:', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('openreader:privacyAccepted', onPrivacyAccepted);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('openreader:privacyAccepted', onPrivacyAccepted);
|
||||||
|
};
|
||||||
|
}, [authEnabled, maybeOpenFirstVisitSettings, replayDeferredChangelogOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSettingsOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
void (async () => {
|
||||||
|
const gate = await canOpenSettingsNow();
|
||||||
|
if (!gate.allowed && !cancelled) {
|
||||||
|
closeSettings();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [canOpenSettingsNow, closeSettings, isSettingsOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return scheduleChangelogCheck({
|
||||||
|
authEnabled,
|
||||||
|
isSessionPending,
|
||||||
|
sessionUserId,
|
||||||
|
appVersion,
|
||||||
|
completedRef: changelogVersionCheckKeyRef,
|
||||||
|
inFlightRef: changelogVersionCheckInFlightRef,
|
||||||
|
postCheck: postChangelogVersionCheck,
|
||||||
|
onShouldOpen: () => {
|
||||||
|
void requestOpenSettings({ changelog: true });
|
||||||
|
},
|
||||||
|
delayMs: 120,
|
||||||
|
retryDelayMs: 400,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
appVersion,
|
||||||
|
authEnabled,
|
||||||
|
isSessionPending,
|
||||||
|
postChangelogVersionCheck,
|
||||||
|
sessionUserId,
|
||||||
|
requestOpenSettings,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestOpenSettings,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
src/lib/shared/onboarding-state.ts
Normal file
45
src/lib/shared/onboarding-state.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
export type OnboardingStorageScope = 'local-dexie' | 'server-user-preferences' | 'hybrid';
|
||||||
|
|
||||||
|
export type OnboardingStateDescriptor = {
|
||||||
|
/**
|
||||||
|
* Where this state is persisted today.
|
||||||
|
* - local-dexie: browser/device-local only
|
||||||
|
* - server-user-preferences: per-user on server
|
||||||
|
* - hybrid: intentionally persisted in both places
|
||||||
|
*/
|
||||||
|
scope: OnboardingStorageScope;
|
||||||
|
/**
|
||||||
|
* Optional local key name when state is stored in Dexie app-config.
|
||||||
|
*/
|
||||||
|
localKey?: string;
|
||||||
|
/**
|
||||||
|
* Optional server-side key when state is stored in user preferences/meta.
|
||||||
|
*/
|
||||||
|
serverKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central registry for onboarding-related state and where each item lives.
|
||||||
|
* Keep this map up to date whenever onboarding persistence changes.
|
||||||
|
*/
|
||||||
|
export const ONBOARDING_STATE_REGISTRY = {
|
||||||
|
privacyAccepted: {
|
||||||
|
scope: 'local-dexie',
|
||||||
|
localKey: 'privacyAccepted',
|
||||||
|
},
|
||||||
|
firstVisitSettingsOpened: {
|
||||||
|
scope: 'local-dexie',
|
||||||
|
localKey: 'firstVisit',
|
||||||
|
},
|
||||||
|
documentsMigrationPrompted: {
|
||||||
|
scope: 'local-dexie',
|
||||||
|
localKey: 'documentsMigrationPrompted',
|
||||||
|
},
|
||||||
|
changelogLastSeenAppVersion: {
|
||||||
|
scope: 'server-user-preferences',
|
||||||
|
serverKey: '_meta.lastSeenAppVersion',
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, OnboardingStateDescriptor>;
|
||||||
|
|
||||||
|
export type OnboardingStateKey = keyof typeof ONBOARDING_STATE_REGISTRY;
|
||||||
|
|
||||||
19
tests/unit/onboarding-state.spec.ts
Normal file
19
tests/unit/onboarding-state.spec.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { ONBOARDING_STATE_REGISTRY } from '../../src/lib/shared/onboarding-state';
|
||||||
|
import { SYNCED_PREFERENCE_KEYS } from '../../src/types/user-state';
|
||||||
|
|
||||||
|
test.describe('onboarding state storage scopes', () => {
|
||||||
|
test('keeps local onboarding flags out of synced server preferences', () => {
|
||||||
|
expect(ONBOARDING_STATE_REGISTRY.privacyAccepted.scope).toBe('local-dexie');
|
||||||
|
expect(ONBOARDING_STATE_REGISTRY.firstVisitSettingsOpened.scope).toBe('local-dexie');
|
||||||
|
expect(ONBOARDING_STATE_REGISTRY.documentsMigrationPrompted.scope).toBe('local-dexie');
|
||||||
|
expect(ONBOARDING_STATE_REGISTRY.changelogLastSeenAppVersion.scope).toBe('server-user-preferences');
|
||||||
|
|
||||||
|
const synced = new Set<string>(SYNCED_PREFERENCE_KEYS);
|
||||||
|
|
||||||
|
expect(synced.has(ONBOARDING_STATE_REGISTRY.privacyAccepted.localKey!)).toBe(false);
|
||||||
|
expect(synced.has(ONBOARDING_STATE_REGISTRY.firstVisitSettingsOpened.localKey!)).toBe(false);
|
||||||
|
expect(synced.has(ONBOARDING_STATE_REGISTRY.documentsMigrationPrompted.localKey!)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Loading…
Reference in a new issue