@trustchex/react-native-sdk 1.475.1 → 1.481.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.
Files changed (108) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +76 -24
  2. package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
  3. package/ios/Camera/TrustchexCameraView.swift +20 -15
  4. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
  5. package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
  6. package/lib/module/Screens/Static/ResultScreen.js +145 -113
  7. package/lib/module/Shared/Components/DebugOverlay.js +5 -2
  8. package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
  9. package/lib/module/Shared/Components/IdentityDocumentCamera.js +185 -30
  10. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
  11. package/lib/module/Shared/Components/NavigationManager.js +7 -9
  12. package/lib/module/Shared/EIDReader/eidReader.js +159 -38
  13. package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
  14. package/lib/module/Shared/Libs/SignalingClient.js +6 -5
  15. package/lib/module/Shared/Libs/crypto.utils.js +25 -3
  16. package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
  17. package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
  18. package/lib/module/Shared/Libs/diagnostics.js +171 -0
  19. package/lib/module/Shared/Libs/http-client.js +19 -6
  20. package/lib/module/Shared/Libs/mrz.utils.js +193 -33
  21. package/lib/module/Shared/Libs/mrzFrameAggregator.js +74 -3
  22. package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
  23. package/lib/module/Shared/Libs/promise.utils.js +4 -3
  24. package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
  25. package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
  26. package/lib/module/Shared/Services/AnalyticsService.js +8 -2
  27. package/lib/module/Shared/Services/DataUploadService.js +114 -127
  28. package/lib/module/Shared/Services/VideoSessionService.js +4 -3
  29. package/lib/module/Translation/Resources/en.js +6 -0
  30. package/lib/module/Translation/Resources/tr.js +6 -0
  31. package/lib/module/Trustchex.js +20 -4
  32. package/lib/module/version.js +1 -1
  33. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  36. package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
  38. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
  39. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +10 -0
  41. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
  42. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
  43. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  44. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  45. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
  46. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
  47. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  48. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
  49. package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
  50. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
  51. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  52. package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
  53. package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
  54. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
  55. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
  56. package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
  57. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  58. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +55 -0
  60. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
  62. package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  64. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
  65. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
  66. package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
  67. package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
  68. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
  69. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  70. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
  71. package/lib/typescript/src/Translation/Resources/en.d.ts +6 -0
  72. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  73. package/lib/typescript/src/Translation/Resources/tr.d.ts +6 -0
  74. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  75. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  76. package/lib/typescript/src/version.d.ts +1 -1
  77. package/package.json +17 -3
  78. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
  79. package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
  80. package/src/Screens/Static/ResultScreen.tsx +172 -162
  81. package/src/Shared/Components/DebugOverlay.tsx +5 -2
  82. package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
  83. package/src/Shared/Components/IdentityDocumentCamera.tsx +200 -29
  84. package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
  85. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
  86. package/src/Shared/Components/NavigationManager.tsx +7 -7
  87. package/src/Shared/Components/TrustchexCamera.tsx +6 -0
  88. package/src/Shared/EIDReader/eidReader.ts +181 -52
  89. package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
  90. package/src/Shared/Libs/SignalingClient.ts +10 -5
  91. package/src/Shared/Libs/crypto.utils.ts +41 -4
  92. package/src/Shared/Libs/deeplink.utils.ts +39 -7
  93. package/src/Shared/Libs/diagnosticReport.ts +206 -0
  94. package/src/Shared/Libs/diagnostics.ts +251 -0
  95. package/src/Shared/Libs/http-client.ts +24 -8
  96. package/src/Shared/Libs/mrz.utils.ts +200 -34
  97. package/src/Shared/Libs/mrzFrameAggregator.ts +112 -2
  98. package/src/Shared/Libs/native-device-info.utils.ts +116 -0
  99. package/src/Shared/Libs/promise.utils.ts +11 -5
  100. package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
  101. package/src/Shared/Libs/shuffle.utils.ts +15 -0
  102. package/src/Shared/Services/AnalyticsService.ts +17 -1
  103. package/src/Shared/Services/DataUploadService.ts +155 -166
  104. package/src/Shared/Services/VideoSessionService.ts +15 -3
  105. package/src/Translation/Resources/en.ts +7 -0
  106. package/src/Translation/Resources/tr.ts +7 -0
  107. package/src/Trustchex.tsx +26 -3
  108. package/src/version.ts +1 -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';
@@ -39,7 +39,32 @@ import { Video as VideoCompressor } from 'react-native-compressor';
39
39
  import { encryptWithAes, getSessionKey } from '../../Shared/Libs/crypto.utils';
40
40
  import Video from 'react-native-video';
41
41
  import StyledButton from '../../Shared/Components/StyledButton';
