@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,21 +15,88 @@ 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
  const ISO_TO_COUNTRY_NAME = {
23
24
  'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada',
24
25
  'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain',
25
26
  'CI': 'Ivory Coast', 'SN': 'Senegal', 'TG': 'Togo', 'ML': 'Mali'
26
27
  };
28
+ const BARCODE_RESPONSE_TEXT_KEYS = [
29
+ 'barcode_data',
30
+ 'raw_barcode',
31
+ 'raw_data',
32
+ 'decoded_data',
33
+ 'decoded_text',
34
+ 'barcode',
35
+ 'text',
36
+ 'value',
37
+ 'data',
38
+ 'detail',
39
+ ];
40
+ /**
41
+ * The image decoder has returned a few slightly different response envelopes
42
+ * over time. Keep that API variation out of the capture flow and only accept
43
+ * values from fields that are known to contain the raw barcode payload.
44
+ */
45
+ const getDecodedBarcodeText = (response, depth = 0) => {
46
+ if (typeof response === 'string') {
47
+ return response.trim().length > 0 ? response : null;
48
+ }
49
+ if (!response || typeof response !== 'object' || depth > 3)
50
+ return null;
51
+ if (Array.isArray(response)) {
52
+ for (const item of response) {
53
+ const value = getDecodedBarcodeText(item, depth + 1);
54
+ if (value)
55
+ return value;
56
+ }
57
+ return null;
58
+ }
59
+ const record = response;
60
+ for (const key of BARCODE_RESPONSE_TEXT_KEYS) {
61
+ const value = record[key];
62
+ if (typeof value === 'string' && value.trim().length > 0)
63
+ return value;
64
+ }
65
+ for (const key of ['result', 'data', 'payload', 'barcodes', 'results']) {
66
+ const nested = record[key];
67
+ if (nested && typeof nested === 'object') {
68
+ const value = getDecodedBarcodeText(nested, depth + 1);
69
+ if (value)
70
+ return value;
71
+ }
72
+ }
73
+ return null;
74
+ };
75
+ const isUsableBarcodeText = (value, allowShortBarcode) => {
76
+ if (typeof value !== 'string')
77
+ return false;
78
+ const trimmed = value.trim();
79
+ const isDecoderError = /^(no (?:bar)?code|barcode (?:not|was not)|unable to decode|failed to decode|error\b)/i.test(trimmed);
80
+ return !isDecoderError && trimmed.length > 0 && (allowShortBarcode || value.length > 50);
81
+ };
27
82
  const SHORT_TO_LONG_REGION = {
28
83
  "AB": "Alberta", "BC": "British Columbia", "MB": "Manitoba", "NB": "New Brunswick",
29
84
  "NL": "Newfoundland and Labrador", "NT": "Northwest Territories", "NS": "Nova Scotia",
30
85
  "NU": "Nunavut", "ON": "Ontario", "PE": "Prince Edward Island", "QC": "Quebec",
31
86
  "SK": "Saskatchewan", "YT": "Yukon"
32
87
  };
88
+ const DOCUMENT_TYPE_MAPPING = {
89
+ nationalId: 'national_id',
90
+ national_id: 'national_id',
91
+ identity_card: 'national_id',
92
+ passport: 'passport',
93
+ driversLicense: 'drivers_licence',
94
+ drivers_licence: 'drivers_licence',
95
+ residencePermit: 'permanent_residence',
96
+ permanent_residence: 'permanent_residence',
97
+ healthInsuranceCard: 'health_insurance_card',
98
+ health_insurance_card: 'health_insurance_card',
99
+ };
33
100
  const ExpiringCodeTimer = ({ initialCode, language }) => {
34
101
  const [timeLeft, setTimeLeft] = useState(300);
35
102
  useEffect(() => {
@@ -62,6 +129,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
62
129
  const [isProcessingCapture, setIsProcessingCapture] = useState(false);
63
130
  const [processingImagePath, setProcessingImagePath] = useState(null);
64
131
  const [cameraKey, setCameraKey] = useState(0);
132
+ const cameraGenerationRef = useRef(0);
65
133
  const [isRebootingCamera, setIsRebootingCamera] = useState(false);
66
134
  const [cameraType, setCameraType] = useState('back');
67
135
  const [showQRModal, setShowQRModal] = useState(false);
@@ -71,6 +139,13 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
71
139
  const [scanPhase, setScanPhase] = useState('image');
72
140
  const [pendingCAImage, setPendingCAImage] = useState(null);
73
141
  const scannedBarcodeRef = useRef(null);
142
+ const barcodeImageDecodeSessionRef = useRef(null);
143
+ const barcodeServiceDecodeSessionRef = useRef(null);
144
+ const barcodeServiceAttemptsRef = useRef({ session: null, count: 0 });
145
+ const barcodeFinalizingRef = useRef(false);
146
+ const barcodeCompletedRef = useRef(false);
147
+ const barcodeScanSessionRef = useRef(0);
148
+ const nativeBarcodeDecoderWarningShownRef = useRef(false);
74
149
  const [generatedCode, setGeneratedCode] = useState(null);
75
150
  const [isGeneratingCode, setIsGeneratingCode] = useState(false);
76
151
  const { actions, state, env, apiKey } = useTemplateKYCFlowContext();
@@ -84,19 +159,13 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
84
159
  const countrySelectionComponent = state.template.components.find(c => c.type === 'country_selection');
85
160
  return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
86
161
  }, [state.template.components, state.componentData]);
87
- const documentTypeMapping = {
88
- 'nationalId': 'national_id', 'national_id': 'national_id', 'identity_card': 'national_id',
89
- 'passport': 'passport', 'driversLicense': 'drivers_licence', 'drivers_licence': 'drivers_licence',
90
- 'residencePermit': 'permanent_residence', 'permanent_residence': 'permanent_residence',
91
- 'healthInsuranceCard': 'health_insurance_card', 'health_insurance_card': 'health_insurance_card',
92
- };
93
162
  const selectedDocumentType = useMemo(() => {
94
163
  if (!countrySelectionData?.documentType)
95
164
  return null;
96
165
  const backendDocType = countrySelectionData.documentType;
97
- const mappedType = documentTypeMapping[backendDocType] || backendDocType;
166
+ const mappedType = DOCUMENT_TYPE_MAPPING[backendDocType] || backendDocType;
98
167
  return { type: mappedType, region: countrySelectionData.region || 'root' };
99
- }, [countrySelectionData, documentTypeMapping]);
168
+ }, [countrySelectionData]);
100
169
  const countryData = useMemo(() => countrySelectionData, [countrySelectionData]);
