summaryrefslogtreecommitdiff
path: root/apps/mobile/contexts
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-18 17:44:50 +0000
committerGitHub <noreply@github.com>2026-01-18 17:44:50 +0000
commit869f21ee2efaefed6a5aa4fbd417c25df8dec02a (patch)
tree2a90820ac817173e5b7154e0ba5e4f5d095f9613 /apps/mobile/contexts
parent219bca168508e1ea5e91e8a9ce98338afeddfbd2 (diff)
downloadsoryu-869f21ee2efaefed6a5aa4fbd417c25df8dec02a.tar.gz
soryu-869f21ee2efaefed6a5aa4fbd417c25df8dec02a.zip
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>
Diffstat (limited to 'apps/mobile/contexts')
-rw-r--r--apps/mobile/contexts/AuthContext.tsx298
1 files changed, 298 insertions, 0 deletions
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 };