Skip to main content

Final Project

Capstone Project: TaskFlow

Build a complete production-ready task management app

šŸŽÆ Project Overview

In this capstone project, you'll build TaskFlow — a full-featured task management app that demonstrates everything you've learned throughout this course. You'll take it from initial setup all the way to app store submission.

Project Requirements

TaskFlow is a task management application that helps users organize their work and personal tasks. The app must demonstrate proficiency in all major areas covered in this course.

Technical Requirements

šŸ“± Core

  • Expo SDK 52+
  • TypeScript
  • Expo Router for navigation
  • Proper project structure

šŸŽØ UI/UX

  • Responsive layouts
  • Dark mode support
  • Custom components
  • Meaningful animations

šŸ“Š Data

  • Local persistence
  • State management
  • API integration (optional)
  • Offline support

šŸ”§ Native

  • Push notifications
  • Haptic feedback
  • At least 2 other native APIs

Minimum Viable Product (MVP)

Feature Required Bonus
Create, edit, delete tasks āœ…
Task categories/projects āœ…
Due dates and reminders āœ…
Local data persistence āœ…
Push notifications āœ…
Dark mode āœ…
Animations (list, transitions) āœ…
Unit tests (>60% coverage) āœ…
Cloud sync ⭐
User authentication ⭐
Widgets (iOS/Android) ⭐
Voice input for tasks ⭐

Feature Specifications

Task Management

// Task data model
interface Task {
  id: string;
  title: string;
  description?: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  dueDate?: Date;
  reminderDate?: Date;
  projectId?: string;
  tags: string[];
  subtasks: Subtask[];
  createdAt: Date;
  updatedAt: Date;
}

interface Subtask {
  id: string;
  title: string;
  completed: boolean;
}

interface Project {
  id: string;
  name: string;
  color: string;
  icon: string;
  taskCount: number;
  createdAt: Date;
}

Screen Requirements

flowchart TB
    subgraph Tabs["Tab Navigation"]
        T1[Today]
        T2[Inbox]
        T3[Projects]
        T4[Settings]
    end
    
    subgraph Modals["Modal Screens"]
        M1[Add Task]
        M2[Edit Task]
        M3[Add Project]
    end
    
    subgraph Stack["Stack Screens"]
        S1[Project Details]
        S2[Task Details]
        S3[Search]
    end
    
    T1 --> M1
    T1 --> S2
    T2 --> M1
    T2 --> S2
    T3 --> S1
    T3 --> M3
    S1 --> M1
    S1 --> S2

Screen Descriptions

Today Screen

  • Shows tasks due today
  • Overdue tasks section (highlighted)
  • Quick add task FAB
  • Swipe to complete/delete
  • Pull to refresh

Inbox Screen

  • All tasks without a project
  • Filter by status, priority, date
  • Search functionality
  • Bulk actions (select multiple)

Projects Screen

  • List of all projects with task counts
  • Create new project
  • Drag to reorder projects
  • Project color/icon customization

Settings Screen

  • Theme toggle (light/dark/system)
  • Notification preferences
  • Default reminder time
  • Data export/import
  • About & version info

App Architecture

Project Structure

