@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.
- package/lib/components/VerifyAIScanner.js +75 -4
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +5 -1
- package/src/components/VerifyAIScanner.tsx +96 -3
- package/src/version.ts +1 -1
|
@@ -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,
|
|
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:
|
|
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:
|
|
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.
|
|
1
|
+
export declare const SDK_VERSION = "2.4.9";
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.4.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1
|
+
export const SDK_VERSION = '2.4.9';
|