@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
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBAgUE"}
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.512.1";
1
+ export declare const SDK_VERSION = "1.513.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustchex/react-native-sdk",
3
- "version": "1.512.1",
3
+ "version": "1.513.0",
4
4
  "description": "Trustchex mobile app react native SDK for android or ios devices",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -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
- captureNextFrame.current = false;
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
- // A proof photo is the whole point of this step.
74
- const canContinue = !!proofImage;
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
- <SafeAreaView style={styles.cameraContainer}>
280
+ <View style={styles.cameraContainer}>
94
281
  <TrustchexCamera
95
- style={styles.camera}
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
- <View style={[styles.cameraControls, { paddingBottom: insets.bottom + 16 }]}>
100
- <Text style={styles.cameraHint}>
101
- {t('addressCaptureScreen.proofCameraHint')}
102
- </Text>
103
- <StyledButton onPress={() => (captureNextFrame.current = true)}>
104
- {t('addressCaptureScreen.capture')}
105
- </StyledButton>
106
- <TouchableOpacity onPress={() => setIsCameraOpen(false)}>
107
- <Text style={styles.cancel}>{t('general.cancel')}</Text>
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
- </SafeAreaView>
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.container}>
116
- <ScrollView
117
- contentContainerStyle={[styles.content, { paddingBottom: insets.bottom + 24 }]}
118
- keyboardShouldPersistTaps="handled"
119
- >
120
- <Text style={styles.title}>{t('addressCaptureScreen.title')}</Text>
121
- <Text style={styles.subtitle}>{t('addressCaptureScreen.subtitle')}</Text>
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.label}>
383
+ <View style={styles.chipAddressBox}>
384
+ <Text style={styles.chipAddressLabel}>
126
385
  {t('addressCaptureScreen.addressLabel')}
127
386
  </Text>
128
- <View style={styles.readonlyAddress}>
129
- <Text style={styles.readonlyAddressText}>{chipAddress}</Text>
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
- <StyledButton onPress={() => setIsCameraOpen(true)}>
147
- {proofImage
148
- ? t('addressCaptureScreen.retakeProof')
149
- : t('addressCaptureScreen.captureProof')}
150
- </StyledButton>
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
- container: { flex: 1, backgroundColor: '#FFFFFF' },
166
- content: { padding: 20 },
167
- title: { fontSize: 22, fontWeight: '700', color: '#111', marginBottom: 8 },
168
- subtitle: { fontSize: 15, color: '#555', marginBottom: 16 },
169
- chipHint: { fontSize: 13, color: '#2E7D32', marginBottom: 12 },
170
- label: { fontSize: 14, fontWeight: '600', color: '#222', marginTop: 16, marginBottom: 6 },
171
- readonlyAddress: {
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
- readonlyAddressText: { fontSize: 15, color: '#111' },
180
- proofHint: { fontSize: 13, color: '#777', marginBottom: 10 },
181
- proofPreview: {
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
- height: 180,
184
- borderRadius: 8,
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
- backgroundColor: '#EEE',
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
- footer: { paddingHorizontal: 20, paddingTop: 8, backgroundColor: '#FFFFFF' },
491
+ previewActions: { flexDirection: 'row', gap: 12 },
492
+ previewActionItem: { flex: 1 },
189
493
  cameraContainer: { flex: 1, backgroundColor: '#000' },
190
- camera: { flex: 1 },
191
- cameraControls: { paddingHorizontal: 20, gap: 12, backgroundColor: '#000' },
192
- cameraHint: { color: '#FFF', textAlign: 'center', marginBottom: 8 },
193
- cancel: { color: '#FFF', textAlign: 'center', marginTop: 8, fontSize: 15 },
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
- // runs a one-shot OCR pass over it to recover the diacritic printed
122
- // name when the chip's DG11 is absent or one-sided (e.g. Turkish
123
- // passports). The live camera only OCRs a cropped MRZ region, so the
124
- // printed name must be recovered from the full image here.
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) {