@trustchex/react-native-sdk 1.489.0 → 1.490.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 (27) hide show
  1. package/lib/module/Screens/Static/ResultScreen.js +1 -0
  2. package/lib/module/Shared/Components/DiagnosticReportButton.js +16 -14
  3. package/lib/module/Shared/Libs/diagnosticAuth.js +36 -0
  4. package/lib/module/Shared/Libs/sendDiagnosticReport.js +83 -103
  5. package/lib/module/Translation/Resources/en.js +6 -7
  6. package/lib/module/Translation/Resources/tr.js +6 -7
  7. package/lib/module/version.js +1 -1
  8. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  9. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +9 -6
  10. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -1
  11. package/lib/typescript/src/Shared/Libs/diagnosticAuth.d.ts +8 -0
  12. package/lib/typescript/src/Shared/Libs/diagnosticAuth.d.ts.map +1 -0
  13. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +26 -10
  14. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -1
  15. package/lib/typescript/src/Translation/Resources/en.d.ts +4 -5
  16. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  17. package/lib/typescript/src/Translation/Resources/tr.d.ts +4 -5
  18. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  19. package/lib/typescript/src/version.d.ts +1 -1
  20. package/package.json +1 -5
  21. package/src/Screens/Static/ResultScreen.tsx +1 -0
  22. package/src/Shared/Components/DiagnosticReportButton.tsx +23 -14
  23. package/src/Shared/Libs/diagnosticAuth.ts +47 -0
  24. package/src/Shared/Libs/sendDiagnosticReport.ts +91 -118
  25. package/src/Translation/Resources/en.ts +6 -9
  26. package/src/Translation/Resources/tr.ts +6 -9
  27. package/src/version.ts +1 -1
@@ -762,6 +762,7 @@ const ResultScreen = () => {
762
762
  },
763
763
  children: t('resultScreen.demoStartOver')
