@pi-spi/app
v1.3.0
Published
SDK React Native permettant d'intégrer PI-SPI (transactions, demande d'annulation, retour de fond, demande de paiement, QR Code, gestion des alias, gestion des abonnements, notifications, profil, compte) dans une application mobile existante.
Keywords
Readme
@pi-spi/app
SDK React Native permettant d'intégrer PI-SPI (transactions, demande d'annulation, retour de fond, demande de paiement, QR Code, gestion des alias, gestion des abonnements, notifications, profil, compte) dans une application mobile existante, sans reprendre dans son ensemble l'app de référence dont le code source a été partagé avec tous les participants.
Aperçu

Documentation de l'API
Le SDK n'expose que le sous-ensemble de l'API PI-SPI dont il a réellement besoin (voir « Ce que le SDK ne fait pas » ci-dessous pour ce qui est volontairement hors périmètre). La référence complète — tous les endpoints, schémas de requête/réponse, exemples — est disponible ici :
developers.pi-bceao.com/api-reference/mob-sdk
Ce que le SDK ne fait pas
Contrairement à l'app de référence du code source partagé, ce SDK n'implémente aucune authentification :
- Pas de login, pas de Keycloak/PKCE
- Pas de vérification PIN/biométrique avant confirmation d'une transaction
- Pas de refresh de token (le SDK ne parle jamais à un IDP)
- Pas de fournisseur de notifications push (Firebase, APNs, ...) — voir la section Notifications push pour le point d'intégration fourni
Tout cela reste entièrement à la charge de l'application hôte. Le SDK fait confiance à l'appelant : si l'hôte affiche l'écran du SDK, c'est que l'utilisateur est authentifié et autorisé.
Le SDK ne stocke et ne vérifie aucun secret (ni PIN, ni empreinte) —
mais il garantit que la vérification a bien lieu : avant chaque action
sensible (confirmer une transaction, supprimer un alias...), il appelle
PiSpiIdentificationConfig.requireStepUpIdentification, qui délègue la
vérification à l'hôte et ne renvoie qu'un booléen. Voir la section
requireStepUpIdentification plus bas.
Installation
npm install @pi-spi/app
npx expo install @react-navigation/native @react-navigation/native-stack \
react-native-screens react-native-safe-area-context \
@react-native-async-storage/async-storage @expo/vector-icons \
expo-secure-store expo-crypto expo-network expo-audio expo-image \
expo-font expo-asset expo-video expo-contacts expo-location \
expo-clipboard react-native-svg expo-image-picker expo-file-system \
expo-print expo-sharing expo-camera react-native-view-shot \
react-native-webviewLe SDK s'appuie sur des modules Expo, déclarés en peerDependencies : il
fonctionne dans une app Expo (development build, pas Expo Go) comme dans
une app React Native « bare » où les modules Expo sont installés
(npx install-expo-modules@latest). npx expo install choisit les
versions compatibles avec votre SDK Expo (testé avec Expo 57 /
React Native 0.86).
Intégration
PiSpiSdkConfig regroupe ses champs par thème : api
(backend/authentification), appConfig (identité/préférences de l'app
hôte), identification (step-up) et log (journalisation, optionnel).
user et onClose restent au niveau racine. Chaque classe et chaque
champ sont documentés dans
src/core/pi_spi_sdk_config.ts.
import {
PiSpiApiConfig, PiSpiAppConfig, PiSpiCompte, PiSpiIdentificationConfig,
PiSpiSdkConfig, PiSpiSdkEntry, PiSpiUserIdentity,
} from '@pi-spi/app';
const config = new PiSpiSdkConfig({
user: new PiSpiUserIdentity({
firstName: user.firstName,
lastName: user.lastName,
fullName: user.fullName,
// Pays de domiciliation du compte : code UEMOA (voir
// PiSpiUserIdentity.supportedPaysCodes).
pays: user.pays,
avatar: user.avatarUrl,
// La liste des comptes du client — jamais vide, voir la section
// "Comptes multiples" plus bas. Un seul élément si le client n'a
// qu'un compte.
comptes: [new PiSpiCompte({ numero: user.accountNumber, libelle: 'Courant', solde: user.solde })],
}),
api: new PiSpiApiConfig({
apiBaseUrl: 'https://api.exemple-participant.com',
// Token valide au moment de l'ouverture du SDK.
accessToken: monAuthService.currentAccessToken,
// Appelé quand une requête échoue en 401/403 avec le token courant :
// l'hôte le rafraîchit et retourne le nouveau token (ou null s'il n'y
// parvient pas). Après 3 échecs, le SDK appelle onSessionExpired.
onAccessTokenExpired: async () => monAuthService.refreshAccessToken(),
// Appelé si onAccessTokenExpired ne parvient plus à fournir de token
// valide après 3 tentatives : une vraie erreur de session.
onSessionExpired: async () => {
closeSdk();
monAuthService.redirectToLogin();
},
}),
appConfig: new PiSpiAppConfig({
pspCodeMembre: 'SNB000',
// Langue d'affichage du SDK (fr/en/pt) : l'hôte gère son propre
// paramétrage de langue, le SDK n'en a pas. Voir "Localisation" plus bas.
defaultLocale: monAuthService.langueActuelle, // ex. 'fr'
}),
identification: new PiSpiIdentificationConfig({
// Appelé avant chaque action sensible : à vous de vérifier PIN/biométrie
// et de renvoyer true/false. Voir la section dédiée plus bas.
requireStepUpIdentification: async () => monAuthService.demanderConfirmation(),
}),
// Optionnel : appelé pour une fermeture volontaire du SDK (bouton retour
// sans écran derrière, bouton de fermeture de l'accueil...) — pas une
// erreur. Si omis, api.onSessionExpired est utilisé à la place. Voir
// "onClose vs onSessionExpired" plus bas.
onClose: async () => closeSdk(),
});
// Une fois l'utilisateur authentifié, affichez le SDK en plein écran :
{sdkOpen && <PiSpiSdkEntry config={config} />}Le SDK embarque sa propre navigation (un NavigationContainer
indépendant : il ne perturbe pas celui de l'hôte, même si l'hôte utilise
lui aussi React Navigation), son propre thème et ses propres traductions :
l'hôte n'a rien d'autre à déclarer.
Exemple d'intégration
Le package publié contient l'app hôte d'exemple, consultable depuis
l'onglet Code de la page npm (ou dans node_modules/@pi-spi/app/example/
une fois le package installé) :
example/App.tsx: une app hôte factice qui ouvre le SDK en mode démo, avec un ou plusieurs comptes, dans chacun des thèmes, qui simule un push (PiSpiPush.handleIncoming), un écran de vérification renforcée (requireStepUpIdentification), et qui gère le bouton retour Android ;example/app.json: la configuration Expo, avec les plugins et les messages des permissions natives.
Une application hôte d'exemple plus complète (connexion, bouton PI-SPI,
rafraîchissement du token, vérification renforcée biométrique avec
expo-local-authentication, notifications push) est disponible dans le
dépôt public mobile-app-sdk-exemple, dossier
react-native/.
Les logos π et PI-SPI utilisés sur les boutons de l'exemple, dans toutes leurs variantes de couleurs, sont disponibles dans mobile-app-flutter-sdk-assets/variante_logo_pi_et_pispi.
PiSpiUserIdentity
Identité de l'utilisateur connecté côté hôte. Le SDK ne gère aucune authentification lui-même : ces informations sont uniquement affichées (nom, avatar) ou utilisées pour construire les requêtes (numéro de compte). Aucune n'est vérifiée ni stockée par le SDK au-delà de la session en cours.
| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| firstName | string | oui | Prénom du client. |
| lastName | string | oui | Nom du client. |
| fullName | string | oui | Nom complet affiché dans l'UI du SDK (voir nomComplet(), qui se replie sur firstName + lastName si vide). |
| pays | string | oui | Code pays UEMOA de domiciliation du compte (pas nécessairement celui du client) : BJ, BF, CI, GW, ML, NE, SN ou TG (voir PiSpiUserIdentity.supportedPaysCodes). Toute autre valeur lève une erreur en développement. |
| comptes | PiSpiCompte[] | oui | Comptes du client, jamais vide — voir « Comptes multiples » plus bas. |
| avatar | string | non | URL de la photo de profil du client, affichée dans l'UI du SDK. |
| currentAccount | string | non | Compte actif par défaut avant toute sélection — voir « Comptes multiples » plus bas. |
PiSpiCompte (élément de comptes) :
| Champ | Type | Description |
| --- | --- | --- |
| numero | string | Numéro du compte (ex. numéro de compte bancaire, numéro de téléphone EME ou tout autre identifiant de compte du client). |
| libelle | string | Libellé affiché à l'utilisateur (ex. "Courant", "Épargne"). |
| solde | number | Solde du compte, affiché dans l'écran de sélection. |
Comptes multiples (PiSpiUserIdentity.comptes)
comptes ne doit jamais être vide : c'est la liste des comptes
bancaires du client.
new PiSpiUserIdentity({
// ...
comptes: [
new PiSpiCompte({ numero: 'SN0011...431', libelle: 'Courant', solde: 9240320 }),
new PiSpiCompte({ numero: 'SN0011...432', libelle: 'Chèque', solde: 240000 }),
new PiSpiCompte({ numero: 'SN0011...430', libelle: 'Épargne', solde: 32240320 }),
],
});Un seul compte : rien à faire de plus, il est utilisé directement — pas d'écran de sélection, pas de carrousel.
Plusieurs comptes : à chaque ouverture du SDK, un écran « Sélectionner un compte » s'affiche (juste après l'introduction/permissions au premier lancement, sinon directement) — le client choisit celui à utiliser pour cette session. Ce choix devient ensuite le compte actif de toute la session (alias PI-SPI recherché/créé pour lui, QR partagé, transactions envoyées...) jusqu'à la fermeture du SDK. Le compte actif peut aussi être changé sans rouvrir le SDK, via un carrousel horizontal sur la carte compte de l'accueil : swiper d'un compte à l'autre re-sélectionne immédiatement le compte actif ; si le compte affiché n'a pas encore d'alias PI-SPI, l'écran de création s'ouvre automatiquement pour lui (directement sur le choix du type d'alias — le compte est déjà connu, il n'est jamais redemandé).
currentAccount reste disponible, optionnel : c'est la valeur par défaut
avant que le client n'ait choisi un compte (non fourni, comptes[0].numero
est utilisé) — l'écran de sélection s'affiche tout de même à chaque
ouverture dès que comptes en contient plusieurs, quelle que soit la
valeur de currentAccount.
Localisation
Contrainte : le SDK ne peut être utilisé qu'en français, anglais ou
portugais ('fr', 'en', 'pt'). Passer une autre langue à
defaultLocale lève une erreur en développement
(PiSpiAppConfig.supportedLocaleCodes).
Le SDK n'a pas son propre sélecteur de langue dans ses écrans de
paramètres : c'est PiSpiAppConfig.defaultLocale qui détermine la langue
affichée ('fr' par défaut), indépendamment de la langue du téléphone.
C'est à l'hôte de gérer son propre paramétrage de langue et de transmettre
la valeur actuelle :
new PiSpiSdkConfig({
// ...
appConfig: new PiSpiAppConfig({
// ...
defaultLocale: monAuthService.langueActuelle, // 'fr', 'en' ou 'pt' uniquement
}),
});Si votre app hôte supporte d'autres langues, mappez-les vers la langue la
plus proche parmi fr/en/pt avant de les transmettre au SDK.
defaultTheme
Champ optionnel (PiSpiAppConfig.defaultTheme) : si vous le fournissez,
il est appliqué immédiatement et persisté comme nouveau thème par
défaut à chaque ouverture du SDK — y compris si le client avait
lui-même changé de thème depuis les écrans de paramètres du SDK lors
d'une session précédente. Si vous ne le fournissez pas, la préférence
locale actuelle est conservée telle quelle (ou Themes.light si aucune
n'existe encore).
import { Themes } from '@pi-spi/app';
new PiSpiSdkConfig({
// ...
appConfig: new PiSpiAppConfig({
// ...
defaultTheme: Themes.dark, // optionnel ; omettre pour ne rien imposer
}),
});Une fois le SDK ouvert, l'utilisateur reste libre de changer de thème
depuis ses écrans de paramètres — si vous l'y autorisez, voir
canChangeTheme ci-dessous ; ce choix persiste
jusqu'à la prochaine ouverture où l'hôte fournit lui-même une valeur.
canChangeTheme
Champ optionnel (PiSpiAppConfig.canChangeTheme) : autorise le client à
changer lui-même de thème depuis les écrans de paramètres du SDK. Tant
qu'il vaut false, l'entrée « Thème » est masquée dans les paramètres,
et le thème reste celui fixé par defaultTheme (ou la
préférence déjà persistée) pour toute la session.
Non fourni, sa valeur par défaut dépend de defaultTheme : false
si vous fournissez un defaultTheme (vous l'imposez alors pour la
session, cohérent avec defaultLocale), true sinon (vous n'avez
exprimé aucun avis sur le thème, donc autant laisser le client choisir).
Passez canChangeTheme explicitement pour découpler ce comportement de
defaultTheme.
new PiSpiSdkConfig({
// ...
appConfig: new PiSpiAppConfig({
// ...
defaultTheme: Themes.dark,
canChangeTheme: true, // sinon, comme defaultTheme est fourni, l'entrée « Thème » resterait masquée
}),
});La page d'accueil du SDK affiche toujours la variante avec QR code — il n'y a pas de choix de mise en page à configurer.
Thèmes disponibles
Le SDK propose 10 thèmes (Themes.system, .light, .dark, .yellow,
.green, .blue, .red, .orange, .purple, .pink) — voir
defaultTheme et canChangeTheme
ci-dessus pour les imposer ou laisser le client choisir depuis les écrans
de paramètres du SDK.
| Aperçu | Valeur | Description |
| --- | --- | --- |
|
| Themes.system | Suit le thème clair/sombre de l'appareil. Défaut si aucun thème n'est fourni ni déjà persisté. |
|
| Themes.light | Thème clair. |
|
| Themes.dark | Thème sombre. |
|
| Themes.yellow | Thème clair, accent jaune. |
|
| Themes.green | Thème clair, accent vert. |
|
| Themes.blue | Thème clair, accent bleu. |
|
| Themes.red | Thème clair, accent rouge. |
|
| Themes.orange | Thème clair, accent orange. |
|
| Themes.purple | Thème clair, accent violet. |
|
| Themes.pink | Thème clair, accent rose. |
Permissions natives (Android / iOS)
Le SDK ne déclare pas lui-même les permissions natives qu'il utilise : c'est à chaque application hôte de les ajouter, avec ses propres messages, sinon les fonctionnalités correspondantes (scan QR, contacts, géolocalisation...) seront refusées par l'OS.
App Expo — via les config plugins dans app.json :
"plugins": [
["expo-contacts", { "contactsPermission": "…pour envoyer de l'argent à un contact" }],
["expo-location", { "locationWhenInUsePermission": "…pour vérifier vos transactions et prévenir la fraude" }],
"expo-sharing",
["expo-camera", { "cameraPermission": "…pour scanner un QR code PI-SPI" }],
["expo-image-picker", {
"photosPermission": "…pour joindre un ticket de caisse ou importer un QR code",
"cameraPermission": "…pour photographier un ticket de caisse"
}]
]Voir example/app.json pour une version prête à copier.
App React Native « bare » — Android, dans
android/app/src/main/AndroidManifest.xml :
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.VIBRATE" />iOS, dans ios/<App>/Info.plist :
<key>NSCameraUsageDescription</key>
<string>...pour scanner les QR codes</string>
<key>NSContactsUsageDescription</key>
<string>...pour envoyer de l'argent à un contact</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>...pour vérifier vos transactions et prévenir la fraude</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>...pour importer un QR code depuis la galerie</string>VIBRATE sert aux notifications reçues pendant que le SDK est ouvert
(option « Vibrations » des paramètres) ; Expo l'ajoute par défaut.
La permission notification n'est demandée par aucun écran du SDK. Si
l'hôte a installé expo-notifications, le SDK s'en sert pour lire son
état ; sinon, il la considère simplement comme non accordée.
Bouton retour Android
Le SDK gère le bouton retour dans sa propre pile d'écrans. Quand il n'a
plus d'écran à dépiler, l'événement remonte à l'hôte : écoutez-le
(BackHandler) pour refermer le SDK, comme le fait example/App.tsx.
Évitez d'afficher le SDK dans un <Modal> React Native sur Android : le
bouton retour y déclenche onRequestClose au lieu de la navigation
interne du SDK.
requireStepUpIdentification
Le SDK appelle ce callback juste avant de dispatcher une action sensible
(confirmer une transaction, un envoi programmé, une demande de paiement
acceptée, un paiement partagé, un retour de fonds, une annulation,
supprimer un alias). Si le callback renvoie false, l'action est
simplement annulée.
requireStepUpIdentification: async () => {
// Remplacez par votre propre vérification (écran PIN,
// expo-local-authentication...).
const ok = await monAuthService.demanderPinOuBiometrie();
return ok;
},Le SDK ne connaît jamais le PIN ni la méthode utilisée : c'est
volontaire, pour ne pas dupliquer un mécanisme de sécurité que l'app hôte
possède déjà (voir confirmationMethodLabel
pour indiquer a posteriori la méthode employée). Ce callback est ce qui
garantit qu'aucune action sensible ne peut être confirmée sans step-up,
même si un développeur oublie de le faire lui-même avant d'appeler le SDK.
Notifications push
Le SDK n'embarque aucun fournisseur de push, ni la logique d'enregistrement
du token auprès du backend : c'est l'application hôte qui gère sa propre
configuration de push de bout en bout (Firebase, APNs,
expo-notifications, OneSignal...) — y compris l'enregistrement du token
auprès du backend PI-SPI. PiSpiPush ne fait qu'un pont entre un message
push déjà reçu par l'hôte et l'UI du SDK :
import { PiSpiPush } from '@pi-spi/app';
// Dans le handler de message push de l'hôte (ex. messaging().onMessage) :
messaging().onMessage(async (remoteMessage) => {
PiSpiPush.handleIncoming(
remoteMessage.data ?? {}, // notificationId, idObject, type, montant, clientAlias, ...
{ title: remoteMessage.notification?.title, body: remoteMessage.notification?.body },
);
});handleIncoming republie l'événement dans le bus interne du SDK, ce qui
déclenche le même comportement (bannière ou boîte dans l'app, son,
vibration, rafraîchissement de la liste, badge) que dans l'app de
référence — sans que le SDK ait besoin de savoir comment le push a été
reçu. Le contenu exact de data dépend du type de notification à
l'origine du push : voir le schéma
NotificationPushData
de l'openapi du SDK.
Première utilisation (introduction + permission contacts)
Comme dans l'app de référence, la toute première fois que l'utilisateur
ouvre le SDK (tant qu'il n'a jamais atteint la fin de l'introduction),
PiSpiSdkEntry affiche automatiquement, avant l'accueil :
- L'introduction (6 écrans vidéo swipeables) — le dernier écran a un
bouton « Continuer » (au lieu du bouton de connexion de l'app de
référence, puisque le SDK n'a pas de login) qui marque
ConfigKey.introductionPassedet enchaîne directement sur l'étape suivante. - La demande de permission contacts — explique pourquoi le SDK demande l'accès aux contacts avant de la solliciter réellement. Contrairement à l'app de référence, cette étape ne redemande pas la permission notification (le SDK n'a pas de fournisseur de push, voir Notifications push).
Cet état est persisté localement par le SDK lui-même : il n'y a rien à configurer côté hôte, et ce flow ne s'affiche plus jamais une fois passé.
Mode démo
PiSpiApiConfig.demoMode: true remplace tous les appels réseau par les
réponses de assets/json/ (les mêmes que le SDK Flutter). Utile pour
visualiser le SDK sans backend PI-SPI ; ne jamais l'activer en production.
confirmationMethodLabel
L'API persiste la méthode de confirmation utilisée pour une transaction
("pin", "fingerprint", ... dans l'app de référence). Le SDK n'ayant pas
connaissance de la politique de sécurité de l'hôte, il envoie la valeur de
PiSpiIdentificationConfig.confirmationMethodLabel (par défaut
"external"). Vérifiez avec l'équipe backend PI-SPI si ce champ a une
contrainte d'énumération avant mise en production.
logLevel
Niveau de journalisation du SDK, configurable via
PiSpiLogConfig.logLevel : "debug" (défaut), "trace", "info",
"warning", "error", "fatal" ou "off". Une valeur inconnue équivaut
à tout journaliser.
new PiSpiSdkConfig({
// ...
log: new PiSpiLogConfig({
logLevel: 'warning', // à réduire en production
}),
});Le SDK n'installe pas de crash reporting lui-même (voir « Ce que le SDK ne fait pas ») : ces logs sont uniquement destinés au débogage pendant l'intégration.
onError
Optionnel (équivalent de onFlutterError dans le SDK Flutter) : callback
appelé pour chaque erreur JS interceptée pendant que le SDK est affiché
(erreur de rendu d'un écran du SDK, ou erreur globale), pour que l'hôte la
remonte à son propre outil de crash reporting (Crashlytics, Sentry...).
new PiSpiSdkConfig({
// ...
log: new PiSpiLogConfig({
onError: (error, isFatal) => {
MyCrashReporter.recordError(error, isFatal);
},
}),
});Ce callback ne fait que transmettre l'erreur : il n'écrase jamais un gestionnaire global déjà défini par l'hôte (celui-ci continue d'être appelé juste après), et le SDK ne quitte jamais l'application à sa place — cela reste entièrement la décision de l'hôte.
onClose vs onSessionExpired
Le SDK rend la main à l'hôte dans deux situations bien différentes :
api.onSessionExpired(obligatoire) : une vraie erreur —api.onAccessTokenExpiredne parvient plus à fournir de token valide après 3 tentatives (401/403 persistant). L'hôte voudra probablement rediriger vers sa propre connexion.onClose(optionnel, au niveau racine dePiSpiSdkConfig) : une fermeture volontaire, sans erreur — le client a fermé le SDK depuis un bouton retour ou de fermeture. Il n'y a simplement plus rien à afficher.
Si onClose n'est pas fourni, le SDK utilise api.onSessionExpired à sa
place. Ne fournissez onClose que si vous voulez réagir différemment à
ces deux situations (typiquement : ne pas afficher un message « session
expirée » lors d'une fermeture volontaire).
Développement
npm install
npm run gen-l10n # régénère src/l10n/*.ts depuis src/l10n/intl_*.arb
npm run typecheck
npm test
cd example
npm install
npx expo run:android # ou run:ios (development build)Les traductions sont écrites dans les mêmes fichiers .arb que le SDK
Flutter (src/l10n/intl_fr.arb, intl_en.arb, intl_pt.arb) ; le script
scripts/gen-l10n.js en génère les fichiers TypeScript (ne pas les
modifier à la main).
Le dossier example/ contient une application hôte factice qui ouvre le
SDK en mode démo, avec un ou plusieurs comptes, dans chacun des thèmes, et
qui sait simuler un push, pour valider l'intégration de bout en bout. Elle
consomme directement les sources du SDK (../src, voir
example/metro.config.js).