101
170
  // ==========================================
102
171
  // 🚨 CRITICAL FIX: REF MIRRORS FOR IOS BRIDGE
@@ -160,26 +229,52 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
160
229
  }, [countryData?.code, selectedDocumentType?.type]);
161
230
  const targetBarcodeTypes = useMemo(() => {
162
231
  const docType = selectedDocumentType?.type;
163
- if (docType === 'health_insurance_card')
164
- return ['code-128', 'code-39', 'pdf-417'];
232
+ if (docType === 'health_insurance_card') {
233
+ return [
234
+ 'pdf-417', 'code-128', 'code-39', 'code-93', 'codabar',
235
+ // Vision Camera's Android implementation rejects `itf-14`; its `itf`
236
+ // reader already covers ITF-14 payloads.
237
+ 'ean-13', 'ean-8', 'itf', 'upc-a', 'upc-e',
238
+ ];
239
+ }
165
240
  return ['pdf-417'];
166
- }, [selectedDocumentType]);
241
+ }, [selectedDocumentType?.type]);
167
242
  const refreshCamera = () => {
243
+ const nextCameraGeneration = cameraGenerationRef.current + 1;
244
+ cameraGenerationRef.current = nextCameraGeneration;
245
+ barcodeScanSessionRef.current += 1;
246
+ barcodeImageDecodeSessionRef.current = null;
247
+ barcodeFinalizingRef.current = false;
248
+ barcodeCompletedRef.current = false;
249
+ scannedBarcodeRef.current = null;
250
+ isAnalyzingRef.current = false;
251
+ isProcessingCaptureRef.current = false;
252
+ setIsProcessingCapture(false);
253
+ setProcessingImagePath(null);
168
254
  setIsRebootingCamera(true);
169
- if (scanPhase === 'barcode') {
255
+ if (scanPhaseRef.current === 'barcode') {
256
+ const activeSide = currentSideRef.current;
257
+ scanPhaseRef.current = 'image';
170
258
  setScanPhase('image');
171
259
  setPendingCAImage(null);
172
- scannedBarcodeRef.current = null;
173
- setCapturedImages((prev) => { const newState = { ...prev }; delete newState[currentSide]; return newState; });
260
+ const nextImages = { ...capturedImagesRef.current };
261
+ delete nextImages[activeSide];
262
+ capturedImagesRef.current = nextImages;
263
+ setCapturedImages(nextImages);
174
264
  if (value) {
175
265
  const newValue = { ...value };
176
- delete newValue[currentSide];
266
+ delete newValue[activeSide];
177
267
  onValueChange(newValue);
178
268
  }
179
269
  setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: '', mrz: '', bbox: undefined });
180
270
  }
271
+ else {
272
+ setSilentCaptureResult(prev => ({ ...prev, success: false, isAnalyzing: false, error: '' }));
273
+ }
181
274
  setTimeout(() => {
182
- setCameraKey(prev => prev + 1);
275
+ if (cameraGenerationRef.current !== nextCameraGeneration)
276
+ return;
277
+ setCameraKey(nextCameraGeneration);
183
278
  setIsRebootingCamera(false);
184
279
  }, 1000);
185
280
  };
