@trustchex/react-native-sdk 1.489.0 → 1.491.0
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/lib/module/Screens/Static/ResultScreen.js +1 -0
- package/lib/module/Shared/Components/DiagnosticReportButton.js +16 -14
- package/lib/module/Shared/Libs/diagnosticPow.js +45 -0
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +91 -99
- package/lib/module/Translation/Resources/en.js +6 -7
- package/lib/module/Translation/Resources/tr.js +6 -7
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +9 -6
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticPow.d.ts +22 -0
- package/lib/typescript/src/Shared/Libs/diagnosticPow.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +9 -11
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +4 -5
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +4 -5
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -5
- package/src/Screens/Static/ResultScreen.tsx +1 -0
- package/src/Shared/Components/DiagnosticReportButton.tsx +23 -14
- package/src/Shared/Libs/diagnosticPow.ts +54 -0
- package/src/Shared/Libs/sendDiagnosticReport.ts +99 -116
- package/src/Translation/Resources/en.ts +6 -9
- package/src/Translation/Resources/tr.ts +6 -9
- 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
|
|
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 (
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
4
|
+
import { Buffer } from 'buffer';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Proof-of-work solver for the demo-diagnostics authenticity flow.
|
|
8
|
+
*
|
|
9
|
+
* No secret ships in the SDK. To upload a demo report, the SDK first mints a
|
|
10
|
+
* server-signed challenge token (`POST …/diagnostics/challenge`) and then finds
|
|
11
|
+
* a `solution` nonce such that `SHA256(challenge.solution)` has at least
|
|
12
|
+
* `difficulty` leading zero bits (hashcash-style). The server re-verifies the
|
|
13
|
+
* token signature (server-only key) and the PoW. See web-app diagnostic-pow.ts.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const leadingZeroBits = bytes => {
|
|
17
|
+
let bits = 0;
|
|
18
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
19
|
+
const b = bytes[i];
|
|
20
|
+
if (b === 0) {
|
|
21
|
+
bits += 8;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// bits before the highest set bit in this byte (b is 1..255)
|
|
25
|
+
bits += Math.clz32(b) - 24;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
return bits;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Solve the PoW for a challenge. Returns the solution nonce as a string.
|
|
33
|
+
* Bounded by `maxIterations` so a pathological difficulty can't hang the app;
|
|
34
|
+
* returns null if unsolved within the bound.
|
|
35
|
+
*/
|
|
36
|
+
export const solvePow = (challenge, difficulty, maxIterations = 8_000_000) => {
|
|
37
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
38
|
+
const solution = i.toString();
|
|
39
|
+
const digest = sha256(Buffer.from(`${challenge}.${solution}`, 'utf-8'));
|
|
40
|
+
if (leadingZeroBits(digest) >= difficulty) {
|
|
41
|
+
return solution;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
@@ -1,130 +1,122 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Assembles and
|
|
4
|
+
* Assembles and uploads the support diagnostic report to the backend.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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 no credential to authenticate with. Instead, authenticity is
|
|
9
|
+
* proven with a proof-of-work: the SDK mints a server-signed challenge token
|
|
10
|
+
* (no secret ships in the SDK), solves it, and submits the solution with the
|
|
11
|
+
* report over HTTPS. The backend verifies the token (server-only key) + PoW and
|
|
12
|
+
* encrypts the PII fields at rest. Internal developers view and download the
|
|
13
|
+
* reports from a hidden dashboard page.
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* draft regardless of the chosen app.
|
|
15
|
+
* Uploaded payload (one JSON body):
|
|
16
|
+
* - non-PII technical metadata (platform, sdkVersion, documentType,
|
|
17
|
+
* nfcErrorCode, paceFallbackReason, succeeded) for the list/filter,
|
|
18
|
+
* - `diagnosticsJson` — technical signals (device, camera, MRZ, NFC steps),
|
|
19
|
+
* - `scanDataJson` — the scanned identity data (PII),
|
|
20
|
+
* - `images` — the captured document/face images (base64, PII).
|
|
20
21
|
*/
|
|
21
|
-
import RNFS from 'react-native-fs';
|
|
22
22
|
import { diagnostics } from "./diagnostics.js";
|
|
23
23
|
import { gatherDeviceDetails } from "./native-device-info.utils.js";
|
|
24
24
|
import { buildDiagnosticReport } from "./diagnosticReport.js";
|
|
25
|
+
import { solvePow } from "./diagnosticPow.js";
|
|
25
26
|
import { logError } from "./debug.utils.js";
|
|
27
|
+
const mimeToType = mime => mime === 'png' ? 'image/png' : 'image/jpeg';
|
|
26
28
|
|
|
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
29
|
/**
|
|
56
|
-
* Build the report
|
|
57
|
-
* {
|
|
30
|
+
* Build the report and upload it (over HTTPS) to the demo-diagnostics endpoint.
|
|
31
|
+
* Returns { uploaded } — false on any error (best-effort; a failed report must
|
|
32
|
+
* never disrupt the user's flow).
|
|
58
33
|
*/
|
|
59
34
|
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
35
|
const {
|
|
36
|
+
baseUrl,
|
|
37
|
+
sessionId,
|
|
68
38
|
scan,
|
|
69
39
|
sdkVersion,
|
|
70
|
-
sessionId,
|
|
71
40
|
images = []
|
|
72
41
|
} = params;
|
|
42
|
+
if (!baseUrl) {
|
|
43
|
+
return {
|
|
44
|
+
uploaded: false
|
|
45
|
+
};
|
|
46
|
+
}
|
|
73
47
|
const now = Date.now();
|
|
74
48
|
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
49
|
try {
|
|
85
|
-
await
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
50
|
+
const device = await gatherDeviceDetails();
|
|
51
|
+
const snapshot = diagnostics.snapshot(now, iso);
|
|
52
|
+
const report = buildDiagnosticReport({
|
|
53
|
+
diag: snapshot,
|
|
54
|
+
device,
|
|
55
|
+
scan,
|
|
56
|
+
sdkVersion,
|
|
57
|
+
sessionId
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Queryable, non-PII technical metadata for the dashboard list/filter.
|
|
61
|
+
const nfc = snapshot.nfc;
|
|
62
|
+
const failingSw = nfc.failureStep ? nfc.steps.find(s => s.step === nfc.failureStep && !s.ok)?.sw : undefined;
|
|
63
|
+
const nfcErrorCode = nfc.attempted && !nfc.succeeded ? `NFC-${(nfc.errorCategory ?? 'reading_error').toUpperCase()}${failingSw ? `-${failingSw}` : ''}` : null;
|
|
64
|
+
const payload = {
|
|
65
|
+
// metadata (stored in plaintext, no PII)
|
|
66
|
+
platform: device.platform,
|
|
67
|
+
osVersion: device.systemVersion,
|
|
68
|
+
sdkVersion,
|
|
69
|
+
documentType: scan.documentType ?? snapshot.documentType ?? null,
|
|
70
|
+
dataSource: scan.dataSource ?? snapshot.dataSource ?? null,
|
|
71
|
+
nfcErrorCode,
|
|
72
|
+
paceFallbackReason: nfc.paceFallbackReason ?? null,
|
|
73
|
+
succeeded: nfc.attempted ? nfc.succeeded : null,
|
|
74
|
+
// full report (PII, encrypted at rest by the backend)
|
|
75
|
+
diagnosticsJson: report.diagnosticsJson,
|
|
76
|
+
scanDataJson: report.scanDataJson,
|
|
77
|
+
images: images.filter(img => img.base64).map(img => ({
|
|
78
|
+
name: img.name,
|
|
79
|
+
base64: img.base64,
|
|
80
|
+
mimeType: mimeToType(img.mime)
|
|
81
|
+
}))
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Authenticity via proof of work: mint a server-signed challenge, solve it,
|
|
85
|
+
// and submit the solution. No secret ships in the SDK.
|
|
86
|
+
const challengeRes = await fetch(`${baseUrl}/api/app/mobile/diagnostics/challenge`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json'
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (!challengeRes.ok) {
|
|
93
|
+
return {
|
|
94
|
+
uploaded: false
|
|
95
|
+
};
|
|
92
96
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
});
|
|
97
|
+
const challenge = await challengeRes.json();
|
|
98
|
+
const solution = solvePow(challenge.challenge, challenge.difficulty);
|
|
99
|
+
if (!solution) {
|
|
104
100
|
return {
|
|
105
|
-
|
|
101
|
+
uploaded: false
|
|
106
102
|
};
|
|
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
103
|
}
|
|
104
|
+
const response = await fetch(`${baseUrl}/api/app/mobile/diagnostics`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: {
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
'X-Trustchex-Pow-Token': challenge.token,
|
|
109
|
+
'X-Trustchex-Pow-Solution': solution
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify(payload)
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
uploaded: response.ok
|
|
115
|
+
};
|
|
119
116
|
} catch (e) {
|
|
120
|
-
logError('Diagnostics', 'Failed to
|
|
117
|
+
logError('Diagnostics', 'Failed to upload diagnostic report', e instanceof Error ? e.message : String(e));
|
|
121
118
|
return {
|
|
122
|
-
|
|
119
|
+
uploaded: false
|
|
123
120
|
};
|
|
124
|
-
} finally {
|
|
125
|
-
// Best-effort cleanup — temp files are short-lived.
|
|
126
|
-
setTimeout(() => {
|
|
127
|
-
RNFS.unlink(dir).catch(() => {});
|
|
128
|
-
}, 60_000);
|
|
129
121
|
}
|
|
130
122
|
};
|
|
@@ -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': '
|
|
50
|
-
'diagnosticReport.noticeBody': 'This
|
|
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.
|
|
54
|
-
'diagnosticReport.
|
|
55
|
-
'diagnosticReport.
|
|
56
|
-
'diagnosticReport.
|
|
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': '
|
|
50
|
-
'diagnosticReport.noticeBody': 'Bu işlem; tarama verilerinizi, belge görüntülerinizi ve teknik ayrıntıları
|
|
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.
|
|
54
|
-
'diagnosticReport.
|
|
55
|
-
'diagnosticReport.
|
|
56
|
-
'diagnosticReport.
|
|
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',
|
package/lib/module/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResultScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Static/ResultScreen.tsx"],"names":[],"mappings":"AA8EA,QAAA,MAAM,YAAY,+
|
|
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
|
-
|
|
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 (
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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,
|
|
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,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof-of-work solver for the demo-diagnostics authenticity flow.
|
|
3
|
+
*
|
|
4
|
+
* No secret ships in the SDK. To upload a demo report, the SDK first mints a
|
|
5
|
+
* server-signed challenge token (`POST …/diagnostics/challenge`) and then finds
|
|
6
|
+
* a `solution` nonce such that `SHA256(challenge.solution)` has at least
|
|
7
|
+
* `difficulty` leading zero bits (hashcash-style). The server re-verifies the
|
|
8
|
+
* token signature (server-only key) and the PoW. See web-app diagnostic-pow.ts.
|
|
9
|
+
*/
|
|
10
|
+
export interface PowChallenge {
|
|
11
|
+
token: string;
|
|
12
|
+
challenge: string;
|
|
13
|
+
difficulty: number;
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Solve the PoW for a challenge. Returns the solution nonce as a string.
|
|
18
|
+
* Bounded by `maxIterations` so a pathological difficulty can't hang the app;
|
|
19
|
+
* returns null if unsolved within the bound.
|
|
20
|
+
*/
|
|
21
|
+
export declare const solvePow: (challenge: string, difficulty: number, maxIterations?: number) => string | null;
|
|
22
|
+
//# sourceMappingURL=diagnosticPow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnosticPow.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/diagnosticPow.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAiBD;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GACnB,WAAW,MAAM,EACjB,YAAY,MAAM,EAClB,sBAAyB,KACxB,MAAM,GAAG,IASX,CAAC"}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import { type ScanDataForReport } from './diagnosticReport';
|
|
2
|
-
/**
|
|
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.
|
|
5
|
-
*/
|
|
6
|
-
export declare const isDiagnosticSharingAvailable: () => boolean;
|
|
7
2
|
export interface DiagnosticImage {
|
|
8
3
|
/** Stable file name without extension, e.g. "mrz-side". */
|
|
9
4
|
name: string;
|
|
@@ -11,20 +6,23 @@ export interface DiagnosticImage {
|
|
|
11
6
|
mime: 'jpg' | 'png';
|
|
12
7
|
}
|
|
13
8
|
export interface SendDiagnosticParams {
|
|
9
|
+
/** Backend base URL (same value the SDK was initialised with). */
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
/** Demo session id (informational; the endpoint is unauthenticated). */
|
|
12
|
+
sessionId?: string;
|
|
14
13
|
scan: ScanDataForReport;
|
|
15
14
|
sdkVersion: string;
|
|
16
|
-
sessionId?: string;
|
|
17
15
|
/** base64 images to attach (MRZ side / front / face / NFC chip face). */
|
|
18
16
|
images?: DiagnosticImage[];
|
|
19
17
|
}
|
|
20
18
|
export interface SendResult {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
dismissed?: boolean;
|
|
19
|
+
/** True when the report was uploaded and stored. */
|
|
20
|
+
uploaded: boolean;
|
|
24
21
|
}
|
|
25
22
|
/**
|
|
26
|
-
* Build the report
|
|
27
|
-
* {
|
|
23
|
+
* Build the report and upload it (over HTTPS) to the demo-diagnostics endpoint.
|
|
24
|
+
* Returns { uploaded } — false on any error (best-effort; a failed report must
|
|
25
|
+
* never disrupt the user's flow).
|
|
28
26
|
*/
|
|
29
27
|
export declare const sendDiagnosticReport: (params: SendDiagnosticParams) => Promise<SendResult>;
|
|
30
28
|
//# 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;
|
|
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;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,CAsFpB,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.
|
|
52
|
-
'diagnosticReport.
|
|
53
|
-
'diagnosticReport.
|
|
54
|
-
'diagnosticReport.
|
|
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":"
|
|
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.
|
|
52
|
-
'diagnosticReport.
|
|
53
|
-
'diagnosticReport.
|
|
54
|
-
'diagnosticReport.
|
|
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":"
|
|
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.
|
|
1
|
+
export declare const SDK_VERSION = "1.491.0";
|
|
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.
|
|
3
|
+
"version": "1.491.0",
|
|
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
|
},
|
|
@@ -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
|
-
|
|
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 (
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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,54 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
2
|
+
import { Buffer } from 'buffer';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Proof-of-work solver for the demo-diagnostics authenticity flow.
|
|
6
|
+
*
|
|
7
|
+
* No secret ships in the SDK. To upload a demo report, the SDK first mints a
|
|
8
|
+
* server-signed challenge token (`POST …/diagnostics/challenge`) and then finds
|
|
9
|
+
* a `solution` nonce such that `SHA256(challenge.solution)` has at least
|
|
10
|
+
* `difficulty` leading zero bits (hashcash-style). The server re-verifies the
|
|
11
|
+
* token signature (server-only key) and the PoW. See web-app diagnostic-pow.ts.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface PowChallenge {
|
|
15
|
+
token: string;
|
|
16
|
+
challenge: string;
|
|
17
|
+
difficulty: number;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const leadingZeroBits = (bytes: Uint8Array): number => {
|
|
22
|
+
let bits = 0;
|
|
23
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
24
|
+
const b = bytes[i];
|
|
25
|
+
if (b === 0) {
|
|
26
|
+
bits += 8;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// bits before the highest set bit in this byte (b is 1..255)
|
|
30
|
+
bits += Math.clz32(b) - 24;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
return bits;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Solve the PoW for a challenge. Returns the solution nonce as a string.
|
|
38
|
+
* Bounded by `maxIterations` so a pathological difficulty can't hang the app;
|
|
39
|
+
* returns null if unsolved within the bound.
|
|
40
|
+
*/
|
|
41
|
+
export const solvePow = (
|
|
42
|
+
challenge: string,
|
|
43
|
+
difficulty: number,
|
|
44
|
+
maxIterations = 8_000_000
|
|
45
|
+
): string | null => {
|
|
46
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
47
|
+
const solution = i.toString();
|
|
48
|
+
const digest = sha256(Buffer.from(`${challenge}.${solution}`, 'utf-8'));
|
|
49
|
+
if (leadingZeroBits(digest) >= difficulty) {
|
|
50
|
+
return solution;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
@@ -1,58 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Assembles and
|
|
2
|
+
* Assembles and uploads the support diagnostic report to the backend.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 no credential to authenticate with. Instead, authenticity is
|
|
7
|
+
* proven with a proof-of-work: the SDK mints a server-signed challenge token
|
|
8
|
+
* (no secret ships in the SDK), solves it, and submits the solution with the
|
|
9
|
+
* report over HTTPS. The backend verifies the token (server-only key) + PoW and
|
|
10
|
+
* encrypts the PII fields at rest. Internal developers view and download the
|
|
11
|
+
* reports from a hidden dashboard page.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 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).
|
|
18
19
|
*/
|
|
19
|
-
import RNFS from 'react-native-fs';
|
|
20
20
|
import { diagnostics } from './diagnostics';
|
|
21
21
|
import { gatherDeviceDetails } from './native-device-info.utils';
|
|
22
22
|
import {
|
|
23
23
|
buildDiagnosticReport,
|
|
24
24
|
type ScanDataForReport,
|
|
25
25
|
} from './diagnosticReport';
|
|
26
|
+
import { solvePow, type PowChallenge } from './diagnosticPow';
|
|
26
27
|
import { logError } from './debug.utils';
|
|
27
28
|
|
|
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
29
|
export interface DiagnosticImage {
|
|
57
30
|
/** Stable file name without extension, e.g. "mrz-side". */
|
|
58
31
|
name: string;
|
|
@@ -61,105 +34,115 @@ export interface DiagnosticImage {
|
|
|
61
34
|
}
|
|
62
35
|
|
|
63
36
|
export interface SendDiagnosticParams {
|
|
37
|
+
/** Backend base URL (same value the SDK was initialised with). */
|
|
38
|
+
baseUrl: string;
|
|
39
|
+
/** Demo session id (informational; the endpoint is unauthenticated). */
|
|
40
|
+
sessionId?: string;
|
|
64
41
|
scan: ScanDataForReport;
|
|
65
42
|
sdkVersion: string;
|
|
66
|
-
sessionId?: string;
|
|
67
43
|
/** base64 images to attach (MRZ side / front / face / NFC chip face). */
|
|
68
44
|
images?: DiagnosticImage[];
|
|
69
45
|
}
|
|
70
46
|
|
|
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
47
|
export interface SendResult {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
dismissed?: boolean;
|
|
48
|
+
/** True when the report was uploaded and stored. */
|
|
49
|
+
uploaded: boolean;
|
|
87
50
|
}
|
|
88
51
|
|
|
52
|
+
const mimeToType = (mime: 'jpg' | 'png'): string =>
|
|
53
|
+
mime === 'png' ? 'image/png' : 'image/jpeg';
|
|
54
|
+
|
|
89
55
|
/**
|
|
90
|
-
* Build the report
|
|
91
|
-
* {
|
|
56
|
+
* Build the report and upload it (over HTTPS) to the demo-diagnostics endpoint.
|
|
57
|
+
* Returns { uploaded } — false on any error (best-effort; a failed report must
|
|
58
|
+
* never disrupt the user's flow).
|
|
92
59
|
*/
|
|
93
60
|
export const sendDiagnosticReport = async (
|
|
94
61
|
params: SendDiagnosticParams
|
|
95
62
|
): Promise<SendResult> => {
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
98
|
-
|
|
99
|
-
return { shared: false };
|
|
63
|
+
const { baseUrl, sessionId, scan, sdkVersion, images = [] } = params;
|
|
64
|
+
if (!baseUrl) {
|
|
65
|
+
return { uploaded: false };
|
|
100
66
|
}
|
|
101
|
-
|
|
67
|
+
|
|
102
68
|
const now = Date.now();
|
|
103
69
|
const iso = new Date(now).toISOString();
|
|
104
70
|
|
|
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
71
|
try {
|
|
116
|
-
await
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
72
|
+
const device = await gatherDeviceDetails();
|
|
73
|
+
const snapshot = diagnostics.snapshot(now, iso);
|
|
74
|
+
const report = buildDiagnosticReport({
|
|
75
|
+
diag: snapshot,
|
|
76
|
+
device,
|
|
77
|
+
scan,
|
|
78
|
+
sdkVersion,
|
|
79
|
+
sessionId,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Queryable, non-PII technical metadata for the dashboard list/filter.
|
|
83
|
+
const nfc = snapshot.nfc;
|
|
84
|
+
const failingSw = nfc.failureStep
|
|
85
|
+
? nfc.steps.find((s) => s.step === nfc.failureStep && !s.ok)?.sw
|
|
86
|
+
: undefined;
|
|
87
|
+
const nfcErrorCode =
|
|
88
|
+
nfc.attempted && !nfc.succeeded
|
|
89
|
+
? `NFC-${(nfc.errorCategory ?? 'reading_error').toUpperCase()}${failingSw ? `-${failingSw}` : ''}`
|
|
90
|
+
: null;
|
|
91
|
+
|
|
92
|
+
const payload = {
|
|
93
|
+
// metadata (stored in plaintext, no PII)
|
|
94
|
+
platform: device.platform,
|
|
95
|
+
osVersion: device.systemVersion,
|
|
96
|
+
sdkVersion,
|
|
97
|
+
documentType: scan.documentType ?? snapshot.documentType ?? null,
|
|
98
|
+
dataSource: scan.dataSource ?? snapshot.dataSource ?? null,
|
|
99
|
+
nfcErrorCode,
|
|
100
|
+
paceFallbackReason: nfc.paceFallbackReason ?? null,
|
|
101
|
+
succeeded: nfc.attempted ? nfc.succeeded : null,
|
|
102
|
+
// full report (PII, encrypted at rest by the backend)
|
|
103
|
+
diagnosticsJson: report.diagnosticsJson,
|
|
104
|
+
scanDataJson: report.scanDataJson,
|
|
105
|
+
images: images
|
|
106
|
+
.filter((img) => img.base64)
|
|
107
|
+
.map((img) => ({
|
|
108
|
+
name: img.name,
|
|
109
|
+
base64: img.base64,
|
|
110
|
+
mimeType: mimeToType(img.mime),
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Authenticity via proof of work: mint a server-signed challenge, solve it,
|
|
115
|
+
// and submit the solution. No secret ships in the SDK.
|
|
116
|
+
const challengeRes = await fetch(
|
|
117
|
+
`${baseUrl}/api/app/mobile/diagnostics/challenge`,
|
|
118
|
+
{ method: 'POST', headers: { 'Content-Type': 'application/json' } }
|
|
123
119
|
);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
urls.push(
|
|
127
|
-
await writeTemp(dir, `${img.name}.${img.mime}`, img.base64, 'base64')
|
|
128
|
-
);
|
|
120
|
+
if (!challengeRes.ok) {
|
|
121
|
+
return { uploaded: false };
|
|
129
122
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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;
|
|
123
|
+
const challenge = (await challengeRes.json()) as PowChallenge;
|
|
124
|
+
const solution = solvePow(challenge.challenge, challenge.difficulty);
|
|
125
|
+
if (!solution) {
|
|
126
|
+
return { uploaded: false };
|
|
151
127
|
}
|
|
128
|
+
|
|
129
|
+
const response = await fetch(`${baseUrl}/api/app/mobile/diagnostics`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'X-Trustchex-Pow-Token': challenge.token,
|
|
134
|
+
'X-Trustchex-Pow-Solution': solution,
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify(payload),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return { uploaded: response.ok };
|
|
152
140
|
} catch (e) {
|
|
153
141
|
logError(
|
|
154
142
|
'Diagnostics',
|
|
155
|
-
'Failed to
|
|
143
|
+
'Failed to upload diagnostic report',
|
|
156
144
|
e instanceof Error ? e.message : String(e)
|
|
157
145
|
);
|
|
158
|
-
return {
|
|
159
|
-
} finally {
|
|
160
|
-
// Best-effort cleanup — temp files are short-lived.
|
|
161
|
-
setTimeout(() => {
|
|
162
|
-
RNFS.unlink(dir).catch(() => {});
|
|
163
|
-
}, 60_000);
|
|
146
|
+
return { uploaded: false };
|
|
164
147
|
}
|
|
165
148
|
};
|
|
@@ -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': '
|
|
57
|
+
'diagnosticReport.noticeTitle': 'Report a scanning issue',
|
|
58
58
|
'diagnosticReport.noticeBody':
|
|
59
|
-
'This
|
|
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.
|
|
63
|
-
'diagnosticReport.
|
|
64
|
-
|
|
65
|
-
'diagnosticReport.
|
|
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': '
|
|
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ı
|
|
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.
|
|
63
|
-
'diagnosticReport.
|
|
64
|
-
|
|
65
|
-
'diagnosticReport.
|
|
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