> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 51. Capstone Project

> Build a complete enterprise-grade React Native application from scratch

<Frame>
  <img src="https://mintlify.s3.us-west-1.amazonaws.com/devweeekends/images/courses/react-native-crash-course/capstone.svg" alt="Capstone Project" />
</Frame>

## Project Overview

<Info>
  **Estimated Time**: 40+ hours | **Difficulty**: Advanced | **Prerequisites**: All previous modules
</Info>

Congratulations on reaching the capstone project. This is where all the modules come together into something real. You will build **TaskFlow** -- a comprehensive project management application with real-time collaboration, offline support, and enterprise-grade features. The goal is not to build a toy app, but to simulate the experience of building a production mobile application from scratch, making the same architectural decisions and trade-offs a senior mobile engineer faces every day.

**What You'll Build:**

* Full-featured project management app
* Real-time collaboration
* Offline-first architecture
* Push notifications
* Analytics & monitoring
* CI/CD pipeline

***

## TaskFlow - Project Management App

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                         TaskFlow Application                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   Core Features                          Advanced Features                   │
│   ─────────────                          ─────────────────                   │
│   • User authentication                  • Real-time sync                    │
│   • Project management                   • Offline support                   │
│   • Task boards (Kanban)                 • Push notifications                │
│   • Team collaboration                   • File attachments                  │
│   • Comments & mentions                  • Activity timeline                 │
│   • Due dates & reminders                • Search & filters                  │
│                                                                              │
│   Technical Requirements                 Enterprise Features                 │
│   ─────────────────────                  ───────────────────                 │
│   • TypeScript                           • Analytics                         │
│   • React Navigation                     • Error tracking                    │
│   • Zustand + React Query                • Feature flags                     │
│   • Reanimated animations                • A/B testing                       │
│   • Unit & E2E tests                     • CI/CD pipeline                    │
│   • Accessibility                        • App Store deployment              │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

***

## Phase 1: Project Setup (Week 1)

The first week is about building a solid foundation. Resist the urge to start coding features immediately -- investing in proper project structure, TypeScript configuration, and tooling pays dividends for the remaining weeks. A common mistake is rushing to build the Kanban board on day one and then spending week three refactoring the navigation because the initial structure could not accommodate the auth flow.

### 1.1 Initialize Project

```bash theme={null}
# Create new Expo project with TypeScript
npx create-expo-app@latest taskflow --template expo-template-blank-typescript

cd taskflow

# Install core dependencies
npx expo install expo-router expo-linking expo-constants expo-status-bar

# Install navigation
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs

# Install state management
npm install zustand @tanstack/react-query

# Install UI libraries
npm install react-native-reanimated react-native-gesture-handler
npm install @expo/vector-icons

# Install form handling
npm install react-hook-form zod @hookform/resolvers

# Install storage
npx expo install expo-secure-store @react-native-async-storage/async-storage

# Install dev dependencies
npm install -D @types/react @testing-library/react-native jest-expo
```

### 1.2 Project Structure

