@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
|
@@ -40,6 +40,12 @@ const FORMAT_GEOMETRY = {
|
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
const MRZ_CHAR = /^[A-Z0-9<]$/;
|
|
43
|
+
/**
|
|
44
|
+
* Floor applied to a frame's confidence multiplier. A frame ML Kit was very
|
|
45
|
+
* unsure about still contributes (it may carry the only reading of a cell), but
|
|
46
|
+
* at a fraction of a confident frame's weight.
|
|
47
|
+
*/
|
|
48
|
+
const MIN_CONFIDENCE_WEIGHT = 0.25;
|
|
43
49
|
/**
|
|
44
50
|
* Reconstructs fixed-width MRZ candidate line-sets for a frame's raw OCR text.
|
|
45
51
|
* Reuses the same reconstruction the single-frame validator uses, so the
|
|
@@ -101,6 +107,10 @@ const linesForFrame = text => {
|
|
|
101
107
|
* is valid + stable. Reset between documents with `reset`.
|
|
102
108
|
*/
|
|
103
109
|
export class MRZFrameAggregator {
|
|
110
|
+
// Minimum winning vote margin a CONTESTED cell needs before the consensus is
|
|
111
|
+
// considered confident (and thus eligible to be "stable"). Guards against an
|
|
112
|
+
// early look-alike misread (A↔4, S↔5, B↔8) locking in on a 1-vote lead.
|
|
113
|
+
|
|
104
114
|
// format -> per-line, per-column vote tallies
|
|
105
115
|
tallies = {};
|
|
106
116
|
frameCount = 0;
|
|
@@ -111,6 +121,7 @@ export class MRZFrameAggregator {
|
|
|
111
121
|
constructor(options = {}) {
|
|
112
122
|
this.stabilityTarget = Math.max(1, options.stabilityTarget ?? 2);
|
|
113
123
|
this.maxFrames = Math.max(1, options.maxFrames ?? 60);
|
|
124
|
+
this.minMargin = Math.max(0, options.minMargin ?? 2);
|
|
114
125
|
}
|
|
115
126
|
reset() {
|
|
116
127
|
this.tallies = {};
|
|
@@ -176,7 +187,12 @@ export class MRZFrameAggregator {
|
|
|
176
187
|
* consensus is returned.
|
|
177
188
|
*/
|
|
178
189
|
addFrame(input) {
|
|
179
|
-
const
|
|
190
|
+
const baseWeight = input.weight && input.weight > 0 ? input.weight : 1;
|
|
191
|
+
// Fold OCR confidence into the weight (clamped to [floor, 1]). Absent
|
|
192
|
+
// confidence leaves the weight unchanged, so callers without per-symbol
|
|
193
|
+
// confidence (e.g. iOS ML Kit) keep the prior sharpness-only behaviour.
|
|
194
|
+
const confidenceFactor = typeof input.confidence === 'number' && Number.isFinite(input.confidence) ? Math.min(1, Math.max(MIN_CONFIDENCE_WEIGHT, input.confidence)) : 1;
|
|
195
|
+
const weight = baseWeight * confidenceFactor;
|
|
180
196
|
const lines = linesForFrame(input.text);
|
|
181
197
|
if (lines) {
|
|
182
198
|
// Determine the format this frame's lines fit (by line count + width).
|
|
@@ -224,6 +240,47 @@ export class MRZFrameAggregator {
|
|
|
224
240
|
this.frameCount = Math.ceil(this.frameCount / 2);
|
|
225
241
|
}
|
|
226
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Is every CONTESTED cell of this format decided by a clear margin?
|
|
245
|
+
*
|
|
246
|
+
* A cell is contested when more than one distinct character has received votes
|
|
247
|
+
* (e.g. position 5 of a Turkish doc number gets both "A" and the look-alike
|
|
248
|
+
* "4"). Per-character voting alone reports the consensus "stable" the instant an
|
|
249
|
+
* early misread takes a 1-vote lead and stays unchanged — so a transient
|
|
250
|
+
* "A"→"4" confusion locks in before the correct glyph accumulates enough votes.
|
|
251
|
+
*
|
|
252
|
+
* Requiring the winner to lead the runner-up by `minMargin` weight on every
|
|
253
|
+
* contested cell defers stability until the vote has genuinely settled: a 2-vs-1
|
|
254
|
+
* early lead is NOT confident; a 4-vs-1 (or clean uncontested) reading is. This
|
|
255
|
+
* is what turns "looks valid this frame" into "the camera agrees across frames".
|
|
256
|
+
*/
|
|
257
|
+
consensusIsConfident(format) {
|
|
258
|
+
return this.minContestedMargin(format) >= this.minMargin;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Smallest winning vote margin across all CONTESTED cells (where >1 distinct
|
|
263
|
+
* char was voted). Returns Infinity when no cell is contested. Used both to
|
|
264
|
+
* gate confidence and to surface a diagnostic of how narrowly a look-alike was
|
|
265
|
+
* resolved at acceptance.
|
|
266
|
+
*/
|
|
267
|
+
minContestedMargin(format) {
|
|
268
|
+
const geom = FORMAT_GEOMETRY[format];
|
|
269
|
+
const tally = this.tallies[format];
|
|
270
|
+
let min = Infinity;
|
|
271
|
+
for (let li = 0; li < geom.lines; li++) {
|
|
272
|
+
for (let ci = 0; ci < geom.width; ci++) {
|
|
273
|
+
const cell = tally[li][ci];
|
|
274
|
+
const weights = Object.values(cell);
|
|
275
|
+
if (weights.length < 2) continue; // uncontested cell — no ambiguity
|
|
276
|
+
weights.sort((a, b) => b - a);
|
|
277
|
+
const margin = weights[0] - weights[1];
|
|
278
|
+
if (margin < min) min = margin;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return min;
|
|
282
|
+
}
|
|
283
|
+
|
|
227
284
|
/** Read out the winning character per cell for a format. */
|
|
228
285
|
consensusLines(format) {
|
|
229
286
|
const geom = FORMAT_GEOMETRY[format];
|
|
@@ -259,12 +316,15 @@ export class MRZFrameAggregator {
|
|
|
259
316
|
mrz: null,
|
|
260
317
|
validation: null,
|
|
261
318
|
frames: this.frameCount,
|
|
262
|
-
stable: false
|
|
319
|
+
stable: false,
|
|
320
|
+
stableStreak: 0,
|
|
321
|
+
minContestedMargin: Infinity
|
|
263
322
|
};
|
|
264
323
|
}
|
|
265
324
|
let bestMrz = null;
|
|
266
325
|
let bestValidation = null;
|
|
267
326
|
let bestValid = false;
|
|
327
|
+
let bestFormat = null;
|
|
268
328
|
for (const format of formats) {
|
|
269
329
|
const lines = this.consensusLines(format);
|
|
270
330
|
const mrz = lines.join('\n');
|
|
@@ -275,9 +335,11 @@ export class MRZFrameAggregator {
|
|
|
275
335
|
bestValid = true;
|
|
276
336
|
bestMrz = validation.correctedMrz ?? mrz;
|
|
277
337
|
bestValidation = validation;
|
|
338
|
+
bestFormat = format;
|
|
278
339
|
} else if (!bestValid && bestMrz === null) {
|
|
279
340
|
bestMrz = mrz;
|
|
280
341
|
bestValidation = validation;
|
|
342
|
+
bestFormat = format;
|
|
281
343
|
}
|
|
282
344
|
}
|
|
283
345
|
|
|
@@ -290,11 +352,20 @@ export class MRZFrameAggregator {
|
|
|
290
352
|
this.stableStreak = stableKey ? 1 : 0;
|
|
291
353
|
}
|
|
292
354
|
this.lastConsensusMrz = stableKey;
|
|
355
|
+
|
|
356
|
+
// "Stable" requires BOTH an unchanged streak AND a confident vote: every
|
|
357
|
+
// contested cell must have resolved by a clear margin. The streak alone lets
|
|
358
|
+
// an early look-alike misread (A↔4) lock in the instant it takes a 1-vote
|
|
359
|
+
// lead and stops changing — the confidence gate defers acceptance until the
|
|
360
|
+
// correct glyph has genuinely won, not merely arrived first.
|
|
361
|
+
const confident = bestValid && bestFormat != null && this.consensusIsConfident(bestFormat);
|
|
293
362
|
return {
|
|
294
363
|
mrz: bestMrz,
|
|
295
364
|
validation: bestValidation,
|
|
296
365
|
frames: this.frameCount,
|
|
297
|
-
stable: bestValid && this.stableStreak >= this.stabilityTarget
|
|
366
|
+
stable: bestValid && confident && this.stableStreak >= this.stabilityTarget,
|
|
367
|
+
stableStreak: this.stableStreak,
|
|
368
|
+
minContestedMargin: bestFormat != null ? this.minContestedMargin(bestFormat) : Infinity
|
|
298
369
|
};
|
|
299
370
|
}
|
|
300
371
|
}
|
|
@@ -2,6 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
import { Platform } from 'react-native';
|
|
4
4
|
import DeviceInfo from 'react-native-device-info';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Full, non-PII device details for the support diagnostic report. Device-
|
|
8
|
+
* specific scanning/NFC issues correlate with hardware, OS level, memory and
|
|
9
|
+
* NFC capability, so we capture a broad-but-safe set. Excludes anything that
|
|
10
|
+
* identifies a person (uniqueId is a per-install vendor id, not PII, and is kept
|
|
11
|
+
* only for correlating repeat reports from the same device).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Resolve an async getter to a number, tolerating failure. */
|
|
15
|
+
const num = async p => {
|
|
16
|
+
try {
|
|
17
|
+
const v = await p;
|
|
18
|
+
return typeof v === 'number' && isFinite(v) ? v : undefined;
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Resolve an async getter, falling back on failure. */
|
|
25
|
+
const safe = async (p, fallback) => {
|
|
26
|
+
try {
|
|
27
|
+
return await p;
|
|
28
|
+
} catch {
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Read a synchronous getter, tolerating failure. */
|
|
34
|
+
const sync = (fn, fallback) => {
|
|
35
|
+
try {
|
|
36
|
+
return fn();
|
|
37
|
+
} catch {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Gather the full non-PII device profile for a diagnostic report. */
|
|
43
|
+
export const gatherDeviceDetails = async () => {
|
|
44
|
+
const bytesToMB = b => b === undefined ? undefined : Math.round(b / (1024 * 1024));
|
|
45
|
+
const isAndroid = Platform.OS === 'android';
|
|
46
|
+
const [manufacturer, isEmulator, apiLevel, installerPackage, totalMemory, usedMemory, totalDisk, freeDisk, batteryLevel, fontScale] = await Promise.all([safe(DeviceInfo.getManufacturer(), 'unknown'), safe(DeviceInfo.isEmulator(), false), isAndroid ? num(DeviceInfo.getApiLevel()) : Promise.resolve(undefined), safe(DeviceInfo.getInstallerPackageName(), 'unknown'), num(DeviceInfo.getTotalMemory()), num(DeviceInfo.getUsedMemory()), num(DeviceInfo.getTotalDiskCapacity()), num(DeviceInfo.getFreeDiskStorage()), num(DeviceInfo.getBatteryLevel()), num(DeviceInfo.getFontScale())]);
|
|
47
|
+
return {
|
|
48
|
+
platform: Platform.OS,
|
|
49
|
+
systemName: isAndroid ? 'Android' : 'iOS',
|
|
50
|
+
systemVersion: sync(() => DeviceInfo.getSystemVersion(), 'unknown'),
|
|
51
|
+
apiLevel,
|
|
52
|
+
brand: sync(() => DeviceInfo.getBrand(), 'unknown'),
|
|
53
|
+
manufacturer,
|
|
54
|
+
model: sync(() => DeviceInfo.getModel(), 'unknown'),
|
|
55
|
+
deviceId: sync(() => DeviceInfo.getDeviceId(), 'unknown'),
|
|
56
|
+
deviceType: sync(() => DeviceInfo.getDeviceType(), 'unknown'),
|
|
57
|
+
isTablet: sync(() => DeviceInfo.isTablet(), false),
|
|
58
|
+
isEmulator,
|
|
59
|
+
hasNotch: sync(() => DeviceInfo.hasNotch(), false),
|
|
60
|
+
totalMemoryMB: bytesToMB(totalMemory),
|
|
61
|
+
usedMemoryMB: bytesToMB(usedMemory),
|
|
62
|
+
totalDiskMB: bytesToMB(totalDisk),
|
|
63
|
+
freeDiskMB: bytesToMB(freeDisk),
|
|
64
|
+
batteryLevel,
|
|
65
|
+
appVersion: sync(() => DeviceInfo.getVersion(), 'unknown'),
|
|
66
|
+
buildNumber: sync(() => DeviceInfo.getBuildNumber(), 'unknown'),
|
|
67
|
+
bundleId: sync(() => DeviceInfo.getBundleId(), 'unknown'),
|
|
68
|
+
fontScale,
|
|
69
|
+
installerPackage
|
|
70
|
+
};
|
|
71
|
+
};
|
|
5
72
|
export const NativeDeviceInfo = {
|
|
6
73
|
generateHumanReadableIdentifier: async () => {
|
|
7
74
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { debugLog } from "./debug.utils.js";
|
|
3
4
|
const runWithRetry = async (fn, maxRetries = 3, delay = 1000) => {
|
|
4
5
|
let retries = 0;
|
|
5
6
|
let lastError;
|
|
@@ -7,11 +8,11 @@ const runWithRetry = async (fn, maxRetries = 3, delay = 1000) => {
|
|
|
7
8
|
while (retries < maxRetries) {
|
|
8
9
|
try {
|
|
9
10
|
if (retries > 0) {
|
|
10
|
-
|
|
11
|
+
debugLog('Retry', `[Retry] Attempt ${retries + 1}/${maxRetries}...`);
|
|
11
12
|
}
|
|
12
13
|
result = await fn();
|
|
13
14
|
if (retries > 0) {
|
|
14
|
-
|
|
15
|
+
debugLog('Retry', `[Retry] ✓ Success on attempt ${retries + 1}`);
|
|
15
16
|
}
|
|
16
17
|
return result;
|
|
17
18
|
} catch (error) {
|
|
@@ -20,7 +21,7 @@ const runWithRetry = async (fn, maxRetries = 3, delay = 1000) => {
|
|
|
20
21
|
console.error(`[Retry] ✗ Attempt ${retries}/${maxRetries} failed:`, error instanceof Error ? error.message : error);
|
|
21
22
|
if (retries < maxRetries) {
|
|
22
23
|
const waitTime = delay * retries;
|
|
23
|
-
|
|
24
|
+
debugLog('Retry', `[Retry] Waiting ${waitTime}ms before retry...`);
|
|
24
25
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Assembles and shares the support diagnostic report.
|
|
5
|
+
*
|
|
6
|
+
* `react-native-share` is an OPTIONAL dependency: the host app opts into the
|
|
7
|
+
* diagnostic feature by installing it. When it's present we open the OS share
|
|
8
|
+
* sheet with the full report (the user picks email / WhatsApp / Drive / etc. and
|
|
9
|
+
* reviews before sending); when it's absent the feature is silently unavailable
|
|
10
|
+
* — `isDiagnosticSharingAvailable()` returns false and the result-screen button
|
|
11
|
+
* renders nothing, so apps that don't want it incur no dependency.
|
|
12
|
+
*
|
|
13
|
+
* Attached files (staged to a temp dir via react-native-fs, already an SDK dep):
|
|
14
|
+
* - `diagnostics.json` — technical signals (device, camera, MRZ, NFC steps),
|
|
15
|
+
* - `scan-data.json` — the scanned identity data (PII; user reviews it),
|
|
16
|
+
* - the captured document/face images.
|
|
17
|
+
*
|
|
18
|
+
* The readable summary rides as the share `message` so it's visible in the
|
|
19
|
+
* draft regardless of the chosen app.
|
|
20
|
+
*/
|
|
21
|
+
import RNFS from 'react-native-fs';
|
|
22
|
+
import { diagnostics } from "./diagnostics.js";
|
|
23
|
+
import { gatherDeviceDetails } from "./native-device-info.utils.js";
|
|
24
|
+
import { buildDiagnosticReport } from "./diagnosticReport.js";
|
|
25
|
+
import { logError } from "./debug.utils.js";
|
|
26
|
+
|
|
27
|
+
// `react-native-share` is OPTIONAL. Load it softly so the SDK bundles and runs
|
|
28
|
+
// without it; only host apps that installed it get the diagnostic feature. A
|
|
29
|
+
// static `import` would make Metro fail to resolve the module when it's absent,
|
|
30
|
+
// so we use a guarded require and keep the reference typed loosely.
|
|
31
|
+
|
|
32
|
+
let shareModule;
|
|
33
|
+
const loadShare = () => {
|
|
34
|
+
if (shareModule !== undefined) return shareModule;
|
|
35
|
+
try {
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
37
|
+
const mod = require('react-native-share');
|
|
38
|
+
shareModule = mod?.default ?? mod;
|
|
39
|
+
} catch {
|
|
40
|
+
shareModule = null; // not installed — feature unavailable
|
|
41
|
+
}
|
|
42
|
+
return shareModule;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether diagnostic sharing is available (i.e. `react-native-share` is
|
|
47
|
+
* installed in the host app). The result-screen button hides itself when false.
|
|
48
|
+
*/
|
|
49
|
+
export const isDiagnosticSharingAvailable = () => loadShare() != null;
|
|
50
|
+
const writeTemp = async (dir, name, data, encoding) => {
|
|
51
|
+
const path = `${dir}/${name}`;
|
|
52
|
+
await RNFS.writeFile(path, data, encoding);
|
|
53
|
+
return path.startsWith('file://') ? path : `file://${path}`;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Build the report, stage attachments, and present the share sheet. Returns
|
|
57
|
+
* { shared } — false on error, with `dismissed` set when the user cancelled.
|
|
58
|
+
*/
|
|
59
|
+
export const sendDiagnosticReport = async params => {
|
|
60
|
+
const share = loadShare();
|
|
61
|
+
if (!share) {
|
|
62
|
+
// Optional dependency not installed — nothing to do.
|
|
63
|
+
return {
|
|
64
|
+
shared: false
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const {
|
|
68
|
+
scan,
|
|
69
|
+
sdkVersion,
|
|
70
|
+
sessionId,
|
|
71
|
+
images = []
|
|
72
|
+
} = params;
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const iso = new Date(now).toISOString();
|
|
75
|
+
const device = await gatherDeviceDetails();
|
|
76
|
+
const report = buildDiagnosticReport({
|
|
77
|
+
diag: diagnostics.snapshot(now, iso),
|
|
78
|
+
device,
|
|
79
|
+
scan,
|
|
80
|
+
sdkVersion,
|
|
81
|
+
sessionId
|
|
82
|
+
});
|
|
83
|
+
const dir = `${RNFS.TemporaryDirectoryPath}/trustchex-diagnostics-${now}`;
|
|
84
|
+
try {
|
|
85
|
+
await RNFS.mkdir(dir);
|
|
86
|
+
const urls = [];
|
|
87
|
+
urls.push(await writeTemp(dir, 'diagnostics.json', report.diagnosticsJson, 'utf8'));
|
|
88
|
+
urls.push(await writeTemp(dir, 'scan-data.json', report.scanDataJson, 'utf8'));
|
|
89
|
+
for (const img of images) {
|
|
90
|
+
if (!img.base64) continue;
|
|
91
|
+
urls.push(await writeTemp(dir, `${img.name}.${img.mime}`, img.base64, 'base64'));
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
await share.open({
|
|
95
|
+
title: report.subject,
|
|
96
|
+
subject: report.subject,
|
|
97
|
+
// used by email targets
|
|
98
|
+
message: report.body,
|
|
99
|
+
urls,
|
|
100
|
+
// Per-file MIME types so targets render attachments correctly.
|
|
101
|
+
filenames: urls.map(u => u.split('/').pop() ?? 'attachment'),
|
|
102
|
+
failOnCancel: false
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
shared: true
|
|
106
|
+
};
|
|
107
|
+
} catch (shareErr) {
|
|
108
|
+
// react-native-share throws on user cancel unless failOnCancel:false; treat
|
|
109
|
+
// a cancel as a non-error dismissal.
|
|
110
|
+
const msg = shareErr instanceof Error ? shareErr.message : String(shareErr);
|
|
111
|
+
if (/cancel|dismiss|user did not share/i.test(msg)) {
|
|
112
|
+
return {
|
|
113
|
+
shared: false,
|
|
114
|
+
dismissed: true
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
throw shareErr;
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
logError('Diagnostics', 'Failed to share diagnostic report', e instanceof Error ? e.message : String(e));
|
|
121
|
+
return {
|
|
122
|
+
shared: false
|
|
123
|
+
};
|
|
124
|
+
} finally {
|
|
125
|
+
// Best-effort cleanup — temp files are short-lived.
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
RNFS.unlink(dir).catch(() => {});
|
|
128
|
+
}, 60_000);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import 'react-native-get-random-values';
|
|
4
|
+
|
|
5
|
+
// Unbiased Fisher–Yates shuffle backed by a CSPRNG. Used for the liveness
|
|
6
|
+
// instruction order, which acts as an anti-replay challenge and must not be
|
|
7
|
+
// predictable.
|
|
8
|
+
export const secureShuffle = items => {
|
|
9
|
+
const result = [...items];
|
|
10
|
+
const random = new Uint32Array(1);
|
|
11
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
12
|
+
crypto.getRandomValues(random);
|
|
13
|
+
const j = random[0] % (i + 1);
|
|
14
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
@@ -263,7 +263,13 @@ class AnalyticsService {
|
|
|
263
263
|
throw new Error('Rate limit exceeded');
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
//
|
|
266
|
+
// Auth failures are transient from the SDK's perspective (e.g. a token
|
|
267
|
+
// rollout); throw so the batch is retried instead of silently dropped.
|
|
268
|
+
if (response.status === 401 || response.status === 403) {
|
|
269
|
+
throw new Error(`Authentication error: ${response.status}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// For other 4xx errors (Bad Request, etc.), do NOT retry.
|
|
267
273
|
// We return successfully so the bad events are removed from the queue.
|
|
268
274
|
if (response.status >= 400 && response.status < 500) {
|
|
269
275
|
if (__DEV__) debugWarn('AnalyticsService', `[Analytics] Dropping batch due to ${response.status} error`);
|
|
@@ -354,7 +360,7 @@ class AnalyticsService {
|
|
|
354
360
|
sanitizeMetadata(metadata) {
|
|
355
361
|
if (!metadata) return undefined;
|
|
356
362
|
const sanitized = {};
|
|
357
|
-
const forbiddenKeys = ['email', 'emailaddress', 'phone', 'phonenumber', 'firstname', 'lastname', 'fullname', 'address', 'streetaddress', 'ssn', 'socialsecurity', 'passport', 'passportnumber', 'driverlicense', 'creditcard', 'cardnumber', 'cvv', 'password', 'apikey', 'accesstoken'];
|
|
363
|
+
const forbiddenKeys = ['email', 'emailaddress', 'phone', 'phonenumber', 'firstname', 'lastname', 'fullname', 'address', 'streetaddress', 'ssn', 'socialsecurity', 'passport', 'passportnumber', 'driverlicense', 'creditcard', 'cardnumber', 'cvv', 'password', 'apikey', 'accesstoken', 'documentnumber', 'personalnumber', 'nationalid', 'tckimlik', 'mrz', 'mrztext', 'birthdate', 'dateofbirth', 'sessionkey', 'sessiontoken'];
|
|
358
364
|
for (const [key, value] of Object.entries(metadata)) {
|
|
359
365
|
const lowerKey = key.toLowerCase();
|
|
360
366
|
|