@switchlabs/verify-ai-react-native 0.1.0 → 0.1.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.
- package/lib/components/VerifyAIScanner.js +137 -63
- package/lib/hooks/useVerifyAI.d.ts +1 -1
- package/lib/hooks/useVerifyAI.js +11 -2
- package/lib/index.d.ts +1 -2
- package/lib/index.js +0 -4
- package/package.json +24 -2
- package/src/components/VerifyAIScanner.tsx +204 -94
- package/src/hooks/useVerifyAI.ts +12 -2
- package/src/index.ts +3 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useState, useCallback } from 'react';
|
|
3
3
|
import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, } from 'react-native';
|
|
4
4
|
import { CameraView, useCameraPermissions, } from 'expo-camera';
|
|
@@ -48,6 +48,7 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
|
|
|
48
48
|
setResult(verificationResult);
|
|
49
49
|
setStatus('success');
|
|
50
50
|
onResult?.(verificationResult);
|
|
51
|
+
setTimeout(() => setStatus('idle'), 3000);
|
|
51
52
|
}
|
|
52
53
|
else {
|
|
53
54
|
// null result means queued for offline
|
|
@@ -72,19 +73,27 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
|
|
|
72
73
|
if (!permission.granted) {
|
|
73
74
|
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" }) })] }));
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
const showBottomCard = status === 'success' || status === 'error';
|
|
77
|
+
return (_jsx(View, { style: [styles.container, style], children: _jsx(CameraView, { ref: cameraRef, style: styles.camera, facing: "back", children: _jsxs(View, { style: styles.overlay, children: [overlay?.title && (_jsx(View, { style: styles.topBar, children: _jsx(Text, { style: styles.titleText, children: overlay.title }) })), overlay?.showGuideFrame && (_jsx(View, { style: styles.guideContainer, children: _jsxs(View, { style: [
|
|
78
|
+
styles.guideFrame,
|
|
79
|
+
overlay.guideFrameAspectRatio
|
|
80
|
+
? { aspectRatio: overlay.guideFrameAspectRatio }
|
|
81
|
+
: undefined,
|
|
82
|
+
], children: [_jsx(View, { style: [styles.corner, styles.cornerTopLeft] }), _jsx(View, { style: [styles.corner, styles.cornerTopRight] }), _jsx(View, { style: [styles.corner, styles.cornerBottomLeft] }), _jsx(View, { style: [styles.corner, styles.cornerBottomRight] })] }) })), status === 'processing' && (_jsxs(View, { style: styles.processingOverlay, children: [_jsx(ActivityIndicator, { size: "large", color: "#fff" }), _jsx(Text, { style: styles.statusText, children: "Analyzing photo..." })] })), showBottomCard && _jsx(View, { style: styles.cardBackdrop }), _jsxs(View, { style: styles.bottomArea, children: [status === 'success' && result && (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [
|
|
83
|
+
styles.resultIconCircle,
|
|
84
|
+
result.is_compliant ? styles.resultIconPass : styles.resultIconFail,
|
|
85
|
+
], children: _jsx(Text, { style: styles.resultIcon, children: result.is_compliant ? '\u2713' : '\u2717' }) }), _jsx(Text, { style: [
|
|
86
|
+
styles.resultLabel,
|
|
87
|
+
result.is_compliant ? styles.resultLabelPass : styles.resultLabelFail,
|
|
88
|
+
], children: result.is_compliant ? 'Verified' : 'Not Verified' })] }), _jsx(Text, { style: styles.feedbackText, children: result.feedback })] })), status === 'error' && (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [styles.resultIconCircle, styles.resultIconError], children: _jsx(Text, { style: styles.resultIcon, children: "!" }) }), _jsx(Text, { style: [styles.resultLabel, styles.resultLabelError], children: "Something went wrong" })] }), _jsx(Text, { style: styles.feedbackText, children: "We couldn't process your photo. Please try again." })] })), !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: [
|
|
89
|
+
styles.captureButton,
|
|
90
|
+
(status === 'capturing' || status === 'processing') &&
|
|
91
|
+
styles.captureButtonDisabled,
|
|
92
|
+
], onPress: handleCapture, disabled: status === 'capturing' || status === 'processing', activeOpacity: 0.7, children: _jsx(View, { style: styles.captureButtonInner }) }) }))] }))] })] }) }) }));
|
|
87
93
|
}
|
|
94
|
+
const CORNER_SIZE = 30;
|
|
95
|
+
const CORNER_THICKNESS = 3;
|
|
96
|
+
const CORNER_COLOR = 'rgba(255, 255, 255, 0.7)';
|
|
88
97
|
const styles = StyleSheet.create({
|
|
89
98
|
container: {
|
|
90
99
|
flex: 1,
|
|
@@ -107,6 +116,7 @@ const styles = StyleSheet.create({
|
|
|
107
116
|
fontSize: 18,
|
|
108
117
|
fontWeight: '600',
|
|
109
118
|
},
|
|
119
|
+
// Guide frame with corner brackets
|
|
110
120
|
guideContainer: {
|
|
111
121
|
flex: 1,
|
|
112
122
|
justifyContent: 'center',
|
|
@@ -116,66 +126,57 @@ const styles = StyleSheet.create({
|
|
|
116
126
|
guideFrame: {
|
|
117
127
|
width: '100%',
|
|
118
128
|
aspectRatio: 4 / 3,
|
|
119
|
-
borderWidth: 2,
|
|
120
|
-
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
121
|
-
borderRadius: 12,
|
|
122
|
-
borderStyle: 'dashed',
|
|
123
129
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
},
|
|
129
|
-
instructionsText: {
|
|
130
|
-
color: 'rgba(255, 255, 255, 0.8)',
|
|
131
|
-
fontSize: 14,
|
|
132
|
-
textAlign: 'center',
|
|
133
|
-
},
|
|
134
|
-
statusOverlay: {
|
|
135
|
-
...StyleSheet.absoluteFillObject,
|
|
136
|
-
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
137
|
-
justifyContent: 'center',
|
|
138
|
-
alignItems: 'center',
|
|
139
|
-
gap: 16,
|
|
130
|
+
corner: {
|
|
131
|
+
position: 'absolute',
|
|
132
|
+
width: CORNER_SIZE,
|
|
133
|
+
height: CORNER_SIZE,
|
|
140
134
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
135
|
+
cornerTopLeft: {
|
|
136
|
+
top: 0,
|
|
137
|
+
left: 0,
|
|
138
|
+
borderTopWidth: CORNER_THICKNESS,
|
|
139
|
+
borderLeftWidth: CORNER_THICKNESS,
|
|
140
|
+
borderColor: CORNER_COLOR,
|
|
141
|
+
borderTopLeftRadius: 4,
|
|
145
142
|
},
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
cornerTopRight: {
|
|
144
|
+
top: 0,
|
|
145
|
+
right: 0,
|
|
146
|
+
borderTopWidth: CORNER_THICKNESS,
|
|
147
|
+
borderRightWidth: CORNER_THICKNESS,
|
|
148
|
+
borderColor: CORNER_COLOR,
|
|
149
|
+
borderTopRightRadius: 4,
|
|
150
150
|
},
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
cornerBottomLeft: {
|
|
152
|
+
bottom: 0,
|
|
153
|
+
left: 0,
|
|
154
|
+
borderBottomWidth: CORNER_THICKNESS,
|
|
155
|
+
borderLeftWidth: CORNER_THICKNESS,
|
|
156
|
+
borderColor: CORNER_COLOR,
|
|
157
|
+
borderBottomLeftRadius: 4,
|
|
153
158
|
},
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
cornerBottomRight: {
|
|
160
|
+
bottom: 0,
|
|
161
|
+
right: 0,
|
|
162
|
+
borderBottomWidth: CORNER_THICKNESS,
|
|
163
|
+
borderRightWidth: CORNER_THICKNESS,
|
|
164
|
+
borderColor: CORNER_COLOR,
|
|
165
|
+
borderBottomRightRadius: 4,
|
|
156
166
|
},
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
// Bottom area: flex column for instructions + capture button
|
|
168
|
+
bottomArea: {
|
|
169
|
+
paddingBottom: 40,
|
|
170
|
+
alignItems: 'center',
|
|
161
171
|
},
|
|
162
|
-
|
|
163
|
-
color: '
|
|
172
|
+
instructionsText: {
|
|
173
|
+
color: 'rgba(255, 255, 255, 0.8)',
|
|
164
174
|
fontSize: 14,
|
|
165
175
|
textAlign: 'center',
|
|
166
|
-
paddingHorizontal:
|
|
167
|
-
|
|
168
|
-
},
|
|
169
|
-
errorText: {
|
|
170
|
-
color: '#ef4444',
|
|
171
|
-
fontSize: 16,
|
|
172
|
-
fontWeight: '500',
|
|
176
|
+
paddingHorizontal: 20,
|
|
177
|
+
marginBottom: 20,
|
|
173
178
|
},
|
|
174
|
-
|
|
175
|
-
position: 'absolute',
|
|
176
|
-
bottom: 40,
|
|
177
|
-
left: 0,
|
|
178
|
-
right: 0,
|
|
179
|
+
captureButtonRow: {
|
|
179
180
|
alignItems: 'center',
|
|
180
181
|
},
|
|
181
182
|
captureButton: {
|
|
@@ -196,6 +197,79 @@ const styles = StyleSheet.create({
|
|
|
196
197
|
borderRadius: 29,
|
|
197
198
|
backgroundColor: '#fff',
|
|
198
199
|
},
|
|
200
|
+
// Processing spinner overlay
|
|
201
|
+
processingOverlay: {
|
|
202
|
+
...StyleSheet.absoluteFillObject,
|
|
203
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
204
|
+
justifyContent: 'center',
|
|
205
|
+
alignItems: 'center',
|
|
206
|
+
gap: 16,
|
|
207
|
+
},
|
|
208
|
+
statusText: {
|
|
209
|
+
color: '#fff',
|
|
210
|
+
fontSize: 16,
|
|
211
|
+
fontWeight: '500',
|
|
212
|
+
},
|
|
213
|
+
// Bottom card for results and errors
|
|
214
|
+
cardBackdrop: {
|
|
215
|
+
...StyleSheet.absoluteFillObject,
|
|
216
|
+
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
217
|
+
},
|
|
218
|
+
resultCard: {
|
|
219
|
+
backgroundColor: '#fff',
|
|
220
|
+
borderRadius: 20,
|
|
221
|
+
paddingVertical: 24,
|
|
222
|
+
paddingHorizontal: 24,
|
|
223
|
+
marginHorizontal: 16,
|
|
224
|
+
alignItems: 'center',
|
|
225
|
+
gap: 12,
|
|
226
|
+
},
|
|
227
|
+
resultCardHeader: {
|
|
228
|
+
flexDirection: 'row',
|
|
229
|
+
alignItems: 'center',
|
|
230
|
+
gap: 12,
|
|
231
|
+
},
|
|
232
|
+
resultIconCircle: {
|
|
233
|
+
width: 36,
|
|
234
|
+
height: 36,
|
|
235
|
+
borderRadius: 18,
|
|
236
|
+
justifyContent: 'center',
|
|
237
|
+
alignItems: 'center',
|
|
238
|
+
},
|
|
239
|
+
resultIconPass: {
|
|
240
|
+
backgroundColor: '#22c55e',
|
|
241
|
+
},
|
|
242
|
+
resultIconFail: {
|
|
243
|
+
backgroundColor: '#ef4444',
|
|
244
|
+
},
|
|
245
|
+
resultIconError: {
|
|
246
|
+
backgroundColor: '#f59e0b',
|
|
247
|
+
},
|
|
248
|
+
resultIcon: {
|
|
249
|
+
color: '#fff',
|
|
250
|
+
fontSize: 20,
|
|
251
|
+
fontWeight: '700',
|
|
252
|
+
},
|
|
253
|
+
resultLabel: {
|
|
254
|
+
fontSize: 20,
|
|
255
|
+
fontWeight: '700',
|
|
256
|
+
},
|
|
257
|
+
resultLabelPass: {
|
|
258
|
+
color: '#15803d',
|
|
259
|
+
},
|
|
260
|
+
resultLabelFail: {
|
|
261
|
+
color: '#dc2626',
|
|
262
|
+
},
|
|
263
|
+
resultLabelError: {
|
|
264
|
+
color: '#b45309',
|
|
265
|
+
},
|
|
266
|
+
feedbackText: {
|
|
267
|
+
color: '#4b5563',
|
|
268
|
+
fontSize: 15,
|
|
269
|
+
textAlign: 'center',
|
|
270
|
+
lineHeight: 22,
|
|
271
|
+
},
|
|
272
|
+
// Permission screen
|
|
199
273
|
permissionContainer: {
|
|
200
274
|
justifyContent: 'center',
|
|
201
275
|
alignItems: 'center',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VerifyAIClient } from '../client';
|
|
2
|
-
import { OfflineQueue } from '../storage/offlineQueue';
|
|
2
|
+
import type { OfflineQueue } from '../storage/offlineQueue';
|
|
3
3
|
import type { VerifyAIConfig, VerificationRequest, VerificationResult, VerificationListParams, VerificationListResponse } from '../types';
|
|
4
4
|
export interface UseVerifyAIReturn {
|
|
5
5
|
/** Submit a verification. Uses offline queue if offlineMode is enabled and request fails. */
|
package/lib/hooks/useVerifyAI.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { useMemo, useCallback, useState, useEffect } from 'react';
|
|
2
2
|
import { AppState } from 'react-native';
|
|
3
3
|
import { VerifyAIClient } from '../client';
|
|
4
|
-
|
|
4
|
+
// Lazy-load OfflineQueue to avoid hard dependency on @react-native-async-storage/async-storage.
|
|
5
|
+
// Consumers who don't use offlineMode won't need it installed.
|
|
6
|
+
let OfflineQueueClass = null;
|
|
7
|
+
try {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
9
|
+
OfflineQueueClass = require('../storage/offlineQueue').OfflineQueue;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// AsyncStorage not installed — offlineMode will be unavailable
|
|
13
|
+
}
|
|
5
14
|
/**
|
|
6
15
|
* React hook for Verify AI. Provides verification methods,
|
|
7
16
|
* loading/error state, and optional offline queue management.
|
|
@@ -26,7 +35,7 @@ import { OfflineQueue } from '../storage/offlineQueue';
|
|
|
26
35
|
*/
|
|
27
36
|
export function useVerifyAI(config) {
|
|
28
37
|
const client = useMemo(() => new VerifyAIClient(config), [config.apiKey, config.baseUrl, config.timeout]);
|
|
29
|
-
const offlineQueue = useMemo(() => (config.offlineMode ? new
|
|
38
|
+
const offlineQueue = useMemo(() => (config.offlineMode && OfflineQueueClass ? new OfflineQueueClass(client) : null), [client, config.offlineMode]);
|
|
30
39
|
const [loading, setLoading] = useState(false);
|
|
31
40
|
const [error, setError] = useState(null);
|
|
32
41
|
const [lastResult, setLastResult] = useState(null);
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export { VerifyAIClient, VerifyAIRequestError } from './client';
|
|
2
2
|
export { useVerifyAI } from './hooks/useVerifyAI';
|
|
3
3
|
export type { UseVerifyAIReturn } from './hooks/useVerifyAI';
|
|
4
|
-
export { VerifyAIScanner } from './components/VerifyAIScanner';
|
|
5
4
|
export type { VerifyAIScannerProps } from './components/VerifyAIScanner';
|
|
6
|
-
export { OfflineQueue } from './storage/offlineQueue';
|
|
5
|
+
export type { OfflineQueue } from './storage/offlineQueue';
|
|
7
6
|
export type { VerifyAIConfig, VerificationRequest, VerificationResult, VerificationListResponse, VerificationListParams, QueueItem, VerifyAIError, ScannerStatus, ScannerOverlayConfig, } from './types';
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,3 @@
|
|
|
2
2
|
export { VerifyAIClient, VerifyAIRequestError } from './client';
|
|
3
3
|
// Hooks
|
|
4
4
|
export { useVerifyAI } from './hooks/useVerifyAI';
|
|
5
|
-
// Components
|
|
6
|
-
export { VerifyAIScanner } from './components/VerifyAIScanner';
|
|
7
|
-
// Offline Queue
|
|
8
|
-
export { OfflineQueue } from './storage/offlineQueue';
|
package/package.json
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchlabs/verify-ai-react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "React Native SDK for Verify AI - photo verification with AI vision processing",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
|
-
"module": "./lib/index.js",
|
|
7
6
|
"types": "./lib/index.d.ts",
|
|
8
7
|
"source": "./src/index.ts",
|
|
9
8
|
"react-native": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"react-native": "./src/index.ts",
|
|
12
|
+
"types": "./lib/index.d.ts",
|
|
13
|
+
"default": "./lib/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./scanner": {
|
|
16
|
+
"react-native": "./src/components/VerifyAIScanner.tsx",
|
|
17
|
+
"types": "./lib/components/VerifyAIScanner.d.ts",
|
|
18
|
+
"default": "./lib/components/VerifyAIScanner.js"
|
|
19
|
+
},
|
|
20
|
+
"./offline": {
|
|
21
|
+
"react-native": "./src/storage/offlineQueue.ts",
|
|
22
|
+
"types": "./lib/storage/offlineQueue.d.ts",
|
|
23
|
+
"default": "./lib/storage/offlineQueue.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"typesVersions": {
|
|
27
|
+
"*": {
|
|
28
|
+
"scanner": ["./lib/components/VerifyAIScanner.d.ts"],
|
|
29
|
+
"offline": ["./lib/storage/offlineQueue.d.ts"]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
10
32
|
"files": [
|
|
11
33
|
"src",
|
|
12
34
|
"lib"
|
|
@@ -94,6 +94,7 @@ export function VerifyAIScanner({
|
|
|
94
94
|
setResult(verificationResult);
|
|
95
95
|
setStatus('success');
|
|
96
96
|
onResult?.(verificationResult);
|
|
97
|
+
setTimeout(() => setStatus('idle'), 3000);
|
|
97
98
|
} else {
|
|
98
99
|
// null result means queued for offline
|
|
99
100
|
setStatus('idle');
|
|
@@ -127,6 +128,8 @@ export function VerifyAIScanner({
|
|
|
127
128
|
);
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
const showBottomCard = status === 'success' || status === 'error';
|
|
132
|
+
|
|
130
133
|
return (
|
|
131
134
|
<View style={[styles.container, style]}>
|
|
132
135
|
<CameraView ref={cameraRef} style={styles.camera} facing="back">
|
|
@@ -147,68 +150,105 @@ export function VerifyAIScanner({
|
|
|
147
150
|
? { aspectRatio: overlay.guideFrameAspectRatio }
|
|
148
151
|
: undefined,
|
|
149
152
|
]}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
>
|
|
154
|
+
{/* Corner brackets */}
|
|
155
|
+
<View style={[styles.corner, styles.cornerTopLeft]} />
|
|
156
|
+
<View style={[styles.corner, styles.cornerTopRight]} />
|
|
157
|
+
<View style={[styles.corner, styles.cornerBottomLeft]} />
|
|
158
|
+
<View style={[styles.corner, styles.cornerBottomRight]} />
|
|
159
|
+
</View>
|
|
157
160
|
</View>
|
|
158
161
|
)}
|
|
159
162
|
|
|
160
|
-
{/*
|
|
163
|
+
{/* Processing spinner */}
|
|
161
164
|
{status === 'processing' && (
|
|
162
|
-
<View style={styles.
|
|
165
|
+
<View style={styles.processingOverlay}>
|
|
163
166
|
<ActivityIndicator size="large" color="#fff" />
|
|
164
167
|
<Text style={styles.statusText}>Analyzing photo...</Text>
|
|
165
168
|
</View>
|
|
166
169
|
)}
|
|
167
170
|
|
|
168
|
-
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
171
|
+
{/* Semi-transparent backdrop for result/error cards */}
|
|
172
|
+
{showBottomCard && <View style={styles.cardBackdrop} />}
|
|
173
|
+
|
|
174
|
+
{/* Bottom area: instructions + capture button OR result card */}
|
|
175
|
+
<View style={styles.bottomArea}>
|
|
176
|
+
{status === 'success' && result && (
|
|
177
|
+
<View style={styles.resultCard}>
|
|
178
|
+
<View style={styles.resultCardHeader}>
|
|
179
|
+
<View
|
|
180
|
+
style={[
|
|
181
|
+
styles.resultIconCircle,
|
|
182
|
+
result.is_compliant ? styles.resultIconPass : styles.resultIconFail,
|
|
183
|
+
]}
|
|
184
|
+
>
|
|
185
|
+
<Text style={styles.resultIcon}>
|
|
186
|
+
{result.is_compliant ? '\u2713' : '\u2717'}
|
|
187
|
+
</Text>
|
|
188
|
+
</View>
|
|
189
|
+
<Text
|
|
190
|
+
style={[
|
|
191
|
+
styles.resultLabel,
|
|
192
|
+
result.is_compliant ? styles.resultLabelPass : styles.resultLabelFail,
|
|
193
|
+
]}
|
|
194
|
+
>
|
|
195
|
+
{result.is_compliant ? 'Verified' : 'Not Verified'}
|
|
196
|
+
</Text>
|
|
197
|
+
</View>
|
|
198
|
+
<Text style={styles.feedbackText}>{result.feedback}</Text>
|
|
179
199
|
</View>
|
|
180
|
-
|
|
181
|
-
</View>
|
|
182
|
-
)}
|
|
200
|
+
)}
|
|
183
201
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
202
|
+
{status === 'error' && (
|
|
203
|
+
<View style={styles.resultCard}>
|
|
204
|
+
<View style={styles.resultCardHeader}>
|
|
205
|
+
<View style={[styles.resultIconCircle, styles.resultIconError]}>
|
|
206
|
+
<Text style={styles.resultIcon}>!</Text>
|
|
207
|
+
</View>
|
|
208
|
+
<Text style={[styles.resultLabel, styles.resultLabelError]}>
|
|
209
|
+
Something went wrong
|
|
210
|
+
</Text>
|
|
211
|
+
</View>
|
|
212
|
+
<Text style={styles.feedbackText}>
|
|
213
|
+
We couldn't process your photo. Please try again.
|
|
214
|
+
</Text>
|
|
215
|
+
</View>
|
|
216
|
+
)}
|
|
190
217
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
{!showBottomCard && (
|
|
219
|
+
<>
|
|
220
|
+
{overlay?.instructions && status === 'idle' && (
|
|
221
|
+
<Text style={styles.instructionsText}>{overlay.instructions}</Text>
|
|
222
|
+
)}
|
|
223
|
+
{showCaptureButton && (
|
|
224
|
+
<View style={styles.captureButtonRow}>
|
|
225
|
+
<TouchableOpacity
|
|
226
|
+
style={[
|
|
227
|
+
styles.captureButton,
|
|
228
|
+
(status === 'capturing' || status === 'processing') &&
|
|
229
|
+
styles.captureButtonDisabled,
|
|
230
|
+
]}
|
|
231
|
+
onPress={handleCapture}
|
|
232
|
+
disabled={status === 'capturing' || status === 'processing'}
|
|
233
|
+
activeOpacity={0.7}
|
|
234
|
+
>
|
|
235
|
+
<View style={styles.captureButtonInner} />
|
|
236
|
+
</TouchableOpacity>
|
|
237
|
+
</View>
|
|
238
|
+
)}
|
|
239
|
+
</>
|
|
240
|
+
)}
|
|
205
241
|
</View>
|
|
206
|
-
|
|
242
|
+
</View>
|
|
207
243
|
</CameraView>
|
|
208
244
|
</View>
|
|
209
245
|
);
|
|
210
246
|
}
|
|
211
247
|
|
|
248
|
+
const CORNER_SIZE = 30;
|
|
249
|
+
const CORNER_THICKNESS = 3;
|
|
250
|
+
const CORNER_COLOR = 'rgba(255, 255, 255, 0.7)';
|
|
251
|
+
|
|
212
252
|
const styles = StyleSheet.create({
|
|
213
253
|
container: {
|
|
214
254
|
flex: 1,
|
|
@@ -231,6 +271,8 @@ const styles = StyleSheet.create({
|
|
|
231
271
|
fontSize: 18,
|
|
232
272
|
fontWeight: '600',
|
|
233
273
|
},
|
|
274
|
+
|
|
275
|
+
// Guide frame with corner brackets
|
|
234
276
|
guideContainer: {
|
|
235
277
|
flex: 1,
|
|
236
278
|
justifyContent: 'center',
|
|
@@ -240,66 +282,58 @@ const styles = StyleSheet.create({
|
|
|
240
282
|
guideFrame: {
|
|
241
283
|
width: '100%',
|
|
242
284
|
aspectRatio: 4 / 3,
|
|
243
|
-
borderWidth: 2,
|
|
244
|
-
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
245
|
-
borderRadius: 12,
|
|
246
|
-
borderStyle: 'dashed',
|
|
247
|
-
},
|
|
248
|
-
instructionsContainer: {
|
|
249
|
-
paddingHorizontal: 20,
|
|
250
|
-
paddingBottom: 20,
|
|
251
|
-
alignItems: 'center',
|
|
252
285
|
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
},
|
|
258
|
-
statusOverlay: {
|
|
259
|
-
...StyleSheet.absoluteFillObject,
|
|
260
|
-
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
261
|
-
justifyContent: 'center',
|
|
262
|
-
alignItems: 'center',
|
|
263
|
-
gap: 16,
|
|
286
|
+
corner: {
|
|
287
|
+
position: 'absolute',
|
|
288
|
+
width: CORNER_SIZE,
|
|
289
|
+
height: CORNER_SIZE,
|
|
264
290
|
},
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
291
|
+
cornerTopLeft: {
|
|
292
|
+
top: 0,
|
|
293
|
+
left: 0,
|
|
294
|
+
borderTopWidth: CORNER_THICKNESS,
|
|
295
|
+
borderLeftWidth: CORNER_THICKNESS,
|
|
296
|
+
borderColor: CORNER_COLOR,
|
|
297
|
+
borderTopLeftRadius: 4,
|
|
269
298
|
},
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
299
|
+
cornerTopRight: {
|
|
300
|
+
top: 0,
|
|
301
|
+
right: 0,
|
|
302
|
+
borderTopWidth: CORNER_THICKNESS,
|
|
303
|
+
borderRightWidth: CORNER_THICKNESS,
|
|
304
|
+
borderColor: CORNER_COLOR,
|
|
305
|
+
borderTopRightRadius: 4,
|
|
274
306
|
},
|
|
275
|
-
|
|
276
|
-
|
|
307
|
+
cornerBottomLeft: {
|
|
308
|
+
bottom: 0,
|
|
309
|
+
left: 0,
|
|
310
|
+
borderBottomWidth: CORNER_THICKNESS,
|
|
311
|
+
borderLeftWidth: CORNER_THICKNESS,
|
|
312
|
+
borderColor: CORNER_COLOR,
|
|
313
|
+
borderBottomLeftRadius: 4,
|
|
277
314
|
},
|
|
278
|
-
|
|
279
|
-
|
|
315
|
+
cornerBottomRight: {
|
|
316
|
+
bottom: 0,
|
|
317
|
+
right: 0,
|
|
318
|
+
borderBottomWidth: CORNER_THICKNESS,
|
|
319
|
+
borderRightWidth: CORNER_THICKNESS,
|
|
320
|
+
borderColor: CORNER_COLOR,
|
|
321
|
+
borderBottomRightRadius: 4,
|
|
280
322
|
},
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
323
|
+
|
|
324
|
+
// Bottom area: flex column for instructions + capture button
|
|
325
|
+
bottomArea: {
|
|
326
|
+
paddingBottom: 40,
|
|
327
|
+
alignItems: 'center',
|
|
285
328
|
},
|
|
286
|
-
|
|
287
|
-
color: '
|
|
329
|
+
instructionsText: {
|
|
330
|
+
color: 'rgba(255, 255, 255, 0.8)',
|
|
288
331
|
fontSize: 14,
|
|
289
332
|
textAlign: 'center',
|
|
290
|
-
paddingHorizontal:
|
|
291
|
-
|
|
292
|
-
},
|
|
293
|
-
errorText: {
|
|
294
|
-
color: '#ef4444',
|
|
295
|
-
fontSize: 16,
|
|
296
|
-
fontWeight: '500',
|
|
333
|
+
paddingHorizontal: 20,
|
|
334
|
+
marginBottom: 20,
|
|
297
335
|
},
|
|
298
|
-
|
|
299
|
-
position: 'absolute',
|
|
300
|
-
bottom: 40,
|
|
301
|
-
left: 0,
|
|
302
|
-
right: 0,
|
|
336
|
+
captureButtonRow: {
|
|
303
337
|
alignItems: 'center',
|
|
304
338
|
},
|
|
305
339
|
captureButton: {
|
|
@@ -320,6 +354,82 @@ const styles = StyleSheet.create({
|
|
|
320
354
|
borderRadius: 29,
|
|
321
355
|
backgroundColor: '#fff',
|
|
322
356
|
},
|
|
357
|
+
|
|
358
|
+
// Processing spinner overlay
|
|
359
|
+
processingOverlay: {
|
|
360
|
+
...StyleSheet.absoluteFillObject,
|
|
361
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
362
|
+
justifyContent: 'center',
|
|
363
|
+
alignItems: 'center',
|
|
364
|
+
gap: 16,
|
|
365
|
+
},
|
|
366
|
+
statusText: {
|
|
367
|
+
color: '#fff',
|
|
368
|
+
fontSize: 16,
|
|
369
|
+
fontWeight: '500',
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
// Bottom card for results and errors
|
|
373
|
+
cardBackdrop: {
|
|
374
|
+
...StyleSheet.absoluteFillObject,
|
|
375
|
+
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
376
|
+
},
|
|
377
|
+
resultCard: {
|
|
378
|
+
backgroundColor: '#fff',
|
|
379
|
+
borderRadius: 20,
|
|
380
|
+
paddingVertical: 24,
|
|
381
|
+
paddingHorizontal: 24,
|
|
382
|
+
marginHorizontal: 16,
|
|
383
|
+
alignItems: 'center',
|
|
384
|
+
gap: 12,
|
|
385
|
+
},
|
|
386
|
+
resultCardHeader: {
|
|
387
|
+
flexDirection: 'row',
|
|
388
|
+
alignItems: 'center',
|
|
389
|
+
gap: 12,
|
|
390
|
+
},
|
|
391
|
+
resultIconCircle: {
|
|
392
|
+
width: 36,
|
|
393
|
+
height: 36,
|
|
394
|
+
borderRadius: 18,
|
|
395
|
+
justifyContent: 'center',
|
|
396
|
+
alignItems: 'center',
|
|
397
|
+
},
|
|
398
|
+
resultIconPass: {
|
|
399
|
+
backgroundColor: '#22c55e',
|
|
400
|
+
},
|
|
401
|
+
resultIconFail: {
|
|
402
|
+
backgroundColor: '#ef4444',
|
|
403
|
+
},
|
|
404
|
+
resultIconError: {
|
|
405
|
+
backgroundColor: '#f59e0b',
|
|
406
|
+
},
|
|
407
|
+
resultIcon: {
|
|
408
|
+
color: '#fff',
|
|
409
|
+
fontSize: 20,
|
|
410
|
+
fontWeight: '700',
|
|
411
|
+
},
|
|
412
|
+
resultLabel: {
|
|
413
|
+
fontSize: 20,
|
|
414
|
+
fontWeight: '700',
|
|
415
|
+
},
|
|
416
|
+
resultLabelPass: {
|
|
417
|
+
color: '#15803d',
|
|
418
|
+
},
|
|
419
|
+
resultLabelFail: {
|
|
420
|
+
color: '#dc2626',
|
|
421
|
+
},
|
|
422
|
+
resultLabelError: {
|
|
423
|
+
color: '#b45309',
|
|
424
|
+
},
|
|
425
|
+
feedbackText: {
|
|
426
|
+
color: '#4b5563',
|
|
427
|
+
fontSize: 15,
|
|
428
|
+
textAlign: 'center',
|
|
429
|
+
lineHeight: 22,
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
// Permission screen
|
|
323
433
|
permissionContainer: {
|
|
324
434
|
justifyContent: 'center',
|
|
325
435
|
alignItems: 'center',
|
package/src/hooks/useVerifyAI.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useRef, useMemo, useCallback, useState, useEffect } from 'react';
|
|
2
2
|
import { AppState, type AppStateStatus } from 'react-native';
|
|
3
3
|
import { VerifyAIClient } from '../client';
|
|
4
|
-
import { OfflineQueue } from '../storage/offlineQueue';
|
|
4
|
+
import type { OfflineQueue } from '../storage/offlineQueue';
|
|
5
5
|
import type {
|
|
6
6
|
VerifyAIConfig,
|
|
7
7
|
VerificationRequest,
|
|
@@ -10,6 +10,16 @@ import type {
|
|
|
10
10
|
VerificationListResponse,
|
|
11
11
|
} from '../types';
|
|
12
12
|
|
|
13
|
+
// Lazy-load OfflineQueue to avoid hard dependency on @react-native-async-storage/async-storage.
|
|
14
|
+
// Consumers who don't use offlineMode won't need it installed.
|
|
15
|
+
let OfflineQueueClass: (new (client: VerifyAIClient) => OfflineQueue) | null = null;
|
|
16
|
+
try {
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
|
+
OfflineQueueClass = require('../storage/offlineQueue').OfflineQueue;
|
|
19
|
+
} catch {
|
|
20
|
+
// AsyncStorage not installed — offlineMode will be unavailable
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
export interface UseVerifyAIReturn {
|
|
14
24
|
/** Submit a verification. Uses offline queue if offlineMode is enabled and request fails. */
|
|
15
25
|
verify: (request: VerificationRequest) => Promise<VerificationResult | null>;
|
|
@@ -58,7 +68,7 @@ export interface UseVerifyAIReturn {
|
|
|
58
68
|
export function useVerifyAI(config: VerifyAIConfig): UseVerifyAIReturn {
|
|
59
69
|
const client = useMemo(() => new VerifyAIClient(config), [config.apiKey, config.baseUrl, config.timeout]);
|
|
60
70
|
const offlineQueue = useMemo(
|
|
61
|
-
() => (config.offlineMode ? new
|
|
71
|
+
() => (config.offlineMode && OfflineQueueClass ? new OfflineQueueClass(client) : null),
|
|
62
72
|
[client, config.offlineMode]
|
|
63
73
|
);
|
|
64
74
|
|
package/src/index.ts
CHANGED
|
@@ -5,12 +5,11 @@ export { VerifyAIClient, VerifyAIRequestError } from './client';
|
|
|
5
5
|
export { useVerifyAI } from './hooks/useVerifyAI';
|
|
6
6
|
export type { UseVerifyAIReturn } from './hooks/useVerifyAI';
|
|
7
7
|
|
|
8
|
-
// Components
|
|
9
|
-
export { VerifyAIScanner } from './components/VerifyAIScanner';
|
|
8
|
+
// Components — import { VerifyAIScanner } from '@switchlabs/verify-ai-react-native/scanner'
|
|
10
9
|
export type { VerifyAIScannerProps } from './components/VerifyAIScanner';
|
|
11
10
|
|
|
12
|
-
// Offline Queue
|
|
13
|
-
export { OfflineQueue } from './storage/offlineQueue';
|
|
11
|
+
// Offline Queue — import { OfflineQueue } from '@switchlabs/verify-ai-react-native/offline'
|
|
12
|
+
export type { OfflineQueue } from './storage/offlineQueue';
|
|
14
13
|
|
|
15
14
|
// Types
|
|
16
15
|
export type {
|