@trustchex/react-native-sdk 1.478.7 → 1.483.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +57 -3
- package/ios/Camera/TrustchexCameraView.swift +20 -15
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +0 -3
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +21 -0
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +1 -1
- package/lib/module/Screens/Static/ResultScreen.js +75 -16
- package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
- package/lib/module/Shared/Components/EIDScanner.js +52 -19
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +132 -8
- package/lib/module/Shared/Components/NavigationManager.js +7 -9
- package/lib/module/Shared/Contexts/AppContext.js +3 -0
- package/lib/module/Shared/EIDReader/eidReader.js +167 -30
- package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +77 -0
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +7 -0
- package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
- package/lib/module/Shared/Libs/diagnostics.js +171 -0
- package/lib/module/Shared/Libs/documentDataNormalizer.js +104 -0
- package/lib/module/Shared/Libs/mrz.utils.js +0 -5
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +32 -5
- package/lib/module/Shared/Libs/mrzTransliteration.js +80 -0
- package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
- package/lib/module/Shared/Services/DataUploadService.js +6 -3
- package/lib/module/Shared/Types/documentData.js +1 -0
- package/lib/module/Shared/Types/documentReadResult.js +3 -0
- package/lib/module/Translation/Resources/en.js +5 -0
- package/lib/module/Translation/Resources/tr.js +5 -0
- package/lib/module/Trustchex.js +31 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/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.types.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +5 -0
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +2 -0
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +35 -0
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts +12 -0
- package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +33 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +3 -2
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/documentData.d.ts +44 -0
- package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +44 -0
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/mrzFields.d.ts +1 -1
- package/lib/typescript/src/Shared/Types/mrzFields.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +5 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +5 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts +9 -0
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +5 -1
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +0 -4
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +23 -0
- package/src/Screens/Dynamic/VideoCallScreen.tsx +5 -1
- package/src/Screens/Static/ResultScreen.tsx +85 -16
- package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
- package/src/Shared/Components/EIDScanner.tsx +67 -18
- package/src/Shared/Components/IdentityDocumentCamera.tsx +145 -7
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
- package/src/Shared/Components/NavigationManager.tsx +7 -7
- package/src/Shared/Components/TrustchexCamera.tsx +6 -0
- package/src/Shared/Contexts/AppContext.ts +8 -0
- package/src/Shared/EIDReader/eidReader.ts +193 -47
- package/src/Shared/EIDReader/lds/icao/dg11File.ts +94 -0
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +7 -0
- package/src/Shared/Libs/diagnosticReport.ts +206 -0
- package/src/Shared/Libs/diagnostics.ts +251 -0
- package/src/Shared/Libs/documentDataNormalizer.ts +108 -0
- package/src/Shared/Libs/mrz.utils.ts +0 -5
- package/src/Shared/Libs/mrzFrameAggregator.ts +61 -3
- package/src/Shared/Libs/mrzTransliteration.ts +131 -0
- package/src/Shared/Libs/native-device-info.utils.ts +116 -0
- package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
- package/src/Shared/Services/DataUploadService.ts +9 -3
- package/src/Shared/Types/documentData.ts +52 -0
- package/src/Shared/Types/documentReadResult.ts +47 -0
- package/src/Shared/Types/mrzFields.ts +0 -5
- package/src/Translation/Resources/en.ts +6 -0
- package/src/Translation/Resources/tr.ts +6 -0
- package/src/Trustchex.tsx +48 -3
- package/src/index.tsx +7 -0
- package/src/version.ts +1 -1
|
@@ -1,6 +1,122 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
import DeviceInfo from 'react-native-device-info';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Full, non-PII device details for the support diagnostic report. Device-
|
|
6
|
+
* specific scanning/NFC issues correlate with hardware, OS level, memory and
|
|
7
|
+
* NFC capability, so we capture a broad-but-safe set. Excludes anything that
|
|
8
|
+
* identifies a person (uniqueId is a per-install vendor id, not PII, and is kept
|
|
9
|
+
* only for correlating repeat reports from the same device).
|
|
10
|
+
*/
|
|
11
|
+
export interface DeviceDetails {
|
|
12
|
+
platform: string; // 'ios' | 'android'
|
|
13
|
+
systemName: string; // 'iOS' | 'Android'
|
|
14
|
+
systemVersion: string;
|
|
15
|
+
apiLevel?: number; // Android API level
|
|
16
|
+
brand: string;
|
|
17
|
+
manufacturer: string;
|
|
18
|
+
model: string;
|
|
19
|
+
deviceId: string; // hardware model code, e.g. "iPhone16,1" / "tegu"
|
|
20
|
+
deviceType: string; // Handset / Tablet / …
|
|
21
|
+
isTablet: boolean;
|
|
22
|
+
isEmulator: boolean;
|
|
23
|
+
hasNotch: boolean;
|
|
24
|
+
totalMemoryMB?: number;
|
|
25
|
+
usedMemoryMB?: number;
|
|
26
|
+
totalDiskMB?: number;
|
|
27
|
+
freeDiskMB?: number;
|
|
28
|
+
batteryLevel?: number; // 0..1
|
|
29
|
+
appVersion: string;
|
|
30
|
+
buildNumber: string;
|
|
31
|
+
bundleId: string;
|
|
32
|
+
fontScale?: number;
|
|
33
|
+
installerPackage?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Resolve an async getter to a number, tolerating failure. */
|
|
37
|
+
const num = async (p: Promise<number>): Promise<number | undefined> => {
|
|
38
|
+
try {
|
|
39
|
+
const v = await p;
|
|
40
|
+
return typeof v === 'number' && isFinite(v) ? v : undefined;
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Resolve an async getter, falling back on failure. */
|
|
47
|
+
const safe = async <T>(p: Promise<T>, fallback: T): Promise<T> => {
|
|
48
|
+
try {
|
|
49
|
+
return await p;
|
|
50
|
+
} catch {
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Read a synchronous getter, tolerating failure. */
|
|
56
|
+
const sync = <T>(fn: () => T, fallback: T): T => {
|
|
57
|
+
try {
|
|
58
|
+
return fn();
|
|
59
|
+
} catch {
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Gather the full non-PII device profile for a diagnostic report. */
|
|
65
|
+
export const gatherDeviceDetails = async (): Promise<DeviceDetails> => {
|
|
66
|
+
const bytesToMB = (b?: number) =>
|
|
67
|
+
b === undefined ? undefined : Math.round(b / (1024 * 1024));
|
|
68
|
+
const isAndroid = Platform.OS === 'android';
|
|
69
|
+
|
|
70
|
+
const [
|
|
71
|
+
manufacturer,
|
|
72
|
+
isEmulator,
|
|
73
|
+
apiLevel,
|
|
74
|
+
installerPackage,
|
|
75
|
+
totalMemory,
|
|
76
|
+
usedMemory,
|
|
77
|
+
totalDisk,
|
|
78
|
+
freeDisk,
|
|
79
|
+
batteryLevel,
|
|
80
|
+
fontScale,
|
|
81
|
+
] = await Promise.all([
|
|
82
|
+
safe(DeviceInfo.getManufacturer(), 'unknown'),
|
|
83
|
+
safe(DeviceInfo.isEmulator(), false),
|
|
84
|
+
isAndroid ? num(DeviceInfo.getApiLevel()) : Promise.resolve(undefined),
|
|
85
|
+
safe(DeviceInfo.getInstallerPackageName(), 'unknown'),
|
|
86
|
+
num(DeviceInfo.getTotalMemory()),
|
|
87
|
+
num(DeviceInfo.getUsedMemory()),
|
|
88
|
+
num(DeviceInfo.getTotalDiskCapacity()),
|
|
89
|
+
num(DeviceInfo.getFreeDiskStorage()),
|
|
90
|
+
num(DeviceInfo.getBatteryLevel()),
|
|
91
|
+
num(DeviceInfo.getFontScale()),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
platform: Platform.OS,
|
|
96
|
+
systemName: isAndroid ? 'Android' : 'iOS',
|
|
97
|
+
systemVersion: sync(() => DeviceInfo.getSystemVersion(), 'unknown'),
|
|
98
|
+
apiLevel,
|
|
99
|
+
brand: sync(() => DeviceInfo.getBrand(), 'unknown'),
|
|
100
|
+
manufacturer,
|
|
101
|
+
model: sync(() => DeviceInfo.getModel(), 'unknown'),
|
|
102
|
+
deviceId: sync(() => DeviceInfo.getDeviceId(), 'unknown'),
|
|
103
|
+
deviceType: sync(() => DeviceInfo.getDeviceType(), 'unknown'),
|
|
104
|
+
isTablet: sync(() => DeviceInfo.isTablet(), false),
|
|
105
|
+
isEmulator,
|
|
106
|
+
hasNotch: sync(() => DeviceInfo.hasNotch(), false),
|
|
107
|
+
totalMemoryMB: bytesToMB(totalMemory),
|
|
108
|
+
usedMemoryMB: bytesToMB(usedMemory),
|
|
109
|
+
totalDiskMB: bytesToMB(totalDisk),
|
|
110
|
+
freeDiskMB: bytesToMB(freeDisk),
|
|
111
|
+
batteryLevel,
|
|
112
|
+
appVersion: sync(() => DeviceInfo.getVersion(), 'unknown'),
|
|
113
|
+
buildNumber: sync(() => DeviceInfo.getBuildNumber(), 'unknown'),
|
|
114
|
+
bundleId: sync(() => DeviceInfo.getBundleId(), 'unknown'),
|
|
115
|
+
fontScale,
|
|
116
|
+
installerPackage,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
4
120
|
export const NativeDeviceInfo = {
|
|
5
121
|
generateHumanReadableIdentifier: async (): Promise<string> => {
|
|
6
122
|
try {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assembles and shares the support diagnostic report.
|
|
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.
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
import RNFS from 'react-native-fs';
|
|
20
|
+
import { diagnostics } from './diagnostics';
|
|
21
|
+
import { gatherDeviceDetails } from './native-device-info.utils';
|
|
22
|
+
import {
|
|
23
|
+
buildDiagnosticReport,
|
|
24
|
+
type ScanDataForReport,
|
|
25
|
+
} from './diagnosticReport';
|
|
26
|
+
import { logError } from './debug.utils';
|
|
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
|
+
export interface DiagnosticImage {
|
|
57
|
+
/** Stable file name without extension, e.g. "mrz-side". */
|
|
58
|
+
name: string;
|
|
59
|
+
base64: string;
|
|
60
|
+
mime: 'jpg' | 'png';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SendDiagnosticParams {
|
|
64
|
+
scan: ScanDataForReport;
|
|
65
|
+
sdkVersion: string;
|
|
66
|
+
sessionId?: string;
|
|
67
|
+
/** base64 images to attach (MRZ side / front / face / NFC chip face). */
|
|
68
|
+
images?: DiagnosticImage[];
|
|
69
|
+
}
|
|
70
|
+
|
|
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
|
+
export interface SendResult {
|
|
84
|
+
shared: boolean;
|
|
85
|
+
/** True when the user dismissed the share sheet (not an error). */
|
|
86
|
+
dismissed?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build the report, stage attachments, and present the share sheet. Returns
|
|
91
|
+
* { shared } — false on error, with `dismissed` set when the user cancelled.
|
|
92
|
+
*/
|
|
93
|
+
export const sendDiagnosticReport = async (
|
|
94
|
+
params: SendDiagnosticParams
|
|
95
|
+
): Promise<SendResult> => {
|
|
96
|
+
const share = loadShare();
|
|
97
|
+
if (!share) {
|
|
98
|
+
// Optional dependency not installed — nothing to do.
|
|
99
|
+
return { shared: false };
|
|
100
|
+
}
|
|
101
|
+
const { scan, sdkVersion, sessionId, images = [] } = params;
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const iso = new Date(now).toISOString();
|
|
104
|
+
|
|
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
|
+
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
|
+
}
|
|
130
|
+
|
|
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
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
logError(
|
|
154
|
+
'Diagnostics',
|
|
155
|
+
'Failed to share diagnostic report',
|
|
156
|
+
e instanceof Error ? e.message : String(e)
|
|
157
|
+
);
|
|
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);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ScannedIdentityDocument,
|
|
6
6
|
LivenessDetection,
|
|
7
7
|
} from '../Types/identificationInfo';
|
|
8
|
+
import type { DocumentName } from '../Types/documentReadResult';
|
|
8
9
|
import { getSessionKey, encryptWithAes } from '../Libs/crypto.utils';
|
|
9
10
|
import mrzUtils from '../Libs/mrz.utils';
|
|
10
11
|
import httpClient, { getSessionToken } from '../Libs/http-client';
|
|
@@ -46,7 +47,8 @@ export class DataUploadService {
|
|
|
46
47
|
async submitDocumentData(
|
|
47
48
|
identificationId: string,
|
|
48
49
|
scannedDocument: ScannedIdentityDocument,
|
|
49
|
-
sessionKey: string
|
|
50
|
+
sessionKey: string,
|
|
51
|
+
documentName?: DocumentName
|
|
50
52
|
): Promise<void> {
|
|
51
53
|
if (!scannedDocument || scannedDocument.documentType === 'UNKNOWN') {
|
|
52
54
|
debugLog('DataUploadService', 'No document data to submit');
|
|
@@ -63,6 +65,9 @@ export class DataUploadService {
|
|
|
63
65
|
type: mrzFields.documentCode,
|
|
64
66
|
name: mrzFields.firstName,
|
|
65
67
|
surname: mrzFields.lastName,
|
|
68
|
+
displayName: documentName?.source === 'dg11' ? documentName.displayFirst : undefined,
|
|
69
|
+
displaySurname: documentName?.source === 'dg11' ? documentName.displayLast : undefined,
|
|
70
|
+
nameSource: documentName?.source === 'dg11' ? documentName.source : undefined,
|
|
66
71
|
gender: this.getGenderEnumType(mrzFields.sex),
|
|
67
72
|
number: mrzFields.documentNumber,
|
|
68
73
|
country: mrzFields.issuingState,
|
|
@@ -270,7 +275,8 @@ export class DataUploadService {
|
|
|
270
275
|
*/
|
|
271
276
|
async uploadCollectedData(
|
|
272
277
|
identificationInfo: IdentificationInfo,
|
|
273
|
-
onProgress?: (progress: number) => void
|
|
278
|
+
onProgress?: (progress: number) => void,
|
|
279
|
+
documentName?: DocumentName
|
|
274
280
|
): Promise<boolean> {
|
|
275
281
|
const { identificationId, sessionId, scannedDocument, livenessDetection } =
|
|
276
282
|
identificationInfo;
|
|
@@ -329,7 +335,7 @@ export class DataUploadService {
|
|
|
329
335
|
if (scannedDocument) {
|
|
330
336
|
onProgress?.(0.1);
|
|
331
337
|
await runWithRetry(() =>
|
|
332
|
-
this.submitDocumentData(identificationId, scannedDocument, authToken)
|
|
338
|
+
this.submitDocumentData(identificationId, scannedDocument, authToken, documentName)
|
|
333
339
|
);
|
|
334
340
|
onProgress?.(0.3);
|
|
335
341
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified, source-independent document data produced after a scan — whether
|
|
3
|
+
* the data came from an NFC chip read or camera OCR. All fields are normalized
|
|
4
|
+
* to consistent formats regardless of which path produced them.
|
|
5
|
+
*/
|
|
6
|
+
export interface DocumentData {
|
|
7
|
+
/**
|
|
8
|
+
* Document type.
|
|
9
|
+
* - `"P"` — passport (TD3)
|
|
10
|
+
* - `"I"` — ID card (TD1 / TD2)
|
|
11
|
+
*/
|
|
12
|
+
documentType: 'P' | 'I' | string;
|
|
13
|
+
|
|
14
|
+
/** 3-letter ICAO issuing country code, e.g. `"TUR"`, `"DEU"`. */
|
|
15
|
+
issuingCountry: string;
|
|
16
|
+
|
|
17
|
+
/** 3-letter ICAO nationality code, e.g. `"TUR"`. */
|
|
18
|
+
nationality: string;
|
|
19
|
+
|
|
20
|
+
/** Document serial / number exactly as encoded on the document. */
|
|
21
|
+
documentNumber: string;
|
|
22
|
+
|
|
23
|
+
/** National identity number where applicable (e.g. Turkish TC kimlik no). Null if not present. */
|
|
24
|
+
personalNumber: string | null;
|
|
25
|
+
|
|
26
|
+
/** Last name — raw MRZ ASCII (OCR-B), e.g. `"OEZTUERK"`. */
|
|
27
|
+
lastName: string;
|
|
28
|
+
|
|
29
|
+
/** First name(s) — raw MRZ ASCII (OCR-B), e.g. `"GOEKHAN MEHMET"`. */
|
|
30
|
+
firstName: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sex / gender.
|
|
34
|
+
* - `"M"` — male
|
|
35
|
+
* - `"F"` — female
|
|
36
|
+
* - `"X"` — unspecified / non-binary
|
|
37
|
+
* - `"U"` — unknown (not encoded on document)
|
|
38
|
+
*/
|
|
39
|
+
sex: 'M' | 'F' | 'X' | 'U';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Date of birth in `YYYY-MM-DD` format, e.g. `"1985-01-15"`.
|
|
43
|
+
* Null if the document does not encode it.
|
|
44
|
+
*/
|
|
45
|
+
dateOfBirth: string | null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Document expiry date in `YYYY-MM-DD` format, e.g. `"2030-12-31"`.
|
|
49
|
+
* Null if the document does not encode it.
|
|
50
|
+
*/
|
|
51
|
+
dateOfExpiry: string | null;
|
|
52
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DocumentData } from './documentData';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How the display name was derived from the raw MRZ ASCII name.
|
|
5
|
+
*
|
|
6
|
+
* - `dg11` — read from the chip's DG11 supplemental data file (exact printed name)
|
|
7
|
+
* - `reverse_table` — reconstructed via country-aware ICAO 9303 reverse transliteration table
|
|
8
|
+
* - `raw` — no conversion applied; display equals the raw MRZ value
|
|
9
|
+
*/
|
|
10
|
+
export type DocumentNameSource = 'dg11' | 'reverse_table' | 'raw';
|
|
11
|
+
|
|
12
|
+
export interface DocumentName {
|
|
13
|
+
/** First name exactly as stored in the MRZ (ASCII, OCR-B, e.g. "GOEKHAN"). */
|
|
14
|
+
rawFirst: string;
|
|
15
|
+
/** Last name exactly as stored in the MRZ (ASCII, OCR-B, e.g. "OEZTUERK"). */
|
|
16
|
+
rawLast: string;
|
|
17
|
+
/** First name in best available Unicode form (e.g. "GÖKHAN"). */
|
|
18
|
+
displayFirst: string;
|
|
19
|
+
/** Last name in best available Unicode form (e.g. "ÖZTÜRK"). */
|
|
20
|
+
displayLast: string;
|
|
21
|
+
/** How displayFirst / displayLast were derived. */
|
|
22
|
+
source: DocumentNameSource;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DocumentFaceImage {
|
|
26
|
+
/** Base64-encoded image data (no data-URI prefix). */
|
|
27
|
+
data: string;
|
|
28
|
+
/** MIME type, e.g. "image/jpeg" or "image/png". */
|
|
29
|
+
mimeType: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DocumentReadResult {
|
|
33
|
+
/** Normalized document data — consistent shape regardless of NFC or OCR source. */
|
|
34
|
+
document: DocumentData;
|
|
35
|
+
/**
|
|
36
|
+
* Name in both raw MRZ ASCII form and best-available Unicode display form.
|
|
37
|
+
* rawFirst / rawLast mirror document.firstName / document.lastName.
|
|
38
|
+
* displayFirst / displayLast are the Unicode-converted equivalents.
|
|
39
|
+
*/
|
|
40
|
+
name: DocumentName;
|
|
41
|
+
/**
|
|
42
|
+
* Face image — from the chip's DG2 file on the NFC path, or extracted from
|
|
43
|
+
* the document photo on the camera OCR path. Optional: only present when a
|
|
44
|
+
* face image was successfully obtained.
|
|
45
|
+
*/
|
|
46
|
+
face?: DocumentFaceImage;
|
|
47
|
+
}
|
|
@@ -7,18 +7,13 @@ export type MRZFieldName =
|
|
|
7
7
|
| 'documentCode'
|
|
8
8
|
| 'issuingState'
|
|
9
9
|
| 'documentNumber'
|
|
10
|
-
| 'documentNumberCheckDigit'
|
|
11
10
|
| 'nationality'
|
|
12
11
|
| 'lastName'
|
|
13
12
|
| 'firstName'
|
|
14
13
|
| 'sex'
|
|
15
14
|
| 'birthDate'
|
|
16
|
-
| 'birthDateCheckDigit'
|
|
17
15
|
| 'expirationDate'
|
|
18
|
-
| 'expirationDateCheckDigit'
|
|
19
16
|
| 'personalNumber'
|
|
20
|
-
| 'personalNumberCheckDigit'
|
|
21
|
-
| 'compositeCheckDigit'
|
|
22
17
|
| 'optional1'
|
|
23
18
|
| 'optional2';
|
|
24
19
|
|
|
@@ -53,6 +53,12 @@ export default {
|
|
|
53
53
|
'resultScreen.demoVerbalConsentTitle': 'Title',
|
|
54
54
|
'resultScreen.demoVerbalConsentText': 'Text',
|
|
55
55
|
'resultScreen.demoStartOver': 'Start Over',
|
|
56
|
+
'diagnosticReport.button': 'Report a scanning issue',
|
|
57
|
+
'diagnosticReport.noticeTitle': 'Share diagnostic report',
|
|
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.',
|
|
60
|
+
'diagnosticReport.continue': 'Continue',
|
|
61
|
+
'diagnosticReport.cancel': 'Cancel',
|
|
56
62
|
'livenessDetectionScreen.guideHeader': 'Face Verification',
|
|
57
63
|
'livenessDetectionScreen.guideText':
|
|
58
64
|
'Please ensure the following for best results:',
|
|
@@ -53,6 +53,12 @@ export default {
|
|
|
53
53
|
'resultScreen.demoVerbalConsentTitle': 'Başlık',
|
|
54
54
|
'resultScreen.demoVerbalConsentText': 'Metin',
|
|
55
55
|
'resultScreen.demoStartOver': 'Baştan Başla',
|
|
56
|
+
'diagnosticReport.button': 'Tarama sorununu bildir',
|
|
57
|
+
'diagnosticReport.noticeTitle': 'Tanılama raporu paylaş',
|
|
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.',
|
|
60
|
+
'diagnosticReport.continue': 'Devam et',
|
|
61
|
+
'diagnosticReport.cancel': 'İptal',
|
|
56
62
|
'livenessDetectionScreen.guideHeader': 'Yüz Doğrulaması',
|
|
57
63
|
'livenessDetectionScreen.guideText':
|
|
58
64
|
'En iyi sonuç için lütfen şunları sağlayın:',
|
package/src/Trustchex.tsx
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect,
|
|
3
|
+
useState,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import { NavigationContainer } from '@react-navigation/native';
|
|
3
9
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
4
10
|
import { View, ActivityIndicator, StyleSheet } from 'react-native';
|
|
@@ -22,6 +28,7 @@ import MRZTestScreen from './Screens/Debug/MRZTestScreen';
|
|
|
22
28
|
import BarcodeTestScreen from './Screens/Debug/BarcodeTestScreen';
|
|
23
29
|
import NFCScanTestScreen from './Screens/Debug/NFCScanTestScreen';
|
|
24
30
|
import AppContext, { type AppContextType } from './Shared/Contexts/AppContext';
|
|
31
|
+
import type { DocumentReadResult } from './Shared/Types/documentReadResult';
|
|
25
32
|
import DebugNavigationPanel from './Shared/Components/DebugNavigationPanel';
|
|
26
33
|
import i18n from './Translation';
|
|
27
34
|
import { initializeTTS } from './Shared/Libs/tts.utils';
|
|
@@ -49,6 +56,14 @@ interface TrustchexProps {
|
|
|
49
56
|
locale?: 'en' | 'tr';
|
|
50
57
|
onCompleted?: () => void;
|
|
51
58
|
onError?: (error: string) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Called as soon as document data is available — immediately after a
|
|
61
|
+
* successful NFC chip read (before the user taps "Approve and Continue")
|
|
62
|
+
* or after a successful camera OCR scan. Receives MRZ fields (raw,
|
|
63
|
+
* untouched ASCII), a name object with both raw and best-available Unicode
|
|
64
|
+
* display form, and optionally the face image.
|
|
65
|
+
*/
|
|
66
|
+
onDocumentRead?: (result: DocumentReadResult) => void;
|
|
52
67
|
enableAnalytics?: boolean;
|
|
53
68
|
debug?: boolean;
|
|
54
69
|
logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent';
|
|
@@ -68,6 +83,7 @@ const Trustchex: React.FC<TrustchexProps> = ({
|
|
|
68
83
|
locale: propLocale,
|
|
69
84
|
onCompleted,
|
|
70
85
|
onError,
|
|
86
|
+
onDocumentRead,
|
|
71
87
|
enableAnalytics = true,
|
|
72
88
|
debug = false,
|
|
73
89
|
logLevel = 'trace',
|
|
@@ -80,6 +96,7 @@ const Trustchex: React.FC<TrustchexProps> = ({
|
|
|
80
96
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
81
97
|
const [analyticsInitialized, setAnalyticsInitialized] = useState(false);
|
|
82
98
|
const [isDemoSession, setIsDemoSession] = useState(false);
|
|
99
|
+
const [lastDocumentRead, setLastDocumentReadState] = useState<DocumentReadResult | undefined>(undefined);
|
|
83
100
|
|
|
84
101
|
const branding = useMemo(
|
|
85
102
|
() => ({
|
|
@@ -96,11 +113,34 @@ const Trustchex: React.FC<TrustchexProps> = ({
|
|
|
96
113
|
// discarding those mutations and causing screens to see undefined values.
|
|
97
114
|
const contextRef = useRef<AppContextType | null>(null);
|
|
98
115
|
|
|
116
|
+
// Wrap the demo-session setter so it ALSO mutates the persistent context
|
|
117
|
+
// object synchronously. The React state update is async, but the verification
|
|
118
|
+
// flow navigates to the next step in the same tick and advances thereafter by
|
|
119
|
+
// direct mutation (never re-rendering Trustchex). If isDemoSession lived only
|
|
120
|
+
// as a state-derived memo field, downstream screens (e.g. ResultScreen, which
|
|
121
|
+
// gates the diagnostics button on it) could read a stale `false`. Mutating the
|
|
122
|
+
// ref makes the flag travel with the object every screen holds.
|
|
123
|
+
const setIsDemoSessionAndPersist = useCallback((value: boolean) => {
|
|
124
|
+
if (contextRef.current) {
|
|
125
|
+
contextRef.current.isDemoSession = value;
|
|
126
|
+
}
|
|
127
|
+
setIsDemoSession(value);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
const setLastDocumentRead = useCallback((result: DocumentReadResult) => {
|
|
131
|
+
if (contextRef.current) {
|
|
132
|
+
contextRef.current.lastDocumentRead = result;
|
|
133
|
+
}
|
|
134
|
+
setLastDocumentReadState(result);
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
99
137
|
const contextValue = useMemo(() => {
|
|
100
138
|
const prev = contextRef.current;
|
|
101
139
|
|
|
102
140
|
const value: AppContextType = {
|
|
103
|
-
|
|
141
|
+
// Prefer a direct mutation carried on the persistent object over the
|
|
142
|
+
// (possibly not-yet-flushed) state, so the demo flag can't desync.
|
|
143
|
+
isDemoSession: prev?.isDemoSession || isDemoSession,
|
|
104
144
|
baseUrl: baseUrl || '',
|
|
105
145
|
locale,
|
|
106
146
|
branding,
|
|
@@ -120,11 +160,14 @@ const Trustchex: React.FC<TrustchexProps> = ({
|
|
|
120
160
|
currentWorkflowStep: prev?.currentWorkflowStep,
|
|
121
161
|
isDebugNavigated: prev?.isDebugNavigated,
|
|
122
162
|
isTestVideoSession: prev?.isTestVideoSession,
|
|
163
|
+
lastDocumentRead,
|
|
123
164
|
onCompleted,
|
|
124
165
|
onError,
|
|
166
|
+
onDocumentRead,
|
|
167
|
+
setLastDocumentRead,
|
|
125
168
|
setSessionId,
|
|
126
169
|
setBaseUrl,
|
|
127
|
-
setIsDemoSession,
|
|
170
|
+
setIsDemoSession: setIsDemoSessionAndPersist,
|
|
128
171
|
};
|
|
129
172
|
|
|
130
173
|
// Update state-derived identificationInfo fields without replacing
|
|
@@ -146,6 +189,8 @@ const Trustchex: React.FC<TrustchexProps> = ({
|
|
|
146
189
|
onCompleted,
|
|
147
190
|
onError,
|
|
148
191
|
isDemoSession,
|
|
192
|
+
lastDocumentRead,
|
|
193
|
+
setIsDemoSessionAndPersist,
|
|
149
194
|
]);
|
|
150
195
|
|
|
151
196
|
// Initialize analytics when SDK mounts with valid session
|
package/src/index.tsx
CHANGED
|
@@ -51,5 +51,12 @@ export type {
|
|
|
51
51
|
export { default as mrzUtils } from './Shared/Libs/mrz.utils';
|
|
52
52
|
export type { MRZFormat, MRZValidationResult } from './Shared/Libs/mrz.utils';
|
|
53
53
|
export type { MRZFields, MRZFieldName } from './Shared/Types/mrzFields';
|
|
54
|
+
export type { DocumentData } from './Shared/Types/documentData';
|
|
55
|
+
export type {
|
|
56
|
+
DocumentReadResult,
|
|
57
|
+
DocumentName,
|
|
58
|
+
DocumentNameSource,
|
|
59
|
+
DocumentFaceImage,
|
|
60
|
+
} from './Shared/Types/documentReadResult';
|
|
54
61
|
|
|
55
62
|
export default Trustchex;
|
package/src/version.ts
CHANGED