@sanctum-key/react-native-sdk 1.0.53 → 1.0.54

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 (124) hide show
  1. package/README.md +7 -2
  2. package/android/build/.transforms/3db9b15041d4285a820fc84462995ea1/results.bin +1 -0
  3. package/android/build/.transforms/3db9b15041d4285a820fc84462995ea1/transformed/classes/classes_dex/classes.dex +0 -0
  4. package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
  5. package/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar +0 -0
  6. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab +0 -0
  7. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at +0 -0
  8. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab +0 -0
  9. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream +0 -0
  10. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len +0 -0
  11. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len +0 -0
  12. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at +0 -0
  13. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i +0 -0
  14. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab +0 -0
  15. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream +0 -0
  16. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len +0 -0
  17. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len +0 -0
  18. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at +0 -0
  19. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i +0 -0
  20. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab +0 -0
  21. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream +0 -0
  22. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len +0 -0
  23. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len +0 -0
  24. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at +0 -0
  25. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i +0 -0
  26. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab +0 -0
  27. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream +0 -0
  28. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len +0 -0
  29. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len +0 -0
  30. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at +0 -0
  31. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i +0 -0
  32. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab +0 -0
  33. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at +0 -0
  34. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab +0 -0
  35. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at +0 -0
  36. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab +0 -0
  37. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at +0 -0
  38. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab +1 -1
  39. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab +0 -0
  40. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at +0 -0
  41. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab +0 -0
  42. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream +0 -0
  43. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len +0 -0
  44. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len +0 -0
  45. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at +0 -0
  46. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i +0 -0
  47. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab +0 -0
  48. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream +0 -0
  49. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len +0 -0
  50. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len +0 -0
  51. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at +0 -0
  52. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i +0 -0
  53. package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
  54. package/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin +0 -0
  55. package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
  56. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  57. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/SanctumKeySdkModule$BarcodeRegion.class +0 -0
  58. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/SanctumKeySdkModule$Companion.class +0 -0
  59. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$1$5$1.class → SanctumKeySdkModule$definition$1$6$1.class} +0 -0
  60. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$1$5$2.class → SanctumKeySdkModule$definition$1$6$2.class} +0 -0
  61. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$AsyncFunction$1.class → SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunction$1.class} +0 -0
  62. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$AsyncFunction$5.class → SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunction$2.class} +0 -0
  63. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$AsyncFunction$3.class → SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunction$3.class} +0 -0
  64. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$AsyncFunction$4.class → SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunction$4.class} +0 -0
  65. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$AsyncFunction$2.class → SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunction$5.class} +0 -0
  66. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$AsyncFunction$6.class → SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunction$6.class} +0 -0
  67. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$AsyncFunctionWithPromise$1.class → SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunctionWithPromise$1.class} +0 -0
  68. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$AsyncFunctionWithPromise$3.class → SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunctionWithPromise$2.class} +0 -0
  69. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunctionWithPromise$3.class +0 -0
  70. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunctionWithPromise$4.class +0 -0
  71. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$AsyncFunctionWithPromise$2.class → SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunctionWithPromise$5.class} +0 -0
  72. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunctionWithPromise$6.class +0 -0
  73. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$AsyncFunctionWithPromise$4.class → SanctumKeySdkModule$definition$lambda$16$$inlined$AsyncFunctionWithPromise$7.class} +0 -0
  74. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$$inlined$View$1.class → SanctumKeySdkModule$definition$lambda$16$$inlined$View$1.class} +0 -0
  75. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$lambda$4$$inlined$Prop$1.class → SanctumKeySdkModule$definition$lambda$16$lambda$15$$inlined$Prop$1.class} +0 -0
  76. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/{SanctumKeySdkModule$definition$lambda$5$lambda$4$$inlined$Prop$2.class → SanctumKeySdkModule$definition$lambda$16$lambda$15$$inlined$Prop$2.class} +0 -0
  77. package/android/build/tmp/kotlin-classes/debug/kyc/sanctumkey/com/SanctumKeySdkModule.class +0 -0
  78. package/android/build.gradle +7 -0
  79. package/android/src/main/java/kyc/sanctumkey/com/SanctumKeySdkModule.kt +428 -1
  80. package/build/package.json +1 -1
  81. package/build/src/SanctumKeySdkModule.d.ts +1 -0
  82. package/build/src/SanctumKeySdkModule.d.ts.map +1 -1
  83. package/build/src/SanctumKeySdkModule.js.map +1 -1
  84. package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
  85. package/build/src/components/EnhancedCameraView.js +50 -41
  86. package/build/src/components/EnhancedCameraView.js.map +1 -1
  87. package/build/src/components/EnhancedCameraView.web.d.ts +2 -0
  88. package/build/src/components/EnhancedCameraView.web.d.ts.map +1 -1
  89. package/build/src/components/EnhancedCameraView.web.js +11 -6
  90. package/build/src/components/EnhancedCameraView.web.js.map +1 -1
  91. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  92. package/build/src/components/KYCElements/IDCardCapture.js +499 -78
  93. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  94. package/build/src/components/TemplateKYCExample.d.ts.map +1 -1
  95. package/build/src/components/TemplateKYCExample.js +3 -9
  96. package/build/src/components/TemplateKYCExample.js.map +1 -1
  97. package/build/src/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
  98. package/build/src/components/TemplateKYCFlowRefactored.js +0 -2
  99. package/build/src/components/TemplateKYCFlowRefactored.js.map +1 -1
  100. package/build/src/components/WebBarcodeScanner.d.ts +2 -0
  101. package/build/src/components/WebBarcodeScanner.d.ts.map +1 -1
  102. package/build/src/components/WebBarcodeScanner.js +99 -85
  103. package/build/src/components/WebBarcodeScanner.js.map +1 -1
  104. package/build/src/modules/api/KYCService.d.ts +1 -0
  105. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  106. package/build/src/modules/api/KYCService.js +1 -1
  107. package/build/src/modules/api/KYCService.js.map +1 -1
  108. package/build/src/modules/api/types.d.ts +1 -0
  109. package/build/src/modules/api/types.d.ts.map +1 -1
  110. package/build/src/modules/api/types.js.map +1 -1
  111. package/package.json +1 -1
  112. package/plugin/build/withVisionCamera.d.ts +1 -0
  113. package/plugin/build/withVisionCamera.js +13 -1
  114. package/plugin/src/withVisionCamera.js +18 -2
  115. package/plugin/src/withVisionCamera.ts +19 -2
  116. package/src/SanctumKeySdkModule.ts +1 -0
  117. package/src/components/EnhancedCameraView.tsx +52 -41
  118. package/src/components/EnhancedCameraView.web.tsx +15 -5
  119. package/src/components/KYCElements/IDCardCapture.tsx +552 -75
  120. package/src/components/TemplateKYCExample.tsx +6 -14
  121. package/src/components/TemplateKYCFlowRefactored.tsx +0 -2
  122. package/src/components/WebBarcodeScanner.tsx +147 -198
  123. package/src/modules/api/KYCService.ts +3 -3
  124. package/src/modules/api/types.ts +1 -0
