@switchlabs/verify-ai-react-native 0.1.1 → 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/client/index.js +2 -7
- package/lib/components/VerifyAIScanner.js +149 -79
- package/lib/hooks/useVerifyAI.js +17 -20
- package/lib/index.js +2 -8
- package/lib/storage/offlineQueue.js +19 -26
- package/lib/types/index.js +1 -2
- package/package.json +1 -1
- package/src/components/VerifyAIScanner.tsx +203 -94
package/lib/client/index.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VerifyAIRequestError = exports.VerifyAIClient = void 0;
|
|
4
1
|
const DEFAULT_BASE_URL = 'https://verify.switchlabs.dev/api/v1';
|
|
5
2
|
const DEFAULT_TIMEOUT = 30000;
|
|
6
|
-
class VerifyAIClient {
|
|
3
|
+
export class VerifyAIClient {
|
|
7
4
|
constructor(config) {
|
|
8
5
|
if (!config.apiKey) {
|
|
9
6
|
throw new Error('VerifyAI: apiKey is required');
|
|
@@ -108,8 +105,7 @@ class VerifyAIClient {
|
|
|
108
105
|
return this.request(`/verifications/${id}`);
|
|
109
106
|
}
|
|
110
107
|
}
|
|
111
|
-
|
|
112
|
-
class VerifyAIRequestError extends Error {
|
|
108
|
+
export class VerifyAIRequestError extends Error {
|
|
113
109
|
constructor(message, status, body) {
|
|
114
110
|
super(message);
|
|
115
111
|
this.name = 'VerifyAIRequestError';
|
|
@@ -126,4 +122,3 @@ class VerifyAIRequestError extends Error {
|
|
|
126
122
|
return this.body.upgrade_url;
|
|
127
123
|
}
|
|
128
124
|
}
|
|
129
|
-
exports.VerifyAIRequestError = VerifyAIRequestError;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const react_1 = require("react");
|
|
6
|
-
const react_native_1 = require("react-native");
|
|
7
|
-
const expo_camera_1 = require("expo-camera");
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState, useCallback } from 'react';
|
|
3
|
+
import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, } from 'react-native';
|
|
4
|
+
import { CameraView, useCameraPermissions, } from 'expo-camera';
|
|
8
5
|
/**
|
|
9
6
|
* Camera scanner component for capturing verification photos.
|
|
10
7
|
* Uses expo-camera for the camera view and provides a simple capture UI.
|
|
@@ -26,12 +23,12 @@ const expo_camera_1 = require("expo-camera");
|
|
|
26
23
|
* />
|
|
27
24
|
* ```
|
|
28
25
|
*/
|
|
29
|
-
function VerifyAIScanner({ onCapture, onResult, onError, overlay, style, showCaptureButton = true, captureRef, }) {
|
|
30
|
-
const cameraRef =
|
|
31
|
-
const [status, setStatus] =
|
|
32
|
-
const [result, setResult] =
|
|
33
|
-
const [permission, requestPermission] =
|
|
34
|
-
const handleCapture =
|
|
26
|
+
export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style, showCaptureButton = true, captureRef, }) {
|
|
27
|
+
const cameraRef = useRef(null);
|
|
28
|
+
const [status, setStatus] = useState('idle');
|
|
29
|
+
const [result, setResult] = useState(null);
|
|
30
|
+
const [permission, requestPermission] = useCameraPermissions();
|
|
31
|
+
const handleCapture = useCallback(async () => {
|
|
35
32
|
if (!cameraRef.current || status === 'capturing' || status === 'processing')
|
|
36
33
|
return;
|
|
37
34
|
setStatus('capturing');
|
|
@@ -71,25 +68,33 @@ function VerifyAIScanner({ onCapture, onResult, onError, overlay, style, showCap
|
|
|
71
68
|
captureRef.current = handleCapture;
|
|
72
69
|
}
|
|
73
70
|
if (!permission) {
|
|
74
|
-
return (
|
|
71
|
+
return _jsx(View, { style: [styles.container, style] });
|
|
75
72
|
}
|
|
76
73
|
if (!permission.granted) {
|
|
77
|
-
return ((
|
|
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" }) })] }));
|
|
78
75
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 }) }) }))] }))] })] }) }) }));
|
|
91
93
|
}
|
|
92
|
-
const
|
|
94
|
+
const CORNER_SIZE = 30;
|
|
95
|
+
const CORNER_THICKNESS = 3;
|
|
96
|
+
const CORNER_COLOR = 'rgba(255, 255, 255, 0.7)';
|
|
97
|
+
const styles = StyleSheet.create({
|
|
93
98
|
container: {
|
|
94
99
|
flex: 1,
|
|
95
100
|
backgroundColor: '#000',
|
|
@@ -98,7 +103,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
98
103
|
flex: 1,
|
|
99
104
|
},
|
|
100
105
|
overlay: {
|
|
101
|
-
...
|
|
106
|
+
...StyleSheet.absoluteFillObject,
|
|
102
107
|
justifyContent: 'space-between',
|
|
103
108
|
},
|
|
104
109
|
topBar: {
|
|
@@ -111,6 +116,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
111
116
|
fontSize: 18,
|
|
112
117
|
fontWeight: '600',
|
|
113
118
|
},
|
|
119
|
+
// Guide frame with corner brackets
|
|
114
120
|
guideContainer: {
|
|
115
121
|
flex: 1,
|
|
116
122
|
justifyContent: 'center',
|
|
@@ -120,66 +126,57 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
120
126
|
guideFrame: {
|
|
121
127
|
width: '100%',
|
|
122
128
|
aspectRatio: 4 / 3,
|
|
123
|
-
borderWidth: 2,
|
|
124
|
-
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
125
|
-
borderRadius: 12,
|
|
126
|
-
borderStyle: 'dashed',
|
|
127
129
|
},
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
},
|
|
133
|
-
instructionsText: {
|
|
134
|
-
color: 'rgba(255, 255, 255, 0.8)',
|
|
135
|
-
fontSize: 14,
|
|
136
|
-
textAlign: 'center',
|
|
137
|
-
},
|
|
138
|
-
statusOverlay: {
|
|
139
|
-
...react_native_1.StyleSheet.absoluteFillObject,
|
|
140
|
-
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
141
|
-
justifyContent: 'center',
|
|
142
|
-
alignItems: 'center',
|
|
143
|
-
gap: 16,
|
|
130
|
+
corner: {
|
|
131
|
+
position: 'absolute',
|
|
132
|
+
width: CORNER_SIZE,
|
|
133
|
+
height: CORNER_SIZE,
|
|
144
134
|
},
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
135
|
+
cornerTopLeft: {
|
|
136
|
+
top: 0,
|
|
137
|
+
left: 0,
|
|
138
|
+
borderTopWidth: CORNER_THICKNESS,
|
|
139
|
+
borderLeftWidth: CORNER_THICKNESS,
|
|
140
|
+
borderColor: CORNER_COLOR,
|
|
141
|
+
borderTopLeftRadius: 4,
|
|
149
142
|
},
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
143
|
+
cornerTopRight: {
|
|
144
|
+
top: 0,
|
|
145
|
+
right: 0,
|
|
146
|
+
borderTopWidth: CORNER_THICKNESS,
|
|
147
|
+
borderRightWidth: CORNER_THICKNESS,
|
|
148
|
+
borderColor: CORNER_COLOR,
|
|
149
|
+
borderTopRightRadius: 4,
|
|
154
150
|
},
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
cornerBottomLeft: {
|
|
152
|
+
bottom: 0,
|
|
153
|
+
left: 0,
|
|
154
|
+
borderBottomWidth: CORNER_THICKNESS,
|
|
155
|
+
borderLeftWidth: CORNER_THICKNESS,
|
|
156
|
+
borderColor: CORNER_COLOR,
|
|
157
|
+
borderBottomLeftRadius: 4,
|
|
157
158
|
},
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
cornerBottomRight: {
|
|
160
|
+
bottom: 0,
|
|
161
|
+
right: 0,
|
|
162
|
+
borderBottomWidth: CORNER_THICKNESS,
|
|
163
|
+
borderRightWidth: CORNER_THICKNESS,
|
|
164
|
+
borderColor: CORNER_COLOR,
|
|
165
|
+
borderBottomRightRadius: 4,
|
|
160
166
|
},
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
// Bottom area: flex column for instructions + capture button
|
|
168
|
+
bottomArea: {
|
|
169
|
+
paddingBottom: 40,
|
|
170
|
+
alignItems: 'center',
|
|
165
171
|
},
|
|
166
|
-
|
|
167
|
-
color: '
|
|
172
|
+
instructionsText: {
|
|
173
|
+
color: 'rgba(255, 255, 255, 0.8)',
|
|
168
174
|
fontSize: 14,
|
|
169
175
|
textAlign: 'center',
|
|
170
|
-
paddingHorizontal:
|
|
171
|
-
|
|
172
|
-
},
|
|
173
|
-
errorText: {
|
|
174
|
-
color: '#ef4444',
|
|
175
|
-
fontSize: 16,
|
|
176
|
-
fontWeight: '500',
|
|
176
|
+
paddingHorizontal: 20,
|
|
177
|
+
marginBottom: 20,
|
|
177
178
|
},
|
|
178
|
-
|
|
179
|
-
position: 'absolute',
|
|
180
|
-
bottom: 40,
|
|
181
|
-
left: 0,
|
|
182
|
-
right: 0,
|
|
179
|
+
captureButtonRow: {
|
|
183
180
|
alignItems: 'center',
|
|
184
181
|
},
|
|
185
182
|
captureButton: {
|
|
@@ -200,6 +197,79 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
200
197
|
borderRadius: 29,
|
|
201
198
|
backgroundColor: '#fff',
|
|
202
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
|
|
203
273
|
permissionContainer: {
|
|
204
274
|
justifyContent: 'center',
|
|
205
275
|
alignItems: 'center',
|
package/lib/hooks/useVerifyAI.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const react_1 = require("react");
|
|
5
|
-
const react_native_1 = require("react-native");
|
|
6
|
-
const client_1 = require("../client");
|
|
1
|
+
import { useMemo, useCallback, useState, useEffect } from 'react';
|
|
2
|
+
import { AppState } from 'react-native';
|
|
3
|
+
import { VerifyAIClient } from '../client';
|
|
7
4
|
// Lazy-load OfflineQueue to avoid hard dependency on @react-native-async-storage/async-storage.
|
|
8
5
|
// Consumers who don't use offlineMode won't need it installed.
|
|
9
6
|
let OfflineQueueClass = null;
|
|
@@ -36,22 +33,22 @@ catch {
|
|
|
36
33
|
* };
|
|
37
34
|
* ```
|
|
38
35
|
*/
|
|
39
|
-
function useVerifyAI(config) {
|
|
40
|
-
const client =
|
|
41
|
-
const offlineQueue =
|
|
42
|
-
const [loading, setLoading] =
|
|
43
|
-
const [error, setError] =
|
|
44
|
-
const [lastResult, setLastResult] =
|
|
45
|
-
const [queueSize, setQueueSize] =
|
|
36
|
+
export function useVerifyAI(config) {
|
|
37
|
+
const client = useMemo(() => new VerifyAIClient(config), [config.apiKey, config.baseUrl, config.timeout]);
|
|
38
|
+
const offlineQueue = useMemo(() => (config.offlineMode && OfflineQueueClass ? new OfflineQueueClass(client) : null), [client, config.offlineMode]);
|
|
39
|
+
const [loading, setLoading] = useState(false);
|
|
40
|
+
const [error, setError] = useState(null);
|
|
41
|
+
const [lastResult, setLastResult] = useState(null);
|
|
42
|
+
const [queueSize, setQueueSize] = useState(0);
|
|
46
43
|
// Refresh queue size
|
|
47
|
-
const refreshQueueSize =
|
|
44
|
+
const refreshQueueSize = useCallback(async () => {
|
|
48
45
|
if (offlineQueue) {
|
|
49
46
|
const size = await offlineQueue.getQueueSize();
|
|
50
47
|
setQueueSize(size);
|
|
51
48
|
}
|
|
52
49
|
}, [offlineQueue]);
|
|
53
50
|
// Process queue on app foreground
|
|
54
|
-
|
|
51
|
+
useEffect(() => {
|
|
55
52
|
if (!offlineQueue)
|
|
56
53
|
return;
|
|
57
54
|
refreshQueueSize();
|
|
@@ -60,10 +57,10 @@ function useVerifyAI(config) {
|
|
|
60
57
|
offlineQueue.processQueue().then(() => refreshQueueSize());
|
|
61
58
|
}
|
|
62
59
|
};
|
|
63
|
-
const subscription =
|
|
60
|
+
const subscription = AppState.addEventListener('change', handleAppState);
|
|
64
61
|
return () => subscription.remove();
|
|
65
62
|
}, [offlineQueue, refreshQueueSize]);
|
|
66
|
-
const verify =
|
|
63
|
+
const verify = useCallback(async (request) => {
|
|
67
64
|
setLoading(true);
|
|
68
65
|
setError(null);
|
|
69
66
|
try {
|
|
@@ -86,9 +83,9 @@ function useVerifyAI(config) {
|
|
|
86
83
|
setLoading(false);
|
|
87
84
|
}
|
|
88
85
|
}, [client, offlineQueue, refreshQueueSize]);
|
|
89
|
-
const listVerifications =
|
|
90
|
-
const getVerification =
|
|
91
|
-
const processQueue =
|
|
86
|
+
const listVerifications = useCallback((params) => client.listVerifications(params), [client]);
|
|
87
|
+
const getVerification = useCallback((id) => client.getVerification(id), [client]);
|
|
88
|
+
const processQueue = useCallback(async () => {
|
|
92
89
|
if (!offlineQueue)
|
|
93
90
|
return;
|
|
94
91
|
await offlineQueue.processQueue((_, result) => setLastResult(result));
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useVerifyAI = exports.VerifyAIRequestError = exports.VerifyAIClient = void 0;
|
|
4
1
|
// Client
|
|
5
|
-
|
|
6
|
-
Object.defineProperty(exports, "VerifyAIClient", { enumerable: true, get: function () { return client_1.VerifyAIClient; } });
|
|
7
|
-
Object.defineProperty(exports, "VerifyAIRequestError", { enumerable: true, get: function () { return client_1.VerifyAIRequestError; } });
|
|
2
|
+
export { VerifyAIClient, VerifyAIRequestError } from './client';
|
|
8
3
|
// Hooks
|
|
9
|
-
|
|
10
|
-
Object.defineProperty(exports, "useVerifyAI", { enumerable: true, get: function () { return useVerifyAI_1.useVerifyAI; } });
|
|
4
|
+
export { useVerifyAI } from './hooks/useVerifyAI';
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.OfflineQueue = void 0;
|
|
7
|
-
const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
8
2
|
const MANIFEST_KEY = '@verifyai/queue_manifest';
|
|
9
3
|
const ITEM_PREFIX = '@verifyai/queue_item_';
|
|
10
4
|
const LEGACY_KEY = '@verifyai/offline_queue';
|
|
11
|
-
class OfflineQueue {
|
|
5
|
+
export class OfflineQueue {
|
|
12
6
|
constructor(client) {
|
|
13
7
|
this.processing = false;
|
|
14
8
|
this.migrated = false;
|
|
@@ -22,13 +16,13 @@ class OfflineQueue {
|
|
|
22
16
|
if (this.migrated)
|
|
23
17
|
return;
|
|
24
18
|
this.migrated = true;
|
|
25
|
-
const legacy = await
|
|
19
|
+
const legacy = await AsyncStorage.getItem(LEGACY_KEY);
|
|
26
20
|
if (!legacy)
|
|
27
21
|
return;
|
|
28
22
|
try {
|
|
29
23
|
const items = JSON.parse(legacy);
|
|
30
24
|
if (!Array.isArray(items) || items.length === 0) {
|
|
31
|
-
await
|
|
25
|
+
await AsyncStorage.removeItem(LEGACY_KEY);
|
|
32
26
|
return;
|
|
33
27
|
}
|
|
34
28
|
const ids = [];
|
|
@@ -37,24 +31,24 @@ class OfflineQueue {
|
|
|
37
31
|
ids.push(item.id);
|
|
38
32
|
pairs.push([`${ITEM_PREFIX}${item.id}`, JSON.stringify(item)]);
|
|
39
33
|
}
|
|
40
|
-
await
|
|
34
|
+
await AsyncStorage.multiSet([
|
|
41
35
|
[MANIFEST_KEY, JSON.stringify(ids)],
|
|
42
36
|
...pairs,
|
|
43
37
|
]);
|
|
44
|
-
await
|
|
38
|
+
await AsyncStorage.removeItem(LEGACY_KEY);
|
|
45
39
|
}
|
|
46
40
|
catch {
|
|
47
41
|
// If migration fails, remove corrupt legacy data
|
|
48
|
-
await
|
|
42
|
+
await AsyncStorage.removeItem(LEGACY_KEY);
|
|
49
43
|
}
|
|
50
44
|
}
|
|
51
45
|
async getManifest() {
|
|
52
46
|
await this.migrateIfNeeded();
|
|
53
|
-
const raw = await
|
|
47
|
+
const raw = await AsyncStorage.getItem(MANIFEST_KEY);
|
|
54
48
|
return raw ? JSON.parse(raw) : [];
|
|
55
49
|
}
|
|
56
50
|
async setManifest(ids) {
|
|
57
|
-
await
|
|
51
|
+
await AsyncStorage.setItem(MANIFEST_KEY, JSON.stringify(ids));
|
|
58
52
|
}
|
|
59
53
|
/**
|
|
60
54
|
* Add a verification request to the offline queue.
|
|
@@ -69,7 +63,7 @@ class OfflineQueue {
|
|
|
69
63
|
};
|
|
70
64
|
const ids = await this.getManifest();
|
|
71
65
|
ids.push(item.id);
|
|
72
|
-
await
|
|
66
|
+
await AsyncStorage.setItem(`${ITEM_PREFIX}${item.id}`, JSON.stringify(item));
|
|
73
67
|
await this.setManifest(ids);
|
|
74
68
|
return item.id;
|
|
75
69
|
}
|
|
@@ -81,7 +75,7 @@ class OfflineQueue {
|
|
|
81
75
|
if (ids.length === 0)
|
|
82
76
|
return [];
|
|
83
77
|
const keys = ids.map((id) => `${ITEM_PREFIX}${id}`);
|
|
84
|
-
const pairs = await
|
|
78
|
+
const pairs = await AsyncStorage.multiGet(keys);
|
|
85
79
|
const items = [];
|
|
86
80
|
for (const [, value] of pairs) {
|
|
87
81
|
if (value) {
|
|
@@ -109,7 +103,7 @@ class OfflineQueue {
|
|
|
109
103
|
const ids = await this.getManifest();
|
|
110
104
|
const filtered = ids.filter((i) => i !== id);
|
|
111
105
|
await this.setManifest(filtered);
|
|
112
|
-
await
|
|
106
|
+
await AsyncStorage.removeItem(`${ITEM_PREFIX}${id}`);
|
|
113
107
|
}
|
|
114
108
|
/**
|
|
115
109
|
* Clear all items from the queue.
|
|
@@ -118,10 +112,10 @@ class OfflineQueue {
|
|
|
118
112
|
const ids = await this.getManifest();
|
|
119
113
|
if (ids.length > 0) {
|
|
120
114
|
const keys = ids.map((id) => `${ITEM_PREFIX}${id}`);
|
|
121
|
-
await
|
|
115
|
+
await AsyncStorage.multiRemove([MANIFEST_KEY, ...keys]);
|
|
122
116
|
}
|
|
123
117
|
else {
|
|
124
|
-
await
|
|
118
|
+
await AsyncStorage.removeItem(MANIFEST_KEY);
|
|
125
119
|
}
|
|
126
120
|
}
|
|
127
121
|
/**
|
|
@@ -145,7 +139,7 @@ class OfflineQueue {
|
|
|
145
139
|
try {
|
|
146
140
|
const ids = await this.getManifest();
|
|
147
141
|
for (const id of ids) {
|
|
148
|
-
const raw = await
|
|
142
|
+
const raw = await AsyncStorage.getItem(`${ITEM_PREFIX}${id}`);
|
|
149
143
|
if (!raw)
|
|
150
144
|
continue;
|
|
151
145
|
let item;
|
|
@@ -154,24 +148,24 @@ class OfflineQueue {
|
|
|
154
148
|
}
|
|
155
149
|
catch {
|
|
156
150
|
// Remove corrupt item
|
|
157
|
-
await
|
|
151
|
+
await AsyncStorage.removeItem(`${ITEM_PREFIX}${id}`);
|
|
158
152
|
continue;
|
|
159
153
|
}
|
|
160
154
|
try {
|
|
161
155
|
const result = await this.client.verify(item.request);
|
|
162
156
|
processed++;
|
|
163
|
-
await
|
|
157
|
+
await AsyncStorage.removeItem(`${ITEM_PREFIX}${id}`);
|
|
164
158
|
onResult?.(item.id, result);
|
|
165
159
|
}
|
|
166
160
|
catch {
|
|
167
161
|
item.retryCount++;
|
|
168
162
|
if (item.retryCount < maxRetries) {
|
|
169
|
-
await
|
|
163
|
+
await AsyncStorage.setItem(`${ITEM_PREFIX}${id}`, JSON.stringify(item));
|
|
170
164
|
remainingIds.push(id);
|
|
171
165
|
}
|
|
172
166
|
else {
|
|
173
167
|
failed++;
|
|
174
|
-
await
|
|
168
|
+
await AsyncStorage.removeItem(`${ITEM_PREFIX}${id}`);
|
|
175
169
|
}
|
|
176
170
|
}
|
|
177
171
|
}
|
|
@@ -183,4 +177,3 @@ class OfflineQueue {
|
|
|
183
177
|
}
|
|
184
178
|
}
|
|
185
179
|
}
|
|
186
|
-
exports.OfflineQueue = OfflineQueue;
|
package/lib/types/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -128,6 +128,8 @@ export function VerifyAIScanner({
|
|
|
128
128
|
);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
const showBottomCard = status === 'success' || status === 'error';
|
|
132
|
+
|
|
131
133
|
return (
|
|
132
134
|
<View style={[styles.container, style]}>
|
|
133
135
|
<CameraView ref={cameraRef} style={styles.camera} facing="back">
|
|
@@ -148,68 +150,105 @@ export function VerifyAIScanner({
|
|
|
148
150
|
? { aspectRatio: overlay.guideFrameAspectRatio }
|
|
149
151
|
: undefined,
|
|
150
152
|
]}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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>
|
|
158
160
|
</View>
|
|
159
161
|
)}
|
|
160
162
|
|
|
161
|
-
{/*
|
|
163
|
+
{/* Processing spinner */}
|
|
162
164
|
{status === 'processing' && (
|
|
163
|
-
<View style={styles.
|
|
165
|
+
<View style={styles.processingOverlay}>
|
|
164
166
|
<ActivityIndicator size="large" color="#fff" />
|
|
165
167
|
<Text style={styles.statusText}>Analyzing photo...</Text>
|
|
166
168
|
</View>
|
|
167
169
|
)}
|
|
168
170
|
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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>
|
|
180
199
|
</View>
|
|
181
|
-
|
|
182
|
-
</View>
|
|
183
|
-
)}
|
|
200
|
+
)}
|
|
184
201
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
)}
|
|
191
217
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
)}
|
|
206
241
|
</View>
|
|
207
|
-
|
|
242
|
+
</View>
|
|
208
243
|
</CameraView>
|
|
209
244
|
</View>
|
|
210
245
|
);
|
|
211
246
|
}
|
|
212
247
|
|
|
248
|
+
const CORNER_SIZE = 30;
|
|
249
|
+
const CORNER_THICKNESS = 3;
|
|
250
|
+
const CORNER_COLOR = 'rgba(255, 255, 255, 0.7)';
|
|
251
|
+
|
|
213
252
|
const styles = StyleSheet.create({
|
|
214
253
|
container: {
|
|
215
254
|
flex: 1,
|
|
@@ -232,6 +271,8 @@ const styles = StyleSheet.create({
|
|
|
232
271
|
fontSize: 18,
|
|
233
272
|
fontWeight: '600',
|
|
234
273
|
},
|
|
274
|
+
|
|
275
|
+
// Guide frame with corner brackets
|
|
235
276
|
guideContainer: {
|
|
236
277
|
flex: 1,
|
|
237
278
|
justifyContent: 'center',
|
|
@@ -241,66 +282,58 @@ const styles = StyleSheet.create({
|
|
|
241
282
|
guideFrame: {
|
|
242
283
|
width: '100%',
|
|
243
284
|
aspectRatio: 4 / 3,
|
|
244
|
-
borderWidth: 2,
|
|
245
|
-
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
246
|
-
borderRadius: 12,
|
|
247
|
-
borderStyle: 'dashed',
|
|
248
|
-
},
|
|
249
|
-
instructionsContainer: {
|
|
250
|
-
paddingHorizontal: 20,
|
|
251
|
-
paddingBottom: 20,
|
|
252
|
-
alignItems: 'center',
|
|
253
285
|
},
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
},
|
|
259
|
-
statusOverlay: {
|
|
260
|
-
...StyleSheet.absoluteFillObject,
|
|
261
|
-
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
262
|
-
justifyContent: 'center',
|
|
263
|
-
alignItems: 'center',
|
|
264
|
-
gap: 16,
|
|
286
|
+
corner: {
|
|
287
|
+
position: 'absolute',
|
|
288
|
+
width: CORNER_SIZE,
|
|
289
|
+
height: CORNER_SIZE,
|
|
265
290
|
},
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
291
|
+
cornerTopLeft: {
|
|
292
|
+
top: 0,
|
|
293
|
+
left: 0,
|
|
294
|
+
borderTopWidth: CORNER_THICKNESS,
|
|
295
|
+
borderLeftWidth: CORNER_THICKNESS,
|
|
296
|
+
borderColor: CORNER_COLOR,
|
|
297
|
+
borderTopLeftRadius: 4,
|
|
270
298
|
},
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
299
|
+
cornerTopRight: {
|
|
300
|
+
top: 0,
|
|
301
|
+
right: 0,
|
|
302
|
+
borderTopWidth: CORNER_THICKNESS,
|
|
303
|
+
borderRightWidth: CORNER_THICKNESS,
|
|
304
|
+
borderColor: CORNER_COLOR,
|
|
305
|
+
borderTopRightRadius: 4,
|
|
275
306
|
},
|
|
276
|
-
|
|
277
|
-
|
|
307
|
+
cornerBottomLeft: {
|
|
308
|
+
bottom: 0,
|
|
309
|
+
left: 0,
|
|
310
|
+
borderBottomWidth: CORNER_THICKNESS,
|
|
311
|
+
borderLeftWidth: CORNER_THICKNESS,
|
|
312
|
+
borderColor: CORNER_COLOR,
|
|
313
|
+
borderBottomLeftRadius: 4,
|
|
278
314
|
},
|
|
279
|
-
|
|
280
|
-
|
|
315
|
+
cornerBottomRight: {
|
|
316
|
+
bottom: 0,
|
|
317
|
+
right: 0,
|
|
318
|
+
borderBottomWidth: CORNER_THICKNESS,
|
|
319
|
+
borderRightWidth: CORNER_THICKNESS,
|
|
320
|
+
borderColor: CORNER_COLOR,
|
|
321
|
+
borderBottomRightRadius: 4,
|
|
281
322
|
},
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
323
|
+
|
|
324
|
+
// Bottom area: flex column for instructions + capture button
|
|
325
|
+
bottomArea: {
|
|
326
|
+
paddingBottom: 40,
|
|
327
|
+
alignItems: 'center',
|
|
286
328
|
},
|
|
287
|
-
|
|
288
|
-
color: '
|
|
329
|
+
instructionsText: {
|
|
330
|
+
color: 'rgba(255, 255, 255, 0.8)',
|
|
289
331
|
fontSize: 14,
|
|
290
332
|
textAlign: 'center',
|
|
291
|
-
paddingHorizontal:
|
|
292
|
-
|
|
293
|
-
},
|
|
294
|
-
errorText: {
|
|
295
|
-
color: '#ef4444',
|
|
296
|
-
fontSize: 16,
|
|
297
|
-
fontWeight: '500',
|
|
333
|
+
paddingHorizontal: 20,
|
|
334
|
+
marginBottom: 20,
|
|
298
335
|
},
|
|
299
|
-
|
|
300
|
-
position: 'absolute',
|
|
301
|
-
bottom: 40,
|
|
302
|
-
left: 0,
|
|
303
|
-
right: 0,
|
|
336
|
+
captureButtonRow: {
|
|
304
337
|
alignItems: 'center',
|
|
305
338
|
},
|
|
306
339
|
captureButton: {
|
|
@@ -321,6 +354,82 @@ const styles = StyleSheet.create({
|
|
|
321
354
|
borderRadius: 29,
|
|
322
355
|
backgroundColor: '#fff',
|
|
323
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
|
|
324
433
|
permissionContainer: {
|
|
325
434
|
justifyContent: 'center',
|
|
326
435
|
alignItems: 'center',
|