@trustchex/react-native-sdk 1.475.1 → 1.481.1

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 (108) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +76 -24
  2. package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
  3. package/ios/Camera/TrustchexCameraView.swift +20 -15
  4. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
  5. package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
  6. package/lib/module/Screens/Static/ResultScreen.js +145 -113
  7. package/lib/module/Shared/Components/DebugOverlay.js +5 -2
  8. package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
  9. package/lib/module/Shared/Components/IdentityDocumentCamera.js +185 -30
  10. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
  11. package/lib/module/Shared/Components/NavigationManager.js +7 -9
  12. package/lib/module/Shared/EIDReader/eidReader.js +159 -38
  13. package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
  14. package/lib/module/Shared/Libs/SignalingClient.js +6 -5
  15. package/lib/module/Shared/Libs/crypto.utils.js +25 -3
  16. package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
  17. package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
  18. package/lib/module/Shared/Libs/diagnostics.js +171 -0
  19. package/lib/module/Shared/Libs/http-client.js +19 -6
  20. package/lib/module/Shared/Libs/mrz.utils.js +193 -33
  21. package/lib/module/Shared/Libs/mrzFrameAggregator.js +74 -3
  22. package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
  23. package/lib/module/Shared/Libs/promise.utils.js +4 -3
  24. package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
  25. package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
  26. package/lib/module/Shared/Services/AnalyticsService.js +8 -2
  27. package/lib/module/Shared/Services/DataUploadService.js +114 -127
  28. package/lib/module/Shared/Services/VideoSessionService.js +4 -3
  29. package/lib/module/Translation/Resources/en.js +6 -0
  30. package/lib/module/Translation/Resources/tr.js +6 -0
  31. package/lib/module/Trustchex.js +20 -4
  32. package/lib/module/version.js +1 -1
  33. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  36. package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
  38. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
  39. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +10 -0
  41. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
  42. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
  43. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  44. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  45. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
  46. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
  47. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  48. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
  49. package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
  50. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
  51. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  52. package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
  53. package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
  54. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
  55. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
  56. package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
  57. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  58. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +55 -0
  60. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
  62. package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  64. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
  65. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
  66. package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
  67. package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
  68. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
  69. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  70. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
  71. package/lib/typescript/src/Translation/Resources/en.d.ts +6 -0
  72. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  73. package/lib/typescript/src/Translation/Resources/tr.d.ts +6 -0
  74. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  75. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  76. package/lib/typescript/src/version.d.ts +1 -1
  77. package/package.json +17 -3
  78. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
  79. package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
  80. package/src/Screens/Static/ResultScreen.tsx +172 -162
  81. package/src/Shared/Components/DebugOverlay.tsx +5 -2
  82. package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
  83. package/src/Shared/Components/IdentityDocumentCamera.tsx +200 -29
  84. package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
  85. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
  86. package/src/Shared/Components/NavigationManager.tsx +7 -7
  87. package/src/Shared/Components/TrustchexCamera.tsx +6 -0
  88. package/src/Shared/EIDReader/eidReader.ts +181 -52
  89. package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
  90. package/src/Shared/Libs/SignalingClient.ts +10 -5
  91. package/src/Shared/Libs/crypto.utils.ts +41 -4
  92. package/src/Shared/Libs/deeplink.utils.ts +39 -7
  93. package/src/Shared/Libs/diagnosticReport.ts +206 -0
  94. package/src/Shared/Libs/diagnostics.ts +251 -0
  95. package/src/Shared/Libs/http-client.ts +24 -8
  96. package/src/Shared/Libs/mrz.utils.ts +200 -34
  97. package/src/Shared/Libs/mrzFrameAggregator.ts +112 -2
  98. package/src/Shared/Libs/native-device-info.utils.ts +116 -0
  99. package/src/Shared/Libs/promise.utils.ts +11 -5
  100. package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
  101. package/src/Shared/Libs/shuffle.utils.ts +15 -0
  102. package/src/Shared/Services/AnalyticsService.ts +17 -1
  103. package/src/Shared/Services/DataUploadService.ts +155 -166
  104. package/src/Shared/Services/VideoSessionService.ts +15 -3
  105. package/src/Translation/Resources/en.ts +7 -0
  106. package/src/Translation/Resources/tr.ts +7 -0
  107. package/src/Trustchex.tsx +26 -3
  108. package/src/version.ts +1 -1
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Builds the support diagnostic report from a scan session.
3
+ *
4
+ * Produces three things the result screen turns into a reviewable email:
5
+ * - `diagnostics.json` — technical signals (device, camera, MRZ, NFC steps).
6
+ * - `scan-data.json` — the scanned identity data + raw MRZ (PII; the user
7
+ * reviews it in the draft and chooses to send).
8
+ * - a short, size-bounded plain-text BODY summary that always fits an email.
9
+ *
10
+ * The full detail lives in the JSON attachments; the email body is intentionally
11
+ * compact so the draft opens reliably on every mail client.
12
+ */
13
+ import type { DiagnosticsSnapshot } from './diagnostics';
14
+ import type { DeviceDetails } from './native-device-info.utils';
15
+ import type { MRZFields } from '../Types/mrzFields';
16
+
17
+ /** ICAO 9303 7-3-1 weighted check digit (local copy; mirrors mrz.utils). */
18
+ const icaoCheckDigit = (str: string): string => {
19
+ const weights = [7, 3, 1];
20
+ let sum = 0;
21
+ for (let i = 0; i < str.length; i++) {
22
+ const c = str[i];
23
+ const v =
24
+ c === '<'
25
+ ? 0
26
+ : c >= '0' && c <= '9'
27
+ ? c.charCodeAt(0) - 48
28
+ : c.charCodeAt(0) - 55;
29
+ sum = (sum + weights[i % 3] * v) % 10;
30
+ }
31
+ return String(sum);
32
+ };
33
+
34
+ export interface ScanDataForReport {
35
+ documentType?: string;
36
+ dataSource?: string;
37
+ mrzText?: string | null;
38
+ mrzFields?: MRZFields | null;
39
+ barcodeValue?: string | null;
40
+ /** Which images were captured (presence only, used for the body summary). */
41
+ images?: {
42
+ mrzSide?: boolean;
43
+ front?: boolean;
44
+ face?: boolean;
45
+ nfcFace?: boolean;
46
+ };
47
+ }
48
+
49
+ interface Td1CheckResult {
50
+ checkDigits: Record<string, 'pass' | 'fail'>;
51
+ /** True when the Turkish leading-letter (digits-only) convention was applied. */
52
+ turkishLetterPrefix: boolean;
53
+ }
54
+
55
+ /** Compute per-field check-digit pass/fail for a TD1 MRZ (best-effort). */
56
+ const computeTd1CheckDigits = (
57
+ mrzText?: string | null
58
+ ): Td1CheckResult | undefined => {
59
+ if (!mrzText) return undefined;
60
+ const lines = mrzText
61
+ .split('\n')
62
+ .map((l) => l.trim())
63
+ .filter(Boolean);
64
+ if (lines.length < 2 || lines[0].length < 30 || lines[1].length < 30) {
65
+ return undefined; // only TD1 handled here; other formats omitted from summary
66
+ }
67
+ const l1 = lines[0];
68
+ const l2 = lines[1];
69
+ const result: Record<string, 'pass' | 'fail'> = {};
70
+ const check = (data: string, printed: string) =>
71
+ icaoCheckDigit(data) === printed ? 'pass' : 'fail';
72
+
73
+ // Turkish convention: a doc number may carry a leading serial LETTER, and the
74
+ // doc-number check digit is computed over the DIGITS ONLY (letter dropped).
75
+ // The TD1 composite then also diverges from strict ICAO for such cards (no
76
+ // interpretation of the letter reproduces it). To avoid reporting a misleading
77
+ // "fail" on a card the SDK legitimately accepted, detect the letter-prefix and
78
+ // treat both the doc-number and composite as validated under that convention.
79
+ const docData = l1.slice(5, 14);
80
+ const docPrinted = l1[14];
81
+ const hasLetterPrefix = /^[A-Z]/.test(docData);
82
+ const strictDocOk = icaoCheckDigit(docData) === docPrinted;
83
+ const turkishDocOk =
84
+ hasLetterPrefix && icaoCheckDigit(docData.slice(1)) === docPrinted;
85
+ const docOk = strictDocOk || turkishDocOk;
86
+ result.documentNumber = docOk ? 'pass' : 'fail';
87
+ result.birthDate = check(l2.slice(0, 6), l2[6]);
88
+ result.expiry = check(l2.slice(8, 14), l2[14]);
89
+
90
+ const composite =
91
+ l1.slice(5, 30) + l2.slice(0, 7) + l2.slice(8, 15) + l2.slice(18, 29);
92
+ const compStrict = check(composite, l2[29]);
93
+ // If the strict composite fails ONLY because of the Turkish letter-prefix
94
+ // doc number (which itself passed under the digits-only rule), don't surface a
95
+ // misleading failure — the SDK accepts these cards.
96
+ result.composite =
97
+ compStrict === 'fail' && hasLetterPrefix && docOk ? 'pass' : compStrict;
98
+ // The Turkish convention was the deciding factor only when strict ICAO would
99
+ // have failed the doc number but the digits-only rule passes it.
100
+ const turkishLetterPrefix = !strictDocOk && turkishDocOk;
101
+ return { checkDigits: result, turkishLetterPrefix };
102
+ };
103
+
104
+ export interface DiagnosticReportBundle {
105
+ /** Pretty-printed technical report. */
106
+ diagnosticsJson: string;
107
+ /** Pretty-printed scanned identity data (PII). */
108
+ scanDataJson: string;
109
+ /** Compact, size-bounded plain-text email body. */
110
+ body: string;
111
+ /** Suggested email subject. */
112
+ subject: string;
113
+ }
114
+
115
+ const MAX_BODY_BYTES = 4096;
116
+
117
+ const yesNo = (b: boolean | undefined) => (b ? 'yes' : 'no');
118
+
119
+ /** Assemble the full report bundle. Pure — no I/O, no native calls. */
120
+ export const buildDiagnosticReport = (params: {
121
+ diag: DiagnosticsSnapshot;
122
+ device: DeviceDetails;
123
+ scan: ScanDataForReport;
124
+ sdkVersion: string;
125
+ sessionId?: string;
126
+ }): DiagnosticReportBundle => {
127
+ const { diag, device, scan, sdkVersion, sessionId } = params;
128
+
129
+ const computed = computeTd1CheckDigits(scan.mrzText);
130
+ const checkDigits = diag.mrz.checkDigits ?? computed?.checkDigits;
131
+ // Set the Turkish-letter-prefix flag from the final accepted MRZ (the collector
132
+ // can't know it during scanning; the report does, from the MRZ text).
133
+ const turkishLetterPrefixApplied =
134
+ diag.mrz.turkishLetterPrefixApplied || computed?.turkishLetterPrefix === true;
135
+
136
+ // --- diagnostics.json (technical, low-PII) ---
137
+ const diagnosticsObj = {
138
+ schemaVersion: diag.schemaVersion,
139
+ generatedAt: diag.generatedAt,
140
+ sessionId: sessionId ?? null,
141
+ sdk: { platform: device.platform, sdkVersion, buildNumber: device.buildNumber },
142
+ device,
143
+ scan: {
144
+ documentType: scan.documentType ?? diag.documentType ?? null,
145
+ dataSource: scan.dataSource ?? diag.dataSource ?? null,
146
+ totalDurationMs: diag.totalDurationMs,
147
+ camera: diag.camera,
148
+ },
149
+ mrz: {
150
+ ...diag.mrz,
151
+ checkDigits: checkDigits ?? null,
152
+ turkishLetterPrefixApplied,
153
+ },
154
+ nfc: diag.nfc,
155
+ errors: diag.errors,
156
+ };
157
+
158
+ // --- scan-data.json (PII; user reviews before sending) ---
159
+ const scanDataObj = {
160
+ documentType: scan.documentType ?? diag.documentType ?? null,
161
+ dataSource: scan.dataSource ?? diag.dataSource ?? null,
162
+ mrzText: scan.mrzText ?? null,
163
+ fields: scan.mrzFields ?? null,
164
+ barcodeValue: scan.barcodeValue ?? null,
165
+ };
166
+
167
+ // --- compact email body (always under MAX_BODY_BYTES) ---
168
+ const nfc = diag.nfc;
169
+ const nfcLine = nfc.attempted
170
+ ? `${nfc.authProtocol}${nfc.paceFellBackToBac ? '→BAC' : ''} · ${
171
+ nfc.succeeded ? 'success' : `failed at ${nfc.failureStep ?? '?'} (${nfc.errorCategory ?? 'error'})`
172
+ }`
173
+ : 'not attempted';
174
+ const checksLine = checkDigits
175
+ ? Object.entries(checkDigits)
176
+ .map(([k, v]) => `${k}:${v}`)
177
+ .join(' ')
178
+ : 'n/a';
179
+
180
+ const body = [
181
+ 'TRUSTCHEX SCAN REPORT',
182
+ `Device: ${device.brand} ${device.model} · ${device.systemName} ${device.systemVersion}${device.apiLevel ? ` (API ${device.apiLevel})` : ''}`,
183
+ `SDK: ${sdkVersion} (build ${device.buildNumber}) · ${device.manufacturer} · ${device.deviceId}`,
184
+ `Document: ${scanDataObj.documentType ?? 'unknown'} · source: ${scanDataObj.dataSource ?? 'unknown'}`,
185
+ `MRZ: ${diag.mrz.reachedStable ? 'stable' : 'NOT stable'} after ${diag.mrz.framesProcessed} frames · checks: ${checksLine}`,
186
+ `Camera: ${diag.camera.frames} frames · brightness ${diag.camera.avgBrightness} (low:${yesNo(diag.camera.hadLowBrightness)} blur:${yesNo(diag.camera.hadBlurryFrames)})`,
187
+ `NFC: ${nfcLine}`,
188
+ sessionId ? `Session: ${sessionId}` : '',
189
+ '',
190
+ 'Describe the issue here:',
191
+ '',
192
+ '',
193
+ '— Full diagnostics and document images are attached. Please review before sending; do not edit the attachments. —',
194
+ ]
195
+ .filter(Boolean)
196
+ .join('\n');
197
+
198
+ const subject = `Trustchex scan report — ${scanDataObj.documentType ?? 'doc'} — ${device.brand} ${device.model} — v${sdkVersion}${sessionId ? ` — ${sessionId}` : ''}`;
199
+
200
+ return {
201
+ diagnosticsJson: JSON.stringify(diagnosticsObj, null, 2),
202
+ scanDataJson: JSON.stringify(scanDataObj, null, 2),
203
+ body: body.length > MAX_BODY_BYTES ? body.slice(0, MAX_BODY_BYTES - 1) + '…' : body,
204
+ subject,
205
+ };
206
+ };
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Diagnostics collector for the "Report a scanning issue" feature.
3
+ *
4
+ * Accumulates a PII-light technical snapshot of a single scan session — camera
5
+ * quality, MRZ consensus, and (the highest-value, most device-specific part) a
6
+ * structured per-step NFC/eID read log. The result screen serialises this into
7
+ * the diagnostic email the user reviews and sends to support.
8
+ *
9
+ * Design notes:
10
+ * - This is a MODULE-LEVEL SINGLETON so the deep camera/EID call stack can record
11
+ * into it without threading a prop/context through every layer. Call `reset()`
12
+ * at the start of a scan flow.
13
+ * - It stores only TECHNICAL signals (counts, timings, brightness, APDU status
14
+ * words, pass/fail). It deliberately holds NO identity data, images, MRZ text,
15
+ * keys, or nonces — the scanned identity/images shown to the user come from the
16
+ * result screen's own state, not from here, and are attached separately with
17
+ * the user's explicit review in the email draft.
18
+ */
19
+
20
+ /** A single step of the NFC/eID read, with its outcome and timing. */
21
+ export interface NfcStep {
22
+ /** Step name: 'open' | 'selectMF' | 'readCardAccess' | 'paceAuth' | 'bacAuth'
23
+ * | 'selectApplet' | 'readDG1' | 'readDG2' | … */
24
+ step: string;
25
+ ok: boolean;
26
+ ms: number;
27
+ /** APDU status word (hex, e.g. "6982") when the step failed at the APDU level. */
28
+ sw?: string;
29
+ /** Coarse, non-PII error CATEGORY when the step failed (e.g. "auth_failed",
30
+ * "tag_lost", "reading_error") — never the raw error text. */
31
+ error?: string;
32
+ }
33
+
34
+ export interface NfcDiagnostics {
35
+ attempted: boolean;
36
+ chipDetected: boolean;
37
+ /** 'PACE' | 'BAC' | 'none' */
38
+ authProtocol: 'PACE' | 'BAC' | 'none';
39
+ paceFellBackToBac: boolean;
40
+ /** PACE object identifier string when PACE was attempted. */
41
+ paceOid?: string;
42
+ /** Why PACE wasn't used / fell back: 'no_card_access' | 'no_pace_info' |
43
+ * 'pace_failed'. Distinguishes device/chip-specific PACE issues. */
44
+ paceFallbackReason?: string;
45
+ steps: NfcStep[];
46
+ /** Name of the first failing step, if any. */
47
+ failureStep?: string;
48
+ /** Coarse category: 'auth_failed' | 'tag_lost' | 'timeout' | 'unsupported'
49
+ * | 'not_enabled' | 'user_cancelled' | 'reading_error' | null */
50
+ errorCategory?: string;
51
+ retryCount: number;
52
+ succeeded: boolean;
53
+ }
54
+
55
+ export interface CameraDiagnostics {
56
+ frames: number;
57
+ frameW: number;
58
+ frameH: number;
59
+ avgBrightness: number;
60
+ minBrightness: number;
61
+ maxBrightness: number;
62
+ hadLowBrightness: boolean;
63
+ hadBlurryFrames: boolean;
64
+ /**
65
+ * The thresholds the measured values were judged against, so support can read
66
+ * "117 measured vs 65 floor" rather than just a pass/fail flag. Recorded once.
67
+ */
68
+ thresholds?: {
69
+ brightnessLow?: number;
70
+ brightnessHigh?: number;
71
+ minBrightness?: number;
72
+ blur?: number;
73
+ };
74
+ }
75
+
76
+ export interface MrzDiagnostics {
77
+ framesProcessed: number;
78
+ reachedStable: boolean;
79
+ stabilityFrames: number;
80
+ /** Smallest winning vote margin across contested cells at acceptance. */
81
+ minContestedMargin: number;
82
+ /**
83
+ * Largest MRZ-band pixel height seen (max OCR'd text-block height). Compared
84
+ * to the ~16px/character ML Kit floor, this explains "scans on device A,
85
+ * fails on device B" — a smaller band falls below the OCR resolution floor.
86
+ */
87
+ maxMrzBandHeightPx?: number;
88
+ format?: string;
89
+ /** Per-field check-digit outcomes: 'pass' | 'fail' (no values, no PII). */
90
+ checkDigits?: Record<string, 'pass' | 'fail'>;
91
+ /** True when the Turkish leading-serial-letter check-digit convention was used
92
+ * to accept the document number (set at report-build time from the final MRZ). */
93
+ turkishLetterPrefixApplied: boolean;
94
+ }
95
+
96
+ export interface DiagnosticsSnapshot {
97
+ schemaVersion: number;
98
+ generatedAt: string;
99
+ documentType?: string;
100
+ dataSource?: string;
101
+ totalDurationMs: number;
102
+ camera: CameraDiagnostics;
103
+ mrz: MrzDiagnostics;
104
+ nfc: NfcDiagnostics;
105
+ /** Sanitised, non-PII error entries collected during the session. */
106
+ errors: Array<{ code?: string; category?: string; screen?: string }>;
107
+ }
108
+
109
+ const emptyCamera = (): CameraDiagnostics => ({
110
+ frames: 0,
111
+ frameW: 0,
112
+ frameH: 0,
113
+ avgBrightness: 0,
114
+ minBrightness: 0,
115
+ maxBrightness: 0,
116
+ hadLowBrightness: false,
117
+ hadBlurryFrames: false,
118
+ });
119
+
120
+ const emptyMrz = (): MrzDiagnostics => ({
121
+ framesProcessed: 0,
122
+ reachedStable: false,
123
+ stabilityFrames: 0,
124
+ minContestedMargin: 0,
125
+ turkishLetterPrefixApplied: false,
126
+ });
127
+
128
+ const emptyNfc = (): NfcDiagnostics => ({
129
+ attempted: false,
130
+ chipDetected: false,
131
+ authProtocol: 'none',
132
+ paceFellBackToBac: false,
133
+ steps: [],
134
+ retryCount: 0,
135
+ succeeded: false,
136
+ });
137
+
138
+ /**
139
+ * Accumulates one scan session's diagnostics. A single shared instance is
140
+ * exported below; tests can construct their own.
141
+ */
142
+ export class DiagnosticsCollector {
143
+ private startedAtMs = 0;
144
+ private documentType?: string;
145
+ private dataSource?: string;
146
+ private camera = emptyCamera();
147
+ private mrz = emptyMrz();
148
+ private nfc = emptyNfc();
149
+ private errors: DiagnosticsSnapshot['errors'] = [];
150
+
151
+ /** Begin a fresh session — call when a scan flow starts. */
152
+ reset(nowMs: number): void {
153
+ this.startedAtMs = nowMs;
154
+ this.documentType = undefined;
155
+ this.dataSource = undefined;
156
+ this.camera = emptyCamera();
157
+ this.mrz = emptyMrz();
158
+ this.nfc = emptyNfc();
159
+ this.errors = [];
160
+ }
161
+
162
+ setDocument(documentType?: string, dataSource?: string): void {
163
+ if (documentType) this.documentType = documentType;
164
+ if (dataSource) this.dataSource = dataSource;
165
+ }
166
+
167
+ /** Merge a camera snapshot (called periodically as frames arrive). */
168
+ setCamera(c: Partial<CameraDiagnostics>): void {
169
+ this.camera = { ...this.camera, ...c };
170
+ }
171
+
172
+ /** Merge MRZ consensus metrics. `reachedStable` is STICKY (once true, stays
173
+ * true) so a later frame's flicker can't unset that the scan did stabilise;
174
+ * `maxMrzBandHeightPx` keeps the largest seen. */
175
+ setMrz(m: Partial<MrzDiagnostics>): void {
176
+ const reachedStable = this.mrz.reachedStable || m.reachedStable === true;
177
+ const maxMrzBandHeightPx =
178
+ Math.max(
179
+ this.mrz.maxMrzBandHeightPx ?? 0,
180
+ m.maxMrzBandHeightPx ?? 0
181
+ ) || undefined;
182
+ this.mrz = { ...this.mrz, ...m, reachedStable, maxMrzBandHeightPx };
183
+ }
184
+
185
+ // ---- NFC: the structured per-step log ----
186
+
187
+ nfcAttemptStarted(): void {
188
+ // A fresh attempt: bump retry count if a previous attempt left steps.
189
+ if (this.nfc.attempted) this.nfc.retryCount += 1;
190
+ this.nfc.attempted = true;
191
+ }
192
+
193
+ nfcChipDetected(): void {
194
+ this.nfc.chipDetected = true;
195
+ }
196
+
197
+ nfcSetAuth(protocol: 'PACE' | 'BAC' | 'none', oid?: string): void {
198
+ this.nfc.authProtocol = protocol;
199
+ if (oid) this.nfc.paceOid = oid;
200
+ }
201
+
202
+ nfcPaceFellBackToBac(reason?: string): void {
203
+ this.nfc.paceFellBackToBac = true;
204
+ if (reason && !this.nfc.paceFallbackReason) {
205
+ this.nfc.paceFallbackReason = reason;
206
+ }
207
+ }
208
+
209
+ /** Record one completed step. Keep messages short and free of PII. */
210
+ nfcStep(step: NfcStep): void {
211
+ this.nfc.steps.push(step);
212
+ if (!step.ok && !this.nfc.failureStep) {
213
+ this.nfc.failureStep = step.step;
214
+ }
215
+ }
216
+
217
+ nfcResult(succeeded: boolean, errorCategory?: string): void {
218
+ this.nfc.succeeded = succeeded;
219
+ if (succeeded) {
220
+ // The read ultimately succeeded — transient per-attempt failures (e.g. a
221
+ // recovered DG2 retry, or the expected EF.CardAccess 6982 on a BAC-only
222
+ // card) must NOT be reported as the outcome. Clear the failure markers.
223
+ this.nfc.failureStep = undefined;
224
+ this.nfc.errorCategory = undefined;
225
+ } else if (errorCategory) {
226
+ this.nfc.errorCategory = errorCategory;
227
+ }
228
+ }
229
+
230
+ addError(e: { code?: string; category?: string; screen?: string }): void {
231
+ this.errors.push(e);
232
+ }
233
+
234
+ /** Build the serialisable snapshot. */
235
+ snapshot(nowMs: number, isoNow: string): DiagnosticsSnapshot {
236
+ return {
237
+ schemaVersion: 1,
238
+ generatedAt: isoNow,
239
+ documentType: this.documentType,
240
+ dataSource: this.dataSource,
241
+ totalDurationMs: this.startedAtMs ? Math.max(0, nowMs - this.startedAtMs) : 0,
242
+ camera: { ...this.camera },
243
+ mrz: { ...this.mrz },
244
+ nfc: { ...this.nfc, steps: this.nfc.steps.map((s) => ({ ...s })) },
245
+ errors: this.errors.map((e) => ({ ...e })),
246
+ };
247
+ }
248
+ }
249
+
250
+ /** Shared session-scoped collector used across the scan flow. */
251
+ export const diagnostics = new DiagnosticsCollector();
@@ -1,4 +1,5 @@
1
1
  import { trackApiCall, trackError } from './analytics.utils';