taskflow/
ā”œā”€ā”€ app/                      # Expo Router screens
│   ā”œā”€ā”€ (tabs)/
│   │   ā”œā”€ā”€ _layout.tsx
│   │   ā”œā”€ā”€ index.tsx         # Today
│   │   ā”œā”€ā”€ inbox.tsx
│   │   ā”œā”€ā”€ projects.tsx
│   │   └── settings.tsx
│   ā”œā”€ā”€ task/
│   │   └── [id].tsx          # Task details
│   ā”œā”€ā”€ project/
│   │   └── [id].tsx          # Project details
│   ā”œā”€ā”€ _layout.tsx           # Root layout
│   └── +not-found.tsx
ā”œā”€ā”€ components/
│   ā”œā”€ā”€ ui/                   # Reusable UI components
│   │   ā”œā”€ā”€ Button.tsx
│   │   ā”œā”€ā”€ Input.tsx
│   │   ā”œā”€ā”€ Card.tsx
│   │   └── ...
│   ā”œā”€ā”€ tasks/                # Task-related components
│   │   ā”œā”€ā”€ TaskItem.tsx
│   │   ā”œā”€ā”€ TaskList.tsx
│   │   ā”œā”€ā”€ TaskForm.tsx
│   │   └── ...
│   ā”œā”€ā”€ projects/             # Project components
│   └── common/               # Shared components
ā”œā”€ā”€ hooks/
│   ā”œā”€ā”€ useTasks.ts
│   ā”œā”€ā”€ useProjects.ts
│   ā”œā”€ā”€ useTheme.ts
│   └── useNotifications.ts
ā”œā”€ā”€ services/
│   ā”œā”€ā”€ storage.ts            # AsyncStorage wrapper
│   ā”œā”€ā”€ notifications.ts      # Push notification logic
│   └── analytics.ts          # Event tracking
ā”œā”€ā”€ stores/                   # State management (Zustand)
│   ā”œā”€ā”€ taskStore.ts
│   ā”œā”€ā”€ projectStore.ts
│   └── settingsStore.ts
ā”œā”€ā”€ utils/
│   ā”œā”€ā”€ dates.ts
│   ā”œā”€ā”€ colors.ts
│   └── validators.ts
ā”œā”€ā”€ constants/
│   ā”œā”€ā”€ theme.ts
│   └── config.ts
ā”œā”€ā”€ types/
│   └── index.ts
└── __tests__/

State Management

// stores/taskStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface TaskState {
  tasks: Task[];
  isLoading: boolean;
  
  // Actions
  addTask: (task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>) => void;
  updateTask: (id: string, updates: Partial<Task>) => void;
  deleteTask: (id: string) => void;
  toggleComplete: (id: string) => void;
  
  // Selectors
  getTasksByProject: (projectId: string) => Task[];
  getTodayTasks: () => Task[];
  getOverdueTasks: () => Task[];
}

export const useTaskStore = create<TaskState>()(
  persist(
    (set, get) => ({
      tasks: [],
      isLoading: false,
      
      addTask: (taskData) => {
        const newTask: Task = {
          ...taskData,
          id: generateId(),
          createdAt: new Date(),
          updatedAt: new Date(),
        };
        set((state) => ({ tasks: [...state.tasks, newTask] }));
      },
      
      updateTask: (id, updates) => {
        set((state) => ({
          tasks: state.tasks.map((task) =>
            task.id === id
              ? { ...task, ...updates, updatedAt: new Date() }
              : task
          ),
        }));
      },
      
      deleteTask: (id) => {
        set((state) => ({
          tasks: state.tasks.filter((task) => task.id !== id),
        }));
      },
      
      toggleComplete: (id) => {
        const task = get().tasks.find((t) => t.id === id);
        if (task) {
          get().updateTask(id, { completed: !task.completed });
        }
      },
      
      getTasksByProject: (projectId) => {
        return get().tasks.filter((task) => task.projectId === projectId);
      },
      
      getTodayTasks: () => {
        const today = new Date();
        return get().tasks.filter((task) => {
          if (!task.dueDate) return false;
          return isSameDay(task.dueDate, today);
        });
      },
      
      getOverdueTasks: () => {
        const today = new Date();
        return get().tasks.filter((task) => {
          if (!task.dueDate || task.completed) return false;
          return task.dueDate < today;
        });
      },
    }),
    {
      name: 'task-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

Phase 1: Project Setup

ā±ļø Estimated time: 2-3 hours

Objectives

  • Initialize Expo project with TypeScript
  • Set up project structure
  • Configure ESLint and Prettier
  • Set up Expo Router navigation
  • Create theme system with dark mode

Tasks

1.1 Initialize Project

# Create new Expo project
npx create-expo-app@latest taskflow -t tabs

cd taskflow

# Install additional dependencies
npx expo install zustand @react-native-async-storage/async-storage
npx expo install expo-haptics expo-notifications expo-local-authentication
npx expo install react-native-reanimated react-native-gesture-handler
npm install date-fns uuid
npm install -D @types/uuid

1.2 Configure Theme System

// constants/theme.ts
export const lightTheme = {
  colors: {
    primary: '#6366F1',
    background: '#FFFFFF',
    surface: '#F3F4F6',
    text: '#1F2937',
    textSecondary: '#6B7280',
    border: '#E5E7EB',
    error: '#EF4444',
    success: '#10B981',
    warning: '#F59E0B',
  },
  spacing: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 32,
  },
  borderRadius: {
    sm: 4,
    md: 8,
    lg: 16,
    full: 9999,
  },
};

export const darkTheme = {
  ...lightTheme,
  colors: {
    ...lightTheme.colors,
    background: '#111827',
    surface: '#1F2937',
    text: '#F9FAFB',
    textSecondary: '#9CA3AF',
    border: '#374151',
  },
};

1.3 Set Up Navigation Structure

// app/_layout.tsx
import { ThemeProvider } from '@/contexts/ThemeContext';
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <ThemeProvider>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen 
          name="task/[id]" 
          options={{ presentation: 'modal', title: 'Task Details' }} 
        />
        <Stack.Screen 
          name="project/[id]" 
          options={{ title: 'Project' }} 
        />
      </Stack>
    </ThemeProvider>
  );
}

