diff --git a/src/store/index.ts b/src/store/index.ts index 2f5e558..1ef2417 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,5 +1,6 @@ +import * as SecureStore from "expo-secure-store"; import { create } from "zustand"; -import { devtools, subscribeWithSelector } from "zustand/middleware"; +import { createJSONStorage, devtools, persist, subscribeWithSelector } from "zustand/middleware"; export interface User { id: string; @@ -30,42 +31,90 @@ interface AppState { setError: (error: string | null) => void; } +/** + * Custom storage adapter backed by expo-secure-store. + * Secure store only supports string values and has a 2KB per-key limit, + * so we serialise the entire persisted slice as a single JSON string. + */ +const secureStorage = createJSONStorage(() => ({ + getItem: async (key: string): Promise => { + try { + return await SecureStore.getItemAsync(key); + } catch { + return null; + } + }, + setItem: async (key: string, value: string): Promise => { + try { + await SecureStore.setItemAsync(key, value); + } catch { + // Silently fail — store will fall back to in-memory state + } + }, + removeItem: async (key: string): Promise => { + try { + await SecureStore.deleteItemAsync(key); + } catch { + // Silently fail + } + }, +})); + export const useAppStore = create()( devtools( - subscribeWithSelector((set) => ({ - user: null, - isAuthenticated: false, - isAuthLoading: false, - authError: null, - accessToken: null, - refreshToken: null, - sessionExpiresAt: null, - theme: "light", - isLoading: false, - error: null, - setUser: (user) => set({ user, isAuthenticated: !!user }, false, "setUser"), - setTheme: (theme) => set({ theme }, false, "setTheme"), - setTokens: (accessToken, refreshToken, sessionExpiresAt) => - set({ accessToken, refreshToken, sessionExpiresAt }, false, "setTokens"), - setAuthLoading: (isAuthLoading) => set({ isAuthLoading }, false, "setAuthLoading"), - setAuthError: (authError) => set({ authError }, false, "setAuthError"), - logout: () => - set( - { - user: null, - isAuthenticated: false, - isAuthLoading: false, - authError: null, - accessToken: null, - refreshToken: null, - sessionExpiresAt: null, - }, - false, - "logout" - ), - setLoading: (isLoading) => set({ isLoading }, false, "setLoading"), - setError: (error) => set({ error }, false, "setError"), - })), + persist( + subscribeWithSelector((set) => ({ + user: null, + isAuthenticated: false, + isAuthLoading: false, + authError: null, + accessToken: null, + refreshToken: null, + sessionExpiresAt: null, + theme: "light", + isLoading: false, + error: null, + setUser: (user) => set({ user, isAuthenticated: !!user }, false, "setUser"), + setTheme: (theme) => set({ theme }, false, "setTheme"), + setTokens: (accessToken, refreshToken, sessionExpiresAt) => + set({ accessToken, refreshToken, sessionExpiresAt }, false, "setTokens"), + setAuthLoading: (isAuthLoading) => set({ isAuthLoading }, false, "setAuthLoading"), + setAuthError: (authError) => set({ authError }, false, "setAuthError"), + logout: () => + set( + { + user: null, + isAuthenticated: false, + isAuthLoading: false, + authError: null, + accessToken: null, + refreshToken: null, + sessionExpiresAt: null, + }, + false, + "logout" + ), + setLoading: (isLoading) => set({ isLoading }, false, "setLoading"), + setError: (error) => set({ error }, false, "setError"), + })), + { + name: "app-auth-storage", + storage: secureStorage, + /** + * Only persist auth-related and UI preference state. + * Transient flags (isLoading, isAuthLoading, error, authError) + * are intentionally excluded — they should always start fresh. + */ + partialize: (state) => ({ + user: state.user, + isAuthenticated: state.isAuthenticated, + accessToken: state.accessToken, + refreshToken: state.refreshToken, + sessionExpiresAt: state.sessionExpiresAt, + theme: state.theme, + }), + } + ), { name: "AppStore" } ) );