```
taskflow/
├── app/                          # Expo Router screens
│   ├── (auth)/
│   │   ├── login.tsx
│   │   ├── register.tsx
│   │   └── forgot-password.tsx
│   ├── (tabs)/
│   │   ├── _layout.tsx
│   │   ├── index.tsx            # Dashboard
│   │   ├── projects.tsx
│   │   ├── notifications.tsx
│   │   └── profile.tsx
│   ├── project/
│   │   ├── [id]/
│   │   │   ├── index.tsx        # Project detail
│   │   │   ├── board.tsx        # Kanban board
│   │   │   ├── tasks.tsx        # Task list
│   │   │   └── settings.tsx
│   │   └── create.tsx
│   ├── task/
│   │   ├── [id].tsx             # Task detail
│   │   └── create.tsx
│   ├── _layout.tsx
│   └── +not-found.tsx
├── src/
│   ├── components/
│   │   ├── ui/                  # Base UI components
│   │   │   ├── Button.tsx
│   │   │   ├── Input.tsx
│   │   │   ├── Card.tsx
│   │   │   ├── Avatar.tsx
│   │   │   ├── Badge.tsx
│   │   │   └── index.ts
│   │   ├── forms/
│   │   │   ├── LoginForm.tsx
│   │   │   ├── TaskForm.tsx
│   │   │   └── ProjectForm.tsx
│   │   ├── project/
│   │   │   ├── ProjectCard.tsx
│   │   │   ├── ProjectList.tsx
│   │   │   └── KanbanBoard.tsx
│   │   ├── task/
│   │   │   ├── TaskCard.tsx
│   │   │   ├── TaskList.tsx
│   │   │   └── TaskDetail.tsx
│   │   └── common/
│   │       ├── Header.tsx
│   │       ├── EmptyState.tsx
│   │       └── ErrorBoundary.tsx
│   ├── hooks/
│   │   ├── useAuth.ts
│   │   ├── useProjects.ts
│   │   ├── useTasks.ts
│   │   ├── useOffline.ts
│   │   └── useNotifications.ts
│   ├── services/
│   │   ├── api/
│   │   │   ├── client.ts
│   │   │   ├── auth.ts
│   │   │   ├── projects.ts
│   │   │   └── tasks.ts
│   │   ├── storage/
│   │   │   ├── secure.ts
│   │   │   └── async.ts
│   │   └── notifications/
│   │       └── push.ts
│   ├── stores/
│   │   ├── authStore.ts
│   │   ├── projectStore.ts
│   │   └── uiStore.ts
│   ├── types/
│   │   ├── auth.ts
│   │   ├── project.ts
│   │   ├── task.ts
│   │   └── api.ts
│   ├── utils/
│   │   ├── date.ts
│   │   ├── validation.ts
│   │   └── helpers.ts
│   ├── constants/
│   │   ├── colors.ts
│   │   ├── spacing.ts
│   │   └── config.ts
│   └── theme/
│       ├── index.ts
│       └── tokens.ts
├── __tests__/
│   ├── components/
│   ├── hooks/
│   └── screens/
├── e2e/
│   ├── auth.test.ts
│   ├── projects.test.ts
│   └── tasks.test.ts
├── assets/
├── app.json
├── eas.json
├── tsconfig.json
└── package.json
```

### 1.3 TypeScript Configuration

```json theme={null}
// tsconfig.json
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@hooks/*": ["src/hooks/*"],
      "@services/*": ["src/services/*"],
      "@stores/*": ["src/stores/*"],
      "@types/*": ["src/types/*"],
      "@utils/*": ["src/utils/*"],
      "@constants/*": ["src/constants/*"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}
```

***

## Phase 2: Core Features (Week 2-3)

This phase builds the features users interact with directly. Start with authentication (it gates everything else), then project management, then the Kanban board. This order matters because each feature depends on the one before it.

A useful mental model: think of this phase as building a house. Authentication is the front door -- you need it before anything inside makes sense. The project list is the hallway. The Kanban board is the main living room where users spend most of their time.

### 2.1 Authentication System

```tsx theme={null}
// src/types/auth.ts
export interface User {
  id: string;
  email: string;
  name: string;
  avatar?: string;
  createdAt: string;
}

export interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
}

export interface LoginCredentials {
  email: string;
  password: string;
}

export interface RegisterData extends LoginCredentials {
  name: string;
  confirmPassword: string;
}
```

```tsx theme={null}
// src/stores/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
import { User, LoginCredentials, RegisterData } from '@/types/auth';
import { authApi } from '@/services/api/auth';

interface AuthStore {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  
  login: (credentials: LoginCredentials) => Promise<void>;
  register: (data: RegisterData) => Promise<void>;
  logout: () => Promise<void>;
  refreshToken: () => Promise<void>;
  updateProfile: (data: Partial<User>) => Promise<void>;
}

export const useAuthStore = create<AuthStore>()(
  persist(
    (set, get) => ({
      user: null,
      isAuthenticated: false,
      isLoading: false,

      login: async (credentials) => {
        set({ isLoading: true });
        try {
          const { user, token, refreshToken } = await authApi.login(credentials);
          
          // Store tokens securely
          await SecureStore.setItemAsync('accessToken', token);
          await SecureStore.setItemAsync('refreshToken', refreshToken);
          
          set({ user, isAuthenticated: true, isLoading: false });
        } catch (error) {
          set({ isLoading: false });
          throw error;
        }
      },

      register: async (data) => {
        set({ isLoading: true });
        try {
          const { user, token, refreshToken } = await authApi.register(data);
          
          await SecureStore.setItemAsync('accessToken', token);
          await SecureStore.setItemAsync('refreshToken', refreshToken);
          
          set({ user, isAuthenticated: true, isLoading: false });
        } catch (error) {
          set({ isLoading: false });
          throw error;
        }
      },

      logout: async () => {
        await SecureStore.deleteItemAsync('accessToken');
        await SecureStore.deleteItemAsync('refreshToken');
        set({ user: null, isAuthenticated: false });
      },

      refreshToken: async () => {
        const refreshToken = await SecureStore.getItemAsync('refreshToken');
        if (!refreshToken) throw new Error('No refresh token');
        
        const { token } = await authApi.refresh(refreshToken);
        await SecureStore.setItemAsync('accessToken', token);
      },

      updateProfile: async (data) => {
        const user = get().user;
        if (!user) throw new Error('Not authenticated');
        
        const updatedUser = await authApi.updateProfile(user.id, data);
        set({ user: updatedUser });
      },
    }),
    {
      name: 'auth-storage',
      storage: createJSONStorage(() => AsyncStorage),
      partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated }),
    }
  )
);
```