@@ -190,6 +285,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
190
285
  useEffect(() => {
191
286
  if (value && Object.keys(value).length > 0 && JSON.stringify(value) !== JSON.stringify(capturedImages)) {
192
287
  const updatedImages = value;
288
+ capturedImagesRef.current = updatedImages;
193
289
  setCapturedImages(updatedImages);
194
290
  const currentImageData = updatedImages[currentSide];
195
291
  if (currentImageData?.dir) {
@@ -232,12 +328,124 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
232
328
  actions.showCustomStepper(false);
233
329
  }
234
330
  };
331
+ const startBackCapture = () => {
332
+ sdkLogger.info('Preparing camera for back document capture');
333
+ barcodeScanSessionRef.current += 1;
334
+ barcodeImageDecodeSessionRef.current = null;
335
+ barcodeFinalizingRef.current = false;
336
+ barcodeCompletedRef.current = false;
337
+ scannedBarcodeRef.current = null;
338
+ isAnalyzingRef.current = false;
339
+ isProcessingCaptureRef.current = false;
340
+ currentSideRef.current = 'back';
341
+ scanPhaseRef.current = 'image';
342
+ const nextImages = { ...capturedImagesRef.current };
343
+ delete nextImages.back;
344
+ capturedImagesRef.current = nextImages;
345
+ setCapturedImages(nextImages);
346
+ setBboxBySide(prev => {
347
+ const next = { ...prev };
348
+ delete next.back;
349
+ return next;
350
+ });
351
+ if (value && Object.prototype.hasOwnProperty.call(value, 'back')) {
352
+ const nextValue = { ...value };
353
+ delete nextValue.back;
354
+ onValueChange(nextValue);
355
+ }
356
+ setSilentCaptureResult({
357
+ success: false,
358
+ isAnalyzing: false,
359
+ path: '',
360
+ error: '',
361
+ templatePath: '',
362
+ mrz: '',
363
+ barcode: undefined,
364
+ bbox: undefined,
365
+ });
366
+ setPendingCAImage(null);
367
+ setProcessingImagePath(null);
368
+ setIsProcessingCapture(false);
369
+ setIsRebootingCamera(false);
370
+ setCurrentSide('back');
371
+ cameraGenerationRef.current += 1;
372
+ setCameraKey(cameraGenerationRef.current);
373
+ setShowCamera(true);
374
+ actions.showCustomStepper(false);
375
+ };
376
+ const cancelCurrentCapture = () => {
377
+ cameraGenerationRef.current += 1;
378
+ barcodeScanSessionRef.current += 1;
379
+ barcodeImageDecodeSessionRef.current = null;
380
+ barcodeFinalizingRef.current = false;
381
+ barcodeCompletedRef.current = false;
382
+ scannedBarcodeRef.current = null;
383
+ isAnalyzingRef.current = false;
384
+ isProcessingCaptureRef.current = false;
385
+ scanPhaseRef.current = 'image';
386
+ const savedImage = capturedImagesRef.current[currentSideRef.current];
387
+ setScanPhase('image');
388
+ setPendingCAImage(null);
389
+ setIsProcessingCapture(false);
390
+ setProcessingImagePath(null);
391
+ setIsRebootingCamera(false);
392
+ setShowCamera(false);
393
+ setSilentCaptureResult(savedImage?.dir
394
+ ? {
395
+ path: savedImage.dir,
396
+ success: true,
397
+ isAnalyzing: false,
398
+ error: '',
399
+ mrz: savedImage.mrz || '',
400
+ templatePath: savedImage.templatePath || '',
401
+ bbox: bboxBySide[currentSideRef.current],
402
+ }
403
+ : { path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
404
+ };
405
+ const returnToFrontReview = () => {
406
+ cameraGenerationRef.current += 1;
407
+ barcodeScanSessionRef.current += 1;
408
+ barcodeImageDecodeSessionRef.current = null;
409
+ barcodeFinalizingRef.current = false;
410
+ barcodeCompletedRef.current = false;
411
+ scannedBarcodeRef.current = null;
412
+ isAnalyzingRef.current = false;
413
+ isProcessingCaptureRef.current = false;
414
+ currentSideRef.current = 'front';
415
+ scanPhaseRef.current = 'image';
416
+ const frontImage = capturedImagesRef.current.front;
417
+ setScanPhase('image');
418
+ setPendingCAImage(null);
419
+ setCurrentSide('front');
420
+ setShowCamera(false);
421
+ setIsProcessingCapture(false);
422
+ setProcessingImagePath(null);
423
+ setIsRebootingCamera(false);
424
+ setSilentCaptureResult(frontImage?.dir
425
+ ? {
426
+ path: frontImage.dir,
427
+ success: true,
428
+ isAnalyzing: false,
429
+ error: '',
430
+ mrz: frontImage.mrz || '',
431
+ templatePath: frontImage.templatePath || '',
432
+ bbox: bboxBySide.front,
433
+ }
434
+ : { path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
435
+ };
235
436
  const cameraConfig = useMemo(() => {
236
437
  let instructions = selectedDocumentType
237
438
  ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr)
238
439
  : getLocalizedText(component.instructions);
239
- if (scanPhase === 'barcode')
440
+ if (scanPhase === 'barcode') {
240
441
  instructions = '';
442
+ }
443
+ else {
444
+ const stabilityHint = locale === 'en'
445
+ ? 'Position the entire ID, then hold steady. Capture starts automatically.'
446
+ : 'Positionnez la pièce entière, puis restez immobile. La capture démarre automatiquement.';
447
+ instructions = instructions ? `${instructions}\n${stabilityHint}` : stabilityHint;
448
+ }
241
449
  const isLandscape = screenWidth > screenHeight;
242
450
  let xMin, xMax, yMin, yMax;
243
451
  if (Platform.OS === 'web') {
@@ -272,21 +480,38 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
272
480
  };
273
481
  }, [selectedDocumentType, locale, component.instructions, cameraType, screenWidth, screenHeight, scanPhase]);
274
482
  const retakePicture = (sideToRetake) => {
483
+ barcodeScanSessionRef.current += 1;
484
+ barcodeImageDecodeSessionRef.current = null;
485
+ barcodeFinalizingRef.current = false;
486
+ barcodeCompletedRef.current = false;
487
+ scannedBarcodeRef.current = null;
488
+ isAnalyzingRef.current = false;
489
+ isProcessingCaptureRef.current = false;
490
+ currentSideRef.current = sideToRetake;
491
+ scanPhaseRef.current = 'image';
492
+ setCurrentSide(sideToRetake);
275
493
  setIsProcessingCapture(false);
276
494
  setProcessingImagePath(null);
277
- scannedBarcodeRef.current = null;
278
495
  setScanPhase('image');
279
496
  setPendingCAImage(null);
280
497
  setSilentCaptureResult({ path: '', success: false, isAnalyzing: false, error: '', templatePath: '', mrz: '', bbox: undefined });
281
- setShowCamera(true);
282
- refreshCamera();
283
- actions.showCustomStepper(false);
284
- setCapturedImages((prev) => { const newState = { ...prev }; delete newState[sideToRetake]; return newState; });
498
+ const nextImages = { ...capturedImagesRef.current };
499
+ delete nextImages[sideToRetake];
500
+ capturedImagesRef.current = nextImages;
501
+ setCapturedImages(nextImages);
502
+ setBboxBySide(prev => {
503
+ const next = { ...prev };
504
+ delete next[sideToRetake];
505
+ return next;
506
+ });
285
507
  if (value) {
286
508
  const newValue = { ...value };
287
509
  delete newValue[sideToRetake];
288
510
  onValueChange(newValue);
289
511
  }
512
+ setShowCamera(true);
513
+ refreshCamera();
514
+ actions.showCustomStepper(false);
290
515
  };
291
516
  const getCurrentSideVerification = (side, countryKey, cData) => {
292
517
  const rawDocType = cData?.documentType;
@@ -339,9 +564,10 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
339
564
  const found = mapping[regionKey].find((item) => item.py_file === pyName);
340
565
  return found?.[side] || null;
341
566
  };
342
- const autoCapture = async (capturePath, verified) => {
343
- if (isProcessingCapture)
567
+ const autoCapture = async (capturePath, verified, captureSide = currentSideRef.current, captureGeneration = cameraGenerationRef.current) => {
568
+ if (isProcessingCaptureRef.current)
344
569
  return;
570
+ isProcessingCaptureRef.current = true;
345
571
  setIsProcessingCapture(true);
346
572
  const safeCapturePath = (Platform.OS === 'ios' && !capturePath.startsWith('file://')) ? `file://${capturePath}` : capturePath;
347
573
  setProcessingImagePath(safeCapturePath);
@@ -374,11 +600,14 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
374
600
  }
375
601
  const safeCroppedPath = (Platform.OS === 'ios' && !imagePathForUpload.startsWith('file://')) ? `file://${imagePathForUpload}` : imagePathForUpload;
376
602
  const base64 = await pathToBase64(safeCroppedPath);
377
- const side = currentSideRef.current;
603
+ if (cameraGenerationRef.current !== captureGeneration)
604
+ return;
605
+ const side = captureSide;
378
606
  const newImages = {
379
607
  ...capturedImagesRef.current,
380
608
  [side]: { dir: safeCroppedPath, file: base64, mrz: verified.mrz || "", templatePath: verified.templatePath },
381
609
  };
610
+ capturedImagesRef.current = newImages;
382
611
  setCapturedImages(newImages);
383
612
  sdkLogger.info(`User successfully captured ${side} document`);
384
613
  if (verified.country && verified.documentType) {
@@ -387,20 +616,27 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
387
616
  onValueChange({ ...newImages, country: verified.country, documentType: resolvedDocumentType, ...(verified.region ? { region: verified.region } : {}) });
388
617
  }
389
618
  setTimeout(() => {
619
+ if (cameraGenerationRef.current !== captureGeneration)
620
+ return;
390
621
  setShowCamera(false);
391
622
  actions.showCustomStepper(true);
392
623
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
624
+ isProcessingCaptureRef.current = false;
393
625
  setIsProcessingCapture(false);
394
626
  setProcessingImagePath(null);
627
+ scanPhaseRef.current = 'image';
395
628
  setScanPhase('image');
396
629
  setPendingCAImage(null);
397
630
  }, 600);
398
631
  }
399
632
  catch (e) {
633
+ if (cameraGenerationRef.current !== captureGeneration)
634
+ return;
400
635
  sdkLogger.error(`Auto-capture formatting/saving failed`, { error: e.message });
401
636
  const friendlyError = state.currentLanguage === 'en' ? 'Unable to process document. Please try again.' : 'Impossible de traiter le document. Veuillez réessayer.';
402
637
  showAlert('Error', friendlyError);
403
638
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false }));
639
+ isProcessingCaptureRef.current = false;
404
640
  setIsProcessingCapture(false);
405
641
  setProcessingImagePath(null);
406
642
  }
@@ -433,13 +669,25 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
433
669
  return method.includes('BARCODE') || method.includes('2D');
434
670
  });
