@trustchex/react-native-sdk 1.475.1 → 1.481.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +76 -24
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
- package/ios/Camera/TrustchexCameraView.swift +20 -15
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
- package/lib/module/Screens/Static/ResultScreen.js +145 -113
- package/lib/module/Shared/Components/DebugOverlay.js +5 -2
- package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +185 -30
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
- package/lib/module/Shared/Components/NavigationManager.js +7 -9
- package/lib/module/Shared/EIDReader/eidReader.js +159 -38
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/lib/module/Shared/Libs/SignalingClient.js +6 -5
- package/lib/module/Shared/Libs/crypto.utils.js +25 -3
- package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
- package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
- package/lib/module/Shared/Libs/diagnostics.js +171 -0
- package/lib/module/Shared/Libs/http-client.js +19 -6
- package/lib/module/Shared/Libs/mrz.utils.js +193 -33
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +74 -3
- package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
- package/lib/module/Shared/Libs/promise.utils.js +4 -3
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
- package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
- package/lib/module/Shared/Services/AnalyticsService.js +8 -2
- package/lib/module/Shared/Services/DataUploadService.js +114 -127
- package/lib/module/Shared/Services/VideoSessionService.js +4 -3
- package/lib/module/Translation/Resources/en.js +6 -0
- package/lib/module/Translation/Resources/tr.js +6 -0
- package/lib/module/Trustchex.js +20 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugOverlay.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/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/IdentityDocumentCamera.utils.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.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/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- 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/http-client.d.ts +2 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +55 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- 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/promise.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/Libs/shuffle.utils.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +17 -3
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
- package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
- package/src/Screens/Static/ResultScreen.tsx +172 -162
- package/src/Shared/Components/DebugOverlay.tsx +5 -2
- package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
- package/src/Shared/Components/IdentityDocumentCamera.tsx +200 -29
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
- package/src/Shared/Components/NavigationManager.tsx +7 -7
- package/src/Shared/Components/TrustchexCamera.tsx +6 -0
- package/src/Shared/EIDReader/eidReader.ts +181 -52
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/src/Shared/Libs/SignalingClient.ts +10 -5
- package/src/Shared/Libs/crypto.utils.ts +41 -4
- package/src/Shared/Libs/deeplink.utils.ts +39 -7
- package/src/Shared/Libs/diagnosticReport.ts +206 -0
- package/src/Shared/Libs/diagnostics.ts +251 -0
- package/src/Shared/Libs/http-client.ts +24 -8
- package/src/Shared/Libs/mrz.utils.ts +200 -34
- package/src/Shared/Libs/mrzFrameAggregator.ts +112 -2
- package/src/Shared/Libs/native-device-info.utils.ts +116 -0
- package/src/Shared/Libs/promise.utils.ts +11 -5
- package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
- package/src/Shared/Libs/shuffle.utils.ts +15 -0
- package/src/Shared/Services/AnalyticsService.ts +17 -1
- package/src/Shared/Services/DataUploadService.ts +155 -166
- package/src/Shared/Services/VideoSessionService.ts +15 -3
- package/src/Translation/Resources/en.ts +7 -0
- package/src/Translation/Resources/tr.ts +7 -0
- package/src/Trustchex.tsx +26 -3
- package/src/version.ts +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Alert } from 'react-native';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import StyledButton from "./StyledButton.js";
|
|
7
|
+
import { sendDiagnosticReport, isDiagnosticSharingAvailable } from "../Libs/sendDiagnosticReport.js";
|
|
8
|
+
import { SDK_VERSION } from "../../version.js";
|
|
9
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
/**
|
|
11
|
+
* "Report a scanning issue" button for the result screen.
|
|
12
|
+
*
|
|
13
|
+
* Shows a brief notice describing what will be shared (the draft itself is the
|
|
14
|
+
* user's review/consent step), then opens the OS share sheet — the user picks
|
|
15
|
+
* email / WhatsApp / Drive / etc. and sends the diagnostic bundle to support.
|
|
16
|
+
* Self-contained so the result screen only needs to render it with the scan
|
|
17
|
+
* data + captured images.
|
|
18
|
+
*/
|
|
19
|
+
const DiagnosticReportButton = ({
|
|
20
|
+
scan,
|
|
21
|
+
sessionId,
|
|
22
|
+
images
|
|
23
|
+
}) => {
|
|
24
|
+
const {
|
|
25
|
+
t
|
|
26
|
+
} = useTranslation();
|
|
27
|
+
const [sending, setSending] = useState(false);
|
|
28
|
+
|
|
29
|
+
// `react-native-share` is optional — hide the button entirely when the host
|
|
30
|
+
// app hasn't installed it, so apps that don't want diagnostics see no UI.
|
|
31
|
+
if (!isDiagnosticSharingAvailable()) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const onPress = () => {
|
|
35
|
+
if (sending) return;
|
|
36
|
+
// Brief notice — the share draft is the actual review/consent step.
|
|
37
|
+
Alert.alert(t('diagnosticReport.noticeTitle'), t('diagnosticReport.noticeBody'), [{
|
|
38
|
+
text: t('diagnosticReport.cancel'),
|
|
39
|
+
style: 'cancel'
|
|
40
|
+
}, {
|
|
41
|
+
text: t('diagnosticReport.continue'),
|
|
42
|
+
onPress: async () => {
|
|
43
|
+
setSending(true);
|
|
44
|
+
try {
|
|
45
|
+
await sendDiagnosticReport({
|
|
46
|
+
scan,
|
|
47
|
+
sdkVersion: SDK_VERSION,
|
|
48
|
+
sessionId,
|
|
49
|
+
images
|
|
50
|
+
});
|
|
51
|
+
} finally {
|
|
52
|
+
setSending(false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}]);
|
|
56
|
+
};
|
|
57
|
+
return /*#__PURE__*/_jsx(StyledButton, {
|
|
58
|
+
mode: "outlined",
|
|
59
|
+
onPress: onPress,
|
|
60
|
+
disabled: sending,
|
|
61
|
+
children: t('diagnosticReport.button')
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
export default DiagnosticReportButton;
|
|
@@ -13,6 +13,7 @@ import { useKeepAwake } from "../Libs/native-keep-awake.utils.js";
|
|
|
13
13
|
import { useIsFocused } from '@react-navigation/native';
|
|
14
14
|
import { useTranslation } from 'react-i18next';
|
|
15
15
|
import { debugLog, logError, isDebugEnabled } from "../Libs/debug.utils.js";
|
|
16
|
+
import { diagnostics } from "../Libs/diagnostics.js";
|
|
16
17
|
import LottieView from 'lottie-react-native';
|
|
17
18
|
import StyledButton from "./StyledButton.js";
|
|
18
19
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
@@ -22,7 +23,7 @@ import { useTheme } from "../Contexts/ThemeContext.js";
|
|
|
22
23
|
import DebugOverlay, { TestModePanel } from "./DebugOverlay.js";
|
|
23
24
|
import { getStatusMessage, getFrameToScreenTransform, transformBoundsToScreen, getScanAreaBounds, angleBetweenPoints, detectDocumentType, determineDocumentTypeToSet, areMRZFieldsEqual, hasRequiredMRZFields, validateFacePosition } from "./IdentityDocumentCamera.utils.js";
|
|
24
25
|
import { handlePassportFlow, handleIDFrontFlow, handleIDBackFlow, getNextStepAfterHologram } from "./IdentityDocumentCamera.flows.js";
|
|
25
|
-
import { HOLOGRAM_IMAGE_COUNT, HOLOGRAM_DETECTION_THRESHOLD, HOLOGRAM_DETECTION_RETRY_COUNT, HOLOGRAM_CAPTURE_INTERVAL, HOLOGRAM_MAX_FRAMES_WITHOUT_FACE, MIN_BRIGHTNESS_THRESHOLD, MAX_BRIGHTNESS_THRESHOLD, FACE_EDGE_MARGIN_PERCENT, MAX_CONSECUTIVE_QUALITY_FAILURES,
|
|
26
|
+
import { HOLOGRAM_IMAGE_COUNT, HOLOGRAM_DETECTION_THRESHOLD, HOLOGRAM_DETECTION_RETRY_COUNT, HOLOGRAM_CAPTURE_INTERVAL, HOLOGRAM_MAX_FRAMES_WITHOUT_FACE, MIN_BRIGHTNESS_THRESHOLD, MAX_BRIGHTNESS_THRESHOLD, FACE_EDGE_MARGIN_PERCENT, MAX_CONSECUTIVE_QUALITY_FAILURES, REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS, SIGNATURE_TEXT_REGEX, MRZ_BLOCK_PATTERN, PASSPORT_MRZ_PATTERN, MIN_CARD_FACE_SIZE_PERCENT } from "./IdentityDocumentCamera.constants.js";
|
|
26
27
|
|
|
27
28
|
// Re-export types for backward compatibility
|
|
28
29
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
@@ -52,13 +53,31 @@ const IdentityDocumentCamera = ({
|
|
|
52
53
|
}, []);
|
|
53
54
|
const isCameraInitialized = useRef(false);
|
|
54
55
|
const [currentFaceImage, setCurrentFaceImage] = useState(undefined);
|
|
56
|
+
// Synchronous mirror of the locked face image (see currentHologramImageRef).
|
|
57
|
+
// Used as a fallback when completing SCAN_HOLOGRAM: the torch is on during
|
|
58
|
+
// hologram capture so the live frame's face is often rejected, leaving the
|
|
59
|
+
// state-derived faceImageToUse null within the stale closure — without this
|
|
60
|
+
// the completion gate's `&& !!faceImageToUse` would never pass.
|
|
61
|
+
const currentFaceImageRef = useRef(undefined);
|
|
55
62
|
const [_currentHologramMaskImage, setCurrentHologramMaskImage] = useState(undefined);
|
|
56
63
|
const [currentHologramImage, setCurrentHologramImage] = useState(undefined);
|
|
64
|
+
// Mirror of currentHologramImage for synchronous reads inside the per-frame
|
|
65
|
+
// callback. The setState above is async and currentHologramImage is a
|
|
66
|
+
// useCallback dependency, so frames processed before React re-renders the
|
|
67
|
+
// closure would otherwise still see the old (undefined) value — causing the
|
|
68
|
+
// SCAN_HOLOGRAM step to re-collect faces forever and never complete after a
|
|
69
|
+
// successful detection. Always keep this ref in lockstep with the state.
|
|
70
|
+
const currentHologramImageRef = useRef(undefined);
|
|
57
71
|
const [currentSecondaryFaceImage, setCurrentSecondaryFaceImage] = useState(undefined);
|
|
58
72
|
const [isBrightnessLow, setIsBrightnessLow] = useState(false);
|
|
59
73
|
const [isFrameBlurry, setIsFrameBlurry] = useState(false);
|
|
60
74
|
const [hasGuideShown, setHasGuideShown] = useState(false);
|
|
61
75
|
const [status, setStatus] = useState('SEARCHING');
|
|
76
|
+
// True once the multi-frame voted MRZ consensus is STABLE + valid (the same
|
|
77
|
+
// reading matched across `stabilityTarget` consecutive frames, with a clear
|
|
78
|
+
// vote margin on contested cells). Drives the green "GO" signal — we never show
|
|
79
|
+
// green on an early/transient valid read, only on a reliably-detected MRZ.
|
|
80
|
+
const [mrzReliable, setMrzReliable] = useState(false);
|
|
62
81
|
const [nextStep, setNextStep] = useState('SCAN_ID_FRONT_OR_PASSPORT');
|
|
63
82
|
const [_completedStep, setCompletedStep] = useState(null);
|
|
64
83
|
const [detectedDocumentType, setDetectedDocumentType] = useState('UNKNOWN');
|
|
@@ -74,8 +93,11 @@ const IdentityDocumentCamera = ({
|
|
|
74
93
|
// Multi-frame per-character voting: fuses OCR across frames so a single-frame
|
|
75
94
|
// misread (B↔8, S↔5, <↔K, dropped/inserted digit) is outvoted instead of
|
|
76
95
|
// resetting progress. Drives the same `parsedMRZData`/stability the flow uses.
|
|
96
|
+
// stabilityTarget: 3 — the voted consensus must read the SAME valid MRZ across
|
|
97
|
+
// 3 consecutive contributing frames before we accept, so a transient look-alike
|
|
98
|
+
// misread (e.g. OCR-B "4" vs "A") can never lock in on one or two early frames.
|
|
77
99
|
const mrzAggregator = useRef(new MRZFrameAggregator({
|
|
78
|
-
stabilityTarget:
|
|
100
|
+
stabilityTarget: 3
|
|
79
101
|
}));
|
|
80
102
|
|
|
81
103
|
// Document type stability tracking - require consistent detections from good quality frames
|
|
@@ -102,10 +124,28 @@ const IdentityDocumentCamera = ({
|
|
|
102
124
|
const isHologramDetectionInProgress = useRef(false); // Prevent concurrent hologram detection calls
|
|
103
125
|
const isCompletionCallbackInvoked = useRef(false); // Prevent multiple callback invocations when COMPLETED
|
|
104
126
|
const isStepTransitionInProgress = useRef(false); // Prevent duplicate step transitions from rapid frames
|
|
127
|
+
// The step a transition is currently in progress FROM. The latch above is
|
|
128
|
+
// reset asynchronously (via a nextStep effect), so in fast flows (e.g. demo
|
|
129
|
+
// mode auto-advancing) a later transition from a DIFFERENT step could be
|
|
130
|
+
// wrongly blocked while the latch is still set for the previous step. Gating
|
|
131
|
+
// on the originating step lets a genuine next-step transition through while
|
|
132
|
+
// still de-duping rapid repeat frames of the SAME transition.
|
|
133
|
+
const stepTransitionFrom = useRef(null);
|
|
105
134
|
const isCompletionAnnouncementSpoken = useRef(false); // Prevent duplicate "scan completed" voice guidance
|
|
106
135
|
|
|
107
136
|
const faceDetectionErrorCount = useRef(0);
|
|
108
137
|
const brightnessHistory = useRef([]);
|
|
138
|
+
// Running camera-quality stats for the support diagnostic report.
|
|
139
|
+
const diagStats = useRef({
|
|
140
|
+
frames: 0,
|
|
141
|
+
brightnessSum: 0,
|
|
142
|
+
minBrightness: 255,
|
|
143
|
+
maxBrightness: 0,
|
|
144
|
+
frameW: 0,
|
|
145
|
+
frameH: 0,
|
|
146
|
+
hadLowBrightness: false,
|
|
147
|
+
hadBlurryFrames: false
|
|
148
|
+
});
|
|
109
149
|
const [faceDetectionEnabled, setFaceDetectionEnabled] = useState(true);
|
|
110
150
|
const faceImages = useRef([]);
|
|
111
151
|
const hologramImageCountRef = useRef(0);
|
|
@@ -143,6 +183,7 @@ const IdentityDocumentCamera = ({
|
|
|
143
183
|
setIsActive(true);
|
|
144
184
|
isCompletionCallbackInvoked.current = false; // Reset callback flag when starting new scan
|
|
145
185
|
isStepTransitionInProgress.current = false;
|
|
186
|
+
stepTransitionFrom.current = null;
|
|
146
187
|
isCompletionAnnouncementSpoken.current = false;
|
|
147
188
|
} else {
|
|
148
189
|
setIsActive(false);
|
|
@@ -157,12 +198,28 @@ const IdentityDocumentCamera = ({
|
|
|
157
198
|
lastValidMRZFields.current = null;
|
|
158
199
|
validMRZConsecutiveCount.current = 0;
|
|
159
200
|
mrzAggregator.current.reset();
|
|
201
|
+
setMrzReliable(false);
|
|
202
|
+
// Start a fresh diagnostics session for the support report.
|
|
203
|
+
diagnostics.reset(Date.now());
|
|
204
|
+
diagStats.current = {
|
|
205
|
+
frames: 0,
|
|
206
|
+
brightnessSum: 0,
|
|
207
|
+
minBrightness: 255,
|
|
208
|
+
maxBrightness: 0,
|
|
209
|
+
frameW: 0,
|
|
210
|
+
frameH: 0,
|
|
211
|
+
hadLowBrightness: false,
|
|
212
|
+
hadBlurryFrames: false
|
|
213
|
+
};
|
|
160
214
|
cachedBarcode.current = null;
|
|
161
215
|
isCompletionCallbackInvoked.current = false;
|
|
162
216
|
isStepTransitionInProgress.current = false;
|
|
217
|
+
stepTransitionFrom.current = null;
|
|
163
218
|
isCompletionAnnouncementSpoken.current = false;
|
|
164
219
|
// Clear all captured image states from previous scan
|
|
220
|
+
currentFaceImageRef.current = undefined;
|
|
165
221
|
setCurrentFaceImage(undefined);
|
|
222
|
+
currentHologramImageRef.current = undefined;
|
|
166
223
|
setCurrentHologramImage(undefined);
|
|
167
224
|
setCurrentSecondaryFaceImage(undefined);
|
|
168
225
|
resetLastMessage();
|
|
@@ -175,9 +232,12 @@ const IdentityDocumentCamera = ({
|
|
|
175
232
|
setLatestHologramFaceImage(undefined);
|
|
176
233
|
isCompletionCallbackInvoked.current = false; // Reset callback flag on unmount
|
|
177
234
|
isStepTransitionInProgress.current = false;
|
|
235
|
+
stepTransitionFrom.current = null;
|
|
178
236
|
isCompletionAnnouncementSpoken.current = false;
|
|
179
237
|
// Clear all captured image states on unmount
|
|
238
|
+
currentFaceImageRef.current = undefined;
|
|
180
239
|
setCurrentFaceImage(undefined);
|
|
240
|
+
currentHologramImageRef.current = undefined;
|
|
181
241
|
setCurrentHologramImage(undefined);
|
|
182
242
|
setCurrentSecondaryFaceImage(undefined);
|
|
183
243
|
resetLastMessage();
|
|
@@ -223,6 +283,7 @@ const IdentityDocumentCamera = ({
|
|
|
223
283
|
isCompletionCallbackInvoked.current = false;
|
|
224
284
|
}
|
|
225
285
|
isStepTransitionInProgress.current = false;
|
|
286
|
+
stepTransitionFrom.current = null;
|
|
226
287
|
}, [nextStep]);
|
|
227
288
|
|
|
228
289
|
// Update status bar based on guide visibility
|
|
@@ -356,6 +417,7 @@ const IdentityDocumentCamera = ({
|
|
|
356
417
|
setHologramImageCount(0);
|
|
357
418
|
setLatestHologramFaceImage(undefined);
|
|
358
419
|
// Clear previous hologram state to prevent premature completion
|
|
420
|
+
currentHologramImageRef.current = undefined;
|
|
359
421
|
setCurrentHologramImage(undefined);
|
|
360
422
|
setCurrentHologramMaskImage(undefined);
|
|
361
423
|
}
|
|
@@ -386,6 +448,7 @@ const IdentityDocumentCamera = ({
|
|
|
386
448
|
lastValidMRZFields.current = null;
|
|
387
449
|
// New MRZ expected on the back — start voting fresh.
|
|
388
450
|
mrzAggregator.current.reset();
|
|
451
|
+
setMrzReliable(false);
|
|
389
452
|
}
|
|
390
453
|
validMRZConsecutiveCount.current = 0;
|
|
391
454
|
cachedBarcode.current = null; // Clear cached barcode on step change
|
|
@@ -398,10 +461,14 @@ const IdentityDocumentCamera = ({
|
|
|
398
461
|
}
|
|
399
462
|
}, [setIsTorchOn]);
|
|
400
463
|
const transitionStepWithCallback = useCallback((nextStepType, fromStep, scannedData) => {
|
|
401
|
-
|
|
464
|
+
// De-dupe only rapid repeat frames of the SAME originating step. A
|
|
465
|
+
// transition from a different step is a genuine advance (the async latch
|
|
466
|
+
// reset may not have run yet in fast flows like demo mode), so allow it.
|
|
467
|
+
if (isStepTransitionInProgress.current && stepTransitionFrom.current === fromStep) {
|
|
402
468
|
return;
|
|
403
469
|
}
|
|
404
470
|
isStepTransitionInProgress.current = true;
|
|
471
|
+
stepTransitionFrom.current = fromStep;
|
|
405
472
|
|
|
406
473
|
// Torch only needed during SCAN_HOLOGRAM - turn off for all other transitions
|
|
407
474
|
if (nextStepType !== 'SCAN_HOLOGRAM') {
|
|
@@ -557,9 +624,30 @@ const IdentityDocumentCamera = ({
|
|
|
557
624
|
// of letting one noisy frame reset progress.
|
|
558
625
|
const mrzBandHeight = scannedText?.blocks?.length ? Math.max(0, ...scannedText.blocks.map(b => b.blockFrame?.height ?? 0)) : 0;
|
|
559
626
|
const frameWeight = mrzBandHeight > 0 ? mrzBandHeight : 1;
|
|
627
|
+
// ML Kit per-symbol confidence over MRZ characters (Android bundled library
|
|
628
|
+
// only; undefined on iOS). When present it down-weights frames the
|
|
629
|
+
// recognizer was unsure about, so a confident frame outvotes a shaky one of
|
|
630
|
+
// the same band height on look-alike ties (A↔4, S↔5, B↔8).
|
|
631
|
+
const frameConfidence = scannedText?.textConfidence?.mean;
|
|
560
632
|
const consensus = mrzAggregator.current.addFrame({
|
|
561
633
|
text: textForValidation,
|
|
562
|
-
weight: frameWeight
|
|
634
|
+
weight: frameWeight,
|
|
635
|
+
confidence: frameConfidence
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Record MRZ consensus metrics for the support diagnostic report (counts
|
|
639
|
+
// and pass/fail only — never the MRZ values themselves). `reachedStable`
|
|
640
|
+
// is sticky in the collector, so a later flicker can't unset it.
|
|
641
|
+
// `stabilityFrames`/`minContestedMargin` and the MRZ band pixel height are
|
|
642
|
+
// device-variance signals (convergence speed, look-alike resolution
|
|
643
|
+
// confidence, and whether the band cleared the ~16px/char OCR floor).
|
|
644
|
+
diagnostics.setMrz({
|
|
645
|
+
framesProcessed: consensus.frames,
|
|
646
|
+
reachedStable: consensus.stable,
|
|
647
|
+
format: consensus.validation?.format ?? undefined,
|
|
648
|
+
stabilityFrames: consensus.stableStreak,
|
|
649
|
+
minContestedMargin: Number.isFinite(consensus.minContestedMargin) ? consensus.minContestedMargin : 0,
|
|
650
|
+
maxMrzBandHeightPx: mrzBandHeight
|
|
563
651
|
});
|
|
564
652
|
|
|
565
653
|
// Use the fused consensus when it is valid; otherwise fall back to a
|
|
@@ -583,6 +671,9 @@ const IdentityDocumentCamera = ({
|
|
|
583
671
|
// be wrong (ID_FRONT locked before passport MRZ became readable)
|
|
584
672
|
const documentType = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectDocumentType(primaryFaces, text, parsedMRZData?.fields, frameWidth, mrzText) : parsedMRZData?.fields?.documentCode === 'P' ? 'PASSPORT' : detectedDocumentType;
|
|
585
673
|
|
|
674
|
+
// Record the detected document type for the diagnostic report.
|
|
675
|
+
if (documentType) diagnostics.setDocument(documentType);
|
|
676
|
+
|
|
586
677
|
// Crop faces once document type is confirmed or we're past the initial step
|
|
587
678
|
const shouldCropFaces = documentType === 'ID_FRONT' || documentType === 'PASSPORT' || nextStep !== 'SCAN_ID_FRONT_OR_PASSPORT';
|
|
588
679
|
const croppedFaces = shouldCropFaces ? await getFaceImages(primaryFaces, image ?? '', frameWidth, frameHeight) : [];
|
|
@@ -636,6 +727,7 @@ const IdentityDocumentCamera = ({
|
|
|
636
727
|
// Continue scanning without locking this face
|
|
637
728
|
} else {
|
|
638
729
|
faceImageToUse = croppedFaces[0];
|
|
730
|
+
currentFaceImageRef.current = croppedFaces[0];
|
|
639
731
|
setCurrentFaceImage(croppedFaces[0]);
|
|
640
732
|
}
|
|
641
733
|
}
|
|
@@ -650,15 +742,6 @@ const IdentityDocumentCamera = ({
|
|
|
650
742
|
// During SCAN_HOLOGRAM, allow processing even if text is not readable
|
|
651
743
|
}
|
|
652
744
|
|
|
653
|
-
// Capture test mode data
|
|
654
|
-
if (testMode && text && text.includes('<')) {
|
|
655
|
-
const mrzOnlyText = scannedText?.mrzOnlyText || text;
|
|
656
|
-
setTestModeData({
|
|
657
|
-
mrzText: mrzOnlyText,
|
|
658
|
-
timestamp: Date.now()
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
|
|
662
745
|
// MRZ stability: require consistent valid reads across frames
|
|
663
746
|
// Skip during SCAN_HOLOGRAM - document type already locked
|
|
664
747
|
const mrzHasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
@@ -674,12 +757,37 @@ const IdentityDocumentCamera = ({
|
|
|
674
757
|
}
|
|
675
758
|
// else: Invalid/no MRZ - skip frame without resetting (allows temporary OCR noise)
|
|
676
759
|
|
|
677
|
-
//
|
|
678
|
-
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
//
|
|
682
|
-
|
|
760
|
+
// Accept ONLY when the multi-frame voted consensus is stable.
|
|
761
|
+
//
|
|
762
|
+
// Early frames can OCR an ambiguous glyph wrongly yet still produce a
|
|
763
|
+
// check-digit-VALID reading — e.g. a Turkish document number "A53032352"
|
|
764
|
+
// misread as "453032352" (the OCR-B 4 looks like A, and 453032352 also
|
|
765
|
+
// passes its check digit). The old code also accepted on a legacy path of
|
|
766
|
+
// "N identical valid single-frame reads", which those transient frames
|
|
767
|
+
// satisfy — locking in the WRONG number before the voted consensus settles
|
|
768
|
+
// on the correct one. We now require `consensus.stable`: the per-character
|
|
769
|
+
// vote must be unchanged across `stabilityTarget` consecutive contributing
|
|
770
|
+
// frames, which is exactly the "only accept once stable" guarantee.
|
|
771
|
+
// `addFrame` runs for every frame above, so the consensus is always current.
|
|
772
|
+
const mrzStableAndValid = consensus.stable && parsedMRZData?.valid === true;
|
|
773
|
+
|
|
774
|
+
// Surface the reliable-detection signal to the render so the UI can show a
|
|
775
|
+
// green "GO" the moment (and only once) the consensus is stable + valid.
|
|
776
|
+
// Never flips green on an early/transient valid read.
|
|
777
|
+
setMrzReliable(prev => prev === mrzStableAndValid ? prev : mrzStableAndValid);
|
|
778
|
+
|
|
779
|
+
// Test-mode panel: show the MRZ ONLY once it is stable + valid (the same
|
|
780
|
+
// consensus matched across `stabilityTarget` consecutive frames). Showing
|
|
781
|
+
// the raw per-frame OCR here would flash transient, possibly mis-converted
|
|
782
|
+
// readings (e.g. a momentary "4" for "A") before the vote settles — the
|
|
783
|
+
// opposite of what the screen should convey. We display the corrected,
|
|
784
|
+
// check-digit-valid consensus and never an unstable intermediate.
|
|
785
|
+
if (testMode && mrzStableAndValid && mrzText) {
|
|
786
|
+
setTestModeData(prev => prev?.mrzText === mrzText ? prev : {
|
|
787
|
+
mrzText,
|
|
788
|
+
timestamp: Date.now()
|
|
789
|
+
});
|
|
790
|
+
}
|
|
683
791
|
|
|
684
792
|
// ============================================================================
|
|
685
793
|
// SCAN_ID_BACK STEP - Validate MRZ + barcode on back of ID card
|
|
@@ -834,8 +942,8 @@ const IdentityDocumentCamera = ({
|
|
|
834
942
|
// Skip face position validation for hologram — flash toggling causes position jitter
|
|
835
943
|
if (primaryFaceOnly) {
|
|
836
944
|
hologramFramesWithoutFace.current = 0;
|
|
837
|
-
if (
|
|
838
|
-
scannedData.hologramImage =
|
|
945
|
+
if (currentHologramImageRef.current) {
|
|
946
|
+
scannedData.hologramImage = currentHologramImageRef.current;
|
|
839
947
|
} else if (faceImages.current.length < HOLOGRAM_IMAGE_COUNT) {
|
|
840
948
|
// Space out captures for better variation
|
|
841
949
|
const timeSinceLastCapture = Date.now() - lastHologramCaptureTime.current;
|
|
@@ -895,6 +1003,7 @@ const IdentityDocumentCamera = ({
|
|
|
895
1003
|
if (hologram) {
|
|
896
1004
|
setCurrentHologramMaskImage(hologramMask);
|
|
897
1005
|
scannedData.hologramImage = hologram;
|
|
1006
|
+
currentHologramImageRef.current = hologram;
|
|
898
1007
|
setCurrentHologramImage(hologram);
|
|
899
1008
|
if (isDebugEnabled()) {
|
|
900
1009
|
debugLog('IdentityDocumentCamera', '[Hologram] ✓ Saved hologram image');
|
|
@@ -920,8 +1029,8 @@ const IdentityDocumentCamera = ({
|
|
|
920
1029
|
} else {
|
|
921
1030
|
hologramFramesWithoutFace.current++;
|
|
922
1031
|
}
|
|
923
|
-
} else if (
|
|
924
|
-
scannedData.hologramImage =
|
|
1032
|
+
} else if (currentHologramImageRef.current) {
|
|
1033
|
+
scannedData.hologramImage = currentHologramImageRef.current;
|
|
925
1034
|
}
|
|
926
1035
|
|
|
927
1036
|
// Secondary face capture (continuous during initial scan and hologram detection)
|
|
@@ -1055,10 +1164,20 @@ const IdentityDocumentCamera = ({
|
|
|
1055
1164
|
return;
|
|
1056
1165
|
}
|
|
1057
1166
|
|
|
1058
|
-
// Complete when hologram captured OR retries exhausted (not mid-collection)
|
|
1059
|
-
|
|
1167
|
+
// Complete when hologram captured OR retries exhausted (not mid-collection).
|
|
1168
|
+
// Fall back to the previously locked face (ref) when the live frame's face
|
|
1169
|
+
// was rejected — the torch is on during hologram capture, so faceImageToUse
|
|
1170
|
+
// is frequently null even though a valid face was locked during the front
|
|
1171
|
+
// scan. Without this the gate's face requirement could never be satisfied.
|
|
1172
|
+
const faceForCompletion = faceImageToUse ?? currentFaceImageRef.current;
|
|
1173
|
+
const stepComplete = (!!scannedData.hologramImage || maxRetriesReached && !isCollecting) && !!faceForCompletion; // Require face before completing
|
|
1060
1174
|
|
|
1061
1175
|
if (stepComplete) {
|
|
1176
|
+
// Make sure the locked face rides along even if this frame's face was
|
|
1177
|
+
// rejected.
|
|
1178
|
+
if (!scannedData.faceImage && faceForCompletion) {
|
|
1179
|
+
scannedData.faceImage = faceForCompletion;
|
|
1180
|
+
}
|
|
1062
1181
|
// Ensure preserved MRZ data is included (current frame may not have
|
|
1063
1182
|
// readable MRZ due to flash/tilting during hologram capture)
|
|
1064
1183
|
if (!scannedData.mrzText && lastValidMRZText.current) {
|
|
@@ -1170,6 +1289,15 @@ const IdentityDocumentCamera = ({
|
|
|
1170
1289
|
brightnessHistory.current.shift();
|
|
1171
1290
|
}
|
|
1172
1291
|
const avgBrightness = brightnessHistory.current.reduce((a, b) => a + b, 0) / brightnessHistory.current.length;
|
|
1292
|
+
|
|
1293
|
+
// Accumulate camera diagnostics (frame count, brightness range, dims) for
|
|
1294
|
+
// the support diagnostic report. Cheap, side-effect-only.
|
|
1295
|
+
diagStats.current.frames += 1;
|
|
1296
|
+
diagStats.current.brightnessSum += frameBrightness;
|
|
1297
|
+
diagStats.current.minBrightness = Math.min(diagStats.current.minBrightness, frameBrightness);
|
|
1298
|
+
diagStats.current.maxBrightness = Math.max(diagStats.current.maxBrightness, frameBrightness);
|
|
1299
|
+
diagStats.current.frameW = frame.width ?? diagStats.current.frameW;
|
|
1300
|
+
diagStats.current.frameH = frame.height ?? diagStats.current.frameH;
|
|
1173
1301
|
const isOverallBright = avgBrightness >= MIN_BRIGHTNESS_THRESHOLD;
|
|
1174
1302
|
|
|
1175
1303
|
// Check brightness in center region (area of interest)
|
|
@@ -1180,6 +1308,7 @@ const IdentityDocumentCamera = ({
|
|
|
1180
1308
|
isRegionBright = isOverallBright;
|
|
1181
1309
|
}
|
|
1182
1310
|
setIsBrightnessLow(!isRegionBright);
|
|
1311
|
+
if (!isRegionBright) diagStats.current.hadLowBrightness = true;
|
|
1183
1312
|
|
|
1184
1313
|
// Check blur only in center region (area of interest) to avoid false positives
|
|
1185
1314
|
// from iOS depth-of-field background blur
|
|
@@ -1200,10 +1329,30 @@ const IdentityDocumentCamera = ({
|
|
|
1200
1329
|
);
|
|
1201
1330
|
isNotBlurry = !isBlurry;
|
|
1202
1331
|
setIsFrameBlurry(isBlurry);
|
|
1332
|
+
if (isBlurry) diagStats.current.hadBlurryFrames = true;
|
|
1203
1333
|
} catch (error) {
|
|
1204
1334
|
setIsFrameBlurry(false);
|
|
1205
1335
|
}
|
|
1206
1336
|
|
|
1337
|
+
// Flush accumulated camera stats into the diagnostics collector.
|
|
1338
|
+
const ds = diagStats.current;
|
|
1339
|
+
diagnostics.setCamera({
|
|
1340
|
+
frames: ds.frames,
|
|
1341
|
+
frameW: ds.frameW,
|
|
1342
|
+
frameH: ds.frameH,
|
|
1343
|
+
avgBrightness: ds.frames > 0 ? Math.round(ds.brightnessSum / ds.frames) : 0,
|
|
1344
|
+
minBrightness: ds.minBrightness === 255 && ds.frames === 0 ? 0 : ds.minBrightness,
|
|
1345
|
+
maxBrightness: ds.maxBrightness,
|
|
1346
|
+
hadLowBrightness: ds.hadLowBrightness,
|
|
1347
|
+
hadBlurryFrames: ds.hadBlurryFrames,
|
|
1348
|
+
// The actual gates the measured values are judged against — lets support
|
|
1349
|
+
// read "measured 45 vs floor 45" instead of just a pass/fail flag.
|
|
1350
|
+
thresholds: {
|
|
1351
|
+
minBrightness: MIN_BRIGHTNESS_THRESHOLD,
|
|
1352
|
+
blur: 50
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1207
1356
|
// Only proceed if image quality is acceptable
|
|
1208
1357
|
const hasAcceptableQuality = isRegionBright && isNotBlurry;
|
|
1209
1358
|
|
|
@@ -1248,6 +1397,7 @@ const IdentityDocumentCamera = ({
|
|
|
1248
1397
|
resultText,
|
|
1249
1398
|
mrzOnlyText: undefined,
|
|
1250
1399
|
// Will be set below if MRZ blocks detected
|
|
1400
|
+
textConfidence: frame.textConfidence,
|
|
1251
1401
|
blocks: textBlocks.map(block => ({
|
|
1252
1402
|
blockText: block.text || '',
|
|
1253
1403
|
blockFrame: block.blockFrame ?? {
|
|
@@ -1635,8 +1785,10 @@ const IdentityDocumentCamera = ({
|
|
|
1635
1785
|
}), /*#__PURE__*/_jsx(AnimatedText, {
|
|
1636
1786
|
style: [styles.topZoneText,
|
|
1637
1787
|
// Priority order for coloring (later styles override earlier ones)
|
|
1638
|
-
// 1. Success (green) - scan completed
|
|
1639
|
-
|
|
1788
|
+
// 1. Success/GO (green) - scan completed OR MRZ reliably detected
|
|
1789
|
+
// (stable consensus). This is the "GO" signal; never shown on a
|
|
1790
|
+
// transient/early valid read.
|
|
1791
|
+
(nextStep === 'COMPLETED' || mrzReliable) && styles.topZoneTextSuccess,
|
|
1640
1792
|
// 2. Error (red) - wrong side - with flash opacity
|
|
1641
1793
|
status === 'INCORRECT' && styles.topZoneTextError, status === 'INCORRECT' && {
|
|
1642
1794
|
opacity: errorFlashAnim
|
|
@@ -1647,7 +1799,7 @@ const IdentityDocumentCamera = ({
|
|
|
1647
1799
|
status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0 && !isBrightnessLow && !isFrameBlurry && styles.topZoneTextScanning
|
|
1648
1800
|
// 5. Default (white) - aligning (not all detected OR elements outside scan area)
|
|
1649
1801
|
],
|
|
1650
|
-
children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t)
|
|
1802
|
+
children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t, mrzReliable)
|
|
1651
1803
|
})]
|
|
1652
1804
|
}), /*#__PURE__*/_jsx(View, {
|
|
1653
1805
|
style: styles.leftZone
|
|
@@ -1657,8 +1809,11 @@ const IdentityDocumentCamera = ({
|
|
|
1657
1809
|
style: styles.bottomZone
|
|
1658
1810
|
}), /*#__PURE__*/_jsx(View, {
|
|
1659
1811
|
style: [styles.scanArea, {
|
|
1660
|
-
borderColor:
|
|
1661
|
-
|
|
1812
|
+
borderColor:
|
|
1813
|
+
// Green once the scan is done OR the MRZ is reliably detected
|
|
1814
|
+
// (stable consensus) — the "GO" signal. Never green before that.
|
|
1815
|
+
nextStep === 'COMPLETED' || mrzReliable ? '#4CAF50' : status === 'INCORRECT' ? '#f44336' : status === 'SCANNING' ? '#2196F3' : isBrightnessLow || isFrameBlurry ? '#FFC107' : 'white',
|
|
1816
|
+
borderWidth: mrzReliable || status === 'SCANNING' ? 3 : 2
|
|
1662
1817
|
}],
|
|
1663
1818
|
children: nextStep === 'COMPLETED' ? /*#__PURE__*/_jsx(LottieView, {
|
|
1664
1819
|
source: require('../../Shared/Animations/success.json'),
|
|
@@ -78,10 +78,17 @@ export function transformBoundsToScreen(bounds, scale, offsetX, offsetY) {
|
|
|
78
78
|
* Unified status message logic used by both voice guidance and render text.
|
|
79
79
|
* Returns the appropriate i18n key arguments for the current scan state.
|
|
80
80
|
*/
|
|
81
|
-
export function getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t) {
|
|
81
|
+
export function getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t, mrzReliable = false) {
|
|
82
82
|
if (nextStep === 'COMPLETED') {
|
|
83
83
|
return t('identityDocumentCamera.scanCompleted');
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
// MRZ reliably detected (stable consensus) — the green "GO" message. Ranks
|
|
87
|
+
// above the in-progress states so the user gets a clear positive signal the
|
|
88
|
+
// moment the reading has settled, but never on an early/transient valid read.
|
|
89
|
+
if (mrzReliable) {
|
|
90
|
+
return t('identityDocumentCamera.mrzDetected');
|
|
91
|
+
}
|
|
85
92
|
if (status === 'INCORRECT') {
|
|
86
93
|
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
87
94
|
return t('identityDocumentCamera.alignIDFront');
|
|
@@ -5,7 +5,6 @@ import AppContext from "../Contexts/AppContext.js";
|
|
|
5
5
|
import { CommonActions, useNavigation, usePreventRemove } from '@react-navigation/native';
|
|
6
6
|
import { View, StyleSheet, Alert } from 'react-native';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
9
8
|
import i18n from "../../Translation/index.js";
|
|
10
9
|
import StyledButton from "./StyledButton.js";
|
|
11
10
|
import { analyticsService } from "../Services/AnalyticsService.js";
|
|
@@ -26,7 +25,6 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
|
|
|
26
25
|
const {
|
|
27
26
|
t
|
|
28
27
|
} = useTranslation();
|
|
29
|
-
const insets = useSafeAreaInsets();
|
|
30
28
|
const routes = {
|
|
31
29
|
VERIFICATION_SESSION_CHECK: 'VerificationSessionCheckScreen',
|
|
32
30
|
DYNAMIC_ROUTES: {
|
|
@@ -185,9 +183,7 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
|
|
|
185
183
|
allowSkipStep: appContext.currentWorkflowStep?.required
|
|
186
184
|
}));
|
|
187
185
|
return appContext.currentWorkflowStep && (!appContext.currentWorkflowStep?.required || canSkipStep) && /*#__PURE__*/_jsx(View, {
|
|
188
|
-
style:
|
|
189
|
-
paddingBottom: insets.bottom
|
|
190
|
-
}],
|
|
186
|
+
style: styles.container,
|
|
191
187
|
children: /*#__PURE__*/_jsx(StyledButton, {
|
|
192
188
|
mode: "text",
|
|
193
189
|
onPress: goToNextRouteWithAlert,
|
|
@@ -196,11 +192,13 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
|
|
|
196
192
|
});
|
|
197
193
|
});
|
|
198
194
|
const styles = StyleSheet.create({
|
|
195
|
+
// Hug the text button: centered, minimal padding, no full-width opaque bar so
|
|
196
|
+
// it occupies the least area and never overlays sibling content. The screen's
|
|
197
|
+
// footer wrapper already applies the bottom safe-area inset.
|
|
199
198
|
container: {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
backgroundColor: 'white'
|
|
199
|
+
alignSelf: 'center',
|
|
200
|
+
paddingHorizontal: 16,
|
|
201
|
+
paddingVertical: 8
|
|
204
202
|
}
|
|
205
203
|
});
|
|
206
204
|
export default NavigationManager;
|