### 2.2 Project Management

```tsx theme={null}
// src/types/project.ts
export interface Project {
  id: string;
  name: string;
  description?: string;
  color: string;
  icon: string;
  ownerId: string;
  members: ProjectMember[];
  taskCount: number;
  completedTaskCount: number;
  createdAt: string;
  updatedAt: string;
}

export interface ProjectMember {
  userId: string;
  role: 'owner' | 'admin' | 'member' | 'viewer';
  user: {
    id: string;
    name: string;
    avatar?: string;
  };
}

export type TaskStatus = 'todo' | 'in_progress' | 'review' | 'done';
export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';

export interface Task {
  id: string;
  projectId: string;
  title: string;
  description?: string;
  status: TaskStatus;
  priority: TaskPriority;
  assigneeId?: string;
  assignee?: {
    id: string;
    name: string;
    avatar?: string;
  };
  dueDate?: string;
  tags: string[];
  attachments: Attachment[];
  comments: Comment[];
  createdAt: string;
  updatedAt: string;
}

export interface Attachment {
  id: string;
  name: string;
  url: string;
  type: string;
  size: number;
}

export interface Comment {
  id: string;
  taskId: string;
  userId: string;
  user: {
    id: string;
    name: string;
    avatar?: string;
  };
  content: string;
  mentions: string[];
  createdAt: string;
}
```

### 2.3 Kanban Board Component

The Kanban board is the centerpiece of TaskFlow and the most technically challenging component. It combines horizontal scrolling (between columns), vertical scrolling (within columns), drag-and-drop gesture handling, and animated state transitions. Getting this right requires Reanimated for performant animations and Gesture Handler for responsive drag interactions.

<Tip>
  **Architecture decision:** The board uses a horizontally-scrolling FlatList of columns, where each column is a vertically-scrolling list of task cards. This nested scrolling pattern is common in production apps (Trello, Jira, Asana all use it). The key challenge is preventing gesture conflicts -- a horizontal swipe should scroll columns, while a long-press-then-drag should move a task card. The `Gesture.Pan()` with `onStart` after a long press avoids this ambiguity.
</Tip>

