feat: sync onboarding status across devices #285
This commit is contained in:
@@ -67,7 +67,7 @@ export function AppearanceSettings() {
|
||||
const tAdvanced = useTranslations('settings.advanced');
|
||||
const tTour = useTranslations('tour');
|
||||
const { theme, setTheme } = useThemeStore();
|
||||
const { fontSize, density, animationsEnabled, senderFavicons, showAvatarsInJunk, updateSetting } = useSettingsStore();
|
||||
const { fontSize, density, animationsEnabled, senderFavicons, showAvatarsInJunk, showOnboardingOnNewDevices, updateSetting } = useSettingsStore();
|
||||
const { startTour, resetTourCompletion } = useTour();
|
||||
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
||||
|
||||
@@ -145,6 +145,13 @@ export function AppearanceSettings() {
|
||||
{tTour('restart_button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={tTour('show_on_new_devices_title')} description={tTour('show_on_new_devices_desc')}>
|
||||
<ToggleSwitch
|
||||
checked={showOnboardingOnNewDevices}
|
||||
onChange={(checked) => updateSetting('showOnboardingOnNewDevices', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter, usePathname } from "@/i18n/navigation";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { useCalendarStore } from "@/stores/calendar-store";
|
||||
import { useWebDAVStore } from "@/stores/webdav-store";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { getTourSteps, type TourStep } from "./tour-steps";
|
||||
import { TourOverlay } from "./tour-overlay";
|
||||
|
||||
@@ -38,6 +39,9 @@ export function TourProvider({ children }: { children: ReactNode }) {
|
||||
const { isDemoMode } = useAuthStore();
|
||||
const { supportsCalendar } = useCalendarStore();
|
||||
const { supportsWebDAV } = useWebDAVStore();
|
||||
const tourCompleted = useSettingsStore((s) => s.tourCompleted);
|
||||
const showOnboardingOnNewDevices = useSettingsStore((s) => s.showOnboardingOnNewDevices);
|
||||
const updateSetting = useSettingsStore((s) => s.updateSetting);
|
||||
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
@@ -46,10 +50,29 @@ export function TourProvider({ children }: { children: ReactNode }) {
|
||||
const steps = getTourSteps({ isDemoMode, supportsCalendar, supportsWebDAV: supportsWebDAV !== false });
|
||||
|
||||
useEffect(() => {
|
||||
// One-time migration: if the legacy per-device flag is set but the synced
|
||||
// setting isn't yet, mirror it into synced state.
|
||||
try {
|
||||
setHasCompletedTour(localStorage.getItem(TOUR_COMPLETED_KEY) === "true");
|
||||
const legacy = localStorage.getItem(TOUR_COMPLETED_KEY) === "true";
|
||||
if (legacy && !tourCompleted) {
|
||||
updateSetting("tourCompleted", true);
|
||||
}
|
||||
} catch { /* */ }
|
||||
}, []);
|
||||
}, [tourCompleted, updateSetting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tourCompleted) {
|
||||
setHasCompletedTour(false);
|
||||
return;
|
||||
}
|
||||
if (showOnboardingOnNewDevices) {
|
||||
try {
|
||||
setHasCompletedTour(localStorage.getItem(TOUR_COMPLETED_KEY) === "true");
|
||||
return;
|
||||
} catch { /* */ }
|
||||
}
|
||||
setHasCompletedTour(true);
|
||||
}, [tourCompleted, showOnboardingOnNewDevices]);
|
||||
|
||||
const startTour = useCallback(() => {
|
||||
let resumeStep = 0;
|
||||
@@ -85,11 +108,12 @@ export function TourProvider({ children }: { children: ReactNode }) {
|
||||
const completeTour = useCallback(() => {
|
||||
setIsActive(false);
|
||||
setHasCompletedTour(true);
|
||||
updateSetting("tourCompleted", true);
|
||||
try {
|
||||
localStorage.setItem(TOUR_COMPLETED_KEY, "true");
|
||||
localStorage.removeItem(TOUR_CURRENT_STEP_KEY);
|
||||
} catch { /* */ }
|
||||
}, []);
|
||||
}, [updateSetting]);
|
||||
|
||||
const nextStep = useCallback(() => {
|
||||
if (currentStep >= steps.length - 1) {
|
||||
@@ -131,11 +155,12 @@ export function TourProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const resetTourCompletion = useCallback(() => {
|
||||
setHasCompletedTour(false);
|
||||
updateSetting("tourCompleted", false);
|
||||
try {
|
||||
localStorage.removeItem(TOUR_COMPLETED_KEY);
|
||||
localStorage.removeItem(TOUR_CURRENT_STEP_KEY);
|
||||
} catch { /* */ }
|
||||
}, []);
|
||||
}, [updateSetting]);
|
||||
|
||||
const value: TourContextValue = {
|
||||
isActive,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { X, Lightbulb, Settings, PlayCircle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "@/i18n/navigation";
|
||||
import { useTour } from "@/components/tour/tour-provider";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
|
||||
const ONBOARDING_KEY = "onboarding_completed";
|
||||
|
||||
@@ -13,23 +14,47 @@ export function WelcomeBanner() {
|
||||
const t = useTranslations("welcome");
|
||||
const router = useRouter();
|
||||
const { startTour } = useTour();
|
||||
const onboardingCompleted = useSettingsStore((s) => s.onboardingCompleted);
|
||||
const showOnboardingOnNewDevices = useSettingsStore((s) => s.showOnboardingOnNewDevices);
|
||||
const updateSetting = useSettingsStore((s) => s.updateSetting);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// One-time migration: if the legacy per-device flag is set but the synced
|
||||
// setting isn't yet, mirror it into synced state so the user isn't shown
|
||||
// the banner again on this device after the upgrade.
|
||||
try {
|
||||
if (!localStorage.getItem(ONBOARDING_KEY)) {
|
||||
setVisible(true);
|
||||
const legacy = localStorage.getItem(ONBOARDING_KEY) === "true";
|
||||
if (legacy && !onboardingCompleted) {
|
||||
updateSetting("onboardingCompleted", true);
|
||||
}
|
||||
} catch { /* localStorage unavailable */ }
|
||||
}, []);
|
||||
}, [onboardingCompleted, updateSetting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onboardingCompleted) {
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
if (showOnboardingOnNewDevices) {
|
||||
try {
|
||||
if (localStorage.getItem(ONBOARDING_KEY) !== "true") {
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
} catch { /* localStorage unavailable */ }
|
||||
}
|
||||
setVisible(false);
|
||||
}, [onboardingCompleted, showOnboardingOnNewDevices]);
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
setDismissed(true);
|
||||
updateSetting("onboardingCompleted", true);
|
||||
try {
|
||||
localStorage.setItem(ONBOARDING_KEY, "true");
|
||||
} catch { /* localStorage unavailable */ }
|
||||
}, []);
|
||||
}, [updateSetting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
||||
Reference in New Issue
Block a user