Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions src/stores/auth-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ interface UsageStats {
};
}

// Normalize usage stats at the boundary (API responses + localStorage rehydration).
// Guarantees the full shape so render code can trust .toLocaleString() on every field.
// Without this, a stale persisted shape or a partial API response crashes the dashboard
// during hydration with "Cannot read properties of undefined (reading 'toLocaleString')".
function normalizeUsageStats(stats: unknown): UsageStats | null {
if (!stats || typeof stats !== "object") return null;
const s = stats as Partial<UsageStats> & {
limits?: Partial<UsageStats["limits"]>;
usage?: Partial<UsageStats["usage"]>;
};
const num = (v: unknown, fallback = 0): number =>
typeof v === "number" && Number.isFinite(v) ? v : fallback;
return {
planName: typeof s.planName === "string" && s.planName.length > 0 ? s.planName : "Free",
limits: {
uploads: num(s.limits?.uploads),
profiles: num(s.limits?.profiles),
},
usage: {
uploads: num(s.usage?.uploads),
profiles: num(s.usage?.profiles),
},
};
}

interface AuthState {
apiKey: string | null;
usageStats: UsageStats | null;
Expand All @@ -36,7 +61,7 @@ export const useAuthStore = create<AuthState>()(
error: null,
hasHydrated: false,
setApiKey: (key) => set({ apiKey: key, error: null }),
setUsageStats: (stats) => set({ usageStats: stats }),
setUsageStats: (stats) => set({ usageStats: normalizeUsageStats(stats) }),
setIsValidating: (validating) => set({ isValidating: validating }),
setError: (error) => set({ error }),
setHasHydrated: (hydrated) => set({ hasHydrated: hydrated }),
Expand All @@ -53,8 +78,14 @@ export const useAuthStore = create<AuthState>()(
apiKey: state.apiKey,
usageStats: state.usageStats,
}),
// Sanitize the rehydrated shape before the dashboard mounts. Old persisted
// shapes (pre-normalize) or any localStorage corruption get coerced to a
// safe default instead of crashing the layout on first paint.
onRehydrateStorage: () => (state) => {
state?.setHasHydrated(true);
if (state) {
state.usageStats = normalizeUsageStats(state.usageStats);
state.setHasHydrated(true);
}
},
}
)
Expand Down
Loading