Project Overview
Estimated Time: 40+ hours | Difficulty: Advanced | Prerequisites: All previous modules
- Full-featured project management app
- Real-time collaboration
- Offline-first architecture
- Push notifications
- Analytics & monitoring
- CI/CD pipeline
TaskFlow - Project Management App
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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
Copy
# 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
Copy
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
Copy
// 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
Copy
// 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;
}
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// __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
Copy
// 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();
});
});
Copy
// 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
Copy
# .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
Copy
// 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
- GitHub Repository with complete source code
- README.md with setup instructions
- Demo Video (5-10 minutes) showcasing features
- Architecture Document explaining design decisions
- Test Coverage Report (minimum 70%)
- Working CI/CD Pipeline
- 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