@@ -15,10 +15,11 @@ import { cropByObb, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../uti
15
15
  import REGION_MAPPING from '../../config/region_mapping.json';
16
16
  import QRCode from 'react-native-qrcode-svg';
17
17
  import { cropImageWithBBoxWithTolerance } from '../../utils/cropByObb';
18
- import kycService, { authentification, clearAuthToken } from '../../modules/api/KYCService';
18
+ import kycService, { authentification, clearAuthToken, errorMessage } from '../../modules/api/KYCService';
19
19
  import { Linking } from 'react-native';
20
20
  import { Camera } from 'react-native-vision-camera';
21
21
  import { sdkLogger } from '../../utils/logger';
22
+ import SanctumKeySdkModule from '../../SanctumKeySdkModule';
22
23
 
23
24
  const ISO_TO_COUNTRY_NAME: Record<string, string> = {
24
25
  'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada',
@@ -33,6 +34,71 @@ interface IIDCardPayload {
33
34
  templatePath?: string;
34
35
  }
35
36
 
37
+ interface PendingCanadianImage {
38
+ path: string;
39
+ templateResponse: any;
40
+ templatePath: string;
41
+ bbox?: IBbox;
42
+ }
43
+
44
+ const BARCODE_RESPONSE_TEXT_KEYS = [
45
+ 'barcode_data',
46
+ 'raw_barcode',
47
+ 'raw_data',
48
+ 'decoded_data',
49
+ 'decoded_text',
50
+ 'barcode',
51
+ 'text',
52
+ 'value',
53
+ 'data',
54
+ 'detail',
55
+ ] as const;
56
+
57
+ /**
58
+ * The image decoder has returned a few slightly different response envelopes
59
+ * over time. Keep that API variation out of the capture flow and only accept
60
+ * values from fields that are known to contain the raw barcode payload.
61
+ */
62
+ const getDecodedBarcodeText = (response: unknown, depth = 0): string | null => {
63
+ if (typeof response === 'string') {
64
+ return response.trim().length > 0 ? response : null;
65
+ }
66
+
67
+ if (!response || typeof response !== 'object' || depth > 3) return null;
68
+
69
+ if (Array.isArray(response)) {
70
+ for (const item of response) {
71
+ const value = getDecodedBarcodeText(item, depth + 1);
72
+ if (value) return value;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ const record = response as Record<string, unknown>;
78
+ for (const key of BARCODE_RESPONSE_TEXT_KEYS) {
79
+ const value = record[key];
80
+ if (typeof value === 'string' && value.trim().length > 0) return value;
81
+ }
82
+
83
+ for (const key of ['result', 'data', 'payload', 'barcodes', 'results'] as const) {
84
+ const nested = record[key];
85
+ if (nested && typeof nested === 'object') {
86
+ const value = getDecodedBarcodeText(nested, depth + 1);
87
+ if (value) return value;
88
+ }
89
+ }
90
+
91
+ return null;
92
+ };
93
+
94
+ const isUsableBarcodeText = (value: unknown, allowShortBarcode: boolean): value is string => {
95
+ if (typeof value !== 'string') return false;
96
+
97
+ const trimmed = value.trim();
98
+ const isDecoderError = /^(no (?:bar)?code|barcode (?:not|was not)|unable to decode|failed to decode|error\b)/i.test(trimmed);
99
+ return !isDecoderError && trimmed.length > 0 && (allowShortBarcode || value.length > 50);
100
+ };
101
+
36
102
  interface IDCardCaptureProps {
37
103
  component: TemplateComponent;
38
104
  value?: Record<string, IIDCardPayload>;
@@ -49,6 +115,19 @@ const SHORT_TO_LONG_REGION: Record<string, string> = {
49
115
  "SK": "Saskatchewan", "YT": "Yukon"
50
116
  };
51
117
 
118
+ const DOCUMENT_TYPE_MAPPING: Record<string, GovernmentDocumentType> = {
119
+ nationalId: 'national_id',
120
+ national_id: 'national_id',
121
+ identity_card: 'national_id',
122
+ passport: 'passport',
123
+ driversLicense: 'drivers_licence',
124
+ drivers_licence: 'drivers_licence',
125
+ residencePermit: 'permanent_residence',
126
+ permanent_residence: 'permanent_residence',
127
+ healthInsuranceCard: 'health_insurance_card',
128
+ health_insurance_card: 'health_insurance_card',
129
+ };
130
+
52
131
  const ExpiringCodeTimer = ({ initialCode, language }: { initialCode: string, language: string }) => {
53
132
  const [timeLeft, setTimeLeft] = useState(300);
54
133
  useEffect(() => {
@@ -85,6 +164,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
85
164
  const [isProcessingCapture, setIsProcessingCapture] = useState(false);
86
165
  const [processingImagePath, setProcessingImagePath] = useState<string | null>(null);
87
166
  const [cameraKey, setCameraKey] = useState(0);
167
+ const cameraGenerationRef = useRef(0);
88
168
  const [isRebootingCamera, setIsRebootingCamera] = useState(false);
89
169
  const [cameraType, setCameraType] = useState<'back' | 'front'>('back');
90
170
  const [showQRModal, setShowQRModal] = useState(false);
@@ -92,8 +172,15 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
92
172
  console.log(currentUrl)
93
173
  const [qrPlatform, setQrPlatform] = useState<'android' | 'ios'>('android');
94
174
  const [scanPhase, setScanPhase] = useState<'image' | 'barcode'>('image');
95
- const [pendingCAImage, setPendingCAImage] = useState<{ path: string, templateResponse: any, templatePath: string, bbox?: IBbox } | null>(null);
175
+ const [pendingCAImage, setPendingCAImage] = useState<PendingCanadianImage | null>(null);
96
176
  const scannedBarcodeRef = useRef<string | null>(null);
177
+ const barcodeImageDecodeSessionRef = useRef<number | null>(null);
178
+ const barcodeServiceDecodeSessionRef = useRef<number | null>(null);
179
+ const barcodeServiceAttemptsRef = useRef<{ session: number | null; count: number }>({ session: null, count: 0 });
180
+ const barcodeFinalizingRef = useRef(false);
181
+ const barcodeCompletedRef = useRef(false);
182
+ const barcodeScanSessionRef = useRef(0);
183
+ const nativeBarcodeDecoderWarningShownRef = useRef(false);
97
184
  const [generatedCode, setGeneratedCode] = useState<string | null>(null);
98
185
  const [isGeneratingCode, setIsGeneratingCode] = useState(false);
99
186
  const { actions, state, env, apiKey } = useTemplateKYCFlowContext();
@@ -113,19 +200,12 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
113
200
  return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
114
201
  }, [state.template.components, state.componentData]);
115
202
 
116
- const documentTypeMapping: Record<string, GovernmentDocumentType> = {
117
- 'nationalId': 'national_id', 'national_id': 'national_id', 'identity_card': 'national_id',
118
- 'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
119
- 'residencePermit': 'permanent_residence', 'permanent_residence': 'permanent_residence',
120
- 'healthInsuranceCard': 'health_insurance_card', 'health_insurance_card': 'health_insurance_card',
121
- };
122
-
123
203
  const selectedDocumentType = useMemo<{ type: GovernmentDocumentType; region: string } | null>(() => {
124
204
  if (!countrySelectionData?.documentType) return null;
125
205
  const backendDocType = countrySelectionData.documentType;
126
- const mappedType = documentTypeMapping[backendDocType] || backendDocType as GovernmentDocumentType;
206
+ const mappedType = DOCUMENT_TYPE_MAPPING[backendDocType] || backendDocType as GovernmentDocumentType;
127
207
  return { type: mappedType, region: countrySelectionData.region || 'root' };
128
- }, [countrySelectionData, documentTypeMapping]);
208
+ }, [countrySelectionData]);
129
209
 
130
210
  const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
131
211
 
@@ -187,22 +267,47 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
187
267
 
188
268
  const targetBarcodeTypes = useMemo<any[]>(() => {
189
269
  const docType = selectedDocumentType?.type;
190
- if (docType === 'health_insurance_card') return ['code-128', 'code-39', 'pdf-417'];
270
+ if (docType === 'health_insurance_card') {
271
+ return [
272
+ 'pdf-417', 'code-128', 'code-39', 'code-93', 'codabar',
273
+ // Vision Camera's Android implementation rejects `itf-14`; its `itf`
274
+ // reader already covers ITF-14 payloads.
275
+ 'ean-13', 'ean-8', 'itf', 'upc-a', 'upc-e',
276
+ ];
277
+ }
191
278
  return ['pdf-417'];
192
- }, [selectedDocumentType]);
279
+ }, [selectedDocumentType?.type]);
193
280
 
194
281
  const refreshCamera = () => {
282
+ const nextCameraGeneration = cameraGenerationRef.current + 1;
283
+ cameraGenerationRef.current = nextCameraGeneration;
284
+ barcodeScanSessionRef.current += 1;
285
+ barcodeImageDecodeSessionRef.current = null;
286
+ barcodeFinalizingRef.current = false;
287
+ barcodeCompletedRef.current = false;
288
+ scannedBarcodeRef.current = null;
289
+ isAnalyzingRef.current = false;
290
+ isProcessingCaptureRef.current = false;
291
+ setIsProcessingCapture(false);
292
+ setProcessingImagePath(null);
195
293
  setIsRebootingCamera(true);
196
- if (scanPhase === 'barcode') {
294
+ if (scanPhaseRef.current === 'barcode') {
295
+ const activeSide = currentSideRef.current;
296
+ scanPhaseRef.current = 'image';
197
297
  setScanPhase('image');
198
298
  setPendingCAImage(null);
199
- scannedBarcodeRef.current = null;
200
- setCapturedImages((prev) => { const newState = { ...prev }; delete newState[currentSide]; return newState; });
201
- if (value) { const newValue = { ...value }; delete newValue[currentSide]; onValueChange(newValue); }
299
+ const nextImages = { ...capturedImagesRef.current };
300
+ delete nextImages[activeSide];
301
+ capturedImagesRef.current = nextImages;
302
+ setCapturedImages(nextImages);
303
+ if (value) { const newValue = { ...value }; delete newValue[activeSide]; onValueChange(newValue); }
202
304
  setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: '', mrz: '', bbox: undefined });
305
+ } else {
306
+ setSilentCaptureResult(prev => ({ ...prev, success: false, isAnalyzing: false, error: '' }));
203
307
  }
204
308
  setTimeout(() => {
205
- setCameraKey(prev => prev + 1);
309
+ if (cameraGenerationRef.current !== nextCameraGeneration) return;
310
+ setCameraKey(nextCameraGeneration);
206
311
  setIsRebootingCamera(false);
207
312
  }, 1000);
208
313
  };
@@ -215,6 +320,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
215
320
  useEffect(() => {
216
321
  if (value && Object.keys(value).length > 0 && JSON.stringify(value) !== JSON.stringify(capturedImages)) {
217
322
  const updatedImages = value as Record<string, IIDCardPayload>;
323
+ capturedImagesRef.current = updatedImages;
218
324
  setCapturedImages(updatedImages);
219
325
  const currentImageData = updatedImages[currentSide];
220
326
  if (currentImageData?.dir) {
@@ -261,12 +367,132 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
261
367
  }
262
368
  };
263
369
 
370
+ const startBackCapture = () => {
371
+ sdkLogger.info('Preparing camera for back document capture');
372
+ barcodeScanSessionRef.current += 1;
373
+ barcodeImageDecodeSessionRef.current = null;
374
+ barcodeFinalizingRef.current = false;
375
+ barcodeCompletedRef.current = false;
376
+ scannedBarcodeRef.current = null;
377
+ isAnalyzingRef.current = false;
378
+ isProcessingCaptureRef.current = false;
379
+ currentSideRef.current = 'back';
380
+ scanPhaseRef.current = 'image';
381
+
382
+ const nextImages = { ...capturedImagesRef.current };
383
+ delete nextImages.back;
384
+ capturedImagesRef.current = nextImages;
385
+ setCapturedImages(nextImages);
386
+ setBboxBySide(prev => {
387
+ const next = { ...prev };
388
+ delete next.back;
389
+ return next;
390
+ });
391
+
392
+ if (value && Object.prototype.hasOwnProperty.call(value, 'back')) {
393
+ const nextValue = { ...value };
394
+ delete nextValue.back;
395
+ onValueChange(nextValue);
396
+ }
397
+
398
+ setSilentCaptureResult({
399
+ success: false,
400
+ isAnalyzing: false,
401
+ path: '',
402
+ error: '',
403
+ templatePath: '',
404
+ mrz: '',
405
+ barcode: undefined,
406
+ bbox: undefined,
407
+ });
408
+ setPendingCAImage(null);
409
+ setProcessingImagePath(null);
410
+ setIsProcessingCapture(false);
411
+ setIsRebootingCamera(false);
412
+ setCurrentSide('back');
413
+ cameraGenerationRef.current += 1;
414
+ setCameraKey(cameraGenerationRef.current);
415
+ setShowCamera(true);
416
+ actions.showCustomStepper(false);
417
+ };
418
+
419
+ const cancelCurrentCapture = () => {
420
+ cameraGenerationRef.current += 1;
421
+ barcodeScanSessionRef.current += 1;
422
+ barcodeImageDecodeSessionRef.current = null;
423
+ barcodeFinalizingRef.current = false;
424
+ barcodeCompletedRef.current = false;
425
+ scannedBarcodeRef.current = null;
426
+ isAnalyzingRef.current = false;
427
+ isProcessingCaptureRef.current = false;
428
+ scanPhaseRef.current = 'image';
429
+
430
+ const savedImage = capturedImagesRef.current[currentSideRef.current];
431
+ setScanPhase('image');
432
+ setPendingCAImage(null);
433
+ setIsProcessingCapture(false);
434
+ setProcessingImagePath(null);
435
+ setIsRebootingCamera(false);
436
+ setShowCamera(false);
437
+ setSilentCaptureResult(savedImage?.dir
438
+ ? {
439
+ path: savedImage.dir,
440
+ success: true,
441
+ isAnalyzing: false,
442
+ error: '',
443
+ mrz: savedImage.mrz || '',
444
+ templatePath: savedImage.templatePath || '',
445
+ bbox: bboxBySide[currentSideRef.current],
446
+ }
447
+ : { path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
448
+ };
449
+
450
+ const returnToFrontReview = () => {
451
+ cameraGenerationRef.current += 1;
452
+ barcodeScanSessionRef.current += 1;
453
+ barcodeImageDecodeSessionRef.current = null;
454
+ barcodeFinalizingRef.current = false;
455
+ barcodeCompletedRef.current = false;
456
+ scannedBarcodeRef.current = null;
457
+ isAnalyzingRef.current = false;
458
+ isProcessingCaptureRef.current = false;
459
+ currentSideRef.current = 'front';
460
+ scanPhaseRef.current = 'image';
461
+
462
+ const frontImage = capturedImagesRef.current.front;
463
+ setScanPhase('image');
464
+ setPendingCAImage(null);
465
+ setCurrentSide('front');
466
+ setShowCamera(false);
467
+ setIsProcessingCapture(false);
468
+ setProcessingImagePath(null);
469
+ setIsRebootingCamera(false);
470
+ setSilentCaptureResult(frontImage?.dir
471
+ ? {
472
+ path: frontImage.dir,
473
+ success: true,
474
+ isAnalyzing: false,
475
+ error: '',
476
+ mrz: frontImage.mrz || '',
477
+ templatePath: frontImage.templatePath || '',
478
+ bbox: bboxBySide.front,
479
+ }
480
+ : { path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
481
+ };
482
+
264
483
  const cameraConfig = useMemo(() => {
265
484
  let instructions = selectedDocumentType
266
485
  ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr)
267
486
  : getLocalizedText(component.instructions as Record<string, LocalizedText>);
268
487
 
269
- if (scanPhase === 'barcode') instructions = '';
488
+ if (scanPhase === 'barcode') {
489
+ instructions = '';
490
+ } else {
491
+ const stabilityHint = locale === 'en'
492
+ ? 'Position the entire ID, then hold steady. Capture starts automatically.'
493
+ : 'Positionnez la pièce entière, puis restez immobile. La capture démarre automatiquement.';
494
+ instructions = instructions ? `${instructions}\n${stabilityHint}` : stabilityHint;
495
+ }
270
496
 
271
497
  const isLandscape = screenWidth > screenHeight;
272
498
  let xMin, xMax, yMin, yMax;
@@ -306,12 +532,34 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
306
532
 
307
533
 
308
534
  const retakePicture = (sideToRetake: 'front' | 'back') => {
309
- setIsProcessingCapture(false); setProcessingImagePath(null); scannedBarcodeRef.current = null;
310
- setScanPhase('image'); setPendingCAImage(null);
535
+ barcodeScanSessionRef.current += 1;
536
+ barcodeImageDecodeSessionRef.current = null;
537
+ barcodeFinalizingRef.current = false;
538
+ barcodeCompletedRef.current = false;
539
+ scannedBarcodeRef.current = null;
540
+ isAnalyzingRef.current = false;
541
+ isProcessingCaptureRef.current = false;
542
+ currentSideRef.current = sideToRetake;
543
+ scanPhaseRef.current = 'image';
544
+ setCurrentSide(sideToRetake);
545
+ setIsProcessingCapture(false);
546
+ setProcessingImagePath(null);
547
+ setScanPhase('image');
548
+ setPendingCAImage(null);
311
549
  setSilentCaptureResult({ path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
312
- setShowCamera(true); refreshCamera(); actions.showCustomStepper(false);
313
- setCapturedImages((prev) => { const newState = { ...prev }; delete newState[sideToRetake]; return newState; });
550
+ const nextImages = { ...capturedImagesRef.current };
551
+ delete nextImages[sideToRetake];
552
+ capturedImagesRef.current = nextImages;
553
+ setCapturedImages(nextImages);
554
+ setBboxBySide(prev => {
555
+ const next = { ...prev };
556
+ delete next[sideToRetake];
557
+ return next;
558
+ });
314
559
  if (value) { const newValue = { ...value }; delete newValue[sideToRetake]; onValueChange(newValue); }
560
+ setShowCamera(true);
561
+ refreshCamera();
562
+ actions.showCustomStepper(false);
315
563
  };
316
564
 
317
565
  const getCurrentSideVerification = (side: string, countryKey: string, cData: any) => {
@@ -362,8 +610,14 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
362
610
  return found?.[side] || null;
363
611
  };
364
612
 
365
- const autoCapture = async (capturePath: string, verified: ISilentCaptureResult & { region?: string }) => {
366
- if (isProcessingCapture) return;
613
+ const autoCapture = async (
614
+ capturePath: string,
615
+ verified: ISilentCaptureResult & { region?: string },
616
+ captureSide: 'front' | 'back' = currentSideRef.current,
617
+ captureGeneration: number = cameraGenerationRef.current,
618
+ ) => {
619
+ if (isProcessingCaptureRef.current) return;
620
+ isProcessingCaptureRef.current = true;
367
621
  setIsProcessingCapture(true);
368
622
  const safeCapturePath = (Platform.OS === 'ios' && !capturePath.startsWith('file://')) ? `file://${capturePath}` : capturePath;
369
623
  setProcessingImagePath(safeCapturePath);
@@ -402,13 +656,14 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
402
656
 
403
657
  const safeCroppedPath = (Platform.OS === 'ios' && !imagePathForUpload.startsWith('file://')) ? `file://${imagePathForUpload}` : imagePathForUpload;
404
658
  const base64 = await pathToBase64(safeCroppedPath);
405
- const side = currentSideRef.current;
659
+ if (cameraGenerationRef.current !== captureGeneration) return;
660
+ const side = captureSide;
406
661
 
407
662
  const newImages = {
408
663
  ...capturedImagesRef.current,
409
664
  [side]: { dir: safeCroppedPath, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
410
665
  };
411
-
666
+ capturedImagesRef.current = newImages;
412
667
  setCapturedImages(newImages);
413
668
 
414
669
  sdkLogger.info(`User successfully captured ${side} document`);
@@ -420,17 +675,22 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
420
675
  }
421
676
 
422
677
  setTimeout(() => {
678
+ if (cameraGenerationRef.current !== captureGeneration) return;
423
679
  setShowCamera(false); actions.showCustomStepper(true);
424
680
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
681
+ isProcessingCaptureRef.current = false;
425
682
  setIsProcessingCapture(false); setProcessingImagePath(null);
683
+ scanPhaseRef.current = 'image';
426
684
  setScanPhase('image'); setPendingCAImage(null);
427
685
  }, 600);
428
686
 
429
687
  } catch (e: any) {
688
+ if (cameraGenerationRef.current !== captureGeneration) return;
430
689
  sdkLogger.error(`Auto-capture formatting/saving failed`, { error: e.message });
431
690
  const friendlyError = state.currentLanguage === 'en' ? 'Unable to process document. Please try again.' : 'Impossible de traiter le document. Veuillez réessayer.';
432
691
  showAlert('Error', friendlyError);
433
692
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false }));
693
+ isProcessingCaptureRef.current = false;
434
694
  setIsProcessingCapture(false); setProcessingImagePath(null);
435
695
  }
436
696
  };
@@ -463,14 +723,32 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
463
723
  });
464
724
  };
465
725
 
466
- const processCanadianBarcode = async (barcodeString: string) => {
467
- if (isProcessingCaptureRef.current || !pendingCAImage) return;
726
+ const processCanadianBarcode = async (barcodeString: string, pendingImage: PendingCanadianImage | null = pendingCAImage) => {
727
+ if (
728
+ scanPhaseRef.current !== 'barcode' ||
729
+ isProcessingCaptureRef.current ||
730
+ barcodeFinalizingRef.current ||
731
+ barcodeCompletedRef.current ||
732
+ !pendingImage
733
+ ) return;
734
+
735
+ const docType = selectedDocTypeRef.current;
736
+ const cData = countryDataRef.current;
737
+ if (!docType || !isUsableBarcodeText(barcodeString, docType.type === 'health_insurance_card')) return;
738
+
739
+ const scanSession = barcodeScanSessionRef.current;
740
+ const captureGeneration = cameraGenerationRef.current;
741
+ const isStaleBarcode = () => (
742
+ barcodeScanSessionRef.current !== scanSession ||
743
+ cameraGenerationRef.current !== captureGeneration ||
744
+ scanPhaseRef.current !== 'barcode'
745
+ );
746
+
747
+ barcodeFinalizingRef.current = true;
468
748
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: true, error: '' }));
469
749
 
470
750
  try {
471
- const docType = selectedDocTypeRef.current;
472
- const cData = countryDataRef.current;
473
- const isHIC = docType!.type === 'health_insurance_card';
751
+ const isHIC = docType.type === 'health_insurance_card';
474
752
  let parsedDictionary;
475
753
 
476
754
  if (isHIC && barcodeString.length < 50) {
@@ -479,27 +757,169 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
479
757
  const token = await authentification();
480
758
  parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeString, token }, env);
481
759
  }
760
+ if (isStaleBarcode()) return;
482
761
  if (!parsedDictionary) throw new Error("Could not parse the barcode data.");
762
+
763
+ barcodeCompletedRef.current = true;
483
764
 
484
765
  const verifiedResult: ISilentCaptureResult & { region?: string } = {
485
- path: pendingCAImage.path, templatePath: pendingCAImage.templatePath, bbox: pendingCAImage.bbox, success: true,
766
+ path: pendingImage.path, templatePath: pendingImage.templatePath, bbox: pendingImage.bbox, success: true,
486
767
  mrz: JSON.stringify(parsedDictionary), barcode: barcodeString, isAnalyzing: false,
487
- country: cData?.code, documentType: docType!.type, region: docType!.region !== 'root' ? docType!.region : undefined,
768
+ country: cData?.code, documentType: docType.type, region: docType.region !== 'root' ? docType.region : undefined,
488
769
  };
489
770
 
490
771
  setSilentCaptureResult(verifiedResult);
491
- if (pendingCAImage.bbox) {
492
- const safeBbox = pendingCAImage.bbox as IBbox;
772
+ if (pendingImage.bbox) {
773
+ const safeBbox = pendingImage.bbox as IBbox;
493
774
  setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
494
775
  }
495
- await autoCapture(pendingCAImage.path, verifiedResult);
776
+ await autoCapture(pendingImage.path, verifiedResult, 'back', captureGeneration);
777
+ } catch (error: any) {
778
+ if (isStaleBarcode()) return;
779
+ barcodeCompletedRef.current = false;
780
+ scannedBarcodeRef.current = null;
781
+ sdkLogger.warn('Decoded barcode could not be formatted; continuing to scan', { error: error?.message });
782
+ setSilentCaptureResult(prev => ({
783
+ ...prev,
784
+ isAnalyzing: false,
785
+ success: false,
786
+ error: state.currentLanguage === 'en'
787
+ ? 'Barcode detected but not readable yet. Hold it steady and try again.'
788
+ : 'Code-barres détecté mais pas encore lisible. Maintenez-le stable et réessayez.',
789
+ }));
790
+ } finally {
791
+ if (barcodeScanSessionRef.current === scanSession) {
792
+ barcodeFinalizingRef.current = false;
793
+ }
794
+ }
795
+ };
796
+
797
+ /**
798
+ * Live scanners favor speed and can receive a relatively small analysis
799
+ * frame. This fallback sends the camera's full-resolution still image to the
800
+ * dedicated decoder, which is much more reliable for high-density PDF417.
801
+ */
802
+ const decodeBarcodeFromImage = async (
803
+ imagePath: string,
804
+ pendingImage: PendingCanadianImage | null = pendingCAImage,
805
+ allowServiceFallback = true,
806
+ ) => {
807
+ if (
808
+ scanPhaseRef.current !== 'barcode' ||
809
+ !pendingImage ||
810
+ barcodeImageDecodeSessionRef.current === barcodeScanSessionRef.current ||
811
+ barcodeFinalizingRef.current ||
812
+ barcodeCompletedRef.current ||
813
+ isProcessingCaptureRef.current
814
+ ) return;
815
+
816
+ const docType = selectedDocTypeRef.current;
817
+ if (!docType) return;
818
+
819
+ const scanSession = barcodeScanSessionRef.current;
820
+ barcodeImageDecodeSessionRef.current = scanSession;
821
+ sdkLogger.info('Attempting high-resolution barcode decode', {
822
+ platform: Platform.OS,
823
+ session: scanSession,
824
+ });
825
+ try {
826
+ if (Platform.OS === 'android' && SanctumKeySdkModule.decodeBarcodeFromImage) {
827
+ try {
828
+ const localBarcodeText = await SanctumKeySdkModule.decodeBarcodeFromImage(
829
+ imagePath,
830
+ targetBarcodeTypes as string[],
831
+ );
832
+ if (scanSession !== barcodeScanSessionRef.current || scanPhaseRef.current !== 'barcode') return;
833
+
834
+ if (isUsableBarcodeText(localBarcodeText, docType.type === 'health_insurance_card')) {
835
+ scannedBarcodeRef.current = localBarcodeText;
836
+ sdkLogger.info('Barcode decoded by enhanced on-device fallback', {
837
+ length: localBarcodeText.length,
838
+ });
839
+ await processCanadianBarcode(localBarcodeText, pendingImage);
840
+ return;
841
+ }
842
+ sdkLogger.info('On-device decoders did not find a usable barcode in this frame', {
843
+ session: scanSession,
844
+ });
845
+ } catch (localError: any) {
846
+ sdkLogger.info('Enhanced on-device barcode fallback could not decode this frame', {
847
+ error: String(errorMessage(localError) || 'Unknown local decoder error'),
848
+ });
849
+ }
850
+ } else if (Platform.OS === 'android' && !nativeBarcodeDecoderWarningShownRef.current) {
851
+ nativeBarcodeDecoderWarningShownRef.current = true;
852
+ sdkLogger.warn('Enhanced on-device barcode decoder is unavailable; rebuild the Android app.');
853
+ }
854
+ } finally {
855
+ if (barcodeImageDecodeSessionRef.current === scanSession) {
856
+ barcodeImageDecodeSessionRef.current = null;
857
+ }
858
+ }
859
+
860
+ if (scanSession !== barcodeScanSessionRef.current || scanPhaseRef.current !== 'barcode') return;
861
+ if (!allowServiceFallback) {
862
+ sdkLogger.info('Verified back image did not yield a barcode locally; waiting for a close-up frame');
863
+ return;
864
+ }
865
+
866
+ // Do not let a slow network request block local decoding of subsequent
867
+ // high-resolution frames. At most two service calls are made per session.
868
+ if (barcodeServiceDecodeSessionRef.current === scanSession) return;
869
+ if (barcodeServiceAttemptsRef.current.session !== scanSession) {
870
+ barcodeServiceAttemptsRef.current = { session: scanSession, count: 0 };
871
+ }
872
+ if (barcodeServiceAttemptsRef.current.count >= 2) return;
873
+
874
+ barcodeServiceAttemptsRef.current.count += 1;
875
+ barcodeServiceDecodeSessionRef.current = scanSession;
876
+ try {
877
+ const token = await authentification();
878
+ if (scanSession !== barcodeScanSessionRef.current || scanPhaseRef.current !== 'barcode') return;
879
+
880
+ const backendDocType = GovernmentDocumentTypeShorted[docType.type as keyof typeof GovernmentDocumentTypeShorted] || docType.type;
881
+ const response = await kycService.extractBarcode({
882
+ fileUri: imagePath,
883
+ token,
884
+ docType: backendDocType,
885
+ postfix: 'back',
886
+ timeoutMs: 15000,
887
+ }, env);
888
+ if (scanSession !== barcodeScanSessionRef.current || scanPhaseRef.current !== 'barcode') return;
889
+
890
+ const barcodeText = getDecodedBarcodeText(response);
891
+ if (isUsableBarcodeText(barcodeText, docType.type === 'health_insurance_card')) {
892
+ scannedBarcodeRef.current = barcodeText;
893
+ sdkLogger.info('Barcode decoded by the high-resolution service fallback', { length: barcodeText.length });
894
+ await processCanadianBarcode(barcodeText, pendingImage);
895
+ } else {
896
+ sdkLogger.info('Barcode service returned no usable data; local scanning remains active', {
897
+ session: scanSession,
898
+ });
899
+ }
496
900
  } catch (error: any) {
497
- setScanPhase('image'); setPendingCAImage(null); scannedBarcodeRef.current = null;
498
- setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: 'Failed to process barcode. Please retake the photo.' }));
901
+ sdkLogger.info('Barcode service fallback failed; local scanning remains active', {
902
+ error: String(errorMessage(error) || 'Unknown barcode service error'),
903
+ });
904
+ } finally {
905
+ if (barcodeServiceDecodeSessionRef.current === scanSession) {
906
+ barcodeServiceDecodeSessionRef.current = null;
907
+ }
499
908
  }
