@trustchex/react-native-sdk 1.488.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.
- package/ios/ImageDecoder/ImageDecoderModule.m +12 -0
- package/ios/ImageDecoder/ImageDecoderModule.swift +43 -0
- package/lib/module/Screens/Static/ResultScreen.js +1 -0
- package/lib/module/Shared/Components/DiagnosticReportButton.js +16 -14
- package/lib/module/Shared/Components/EIDScanner.js +5 -1
- package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +22 -2
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +1 -1
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +7 -7
- package/lib/module/Shared/EIDReader/eidReader.js +15 -3
- package/lib/module/Shared/Libs/diagnosticAuth.js +36 -0
- package/lib/module/Shared/Libs/jp2Decode.js +43 -0
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +83 -103
- 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/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +11 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticAuth.d.ts +8 -0
- package/lib/typescript/src/Shared/Libs/diagnosticAuth.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/jp2Decode.d.ts +13 -0
- package/lib/typescript/src/Shared/Libs/jp2Decode.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +26 -10
- 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/Components/EIDScanner.tsx +5 -1
- package/src/Shared/Components/IdentityDocumentCamera.flows.ts +22 -2
- package/src/Shared/Components/IdentityDocumentCamera.tsx +1 -2
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +6 -8
- package/src/Shared/EIDReader/eidReader.ts +19 -4
- package/src/Shared/Libs/diagnosticAuth.ts +47 -0
- package/src/Shared/Libs/jp2Decode.ts +48 -0
- package/src/Shared/Libs/sendDiagnosticReport.ts +91 -118
- package/src/Translation/Resources/en.ts +6 -9
- package/src/Translation/Resources/tr.ts +6 -9
- package/src/version.ts +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
@interface RCT_EXTERN_MODULE(ImageDecoderModule, NSObject)
|
|
4
|
+
|
|
5
|
+
// Decode a JPEG2000 (JP2) base64 string and re-encode it as JPEG base64.
|
|
6
|
+
// iOS's <Image> cannot render an image/jp2 data URI, but ImageIO/UIImage
|
|
7
|
+
// decodes JPEG2000 natively, so we round-trip it to JPEG here.
|
|
8
|
+
RCT_EXTERN_METHOD(decodeJp2ToJpeg:(NSString *)base64Jp2
|
|
9
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
10
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
11
|
+
|
|
12
|
+
@end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
import UIKit
|
|
4
|
+
import ImageIO
|
|
5
|
+
|
|
6
|
+
/// Decodes images that React Native's <Image> cannot render directly on iOS.
|
|
7
|
+
///
|
|
8
|
+
/// The driving case is the passport chip portrait (DG2), which is frequently
|
|
9
|
+
/// stored as JPEG2000 (image/jp2). Android's image pipeline decodes JP2 data
|
|
10
|
+
/// URIs natively, but iOS's <Image> does not — so the chip photo shows as a
|
|
11
|
+
/// placeholder on iOS. iOS's ImageIO *can* decode JPEG2000, so we round-trip
|
|
12
|
+
/// the JP2 to JPEG here and hand back base64 the JS <Image> can render.
|
|
13
|
+
@objc(ImageDecoderModule)
|
|
14
|
+
class ImageDecoderModule: NSObject {
|
|
15
|
+
|
|
16
|
+
@objc
|
|
17
|
+
static func requiresMainQueueSetup() -> Bool {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@objc
|
|
22
|
+
func decodeJp2ToJpeg(_ base64Jp2: String,
|
|
23
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
24
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
25
|
+
guard let jp2Data = Data(base64Encoded: base64Jp2) else {
|
|
26
|
+
reject("INVALID_BASE64", "Could not decode base64 JP2 input", nil)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ImageIO decodes JPEG2000 on iOS even though <Image> won't render it.
|
|
31
|
+
guard let image = UIImage(data: jp2Data) else {
|
|
32
|
+
reject("DECODE_FAILED", "ImageIO could not decode the JP2 data", nil)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
guard let jpegData = image.jpegData(compressionQuality: 0.95) else {
|
|
37
|
+
reject("ENCODE_FAILED", "Could not re-encode decoded image as JPEG", nil)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
resolve(jpegData.base64EncodedString())
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -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
|
}
|
|
@@ -418,7 +418,11 @@ const EIDScanner = ({
|
|
|
418
418
|
style: styles.idCardPhotoContainer,
|
|
419
419
|
children: /*#__PURE__*/_jsx(View, {
|
|
420
420
|
style: styles.idCardPhotoFrame,
|
|
421
|
-
children: documentFaceImage && (documentFaceImageMimeType === 'image/jpeg' || documentFaceImageMimeType === 'image/png'
|
|
421
|
+
children: documentFaceImage && (documentFaceImageMimeType === 'image/jpeg' || documentFaceImageMimeType === 'image/png' ||
|
|
422
|
+
// Android's <Image> decodes a raw JP2 data URI; on iOS
|
|
423
|
+
// the chip JP2 is converted to JPEG before this point
|
|
424
|
+
// (native ImageIO), so iOS never reaches here as jp2.
|
|
425
|
+
documentFaceImageMimeType === 'image/jp2') ? /*#__PURE__*/_jsx(Image, {
|
|
422
426
|
source: {
|
|
423
427
|
uri: `data:${documentFaceImageMimeType};base64,${documentFaceImage}`
|
|
424
428
|
},
|
|
@@ -261,12 +261,32 @@ export function handleIDBackFlow(mrzText, mrzFields, mrzValid, mrzStableAndValid
|
|
|
261
261
|
*
|
|
262
262
|
* PASSPORT: → COMPLETED (no back side)
|
|
263
263
|
* ID CARD: → SCAN_ID_BACK
|
|
264
|
+
*
|
|
265
|
+
* Routing is PASSPORT-BIASED on purpose: a passport has no back side, so if we
|
|
266
|
+
* route it to SCAN_ID_BACK it deadlocks forever waiting for a back that will
|
|
267
|
+
* never appear. We therefore only go to SCAN_ID_BACK when there is POSITIVE
|
|
268
|
+
* evidence the document is an ID card (a parsed MRZ document code that is
|
|
269
|
+
* present and not 'P'). When the type is ambiguous — e.g. the passport MRZ was
|
|
270
|
+
* never read as 'P' so the front step locked `detectedDocumentType` as
|
|
271
|
+
* 'ID_FRONT' (the torch/tilt during hologram capture makes the MRZ unreadable),
|
|
272
|
+
* or no MRZ code survived at all — we COMPLETE rather than wait for a
|
|
273
|
+
* non-existent back side. A two-sided ID card always yields an 'I'/'A'/'C'
|
|
274
|
+
* (non-'P') MRZ code, so genuine ID cards still route to SCAN_ID_BACK.
|
|
264
275
|
*/
|
|
265
276
|
export function getNextStepAfterHologram(detectedDocumentType, currentFrameDocType, mrzDocCode) {
|
|
266
|
-
//
|
|
277
|
+
// Any positive passport signal → done (no back side).
|
|
267
278
|
const isPassport = detectedDocumentType === 'PASSPORT' || currentFrameDocType === 'PASSPORT' || mrzDocCode === 'P';
|
|
268
279
|
if (isPassport) {
|
|
269
280
|
return 'COMPLETED';
|
|
270
281
|
}
|
|
271
|
-
|
|
282
|
+
|
|
283
|
+
// Only continue to the back side when we have POSITIVE proof it's an ID card:
|
|
284
|
+
// a real MRZ document code that is present and not a passport. Without that
|
|
285
|
+
// proof, default to COMPLETED so a misclassified passport can't hang waiting
|
|
286
|
+
// for a back side that doesn't exist.
|
|
287
|
+
const hasIdCardMrzCode = !!mrzDocCode && mrzDocCode !== 'P';
|
|
288
|
+
if (hasIdCardMrzCode) {
|
|
289
|
+
return 'SCAN_ID_BACK';
|
|
290
|
+
}
|
|
291
|
+
return 'COMPLETED';
|
|
272
292
|
}
|
|
@@ -1799,7 +1799,7 @@ const IdentityDocumentCamera = ({
|
|
|
1799
1799
|
status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0 && !isBrightnessLow && !isFrameBlurry && styles.topZoneTextScanning
|
|
1800
1800
|
// 5. Default (white) - aligning (not all detected OR elements outside scan area)
|
|
1801
1801
|
],
|
|
1802
|
-
children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t
|
|
1802
|
+
children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t)
|
|
1803
1803
|
})]
|
|
1804
1804
|
}), /*#__PURE__*/_jsx(View, {
|
|
1805
1805
|
style: styles.leftZone
|
|
@@ -78,17 +78,17 @@ export function transformBoundsToScreen(bounds, scale, offsetX, offsetY) {
|
|
|
78
78
|
* Unified status message logic used by both voice guidance and render text.
|
|
79
79
|
* Returns the appropriate i18n key arguments for the current scan state.
|
|
80
80
|
*/
|
|
81
|
-
export function getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t
|
|
81
|
+
export function getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t) {
|
|
82
82
|
if (nextStep === 'COMPLETED') {
|
|
83
83
|
return t('identityDocumentCamera.scanCompleted');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// MRZ
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
// NOTE: the "MRZ detected — hold steady" message was intentionally removed
|
|
87
|
+
// (along with its former `mrzReliable` parameter). User-facing guidance
|
|
88
|
+
// should only describe the SCAN STEP (front side, back side, hologram, etc.),
|
|
89
|
+
// not low-level MRZ-detection state. We fall through to the step-specific
|
|
90
|
+
// messages below.
|
|
91
|
+
|
|
92
92
|
if (status === 'INCORRECT') {
|
|
93
93
|
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
94
94
|
return t('identityDocumentCamera.alignIDFront');
|
|
@@ -12,6 +12,7 @@ import { Buffer } from 'buffer';
|
|
|
12
12
|
import { InputStream } from "./java/inputStream.js";
|
|
13
13
|
import { debugLog } from "../Libs/debug.utils.js";
|
|
14
14
|
import { diagnostics } from "../Libs/diagnostics.js";
|
|
15
|
+
import { decodeJp2ToJpeg } from "../Libs/jp2Decode.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Extract an APDU status word (hex, e.g. "6982") from an error message if the
|
|
@@ -220,7 +221,7 @@ function pixelsToPng(pixels, width, height, channels) {
|
|
|
220
221
|
// conversion" hang. A JS timeout cannot interrupt a synchronous call, so we must
|
|
221
222
|
// gate BEFORE decoding. ~12 KB decodes fast; beyond that we keep the raw JP2.
|
|
222
223
|
const MAX_JP2_DECODE_BYTES = 12 * 1024;
|
|
223
|
-
function convertJP2IfNeeded(imageBuffer, mimeType) {
|
|
224
|
+
async function convertJP2IfNeeded(imageBuffer, mimeType) {
|
|
224
225
|
if (mimeType !== 'image/jp2') {
|
|
225
226
|
return {
|
|
226
227
|
base64: imageBuffer.toString('base64'),
|
|
@@ -228,10 +229,21 @@ function convertJP2IfNeeded(imageBuffer, mimeType) {
|
|
|
228
229
|
};
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
// Prefer the native decoder (iOS ImageIO). It decodes JPEG2000 of any size
|
|
233
|
+
// without blocking the JS thread, so it both fixes the iOS preview (RN's
|
|
234
|
+
// <Image> can't render a raw image/jp2 data URI) and avoids the slow pure-JS
|
|
235
|
+
// decoder entirely. Returns null on Android / when unavailable → fall through.
|
|
236
|
+
const native = await decodeJp2ToJpeg(imageBuffer);
|
|
237
|
+
if (native) {
|
|
238
|
+
debugLog('EID', `[EID] JP2 decoded natively → ${native.mimeType} (${native.base64.length} base64 chars)`);
|
|
239
|
+
return native;
|
|
240
|
+
}
|
|
241
|
+
|
|
231
242
|
// Too large to decode synchronously without freezing the UI — keep the raw
|
|
232
243
|
// JP2. The face image is still captured (the verification backend / face-match
|
|
233
244
|
// accept JPEG2000); only the on-screen preview can't render it. This lets the
|
|
234
|
-
// NFC read COMPLETE instead of hanging.
|
|
245
|
+
// NFC read COMPLETE instead of hanging. (On Android the raw JP2 still renders
|
|
246
|
+
// in <Image>; on iOS the native path above normally handles it.)
|
|
235
247
|
if (imageBuffer.length > MAX_JP2_DECODE_BYTES) {
|
|
236
248
|
debugLog('EID', `[EID] JP2 face image is ${imageBuffer.length} bytes (> ${MAX_JP2_DECODE_BYTES}); skipping in-JS decode to avoid blocking, keeping raw JP2`);
|
|
237
249
|
return {
|
|
@@ -642,7 +654,7 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
642
654
|
const buffer = Buffer.alloc(imageLength);
|
|
643
655
|
await imageInputStream.readBytesWithOffset(buffer, 0, imageLength);
|
|
644
656
|
const rawMimeType = faceImageInfo.getMimeType();
|
|
645
|
-
const converted = convertJP2IfNeeded(buffer, rawMimeType);
|
|
657
|
+
const converted = await convertJP2IfNeeded(buffer, rawMimeType);
|
|
646
658
|
imageAsBase64 = converted.base64;
|
|
647
659
|
mimeType = converted.mimeType;
|
|
648
660
|
}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { NativeModules, Platform } from 'react-native';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Native JPEG2000 decoder bridge.
|
|
7
|
+
*
|
|
8
|
+
* The passport chip portrait (DG2) is frequently stored as JPEG2000
|
|
9
|
+
* (image/jp2). React Native's <Image> renders a JP2 data URI on Android (its
|
|
10
|
+
* Fresco/Skia pipeline decodes it) but NOT on iOS (UIImage-backed <Image>
|
|
11
|
+
* refuses the data URI), so the chip photo showed as a placeholder on iOS.
|
|
12
|
+
*
|
|
13
|
+
* iOS's ImageIO *can* decode JPEG2000, so the native `ImageDecoderModule`
|
|
14
|
+
* round-trips the JP2 to JPEG. We only need this on iOS; on Android the raw
|
|
15
|
+
* JP2 already renders, so there is no native counterpart and we no-op.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const ImageDecoder = NativeModules.ImageDecoderModule;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decode a JPEG2000 buffer to JPEG using the native iOS decoder.
|
|
22
|
+
*
|
|
23
|
+
* Returns the JPEG bytes + mime type on success, or null when native decode is
|
|
24
|
+
* unavailable (Android, or the module isn't linked) or fails — callers should
|
|
25
|
+
* fall back to their existing behaviour (keep raw JP2).
|
|
26
|
+
*/
|
|
27
|
+
export async function decodeJp2ToJpeg(jp2Buffer) {
|
|
28
|
+
if (Platform.OS !== 'ios' || !ImageDecoder?.decodeJp2ToJpeg) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const jpegBase64 = await ImageDecoder.decodeJp2ToJpeg(jp2Buffer.toString('base64'));
|
|
33
|
+
if (!jpegBase64) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
base64: jpegBase64,
|
|
38
|
+
mimeType: 'image/jpeg'
|
|
39
|
+
};
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,130 +1,110 @@
|
|
|
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 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
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
-
*
|
|
47
|
-
*
|
|
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
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
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
|
|
105
|
+
logError('Diagnostics', 'Failed to upload diagnostic report', e instanceof Error ? e.message : String(e));
|
|
121
106
|
return {
|
|
122
|
-
|
|
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': '
|
|
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"}
|