764
764
  }), /*#__PURE__*/_jsx(DiagnosticReportButton, {
765
+ baseUrl: appContext.baseUrl,
765
766
  sessionId: appContext.identificationInfo?.sessionId,
766
767
  scan: {
767
768
  documentType: appContext.identificationInfo?.scannedDocument?.documentType,
@@ -4,36 +4,34 @@ import React, { useState } from 'react';
4
4
  import { Alert } from 'react-native';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import StyledButton from "./StyledButton.js";
7
- import { sendDiagnosticReport, isDiagnosticSharingAvailable } from "../Libs/sendDiagnosticReport.js";
7
+ import { sendDiagnosticReport } from "../Libs/sendDiagnosticReport.js";
8
8
  import { SDK_VERSION } from "../../version.js";
9
9
  import { jsx as _jsx } from "react/jsx-runtime";
10
10
  /**
11
11
  * "Report a scanning issue" button for the result screen.
12
12
  *
13
- * Shows a brief notice describing what will be shared (the draft itself is the
14
- * user's review/consent step), then opens the OS share sheet the user picks
15
- * email / WhatsApp / Drive / etc. and sends the diagnostic bundle to support.
16
- * Self-contained so the result screen only needs to render it with the scan
17
- * data + captured images.
13
+ * Shows a brief notice describing what will be shared (acting as the user's
14
+ * consent step), then uploads the diagnostic bundle (technical signals +
15
+ * scanned data + captured images) to the backend, encrypted in transit and at
16
+ * rest. Operators view and download the reports from the dashboard.
17
+ *
18
+ * Hidden when there's no session/baseUrl to upload to (e.g. before init).
18
19
  */
19
20
  const DiagnosticReportButton = ({
20
- scan,
21
+ baseUrl,
21
22
  sessionId,
23
+ scan,
22
24
  images
23
25
  }) => {
24
26
  const {
25
27
  t
26
28
  } = useTranslation();
27
29
  const [sending, setSending] = useState(false);
28
-
29
- // `react-native-share` is optional — hide the button entirely when the host
30
- // app hasn't installed it, so apps that don't want diagnostics see no UI.
31
- if (!isDiagnosticSharingAvailable()) {
30
+ if (!baseUrl || !sessionId) {
32
31
  return null;
33
32
  }
34
33
  const onPress = () => {
35
34
  if (sending) return;
36
- // Brief notice — the share draft is the actual review/consent step.
37
35
  Alert.alert(t('diagnosticReport.noticeTitle'), t('diagnosticReport.noticeBody'), [{
38
36
  text: t('diagnosticReport.cancel'),
39
37
  style: 'cancel'
@@ -42,12 +40,16 @@ const DiagnosticReportButton = ({
42
40
  onPress: async () => {
43
41
  setSending(true);
44
42
  try {
45
- await sendDiagnosticReport({
43
+ const {
44
+ uploaded
45
+ } = await sendDiagnosticReport({
46
+ baseUrl,
47
+ sessionId,
46
48
  scan,
47
49
  sdkVersion: SDK_VERSION,
48
- sessionId,
49
50
  images
50
51
  });
52
+ Alert.alert(uploaded ? t('diagnosticReport.sentTitle') : t('diagnosticReport.failedTitle'), uploaded ? t('diagnosticReport.sentBody') : t('diagnosticReport.failedBody'));
51
53
  } finally {
52
54
  setSending(false);
53
55
  }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ import { Buffer } from 'buffer';
4
+ import { hmac } from '@noble/hashes/hmac';
5
+ import { sha256 } from '@noble/hashes/sha2';
6
+
7
+ /**
8
+ * Authenticity signing for the demo-diagnostics upload.
9
+ *
10
+ * Demo reports have no session/account to authenticate against, so the SDK signs
11
+ * each upload with HMAC-SHA256 over `timestamp.nonce.rawBody` using a shared
12
+ * secret. The backend recomputes and rejects mismatches and stale timestamps
13
+ * (replay). The secret ships in the SDK, so it's an obfuscation barrier (paired
14
+ * with server rate limiting), not a true secret — rotate via the env var on both
15
+ * sides. MUST stay byte-for-byte in sync with web-app/src/lib/diagnostic-hmac.ts.
16
+ */
17
+ const SECRET = process.env.TRUSTCHEX_DIAGNOSTICS_SECRET ?? 'tcx-diag-v1-2f9c4a7e8b1d4f63a05e9c7b3d61f8a2';
18
+ const randomNonce = () => {
19
+ // 16 random bytes as hex. crypto.getRandomValues is polyfilled by
20
+ // react-native-get-random-values (already imported in crypto.utils).
21
+ const bytes = new Uint8Array(16);
22
+ globalThis.crypto.getRandomValues(bytes);
23
+ return Buffer.from(bytes).toString('hex');
24
+ };
25
+
26
+ /** Sign a raw JSON body for the demo-diagnostics endpoint. */
27
+ export const signDiagnosticBody = rawBody => {
28
+ const timestamp = Date.now().toString();
29
+ const nonce = randomNonce();
30
+ const mac = hmac(sha256, Buffer.from(SECRET, 'utf-8'), Buffer.from(`${timestamp}.${nonce}.${rawBody}`, 'utf-8'));
31
+ return {
32
+ timestamp,
33
+ nonce,
34
+ signature: Buffer.from(mac).toString('hex')
35
+ };
36
+ };
@@ -1,130 +1,110 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * Assembles and shares the support diagnostic report.
4
+ * Assembles and uploads the support diagnostic report to the backend.
5
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.
6
+ * This runs from the SDK's DEMO flow only, which is simulated client-side and
7
+ * has no real verification session or account — so there's no session key to
8
+ * encrypt with and nothing to authenticate against. The report is POSTed
9
+ * unauthenticated to the global demo-diagnostics endpoint over HTTPS (encrypted
10
+ * in transit); the backend encrypts the PII fields at rest. Internal developers
11
+ * view and download the reports from a hidden dashboard page.
12
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.
13
+ * Uploaded payload (one JSON body):
14
+ * - non-PII technical metadata (platform, sdkVersion, documentType,
15
+ * nfcErrorCode, paceFallbackReason, succeeded) for the list/filter,
16
+ * - `diagnosticsJson` technical signals (device, camera, MRZ, NFC steps),
17
+ * - `scanDataJson` — the scanned identity data (PII),
18
+ * - `images` — the captured document/face images (base64, PII).
20
19
  */
21
- import RNFS from 'react-native-fs';
20
+ import 'react-native-get-random-values';
22
21
  import { diagnostics } from "./diagnostics.js";
23
22
  import { gatherDeviceDetails } from "./native-device-info.utils.js";
24
23
  import { buildDiagnosticReport } from "./diagnosticReport.js";
24
+ import { signDiagnosticBody } from "./diagnosticAuth.js";
25
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
- };
26
+ const mimeToType = mime => mime === 'png' ? 'image/png' : 'image/jpeg';
44
27
 
45
28
  /**
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.
29
+ * Build the report and upload it (over HTTPS) to the demo-diagnostics endpoint.
30
+ * Returns { uploaded } false on any error (best-effort; a failed report must
31
+ * never disrupt the user's flow).
58
32
  */
59
33
  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
34
  const {
35
+ baseUrl,
36
+ sessionId,
68
37
  scan,
69
38
  sdkVersion,
70
- sessionId,
71
39
  images = []
72
40
  } = params;
41
+ if (!baseUrl) {
42
+ return {
43
+ uploaded: false
44
+ };
45
+ }
73
46
  const now = Date.now();
74
47
  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
48
  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
- }
49
+ const device = await gatherDeviceDetails();
50
+ const snapshot = diagnostics.snapshot(now, iso);
51
+ const report = buildDiagnosticReport({
52
+ diag: snapshot,
53
+ device,
54
+ scan,
55
+ sdkVersion,
56
+ sessionId
57
+ });
58
+
59
+ // Queryable, non-PII technical metadata for the dashboard list/filter.
60
+ const nfc = snapshot.nfc;
61
+ const failingSw = nfc.failureStep ? nfc.steps.find(s => s.step === nfc.failureStep && !s.ok)?.sw : undefined;
62
+ const nfcErrorCode = nfc.attempted && !nfc.succeeded ? `NFC-${(nfc.errorCategory ?? 'reading_error').toUpperCase()}${failingSw ? `-${failingSw}` : ''}` : null;
63
+ const payload = {
64
+ // metadata (stored in plaintext, no PII)
65
+ platform: device.platform,
66
+ osVersion: device.systemVersion,
67
+ sdkVersion,
68
+ documentType: scan.documentType ?? snapshot.documentType ?? null,
69
+ dataSource: scan.dataSource ?? snapshot.dataSource ?? null,
70
+ nfcErrorCode,
71
+ paceFallbackReason: nfc.paceFallbackReason ?? null,
72
+ succeeded: nfc.attempted ? nfc.succeeded : null,
73
+ // full report (PII, encrypted at rest by the backend)
74
+ diagnosticsJson: report.diagnosticsJson,
75
+ scanDataJson: report.scanDataJson,
76
+ images: images.filter(img => img.base64).map(img => ({
77
+ name: img.name,
78
+ base64: img.base64,
79
+ mimeType: mimeToType(img.mime)
80
+ }))
81
+ };
82
+
83
+ // Sign the EXACT body bytes so the backend can verify authenticity. Use
84
+ // fetch directly (httpClient doesn't expose custom headers / raw body).
85
+ const rawBody = JSON.stringify(payload);
86
+ const {
87
+ timestamp,
88
+ nonce,
89
+ signature
90
+ } = signDiagnosticBody(rawBody);
91
+ const response = await fetch(`${baseUrl}/api/app/mobile/diagnostics`, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ 'X-Trustchex-Timestamp': timestamp,
96
+ 'X-Trustchex-Nonce': nonce,
97
+ 'X-Trustchex-Signature': signature
98
+ },
99
+ body: rawBody
100
+ });
101
+ return {
102
+ uploaded: response.ok
103
+ };
119
104
  } catch (e) {
120
- logError('Diagnostics', 'Failed to share diagnostic report', e instanceof Error ? e.message : String(e));
105
+ logError('Diagnostics', 'Failed to upload diagnostic report', e instanceof Error ? e.message : String(e));
121
106
  return {
122
- shared: false
107
+ uploaded: false
123
108
  };
124
- } finally {
125
- // Best-effort cleanup — temp files are short-lived.
126
- setTimeout(() => {
127
- RNFS.unlink(dir).catch(() => {});
128
- }, 60_000);
129
109
  }
130
110
  };
@@ -46,15 +46,14 @@ export default {
46
46
  'resultScreen.demoVerbalConsentText': 'Text',
47
47
  'resultScreen.demoStartOver': 'Start Over',
48
48
  'diagnosticReport.button': 'Report a scanning issue',
49
- 'diagnosticReport.noticeTitle': 'Share diagnostic report',
50
- 'diagnosticReport.noticeBody': 'This opens a draft with your scan data, document images and technical details so support can help resolve a scanning issue. Review it before sending.',
49
+ 'diagnosticReport.noticeTitle': 'Report a scanning issue',
50
+ 'diagnosticReport.noticeBody': 'This sends your scan data, document images and technical details to support so they can help resolve a scanning issue.',
51
51
  'diagnosticReport.continue': 'Continue',
52
52
  'diagnosticReport.cancel': 'Cancel',
53
- 'diagnosticReport.shareFailedTitle': "Couldn't open the share sheet",
54
- 'diagnosticReport.shareFailedBody': "We couldn't attach the report to share. Send it securely to support instead?",
55
- 'diagnosticReport.uploadAction': 'Send to support',
56
- 'diagnosticReport.uploadSuccess': 'Diagnostic report sent to support.',
57
- 'diagnosticReport.uploadFailed': "Couldn't send the report. Please try again later.",
53
+ 'diagnosticReport.sentTitle': 'Report sent',
54
+ 'diagnosticReport.sentBody': 'Your diagnostic report was sent to support. Thank you.',
55
+ 'diagnosticReport.failedTitle': "Couldn't send the report",
56
+ 'diagnosticReport.failedBody': 'Please try again later.',
58
57
  'livenessDetectionScreen.guideHeader': 'Face Verification',
59
58
  'livenessDetectionScreen.guideText': 'Please ensure the following for best results:',
60
59
  'livenessDetectionScreen.guidePoint1': 'Remove glasses, hats, or face coverings',
@@ -46,15 +46,14 @@ export default {
46
46
  'resultScreen.demoVerbalConsentText': 'Metin',
47
47
  'resultScreen.demoStartOver': 'Baştan Başla',
48
48
  'diagnosticReport.button': 'Tarama sorununu bildir',
49
- 'diagnosticReport.noticeTitle': 'Tanılama raporu paylaş',
50
- 'diagnosticReport.noticeBody': 'Bu işlem; tarama verilerinizi, belge görüntülerinizi ve teknik ayrıntıları içeren bir taslak açar; böylece destek ekibi tarama sorununu çözebilir. Göndermeden önce gözden geçirin.',
49
+ 'diagnosticReport.noticeTitle': 'Tarama sorununu bildir',
50
+ 'diagnosticReport.noticeBody': 'Bu işlem; tarama verilerinizi, belge görüntülerinizi ve teknik ayrıntıları destek ekibine gönderir; böylece tarama sorununu çözebilirler.',
51
51
  'diagnosticReport.continue': 'Devam et',
52
52
  'diagnosticReport.cancel': 'İptal',
53
- 'diagnosticReport.shareFailedTitle': 'Paylaşım ekranı açılamadı',
54
- 'diagnosticReport.shareFailedBody': 'Raporu paylaşmak için ekleyemedik. Bunun yerine destek ekibine güvenli şekilde gönderelim mi?',
55
- 'diagnosticReport.uploadAction': 'Desteğe gönder',
56
- 'diagnosticReport.uploadSuccess': 'Tanılama raporu destek ekibine gönderildi.',
57
- 'diagnosticReport.uploadFailed': 'Rapor gönderilemedi. Lütfen daha sonra tekrar deneyin.',
53
+ 'diagnosticReport.sentTitle': 'Rapor gönderildi',
54
+ 'diagnosticReport.sentBody': 'Tanılama raporunuz destek ekibine gönderildi. Teşekkürler.',
55
+ 'diagnosticReport.failedTitle': 'Rapor gönderilemedi',
56
+ 'diagnosticReport.failedBody': 'Lütfen daha sonra tekrar deneyin.',
58
57
  'livenessDetectionScreen.guideHeader': 'Yüz Doğrulaması',
59
58
  'livenessDetectionScreen.guideText': 'En iyi sonuç için lütfen şunları sağlayın:',
60
59
  'livenessDetectionScreen.guidePoint1': 'Gözlük, şapka veya yüz aksesuarlarını çıkarın',
@@ -2,4 +2,4 @@
2
2
 
3
3
  // This file is auto-generated. Do not edit manually.
4
4
  // Version is synced from package.json during build.
5
- export const SDK_VERSION = '1.489.0';
5
+ export const SDK_VERSION = '1.490.1';
@@ -1 +1 @@
1
- {"version":3,"file":"ResultScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Static/ResultScreen.tsx"],"names":[],"mappings":"AA8EA,QAAA,MAAM,YAAY,+CAyhCjB,CAAC;AA+IF,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"ResultScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Static/ResultScreen.tsx"],"names":[],"mappings":"AA8EA,QAAA,MAAM,YAAY,+CA0hCjB,CAAC;AA+IF,eAAe,YAAY,CAAC"}
@@ -2,18 +2,21 @@ import React from 'react';
2
2
  import { type DiagnosticImage } from '../Libs/sendDiagnosticReport';
3
3
  import type { ScanDataForReport } from '../Libs/diagnosticReport';
4
4
  export interface DiagnosticReportButtonProps {
5
- scan: ScanDataForReport;
5
+ /** Backend base URL the SDK was initialised with. */
6
+ baseUrl: string;
6
7
  sessionId?: string;
8
+ scan: ScanDataForReport;
7
9
  images?: DiagnosticImage[];
8
10
  }
9
11
  /**
10
12
  * "Report a scanning issue" button for the result screen.
11
13
  *
12
- * Shows a brief notice describing what will be shared (the draft itself is the
13
- * user's review/consent step), then opens the OS share sheet the user picks
14
- * email / WhatsApp / Drive / etc. and sends the diagnostic bundle to support.
15
- * Self-contained so the result screen only needs to render it with the scan
16
- * data + captured images.
14
+ * Shows a brief notice describing what will be shared (acting as the user's
15
+ * consent step), then uploads the diagnostic bundle (technical signals +
16
+ * scanned data + captured images) to the backend, encrypted in transit and at
17
+ * rest. Operators view and download the reports from the dashboard.
18
+ *
19
+ * Hidden when there's no session/baseUrl to upload to (e.g. before init).
17
20
  */
18
21
  declare const DiagnosticReportButton: React.FC<DiagnosticReportButtonProps>;
19
22
  export default DiagnosticReportButton;
@@ -1 +1 @@
1
- {"version":3,"file":"DiagnosticReportButton.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/DiagnosticReportButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAIxC,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGlE,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,iBAAiB,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,QAAA,MAAM,sBAAsB,EAAE,KAAK,CAAC,EAAE,CAAC,2BAA2B,CA+CjE,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
1
+ {"version":3,"file":"DiagnosticReportButton.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/DiagnosticReportButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAIxC,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGlE,MAAM,WAAW,2BAA2B;IAC1C,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,iBAAiB,CAAC;IACxB,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,QAAA,MAAM,sBAAsB,EAAE,KAAK,CAAC,EAAE,CAAC,2BAA2B,CAsDjE,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
@@ -0,0 +1,8 @@
1
+ export interface DiagnosticSignature {
2
+ timestamp: string;
3
+ nonce: string;
4
+ signature: string;
5
+ }
6
+ /** Sign a raw JSON body for the demo-diagnostics endpoint. */
7
+ export declare const signDiagnosticBody: (rawBody: string) => DiagnosticSignature;
8
+ //# sourceMappingURL=diagnosticAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnosticAuth.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnosticAuth.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAUD,8DAA8D;AAC9D,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,mBAapD,CAAC"}
@@ -1,9 +1,22 @@
1
- import { type ScanDataForReport } from './diagnosticReport';
2
1
  /**
3
- * Whether diagnostic sharing is available (i.e. `react-native-share` is
4
- * installed in the host app). The result-screen button hides itself when false.
2
+ * Assembles and uploads the support diagnostic report to the backend.
3
+ *
4
+ * This runs from the SDK's DEMO flow only, which is simulated client-side and
5
+ * has no real verification session or account — so there's no session key to
6
+ * encrypt with and nothing to authenticate against. The report is POSTed
7
+ * unauthenticated to the global demo-diagnostics endpoint over HTTPS (encrypted
8
+ * in transit); the backend encrypts the PII fields at rest. Internal developers
9
+ * view and download the reports from a hidden dashboard page.
10
+ *
11
+ * Uploaded payload (one JSON body):
12
+ * - non-PII technical metadata (platform, sdkVersion, documentType,
13
+ * nfcErrorCode, paceFallbackReason, succeeded) for the list/filter,
14
+ * - `diagnosticsJson` — technical signals (device, camera, MRZ, NFC steps),
15
+ * - `scanDataJson` — the scanned identity data (PII),
16
+ * - `images` — the captured document/face images (base64, PII).
5
17
  */
6
- export declare const isDiagnosticSharingAvailable: () => boolean;
18
+ import 'react-native-get-random-values';
19
+ import { type ScanDataForReport } from './diagnosticReport';
7
20
  export interface DiagnosticImage {
8
21
  /** Stable file name without extension, e.g. "mrz-side". */
9
22
  name: string;
@@ -11,20 +24,23 @@ export interface DiagnosticImage {
11
24
  mime: 'jpg' | 'png';
12
25
  }
13
26
  export interface SendDiagnosticParams {
27
+ /** Backend base URL (same value the SDK was initialised with). */
28
+ baseUrl: string;
29
+ /** Demo session id (informational; the endpoint is unauthenticated). */
30
+ sessionId?: string;
14
31
  scan: ScanDataForReport;
15
32
  sdkVersion: string;
16
- sessionId?: string;
17
33
  /** base64 images to attach (MRZ side / front / face / NFC chip face). */
18
34
  images?: DiagnosticImage[];
19
35
  }
20
36
  export interface SendResult {
21
- shared: boolean;
22
- /** True when the user dismissed the share sheet (not an error). */
23
- dismissed?: boolean;
37
+ /** True when the report was uploaded and stored. */
38
+ uploaded: boolean;
24
39
  }
25
40
  /**
26
- * Build the report, stage attachments, and present the share sheet. Returns
27
- * { shared } — false on error, with `dismissed` set when the user cancelled.
41
+ * Build the report and upload it (over HTTPS) to the demo-diagnostics endpoint.
42
+ * Returns { uploaded } — false on any error (best-effort; a failed report must
43
+ * never disrupt the user's flow).
28
44
  */
29
45
  export declare const sendDiagnosticReport: (params: SendDiagnosticParams) => Promise<SendResult>;
30
46
  //# sourceMappingURL=sendDiagnosticReport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sendDiagnosticReport.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/sendDiagnosticReport.ts"],"names":[],"mappings":"AAqBA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAyB5B;;;GAGG;AACH,eAAO,MAAM,4BAA4B,QAAO,OAA8B,CAAC;AAE/E,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,iBAAiB,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC5B;AAcD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,oBAAoB,KAC3B,OAAO,CAAC,UAAU,CAsEpB,CAAC"}
1
+ {"version":3,"file":"sendDiagnosticReport.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/sendDiagnosticReport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,gCAAgC,CAAC;AAGxC,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAI5B,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,iBAAiB,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;CACnB;AAKD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,oBAAoB,KAC3B,OAAO,CAAC,UAAU,CA6EpB,CAAC"}
@@ -48,11 +48,10 @@ declare const _default: {
48
48
  'diagnosticReport.noticeBody': string;
49
49
  'diagnosticReport.continue': string;
50
50
  'diagnosticReport.cancel': string;
51
- 'diagnosticReport.shareFailedTitle': string;
52
- 'diagnosticReport.shareFailedBody': string;
53
- 'diagnosticReport.uploadAction': string;
54
- 'diagnosticReport.uploadSuccess': string;
55
- 'diagnosticReport.uploadFailed': string;
51
+ 'diagnosticReport.sentTitle': string;
52
+ 'diagnosticReport.sentBody': string;
53
+ 'diagnosticReport.failedTitle': string;
54
+ 'diagnosticReport.failedBody': string;
56
55
  'livenessDetectionScreen.guideHeader': string;
57
56
  'livenessDetectionScreen.guideText': string;
58
57
  'livenessDetectionScreen.guidePoint1': string;
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/en.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBAmRE"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/en.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBAgRE"}
@@ -48,11 +48,10 @@ declare const _default: {
48
48
  'diagnosticReport.noticeBody': string;
49
49
  'diagnosticReport.continue': string;
50
50
  'diagnosticReport.cancel': string;
51
- 'diagnosticReport.shareFailedTitle': string;
52
- 'diagnosticReport.shareFailedBody': string;
53
- 'diagnosticReport.uploadAction': string;
54
- 'diagnosticReport.uploadSuccess': string;
55
- 'diagnosticReport.uploadFailed': string;
51
+ 'diagnosticReport.sentTitle': string;
52
+ 'diagnosticReport.sentBody': string;
53
+ 'diagnosticReport.failedTitle': string;
54
+ 'diagnosticReport.failedBody': string;
56
55
  'livenessDetectionScreen.guideHeader': string;
57
56
  'livenessDetectionScreen.guideText': string;
58
57
  'livenessDetectionScreen.guidePoint1': string;
@@ -1 +1 @@
1
- {"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/tr.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBAsRE"}
1
+ {"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/tr.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBAmRE"}
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "1.489.0";
1
+ export declare const SDK_VERSION = "1.490.1";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustchex/react-native-sdk",
3
- "version": "1.489.0",
3
+ "version": "1.490.1",
4
4
  "description": "Trustchex mobile app react native SDK for android or ios devices",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -169,7 +169,6 @@
169
169
  "react-native-nfc-manager": ">=3.16.2",
170
170
  "react-native-safe-area-context": ">=5.5.2",
171
171
  "react-native-screens": ">=4.18.0",
172
- "react-native-share": ">=12.0.0",
173
172
  "react-native-sse": ">=1.1.0",
174
173
  "react-native-svg": ">=15.12.0",
175
174
  "react-native-tts": ">=4.1.1",
@@ -217,9 +216,6 @@
217
216
  "react-native-screens": {
218
217
  "optional": false
219
218
  },
220
- "react-native-share": {
221
- "optional": true
222
- },
223
219
  "react-native-sse": {
224
220
  "optional": false
225
221
  },
@@ -1082,6 +1082,7 @@ const ResultScreen = () => {
1082
1082
  {t('resultScreen.demoStartOver')}
1083
1083
  </StyledButton>
1084
1084
  <DiagnosticReportButton
1085
+ baseUrl={appContext.baseUrl}
1085
1086
  sessionId={appContext.identificationInfo?.sessionId}
1086
1087
  scan={{
1087
1088
  documentType:
@@ -4,44 +4,44 @@ import { useTranslation } from 'react-i18next';
4
4
  import StyledButton from './StyledButton';
5
5
  import {
6
6
  sendDiagnosticReport,
7
- isDiagnosticSharingAvailable,
8
7
  type DiagnosticImage,
9
8
  } from '../Libs/sendDiagnosticReport';
10
9
  import type { ScanDataForReport } from '../Libs/diagnosticReport';
11
10
  import { SDK_VERSION } from '../../version';
12
11
 
13
12
  export interface DiagnosticReportButtonProps {
14
- scan: ScanDataForReport;
13
+ /** Backend base URL the SDK was initialised with. */
14
+ baseUrl: string;
15
15
  sessionId?: string;
16
+ scan: ScanDataForReport;
16
17
  images?: DiagnosticImage[];
17
18
  }
18
19
 
19
20
  /**
20
21
  * "Report a scanning issue" button for the result screen.
21
22
  *
22
- * Shows a brief notice describing what will be shared (the draft itself is the
23
- * user's review/consent step), then opens the OS share sheet the user picks
24
- * email / WhatsApp / Drive / etc. and sends the diagnostic bundle to support.
25
- * Self-contained so the result screen only needs to render it with the scan
26
- * data + captured images.
23
+ * Shows a brief notice describing what will be shared (acting as the user's
24
+ * consent step), then uploads the diagnostic bundle (technical signals +
25
+ * scanned data + captured images) to the backend, encrypted in transit and at
26
+ * rest. Operators view and download the reports from the dashboard.
27
+ *
28
+ * Hidden when there's no session/baseUrl to upload to (e.g. before init).
27
29
  */
28
30
  const DiagnosticReportButton: React.FC<DiagnosticReportButtonProps> = ({
29
- scan,
31
+ baseUrl,
30
32
  sessionId,
33
+ scan,
31
34
  images,
32
35
  }) => {
33
36
  const { t } = useTranslation();
34
37
  const [sending, setSending] = useState(false);
35
38
 
36
- // `react-native-share` is optional — hide the button entirely when the host
37
- // app hasn't installed it, so apps that don't want diagnostics see no UI.
38
- if (!isDiagnosticSharingAvailable()) {
39
+ if (!baseUrl || !sessionId) {
39
40
  return null;
40
41
  }
41
42
 
42
43
  const onPress = () => {
43
44
  if (sending) return;
44
- // Brief notice — the share draft is the actual review/consent step.
45
45
  Alert.alert(
46
46
  t('diagnosticReport.noticeTitle'),
47
47
  t('diagnosticReport.noticeBody'),
@@ -52,12 +52,21 @@ const DiagnosticReportButton: React.FC<DiagnosticReportButtonProps> = ({
52
52
  onPress: async () => {
53
53
  setSending(true);
54
54
  try {
55
- await sendDiagnosticReport({
55
+ const { uploaded } = await sendDiagnosticReport({
56
+ baseUrl,
57
+ sessionId,
56
58
  scan,
57
59
  sdkVersion: SDK_VERSION,
58
- sessionId,
59
60
  images,
60
61
  });
62
+ Alert.alert(
63
+ uploaded
64
+ ? t('diagnosticReport.sentTitle')
65
+ : t('diagnosticReport.failedTitle'),
66
+ uploaded
67
+ ? t('diagnosticReport.sentBody')
68
+ : t('diagnosticReport.failedBody')
69
+ );
61
70
  } finally {
62
71
  setSending(false);
63
72
  }
@@ -0,0 +1,47 @@
1
+ import { Buffer } from 'buffer';
2
+ import { hmac } from '@noble/hashes/hmac';
3
+ import { sha256 } from '@noble/hashes/sha2';
4
+
5
+ /**
6
+ * Authenticity signing for the demo-diagnostics upload.
7
+ *
8
+ * Demo reports have no session/account to authenticate against, so the SDK signs
9
+ * each upload with HMAC-SHA256 over `timestamp.nonce.rawBody` using a shared
10
+ * secret. The backend recomputes and rejects mismatches and stale timestamps
11
+ * (replay). The secret ships in the SDK, so it's an obfuscation barrier (paired
12
+ * with server rate limiting), not a true secret — rotate via the env var on both
13
+ * sides. MUST stay byte-for-byte in sync with web-app/src/lib/diagnostic-hmac.ts.
14
+ */
15
+ const SECRET =
16
+ process.env.TRUSTCHEX_DIAGNOSTICS_SECRET ??
17
+ 'tcx-diag-v1-2f9c4a7e8b1d4f63a05e9c7b3d61f8a2';
18
+
19
+ export interface DiagnosticSignature {
20
+ timestamp: string;
21
+ nonce: string;
22
+ signature: string;
23
+ }
24
+
25
+ const randomNonce = (): string => {
26
+ // 16 random bytes as hex. crypto.getRandomValues is polyfilled by
27
+ // react-native-get-random-values (already imported in crypto.utils).
28
+ const bytes = new Uint8Array(16);
29
+ globalThis.crypto.getRandomValues(bytes);
30
+ return Buffer.from(bytes).toString('hex');
31
+ };
32
+
33
+ /** Sign a raw JSON body for the demo-diagnostics endpoint. */
34
+ export const signDiagnosticBody = (rawBody: string): DiagnosticSignature => {
35
+ const timestamp = Date.now().toString();
36
+ const nonce = randomNonce();
37
+ const mac = hmac(
38
+ sha256,
39
+ Buffer.from(SECRET, 'utf-8'),
40
+ Buffer.from(`${timestamp}.${nonce}.${rawBody}`, 'utf-8')
41
+ );
42
+ return {
43
+ timestamp,
44
+ nonce,
45
+ signature: Buffer.from(mac).toString('hex'),
46
+ };
47
+ };
@@ -1,58 +1,30 @@
1
1
  /**
2
- * Assembles and shares the support diagnostic report.
2
+ * Assembles and uploads the support diagnostic report to the backend.
3
3
  *
4
- * `react-native-share` is an OPTIONAL dependency: the host app opts into the
5
- * diagnostic feature by installing it. When it's present we open the OS share
6
- * sheet with the full report (the user picks email / WhatsApp / Drive / etc. and
7
- * reviews before sending); when it's absent the feature is silently unavailable
8
- * `isDiagnosticSharingAvailable()` returns false and the result-screen button
9
- * renders nothing, so apps that don't want it incur no dependency.
4
+ * This runs from the SDK's DEMO flow only, which is simulated client-side and
5
+ * has no real verification session or account — so there's no session key to
6
+ * encrypt with and nothing to authenticate against. The report is POSTed
7
+ * unauthenticated to the global demo-diagnostics endpoint over HTTPS (encrypted
8
+ * in transit); the backend encrypts the PII fields at rest. Internal developers
9
+ * view and download the reports from a hidden dashboard page.
10
10
  *
11
- * Attached files (staged to a temp dir via react-native-fs, already an SDK dep):
12
- * - `diagnostics.json` technical signals (device, camera, MRZ, NFC steps),
13
- * - `scan-data.json` — the scanned identity data (PII; user reviews it),
14
- * - the captured document/face images.
15
- *
16
- * The readable summary rides as the share `message` so it's visible in the
17
- * draft regardless of the chosen app.
11
+ * Uploaded payload (one JSON body):
12
+ * - non-PII technical metadata (platform, sdkVersion, documentType,
13
+ * nfcErrorCode, paceFallbackReason, succeeded) for the list/filter,
14
+ * - `diagnosticsJson` technical signals (device, camera, MRZ, NFC steps),
15
+ * - `scanDataJson` — the scanned identity data (PII),
16
+ * - `images` — the captured document/face images (base64, PII).
18
17
  */
19
- import RNFS from 'react-native-fs';
18
+ import 'react-native-get-random-values';
20
19
  import { diagnostics } from './diagnostics';
21
20
  import { gatherDeviceDetails } from './native-device-info.utils';
22
21
  import {
23
22
  buildDiagnosticReport,
24
23
  type ScanDataForReport,
25
24
  } from './diagnosticReport';
25
+ import { signDiagnosticBody } from './diagnosticAuth';
26
26
  import { logError } from './debug.utils';
27
27
 
28
- // `react-native-share` is OPTIONAL. Load it softly so the SDK bundles and runs
29
- // without it; only host apps that installed it get the diagnostic feature. A
30
- // static `import` would make Metro fail to resolve the module when it's absent,
31
- // so we use a guarded require and keep the reference typed loosely.
32
- type ShareModule = {
33
- open: (options: Record<string, unknown>) => Promise<unknown>;
34
- };
35
-
36
- let shareModule: ShareModule | null | undefined;
37
-
38
- const loadShare = (): ShareModule | null => {
39
- if (shareModule !== undefined) return shareModule;
40
- try {
41
- // eslint-disable-next-line @typescript-eslint/no-var-requires
42
- const mod = require('react-native-share');
43
- shareModule = (mod?.default ?? mod) as ShareModule;
44
- } catch {
45
- shareModule = null; // not installed — feature unavailable
46
- }
47
- return shareModule;
48
- };
49
-
50
- /**
51
- * Whether diagnostic sharing is available (i.e. `react-native-share` is
52
- * installed in the host app). The result-screen button hides itself when false.
53
- */
54
- export const isDiagnosticSharingAvailable = (): boolean => loadShare() != null;
55
-
56
28
  export interface DiagnosticImage {
57
29
  /** Stable file name without extension, e.g. "mrz-side". */
58
30
  name: string;
@@ -61,105 +33,106 @@ export interface DiagnosticImage {
61
33
  }
62
34
 
63
35
  export interface SendDiagnosticParams {
36
+ /** Backend base URL (same value the SDK was initialised with). */
37
+ baseUrl: string;
38
+ /** Demo session id (informational; the endpoint is unauthenticated). */
39
+ sessionId?: string;
64
40
  scan: ScanDataForReport;
65
41
  sdkVersion: string;
66
- sessionId?: string;
67
42
  /** base64 images to attach (MRZ side / front / face / NFC chip face). */
68
43
  images?: DiagnosticImage[];
69
44
  }
70
45
 
71
-
72
- const writeTemp = async (
73
- dir: string,
74
- name: string,
75
- data: string,
76
- encoding: 'utf8' | 'base64'
77
- ): Promise<string> => {
78
- const path = `${dir}/${name}`;
79
- await RNFS.writeFile(path, data, encoding);
80
- return path.startsWith('file://') ? path : `file://${path}`;
81
- };
82
-
83
46
  export interface SendResult {
84
- shared: boolean;
85
- /** True when the user dismissed the share sheet (not an error). */
86
- dismissed?: boolean;
47
+ /** True when the report was uploaded and stored. */
48
+ uploaded: boolean;
87
49
  }
88
50
 
51
+ const mimeToType = (mime: 'jpg' | 'png'): string =>
52
+ mime === 'png' ? 'image/png' : 'image/jpeg';
53
+
89
54
  /**
90
- * Build the report, stage attachments, and present the share sheet. Returns
91
- * { shared } — false on error, with `dismissed` set when the user cancelled.
55
+ * Build the report and upload it (over HTTPS) to the demo-diagnostics endpoint.
56
+ * Returns { uploaded } — false on any error (best-effort; a failed report must
57
+ * never disrupt the user's flow).
92
58
  */
93
59
  export const sendDiagnosticReport = async (
94
60
  params: SendDiagnosticParams
95
61
  ): Promise<SendResult> => {
96
- const share = loadShare();
97
- if (!share) {
98
- // Optional dependency not installed — nothing to do.
99
- return { shared: false };
62
+ const { baseUrl, sessionId, scan, sdkVersion, images = [] } = params;
63
+ if (!baseUrl) {
64
+ return { uploaded: false };
100
65
  }
101
- const { scan, sdkVersion, sessionId, images = [] } = params;
66
+
102
67
  const now = Date.now();
103
68
  const iso = new Date(now).toISOString();
104
69
 
105
- const device = await gatherDeviceDetails();
106
- const report = buildDiagnosticReport({
107
- diag: diagnostics.snapshot(now, iso),
108
- device,
109
- scan,
110
- sdkVersion,
111
- sessionId,
112
- });
113
-
114
- const dir = `${RNFS.TemporaryDirectoryPath}/trustchex-diagnostics-${now}`;
115
70
  try {
116
- await RNFS.mkdir(dir);
117
- const urls: string[] = [];
118
- urls.push(
119
- await writeTemp(dir, 'diagnostics.json', report.diagnosticsJson, 'utf8')
120
- );
121
- urls.push(
122
- await writeTemp(dir, 'scan-data.json', report.scanDataJson, 'utf8')
123
- );
124
- for (const img of images) {
125
- if (!img.base64) continue;
126
- urls.push(
127
- await writeTemp(dir, `${img.name}.${img.mime}`, img.base64, 'base64')
128
- );
129
- }
71
+ const device = await gatherDeviceDetails();
72
+ const snapshot = diagnostics.snapshot(now, iso);
73
+ const report = buildDiagnosticReport({
74
+ diag: snapshot,
75
+ device,
76
+ scan,
77
+ sdkVersion,
78
+ sessionId,
79
+ });
80
+
81
+ // Queryable, non-PII technical metadata for the dashboard list/filter.
82
+ const nfc = snapshot.nfc;
83
+ const failingSw = nfc.failureStep
84
+ ? nfc.steps.find((s) => s.step === nfc.failureStep && !s.ok)?.sw
85
+ : undefined;
86
+ const nfcErrorCode =
87
+ nfc.attempted && !nfc.succeeded
88
+ ? `NFC-${(nfc.errorCategory ?? 'reading_error').toUpperCase()}${failingSw ? `-${failingSw}` : ''}`
89
+ : null;
90
+
91
+ const payload = {
92
+ // metadata (stored in plaintext, no PII)
93
+ platform: device.platform,
94
+ osVersion: device.systemVersion,
95
+ sdkVersion,
96
+ documentType: scan.documentType ?? snapshot.documentType ?? null,
97
+ dataSource: scan.dataSource ?? snapshot.dataSource ?? null,
98
+ nfcErrorCode,
99
+ paceFallbackReason: nfc.paceFallbackReason ?? null,
100
+ succeeded: nfc.attempted ? nfc.succeeded : null,
101
+ // full report (PII, encrypted at rest by the backend)
102
+ diagnosticsJson: report.diagnosticsJson,
103
+ scanDataJson: report.scanDataJson,
104
+ images: images
105
+ .filter((img) => img.base64)
106
+ .map((img) => ({
107
+ name: img.name,
108
+ base64: img.base64,
109
+ mimeType: mimeToType(img.mime),
110
+ })),
111
+ };
112
+
113
+ // Sign the EXACT body bytes so the backend can verify authenticity. Use
114
+ // fetch directly (httpClient doesn't expose custom headers / raw body).
115
+ const rawBody = JSON.stringify(payload);
116
+ const { timestamp, nonce, signature } = signDiagnosticBody(rawBody);
117
+
118
+ const response = await fetch(`${baseUrl}/api/app/mobile/diagnostics`, {
119
+ method: 'POST',
120
+ headers: {
121
+ 'Content-Type': 'application/json',
122
+ 'X-Trustchex-Timestamp': timestamp,
123
+ 'X-Trustchex-Nonce': nonce,
124
+ 'X-Trustchex-Signature': signature,
125
+ },
126
+ body: rawBody,
127
+ });
130
128
 
131
- try {
132
- await share.open({
133
- title: report.subject,
134
- subject: report.subject, // used by email targets
135
- message: report.body,
136
- urls,
137
- // Per-file MIME types so targets render attachments correctly.
138
- filenames: urls.map((u) => u.split('/').pop() ?? 'attachment'),
139
- failOnCancel: false,
140
- });
141
- return { shared: true };
142
- } catch (shareErr) {
143
- // react-native-share throws on user cancel unless failOnCancel:false; treat
144
- // a cancel as a non-error dismissal.
145
- const msg =
146
- shareErr instanceof Error ? shareErr.message : String(shareErr);
147
- if (/cancel|dismiss|user did not share/i.test(msg)) {
148
- return { shared: false, dismissed: true };
149
- }
150
- throw shareErr;
151
- }
129
+ return { uploaded: response.ok };
152
130
  } catch (e) {
153
131
  logError(
154
132
  'Diagnostics',
155
- 'Failed to share diagnostic report',
133
+ 'Failed to upload diagnostic report',
156
134
  e instanceof Error ? e.message : String(e)
157
135
  );
158
- return { shared: false };
159
- } finally {
160
- // Best-effort cleanup — temp files are short-lived.
161
- setTimeout(() => {
162
- RNFS.unlink(dir).catch(() => {});
163
- }, 60_000);
136
+ return { uploaded: false };
164
137
  }
165
138
  };
@@ -54,18 +54,15 @@ export default {
54
54
  'resultScreen.demoVerbalConsentText': 'Text',
55
55
  'resultScreen.demoStartOver': 'Start Over',
56
56
  'diagnosticReport.button': 'Report a scanning issue',
57
- 'diagnosticReport.noticeTitle': 'Share diagnostic report',
57
+ 'diagnosticReport.noticeTitle': 'Report a scanning issue',
58
58
  'diagnosticReport.noticeBody':
59
- 'This opens a draft with your scan data, document images and technical details so support can help resolve a scanning issue. Review it before sending.',
59
+ 'This sends your scan data, document images and technical details to support so they can help resolve a scanning issue.',
60
60
  'diagnosticReport.continue': 'Continue',
61
61
  'diagnosticReport.cancel': 'Cancel',
62
- 'diagnosticReport.shareFailedTitle': "Couldn't open the share sheet",
63
- 'diagnosticReport.shareFailedBody':
64
- "We couldn't attach the report to share. Send it securely to support instead?",
65
- 'diagnosticReport.uploadAction': 'Send to support',
66
- 'diagnosticReport.uploadSuccess': 'Diagnostic report sent to support.',
67
- 'diagnosticReport.uploadFailed':
68
- "Couldn't send the report. Please try again later.",
62
+ 'diagnosticReport.sentTitle': 'Report sent',
63
+ 'diagnosticReport.sentBody': 'Your diagnostic report was sent to support. Thank you.',
64
+ 'diagnosticReport.failedTitle': "Couldn't send the report",
65
+ 'diagnosticReport.failedBody': 'Please try again later.',
69
66
  'livenessDetectionScreen.guideHeader': 'Face Verification',
70
67
  'livenessDetectionScreen.guideText':
71
68
  'Please ensure the following for best results:',
@@ -54,18 +54,15 @@ export default {
54
54
  'resultScreen.demoVerbalConsentText': 'Metin',
55
55
  'resultScreen.demoStartOver': 'Baştan Başla',
56
56
  'diagnosticReport.button': 'Tarama sorununu bildir',
57
- 'diagnosticReport.noticeTitle': 'Tanılama raporu paylaş',
57
+ 'diagnosticReport.noticeTitle': 'Tarama sorununu bildir',
58
58
  'diagnosticReport.noticeBody':
59
- 'Bu işlem; tarama verilerinizi, belge görüntülerinizi ve teknik ayrıntıları içeren bir taslak açar; böylece destek ekibi tarama sorununu çözebilir. Göndermeden önce gözden geçirin.',
59
+ 'Bu işlem; tarama verilerinizi, belge görüntülerinizi ve teknik ayrıntıları destek ekibine gönderir; böylece tarama sorununu çözebilirler.',
60
60
  'diagnosticReport.continue': 'Devam et',
61
61
  'diagnosticReport.cancel': 'İptal',
62
- 'diagnosticReport.shareFailedTitle': 'Paylaşım ekranı açılamadı',
63
- 'diagnosticReport.shareFailedBody':
64
- 'Raporu paylaşmak için ekleyemedik. Bunun yerine destek ekibine güvenli şekilde gönderelim mi?',
65
- 'diagnosticReport.uploadAction': 'Desteğe gönder',
66
- 'diagnosticReport.uploadSuccess': 'Tanılama raporu destek ekibine gönderildi.',
67
- 'diagnosticReport.uploadFailed':
68
- 'Rapor gönderilemedi. Lütfen daha sonra tekrar deneyin.',
62
+ 'diagnosticReport.sentTitle': 'Rapor gönderildi',
63
+ 'diagnosticReport.sentBody': 'Tanılama raporunuz destek ekibine gönderildi. Teşekkürler.',
64
+ 'diagnosticReport.failedTitle': 'Rapor gönderilemedi',
65
+ 'diagnosticReport.failedBody': 'Lütfen daha sonra tekrar deneyin.',
69
66
  'livenessDetectionScreen.guideHeader': 'Yüz Doğrulaması',
70
67
  'livenessDetectionScreen.guideText':
71
68
  'En iyi sonuç için lütfen şunları sağlayın:',
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
2
  // Version is synced from package.json during build.
3
- export const SDK_VERSION = '1.489.0';
3
+ export const SDK_VERSION = '1.490.1';