@trustchex/react-native-sdk 1.409.0 → 1.464.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 (156) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKModule.kt +2 -8
  2. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +59 -1
  3. package/ios/Camera/TrustchexCameraView.swift +9 -1
  4. package/lib/module/Screens/Debug/NFCScanTestScreen.js +635 -0
  5. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +1 -4
  6. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +17 -4
  7. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +102 -23
  8. package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1079 -0
  9. package/lib/module/Screens/Dynamic/VideoCallScreen.js +3 -1
  10. package/lib/module/Screens/Static/ResultScreen.js +128 -22
  11. package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +8 -0
  12. package/lib/module/Shared/Animations/recording.json +1 -0
  13. package/lib/module/Shared/Components/DebugNavigationPanel.js +69 -71
  14. package/lib/module/Shared/Components/EIDScanner.js +212 -108
  15. package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +5 -3
  16. package/lib/module/Shared/Components/IdentityDocumentCamera.js +53 -36
  17. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +13 -4
  18. package/lib/module/Shared/Components/NavigationManager.js +24 -16
  19. package/lib/module/Shared/EIDReader/aesSecureMessagingWrapper.js +51 -0
  20. package/lib/module/Shared/EIDReader/apduLevelPACECapable.js +3 -0
  21. package/lib/module/Shared/EIDReader/bacKey.js +16 -2
  22. package/lib/module/Shared/EIDReader/eidReader.js +354 -13
  23. package/lib/module/Shared/EIDReader/eidService.js +25 -1
  24. package/lib/module/Shared/EIDReader/nfcManagerCardService.js +4 -7
  25. package/lib/module/Shared/EIDReader/paceInfo.js +85 -0
  26. package/lib/module/Shared/EIDReader/paceKeySpec.js +51 -0
  27. package/lib/module/Shared/EIDReader/protocol/paceAPDUSender.js +100 -0
  28. package/lib/module/Shared/EIDReader/protocol/paceProtocol.js +655 -0
  29. package/lib/module/Shared/EIDReader/protocol/paceResult.js +37 -0
  30. package/lib/module/Shared/EIDReader/secureMessagingWrapper.js +27 -4
  31. package/lib/module/Shared/EIDReader/smartcards/commandAPDU.js +2 -1
  32. package/lib/module/Shared/EIDReader/tlv/tlv.helpers.js +1 -1
  33. package/lib/module/Shared/EIDReader/tlv/tlv.utils.js +6 -3
  34. package/lib/module/Shared/EIDReader/utils/aesCrypto.utils.js +189 -0
  35. package/lib/module/Shared/Libs/analytics.utils.js +4 -0
  36. package/lib/module/Shared/Libs/contains.js +1 -40
  37. package/lib/module/Shared/Libs/country-display.utils.js +34 -0
  38. package/lib/module/Shared/Libs/demo.utils.js +8 -0
  39. package/lib/module/Shared/Libs/mrz.utils.js +3 -2
  40. package/lib/module/Shared/Libs/status-bar.utils.js +4 -2
  41. package/lib/module/Shared/Types/analytics.types.js +2 -0
  42. package/lib/module/Translation/Resources/en.js +41 -2
  43. package/lib/module/Translation/Resources/tr.js +41 -2
  44. package/lib/module/Trustchex.js +54 -20
  45. package/lib/module/version.js +1 -1
  46. package/lib/typescript/src/Screens/Debug/NFCScanTestScreen.d.ts +3 -0
  47. package/lib/typescript/src/Screens/Debug/NFCScanTestScreen.d.ts.map +1 -0
  48. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  49. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  50. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  51. package/lib/typescript/src/Screens/Dynamic/VerbalConsentScreen.d.ts +3 -0
  52. package/lib/typescript/src/Screens/Dynamic/VerbalConsentScreen.d.ts.map +1 -0
  53. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
  54. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  55. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  56. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
  57. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  58. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +1 -1
  60. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +5 -0
  62. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  64. package/lib/typescript/src/Shared/EIDReader/aesSecureMessagingWrapper.d.ts +18 -0
  65. package/lib/typescript/src/Shared/EIDReader/aesSecureMessagingWrapper.d.ts.map +1 -0
  66. package/lib/typescript/src/Shared/EIDReader/apduLevelPACECapable.d.ts +23 -0
  67. package/lib/typescript/src/Shared/EIDReader/apduLevelPACECapable.d.ts.map +1 -0
  68. package/lib/typescript/src/Shared/EIDReader/bacKey.d.ts +6 -0
  69. package/lib/typescript/src/Shared/EIDReader/bacKey.d.ts.map +1 -1
  70. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  71. package/lib/typescript/src/Shared/EIDReader/eidService.d.ts +9 -0
  72. package/lib/typescript/src/Shared/EIDReader/eidService.d.ts.map +1 -1
  73. package/lib/typescript/src/Shared/EIDReader/nfcManagerCardService.d.ts.map +1 -1
  74. package/lib/typescript/src/Shared/EIDReader/paceInfo.d.ts +50 -0
  75. package/lib/typescript/src/Shared/EIDReader/paceInfo.d.ts.map +1 -0
  76. package/lib/typescript/src/Shared/EIDReader/paceKeySpec.d.ts +30 -0
  77. package/lib/typescript/src/Shared/EIDReader/paceKeySpec.d.ts.map +1 -0
  78. package/lib/typescript/src/Shared/EIDReader/protocol/paceAPDUSender.d.ts +17 -0
  79. package/lib/typescript/src/Shared/EIDReader/protocol/paceAPDUSender.d.ts.map +1 -0
  80. package/lib/typescript/src/Shared/EIDReader/protocol/paceProtocol.d.ts +105 -0
  81. package/lib/typescript/src/Shared/EIDReader/protocol/paceProtocol.d.ts.map +1 -0
  82. package/lib/typescript/src/Shared/EIDReader/protocol/paceResult.d.ts +24 -0
  83. package/lib/typescript/src/Shared/EIDReader/protocol/paceResult.d.ts.map +1 -0
  84. package/lib/typescript/src/Shared/EIDReader/secureMessagingWrapper.d.ts +15 -0
  85. package/lib/typescript/src/Shared/EIDReader/secureMessagingWrapper.d.ts.map +1 -1
  86. package/lib/typescript/src/Shared/EIDReader/smartcards/commandAPDU.d.ts.map +1 -1
  87. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts.map +1 -1
  88. package/lib/typescript/src/Shared/EIDReader/utils/aesCrypto.utils.d.ts +39 -0
  89. package/lib/typescript/src/Shared/EIDReader/utils/aesCrypto.utils.d.ts.map +1 -0
  90. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
  91. package/lib/typescript/src/Shared/Libs/contains.d.ts +0 -7
  92. package/lib/typescript/src/Shared/Libs/contains.d.ts.map +1 -1
  93. package/lib/typescript/src/Shared/Libs/country-display.utils.d.ts +2 -0
  94. package/lib/typescript/src/Shared/Libs/country-display.utils.d.ts.map +1 -0
  95. package/lib/typescript/src/Shared/Libs/demo.utils.d.ts.map +1 -1
  96. package/lib/typescript/src/Shared/Libs/http-client.d.ts +1 -1
  97. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  98. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  99. package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts.map +1 -1
  100. package/lib/typescript/src/Shared/Types/analytics.types.d.ts +2 -0
  101. package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -1
  102. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +10 -1
  103. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  104. package/lib/typescript/src/Translation/Resources/en.d.ts +40 -1
  105. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  106. package/lib/typescript/src/Translation/Resources/tr.d.ts +40 -1
  107. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  108. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  109. package/lib/typescript/src/version.d.ts +1 -1
  110. package/package.json +7 -4
  111. package/src/Screens/Debug/NFCScanTestScreen.tsx +692 -0
  112. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +1 -4
  113. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +21 -4
  114. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +124 -23
  115. package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1401 -0
  116. package/src/Screens/Dynamic/VideoCallScreen.tsx +3 -1
  117. package/src/Screens/Static/ResultScreen.tsx +183 -31
  118. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +9 -0
  119. package/src/Shared/Animations/recording.json +1 -0
  120. package/src/Shared/Components/DebugNavigationPanel.tsx +73 -48
  121. package/src/Shared/Components/EIDScanner.tsx +222 -111
  122. package/src/Shared/Components/IdentityDocumentCamera.flows.ts +7 -4
  123. package/src/Shared/Components/IdentityDocumentCamera.tsx +199 -184
  124. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +13 -4
  125. package/src/Shared/Components/NavigationManager.tsx +27 -18
  126. package/src/Shared/EIDReader/aesSecureMessagingWrapper.ts +69 -0
  127. package/src/Shared/EIDReader/apduLevelPACECapable.ts +34 -0
  128. package/src/Shared/EIDReader/bacKey.ts +24 -8
  129. package/src/Shared/EIDReader/eidReader.ts +398 -12
  130. package/src/Shared/EIDReader/eidService.ts +49 -1
  131. package/src/Shared/EIDReader/nfcManagerCardService.ts +4 -6
  132. package/src/Shared/EIDReader/paceInfo.ts +159 -0
  133. package/src/Shared/EIDReader/paceKeySpec.ts +56 -0
  134. package/src/Shared/EIDReader/protocol/paceAPDUSender.ts +163 -0
  135. package/src/Shared/EIDReader/protocol/paceProtocol.ts +946 -0
  136. package/src/Shared/EIDReader/protocol/paceResult.ts +62 -0
  137. package/src/Shared/EIDReader/secureMessagingWrapper.ts +28 -10
  138. package/src/Shared/EIDReader/smartcards/commandAPDU.ts +2 -1
  139. package/src/Shared/EIDReader/tlv/tlv.helpers.ts +1 -1
  140. package/src/Shared/EIDReader/tlv/tlv.utils.ts +8 -5
  141. package/src/Shared/EIDReader/utils/aesCrypto.utils.ts +217 -0
  142. package/src/Shared/Libs/analytics.utils.ts +4 -0
  143. package/src/Shared/Libs/contains.ts +0 -53
  144. package/src/Shared/Libs/country-display.utils.ts +55 -0
  145. package/src/Shared/Libs/crypto.utils.ts +2 -2
  146. package/src/Shared/Libs/demo.utils.ts +10 -0
  147. package/src/Shared/Libs/http-client.ts +12 -4
  148. package/src/Shared/Libs/mrz.utils.ts +3 -2
  149. package/src/Shared/Libs/status-bar.utils.ts +4 -2
  150. package/src/Shared/Services/VideoSessionService.ts +1 -1
  151. package/src/Shared/Types/analytics.types.ts +2 -0
  152. package/src/Shared/Types/identificationInfo.ts +11 -0
  153. package/src/Translation/Resources/en.ts +63 -3
  154. package/src/Translation/Resources/tr.ts +62 -3
  155. package/src/Trustchex.tsx +53 -17
  156. package/src/version.ts +1 -1
