Skip to main content
Capstone Project

Project Overview

Estimated Time: 40+ hours | Difficulty: Advanced | Prerequisites: All previous modules
Congratulations on reaching the capstone project! You’ll now apply everything you’ve learned to build TaskFlow - a comprehensive project management application with real-time collaboration, offline support, and enterprise-grade features. 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)

1.1 Initialize Project

# 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

// 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)

2.1 Authentication System

// 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;
}
// 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

// 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

// 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)

3.1 Real-time Sync with WebSocket

// 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

// 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

// 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)

4.1 Unit Tests

// __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

// 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();
  });
});
// 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)

5.1 CI/CD Pipeline

# .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

// 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"
      }
    }
  }
}

Evaluation Criteria

Code Quality (25%)

  • TypeScript usage
  • Clean architecture
  • Code organization
  • Best practices

Features (25%)

  • Core functionality
  • Advanced features
  • Error handling
  • Edge cases

Testing (20%)

  • Unit test coverage
  • Integration tests
  • E2E tests
  • Test quality

UX/Performance (15%)

  • Smooth animations
  • Fast load times
  • Offline support
  • Accessibility

DevOps (15%)

  • CI/CD pipeline
  • Environment management
  • Monitoring setup
  • 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’ve completed the React Native Enterprise Mastery course. You now have the skills to build production-ready mobile applications used by millions of users.

Get Certified

Complete the certification exam to validate your skills