```tsx theme={null}
// src/components/project/KanbanBoard.tsx
import { View, StyleSheet, ScrollView } from 'react-native';
import { useCallback, useState } from 'react';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { Task, TaskStatus } from '@/types/project';
import { TaskCard } from '../task/TaskCard';
import { Text } from '../ui/Text';

interface KanbanBoardProps {
  tasks: Task[];
  onTaskMove: (taskId: string, newStatus: TaskStatus) => void;
  onTaskPress: (task: Task) => void;
}

const COLUMNS: { status: TaskStatus; title: string; color: string }[] = [
  { status: 'todo', title: 'To Do', color: '#6b7280' },
  { status: 'in_progress', title: 'In Progress', color: '#3b82f6' },
  { status: 'review', title: 'Review', color: '#f59e0b' },
  { status: 'done', title: 'Done', color: '#22c55e' },
];

export function KanbanBoard({ tasks, onTaskMove, onTaskPress }: KanbanBoardProps) {
  const [draggingTask, setDraggingTask] = useState<Task | null>(null);

  const getTasksByStatus = useCallback(
    (status: TaskStatus) => tasks.filter((task) => task.status === status),
    [tasks]
  );

  return (
    <ScrollView
      horizontal
      showsHorizontalScrollIndicator={false}
      contentContainerStyle={styles.container}
    >
      {COLUMNS.map((column) => (
        <KanbanColumn
          key={column.status}
          title={column.title}
          color={column.color}
          tasks={getTasksByStatus(column.status)}
          onTaskPress={onTaskPress}
          onTaskDrop={(taskId) => onTaskMove(taskId, column.status)}
          isDragging={!!draggingTask}
        />
      ))}
    </ScrollView>
  );
}

interface KanbanColumnProps {
  title: string;
  color: string;
  tasks: Task[];
  onTaskPress: (task: Task) => void;
  onTaskDrop: (taskId: string) => void;
  isDragging: boolean;
}

function KanbanColumn({
  title,
  color,
  tasks,
  onTaskPress,
  onTaskDrop,
  isDragging,
}: KanbanColumnProps) {
  const isHovered = useSharedValue(false);

  const columnStyle = useAnimatedStyle(() => ({
    backgroundColor: isHovered.value ? `${color}20` : '#f9fafb',
    borderColor: isHovered.value ? color : '#e5e7eb',
  }));

  return (
    <Animated.View style={[styles.column, columnStyle]}>
      <View style={styles.columnHeader}>
        <View style={[styles.statusDot, { backgroundColor: color }]} />
        <Text style={styles.columnTitle}>{title}</Text>
        <View style={styles.taskCount}>
          <Text style={styles.taskCountText}>{tasks.length}</Text>
        </View>
      </View>

      <ScrollView
        style={styles.taskList}
        showsVerticalScrollIndicator={false}
      >
        {tasks.map((task) => (
          <DraggableTaskCard
            key={task.id}
            task={task}
            onPress={() => onTaskPress(task)}
          />
        ))}
      </ScrollView>
    </Animated.View>
  );
}

interface DraggableTaskCardProps {
  task: Task;
  onPress: () => void;
}

function DraggableTaskCard({ task, onPress }: DraggableTaskCardProps) {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  const scale = useSharedValue(1);
  const zIndex = useSharedValue(0);

  const gesture = Gesture.Pan()
    .onStart(() => {
      scale.value = withSpring(1.05);
      zIndex.value = 100;
    })
    .onUpdate((event) => {
      translateX.value = event.translationX;
      translateY.value = event.translationY;
    })
    .onEnd(() => {
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
      scale.value = withSpring(1);
      zIndex.value = 0;
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
      { scale: scale.value },
    ],
    zIndex: zIndex.value,
  }));

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={animatedStyle}>
        <TaskCard task={task} onPress={onPress} />
      </Animated.View>
    </GestureDetector>
  );
}

const styles = StyleSheet.create({
  container: {
    padding: 16,
    gap: 16,
  },
  column: {
    width: 300,
    backgroundColor: '#f9fafb',
    borderRadius: 12,
    borderWidth: 2,
    borderColor: '#e5e7eb',
    padding: 12,
    maxHeight: '100%',
  },
  columnHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 12,
    gap: 8,
  },
  statusDot: {
    width: 8,
    height: 8,
    borderRadius: 4,
  },
  columnTitle: {
    fontSize: 14,
    fontWeight: '600',
    color: '#374151',
    flex: 1,
  },
  taskCount: {
    backgroundColor: '#e5e7eb',
    paddingHorizontal: 8,
    paddingVertical: 2,
    borderRadius: 10,
  },
  taskCountText: {
    fontSize: 12,
    fontWeight: '500',
    color: '#6b7280',
  },
  taskList: {
    flex: 1,
  },
});
```

***

## Phase 3: Advanced Features (Week 4-5)

This phase adds the features that separate a demo app from a production app: real-time collaboration, offline support, and push notifications. These are the hardest features to implement correctly because they involve distributed systems concerns -- what happens when two users edit the same task simultaneously? What happens when a user makes changes offline and then reconnects?

Do not aim for perfection here. Implement the simplest version that works, handle the most common edge cases, and document the known limitations. A "last writer wins" conflict resolution strategy is simpler than operational transforms and sufficient for most project management use cases.

**Conflict resolution decision matrix for TaskFlow:**

| Scenario                                                   | Strategy                                                           | Implementation                                                                                          |
| ---------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| Two users edit different fields of the same task           | Field-level merge -- both edits survive                            | Server merges by field timestamp: if User A edits `title` and User B edits `status`, both changes apply |
| Two users edit the same field of the same task             | Last writer wins (by server timestamp)                             | Server accepts the latest `updatedAt`; the other edit is silently overwritten                           |
| User edits a task offline that was deleted by another user | Delete wins                                                        | On sync, server returns 404; client removes the task from local store and shows "This task was deleted" |
| User creates a task offline with a UUID that collides      | Client-generated UUIDs (v4) -- collision probability is negligible | Use `crypto.randomUUID()` or uuid library; server rejects duplicates with 409                           |

### 3.1 Real-time Sync with WebSocket

