@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.
@@ -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
- return (_jsx(View, { style: [styles.container, style], children: _jsxs(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: _jsx(View, { style: [
76
- styles.guideFrame,
77
- overlay.guideFrameAspectRatio
78
- ? { aspectRatio: overlay.guideFrameAspectRatio }
79
- : undefined,
80
- ] }) })), overlay?.instructions && status === 'idle' && (_jsx(View, { style: styles.instructionsContainer, children: _jsx(Text, { style: styles.instructionsText, children: overlay.instructions }) })), status === 'processing' && (_jsxs(View, { style: styles.statusOverlay, children: [_jsx(ActivityIndicator, { size: "large", color: "#fff" }), _jsx(Text, { style: styles.statusText, children: "Analyzing photo..." })] })), status === 'success' && result && (_jsxs(View, { style: styles.statusOverlay, children: [_jsx(View, { style: [
81
- styles.resultBadge,
82
- result.is_compliant ? styles.resultPass : styles.resultFail,
83
- ], children: _jsx(Text, { style: styles.resultText, children: result.is_compliant ? 'PASS' : 'FAIL' }) }), _jsx(Text, { style: styles.feedbackText, children: result.feedback })] })), status === 'error' && (_jsx(View, { style: styles.statusOverlay, children: _jsx(Text, { style: styles.errorText, children: "Verification failed. Try again." }) }))] }), showCaptureButton && (_jsx(View, { style: styles.bottomBar, children: _jsx(TouchableOpacity, { style: [
84
- styles.captureButton,
85
- (status === 'capturing' || status === 'processing') && styles.captureButtonDisabled,
86
- ], onPress: handleCapture, disabled: status === 'capturing' || status === 'processing', activeOpacity: 0.7, children: _jsx(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 }) }) }))] }))] })] }) }) }));
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
- instructionsContainer: {
125
- paddingHorizontal: 20,
126
- paddingBottom: 20,
127
- alignItems: 'center',
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
- statusText: {
142
- color: '#fff',
143
- fontSize: 16,
144
- 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,
145
142
  },
146
- resultBadge: {
147
- paddingHorizontal: 24,
148
- paddingVertical: 12,
149
- 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,
150
150
  },
151
- resultPass: {
152
- 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,
153
158
  },
154
- resultFail: {
155
- 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,
156
166
  },
157
- resultText: {
158
- color: '#fff',
159
- fontSize: 24,
160
- fontWeight: '700',
167
+ // Bottom area: flex column for instructions + capture button
168
+ bottomArea: {
169
+ paddingBottom: 40,
170
+ alignItems: 'center',
161
171
  },
162
- feedbackText: {
163
- color: '#fff',
172
+ instructionsText: {
173
+ color: 'rgba(255, 255, 255, 0.8)',
164
174
  fontSize: 14,
165
175
  textAlign: 'center',
166
- paddingHorizontal: 40,
167
- lineHeight: 20,
168
- },
169
- errorText: {
170
- color: '#ef4444',
171
- fontSize: 16,
172
- fontWeight: '500',
176
+ paddingHorizontal: 20,
177
+ marginBottom: 20,
173
178
  },
174
- bottomBar: {
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. */
@@ -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
- import { OfflineQueue } from '../storage/offlineQueue';
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 OfflineQueue(client) : null), [client, config.offlineMode]);
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.0",
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
- </View>
152
- )}
153
-
154
- {overlay?.instructions && status === 'idle' && (
155
- <View style={styles.instructionsContainer}>
156
- <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>
157
160
  </View>
158
161
  )}
159
162
 
160
- {/* Status indicators */}
163
+ {/* Processing spinner */}
161
164
  {status === 'processing' && (
162
- <View style={styles.statusOverlay}>
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
- {status === 'success' && result && (
169
- <View style={styles.statusOverlay}>
170
- <View
171
- style={[
172
- styles.resultBadge,
173
- result.is_compliant ? styles.resultPass : styles.resultFail,
174
- ]}
175
- >
176
- <Text style={styles.resultText}>
177
- {result.is_compliant ? 'PASS' : 'FAIL'}
178
- </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>
179
199
  </View>
180
- <Text style={styles.feedbackText}>{result.feedback}</Text>
181
- </View>
182
- )}
200
+ )}
183
201
 
184
- {status === 'error' && (
185
- <View style={styles.statusOverlay}>
186
- <Text style={styles.errorText}>Verification failed. Try again.</Text>
187
- </View>
188
- )}
189
- </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
+ )}
190
217
 
191
- {/* Capture button */}
192
- {showCaptureButton && (
193
- <View style={styles.bottomBar}>
194
- <TouchableOpacity
195
- style={[
196
- styles.captureButton,
197
- (status === 'capturing' || status === 'processing') && styles.captureButtonDisabled,
198
- ]}
199
- onPress={handleCapture}
200
- disabled={status === 'capturing' || status === 'processing'}
201
- activeOpacity={0.7}
202
- >
203
- <View style={styles.captureButtonInner} />
204
- </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
+ )}
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
- instructionsText: {
254
- color: 'rgba(255, 255, 255, 0.8)',
255
- fontSize: 14,
256
- textAlign: 'center',
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
- statusText: {
266
- color: '#fff',
267
- fontSize: 16,
268
- 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,
269
298
  },
270
- resultBadge: {
271
- paddingHorizontal: 24,
272
- paddingVertical: 12,
273
- 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,
274
306
  },
275
- resultPass: {
276
- 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,
277
314
  },
278
- resultFail: {
279
- 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,
280
322
  },
281
- resultText: {
282
- color: '#fff',
283
- fontSize: 24,
284
- fontWeight: '700',
323
+
324
+ // Bottom area: flex column for instructions + capture button
325
+ bottomArea: {
326
+ paddingBottom: 40,
327
+ alignItems: 'center',
285
328
  },
286
- feedbackText: {
287
- color: '#fff',
329
+ instructionsText: {
330
+ color: 'rgba(255, 255, 255, 0.8)',
288
331
  fontSize: 14,
289
332
  textAlign: 'center',
290
- paddingHorizontal: 40,
291
- lineHeight: 20,
292
- },
293
- errorText: {
294
- color: '#ef4444',
295
- fontSize: 16,
296
- fontWeight: '500',
333
+ paddingHorizontal: 20,
334
+ marginBottom: 20,
297
335
  },
298
- bottomBar: {
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',
@@ -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 OfflineQueue(client) : null),
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 {