103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
/**
|
|
* 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<void>;
|
|
showGuide: () => void;
|
|
toggleGuide: () => void;
|
|
loadFromBackend: () => Promise<void>;
|
|
syncToBackend: (dismissed: boolean) => Promise<void>;
|
|
}
|
|
|
|
const GUIDE_SETTING_KEY = 'workflow_guide_dismissed';
|
|
|
|
export const useOnboardingStore = create<OnboardingState>()(
|
|
persist<OnboardingState>(
|
|
(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',
|
|
}
|
|
)
|
|
);
|
|
|
|
|