@trustchex/react-native-sdk 1.475.1 → 1.478.7

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.
Files changed (75) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +19 -21
  2. package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
  3. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
  4. package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
  5. package/lib/module/Screens/Static/ResultScreen.js +103 -107
  6. package/lib/module/Shared/Components/DebugOverlay.js +5 -2
  7. package/lib/module/Shared/Components/IdentityDocumentCamera.js +53 -22
  8. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
  9. package/lib/module/Shared/EIDReader/eidReader.js +15 -14
  10. package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +34 -0
  11. package/lib/module/Shared/Libs/SignalingClient.js +6 -5
  12. package/lib/module/Shared/Libs/crypto.utils.js +25 -3
  13. package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
  14. package/lib/module/Shared/Libs/http-client.js +19 -6
  15. package/lib/module/Shared/Libs/mrz.utils.js +193 -33
  16. package/lib/module/Shared/Libs/mrzFrameAggregator.js +45 -1
  17. package/lib/module/Shared/Libs/promise.utils.js +4 -3
  18. package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
  19. package/lib/module/Shared/Services/AnalyticsService.js +8 -2
  20. package/lib/module/Shared/Services/DataUploadService.js +114 -127
  21. package/lib/module/Shared/Services/VideoSessionService.js +4 -3
  22. package/lib/module/Translation/Resources/en.js +1 -0
  23. package/lib/module/Translation/Resources/tr.js +1 -0
  24. package/lib/module/version.js +1 -1
  25. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  26. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  27. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  28. package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
  29. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  30. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
  31. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  32. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  33. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
  34. package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
  36. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
  38. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  39. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +22 -0
  41. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
  42. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  43. package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
  44. package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
  45. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
  46. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  47. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
  48. package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
  49. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  50. package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
  51. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  52. package/lib/typescript/src/version.d.ts +1 -1
  53. package/package.json +13 -3
  54. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
  55. package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
  56. package/src/Screens/Static/ResultScreen.tsx +126 -161
  57. package/src/Shared/Components/DebugOverlay.tsx +5 -2
  58. package/src/Shared/Components/IdentityDocumentCamera.tsx +55 -22
  59. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
  60. package/src/Shared/EIDReader/eidReader.ts +23 -14
  61. package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +34 -0
  62. package/src/Shared/Libs/SignalingClient.ts +10 -5
  63. package/src/Shared/Libs/crypto.utils.ts +41 -4
  64. package/src/Shared/Libs/deeplink.utils.ts +39 -7
  65. package/src/Shared/Libs/http-client.ts +24 -8
  66. package/src/Shared/Libs/mrz.utils.ts +200 -34
  67. package/src/Shared/Libs/mrzFrameAggregator.ts +53 -1
  68. package/src/Shared/Libs/promise.utils.ts +11 -5
  69. package/src/Shared/Libs/shuffle.utils.ts +15 -0
  70. package/src/Shared/Services/AnalyticsService.ts +17 -1
  71. package/src/Shared/Services/DataUploadService.ts +155 -166
  72. package/src/Shared/Services/VideoSessionService.ts +15 -3
  73. package/src/Translation/Resources/en.ts +1 -0
  74. package/src/Translation/Resources/tr.ts +1 -0
  75. package/src/version.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"VideoSessionService.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Services/VideoSessionService.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,SAAS,GAAG,YAAY,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,gBAAgB,CAAC,CAAS;gBAEtB,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM;IAKtD;;OAEG;IACU,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,CAAC;IA+B3D;;OAEG;IACU,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA0BtE;;OAEG;IACU,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5D;;OAEG;IACU,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc3D;;OAEG;IACI,uBAAuB,CAC5B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACpC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAC9B,MAAM,IAAI;CAqEd"}
