@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.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +76 -24
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
- package/ios/Camera/TrustchexCameraView.swift +20 -15
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
- package/lib/module/Screens/Static/ResultScreen.js +145 -113
- package/lib/module/Shared/Components/DebugOverlay.js +5 -2
- package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +185 -30
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
- package/lib/module/Shared/Components/NavigationManager.js +7 -9
- package/lib/module/Shared/EIDReader/eidReader.js +159 -38
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/lib/module/Shared/Libs/SignalingClient.js +6 -5
- package/lib/module/Shared/Libs/crypto.utils.js +25 -3
- package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
- package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
- package/lib/module/Shared/Libs/diagnostics.js +171 -0
- package/lib/module/Shared/Libs/http-client.js +19 -6
- package/lib/module/Shared/Libs/mrz.utils.js +193 -33
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +74 -3
- package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
- package/lib/module/Shared/Libs/promise.utils.js +4 -3
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
- package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
- package/lib/module/Shared/Services/AnalyticsService.js +8 -2
- package/lib/module/Shared/Services/DataUploadService.js +114 -127
- package/lib/module/Shared/Services/VideoSessionService.js +4 -3
- package/lib/module/Translation/Resources/en.js +6 -0
- package/lib/module/Translation/Resources/tr.js +6 -0
- package/lib/module/Trustchex.js +20 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +55 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +17 -3
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
- package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
- package/src/Screens/Static/ResultScreen.tsx +172 -162
- package/src/Shared/Components/DebugOverlay.tsx +5 -2
- package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
- package/src/Shared/Components/IdentityDocumentCamera.tsx +200 -29
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
- package/src/Shared/Components/NavigationManager.tsx +7 -7
- package/src/Shared/Components/TrustchexCamera.tsx +6 -0
- package/src/Shared/EIDReader/eidReader.ts +181 -52
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/src/Shared/Libs/SignalingClient.ts +10 -5
- package/src/Shared/Libs/crypto.utils.ts +41 -4
- package/src/Shared/Libs/deeplink.utils.ts +39 -7
- package/src/Shared/Libs/diagnosticReport.ts +206 -0
- package/src/Shared/Libs/diagnostics.ts +251 -0
- package/src/Shared/Libs/http-client.ts +24 -8
- package/src/Shared/Libs/mrz.utils.ts +200 -34
- package/src/Shared/Libs/mrzFrameAggregator.ts +112 -2
- package/src/Shared/Libs/native-device-info.utils.ts +116 -0
- package/src/Shared/Libs/promise.utils.ts +11 -5
- package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
- package/src/Shared/Libs/shuffle.utils.ts +15 -0
- package/src/Shared/Services/AnalyticsService.ts +17 -1
- package/src/Shared/Services/DataUploadService.ts +155 -166
- package/src/Shared/Services/VideoSessionService.ts +15 -3
- package/src/Translation/Resources/en.ts +7 -0
- package/src/Translation/Resources/tr.ts +7 -0
- package/src/Trustchex.tsx +26 -3
- 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
|
-
|
|
79
|
+
debugLog('HTTP', `${httpMethod} ${url}`);
|
|
68
80
|
if (body) {
|
|
69
|
-
|
|
70
|
-
'
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
96
|
-
'
|
|
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
|
-
|
|
118
|
+
debugLog('HTTP', 'Response body: (non-JSON)');
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
// Track API call performance (non-blocking)
|