Deliverables

  • āœ… Project runs without errors
  • āœ… Tab navigation works
  • āœ… Theme switching works
  • āœ… Project structure matches specification

Phase 2: Core Features

ā±ļø Estimated time: 8-12 hours

Objectives

  • Implement task CRUD operations
  • Create project management
  • Build reusable UI components
  • Implement data persistence

Tasks

2.1 Build Task Components

// components/tasks/TaskItem.tsx
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import Animated, { 
  useAnimatedStyle, 
  withSpring 
} from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';

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

export function TaskItem({ task, onToggle, onPress }: TaskItemProps) {
  const handleToggle = () => {
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    onToggle();
  };

  return (
    <Pressable style={styles.container} onPress={onPress}>
      <Pressable 
        style={styles.checkbox} 
        onPress={handleToggle}
        hitSlop={8}
      >
        <Ionicons
          name={task.completed ? 'checkmark-circle' : 'ellipse-outline'}
          size={24}
          color={task.completed ? '#10B981' : '#9CA3AF'}
        />
      </Pressable>
      
      <View style={styles.content}>
        <Text style={[
          styles.title,
          task.completed && styles.completedText
        ]}>
          {task.title}
        </Text>
        
        {task.dueDate && (
          <Text style={styles.dueDate}>
            {formatDueDate(task.dueDate)}
          </Text>
        )}
      </View>
      
      <PriorityBadge priority={task.priority} />
    </Pressable>
  );
}

2.2 Implement Task Form

// components/tasks/TaskForm.tsx
import { useState } from 'react';
import { View, TextInput, StyleSheet } from 'react-native';
import { DatePicker } from '@/components/ui/DatePicker';
import { PrioritySelector } from '@/components/ui/PrioritySelector';
import { ProjectPicker } from '@/components/ui/ProjectPicker';
import { Button } from '@/components/ui/Button';

interface TaskFormProps {
  initialData?: Partial<Task>;
  onSubmit: (data: TaskFormData) => void;
  onCancel: () => void;
}

