summaryrefslogtreecommitdiff
path: root/apps/mobile/stores
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/stores
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/stores')
-rw-r--r--apps/mobile/stores/authStore.ts235
-rw-r--r--apps/mobile/stores/taskStore.ts84
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)
+ );