435
671
  };
436
- const processCanadianBarcode = async (barcodeString) => {
437
- if (isProcessingCaptureRef.current || !pendingCAImage)
672
+ const processCanadianBarcode = async (barcodeString, pendingImage = pendingCAImage) => {
673
+ if (scanPhaseRef.current !== 'barcode' ||
674
+ isProcessingCaptureRef.current ||
675
+ barcodeFinalizingRef.current ||
676
+ barcodeCompletedRef.current ||
677
+ !pendingImage)
438
678
  return;
679
+ const docType = selectedDocTypeRef.current;
680
+ const cData = countryDataRef.current;
681
+ if (!docType || !isUsableBarcodeText(barcodeString, docType.type === 'health_insurance_card'))
682
+ return;
683
+ const scanSession = barcodeScanSessionRef.current;
684
+ const captureGeneration = cameraGenerationRef.current;
685
+ const isStaleBarcode = () => (barcodeScanSessionRef.current !== scanSession ||
686
+ cameraGenerationRef.current !== captureGeneration ||
687
+ scanPhaseRef.current !== 'barcode');
688
+ barcodeFinalizingRef.current = true;
439
689
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: true, error: '' }));
440
690
  try {
441
- const docType = selectedDocTypeRef.current;
442
- const cData = countryDataRef.current;
443
691
  const isHIC = docType.type === 'health_insurance_card';
444
692
  let parsedDictionary;
445
693
  if (isHIC && barcodeString.length < 50) {
@@ -449,28 +697,166 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
449
697
  const token = await authentification();
450
698
  parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeString, token }, env);
451
699
  }
700
+ if (isStaleBarcode())
701
+ return;
452
702
  if (!parsedDictionary)
453
703
  throw new Error("Could not parse the barcode data.");
704
+ barcodeCompletedRef.current = true;
454
705
  const verifiedResult = {
455
- path: pendingCAImage.path, templatePath: pendingCAImage.templatePath, bbox: pendingCAImage.bbox, success: true,
706
+ path: pendingImage.path, templatePath: pendingImage.templatePath, bbox: pendingImage.bbox, success: true,
456
707
  mrz: JSON.stringify(parsedDictionary), barcode: barcodeString, isAnalyzing: false,
457
708
  country: cData?.code, documentType: docType.type, region: docType.region !== 'root' ? docType.region : undefined,
458
709
  };
