@trustchex/react-native-sdk 1.493.1 → 1.494.0
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/Shared/Components/DiagnosticReportButton.js +118 -29
- package/lib/module/Shared/Components/NavigationManager.js +12 -0
- package/lib/module/Shared/Libs/diagnosticPow.js +56 -3
- package/lib/module/Shared/Libs/diagnosticReport.js +8 -1
- package/lib/module/Shared/Libs/diagnostics.js +61 -2
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +20 -3
- package/lib/module/Translation/Resources/en.js +5 -2
- package/lib/module/Translation/Resources/tr.js +5 -2
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +5 -4
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticPow.d.ts +26 -3
- package/lib/typescript/src/Shared/Libs/diagnosticPow.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +39 -1
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +9 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +3 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +3 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Shared/Components/DiagnosticReportButton.tsx +128 -34
- package/src/Shared/Components/NavigationManager.tsx +12 -0
- package/src/Shared/Libs/diagnosticPow.ts +71 -3
- package/src/Shared/Libs/diagnosticReport.ts +21 -3
- package/src/Shared/Libs/diagnostics.ts +77 -2
- package/src/Shared/Libs/sendDiagnosticReport.ts +37 -3
- package/src/Translation/Resources/en.ts +6 -2
- package/src/Translation/Resources/tr.ts +6 -2
- package/src/version.ts +1 -1
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useState } from 'react';
|
|
4
|
-
import { Alert } from 'react-native';
|
|
3
|
+
import React, { useRef, useState } from 'react';
|
|
4
|
+
import { ActivityIndicator, Alert, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import StyledButton from "./StyledButton.js";
|
|
7
7
|
import { sendDiagnosticReport } from "../Libs/sendDiagnosticReport.js";
|
|
8
8
|
import { SDK_VERSION } from "../../version.js";
|
|
9
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
10
10
|
/**
|
|
11
11
|
* "Report a scanning issue" button for the result screen.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Uploads the diagnostic bundle to the backend (proof-of-work authenticated,
|
|
14
|
+
* encrypted in transit + at rest). The PoW solve is the only slow step, so while
|
|
15
|
+
* sending we show a NON-DISMISSIBLE modal overlay with progress — the user can't
|
|
16
|
+
* dismiss it accidentally (tap-outside / Android-back do nothing), but can
|
|
17
|
+
* explicitly Cancel, which aborts the solve. A thank-you shows on success.
|
|
17
18
|
*
|
|
18
19
|
* Hidden when there's no session/baseUrl to upload to (e.g. before init).
|
|
19
20
|
*/
|
|
@@ -27,9 +28,36 @@ const DiagnosticReportButton = ({
|
|
|
27
28
|
t
|
|
28
29
|
} = useTranslation();
|
|
29
30
|
const [sending, setSending] = useState(false);
|
|
31
|
+
const [progress, setProgress] = useState(0);
|
|
32
|
+
// Ref so the running solve sees cancellation immediately (no stale closure).
|
|
33
|
+
const cancelledRef = useRef(false);
|
|
30
34
|
if (!baseUrl || !sessionId) {
|
|
31
35
|
return null;
|
|
32
36
|
}
|
|
37
|
+
const doSend = async () => {
|
|
38
|
+
cancelledRef.current = false;
|
|
39
|
+
setProgress(0);
|
|
40
|
+
setSending(true);
|
|
41
|
+
try {
|
|
42
|
+
const {
|
|
43
|
+
uploaded,
|
|
44
|
+
cancelled
|
|
45
|
+
} = await sendDiagnosticReport({
|
|
46
|
+
baseUrl,
|
|
47
|
+
sessionId,
|
|
48
|
+
scan,
|
|
49
|
+
sdkVersion: SDK_VERSION,
|
|
50
|
+
images,
|
|
51
|
+
onPowProgress: setProgress,
|
|
52
|
+
shouldCancel: () => cancelledRef.current
|
|
53
|
+
});
|
|
54
|
+
setSending(false);
|
|
55
|
+
if (cancelled) return; // user cancelled — no alert
|
|
56
|
+
Alert.alert(uploaded ? t('diagnosticReport.sentTitle') : t('diagnosticReport.failedTitle'), uploaded ? t('diagnosticReport.sentBody') : t('diagnosticReport.failedBody'));
|
|
57
|
+
} catch {
|
|
58
|
+
setSending(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
33
61
|
const onPress = () => {
|
|
34
62
|
if (sending) return;
|
|
35
63
|
Alert.alert(t('diagnosticReport.noticeTitle'), t('diagnosticReport.noticeBody'), [{
|
|
@@ -37,30 +65,91 @@ const DiagnosticReportButton = ({
|
|
|
37
65
|
style: 'cancel'
|
|
38
66
|
}, {
|
|
39
67
|
text: t('diagnosticReport.continue'),
|
|
40
|
-
onPress:
|
|
41
|
-
setSending(true);
|
|
42
|
-
try {
|
|
43
|
-
const {
|
|
44
|
-
uploaded
|
|
45
|
-
} = await sendDiagnosticReport({
|
|
46
|
-
baseUrl,
|
|
47
|
-
sessionId,
|
|
48
|
-
scan,
|
|
49
|
-
sdkVersion: SDK_VERSION,
|
|
50
|
-
images
|
|
51
|
-
});
|
|
52
|
-
Alert.alert(uploaded ? t('diagnosticReport.sentTitle') : t('diagnosticReport.failedTitle'), uploaded ? t('diagnosticReport.sentBody') : t('diagnosticReport.failedBody'));
|
|
53
|
-
} finally {
|
|
54
|
-
setSending(false);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
68
|
+
onPress: doSend
|
|
57
69
|
}]);
|
|
58
70
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
const pct = Math.round(progress * 100);
|
|
72
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
73
|
+
children: [/*#__PURE__*/_jsx(StyledButton, {
|
|
74
|
+
mode: "outlined",
|
|
75
|
+
onPress: onPress,
|
|
76
|
+
children: t('diagnosticReport.button')
|
|
77
|
+
}), /*#__PURE__*/_jsx(Modal, {
|
|
78
|
+
visible: sending,
|
|
79
|
+
transparent: true,
|
|
80
|
+
animationType: "fade"
|
|
81
|
+
// Block accidental dismissal: Android hardware-back does nothing while
|
|
82
|
+
// sending. There is no tap-outside-to-close (we render our own backdrop).
|
|
83
|
+
,
|
|
84
|
+
onRequestClose: () => {},
|
|
85
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
86
|
+
style: styles.backdrop,
|
|
87
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
88
|
+
style: styles.card,
|
|
89
|
+
children: [/*#__PURE__*/_jsx(ActivityIndicator, {
|
|
90
|
+
size: "large"
|
|
91
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
92
|
+
style: styles.title,
|
|
93
|
+
children: t('diagnosticReport.sending')
|
|
94
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
95
|
+
style: styles.progress,
|
|
96
|
+
children: pct > 0 ? t('diagnosticReport.sendingProgress', {
|
|
97
|
+
pct
|
|
98
|
+
}) : t('diagnosticReport.sendingPreparing')
|
|
99
|
+
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
100
|
+
accessibilityRole: "button",
|
|
101
|
+
style: styles.cancelBtn,
|
|
102
|
+
onPress: () => {
|
|
103
|
+
cancelledRef.current = true;
|
|
104
|
+
},
|
|
105
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
106
|
+
style: styles.cancelText,
|
|
107
|
+
children: t('diagnosticReport.cancel')
|
|
108
|
+
})
|
|
109
|
+
})]
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})]
|
|
64
113
|
});
|
|
65
114
|
};
|
|
115
|
+
const styles = StyleSheet.create({
|
|
116
|
+
backdrop: {
|
|
117
|
+
flex: 1,
|
|
118
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
119
|
+
alignItems: 'center',
|
|
120
|
+
justifyContent: 'center',
|
|
121
|
+
padding: 24
|
|
122
|
+
},
|
|
123
|
+
card: {
|
|
124
|
+
width: '100%',
|
|
125
|
+
maxWidth: 320,
|
|
126
|
+
backgroundColor: 'white',
|
|
127
|
+
borderRadius: 16,
|
|
128
|
+
paddingVertical: 24,
|
|
129
|
+
paddingHorizontal: 20,
|
|
130
|
+
alignItems: 'center',
|
|
131
|
+
gap: 12
|
|
132
|
+
},
|
|
133
|
+
title: {
|
|
134
|
+
fontSize: 16,
|
|
135
|
+
fontWeight: '600',
|
|
136
|
+
color: '#111827',
|
|
137
|
+
textAlign: 'center'
|
|
138
|
+
},
|
|
139
|
+
progress: {
|
|
140
|
+
fontSize: 14,
|
|
141
|
+
color: '#6B7280',
|
|
142
|
+
textAlign: 'center'
|
|
143
|
+
},
|
|
144
|
+
cancelBtn: {
|
|
145
|
+
marginTop: 8,
|
|
146
|
+
paddingVertical: 10,
|
|
147
|
+
paddingHorizontal: 24
|
|
148
|
+
},
|
|
149
|
+
cancelText: {
|
|
150
|
+
fontSize: 15,
|
|
151
|
+
fontWeight: '600',
|
|
152
|
+
color: '#DC2626'
|
|
153
|
+
}
|
|
154
|
+
});
|
|
66
155
|
export default DiagnosticReportButton;
|
|
@@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
8
8
|
import i18n from "../../Translation/index.js";
|
|
9
9
|
import StyledButton from "./StyledButton.js";
|
|
10
10
|
import { analyticsService } from "../Services/AnalyticsService.js";
|
|
11
|
+
import { diagnostics } from "../Libs/diagnostics.js";
|
|
11
12
|
|
|
12
13
|
// Navigation lock - use a ref-like pattern so unmounting resets state.
|
|
13
14
|
// The module-level object is shared across all instances but the
|
|
@@ -56,11 +57,22 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
|
|
|
56
57
|
}
|
|
57
58
|
const currentStepIndex = workflowSteps?.findIndex(step => step.id ? step.id === currentWorkFlowStep?.id : step.type === currentWorkFlowStep?.type) ?? -1;
|
|
58
59
|
const nextStep = workflowSteps && currentStepIndex < workflowSteps.length ? workflowSteps[currentStepIndex + 1] : null;
|
|
60
|
+
|
|
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.
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
if (currentWorkFlowStep?.type) {
|
|
66
|
+
diagnostics.stepEnded(currentWorkFlowStep.type, 'success', now);
|
|
67
|
+
}
|
|
59
68
|
if (!nextStep) {
|
|
60
69
|
appContext.currentWorkflowStep = undefined;
|
|
61
70
|
return routes.RESULT;
|
|
62
71
|
}
|
|
63
72
|
appContext.currentWorkflowStep = nextStep;
|
|
73
|
+
if (nextStep.type) {
|
|
74
|
+
diagnostics.stepStarted(nextStep.type, now);
|
|
75
|
+
}
|
|
64
76
|
if (nextStep.type === 'CONTRACT_ACCEPTANCE') {
|
|
65
77
|
return routes.DYNAMIC_ROUTES.CONTRACT_ACCEPTANCE;
|
|
66
78
|
}
|
|
@@ -29,9 +29,11 @@ const leadingZeroBits = bytes => {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Solve the PoW for a challenge. Returns the solution nonce as a
|
|
33
|
-
* Bounded by `maxIterations` so a pathological difficulty can't hang the
|
|
34
|
-
* returns null if unsolved within the bound.
|
|
32
|
+
* Solve the PoW for a challenge (synchronous). Returns the solution nonce as a
|
|
33
|
+
* string. Bounded by `maxIterations` so a pathological difficulty can't hang the
|
|
34
|
+
* app; returns null if unsolved within the bound.
|
|
35
|
+
*
|
|
36
|
+
* Prefer `solvePowAsync` on the UI thread — this blocks until solved.
|
|
35
37
|
*/
|
|
36
38
|
export const solvePow = (challenge, difficulty, maxIterations = 8_000_000) => {
|
|
37
39
|
for (let i = 0; i < maxIterations; i++) {
|
|
@@ -42,4 +44,55 @@ export const solvePow = (challenge, difficulty, maxIterations = 8_000_000) => {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
return null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Thrown by solvePowAsync / sendDiagnosticReport when the user cancels. */
|
|
50
|
+
export class PowCancelledError extends Error {
|
|
51
|
+
constructor() {
|
|
52
|
+
super('Proof-of-work cancelled');
|
|
53
|
+
this.name = 'PowCancelledError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Async, UI-friendly PoW solver. Hashes in small chunks and yields to the event
|
|
58
|
+
* loop between chunks so the JS thread (and the UI) stays responsive instead of
|
|
59
|
+
* freezing during the solve. Reports coarse progress (0..1, best-effort against
|
|
60
|
+
* the expected work for the difficulty) via `onProgress`, and aborts (throwing
|
|
61
|
+
* PowCancelledError) when `shouldCancel` returns true.
|
|
62
|
+
*
|
|
63
|
+
* Returns the solution nonce, or null if unsolved within `maxIterations`.
|
|
64
|
+
*/
|
|
65
|
+
export const solvePowAsync = async (challenge, difficulty, options = {}) => {
|
|
66
|
+
const {
|
|
67
|
+
onProgress,
|
|
68
|
+
shouldCancel,
|
|
69
|
+
maxIterations = 8_000_000,
|
|
70
|
+
// Hash a large chunk between yields: the per-yield setTimeout(0) round-trip
|
|
71
|
+
// on Hermes costs ~ms, so yielding every 2k hashes dominated wall-clock.
|
|
72
|
+
// 25k keeps the UI responsive + cancel snappy (a chunk is a few tens of ms)
|
|
73
|
+
// while spending almost all time hashing, not bouncing through the event loop.
|
|
74
|
+
chunkSize = 25_000
|
|
75
|
+
} = options;
|
|
76
|
+
// Expected hashes ≈ 2^difficulty; used only to render a smooth-ish bar.
|
|
77
|
+
const expected = Math.max(1, Math.pow(2, difficulty));
|
|
78
|
+
// Encode the fixed "challenge." prefix once; per iteration we only encode the
|
|
79
|
+
// small varying nonce and concat, instead of re-UTF-8-encoding the whole
|
|
80
|
+
// string (the challenge is 32+ chars) on every hash.
|
|
81
|
+
const prefix = Buffer.from(`${challenge}.`, 'utf-8');
|
|
82
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
83
|
+
const digest = sha256(Buffer.concat([prefix, Buffer.from(i.toString(), 'utf-8')]));
|
|
84
|
+
if (leadingZeroBits(digest) >= difficulty) {
|
|
85
|
+
onProgress?.(1);
|
|
86
|
+
return i.toString();
|
|
87
|
+
}
|
|
88
|
+
if (i % chunkSize === chunkSize - 1) {
|
|
89
|
+
if (shouldCancel?.()) {
|
|
90
|
+
throw new PowCancelledError();
|
|
91
|
+
}
|
|
92
|
+
onProgress?.(Math.min(0.99, i / expected));
|
|
93
|
+
// Yield so React can paint the progress and the UI stays responsive.
|
|
94
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
45
98
|
};
|
|
@@ -114,6 +114,9 @@ export const buildDiagnosticReport = params => {
|
|
|
114
114
|
buildNumber: device.buildNumber
|
|
115
115
|
},
|
|
116
116
|
device,
|
|
117
|
+
// Whole-flow, per-step record (contract → document → eID → liveness →
|
|
118
|
+
// consent → video). Diagnostics are NOT just the NFC read.
|
|
119
|
+
steps: diag.steps,
|
|
117
120
|
scan: {
|
|
118
121
|
documentType: scan.documentType ?? diag.documentType ?? null,
|
|
119
122
|
dataSource: scan.dataSource ?? diag.dataSource ?? null,
|
|
@@ -145,7 +148,11 @@ export const buildDiagnosticReport = params => {
|
|
|
145
148
|
const nfcCode = nfcErrorCode(nfc.errorCategory, failingSw);
|
|
146
149
|
const nfcLine = nfc.attempted ? `${nfc.authProtocol}${nfc.paceFellBackToBac ? '→BAC' : ''} · ${nfc.succeeded ? 'success' : `failed at ${nfc.failureStep ?? '?'} (${nfc.errorCategory ?? 'error'}) [${nfcCode}]`}` : 'not attempted';
|
|
147
150
|
const checksLine = checkDigits ? Object.entries(checkDigits).map(([k, v]) => `${k}:${v}`).join(' ') : 'n/a';
|
|
148
|
-
|
|
151
|
+
|
|
152
|
+
// Compact per-step line, e.g. "Steps: CONTRACT_ACCEPTANCE:success
|
|
153
|
+
// IDENTITY_DOCUMENT_SCAN:success LIVENESS_CHECK:failed".
|
|
154
|
+
const stepsLine = diag.steps.length ? diag.steps.map(s => `${s.step}:${s.outcome}`).join(' ') : 'none recorded';
|
|
155
|
+
const body = ['TRUSTCHEX SCAN REPORT', `Device: ${device.brand} ${device.model} · ${device.systemName} ${device.systemVersion}${device.apiLevel ? ` (API ${device.apiLevel})` : ''}`, `SDK: ${sdkVersion} (build ${device.buildNumber}) · ${device.manufacturer} · ${device.deviceId}`, `Document: ${scanDataObj.documentType ?? 'unknown'} · source: ${scanDataObj.dataSource ?? 'unknown'}`, `Steps: ${stepsLine}`, `MRZ: ${diag.mrz.reachedStable ? 'stable' : 'NOT stable'} after ${diag.mrz.framesProcessed} frames · checks: ${checksLine}`, `Camera: ${diag.camera.frames} frames · brightness ${diag.camera.avgBrightness} (low:${yesNo(diag.camera.hadLowBrightness)} blur:${yesNo(diag.camera.hadBlurryFrames)})`, `NFC: ${nfcLine}`, sessionId ? `Session: ${sessionId}` : '', '', 'Describe the issue here:', '', '', '— Full diagnostics and document images are attached. Please review before sending; do not edit the attachments. —'].filter(Boolean).join('\n');
|
|
149
156
|
const subject = `Trustchex scan report — ${scanDataObj.documentType ?? 'doc'} — ${device.brand} ${device.model} — v${sdkVersion}${sessionId ? ` — ${sessionId}` : ''}`;
|
|
150
157
|
return {
|
|
151
158
|
diagnosticsJson: JSON.stringify(diagnosticsObj, null, 2),
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
|
|
22
22
|
/** A single step of the NFC/eID read, with its outcome and timing. */
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A single workflow step's outcome. Diagnostics are NOT just about NFC — every
|
|
26
|
+
* step of the verification flow (contract, document scan, eID, liveness, verbal
|
|
27
|
+
* consent, video call) records one of these so a support/AI reader sees the
|
|
28
|
+
* whole journey: what ran, how long it took, whether it succeeded, and a small
|
|
29
|
+
* non-PII `details` bag specific to the step.
|
|
30
|
+
*/
|
|
31
|
+
|
|
24
32
|
const emptyCamera = () => ({
|
|
25
33
|
frames: 0,
|
|
26
34
|
frameW: 0,
|
|
@@ -58,9 +66,28 @@ export class DiagnosticsCollector {
|
|
|
58
66
|
mrz = emptyMrz();
|
|
59
67
|
nfc = emptyNfc();
|
|
60
68
|
errors = [];
|
|
69
|
+
steps = [];
|
|
70
|
+
// Open steps keyed by step name → start time, for duration on stepEnded.
|
|
71
|
+
openSteps = new Map();
|
|
61
72
|
|
|
62
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Reset the per-SCAN signals (camera/MRZ/NFC) — called when a scan flow
|
|
75
|
+
* starts. Deliberately PRESERVES the workflow `steps` log and accumulated
|
|
76
|
+
* errors: those span the whole verification journey (contract → … → result),
|
|
77
|
+
* and a scan starting mid-flow must not erase steps already completed. Use
|
|
78
|
+
* `resetSession` to clear everything for a brand-new session.
|
|
79
|
+
*/
|
|
63
80
|
reset(nowMs) {
|
|
81
|
+
if (!this.startedAtMs) this.startedAtMs = nowMs;
|
|
82
|
+
this.documentType = undefined;
|
|
83
|
+
this.dataSource = undefined;
|
|
84
|
+
this.camera = emptyCamera();
|
|
85
|
+
this.mrz = emptyMrz();
|
|
86
|
+
this.nfc = emptyNfc();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Clear the ENTIRE session (steps, errors, timings) — new verification. */
|
|
90
|
+
resetSession(nowMs) {
|
|
64
91
|
this.startedAtMs = nowMs;
|
|
65
92
|
this.documentType = undefined;
|
|
66
93
|
this.dataSource = undefined;
|
|
@@ -68,12 +95,38 @@ export class DiagnosticsCollector {
|
|
|
68
95
|
this.mrz = emptyMrz();
|
|
69
96
|
this.nfc = emptyNfc();
|
|
70
97
|
this.errors = [];
|
|
98
|
+
this.steps = [];
|
|
99
|
+
this.openSteps.clear();
|
|
71
100
|
}
|
|
72
101
|
setDocument(documentType, dataSource) {
|
|
73
102
|
if (documentType) this.documentType = documentType;
|
|
74
103
|
if (dataSource) this.dataSource = dataSource;
|
|
75
104
|
}
|
|
76
105
|
|
|
106
|
+
// ---- Workflow steps (whole-flow, not just NFC) ----
|
|
107
|
+
|
|
108
|
+
/** Mark a workflow step as started (records start time for duration). */
|
|
109
|
+
stepStarted(step, nowMs) {
|
|
110
|
+
this.openSteps.set(step, nowMs);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Record a step's outcome. Pairs with `stepStarted` for duration; if the step
|
|
115
|
+
* was never started, durationMs is 0. `details` is a small non-PII bag of
|
|
116
|
+
* step-specific signals (counts, flags, retries).
|
|
117
|
+
*/
|
|
118
|
+
stepEnded(step, outcome, nowMs, details) {
|
|
119
|
+
const startedAt = this.openSteps.get(step);
|
|
120
|
+
this.openSteps.delete(step);
|
|
121
|
+
this.steps.push({
|
|
122
|
+
step,
|
|
123
|
+
outcome,
|
|
124
|
+
// `startedAt` can legitimately be 0 (session t0), so check for undefined.
|
|
125
|
+
durationMs: startedAt !== undefined ? Math.max(0, nowMs - startedAt) : 0,
|
|
126
|
+
details: details && Object.keys(details).length ? details : undefined
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
77
130
|
/** Merge a camera snapshot (called periodically as frames arrive). */
|
|
78
131
|
setCamera(c) {
|
|
79
132
|
this.camera = {
|
|
@@ -165,11 +218,17 @@ export class DiagnosticsCollector {
|
|
|
165
218
|
/** Build the serialisable snapshot. */
|
|
166
219
|
snapshot(nowMs, isoNow) {
|
|
167
220
|
return {
|
|
168
|
-
schemaVersion:
|
|
221
|
+
schemaVersion: 2,
|
|
169
222
|
generatedAt: isoNow,
|
|
170
223
|
documentType: this.documentType,
|
|
171
224
|
dataSource: this.dataSource,
|
|
172
225
|
totalDurationMs: this.startedAtMs ? Math.max(0, nowMs - this.startedAtMs) : 0,
|
|
226
|
+
steps: this.steps.map(s => ({
|
|
227
|
+
...s,
|
|
228
|
+
details: s.details ? {
|
|
229
|
+
...s.details
|
|
230
|
+
} : undefined
|
|
231
|
+
})),
|
|
173
232
|
camera: {
|
|
174
233
|
...this.camera
|
|
175
234
|
},
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { diagnostics } from "./diagnostics.js";
|
|
23
23
|
import { gatherDeviceDetails } from "./native-device-info.utils.js";
|
|
24
24
|
import { buildDiagnosticReport } from "./diagnosticReport.js";
|
|
25
|
-
import {
|
|
25
|
+
import { solvePowAsync, PowCancelledError } from "./diagnosticPow.js";
|
|
26
26
|
import { logError } from "./debug.utils.js";
|
|
27
27
|
const mimeToType = mime => mime === 'png' ? 'image/png' : 'image/jpeg';
|
|
28
28
|
|
|
@@ -37,7 +37,9 @@ export const sendDiagnosticReport = async params => {
|
|
|
37
37
|
sessionId,
|
|
38
38
|
scan,
|
|
39
39
|
sdkVersion,
|
|
40
|
-
images = []
|
|
40
|
+
images = [],
|
|
41
|
+
onPowProgress,
|
|
42
|
+
shouldCancel
|
|
41
43
|
} = params;
|
|
42
44
|
if (!baseUrl) {
|
|
43
45
|
return {
|
|
@@ -95,12 +97,21 @@ export const sendDiagnosticReport = async params => {
|
|
|
95
97
|
};
|
|
96
98
|
}
|
|
97
99
|
const challenge = await challengeRes.json();
|
|
98
|
-
const solution =
|
|
100
|
+
const solution = await solvePowAsync(challenge.challenge, challenge.difficulty, {
|
|
101
|
+
onProgress: onPowProgress,
|
|
102
|
+
shouldCancel
|
|
103
|
+
});
|
|
99
104
|
if (!solution) {
|
|
100
105
|
return {
|
|
101
106
|
uploaded: false
|
|
102
107
|
};
|
|
103
108
|
}
|
|
109
|
+
if (shouldCancel?.()) {
|
|
110
|
+
return {
|
|
111
|
+
uploaded: false,
|
|
112
|
+
cancelled: true
|
|
113
|
+
};
|
|
114
|
+
}
|
|
104
115
|
const response = await fetch(`${baseUrl}/api/app/mobile/diagnostics`, {
|
|
105
116
|
method: 'POST',
|
|
106
117
|
headers: {
|
|
@@ -114,6 +125,12 @@ export const sendDiagnosticReport = async params => {
|
|
|
114
125
|
uploaded: response.ok
|
|
115
126
|
};
|
|
116
127
|
} catch (e) {
|
|
128
|
+
if (e instanceof PowCancelledError) {
|
|
129
|
+
return {
|
|
130
|
+
uploaded: false,
|
|
131
|
+
cancelled: true
|
|
132
|
+
};
|
|
133
|
+
}
|
|
117
134
|
logError('Diagnostics', 'Failed to upload diagnostic report', e instanceof Error ? e.message : String(e));
|
|
118
135
|
return {
|
|
119
136
|
uploaded: false
|
|
@@ -50,8 +50,11 @@ export default {
|
|
|
50
50
|
'diagnosticReport.noticeBody': 'This sends your scan data, document images and technical details to support so they can help resolve a scanning issue.',
|
|
51
51
|
'diagnosticReport.continue': 'Continue',
|
|
52
52
|
'diagnosticReport.cancel': 'Cancel',
|
|
53
|
-
'diagnosticReport.
|
|
54
|
-
'diagnosticReport.
|
|
53
|
+
'diagnosticReport.sending': 'Sending diagnostic report',
|
|
54
|
+
'diagnosticReport.sendingPreparing': 'Preparing…',
|
|
55
|
+
'diagnosticReport.sendingProgress': 'Preparing… {{pct}}%',
|
|
56
|
+
'diagnosticReport.sentTitle': 'Thank you!',
|
|
57
|
+
'diagnosticReport.sentBody': 'Thanks for sending your diagnostic data — it helps us improve scanning. Our team will look into the issue.',
|
|
55
58
|
'diagnosticReport.failedTitle': "Couldn't send the report",
|
|
56
59
|
'diagnosticReport.failedBody': 'Please try again later.',
|
|
57
60
|
'livenessDetectionScreen.guideHeader': 'Face Verification',
|
|
@@ -50,8 +50,11 @@ export default {
|
|
|
50
50
|
'diagnosticReport.noticeBody': 'Bu işlem; tarama verilerinizi, belge görüntülerinizi ve teknik ayrıntıları destek ekibine gönderir; böylece tarama sorununu çözebilirler.',
|
|
51
51
|
'diagnosticReport.continue': 'Devam et',
|
|
52
52
|
'diagnosticReport.cancel': 'İptal',
|
|
53
|
-
'diagnosticReport.
|
|
54
|
-
'diagnosticReport.
|
|
53
|
+
'diagnosticReport.sending': 'Tanılama raporu gönderiliyor',
|
|
54
|
+
'diagnosticReport.sendingPreparing': 'Hazırlanıyor…',
|
|
55
|
+
'diagnosticReport.sendingProgress': 'Hazırlanıyor… %{{pct}}',
|
|
56
|
+
'diagnosticReport.sentTitle': 'Teşekkürler!',
|
|
57
|
+
'diagnosticReport.sentBody': 'Tanılama verilerinizi gönderdiğiniz için teşekkürler — taramayı iyileştirmemize yardımcı oluyor. Ekibimiz sorunu inceleyecek.',
|
|
55
58
|
'diagnosticReport.failedTitle': 'Rapor gönderilemedi',
|
|
56
59
|
'diagnosticReport.failedBody': 'Lütfen daha sonra tekrar deneyin.',
|
|
57
60
|
'livenessDetectionScreen.guideHeader': 'Yüz Doğrulaması',
|
package/lib/module/version.js
CHANGED
|
@@ -11,10 +11,11 @@ export interface DiagnosticReportButtonProps {
|
|
|
11
11
|
/**
|
|
12
12
|
* "Report a scanning issue" button for the result screen.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Uploads the diagnostic bundle to the backend (proof-of-work authenticated,
|
|
15
|
+
* encrypted in transit + at rest). The PoW solve is the only slow step, so while
|
|
16
|
+
* sending we show a NON-DISMISSIBLE modal overlay with progress — the user can't
|
|
17
|
+
* dismiss it accidentally (tap-outside / Android-back do nothing), but can
|
|
18
|
+
* explicitly Cancel, which aborts the solve. A thank-you shows on success.
|
|
18
19
|
*
|
|
19
20
|
* Hidden when there's no session/baseUrl to upload to (e.g. before init).
|
|
20
21
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DiagnosticReportButton.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/DiagnosticReportButton.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"DiagnosticReportButton.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/DiagnosticReportButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2B,MAAM,OAAO,CAAC;AAYhD,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGlE,MAAM,WAAW,2BAA2B;IAC1C,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,iBAAiB,CAAC;IACxB,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED;;;;;;;;;;GAUG;AACH,QAAA,MAAM,sBAAsB,EAAE,KAAK,CAAC,EAAE,CAAC,2BAA2B,CAkGjE,CAAC;AA2CF,eAAe,sBAAsB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavigationManager.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/NavigationManager.tsx"],"names":[],"mappings":"AAAA,OAAO,KAKN,MAAM,OAAO,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,wFAoQtB,CAAC;AAaF,eAAe,iBAAiB,CAAC"}
|
|
@@ -14,9 +14,32 @@ export interface PowChallenge {
|
|
|
14
14
|
expiresAt: number;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Solve the PoW for a challenge. Returns the solution nonce as a
|
|
18
|
-
* Bounded by `maxIterations` so a pathological difficulty can't hang the
|
|
19
|
-
* returns null if unsolved within the bound.
|
|
17
|
+
* Solve the PoW for a challenge (synchronous). Returns the solution nonce as a
|
|
18
|
+
* string. Bounded by `maxIterations` so a pathological difficulty can't hang the
|
|
19
|
+
* app; returns null if unsolved within the bound.
|
|
20
|
+
*
|
|
21
|
+
* Prefer `solvePowAsync` on the UI thread — this blocks until solved.
|
|
20
22
|
*/
|
|
21
23
|
export declare const solvePow: (challenge: string, difficulty: number, maxIterations?: number) => string | null;
|
|
24
|
+
/** Thrown by solvePowAsync / sendDiagnosticReport when the user cancels. */
|
|
25
|
+
export declare class PowCancelledError extends Error {
|
|
26
|
+
constructor();
|
|
27
|
+
}
|
|
28
|
+
export interface SolvePowAsyncOptions {
|
|
29
|
+
onProgress?: (fraction: number) => void;
|
|
30
|
+
/** Polled between chunks; return true to abort. Throws PowCancelledError. */
|
|
31
|
+
shouldCancel?: () => boolean;
|
|
32
|
+
maxIterations?: number;
|
|
33
|
+
chunkSize?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Async, UI-friendly PoW solver. Hashes in small chunks and yields to the event
|
|
37
|
+
* loop between chunks so the JS thread (and the UI) stays responsive instead of
|
|
38
|
+
* freezing during the solve. Reports coarse progress (0..1, best-effort against
|
|
39
|
+
* the expected work for the difficulty) via `onProgress`, and aborts (throwing
|
|
40
|
+
* PowCancelledError) when `shouldCancel` returns true.
|
|
41
|
+
*
|
|
42
|
+
* Returns the solution nonce, or null if unsolved within `maxIterations`.
|
|
43
|
+
*/
|
|
44
|
+
export declare const solvePowAsync: (challenge: string, difficulty: number, options?: SolvePowAsyncOptions) => Promise<string | null>;
|
|
22
45
|
//# sourceMappingURL=diagnosticPow.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnosticPow.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnosticPow.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAiBD
|
|
1
|
+
{"version":3,"file":"diagnosticPow.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnosticPow.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAiBD;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,GACnB,WAAW,MAAM,EACjB,YAAY,MAAM,EAClB,sBAAyB,KACxB,MAAM,GAAG,IASX,CAAC;AAEF,4EAA4E;AAC5E,qBAAa,iBAAkB,SAAQ,KAAK;;CAK3C;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GACxB,WAAW,MAAM,EACjB,YAAY,MAAM,EAClB,UAAS,oBAAyB,KACjC,OAAO,CAAC,MAAM,GAAG,IAAI,CAmCvB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnosticReport.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnosticReport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA2CpD,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,6EAA6E;IAC7E,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;CACH;AAyDD,MAAM,WAAW,sBAAsB;IACrC,uCAAuC;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;CACjB;AAMD,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,GAAI,QAAQ;IAC5C,IAAI,EAAE,mBAAmB,CAAC;IAC1B,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,iBAAiB,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,KAAG,
|
|
1
|
+
{"version":3,"file":"diagnosticReport.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnosticReport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA2CpD,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,6EAA6E;IAC7E,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;CACH;AAyDD,MAAM,WAAW,sBAAsB;IACrC,uCAAuC;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;CACjB;AAMD,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,GAAI,QAAQ;IAC5C,IAAI,EAAE,mBAAmB,CAAC;IAC1B,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,iBAAiB,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,KAAG,sBAyGH,CAAC"}
|
|
@@ -90,12 +90,32 @@ export interface MrzDiagnostics {
|
|
|
90
90
|
* to accept the document number (set at report-build time from the final MRZ). */
|
|
91
91
|
turkishLetterPrefixApplied: boolean;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* A single workflow step's outcome. Diagnostics are NOT just about NFC — every
|
|
95
|
+
* step of the verification flow (contract, document scan, eID, liveness, verbal
|
|
96
|
+
* consent, video call) records one of these so a support/AI reader sees the
|
|
97
|
+
* whole journey: what ran, how long it took, whether it succeeded, and a small
|
|
98
|
+
* non-PII `details` bag specific to the step.
|
|
99
|
+
*/
|
|
100
|
+
export interface WorkflowStepRecord {
|
|
101
|
+
/** Step type, e.g. 'CONTRACT_ACCEPTANCE' | 'IDENTITY_DOCUMENT_SCAN' |
|
|
102
|
+
* 'IDENTITY_DOCUMENT_EID_SCAN' | 'LIVENESS_CHECK' | 'VERBAL_CONSENT' |
|
|
103
|
+
* 'VIDEO_CALL'. */
|
|
104
|
+
step: string;
|
|
105
|
+
/** 'success' | 'failed' | 'skipped' | 'cancelled' | 'started' (still running). */
|
|
106
|
+
outcome: 'success' | 'failed' | 'skipped' | 'cancelled' | 'started';
|
|
107
|
+
durationMs: number;
|
|
108
|
+
/** Coarse, non-PII per-step signals (counts, flags, retries — never PII). */
|
|
109
|
+
details?: Record<string, string | number | boolean | null>;
|
|
110
|
+
}
|
|
93
111
|
export interface DiagnosticsSnapshot {
|
|
94
112
|
schemaVersion: number;
|
|
95
113
|
generatedAt: string;
|
|
96
114
|
documentType?: string;
|
|
97
115
|
dataSource?: string;
|
|
98
116
|
totalDurationMs: number;
|
|
117
|
+
/** Per-step record of the WHOLE flow, in the order steps started. */
|
|
118
|
+
steps: WorkflowStepRecord[];
|
|
99
119
|
camera: CameraDiagnostics;
|
|
100
120
|
mrz: MrzDiagnostics;
|
|
101
121
|
nfc: NfcDiagnostics;
|
|
@@ -118,9 +138,27 @@ export declare class DiagnosticsCollector {
|
|
|
118
138
|
private mrz;
|
|
119
139
|
private nfc;
|
|
120
140
|
private errors;
|
|
121
|
-
|
|
141
|
+
private steps;
|
|
142
|
+
private openSteps;
|
|
143
|
+
/**
|
|
144
|
+
* Reset the per-SCAN signals (camera/MRZ/NFC) — called when a scan flow
|
|
145
|
+
* starts. Deliberately PRESERVES the workflow `steps` log and accumulated
|
|
146
|
+
* errors: those span the whole verification journey (contract → … → result),
|
|
147
|
+
* and a scan starting mid-flow must not erase steps already completed. Use
|
|
148
|
+
* `resetSession` to clear everything for a brand-new session.
|
|
149
|
+
*/
|
|
122
150
|
reset(nowMs: number): void;
|
|
151
|
+
/** Clear the ENTIRE session (steps, errors, timings) — new verification. */
|
|
152
|
+
resetSession(nowMs: number): void;
|
|
123
153
|
setDocument(documentType?: string, dataSource?: string): void;
|
|
154
|
+
/** Mark a workflow step as started (records start time for duration). */
|
|
155
|
+
stepStarted(step: string, nowMs: number): void;
|
|
156
|
+
/**
|
|
157
|
+
* Record a step's outcome. Pairs with `stepStarted` for duration; if the step
|
|
158
|
+
* was never started, durationMs is 0. `details` is a small non-PII bag of
|
|
159
|
+
* step-specific signals (counts, flags, retries).
|
|
160
|
+
*/
|
|
161
|
+
stepEnded(step: string, outcome: WorkflowStepRecord['outcome'], nowMs: number, details?: WorkflowStepRecord['details']): void;
|
|
124
162
|
/** Merge a camera snapshot (called periodically as frames arrive). */
|
|
125
163
|
setCamera(c: Partial<CameraDiagnostics>): void;
|
|
126
164
|
/** Merge MRZ consensus metrics. `reachedStable` is STICKY (once true, stays
|