From 869f21ee2efaefed6a5aa4fbd417c25df8dec02a Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 18 Jan 2026 17:44:50 +0000 Subject: Add React Native mobile app for Makima (#3) * [WIP] Heartbeat checkpoint - 2026-01-18 02:58:27 UTC * feat(mobile): complete mobile app integration and verification - Add ThemeColors type export to Colors.ts for type safety - Export SUPABASE_URL from supabase.ts and use environment variables - Update .env.example with correct default URLs - Add comprehensive README.md with setup instructions Verified: - TypeScript compiles without errors - App exports successfully for iOS and Android - All screens accessible (login, dashboard, tasks, settings, task detail) - Auth flow working with Zustand store and Supabase Co-Authored-By: Claude Opus 4.5 * Task completion checkpoint --------- Co-authored-by: Claude Opus 4.5 --- apps/mobile/stores/authStore.ts | 235 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 apps/mobile/stores/authStore.ts (limited to 'apps/mobile/stores/authStore.ts') 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; + /** Sign out the current user */ + signOut: () => Promise; + /** Initialize the auth store */ + initialize: () => Promise; + /** Refresh the current session */ + refresh: () => Promise; + /** 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((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 => { + 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 => { + 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 => { + // 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 => { + 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); -- cgit v1.2.3