export function TaskForm({ initialData, onSubmit, onCancel }: TaskFormProps) {
  const [title, setTitle] = useState(initialData?.title || '');
  const [description, setDescription] = useState(initialData?.description || '');
  const [priority, setPriority] = useState(initialData?.priority || 'medium');
  const [dueDate, setDueDate] = useState(initialData?.dueDate);
  const [projectId, setProjectId] = useState(initialData?.projectId);

  const handleSubmit = () => {
    if (!title.trim()) return;
    
    onSubmit({
      title: title.trim(),
      description: description.trim(),
      priority,
      dueDate,
      projectId,
    });
  };

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.titleInput}
        placeholder="Task title"
        value={title}
        onChangeText={setTitle}
        autoFocus
      />
      
      <TextInput
        style={styles.descriptionInput}
        placeholder="Description (optional)"
        value={description}
        onChangeText={setDescription}
        multiline
      />
      
      <PrioritySelector value={priority} onChange={setPriority} />
      
      <DatePicker 
        label="Due date" 
        value={dueDate} 
        onChange={setDueDate} 
      />
      
      <ProjectPicker value={projectId} onChange={setProjectId} />
      
      <View style={styles.buttons}>
        <Button variant="secondary" onPress={onCancel}>
          Cancel
        </Button>
        <Button onPress={handleSubmit} disabled={!title.trim()}>
          {initialData ? 'Save' : 'Add Task'}
        </Button>
      </View>
    </View>
  );
}

2.3 Build Today Screen

// app/(tabs)/index.tsx
import { View, StyleSheet } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { useTaskStore } from '@/stores/taskStore';
import { TaskItem } from '@/components/tasks/TaskItem';
import { EmptyState } from '@/components/common/EmptyState';
import { SectionHeader } from '@/components/common/SectionHeader';
import { FAB } from '@/components/ui/FAB';
import { router } from 'expo-router';

export default function TodayScreen() {
  const tasks = useTaskStore((s) => s.getTodayTasks());
  const overdueTasks = useTaskStore((s) => s.getOverdueTasks());
  const toggleComplete = useTaskStore((s) => s.toggleComplete);

  const sections = [
    { title: 'Overdue', data: overdueTasks, type: 'overdue' },
    { title: 'Today', data: tasks, type: 'today' },
  ].filter((s) => s.data.length > 0);

  const allTasks = sections.flatMap((s) => [
    { type: 'header', title: s.title, key: s.title },
    ...s.data.map((t) => ({ type: 'task', task: t, key: t.id })),
  ]);

  return (
    <View style={styles.container}>
      <FlashList
        data={allTasks}
        estimatedItemSize={60}
        renderItem={({ item }) => {
          if (item.type === 'header') {
            return <SectionHeader title={item.title} />;
          }
          return (
            <TaskItem
              task={item.task}
              onToggle={() => toggleComplete(item.task.id)}
              onPress={() => router.push(`/task/${item.task.id}`)}
            />
          );
        }}
        ListEmptyComponent={
          <EmptyState
            icon="checkmark-done"
            title="All caught up!"
            message="No tasks for today"
          />
        }
      />
      
      <FAB 
        icon="add" 
        onPress={() => router.push('/task/new')} 
      />
    </View>
  );
}

Deliverables

  • āœ… Can create, edit, delete tasks
  • āœ… Tasks persist after app restart
  • āœ… Projects can be created and assigned
  • āœ… Today view shows correct tasks
  • āœ… Filter and search work

Phase 3: Native Features

ā±ļø Estimated time: 4-6 hours

Objectives

  • Implement push notifications for reminders
  • Add haptic feedback throughout
  • Implement at least 2 additional native features

Tasks

3.1 Push Notifications

// services/notifications.ts
import * as Notifications from 'expo-notifications';
import { Task } from '@/types';

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

export async function scheduleTaskReminder(task: Task) {
  if (!task.reminderDate) return;

  await Notifications.scheduleNotificationAsync({
    content: {
      title: 'Task Reminder',
      body: task.title,
      data: { taskId: task.id },
    },
    trigger: {
      date: task.reminderDate,
    },
  });
}

