@vanira/sdk-react-native 0.0.2

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 (148) hide show
  1. package/README.md +239 -0
  2. package/package.json +53 -0
  3. package/src/__tests__/WebRTCClient.integration.test.ts +396 -0
  4. package/src/__tests__/adapters.test.ts +475 -0
  5. package/src/__tests__/httpResponse.test.ts +25 -0
  6. package/src/__tests__/mocks/react-native-incall-manager.ts +8 -0
  7. package/src/__tests__/mocks/react-native-permissions.ts +15 -0
  8. package/src/__tests__/mocks/react-native-webrtc.ts +6 -0
  9. package/src/__tests__/mocks/react-native.ts +28 -0
  10. package/src/__tests__/preset.test.ts +239 -0
  11. package/src/__tests__/resolveRuntimeConfig.test.ts +90 -0
  12. package/src/__tests__/storage.test.ts +211 -0
  13. package/src/__tests__/webrtcSignaling.test.ts +42 -0
  14. package/src/adapters/PeerConnectionAdapter.ts +101 -0
  15. package/src/adapters/browser/BrowserAudioAdapter.ts +43 -0
  16. package/src/adapters/browser/BrowserDataChannelAdapter.ts +69 -0
  17. package/src/adapters/browser/BrowserMediaAdapter.ts +15 -0
  18. package/src/adapters/browser/BrowserPeerAdapter.ts +14 -0
  19. package/src/adapters/browser/index.ts +4 -0
  20. package/src/adapters/interfaces.ts +84 -0
  21. package/src/adapters/react-native/RNAudioAdapter.ts +42 -0
  22. package/src/adapters/react-native/RNDataChannelAdapter.ts +79 -0
  23. package/src/adapters/react-native/RNMediaAdapter.ts +46 -0
  24. package/src/adapters/react-native/RNPeerAdapter.ts +28 -0
  25. package/src/adapters/react-native/callAudioRouting.ts +115 -0
  26. package/src/adapters/react-native/decodeUtf8.ts +72 -0
  27. package/src/adapters/react-native/index.ts +4 -0
  28. package/src/adapters/react-native/rnUploadFile.ts +76 -0
  29. package/src/adapters/storage/BrowserDualStorageAdapter.ts +71 -0
  30. package/src/adapters/storage/MemoryStorageAdapter.ts +50 -0
  31. package/src/adapters/storage/StorageAdapter.ts +21 -0
  32. package/src/adapters/storage/createSyncStorageAdapter.ts +40 -0
  33. package/src/adapters/storage/index.ts +7 -0
  34. package/src/api/services/ChatService.ts +304 -0
  35. package/src/api/services/ConfigService.ts +33 -0
  36. package/src/assets/icons.js +35 -0
  37. package/src/cdn.ts +68 -0
  38. package/src/core/CallSessionStore.ts +137 -0
  39. package/src/core/DraggableController.ts +83 -0
  40. package/src/core/SessionManager.ts +322 -0
  41. package/src/core/VaniraAI.ts +464 -0
  42. package/src/core/WebRTCClient.ts +1012 -0
  43. package/src/core/httpResponse.ts +22 -0
  44. package/src/core/iceServers.ts +18 -0
  45. package/src/core/toolCallNormalize.ts +80 -0
  46. package/src/core/voice-client.js +236 -0
  47. package/src/core/webrtcSignaling.ts +72 -0
  48. package/src/index.js +34 -0
  49. package/src/index.ts +6 -0
  50. package/src/platforms/browser.ts +67 -0
  51. package/src/platforms/react-native.ts +105 -0
  52. package/src/presets/BookingCalendarModal.tsx +457 -0
  53. package/src/presets/CameraModal.tsx +576 -0
  54. package/src/presets/DynamicFormModal.tsx +378 -0
  55. package/src/presets/NativePresetRenderer.tsx +350 -0
  56. package/src/presets/NavigateHandler.tsx +75 -0
  57. package/src/presets/PresetHost.tsx +155 -0
  58. package/src/presets/PresetShellModal.tsx +97 -0
  59. package/src/presets/UploadModal.tsx +321 -0
  60. package/src/presets/calendar/calendarUtils.ts +386 -0
  61. package/src/presets/call/CallSpeakerToggle.tsx +59 -0
  62. package/src/presets/call/callAudioRouting.ts +2 -0
  63. package/src/presets/call/useCallSpeaker.ts +31 -0
  64. package/src/presets/camera/cameraPermissions.ts +18 -0
  65. package/src/presets/camera/cameraStream.ts +19 -0
  66. package/src/presets/camera/cameraUtils.ts +21 -0
  67. package/src/presets/camera/useLivenessFlow.ts +95 -0
  68. package/src/presets/chalkboard/ChalkboardOverlay.tsx +156 -0
  69. package/src/presets/chalkboard/EraseTextHandler.tsx +95 -0
  70. package/src/presets/chalkboard/TypeTextHandler.tsx +107 -0
  71. package/src/presets/chalkboard/boardAbort.ts +36 -0
  72. package/src/presets/chalkboard/boardQueue.ts +620 -0
  73. package/src/presets/chalkboard/chalkboardSession.ts +75 -0
  74. package/src/presets/chalkboard/drawUtils.ts +123 -0
  75. package/src/presets/chalkboard/textUtils.ts +109 -0
  76. package/src/presets/clipRegion/ClipRegionModal.tsx +261 -0
  77. package/src/presets/clipRegion/clipRegionBridge.ts +19 -0
  78. package/src/presets/form/formValidation.ts +104 -0
  79. package/src/presets/form/parseFormFields.ts +171 -0
  80. package/src/presets/host/HostElementPresetHandler.tsx +155 -0
  81. package/src/presets/host/hostPresetBridge.ts +71 -0
  82. package/src/presets/index.ts +63 -0
  83. package/src/presets/liveScreen/CloseLiveScreenHandler.tsx +36 -0
  84. package/src/presets/liveScreen/LiveScreenCaptureHost.tsx +312 -0
  85. package/src/presets/liveScreen/LiveScreenHandler.tsx +25 -0
  86. package/src/presets/liveScreen/LiveScreenPipOverlay.tsx +6 -0
  87. package/src/presets/liveScreen/liveScreenSession.ts +73 -0
  88. package/src/presets/liveVision/CloseLiveVisionHandler.tsx +29 -0
  89. package/src/presets/liveVision/LiveVisionCameraHost.tsx +317 -0
  90. package/src/presets/liveVision/LiveVisionHandler.tsx +26 -0
  91. package/src/presets/liveVision/LiveVisionPipOverlay.tsx +7 -0
  92. package/src/presets/liveVision/liveVisionFrameLoop.ts +38 -0
  93. package/src/presets/liveVision/liveVisionSession.ts +75 -0
  94. package/src/presets/liveVision/liveVisionUpload.ts +62 -0
  95. package/src/presets/navigation/internalRouteRegistry.ts +25 -0
  96. package/src/presets/navigation/navigationBridge.ts +76 -0
  97. package/src/presets/navigation/navigationTypes.ts +12 -0
  98. package/src/presets/parseToolCall.ts +60 -0
  99. package/src/presets/presetClientAdapter.ts +29 -0
  100. package/src/presets/presetCompletion.ts +91 -0
  101. package/src/presets/presetEventHelpers.ts +45 -0
  102. package/src/presets/registry.ts +128 -0
  103. package/src/presets/streaming/mediaFrameUpload.ts +93 -0
  104. package/src/presets/types.ts +74 -0
  105. package/src/presets/upload/pickUploadFile.ts +256 -0
  106. package/src/presets/upload/uploadFormats.ts +163 -0
  107. package/src/presets/upload/uploadUtils.ts +68 -0
  108. package/src/react/PresetRenderer.tsx +144 -0
  109. package/src/react/index.ts +1 -0
  110. package/src/runtime/browserRuntime.ts +54 -0
  111. package/src/runtime/platform.ts +17 -0
  112. package/src/runtime/reactNativeRuntime.ts +68 -0
  113. package/src/runtime/resolveRuntimeConfig.ts +75 -0
  114. package/src/runtime/runtimeBundles.ts +74 -0
  115. package/src/runtime/types.ts +135 -0
  116. package/src/types/react-native-incall-manager.d.ts +17 -0
  117. package/src/types/react-native-webrtc.d.ts +47 -0
  118. package/src/types.ts +133 -0
  119. package/src/ui/VaniraWidget.ts +87 -0
  120. package/src/ui/abstraction/AbstractWidgetProvider.ts +18 -0
  121. package/src/ui/abstraction/interfaces.ts +12 -0
  122. package/src/ui/adapters/VaniraChatAdapter.ts +42 -0
  123. package/src/ui/components/AvatarView.ts +81 -0
  124. package/src/ui/components/ChatWindow.ts +263 -0
  125. package/src/ui/components/FloatingButton.ts +163 -0
  126. package/src/ui/components/FloatingWelcomeChips.ts +137 -0
  127. package/src/ui/components/Panel.ts +120 -0
  128. package/src/ui/components/VoiceOrb.ts +79 -0
  129. package/src/ui/components/VoiceOverlay.ts +497 -0
  130. package/src/ui/components/index.ts +7 -0
  131. package/src/ui/factory/WidgetFactory.ts +16 -0
  132. package/src/ui/icons_data.ts +2 -0
  133. package/src/ui/presets/WidgetPresetRenderer.ts +1802 -0
  134. package/src/ui/presets/types.ts +16 -0
  135. package/src/ui/providers/VaniraInternalProvider.ts +1066 -0
  136. package/src/ui/styles/index.ts +323 -0
  137. package/src/ui/styles/keyframes.ts +76 -0
  138. package/src/ui/styles/theme.ts +57 -0
  139. package/src/ui/styles/widget.css.ts +838 -0
  140. package/src/ui/utils.ts +37 -0
  141. package/src/ui/views/AbstractChatView.ts +93 -0
  142. package/src/ui/views/AbstractVoiceView.ts +57 -0
  143. package/src/ui/views/AvatarOnlyView.ts +78 -0
  144. package/src/ui/views/ChatAvatarView.ts +66 -0
  145. package/src/ui/views/ChatOnlyView.ts +28 -0
  146. package/src/ui/views/ChatVoiceView.ts +15 -0
  147. package/src/ui/views/VoiceOnlyView.ts +25 -0
  148. package/src/ui/views/index.ts +5 -0
