@pi-spi/app 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/example/App.tsx +470 -0
- package/example/app.json +62 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -136,6 +136,24 @@ indépendant : il ne perturbe pas celui de l'hôte, même si l'hôte utilise
|
|
|
136
136
|
lui aussi React Navigation), son propre thème et ses propres traductions :
|
|
137
137
|
l'hôte n'a rien d'autre à déclarer.
|
|
138
138
|
|
|
139
|
+
### Exemple d'intégration
|
|
140
|
+
|
|
141
|
+
Le package publié contient l'app hôte d'exemple, consultable depuis
|
|
142
|
+
l'onglet **Code** de la page npm (ou dans `node_modules/@pi-spi/app/example/`
|
|
143
|
+
une fois le package installé) :
|
|
144
|
+
|
|
145
|
+
- [`example/App.tsx`](example/App.tsx) : une app hôte factice qui ouvre le
|
|
146
|
+
SDK en mode démo, avec un ou plusieurs comptes, dans chacun des thèmes,
|
|
147
|
+
qui simule un push (`PiSpiPush.handleIncoming`), un écran de
|
|
148
|
+
vérification renforcée (`requireStepUpIdentification`), et qui gère le
|
|
149
|
+
bouton retour Android ;
|
|
150
|
+
- [`example/app.json`](example/app.json) : la configuration Expo, avec les
|
|
151
|
+
plugins et les messages des permissions natives.
|
|
152
|
+
|
|
153
|
+
Les logos π et PI-SPI utilisés sur les boutons de l'exemple, dans toutes
|
|
154
|
+
leurs variantes de couleurs, sont disponibles dans
|
|
155
|
+
[mobile-app-flutter-sdk-assets/variante_logo_pi_et_pispi](https://github.com/pisfn-sandbox/mobile-app-flutter-sdk-assets/tree/main/variante_logo_pi_et_pispi).
|
|
156
|
+
|
|
139
157
|
### `PiSpiUserIdentity`
|
|
140
158
|
|
|
141
159
|
Identité de l'utilisateur connecté côté hôte. Le SDK ne gère aucune
|
package/example/App.tsx
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { MaterialIcons } from '@expo/vector-icons';
|
|
2
|
+
import {
|
|
3
|
+
PiSpiApiConfig,
|
|
4
|
+
PiSpiAppConfig,
|
|
5
|
+
PiSpiCompte,
|
|
6
|
+
PiSpiIdentificationConfig,
|
|
7
|
+
PiSpiLogConfig,
|
|
8
|
+
PiSpiPush,
|
|
9
|
+
PiSpiSdkConfig,
|
|
10
|
+
PiSpiSdkEntry,
|
|
11
|
+
PiSpiUserIdentity,
|
|
12
|
+
Themes,
|
|
13
|
+
} from '@pi-spi/app';
|
|
14
|
+
import { StatusBar } from 'expo-status-bar';
|
|
15
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
ActivityIndicator,
|
|
18
|
+
Alert,
|
|
19
|
+
BackHandler,
|
|
20
|
+
Image,
|
|
21
|
+
Pressable,
|
|
22
|
+
ScrollView,
|
|
23
|
+
StyleSheet,
|
|
24
|
+
Text,
|
|
25
|
+
View,
|
|
26
|
+
type ImageSourcePropType,
|
|
27
|
+
} from 'react-native';
|
|
28
|
+
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Application hôte factice : simule une app participant déjà authentifiée,
|
|
32
|
+
* qui affiche l'écran du SDK PI-SPI par-dessus la sienne.
|
|
33
|
+
*
|
|
34
|
+
* **L'essentiel à lire pour intégrer le SDK dans une vraie app est la
|
|
35
|
+
* fonction `openPiSpiSdk` ci-dessous** : c'est le seul point d'entrée du
|
|
36
|
+
* SDK, tout le reste de ce fichier (tuiles, `StepUpAuthPage`, `PinKeypad`)
|
|
37
|
+
* n'est que du décor pour cette démo.
|
|
38
|
+
*
|
|
39
|
+
* Tourne en `demoMode: true` (aucune requête réelle, données mockées
|
|
40
|
+
* depuis `assets/json/` du SDK) pour pouvoir visualiser le SDK sans
|
|
41
|
+
* backend. Repassez `demoMode` à `false` et fournissez une vraie
|
|
42
|
+
* `apiBaseUrl` + un vrai token pour tester contre un backend PI-SPI réel.
|
|
43
|
+
*/
|
|
44
|
+
export default function FakeHostApp() {
|
|
45
|
+
return (
|
|
46
|
+
<SafeAreaProvider>
|
|
47
|
+
<FakeHostHomePage />
|
|
48
|
+
</SafeAreaProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Mode démo : aucune requête réelle n'est faite (API mockée), donc ni
|
|
53
|
+
// l'URL ni le token n'ont besoin d'être valides.
|
|
54
|
+
const API_BASE_URL = 'https://sandbox.pi-bceao.com/api';
|
|
55
|
+
const FAKE_ACCESS_TOKEN = 'DEMO_MODE_TOKEN';
|
|
56
|
+
|
|
57
|
+
type TileSpec = { asset: ImageSourcePropType; background: string; theme?: Themes; wide?: boolean };
|
|
58
|
+
|
|
59
|
+
// Les logos π et PI-SPI des boutons (toutes les variantes de couleurs)
|
|
60
|
+
// sont disponibles ici, pour les reprendre dans votre propre app :
|
|
61
|
+
// https://github.com/pisfn-sandbox/mobile-app-flutter-sdk-assets/tree/main/variante_logo_pi_et_pispi
|
|
62
|
+
const PI_MARRON = require('./assets/host/pi_marron.png');
|
|
63
|
+
const PI_BLANC = require('./assets/host/pi_blanc.png');
|
|
64
|
+
const PI_JAUNE = require('./assets/host/pi_jaune.png');
|
|
65
|
+
const LOGO_BLANC = require('./assets/host/logo_blanc.png');
|
|
66
|
+
const LOGO_MARRON_JAUNE = require('./assets/host/logo_maron_jaune.png');
|
|
67
|
+
const LOGO_JAUNE_MARRON = require('./assets/host/logo_jaune_maron.png');
|
|
68
|
+
const LOGO_JAUNE_BLANC = require('./assets/host/logo_jaune_blanc.png');
|
|
69
|
+
|
|
70
|
+
const SINGLE_ACCOUNT_TILES: TileSpec[] = [
|
|
71
|
+
{ asset: PI_MARRON, background: '#F5F0E8' },
|
|
72
|
+
{ asset: PI_BLANC, background: '#151413', theme: Themes.dark },
|
|
73
|
+
{ asset: PI_JAUNE, background: '#F5F0E8', theme: Themes.yellow },
|
|
74
|
+
{ asset: PI_BLANC, background: '#2E7D32', theme: Themes.green },
|
|
75
|
+
{ asset: PI_JAUNE, background: '#1565C0', theme: Themes.blue },
|
|
76
|
+
{ asset: PI_BLANC, background: '#C62828', theme: Themes.red },
|
|
77
|
+
{ asset: PI_BLANC, background: '#EF6C00', theme: Themes.orange },
|
|
78
|
+
{ asset: PI_JAUNE, background: '#6A1B9A', theme: Themes.purple },
|
|
79
|
+
{ asset: PI_BLANC, background: '#AD1457', theme: Themes.pink },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const MULTI_ACCOUNT_TILES: TileSpec[] = [
|
|
83
|
+
{ asset: LOGO_MARRON_JAUNE, background: '#F5F0E8', wide: true },
|
|
84
|
+
{ asset: LOGO_BLANC, background: '#151413', theme: Themes.dark, wide: true },
|
|
85
|
+
{ asset: LOGO_MARRON_JAUNE, background: '#F5F0E8', theme: Themes.light, wide: true },
|
|
86
|
+
{ asset: LOGO_JAUNE_MARRON, background: '#F5F0E8', theme: Themes.yellow, wide: true },
|
|
87
|
+
{ asset: LOGO_BLANC, background: '#2E7D32', theme: Themes.green, wide: true },
|
|
88
|
+
{ asset: LOGO_JAUNE_BLANC, background: '#1565C0', theme: Themes.blue, wide: true },
|
|
89
|
+
{ asset: LOGO_BLANC, background: '#C62828', theme: Themes.red, wide: true },
|
|
90
|
+
{ asset: LOGO_BLANC, background: '#EF6C00', theme: Themes.orange, wide: true },
|
|
91
|
+
{ asset: LOGO_JAUNE_BLANC, background: '#6A1B9A', theme: Themes.purple, wide: true },
|
|
92
|
+
{ asset: LOGO_BLANC, background: '#AD1457', theme: Themes.pink, wide: true },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const PUSH_TILES: TileSpec[] = [
|
|
96
|
+
{ asset: LOGO_JAUNE_BLANC, background: '#1565C0', theme: Themes.blue, wide: true },
|
|
97
|
+
{ asset: LOGO_BLANC, background: '#C62828', theme: Themes.red, wide: true },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Accueil de l'app hôte factice : une grille de trois scénarios de démo,
|
|
102
|
+
* chacun ouvrant le SDK dans une configuration différente, et chaque tuile
|
|
103
|
+
* avec son propre thème de démarrage (`PiSpiAppConfig.defaultTheme`).
|
|
104
|
+
*
|
|
105
|
+
* - **Compte unique** : le SDK saute l'écran de sélection de compte et le
|
|
106
|
+
* carrousel de l'accueil.
|
|
107
|
+
* - **Comptes multiples** : écran de sélection à l'ouverture, puis
|
|
108
|
+
* carrousel sur l'accueil du SDK.
|
|
109
|
+
* - **Simuler un push** : ouvre le SDK normalement, puis simule 3 s plus
|
|
110
|
+
* tard la réception d'un message push (`PiSpiPush.handleIncoming`).
|
|
111
|
+
*/
|
|
112
|
+
function FakeHostHomePage() {
|
|
113
|
+
const [sdkConfig, setSdkConfig] = useState<PiSpiSdkConfig | null>(null);
|
|
114
|
+
const stepUp = useStepUpAuth();
|
|
115
|
+
|
|
116
|
+
const closeSdk = useCallback(() => setSdkConfig(null), []);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Point d'entrée du SDK : c'est cette fonction (et uniquement elle) que
|
|
120
|
+
* vous devez adapter pour l'intégrer dans une vraie app.
|
|
121
|
+
*
|
|
122
|
+
* 1. Elle construit un `PiSpiSdkConfig` à partir des informations que
|
|
123
|
+
* l'app hôte possède déjà (l'utilisateur est déjà connecté chez le
|
|
124
|
+
* participant *avant* d'ouvrir le SDK — le SDK ne gère lui-même aucune
|
|
125
|
+
* authentification). Ici tout est codé en dur pour la démo.
|
|
126
|
+
* 2. Elle affiche `<PiSpiSdkEntry config={...} />` par-dessus l'écran de
|
|
127
|
+
* l'hôte (ici, en remplaçant simplement son contenu).
|
|
128
|
+
*/
|
|
129
|
+
const openPiSpiSdk = useCallback(
|
|
130
|
+
(options: { multiCompte?: boolean; theme?: Themes } = {}) => {
|
|
131
|
+
const config = new PiSpiSdkConfig({
|
|
132
|
+
// Identité du client déjà connecté côté hôte. Dans une vraie app,
|
|
133
|
+
// ces valeurs viennent de votre propre backend/session.
|
|
134
|
+
user: new PiSpiUserIdentity({
|
|
135
|
+
firstName: 'Jean',
|
|
136
|
+
lastName: 'Doe',
|
|
137
|
+
fullName: 'Jean Doe',
|
|
138
|
+
// Pays de domiciliation du compte : un code pays UEMOA.
|
|
139
|
+
pays: 'SN',
|
|
140
|
+
// comptes ne doit jamais être vide. Avec un seul élément, le SDK
|
|
141
|
+
// saute l'écran de sélection et le carrousel de l'accueil.
|
|
142
|
+
comptes: options.multiCompte
|
|
143
|
+
? [
|
|
144
|
+
new PiSpiCompte({ numero: 'SNB0000000000000000001', libelle: 'Courant', solde: 4240320 }),
|
|
145
|
+
new PiSpiCompte({ numero: 'SNB0000000000000000002', libelle: 'Chèque', solde: 240000 }),
|
|
146
|
+
new PiSpiCompte({ numero: 'SNB0000000000000000003', libelle: 'Épargne', solde: 32240320 }),
|
|
147
|
+
]
|
|
148
|
+
: [new PiSpiCompte({ numero: 'SNB0000000000000000001', libelle: 'Courant', solde: 4240320 })],
|
|
149
|
+
}),
|
|
150
|
+
// Connexion au backend PI-SPI : URL, authentification, mode démo.
|
|
151
|
+
api: new PiSpiApiConfig({
|
|
152
|
+
apiBaseUrl: API_BASE_URL,
|
|
153
|
+
// Token d'accès (Bearer) valide au moment de l'ouverture du SDK,
|
|
154
|
+
// ajouté par le SDK à chaque requête vers apiBaseUrl.
|
|
155
|
+
accessToken: FAKE_ACCESS_TOKEN,
|
|
156
|
+
// Appelé si une requête échoue en 401 (ou 403 avec un token
|
|
157
|
+
// expiré) : l'hôte rafraîchit son token et le retourne.
|
|
158
|
+
onAccessTokenExpired: async () => FAKE_ACCESS_TOKEN,
|
|
159
|
+
// Le SDK ne parvient plus à obtenir de token valide : on referme
|
|
160
|
+
// son écran et on prévient l'utilisateur.
|
|
161
|
+
onSessionExpired: async () => {
|
|
162
|
+
closeSdk();
|
|
163
|
+
Alert.alert('Session PI-SPI expirée');
|
|
164
|
+
},
|
|
165
|
+
// Mode démo : tous les appels API du SDK sont mockés.
|
|
166
|
+
demoMode: true,
|
|
167
|
+
}),
|
|
168
|
+
// Identité et préférences de l'application hôte.
|
|
169
|
+
appConfig: new PiSpiAppConfig({
|
|
170
|
+
// Code membre PSP du participant auprès de la BCEAO.
|
|
171
|
+
pspCodeMembre: 'SNB000',
|
|
172
|
+
// Le SDK n'a pas son propre sélecteur de langue : c'est l'hôte
|
|
173
|
+
// qui décide (français par défaut).
|
|
174
|
+
defaultLocale: 'fr',
|
|
175
|
+
// Optionnel : appliqué et persisté comme thème par défaut à
|
|
176
|
+
// chaque ouverture (écrase même un choix fait dans le SDK).
|
|
177
|
+
defaultTheme: options.theme,
|
|
178
|
+
}),
|
|
179
|
+
// Politique de step-up avant chaque action sensible.
|
|
180
|
+
identification: new PiSpiIdentificationConfig({
|
|
181
|
+
confirmationMethodLabel: 'external',
|
|
182
|
+
// Appelé par le SDK avant chaque action sensible. Dans une vraie
|
|
183
|
+
// app, remplacez ce faux écran par un vrai écran PIN/biométrie
|
|
184
|
+
// (ex. expo-local-authentication) : le SDK ne voit jamais le
|
|
185
|
+
// secret, seulement le booléen retourné ici.
|
|
186
|
+
requireStepUpIdentification: stepUp.request,
|
|
187
|
+
}),
|
|
188
|
+
// Journalisation et remontée d'erreurs.
|
|
189
|
+
log: new PiSpiLogConfig({
|
|
190
|
+
logLevel: 'debug',
|
|
191
|
+
// Dans une vraie app, transmettez l'erreur à votre outil de
|
|
192
|
+
// crash reporting (Crashlytics, Sentry...).
|
|
193
|
+
onError: (error) => console.log('[Host] Erreur remontée par le SDK :', error),
|
|
194
|
+
}),
|
|
195
|
+
// Fermeture volontaire (bouton retour/fermeture, compte clôturé...) :
|
|
196
|
+
// ce n'est pas une erreur, on referme juste l'écran.
|
|
197
|
+
onClose: async () => closeSdk(),
|
|
198
|
+
});
|
|
199
|
+
setSdkConfig(config);
|
|
200
|
+
},
|
|
201
|
+
[closeSdk, stepUp.request],
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const openPiSpiSdkAndSimulatePush = useCallback(
|
|
205
|
+
(theme?: Themes) => {
|
|
206
|
+
// ouvrir d'abord le SDK
|
|
207
|
+
openPiSpiSdk({ theme });
|
|
208
|
+
// attendre trois secondes (le temps que l'écran du SDK soit affiché)
|
|
209
|
+
// avant de simuler la réception du push
|
|
210
|
+
setTimeout(simulateIncomingPush, 3000);
|
|
211
|
+
},
|
|
212
|
+
[openPiSpiSdk],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Bouton retour Android : le SDK gère d'abord sa propre pile d'écrans ;
|
|
216
|
+
// quand il n'a plus rien à dépiler, l'hôte referme le SDK.
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!sdkConfig) return undefined;
|
|
219
|
+
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
220
|
+
closeSdk();
|
|
221
|
+
return true;
|
|
222
|
+
});
|
|
223
|
+
return () => sub.remove();
|
|
224
|
+
}, [sdkConfig, closeSdk]);
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<View style={styles.flex}>
|
|
228
|
+
<StatusBar style="auto" />
|
|
229
|
+
{sdkConfig ? (
|
|
230
|
+
<PiSpiSdkEntry config={sdkConfig} />
|
|
231
|
+
) : (
|
|
232
|
+
<SafeAreaView style={styles.flex} edges={['top', 'bottom']}>
|
|
233
|
+
<Text style={styles.appBarTitle}>App PSP</Text>
|
|
234
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
235
|
+
<AppSection
|
|
236
|
+
title="Le client a un seul compte"
|
|
237
|
+
tiles={SINGLE_ACCOUNT_TILES}
|
|
238
|
+
onTapTile={(theme) => openPiSpiSdk({ theme })}
|
|
239
|
+
/>
|
|
240
|
+
<AppSection
|
|
241
|
+
title="Le client a plusieurs comptes"
|
|
242
|
+
tiles={MULTI_ACCOUNT_TILES}
|
|
243
|
+
onTapTile={(theme) => openPiSpiSdk({ multiCompte: true, theme })}
|
|
244
|
+
/>
|
|
245
|
+
<AppSection
|
|
246
|
+
title="Simuler un push (transfert reçu)"
|
|
247
|
+
tiles={PUSH_TILES}
|
|
248
|
+
onTapTile={openPiSpiSdkAndSimulatePush}
|
|
249
|
+
/>
|
|
250
|
+
</ScrollView>
|
|
251
|
+
</SafeAreaView>
|
|
252
|
+
)}
|
|
253
|
+
{stepUp.visible && <StepUpAuthPage onDone={stepUp.complete} />}
|
|
254
|
+
</View>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Illustre `PiSpiPush.handleIncoming` : dans une vraie app, ce serait
|
|
260
|
+
* appelé depuis le handler de message push de l'hôte. Si l'écran du SDK
|
|
261
|
+
* est ouvert, ça déclenche le même toast/rafraîchissement qu'un vrai push.
|
|
262
|
+
*/
|
|
263
|
+
function simulateIncomingPush() {
|
|
264
|
+
PiSpiPush.handleIncoming(
|
|
265
|
+
{ idObject: 'ESNB00120240105153554CancelpNvhUk9e', type: 'TRANSFERT_RECU', montant: '1500' },
|
|
266
|
+
{ title: 'Transfert reçu', body: 'Vous avez reçu 1 500 F CFA de Aïda Diop' },
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Affiche le faux écran de step-up et résout la promesse demandée par le SDK. */
|
|
271
|
+
function useStepUpAuth() {
|
|
272
|
+
const [visible, setVisible] = useState(false);
|
|
273
|
+
const resolver = useRef<((ok: boolean) => void) | null>(null);
|
|
274
|
+
|
|
275
|
+
const request = useCallback(
|
|
276
|
+
() =>
|
|
277
|
+
new Promise<boolean>((resolve) => {
|
|
278
|
+
resolver.current = resolve;
|
|
279
|
+
setVisible(true);
|
|
280
|
+
}),
|
|
281
|
+
[],
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const complete = useCallback((ok: boolean) => {
|
|
285
|
+
setVisible(false);
|
|
286
|
+
resolver.current?.(ok);
|
|
287
|
+
resolver.current = null;
|
|
288
|
+
}, []);
|
|
289
|
+
|
|
290
|
+
return { visible, request, complete };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function AppSection({
|
|
294
|
+
title,
|
|
295
|
+
tiles,
|
|
296
|
+
onTapTile,
|
|
297
|
+
}: {
|
|
298
|
+
title: string;
|
|
299
|
+
tiles: TileSpec[];
|
|
300
|
+
onTapTile: (theme?: Themes) => void;
|
|
301
|
+
}) {
|
|
302
|
+
return (
|
|
303
|
+
<View style={styles.section}>
|
|
304
|
+
<Text style={styles.sectionTitle}>{title}</Text>
|
|
305
|
+
<View style={styles.wrap}>
|
|
306
|
+
{tiles.map((tile, i) => (
|
|
307
|
+
<Pressable
|
|
308
|
+
key={i}
|
|
309
|
+
onPress={() => onTapTile(tile.theme)}
|
|
310
|
+
style={({ pressed }) => [
|
|
311
|
+
styles.tile,
|
|
312
|
+
{
|
|
313
|
+
width: tile.wide ? 140 : 72,
|
|
314
|
+
paddingHorizontal: tile.wide ? 12 : 14,
|
|
315
|
+
paddingVertical: tile.wide ? 20 : 14,
|
|
316
|
+
backgroundColor: tile.background,
|
|
317
|
+
opacity: pressed ? 0.8 : 1,
|
|
318
|
+
},
|
|
319
|
+
]}
|
|
320
|
+
>
|
|
321
|
+
<Image source={tile.asset} style={styles.tileImage} resizeMode="contain" />
|
|
322
|
+
</Pressable>
|
|
323
|
+
))}
|
|
324
|
+
</View>
|
|
325
|
+
</View>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const PIN_LENGTH = 4;
|
|
330
|
+
|
|
331
|
+
/** Faux écran de vérification (empreinte simulée, ou code PIN à 4 chiffres). */
|
|
332
|
+
function StepUpAuthPage({ onDone }: { onDone: (ok: boolean) => void }) {
|
|
333
|
+
const [showPinFallback, setShowPinFallback] = useState(false);
|
|
334
|
+
const [scanning, setScanning] = useState(false);
|
|
335
|
+
const [pin, setPin] = useState<number[]>([]);
|
|
336
|
+
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
339
|
+
onDone(false);
|
|
340
|
+
return true;
|
|
341
|
+
});
|
|
342
|
+
return () => sub.remove();
|
|
343
|
+
}, [onDone]);
|
|
344
|
+
|
|
345
|
+
const startBiometricScan = () => {
|
|
346
|
+
setScanning(true);
|
|
347
|
+
setTimeout(() => onDone(true), 1400);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const onDigitPressed = (digit: number) => {
|
|
351
|
+
if (pin.length >= PIN_LENGTH) return;
|
|
352
|
+
const next = [...pin, digit];
|
|
353
|
+
setPin(next);
|
|
354
|
+
if (next.length === PIN_LENGTH) setTimeout(() => onDone(true), 200);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<SafeAreaView style={[StyleSheet.absoluteFill, styles.stepUp]}>
|
|
359
|
+
<Pressable accessibilityRole="button" onPress={() => onDone(false)} style={styles.close} hitSlop={12}>
|
|
360
|
+
<MaterialIcons name="close" size={24} color="#1C1B1F" />
|
|
361
|
+
</Pressable>
|
|
362
|
+
<View style={styles.flex} />
|
|
363
|
+
<Text style={styles.stepUpTitle}>Vérification requise</Text>
|
|
364
|
+
<Text style={styles.stepUpSubtitle}>
|
|
365
|
+
{showPinFallback ? 'Saisissez votre code PIN' : 'Confirmez avec votre empreinte ou Face ID'}
|
|
366
|
+
</Text>
|
|
367
|
+
{!showPinFallback ? (
|
|
368
|
+
<View style={styles.center}>
|
|
369
|
+
<Pressable
|
|
370
|
+
disabled={scanning}
|
|
371
|
+
onPress={startBiometricScan}
|
|
372
|
+
style={[styles.fingerprint, scanning ? styles.fingerprintScanning : null]}
|
|
373
|
+
>
|
|
374
|
+
{scanning ? (
|
|
375
|
+
<ActivityIndicator size="large" color="#6750A4" />
|
|
376
|
+
) : (
|
|
377
|
+
<MaterialIcons name="fingerprint" size={64} color="#6750A4" />
|
|
378
|
+
)}
|
|
379
|
+
</Pressable>
|
|
380
|
+
<Text style={styles.hint}>{scanning ? 'Scan en cours…' : 'Touchez pour scanner'}</Text>
|
|
381
|
+
<Pressable onPress={() => setShowPinFallback(true)} style={styles.link}>
|
|
382
|
+
<Text style={styles.linkText}>Utiliser le code PIN</Text>
|
|
383
|
+
</Pressable>
|
|
384
|
+
</View>
|
|
385
|
+
) : (
|
|
386
|
+
<View style={styles.center}>
|
|
387
|
+
<View style={styles.dots}>
|
|
388
|
+
{Array.from({ length: PIN_LENGTH }, (_, i) => (
|
|
389
|
+
<View key={i} style={[styles.dot, i < pin.length ? styles.dotFilled : null]} />
|
|
390
|
+
))}
|
|
391
|
+
</View>
|
|
392
|
+
<PinKeypad onDigit={onDigitPressed} onBackspace={() => setPin((p) => p.slice(0, -1))} />
|
|
393
|
+
</View>
|
|
394
|
+
)}
|
|
395
|
+
<View style={styles.flex} />
|
|
396
|
+
</SafeAreaView>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function PinKeypad({ onDigit, onBackspace }: { onDigit: (d: number) => void; onBackspace: () => void }) {
|
|
401
|
+
const rows = [
|
|
402
|
+
[1, 2, 3],
|
|
403
|
+
[4, 5, 6],
|
|
404
|
+
[7, 8, 9],
|
|
405
|
+
];
|
|
406
|
+
return (
|
|
407
|
+
<View style={styles.keypad}>
|
|
408
|
+
{rows.map((row) => (
|
|
409
|
+
<View key={row[0]} style={styles.keypadRow}>
|
|
410
|
+
{row.map((d) => (
|
|
411
|
+
<KeypadButton key={d} label={String(d)} onPress={() => onDigit(d)} />
|
|
412
|
+
))}
|
|
413
|
+
</View>
|
|
414
|
+
))}
|
|
415
|
+
<View style={styles.keypadRow}>
|
|
416
|
+
<View style={styles.key} />
|
|
417
|
+
<KeypadButton label="0" onPress={() => onDigit(0)} />
|
|
418
|
+
<Pressable onPress={onBackspace} style={styles.key}>
|
|
419
|
+
<MaterialIcons name="backspace" size={24} color="#1C1B1F" />
|
|
420
|
+
</Pressable>
|
|
421
|
+
</View>
|
|
422
|
+
</View>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function KeypadButton({ label, onPress }: { label: string; onPress: () => void }) {
|
|
427
|
+
return (
|
|
428
|
+
<Pressable onPress={onPress} style={({ pressed }) => [styles.key, pressed ? styles.keyPressed : null]}>
|
|
429
|
+
<Text style={styles.keyText}>{label}</Text>
|
|
430
|
+
</Pressable>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const styles = StyleSheet.create({
|
|
435
|
+
flex: { flex: 1 },
|
|
436
|
+
appBarTitle: { fontSize: 22, fontWeight: '500', paddingHorizontal: 20, paddingVertical: 14 },
|
|
437
|
+
content: { padding: 20, paddingBottom: 40 },
|
|
438
|
+
section: { marginBottom: 28 },
|
|
439
|
+
sectionTitle: { fontSize: 16, fontWeight: '500', marginBottom: 12 },
|
|
440
|
+
wrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 14 },
|
|
441
|
+
tile: { height: 72, borderRadius: 18 },
|
|
442
|
+
tileImage: { flex: 1, width: '100%' },
|
|
443
|
+
stepUp: { backgroundColor: '#FFFBFE', paddingHorizontal: 24 },
|
|
444
|
+
close: { alignSelf: 'flex-start', padding: 12 },
|
|
445
|
+
stepUpTitle: { fontSize: 24, fontWeight: '600', textAlign: 'center' },
|
|
446
|
+
stepUpSubtitle: { fontSize: 14, color: '#79747E', textAlign: 'center', marginTop: 8, marginBottom: 40 },
|
|
447
|
+
center: { alignItems: 'center' },
|
|
448
|
+
fingerprint: {
|
|
449
|
+
width: 120,
|
|
450
|
+
height: 120,
|
|
451
|
+
borderRadius: 60,
|
|
452
|
+
borderWidth: 2,
|
|
453
|
+
borderColor: '#6750A4',
|
|
454
|
+
backgroundColor: '#EADDFF',
|
|
455
|
+
alignItems: 'center',
|
|
456
|
+
justifyContent: 'center',
|
|
457
|
+
},
|
|
458
|
+
fingerprintScanning: { backgroundColor: 'rgba(103,80,164,0.15)' },
|
|
459
|
+
hint: { marginTop: 24, fontSize: 12 },
|
|
460
|
+
link: { marginTop: 32, padding: 8 },
|
|
461
|
+
linkText: { color: '#6750A4', fontSize: 14, fontWeight: '500' },
|
|
462
|
+
dots: { flexDirection: 'row', gap: 16, marginBottom: 32 },
|
|
463
|
+
dot: { width: 14, height: 14, borderRadius: 7, borderWidth: 2, borderColor: '#6750A4' },
|
|
464
|
+
dotFilled: { backgroundColor: '#6750A4' },
|
|
465
|
+
keypad: { gap: 12 },
|
|
466
|
+
keypadRow: { flexDirection: 'row', gap: 24 },
|
|
467
|
+
key: { width: 72, height: 72, borderRadius: 36, alignItems: 'center', justifyContent: 'center' },
|
|
468
|
+
keyPressed: { backgroundColor: '#EADDFF' },
|
|
469
|
+
keyText: { fontSize: 28 },
|
|
470
|
+
});
|
package/example/app.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expo": {
|
|
3
|
+
"name": "PI-SPI SDK (exemple)",
|
|
4
|
+
"slug": "pispi-sdk-example",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"orientation": "portrait",
|
|
7
|
+
"icon": "./assets/icon.png",
|
|
8
|
+
"userInterfaceStyle": "automatic",
|
|
9
|
+
"ios": {
|
|
10
|
+
"supportsTablet": true,
|
|
11
|
+
"bundleIdentifier": "com.bceao.pispi.sdk.example"
|
|
12
|
+
},
|
|
13
|
+
"android": {
|
|
14
|
+
"adaptiveIcon": {
|
|
15
|
+
"backgroundColor": "#E6F4FE",
|
|
16
|
+
"foregroundImage": "./assets/android-icon-foreground.png",
|
|
17
|
+
"backgroundImage": "./assets/android-icon-background.png",
|
|
18
|
+
"monochromeImage": "./assets/android-icon-monochrome.png"
|
|
19
|
+
},
|
|
20
|
+
"predictiveBackGestureEnabled": false,
|
|
21
|
+
"package": "com.bceao.pispi.sdk.example"
|
|
22
|
+
},
|
|
23
|
+
"web": {
|
|
24
|
+
"favicon": "./assets/favicon.png"
|
|
25
|
+
},
|
|
26
|
+
"plugins": [
|
|
27
|
+
"expo-secure-store",
|
|
28
|
+
"expo-audio",
|
|
29
|
+
"expo-image",
|
|
30
|
+
"expo-font",
|
|
31
|
+
"expo-asset",
|
|
32
|
+
"expo-video",
|
|
33
|
+
[
|
|
34
|
+
"expo-contacts",
|
|
35
|
+
{
|
|
36
|
+
"contactsPermission": "Autorisez l’accès à vos contacts pour envoyer de l’argent à un contact."
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
"expo-location",
|
|
41
|
+
{
|
|
42
|
+
"locationWhenInUsePermission": "Votre position est utilisée pour vérifier vos transactions et prévenir la fraude."
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"expo-sharing",
|
|
46
|
+
[
|
|
47
|
+
"expo-camera",
|
|
48
|
+
{
|
|
49
|
+
"cameraPermission": "Autorisez l’accès à l’appareil photo pour scanner un QR code PI-SPI.",
|
|
50
|
+
"recordAudioAndroid": false
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"expo-image-picker",
|
|
55
|
+
{
|
|
56
|
+
"photosPermission": "Autorisez l’accès à vos photos pour joindre un ticket de caisse à une transaction.",
|
|
57
|
+
"cameraPermission": "Autorisez l’accès à l’appareil photo pour photographier un ticket de caisse."
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-spi/app",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "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.",
|
|
5
5
|
"homepage": "https://github.com/pisfn-sandbox/mobile-app-react-native-sdk.git",
|
|
6
6
|
"repository": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
".": {
|
|
17
17
|
"source": "./src/index.ts",
|
|
18
18
|
"types": "./lib/typescript/src/index.d.ts",
|
|
19
|
+
"react-native": "./src/index.ts",
|
|
19
20
|
"default": "./lib/module/index.js"
|
|
20
21
|
},
|
|
21
22
|
"./package.json": "./package.json"
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
"src",
|
|
25
26
|
"lib",
|
|
26
27
|
"assets",
|
|
28
|
+
"example/App.tsx",
|
|
29
|
+
"example/app.json",
|
|
27
30
|
"!**/__tests__",
|
|
28
31
|
"!**/*.arb"
|
|
29
32
|
],
|