```tsx theme={null}
// src/services/realtime/socket.ts
import { io, Socket } from 'socket.io-client';
import * as SecureStore from 'expo-secure-store';
import { useProjectStore } from '@/stores/projectStore';
import { useTaskStore } from '@/stores/taskStore';

class RealtimeService {
  private socket: Socket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;

  async connect() {
    const token = await SecureStore.getItemAsync('accessToken');
    if (!token) return;

    this.socket = io(process.env.EXPO_PUBLIC_WS_URL!, {
      auth: { token },
      transports: ['websocket'],
      reconnection: true,
      reconnectionAttempts: this.maxReconnectAttempts,
      reconnectionDelay: 1000,
    });

    this.setupListeners();
  }

  private setupListeners() {
    if (!this.socket) return;

    this.socket.on('connect', () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0;
    });

    this.socket.on('disconnect', (reason) => {
      console.log('WebSocket disconnected:', reason);
    });

    this.socket.on('connect_error', (error) => {
      console.error('WebSocket connection error:', error);
      this.reconnectAttempts++;
    });

    // Project events
    this.socket.on('project:created', (project) => {
      useProjectStore.getState().addProject(project);
    });

    this.socket.on('project:updated', (project) => {
      useProjectStore.getState().updateProject(project.id, project);
    });

    this.socket.on('project:deleted', ({ projectId }) => {
      useProjectStore.getState().removeProject(projectId);
    });

    // Task events
    this.socket.on('task:created', (task) => {
      useTaskStore.getState().addTask(task);
    });

    this.socket.on('task:updated', (task) => {
      useTaskStore.getState().updateTask(task.id, task);
    });

    this.socket.on('task:moved', ({ taskId, newStatus }) => {
      useTaskStore.getState().moveTask(taskId, newStatus);
    });

    this.socket.on('task:deleted', ({ taskId }) => {
      useTaskStore.getState().removeTask(taskId);
    });

    // Comment events
    this.socket.on('comment:created', ({ taskId, comment }) => {
      useTaskStore.getState().addComment(taskId, comment);
    });
  }

  joinProject(projectId: string) {
    this.socket?.emit('project:join', { projectId });
  }

  leaveProject(projectId: string) {
    this.socket?.emit('project:leave', { projectId });
  }

  disconnect() {
    this.socket?.disconnect();
    this.socket = null;
  }
}

export const realtimeService = new RealtimeService();
```

### 3.2 Offline Support

```tsx theme={null}
// src/hooks/useOfflineSync.ts
import { useEffect, useCallback } from 'react';
import NetInfo from '@react-native-community/netinfo';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useQueryClient } from '@tanstack/react-query';

interface PendingAction {
  id: string;
  type: 'create' | 'update' | 'delete';
  entity: 'task' | 'project' | 'comment';
  data: unknown;
  timestamp: number;
}

const PENDING_ACTIONS_KEY = 'pending_actions';

export function useOfflineSync() {
  const queryClient = useQueryClient();

  // Queue action for offline sync
  const queueAction = useCallback(async (action: Omit<PendingAction, 'id' | 'timestamp'>) => {
    const pendingActions = await getPendingActions();
    const newAction: PendingAction = {
      ...action,
      id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
      timestamp: Date.now(),
    };
    
    await AsyncStorage.setItem(
      PENDING_ACTIONS_KEY,
      JSON.stringify([...pendingActions, newAction])
    );
  }, []);

  // Sync pending actions when online
  const syncPendingActions = useCallback(async () => {
    const pendingActions = await getPendingActions();
    if (pendingActions.length === 0) return;

    const failedActions: PendingAction[] = [];

    for (const action of pendingActions) {
      try {
        await executeAction(action);
      } catch (error) {
        console.error('Failed to sync action:', action, error);
        failedActions.push(action);
      }
    }

    // Keep failed actions for retry
    await AsyncStorage.setItem(PENDING_ACTIONS_KEY, JSON.stringify(failedActions));

    // Invalidate queries to refresh data
    queryClient.invalidateQueries();
  }, [queryClient]);

  // Listen for network changes
  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener((state) => {
      if (state.isConnected && state.isInternetReachable) {
        syncPendingActions();
      }
    });

    return () => unsubscribe();
  }, [syncPendingActions]);

  return { queueAction, syncPendingActions };
}

async function getPendingActions(): Promise<PendingAction[]> {
  const data = await AsyncStorage.getItem(PENDING_ACTIONS_KEY);
  return data ? JSON.parse(data) : [];
}

async function executeAction(action: PendingAction): Promise<void> {
  // Implement API calls based on action type
  const { type, entity, data } = action;
  
  switch (`${entity}:${type}`) {
    case 'task:create':
      await fetch('/api/tasks', {
        method: 'POST',
        body: JSON.stringify(data),
      });
      break;
    case 'task:update':
      await fetch(`/api/tasks/${(data as any).id}`, {
        method: 'PATCH',
        body: JSON.stringify(data),
      });
      break;
    // Add more cases...
  }
}
```