1
+ {"version":3,"file":"VideoSessionService.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Services/VideoSessionService.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,SAAS,GAAG,YAAY,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,gBAAgB,CAAC,CAAS;gBAEtB,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM;IAKtD;;OAEG;IACU,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,CAAC;IA+B3D;;OAEG;IACU,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA0BtE;;OAEG;IACU,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5D;;OAEG;IACU,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc3D;;OAEG;IACI,uBAAuB,CAC5B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACpC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAC9B,MAAM,IAAI;CAgFd"}
@@ -110,6 +110,7 @@ declare const _default: {
110
110
  'identityDocumentCamera.alignIDFrontSide': string;
111
111
  'identityDocumentCamera.alignIDBackSide': string;
112
112
  'identityDocumentCamera.scanCompleted': string;
113
+ 'identityDocumentCamera.mrzDetected': string;
113
114
  'identityDocumentCamera.searchingDocument': string;
114
115
  'identityDocumentCamera.faceDetected': string;
115
116
  'identityDocumentCamera.readingDocument': string;
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/en.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBAyPE"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/en.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBA0PE"}
@@ -110,6 +110,7 @@ declare const _default: {
110
110
  'identityDocumentCamera.alignIDFrontSide': string;
111
111
  'identityDocumentCamera.alignIDBackSide': string;
112
112
  'identityDocumentCamera.scanCompleted': string;
113
+ 'identityDocumentCamera.mrzDetected': string;
113
114
  'identityDocumentCamera.searchingDocument': string;
114
115
  'identityDocumentCamera.faceDetected': string;
115
116
  'identityDocumentCamera.readingDocument': string;
@@ -1 +1 @@
1
- {"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/tr.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBA4PE"}
1
+ {"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/tr.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBA6PE"}
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "1.475.1";
1
+ export declare const SDK_VERSION = "1.478.7";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustchex/react-native-sdk",
3
- "version": "1.475.1",
3
+ "version": "1.478.7",
4
4
  "description": "Trustchex mobile app react native SDK for android or ios devices",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -61,7 +61,7 @@
61
61
  "@react-navigation/native": "7.1.18",
62
62
  "@react-navigation/native-stack": "7.3.27",
63
63
  "@react-navigation/elements": "2.6.5",
64
- "lodash": "4.17.23",
64
+ "lodash": "4.18.0",
65
65
  "brace-expansion": "1.1.13",
66
66
  "minimatch": "3.1.4",
67
67
  "qs": "6.15.2",
@@ -70,7 +70,16 @@
70
70
  "yaml": "2.8.3",
71
71
  "fast-uri": "3.1.2",
72
72
  "flatted": "3.4.2",
73
- "react-native-screens": "4.18.0"
73
+ "react-native-screens": "4.18.0",
74
+ "shell-quote": "1.8.4",
75
+ "handlebars": "4.7.9",
76
+ "node-forge": "1.4.0",
77
+ "@xmldom/xmldom": "0.8.13",
78
+ "basic-ftp": "5.3.1",
79
+ "tmp": "0.2.6",
80
+ "ws": "8.20.1",
81
+ "picomatch": "2.3.2",
82
+ "fast-xml-parser": "5.7.0"
74
83
  },