export async function cancelTaskReminder(taskId: string) {
  const notifications = await Notifications.getAllScheduledNotificationsAsync();
  const notification = notifications.find(
    (n) => n.content.data?.taskId === taskId
  );
  
  if (notification) {
    await Notifications.cancelScheduledNotificationAsync(notification.identifier);
  }
}

export async function requestPermissions() {
  const { status } = await Notifications.requestPermissionsAsync();
  return status === 'granted';
}

3.2 Biometric Lock (Optional Feature)

// hooks/useBiometricAuth.ts
import * as LocalAuthentication from 'expo-local-authentication';
import { useState, useEffect } from 'react';

export function useBiometricAuth() {
  const [isAvailable, setIsAvailable] = useState(false);
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  useEffect(() => {
    checkAvailability();
  }, []);

  async function checkAvailability() {
    const compatible = await LocalAuthentication.hasHardwareAsync();
    const enrolled = await LocalAuthentication.isEnrolledAsync();
    setIsAvailable(compatible && enrolled);
  }

  async function authenticate() {
    const result = await LocalAuthentication.authenticateAsync({
      promptMessage: 'Authenticate to access TaskFlow',
      fallbackLabel: 'Use passcode',
    });
    
    setIsAuthenticated(result.success);
    return result.success;
  }

  return { isAvailable, isAuthenticated, authenticate };
}

3.3 Share Tasks

// utils/share.ts
import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';
import { Task } from '@/types';

export async function shareTask(task: Task) {
  const content = `
šŸ“‹ ${task.title}
${task.description ? `\n${task.description}` : ''}
${task.dueDate ? `\nšŸ“… Due: ${formatDate(task.dueDate)}` : ''}
${task.priority !== 'medium' ? `\n⚔ Priority: ${task.priority}` : ''}
  `.trim();

  await Share.share({
    message: content,
    title: task.title,
  });
}

export async function exportTasksToFile(tasks: Task[]) {
  const json = JSON.stringify(tasks, null, 2);
  const path = `${FileSystem.documentDirectory}taskflow-export.json`;
  
  await FileSystem.writeAsStringAsync(path, json);
  
  if (await Sharing.isAvailableAsync()) {
    await Sharing.shareAsync(path);
  }
}

Native Features Checklist

  • āœ… Push notifications for task reminders
  • āœ… Haptic feedback on interactions
  • Choose 2+ from:
    • Biometric authentication
    • Share functionality
    • File export/import
    • Camera for task attachments
    • Location-based reminders

Phase 4: Polish & Animation

ā±ļø Estimated time: 4-6 hours

Objectives

  • Add meaningful animations throughout
  • Implement gesture-based interactions
  • Polish UI details and transitions

Required Animations

4.1 Swipe to Complete/Delete

// components/tasks/SwipeableTaskItem.tsx
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  runOnJS,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

