@trustchex/react-native-sdk 1.495.6 → 1.495.8

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 (24) hide show
  1. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +13 -1
  2. package/lib/module/Shared/Components/EIDScanner.js +41 -1
  3. package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +25 -15
  4. package/lib/module/Shared/Components/IdentityDocumentCamera.js +8 -3
  5. package/lib/module/Shared/Components/NavigationManager.js +31 -10
  6. package/lib/module/Shared/Libs/diagnostics.js +35 -3
  7. package/lib/module/version.js +1 -1
  8. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  9. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  10. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  11. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +20 -10
  12. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
  13. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  14. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +11 -1
  15. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -1
  16. package/lib/typescript/src/version.d.ts +1 -1
  17. package/package.json +3 -3
  18. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +15 -3
  19. package/src/Shared/Components/EIDScanner.tsx +43 -1
  20. package/src/Shared/Components/IdentityDocumentCamera.flows.ts +26 -15
  21. package/src/Shared/Components/IdentityDocumentCamera.tsx +8 -4
  22. package/src/Shared/Components/NavigationManager.tsx +44 -16
  23. package/src/Shared/Libs/diagnostics.ts +38 -3
  24. package/src/version.ts +1 -1
@@ -35,11 +35,23 @@ const IdentityDocumentScanningScreen = () => {
35
35
  }, []);