@@ -0,0 +1,576 @@
1
+ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Pressable,
5
+ StyleSheet,
6
+ Switch,
7
+ Text,
8
+ View,
9
+ } from 'react-native';
10
+ import {RTCView} from 'react-native-webrtc';
11
+ import ViewShot from 'react-native-view-shot';
12
+ import {CallSpeakerToggle} from './call/CallSpeakerToggle';
13
+ import {ensureCameraPermission} from './camera/cameraPermissions';
14
+ import {closeCameraStream, openCameraStream} from './camera/cameraStream';
15
+ import {
16
+ buildCaptureFileName,
17
+ parseFacingMode,
18
+ parseLivenessDefault,
19
+ type FacingMode,
20
+ } from './camera/cameraUtils';
21
+ import {useLivenessFlow} from './camera/useLivenessFlow';
22
+ import type {RNUploadFile} from './upload/uploadUtils';
23
+ import {PresetShellModal} from './PresetShellModal';
24
+ import type {PresetClient, PresetContext} from './types';
25
+
26
+ type Props = {
27
+ visible: boolean;
28
+ ctx: PresetContext;
29
+ client: PresetClient | null;
30
+ onClose: () => void;
31
+ onCancel: (reason?: string) => void;
32
+ onComplete: (payload: Record<string, unknown>) => void;
33
+ };
34
+
35
+ const SUCCESS_DELAY_MS = 400;
36
+
37
+ type CameraPhase = 'opening' | 'live' | 'capturing' | 'error';
38
+
39
+ export function CameraModal({
40
+ visible,
41
+ ctx,
42
+ client,
43
+ onClose,
44
+ onCancel,
45
+ onComplete,
46
+ }: Props) {
47
+ const merged = useMemo(
48
+ () => ({...ctx.args, ...ctx.clientFields}),
49
+ [ctx.args, ctx.clientFields],
50
+ );
51
+
52
+ const title = String(merged.title ?? 'Take a Photo');
53
+ const description = String(
54
+ merged.description ?? 'Point your camera at the subject, then tap capture.',
55
+ );
56
+ const initialFacing = parseFacingMode(merged.facing_mode ?? ctx.args.facing_mode);
57
+ const reason = String(merged.reason ?? ctx.args.reason ?? 'camera_capture');
58
+ const defaultLiveness = parseLivenessDefault(ctx.clientFields, ctx.args);
59
+
60
+ const [phase, setPhase] = useState<CameraPhase>('opening');
61
+ const [activeFacing, setActiveFacing] = useState<FacingMode>(initialFacing);
62
+ const [livenessEnabled, setLivenessEnabled] = useState(defaultLiveness);
63
+ const [captureReady, setCaptureReady] = useState(!defaultLiveness);
64
+ const [streamUrl, setStreamUrl] = useState<string | null>(null);
65
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
66
+ const [statusMessage, setStatusMessage] = useState<string | null>(null);
67
+
68
+ const streamRef = useRef<import('react-native-webrtc').MediaStream | null>(
69
+ null,
70
+ );
71
+ const viewShotRef = useRef<ViewShot>(null);
72
+
73
+ const stopStream = useCallback(() => {
74
+ closeCameraStream(streamRef.current);
75
+ streamRef.current = null;
76
+ setStreamUrl(null);
77
+ }, []);
78
+
79
+ const {phase: livenessPhase, progress, guideText, reset: resetLiveness} =
80
+ useLivenessFlow({
81
+ enabled: livenessEnabled && phase === 'live',
82
+ onVerified: () => setCaptureReady(true),
83
+ });
84
+
85
+ useEffect(() => {
86
+ if (!visible) {
87
+ stopStream();
88
+ return;
89
+ }
90
+ setActiveFacing(initialFacing);
91
+ setLivenessEnabled(defaultLiveness);
92
+ setCaptureReady(!defaultLiveness);
93
+ resetLiveness();
94
+ }, [defaultLiveness, initialFacing, resetLiveness, stopStream, visible, ctx.toolCallId]);
95
+
96
+ useEffect(() => {
97
+ if (!visible) {
98
+ return;
99
+ }
100
+
101
+ let cancelled = false;
102
+ setPhase('opening');
103
+ setErrorMessage(null);
104
+ setStatusMessage(null);
105
+ stopStream();
106
+
107
+ (async () => {
108
+ const cameraOk = await ensureCameraPermission();
109
+ if (!cameraOk) {
110
+ if (!cancelled) {
111
+ setPhase('error');
112
+ setErrorMessage('Camera access denied. Please allow camera permissions.');
113
+ }
114
+ return;
115
+ }
116
+
117
+ try {
118
+ const stream = await openCameraStream(activeFacing);
119
+ if (cancelled) {
120
+ closeCameraStream(stream);
121
+ return;
122
+ }
123
+ streamRef.current = stream;
124
+ setStreamUrl(stream.toURL());
125
+ setPhase('live');
126
+ if (livenessEnabled) {
127
+ setCaptureReady(false);
128
+ resetLiveness();
129
+ }
130
+ } catch (err: unknown) {
131
+ if (!cancelled) {
132
+ const message =
133
+ err instanceof Error
134
+ ? err.message
135
+ : 'Camera access denied. Please allow camera permissions.';
136
+ setPhase('error');
137
+ setErrorMessage(message);
138
+ }
139
+ }
140
+ })();
141
+
142
+ return () => {
143
+ cancelled = true;
144
+ stopStream();
145
+ };
146
+ }, [
147
+ activeFacing,
148
+ livenessEnabled,
149
+ resetLiveness,
150
+ stopStream,
151
+ visible,
152
+ ctx.toolCallId,
153
+ ]);
154
+
155
+ const switchFacing = useCallback(
156
+ (next: FacingMode) => {
157
+ if (next === activeFacing || phase === 'capturing') {
158
+ return;
159
+ }
160
+ setActiveFacing(next);
161
+ if (livenessEnabled) {
162
+ setCaptureReady(false);
163
+ }
164
+ },
165
+ [activeFacing, livenessEnabled, phase],
166
+ );
167
+
168
+ const handleLivenessToggle = useCallback(
169
+ (value: boolean) => {
170
+ setLivenessEnabled(value);
171
+ setCaptureReady(!value);
172
+ resetLiveness();
173
+ },
174
+ [resetLiveness],
175
+ );
176
+
177
+ const handleCapture = useCallback(async () => {
178
+ if (!captureReady || phase !== 'live' || !viewShotRef.current) {
179
+ return;
180
+ }
181
+ if (!client?.uploadMedia) {
182
+ setErrorMessage('Upload is not available — start a voice call first.');
183
+ return;
184
+ }
185
+
186
+ setPhase('capturing');
187
+ setStatusMessage('Sending photo…');
188
+ setErrorMessage(null);
189
+
190
+ try {
191
+ const uri = await viewShotRef.current.capture?.();
192
+ if (!uri) {
193
+ throw new Error('Capture failed');
194
+ }
195
+
196
+ stopStream();
197
+
198
+ const fileName = buildCaptureFileName();
199
+ const file: RNUploadFile = {
200
+ uri,
201
+ type: 'image/jpeg',
202
+ name: fileName,
203
+ };
204
+
205
+ const response = await client.uploadMedia(
206
+ file,
207
+ reason,
208
+ 'User captured a photo via camera',
209
+ );
210
+
211
+ setStatusMessage('✓ Photo sent!');
212
+ setTimeout(() => {
213
+ onComplete({
214
+ file_name: fileName,
215
+ file_type: 'image/jpeg',
216
+ file_size: file.size ?? 0,
217
+ media_id: response.media_id,
218
+ url: response.url,
219
+ message: 'User captured and sent a photo.',
220
+ });
221
+ }, SUCCESS_DELAY_MS);
222
+ } catch (err: unknown) {
223
+ const message = err instanceof Error ? err.message : 'Capture failed';
224
+ setPhase('live');
225
+ setStatusMessage(null);
226
+ setErrorMessage(message);
227
+ }
228
+ }, [captureReady, client, onComplete, phase, reason, stopStream]);
229
+
230
+ const handleClose = useCallback(() => {
231
+ stopStream();
232
+ onCancel('User closed');
233
+ }, [onCancel, stopStream]);
234
+
235
+ const handleCancelPress = useCallback(() => {
236
+ stopStream();
237
+ onCancel('User cancelled');
238
+ }, [onCancel, stopStream]);
239
+
240
+ const showGuide = livenessEnabled && phase === 'live';
241
+ const livenessVerified = livenessPhase === 'verified';
242
+ const canSwitchCamera = phase === 'live' || phase === 'opening';
243
+
244
+ return (
245
+ <PresetShellModal
246
+ visible={visible}
247
+ title={title}
248
+ subtitle={description}
249
+ onClose={handleClose}
250
+ hideDefaultClose>
251
+ <CallSpeakerToggle compact disabled={phase === 'capturing'} />
252
+
253
+ <View style={styles.facingRow}>
254
+ <Pressable
255
+ style={[
256
+ styles.facingChip,
257
+ activeFacing === 'environment' && styles.facingChipActive,
258
+ !canSwitchCamera && styles.facingChipDisabled,
259
+ ]}
260
+ onPress={() => switchFacing('environment')}
261
+ disabled={!canSwitchCamera || activeFacing === 'environment'}>
262
+ <Text
263
+ style={[
264
+ styles.facingChipText,
265
+ activeFacing === 'environment' && styles.facingChipTextActive,
266
+ ]}>
267
+ Back camera
268
+ </Text>
269
+ </Pressable>
270
+ <Pressable
271
+ style={[
272
+ styles.facingChip,
273
+ activeFacing === 'user' && styles.facingChipActive,
274
+ !canSwitchCamera && styles.facingChipDisabled,
275
+ ]}
276
+ onPress={() => switchFacing('user')}
277
+ disabled={!canSwitchCamera || activeFacing === 'user'}>
278
+ <Text
279
+ style={[
280
+ styles.facingChipText,
281
+ activeFacing === 'user' && styles.facingChipTextActive,
282
+ ]}>
283
+ Front camera
284
+ </Text>
285
+ </Pressable>
286
+ </View>
287
+
288
+ <View style={styles.livenessRow}>
289
+ <Text style={styles.livenessLabel}>Liveness Verification</Text>
290
+ <Switch
291
+ value={livenessEnabled}
292
+ onValueChange={handleLivenessToggle}
293
+ disabled={phase === 'capturing'}
294
+ />
295
+ </View>
296
+
297
+ <View style={styles.videoWrap}>
298
+ {phase === 'opening' ? (
299
+ <View style={styles.overlay}>
300
+ <ActivityIndicator color="#fff" size="large" />
301
+ <Text style={styles.overlayText}>Opening camera…</Text>
302
+ </View>
303
+ ) : null}
304
+
305
+ {phase === 'error' ? (
306
+ <View style={styles.overlay}>
307
+ <Text style={styles.errorOverlayText}>{errorMessage}</Text>
308
+ </View>
309
+ ) : null}
310
+
311
+ {streamUrl && phase !== 'error' ? (
312
+ <ViewShot
313
+ ref={viewShotRef}
314
+ style={styles.viewShot}
315
+ options={{format: 'jpg', quality: 0.85}}>
316
+ <RTCView
317
+ streamURL={streamUrl}
318
+ style={styles.video}
319
+ objectFit="cover"
320
+ mirror={activeFacing === 'user'}
321
+ />
322
+ </ViewShot>
323
+ ) : null}
324
+
325
+ {phase === 'live' ? (
326
+ <View style={styles.liveBadge}>
327
+ <View style={styles.liveDot} />
328
+ <Text style={styles.liveText}>LIVE</Text>
329
+ </View>
330
+ ) : null}
331
+
332
+ {showGuide ? (
333
+ <View
334
+ style={[
335
+ styles.faceGuide,
336
+ livenessVerified && styles.faceGuideVerified,
337
+ ]}
338
+ pointerEvents="none">
339
+ <Text
340
+ style={[
341
+ styles.guideText,
342
+ livenessVerified && styles.guideTextVerified,
343
+ ]}>
344
+ {guideText}
345
+ </Text>
346
+ </View>
347
+ ) : null}
348
+ </View>
349
+
350
+ {showGuide ? (
351
+ <View style={styles.progressBlock}>
352
+ <View style={styles.progressHeader}>
353
+ <Text style={styles.progressLabel}>LIVENESS ANALYSIS</Text>
354
+ <Text
355
+ style={[
356
+ styles.progressPct,
357
+ livenessVerified && styles.progressPctVerified,
358
+ ]}>
359
+ {progress}%
360
+ </Text>
361
+ </View>
362
+ <View style={styles.progressTrack}>
363
+ <View
364
+ style={[
365
+ styles.progressFill,
366
+ {width: `${progress}%`},
367
+ livenessVerified && styles.progressFillVerified,
368
+ ]}
369
+ />
370
+ </View>
371
+ </View>
372
+ ) : null}
373
+
374
+ {statusMessage ? (
375
+ <View style={styles.statusRow}>
376
+ {phase === 'capturing' ? <ActivityIndicator color="#4f46e5" /> : null}
377
+ <Text style={styles.statusText}>{statusMessage}</Text>
378
+ </View>
379
+ ) : null}
380
+ {errorMessage && phase !== 'error' ? (
381
+ <Text style={styles.errorText}>{errorMessage}</Text>
382
+ ) : null}
383
+
384
+ <View style={styles.actions}>
385
+ <Pressable
386
+ style={styles.cancelBtn}
387
+ onPress={handleCancelPress}
388
+ disabled={phase === 'capturing'}>
389
+ <Text style={styles.cancelBtnText}>Cancel</Text>
390
+ </Pressable>
391
+ <Pressable
392
+ style={[
393
+ styles.captureBtn,
394
+ (!captureReady || phase !== 'live') && styles.captureDisabled,
395
+ ]}
396
+ onPress={handleCapture}
397
+ disabled={!captureReady || phase !== 'live'}>
398
+ {phase === 'capturing' ? (
399
+ <ActivityIndicator color="#fff" />
400
+ ) : (
401
+ <Text style={styles.captureBtnText}>Capture & Send</Text>
402
+ )}
403
+ </Pressable>
404
+ </View>
405
+ </PresetShellModal>
406
+ );
407
+ }
408
+
409
+ const styles = StyleSheet.create({
410
+ facingRow: {
411
+ flexDirection: 'row',
412
+ gap: 8,
413
+ marginBottom: 10,
414
+ },
415
+ facingChip: {
416
+ flex: 1,
417
+ paddingVertical: 9,
418
+ borderRadius: 10,
419
+ borderWidth: 1,
420
+ borderColor: '#e5e7eb',
421
+ backgroundColor: '#f9fafb',
422
+ alignItems: 'center',
423
+ },
424
+ facingChipActive: {
425
+ backgroundColor: '#4f46e5',
426
+ borderColor: '#4f46e5',
427
+ },
428
+ facingChipDisabled: {opacity: 0.6},
429
+ facingChipText: {fontSize: 12, fontWeight: '600', color: '#374151'},
430
+ facingChipTextActive: {color: '#fff'},
431
+ livenessRow: {
432
+ flexDirection: 'row',
433
+ alignItems: 'center',
434
+ justifyContent: 'space-between',
435
+ backgroundColor: '#f9fafb',
436
+ borderWidth: 1,
437
+ borderColor: '#e5e7eb',
438
+ borderRadius: 10,
439
+ paddingHorizontal: 12,
440
+ paddingVertical: 8,
441
+ marginBottom: 12,
442
+ },
443
+ livenessLabel: {fontSize: 12, fontWeight: '600', color: '#374151'},
444
+ videoWrap: {
445
+ position: 'relative',
446
+ borderRadius: 12,
447
+ overflow: 'hidden',
448
+ backgroundColor: '#0f172a',
449
+ minHeight: 240,
450
+ aspectRatio: 4 / 3,
451
+ },
452
+ viewShot: {flex: 1},
453
+ video: {flex: 1, minHeight: 240},
454
+ overlay: {
455
+ ...StyleSheet.absoluteFill,
456
+ alignItems: 'center',
457
+ justifyContent: 'center',
458
+ gap: 12,
459
+ backgroundColor: '#0f172a',
460
+ zIndex: 2,
461
+ },
462
+ overlayText: {color: '#94a3b8', fontSize: 13},
463
+ errorOverlayText: {
464
+ color: '#f87171',
465
+ fontSize: 13,
466
+ textAlign: 'center',
467
+ paddingHorizontal: 24,
468
+ },
469
+ liveBadge: {
470
+ position: 'absolute',
471
+ top: 10,
472
+ left: 10,
473
+ flexDirection: 'row',
474
+ alignItems: 'center',
475
+ gap: 6,
476
+ backgroundColor: 'rgba(15,23,42,0.75)',
477
+ paddingHorizontal: 8,
478
+ paddingVertical: 4,
479
+ borderRadius: 6,
480
+ zIndex: 3,
481
+ },
482
+ liveDot: {
483
+ width: 8,
484
+ height: 8,
485
+ borderRadius: 4,
486
+ backgroundColor: '#ef4444',
487
+ },
488
+ liveText: {color: '#fff', fontSize: 11, fontWeight: '700'},
489
+ faceGuide: {
490
+ position: 'absolute',
491
+ alignSelf: 'center',
492
+ top: '18%',
493
+ width: 140,
494
+ height: 180,
495
+ borderWidth: 3,
496
+ borderColor: 'rgba(255,255,255,0.45)',
497
+ borderRadius: 90,
498
+ borderStyle: 'dashed',
499
+ zIndex: 3,
500
+ alignItems: 'center',
501
+ justifyContent: 'flex-end',
502
+ paddingBottom: 8,
503
+ },
504
+ faceGuideVerified: {borderColor: '#10b981'},
505
+ guideText: {
506
+ position: 'absolute',
507
+ bottom: -42,
508
+ left: -40,
509
+ right: -40,
510
+ textAlign: 'center',
511
+ color: '#fff',
512
+ fontSize: 11,
513
+ fontWeight: '600',
514
+ backgroundColor: 'rgba(15,23,42,0.85)',
515
+ paddingHorizontal: 10,
516
+ paddingVertical: 6,
517
+ borderRadius: 6,
518
+ },
519
+ guideTextVerified: {color: '#10b981'},
520
+ progressBlock: {marginTop: 12},
521
+ progressHeader: {
522
+ flexDirection: 'row',
523
+ justifyContent: 'space-between',
524
+ marginBottom: 6,
525
+ },
526
+ progressLabel: {
527
+ fontSize: 11,
528
+ fontWeight: '700',
529
+ color: '#6b7280',
530
+ letterSpacing: 0.5,
531
+ },
532
+ progressPct: {fontSize: 11, fontWeight: '700', color: '#6366f1'},
533
+ progressPctVerified: {color: '#10b981'},
534
+ progressTrack: {
535
+ height: 6,
536
+ backgroundColor: '#f3f4f6',
537
+ borderRadius: 3,
538
+ overflow: 'hidden',
539
+ },
540
+ progressFill: {height: '100%', backgroundColor: '#6366f1'},
541
+ progressFillVerified: {backgroundColor: '#10b981'},
542
+ statusRow: {
543
+ flexDirection: 'row',
544
+ alignItems: 'center',
545
+ gap: 8,
546
+ marginTop: 12,
547
+ justifyContent: 'center',
548
+ },
549
+ statusText: {fontSize: 13, color: '#22c55e', fontWeight: '600'},
550
+ errorText: {
551
+ fontSize: 13,
552
+ color: '#ef4444',
553
+ textAlign: 'center',
554
+ marginTop: 12,
555
+ },
556
+ actions: {flexDirection: 'row', gap: 10, marginTop: 14},
557
+ cancelBtn: {
558
+ flex: 1,
559
+ paddingVertical: 12,
560
+ borderRadius: 10,
561
+ backgroundColor: '#f3f4f6',
562
+ alignItems: 'center',
563
+ },
564
+ cancelBtnText: {color: '#111', fontWeight: '600'},
565
+ captureBtn: {
566
+ flex: 1,
567
+ paddingVertical: 12,
568
+ borderRadius: 10,
569
+ backgroundColor: '#4f46e5',
570
+ alignItems: 'center',
571
+ justifyContent: 'center',
572
+ minHeight: 44,
573
+ },
574
+ captureDisabled: {opacity: 0.5},
575
+ captureBtnText: {color: '#fff', fontWeight: '700'},
576
+ });