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