500
909
  };
501
910
 
502
911
  const handleExitKYCFlow = () => {
912
+ cameraGenerationRef.current += 1;
913
+ barcodeScanSessionRef.current += 1;
914
+ barcodeImageDecodeSessionRef.current = null;
915
+ barcodeFinalizingRef.current = false;
916
+ barcodeCompletedRef.current = false;
917
+ scannedBarcodeRef.current = null;
918
+ isAnalyzingRef.current = false;
919
+ isProcessingCaptureRef.current = false;
920
+ currentSideRef.current = 'front';
921
+ scanPhaseRef.current = 'image';
922
+ capturedImagesRef.current = {};
503
923
  clearAuthToken();
504
924
  // 1. Clear local UI and camera states immediately
505
925
  setCapturedImages({});
@@ -507,7 +927,6 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
507
927
  setCurrentSide('front');
508
928
  setScanPhase('image');
509
929
  setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '' });
510
- scannedBarcodeRef.current = null;
511
930
  setPendingCAImage(null);
512
931
  setIsProcessingCapture(false);
513
932
  setProcessingImagePath(null);
@@ -525,9 +944,16 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
525
944
  }
526
945
  };
527
946
 
528
- const handleSilentCapture = async (result: { success: boolean; path?: string; error?: string; barcode?: unknown }) => {
947
+ const handleSilentCapture = async (
948
+ result: { success: boolean; path?: string; error?: string; barcode?: unknown },
949
+ frameGeneration: number = cameraGenerationRef.current,
950
+ ) => {
951
+ if (cameraGenerationRef.current !== frameGeneration) return;
529
952
  if (isProcessingCaptureRef.current) return;
530
- if (scanPhaseRef.current === 'barcode') return;
953
+ if (scanPhaseRef.current === 'barcode') {
954
+ if (result.success && result.path) void decodeBarcodeFromImage(result.path);
955
+ return;
956
+ }
531
957
  if (isAnalyzingRef.current) return;
532
958
 
533
959
  // 🚨 SYNCHRONOUS LOCK: Block all subsequent camera frames immediately
@@ -537,6 +963,11 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
537
963
  const captured = capturedImagesRef.current;
538
964
  const docType = selectedDocTypeRef.current;
539
965
  const cData = countryDataRef.current;
966
+ const isStaleFrame = () => (
967
+ cameraGenerationRef.current !== frameGeneration ||
968
+ currentSideRef.current !== side ||
969
+ scanPhaseRef.current !== 'image'
970
+ );
540
971
 
541
972
  if (result.success && result.path) {
542
973
  setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
@@ -555,7 +986,7 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
555
986
  const issuingSubdivision = docType.region !== 'root' ? docType.region : undefined;
556
987
 
557
988
  const watchdog = setTimeout(() => {
558
- if (isAnalyzingRef.current) {
989
+ if (!isStaleFrame() && isAnalyzingRef.current) {
559
990
  isAnalyzingRef.current = false; // 🚨 UNLOCK inside watchdog
560
991
  setSilentCaptureResult((prev) => ({
561
992
  ...prev,
@@ -575,6 +1006,10 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
575
1006
  postfix: side,
576
1007
  issuingSubdivision: issuingSubdivision
577
1008
  }, env);
1009
+ if (isStaleFrame()) {
1010
+ clearTimeout(watchdog);
1011
+ return;
1012
+ }
578
1013
 
579
1014
  templateResponse = templateType;
580
1015
  if (templateType.template_path) templatePath = templateType.template_path;
@@ -589,6 +1024,10 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
589
1024
  }
590
1025
  try {
591
1026
  const crop = await cropByObb(safeCapturePath, (templateType as any).card_obb);
1027
+ if (isStaleFrame()) {
1028
+ clearTimeout(watchdog);
1029
+ return;
1030
+ }
592
1031
  templateBbox = crop.bbox;
593
1032
  } catch {}
594
1033
  }
@@ -634,6 +1073,11 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
634
1073
  }, env);
635
1074
  }
636
1075
 
1076
+ if (isStaleFrame()) {
1077
+ clearTimeout(watchdog);
1078
+ return;
1079
+ }
1080
+
637
1081
  const bbox = verificationRes?.bbox || templateBbox;
638
1082
  const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
639
1083
 
@@ -650,6 +1094,10 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
650
1094
  const token = await authentification();
651
1095
  parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
652
1096
  }
1097
+ if (isStaleFrame()) {
1098
+ clearTimeout(watchdog);
1099
+ return;
1100
+ }
653
1101
  const verifiedResult: ISilentCaptureResult & { region?: string } = {
654
1102
  path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
655
1103
  mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
@@ -661,16 +1109,26 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
661
1109
  if (bbox) setBboxBySide(prev => ({ ...prev, ['back']: bbox as IBbox }));
662
1110
 
663
1111
  isAnalyzingRef.current = false; // 🚨 UNLOCK on success
664
- await autoCapture(safeCapturePath!, verifiedResult);
1112
+ await autoCapture(safeCapturePath!, verifiedResult, side, frameGeneration);
665
1113
  return;
666
1114
  }
667
1115
 
668
- sdkLogger.info(`Verification passed. Waiting for barcode scan phase.`);
1116
+ sdkLogger.info('Verification passed. Starting barcode scan phase.', {
1117
+ platform: Platform.OS,
1118
+ formats: targetBarcodeTypes,
1119
+ });
669
1120
  clearTimeout(watchdog);
670
- setPendingCAImage({ path: safeCapturePath!, templateResponse, templatePath, bbox });
1121
+ const pendingImage = { path: safeCapturePath!, templateResponse, templatePath, bbox };
1122
+ barcodeScanSessionRef.current += 1;
1123
+ barcodeCompletedRef.current = false;
1124
+ scanPhaseRef.current = 'barcode';
1125
+ setPendingCAImage(pendingImage);
671
1126
  setScanPhase('barcode');
672
1127
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
673
1128
  isAnalyzingRef.current = false; // 🚨 UNLOCK before phase shift
1129
+ if (Platform.OS === 'android') {
1130
+ void decodeBarcodeFromImage(safeCapturePath!, pendingImage, false);
1131
+ }
674
1132
  return;
675
1133
  }
676
1134
 
@@ -684,10 +1142,11 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
684
1142
  if (bbox) setBboxBySide(prev => ({ ...prev, [side]: bbox }));
685
1143
 
686
1144
  isAnalyzingRef.current = false; // 🚨 UNLOCK on final success
687
- await autoCapture(safeCapturePath, verifiedResult);
1145
+ await autoCapture(safeCapturePath, verifiedResult, side, frameGeneration);
688
1146
 
689
1147
  } catch (error: any) {
690
1148
  clearTimeout(watchdog);
1149
+ if (isStaleFrame()) return;
691
1150
  const rawMessage = (typeof error === 'string' ? error : (error?.message || '')).toLowerCase();
692
1151
  const statusCode = error?.response?.status || error?.status;
693
1152
 
@@ -741,11 +1200,12 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
741
1200
  useEffect(() => {
742
1201
  if (scanPhase === 'barcode' && pendingCAImage && silentCaptureResult.barcode) {
743
1202
  const bData = silentCaptureResult.barcode;
744
- if (typeof bData === 'string' && bData.length > 50) {
1203
+ const allowShortBarcode = selectedDocumentType?.type === 'health_insurance_card';
1204
+ if (isUsableBarcodeText(bData, allowShortBarcode)) {
745
1205
  processCanadianBarcode(bData);
746
1206
  }
747
1207
  }
748
- }, [scanPhase, pendingCAImage, silentCaptureResult.barcode]);
1208
+ }, [scanPhase, pendingCAImage, silentCaptureResult.barcode, selectedDocumentType?.type]);
749
1209
 