42
+ import DiagnosticReportButton from '../../Shared/Components/DiagnosticReportButton';
43
+ import type { DiagnosticImage } from '../../Shared/Libs/sendDiagnosticReport';
42
44
  import NativeDeviceInfo from '../../Shared/Libs/native-device-info.utils';
45
+
46
+ /**
47
+ * Collect the captured document/face images (base64) as attachments for a
48
+ * diagnostic report. On an ID card the BACK image is the MRZ side; on a passport
49
+ * the front carries the MRZ. The NFC chip face arrives as `faceImage` with its
50
+ * own mime type, which may be png.
51
+ */
52
+ const collectDiagnosticImages = (
53
+ doc?: ScannedIdentityDocument
54
+ ): DiagnosticImage[] => {
55
+ if (!doc) return [];
56
+ const out: DiagnosticImage[] = [];
57
+ const push = (name: string, b64?: string, mime: 'jpg' | 'png' = 'jpg') => {
58
+ if (b64 && b64 !== '') out.push({ name, base64: b64, mime });
59
+ };
60
+ push('mrz-side', doc.backImage);
61
+ push('front', doc.frontImage);
62
+ const faceMime = doc.faceImageMimeType === 'image/png' ? 'png' : 'jpg';
63
+ push('face', doc.faceImage, faceMime);
64
+ push('secondary-face', doc.secondaryFaceImage);
65
+ push('hologram', doc.hologramImage);
66
+ return out;
67
+ };
43
68
  import {
44
69
  trackError,
45
70
  trackFunnelStep,
@@ -285,11 +310,15 @@ const ResultScreen = () => {
285
310
  return;
286
311
  }
287
312
 
313
+ const uploadSessionToken = getSessionToken();
288
314
  const uploadFileOptions: RNFS.UploadFileOptions = {
289
315
  toUrl: `${apiUrl}/identifications/${identificationId}/media`,
290
316
  method: 'POST',
291
317
  headers: {
292
318
  Accept: 'application/json',
319
+ ...(uploadSessionToken
320
+ ? { 'X-Session-Token': uploadSessionToken }
321
+ : {}),
293
322
  },
294
323
  files: [],
295
324
  progress: (res) => {
@@ -299,186 +328,143 @@ const ResultScreen = () => {
299
328
  },
300
329
  } satisfies RNFS.UploadFileOptions;
301
330
 
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'
331
+ // Plaintext identity images/video staged on disk for the multipart
332
+ // upload; everything tracked here is deleted in the finally below.
333
+ const tempPaths: string[] = [];
334
+
335
+ const writeTempImage = async (
336
+ fileName: string,
337
+ base64Data: string,
338
+ filetype: string = 'image/jpeg'
339
+ ) => {
340
+ const filePath = decodeURIComponent(
341
+ `${RNFS.TemporaryDirectoryPath}/${fileName}`
310
342
  );
343
+ await RNFS.writeFile(filePath, base64Data, 'base64');
344
+ tempPaths.push(filePath);
311
345
  uploadFileOptions.files.push({
312
346
  name: 'files',
313
- filename: 'DOCUMENT_FRONT_IMAGE.jpg',
314
- filepath: decodeURIComponent(
315
- `${RNFS.TemporaryDirectoryPath}/DOCUMENT_FRONT_IMAGE.jpg`
316
- ),
317
- filetype: 'image/jpeg',
347
+ filename: fileName,
348
+ filepath: filePath,
349
+ filetype,
318
350
  });
319
- }
351
+ return filePath;
352
+ };
320
353
 
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
- }
354
+ try {
355
+ const frontImage = scannedIdentityDocument?.frontImage;
356
+ if (frontImage && frontImage !== '') {
357
+ await writeTempImage('DOCUMENT_FRONT_IMAGE.jpg', frontImage);
358
+ }
339
359
 
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
- }
360
+ const backImage = scannedIdentityDocument?.backImage;
361
+ if (backImage && backImage !== '') {
362
+ await writeTempImage('DOCUMENT_BACK_IMAGE.jpg', backImage);
363
+ }
365
364
 
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
- }
365
+ const faceImage = scannedIdentityDocument?.faceImage;
366
+ if (faceImage && faceImage !== '') {
367
+ scannedIdentityDocument.faceImageMimeType ||= 'image/jpeg';
368
+ const extension =
369
+ scannedIdentityDocument.faceImageMimeType === 'image/jp2'
370
+ ? 'jp2'
371
+ : scannedIdentityDocument.faceImageMimeType === 'image/png'
372
+ ? 'png'
373
+ : 'jpg';
374
+ await writeTempImage(
375
+ `FACE_IMAGE.${extension}`,
376
+ faceImage,
377
+ scannedIdentityDocument.faceImageMimeType
378
+ );
379
+ }
384
380
 
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
- }
381
+ const secondaryFaceImage = scannedIdentityDocument?.secondaryFaceImage;
382
+ if (secondaryFaceImage && secondaryFaceImage !== '') {
383
+ await writeTempImage(
384
+ 'DOCUMENT_SECONDARY_FACE_IMAGE.jpg',
385
+ secondaryFaceImage
386
+ );
387
+ }
403
388
 
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
- }
389
+ const hologramImage = scannedIdentityDocument?.hologramImage;
390
+ if (hologramImage && hologramImage !== '') {
391
+ await writeTempImage('DOCUMENT_HOLOGRAM_IMAGE.jpg', hologramImage);
433
392
  }
