@switchlabs/verify-ai-react-native 2.4.7 → 2.4.9

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.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useState, useCallback, useEffect } from 'react';
3
- import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, AppState, useWindowDimensions, } from 'react-native';
3
+ import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, AppState, Platform, useWindowDimensions, } from 'react-native';
4
4
  import { CameraView, useCameraPermissions, } from 'expo-camera';
5
5
  import { useTelemetry } from '../telemetry/TelemetryContext';
6
6
  /** Quality used when expo-image-manipulator is not available (lower = smaller). */
@@ -93,6 +93,66 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
93
93
  const { width: windowWidth, height: windowHeight } = useWindowDimensions();
94
94
  const isLandscape = windowWidth > windowHeight;
95
95
  const prevDimensionsRef = useRef({ width: windowWidth, height: windowHeight });
96
+ // Track physical device orientation independently of interface orientation.
97
+ // When the host app is orientation-locked, window dimensions don't change on
98
+ // rotation. iOS uses expo-camera's responsive-orientation callback; Android
99
+ // uses the accelerometer via expo-sensors (the callback is iOS-only). Either
100
+ // way we rotate the overlay UI to stay readable from the user's viewpoint.
101
+ const [physicalOrientation, setPhysicalOrientation] = useState('portrait');
102
+ useEffect(() => {
103
+ if (Platform.OS !== 'android')
104
+ return;
105
+ let subscription = null;
106
+ let cancelled = false;
107
+ let lastOrientation = 'portrait';
108
+ (async () => {
109
+ let Accelerometer = null;
110
+ try {
111
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
112
+ const mod = require('expo-sensors');
113
+ Accelerometer = mod?.Accelerometer ?? null;
114
+ }
115
+ catch {
116
+ return;
117
+ }
118
+ if (cancelled || !Accelerometer)
119
+ return;
120
+ Accelerometer.setUpdateInterval(500);
121
+ subscription = Accelerometer.addListener(({ x, y }) => {
122
+ let next;
123
+ if (Math.abs(x) > Math.abs(y) + 0.2) {
124
+ next = x > 0 ? 'landscapeRight' : 'landscapeLeft';
125
+ }
126
+ else if (Math.abs(y) > Math.abs(x) + 0.2) {
127
+ next = y > 0 ? 'portraitUpsideDown' : 'portrait';
128
+ }
129
+ else {
130
+ return; // ambiguous tilt — ignore
131
+ }
132
+ if (next !== lastOrientation) {
133
+ lastOrientation = next;
134
+ setPhysicalOrientation(next);
135
+ }
136
+ });
137
+ })();
138
+ return () => {
139
+ cancelled = true;
140
+ subscription?.remove();
141
+ };
142
+ }, []);
143
+ const overlayRotationDeg = (() => {
144
+ switch (physicalOrientation) {
145
+ case 'landscapeLeft':
146
+ return 90;
147
+ case 'landscapeRight':
148
+ return -90;
149
+ case 'portraitUpsideDown':
150
+ return 180;
151
+ case 'portrait':
152
+ default:
153
+ return 0;
154
+ }
155
+ })();
96
156
  // Detect orientation changes and remount camera after rotation settles.
97
157
  // On iOS, AVCaptureVideoPreviewLayer distorts if remounted during the rotation
98
158
  // animation — the native preview layer initializes with transitional bounds.
@@ -369,13 +429,21 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
369
429
  return (_jsxs(View, { style: [styles.container, styles.permissionContainer, style], children: [_jsx(Text, { style: styles.permissionText, children: "Camera access is required for photo verification" }), _jsx(TouchableOpacity, { style: styles.permissionButton, onPress: requestPermission, children: _jsx(Text, { style: styles.permissionButtonText, children: "Grant Camera Access" }) })] }));
370
430
  }
371
431
  const showBottomCard = status === 'success' || status === 'error';