750
1210
  const handleError = (event: { message: string, name?: string, code?: string }) => {
751
1211
  const errString = `${event.message} ${event.name} ${event.code}`.toLowerCase();
@@ -756,7 +1216,7 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
756
1216
 
757
1217
  sdkLogger.error("Hardware Camera Error encountered", { ...event, friendlyMessage }); // 🚨 LOG
758
1218
  showAlert(state.currentLanguage === 'en' ? 'Camera Error' : 'Erreur', friendlyMessage);
759
- setShowCamera(false); setIsProcessingCapture(false);
1219
+ cancelCurrentCapture();
760
1220
  };
761
1221
 
762
1222
  useEffect(() => { actions.showCustomStepper(!showCamera); }, [showCamera]);
@@ -799,15 +1259,35 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
799
1259
  </View>
800
1260
  )}
801
1261
  <EnhancedCameraView
802
- key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType}
1262
+ key={`${currentSide}-${scanPhase}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType}
803
1263
  style={{ ...StyleSheet.absoluteFillObject, ...(Platform.OS === 'web' ? { width: '100%', height: '100%', left: 0, top: 0 } as any : {}) }}
804
- onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult}
805
- isScanningBarcode={currentSide === 'back'} barcodeTypes={targetBarcodeTypes}
1264
+ onError={handleError}
1265
+ onSilentCapture={(result) => {
1266
+ if (
1267
+ cameraGenerationRef.current !== cameraKey ||
1268
+ currentSideRef.current !== currentSide ||
1269
+ scanPhaseRef.current !== scanPhase
1270
+ ) {
1271
+ sdkLogger.info('Discarded a stale camera frame after capture-mode transition', {
1272
+ frameGeneration: cameraKey,
1273
+ activeGeneration: cameraGenerationRef.current,
1274
+ frameSide: currentSide,
1275
+ activeSide: currentSideRef.current,
1276
+ framePhase: scanPhase,
1277
+ activePhase: scanPhaseRef.current,
1278
+ });
1279
+ return;
1280
+ }
1281
+ void handleSilentCapture(result, cameraKey);
1282
+ }}
1283
+ silentCaptureResult={silentCaptureResult}
1284
+ captureStabilizationDelayMs={scanPhase === 'barcode' ? 3000 : 4000}
1285
+ isScanningBarcode={currentSide === 'back' && scanPhase === 'barcode'} barcodeTypes={targetBarcodeTypes}
806
1286
  onBarcodeScanned={(barcodeString) => {
807
- const docTypeStr = selectedDocumentType?.type;
808
- const isFullDataDoc = docTypeStr === 'drivers_licence' || docTypeStr === 'national_id' || docTypeStr === 'identity_card';
809
- if (isFullDataDoc && barcodeString.length < 50) return;
1287
+ const allowShortBarcode = selectedDocumentType?.type === 'health_insurance_card';
1288
+ if (!isUsableBarcodeText(barcodeString, allowShortBarcode)) return;
810
1289
  if (scannedBarcodeRef.current !== barcodeString) {
1290
+ sdkLogger.info('Barcode detected by live camera scanner', { length: barcodeString.length });
811
1291
  scannedBarcodeRef.current = barcodeString;
812
1292
  if (scanPhase === 'barcode' && pendingCAImage) {
813
1293
  processCanadianBarcode(barcodeString);
@@ -823,14 +1303,11 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
823
1303
  stepperProps={{
824
1304
  back: () => {
825
1305
  if (currentSide === 'back') {
826
- scannedBarcodeRef.current = null; setScanPhase('image'); setPendingCAImage(null); setCurrentSide('front'); setShowCamera(false); setIsProcessingCapture(false); setProcessingImagePath(null);
827
- if (capturedImages['front']?.dir) {
828
- const frontImage = capturedImages['front'];
829
- setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
830
- } else {
831
- setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' }));
832
- }
833
- } else { actions.previousComponent(); }
1306
+ returnToFrontReview();
1307
+ } else {
1308
+ cancelCurrentCapture();
1309
+ actions.previousComponent();
1310
+ }
834
1311
  },
835
1312
  selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '', step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
836
1313
  }}
@@ -843,7 +1320,11 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
843
1320
  <View style={styles.barcodeIconContainer}><Text style={styles.barcodeIcon}>✓</Text></View>
844
1321
  <View style={styles.barcodeTextContainer}>
845
1322
  <Text style={styles.barcodePromptTitle}>{state.currentLanguage === 'en' ? 'Image Verified' : 'Image Vérifiée'}</Text>
846
- <Text style={styles.barcodePromptDesc}>{state.currentLanguage === 'en' ? 'Move closer to fill the screen with the 2D barcode.' : 'Rapprochez-vous pour scanner le code-barres 2D.'}</Text>
1323
+ <Text style={styles.barcodePromptDesc}>
1324
+ {state.currentLanguage === 'en'
1325
+ ? 'Proceed to scan the barcode. Move closer but ensure it is fully visible in the frame.'
1326
+ : 'Procédez au scan du code-barres. Rapprochez-vous, mais assurez-vous qu\'il soit entièrement visible dans le cadre.'}
1327
+ </Text>
847
1328
  </View>
848
1329
  </View>
849
1330
  </View>
@@ -869,14 +1350,14 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
869
1350
  <View style={styles.webBottomControlBar}>
870
1351
  {silentCaptureResult.error && <View style={styles.floatingErrorBanner}><Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text></View>}
871
1352
  <View style={styles.webActionButtonsRow}>
872
- <TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}><Text style={styles.webSecondaryButtonText}>Cancel</Text></TouchableOpacity>
1353
+ <TouchableOpacity style={styles.webSecondaryButton} onPress={cancelCurrentCapture}><Text style={styles.webSecondaryButtonText}>Cancel</Text></TouchableOpacity>
873
1354
  <TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}><Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text></TouchableOpacity>
874
1355
  </View>
875
1356
  </View>
876
1357
  ) : !isBusy ? (
877
1358
  <View style={styles.escapeHatchContainer}>
878
1359
  <TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}><Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text></TouchableOpacity>
879
- <TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}><Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text></TouchableOpacity>
1360
+ <TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={cancelCurrentCapture}><Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text></TouchableOpacity>
880
1361
  </View>
881
1362
  ) : null}
882
1363
  {silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (
@@ -933,14 +1414,10 @@ const handleSilentCapture = async (result: { success: boolean; path?: string; er
933
1414
  documentType: selectedDocumentType.type
934
1415
  });
935
1416
  actions.nextComponent();
1417
+ } else {
1418
+ sdkLogger.info("User finished front verification, proceeding to back");
1419
+ startBackCapture();
936
1420
  }
937
- else {
938
- sdkLogger.info("User finished front verification, proceeding to back");
939
- setShowCamera(true);
940
- refreshCamera();
941
- setCurrentSide('back');
942
- setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined }); setIsProcessingCapture(false); setProcessingImagePath(null); setScanPhase('image'); setPendingCAImage(null);
943
- }
944
1421
  }}
945
1422
  variant="primary" size="large" fullWidth />
946
1423
  </>
@@ -1234,4 +1711,4 @@ const styles = StyleSheet.create({
1234
1711
  color: '#EF4444', // Red to indicate urgency
1235
1712
  fontWeight: '600',
1236
1713
  },
1237
- });
1714
+ });