459
710
  setSilentCaptureResult(verifiedResult);
460
- if (pendingCAImage.bbox) {
461
- const safeBbox = pendingCAImage.bbox;
711
+ if (pendingImage.bbox) {
712
+ const safeBbox = pendingImage.bbox;
462
713
  setBboxBySide(prev => ({ ...prev, ['back']: safeBbox }));
463
714
  }
464
- await autoCapture(pendingCAImage.path, verifiedResult);
715
+ await autoCapture(pendingImage.path, verifiedResult, 'back', captureGeneration);
465
716
  }
466
717
  catch (error) {
467
- setScanPhase('image');
468
- setPendingCAImage(null);
718
+ if (isStaleBarcode())
719
+ return;
720
+ barcodeCompletedRef.current = false;
469
721
  scannedBarcodeRef.current = null;
470
- setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: 'Failed to process barcode. Please retake the photo.' }));
722
+ sdkLogger.warn('Decoded barcode could not be formatted; continuing to scan', { error: error?.message });
723
+ setSilentCaptureResult(prev => ({
724
+ ...prev,
725
+ isAnalyzing: false,
726
+ success: false,
727
+ error: state.currentLanguage === 'en'
728
+ ? 'Barcode detected but not readable yet. Hold it steady and try again.'
729
+ : 'Code-barres détecté mais pas encore lisible. Maintenez-le stable et réessayez.',
730
+ }));
731
+ }
732
+ finally {
733
+ if (barcodeScanSessionRef.current === scanSession) {
734
+ barcodeFinalizingRef.current = false;
735
+ }
736
+ }
737
+ };
738
+ /**
739
+ * Live scanners favor speed and can receive a relatively small analysis
740
+ * frame. This fallback sends the camera's full-resolution still image to the
741
+ * dedicated decoder, which is much more reliable for high-density PDF417.
742
+ */
743
+ const decodeBarcodeFromImage = async (imagePath, pendingImage = pendingCAImage, allowServiceFallback = true) => {
744
+ if (scanPhaseRef.current !== 'barcode' ||
745
+ !pendingImage ||
746
+ barcodeImageDecodeSessionRef.current === barcodeScanSessionRef.current ||
747
+ barcodeFinalizingRef.current ||
748
+ barcodeCompletedRef.current ||
749
+ isProcessingCaptureRef.current)
750
+ return;
751
+ const docType = selectedDocTypeRef.current;
752
+ if (!docType)
753
+ return;
754
+ const scanSession = barcodeScanSessionRef.current;
755
+ barcodeImageDecodeSessionRef.current = scanSession;
756
+ sdkLogger.info('Attempting high-resolution barcode decode', {
757
+ platform: Platform.OS,
758
+ session: scanSession,
759
+ });
760
+ try {
761
+ if (Platform.OS === 'android' && SanctumKeySdkModule.decodeBarcodeFromImage) {
762
+ try {
763
+ const localBarcodeText = await SanctumKeySdkModule.decodeBarcodeFromImage(imagePath, targetBarcodeTypes);
764
+ if (scanSession !== barcodeScanSessionRef.current || scanPhaseRef.current !== 'barcode')
765
+ return;
766
+ if (isUsableBarcodeText(localBarcodeText, docType.type === 'health_insurance_card')) {
767
+ scannedBarcodeRef.current = localBarcodeText;
768
+ sdkLogger.info('Barcode decoded by enhanced on-device fallback', {
769
+ length: localBarcodeText.length,
770
+ });
771
+ await processCanadianBarcode(localBarcodeText, pendingImage);
772
+ return;
773
+ }
774
+ sdkLogger.info('On-device decoders did not find a usable barcode in this frame', {
775
+ session: scanSession,
776
+ });
777
+ }
778
+ catch (localError) {
779
+ sdkLogger.info('Enhanced on-device barcode fallback could not decode this frame', {
780
+ error: String(errorMessage(localError) || 'Unknown local decoder error'),
781
+ });
782
+ }
783
+ }
784
+ else if (Platform.OS === 'android' && !nativeBarcodeDecoderWarningShownRef.current) {
785
+ nativeBarcodeDecoderWarningShownRef.current = true;
786
+ sdkLogger.warn('Enhanced on-device barcode decoder is unavailable; rebuild the Android app.');
787
+ }
788
+ }
789
+ finally {
790
+ if (barcodeImageDecodeSessionRef.current === scanSession) {
791
+ barcodeImageDecodeSessionRef.current = null;
792
+ }
793
+ }
794
+ if (scanSession !== barcodeScanSessionRef.current || scanPhaseRef.current !== 'barcode')
795
+ return;
796
+ if (!allowServiceFallback) {
797
+ sdkLogger.info('Verified back image did not yield a barcode locally; waiting for a close-up frame');
798
+ return;
799
+ }
800
+ // Do not let a slow network request block local decoding of subsequent
801
+ // high-resolution frames. At most two service calls are made per session.
802
+ if (barcodeServiceDecodeSessionRef.current === scanSession)
803
+ return;
804
+ if (barcodeServiceAttemptsRef.current.session !== scanSession) {
805
+ barcodeServiceAttemptsRef.current = { session: scanSession, count: 0 };
806
+ }
807
+ if (barcodeServiceAttemptsRef.current.count >= 2)
808
+ return;
809
+ barcodeServiceAttemptsRef.current.count += 1;
810
+ barcodeServiceDecodeSessionRef.current = scanSession;
811
+ try {
812
+ const token = await authentification();
813
+ if (scanSession !== barcodeScanSessionRef.current || scanPhaseRef.current !== 'barcode')
814
+ return;
815
+ const backendDocType = GovernmentDocumentTypeShorted[docType.type] || docType.type;
816
+ const response = await kycService.extractBarcode({
817
+ fileUri: imagePath,
818
+ token,
819
+ docType: backendDocType,
820
+ postfix: 'back',
821
+ timeoutMs: 15000,
822
+ }, env);
823
+ if (scanSession !== barcodeScanSessionRef.current || scanPhaseRef.current !== 'barcode')
824
+ return;
825
+ const barcodeText = getDecodedBarcodeText(response);
826
+ if (isUsableBarcodeText(barcodeText, docType.type === 'health_insurance_card')) {
827
+ scannedBarcodeRef.current = barcodeText;
828
+ sdkLogger.info('Barcode decoded by the high-resolution service fallback', { length: barcodeText.length });
829
+ await processCanadianBarcode(barcodeText, pendingImage);
830
+ }
831
+ else {
832
+ sdkLogger.info('Barcode service returned no usable data; local scanning remains active', {
833
+ session: scanSession,
834
+ });
835
+ }
836
+ }
837
+ catch (error) {
838
+ sdkLogger.info('Barcode service fallback failed; local scanning remains active', {
839
+ error: String(errorMessage(error) || 'Unknown barcode service error'),
840
+ });
841
+ }
842
+ finally {
843
+ if (barcodeServiceDecodeSessionRef.current === scanSession) {
844
+ barcodeServiceDecodeSessionRef.current = null;
845
+ }
471
846
  }
472
847
  };