36
36
  useEffect(() => {
37
37
  if ((idFrontSideData && idBackSideData || passportData) && appContext.identificationInfo && !appContext.identificationInfo.scannedDocument) {
38
+ const mrzFields = idBackSideData?.mrzFields || passportData?.mrzFields;
39
+
40
+ // A document can complete capture (face/hologram/images) without ever
41
+ // reaching a stable MRZ consensus, in which case mrzFields is undefined.
42
+ // Bail out cleanly here: setting the scannedDocument guard below and THEN
43
+ // dereferencing mrzFields would throw mid-effect, leaving the guard set so
44
+ // re-entry is blocked forever — the user would be stranded on this screen
45
+ // with no error and no way forward. Reset and surface the failure instead.
46
+ if (!mrzFields) {
47
+ appContext.onError?.(t('general.error'));
48
+ navigationManagerRef.current?.reset();
49
+ return;
50
+ }
38
51
  appContext.identificationInfo.scannedDocument = {
39
52
  documentType: 'UNKNOWN',
40
53
  dataSource: 'UNKNOWN'
41
54
  };
42
- const mrzFields = idBackSideData?.mrzFields || passportData?.mrzFields;
43
55
  const mrzText = idBackSideData?.mrzText || passportData?.mrzText;
44
56
  appContext.identificationInfo.scannedDocument.dataSource = 'MRZ';
45
57
  appContext.identificationInfo.scannedDocument.mrzFields = mrzFields;
@@ -20,7 +20,14 @@ import { getLocalizedCountryName } from "../Libs/country-display.utils.js";
20
20
  import { useKeepAwake } from "../Libs/native-keep-awake.utils.js";
21
21
  import { speak, resetLastMessage } from "../Libs/tts.utils.js";
22
22
  import { trackEIDScanStart, trackEIDScanComplete, trackEIDScanFailed } from "../Libs/analytics.utils.js";
23
+
24
+ // Hard ceiling for a single NFC read attempt. A healthy read — even a passport
25
+ // with a slow curve (brainpoolP512r1 ECDH) plus a full DG2 face image — finishes
26
+ // well within this; the watchdog only trips on a genuine stall so the user never
27
+ // sits on the "reading document" spinner indefinitely. Deliberately generous to
28
+ // avoid aborting slow-but-progressing reads.
23
29
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
30
+ const NFC_READ_TIMEOUT_MS = 90_000;
24
31
  const EIDScanner = ({
25
32
  documentNumber,
26
33
  dateOfBirth,
@@ -173,7 +180,13 @@ const EIDScanner = ({
173
180
  await trackEIDScanStart(docType, hasNfc, isEnabled, currentAttempt).catch(() => {});
174
181
  try {
175
182
  if (documentNumber && dateOfBirth && dateOfExpiry) {
176
- const passportData = await eidReader(documentNumber, dateOfBirth, dateOfExpiry, progressValue => {
183
+ // Watchdog: a chip that stalls mid-read (card slips, RF field drops in a
184
+ // way the per-APDU transceive timeout doesn't catch, or session open/
185
+ // close hangs) would otherwise leave eidReader pending forever — the
186
+ // "reading document" spinner never clears and the user is stuck with no
187
+ // error. Cap the whole read; on timeout we throw a categorized error so
188
+ // the catch below surfaces a retry prompt and finally resets the UI.
189
+ const readPromise = eidReader(documentNumber, dateOfBirth, dateOfExpiry, progressValue => {
177
190
  setProgress(progressValue);
178
191
  // Update stage message based on progress
179
192
  if (progressValue <= 20) {
@@ -186,6 +199,23 @@ const EIDScanner = ({
186
199
  setProgressStage(t('eidScannerScreen.completing'));
187
200
  }
188
201
  });
202
+ let watchdog;
203
+ const watchdogPromise = new Promise((_, reject) => {
204
+ watchdog = setTimeout(() => {
205
+ reject(Object.assign(new Error('NFC read timed out'), {
206
+ nfcCategory: 'timeout',
207
+ nfcCode: 'NFC-TIMEOUT'
208
+ }));
209
+ }, NFC_READ_TIMEOUT_MS);
210
+ });
211
+ let passportData;
212
+ try {
213
+ passportData = await Promise.race([readPromise, watchdogPromise]);
214
+ } finally {
215
+ if (watchdog) {
216
+ clearTimeout(watchdog);
217
+ }
218
+ }
189
219
  if (passportData) {
190
220
  const scanDuration = Date.now() - startTime;
191
221
  const documentData = normalizeFromMRZInfo(passportData.mrz);
@@ -214,6 +244,16 @@ const EIDScanner = ({
214
244
  setIsScanned(true);
215
245
  }
216
246
  await trackEIDScanComplete(docType, scanDuration, currentAttempt).catch(() => {});
247
+ } else {
248
+ // eidReader resolved without throwing but produced no data. Without
249
+ // this branch the success path is skipped silently and the user is
250
+ // left staring at the spinner until finally{} clears it — with no
251
+ // explanation. Treat it as a read failure so the catch surfaces a
252
+ // retry prompt.
253
+ throw Object.assign(new Error('NFC read returned no data'), {
254
+ nfcCategory: 'unknown',
255
+ nfcCode: 'NFC-EMPTY'
256
+ });
217
257
  }
218
258
  } else {
219
259
  const scanDuration = Date.now() - startTime;
@@ -262,16 +262,26 @@ export function handleIDBackFlow(mrzText, mrzFields, mrzValid, mrzStableAndValid
262
262
  * PASSPORT: → COMPLETED (no back side)
263
263
  * ID CARD: → SCAN_ID_BACK
264
264
  *
265
- * Routing is PASSPORT-BIASED on purpose: a passport has no back side, so if we
266
- * route it to SCAN_ID_BACK it deadlocks forever waiting for a back that will
267
- * never appear. We therefore only go to SCAN_ID_BACK when there is POSITIVE
268
- * evidence the document is an ID card (a parsed MRZ document code that is
269
- * present and not 'P'). When the type is ambiguous — e.g. the passport MRZ was
270
- * never read as 'P' so the front step locked `detectedDocumentType` as
271
- * 'ID_FRONT' (the torch/tilt during hologram capture makes the MRZ unreadable),
272
- * or no MRZ code survived at all we COMPLETE rather than wait for a
273
- * non-existent back side. A two-sided ID card always yields an 'I'/'A'/'C'
274
- * (non-'P') MRZ code, so genuine ID cards still route to SCAN_ID_BACK.
265
+ * Routing is PASSPORT-BIASED for safety a passport has no back side, so any
266
+ * positive passport signal (a locked/current PASSPORT classification, or an MRZ
267
+ * document code of 'P') COMPLETES immediately and never routes to SCAN_ID_BACK
268
+ * (which would deadlock waiting for a back that never appears).
269
+ *
270
+ * For ID cards, we route to SCAN_ID_BACK on POSITIVE ID-card evidence, which is
271
+ * EITHER:
272
+ * (a) a parsed MRZ document code that is present and not 'P' ('I'/'A'/'C'), OR
273
+ * (b) the front was classified as ID_FRONT (and there is no passport signal).
274
+ *
275
+ * Case (b) is essential: an ID card's FRONT has no MRZ at all (the MRZ lives on
276
+ * the back), so by the time we finish the front + hologram there is usually NO
277
+ * MRZ code to key on. Requiring a code (the old behaviour) sent every ordinary
278
+ * ID card straight to COMPLETED and silently skipped the back-side scan.
279
+ * Conversely a passport's front DOES carry the MRZ, so a genuine passport that
280
+ * reached this point almost always exposes a 'P' code (or a PASSPORT lock) and
281
+ * is caught by the passport guard above. The residual ambiguous case — a
282
+ * passport misread/locked as ID_FRONT with no surviving 'P' — is rare and
283
+ * self-corrects: SCAN_ID_BACK simply won't find an ID-back MRZ/barcode and the
284
+ * user can cancel, which is far better than skipping the back of every real ID.
275
285
  */
276
286
  export function getNextStepAfterHologram(detectedDocumentType, currentFrameDocType, mrzDocCode) {
277
287
  // Any positive passport signal → done (no back side).
@@ -280,12 +290,12 @@ export function getNextStepAfterHologram(detectedDocumentType, currentFrameDocTy
280
290
  return 'COMPLETED';
281
291
  }
282
292
 
283
- // Only continue to the back side when we have POSITIVE proof it's an ID card:
284
- // a real MRZ document code that is present and not a passport. Without that
285
- // proof, default to COMPLETED so a misclassified passport can't hang waiting
286
- // for a back side that doesn't exist.
293
+ // Positive ID-card evidence → scan the back. Either a non-'P' MRZ code, or a
294
+ // front classified as ID_FRONT (an ID front legitimately has no MRZ, so the
295
+ // classification is the only signal we get).
287
296
  const hasIdCardMrzCode = !!mrzDocCode && mrzDocCode !== 'P';
288
- if (hasIdCardMrzCode) {
297
+ const isIdFront = detectedDocumentType === 'ID_FRONT' || currentFrameDocType === 'ID_FRONT';
298
+ if (hasIdCardMrzCode || isIdFront) {
289
299
  return 'SCAN_ID_BACK';
290
300
  }
291
301
  return 'COMPLETED';
@@ -1168,10 +1168,15 @@ const IdentityDocumentCamera = ({
1168
1168
  // Fall back to the previously locked face (ref) when the live frame's face
1169
1169
  // was rejected — the torch is on during hologram capture, so faceImageToUse
1170
1170
  // is frequently null even though a valid face was locked during the front
1171
- // scan. Without this the gate's face requirement could never be satisfied.
1171
+ // scan.
1172
1172
  const faceForCompletion = faceImageToUse ?? currentFaceImageRef.current;
1173
- const stepComplete = (!!scannedData.hologramImage || maxRetriesReached && !isCollecting) && !!faceForCompletion; // Require face before completing
1174
-
1173
+ // The face must NOT gate completion: if the front-scan face capture
1174
+ // failed, currentFaceImageRef is null and the live frame's face is
1175
+ // routinely rejected under the hologram torch — requiring a face here
1176
+ // would wedge the user on the hologram step forever even after a valid
1177
+ // hologram is captured or retries are exhausted. Attach the face if we
1178
+ // have one (below), but advance regardless.
1179
+ const stepComplete = !!scannedData.hologramImage || maxRetriesReached && !isCollecting;
1175
1180
  if (stepComplete) {
1176
1181
  // Make sure the locked face rides along even if this frame's face was
1177
1182
  // rejected.
@@ -26,6 +26,10 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
26
26
  const {
27
27
  t
28
28
  } = useTranslation();
29
+ // Track the lock-release timer so we can cancel it on unmount. Without this
30
+ // the timer is orphaned, and the shared module-level navigationLock relies
31
+ // solely on the focus/unmount resets below to clear `isNavigating`.
32
+ const lockReleaseTimer = React.useRef(null);
29
33
  const routes = {
30
34
  VERIFICATION_SESSION_CHECK: 'VerificationSessionCheckScreen',
31
35
  DYNAMIC_ROUTES: {
@@ -45,10 +49,18 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
45
49
  navigation.addListener('focus', handleFocus);
46
50
  return () => {
47
51
  navigation.removeListener('focus', handleFocus);
52
+ // Fully release the shared lock on unmount so a navigation started by
53
+ // this (now gone) screen can never strand the next screen: clear both
54
+ // the flag and the rate-limit timestamp, and cancel the pending timer.
48
55
  navigationLock.isNavigating = false;
56
+ navigationLock.lastNavigationTime = 0;
57
+ if (lockReleaseTimer.current) {
58
+ clearTimeout(lockReleaseTimer.current);
59
+ lockReleaseTimer.current = null;
60
+ }
49
61
  };
50
62
  }, [navigation]);
51
- const getNextRoute = useCallback((workflowSteps, currentWorkFlowStep) => {
63
+ const getNextRoute = useCallback((workflowSteps, currentWorkFlowStep, outcome = 'success') => {
52
64
  // If this was a debug navigation, go directly to result screen
53
65
  if (appContext.isDebugNavigated) {
54
66
  appContext.isDebugNavigated = false;
@@ -58,12 +70,13 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
58
70
  const currentStepIndex = workflowSteps?.findIndex(step => step.id ? step.id === currentWorkFlowStep?.id : step.type === currentWorkFlowStep?.type) ?? -1;
59
71
  const nextStep = workflowSteps && currentStepIndex < workflowSteps.length ? workflowSteps[currentStepIndex + 1] : null;
60
72
 
61
- // Diagnostics: advancing past the current step means it completed. Record
62
- // its outcome (success the user moved on) and start-time the next step,
63
- // so the report captures the WHOLE flow, not just the NFC read.
73
+ // Diagnostics: advancing past the current step records its outcome
74
+ // 'success' on a normal advance, 'skipped' when the user used the skip
75
+ // button and start-times the next step, so the report captures the
76
+ // WHOLE flow, not just the NFC read.
64
77
  const now = Date.now();
65
78
  if (currentWorkFlowStep?.type) {
66
- diagnostics.stepEnded(currentWorkFlowStep.type, 'success', now);
79
+ diagnostics.stepEnded(currentWorkFlowStep.type, outcome, now);
67
80
  }
68
81
  if (!nextStep) {
69
82
  appContext.currentWorkflowStep = undefined;
@@ -93,7 +106,7 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
93
106
  }
94
107
  return routes.VERIFICATION_SESSION_CHECK;
95
108
  }, [appContext, routes.VERIFICATION_SESSION_CHECK, routes.RESULT, routes.DYNAMIC_ROUTES.CONTRACT_ACCEPTANCE, routes.DYNAMIC_ROUTES.IDENTITY_DOCUMENT_SCAN, routes.DYNAMIC_ROUTES.IDENTITY_DOCUMENT_EID_SCAN, routes.DYNAMIC_ROUTES.LIVENESS_CHECK, routes.DYNAMIC_ROUTES.VIDEO_CALL, routes.DYNAMIC_ROUTES.VERBAL_CONSENT]);
96
- const goToNextRoute = useCallback(() => {
109
+ const goToNextRoute = useCallback((outcome = 'success') => {
97
110
  const currentTime = Date.now();
98
111
  const minTimeBetweenNavigation = 1000;
99
112
  if (navigationLock.isNavigating || currentTime - navigationLock.lastNavigationTime < minTimeBetweenNavigation) {
@@ -102,7 +115,7 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
102
115
  navigationLock.isNavigating = true;
103
116
  navigationLock.lastNavigationTime = currentTime;
104
117
  try {
105
- const nextRoute = getNextRoute(appContext.workflowSteps, appContext.currentWorkflowStep);
118
+ const nextRoute = getNextRoute(appContext.workflowSteps, appContext.currentWorkflowStep, outcome);
106
119
  navigation.dispatch(CommonActions.navigate({
107
120
  name: nextRoute,
108
121
  params: {
@@ -113,8 +126,12 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
113
126
  navigationLock.isNavigating = false;
114
127
  throw error;
115
128
  }
116
- setTimeout(() => {
129
+ if (lockReleaseTimer.current) {
130
+ clearTimeout(lockReleaseTimer.current);
131
+ }
132
+ lockReleaseTimer.current = setTimeout(() => {
117
133
  navigationLock.isNavigating = false;
134
+ lockReleaseTimer.current = null;
118
135
  }, 1000);
119
136
  }, [getNextRoute, appContext.workflowSteps, appContext.currentWorkflowStep, navigation]);
120
137
  const goToNextRouteWithAlert = useCallback(() => {
@@ -126,7 +143,7 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
126
143
  style: 'cancel'
127
144
  }, {
128
145
  text: t('general.yes'),
129
- onPress: goToNextRoute
146
+ onPress: () => goToNextRoute('skipped')
130
147
  }]);
131
148
  }, [goToNextRoute, t]);
132
149
  const reset = useCallback(() => {
@@ -178,8 +195,12 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
178
195
  navigationLock.isNavigating = false;
179
196
  throw error;
180
197
  }
181
- setTimeout(() => {
198
+ if (lockReleaseTimer.current) {
199
+ clearTimeout(lockReleaseTimer.current);
200
+ }
201
+ lockReleaseTimer.current = setTimeout(() => {
182
202
  navigationLock.isNavigating = false;
203
+ lockReleaseTimer.current = null;
183
204
  }, 1000);
184
205
  }, [appContext, navigation, routes.VERIFICATION_SESSION_CHECK]);
185
206
  usePreventRemove(true, ({
@@ -105,7 +105,11 @@ export class DiagnosticsCollector {
105
105
 
106
106
  // ---- Workflow steps (whole-flow, not just NFC) ----
107
107
 
108
- /** Mark a workflow step as started (records start time for duration). */
108
+ /**
109
+ * Mark a workflow step as started (records start time for duration).
110
+ * Re-starting a step that is already open just refreshes its start time
111
+ * (a re-render / re-entry of the same step is not a new step).
112
+ */
109
113
  stepStarted(step, nowMs) {
110
114
  this.openSteps.set(step, nowMs);
111
115
  }
@@ -114,15 +118,34 @@ export class DiagnosticsCollector {
114
118
  * Record a step's outcome. Pairs with `stepStarted` for duration; if the step
115
119
  * was never started, durationMs is 0. `details` is a small non-PII bag of
116
120
  * step-specific signals (counts, flags, retries).
121
+ *
122
+ * IDEMPOTENT per open step: the navigation layer can call this more than once
123
+ * for the same logical transition (re-renders, the nav-lock window, a deferred
124
+ * navigate timer). We only record when the step is actually OPEN, then close
125
+ * it — a second call for an already-ended step is ignored instead of pushing a
126
+ * duplicate row into the report.
117
127
  */
118
128
  stepEnded(step, outcome, nowMs, details) {
119
129
  const startedAt = this.openSteps.get(step);
130
+ if (startedAt === undefined) {
131
+ // Not open: either already ended (duplicate call → ignore), or ended
132
+ // without a matching start. Allow a first record for a never-started step
133
+ // (durationMs 0), but suppress repeats of a step already in the log.
134
+ if (this.steps.some(s => s.step === step)) return;
135
+ this.steps.push({
136
+ step,
137
+ outcome,
138
+ durationMs: 0,
139
+ details: details && Object.keys(details).length ? details : undefined
140
+ });
141
+ return;
142
+ }
120
143
  this.openSteps.delete(step);
121
144
  this.steps.push({
122
145
  step,
123
146
  outcome,
124
147
  // `startedAt` can legitimately be 0 (session t0), so check for undefined.
125
- durationMs: startedAt !== undefined ? Math.max(0, nowMs - startedAt) : 0,
148
+ durationMs: Math.max(0, nowMs - startedAt),
126
149
  details: details && Object.keys(details).length ? details : undefined
127
150
  });
128
151
  }
@@ -217,13 +240,22 @@ export class DiagnosticsCollector {
217
240
 
218
241
  /** Build the serialisable snapshot. */
219
242
  snapshot(nowMs, isoNow) {
243
+ // Any step that was started but never ended is one the user stopped on / the
244
+ // flow was abandoned at — surface it as 'skipped' (with its elapsed time) so
245
+ // the report shows the WHOLE journey instead of silently dropping the final,
246
+ // incomplete step. Appended in start order, after the recorded steps.
247
+ const openStepRecords = Array.from(this.openSteps.entries()).map(([step, startedAt]) => ({
248
+ step,
249
+ outcome: 'skipped',
250
+ durationMs: Math.max(0, nowMs - startedAt)
251
+ }));
220
252
  return {
221
253
  schemaVersion: 2,
222
254
  generatedAt: isoNow,
223
255
  documentType: this.documentType,
224
256
  dataSource: this.dataSource,
225
257
  totalDurationMs: this.startedAtMs ? Math.max(0, nowMs - this.startedAtMs) : 0,
226
- steps: this.steps.map(s => ({
258
+ steps: [...this.steps, ...openStepRecords].map(s => ({
227
259
  ...s,
228
260
  details: s.details ? {
229
261
  ...s.details
@@ -2,4 +2,4 @@
2
2
 
3
3
  // This file is auto-generated. Do not edit manually.
4
4
  // Version is synced from package.json during build.
5
- export const SDK_VERSION = '1.495.6';
5
+ export const SDK_VERSION = '1.495.8';
@@ -1 +1 @@
1
- {"version":3,"file":"IdentityDocumentScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx"],"names":[],"mappings":"AAuBA,QAAA,MAAM,8BAA8B,+CA4KnC,CAAC;AAgBF,eAAe,8BAA8B,CAAC"}
1
+ {"version":3,"file":"IdentityDocumentScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx"],"names":[],"mappings":"AAuBA,QAAA,MAAM,8BAA8B,+CAwLnC,CAAC;AAgBF,eAAe,8BAA8B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"EIDScanner.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/EIDScanner.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAiBpD,UAAU,eAAe;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CACd,OAAO,EAAE,SAAS,EAClB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,KACtB,IAAI,CAAC;IACV,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CAC/C;AAED,QAAA,MAAM,UAAU,GAAI,6FAOjB,eAAe,4CAywBjB,CAAC;AA4NF,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"EIDScanner.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/EIDScanner.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAwBpD,UAAU,eAAe;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CACd,OAAO,EAAE,SAAS,EAClB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,KACtB,IAAI,CAAC;IACV,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CAC/C;AAED,QAAA,MAAM,UAAU,GAAI,6FAOjB,eAAe,4CA4yBjB,CAAC;AA4NF,eAAe,UAAU,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"IdentityDocumentCamera.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/IdentityDocumentCamera.tsx"],"names":[],"mappings":"AAyEA,OAAO,KAAK,EACV,mBAAmB,EACnB,SAAS,EACT,2BAA2B,EAK5B,MAAM,gCAAgC,CAAC;AAGxC,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;AAC5E,YAAY,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAMnE,QAAA,MAAM,sBAAsB,GAAI,uDAI7B,2BAA2B,4CAmjF7B,CAAC;AAiIF,eAAe,sBAAsB,CAAC"}
1
+ {"version":3,"file":"IdentityDocumentCamera.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/IdentityDocumentCamera.tsx"],"names":[],"mappings":"AAyEA,OAAO,KAAK,EACV,mBAAmB,EACnB,SAAS,EACT,2BAA2B,EAK5B,MAAM,gCAAgC,CAAC;AAGxC,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;AAC5E,YAAY,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAMnE,QAAA,MAAM,sBAAsB,GAAI,uDAI7B,2BAA2B,4CAujF7B,CAAC;AAiIF,eAAe,sBAAsB,CAAC"}
@@ -84,16 +84,26 @@ export declare function handleIDBackFlow(mrzText: string | null, mrzFields: MRZF
84
84
  * PASSPORT: → COMPLETED (no back side)
85
85
  * ID CARD: → SCAN_ID_BACK
86
86
  *
87
- * Routing is PASSPORT-BIASED on purpose: a passport has no back side, so if we
88
- * route it to SCAN_ID_BACK it deadlocks forever waiting for a back that will
89
- * never appear. We therefore only go to SCAN_ID_BACK when there is POSITIVE
90
- * evidence the document is an ID card (a parsed MRZ document code that is
91
- * present and not 'P'). When the type is ambiguous — e.g. the passport MRZ was
92
- * never read as 'P' so the front step locked `detectedDocumentType` as
93
- * 'ID_FRONT' (the torch/tilt during hologram capture makes the MRZ unreadable),
94
- * or no MRZ code survived at all we COMPLETE rather than wait for a
95
- * non-existent back side. A two-sided ID card always yields an 'I'/'A'/'C'
96
- * (non-'P') MRZ code, so genuine ID cards still route to SCAN_ID_BACK.
87
+ * Routing is PASSPORT-BIASED for safety a passport has no back side, so any
88
+ * positive passport signal (a locked/current PASSPORT classification, or an MRZ
89
+ * document code of 'P') COMPLETES immediately and never routes to SCAN_ID_BACK
90
+ * (which would deadlock waiting for a back that never appears).
91
+ *
92
+ * For ID cards, we route to SCAN_ID_BACK on POSITIVE ID-card evidence, which is
93
+ * EITHER:
94
+ * (a) a parsed MRZ document code that is present and not 'P' ('I'/'A'/'C'), OR
95
+ * (b) the front was classified as ID_FRONT (and there is no passport signal).
96
+ *
97
+ * Case (b) is essential: an ID card's FRONT has no MRZ at all (the MRZ lives on
98
+ * the back), so by the time we finish the front + hologram there is usually NO
99
+ * MRZ code to key on. Requiring a code (the old behaviour) sent every ordinary
100
+ * ID card straight to COMPLETED and silently skipped the back-side scan.
101
+ * Conversely a passport's front DOES carry the MRZ, so a genuine passport that
102
+ * reached this point almost always exposes a 'P' code (or a PASSPORT lock) and
103
+ * is caught by the passport guard above. The residual ambiguous case — a
104
+ * passport misread/locked as ID_FRONT with no surviving 'P' — is rare and
105
+ * self-corrects: SCAN_ID_BACK simply won't find an ID-back MRZ/barcode and the
106
+ * user can cancel, which is far better than skipping the back of every real ID.
97
107
  */
98
108
  export declare function getNextStepAfterHologram(detectedDocumentType: 'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN', currentFrameDocType: 'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN', mrzDocCode: string | undefined): 'COMPLETED' | 'SCAN_ID_BACK';
99
109
  //# sourceMappingURL=IdentityDocumentCamera.flows.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"IdentityDocumentCamera.flows.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/IdentityDocumentCamera.flows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAUpD,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EACP,mBAAmB,GACnB,cAAc,GACd,qBAAqB,GACrB,sBAAsB,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EACP,mBAAmB,GACnB,oBAAoB,GACpB,qBAAqB,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,cAAc,GAAG,kBAAkB,GAAG,sBAAsB,CAAC;CAC3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,iBAAiB,EAAE,OAAO,EAC1B,WAAW,EAAE,OAAO,EACpB,iBAAiB,EAAE,OAAO,EAC1B,YAAY,EAAE,OAAO,GACpB,kBAAkB,CAgEpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,UAAU,EAAE,MAAM,GACjB,iBAAiB,CA0EnB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,QAAQ,EAAE,OAAO,EACjB,iBAAiB,EAAE,OAAO,EAC1B,iBAAiB,EAAE,OAAO,EAC1B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,WAAW,EAAE,OAAO,GACnB,gBAAgB,CA2DlB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CACtC,oBAAoB,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,EACrE,mBAAmB,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,EACpE,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,WAAW,GAAG,cAAc,CAqB9B"}
1
+ {"version":3,"file":"IdentityDocumentCamera.flows.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/IdentityDocumentCamera.flows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAUpD,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EACP,mBAAmB,GACnB,cAAc,GACd,qBAAqB,GACrB,sBAAsB,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EACP,mBAAmB,GACnB,oBAAoB,GACpB,qBAAqB,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,cAAc,GAAG,kBAAkB,GAAG,sBAAsB,CAAC;CAC3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,iBAAiB,EAAE,OAAO,EAC1B,WAAW,EAAE,OAAO,EACpB,iBAAiB,EAAE,OAAO,EAC1B,YAAY,EAAE,OAAO,GACpB,kBAAkB,CAgEpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,UAAU,EAAE,MAAM,GACjB,iBAAiB,CA0EnB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,QAAQ,EAAE,OAAO,EACjB,iBAAiB,EAAE,OAAO,EAC1B,iBAAiB,EAAE,OAAO,EAC1B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,WAAW,EAAE,OAAO,GACnB,gBAAgB,CA2DlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,wBAAwB,CACtC,oBAAoB,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,EACrE,mBAAmB,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,EACpE,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,WAAW,GAAG,cAAc,CAsB9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"NavigationManager.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/NavigationManager.tsx"],"names":[],"mappings":"AAAA,OAAO,KAKN,MAAM,OAAO,CAAC;AAoBf,MAAM,MAAM,oBAAoB,GAAG;IACjC,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,QAAA,MAAM,iBAAiB,wFAoQtB,CAAC;AAaF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"NavigationManager.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/NavigationManager.tsx"],"names":[],"mappings":"AAAA,OAAO,KAKN,MAAM,OAAO,CAAC;AAoBf,MAAM,MAAM,oBAAoB,GAAG;IACjC,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,QAAA,MAAM,iBAAiB,wFAgStB,CAAC;AAaF,eAAe,iBAAiB,CAAC"}
@@ -151,12 +151,22 @@ export declare class DiagnosticsCollector {
151
151
  /** Clear the ENTIRE session (steps, errors, timings) — new verification. */
152
152
  resetSession(nowMs: number): void;
153
153
  setDocument(documentType?: string, dataSource?: string): void;
154
- /** Mark a workflow step as started (records start time for duration). */
154
+ /**
155
+ * Mark a workflow step as started (records start time for duration).
156
+ * Re-starting a step that is already open just refreshes its start time
157
+ * (a re-render / re-entry of the same step is not a new step).
158
+ */
155
159
  stepStarted(step: string, nowMs: number): void;
156
160
  /**
157
161
  * Record a step's outcome. Pairs with `stepStarted` for duration; if the step
158
162
  * was never started, durationMs is 0. `details` is a small non-PII bag of
159
163
  * step-specific signals (counts, flags, retries).
164
+ *
165
+ * IDEMPOTENT per open step: the navigation layer can call this more than once
166
+ * for the same logical transition (re-renders, the nav-lock window, a deferred
167
+ * navigate timer). We only record when the step is actually OPEN, then close
168
+ * it — a second call for an already-ended step is ignored instead of pushing a
169
+ * duplicate row into the report.
160
170
  */
161
171
  stepEnded(step: string, outcome: WorkflowStepRecord['outcome'], nowMs: number, details?: WorkflowStepRecord['details']): void;
162
172
  /** Merge a camera snapshot (called periodically as frames arrive). */
@@ -1 +1 @@
1
- {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnostics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnostics.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "1.495.6";
1
+ export declare const SDK_VERSION = "1.495.8";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustchex/react-native-sdk",
3
- "version": "1.495.6",
3
+ "version": "1.495.8",
4
4
  "description": "Trustchex mobile app react native SDK for android or ios devices",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -103,7 +103,7 @@
103
103
  "@eslint/compat": "^1.2.7",
104
104
  "@eslint/eslintrc": "^3.3.0",
105
105
  "@eslint/js": "^9.22.0",
106
- "@react-native-community/cli": "18.0.0",
106
+ "@react-native-community/cli": "18.0.1",
107
107
  "@react-native/babel-preset": "0.79.7",
108
108
  "@react-native/eslint-config": "^0.78.0",
109
109
  "@react-navigation/native": "^7.1.16",
@@ -138,7 +138,7 @@
138
138
  "react-native-webrtc": "^124.0.0",
139
139
  "react-native-webview": "^13.15.0",
140
140
  "release-it": "^17.10.0",
141
- "turbo": "^1.10.7",
141
+ "turbo": "^2.9.14",
142
142
  "typescript": "^5.8.3"
143
143
  },
144
144
  "dependencies": {
@@ -54,14 +54,26 @@ const IdentityDocumentScanningScreen = () => {
54
54
  appContext.identificationInfo &&
55
55
  !appContext.identificationInfo.scannedDocument
56
56
  ) {
57
+ const mrzFields = (idBackSideData?.mrzFields ||
58
+ passportData?.mrzFields) as MRZFields | undefined;
59
+
60
+ // A document can complete capture (face/hologram/images) without ever
61
+ // reaching a stable MRZ consensus, in which case mrzFields is undefined.
62
+ // Bail out cleanly here: setting the scannedDocument guard below and THEN
63
+ // dereferencing mrzFields would throw mid-effect, leaving the guard set so
64
+ // re-entry is blocked forever — the user would be stranded on this screen
65
+ // with no error and no way forward. Reset and surface the failure instead.
66
+ if (!mrzFields) {
67
+ appContext.onError?.(t('general.error'));
68
+ navigationManagerRef.current?.reset();
69
+ return;
70
+ }
71
+
57
72
  appContext.identificationInfo.scannedDocument = {
58
73
  documentType: 'UNKNOWN',
59
74
  dataSource: 'UNKNOWN',
60
75
  };
61
76
 
62
- const mrzFields = (idBackSideData?.mrzFields ||
63
- passportData?.mrzFields) as MRZFields;
64
-
65
77
  const mrzText = idBackSideData?.mrzText || passportData?.mrzText;
66
78
 
67
79
  appContext.identificationInfo.scannedDocument.dataSource = 'MRZ';
@@ -26,6 +26,13 @@ import {
26
26
  trackEIDScanFailed,
27
27
  } from '../Libs/analytics.utils';
28
28
 
29
+ // Hard ceiling for a single NFC read attempt. A healthy read — even a passport
30
+ // with a slow curve (brainpoolP512r1 ECDH) plus a full DG2 face image — finishes
31
+ // well within this; the watchdog only trips on a genuine stall so the user never
32
+ // sits on the "reading document" spinner indefinitely. Deliberately generous to
33
+ // avoid aborting slow-but-progressing reads.
34
+ const NFC_READ_TIMEOUT_MS = 90_000;
35
+
29
36
  interface eIDScannerProps {
30
37
  documentNumber: string;
31
38
  dateOfBirth: string;
@@ -241,7 +248,13 @@ const EIDScanner = ({
241
248
 
242
249
  try {
243
250
  if (documentNumber && dateOfBirth && dateOfExpiry) {
244
- const passportData = await eidReader(
251
+ // Watchdog: a chip that stalls mid-read (card slips, RF field drops in a
252
+ // way the per-APDU transceive timeout doesn't catch, or session open/
253
+ // close hangs) would otherwise leave eidReader pending forever — the
254
+ // "reading document" spinner never clears and the user is stuck with no
255
+ // error. Cap the whole read; on timeout we throw a categorized error so
256
+ // the catch below surfaces a retry prompt and finally resets the UI.
257
+ const readPromise = eidReader(
245
258
  documentNumber,
246
259
  dateOfBirth,
247
260
  dateOfExpiry,
@@ -259,6 +272,25 @@ const EIDScanner = ({
259
272
  }
260
273
  }
261
274
  );
275
+ let watchdog: ReturnType<typeof setTimeout> | undefined;
276
+ const watchdogPromise = new Promise<never>((_, reject) => {
277
+ watchdog = setTimeout(() => {
278
+ reject(
279
+ Object.assign(new Error('NFC read timed out'), {
280
+ nfcCategory: 'timeout',
281
+ nfcCode: 'NFC-TIMEOUT',
282
+ })
283
+ );
284
+ }, NFC_READ_TIMEOUT_MS);
285
+ });
286
+ let passportData;
287
+ try {
288
+ passportData = await Promise.race([readPromise, watchdogPromise]);
289
+ } finally {
290
+ if (watchdog) {
291
+ clearTimeout(watchdog);
292
+ }
293
+ }
262
294
  if (passportData) {
263
295
  const scanDuration = Date.now() - startTime;
264
296
  const documentData = normalizeFromMRZInfo(passportData.mrz);
@@ -302,6 +334,16 @@ const EIDScanner = ({
302
334
  scanDuration,
303
335
  currentAttempt
304
336
  ).catch(() => {});
337
+ } else {
338
+ // eidReader resolved without throwing but produced no data. Without
339
+ // this branch the success path is skipped silently and the user is
340
+ // left staring at the spinner until finally{} clears it — with no
341
+ // explanation. Treat it as a read failure so the catch surfaces a
342
+ // retry prompt.
343
+ throw Object.assign(new Error('NFC read returned no data'), {
344
+ nfcCategory: 'unknown',
345
+ nfcCode: 'NFC-EMPTY',
346
+ });
305
347
  }
306
348
  } else {
307
349
  const scanDuration = Date.now() - startTime;
@@ -326,16 +326,26 @@ export function handleIDBackFlow(
326
326
  * PASSPORT: → COMPLETED (no back side)
327
327
  * ID CARD: → SCAN_ID_BACK
328
328
  *
329
- * Routing is PASSPORT-BIASED on purpose: a passport has no back side, so if we
330
- * route it to SCAN_ID_BACK it deadlocks forever waiting for a back that will
331
- * never appear. We therefore only go to SCAN_ID_BACK when there is POSITIVE
332
- * evidence the document is an ID card (a parsed MRZ document code that is
333
- * present and not 'P'). When the type is ambiguous — e.g. the passport MRZ was
334
- * never read as 'P' so the front step locked `detectedDocumentType` as
335
- * 'ID_FRONT' (the torch/tilt during hologram capture makes the MRZ unreadable),
336
- * or no MRZ code survived at all we COMPLETE rather than wait for a
337
- * non-existent back side. A two-sided ID card always yields an 'I'/'A'/'C'
338
- * (non-'P') MRZ code, so genuine ID cards still route to SCAN_ID_BACK.
329
+ * Routing is PASSPORT-BIASED for safety a passport has no back side, so any
330
+ * positive passport signal (a locked/current PASSPORT classification, or an MRZ
331
+ * document code of 'P') COMPLETES immediately and never routes to SCAN_ID_BACK
332
+ * (which would deadlock waiting for a back that never appears).
333
+ *
334
+ * For ID cards, we route to SCAN_ID_BACK on POSITIVE ID-card evidence, which is
335
+ * EITHER:
336
+ * (a) a parsed MRZ document code that is present and not 'P' ('I'/'A'/'C'), OR
337
+ * (b) the front was classified as ID_FRONT (and there is no passport signal).
338
+ *
339
+ * Case (b) is essential: an ID card's FRONT has no MRZ at all (the MRZ lives on
340
+ * the back), so by the time we finish the front + hologram there is usually NO
341
+ * MRZ code to key on. Requiring a code (the old behaviour) sent every ordinary
342
+ * ID card straight to COMPLETED and silently skipped the back-side scan.
343
+ * Conversely a passport's front DOES carry the MRZ, so a genuine passport that
344
+ * reached this point almost always exposes a 'P' code (or a PASSPORT lock) and
345
+ * is caught by the passport guard above. The residual ambiguous case — a
346
+ * passport misread/locked as ID_FRONT with no surviving 'P' — is rare and
347
+ * self-corrects: SCAN_ID_BACK simply won't find an ID-back MRZ/barcode and the
348
+ * user can cancel, which is far better than skipping the back of every real ID.
339
349
  */
340
350
  export function getNextStepAfterHologram(
341
351
  detectedDocumentType: 'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN',
@@ -352,12 +362,13 @@ export function getNextStepAfterHologram(
352
362
  return 'COMPLETED';
353
363
  }
354
364
 
355
- // Only continue to the back side when we have POSITIVE proof it's an ID card:
356
- // a real MRZ document code that is present and not a passport. Without that
357
- // proof, default to COMPLETED so a misclassified passport can't hang waiting
358
- // for a back side that doesn't exist.
365
+ // Positive ID-card evidence → scan the back. Either a non-'P' MRZ code, or a
366
+ // front classified as ID_FRONT (an ID front legitimately has no MRZ, so the
367
+ // classification is the only signal we get).
359
368
  const hasIdCardMrzCode = !!mrzDocCode && mrzDocCode !== 'P';
360
- if (hasIdCardMrzCode) {
369
+ const isIdFront =
370
+ detectedDocumentType === 'ID_FRONT' || currentFrameDocType === 'ID_FRONT';
371
+ if (hasIdCardMrzCode || isIdFront) {
361
372
  return 'SCAN_ID_BACK';
362
373
  }
363
374
 
@@ -1731,12 +1731,16 @@ const IdentityDocumentCamera = ({
1731
1731
  // Fall back to the previously locked face (ref) when the live frame's face
1732
1732
  // was rejected — the torch is on during hologram capture, so faceImageToUse
1733
1733
  // is frequently null even though a valid face was locked during the front
1734
- // scan. Without this the gate's face requirement could never be satisfied.
1734
+ // scan.
1735
1735
  const faceForCompletion = faceImageToUse ?? currentFaceImageRef.current;
1736
+ // The face must NOT gate completion: if the front-scan face capture
1737
+ // failed, currentFaceImageRef is null and the live frame's face is
1738
+ // routinely rejected under the hologram torch — requiring a face here
1739
+ // would wedge the user on the hologram step forever even after a valid
1740
+ // hologram is captured or retries are exhausted. Attach the face if we
1741
+ // have one (below), but advance regardless.
1736
1742
  const stepComplete =
1737
- (!!scannedData.hologramImage ||
1738
- (maxRetriesReached && !isCollecting)) &&
1739
- !!faceForCompletion; // Require face before completing
1743
+ !!scannedData.hologramImage || (maxRetriesReached && !isCollecting);
1740
1744
 
1741
1745
  if (stepComplete) {
1742
1746
  // Make sure the locked face rides along even if this frame's face was
@@ -37,6 +37,12 @@ const NavigationManager = forwardRef(
37
37
  const appContext = useContext(AppContext);
38
38
  const navigation = useNavigation();
39
39
  const { t } = useTranslation();
40
+ // Track the lock-release timer so we can cancel it on unmount. Without this
41
+ // the timer is orphaned, and the shared module-level navigationLock relies
42
+ // solely on the focus/unmount resets below to clear `isNavigating`.
43
+ const lockReleaseTimer = React.useRef<ReturnType<
44
+ typeof setTimeout
45
+ > | null>(null);
40
46
 
41
47
  const routes = {
42
48
  VERIFICATION_SESSION_CHECK: 'VerificationSessionCheckScreen',
@@ -60,14 +66,23 @@ const NavigationManager = forwardRef(
60
66
 
61
67
  return () => {
62
68
  navigation.removeListener('focus', handleFocus);
69
+ // Fully release the shared lock on unmount so a navigation started by
70
+ // this (now gone) screen can never strand the next screen: clear both
71
+ // the flag and the rate-limit timestamp, and cancel the pending timer.
63
72
  navigationLock.isNavigating = false;
73
+ navigationLock.lastNavigationTime = 0;
74
+ if (lockReleaseTimer.current) {
75
+ clearTimeout(lockReleaseTimer.current);
76
+ lockReleaseTimer.current = null;
77
+ }
64
78
  };
65
79
  }, [navigation]);
66
80
 
67
81
  const getNextRoute = useCallback(
68
82
  (
69
83
  workflowSteps?: WorkflowStep[],
70
- currentWorkFlowStep?: WorkflowStep
84
+ currentWorkFlowStep?: WorkflowStep,
85
+ outcome: 'success' | 'skipped' = 'success'
71
86
  ): string => {
72
87
  // If this was a debug navigation, go directly to result screen
73
88
  if (appContext.isDebugNavigated) {
@@ -88,12 +103,13 @@ const NavigationManager = forwardRef(
88
103
  ? workflowSteps[currentStepIndex + 1]
89
104
  : null;
90
105
 
91
- // Diagnostics: advancing past the current step means it completed. Record
92
- // its outcome (success the user moved on) and start-time the next step,
93
- // so the report captures the WHOLE flow, not just the NFC read.
106
+ // Diagnostics: advancing past the current step records its outcome
107
+ // 'success' on a normal advance, 'skipped' when the user used the skip
108
+ // button and start-times the next step, so the report captures the
109
+ // WHOLE flow, not just the NFC read.
94
110
  const now = Date.now();
95
111
  if (currentWorkFlowStep?.type) {
96
- diagnostics.stepEnded(currentWorkFlowStep.type, 'success', now);
112
+ diagnostics.stepEnded(currentWorkFlowStep.type, outcome, now);
97
113
  }
98
114
 
99
115
  if (!nextStep) {
@@ -145,7 +161,8 @@ const NavigationManager = forwardRef(
145
161
  ]
146
162
  );
147
163
 
148
- const goToNextRoute = useCallback(() => {
164
+ const goToNextRoute = useCallback(
165
+ (outcome: 'success' | 'skipped' = 'success') => {
149
166
  const currentTime = Date.now();
150
167
  const minTimeBetweenNavigation = 1000;
151
168
 
@@ -163,7 +180,8 @@ const NavigationManager = forwardRef(
163
180
  try {
164
181
  const nextRoute = getNextRoute(
165
182
  appContext.workflowSteps,
166
- appContext.currentWorkflowStep
183
+ appContext.currentWorkflowStep,
184
+ outcome
167
185
  );
168
186
 
169
187
  navigation.dispatch(
@@ -179,15 +197,21 @@ const NavigationManager = forwardRef(
179
197
  throw error;
180
198
  }
181
199
 
182
- setTimeout(() => {
200
+ if (lockReleaseTimer.current) {
201
+ clearTimeout(lockReleaseTimer.current);
202
+ }
203
+ lockReleaseTimer.current = setTimeout(() => {
183
204
  navigationLock.isNavigating = false;
205
+ lockReleaseTimer.current = null;
184
206
  }, 1000);
185
- }, [
186
- getNextRoute,
187
- appContext.workflowSteps,
188
- appContext.currentWorkflowStep,
189
- navigation,
190
- ]);
207
+ },
208
+ [
209
+ getNextRoute,
210
+ appContext.workflowSteps,
211
+ appContext.currentWorkflowStep,
212
+ navigation,
213
+ ]
214
+ );
191
215
 
192
216
  const goToNextRouteWithAlert = useCallback(() => {
193
217
  if (navigationLock.isNavigating) {
@@ -204,7 +228,7 @@ const NavigationManager = forwardRef(
204
228
  },
205
229
  {
206
230
  text: t('general.yes'),
207
- onPress: goToNextRoute,
231
+ onPress: () => goToNextRoute('skipped'),
208
232
  },
209
233
  ]
210
234
  );
@@ -264,8 +288,12 @@ const NavigationManager = forwardRef(
264
288
  throw error;
265
289
  }
266
290
 
267
- setTimeout(() => {
291
+ if (lockReleaseTimer.current) {
292
+ clearTimeout(lockReleaseTimer.current);
293
+ }
294
+ lockReleaseTimer.current = setTimeout(() => {
268
295
  navigationLock.isNavigating = false;
296
+ lockReleaseTimer.current = null;
269
297
  }, 1000);
270
298
  }, [appContext, navigation, routes.VERIFICATION_SESSION_CHECK]);
271
299
 
@@ -210,7 +210,11 @@ export class DiagnosticsCollector {
210
210
 
211
211
  // ---- Workflow steps (whole-flow, not just NFC) ----
212
212
 
213
- /** Mark a workflow step as started (records start time for duration). */
213
+ /**
214
+ * Mark a workflow step as started (records start time for duration).
215
+ * Re-starting a step that is already open just refreshes its start time
216
+ * (a re-render / re-entry of the same step is not a new step).
217
+ */
214
218
  stepStarted(step: string, nowMs: number): void {
215
219
  this.openSteps.set(step, nowMs);
216
220
  }
@@ -219,6 +223,12 @@ export class DiagnosticsCollector {
219
223
  * Record a step's outcome. Pairs with `stepStarted` for duration; if the step
220
224
  * was never started, durationMs is 0. `details` is a small non-PII bag of
221
225
  * step-specific signals (counts, flags, retries).
226
+ *
227
+ * IDEMPOTENT per open step: the navigation layer can call this more than once
228
+ * for the same logical transition (re-renders, the nav-lock window, a deferred
229
+ * navigate timer). We only record when the step is actually OPEN, then close
230
+ * it — a second call for an already-ended step is ignored instead of pushing a
231
+ * duplicate row into the report.
222
232
  */
223
233
  stepEnded(
224
234
  step: string,
@@ -227,12 +237,25 @@ export class DiagnosticsCollector {
227
237
  details?: WorkflowStepRecord['details']
228
238
  ): void {
229
239
  const startedAt = this.openSteps.get(step);
240
+ if (startedAt === undefined) {
241
+ // Not open: either already ended (duplicate call → ignore), or ended
242
+ // without a matching start. Allow a first record for a never-started step
243
+ // (durationMs 0), but suppress repeats of a step already in the log.
244
+ if (this.steps.some((s) => s.step === step)) return;
245
+ this.steps.push({
246
+ step,
247
+ outcome,
248
+ durationMs: 0,
249
+ details: details && Object.keys(details).length ? details : undefined,
250
+ });
251
+ return;
252
+ }
230
253
  this.openSteps.delete(step);
231
254
  this.steps.push({
232
255
  step,
233
256
  outcome,
234
257
  // `startedAt` can legitimately be 0 (session t0), so check for undefined.
235
- durationMs: startedAt !== undefined ? Math.max(0, nowMs - startedAt) : 0,
258
+ durationMs: Math.max(0, nowMs - startedAt),
236
259
  details: details && Object.keys(details).length ? details : undefined,
237
260
  });
238
261
  }
@@ -326,6 +349,18 @@ export class DiagnosticsCollector {
326
349
 
327
350
  /** Build the serialisable snapshot. */
328
351
  snapshot(nowMs: number, isoNow: string): DiagnosticsSnapshot {
352
+ // Any step that was started but never ended is one the user stopped on / the
353
+ // flow was abandoned at — surface it as 'skipped' (with its elapsed time) so
354
+ // the report shows the WHOLE journey instead of silently dropping the final,
355
+ // incomplete step. Appended in start order, after the recorded steps.
356
+ const openStepRecords: WorkflowStepRecord[] = Array.from(
357
+ this.openSteps.entries()
358
+ ).map(([step, startedAt]) => ({
359
+ step,
360
+ outcome: 'skipped' as const,
361
+ durationMs: Math.max(0, nowMs - startedAt),
362
+ }));
363
+
329
364
  return {
330
365
  schemaVersion: 2,
331
366
  generatedAt: isoNow,
@@ -334,7 +369,7 @@ export class DiagnosticsCollector {
334
369
  totalDurationMs: this.startedAtMs
335
370
  ? Math.max(0, nowMs - this.startedAtMs)
336
371
  : 0,
337
- steps: this.steps.map((s) => ({
372
+ steps: [...this.steps, ...openStepRecords].map((s) => ({
338
373
  ...s,
339
374
  details: s.details ? { ...s.details } : undefined,
340
375
  })),
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
2
  // Version is synced from package.json during build.
3
- export const SDK_VERSION = '1.495.6';
3
+ export const SDK_VERSION = '1.495.8';