@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
@@ -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 weight = input.weight && input.weight > 0 ? input.weight : 1;
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
- console.log(`[Retry] Attempt ${retries + 1}/${maxRetries}...`);
11
+ debugLog('Retry', `[Retry] Attempt ${retries + 1}/${maxRetries}...`);
11
12
  }
12
13
  result = await fn();
13
14
  if (retries > 0) {
14
- console.log(`[Retry] ✓ Success on attempt ${retries + 1}`);
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
- console.log(`[Retry] Waiting ${waitTime}ms before retry...`);
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
- // For other 4xx errors (Bad Request, Unauthorized, etc.), do NOT retry.
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