export function SwipeableTaskItem({ task, onComplete, onDelete }) {
  const translateX = useSharedValue(0);
  const SWIPE_THRESHOLD = 100;

  const gesture = Gesture.Pan()
    .onUpdate((e) => {
      translateX.value = e.translationX;
    })
    .onEnd((e) => {
      if (e.translationX > SWIPE_THRESHOLD) {
        // Swipe right = complete
        translateX.value = withSpring(200, {}, () => {
          runOnJS(onComplete)();
        });
      } else if (e.translationX < -SWIPE_THRESHOLD) {
        // Swipe left = delete
        translateX.value = withSpring(-200, {}, () => {
          runOnJS(onDelete)();
        });
      } else {
        translateX.value = withSpring(0);
      }
    });

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

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

4.2 Layout Animations

// Use Reanimated Layout Animations
import Animated, { 
  FadeIn, 
  FadeOut, 
  Layout 
} from 'react-native-reanimated';

// In your list:
<Animated.View
  entering={FadeIn.duration(300)}
  exiting={FadeOut.duration(200)}
  layout={Layout.springify()}
>
  <TaskItem task={task} />
</Animated.View>

4.3 Micro-interactions

// Checkbox animation
const scale = useSharedValue(1);

const handlePress = () => {
  scale.value = withSequence(
    withSpring(0.8),
    withSpring(1.2),
    withSpring(1)
  );
  onToggle();
};

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

// FAB press animation
const fabScale = useSharedValue(1);

const fabGesture = Gesture.Tap()
  .onBegin(() => {
    fabScale.value = withSpring(0.9);
  })
  .onFinalize(() => {
    fabScale.value = withSpring(1);
  });

Animation Checklist

  • āœ… Swipe gestures on task items
  • āœ… List item enter/exit animations
  • āœ… Checkbox completion animation
  • āœ… Screen transitions
  • āœ… Loading states with skeletons
  • āœ… Pull-to-refresh animation

Phase 5: Testing

ā±ļø Estimated time: 4-6 hours

Objectives

  • Write unit tests for stores and utilities
  • Write component tests for key components
  • Achieve >60% code coverage

Test Examples

Store Tests

// __tests__/stores/taskStore.test.ts
import { useTaskStore } from '@/stores/taskStore';

describe('TaskStore', () => {
  beforeEach(() => {
    useTaskStore.setState({ tasks: [] });
  });

  it('adds a task', () => {
    const { addTask, tasks } = useTaskStore.getState();
    
    addTask({
      title: 'Test Task',
      priority: 'medium',
      completed: false,
      tags: [],
      subtasks: [],
    });

    expect(useTaskStore.getState().tasks).toHaveLength(1);
    expect(useTaskStore.getState().tasks[0].title).toBe('Test Task');
  });

  it('toggles task completion', () => {
    useTaskStore.setState({
      tasks: [{
        id: '1',
        title: 'Test',
        completed: false,
        priority: 'medium',
        tags: [],
        subtasks: [],
        createdAt: new Date(),
        updatedAt: new Date(),
      }],
    });

    useTaskStore.getState().toggleComplete('1');
    
    expect(useTaskStore.getState().tasks[0].completed).toBe(true);
  });

  it('filters today tasks correctly', () => {
    const today = new Date();
    const tomorrow = new Date(today);
    tomorrow.setDate(tomorrow.getDate() + 1);

    useTaskStore.setState({
      tasks: [
        { id: '1', title: 'Today', dueDate: today, ...baseTask },
        { id: '2', title: 'Tomorrow', dueDate: tomorrow, ...baseTask },
      ],
    });

    const todayTasks = useTaskStore.getState().getTodayTasks();
    
    expect(todayTasks).toHaveLength(1);
    expect(todayTasks[0].title).toBe('Today');
  });
});

Component Tests

// __tests__/components/TaskItem.test.tsx
import { render, fireEvent } from '@testing-library/react-native';
import { TaskItem } from '@/components/tasks/TaskItem';

describe('TaskItem', () => {
  const mockTask = {
    id: '1',
    title: 'Test Task',
    completed: false,
    priority: 'high' as const,
  };

  it('renders task title', () => {
    const { getByText } = render(
      <TaskItem task={mockTask} onToggle={jest.fn()} onPress={jest.fn()} />
    );
    
    expect(getByText('Test Task')).toBeTruthy();
  });

  it('calls onToggle when checkbox pressed', () => {
    const onToggle = jest.fn();
    const { getByTestId } = render(
      <TaskItem task={mockTask} onToggle={onToggle} onPress={jest.fn()} />
    );
    
    fireEvent.press(getByTestId('checkbox'));
    
    expect(onToggle).toHaveBeenCalled();
  });

  it('shows strikethrough when completed', () => {
    const completedTask = { ...mockTask, completed: true };
    const { getByText } = render(
      <TaskItem task={completedTask} onToggle={jest.fn()} onPress={jest.fn()} />
    );
    
    const title = getByText('Test Task');
    expect(title.props.style).toContainEqual(
      expect.objectContaining({ textDecorationLine: 'line-through' })
    );
  });
});

Coverage Requirements

  • āœ… Stores: >80% coverage
  • āœ… Utilities: >80% coverage
  • āœ… Components: >50% coverage
  • āœ… Overall: >60% coverage

Phase 6: Production & Deployment

ā±ļø Estimated time: 4-6 hours

Objectives

  • Configure app for production
  • Set up EAS Build and Submit
  • Configure crash reporting
  • Prepare store listings

Production Checklist

6.1 App Configuration

// app.config.ts
export default {
  expo: {
    name: 'TaskFlow',
    slug: 'taskflow',
    version: '1.0.0',
    orientation: 'portrait',
    icon: './assets/icon.png',
    splash: {
      image: './assets/splash.png',
      resizeMode: 'contain',
      backgroundColor: '#6366F1',
    },
    ios: {
      bundleIdentifier: 'com.yourname.taskflow',
      buildNumber: '1',
      supportsTablet: true,
    },
    android: {
      package: 'com.yourname.taskflow',
      versionCode: 1,
      adaptiveIcon: {
        foregroundImage: './assets/adaptive-icon.png',
        backgroundColor: '#6366F1',
      },
    },
    plugins: [
      'expo-router',
      [
        'expo-notifications',
        {
          icon: './assets/notification-icon.png',
          color: '#6366F1',
        },
      ],
      '@sentry/react-native/expo',
    ],
    extra: {
      eas: {
        projectId: 'your-project-id',
      },
    },
  },
};

6.2 EAS Configuration

// eas.json
{
  "cli": {
    "version": ">= 7.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "channel": "development"
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview"
    },
    "production": {
      "channel": "production",
      "autoIncrement": true
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "your@email.com",
        "ascAppId": "your-app-id"
      },
      "android": {
        "serviceAccountKeyPath": "./google-play-key.json",
        "track": "internal"
      }
    }
  }
}