### 3.3 Push Notifications

```tsx theme={null}
// src/services/notifications/push.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
import { router } from 'expo-router';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

export async function registerForPushNotifications(): Promise<string | null> {
  if (!Device.isDevice) {
    console.log('Push notifications require a physical device');
    return null;
  }

  const { status: existingStatus } = await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;

  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }

  if (finalStatus !== 'granted') {
    console.log('Push notification permission denied');
    return null;
  }

  const token = await Notifications.getExpoPushTokenAsync({
    projectId: process.env.EXPO_PUBLIC_PROJECT_ID,
  });

  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#3b82f6',
    });
  }

  return token.data;
}

export function setupNotificationListeners() {
  // Handle notification received while app is foregrounded
  const foregroundSubscription = Notifications.addNotificationReceivedListener(
    (notification) => {
      console.log('Notification received:', notification);
    }
  );

  // Handle notification tap
  const responseSubscription = Notifications.addNotificationResponseReceivedListener(
    (response) => {
      const data = response.notification.request.content.data;
      
      // Navigate based on notification type
      if (data.type === 'task_assigned') {
        router.push(`/task/${data.taskId}`);
      } else if (data.type === 'comment_mention') {
        router.push(`/task/${data.taskId}`);
      } else if (data.type === 'project_invite') {
        router.push(`/project/${data.projectId}`);
      }
    }
  );

  return () => {
    foregroundSubscription.remove();
    responseSubscription.remove();
  };
}
```

***

## Phase 4: Testing & Quality (Week 6)

Testing a capstone project is where you discover whether your architecture is actually testable. If your components are tightly coupled to navigation, global state, and network calls, writing tests becomes a slog of mock setup. If you followed the patterns from earlier modules (dependency injection via hooks, separated business logic, thin screen components), testing will be straightforward.

Focus your testing effort where it matters most: **auth flows** (because bugs here lock users out), **data mutations** (because bugs here corrupt data), and **offline sync** (because bugs here lose user work). Do not spend time snapshot-testing every presentational component.

### 4.1 Unit Tests

```tsx theme={null}
// __tests__/stores/authStore.test.ts
import { renderHook, act } from '@testing-library/react-native';
import { useAuthStore } from '@/stores/authStore';
import { authApi } from '@/services/api/auth';

jest.mock('@/services/api/auth');
jest.mock('expo-secure-store');

describe('authStore', () => {
  beforeEach(() => {
    useAuthStore.setState({
      user: null,
      isAuthenticated: false,
      isLoading: false,
    });
  });

  it('logs in user successfully', async () => {
    const mockUser = { id: '1', email: 'test@example.com', name: 'Test User' };
    (authApi.login as jest.Mock).mockResolvedValue({
      user: mockUser,
      token: 'access-token',
      refreshToken: 'refresh-token',
    });

    const { result } = renderHook(() => useAuthStore());

    await act(async () => {
      await result.current.login({ email: 'test@example.com', password: 'password' });
    });

    expect(result.current.user).toEqual(mockUser);
    expect(result.current.isAuthenticated).toBe(true);
  });

  it('handles login error', async () => {
    (authApi.login as jest.Mock).mockRejectedValue(new Error('Invalid credentials'));

    const { result } = renderHook(() => useAuthStore());

    await expect(
      act(async () => {
        await result.current.login({ email: 'test@example.com', password: 'wrong' });
      })
    ).rejects.toThrow('Invalid credentials');

    expect(result.current.isAuthenticated).toBe(false);
  });
});
```

### 4.2 E2E Tests with Detox

```tsx theme={null}
// e2e/auth.test.ts
describe('Authentication', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should login successfully', async () => {
    await element(by.id('email-input')).typeText('test@example.com');
    await element(by.id('password-input')).typeText('password123');
    await element(by.id('login-button')).tap();

    await expect(element(by.id('dashboard-screen'))).toBeVisible();
  });

  it('should show error for invalid credentials', async () => {
    await element(by.id('email-input')).typeText('test@example.com');
    await element(by.id('password-input')).typeText('wrongpassword');
    await element(by.id('login-button')).tap();

    await expect(element(by.text('Invalid credentials'))).toBeVisible();
  });

  it('should navigate to register screen', async () => {
    await element(by.id('register-link')).tap();

    await expect(element(by.id('register-screen'))).toBeVisible();
  });
});
```