2
+ import { debugLog } from './debug.utils';
2
3
 
3
4
  interface ErrorResponse {
4
5
  message: string;
@@ -46,6 +47,17 @@ export class InternalServerError extends HttpClientError {
46
47
  }
47
48
  }
48
49
 
50
+ // Short-lived per-verification token issued by the backend during the session
51
+ // key exchange; sent on every subsequent call. Older backends don't issue one,
52
+ // in which case requests go out without the header (backwards compatible).
53
+ let sessionToken: string | null = null;
54
+
55
+ export const setSessionToken = (token: string | null) => {
56
+ sessionToken = token;
57
+ };
58
+
59
+ export const getSessionToken = () => sessionToken;
60
+
49
61
  const request = async <TResponse, TBody>(
50
62
  httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE',
51
63
  url: string,
@@ -64,10 +76,11 @@ const request = async <TResponse, TBody>(
64
76
  let statusCode = 0;
65
77
  let success = false;
66
78
 
67
- console.log(`[HTTP] ${httpMethod} ${url}`);
79
+ debugLog('HTTP', `${httpMethod} ${url}`);
68
80
  if (body) {
69
- console.log(
70
- '[HTTP] Request body:',
81
+ debugLog(
82
+ 'HTTP',
83
+ 'Request body:',
71
84
  JSON.stringify(body).substring(0, 200) + '...'
72
85
  );
73
86
  }
@@ -77,14 +90,16 @@ const request = async <TResponse, TBody>(
77
90
  method: httpMethod,
78
91
  headers: {
79
92
  'Content-Type': 'application/json',
93
+ ...(sessionToken ? { 'X-Session-Token': sessionToken } : {}),
80
94
  },
81
95
  body: body ? JSON.stringify(body) : undefined,
82
96
  });
83
97
 
84
98
  statusCode = response.status;
85
99
  success = response.ok;
86
- console.log(
87
- `[HTTP] Response status: ${statusCode} ${response.ok ? '✓' : '✗'}`
100
+ debugLog(
101
+ 'HTTP',
102
+ `Response status: ${statusCode} ${response.ok ? '✓' : '✗'}`
88
103
  );
89
104
 
90
105
  let responseJson = null;
@@ -92,14 +107,15 @@ const request = async <TResponse, TBody>(
92
107
  try {
93
108
  responseJson = await response.json();
94
109
  if (responseJson) {
95
- console.log(
96
- '[HTTP] Response body:',
110
+ debugLog(
111
+ 'HTTP',
112
+ 'Response body:',
97
113
  JSON.stringify(responseJson).substring(0, 200) + '...'
98
114
  );
99
115
  }
100
116
  } catch (error) {
101
117
  // Invalid JSON response
102
- console.log('[HTTP] Response body: (non-JSON)');
118
+ debugLog('HTTP', 'Response body: (non-JSON)');
103
119
  }
104
120
 
105
121
  // Track API call performance (non-blocking)