From 869f21ee2efaefed6a5aa4fbd417c25df8dec02a Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
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 <noreply@anthropic.com>

* Task completion checkpoint

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
---
 apps/mobile/contexts/AuthContext.tsx | 298 +++++++++++++++++++++++++++++++++++
 1 file changed, 298 insertions(+)
 create mode 100644 apps/mobile/contexts/AuthContext.tsx

(limited to 'apps/mobile/contexts/AuthContext.tsx')

diff --git a/apps/mobile/contexts/AuthContext.tsx b/apps/mobile/contexts/AuthContext.tsx
new file mode 100644
index 0000000..6102433
--- /dev/null
+++ b/apps/mobile/contexts/AuthContext.tsx
@@ -0,0 +1,298 @@
+import React, {
+  createContext,
+  useContext,
+  useEffect,
+  useState,
+  useCallback,
+  useMemo,
+  type ReactNode,
+} from 'react';
+import { AppState, type AppStateStatus } from 'react-native';
+import type { Session, User, AuthChangeEvent } from '@supabase/supabase-js';
+import { supabase } from '../lib/supabase';
+import { signIn as authSignIn, signOut as authSignOut, getSession } from '../lib/auth';
+
+/**
+ * Auth context state interface
+ */
+interface AuthState {
+  user: User | null;
+  session: Session | null;
+  isLoading: boolean;
+  isInitialized: boolean;
+  error: string | null;
+}
+
+/**
+ * Auth context actions interface
+ */
+interface AuthActions {
+  signIn: (email: string, password: string) => Promise<boolean>;
+  signOut: () => Promise<void>;
+  clearError: () => void;
+}
+
+/**
+ * Combined auth context type
+ */
+type AuthContextType = AuthState & AuthActions;
+
+/**
+ * Default auth state
+ */
+const defaultAuthState: AuthState = {
+  user: null,
+  session: null,
+  isLoading: true,
+  isInitialized: false,
+  error: null,
+};
+
+/**
+ * Auth context with default values
+ */
+const AuthContext = createContext<AuthContextType | undefined>(undefined);
+
+/**
+ * Props for AuthProvider component
+ */
+interface AuthProviderProps {
+  children: ReactNode;
+}
+
+/**
+ * AuthProvider component that manages authentication state
+ * - Initializes auth state on mount
+ * - Listens to auth state changes
+ * - Auto-refreshes session when app comes to foreground
+ * - Provides auth actions via context
+ */
+export function AuthProvider({ children }: AuthProviderProps) {
+  const [state, setState] = useState<AuthState>(defaultAuthState);
+
+  /**
+   * Update auth state with partial updates
+   */
+  const updateState = useCallback((updates: Partial<AuthState>) => {
+    setState((prev) => ({ ...prev, ...updates }));
+  }, []);
+
+  /**
+   * Initialize auth state by checking for existing session
+   */
+  const initialize = useCallback(async () => {
+    try {
+      const { session, error } = await getSession();
+
+      if (error) {
+        console.warn('Auth initialization warning:', error);
+      }
+
+      updateState({
+        user: session?.user ?? null,
+        session,
+        isLoading: false,
+        isInitialized: true,
+        error: null,
+      });
+    } catch (error) {
+      console.error('Auth initialization error:', error);
+      updateState({
+        user: null,
+        session: null,
+        isLoading: false,
+        isInitialized: true,
+        error: error instanceof Error ? error.message : 'Failed to initialize auth',
+      });
+    }
+  }, [updateState]);
+
+  /**
+   * Sign in with email and password
+   */
+  const signIn = useCallback(
+    async (email: string, password: string): Promise<boolean> => {
+      updateState({ isLoading: true, error: null });
+
+      const result = await authSignIn(email, password);
+
+      if (result.success && result.user && result.session) {
+        updateState({
+          user: result.user,
+          session: result.session,
+          isLoading: false,
+          error: null,
+        });
+        return true;
+      }
+
+      updateState({
+        isLoading: false,
+        error: result.error || 'Sign in failed',
+      });
+      return false;
+    },
+    [updateState]
+  );
+
+  /**
+   * Sign out the current user
+   */
+  const signOut = useCallback(async () => {
+    updateState({ isLoading: true, error: null });
+
+    const result = await authSignOut();
+
+    if (result.success) {
+      updateState({
+        user: null,
+        session: null,
+        isLoading: false,
+        error: null,
+      });
+    } else {
+      updateState({
+        isLoading: false,
+        error: result.error || 'Sign out failed',
+      });
+    }
+  }, [updateState]);
+
+  /**
+   * Clear any auth errors
+   */
+  const clearError = useCallback(() => {
+    updateState({ error: null });
+  }, [updateState]);
+
+  /**
+   * Handle app state changes (foreground/background)
+   * Refresh session when app comes to foreground
+   */
+  useEffect(() => {
+    const handleAppStateChange = async (nextAppState: AppStateStatus) => {
+      if (nextAppState === 'active' && state.session) {
+        // App came to foreground, refresh the session
+        try {
+          const { data, error } = await supabase.auth.refreshSession();
+          if (error) {
+            console.warn('Session refresh warning:', error.message);
+          } else if (data.session) {
+            updateState({
+              session: data.session,
+              user: data.session.user,
+            });
+          }
+        } catch (error) {
+          console.error('Session refresh error:', error);
+        }
+      }
+    };
+
+    const subscription = AppState.addEventListener('change', handleAppStateChange);
+
+    return () => {
+      subscription.remove();
+    };
+  }, [state.session, updateState]);
+
+  /**
+   * Listen to Supabase auth state changes
+   */
+  useEffect(() => {
+    const {
+      data: { subscription },
+    } = supabase.auth.onAuthStateChange(
+      (event: AuthChangeEvent, session: Session | null) => {
+        console.log('Auth state changed:', event);
+
+        switch (event) {
+          case 'SIGNED_IN':
+            updateState({
+              user: session?.user ?? null,
+              session,
+              isLoading: false,
+              error: null,
+            });
+            break;
+          case 'SIGNED_OUT':
+            updateState({
+              user: null,
+              session: null,
+              isLoading: false,
+              error: null,
+            });
+            break;
+          case 'TOKEN_REFRESHED':
+            updateState({
+              session,
+              user: session?.user ?? null,
+            });
+            break;
+          case 'USER_UPDATED':
+            updateState({
+              user: session?.user ?? null,
+            });
+            break;
+          case 'PASSWORD_RECOVERY':
+          case 'MFA_CHALLENGE_VERIFIED':
+            // Handle other events as needed
+            break;
+          default:
+            // INITIAL_SESSION and other events
+            break;
+        }
+      }
+    );
+
+    return () => {
+      subscription.unsubscribe();
+    };
+  }, [updateState]);
+
+  /**
+   * Initialize auth on mount
+   */
+  useEffect(() => {
+    initialize();
+  }, [initialize]);
+
+  /**
+   * Memoized context value
+   */
+  const value = useMemo<AuthContextType>(
+    () => ({
+      ...state,
+      signIn,
+      signOut,
+      clearError,
+    }),
+    [state, signIn, signOut, clearError]
+  );
+
+  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
+}
+
+/**
+ * Hook to access auth context
+ * Must be used within an AuthProvider
+ */
+export function useAuth(): AuthContextType {
+  const context = useContext(AuthContext);
+
+  if (context === undefined) {
+    throw new Error('useAuth must be used within an AuthProvider');
+  }
+
+  return context;
+}
+
+/**
+ * Hook to check if user is authenticated
+ */
+export function useIsAuthenticated(): boolean {
+  const { session } = useAuth();
+  return session !== null;
+}
+
+export type { AuthContextType, AuthState, AuthActions };
-- 
cgit v1.2.3

