@switchlabs/verify-ai-react-native 0.1.0

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.
@@ -0,0 +1,179 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
+ const MANIFEST_KEY = '@verifyai/queue_manifest';
3
+ const ITEM_PREFIX = '@verifyai/queue_item_';
4
+ const LEGACY_KEY = '@verifyai/offline_queue';
5
+ export class OfflineQueue {
6
+ constructor(client) {
7
+ this.processing = false;
8
+ this.migrated = false;
9
+ this.client = client;
10
+ }
11
+ /**
12
+ * Migrate from legacy single-key storage to per-item keys.
13
+ * Runs once per instance, no-ops if already migrated or no legacy data.
14
+ */
15
+ async migrateIfNeeded() {
16
+ if (this.migrated)
17
+ return;
18
+ this.migrated = true;
19
+ const legacy = await AsyncStorage.getItem(LEGACY_KEY);
20
+ if (!legacy)
21
+ return;
22
+ try {
23
+ const items = JSON.parse(legacy);
24
+ if (!Array.isArray(items) || items.length === 0) {
25
+ await AsyncStorage.removeItem(LEGACY_KEY);
26
+ return;
27
+ }
28
+ const ids = [];
29
+ const pairs = [];
30
+ for (const item of items) {
31
+ ids.push(item.id);
32
+ pairs.push([`${ITEM_PREFIX}${item.id}`, JSON.stringify(item)]);
33
+ }
34
+ await AsyncStorage.multiSet([
35
+ [MANIFEST_KEY, JSON.stringify(ids)],
36
+ ...pairs,
37
+ ]);
38
+ await AsyncStorage.removeItem(LEGACY_KEY);
39
+ }
40
+ catch {
41
+ // If migration fails, remove corrupt legacy data
42
+ await AsyncStorage.removeItem(LEGACY_KEY);
43
+ }
44
+ }
45
+ async getManifest() {
46
+ await this.migrateIfNeeded();
47
+ const raw = await AsyncStorage.getItem(MANIFEST_KEY);
48
+ return raw ? JSON.parse(raw) : [];
49
+ }
50
+ async setManifest(ids) {
51
+ await AsyncStorage.setItem(MANIFEST_KEY, JSON.stringify(ids));
52
+ }
53
+ /**
54
+ * Add a verification request to the offline queue.
55
+ * Returns a temporary ID that can be used to track the item.
56
+ */
57
+ async enqueue(request) {
58
+ const item = {
59
+ id: `offline_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
60
+ request,
61
+ createdAt: Date.now(),
62
+ retryCount: 0,
63
+ };
64
+ const ids = await this.getManifest();
65
+ ids.push(item.id);
66
+ await AsyncStorage.setItem(`${ITEM_PREFIX}${item.id}`, JSON.stringify(item));
67
+ await this.setManifest(ids);
68
+ return item.id;
69
+ }
70
+ /**
71
+ * Get all items currently in the offline queue.
72
+ */
73
+ async getQueue() {
74
+ const ids = await this.getManifest();
75
+ if (ids.length === 0)
76
+ return [];
77
+ const keys = ids.map((id) => `${ITEM_PREFIX}${id}`);
78
+ const pairs = await AsyncStorage.multiGet(keys);
79
+ const items = [];
80
+ for (const [, value] of pairs) {
81
+ if (value) {
82
+ try {
83
+ items.push(JSON.parse(value));
84
+ }
85
+ catch {
86
+ // Skip corrupt items
87
+ }
88
+ }
89
+ }
90
+ return items;
91
+ }
92
+ /**
93
+ * Get the number of items waiting in the queue.
94
+ */
95
+ async getQueueSize() {
96
+ const ids = await this.getManifest();
97
+ return ids.length;
98
+ }
99
+ /**
100
+ * Remove an item from the queue by ID.
101
+ */
102
+ async remove(id) {
103
+ const ids = await this.getManifest();
104
+ const filtered = ids.filter((i) => i !== id);
105
+ await this.setManifest(filtered);
106
+ await AsyncStorage.removeItem(`${ITEM_PREFIX}${id}`);
107
+ }
108
+ /**
109
+ * Clear all items from the queue.
110
+ */
111
+ async clear() {
112
+ const ids = await this.getManifest();
113
+ if (ids.length > 0) {
114
+ const keys = ids.map((id) => `${ITEM_PREFIX}${id}`);
115
+ await AsyncStorage.multiRemove([MANIFEST_KEY, ...keys]);
116
+ }
117
+ else {
118
+ await AsyncStorage.removeItem(MANIFEST_KEY);
119
+ }
120
+ }
121
+ /**
122
+ * Process all queued items, sending them to the API.
123
+ * Returns results for successfully processed items.
124
+ *
125
+ * Call this when the device comes back online or on app foreground.
126
+ *
127
+ * @param onResult - Optional callback fired for each successful verification
128
+ * @param maxRetries - Maximum retry attempts before dropping an item (default: 3)
129
+ */
130
+ async processQueue(onResult, maxRetries = 3) {
131
+ if (this.processing) {
132
+ const size = await this.getQueueSize();
133
+ return { processed: 0, failed: 0, remaining: size };
134
+ }
135
+ this.processing = true;
136
+ let processed = 0;
137
+ let failed = 0;
138
+ const remainingIds = [];
139
+ try {
140
+ const ids = await this.getManifest();
141
+ for (const id of ids) {
142
+ const raw = await AsyncStorage.getItem(`${ITEM_PREFIX}${id}`);
143
+ if (!raw)
144
+ continue;
145
+ let item;
146
+ try {
147
+ item = JSON.parse(raw);
148
+ }
149
+ catch {
150
+ // Remove corrupt item
151
+ await AsyncStorage.removeItem(`${ITEM_PREFIX}${id}`);
152
+ continue;
153
+ }
154
+ try {
155
+ const result = await this.client.verify(item.request);
156
+ processed++;
157
+ await AsyncStorage.removeItem(`${ITEM_PREFIX}${id}`);
158
+ onResult?.(item.id, result);
159
+ }
160
+ catch {
161
+ item.retryCount++;
162
+ if (item.retryCount < maxRetries) {
163
+ await AsyncStorage.setItem(`${ITEM_PREFIX}${id}`, JSON.stringify(item));
164
+ remainingIds.push(id);
165
+ }
166
+ else {
167
+ failed++;
168
+ await AsyncStorage.removeItem(`${ITEM_PREFIX}${id}`);
169
+ }
170
+ }
171
+ }
172
+ await this.setManifest(remainingIds);
173
+ return { processed, failed, remaining: remainingIds.length };
174
+ }
175
+ finally {
176
+ this.processing = false;
177
+ }
178
+ }
179
+ }
@@ -0,0 +1,58 @@
1
+ export interface VerifyAIConfig {
2
+ apiKey: string;
3
+ baseUrl?: string;
4
+ timeout?: number;
5
+ offlineMode?: boolean;
6
+ }
7
+ export interface VerificationRequest {
8
+ image: string;
9
+ policy: string;
10
+ metadata?: Record<string, unknown>;
11
+ provider?: 'openai' | 'anthropic' | 'gemini';
12
+ }
13
+ export interface VerificationResult {
14
+ id: string;
15
+ created_at: string;
16
+ status: 'success' | 'error';
17
+ is_compliant: boolean;
18
+ confidence: number;
19
+ policy: string;
20
+ violation_reasons: string[];
21
+ feedback: string;
22
+ metadata: Record<string, unknown>;
23
+ image_url: string | null;
24
+ }
25
+ export interface VerificationListResponse {
26
+ data: VerificationResult[];
27
+ has_more: boolean;
28
+ next_cursor: string | null;
29
+ }
30
+ export interface VerificationListParams {
31
+ limit?: number;
32
+ cursor?: string;
33
+ policy?: string;
34
+ status?: string;
35
+ is_compliant?: boolean;
36
+ start_date?: string;
37
+ end_date?: string;
38
+ }
39
+ export interface QueueItem {
40
+ id: string;
41
+ request: VerificationRequest;
42
+ createdAt: number;
43
+ retryCount: number;
44
+ }
45
+ export interface VerifyAIError {
46
+ error: string;
47
+ status: number;
48
+ current_usage?: number;
49
+ limit?: number;
50
+ upgrade_url?: string;
51
+ }
52
+ export type ScannerStatus = 'idle' | 'capturing' | 'processing' | 'success' | 'error';
53
+ export interface ScannerOverlayConfig {
54
+ title?: string;
55
+ instructions?: string;
56
+ showGuideFrame?: boolean;
57
+ guideFrameAspectRatio?: number;
58
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@switchlabs/verify-ai-react-native",
3
+ "version": "0.1.0",
4
+ "description": "React Native SDK for Verify AI - photo verification with AI vision processing",
5
+ "main": "./lib/index.js",
6
+ "module": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "source": "./src/index.ts",
9
+ "react-native": "./src/index.ts",
10
+ "files": [
11
+ "src",
12
+ "lib"
13
+ ],
14
+ "scripts": {
15
+ "typecheck": "tsc --noEmit",
16
+ "build": "tsc",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "react-native",
21
+ "verify-ai",
22
+ "photo-verification",
23
+ "ai-vision",
24
+ "parking-compliance",
25
+ "damage-detection"
26
+ ],
27
+ "author": "Switch Labs",
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "react": ">=18.0.0",
31
+ "react-native": ">=0.72.0",
32
+ "expo-camera": ">=15.0.0",
33
+ "@react-native-async-storage/async-storage": ">=1.19.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "expo-camera": {
37
+ "optional": true
38
+ },
39
+ "@react-native-async-storage/async-storage": {
40
+ "optional": true
41
+ }
42
+ },
43
+ "devDependencies": {
44
+ "@react-native-async-storage/async-storage": "^2.1.0",
45
+ "@types/react": "^18.2.0",
46
+ "expo-camera": "^16.0.0",
47
+ "react": "^18.2.0",
48
+ "react-native": "^0.76.0",
49
+ "typescript": "^5.5.0"
50
+ }
51
+ }
@@ -0,0 +1,158 @@
1
+ import type {
2
+ VerifyAIConfig,
3
+ VerificationRequest,
4
+ VerificationResult,
5
+ VerificationListResponse,
6
+ VerificationListParams,
7
+ VerifyAIError,
8
+ } from '../types';
9
+
10
+ const DEFAULT_BASE_URL = 'https://verify.switchlabs.dev/api/v1';
11
+ const DEFAULT_TIMEOUT = 30000;
12
+
13
+ export class VerifyAIClient {
14
+ private apiKey: string;
15
+ private baseUrl: string;
16
+ private timeout: number;
17
+
18
+ constructor(config: VerifyAIConfig) {
19
+ if (!config.apiKey) {
20
+ throw new Error('VerifyAI: apiKey is required');
21
+ }
22
+ this.apiKey = config.apiKey;
23
+ this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
24
+ this.timeout = config.timeout || DEFAULT_TIMEOUT;
25
+ }
26
+
27
+ private async request<T>(
28
+ path: string,
29
+ options: RequestInit = {}
30
+ ): Promise<T> {
31
+ const controller = new AbortController();
32
+ const timer = setTimeout(() => controller.abort(), this.timeout);
33
+
34
+ try {
35
+ const response = await fetch(`${this.baseUrl}${path}`, {
36
+ ...options,
37
+ signal: controller.signal,
38
+ headers: {
39
+ 'X-API-Key': this.apiKey,
40
+ ...options.headers,
41
+ },
42
+ });
43
+
44
+ const body = await response.json();
45
+
46
+ if (!response.ok) {
47
+ const error = body as VerifyAIError;
48
+ throw new VerifyAIRequestError(
49
+ error.error || `Request failed with status ${response.status}`,
50
+ response.status,
51
+ error
52
+ );
53
+ }
54
+
55
+ return body as T;
56
+ } finally {
57
+ clearTimeout(timer);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Submit a photo for AI verification.
63
+ *
64
+ * @param request - The verification request with base64 image and policy
65
+ * @returns The verification result with compliance status and feedback
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const result = await client.verify({
70
+ * image: base64ImageData,
71
+ * policy: 'scooter_parking',
72
+ * metadata: { device_id: 'dev_123', gps: { lat: 37.77, lng: -122.42 } },
73
+ * });
74
+ *
75
+ * if (result.is_compliant) {
76
+ * console.log('Parking approved!');
77
+ * } else {
78
+ * console.log('Issues:', result.violation_reasons);
79
+ * console.log('Feedback:', result.feedback);
80
+ * }
81
+ * ```
82
+ */
83
+ async verify(request: VerificationRequest): Promise<VerificationResult> {
84
+ return this.request<VerificationResult>('/verify', {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify(request),
88
+ });
89
+ }
90
+
91
+ /**
92
+ * List past verifications with optional filters.
93
+ *
94
+ * @param params - Pagination and filter parameters
95
+ * @returns Paginated list of verification results
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const { data, has_more, next_cursor } = await client.listVerifications({
100
+ * limit: 20,
101
+ * policy: 'scooter_parking',
102
+ * is_compliant: false,
103
+ * });
104
+ * ```
105
+ */
106
+ async listVerifications(
107
+ params: VerificationListParams = {}
108
+ ): Promise<VerificationListResponse> {
109
+ const searchParams = new URLSearchParams();
110
+ if (params.limit) searchParams.set('limit', String(params.limit));
111
+ if (params.cursor) searchParams.set('cursor', params.cursor);
112
+ if (params.policy) searchParams.set('policy', params.policy);
113
+ if (params.status) searchParams.set('status', params.status);
114
+ if (params.is_compliant !== undefined)
115
+ searchParams.set('is_compliant', String(params.is_compliant));
116
+ if (params.start_date) searchParams.set('start_date', params.start_date);
117
+ if (params.end_date) searchParams.set('end_date', params.end_date);
118
+
119
+ const qs = searchParams.toString();
120
+ return this.request<VerificationListResponse>(
121
+ `/verifications${qs ? `?${qs}` : ''}`
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Get a single verification by ID.
127
+ *
128
+ * @param id - The verification ID (e.g., "ver_8x92m4k9")
129
+ * @returns The full verification result with a fresh signed image URL
130
+ */
131
+ async getVerification(id: string): Promise<VerificationResult> {
132
+ return this.request<VerificationResult>(`/verifications/${id}`);
133
+ }
134
+ }
135
+
136
+ export class VerifyAIRequestError extends Error {
137
+ status: number;
138
+ body: VerifyAIError;
139
+
140
+ constructor(message: string, status: number, body: VerifyAIError) {
141
+ super(message);
142
+ this.name = 'VerifyAIRequestError';
143
+ this.status = status;
144
+ this.body = body;
145
+ }
146
+
147
+ get isRateLimited(): boolean {
148
+ return this.status === 429;
149
+ }
150
+
151
+ get isUnauthorized(): boolean {
152
+ return this.status === 401;
153
+ }
154
+
155
+ get upgradeUrl(): string | undefined {
156
+ return this.body.upgrade_url;
157
+ }
158
+ }