Papillon supports multiple school management systems through a plugin-based service architecture. This guide explains how to add new service integrations and maintain existing ones.
Service Architecture Overview
Each service integration in Papillon follows a standardized plugin pattern that allows the app to work with different school management systems without coupling the UI to specific implementations.
Supported Services
Papillon currently supports:
PRONOTE - French school management system
Skolengo - Modern school platform
EcoleDirecte - French education portal
Multi (ESUP) - University platform
TurboSelf - Canteen management
ARD - Canteen payment system
Izly - Student payment card
Alise - Canteen balance system
Appscho - School management
Lannion - Local school system
Service Plugin Interface
All services must implement the SchoolServicePlugin interface defined in services/shared/types.ts.
Required Properties
export interface SchoolServicePlugin {
// Display name shown to users
displayName : string ;
// Service identifier from Services enum
service : Services ;
// List of supported capabilities
capabilities : Capabilities [];
// Authentication data
authData : Auth ;
// Service-specific session object
session : any ;
// Required: Refresh/initialize the service
refreshAccount : ( credentials : Auth ) => Promise < ServicePlugin >;
// Optional: Feature-specific 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
Capabilities define what features a service supports:
export enum Capabilities {
REFRESH , // Can refresh session
HOMEWORK , // Supports homework
NEWS , // Supports news/announcements
GRADES , // Supports grades
ATTENDANCE , // Supports attendance
ATTENDANCE_PERIODS , // Supports attendance periods
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
HAVE_KIDS , // Multi-student support (parent account)
CANTEEN_BALANCE , // Shows canteen balance
CANTEEN_HISTORY , // Shows transaction history
CANTEEN_BOOKINGS , // Supports meal booking
CANTEEN_QRCODE , // Supports QR code generation
}
Creating a New Service Integration
Create service directory
Create a new directory in services/ with your service name: mkdir services/my-service
Add to Services enum
In stores/account/types.ts, add your service to the Services enum: export enum Services {
PRONOTE = "pronote" ,
SKOLENGO = "skolengo" ,
MY_SERVICE = "my-service" , // Add this
// ...
}
Install API client (if needed)
If there’s an existing API client library: npm install my-service-api
Or create your own API client in the service directory.
Create the main service class
Create services/my-service/index.ts: import { SchoolServicePlugin , Capabilities } from "@/services/shared/types" ;
import { Auth , Services } from "@/stores/account/types" ;
export class MyService implements SchoolServicePlugin {
displayName = "My Service" ;
service = Services . MY_SERVICE ;
capabilities : Capabilities [] = [ Capabilities . REFRESH ];
session : any = undefined ;
authData : Auth = {};
constructor ( public accountId : string ) {}
async refreshAccount ( credentials : Auth ) : Promise < MyService > {
// 1. Authenticate with the service
// 2. Store the session
// 3. Determine capabilities based on user permissions
// 4. Return this
return this ;
}
}
Implement feature methods
Register the service
Add your service to the service registry in services/shared/index.ts: import { MyService } from "@/services/my-service" ;
export function getServiceInstance (
service : Services ,
accountId : string
) : SchoolServicePlugin {
switch ( service ) {
case Services . PRONOTE :
return new Pronote ( accountId );
case Services . MY_SERVICE :
return new MyService ( accountId );
// ...
}
}
Implementing Features
Each feature corresponds to a capability and one or more methods.
Homework
import { Homework } from "@/services/shared/homework" ;
export class MyService implements SchoolServicePlugin {
// ... other properties
async refreshAccount ( credentials : Auth ) : Promise < MyService > {
// After authentication, add capability
this . capabilities . push ( Capabilities . HOMEWORK );
return this ;
}
async getHomeworks ( weekNumber : number ) : Promise < Homework []> {
// 1. Check session validity
if ( ! this . session ) {
error ( "Session is not valid" , "MyService.getHomeworks" );
}
// 2. Fetch homework from API
const rawHomework = await this . session . getHomework ( weekNumber );
// 3. Map to Papillon's Homework interface
return rawHomework . map ( hw => ({
id: hw . id ,
createdByAccount: this . accountId ,
subject: hw . subject ,
description: hw . description ,
dueDate: new Date ( hw . dueDate ),
done: hw . completed ,
// ... more fields
}));
}
}
Grades
import { Period , PeriodGrades } from "@/services/shared/grade" ;
export class MyService implements SchoolServicePlugin {
async getGradesPeriods () : Promise < Period []> {
const periods = await this . session . getPeriods ();
return periods . map ( p => ({
id: p . id ,
name: p . name ,
startDate: new Date ( p . start ),
endDate: new Date ( p . end ),
}));
}
async getGradesForPeriod ( period : Period ) : Promise < PeriodGrades > {
const grades = await this . session . getGrades ( period . id );
return {
grades: grades . map ( g => ({
id: g . id ,
createdByAccount: this . accountId ,
subject: g . subject ,
description: g . description ,
grade: g . value ,
outOf: g . outOf ,
coefficient: g . coefficient ,
average: g . classAverage ,
date: new Date ( g . date ),
})),
averages: grades . subjects . map ( s => ({
subject: s . name ,
average: s . average ,
classAverage: s . classAverage ,
})),
};
}
}
import { CourseDay } from "@/services/shared/timetable" ;
export class MyService implements SchoolServicePlugin {
async getWeeklyTimetable (
weekNumber : number ,
date : Date
) : Promise < CourseDay []> {
const timetable = await this . session . getTimetable ( date );
return timetable . map ( day => ({
date: new Date ( day . date ),
courses: day . lessons . map ( lesson => ({
id: lesson . id ,
createdByAccount: this . accountId ,
subject: lesson . subject ,
teacher: lesson . teacher ,
room: lesson . room ,
startTime: new Date ( lesson . start ),
endTime: new Date ( lesson . end ),
status: lesson . cancelled ? "cancelled" : "confirmed" ,
// ... more fields
})),
}));
}
}
Chat/Messaging
import { Chat , Message , Recipient } from "@/services/shared/chat" ;
export class MyService implements SchoolServicePlugin {
async getChats () : Promise < Chat []> {
const conversations = await this . session . getConversations ();
return conversations . map ( conv => ({
id: conv . id ,
createdByAccount: this . accountId ,
subject: conv . subject ,
creator: conv . author ,
recipients: conv . recipients ,
lastMessage: conv . lastMessage ,
unread: conv . unreadCount > 0 ,
}));
}
async getChatMessages ( chat : Chat ) : Promise < Message []> {
const messages = await this . session . getMessages ( chat . id );
return messages . map ( msg => ({
id: msg . id ,
content: msg . body ,
author: msg . author ,
date: new Date ( msg . date ),
read: msg . read ,
}));
}
async sendMessageInChat ( chat : Chat , content : string ) : Promise < void > {
await this . session . sendMessage ( chat . id , content );
}
}
Data Mapping
Each service has unique data structures. Use mapper functions to convert to Papillon’s standard format.
Create Mappers
Create services/my-service/mappers.ts:
import { Homework } from "@/services/shared/homework" ;
export function mapHomework (
raw : any ,
accountId : string
) : Homework {
return {
id: raw . id ,
createdByAccount: accountId ,
subject: raw . subjectName ,
description: parseHtml ( raw . description ),
dueDate: new Date ( raw . deadline ),
done: raw . status === "completed" ,
// ... more fields
};
}
function parseHtml ( html : string ) : string {
// Remove HTML tags, convert to plain text
return html . replace ( /< [ ^ > ] * >/ g , "" );
}
Shared Types
Use the shared type definitions from services/shared/:
homework.ts - Homework interface
grade.ts - Grades and periods
timetable.ts - Courses and schedules
news.ts - Announcements
attendance.ts - Attendance records
chat.ts - Messages and conversations
canteen.ts - Canteen menus and bookings
Authentication & Session Management
Token Refresh
Implement automatic token refresh:
export class MyService implements SchoolServicePlugin {
tokenExpiration = new Date (). getTime () + ( 5 * 60 * 1000 ); // 5 min
private async checkTokenValidity () : Promise < boolean > {
const now = new Date (). getTime ();
if ( now > this . tokenExpiration ) {
// Refresh token
await this . refreshAccount ( this . authData );
this . tokenExpiration = now + ( 5 * 60 * 1000 );
return new Date (). getTime () <= this . tokenExpiration ;
}
return true ;
}
async getHomeworks ( weekNumber : number ) : Promise < Homework []> {
// Check token before each API call
await this . checkTokenValidity ();
if ( ! this . session ) {
error ( "Session is not valid" , "MyService.getHomeworks" );
}
// Fetch data...
}
}
Secure Storage
Store credentials securely:
import * as SecureStore from "expo-secure-store" ;
export async function saveCredentials (
accountId : string ,
credentials : Auth
) {
await SecureStore . setItemAsync (
`account_ ${ accountId } _auth` ,
JSON . stringify ( credentials )
);
}
export async function loadCredentials (
accountId : string
) : Promise < Auth | null > {
const json = await SecureStore . getItemAsync ( `account_ ${ accountId } _auth` );
return json ? JSON . parse ( json ) : null ;
}
Error Handling
Use the Logger
Never use console.log, use the logger utility:
import { error , warn , info } from "@/utils/logger/logger" ;
export class MyService implements SchoolServicePlugin {
async getHomeworks ( weekNumber : number ) : Promise < Homework []> {
try {
info ( `Fetching homework for week ${ weekNumber } ` , "MyService" );
const homework = await this . session . getHomework ( weekNumber );
return homework . map ( mapHomework );
} catch ( err ) {
error (
`Failed to fetch homework: ${ err . message } ` ,
"MyService.getHomeworks"
);
throw err ;
}
}
}
Custom Error Types
Define service-specific errors in services/errors/:
export class MyServiceError extends Error {
constructor (
message : string ,
public code : string ,
public statusCode ?: number
) {
super ( message );
this . name = "MyServiceError" ;
}
}
export class AuthenticationError extends MyServiceError {
constructor ( message : string ) {
super ( message , "AUTH_ERROR" , 401 );
}
}
Testing Your Service
Manual Testing
Add test credentials in dev mode
Try all features your service supports
Test error cases (invalid credentials, network errors)
Test on both iOS and Android
Check data persistence and caching
Unit Tests
Create tests in services/my-service/__tests__/:
import { MyService } from "../index" ;
describe ( "MyService" , () => {
let service : MyService ;
beforeEach (() => {
service = new MyService ( "test-account" );
});
it ( "should initialize with correct properties" , () => {
expect ( service . displayName ). toBe ( "My Service" );
expect ( service . service ). toBe ( Services . MY_SERVICE );
});
it ( "should fetch homework" , async () => {
// Mock the API
const mockHomework = [ ... ];
service . session = { getHomework: jest . fn (). mockResolvedValue ( mockHomework ) };
const homework = await service . getHomeworks ( 1 );
expect ( homework ). toHaveLength ( mockHomework . length );
});
});
Example: Complete Service Implementation
Here’s a complete minimal service implementation:
index.ts
refresh.ts
homework.ts
import { SchoolServicePlugin , Capabilities } from "@/services/shared/types" ;
import { Homework } from "@/services/shared/homework" ;
import { Auth , Services } from "@/stores/account/types" ;
import { error , info } from "@/utils/logger/logger" ;
import { MyServiceClient } from "my-service-api" ;
export class MyService implements SchoolServicePlugin {
displayName = "My Service" ;
service = Services . MY_SERVICE ;
capabilities : Capabilities [] = [ Capabilities . REFRESH ];
session : MyServiceClient | undefined ;
authData : Auth = {};
tokenExpiration = 0 ;
constructor ( public accountId : string ) {}
async refreshAccount ( credentials : Auth ) : Promise < MyService > {
info ( "Refreshing account" , "MyService" );
try {
// Authenticate
this . session = new MyServiceClient ();
await this . session . login ( credentials . username , credentials . password );
// Store auth data
this . authData = credentials ;
this . tokenExpiration = Date . now () + ( 30 * 60 * 1000 ); // 30 min
// Detect capabilities
if ( this . session . features . homework ) {
this . capabilities . push ( Capabilities . HOMEWORK );
}
if ( this . session . features . grades ) {
this . capabilities . push ( Capabilities . GRADES );
}
return this ;
} catch ( err ) {
error ( `Failed to refresh account: ${ err . message } ` , "MyService" );
throw err ;
}
}
private async checkToken () : Promise < void > {
if ( Date . now () > this . tokenExpiration ) {
await this . refreshAccount ( this . authData );
}
}
async getHomeworks ( weekNumber : number ) : Promise < Homework []> {
await this . checkToken ();
if ( ! this . session ) {
error ( "Session is not valid" , "MyService.getHomeworks" );
}
const homework = await this . session . getHomework ( weekNumber );
return homework . map ( hw => ({
id: hw . id ,
createdByAccount: this . accountId ,
subject: hw . subject ,
description: hw . description ,
dueDate: new Date ( hw . dueDate ),
done: hw . completed ,
}));
}
}
Best Practices
Keep services independent
Don’t import from other services
Use shared types from services/shared/
Each service should be self-contained
Handle API differences gracefully
Map all data to Papillon’s standard format
Handle missing fields with defaults
Document API quirks in comments
Implement defensive coding
Always validate API responses
Check session validity before calls
Handle network errors gracefully
Provide meaningful error messages
Performance considerations
Never log credentials or tokens
Use HTTPS for all requests
Validate SSL certificates
Store sensitive data in SecureStore
Next Steps
When adding a new service, open an issue first to discuss the integration approach with maintainers.