@@ -0,0 +1,1079 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Animated, Easing, Platform, SafeAreaView, StatusBar, StyleSheet, Text, useWindowDimensions, View } from 'react-native';
5
+ import LottieView from 'lottie-react-native';
6
+ import RNFS from 'react-native-fs';
7
+ import Video from 'react-native-video';
8
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
9
+ import { useTranslation } from 'react-i18next';
10
+ import FaceCamera from "../../Shared/Components/FaceCamera.js";
11
+ import NativeCircularProgress from "../../Shared/Components/NativeCircularProgress.js";
12
+ import StyledButton from "../../Shared/Components/StyledButton.js";
13
+ import NavigationManager from "../../Shared/Components/NavigationManager.js";
14
+ import AppContext from "../../Shared/Contexts/AppContext.js";
15
+ import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
16
+ import { trackError, trackVerificationComplete, trackVerificationStart, useScreenTracking } from "../../Shared/Libs/analytics.utils.js";
17
+ import { useKeepAwake } from "../../Shared/Libs/native-keep-awake.utils.js";
18
+ import { speak, stopSpeaking } from "../../Shared/Libs/tts.utils.js";
19
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
20
+ const PREVIEW_EDGE_OFFSET = 10;
21
+ const PREVIEW_SIZE_RATIO = 0.95;
22
+ const TITLE_ROW_MIN_HEIGHT = 52;
23
+ const INSTRUCTION_ROW_MIN_HEIGHT = 80;
24
+ const ACTIONS_RESERVED_HEIGHT_IDLE = 72;
25
+ const ALIGNMENT_CONFIRM_FRAMES = 3;
26
+ const ALIGNMENT_LOST_FRAMES = 4;
27
+ const CONSENT_LINE_HEIGHT = 44;
28
+ const CONSENT_CIRCLE_INSET = 2;
29
+ const AUTO_SCROLL_MIN_INTERVAL_MS = 2200;
30
+ const HORIZONTAL_SCROLL_START_DELAY_MS = 5000;
31
+ const AUTO_STOP_SPEECH_BUFFER_MS = 1000;
32
+ const TARGET_WORDS_PER_SECOND = 1;
33
+ // Constant scroll speed (pixels per second) so rate is identical regardless of text length.
34
+ const SCROLL_PIXELS_PER_SECOND = 80;
35
+ const VerbalConsentScreen = () => {
36
+ useKeepAwake();
37
+ useScreenTracking('verbal_consent_video');
38
+ useStatusBarWhiteBackground();
39
+ const appContext = useContext(AppContext);
40
+ const insets = useSafeAreaInsets();
41
+ const {
42
+ width: screenWidth
43
+ } = useWindowDimensions();
44
+ const {
45
+ t
46
+ } = useTranslation();
47
+ const cameraRef = useRef(null);
48
+ const navigationManagerRef = useRef(null);
49
+ const [hasGuideShown, setHasGuideShown] = useState(false);
50
+ const [isRecording, setIsRecording] = useState(false);
51
+ const [recordingPath, setRecordingPath] = useState('');
52
+ const [recordedSeconds, setRecordedSeconds] = useState(0);
53
+ const [isFaceInsideCircle, setIsFaceInsideCircle] = useState(false);
54
+ const [multipleFacesDetected, setMultipleFacesDetected] = useState(false);
55
+ const [cameraLayout, setCameraLayout] = useState({
56
+ width: 0,
57
+ height: 0
58
+ });
59
+ const [currentLineIndex, setCurrentLineIndex] = useState(0);
60
+ const [isStoppingRecording, setIsStoppingRecording] = useState(false);
61
+ const [activeLineViewportWidth, setActiveLineViewportWidth] = useState(0);
62
+ const [measuredTextWidth, setMeasuredTextWidth] = useState(0);
63
+ const [hasTextScrollStarted, setHasTextScrollStarted] = useState(false);
64
+ const [isAutoScrollPausedByTouch, setIsAutoScrollPausedByTouch] = useState(false);
65
+ const autoStartTriggeredRef = useRef(false);
66
+ const intervalRef = useRef(null);
67
+ const activeLineTranslateX = useRef(new Animated.Value(0)).current;
68
+ const recordingDotOpacity = useRef(new Animated.Value(1)).current;
69
+ const activeLineTranslateXRef = useRef(0);
70
+ const [scrollProgressPercent, setScrollProgressPercent] = useState(0);
71
+ const autoStoppingRef = useRef(false);
72
+ const stopReasonRef = useRef(null);
73
+ const stopInProgressRef = useRef(false);
74
+ const autoStopTimeoutRef = useRef(null);
75
+ const isFaceInsideCircleRef = useRef(false);
76
+ const insideFrameStreakRef = useRef(0);
77
+ const outsideFrameStreakRef = useRef(0);
78
+ const scrollNeedsInitRef = useRef(true);
79
+ const initialDelayAppliedRef = useRef(false);
80
+ const workflowConsentText = appContext.currentWorkflowStep?.data?.verbalConsentText?.trim() || '';
81
+ const workflowConsentTitle = appContext.currentWorkflowStep?.data?.verbalConsentTitle?.trim() || '';
82
+ const consentText = workflowConsentText || t('verbalConsentVideoScreen.defaultConsentText');
83
+ const consentTitle = workflowConsentTitle || t('verbalConsentVideoScreen.title');
84
+ const normalizedConsentText = consentText.replace(/\s+/g, ' ').trim();
85
+
86
+ // Keep the full consent text as a single horizontally scrolling line.
87
+ const consentLines = useMemo(() => {
88
+ return normalizedConsentText ? [normalizedConsentText] : [];
89
+ }, [normalizedConsentText]);
90
+ const consentWords = useMemo(() => normalizedConsentText.split(' ').filter(Boolean), [normalizedConsentText]);
91
+ const consentWordCount = Math.max(1, consentWords.length);
92
+ // Conservative read pace to avoid suggesting stop too early.
93
+ const estimatedReadSeconds = Math.max(6, Math.ceil(consentWordCount / TARGET_WORDS_PER_SECOND));
94
+ const autoScrollIntervalMs = Math.max(AUTO_SCROLL_MIN_INTERVAL_MS, Math.round(estimatedReadSeconds * 1000 / Math.max(1, consentLines.length)));
95
+ const currentLineText = consentLines[currentLineIndex] || '';
96
+ // Use measured width as soon as available to keep visual text end and
97
+ // progress completion tightly aligned.
98
+ const estimatedLineContentWidth = measuredTextWidth > 0 ? measuredTextWidth : currentLineText.length * 24;
99
+ const isConsentComplete = scrollProgressPercent >= 99;
100
+ const currentInstructionKey = (() => {
101
+ if (multipleFacesDetected) {
102
+ return 'livenessDetectionScreen.multipleFacesDetected';
103
+ }
104
+ if (!isFaceInsideCircle) {
105
+ return 'livenessDetectionScreen.placeFaceInsideCircle';
106
+ }
107
+ return 'verbalConsentVideoScreen.readNowPrompt';
108
+ })();
109
+
110
+ // Circle stage uses relative layout: title -> circle -> instruction.
111
+ const containerWidth = cameraLayout.width || screenWidth;
112
+ const previewSize = containerWidth * PREVIEW_SIZE_RATIO;
113
+ const titleTopPadding = Math.max(insets.top, 8);
114
+ const previewRect = {
115
+ minX: (containerWidth - previewSize) / 2,
116
+ minY: 0,
117
+ width: previewSize,
118
+ height: previewSize
119
+ };
120
+ const circleStrokeColor = 'rgba(255, 255, 255, 0.5)';
121
+ const circleDiameter = previewRect.width - CONSENT_CIRCLE_INSET * 2;
122
+ const circleLeft = previewRect.minX + CONSENT_CIRCLE_INSET;
123
+ useEffect(() => {
124
+ if (hasGuideShown) {
125
+ // Camera view uses dark background, keep status text readable.
126
+ StatusBar.setBarStyle('light-content', true);
127
+ } else {
128
+ // Guide screen uses white background.
129
+ StatusBar.setBarStyle('dark-content', true);
130
+ if (Platform.OS === 'android') {
131
+ StatusBar.setBackgroundColor('#ffffff', true);
132
+ }
133
+ }
134
+ }, [hasGuideShown]);
135
+ useEffect(() => {
136
+ if (isRecording) {
137
+ const pulse = Animated.loop(Animated.sequence([Animated.timing(recordingDotOpacity, {
138
+ toValue: 0.2,
139
+ duration: 600,
140
+ easing: Easing.inOut(Easing.ease),
141
+ useNativeDriver: true
142
+ }), Animated.timing(recordingDotOpacity, {
143
+ toValue: 1,
144
+ duration: 600,
145
+ easing: Easing.inOut(Easing.ease),
146
+ useNativeDriver: true
147
+ })]));
148
+ pulse.start();
149
+ return () => pulse.stop();
150
+ } else {
151
+ recordingDotOpacity.setValue(1);
152
+ }
153
+ }, [isRecording, recordingDotOpacity]);
154
+ const clearTimer = useCallback(() => {
155
+ if (intervalRef.current) {
156
+ clearInterval(intervalRef.current);
157
+ intervalRef.current = null;
158
+ }
159
+ }, []);
160
+ const clearAutoStopTimer = useCallback(() => {
161
+ if (autoStopTimeoutRef.current) {
162
+ clearTimeout(autoStopTimeoutRef.current);
163
+ autoStopTimeoutRef.current = null;
164
+ }
165
+ }, []);
166
+ const handleStartRecording = useCallback(() => {
167
+ if (!cameraRef.current || isRecording || !isFaceInsideCircle || multipleFacesDetected) {
168
+ return;
169
+ }
170
+ setRecordingPath('');
171
+ setRecordedSeconds(0);
172
+ setCurrentLineIndex(0);
173
+ setIsAutoScrollPausedByTouch(false);
174
+ setHasTextScrollStarted(false);
175
+ setScrollProgressPercent(0);
176
+ clearAutoStopTimer();
177
+ stopReasonRef.current = null;
178
+ stopInProgressRef.current = false;
179
+ scrollNeedsInitRef.current = true;
180
+ initialDelayAppliedRef.current = false;
181
+ activeLineTranslateX.stopAnimation();
182
+ activeLineTranslateX.setValue(0);
183
+ activeLineTranslateXRef.current = 0;
184
+ trackVerificationStart('VERBAL_CONSENT');
185
+ cameraRef.current.startRecording({
186
+ onRecordingFinished: video => {
187
+ stopInProgressRef.current = false;
188
+ setIsStoppingRecording(false);
189
+ const stopReason = stopReasonRef.current;
190
+ stopReasonRef.current = null;
191
+ if (stopReason === 'manual') {
192
+ setRecordingPath(video.path);
193
+ } else {
194
+ // Recording was discarded - reset state for retry
195
+ setRecordingPath('');
196
+ setRecordedSeconds(0);
197
+ setCurrentLineIndex(0);
198
+ setHasTextScrollStarted(false);
199
+ setScrollProgressPercent(0);
200
+ scrollNeedsInitRef.current = true;
201
+ initialDelayAppliedRef.current = false;
202
+ autoStartTriggeredRef.current = false;
203
+ if (video.path) {
204
+ RNFS.unlink(video.path).catch(() => {
205
+ // Best-effort cleanup for canceled recordings.
206
+ });
207
+ }
208
+ }
209
+ setIsRecording(false);
210
+ clearTimer();
211
+ },
212
+ onRecordingError: error => {
213
+ stopInProgressRef.current = false;
214
+ stopReasonRef.current = null;
215
+ setIsRecording(false);
216
+ setIsStoppingRecording(false);
217
+ setCurrentLineIndex(0);
218
+ setHasTextScrollStarted(false);
219
+ autoStartTriggeredRef.current = false;
220
+ clearTimer();
221
+ trackError('VERBAL_CONSENT_RECORDING_ERROR', error.error || t('verbalConsentVideoScreen.analytics.recordingFailed'), 'verbal_consent_video', 'medium', {
222
+ recoverable: true
223
+ });
224
+ }
225
+ });
226
+ setIsRecording(true);
227
+ clearTimer();
228
+ intervalRef.current = setInterval(() => {
229
+ setRecordedSeconds(prev => prev + 1);
230
+ }, 1000);
231
+ }, [isRecording, isFaceInsideCircle, multipleFacesDetected, clearAutoStopTimer, activeLineTranslateX, clearTimer, t]);
232
+ const updateStableAlignment = useCallback(isInsideCurrentFrame => {
233
+ if (isInsideCurrentFrame) {
234
+ insideFrameStreakRef.current += 1;
235
+ outsideFrameStreakRef.current = 0;
236
+ if (!isFaceInsideCircleRef.current && insideFrameStreakRef.current >= ALIGNMENT_CONFIRM_FRAMES) {
237
+ isFaceInsideCircleRef.current = true;
238
+ setIsFaceInsideCircle(true);
239
+ }
240
+ return;
241
+ }
242
+ outsideFrameStreakRef.current += 1;
243
+ insideFrameStreakRef.current = 0;
244
+ if (isFaceInsideCircleRef.current && outsideFrameStreakRef.current >= ALIGNMENT_LOST_FRAMES) {
245
+ isFaceInsideCircleRef.current = false;
246
+ setIsFaceInsideCircle(false);
247
+ }
248
+ }, []);
249
+ const onFacesDetected = useCallback((faces, _image, _isImageBright, frameWidth, frameHeight) => {
250
+ if (!faces.length) {
251
+ if (isRecording) {
252
+ if (stopReasonRef.current === 'manual') {
253
+ return;
254
+ }
255
+
256
+ // Requirement: stop recording as soon as no face is detected.
257
+ isFaceInsideCircleRef.current = false;
258
+ setIsFaceInsideCircle(false);
259
+ insideFrameStreakRef.current = 0;
260
+ outsideFrameStreakRef.current = ALIGNMENT_LOST_FRAMES;
261
+ } else {
262
+ updateStableAlignment(false);
263
+ }
264
+ setMultipleFacesDetected(false);
265
+ return;
266
+ }
267
+ const multipleFaces = faces.length > 1;
268
+ setMultipleFacesDetected(multipleFaces);
269
+ const face = faces[0];
270
+ // Camera uses FILL_CENTER / resizeAspectFill (cover): uniform scale filling
271
+ // the container, cropping overflow symmetrically. Map the on-screen circle
272
+ // into frame coordinates accounting for that crop.
273
+ const containerHeight = Math.max(1, cameraLayout.height || previewSize);
274
+ const coverScale = Math.max(containerWidth / frameWidth, containerHeight / frameHeight);
275
+ const offsetX = (frameWidth - containerWidth / coverScale) / 2;
276
+ const offsetY = (frameHeight - containerHeight / coverScale) / 2;
277
+ const previewRectInFrame = {
278
+ minX: previewRect.minX / coverScale + offsetX,
279
+ minY: previewRect.minY / coverScale + offsetY,
280
+ width: previewRect.width / coverScale,
281
+ height: previewRect.height / coverScale
282
+ };
283
+
284
+ // Check containment using face center + yaw-stable core radius.
285
+ // When the head turns, the bounding box grows wider but height stays constant,
286
+ // so min(width, height)/2 is a stable representative size.
287
+ const faceCenterX = face.bounds.x + face.bounds.width / 2;
288
+ const faceCenterY = face.bounds.y + face.bounds.height / 2;
289
+ const faceCoreRadius = Math.max(0, Math.min(face.bounds.width, face.bounds.height) / 2 - PREVIEW_EDGE_OFFSET / 2);
290
+ const circleCX = previewRectInFrame.minX + previewRectInFrame.width / 2;
291
+ const circleCY = previewRectInFrame.minY + previewRectInFrame.height / 2;
292
+ const circleR = previewRectInFrame.width / 2 - CONSENT_CIRCLE_INSET / coverScale;
293
+ const faceDx = faceCenterX - circleCX;
294
+ const faceDy = faceCenterY - circleCY;
295
+ const previewContainsFace = faceDx * faceDx + faceDy * faceDy <= (circleR - faceCoreRadius) * (circleR - faceCoreRadius);
296
+ if (isRecording) {
297
+ if (stopReasonRef.current === 'manual') {
298
+ return;
299
+ }
300
+ if (!previewContainsFace || multipleFaces) {
301
+ // During recording, cancel immediately when alignment is lost.
302
+ isFaceInsideCircleRef.current = false;
303
+ setIsFaceInsideCircle(false);
304
+ insideFrameStreakRef.current = 0;
305
+ outsideFrameStreakRef.current = ALIGNMENT_LOST_FRAMES;
306
+ return;
307
+ }
308
+ isFaceInsideCircleRef.current = true;
309
+ setIsFaceInsideCircle(true);
310
+ insideFrameStreakRef.current = ALIGNMENT_CONFIRM_FRAMES;
311
+ outsideFrameStreakRef.current = 0;
312
+ return;
313
+ }
314
+ updateStableAlignment(previewContainsFace && !multipleFaces);
315
+ }, [cameraLayout.height, containerWidth, previewRect.minX, previewRect.minY, previewRect.width, previewRect.height, previewSize, isRecording, updateStableAlignment]);
316
+ const onCameraContainerLayout = useCallback(event => {
317
+ const {
318
+ width,
319
+ height
320
+ } = event.nativeEvent.layout;
321
+ setCameraLayout({
322
+ width,
323
+ height
324
+ });
325
+ }, []);
326
+ const handleStopRecording = useCallback(async (discard = false) => {
327
+ if (!cameraRef.current || !isRecording) {
328
+ return;
329
+ }
330
+ if (stopInProgressRef.current) {
331
+ // If stop is already in progress, allow manual button press to take precedence.
332
+ if (!discard) {
333
+ stopReasonRef.current = 'manual';
334
+ }
335
+ return;
336
+ }
337
+
338
+ // Set immediate visual feedback
339
+ if (!discard) {
340
+ setIsStoppingRecording(true);
341
+ }
342
+ stopReasonRef.current = discard ? 'discard' : 'manual';
343
+ stopInProgressRef.current = true;
344
+ await cameraRef.current.stopRecording();
345
+ // Do NOT setIsRecording(false) here — let onRecordingFinished handle it
346
+ // atomically together with setRecordingPath to avoid a flash of the
347
+ // start-recording UI between stop and preview.
348
+ }, [isRecording]);
349
+ const handleTryAgain = useCallback(() => {
350
+ clearTimer();
351
+ clearAutoStopTimer();
352
+ setIsRecording(false);
353
+ setRecordingPath('');
354
+ setRecordedSeconds(0);
355
+ setCurrentLineIndex(0);
356
+ setIsStoppingRecording(false);
357
+ setMultipleFacesDetected(false);
358
+ setIsFaceInsideCircle(false);
359
+ setScrollProgressPercent(0);
360
+ autoStartTriggeredRef.current = false;
361
+ stopReasonRef.current = null;
362
+ stopInProgressRef.current = false;
363
+ isFaceInsideCircleRef.current = false;
364
+ insideFrameStreakRef.current = 0;
365
+ outsideFrameStreakRef.current = 0;
366
+ scrollNeedsInitRef.current = true;
367
+ initialDelayAppliedRef.current = false;
368
+ }, [clearAutoStopTimer, clearTimer]);
369
+ const persistVerbalConsentData = useCallback(() => {
370
+ if (!recordingPath) {
371
+ return;
372
+ }
373
+ const existingVideos = appContext.identificationInfo.verbalConsentVideos || [];
374
+ appContext.identificationInfo.verbalConsentVideos = [...existingVideos, {
375
+ title: consentTitle,
376
+ text: consentText,
377
+ videoPath: recordingPath,
378
+ recordedSeconds
379
+ }];
380
+ }, [appContext.identificationInfo, consentText, consentTitle, recordedSeconds, recordingPath]);
381
+ const submitVerbalConsent = useCallback(() => {
382
+ if (!recordingPath) {
383
+ return;
384
+ }
385
+ persistVerbalConsentData();
386
+ trackVerificationComplete('VERBAL_CONSENT', true, 1);
387
+ navigationManagerRef.current?.navigateToNextStep();
388
+ }, [persistVerbalConsentData, recordingPath]);
389
+ const canSubmit = !!recordingPath && !isRecording;
390
+ const isPreviewAvailable = !!recordingPath && !isRecording;
391
+ // Show text as soon as face is inside circle so user can read what to say.
392
+ const shouldShowTextOverlay = isFaceInsideCircle && hasGuideShown;
393
+
394
+ // Auto-start recording when face is aligned
395
+ useEffect(() => {
396
+ if (!hasGuideShown) {
397
+ return;
398
+ }
399
+ if (isRecording || isPreviewAvailable || autoStartTriggeredRef.current) {
400
+ return;
401
+ }
402
+ if (isFaceInsideCircle && !multipleFacesDetected) {
403
+ autoStartTriggeredRef.current = true;
404
+ handleStartRecording();
405
+ }
406
+ }, [isFaceInsideCircle, isRecording, isPreviewAvailable, multipleFacesDetected, handleStartRecording, hasGuideShown]);
407
+ useEffect(() => {
408
+ if (!isRecording) {
409
+ autoStoppingRef.current = false;
410
+ return;
411
+ }
412
+ if (stopReasonRef.current === 'manual') {
413
+ return;
414
+ }
415
+ if (isFaceInsideCircle && !multipleFacesDetected) {
416
+ return;
417
+ }
418
+ if (autoStoppingRef.current) {
419
+ return;
420
+ }
421
+
422
+ // Immediately reset scroll and progress so the UI is clean before the
423
+ // async stopRecording resolves.
424
+ activeLineTranslateX.stopAnimation();
425
+ activeLineTranslateX.setValue(0);
426
+ activeLineTranslateXRef.current = 0;
427
+ scrollNeedsInitRef.current = true;
428
+ initialDelayAppliedRef.current = false;
429
+ setScrollProgressPercent(0);
430
+ clearAutoStopTimer();
431
+ stopReasonRef.current = 'discard';
432
+ autoStoppingRef.current = true;
433
+ handleStopRecording(true).finally(() => {
434
+ autoStoppingRef.current = false;
435
+ });
436
+ }, [activeLineTranslateX, clearAutoStopTimer, handleStopRecording, isFaceInsideCircle, isRecording, multipleFacesDetected]);
437
+ useEffect(() => {
438
+ const startX = Math.max(0, activeLineViewportWidth);
439
+ // Complete when the full text has moved out of view to the left.
440
+ const endX = -Math.max(1, estimatedLineContentWidth);
441
+ const totalDistance = Math.max(1, startX - endX);
442
+ const listenerId = activeLineTranslateX.addListener(({
443
+ value
444
+ }) => {
445
+ activeLineTranslateXRef.current = value;
446
+ // value goes from startX (offscreen right) to endX (offscreen left)
447
+ // traveled = startX - value, so 0 at start and totalDistance at end
448
+ const traveled = Math.max(0, startX - value);
449
+ const pct = Math.min(100, traveled / totalDistance * 100);
450
+ setScrollProgressPercent(pct);
451
+ });
452
+ return () => {
453
+ activeLineTranslateX.removeListener(listenerId);
454
+ };
455
+ }, [activeLineTranslateX, estimatedLineContentWidth, activeLineViewportWidth, isRecording]);
456
+ useEffect(() => {
457
+ if (!isRecording) {
458
+ setHasTextScrollStarted(false);
459
+ }
460
+ }, [isRecording]);
461
+ useEffect(() => {
462
+ if (!isRecording || activeLineViewportWidth <= 0) {
463
+ activeLineTranslateX.stopAnimation();
464
+ activeLineTranslateX.setValue(0);
465
+ activeLineTranslateXRef.current = 0;
466
+ scrollNeedsInitRef.current = true;
467
+ setHasTextScrollStarted(false);
468
+ return;
469
+ }
470
+ const contentWidth = Math.max(1, estimatedLineContentWidth);
471
+ if (isAutoScrollPausedByTouch) {
472
+ activeLineTranslateX.stopAnimation(value => {
473
+ activeLineTranslateXRef.current = value;
474
+ });
475
+ return;
476
+ }
477
+ const maxX = activeLineViewportWidth;
478
+ const minX = -contentWidth;
479
+
480
+ // First start after reset: position text at the right edge of the circle
481
+ if (scrollNeedsInitRef.current) {
482
+ scrollNeedsInitRef.current = false;
483
+ activeLineTranslateX.setValue(maxX);
484
+ activeLineTranslateXRef.current = maxX;
485
+ }
486
+ const clampedCurrentX = Math.max(minX, Math.min(maxX, activeLineTranslateXRef.current));
487
+ const remainingDistance = Math.abs(minX - clampedCurrentX);
488
+ if (remainingDistance <= 0.5) {
489
+ activeLineTranslateX.setValue(minX);
490
+ activeLineTranslateXRef.current = minX;
491
+ return;
492
+ }
493
+ const totalRange = maxX - minX;
494
+ const lineDurationMs = Math.max(3000, Math.round(totalRange / SCROLL_PIXELS_PER_SECOND * 1000));
495
+ const progressRatio = totalRange > 0 ? Math.min(1, (maxX - clampedCurrentX) / totalRange) : 0;
496
+ const remainingDurationMs = Math.max(500, Math.round(lineDurationMs * (1 - progressRatio)));
497
+ activeLineTranslateX.stopAnimation();
498
+ activeLineTranslateX.setValue(clampedCurrentX);
499
+ const shouldApplyDelay = !initialDelayAppliedRef.current && clampedCurrentX >= maxX - 1;
500
+ if (shouldApplyDelay) {
501
+ initialDelayAppliedRef.current = true;
502
+ // Show text only when animation is about to actually start moving
503
+ setTimeout(() => {
504
+ setHasTextScrollStarted(true);
505
+ }, HORIZONTAL_SCROLL_START_DELAY_MS);
506
+ } else {
507
+ // No delay, show immediately
508
+ setHasTextScrollStarted(true);
509
+ }
510
+ Animated.timing(activeLineTranslateX, {
511
+ toValue: minX,
512
+ duration: remainingDurationMs,
513
+ delay: shouldApplyDelay ? HORIZONTAL_SCROLL_START_DELAY_MS : 0,
514
+ easing: Easing.linear,
515
+ useNativeDriver: false
516
+ }).start(result => {
517
+ if (result.finished) {
518
+ setScrollProgressPercent(100);
519
+ }
520
+ });
521
+ }, [activeLineTranslateX, activeLineViewportWidth, currentLineIndex, currentLineText, estimatedLineContentWidth, isAutoScrollPausedByTouch, isRecording, shouldShowTextOverlay, consentLines]);
522
+ useEffect(() => {
523
+ if (!isRecording || !shouldShowTextOverlay || isAutoScrollPausedByTouch) {
524
+ return;
525
+ }
526
+ if (currentLineIndex >= consentLines.length - 1 || consentLines.length <= 1) {
527
+ return;
528
+ }
529
+ const intervalId = setInterval(() => {
530
+ setCurrentLineIndex(prev => {
531
+ if (prev >= consentLines.length - 1) {
532
+ return prev;
533
+ }
534
+ return prev + 1;
535
+ });
536
+ }, autoScrollIntervalMs);
537
+ return () => {
538
+ clearInterval(intervalId);
539
+ };
540
+ }, [autoScrollIntervalMs, consentLines.length, currentLineIndex, isAutoScrollPausedByTouch, isRecording, shouldShowTextOverlay]);
541
+ useEffect(() => {
542
+ if (!isRecording || consentLines.length === 0 || isStoppingRecording) {
543
+ clearAutoStopTimer();
544
+ return;
545
+ }
546
+ if (!isConsentComplete) {
547
+ clearAutoStopTimer();
548
+ return;
549
+ }
550
+ if (autoStopTimeoutRef.current || stopInProgressRef.current) {
551
+ return;
552
+ }
553
+
554
+ // Give a brief tail buffer so users can finish the final spoken word.
555
+ autoStopTimeoutRef.current = setTimeout(() => {
556
+ autoStopTimeoutRef.current = null;
557
+ handleStopRecording(false);
558
+ }, AUTO_STOP_SPEECH_BUFFER_MS);
559
+ return () => {
560
+ clearAutoStopTimer();
561
+ };
562
+ }, [clearAutoStopTimer, handleStopRecording, isConsentComplete, isRecording, isStoppingRecording, consentLines.length]);
563
+ useEffect(() => {
564
+ return () => {
565
+ clearAutoStopTimer();
566
+ };
567
+ }, [clearAutoStopTimer]);
568
+ useEffect(() => {
569
+ if (!hasGuideShown) {
570
+ return;
571
+ }
572
+ if (!appContext.currentWorkflowStep?.data?.voiceGuidanceActive) {
573
+ return;
574
+ }
575
+ if (!currentInstructionKey) {
576
+ return;
577
+ }
578
+ speak(t(currentInstructionKey));
579
+ }, [appContext.currentWorkflowStep?.data?.voiceGuidanceActive, currentInstructionKey, hasGuideShown, t]);
580
+ useEffect(() => {
581
+ return () => {
582
+ stopSpeaking().catch(() => {});
583
+ };
584
+ }, []);
585
+ return /*#__PURE__*/_jsxs(_Fragment, {
586
+ children: [!hasGuideShown ? /*#__PURE__*/_jsxs(View, {
587
+ style: [styles.guide, {
588
+ paddingTop: insets.top,
589
+ paddingBottom: insets.bottom
590
+ }],
591
+ children: [/*#__PURE__*/_jsx(LottieView, {
592
+ source: require('../../Shared/Animations/recording.json'),
593
+ style: styles.guideAnimation,
594
+ loop: true,
595
+ autoPlay: true
596
+ }), /*#__PURE__*/_jsx(Text, {
597
+ style: styles.guideHeader,
598
+ children: t('verbalConsentVideoScreen.guideHeader')
599
+ }), /*#__PURE__*/_jsxs(View, {
600
+ style: styles.guidePoints,
601
+ children: [/*#__PURE__*/_jsx(Text, {
602
+ style: styles.guideText,
603
+ children: t('verbalConsentVideoScreen.guideText')
604
+ }), /*#__PURE__*/_jsxs(Text, {
605
+ style: styles.guideText,
606
+ children: ["\u2022 ", t('verbalConsentVideoScreen.guidePoint1')]
607
+ }), /*#__PURE__*/_jsxs(Text, {
608
+ style: styles.guideText,
609
+ children: ["\u2022 ", t('verbalConsentVideoScreen.guidePoint2')]
610
+ }), /*#__PURE__*/_jsxs(Text, {
611
+ style: styles.guideText,
612
+ children: ["\u2022 ", t('verbalConsentVideoScreen.guidePoint3')]
613
+ }), /*#__PURE__*/_jsxs(Text, {
614
+ style: styles.guideText,
615
+ children: ["\u2022 ", t('verbalConsentVideoScreen.guidePoint4')]
616
+ })]
617
+ }), /*#__PURE__*/_jsx(StyledButton, {
618
+ mode: "contained",
619
+ onPress: () => {
620
+ setHasGuideShown(true);
621
+ },
622
+ children: t('general.letsGo')
623
+ })]
624
+ }) : /*#__PURE__*/_jsxs(SafeAreaView, {
625
+ style: styles.container,
626
+ children: [/*#__PURE__*/_jsx(View, {
627
+ style: [styles.cameraContainer, isPreviewAvailable ? styles.cameraContainerPreview : styles.cameraContainerPreview],
628
+ children: isPreviewAvailable ? /*#__PURE__*/_jsxs(View, {
629
+ style: [styles.previewContainer, {
630
+ paddingTop: Math.max(insets.top, 12),
631
+ paddingLeft: 16 + insets.left,
632
+ paddingRight: 16 + insets.right,
633
+ paddingBottom: Math.max(insets.bottom, 12)
634
+ }],
635
+ children: [/*#__PURE__*/_jsx(Video, {
636
+ source: {
637
+ uri: recordingPath
638
+ },
639
+ resizeMode: "contain",
640
+ style: styles.previewVideo,
641
+ controls: true,
642
+ muted: false
643
+ }), /*#__PURE__*/_jsxs(View, {
644
+ style: styles.previewActionsContainer,
645
+ children: [/*#__PURE__*/_jsx(Text, {
646
+ style: styles.previewActionHint,
647
+ children: t('verbalConsentVideoScreen.previewTitle')
648
+ }), /*#__PURE__*/_jsx(StyledButton, {
649
+ mode: "contained",
650
+ onPress: handleTryAgain,
651
+ children: t('verbalConsentVideoScreen.tryAgain')
652
+ }), /*#__PURE__*/_jsx(StyledButton, {
653
+ mode: "outlined",
654
+ onPress: submitVerbalConsent,
655
+ disabled: !canSubmit,
656
+ children: t('verbalConsentVideoScreen.submit')
657
+ })]
658
+ })]
659
+ }) : /*#__PURE__*/_jsx(View, {
660
+ style: [styles.captureSection, {
661
+ paddingLeft: insets.left,
662
+ paddingRight: insets.right
663
+ }],
664
+ onTouchStart: () => {
665
+ if (isRecording && shouldShowTextOverlay && !isAutoScrollPausedByTouch) {
666
+ setIsAutoScrollPausedByTouch(true);
667
+ }
668
+ },
669
+ onTouchEnd: () => {
670
+ if (isRecording && shouldShowTextOverlay && isAutoScrollPausedByTouch) {
671
+ setIsAutoScrollPausedByTouch(false);
672
+ }
673
+ },
674
+ onTouchCancel: () => {
675
+ if (isRecording && shouldShowTextOverlay && isAutoScrollPausedByTouch) {
676
+ setIsAutoScrollPausedByTouch(false);
677
+ }
678
+ },
679
+ children: /*#__PURE__*/_jsxs(View, {
680
+ style: styles.groupedContentContainer,
681
+ children: [/*#__PURE__*/_jsx(View, {
682
+ style: [styles.titleRow, {
683
+ paddingTop: titleTopPadding
684
+ }],
685
+ children: isRecording ? /*#__PURE__*/_jsxs(View, {
686
+ style: styles.recordingIndicator,
687
+ children: [/*#__PURE__*/_jsx(Animated.View, {
688
+ style: [styles.recordingDot, {
689
+ opacity: recordingDotOpacity
690
+ }]
691
+ }), /*#__PURE__*/_jsx(Text, {
692
+ style: styles.recordingText,
693
+ children: "REC"
694
+ })]
695
+ }) : /*#__PURE__*/_jsx(Text, {
696
+ style: styles.topTitleText,
697
+ numberOfLines: 2,
698
+ ellipsizeMode: "tail",
699
+ children: consentTitle
700
+ })
701
+ }), /*#__PURE__*/_jsxs(View, {
702
+ style: [styles.circleStage, {
703
+ height: previewSize
704
+ }],
705
+ onLayout: onCameraContainerLayout,
706
+ children: [/*#__PURE__*/_jsx(View, {
707
+ style: [styles.cameraCircle, {
708
+ width: circleDiameter,
709
+ height: circleDiameter,
710
+ left: circleLeft,
711
+ top: CONSENT_CIRCLE_INSET,
712
+ borderRadius: circleDiameter / 2
713
+ }],
714
+ children: /*#__PURE__*/_jsx(FaceCamera, {
715
+ onCameraInitialized: camera => {
716
+ cameraRef.current = camera;
717
+ },
718
+ onFacesDetected: onFacesDetected
719
+ })
720
+ }), /*#__PURE__*/_jsx(NativeCircularProgress, {
721
+ style: [styles.circleProgress, {
722
+ left: previewRect.minX
723
+ }],
724
+ size: previewRect.width,
725
+ width: 8,
726
+ backgroundWidth: 8,
727
+ fill: isRecording ? scrollProgressPercent : 0,
728
+ tintColor: isRecording ? appContext.branding.primaryColor : 'transparent',
729
+ backgroundColor: shouldShowTextOverlay ? 'rgba(255, 255, 255, 0.3)' : circleStrokeColor
730
+ }), /*#__PURE__*/_jsx(View, {
731
+ style: [styles.textSingleLineContainer, {
732
+ width: circleDiameter,
733
+ height: circleDiameter,
734
+ left: circleLeft,
735
+ top: CONSENT_CIRCLE_INSET,
736
+ borderRadius: circleDiameter / 2
737
+ }, (!shouldShowTextOverlay || !hasTextScrollStarted) && styles.hiddenOffscreen],
738
+ children: /*#__PURE__*/_jsx(View, {
739
+ style: styles.textSingleLineInner,
740
+ onLayout: event => {
741
+ setActiveLineViewportWidth(event.nativeEvent.layout.width);
742
+ },
743
+ children: /*#__PURE__*/_jsx(View, {
744
+ style: [styles.textLineContainer, styles.textLineHighlighted],
745
+ children: /*#__PURE__*/_jsx(View, {
746
+ style: styles.activeLineViewport,
747
+ children: /*#__PURE__*/_jsx(Animated.View, {
748
+ style: [styles.activeLineTrack, {
749
+ transform: [{
750
+ translateX: activeLineTranslateX
751
+ }]
752
+ }],
753
+ children: /*#__PURE__*/_jsx(Text, {
754
+ style: [styles.consentTextLine, styles.consentTextLineActive],
755
+ numberOfLines: 1,
756
+ children: consentLines[currentLineIndex] || ''
757
+ })
758
+ })
759
+ })
760
+ })
761
+ })
762
+ })]
763
+ }), /*#__PURE__*/_jsx(View, {
764
+ style: styles.instructionRow,
765
+ children: /*#__PURE__*/_jsx(Text, {
766
+ style: [styles.circleInstructionText, !currentInstructionKey && styles.hidden],
767
+ numberOfLines: 2,
768
+ ellipsizeMode: "tail",
769
+ children: currentInstructionKey ? t(currentInstructionKey) : ' '
770
+ })
771
+ })]
772
+ })
773
+ })
774
+ }), /*#__PURE__*/_jsx(Text, {
775
+ style: [styles.consentTextLine, styles.consentTextLineActive, styles.offscreenMeasure],
776
+ numberOfLines: 1,
777
+ onTextLayout: e => {
778
+ const lineWidth = e.nativeEvent.lines[0]?.width ?? 0;
779
+ if (lineWidth > 0) {
780
+ setMeasuredTextWidth(Math.ceil(lineWidth));
781
+ }
782
+ },
783
+ children: consentLines[currentLineIndex] || ''
784
+ })]
785
+ }), /*#__PURE__*/_jsx(View, {
786
+ style: [styles.footer, {
787
+ bottom: insets.bottom
788
+ }],
789
+ children: /*#__PURE__*/_jsx(NavigationManager, {
790
+ ref: navigationManagerRef
791
+ })
792
+ })]
793
+ });
794
+ };
795
+ const styles = StyleSheet.create({
796
+ container: {
797
+ flex: 1,
798
+ backgroundColor: '#ffffff'
799
+ },
800
+ footer: {
801
+ position: 'absolute',
802
+ flex: 0,
803
+ bottom: 0,
804
+ width: '100%',
805
+ zIndex: 1
806
+ },
807
+ guide: {
808
+ flex: 1,
809
+ display: 'flex',
810
+ justifyContent: 'center',
811
+ paddingHorizontal: 20,
812
+ gap: 10,
813
+ backgroundColor: 'white'
814
+ },
815
+ guideAnimation: {
816
+ width: 250,
817
+ height: 250,
818
+ alignSelf: 'center'
819
+ },
820
+ guideHeader: {
821
+ color: 'black',
822
+ fontSize: 18,
823
+ textAlign: 'center',
824
+ fontWeight: 'bold'
825
+ },
826
+ guidePoints: {
827
+ display: 'flex',
828
+ gap: 10,
829
+ padding: 10
830
+ },
831
+ guideText: {
832
+ color: 'black',
833
+ fontSize: 14
834
+ },
835
+ cameraContainer: {
836
+ width: '100%'
837
+ },
838
+ cameraContainerPreview: {
839
+ flex: 1
840
+ },
841
+ captureSection: {
842
+ flex: 1,
843
+ overflow: 'hidden',
844
+ backgroundColor: '#ffffff',
845
+ justifyContent: 'center',
846
+ alignItems: 'center'
847
+ },
848
+ groupedContentContainer: {
849
+ width: '100%',
850
+ alignItems: 'center'
851
+ },
852
+ titleRow: {
853
+ minHeight: TITLE_ROW_MIN_HEIGHT,
854
+ justifyContent: 'center',
855
+ alignItems: 'center',
856
+ paddingHorizontal: 16,
857
+ marginBottom: 16
858
+ },
859
+ recordingIndicator: {
860
+ flexDirection: 'row',
861
+ alignItems: 'center',
862
+ gap: 8,
863
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
864
+ paddingVertical: 6,
865
+ paddingHorizontal: 14,
866
+ borderRadius: 20
867
+ },
868
+ recordingDot: {
869
+ width: 10,
870
+ height: 10,
871
+ borderRadius: 5,
872
+ backgroundColor: '#ef4444'
873
+ },
874
+ recordingText: {
875
+ color: '#ffffff',
876
+ fontSize: 14,
877
+ fontWeight: '700',
878
+ letterSpacing: 1
879
+ },
880
+ circleStage: {
881
+ position: 'relative',
882
+ width: '100%',
883
+ backgroundColor: '#ffffff'
884
+ },
885
+ cameraCircle: {
886
+ position: 'absolute',
887
+ overflow: 'hidden'
888
+ },
889
+ instructionRow: {
890
+ height: INSTRUCTION_ROW_MIN_HEIGHT,
891
+ justifyContent: 'center',
892
+ alignItems: 'center',
893
+ paddingHorizontal: 16,
894
+ paddingTop: 10,
895
+ marginTop: 16,
896
+ overflow: 'hidden'
897
+ },
898
+ instructionRowHidden: {
899
+ opacity: 0
900
+ },
901
+ topTitleText: {
902
+ fontSize: 18,
903
+ fontWeight: '700',
904
+ color: '#111827',
905
+ textAlign: 'center',
906
+ backgroundColor: 'rgba(255,255,255,0.95)',
907
+ paddingVertical: 6,
908
+ paddingHorizontal: 10,
909
+ borderRadius: 10
910
+ },
911
+ circleInstructionText: {
912
+ color: '#111827',
913
+ fontSize: 18,
914
+ fontWeight: '700',
915
+ textAlign: 'center',
916
+ backgroundColor: 'rgba(255,255,255,0.92)',
917
+ paddingVertical: 10,
918
+ paddingHorizontal: 14,
919
+ borderRadius: 8,
920
+ lineHeight: 24
921
+ },
922
+ previewContainer: {
923
+ flex: 1,
924
+ backgroundColor: '#ffffff',
925
+ paddingHorizontal: 16,
926
+ paddingTop: 16,
927
+ paddingBottom: 12,
928
+ justifyContent: 'flex-start'
929
+ },
930
+ previewActionsContainer: {
931
+ paddingTop: 10,
932
+ gap: 8
933
+ },
934
+ previewVideo: {
935
+ width: '100%',
936
+ height: '64%',
937
+ borderRadius: 8,
938
+ backgroundColor: '#000000',
939
+ alignSelf: 'center',
940
+ borderWidth: 1,
941
+ borderColor: '#e5e7eb'
942
+ },
943
+ actionsContainerNearCircle: {
944
+ gap: 8,
945
+ paddingTop: 8,
946
+ paddingHorizontal: 20,
947
+ minHeight: ACTIONS_RESERVED_HEIGHT_IDLE
948
+ },
949
+ actionsContainerHidden: {
950
+ opacity: 0,
951
+ pointerEvents: 'none'
952
+ },
953
+ actionsContainerNearCircleRecording: {
954
+ minHeight: ACTIONS_RESERVED_HEIGHT_IDLE,
955
+ paddingTop: 8
956
+ },
957
+ arrowButtonCircle: {
958
+ position: 'absolute',
959
+ width: 56,
960
+ height: 56,
961
+ borderRadius: 28,
962
+ backgroundColor: '#ffffff',
963
+ justifyContent: 'center',
964
+ alignItems: 'center',
965
+ shadowColor: '#000',
966
+ shadowOffset: {
967
+ width: 0,
968
+ height: 2
969
+ },
970
+ shadowOpacity: 0.15,
971
+ shadowRadius: 6,
972
+ elevation: 4,
973
+ borderWidth: 2,
974
+ borderColor: '#e5e7eb'
975
+ },
976
+ arrowTextCircle: {
977
+ fontSize: 26,
978
+ color: '#1f2937',
979
+ fontWeight: '700'
980
+ },
981
+ textSingleLineContainer: {
982
+ position: 'absolute',
983
+ backgroundColor: 'rgba(255, 255, 255, 0.5)',
984
+ overflow: 'hidden',
985
+ justifyContent: 'center'
986
+ },
987
+ textSingleLineInner: {
988
+ flex: 1,
989
+ justifyContent: 'center',
990
+ paddingHorizontal: 0
991
+ },
992
+ textLineContainer: {
993
+ height: CONSENT_LINE_HEIGHT,
994
+ paddingHorizontal: 0,
995
+ marginVertical: 0,
996
+ borderRadius: 0,
997
+ backgroundColor: 'transparent',
998
+ justifyContent: 'center'
999
+ },
1000
+ textLineHighlighted: {
1001
+ backgroundColor: 'transparent'
1002
+ },
1003
+ activeLineViewport: {
1004
+ width: '100%',
1005
+ height: CONSENT_LINE_HEIGHT,
1006
+ overflow: 'hidden'
1007
+ },
1008
+ activeLineTrack: {
1009
+ position: 'absolute',
1010
+ top: 0,
1011
+ bottom: 0,
1012
+ width: 9999,
1013
+ justifyContent: 'center'
1014
+ },
1015
+ consentTextLine: {
1016
+ fontSize: 14,
1017
+ color: 'rgba(156, 163, 175, 0.6)',
1018
+ lineHeight: 22,
1019
+ fontWeight: '400',
1020
+ textAlign: 'left'
1021
+ },
1022
+ consentTextLineActive: {
1023
+ fontSize: 32,
1024
+ color: '#111827',
1025
+ fontWeight: '900',
1026
+ lineHeight: 44
1027
+ },
1028
+ circleProgress: {
1029
+ position: 'absolute',
1030
+ top: 0,
1031
+ zIndex: 2,
1032
+ pointerEvents: 'none'
1033
+ },
1034
+ offscreenMeasure: {
1035
+ position: 'absolute',
1036
+ top: -9999,
1037
+ left: 0,
1038
+ opacity: 0,
1039
+ width: 9999
1040
+ },
1041
+ hidden: {
1042
+ opacity: 0
1043
+ },
1044
+ hiddenOffscreen: {
1045
+ opacity: 0,
1046
+ transform: [{
1047
+ translateY: -10000
1048
+ }]
1049
+ },
1050
+ arrowButtonDisabled: {
1051
+ backgroundColor: '#e5e7eb',
1052
+ shadowOpacity: 0,
1053
+ elevation: 0
1054
+ },
1055
+ arrowTextDisabled: {
1056
+ color: '#9ca3af'
1057
+ },
1058
+ previewActionHint: {
1059
+ fontSize: 13,
1060
+ color: '#4b5563',
1061
+ textAlign: 'center'
1062
+ },
1063
+ recordingBlockedContainer: {
1064
+ borderRadius: 10,
1065
+ backgroundColor: '#ffffff',
1066
+ borderWidth: 1,
1067
+ borderColor: '#d1d5db',
1068
+ paddingVertical: 16,
1069
+ paddingHorizontal: 14
1070
+ },
1071
+ recordingBlockedText: {
1072
+ fontSize: 16,
1073
+ color: '#111827',
1074
+ fontWeight: '600',
1075
+ textAlign: 'center',
1076
+ lineHeight: 22
1077
+ }
1078
+ });
1079
+ export default VerbalConsentScreen;