372
- return (_jsx(View, { style: [styles.container, style], children: _jsx(CameraView, { ref: cameraRef, style: styles.camera, facing: "back", enableTorch: !terminated && enableTorch, onCameraReady: onCameraReady, onMountError: onMountError, responsiveOrientationWhenOrientationLocked: true, children: _jsxs(View, { style: styles.overlay, children: [overlay?.title && (_jsx(View, { style: [styles.topBar, isLandscape && styles.topBarLandscape], children: _jsx(Text, { style: styles.titleText, children: overlay.title }) })), overlay?.showGuideFrame && (_jsxs(View, { style: [styles.guideContainer, isLandscape && styles.guideContainerLandscape], children: [_jsxs(View, { style: [
432
+ return (_jsx(View, { style: [styles.container, style], children: _jsx(CameraView, { ref: cameraRef, style: styles.camera, facing: "back", enableTorch: !terminated && enableTorch, onCameraReady: onCameraReady, onMountError: onMountError, responsiveOrientationWhenOrientationLocked: true, onResponsiveOrientationChanged: (event) => {
433
+ setPhysicalOrientation(event.orientation);
434
+ }, children: _jsxs(View, { style: styles.overlay, children: [overlay?.title && (_jsx(View, { style: [styles.topBar, isLandscape && styles.topBarLandscape], children: _jsx(Text, { style: [
435
+ styles.titleText,
436
+ { transform: [{ rotate: `${overlayRotationDeg}deg` }] },
437
+ ], children: overlay.title }) })), overlay?.showGuideFrame && (_jsxs(View, { style: [styles.guideContainer, isLandscape && styles.guideContainerLandscape], children: [_jsxs(View, { style: [
373
438
  styles.guideFrame,
374
439
  isLandscape && styles.guideFrameLandscape,
375
440
  overlay.guideFrameAspectRatio
376
441
  ? { aspectRatio: overlay.guideFrameAspectRatio }
377
442
  : undefined,
378
- ], children: [overlay.guideOverlayContent && (_jsx(View, { style: [StyleSheet.absoluteFill, { opacity: overlay.guideOverlayOpacity ?? 0.3 }], children: overlay.guideOverlayContent })), _jsx(View, { style: [styles.corner, styles.cornerTopLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerTopRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerBottomLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerBottomRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] })] }), overlay.guideCaption && (_jsx(Text, { style: styles.guideCaptionText, children: overlay.guideCaption }))] })), status === 'processing' && (_jsxs(View, { style: styles.processingOverlay, children: [_jsx(ActivityIndicator, { size: "large", color: "#fff" }), _jsx(Text, { style: styles.statusText, children: overlay?.processingMessage || 'Analyzing photo...' })] })), showBottomCard && _jsx(View, { style: styles.cardBackdrop }), _jsxs(View, { style: [styles.bottomArea, isLandscape && styles.bottomAreaLandscape], children: [status === 'success' && result && (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [
443
+ ], children: [overlay.guideOverlayContent && (_jsx(View, { style: [StyleSheet.absoluteFill, { opacity: overlay.guideOverlayOpacity ?? 0.3 }], children: overlay.guideOverlayContent })), _jsx(View, { style: [styles.corner, styles.cornerTopLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerTopRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerBottomLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerBottomRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] })] }), overlay.guideCaption && (_jsx(Text, { style: [
444
+ styles.guideCaptionText,
445
+ { transform: [{ rotate: `${overlayRotationDeg}deg` }] },
446
+ ], children: overlay.guideCaption }))] })), status === 'processing' && (_jsxs(View, { style: styles.processingOverlay, children: [_jsx(ActivityIndicator, { size: "large", color: "#fff" }), _jsx(Text, { style: styles.statusText, children: overlay?.processingMessage || 'Analyzing photo...' })] })), showBottomCard && _jsx(View, { style: styles.cardBackdrop }), _jsxs(View, { style: [styles.bottomArea, isLandscape && styles.bottomAreaLandscape], children: [status === 'success' && result && (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [
379
447
  styles.resultIconCircle,
380
448
  exhausted
381
449
  ? styles.resultIconExhausted
@@ -419,7 +487,10 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
419
487
  errorMessage = display.message;
420
488
  }
421
489
  return (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [styles.resultIconCircle, styles.resultIconError, overlay?.theme?.failureColor ? { backgroundColor: overlay.theme.failureColor } : undefined], children: _jsx(Text, { style: styles.resultIcon, children: "!" }) }), _jsx(Text, { style: [styles.resultLabel, styles.resultLabelError, overlay?.theme?.failureColor ? { color: overlay.theme.failureColor } : undefined], children: errorTitle })] }), _jsx(Text, { style: styles.feedbackText, children: errorMessage })] }));
422
- })(), !showBottomCard && (_jsxs(_Fragment, { children: [overlay?.instructions && status === 'idle' && (_jsx(Text, { style: styles.instructionsText, children: overlay.instructions })), showCaptureButton && (_jsx(View, { style: styles.captureButtonRow, children: _jsx(TouchableOpacity, { style: [
490
+ })(), !showBottomCard && (_jsxs(_Fragment, { children: [overlay?.instructions && status === 'idle' && (_jsx(Text, { style: [
491
+ styles.instructionsText,
492
+ { transform: [{ rotate: `${overlayRotationDeg}deg` }] },
493
+ ], children: overlay.instructions })), showCaptureButton && (_jsx(View, { style: styles.captureButtonRow, children: _jsx(TouchableOpacity, { style: [
423
494
  styles.captureButton,
424
495
  overlay?.theme?.captureButtonColor ? { borderColor: overlay.theme.captureButtonColor } : undefined,
425
496
  (!cameraReady || status === 'capturing' || status === 'processing') &&
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "2.4.7";
1
+ export declare const SDK_VERSION = "2.4.9";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.4.7';
1
+ export const SDK_VERSION = '2.4.9';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchlabs/verify-ai-react-native",
3
- "version": "2.4.7",
3
+ "version": "2.4.9",
4
4
  "description": "React Native SDK for Verify AI - photo verification with AI vision processing",
5
5
  "main": "./lib/index.js",
6
6
  "types": "./lib/index.d.ts",
@@ -43,6 +43,7 @@
43
43
  "expo-camera": ">=15.0.0",
44
44
  "expo-file-system": ">=17.0.0",
45
45
  "expo-image-manipulator": ">=12.0.0",
46
+ "expo-sensors": ">=13.0.0",
46
47
  "react": ">=18.0.0",
47
48
  "react-native": ">=0.72.0",
48
49
  "react-native-fast-tflite": ">=1.0.0"
@@ -54,6 +55,9 @@
54
55
  "expo-image-manipulator": {
55
56
  "optional": true
56
57
  },
58
+ "expo-sensors": {
59
+ "optional": true
60
+ },
57
61
  "@react-native-async-storage/async-storage": {
58
62
  "optional": true
59
63
  },
@@ -6,6 +6,7 @@ import {
6
6
  StyleSheet,
7
7
  ActivityIndicator,
8
8
  AppState,
9
+ Platform,
9
10
  useWindowDimensions,
10
11
  type ViewStyle,
11
12
  } from 'react-native';
@@ -159,6 +160,74 @@ export function VerifyAIScanner({
159
160
  const isLandscape = windowWidth > windowHeight;
160
161
  const prevDimensionsRef = useRef({ width: windowWidth, height: windowHeight });
161
162
 
163
+ // Track physical device orientation independently of interface orientation.
164
+ // When the host app is orientation-locked, window dimensions don't change on
165
+ // rotation. iOS uses expo-camera's responsive-orientation callback; Android
166
+ // uses the accelerometer via expo-sensors (the callback is iOS-only). Either
167
+ // way we rotate the overlay UI to stay readable from the user's viewpoint.
168
+ const [physicalOrientation, setPhysicalOrientation] = useState<
169
+ 'portrait' | 'portraitUpsideDown' | 'landscapeLeft' | 'landscapeRight'
170
+ >('portrait');
171
+
172
+ useEffect(() => {
173
+ if (Platform.OS !== 'android') return;
174
+ let subscription: { remove: () => void } | null = null;
175
+ let cancelled = false;
176
+ let lastOrientation: typeof physicalOrientation = 'portrait';
177
+
178
+ (async () => {
179
+ let Accelerometer: {
180
+ setUpdateInterval: (ms: number) => void;
181
+ addListener: (
182
+ cb: (data: { x: number; y: number; z: number }) => void,
183
+ ) => { remove: () => void };
184
+ } | null = null;
185
+ try {
186
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
187
+ const mod = require('expo-sensors');
188
+ Accelerometer = mod?.Accelerometer ?? null;
189
+ } catch {
190
+ return;
191
+ }
192
+ if (cancelled || !Accelerometer) return;
193
+
194
+ Accelerometer.setUpdateInterval(500);
195
+ subscription = Accelerometer.addListener(({ x, y }) => {
196
+ let next: typeof physicalOrientation;
197
+ if (Math.abs(x) > Math.abs(y) + 0.2) {
198
+ next = x > 0 ? 'landscapeRight' : 'landscapeLeft';
199
+ } else if (Math.abs(y) > Math.abs(x) + 0.2) {
200
+ next = y > 0 ? 'portraitUpsideDown' : 'portrait';
201
+ } else {
202
+ return; // ambiguous tilt — ignore
203
+ }
204
+ if (next !== lastOrientation) {
205
+ lastOrientation = next;
206
+ setPhysicalOrientation(next);
207
+ }
208
+ });
209
+ })();
210
+
211
+ return () => {
212
+ cancelled = true;
213
+ subscription?.remove();
214
+ };
215
+ }, []);
216
+
217
+ const overlayRotationDeg = (() => {
218
+ switch (physicalOrientation) {
219
+ case 'landscapeLeft':
220
+ return 90;
221
+ case 'landscapeRight':
222
+ return -90;
223
+ case 'portraitUpsideDown':
224
+ return 180;
225
+ case 'portrait':
226
+ default:
227
+ return 0;
228
+ }
229
+ })();
230
+
162
231
  // Detect orientation changes and remount camera after rotation settles.
163
232
  // On iOS, AVCaptureVideoPreviewLayer distorts if remounted during the rotation
164
233
  // animation — the native preview layer initializes with transitional bounds.
@@ -495,12 +564,22 @@ export function VerifyAIScanner({
495
564
  onCameraReady={onCameraReady}
496
565
  onMountError={onMountError}
497
566
  responsiveOrientationWhenOrientationLocked
567
+ onResponsiveOrientationChanged={(event) => {
568
+ setPhysicalOrientation(event.orientation);
569
+ }}
498
570
  >
499
571
  {/* Overlay */}
500
572
  <View style={styles.overlay}>
501
573
  {overlay?.title && (
502
574
  <View style={[styles.topBar, isLandscape && styles.topBarLandscape]}>
503
- <Text style={styles.titleText}>{overlay.title}</Text>
575
+ <Text
576
+ style={[
577
+ styles.titleText,
578
+ { transform: [{ rotate: `${overlayRotationDeg}deg` }] },
579
+ ]}
580
+ >
581
+ {overlay.title}
582
+ </Text>
504
583
  </View>
505
584
  )}
506
585
 
@@ -528,7 +607,14 @@ export function VerifyAIScanner({
528
607
  <View style={[styles.corner, styles.cornerBottomRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined]} />
529
608
  </View>
530
609
  {overlay.guideCaption && (
531
- <Text style={styles.guideCaptionText}>{overlay.guideCaption}</Text>
610
+ <Text
611
+ style={[
612
+ styles.guideCaptionText,
613
+ { transform: [{ rotate: `${overlayRotationDeg}deg` }] },
614
+ ]}
615
+ >
616
+ {overlay.guideCaption}
617
+ </Text>
532
618
  )}
533
619
  </View>
534
620
  )}
@@ -630,7 +716,14 @@ export function VerifyAIScanner({
630
716
  {!showBottomCard && (
631
717
  <>
632
718
  {overlay?.instructions && status === 'idle' && (
633
- <Text style={styles.instructionsText}>{overlay.instructions}</Text>
719
+ <Text
720
+ style={[
721
+ styles.instructionsText,
722
+ { transform: [{ rotate: `${overlayRotationDeg}deg` }] },
723
+ ]}
724
+ >
725
+ {overlay.instructions}
726
+ </Text>
634
727
  )}
635
728
  {showCaptureButton && (
636
729
  <View style={styles.captureButtonRow}>
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.4.7';
1
+ export const SDK_VERSION = '2.4.9';