473
848
  const handleExitKYCFlow = () => {
849
+ cameraGenerationRef.current += 1;
850
+ barcodeScanSessionRef.current += 1;
851
+ barcodeImageDecodeSessionRef.current = null;
852
+ barcodeFinalizingRef.current = false;
853
+ barcodeCompletedRef.current = false;
854
+ scannedBarcodeRef.current = null;
855
+ isAnalyzingRef.current = false;
856
+ isProcessingCaptureRef.current = false;
857
+ currentSideRef.current = 'front';
858
+ scanPhaseRef.current = 'image';
859
+ capturedImagesRef.current = {};
474
860
  clearAuthToken();
475
861
  // 1. Clear local UI and camera states immediately
476
862
  setCapturedImages({});
@@ -479,7 +865,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
479
865
  setCurrentSide('front');
480
866
  setScanPhase('image');
481
867
  setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '' });
482
- scannedBarcodeRef.current = null;
483
868
  setPendingCAImage(null);
484
869
  setIsProcessingCapture(false);
485
870
  setProcessingImagePath(null);
@@ -497,11 +882,16 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
497
882
  console.warn("Could not restart flow. Ensure restartSession is added to your KYC context actions.");
498
883
  }
499
884
  };
500
- const handleSilentCapture = async (result) => {
885
+ const handleSilentCapture = async (result, frameGeneration = cameraGenerationRef.current) => {
886
+ if (cameraGenerationRef.current !== frameGeneration)
887
+ return;
501
888
  if (isProcessingCaptureRef.current)
502
889
  return;
503
- if (scanPhaseRef.current === 'barcode')
890
+ if (scanPhaseRef.current === 'barcode') {
891
+ if (result.success && result.path)
892
+ void decodeBarcodeFromImage(result.path);
504
893
  return;
894
+ }
505
895
  if (isAnalyzingRef.current)
506
896
  return;
507
897
  // 🚨 SYNCHRONOUS LOCK: Block all subsequent camera frames immediately
@@ -510,6 +900,9 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
510
900
  const captured = capturedImagesRef.current;
511
901
  const docType = selectedDocTypeRef.current;
512
902
  const cData = countryDataRef.current;
903
+ const isStaleFrame = () => (cameraGenerationRef.current !== frameGeneration ||
904
+ currentSideRef.current !== side ||
905
+ scanPhaseRef.current !== 'image');
513
906
  if (result.success && result.path) {
514
907
  setSilentCaptureResult((prev) => ({ ...prev, isAnalyzing: true, success: false, error: '' }));
515
908
  const safeCapturePath = Platform.OS === 'ios' && !result.path.startsWith('file://') ? `file://${result.path}` : result.path;
@@ -523,7 +916,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
523
916
  }
524
917
  const issuingSubdivision = docType.region !== 'root' ? docType.region : undefined;
525
918
  const watchdog = setTimeout(() => {
526
- if (isAnalyzingRef.current) {
919
+ if (!isStaleFrame() && isAnalyzingRef.current) {
527
920
  isAnalyzingRef.current = false; // 🚨 UNLOCK inside watchdog
528
921
  setSilentCaptureResult((prev) => ({
529
922
  ...prev,
@@ -542,6 +935,10 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
542
935
  postfix: side,
543
936
  issuingSubdivision: issuingSubdivision
544
937
  }, env);
938
+ if (isStaleFrame()) {
939
+ clearTimeout(watchdog);
940
+ return;
941
+ }
545
942
  templateResponse = templateType;
546
943
  if (templateType.template_path)
547
944
  templatePath = templateType.template_path;
@@ -555,6 +952,10 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
555
952
  }
556
953
  try {
557
954
  const crop = await cropByObb(safeCapturePath, templateType.card_obb);
955
+ if (isStaleFrame()) {
956
+ clearTimeout(watchdog);
957
+ return;
958
+ }
558
959
  templateBbox = crop.bbox;
559
960
  }
560
961
  catch { }
@@ -596,6 +997,10 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
596
997
  frontTemplatePath: frontTemplatePath
597
998
  }, env);
598
999
  }
1000
+ if (isStaleFrame()) {
1001
+ clearTimeout(watchdog);
1002
+ return;
1003
+ }
599
1004
  const bbox = verificationRes?.bbox || templateBbox;
600
1005
  const verifiedMrz = verificationRes?.mrz ? JSON.stringify(verificationRes.mrz) : "";
601
1006
  if (isBarcodeRequired(side, cData, docType)) {
@@ -611,6 +1016,10 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
611
1016
  const token = await authentification();
612
1017
  parsedDictionary = await kycService.formatBarcodeData({ textInput: barcodeData, token }, env);
613
1018
  }
1019
+ if (isStaleFrame()) {
1020
+ clearTimeout(watchdog);
1021
+ return;
1022
+ }
614
1023
  const verifiedResult = {
615
1024
  path: safeCapturePath, templatePath: templatePath, bbox: bbox, success: true,
616
1025
  mrz: JSON.stringify(parsedDictionary), barcode: barcodeData, isAnalyzing: false,
@@ -621,15 +1030,25 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
621
1030
  if (bbox)
622
1031
  setBboxBySide(prev => ({ ...prev, ['back']: bbox }));
623
1032
  isAnalyzingRef.current = false; // 🚨 UNLOCK on success
624
- await autoCapture(safeCapturePath, verifiedResult);
1033
+ await autoCapture(safeCapturePath, verifiedResult, side, frameGeneration);
625
1034
  return;
626
1035
  }
627
- sdkLogger.info(`Verification passed. Waiting for barcode scan phase.`);
1036
+ sdkLogger.info('Verification passed. Starting barcode scan phase.', {
1037
+ platform: Platform.OS,
1038
+ formats: targetBarcodeTypes,
1039
+ });
628
1040
  clearTimeout(watchdog);
629
- setPendingCAImage({ path: safeCapturePath, templateResponse, templatePath, bbox });
1041
+ const pendingImage = { path: safeCapturePath, templateResponse, templatePath, bbox };
1042
+ barcodeScanSessionRef.current += 1;
1043
+ barcodeCompletedRef.current = false;
1044
+ scanPhaseRef.current = 'barcode';
1045
+ setPendingCAImage(pendingImage);
630
1046
  setScanPhase('barcode');
631
1047
  setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false }));