```tsx theme={null}
// e2e/tasks.test.ts
describe('Task Management', () => {
  beforeAll(async () => {
    await device.launchApp();
    // Login first
    await element(by.id('email-input')).typeText('test@example.com');
    await element(by.id('password-input')).typeText('password123');
    await element(by.id('login-button')).tap();
  });

  it('should create a new task', async () => {
    await element(by.id('projects-tab')).tap();
    await element(by.id('project-card-1')).tap();
    await element(by.id('add-task-button')).tap();

    await element(by.id('task-title-input')).typeText('New Test Task');
    await element(by.id('task-description-input')).typeText('Task description');
    await element(by.id('save-task-button')).tap();

    await expect(element(by.text('New Test Task'))).toBeVisible();
  });

  it('should move task between columns', async () => {
    await element(by.id('task-card-1')).longPress();
    await element(by.id('column-in_progress')).tap();

    await expect(
      element(by.id('task-card-1').withAncestor(by.id('column-in_progress')))
    ).toBeVisible();
  });
});
```

***

## Phase 5: Deployment (Week 7)

The final mile is often the hardest. Code signing, provisioning profiles, and store review guidelines will test your patience. The good news: with EAS Build, the most painful parts are handled for you. The bad news: you still need to understand *what* EAS is doing (creating certificates, managing profiles, submitting binaries) so you can debug when things go wrong -- and they will.

Start by deploying to internal testing tracks (TestFlight for iOS, Internal Testing track on Google Play). These do not require full store review and let your testers install builds within minutes. Only submit to production after your internal testers have verified the build on real devices.

### 5.1 CI/CD Pipeline

```yaml theme={null}
# .github/workflows/ci-cd.yml
name: CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test -- --coverage

  build-preview:
    needs: test
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: npm ci
      - run: eas build --platform all --profile preview --non-interactive

  build-production:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: npm ci
      - run: eas build --platform all --profile production --non-interactive
      - run: eas submit --platform all --latest --non-interactive
```

### 5.2 EAS Configuration

```json theme={null}
// eas.json
{
  "cli": {
    "version": ">= 5.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal",
      "env": {
        "APP_ENV": "staging",
        "EXPO_PUBLIC_API_URL": "https://staging-api.taskflow.app"
      }
    },
    "production": {
      "distribution": "store",
      "env": {
        "APP_ENV": "production",
        "EXPO_PUBLIC_API_URL": "https://api.taskflow.app"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "your@email.com",
        "ascAppId": "1234567890"
      },
      "android": {
        "serviceAccountKeyPath": "./google-services.json",
        "track": "production"
      }
    }
  }
}
```

***

## Technology Selection Rationale

Before building, understand *why* each technology was chosen. In interviews and architecture reviews, the ability to articulate trade-offs behind tech choices matters more than the choices themselves.

| Decision           | Choice                     | Why This Over Alternatives                                                                                                |
| ------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Framework**      | Expo (managed workflow)    | Eliminates native build toolchain setup; EAS handles code signing; config plugins cover 95% of native customization needs |
| **Navigation**     | Expo Router (file-based)   | Type-safe routes from file structure; deep linking automatic; aligns with React Server Components direction               |
| **Client state**   | Zustand                    | Minimal boilerplate vs Redux; no provider nesting; works outside React components (useful for WebSocket handlers)         |
| **Server state**   | React Query                | Caching, background refresh, optimistic updates built-in; separates server state from client state cleanly                |
| **Forms**          | React Hook Form + Zod      | Uncontrolled by default (fewer re-renders); Zod schemas reusable for API validation; better TypeScript inference than Yup |
| **Animations**     | Reanimated 3               | Runs on UI thread (no bridge crossing); worklets enable 60fps animations; gesture handler integration for drag-and-drop   |
| **Secure storage** | expo-secure-store          | iOS Keychain + Android Keystore backed; simple API; no native module linking required in Expo                             |
| **Testing**        | Jest + RNTL + Detox        | Jest for unit/hook tests; RNTL for behavioral component tests; Detox for E2E (real device, real gestures)                 |
| **CI/CD**          | EAS Build + GitHub Actions | EAS for builds (no macOS runner needed); GitHub Actions for lint/test/deploy orchestration                                |

### When You Might Choose Differently

These are not universal "best" choices -- they are the best choices *for this project's constraints* (Expo, small team, MVP timeline). Here is when the alternatives win:

