@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.
Files changed (34) hide show
  1. package/lib/module/Shared/Components/DiagnosticReportButton.js +118 -29
  2. package/lib/module/Shared/Components/NavigationManager.js +12 -0
  3. package/lib/module/Shared/Libs/diagnosticPow.js +56 -3
  4. package/lib/module/Shared/Libs/diagnosticReport.js +8 -1
  5. package/lib/module/Shared/Libs/diagnostics.js +61 -2
  6. package/lib/module/Shared/Libs/sendDiagnosticReport.js +20 -3
  7. package/lib/module/Translation/Resources/en.js +5 -2
  8. package/lib/module/Translation/Resources/tr.js +5 -2
  9. package/lib/module/version.js +1 -1
  10. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +5 -4
  11. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -1
  12. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  13. package/lib/typescript/src/Shared/Libs/diagnosticPow.d.ts +26 -3
  14. package/lib/typescript/src/Shared/Libs/diagnosticPow.d.ts.map +1 -1
  15. package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -1
  16. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +39 -1
  17. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -1
  18. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +9 -0
  19. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -1
  20. package/lib/typescript/src/Translation/Resources/en.d.ts +3 -0
  21. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  22. package/lib/typescript/src/Translation/Resources/tr.d.ts +3 -0
  23. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  24. package/lib/typescript/src/version.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/src/Shared/Components/DiagnosticReportButton.tsx +128 -34
  27. package/src/Shared/Components/NavigationManager.tsx +12 -0
  28. package/src/Shared/Libs/diagnosticPow.ts +71 -3
  29. package/src/Shared/Libs/diagnosticReport.ts +21 -3
  30. package/src/Shared/Libs/diagnostics.ts +77 -2
  31. package/src/Shared/Libs/sendDiagnosticReport.ts +37 -3
  32. package/src/Translation/Resources/en.ts +6 -2
  33. package/src/Translation/Resources/tr.ts +6 -2
  34. 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
- * Shows a brief notice describing what will be shared (acting as the user's
14
- * consent step), then uploads the diagnostic bundle (technical signals +
15
- * scanned data + captured images) to the backend, encrypted in transit and at
16
- * rest. Operators view and download the reports from the dashboard.
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: async () => {
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
- return /*#__PURE__*/_jsx(StyledButton, {
60
- mode: "outlined",
61
- onPress: onPress,
62
- disabled: sending,
63
- children: t('diagnosticReport.button')
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 string.
33
- * Bounded by `maxIterations` so a pathological difficulty can't hang the app;
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
- 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'}`, `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');
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
- /** Begin a fresh session — call when a scan flow starts. */
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: 1,
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 { solvePow } from "./diagnosticPow.js";
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 = solvePow(challenge.challenge, challenge.difficulty);
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.sentTitle': 'Report sent',
54
- 'diagnosticReport.sentBody': 'Your diagnostic report was sent to support. Thank you.',
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.sentTitle': 'Rapor gönderildi',
54
- 'diagnosticReport.sentBody': 'Tanılama raporunuz destek ekibine gönderildi. Teşekkürler.',
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ı',
@@ -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.493.1';
5
+ export const SDK_VERSION = '1.494.0';
@@ -11,10 +11,11 @@ export interface DiagnosticReportButtonProps {
11
11
  /**
12
12
  * "Report a scanning issue" button for the result screen.
13
13
  *
14
- * Shows a brief notice describing what will be shared (acting as the user's
15
- * consent step), then uploads the diagnostic bundle (technical signals +
16
- * scanned data + captured images) to the backend, encrypted in transit and at
17
- * rest. Operators view and download the reports from the dashboard.
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,KAAmB,MAAM,OAAO,CAAC;AAIxC,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;;;;;;;;;GASG;AACH,QAAA,MAAM,sBAAsB,EAAE,KAAK,CAAC,EAAE,CAAC,2BAA2B,CAsDjE,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
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;AAmBf,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,wFAyPtB,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,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 string.
18
- * Bounded by `maxIterations` so a pathological difficulty can't hang the app;
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;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GACnB,WAAW,MAAM,EACjB,YAAY,MAAM,EAClB,sBAAyB,KACxB,MAAM,GAAG,IASX,CAAC"}
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,sBAuFH,CAAC"}
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
- /** Begin a fresh session — call when a scan flow starts. */
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