/** * Onboarding Store (Zustand) * Manages welcome/guide screen state and dismissal * Syncs with backend UserSettings for cross-device persistence */ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { fetchUserSetting, createUserSetting, updateUserSetting } from '../services/api'; interface OnboardingState { isGuideDismissed: boolean; isGuideVisible: boolean; isLoading: boolean; lastSyncedAt: Date | null; // Actions dismissGuide: () => Promise; showGuide: () => void; toggleGuide: () => void; loadFromBackend: () => Promise; syncToBackend: (dismissed: boolean) => Promise; } const GUIDE_SETTING_KEY = 'workflow_guide_dismissed'; export const useOnboardingStore = create()( persist( (set, get) => ({ isGuideDismissed: false, isGuideVisible: false, isLoading: false, lastSyncedAt: null, loadFromBackend: async () => { const state = get(); // Avoid hammering the endpoint; re-fetch at most every 5 minutes if (state.lastSyncedAt) { const elapsedMs = Date.now() - state.lastSyncedAt.getTime(); if (elapsedMs < 5 * 60 * 1000) { return; } } if (state.isLoading) return; set({ isLoading: true }); try { const setting = await fetchUserSetting(GUIDE_SETTING_KEY); const dismissed = setting.value?.dismissed === true; set({ isGuideDismissed: dismissed, isGuideVisible: !dismissed, lastSyncedAt: new Date(), isLoading: false }); } catch (error: any) { // 404 means setting doesn't exist yet - that's fine, use local state if (error?.status === 429) { // Throttled: back off and don't spam warnings } else if (error?.status !== 404) { console.warn('Failed to load guide dismissal from backend:', error); } set({ isLoading: false }); } }, syncToBackend: async (dismissed: boolean) => { try { const data = { value: { dismissed, dismissed_at: new Date().toISOString() } }; try { await updateUserSetting(GUIDE_SETTING_KEY, data); } catch (error: any) { // If setting doesn't exist, create it if (error.status === 404) { await createUserSetting({ key: GUIDE_SETTING_KEY, value: data.value }); } else { throw error; } } set({ lastSyncedAt: new Date() }); } catch (error) { console.warn('Failed to sync guide dismissal to backend:', error); // Don't throw - local state is still updated } }, dismissGuide: async () => { set({ isGuideDismissed: true, isGuideVisible: false }); // Sync to backend asynchronously await get().syncToBackend(true); }, showGuide: () => set({ isGuideVisible: true }), toggleGuide: () => set((state) => ({ isGuideVisible: !state.isGuideVisible })), }), { name: 'onboarding-storage', } ) );