- useOnboarding.ts: when the backend gives a definitive answer (true/false, not a null retry failure), re-seed the neode_onboarding_complete localStorage flag accordingly. Fixes the case where a user clears site data on an already-onboarded node — OnboardingWrapper's useVideoBackground computed reads localStorage synchronously, so without this re-seed the intro video would fire again on /login even though RootRedirect correctly sent them straight to /login. - OnboardingWrapper.vue: login background now rotates through bg-intro-1..6 on each /login mount, with the current index persisted to localStorage (neode_login_bg_idx) so subsequent logouts advance rather than repeat the same image. - Dashboard.vue: subsequent-login branch drops the 1.2s showZoomIn entirely. Only the first dashboard entry after onboarding plays the full zoom + glitch reveal; every re-login now just fades in with the welcome typing (~300ms). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
57 lines
2.1 KiB
TypeScript
57 lines
2.1 KiB
TypeScript
/**
|
|
* Onboarding state - prefers backend, falls back to localStorage for mock/offline.
|
|
* Hardened: retries on 502/503, never blocks completion.
|
|
*/
|
|
import { rpcClient } from '@/api/rpc-client'
|
|
|
|
async function callWithRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T | null> {
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
return await fn()
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : ''
|
|
const isRetryable = /502|503|timeout|fetch|network/i.test(msg)
|
|
if (!isRetryable || i === maxRetries - 1) return null
|
|
await new Promise((r) => setTimeout(r, 800 * (i + 1)))
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
export async function isOnboardingComplete(): Promise<boolean> {
|
|
// Prefer the backend — localStorage gets stale across nodes (a
|
|
// browser that onboarded node A would otherwise treat fresh node B
|
|
// as already-onboarded and skip the wizard entirely). Only fall
|
|
// back to localStorage if the backend is unreachable.
|
|
const result = await callWithRetry(() => rpcClient.isOnboardingComplete(), 2)
|
|
if (result !== null) {
|
|
// Re-seed the localStorage cache so non-async consumers
|
|
// (OnboardingWrapper's useVideoBackground computed, etc.) see the
|
|
// right answer after the user clears site data on an already-
|
|
// onboarded node.
|
|
if (result) {
|
|
try { localStorage.setItem('neode_onboarding_complete', '1') } catch {}
|
|
} else {
|
|
try { localStorage.removeItem('neode_onboarding_complete') } catch {}
|
|
}
|
|
return result
|
|
}
|
|
return localStorage.getItem('neode_onboarding_complete') === '1'
|
|
}
|
|
|
|
export async function completeOnboarding(): Promise<void> {
|
|
await callWithRetry(() => rpcClient.completeOnboarding(), 3)
|
|
localStorage.setItem('neode_onboarding_complete', '1')
|
|
localStorage.removeItem('neode_onboarding_step')
|
|
}
|
|
|
|
/** Save current onboarding step so refresh resumes where user left off */
|
|
export function saveOnboardingStep(step: string): void {
|
|
localStorage.setItem('neode_onboarding_step', step)
|
|
}
|
|
|
|
/** Get the last saved onboarding step, or 'intro' if none */
|
|
export function getSavedOnboardingStep(): string {
|
|
return localStorage.getItem('neode_onboarding_step') || 'intro'
|
|
}
|