Skip to main content
Papillon is built with React Native and Expo, following a modular architecture that separates concerns between UI, business logic, data persistence, and external service integrations.

Project Structure

Here’s an overview of the main directories in the Papillon codebase:
Papillon/
├── app/                    # Expo Router screens and layouts
│   ├── (tabs)/            # Bottom tab navigation screens
│   ├── (settings)/        # Settings screens
│   ├── (onboarding)/      # Onboarding flow
│   ├── (features)/        # Feature-specific screens
│   ├── (modals)/          # Modal screens
│   └── _layout.tsx        # Root layout
├── services/              # External service integrations
│   ├── pronote/           # PRONOTE integration
│   ├── skolengo/          # Skolengo integration
│   ├── ecoledirecte/      # EcoleDirecte integration
│   └── shared/            # Shared types and interfaces
├── database/              # WatermelonDB schemas and hooks
│   ├── models/            # Database models
│   ├── mappers/           # API to DB mappers
│   └── schema.ts          # Database schema
├── stores/                # Zustand state management
│   ├── account/           # Account state
│   ├── settings/          # App settings
│   ├── global/            # Global app state
│   └── ...
├── components/            # Reusable React components
├── ui/                    # UI component library (Papillon UI)
│   ├── components/        # Base UI components
│   ├── hooks/             # UI-specific hooks
│   └── utils/             # UI utilities
├── utils/                 # Utility functions
├── constants/             # App constants
├── hooks/                 # Custom React hooks
├── locales/               # i18n translations
└── assets/                # Images, fonts, and other assets

Core Concepts

1. Expo Router (File-based Routing)

Papillon uses Expo Router for navigation, which provides file-based routing similar to Next.js:
// app/(tabs)/index.tsx → Home screen
// app/(settings)/profile.tsx → Profile settings
// app/[id].tsx → Dynamic route with ID parameter
The app/ directory structure defines your navigation:
  • (tabs): Bottom tab navigation
  • (settings): Settings stack
  • (modals): Modal presentations
  • _layout.tsx: Layout wrappers
Expo Router uses typed routes (configured in app.config.ts) for type-safe navigation.

2. Service Architecture

Papillon supports multiple school management systems through a plugin-based service architecture.

Service Plugin Interface

All services implement the SchoolServicePlugin interface:
export interface SchoolServicePlugin {
  displayName: string;              // User-facing name
  service: Services;                // Service identifier enum
  capabilities: Capabilities[];     // Supported features
  authData: Auth;                   // Authentication data
  session: any;                     // Service-specific session

  // Core methods
  refreshAccount: (credentials: Auth) => Promise<ServicePlugin>;
  
  // Optional feature methods
  getHomeworks?: (weekNumber: number) => Promise<Homework[]>;
  getNews?: () => Promise<News[]>;
  getGradesForPeriod?: (period: Period) => Promise<PeriodGrades>;
  getWeeklyTimetable?: (weekNumber: number, date: Date) => Promise<CourseDay[]>;
  // ... more optional methods
}

Capabilities System

Each service declares its capabilities:
export enum Capabilities {
  REFRESH,                // Can refresh session
  HOMEWORK,               // Supports homework
  NEWS,                   // Supports news/announcements
  GRADES,                 // Supports grades
  ATTENDANCE,             // Supports attendance
  CANTEEN_MENU,          // Supports canteen menus
  CHAT_READ,             // Can read messages
  CHAT_CREATE,           // Can create new chats
  CHAT_REPLY,            // Can reply to messages
  TIMETABLE,             // Supports timetable
  // ... more capabilities
}

Example: PRONOTE Service

export class Pronote implements SchoolServicePlugin {
  displayName = "PRONOTE";
  service = Services.PRONOTE;
  capabilities: Capabilities[] = [Capabilities.REFRESH];
  session: SessionHandle | undefined;
  authData: Auth = {};

  async refreshAccount(credentials: Auth): Promise<Pronote> {
    const refresh = await refreshPronoteAccount(this.accountId, credentials);
    this.authData = refresh.auth;
    this.session = refresh.session;
    
    // Add capabilities based on user permissions
    for (const tab of this.session.user.authorizations.tabs) {
      // Map tabs to capabilities
    }
    
    return this;
  }

  async getHomeworks(weekNumber: number): Promise<Homework[]> {
    await this.checkTokenValidty();
    if (this.session) {
      return fetchPronoteHomeworks(this.session, this.accountId, weekNumber);
    }
    error("Session is not valid", "Pronote.getHomeworks");
  }
  
  // ... more methods
}

3. Database Layer (WatermelonDB)

Papillon uses WatermelonDB for local data persistence with reactive queries.

Database Schema

Schemas are defined in database/schema.ts:
export const schema = appSchema({
  version: 1,
  tables: [
    tableSchema({
      name: 'homeworks',
      columns: [
        { name: 'account_id', type: 'string', isIndexed: true },
        { name: 'subject_name', type: 'string' },
        { name: 'description', type: 'string' },
        { name: 'due_date', type: 'number' },
        { name: 'done', type: 'boolean' },
        // ... more columns
      ]
    }),
    // ... more tables
  ]
});

Database Hooks

