@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.
@@ -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
- exports.VerifyAIClient = VerifyAIClient;
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VerifyAIScanner = VerifyAIScanner;
4
- const jsx_runtime_1 = require("react/jsx-runtime");
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 = (0, react_1.useRef)(null);
31
- const [status, setStatus] = (0, react_1.useState)('idle');
32
- const [result, setResult] = (0, react_1.useState)(null);
33
- const [permission, requestPermission] = (0, expo_camera_1.useCameraPermissions)();
34
- const handleCapture = (0, react_1.useCallback)(async () => {
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 (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.container, style] });
71
+ return _jsx(View, { style: [styles.container, style] });
75
72
  }
76
73
  if (!permission.granted) {
77
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, styles.permissionContainer, style], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.permissionText, children: "Camera access is required for photo verification" }), (0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: styles.permissionButton, onPress: requestPermission, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.permissionButtonText, children: "Grant Camera Access" }) })] }));
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
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.container, style], children: (0, jsx_runtime_1.jsxs)(expo_camera_1.CameraView, { ref: cameraRef, style: styles.camera, facing: "back", children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.overlay, children: [overlay?.title && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.topBar, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.titleText, children: overlay.title }) })), overlay?.showGuideFrame && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.guideContainer, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
80
- styles.guideFrame,
81
- overlay.guideFrameAspectRatio
82
- ? { aspectRatio: overlay.guideFrameAspectRatio }
83
- : undefined,
84
- ] }) })), overlay?.instructions && status === 'idle' && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.instructionsContainer, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.instructionsText, children: overlay.instructions }) })), status === 'processing' && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.statusOverlay, children: [(0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { size: "large", color: "#fff" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.statusText, children: "Analyzing photo..." })] })), status === 'success' && result && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.statusOverlay, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
85
- styles.resultBadge,
86
- result.is_compliant ? styles.resultPass : styles.resultFail,
87
- ], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.resultText, children: result.is_compliant ? 'PASS' : 'FAIL' }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.feedbackText, children: result.feedback })] })), status === 'error' && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.statusOverlay, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.errorText, children: "Verification failed. Try again." }) }))] }), showCaptureButton && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.bottomBar, children: (0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: [
88
- styles.captureButton,
89
- (status === 'capturing' || status === 'processing') && styles.captureButtonDisabled,
90
- ], onPress: handleCapture, disabled: status === 'capturing' || status === 'processing', activeOpacity: 0.7, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.captureButtonInner }) }) }))] }) }));
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 styles = react_native_1.StyleSheet.create({
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
- ...react_native_1.StyleSheet.absoluteFillObject,
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
- instructionsContainer: {
129
- paddingHorizontal: 20,
130
- paddingBottom: 20,
131
- alignItems: 'center',
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
- statusText: {
146
- color: '#fff',
147
- fontSize: 16,
148
- fontWeight: '500',
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
- resultBadge: {
151
- paddingHorizontal: 24,
152
- paddingVertical: 12,
153
- borderRadius: 8,
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
- resultPass: {
156
- backgroundColor: 'rgba(34, 197, 94, 0.9)',
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
- resultFail: {
159
- backgroundColor: 'rgba(239, 68, 68, 0.9)',
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
- resultText: {
162
- color: '#fff',
163
- fontSize: 24,
164
- fontWeight: '700',
167
+ // Bottom area: flex column for instructions + capture button
168
+ bottomArea: {
169
+ paddingBottom: 40,
170
+ alignItems: 'center',
165
171
  },
166
- feedbackText: {
167
- color: '#fff',
172
+ instructionsText: {
173
+ color: 'rgba(255, 255, 255, 0.8)',
168
174
  fontSize: 14,
169
175
  textAlign: 'center',
170
- paddingHorizontal: 40,
171
- lineHeight: 20,
172
- },
173
- errorText: {
174
- color: '#ef4444',
175
- fontSize: 16,
176
- fontWeight: '500',
176
+ paddingHorizontal: 20,
177
+ marginBottom: 20,
177
178
  },
178
- bottomBar: {
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',
@@ -1,9 +1,6 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useVerifyAI = useVerifyAI;
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 = (0, react_1.useMemo)(() => new client_1.VerifyAIClient(config), [config.apiKey, config.baseUrl, config.timeout]);
41
- const offlineQueue = (0, react_1.useMemo)(() => (config.offlineMode && OfflineQueueClass ? new OfflineQueueClass(client) : null), [client, config.offlineMode]);
42
- const [loading, setLoading] = (0, react_1.useState)(false);
43
- const [error, setError] = (0, react_1.useState)(null);
44
- const [lastResult, setLastResult] = (0, react_1.useState)(null);
45
- const [queueSize, setQueueSize] = (0, react_1.useState)(0);
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 = (0, react_1.useCallback)(async () => {
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
- (0, react_1.useEffect)(() => {
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 = react_native_1.AppState.addEventListener('change', handleAppState);
60
+ const subscription = AppState.addEventListener('change', handleAppState);
64
61
  return () => subscription.remove();
65
62
  }, [offlineQueue, refreshQueueSize]);
66
- const verify = (0, react_1.useCallback)(async (request) => {
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 = (0, react_1.useCallback)((params) => client.listVerifications(params), [client]);
90
- const getVerification = (0, react_1.useCallback)((id) => client.getVerification(id), [client]);
91
- const processQueue = (0, react_1.useCallback)(async () => {
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
- var client_1 = require("./client");
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
- var useVerifyAI_1 = require("./hooks/useVerifyAI");
10
- Object.defineProperty(exports, "useVerifyAI", { enumerable: true, get: function () { return useVerifyAI_1.useVerifyAI; } });
4
+ export { useVerifyAI } from './hooks/useVerifyAI';
@@ -1,14 +1,8 @@
1
- "use strict";
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 async_storage_1.default.getItem(LEGACY_KEY);
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 async_storage_1.default.removeItem(LEGACY_KEY);
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 async_storage_1.default.multiSet([
34
+ await AsyncStorage.multiSet([
41
35
  [MANIFEST_KEY, JSON.stringify(ids)],
42
36
  ...pairs,
43
37
  ]);
44
- await async_storage_1.default.removeItem(LEGACY_KEY);
38
+ await AsyncStorage.removeItem(LEGACY_KEY);
45
39
  }
46
40
  catch {
47
41
  // If migration fails, remove corrupt legacy data
48
- await async_storage_1.default.removeItem(LEGACY_KEY);
42
+ await AsyncStorage.removeItem(LEGACY_KEY);
49
43
  }
50
44
  }
51
45
  async getManifest() {
52
46
  await this.migrateIfNeeded();
53
- const raw = await async_storage_1.default.getItem(MANIFEST_KEY);
47
+ const raw = await AsyncStorage.getItem(MANIFEST_KEY);
54
48
  return raw ? JSON.parse(raw) : [];
55
49
  }
56
50
  async setManifest(ids) {
57
- await async_storage_1.default.setItem(MANIFEST_KEY, JSON.stringify(ids));
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 async_storage_1.default.setItem(`${ITEM_PREFIX}${item.id}`, JSON.stringify(item));
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 async_storage_1.default.multiGet(keys);
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 async_storage_1.default.removeItem(`${ITEM_PREFIX}${id}`);
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 async_storage_1.default.multiRemove([MANIFEST_KEY, ...keys]);
115
+ await AsyncStorage.multiRemove([MANIFEST_KEY, ...keys]);
122
116
  }
123
117
  else {
124
- await async_storage_1.default.removeItem(MANIFEST_KEY);
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 async_storage_1.default.getItem(`${ITEM_PREFIX}${id}`);
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 async_storage_1.default.removeItem(`${ITEM_PREFIX}${id}`);
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 async_storage_1.default.removeItem(`${ITEM_PREFIX}${id}`);
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 async_storage_1.default.setItem(`${ITEM_PREFIX}${id}`, JSON.stringify(item));
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 async_storage_1.default.removeItem(`${ITEM_PREFIX}${id}`);
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;
@@ -1,2 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchlabs/verify-ai-react-native",
3
- "version": "0.1.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
6
  "types": "./lib/index.d.ts",
@@ -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
- </View>
153
- )}
154
-
155
- {overlay?.instructions && status === 'idle' && (
156
- <View style={styles.instructionsContainer}>
157
- <Text style={styles.instructionsText}>{overlay.instructions}</Text>
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
- {/* Status indicators */}
163
+ {/* Processing spinner */}
162
164
  {status === 'processing' && (
163
- <View style={styles.statusOverlay}>
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
- {status === 'success' && result && (
170
- <View style={styles.statusOverlay}>
171
- <View
172
- style={[
173
- styles.resultBadge,
174
- result.is_compliant ? styles.resultPass : styles.resultFail,
175
- ]}
176
- >
177
- <Text style={styles.resultText}>
178
- {result.is_compliant ? 'PASS' : 'FAIL'}
179
- </Text>
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
- <Text style={styles.feedbackText}>{result.feedback}</Text>
182
- </View>
183
- )}
200
+ )}
184
201
 
185
- {status === 'error' && (
186
- <View style={styles.statusOverlay}>
187
- <Text style={styles.errorText}>Verification failed. Try again.</Text>
188
- </View>
189
- )}
190
- </View>
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
- {/* Capture button */}
193
- {showCaptureButton && (
194
- <View style={styles.bottomBar}>
195
- <TouchableOpacity
196
- style={[
197
- styles.captureButton,
198
- (status === 'capturing' || status === 'processing') && styles.captureButtonDisabled,
199
- ]}
200
- onPress={handleCapture}
201
- disabled={status === 'capturing' || status === 'processing'}
202
- activeOpacity={0.7}
203
- >
204
- <View style={styles.captureButtonInner} />
205
- </TouchableOpacity>
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
- instructionsText: {
255
- color: 'rgba(255, 255, 255, 0.8)',
256
- fontSize: 14,
257
- textAlign: 'center',
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
- statusText: {
267
- color: '#fff',
268
- fontSize: 16,
269
- fontWeight: '500',
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
- resultBadge: {
272
- paddingHorizontal: 24,
273
- paddingVertical: 12,
274
- borderRadius: 8,
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
- resultPass: {
277
- backgroundColor: 'rgba(34, 197, 94, 0.9)',
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
- resultFail: {
280
- backgroundColor: 'rgba(239, 68, 68, 0.9)',
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
- resultText: {
283
- color: '#fff',
284
- fontSize: 24,
285
- fontWeight: '700',
323
+
324
+ // Bottom area: flex column for instructions + capture button
325
+ bottomArea: {
326
+ paddingBottom: 40,
327
+ alignItems: 'center',
286
328
  },
287
- feedbackText: {
288
- color: '#fff',
329
+ instructionsText: {
330
+ color: 'rgba(255, 255, 255, 0.8)',
289
331
  fontSize: 14,
290
332
  textAlign: 'center',
291
- paddingHorizontal: 40,
292
- lineHeight: 20,
293
- },
294
- errorText: {
295
- color: '#ef4444',
296
- fontSize: 16,
297
- fontWeight: '500',
333
+ paddingHorizontal: 20,
334
+ marginBottom: 20,
298
335
  },
299
- bottomBar: {
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',