diff options
Diffstat (limited to 'apps/mobile/stores')
| -rw-r--r-- | apps/mobile/stores/authStore.ts | 235 | ||||
| -rw-r--r-- | apps/mobile/stores/taskStore.ts | 84 |
2 files changed, 319 insertions, 0 deletions
diff --git a/apps/mobile/stores/authStore.ts b/apps/mobile/stores/authStore.ts new file mode 100644 index 0000000..4bc4581 --- /dev/null +++ b/apps/mobile/stores/authStore.ts @@ -0,0 +1,235 @@ +import { create } from 'zustand'; +import type { Session, User, AuthChangeEvent } from '@supabase/supabase-js'; +import { supabase } from '../lib/supabase'; +import { + signIn as authSignIn, + signOut as authSignOut, + getSession, + refreshSession, +} from '../lib/auth'; + +/** + * Auth store state interface + */ +interface AuthState { + /** Current authenticated user */ + user: User | null; + /** Current session */ + session: Session | null; + /** Whether auth operations are in progress */ + isLoading: boolean; + /** Whether the store has been initialized */ + isInitialized: boolean; + /** Last auth error message */ + error: string | null; +} + +/** + * Auth store actions interface + */ +interface AuthActions { + /** Sign in with email and password */ + signIn: (email: string, password: string) => Promise<boolean>; + /** Sign out the current user */ + signOut: () => Promise<void>; + /** Initialize the auth store */ + initialize: () => Promise<void>; + /** Refresh the current session */ + refresh: () => Promise<void>; + /** Clear any auth errors */ + clearError: () => void; + /** Set the auth state (for internal use) */ + setAuth: (user: User | null, session: Session | null) => void; +} + +/** + * Combined auth store type + */ +type AuthStore = AuthState & AuthActions; + +/** + * Zustand store for authentication state management + * + * Usage: + * ```typescript + * import { useAuthStore } from './stores/authStore'; + * + * // In component + * const { user, isLoading, signIn, signOut } = useAuthStore(); + * + * // Or use selectors for performance + * const user = useAuthStore((state) => state.user); + * const signIn = useAuthStore((state) => state.signIn); + * ``` + */ +export const useAuthStore = create<AuthStore>((set, get) => ({ + // Initial state + user: null, + session: null, + isLoading: true, + isInitialized: false, + error: null, + + /** + * Sign in with email and password + */ + signIn: async (email: string, password: string): Promise<boolean> => { + set({ isLoading: true, error: null }); + + const result = await authSignIn(email, password); + + if (result.success && result.user && result.session) { + set({ + user: result.user, + session: result.session, + isLoading: false, + error: null, + }); + return true; + } + + set({ + isLoading: false, + error: result.error || 'Sign in failed', + }); + return false; + }, + + /** + * Sign out the current user + */ + signOut: async (): Promise<void> => { + set({ isLoading: true, error: null }); + + const result = await authSignOut(); + + if (result.success) { + set({ + user: null, + session: null, + isLoading: false, + error: null, + }); + } else { + set({ + isLoading: false, + error: result.error || 'Sign out failed', + }); + } + }, + + /** + * Initialize the auth store by checking for existing session + */ + initialize: async (): Promise<void> => { + // Prevent re-initialization + if (get().isInitialized) { + return; + } + + try { + const { session, error } = await getSession(); + + if (error) { + console.warn('Auth initialization warning:', error); + } + + set({ + user: session?.user ?? null, + session, + isLoading: false, + isInitialized: true, + error: null, + }); + } catch (error) { + console.error('Auth initialization error:', error); + set({ + user: null, + session: null, + isLoading: false, + isInitialized: true, + error: error instanceof Error ? error.message : 'Failed to initialize auth', + }); + } + }, + + /** + * Refresh the current session + */ + refresh: async (): Promise<void> => { + const { session } = get(); + if (!session) return; + + try { + const result = await refreshSession(); + + if (result.session) { + set({ + session: result.session, + user: result.session.user, + }); + } else if (result.error) { + console.warn('Session refresh warning:', result.error); + } + } catch (error) { + console.error('Session refresh error:', error); + } + }, + + /** + * Clear any auth errors + */ + clearError: (): void => { + set({ error: null }); + }, + + /** + * Set the auth state (for internal use by listeners) + */ + setAuth: (user: User | null, session: Session | null): void => { + set({ user, session, isLoading: false }); + }, +})); + +/** + * Setup auth state listener + * Should be called once when the app initializes + */ +export function setupAuthListener(): () => void { + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange( + (event: AuthChangeEvent, session: Session | null) => { + console.log('Auth state changed:', event); + + const { setAuth } = useAuthStore.getState(); + + switch (event) { + case 'SIGNED_IN': + case 'TOKEN_REFRESHED': + case 'USER_UPDATED': + setAuth(session?.user ?? null, session); + break; + case 'SIGNED_OUT': + setAuth(null, null); + break; + default: + break; + } + } + ); + + return () => { + subscription.unsubscribe(); + }; +} + +/** + * Selector hooks for common auth state + */ +export const useUser = () => useAuthStore((state) => state.user); +export const useSession = () => useAuthStore((state) => state.session); +export const useIsLoading = () => useAuthStore((state) => state.isLoading); +export const useIsInitialized = () => useAuthStore((state) => state.isInitialized); +export const useAuthError = () => useAuthStore((state) => state.error); +export const useIsAuthenticated = () => useAuthStore((state) => state.session !== null); diff --git a/apps/mobile/stores/taskStore.ts b/apps/mobile/stores/taskStore.ts new file mode 100644 index 0000000..1a08a45 --- /dev/null +++ b/apps/mobile/stores/taskStore.ts @@ -0,0 +1,84 @@ +import { create } from 'zustand'; +import type { TaskSummary, TaskOutputEntry } from '../lib/api'; + +interface TaskState { + // Data + tasks: TaskSummary[]; + selectedTaskId: string | null; + taskOutputs: Record<string, TaskOutputEntry[]>; + + // Actions + setTasks: (tasks: TaskSummary[]) => void; + updateTask: (taskId: string, update: Partial<TaskSummary>) => void; + selectTask: (taskId: string | null) => void; + appendOutput: (taskId: string, output: TaskOutputEntry) => void; + setTaskOutputs: (taskId: string, outputs: TaskOutputEntry[]) => void; + clearTaskOutputs: (taskId: string) => void; +} + +export const useTaskStore = create<TaskState>((set) => ({ + // Initial state + tasks: [], + selectedTaskId: null, + taskOutputs: {}, + + // Actions + setTasks: (tasks) => + set({ tasks }), + + updateTask: (taskId, update) => + set((state) => ({ + tasks: state.tasks.map((task) => + task.id === taskId ? { ...task, ...update } : task + ), + })), + + selectTask: (taskId) => + set({ selectedTaskId: taskId }), + + appendOutput: (taskId, output) => + set((state) => { + const existing = state.taskOutputs[taskId] ?? []; + // Avoid duplicates by checking ID + if (existing.some((o) => o.id === output.id)) { + return state; + } + return { + taskOutputs: { + ...state.taskOutputs, + [taskId]: [...existing, output], + }, + }; + }), + + setTaskOutputs: (taskId, outputs) => + set((state) => ({ + taskOutputs: { + ...state.taskOutputs, + [taskId]: outputs, + }, + })), + + clearTaskOutputs: (taskId) => + set((state) => { + const { [taskId]: _, ...rest } = state.taskOutputs; + return { taskOutputs: rest }; + }), +})); + +// Selectors for common use cases +export const selectSelectedTask = (state: TaskState) => + state.tasks.find((t) => t.id === state.selectedTaskId); + +export const selectRunningTasks = (state: TaskState) => + state.tasks.filter((t) => + ['running', 'initializing', 'starting'].includes(t.status) + ); + +export const selectPendingTasks = (state: TaskState) => + state.tasks.filter((t) => t.status === 'pending'); + +export const selectCompletedTasks = (state: TaskState) => + state.tasks.filter((t) => + ['done', 'failed', 'merged'].includes(t.status) + ); |