Custom hooks provide reactive access to data:
// database/useHomework.ts
export const useHomework = (accountId: string, weekNumber: number) => {
  const database = useDatabase();
  const [homeworks, setHomeworks] = useState<Homework[]>([]);
  
  useEffect(() => {
    const subscription = database
      .get('homeworks')
      .query(Q.where('account_id', accountId))
      .observe()
      .subscribe(setHomeworks);
      
    return () => subscription.unsubscribe();
  }, [accountId]);
  
  return homeworks;
};

Data Flow

  1. Fetch from API: Service methods retrieve data from external APIs
  2. Map to Database: Mapper functions convert API responses to DB models
  3. Store in DB: Data is persisted using WatermelonDB
  4. Reactive Queries: Components subscribe to database changes
  5. Auto-update UI: UI updates automatically when data changes

4. State Management (Zustand)

Papillon uses Zustand for global state management.

Account Store Example

// stores/account/index.ts
import { create } from 'zustand';

interface AccountState {
  accounts: Account[];
  currentAccount: Account | null;
  addAccount: (account: Account) => void;
  removeAccount: (id: string) => void;
  switchAccount: (id: string) => void;
}

export const useAccountStore = create<AccountState>((set) => ({
  accounts: [],
  currentAccount: null,
  
  addAccount: (account) => 
    set((state) => ({ 
      accounts: [...state.accounts, account] 
    })),
    
  removeAccount: (id) => 
    set((state) => ({ 
      accounts: state.accounts.filter(a => a.id !== id) 
    })),
    
  switchAccount: (id) => 
    set((state) => ({ 
      currentAccount: state.accounts.find(a => a.id === id) 
    })),
}));

Usage in Components

import { useAccountStore } from '@/stores/account';

function AccountSelector() {
  const { accounts, currentAccount, switchAccount } = useAccountStore();
  
  return (
    <View>
      {accounts.map(account => (
        <Pressable 
          key={account.id}
          onPress={() => switchAccount(account.id)}
        >
          <Text>{account.name}</Text>
        </Pressable>
      ))}
    </View>
  );
}

5. UI Components (Papillon UI)

The ui/ directory contains Papillon’s design system.

Component Structure

// ui/components/Button.tsx
import { Pressable, Text } from 'react-native';
import { useTheme } from '@/ui/hooks/useTheme';

export function Button({ title, onPress, variant = 'primary' }) {
  const theme = useTheme();
  
  return (
    <Pressable 
      style={[styles.button, { backgroundColor: theme.colors[variant] }]}
      onPress={onPress}
    >
      <Text style={styles.text}>{title}</Text>
    </Pressable>
  );
}
Papillon UI is a separate package that can be used independently. See the Papillon UI documentation for more details.

Data Flow Example

Here’s how data flows through the app when fetching homework:
1

User requests homework

Component calls useHomework(accountId, weekNumber) hook
2

Check cache

Hook queries WatermelonDB for cached homework
3

Fetch from service if needed

If cache is stale, fetch from service:
const service = getService(account.service); // e.g., Pronote
const homework = await service.getHomeworks(weekNumber);
4

Map and store

Mapper converts API response to database models:
const mapped = homework.map(hw => mapHomeworkToDB(hw, accountId));
await database.batch(...mapped.map(hw => hw.prepareCreate()));
5

UI updates automatically

WatermelonDB notifies observers, React components re-render with new data

Key Technologies

React Native & Expo

  • React Native: 0.81.5 with New Architecture enabled
  • Expo SDK: ~54.0.32
  • Expo Router: File-based routing with typed routes
  • Expo Modules: Native module development

Database & State

  • WatermelonDB: High-performance reactive database
  • Zustand: Lightweight state management
  • MMKV: Fast key-value storage

UI & Animation

  • React Native Reanimated: 60 FPS animations
  • React Native Skia: Advanced graphics
  • React Native Gesture Handler: Native gesture handling
  • Lucide React Native: Icon library

Development Tools

  • TypeScript: Strict mode enabled
  • ESLint: Code linting with custom rules
  • Prettier: Code formatting
  • Jest: Testing framework

Best Practices

Import Organization

Imports are automatically sorted by eslint-plugin-simple-import-sort:
// 1. External dependencies
import { View, Text } from 'react-native';
import { useEffect, useState } from 'react';

// 2. Internal imports with @ alias
import { Pronote } from '@/services/pronote';
import { useAccountStore } from '@/stores/account';
import { Button } from '@/ui/components/Button';

// 3. Relative imports
import { helper } from './helper';

Error Handling

Use the logger utility instead of console:
import { error, warn, info } from '@/utils/logger/logger';

error('Session is not valid', 'Pronote.getHomeworks');
warn('Cache is stale, refetching');
info('Account refreshed successfully');

Type Safety

  • Use TypeScript strict mode
  • Define interfaces for all data structures
  • Use type guards for runtime checks
  • Leverage Expo Router’s typed routes

Performance Considerations

Optimization Strategies

  1. Lazy Loading: Use React.lazy() and Suspense for code splitting
  2. Memoization: Use React.memo, useMemo, and useCallback appropriately
  3. FlashList: Use @shopify/flash-list instead of FlatList for long lists
  4. Reanimated: Keep animations on UI thread with Reanimated worklets
  5. Database Indexing: Index frequently queried columns in WatermelonDB

React Native New Architecture

Papillon uses React Native’s New Architecture:
  • TurboModules: Faster native module initialization
  • Fabric: New rendering system
  • JSI: Direct JavaScript-to-native communication
The New Architecture is enabled in app.config.ts with newArchEnabled: true.

Next Steps

Build docs developers (and LLMs) love