632
1048
  isAnalyzingRef.current = false; // 🚨 UNLOCK before phase shift
1049
+ if (Platform.OS === 'android') {
1050
+ void decodeBarcodeFromImage(safeCapturePath, pendingImage, false);
1051
+ }
633
1052
  return;
634
1053
  }
635
1054
  const verifiedResult = {
@@ -641,10 +1060,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
641
1060
  if (bbox)
642
1061
  setBboxBySide(prev => ({ ...prev, [side]: bbox }));
643
1062
  isAnalyzingRef.current = false; // 🚨 UNLOCK on final success
644
- await autoCapture(safeCapturePath, verifiedResult);
1063
+ await autoCapture(safeCapturePath, verifiedResult, side, frameGeneration);
645
1064
  }
646
1065
  catch (error) {
647
1066
  clearTimeout(watchdog);
1067
+ if (isStaleFrame())
1068
+ return;
648
1069
  const rawMessage = (typeof error === 'string' ? error : (error?.message || '')).toLowerCase();
649
1070
  const statusCode = error?.response?.status || error?.status;
650
1071
  if (statusCode === 401 || statusCode === 403 ||
@@ -692,11 +1113,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
692
1113
  useEffect(() => {
693
1114
  if (scanPhase === 'barcode' && pendingCAImage && silentCaptureResult.barcode) {
694
1115
  const bData = silentCaptureResult.barcode;
695
- if (typeof bData === 'string' && bData.length > 50) {
1116
+ const allowShortBarcode = selectedDocumentType?.type === 'health_insurance_card';
1117
+ if (isUsableBarcodeText(bData, allowShortBarcode)) {
696
1118
  processCanadianBarcode(bData);
697
1119
  }
698
1120
  }
699
- }, [scanPhase, pendingCAImage, silentCaptureResult.barcode]);
1121
+ }, [scanPhase, pendingCAImage, silentCaptureResult.barcode, selectedDocumentType?.type]);
700
1122
  const handleError = (event) => {
701
1123
  const errString = `${event.message} ${event.name} ${event.code}`.toLowerCase();
702
1124
  let friendlyMessage = state.currentLanguage === 'en' ? 'Camera error.' : 'Erreur de caméra.';
@@ -711,8 +1133,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
711
1133
  }
712
1134
  sdkLogger.error("Hardware Camera Error encountered", { ...event, friendlyMessage }); // 🚨 LOG
713
1135
  showAlert(state.currentLanguage === 'en' ? 'Camera Error' : 'Erreur', friendlyMessage);
714
- setShowCamera(false);
715
- setIsProcessingCapture(false);
1136
+ cancelCurrentCapture();
716
1137
  };
717
1138
  useEffect(() => { actions.showCustomStepper(!showCamera); }, [showCamera]);
718
1139
  // Render logic remains identical below...
@@ -744,12 +1165,27 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
744
1165
  {Platform.OS === 'web' && (<View style={styles.webTopControls}>
745
1166
  <TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}><Text style={styles.webFlipText}>🔄 Flip Lens</Text></TouchableOpacity>
746
1167
  </View>)}
