@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.
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +13 -1
- package/lib/module/Shared/Components/EIDScanner.js +41 -1
- package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +25 -15
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +8 -3
- package/lib/module/Shared/Components/NavigationManager.js +31 -10
- package/lib/module/Shared/Libs/diagnostics.js +35 -3
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +20 -10
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +11 -1
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +3 -3
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +15 -3
- package/src/Shared/Components/EIDScanner.tsx +43 -1
- package/src/Shared/Components/IdentityDocumentCamera.flows.ts +26 -15
- package/src/Shared/Components/IdentityDocumentCamera.tsx +8 -4
- package/src/Shared/Components/NavigationManager.tsx +44 -16
- package/src/Shared/Libs/diagnostics.ts +38 -3
- 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
|
-
|
|
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
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
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
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
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
|
-
|
|
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.
|
|
1171
|
+
// scan.
|
|
1172
1172
|
const faceForCompletion = faceImageToUse ?? currentFaceImageRef.current;
|
|
1173
|
-
|
|
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
|
|
62
|
-
//
|
|
63
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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:
|
|
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
|
package/lib/module/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IdentityDocumentScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx"],"names":[],"mappings":"AAuBA,QAAA,MAAM,8BAA8B,+
|
|
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;
|
|
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,
|
|
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
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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
|
|
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,
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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
|
-
|
|
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
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
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
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
92
|
-
//
|
|
93
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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:
|
|
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