434
393
 
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`;
394
+ if (livenessDetection) {
395
+ for (const instruction of livenessDetection.instructions) {
396
+ if (instruction.photo !== '') {
397
+ const filePath = await writeTempImage(
398
+ `LIVENESS_${instruction.instruction}_IMAGE.jpg`,
399
+ instruction.photo
400
+ );
401
+ if (instruction.instruction === 'LOOK_STRAIGHT_AND_BLINK') {
402
+ uploadFileOptions.files.push({
403
+ name: 'files',
404
+ filename: 'SELFIE_IMAGE.jpg',
405
+ filepath: filePath,
406
+ filetype: 'image/jpeg',
407
+ });
408
+ }
409
+ }
444
410
  }
445
- await RNFS.copyFile(livenessVideoPath, videoFilePath);
446
411
 
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);
412
+ const livenessVideoPath = livenessDetection.videoPath;
413
+ if (livenessVideoPath) {
414
+ let videoFilePath: string;
415
+ if (Platform.OS === 'ios') {
416
+ const tmpTimestamp = new Date().getTime();
417
+ const tempDir = `${RNFS.TemporaryDirectoryPath}/${tmpTimestamp}`;
418
+ await RNFS.mkdir(tempDir);
419
+ tempPaths.push(tempDir);
420
+ videoFilePath = `${tempDir}/LIVENESS_VIDEO.mp4`;
421
+ } else {
422
+ videoFilePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_VIDEO.mp4`;
423
+ tempPaths.push(videoFilePath);
459
424
  }
460
- );
425
+ await RNFS.copyFile(livenessVideoPath, videoFilePath);
426
+
427
+ // Compress video with maximum compression settings
428
+ const compressedVideoPath = await VideoCompressor.compress(
429
+ videoFilePath,
430
+ {
431
+ compressionMethod: 'manual',
432
+ bitrate: 500000, // 500 kbps
433
+ maxSize: 1280, // HD 720p
434
+ minimumFileSizeForCompress: 0, // Always compress
435
+ },
436
+ (compressionProgress) => {
437
+ // Map compression progress (0-1) to main progress (40-60%)
438
+ setProgress(40 + compressionProgress * 20);
439
+ }
440
+ );
461
441
 
462
- // Convert file:// URI to regular path for RNFS upload
463
- const normalizedPath = compressedVideoPath.replace('file://', '');
442
+ // Convert file:// URI to regular path for RNFS upload
443
+ const normalizedPath = compressedVideoPath.replace('file://', '');
444
+ tempPaths.push(normalizedPath);
464
445
 
465
- uploadFileOptions.files.push({
466
- name: 'files',
467
- filename: 'LIVENESS_VIDEO.mp4',
468
- filepath: normalizedPath,
469
- filetype: 'video/mp4',
470
- });
446
+ uploadFileOptions.files.push({
447
+ name: 'files',
448
+ filename: 'LIVENESS_VIDEO.mp4',
449
+ filepath: normalizedPath,
450
+ filetype: 'video/mp4',
451
+ });
452
+ }
471
453
  }
472
- }
473
454
 
474
- if (uploadFileOptions.files.length === 0) {
475
- return;
476
- }
455
+ if (uploadFileOptions.files.length === 0) {
456
+ return;
457
+ }
477
458
 
478
- const response = await RNFS.uploadFiles(uploadFileOptions).promise;
459
+ const response = await RNFS.uploadFiles(uploadFileOptions).promise;
479
460
 
480
- if (![200, 201, 204].includes(response.statusCode)) {
481
- throw new Error('Media upload failed');
461
+ if (![200, 201, 204].includes(response.statusCode)) {
462
+ throw new Error('Media upload failed');
463
+ }
464
+ } finally {
465
+ await Promise.all(
466
+ tempPaths.map((path) => RNFS.unlink(path).catch(() => {}))
467
+ );
482
468
  }
483
469
  },
484
470
  [apiUrl]
@@ -518,11 +504,15 @@ const ResultScreen = () => {
518
504
  ''
519
505
  );
520
506
 
