mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 11:10:23 +02:00
Settings page rewritten to use RNR primitives (RadioGroup, Switch,
Avatar, Separator) instead of self-drawn equivalents, removes 3
hardcoded #71717a hex colors in favor of THEME tokens, and adds
Alert.alert confirmation on sign-out with destructive Button variant.
Two new push subscreens under more/settings/:
- profile.tsx edits name + avatar. Avatar tap opens iOS native
ActionSheetIOS (Take Photo / Library / Remove) via
expo-image-picker, then PATCH /api/me.
- notifications.tsx 5 inbox groups + system_notifications toggle,
backed by optimistic PUT /api/notification-preferences.
New mobile-owned query + mutation for notification preferences mirror
the web design (no runtime import — per CLAUDE.md "Mobile-owned
updaters"). auth-store gets setUser action for in-memory user update
after profile PATCH.
ApiClient gains fetchValidated + fetchValidatedWith private helpers
that collapse the fetch+parseWithFallback envelope. 4 settings-related
methods migrated as canary (getMe, updateMe, getNotificationPreferences,
updateNotificationPreferences); remaining 30+ read methods migrate
progressively in later PRs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
80 lines
2.5 KiB
TypeScript
80 lines
2.5 KiB
TypeScript
/**
|
|
* Mobile auth store — Zustand. Logic mirrors packages/core/auth/store.ts:
|
|
* - Token written ONLY on successful verifyCode
|
|
* - 401 → clear token; non-401 (5xx / network blip) → preserve token so
|
|
* the next launch can retry
|
|
* - logout = clear token + clear in-memory user + setToken(null)
|
|
*
|
|
* NOT shared with web/desktop (per Sharing Principles in root CLAUDE.md).
|
|
* Storage backend is expo-secure-store (mobile only); web uses HttpOnly
|
|
* cookies, desktop uses localStorage via StorageAdapter.
|
|
*/
|
|
import { create } from "zustand";
|
|
import type { User } from "@multica/core/types";
|
|
import { api, ApiError } from "./api";
|
|
import { clearToken, getToken, setToken } from "./secure-storage";
|
|
import { useWorkspaceStore } from "./workspace-store";
|
|
|
|
interface AuthState {
|
|
user: User | null;
|
|
isLoading: boolean;
|
|
initialize: () => Promise<void>;
|
|
sendCode: (email: string) => Promise<void>;
|
|
verifyCode: (email: string, code: string) => Promise<User>;
|
|
logout: () => Promise<void>;
|
|
/** Overwrite the in-memory user — call after PATCH /api/me so name/avatar
|
|
* edits land without a refetch. Server response is the source of truth. */
|
|
setUser: (user: User) => void;
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>((set) => ({
|
|
user: null,
|
|
isLoading: true,
|
|
|
|
initialize: async () => {
|
|
// Restore the persisted workspace slug alongside the auth token so the
|
|
// entry redirect (app/index.tsx) can route directly to the last-used
|
|
// workspace without flashing /select-workspace.
|
|
await useWorkspaceStore.getState().restoreSlug();
|
|
|
|
const token = await getToken();
|
|
if (!token) {
|
|
set({ isLoading: false });
|
|
return;
|
|
}
|
|
api.setToken(token);
|
|
try {
|
|
const user = await api.getMe();
|
|
set({ user, isLoading: false });
|
|
} catch (err) {
|
|
// Only clear token on a genuine 401. Network blips / 5xx keep the
|
|
// token so the next launch (or a manual refresh) can retry.
|
|
if (err instanceof ApiError && err.status === 401) {
|
|
await clearToken();
|
|
api.setToken(null);
|
|
}
|
|
set({ user: null, isLoading: false });
|
|
}
|
|
},
|
|
|
|
sendCode: async (email) => {
|
|
await api.sendCode(email);
|
|
},
|
|
|
|
verifyCode: async (email, code) => {
|
|
const { token, user } = await api.verifyCode(email, code);
|
|
await setToken(token);
|
|
api.setToken(token);
|
|
set({ user });
|
|
return user;
|
|
},
|
|
|
|
logout: async () => {
|
|
await clearToken();
|
|
api.setToken(null);
|
|
set({ user: null });
|
|
},
|
|
|
|
setUser: (user) => set({ user }),
|
|
}));
|