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
119 changes: 84 additions & 35 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<string | null> => {
try {
return await SecureStore.getItemAsync(key);
} catch {
return null;
}
},
setItem: async (key: string, value: string): Promise<void> => {
try {
await SecureStore.setItemAsync(key, value);
} catch {
// Silently fail — store will fall back to in-memory state
}
},
removeItem: async (key: string): Promise<void> => {
try {
await SecureStore.deleteItemAsync(key);
} catch {
// Silently fail
}
},
}));

export const useAppStore = create<AppState>()(
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" }
)
);
Expand Down
Loading