747
- <EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={{ ...StyleSheet.absoluteFillObject, ...(Platform.OS === 'web' ? { width: '100%', height: '100%', left: 0, top: 0 } : {}) }} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} isScanningBarcode={currentSide === 'back'} barcodeTypes={targetBarcodeTypes} onBarcodeScanned={(barcodeString) => {
748
- const docTypeStr = selectedDocumentType?.type;
749
- const isFullDataDoc = docTypeStr === 'drivers_licence' || docTypeStr === 'national_id' || docTypeStr === 'identity_card';
750
- if (isFullDataDoc && barcodeString.length < 50)
1168
+ <EnhancedCameraView key={`${currentSide}-${scanPhase}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={{ ...StyleSheet.absoluteFillObject, ...(Platform.OS === 'web' ? { width: '100%', height: '100%', left: 0, top: 0 } : {}) }} onError={handleError} onSilentCapture={(result) => {
1169
+ if (cameraGenerationRef.current !== cameraKey ||
1170
+ currentSideRef.current !== currentSide ||
1171
+ scanPhaseRef.current !== scanPhase) {
1172
+ sdkLogger.info('Discarded a stale camera frame after capture-mode transition', {
1173
+ frameGeneration: cameraKey,
1174
+ activeGeneration: cameraGenerationRef.current,
1175
+ frameSide: currentSide,
1176
+ activeSide: currentSideRef.current,
1177
+ framePhase: scanPhase,
1178
+ activePhase: scanPhaseRef.current,
1179
+ });
1180
+ return;
1181
+ }
1182
+ void handleSilentCapture(result, cameraKey);
1183
+ }} silentCaptureResult={silentCaptureResult} captureStabilizationDelayMs={scanPhase === 'barcode' ? 3000 : 4000} isScanningBarcode={currentSide === 'back' && scanPhase === 'barcode'} barcodeTypes={targetBarcodeTypes} onBarcodeScanned={(barcodeString) => {
1184
+ const allowShortBarcode = selectedDocumentType?.type === 'health_insurance_card';
1185
+ if (!isUsableBarcodeText(barcodeString, allowShortBarcode))
751
1186
  return;
752
1187
  if (scannedBarcodeRef.current !== barcodeString) {
1188
+ sdkLogger.info('Barcode detected by live camera scanner', { length: barcodeString.length });
753
1189
  scannedBarcodeRef.current = barcodeString;
754
1190
  if (scanPhase === 'barcode' && pendingCAImage) {
755
1191
  processCanadianBarcode(barcodeString);
@@ -761,22 +1197,10 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
761
1197
  }} overlayComponent={<IdCardOverlay xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax} instructions={cameraConfig.overlay.guideText} cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0} isSuccess={silentCaptureResult.success || scanPhase === 'barcode'} language={state.currentLanguage} stepperProps={{
762
1198
  back: () => {
763
1199
  if (currentSide === 'back') {
764
- scannedBarcodeRef.current = null;
765
- setScanPhase('image');
766
- setPendingCAImage(null);
767
- setCurrentSide('front');
768
- setShowCamera(false);
769
- setIsProcessingCapture(false);
770
- setProcessingImagePath(null);
771
- if (capturedImages['front']?.dir) {
772
- const frontImage = capturedImages['front'];
773
- setSilentCaptureResult((prev) => ({ ...prev, path: frontImage.dir, success: true, isAnalyzing: false, error: '', mrz: frontImage.mrz || '', templatePath: frontImage.templatePath || '', bbox: bboxBySide['front'] }));
774
- }
775
- else {
776
- setSilentCaptureResult((prev) => ({ ...prev, path: '', success: false, isAnalyzing: false, error: '', templatePath: '' }));
777
- }
1200
+ returnToFrontReview();
778
1201
  }
779
1202
  else {
1203
+ cancelCurrentCapture();
780
1204
  actions.previousComponent();
781
1205
  }
782
1206
  },
@@ -787,7 +1211,11 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
787
1211
  <View style={styles.barcodeIconContainer}><Text style={styles.barcodeIcon}>✓</Text></View>
788
1212
  <View style={styles.barcodeTextContainer}>
789
1213
  <Text style={styles.barcodePromptTitle}>{state.currentLanguage === 'en' ? 'Image Verified' : 'Image Vérifiée'}</Text>
790
- <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>
1214
+ <Text style={styles.barcodePromptDesc}>
1215
+ {state.currentLanguage === 'en'
1216
+ ? 'Proceed to scan the barcode. Move closer but ensure it is fully visible in the frame.'
1217
+ : 'Procédez au scan du code-barres. Rapprochez-vous, mais assurez-vous qu\'il soit entièrement visible dans le cadre.'}
1218
+ </Text>
791
1219
  </View>
792
1220
  </View>
793
1221
  </View>)}
@@ -807,12 +1235,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
807
1235
  {!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
808
1236
  {silentCaptureResult.error && <View style={styles.floatingErrorBanner}><Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text></View>}
809
1237
  <View style={styles.webActionButtonsRow}>
810
- <TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}><Text style={styles.webSecondaryButtonText}>Cancel</Text></TouchableOpacity>
1238
+ <TouchableOpacity style={styles.webSecondaryButton} onPress={cancelCurrentCapture}><Text style={styles.webSecondaryButtonText}>Cancel</Text></TouchableOpacity>
811
1239
  <TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}><Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text></TouchableOpacity>
812
1240
  </View>
813
1241
  </View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
814
1242
  <TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}><Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text></TouchableOpacity>
815
- <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>
1243
+ <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>
816
1244
  </View>) : null}
817
1245
  {silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}><Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text></View>) : null}
818
1246
  </View>
@@ -856,14 +1284,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
856
1284
  }
857
1285
  else {
858
1286
  sdkLogger.info("User finished front verification, proceeding to back");
859
- setShowCamera(true);
860
- refreshCamera();
861
- setCurrentSide('back');
862
- setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '', templatePath: undefined });
863
- setIsProcessingCapture(false);
864
- setProcessingImagePath(null);
865
- setScanPhase('image');
866
- setPendingCAImage(null);
1287
+ startBackCapture();
867
1288
  }
868
1289
  }} variant="primary" size="large" fullWidth/>
869
1290
  </>)}