bceao_pispi_app 0.0.5
bceao_pispi_app: ^0.0.5 copied to clipboard
SDK Flutter 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) d [...]
bceao_pispi_app #
SDK Flutter 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, fermer le
compte), 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.
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.
import 'package:bceao_pispi_app/bceao_pispi_app.dart';
final config = PiSpiSdkConfig(
user: PiSpiUserIdentity(
firstName: user.firstName,
lastName: user.lastName,
fullName: user.fullName,
// Voir PiSpiUserIdentity.supportedPaysCodes pour les contraintes
// (categorie: P/C, genre: 1/2, pays: code UEMOA).
categorie: user.categorie,
nationalite: user.nationalite,
paysResidence: user.paysResidence,
genre: user.genre,
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: [PiSpiCompte(numero: user.accountNumber, libelle: 'Courant', solde: user.solde)],
),
api: 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 {
Navigator.of(context).popUntil((r) => r.isFirst);
monAuthService.redirectToLogin();
},
),
appConfig: 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: Locale(monAuthService.langueActuelle), // ex. 'fr'
),
identification: 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 {
Navigator.of(context).popUntil((r) => r.isFirst);
},
);
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => PiSpiSdkEntry(config: config),
));
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 s'y replie sur firstName+lastName si vide). |
categorie |
String |
oui | "P" (personne physique) ou "C" (commerçant). Toute autre valeur déclenche une assertion. |
nationalite |
String |
oui | Code pays de nationalité du client. |
paysResidence |
String |
oui | Code pays de résidence du client. |
genre |
String |
oui | "1" (masculin) ou "2" (féminin). Toute autre valeur déclenche une assertion. |
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 déclenche une assertion. |
comptes |
List<PiSpiCompte> |
oui | Comptes du client, jamais null ni 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. numero de compte bancaire, numero de téléphone EME ou tout autre identifiant de compte du client). |
libelle |
String |
Libellé affiché à l'utilisateur (ex. "Courant", "Épargne"). |
solde |
double |
Solde du compte, affiché dans l'écran de sélection. |
Comptes multiples (PiSpiUserIdentity.comptes) #
comptes ne doit jamais être null ni vide : c'est la liste des
comptes bancaires du client.
PiSpiUserIdentity(
// ...
comptes: [
PiSpiCompte(numero: 'SN0011...431', libelle: 'Courant', solde: 9240320),
PiSpiCompte(numero: 'SN0011...432', libelle: 'Chèque', solde: 240000),
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.first.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) — ce sont les seules langues gérées par
AppLocalizations. Passer une autre langue à defaultLocale déclenche une
assertion en debug (PiSpiAppConfig.supportedLocaleCodes), et se replie
sur la locale du delegate le plus proche en release.
Le SDK possède son propre AppLocalizations (fr/en/pt). Pour que les
textes s'affichent, ajoutez son delegate à votre propre MaterialApp :
MaterialApp(
localizationsDelegates: [
...GlobalMaterialLocalizations.delegates,
AppLocalizations.delegate, // celui exporté par ce package
],
supportedLocales: AppLocalizations.supportedLocales,
// ...
)
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 locale ambiante de
l'application hôte. C'est à l'hôte de gérer son propre paramétrage de
langue et de transmettre la valeur actuelle :
PiSpiSdkConfig(
// ...
appConfig: PiSpiAppConfig(
// ...
defaultLocale: Locale(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 (null), la
préférence locale actuelle est conservée telle quelle (ou Themes.light
si aucune n'existe encore).
PiSpiSdkConfig(
// ...
appConfig: 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.
PiSpiSdkConfig(
// ...
appConfig: 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 7 thèmes (Themes.system, .light, .dark, .yellow,
.green, .blue, .red) — 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. |
Permissions natives (Android / iOS) #
Ce SDK est un package Dart pur : il ne peut pas déclarer ses propres permissions natives, contrairement à un plugin. C'est à chaque application hôte de les ajouter elle-même, sinon les fonctionnalités correspondantes (scan QR, contacts, géolocalisation...) échoueront silencieusement ou seront refusées par l'OS sans qu'aucune boîte de dialogue de permission n'apparaisse.
Android — dans android/app/src/main/AndroidManifest.xml :
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<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/Runner/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>
Voir example/android/app/src/main/AndroidManifest.xml et
example/ios/Runner/Info.plist pour une version prête à copier.
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 retour de fonds, une annulation, supprimer un alias, fermer
le compte). Si le callback renvoie false, l'action est simplement
annulée.
requireStepUpIdentification: () async {
// Remplacez par votre propre vérification (écran PIN, local_auth...).
final 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 PiSpiIdentificationConfig.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, 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 :
// Dans le handler de message push de l'hôte (ex. FirebaseMessaging.onMessage) :
FirebaseMessaging.onMessage.listen((message) {
PiSpiPush.handleIncoming(
message.data, // notificationId, idObject, type, montant, clientAlias, ...
title: message.notification?.title,
body: message.notification?.body,
);
});
handleIncoming republie l'événement dans le bus interne du SDK, ce qui
déclenche le même comportement (toast en app, 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 message.data
dépend du type de notification à l'origine du push : voir le schéma
NotificationPushData
de l'openapi du SDK.
Thème #
Le SDK n'impose pas son propre MaterialApp : il hérite du Theme et de
la Localizations ambiants de l'hôte, et surcharge uniquement le Theme
localement pour respecter la préférence clair/sombre choisie dans les
paramètres 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 (ConfigKey.introductionPassed) 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é, même si l'utilisateur revient sur
l'app.
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 (package:logger), configurable via
PiSpiLogConfig.logLevel : "debug", "trace", "info" (défaut),
"warning", "error", "fatal" ou "off". Une valeur inconnue équivaut
à tout journaliser.
PiSpiSdkConfig(
// ...
log: 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.
onFlutterError #
Optionnel : callback appelé pour chaque erreur Flutter interceptée pendant que le SDK est affiché, pour que l'hôte la remonte à son propre outil de crash reporting (Crashlytics, Sentry...).
PiSpiSdkConfig(
// ...
log: PiSpiLogConfig(
onFlutterError: (details) {
MyCrashReporter.recordFlutterError(details);
},
),
)
Ce callback ne fait que transmettre l'erreur : il n'écrase jamais un
gestionnaire FlutterError.onError déjà défini par l'hôte (celui-ci
continue d'être appelé juste après), et le SDK n'affiche aucun écran
d'erreur ni ne quitte l'application à sa place — contrairement à l'app de
référence, 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, ou vient de clôturer son compte. Il n'y a simplement plus rien à afficher.
Si onClose n'est pas fourni, le SDK utilise api.onSessionExpired à sa
place (comportement historique, quand un seul callback existait pour les
deux cas). 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 #
flutter pub get
flutter analyze
flutter test
Le dossier example/ contient une application hôte factice qui pousse
PiSpiSdkEntry pour valider l'intégration de bout en bout.