75
84
  "scripts": {
76
85
  "example": "yarn workspace @trustchex/react-native-sdk-example",
@@ -135,6 +144,7 @@
135
144
  "dependencies": {
136
145
  "@noble/ciphers": "^1.3.0",
137
146
  "@noble/curves": "^1.9.4",
147
+ "@noble/hashes": "^1.8.0",
138
148
  "buffer": "^6.0.3",
139
149
  "crypto-js": "^4.2.0",
140
150
  "elliptic": "^6.6.1",
@@ -35,6 +35,7 @@ import { useTranslation } from 'react-i18next';
35
35
  import StyledButton from '../../Shared/Components/StyledButton';
36
36
  import LottieView from 'lottie-react-native';
37
37
  import { speak, resetLastMessage } from '../../Shared/Libs/tts.utils';
38
+ import { secureShuffle } from '../../Shared/Libs/shuffle.utils';
38
39
  import { useStatusBarWhiteBackground } from '../../Shared/Libs/status-bar.utils';
39
40
  import {
40
41
  trackFunnelStep,
@@ -202,8 +203,8 @@ const LivenessDetectionScreen = () => {
202
203
  }, [hasGuideShown]);
203
204
 
204
205
  useEffect(() => {
205
- const il = Object.keys(instructions)
206
- .filter(
206
+ const il = secureShuffle(
207
+ Object.keys(instructions).filter(
207
208
  (instruction) =>
208
209
  !['START', 'FINISH'].includes(instruction) &&
209
210
  // Look straight and blink is always included
@@ -216,7 +217,7 @@ const LivenessDetectionScreen = () => {
216
217
  instruction as any
217
218
  ))
218
219
  )
219
- .sort(() => Math.random() - 0.5);
220
+ );
220
221
  il.unshift('START');
221
222
  il.push('FINISH');
222
223
  setInitialState({
@@ -4,7 +4,8 @@ import { SafeAreaView, StyleSheet } from 'react-native';
4
4
  import AppContext from '../../Shared/Contexts/AppContext';
5
5
  import QrCodeScannerCamera from '../../Shared/Components/QrCodeScannerCamera';
6
6
  import { useNavigation } from '@react-navigation/native';
7
- import { handleDeepLink } from '../../Shared/Libs/deeplink.utils';
7
+ import { handleDeepLink, getUrlHost } from '../../Shared/Libs/deeplink.utils';
8
+ import { logWarn } from '../../Shared/Libs/debug.utils';
8
9
  import { useKeepAwake } from '../../Shared/Libs/native-keep-awake.utils';
9
10
  import { useStatusBarWhiteBackground } from '../../Shared/Libs/status-bar.utils';
10
11
 
@@ -17,7 +18,23 @@ const QrCodeScanningScreen = () => {
17
18
  useStatusBarWhiteBackground();
18
19
 
19
20
  const onQrCodeScanned = (data: string) => {
20
- const [bUrl, sId] = handleDeepLink({ url: data });
21
+ let [bUrl, sId] = handleDeepLink({ url: data });
22
+
23
+ // A scanned QR may not repoint an already-configured backend to another
24
+ // host — uploads carry identity documents and biometrics. Keep the
25
+ // configured baseUrl and only take the session id from the QR.
26
+ const configuredBaseUrl = appContext.baseUrl;
27
+ if (
28
+ bUrl &&
29
+ configuredBaseUrl &&
30
+ getUrlHost(bUrl) !== getUrlHost(configuredBaseUrl)
31
+ ) {
32
+ logWarn(
33
+ 'QrCodeScanningScreen',
34
+ 'Ignoring QR-provided baseUrl with mismatched host'
35
+ );
36
+ bUrl = configuredBaseUrl;
37
+ }
21
38
 
22
39
  if (bUrl && sId) {
23
40
  if (appContext.setBaseUrl) {
@@ -17,7 +17,7 @@ import {
17
17
  } from 'react-native';
18
18
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
19
19
  import AppContext from '../../Shared/Contexts/AppContext';
20
- import httpClient from '../../Shared/Libs/http-client';
20
+ import httpClient, { getSessionToken } from '../../Shared/Libs/http-client';
21
21
  import RNFS from 'react-native-fs';
22
22
  import NativeProgressBar from '../../Shared/Components/NativeProgressBar';
23
23
  import LottieView from 'lottie-react-native';
@@ -285,11 +285,15 @@ const ResultScreen = () => {
285
285
  return;
286
286
  }
287
287
 
288
+ const uploadSessionToken = getSessionToken();
288
289
  const uploadFileOptions: RNFS.UploadFileOptions = {
289
290
  toUrl: `${apiUrl}/identifications/${identificationId}/media`,
290
291
  method: 'POST',
291
292
  headers: {
292
293
  Accept: 'application/json',
294
+ ...(uploadSessionToken
295
+ ? { 'X-Session-Token': uploadSessionToken }
296
+ : {}),
293
297
  },
294
298
  files: [],
295
299
  progress: (res) => {
@@ -299,186 +303,143 @@ const ResultScreen = () => {
299
303
  },
300
304
  } satisfies RNFS.UploadFileOptions;
301
305
 
302
- const frontImage = scannedIdentityDocument?.frontImage;
303
- if (frontImage && frontImage !== '') {
304
- await RNFS.writeFile(
305
- decodeURIComponent(
306
- `${RNFS.TemporaryDirectoryPath}/DOCUMENT_FRONT_IMAGE.jpg`
307
- ),
308
- frontImage,
309
- 'base64'
306
+ // Plaintext identity images/video staged on disk for the multipart
307
+ // upload; everything tracked here is deleted in the finally below.
308
+ const tempPaths: string[] = [];
309
+
310
+ const writeTempImage = async (
311
+ fileName: string,
312
+ base64Data: string,
313
+ filetype: string = 'image/jpeg'
314
+ ) => {
315
+ const filePath = decodeURIComponent(
316
+ `${RNFS.TemporaryDirectoryPath}/${fileName}`
310
317
  );
318
+ await RNFS.writeFile(filePath, base64Data, 'base64');
319
+ tempPaths.push(filePath);
311
320
  uploadFileOptions.files.push({
312
321
  name: 'files',
313
- filename: 'DOCUMENT_FRONT_IMAGE.jpg',
314
- filepath: decodeURIComponent(
315
- `${RNFS.TemporaryDirectoryPath}/DOCUMENT_FRONT_IMAGE.jpg`
316
- ),
317
- filetype: 'image/jpeg',
322
+ filename: fileName,
323
+ filepath: filePath,
324
+ filetype,
318
325
  });
319
- }
326
+ return filePath;
327
+ };
320
328
 
321
- const backImage = scannedIdentityDocument?.backImage;
322
- if (backImage && backImage !== '') {
323
- await RNFS.writeFile(
324
- decodeURIComponent(
325
- `${RNFS.TemporaryDirectoryPath}/DOCUMENT_BACK_IMAGE.jpg`
326
- ),
327
- backImage,
328
- 'base64'
329
- );
330
- uploadFileOptions.files.push({
331
- name: 'files',
332
- filename: 'DOCUMENT_BACK_IMAGE.jpg',
333
- filepath: decodeURIComponent(
334
- `${RNFS.TemporaryDirectoryPath}/DOCUMENT_BACK_IMAGE.jpg`
335
- ),
336
- filetype: 'image/jpeg',
337
- });
338
- }
329
+ try {
330
+ const frontImage = scannedIdentityDocument?.frontImage;
331
+ if (frontImage && frontImage !== '') {
332
+ await writeTempImage('DOCUMENT_FRONT_IMAGE.jpg', frontImage);
333
+ }
339
334
 
340
- const faceImage = scannedIdentityDocument?.faceImage;
341
- if (faceImage && faceImage !== '') {
342
- scannedIdentityDocument.faceImageMimeType ||= 'image/jpeg';
343
- const extension =
344
- scannedIdentityDocument.faceImageMimeType === 'image/jp2'
345
- ? 'jp2'
346
- : scannedIdentityDocument.faceImageMimeType === 'image/png'
347
- ? 'png'
348
- : 'jpg';
349
- await RNFS.writeFile(
350
- decodeURIComponent(
351
- `${RNFS.TemporaryDirectoryPath}/FACE_IMAGE.${extension}`
352
- ),
353
- faceImage,
354
- 'base64'
355
- );
356
- uploadFileOptions.files.push({
357
- name: 'files',
358
- filename: `FACE_IMAGE.${extension}`,
359
- filepath: decodeURIComponent(
360
- `${RNFS.TemporaryDirectoryPath}/FACE_IMAGE.${extension}`
361
- ),
362
- filetype: scannedIdentityDocument.faceImageMimeType,
363
- });
364
- }
335
+ const backImage = scannedIdentityDocument?.backImage;
336
+ if (backImage && backImage !== '') {
337
+ await writeTempImage('DOCUMENT_BACK_IMAGE.jpg', backImage);
338
+ }
365
339
 
366
- const secondaryFaceImage = scannedIdentityDocument?.secondaryFaceImage;
367
- if (secondaryFaceImage && secondaryFaceImage !== '') {
368
- await RNFS.writeFile(
369
- decodeURIComponent(
370
- `${RNFS.TemporaryDirectoryPath}/DOCUMENT_SECONDARY_FACE_IMAGE.jpg`
371
- ),
372
- secondaryFaceImage,
373
- 'base64'
374
- );
375
- uploadFileOptions.files.push({
376
- name: 'files',
377
- filename: 'DOCUMENT_SECONDARY_FACE_IMAGE.jpg',
378
- filepath: decodeURIComponent(
379
- `${RNFS.TemporaryDirectoryPath}/DOCUMENT_SECONDARY_FACE_IMAGE.jpg`
380
- ),
381
- filetype: 'image/jpeg',
382
- });
383
- }
340
+ const faceImage = scannedIdentityDocument?.faceImage;
341
+ if (faceImage && faceImage !== '') {
342
+ scannedIdentityDocument.faceImageMimeType ||= 'image/jpeg';
343
+ const extension =
344
+ scannedIdentityDocument.faceImageMimeType === 'image/jp2'
345
+ ? 'jp2'
346
+ : scannedIdentityDocument.faceImageMimeType === 'image/png'
347
+ ? 'png'
348
+ : 'jpg';
349
+ await writeTempImage(
350
+ `FACE_IMAGE.${extension}`,
351
+ faceImage,
352
+ scannedIdentityDocument.faceImageMimeType
353
+ );
354
+ }
384
355
 
385
- const hologramImage = scannedIdentityDocument?.hologramImage;
386
- if (hologramImage && hologramImage !== '') {
387
- await RNFS.writeFile(
388
- decodeURIComponent(
389
- `${RNFS.TemporaryDirectoryPath}/DOCUMENT_HOLOGRAM_IMAGE.jpg`
390
- ),
391
- hologramImage,
392
- 'base64'
393
- );
394
- uploadFileOptions.files.push({
395
- name: 'files',
396
- filename: 'DOCUMENT_HOLOGRAM_IMAGE.jpg',
397
- filepath: decodeURIComponent(
398
- `${RNFS.TemporaryDirectoryPath}/DOCUMENT_HOLOGRAM_IMAGE.jpg`
399
- ),
400
- filetype: 'image/jpeg',
401
- });
402
- }
356
+ const secondaryFaceImage = scannedIdentityDocument?.secondaryFaceImage;
357
+ if (secondaryFaceImage && secondaryFaceImage !== '') {
358
+ await writeTempImage(
359
+ 'DOCUMENT_SECONDARY_FACE_IMAGE.jpg',
360
+ secondaryFaceImage
361
+ );
362
+ }
403
363
 
404
- if (livenessDetection) {
405
- for (const instruction of livenessDetection.instructions) {
406
- if (instruction.photo !== '') {
407
- await RNFS.writeFile(
408
- decodeURIComponent(
409
- `${RNFS.TemporaryDirectoryPath}/LIVENESS_${instruction.instruction}_IMAGE.jpg`
410
- ),
411
- instruction.photo,
412
- 'base64'
413
- );
414
- uploadFileOptions.files.push({
415
- name: 'files',
416
- filename: `LIVENESS_${instruction.instruction}_IMAGE.jpg`,
417
- filepath: decodeURIComponent(
418
- `${RNFS.TemporaryDirectoryPath}/LIVENESS_${instruction.instruction}_IMAGE.jpg`
419
- ),
420
- filetype: 'image/jpeg',
421
- });
422
- if (instruction.instruction === 'LOOK_STRAIGHT_AND_BLINK') {
423
- uploadFileOptions.files.push({
424
- name: 'files',
425
- filename: 'SELFIE_IMAGE.jpg',
426
- filepath: decodeURIComponent(
427
- `${RNFS.TemporaryDirectoryPath}/LIVENESS_${instruction.instruction}_IMAGE.jpg`
428
- ),
429
- filetype: 'image/jpeg',
430
- });
431
- }
432
- }
364
+ const hologramImage = scannedIdentityDocument?.hologramImage;
365
+ if (hologramImage && hologramImage !== '') {
366
+ await writeTempImage('DOCUMENT_HOLOGRAM_IMAGE.jpg', hologramImage);
433
367
  }
434
368
 
435
- const livenessVideoPath = livenessDetection.videoPath;
436
- if (livenessVideoPath) {
437
- let videoFilePath: string;
438
- if (Platform.OS === 'ios') {
439
- const tmpTimestamp = new Date().getTime();
440
- await RNFS.mkdir(`${RNFS.TemporaryDirectoryPath}/${tmpTimestamp}`);
441
- videoFilePath = `${RNFS.TemporaryDirectoryPath}/${tmpTimestamp}/LIVENESS_VIDEO.mp4`;
442
- } else {
443
- videoFilePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_VIDEO.mp4`;
369
+ if (livenessDetection) {
370
+ for (const instruction of livenessDetection.instructions) {
371
+ if (instruction.photo !== '') {
372
+ const filePath = await writeTempImage(
373
+ `LIVENESS_${instruction.instruction}_IMAGE.jpg`,
374
+ instruction.photo
375
+ );
376
+ if (instruction.instruction === 'LOOK_STRAIGHT_AND_BLINK') {
377
+ uploadFileOptions.files.push({
378
+ name: 'files',
379
+ filename: 'SELFIE_IMAGE.jpg',
380
+ filepath: filePath,
381
+ filetype: 'image/jpeg',
382
+ });
383
+ }
384
+ }
444
385
  }
445
- await RNFS.copyFile(livenessVideoPath, videoFilePath);
446
386
 
447
- // Compress video with maximum compression settings
448
- const compressedVideoPath = await VideoCompressor.compress(
449
- videoFilePath,
450
- {
451
- compressionMethod: 'manual',
452
- bitrate: 500000, // 500 kbps
453
- maxSize: 1280, // HD 720p
454
- minimumFileSizeForCompress: 0, // Always compress
455
- },
456
- (compressionProgress) => {
457
- // Map compression progress (0-1) to main progress (40-60%)
458
- setProgress(40 + compressionProgress * 20);
387
+ const livenessVideoPath = livenessDetection.videoPath;
388
+ if (livenessVideoPath) {
389
+ let videoFilePath: string;
390
+ if (Platform.OS === 'ios') {
391
+ const tmpTimestamp = new Date().getTime();
392
+ const tempDir = `${RNFS.TemporaryDirectoryPath}/${tmpTimestamp}`;
393
+ await RNFS.mkdir(tempDir);
394
+ tempPaths.push(tempDir);
395
+ videoFilePath = `${tempDir}/LIVENESS_VIDEO.mp4`;
396
+ } else {
397
+ videoFilePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_VIDEO.mp4`;
398
+ tempPaths.push(videoFilePath);
459
399
  }
460
- );
400
+ await RNFS.copyFile(livenessVideoPath, videoFilePath);
401
+
402
+ // Compress video with maximum compression settings
403
+ const compressedVideoPath = await VideoCompressor.compress(
404
+ videoFilePath,
405
+ {
406
+ compressionMethod: 'manual',
407
+ bitrate: 500000, // 500 kbps
408
+ maxSize: 1280, // HD 720p
409
+ minimumFileSizeForCompress: 0, // Always compress
410
+ },
411
+ (compressionProgress) => {
412
+ // Map compression progress (0-1) to main progress (40-60%)
413
+ setProgress(40 + compressionProgress * 20);
414
+ }
415
+ );
461
416
 
462
- // Convert file:// URI to regular path for RNFS upload
463
- const normalizedPath = compressedVideoPath.replace('file://', '');
417
+ // Convert file:// URI to regular path for RNFS upload
418
+ const normalizedPath = compressedVideoPath.replace('file://', '');
419
+ tempPaths.push(normalizedPath);
464
420
 
465
- uploadFileOptions.files.push({
466
- name: 'files',
467
- filename: 'LIVENESS_VIDEO.mp4',
468
- filepath: normalizedPath,
469
- filetype: 'video/mp4',
470
- });
421
+ uploadFileOptions.files.push({
422
+ name: 'files',
423
+ filename: 'LIVENESS_VIDEO.mp4',
424
+ filepath: normalizedPath,
425
+ filetype: 'video/mp4',
426
+ });
427
+ }
471
428
  }
472
- }
473
429
 
474
- if (uploadFileOptions.files.length === 0) {
475
- return;
476
- }
430
+ if (uploadFileOptions.files.length === 0) {
431
+ return;
432
+ }
477
433
 
478
- const response = await RNFS.uploadFiles(uploadFileOptions).promise;
434
+ const response = await RNFS.uploadFiles(uploadFileOptions).promise;
479
435
 
480
- if (![200, 201, 204].includes(response.statusCode)) {
481
- throw new Error('Media upload failed');
436
+ if (![200, 201, 204].includes(response.statusCode)) {
437
+ throw new Error('Media upload failed');
438
+ }
439
+ } finally {
440
+ await Promise.all(
441
+ tempPaths.map((path) => RNFS.unlink(path).catch(() => {}))
442
+ );
482
443
  }
483
444
  },
484
445
  [apiUrl]
@@ -518,11 +479,15 @@ const ResultScreen = () => {
518
479
  ''
519
480
  );
520
481
 
482
+ const consentSessionToken = getSessionToken();
521
483
  const response = await RNFS.uploadFiles({
522
484
  toUrl: `${apiUrl}/identifications/${identificationId}/media`,
523
485
  method: 'POST',
524
486
  headers: {
525
487
  Accept: 'application/json',
488
+ ...(consentSessionToken
489
+ ? { 'X-Session-Token': consentSessionToken }
490
+ : {}),
526
491
  },
527
492
  fields: {
528
493
  consentTitle: consentVideo.title,
@@ -625,14 +625,17 @@ export const TestModePanel = React.memo(function TestModePanel({
625
625
  <View style={{ padding: 10 }}>
626
626
  <TextView
627
627
  style={{
628
- color: '#FFA500',
628
+ // Green "GO" header: the panel only ever receives a STABLE + valid
629
+ // consensus MRZ (the caller withholds it until then), so showing it
630
+ // here IS the reliable-detection signal.
631
+ color: '#4CAF50',
629
632
  fontSize: 12,
630
633
  fontWeight: 'bold',
631
634
  marginBottom: 8,
632
635
  textAlign: 'center',
633
636
  }}
634
637
  >
635
- MRZ Text Read
638
+ MRZ STABLE — GO
636
639
  </TextView>
637
640
  <TextView
638
641
  style={{