| If Your Constraint Is...                                | Consider Instead                                                    |
| ------------------------------------------------------- | ------------------------------------------------------------------- |
| Heavy custom native code (Bluetooth, AR, custom camera) | Bare React Native CLI workflow                                      |
| Existing Redux codebase with 100+ reducers              | Redux Toolkit + RTK Query (migration cost of switching is too high) |
| Exclusively GraphQL backend                             | Apollo Client (normalized cache is purpose-built for GraphQL)       |
| Team has no React experience                            | Flutter (single language, less ecosystem fragmentation)             |
| Need pixel-perfect iOS + separate Android UX            | Two native apps (SwiftUI + Jetpack Compose)                         |

***

## Architectural Decision Records

Throughout the capstone, document key decisions as lightweight ADRs (Architecture Decision Records). This practice is what separates a portfolio project from a tutorial copy-paste. When a hiring manager reviews your repo, these records demonstrate senior-level thinking.

| Decision                         | Context                                    | Options Considered                                                   | Chosen                                                             | Rationale                                                                                                                                          |
| -------------------------------- | ------------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Conflict resolution strategy** | Two users edit same task offline           | Operational Transforms (OT), CRDTs, Last Writer Wins (LWW)           | LWW with field-level granularity                                   | OT/CRDTs add significant complexity; field-level LWW means simultaneous edits to different fields both survive; only same-field conflicts are lost |
| **Offline queue structure**      | Pending mutations need ordering            | Single FIFO queue, per-entity queues, event sourcing                 | Single FIFO queue                                                  | Simplest to implement; ordering guarantees are sufficient for task management; per-entity queues add complexity without proportional benefit       |
| **Auth token storage**           | Tokens must survive app restart            | AsyncStorage, MMKV, Secure Store                                     | Secure Store for tokens, AsyncStorage for non-sensitive user prefs | Tokens are high-value targets; Secure Store provides hardware-backed encryption; AsyncStorage is faster for non-sensitive data                     |
| **WebSocket reconnection**       | Network transitions on mobile are frequent | Reconnect immediately, exponential backoff, user-initiated reconnect | Exponential backoff with jitter, max 5 attempts, then user prompt  | Immediate reconnect causes thundering herd; infinite retry wastes battery; user prompt after max attempts is honest UX                             |

***

## Evaluation Criteria

<CardGroup cols={2}>
  <Card title="Code Quality (25%)" icon="code">
    * TypeScript usage
    * Clean architecture
    * Code organization
    * Best practices
  </Card>

  <Card title="Features (25%)" icon="list-check">
    * Core functionality
    * Advanced features
    * Error handling
    * Edge cases
  </Card>

  <Card title="Testing (20%)" icon="flask">
    * Unit test coverage
    * Integration tests
    * E2E tests
    * Test quality
  </Card>

  <Card title="UX/Performance (15%)" icon="gauge">
    * Smooth animations
    * Fast load times
    * Offline support
    * Accessibility
  </Card>

  <Card title="DevOps (15%)" icon="gears">
    * CI/CD pipeline
    * Environment management
    * Monitoring setup
    * Documentation
  </Card>
</CardGroup>

### What Separates Good from Great

Most capstone submissions meet the functional requirements. Here is what distinguishes the top tier:

| Area               | Good (Meets Requirements)                          | Great (Exceeds Expectations)                                                                      |
| ------------------ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **Error handling** | Try/catch around API calls; generic error messages | Error boundaries per feature; contextual error messages; retry with backoff; offline fallbacks    |
| **TypeScript**     | Types on props and state                           | Discriminated unions for state machines; branded types for IDs; strict mode with no `any`         |
| **Testing**        | 70% coverage; snapshot tests                       | Behavioral tests that survive refactors; factory functions for test data; separate test utilities |
| **Offline**        | Queue mutations; replay on reconnect               | Optimistic UI; conflict detection; partial sync; offline indicator in UI                          |
| **Performance**    | App loads and scrolls smoothly                     | Performance budgets in CI; lazy loading for below-fold screens; preloading on navigation intent   |
| **Documentation**  | README with setup instructions                     | ADRs for key decisions; inline comments explaining *why* not *what*; API documentation            |

***

## Submission Requirements

1. **GitHub Repository** with complete source code
2. **README.md** with setup instructions
3. **Demo Video** (5-10 minutes) showcasing features
4. **Architecture Document** explaining design decisions
5. **Test Coverage Report** (minimum 70%)
6. **Working CI/CD Pipeline**
7. **Published App** on TestFlight/Play Store Internal

***

## Congratulations

You have completed the React Native Enterprise Mastery course. You now have the skills to build production-ready mobile applications used by millions of users. More importantly, you understand *why* things are built the way they are -- not just the syntax, but the trade-offs, the failure modes, and the patterns that scale.

<Card title="Get Certified" icon="certificate" href="/certification">
  Complete the certification exam to validate your skills
</Card>
