@trustchex/react-native-sdk 1.512.0 → 1.513.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/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +9 -2
- package/android/src/main/java/com/trustchex/reactnativesdk/opencv/OpenCVModule.kt +54 -0
- package/ios/Camera/TrustchexCameraView.swift +6 -2
- package/ios/OpenCV/OpenCVModule.mm +59 -0
- package/lib/module/Screens/Dynamic/AddressCaptureScreen.js +427 -112
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +9 -4
- package/lib/module/Screens/Static/ResultScreen.js +46 -0
- package/lib/module/Shared/Animations/address-scan.json +1 -0
- package/lib/module/Shared/Components/DebugNavigationPanel.js +10 -0
- package/lib/module/Shared/Components/EIDScanner.js +16 -4
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +100 -19
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +47 -0
- package/lib/module/Shared/Libs/addressProofHeuristic.js +130 -0
- package/lib/module/Shared/Libs/demo.utils.js +4 -0
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +47 -1
- package/lib/module/Shared/Libs/vizName.js +138 -16
- package/lib/module/Translation/Resources/en.js +16 -3
- package/lib/module/Translation/Resources/tr.js +16 -3
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +8 -3
- 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.utils.d.ts +25 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/addressProofHeuristic.d.ts +34 -0
- package/lib/typescript/src/Shared/Libs/addressProofHeuristic.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/demo.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/vizName.d.ts +31 -6
- package/lib/typescript/src/Shared/Libs/vizName.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +0 -5
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +13 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +13 -0
- 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 -1
- package/src/Screens/Dynamic/AddressCaptureScreen.tsx +434 -75
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +14 -4
- package/src/Screens/Static/ResultScreen.tsx +59 -0
- package/src/Shared/Animations/address-scan.json +1 -0
- package/src/Shared/Components/DebugNavigationPanel.tsx +13 -0
- package/src/Shared/Components/EIDScanner.tsx +38 -12
- package/src/Shared/Components/IdentityDocumentCamera.tsx +109 -17
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +53 -0
- package/src/Shared/Libs/addressProofHeuristic.ts +197 -0
- package/src/Shared/Libs/demo.utils.ts +5 -0
- package/src/Shared/Libs/sendDiagnosticReport.ts +65 -7
- package/src/Shared/Libs/vizName.ts +158 -20
- package/src/Shared/Types/identificationInfo.ts +0 -5
- package/src/Translation/Resources/en.ts +28 -3
- package/src/Translation/Resources/tr.ts +28 -3
- package/src/version.ts +1 -1
|
@@ -104,6 +104,19 @@ declare const _default: {
|
|
|
104
104
|
'addressCaptureScreen.retakeProof': string;
|
|
105
105
|
'addressCaptureScreen.proofCameraHint': string;
|
|
106
106
|
'addressCaptureScreen.capture': string;
|
|
107
|
+
'addressCaptureScreen.previewHint': string;
|
|
108
|
+
'addressCaptureScreen.warnNoName': string;
|
|
109
|
+
'addressCaptureScreen.warnNoAddress': string;
|
|
110
|
+
'addressCaptureScreen.warnNoText': string;
|
|
111
|
+
'addressCaptureScreen.guideHeader': string;
|
|
112
|
+
'addressCaptureScreen.guideText': string;
|
|
113
|
+
'addressCaptureScreen.guidePointWholeDocument': string;
|
|
114
|
+
'addressCaptureScreen.guidePointLighting': string;
|
|
115
|
+
'addressCaptureScreen.guidePointSteady': string;
|
|
116
|
+
'addressCaptureScreen.guidePointDocuments': string;
|
|
117
|
+
'addressCaptureScreen.retakeTooDark': string;
|
|
118
|
+
'addressCaptureScreen.retakeTooBright': string;
|
|
119
|
+
'addressCaptureScreen.retakeBlurry': string;
|
|
107
120
|
'eidScannerScreen.faceImageNotFound': string;
|
|
108
121
|
'eidScannerScreen.documentCode': string;
|
|
109
122
|
'eidScannerScreen.nationality': 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,wBAyVE"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.513.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -4,12 +4,14 @@ import {
|
|
|
4
4
|
View,
|
|
5
5
|
Text,
|
|
6
6
|
StyleSheet,
|
|
7
|
-
ScrollView,
|
|
8
7
|
Image,
|
|
9
8
|
TouchableOpacity,
|
|
9
|
+
ActivityIndicator,
|
|
10
|
+
NativeModules,
|
|
10
11
|
} from 'react-native';
|
|
11
12
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
12
13
|
import { useTranslation } from 'react-i18next';
|
|
14
|
+
import LottieView from 'lottie-react-native';
|
|
13
15
|
import {
|
|
14
16
|
TrustchexCamera,
|
|
15
17
|
type Frame,
|
|
@@ -26,6 +28,56 @@ import {
|
|
|
26
28
|
trackVerificationComplete,
|
|
27
29
|
} from '../../Shared/Libs/analytics.utils';
|
|
28
30
|
import { useKeepAwake } from '../../Shared/Libs/native-keep-awake.utils';
|
|
31
|
+
import { assessAddressProof } from '../../Shared/Libs/addressProofHeuristic';
|
|
32
|
+
import { recognizeVizBlocks } from '../../Shared/Libs/vizName';
|
|
33
|
+
import { MIN_BRIGHTNESS_THRESHOLD } from '../../Shared/Components/IdentityDocumentCamera.constants';
|
|
34
|
+
|
|
35
|
+
const { OpenCVModule } = NativeModules;
|
|
36
|
+
|
|
37
|
+
// Quality thresholds for the captured proof photo. The darkness floor is shared
|
|
38
|
+
// with the document camera (MIN_BRIGHTNESS_THRESHOLD) so a tuning pass applies
|
|
39
|
+
// to both. The upper bound here is a FULL-FRAME mean-brightness cap for a
|
|
40
|
+
// washed-out capture — deliberately looser than the document camera's
|
|
41
|
+
// MAX_BRIGHTNESS_THRESHOLD (210), which is a REGION glare threshold, not a
|
|
42
|
+
// full-frame mean.
|
|
43
|
+
const MIN_BRIGHTNESS = MIN_BRIGHTNESS_THRESHOLD; // below → too dark
|
|
44
|
+
const MAX_BRIGHTNESS = 245; // full-frame mean above → washed out / glare
|
|
45
|
+
const BLUR_THRESHOLD = 50; // Laplacian variance below → too blurry
|
|
46
|
+
|
|
47
|
+
type QualityReason = 'dark' | 'bright' | 'blurry' | null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Assess a captured proof photo. Brightness comes from the camera Frame (Y-plane
|
|
51
|
+
* luminance, computed natively per frame — no extra decode); only the blur check
|
|
52
|
+
* needs the image (native OpenCV Laplacian variance). Best-effort: returns ok
|
|
53
|
+
* when the native check is unavailable or throws, so an error never blocks.
|
|
54
|
+
*/
|
|
55
|
+
const assessImageQuality = async (
|
|
56
|
+
base64Image: string,
|
|
57
|
+
frameBrightness: number | undefined
|
|
58
|
+
): Promise<QualityReason> => {
|
|
59
|
+
// Prefer the frame's native brightness; only fall back to a decode when the
|
|
60
|
+
// frame didn't carry one.
|
|
61
|
+
try {
|
|
62
|
+
let brightness = frameBrightness;
|
|
63
|
+
if (brightness == null && OpenCVModule) {
|
|
64
|
+
brightness = await OpenCVModule.getAverageBrightness(base64Image);
|
|
65
|
+
}
|
|
66
|
+
if (brightness != null) {
|
|
67
|
+
if (brightness < MIN_BRIGHTNESS) return 'dark';
|
|
68
|
+
if (brightness > MAX_BRIGHTNESS) return 'bright';
|
|
69
|
+
}
|
|
70
|
+
if (!OpenCVModule) return null;
|
|
71
|
+
const isBlurry: boolean = await OpenCVModule.checkBlurry(
|
|
72
|
+
base64Image,
|
|
73
|
+
BLUR_THRESHOLD
|
|
74
|
+
);
|
|
75
|
+
if (isBlurry) return 'blurry';
|
|
76
|
+
return null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
29
81
|
|
|
30
82
|
/**
|
|
31
83
|
* ADDRESS_CAPTURE step (MASAK Tebliğ No. 32 Article 4/C(1)(d)). The user confirms
|
|
@@ -52,26 +104,112 @@ const AddressCaptureScreen = () => {
|
|
|
52
104
|
appContext.lastDocumentRead?.document.address ??
|
|
53
105
|
'';
|
|
54
106
|
|
|
107
|
+
// Holder name parts (chip/MRZ), used to check the captured proof actually
|
|
108
|
+
// shows this person's name. Prefer the corrected display name when available.
|
|
109
|
+
const docName = appContext.lastDocumentRead?.name;
|
|
110
|
+
const nameParts = [
|
|
111
|
+
docName?.displayFirst,
|
|
112
|
+
docName?.displayLast,
|
|
113
|
+
docName?.rawFirst,
|
|
114
|
+
docName?.rawLast,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// Whether the proof is mandatory is governed by the step's own `required`
|
|
118
|
+
// flag (like every other step) — there is no separate config. A required
|
|
119
|
+
// step blocks Continue until a proof photo is captured; an optional step
|
|
120
|
+
// lets the user proceed (and skip) without one. The skip affordance is the
|
|
121
|
+
// NavigationManager's, rendered on the guide for optional steps.
|
|
122
|
+
|
|
55
123
|
const [proofImage, setProofImage] = useState<string | undefined>();
|
|
56
124
|
const [isCameraOpen, setIsCameraOpen] = useState(false);
|
|
125
|
+
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
|
126
|
+
const [capturing, setCapturing] = useState(false);
|
|
127
|
+
const [qualityReason, setQualityReason] = useState<QualityReason>(null);
|
|
128
|
+
// Soft content warning shown on the preview when the OCR text doesn't look
|
|
129
|
+
// like an address document with the holder's name. Never blocks Continue.
|
|
130
|
+
const [contentWarning, setContentWarning] = useState<
|
|
131
|
+
'noName' | 'noAddress' | 'noText' | null
|
|
132
|
+
>(null);
|
|
57
133
|
const captureNextFrame = useRef(false);
|
|
134
|
+
// Mirrors proofImage for the async OCR guard (a stale OCR result must only
|
|
135
|
+
// apply to the photo currently being previewed).
|
|
136
|
+
const proofImageRef = useRef<string | undefined>(undefined);
|
|
137
|
+
proofImageRef.current = proofImage;
|
|
58
138
|
|
|
59
139
|
useEffect(() => {
|
|
60
140
|
trackVerificationStart('ADDRESS_CAPTURE');
|
|
61
141
|
}, []);
|
|
62
142
|
|
|
143
|
+
const openCamera = () => {
|
|
144
|
+
setQualityReason(null);
|
|
145
|
+
setContentWarning(null);
|
|
146
|
+
setIsPreviewOpen(false);
|
|
147
|
+
setIsCameraOpen(true);
|
|
148
|
+
};
|
|
149
|
+
|
|
63
150
|
const onFrameAvailable = (frame: Frame) => {
|
|
64
|
-
if (!captureNextFrame.current) return;
|
|
151
|
+
if (!captureNextFrame.current || capturing) return;
|
|
65
152
|
const b64 = frame.base64Image;
|
|
66
|
-
if (b64)
|
|
67
|
-
|
|
153
|
+
if (!b64) return;
|
|
154
|
+
captureNextFrame.current = false;
|
|
155
|
+
const frameBrightness = frame.brightness;
|
|
156
|
+
setCapturing(true);
|
|
157
|
+
// Gate acceptance on a quality check: a too-dark / too-bright / blurry
|
|
158
|
+
// photo keeps the camera open with a retake message instead of accepting it.
|
|
159
|
+
assessImageQuality(b64, frameBrightness).then((reason) => {
|
|
160
|
+
setCapturing(false);
|
|
161
|
+
if (reason) {
|
|
162
|
+
setQualityReason(reason);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
setQualityReason(null);
|
|
166
|
+
setContentWarning(null);
|
|
68
167
|
setProofImage(b64);
|
|
168
|
+
// Show the captured photo immediately (Retake / Continue) — don't make
|
|
169
|
+
// the user wait on OCR before seeing their photo.
|
|
69
170
|
setIsCameraOpen(false);
|
|
171
|
+
setIsPreviewOpen(true);
|
|
172
|
+
// Soft content check runs in the background and fills in the warning. We
|
|
173
|
+
// OCR the FULL captured image (not frame.resultText, which the native
|
|
174
|
+
// camera crops to the MRZ ROI), so the name/address is actually in view.
|
|
175
|
+
// Best-effort: any failure → no warning, never blocks Continue.
|
|
176
|
+
assessProofContent(b64).then((warning) => {
|
|
177
|
+
// Only apply if the user is still viewing THIS captured photo.
|
|
178
|
+
if (proofImageRef.current === b64) setContentWarning(warning);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/// OCR the full captured image and run the address-proof heuristic. Returns
|
|
184
|
+
/// the warning key ('noName' | 'noAddress' | 'noText') or null when it looks
|
|
185
|
+
/// fine or the check can't run.
|
|
186
|
+
const assessProofContent = async (
|
|
187
|
+
b64: string
|
|
188
|
+
): Promise<'noName' | 'noAddress' | 'noText' | null> => {
|
|
189
|
+
try {
|
|
190
|
+
const blocks = await recognizeVizBlocks(b64);
|
|
191
|
+
const ocrText = blocks.map((b) => b.text).join('\n');
|
|
192
|
+
const signals = assessAddressProof(ocrText, { nameParts, chipAddress });
|
|
193
|
+
if (signals.ok) return null;
|
|
194
|
+
if (!signals.hasName && !signals.hasAddress) return 'noText';
|
|
195
|
+
return signals.hasName ? 'noAddress' : 'noName';
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
70
198
|
}
|
|
71
199
|
};
|
|
72
200
|
|
|
73
|
-
|
|
74
|
-
|
|
201
|
+
const qualityMessage = (): string => {
|
|
202
|
+
switch (qualityReason) {
|
|
203
|
+
case 'dark':
|
|
204
|
+
return t('addressCaptureScreen.retakeTooDark');
|
|
205
|
+
case 'bright':
|
|
206
|
+
return t('addressCaptureScreen.retakeTooBright');
|
|
207
|
+
case 'blurry':
|
|
208
|
+
return t('addressCaptureScreen.retakeBlurry');
|
|
209
|
+
default:
|
|
210
|
+
return t('addressCaptureScreen.proofCameraHint');
|
|
211
|
+
}
|
|
212
|
+
};
|
|
75
213
|
|
|
76
214
|
const onContinue = () => {
|
|
77
215
|
// Attach the proof to the existing (chip-sourced) address without changing
|
|
@@ -84,113 +222,334 @@ const AddressCaptureScreen = () => {
|
|
|
84
222
|
proofImage,
|
|
85
223
|
proofImageMimeType: proofImage ? 'image/jpeg' : undefined,
|
|
86
224
|
};
|
|
225
|
+
setIsPreviewOpen(false);
|
|
87
226
|
trackVerificationComplete('ADDRESS_CAPTURE', true, 1);
|
|
88
227
|
navigationManagerRef.current?.navigateToNextStep();
|
|
89
228
|
};
|
|
90
229
|
|
|
230
|
+
// Post-capture preview — confirm the photo, then Continue or Retake.
|
|
231
|
+
if (isPreviewOpen && proofImage) {
|
|
232
|
+
return (
|
|
233
|
+
<SafeAreaView style={styles.previewContainer}>
|
|
234
|
+
<View style={styles.previewImageWrap}>
|
|
235
|
+
<Image
|
|
236
|
+
source={{ uri: `data:image/jpeg;base64,${proofImage}` }}
|
|
237
|
+
style={styles.previewImage}
|
|
238
|
+
resizeMode="contain"
|
|
239
|
+
/>
|
|
240
|
+
</View>
|
|
241
|
+
{contentWarning ? (
|
|
242
|
+
<View style={styles.previewWarning}>
|
|
243
|
+
<Text style={styles.previewWarningText}>
|
|
244
|
+
{contentWarning === 'noName'
|
|
245
|
+
? t('addressCaptureScreen.warnNoName')
|
|
246
|
+
: contentWarning === 'noAddress'
|
|
247
|
+
? t('addressCaptureScreen.warnNoAddress')
|
|
248
|
+
: t('addressCaptureScreen.warnNoText')}
|
|
249
|
+
</Text>
|
|
250
|
+
</View>
|
|
251
|
+
) : (
|
|
252
|
+
<Text style={styles.previewHint}>
|
|
253
|
+
{t('addressCaptureScreen.previewHint')}
|
|
254
|
+
</Text>
|
|
255
|
+
)}
|
|
256
|
+
<View
|
|
257
|
+
style={[styles.previewFooter, { paddingBottom: insets.bottom + 12 }]}
|
|
258
|
+
>
|
|
259
|
+
<View style={styles.previewActions}>
|
|
260
|
+
<View style={styles.previewActionItem}>
|
|
261
|
+
<StyledButton mode="outlined" onPress={openCamera}>
|
|
262
|
+
{t('addressCaptureScreen.retakeProof')}
|
|
263
|
+
</StyledButton>
|
|
264
|
+
</View>
|
|
265
|
+
<View style={styles.previewActionItem}>
|
|
266
|
+
<StyledButton onPress={onContinue}>
|
|
267
|
+
{t('general.continue')}
|
|
268
|
+
</StyledButton>
|
|
269
|
+
</View>
|
|
270
|
+
</View>
|
|
271
|
+
{/* Skip below the actions (own row) so it never overlaps them. */}
|
|
272
|
+
<NavigationManager ref={navigationManagerRef} />
|
|
273
|
+
</View>
|
|
274
|
+
</SafeAreaView>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
91
278
|
if (isCameraOpen) {
|
|
92
279
|
return (
|
|
93
|
-
<
|
|
280
|
+
<View style={styles.cameraContainer}>
|
|
94
281
|
<TrustchexCamera
|
|
95
|
-
style={
|
|
282
|
+
style={StyleSheet.absoluteFill}
|
|
283
|
+
cameraType="back"
|
|
284
|
+
// The native camera emits onFrameAvailable when frame processing is on
|
|
285
|
+
// AND either a detector is enabled or includeBase64 is set. We only
|
|
286
|
+
// want the JPEG (no per-frame OCR), so includeBase64 alone keeps frames
|
|
287
|
+
// flowing; the proof photo is OCR'd once, on the still, at capture.
|
|
288
|
+
enableFrameProcessing
|
|
96
289
|
includeBase64
|
|
290
|
+
targetFps={8}
|
|
97
291
|
onFrameAvailable={(e) => onFrameAvailable(e.nativeEvent.frame)}
|
|
98
292
|
/>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
293
|
+
|
|
294
|
+
{/* Framing guide so the user lines up the whole document. The bottom
|
|
295
|
+
inset clears the hint + shutter stack so the border never overlaps
|
|
296
|
+
the "fit the whole document" message. */}
|
|
297
|
+
<View
|
|
298
|
+
style={[
|
|
299
|
+
styles.frameGuide,
|
|
300
|
+
{ top: insets.top + 64, bottom: insets.bottom + 210 },
|
|
301
|
+
]}
|
|
302
|
+
pointerEvents="none"
|
|
303
|
+
/>
|
|
304
|
+
|
|
305
|
+
{/* Floating cancel (X), top-left. */}
|
|
306
|
+
<TouchableOpacity
|
|
307
|
+
style={[styles.cancelButton, { top: insets.top + 12 }]}
|
|
308
|
+
accessibilityLabel={t('general.cancel')}
|
|
309
|
+
onPress={() => {
|
|
310
|
+
captureNextFrame.current = false;
|
|
311
|
+
// Back to the guide (the only non-camera/non-preview screen).
|
|
312
|
+
setIsCameraOpen(false);
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
<Text style={styles.cancelGlyph}>✕</Text>
|
|
316
|
+
</TouchableOpacity>
|
|
317
|
+
|
|
318
|
+
{/* Bottom: hint (red on quality failure) + floating shutter. */}
|
|
319
|
+
<View
|
|
320
|
+
style={[styles.cameraBottom, { paddingBottom: insets.bottom + 28 }]}
|
|
321
|
+
pointerEvents="box-none"
|
|
322
|
+
>
|
|
323
|
+
<View
|
|
324
|
+
style={[
|
|
325
|
+
styles.cameraHintPill,
|
|
326
|
+
qualityReason ? styles.cameraHintPillError : null,
|
|
327
|
+
]}
|
|
328
|
+
>
|
|
329
|
+
<Text style={styles.cameraHint}>{qualityMessage()}</Text>
|
|
330
|
+
</View>
|
|
331
|
+
<TouchableOpacity
|
|
332
|
+
style={styles.shutter}
|
|
333
|
+
disabled={capturing}
|
|
334
|
+
accessibilityLabel={t('addressCaptureScreen.capture')}
|
|
335
|
+
onPress={() => {
|
|
336
|
+
captureNextFrame.current = true;
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
{capturing ? (
|
|
340
|
+
<ActivityIndicator color="#111" />
|
|
341
|
+
) : (
|
|
342
|
+
<View style={styles.shutterInner} />
|
|
343
|
+
)}
|
|
108
344
|
</TouchableOpacity>
|
|
109
345
|
</View>
|
|
110
|
-
</
|
|
346
|
+
</View>
|
|
111
347
|
);
|
|
112
348
|
}
|
|
113
349
|
|
|
350
|
+
// No interim form: the guide is the only non-camera/non-preview screen.
|
|
351
|
+
// Reached only if the camera was cancelled — fall back to the guide.
|
|
114
352
|
return (
|
|
115
|
-
<SafeAreaView style={styles.
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
353
|
+
<SafeAreaView style={styles.guideContainer}>
|
|
354
|
+
<View style={styles.guideContent}>
|
|
355
|
+
<LottieView
|
|
356
|
+
source={require('../../Shared/Animations/address-scan.json')}
|
|
357
|
+
style={styles.guideAnimation}
|
|
358
|
+
loop
|
|
359
|
+
autoPlay
|
|
360
|
+
/>
|
|
361
|
+
<Text style={styles.guideHeader}>
|
|
362
|
+
{t('addressCaptureScreen.guideHeader')}
|
|
363
|
+
</Text>
|
|
364
|
+
{/* Subtitle + bullet points are LEFT-aligned, matching the other guides. */}
|
|
365
|
+
<View style={styles.guidePoints}>
|
|
366
|
+
<Text style={styles.guideText}>
|
|
367
|
+
{t('addressCaptureScreen.guideText')}
|
|
368
|
+
</Text>
|
|
369
|
+
<Text style={styles.guidePoint}>
|
|
370
|
+
• {t('addressCaptureScreen.guidePointWholeDocument')}
|
|
371
|
+
</Text>
|
|
372
|
+
<Text style={styles.guidePoint}>
|
|
373
|
+
• {t('addressCaptureScreen.guidePointLighting')}
|
|
374
|
+
</Text>
|
|
375
|
+
<Text style={styles.guidePoint}>
|
|
376
|
+
• {t('addressCaptureScreen.guidePointSteady')}
|
|
377
|
+
</Text>
|
|
378
|
+
<Text style={styles.guidePoint}>
|
|
379
|
+
• {t('addressCaptureScreen.guidePointDocuments')}
|
|
380
|
+
</Text>
|
|
381
|
+
</View>
|
|
123
382
|
{chipAddress ? (
|
|
124
|
-
|
|
125
|
-
<Text style={styles.
|
|
383
|
+
<View style={styles.chipAddressBox}>
|
|
384
|
+
<Text style={styles.chipAddressLabel}>
|
|
126
385
|
{t('addressCaptureScreen.addressLabel')}
|
|
127
386
|
</Text>
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
</View>
|
|
131
|
-
<Text style={styles.chipHint}>
|
|
387
|
+
<Text style={styles.chipAddressText}>{chipAddress}</Text>
|
|
388
|
+
<Text style={styles.chipAddressHint}>
|
|
132
389
|
{t('addressCaptureScreen.prefilledFromChip')}
|
|
133
390
|
</Text>
|
|
134
|
-
|
|
135
|
-
) : null}
|
|
136
|
-
|
|
137
|
-
<Text style={styles.label}>{t('addressCaptureScreen.proofLabel')}</Text>
|
|
138
|
-
<Text style={styles.proofHint}>{t('addressCaptureScreen.proofHint')}</Text>
|
|
139
|
-
{proofImage ? (
|
|
140
|
-
<Image
|
|
141
|
-
source={{ uri: `data:image/jpeg;base64,${proofImage}` }}
|
|
142
|
-
style={styles.proofPreview}
|
|
143
|
-
resizeMode="cover"
|
|
144
|
-
/>
|
|
391
|
+
</View>
|
|
145
392
|
) : null}
|
|
146
|
-
<
|
|
147
|
-
{
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
</
|
|
151
|
-
</ScrollView>
|
|
152
|
-
|
|
153
|
-
<View style={[styles.footer, { paddingBottom: insets.bottom + 12 }]}>
|
|
154
|
-
<StyledButton disabled={!canContinue} onPress={onContinue}>
|
|
155
|
-
{t('general.continue')}
|
|
156
|
-
</StyledButton>
|
|
393
|
+
<View style={styles.guideButton}>
|
|
394
|
+
<StyledButton onPress={openCamera}>
|
|
395
|
+
{t('general.letsGo')}
|
|
396
|
+
</StyledButton>
|
|
397
|
+
</View>
|
|
157
398
|
</View>
|
|
158
|
-
|
|
159
399
|
<NavigationManager ref={navigationManagerRef} />
|
|
160
400
|
</SafeAreaView>
|
|
161
401
|
);
|
|
162
402
|
};
|
|
163
403
|
|
|
164
404
|
const styles = StyleSheet.create({
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
405
|
+
// Start guide — centered vertically + horizontally, like the other guides.
|
|
406
|
+
guideContainer: {
|
|
407
|
+
flex: 1,
|
|
408
|
+
backgroundColor: '#FFFFFF',
|
|
409
|
+
},
|
|
410
|
+
// Inner wrapper carries the horizontal padding + centering, so it always
|
|
411
|
+
// applies regardless of the SafeAreaView's own inset handling on iOS.
|
|
412
|
+
guideContent: {
|
|
413
|
+
flex: 1,
|
|
414
|
+
paddingHorizontal: 24,
|
|
415
|
+
justifyContent: 'center',
|
|
416
|
+
alignItems: 'center',
|
|
417
|
+
},
|
|
418
|
+
guideAnimation: { width: 220, height: 220, marginBottom: 8 },
|
|
419
|
+
guideHeader: {
|
|
420
|
+
fontSize: 18,
|
|
421
|
+
fontWeight: '700',
|
|
422
|
+
color: '#000',
|
|
423
|
+
textAlign: 'center',
|
|
424
|
+
marginBottom: 10,
|
|
425
|
+
},
|
|
426
|
+
// Subtitle + bullets: left-aligned block, full width.
|
|
427
|
+
guidePoints: { alignSelf: 'stretch', marginTop: 4 },
|
|
428
|
+
guideText: {
|
|
429
|
+
fontSize: 14,
|
|
430
|
+
color: '#000',
|
|
431
|
+
textAlign: 'left',
|
|
432
|
+
marginBottom: 12,
|
|
433
|
+
},
|
|
434
|
+
guidePoint: {
|
|
435
|
+
fontSize: 14,
|
|
436
|
+
color: '#000',
|
|
437
|
+
textAlign: 'left',
|
|
438
|
+
lineHeight: 20,
|
|
439
|
+
marginTop: 10,
|
|
440
|
+
},
|
|
441
|
+
chipAddressBox: {
|
|
442
|
+
alignSelf: 'stretch',
|
|
443
|
+
marginTop: 16,
|
|
444
|
+
padding: 12,
|
|
172
445
|
borderWidth: 1,
|
|
173
446
|
borderColor: '#E0E0E0',
|
|
174
447
|
backgroundColor: '#F7F7F7',
|
|
175
448
|
borderRadius: 8,
|
|
176
|
-
padding: 12,
|
|
177
|
-
minHeight: 56,
|
|
178
449
|
},
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
450
|
+
chipAddressLabel: {
|
|
451
|
+
fontSize: 13,
|
|
452
|
+
fontWeight: '600',
|
|
453
|
+
color: '#222',
|
|
454
|
+
marginBottom: 4,
|
|
455
|
+
},
|
|
456
|
+
chipAddressText: { fontSize: 15, color: '#111' },
|
|
457
|
+
chipAddressHint: { fontSize: 12, color: '#2E7D32', marginTop: 4 },
|
|
458
|
+
guideButton: { alignSelf: 'stretch', marginTop: 24 },
|
|
459
|
+
// Post-capture preview — white screen, matching the rest of the app.
|
|
460
|
+
previewContainer: { flex: 1, backgroundColor: '#FFFFFF' },
|
|
461
|
+
previewImageWrap: { flex: 1, padding: 16 },
|
|
462
|
+
previewImage: {
|
|
463
|
+
flex: 1,
|
|
182
464
|
width: '100%',
|
|
183
|
-
|
|
184
|
-
|
|
465
|
+
borderRadius: 12,
|
|
466
|
+
},
|
|
467
|
+
previewHint: {
|
|
468
|
+
color: '#555',
|
|
469
|
+
fontSize: 13,
|
|
470
|
+
textAlign: 'center',
|
|
471
|
+
paddingHorizontal: 20,
|
|
472
|
+
marginBottom: 12,
|
|
473
|
+
},
|
|
474
|
+
previewWarning: {
|
|
475
|
+
marginHorizontal: 16,
|
|
185
476
|
marginBottom: 12,
|
|
186
|
-
|
|
477
|
+
paddingHorizontal: 14,
|
|
478
|
+
paddingVertical: 10,
|
|
479
|
+
borderRadius: 8,
|
|
480
|
+
backgroundColor: '#FFF4E5',
|
|
481
|
+
borderWidth: 1,
|
|
482
|
+
borderColor: '#F0B27A',
|
|
483
|
+
},
|
|
484
|
+
previewWarningText: { color: '#92400E', fontSize: 13, textAlign: 'center' },
|
|
485
|
+
previewFooter: {
|
|
486
|
+
paddingHorizontal: 20,
|
|
487
|
+
paddingTop: 8,
|
|
488
|
+
backgroundColor: '#FFFFFF',
|
|
489
|
+
gap: 4,
|
|
187
490
|
},
|
|
188
|
-
|
|
491
|
+
previewActions: { flexDirection: 'row', gap: 12 },
|
|
492
|
+
previewActionItem: { flex: 1 },
|
|
189
493
|
cameraContainer: { flex: 1, backgroundColor: '#000' },
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
494
|
+
frameGuide: {
|
|
495
|
+
position: 'absolute',
|
|
496
|
+
left: 20,
|
|
497
|
+
right: 20,
|
|
498
|
+
borderWidth: 2,
|
|
499
|
+
borderColor: 'rgba(255,255,255,0.7)',
|
|
500
|
+
borderRadius: 12,
|
|
501
|
+
zIndex: 1,
|
|
502
|
+
},
|
|
503
|
+
cancelButton: {
|
|
504
|
+
position: 'absolute',
|
|
505
|
+
left: 16,
|
|
506
|
+
width: 44,
|
|
507
|
+
height: 44,
|
|
508
|
+
borderRadius: 22,
|
|
509
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
510
|
+
alignItems: 'center',
|
|
511
|
+
justifyContent: 'center',
|
|
512
|
+
zIndex: 2,
|
|
513
|
+
},
|
|
514
|
+
cancelGlyph: {
|
|
515
|
+
color: '#FFF',
|
|
516
|
+
fontSize: 22,
|
|
517
|
+
fontWeight: '600',
|
|
518
|
+
lineHeight: 24,
|
|
519
|
+
},
|
|
520
|
+
cameraBottom: {
|
|
521
|
+
position: 'absolute',
|
|
522
|
+
left: 0,
|
|
523
|
+
right: 0,
|
|
524
|
+
bottom: 0,
|
|
525
|
+
alignItems: 'center',
|
|
526
|
+
},
|
|
527
|
+
cameraHintPill: {
|
|
528
|
+
maxWidth: '88%',
|
|
529
|
+
paddingHorizontal: 16,
|
|
530
|
+
paddingVertical: 10,
|
|
531
|
+
borderRadius: 10,
|
|
532
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
533
|
+
marginBottom: 20,
|
|
534
|
+
},
|
|
535
|
+
cameraHintPillError: { backgroundColor: 'rgba(176,0,32,0.85)' },
|
|
536
|
+
cameraHint: { color: '#FFF', textAlign: 'center', fontSize: 14 },
|
|
537
|
+
shutter: {
|
|
538
|
+
width: 74,
|
|
539
|
+
height: 74,
|
|
540
|
+
borderRadius: 37,
|
|
541
|
+
borderWidth: 4,
|
|
542
|
+
borderColor: 'rgba(255,255,255,0.85)',
|
|
543
|
+
backgroundColor: 'rgba(255,255,255,0.25)',
|
|
544
|
+
alignItems: 'center',
|
|
545
|
+
justifyContent: 'center',
|
|
546
|
+
},
|
|
547
|
+
shutterInner: {
|
|
548
|
+
width: 56,
|
|
549
|
+
height: 56,
|
|
550
|
+
borderRadius: 28,
|
|
551
|
+
backgroundColor: '#FFF',
|
|
552
|
+
},
|
|
194
553
|
});
|
|
195
554
|
|
|
196
555
|
export default AddressCaptureScreen;
|
|
@@ -118,14 +118,24 @@ const IdentityDocumentEIDScanningScreen = () => {
|
|
|
118
118
|
}
|
|
119
119
|
vizImage={
|
|
120
120
|
// Full data-page image from the EID step's camera scan. EIDScanner
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
121
|
+
// OCRs it to recover the diacritic printed name when the chip's
|
|
122
|
+
// DG11 is absent or one-sided (e.g. Turkish passports). The live
|
|
123
|
+
// camera only OCRs a cropped MRZ region, so the printed name must
|
|
124
|
+
// be recovered from the full image here.
|
|
125
125
|
passportData?.image ??
|
|
126
126
|
idFrontSideData?.image ??
|
|
127
127
|
idBackSideData?.image
|
|
128
128
|
}
|
|
129
|
+
vizImages={
|
|
130
|
+
// All available captures — EIDScanner OCRs each and votes on the
|
|
131
|
+
// recovered name so a single noisy pass can't corrupt it. De-dup
|
|
132
|
+
// happens in EIDScanner, so overlap with `vizImage` is harmless.
|
|
133
|
+
[
|
|
134
|
+
passportData?.image,
|
|
135
|
+
idFrontSideData?.image,
|
|
136
|
+
idBackSideData?.image,
|
|
137
|
+
]
|
|
138
|
+
}
|
|
129
139
|
onScanSuccess={(mrzFields, faceImage, mimeType) => {
|
|
130
140
|
if (mrzFields && faceImage && mimeType) {
|
|
131
141
|
if (!appContext.identificationInfo.scannedDocument) {
|