@pi-spi/app 1.2.0 → 1.2.1

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 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
@@ -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
+ });
@@ -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.0",
3
+ "version": "1.2.1",
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": {
@@ -24,6 +24,8 @@
24
24
  "src",
25
25
  "lib",
26
26
  "assets",
27
+ "example/App.tsx",
28
+ "example/app.json",
27
29
  "!**/__tests__",
28
30
  "!**/*.arb"
29
31
  ],