507
+ const consentSessionToken = getSessionToken();
521
508
  const response = await RNFS.uploadFiles({
522
509
  toUrl: `${apiUrl}/identifications/${identificationId}/media`,
523
510
  method: 'POST',
524
511
  headers: {
525
512
  Accept: 'application/json',
513
+ ...(consentSessionToken
514
+ ? { 'X-Session-Token': consentSessionToken }
515
+ : {}),
526
516
  },
527
517
  fields: {
528
518
  consentTitle: consentVideo.title,
@@ -1080,7 +1070,7 @@ const ResultScreen = () => {
1080
1070
  )}
1081
1071
  </View>
1082
1072
  </ScrollView>
1083
- <View style={{ paddingBottom: insets.bottom }}>
1073
+ <View style={[styles.footer, { paddingBottom: insets.bottom + 20 }]}>
1084
1074
  <StyledButton
1085
1075
  mode="contained"
1086
1076
  onPress={() => {
@@ -1090,6 +1080,26 @@ const ResultScreen = () => {
1090
1080
  >
1091
1081
  {t('resultScreen.demoStartOver')}
1092
1082
  </StyledButton>
1083
+ <DiagnosticReportButton
1084
+ sessionId={appContext.identificationInfo?.sessionId}
1085
+ scan={{
1086
+ documentType:
1087
+ appContext.identificationInfo?.scannedDocument
1088
+ ?.documentType,
1089
+ dataSource:
1090
+ appContext.identificationInfo?.scannedDocument?.dataSource,
1091
+ mrzText:
1092
+ appContext.identificationInfo?.scannedDocument?.mrzText,
1093
+ mrzFields:
1094
+ appContext.identificationInfo?.scannedDocument?.mrzFields,
1095
+ barcodeValue:
1096
+ appContext.identificationInfo?.scannedDocument
1097
+ ?.barcodeValue,
1098
+ }}
1099
+ images={collectDiagnosticImages(
1100
+ appContext.identificationInfo?.scannedDocument
1101
+ )}
1102
+ />
1093
1103
  </View>
1094
1104
  </>
1095
1105
  ) : (
@@ -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={{
@@ -0,0 +1,77 @@
1
+ import React, { useState } from 'react';
2
+ import { Alert } from 'react-native';
3
+ import { useTranslation } from 'react-i18next';
4
+ import StyledButton from './StyledButton';
5
+ import {
6
+ sendDiagnosticReport,
7
+ isDiagnosticSharingAvailable,
8
+ type DiagnosticImage,
9
+ } from '../Libs/sendDiagnosticReport';
10
+ import type { ScanDataForReport } from '../Libs/diagnosticReport';
11
+ import { SDK_VERSION } from '../../version';
12
+
13
+ export interface DiagnosticReportButtonProps {
14
+ scan: ScanDataForReport;
15
+ sessionId?: string;
16
+ images?: DiagnosticImage[];
17
+ }
18
+
19
+ /**
20
+ * "Report a scanning issue" button for the result screen.
21
+ *
22
+ * Shows a brief notice describing what will be shared (the draft itself is the
23
+ * user's review/consent step), then opens the OS share sheet — the user picks
24
+ * email / WhatsApp / Drive / etc. and sends the diagnostic bundle to support.
25
+ * Self-contained so the result screen only needs to render it with the scan
26
+ * data + captured images.
27
+ */
28
+ const DiagnosticReportButton: React.FC<DiagnosticReportButtonProps> = ({
29
+ scan,
30
+ sessionId,
31
+ images,
32
+ }) => {
33
+ const { t } = useTranslation();
34
+ const [sending, setSending] = useState(false);
35
+
36
+ // `react-native-share` is optional — hide the button entirely when the host
37
+ // app hasn't installed it, so apps that don't want diagnostics see no UI.
38
+ if (!isDiagnosticSharingAvailable()) {
39
+ return null;
40
+ }
41
+
42
+ const onPress = () => {
43
+ if (sending) return;
44
+ // Brief notice — the share draft is the actual review/consent step.
45
+ Alert.alert(
46
+ t('diagnosticReport.noticeTitle'),
47
+ t('diagnosticReport.noticeBody'),
48
+ [
49
+ { text: t('diagnosticReport.cancel'), style: 'cancel' },
50
+ {
51
+ text: t('diagnosticReport.continue'),
52
+ onPress: async () => {
53
+ setSending(true);
54
+ try {
55
+ await sendDiagnosticReport({
56
+ scan,
57
+ sdkVersion: SDK_VERSION,
58
+ sessionId,
59
+ images,
60
+ });
61
+ } finally {
62
+ setSending(false);
63
+ }
64
+ },
65
+ },
66
+ ]
67
+ );
68
+ };
69
+
70
+ return (
71
+ <StyledButton mode="outlined" onPress={onPress} disabled={sending}>
72
+ {t('diagnosticReport.button')}
73
+ </StyledButton>
74
+ );
75
+ };
76
+
77
+ export default DiagnosticReportButton;