6.3 Sentry Integration

// App.tsx
import * as Sentry from '@sentry/react-native';

Sentry.init({
  dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
  enabled: !__DEV__,
  tracesSampleRate: 0.2,
});

export default Sentry.wrap(App);

Deployment Steps

  1. Run final tests: npm test
  2. Build preview: eas build --profile preview
  3. Test on real devices
  4. Build production: eas build --profile production
  5. Submit to stores: eas submit --platform all

Evaluation Criteria

Category Weight Criteria
Functionality 25% All required features work correctly
Code Quality 20% Clean, typed, well-organized code
UI/UX 20% Polished, intuitive, platform-appropriate
Animations 10% Smooth, meaningful animations
Testing 15% >60% coverage, quality tests
Production Ready 10% Proper config, error handling, monitoring

Grading Scale

A (90-100%)

Exceeds expectations with bonus features

B (80-89%)

Meets all requirements with polish

C (70-79%)

Meets core requirements

D (60-69%)

Missing some requirements

Resources & Tips

Helpful Resources

Pro Tips

šŸ’” Development Tips

  • Start with the data model and store before UI
  • Build and test features incrementally
  • Use the Expo Go app for rapid development
  • Test on real devices early and often
  • Write tests as you build, not after

āš ļø Common Pitfalls

  • Don't over-engineer — start simple
  • Don't forget dark mode styling
  • Don't skip error handling
  • Don't ignore TypeScript errors
  • Don't wait until the end to test on devices

Timeline Suggestion

Week Focus
Week 1 Phase 1 + Phase 2 (Setup + Core Features)
Week 2 Phase 3 + Phase 4 (Native + Polish)
Week 3 Phase 5 + Phase 6 (Testing + Deployment)

šŸŽ‰ Good Luck!

This capstone project is your opportunity to demonstrate everything you've learned. Take your time, focus on quality, and build something you're proud of. You've got this!