react-native-usesense 1.0.0 → 2.0.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.
package/README.md CHANGED
@@ -1,22 +1,36 @@
1
1
  # UseSense React Native
2
2
 
3
- React Native plugin for human presence verification. Wraps the native UseSense iOS and Android SDKs into a single cross-platform package.
4
-
5
- UseSense verifies that a real human — not a bot, deepfake, or replay attack — is behind the camera during identity verification sessions. Three independent pillars run in parallel on every session:
6
-
7
- - **DeepSense** (Channel & Device Integrity): Platform attestation, runtime integrity, capture pipeline analysis. Produces a `channelTrustScore` (0-100).
8
- - **LiveSense** (Multimodal Proof-of-Life): Facial dynamics, visual integrity, presentation attack detection, audio deepfake detection. Produces a `livenessScore` (0-100).
9
- - **MatchSense** (Identity Collision Detection): 1:N face search (enrollment), 1:1 face verification (authentication), cross-identity risk scoring. Produces a `matchSenseRiskScore` (0-100).
10
-
11
- A fused `presenceConfidence` score and a final `decision` (`approved` / `rejected` / `manual_review`) are returned. The definitive verdict is delivered via HMAC-SHA256 signed webhook to your backend. **The SDK result is for UI feedback only and must never be trusted for access-control decisions.**
3
+ React Native plugin for human presence verification. Wraps the native
4
+ UseSense iOS and Android SDKs into a single cross-platform package.
5
+
6
+ UseSense verifies that a real human — not a bot, deepfake, or replay
7
+ attack — is behind the camera during identity verification sessions.
8
+ Three independent pillars run in parallel on every session:
9
+
10
+ - **DeepSense** (Channel & Device Integrity): platform attestation,
11
+ runtime integrity, capture pipeline analysis.
12
+ - **LiveSense** (Multimodal Proof-of-Life): facial dynamics, visual
13
+ integrity, presentation attack detection, audio deepfake detection.
14
+ - **MatchSense** (Identity Collision Detection): 1:N face search
15
+ on enrollment, 1:1 face verification on authentication,
16
+ cross-identity risk scoring.
17
+
18
+ A final `decision` (`APPROVE` / `REJECT` / `MANUAL_REVIEW`) is
19
+ returned to the React Native side. **The SDK result is intentionally
20
+ redacted — pillar scores, the session signature, and the full verdict
21
+ metadata are NOT exposed to the client.** Those fields are delivered
22
+ to your backend via an HMAC-SHA256 signed webhook. Never trust the
23
+ client-side decision for access-control decisions; it's for UI
24
+ feedback only.
12
25
 
13
26
  ## Requirements
14
27
 
15
- - React Native 0.73+ (New Architecture supported; Old Architecture via interop layer)
16
- - iOS 14.0+
17
- - Android API 24+ (Android 7.0)
18
- - Xcode 15.0+
19
- - Node.js 18+
28
+ - React Native **0.73+** (New Architecture supported; Old
29
+ Architecture via interop layer)
30
+ - iOS **15.0+** (native SDK requires iOS 15)
31
+ - Android **API 28+** (Android 9.0; native SDK requires API 28)
32
+ - Xcode **15.0+**
33
+ - Node.js **18+**
20
34
  - Device with front-facing camera (required on both platforms)
21
35
 
22
36
  ## Installation
@@ -27,167 +41,191 @@ npm install react-native-usesense
27
41
  yarn add react-native-usesense
28
42
  ```
29
43
 
30
- ### iOS Setup
44
+ ### iOS setup
31
45
 
32
46
  ```bash
33
47
  cd ios && pod install
34
48
  ```
35
49
 
36
- The native UseSense iOS SDK is installed automatically as a CocoaPod dependency.
37
-
38
- Add required permissions to your `Info.plist`:
50
+ The native UseSense iOS SDK is installed automatically as a CocoaPod
51
+ dependency from the public CocoaPods trunk (`UseSenseSDK`, pinned to
52
+ `~> 4.2`). Add required permissions to your `Info.plist`:
39
53
 
40
54
  ```xml
41
55
  <key>NSCameraUsageDescription</key>
42
56
  <string>UseSense needs camera access to verify your identity.</string>
43
57
 
44
- <!-- Only if enableAudio is true -->
58
+ <!-- Only if your verification flow includes audio challenges -->
45
59
  <key>NSMicrophoneUsageDescription</key>
46
60
  <string>UseSense needs microphone access for voice verification.</string>
47
-
48
- <!-- Optional, improves DeepSense accuracy -->
49
- <key>NSMotionUsageDescription</key>
50
- <string>UseSense uses motion data to improve verification accuracy.</string>
51
61
  ```
52
62
 
53
- ### Android Setup
63
+ ### Android setup
54
64
 
55
- The native UseSense Android SDK is pulled automatically via Maven Central.
65
+ The native UseSense Android SDK is pulled automatically from Maven
66
+ Central (`ai.usesense:sdk:4.2.1`). `mavenCentral()` is in every
67
+ Android project by default, so no custom repository declaration is
68
+ needed.
56
69
 
57
70
  Add required permissions to your `AndroidManifest.xml`:
58
71
 
59
72
  ```xml
60
73
  <uses-permission android:name="android.permission.CAMERA" />
61
74
  <uses-permission android:name="android.permission.INTERNET" />
62
- <!-- Only if enableAudio is true -->
63
- <uses-permission android:name="android.permission.RECORD_AUDIO" />
75
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
64
76
  ```
65
77
 
66
- No additional Gradle configuration needed. Auto-linking handles registration.
78
+ Auto-linking handles registration; no additional Gradle configuration
79
+ needed.
67
80
 
68
81
  ### New Architecture
69
82
 
70
- The plugin supports React Native's New Architecture (Turbo Modules) out of the box. If your project has the New Architecture enabled, the plugin uses the Turbo Module path automatically. No configuration needed.
83
+ The plugin supports React Native's New Architecture (Turbo Modules)
84
+ out of the box. If your project has the New Architecture enabled, the
85
+ plugin uses the Turbo Module path automatically. For projects still on
86
+ the Old Architecture (Bridge), the plugin works unchanged.
71
87
 
72
- For projects still on the Old Architecture (Bridge), the plugin includes an interop layer and works without changes.
73
-
74
- ## Quick Start
88
+ ## Quick start
75
89
 
76
90
  ```typescript
77
91
  import { UseSense } from 'react-native-usesense';
78
92
 
79
- // 1. Initialize (once, e.g. in App.tsx or app entry)
93
+ // 1. Initialize once, e.g. in your app entry point or the screen
94
+ // that renders verification.
80
95
  await UseSense.initialize({
81
- apiKey: 'your_sandbox_api_key',
82
- environment: 'sandbox',
96
+ apiKey: 'sk_sandbox_your_key_here',
97
+ environment: 'sandbox', // or 'production' or 'auto'
98
+ });
99
+
100
+ // 2. (Optional) Subscribe to lifecycle events for a progress UI.
101
+ const sub = UseSense.addListener((event) => {
102
+ console.log(event.type, event.data);
83
103
  });
84
104
 
85
- // 2. Run a verification session
105
+ // 3. Run a verification session. Presents a full-screen native
106
+ // camera activity.
86
107
  try {
87
- const result = await UseSense.startSession({
108
+ const result = await UseSense.startVerification({
88
109
  sessionType: 'enrollment',
89
110
  });
90
- console.log('Decision:', result.decision);
91
- console.log('Liveness:', result.livenessScore);
92
- console.log('Channel Trust:', result.channelTrustScore);
93
- console.log('MatchSense Risk:', result.matchSenseRiskScore);
111
+ console.log('Decision:', result.decision); // 'APPROVE' | 'REJECT' | 'MANUAL_REVIEW'
112
+ console.log('Session ID:', result.sessionId);
94
113
  } catch (error) {
95
114
  console.error('Verification failed:', error.code, error.message);
115
+ } finally {
116
+ sub.remove();
96
117
  }
97
118
 
98
- // The definitive verdict arrives at your backend via webhook.
99
- // The SDK result is for UI feedback only.
119
+ // 4. Your backend receives the full signed verdict via webhook.
120
+ // That's where you read pillar scores and make access decisions.
100
121
  ```
101
122
 
102
123
  ## Configuration
103
124
 
104
- Full reference for `UseSenseConfig`:
125
+ ```typescript
126
+ interface UseSenseConfig {
127
+ apiKey: string;
128
+ environment?: 'sandbox' | 'production' | 'auto'; // default: 'auto'
129
+ apiEndpoint?: string; // default: https://api.usesense.ai/v1
130
+ branding?: BrandingConfig; // optional UI customization
131
+ }
132
+
133
+ interface BrandingConfig {
134
+ logoUrl?: string;
135
+ primaryColor?: string; // hex, e.g. '#4F7CFF'
136
+ buttonRadius?: number; // in dp
137
+ fontFamily?: string;
138
+ displayName?: string; // Android only
139
+ redirectUrl?: string; // Android only
140
+ }
141
+ ```
105
142
 
106
- | Parameter | Type | Required | Default | Description |
107
- |-----------|------|----------|---------|-------------|
108
- | `apiKey` | `string` | Yes | — | Your API key from the [UseSense dashboard](https://app.usesense.ai) |
109
- | `environment` | `'production' \| 'sandbox'` | No | `'sandbox'` | Target environment |
110
- | `organizationId` | `string` | No | — | Organization ID (inferred from API key) |
111
- | `sessionType` | `'enrollment' \| 'authentication'` | No | `'enrollment'` | Default session type |
112
- | `identityId` | `string` | No | — | Identity ID for authentication sessions |
113
- | `challengePolicy` | `'standard' \| 'enhanced' \| 'adaptive'` | No | `'standard'` | Challenge difficulty policy |
114
- | `enableAudio` | `boolean` | No | `false` | Enable audio capture for voice deepfake detection |
115
- | `timeout` | `number` | No | `60000` | Maximum session duration in milliseconds |
116
- | `metadata` | `Record<string, string>` | No | — | Custom key-value pairs attached to session |
143
+ `environment: 'auto'` (the default) detects the environment from the
144
+ API key prefix: `sk_sandbox_*` / `pk_sandbox_*` → sandbox,
145
+ `sk_prod_*` / `pk_prod_*` → production. Set it explicitly if you want
146
+ to override the auto-detection.
117
147
 
118
- ## Session Types
148
+ ## Session types
119
149
 
120
150
  ### Enrollment
121
151
 
122
- First-time face registration. The system indexes the user's face, performs a 1:N duplicate scan via MatchSense, and creates an identity record if approved.
152
+ First-time face registration. The system captures the user's face,
153
+ runs a 1:N duplicate scan via MatchSense, and creates an identity
154
+ record if approved.
123
155
 
124
156
  ```typescript
125
- const result = await UseSense.startSession({
157
+ const result = await UseSense.startVerification({
126
158
  sessionType: 'enrollment',
159
+ externalUserId: 'user_12345', // your internal user ID
127
160
  });
128
161
 
129
- if (result.decision === 'approved') {
130
- // Identity created. Store result.sessionId for reference.
131
- console.log('Enrollment approved. Presence confidence:', result.presenceConfidence);
162
+ if (result.isApproved) {
163
+ // Identity created. Store result.sessionId for reference; the
164
+ // identity ID arrives on your backend via the webhook.
132
165
  }
133
166
  ```
134
167
 
135
168
  ### Authentication
136
169
 
137
- Returning user claims an existing identity. The system performs 1:1 verification against the enrolled template, plus a 1:N cross-identity scan.
170
+ Returning user claims an existing identity. The system performs a
171
+ 1:1 verification against the enrolled template, plus a 1:N
172
+ cross-identity scan to detect identity swaps. The `identityId` must
173
+ be an identity that was previously enrolled — your backend persisted
174
+ it when the enrollment webhook fired.
138
175
 
139
176
  ```typescript
140
- const result = await UseSense.startSession({
177
+ const result = await UseSense.startVerification({
141
178
  sessionType: 'authentication',
142
- identityId: 'idn_abc123', // required for authentication
179
+ identityId: 'idn_abc123',
180
+ externalUserId: 'user_12345',
143
181
  });
144
182
 
145
- if (result.decision === 'approved') {
146
- console.log('Identity verified');
183
+ if (result.isApproved) {
184
+ // Identity verified. Proceed with login / transaction / action.
147
185
  }
148
186
  ```
149
187
 
150
- ## Handling Results
188
+ ## Handling results
151
189
 
152
- `UseSenseResult` contains the full verification breakdown:
190
+ `UseSenseResult` is the **only** shape returned to JS. It contains
191
+ five fields and three convenience booleans:
153
192
 
154
193
  | Field | Type | Description |
155
194
  |-------|------|-------------|
156
195
  | `sessionId` | `string` | Unique session identifier |
157
- | `decision` | `'approved' \| 'rejected' \| 'manual_review'` | Verification decision |
158
- | `channelTrustScore` | `number` | DeepSense channel integrity score (0-100). Higher is more trusted. |
159
- | `livenessScore` | `number` | LiveSense liveness score (0-100). Higher means more likely live. |
160
- | `matchSenseRiskScore` | `number` | MatchSense risk score (0-100). Lower is better. |
161
- | `presenceConfidence` | `number` | Fused presence confidence (0-100). Higher is more confident. |
162
- | `reasons` | `string[]` | Reasons contributing to the decision |
163
- | `ruleTriggered` | `string?` | Rule that triggered the decision, if any |
164
- | `recommendedAction` | `string?` | Recommended action for the integrator |
165
- | `sessionSignature` | `string` | Cryptographic signature for result verification |
166
-
167
- Handle each decision type in your component:
196
+ | `sessionType` | `string \| null` | `'enrollment'` or `'authentication'` |
197
+ | `identityId` | `string \| null` | Assigned identity ID (enrollment) or verified identity (authentication) |
198
+ | `decision` | `'APPROVE' \| 'REJECT' \| 'MANUAL_REVIEW'` | Final decision from the server |
199
+ | `timestamp` | `string` | ISO 8601 timestamp |
200
+ | `isApproved` | `boolean` | Convenience: `decision === 'APPROVE'` |
201
+ | `isRejected` | `boolean` | Convenience: `decision === 'REJECT'` |
202
+ | `isPendingReview` | `boolean` | Convenience: `decision === 'MANUAL_REVIEW'` |
203
+
204
+ **Pillar scores, the fused confidence value, the session signature,
205
+ and the full verdict metadata are intentionally NOT returned across
206
+ the bridge.** Consume them via the signed webhook delivered to your
207
+ backend. This design prevents client-side reverse-engineering of the
208
+ scoring logic and forces access-control decisions onto the backend
209
+ where they belong.
168
210
 
169
211
  ```tsx
170
212
  import React from 'react';
171
- import { View, Text, Button } from 'react-native';
213
+ import { View, Button } from 'react-native';
172
214
  import { UseSense, UseSenseResult } from 'react-native-usesense';
173
215
 
174
216
  function VerificationScreen({ navigation }) {
175
217
  const handleVerify = async () => {
176
218
  try {
177
- const result: UseSenseResult = await UseSense.startSession({
219
+ const result = await UseSense.startVerification({
178
220
  sessionType: 'enrollment',
179
221
  });
180
222
 
181
- switch (result.decision) {
182
- case 'approved':
183
- navigation.navigate('Home');
184
- break;
185
- case 'rejected':
186
- navigation.navigate('Retry', { reasons: result.reasons });
187
- break;
188
- case 'manual_review':
189
- navigation.navigate('PendingReview', { sessionId: result.sessionId });
190
- break;
223
+ if (result.isApproved) {
224
+ navigation.navigate('Home');
225
+ } else if (result.isRejected) {
226
+ navigation.navigate('Retry', { sessionId: result.sessionId });
227
+ } else if (result.isPendingReview) {
228
+ navigation.navigate('PendingReview', { sessionId: result.sessionId });
191
229
  }
192
230
  } catch (error) {
193
231
  console.error('Session failed:', error.code);
@@ -202,328 +240,184 @@ function VerificationScreen({ navigation }) {
202
240
  }
203
241
  ```
204
242
 
205
- ## Event Listening
243
+ ## Events
206
244
 
207
- Subscribe to real-time events during the verification lifecycle. Call `addListener()` before `startSession()` to receive all events.
208
-
209
- ```tsx
210
- import { UseSense, UseSenseEvent } from 'react-native-usesense';
211
- import { useEffect, useState } from 'react';
212
-
213
- function VerificationScreen() {
214
- const [events, setEvents] = useState<UseSenseEvent[]>([]);
215
-
216
- useEffect(() => {
217
- const listener = UseSense.addListener((event) => {
218
- setEvents(prev => [...prev, event]);
219
-
220
- switch (event.type) {
221
- case 'session_started':
222
- console.log('Session:', event.sessionId);
223
- break;
224
- case 'challenge_presented':
225
- console.log('Challenge:', event.challengeType);
226
- break;
227
- case 'challenge_completed':
228
- console.log('Challenge completed:', event.challengeType);
229
- break;
230
- case 'processing':
231
- console.log('Stage:', event.stage); // 'deepsense' | 'livesense' | 'matchsense' | 'fusion'
232
- break;
233
- case 'session_completed':
234
- console.log('Result:', event.result.decision);
235
- break;
236
- case 'session_error':
237
- console.error('Error:', event.error.code);
238
- break;
239
- }
240
- });
241
-
242
- return () => listener.remove();
243
- }, []);
244
-
245
- // render events...
246
- }
247
- ```
248
-
249
- ## Error Handling
250
-
251
- All errors include a machine-readable `code` and a human-readable `message`.
252
-
253
- | Code | Description | Recovery |
254
- |------|-------------|----------|
255
- | `camera_permission_denied` | Camera access not granted | Prompt user; link to device Settings via `Linking.openSettings()` |
256
- | `microphone_permission_denied` | Microphone not granted (audio enabled) | Prompt or disable audio |
257
- | `session_timeout` | Exceeded configured timeout | Retry with new session |
258
- | `session_expired` | 15-minute server-side expiry | Start new session |
259
- | `network_error` | API unreachable | Check connectivity, retry |
260
- | `sdk_not_initialized` | `startSession()` before `initialize()` | Call `initialize()` at app startup |
261
- | `invalid_config` | Missing or invalid config | Check `apiKey` and required fields |
262
- | `invalid_api_key` | Key rejected by server | Verify in dashboard; check environment |
263
- | `insufficient_credits` | Zero credit balance | Purchase credits in dashboard |
264
- | `identity_not_found` | `identityId` doesn't exist | Verify identity was enrolled |
265
- | `camera_unavailable` | No front camera found | Inform user |
266
- | `device_not_supported` | Fails hardware requirements | Inform user |
267
- | `session_cancelled` | Cancelled by user or programmatically | Handle gracefully |
268
- | `play_integrity_unavailable` | (Android) Play Integrity API unavailable | DeepSense uses fallback signals |
269
- | `server_error` | 5xx from UseSense API | Retry; contact support if persistent |
245
+ Subscribe to real-time lifecycle events via `UseSense.addListener`.
246
+ Events flow from both iOS and Android in the same canonical
247
+ `UPPER_SNAKE_CASE` shape so you can key off `event.type` without any
248
+ platform branching.
270
249
 
271
250
  ```typescript
272
- import { UseSense, UseSenseError } from 'react-native-usesense';
273
- import { Alert, Linking } from 'react-native';
274
-
275
- async function verify() {
276
- try {
277
- const result = await UseSense.startSession({ sessionType: 'enrollment' });
278
- // handle result
279
- } catch (error) {
280
- const err = error as UseSenseError;
281
-
282
- switch (err.code) {
283
- case 'camera_permission_denied':
284
- Alert.alert(
285
- 'Camera Required',
286
- 'UseSense needs camera access to verify your identity.',
287
- [
288
- { text: 'Cancel', style: 'cancel' },
289
- { text: 'Open Settings', onPress: () => Linking.openSettings() },
290
- ],
291
- );
292
- break;
293
-
294
- case 'session_timeout':
295
- case 'session_expired':
296
- case 'network_error':
297
- Alert.alert('Try Again', 'The session could not be completed. Please try again.');
298
- break;
299
-
300
- case 'insufficient_credits':
301
- console.error('UseSense credit balance is zero');
302
- Alert.alert('Service Unavailable', 'Verification is temporarily unavailable.');
303
- break;
304
-
305
- case 'session_cancelled':
306
- // User cancelled -- no action needed
307
- break;
308
-
309
- default:
310
- Alert.alert('Error', err.message);
311
- break;
312
- }
313
- }
314
- }
315
- ```
316
-
317
- ## Server-Side Webhook Verification
318
-
319
- **Never trust the SDK result for access-control decisions.** The SDK response is for UI feedback only — showing the user a success or failure screen. The definitive verdict arrives at your backend via an HMAC-SHA256 signed webhook.
320
-
321
- ### Webhook Payload
322
-
323
- ```json
324
- {
325
- "event": "session.completed",
326
- "session_id": "ses_abc123",
327
- "organization_id": "org_xyz",
328
- "timestamp": "2026-03-12T10:30:00Z",
329
- "data": {
330
- "decision": "approved",
331
- "channel_trust_score": 95,
332
- "liveness_score": 92,
333
- "matchsense_risk_score": 8,
334
- "presence_confidence": 94,
335
- "session_type": "enrollment",
336
- "identity_id": "idn_def456",
337
- "reasons": [],
338
- "rule_triggered": null,
339
- "session_signature": "sig_..."
340
- }
341
- }
342
- ```
343
-
344
- ### Signature Verification — Node.js (Express)
345
-
346
- ```javascript
347
- const crypto = require('crypto');
348
-
349
- app.post('/webhooks/usesense', express.raw({ type: 'application/json' }), (req, res) => {
350
- const signature = req.headers['x-usesense-signature'];
351
- const timestamp = req.headers['x-usesense-timestamp'];
352
-
353
- const payload = `${timestamp}.${req.body}`;
354
- const expected = crypto
355
- .createHmac('sha256', process.env.USESENSE_WEBHOOK_SECRET)
356
- .update(payload)
357
- .digest('hex');
358
-
359
- if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
360
- return res.status(401).send('Invalid signature');
361
- }
362
-
363
- const event = JSON.parse(req.body);
251
+ import { UseSense, UseSenseEvent } from 'react-native-usesense';
364
252
 
365
- if (event.event === 'session.completed') {
366
- const { decision, session_id } = event.data;
367
- // Update your database based on the definitive decision
253
+ const subscription = UseSense.addListener((event: UseSenseEvent) => {
254
+ switch (event.type) {
255
+ case 'SESSION_CREATED':
256
+ console.log('Session started:', event.data);
257
+ break;
258
+ case 'PERMISSIONS_REQUESTED':
259
+ case 'PERMISSIONS_GRANTED':
260
+ case 'PERMISSIONS_DENIED':
261
+ console.log('Permissions:', event.type);
262
+ break;
263
+ case 'CAPTURE_STARTED':
264
+ case 'FRAME_CAPTURED':
265
+ case 'CAPTURE_COMPLETED':
266
+ // Update your capture progress UI
267
+ break;
268
+ case 'CHALLENGE_STARTED':
269
+ case 'CHALLENGE_COMPLETED':
270
+ console.log('Challenge:', event.data);
271
+ break;
272
+ case 'UPLOAD_STARTED':
273
+ case 'UPLOAD_PROGRESS':
274
+ case 'UPLOAD_COMPLETED':
275
+ // Show upload spinner
276
+ break;
277
+ case 'DECISION_RECEIVED':
278
+ console.log('Verdict received from server');
279
+ break;
280
+ case 'ERROR':
281
+ console.error('SDK error:', event.data);
282
+ break;
368
283
  }
369
-
370
- res.status(200).send('OK');
371
284
  });
372
- ```
373
-
374
- ### Signature Verification — Python (Flask)
375
285
 
376
- ```python
377
- import hmac
378
- import hashlib
379
-
380
- @app.route('/webhooks/usesense', methods=['POST'])
381
- def usesense_webhook():
382
- signature = request.headers.get('X-UseSense-Signature')
383
- timestamp = request.headers.get('X-UseSense-Timestamp')
384
-
385
- payload = f"{timestamp}.{request.data.decode()}"
386
- expected = hmac.new(
387
- USESENSE_WEBHOOK_SECRET.encode(),
388
- payload.encode(),
389
- hashlib.sha256
390
- ).hexdigest()
391
-
392
- if not hmac.compare_digest(signature, expected):
393
- return 'Invalid signature', 401
394
-
395
- event = request.get_json()
396
-
397
- if event['event'] == 'session.completed':
398
- decision = event['data']['decision']
399
- session_id = event['session_id']
400
- # Update your database based on the definitive decision
401
-
402
- return 'OK', 200
286
+ // Later, when the screen unmounts:
287
+ subscription.remove();
403
288
  ```
404
289
 
405
- ### Signature Verification — Go (net/http)
290
+ Full event list: `SESSION_CREATED`, `PERMISSIONS_REQUESTED`,
291
+ `PERMISSIONS_GRANTED`, `PERMISSIONS_DENIED`, `CAPTURE_STARTED`,
292
+ `FRAME_CAPTURED`, `CAPTURE_COMPLETED`, `AUDIO_RECORD_STARTED`,
293
+ `AUDIO_RECORD_COMPLETED`, `CHALLENGE_STARTED`, `CHALLENGE_COMPLETED`,
294
+ `UPLOAD_STARTED`, `UPLOAD_PROGRESS`, `UPLOAD_COMPLETED`,
295
+ `COMPLETE_STARTED`, `DECISION_RECEIVED`, `IMAGE_QUALITY_CHECK`,
296
+ `ERROR`.
406
297
 
407
- ```go
408
- func useSenseWebhook(w http.ResponseWriter, r *http.Request) {
409
- signature := r.Header.Get("X-UseSense-Signature")
410
- timestamp := r.Header.Get("X-UseSense-Timestamp")
411
-
412
- body, err := io.ReadAll(r.Body)
413
- if err != nil {
414
- http.Error(w, "Bad request", http.StatusBadRequest)
415
- return
416
- }
298
+ ## Error handling
417
299
 
418
- payload := fmt.Sprintf("%s.%s", timestamp, string(body))
419
- mac := hmac.New(sha256.New, []byte(os.Getenv("USESENSE_WEBHOOK_SECRET")))
420
- mac.Write([]byte(payload))
421
- expected := hex.EncodeToString(mac.Sum(nil))
300
+ `UseSense.startVerification` rejects with a `UseSenseError` on
301
+ failure. Codes are uppercase strings matching the native SDK's
302
+ canonical codes, plus a small set of bridge-specific codes
303
+ (`session_cancelled`, `sdk_not_initialized`, `no_view_controller`,
304
+ `invalid_config`).
422
305
 
423
- if !hmac.Equal([]byte(signature), []byte(expected)) {
424
- http.Error(w, "Invalid signature", http.StatusUnauthorized)
425
- return
426
- }
427
-
428
- var event map[string]interface{}
429
- json.Unmarshal(body, &event)
306
+ ```typescript
307
+ import { UseSense, UseSenseError } from 'react-native-usesense';
430
308
 
431
- // Process the event
432
- w.WriteHeader(http.StatusOK)
433
- w.Write([]byte("OK"))
309
+ try {
310
+ await UseSense.startVerification({ sessionType: 'enrollment' });
311
+ } catch (err) {
312
+ const error = err as UseSenseError;
313
+ switch (error.code) {
314
+ case 'session_cancelled':
315
+ case 'USER_CANCELLED':
316
+ // User tapped cancel / backed out. Silent.
317
+ break;
318
+ case 'CAMERA_PERMISSION_DENIED':
319
+ // Direct the user to app settings.
320
+ break;
321
+ case 'NETWORK_ERROR':
322
+ case 'NETWORK_TIMEOUT':
323
+ // Retry with exponential backoff.
324
+ break;
325
+ case 'SESSION_EXPIRED':
326
+ // Start a fresh session.
327
+ break;
328
+ case 'QUOTA_EXCEEDED':
329
+ case 'INSUFFICIENT_CREDITS':
330
+ // Rate-limited or out of credits. Surface to the user.
331
+ break;
332
+ case 'INVALID_CONFIG':
333
+ case 'sdk_not_initialized':
334
+ // Check your API key and that initialize() ran successfully.
335
+ break;
336
+ default:
337
+ // UNKNOWN_ERROR, SERVER_ERROR, SERVICE_UNAVAILABLE, etc.
338
+ }
434
339
  }
435
340
  ```
436
341
 
437
- ## Sandbox vs Production
438
-
439
- | | Sandbox | Production |
440
- |---|---------|-----------|
441
- | Cost | Free, unlimited | Credit-based |
442
- | API keys | Separate | Separate |
443
- | Features | All features identical | All features identical |
444
- | Deduplication | Elevated risk scores (shared face collection) | Isolated per organization |
445
-
446
- Switch by changing `environment` in config:
447
-
448
- ```typescript
449
- await UseSense.initialize({
450
- apiKey: process.env.USESENSE_API_KEY,
451
- environment: __DEV__ ? 'sandbox' : 'production',
452
- });
453
- ```
454
-
455
- ## Troubleshooting
342
+ Full error code reference: see the [native iOS SDK's
343
+ `UseSenseErrorCode`](https://github.com/qudusadeyemi/usesense-ios-sdk/blob/main/Sources/UseSense/UseSenseError.swift)
344
+ and the [native Android SDK's
345
+ `UseSenseError`](https://github.com/qudusadeyemi/usesense-android-sdk/blob/main/sdk/src/main/java/com/usesense/sdk/UseSenseError.kt).
456
346
 
457
- | Problem | Solution |
458
- |---------|----------|
459
- | `Module 'react-native-usesense' not found` | Run `pod install` for iOS; rebuild for Android; verify auto-linking |
460
- | `Camera permission denied on iOS` | Add `NSCameraUsageDescription` to `Info.plist` |
461
- | `Build fails on iOS with 'pod not found'` | Run `pod repo update` then `pod install` |
462
- | `Build fails on Android with dependency resolution error` | Ensure `mavenCentral()` is in your project-level `repositories` |
463
- | `Session always times out` | Check network connectivity; increase `timeout` in config |
464
- | `Deduplication always returns high risk on sandbox` | Expected — sandbox uses a shared face collection |
465
- | `Events not firing` | Ensure `addListener()` is called before `startSession()` |
466
- | `New Architecture errors` | Ensure codegen ran: `cd android && ./gradlew generateCodegenArtifactsFromSchema` |
467
- | `App crashes on Android with TransactionTooLargeException` | Update to latest plugin version |
347
+ ## Security model: always verify the webhook
468
348
 
469
- ## TypeScript Support
349
+ **The most important section in this document.** Repeat after me:
470
350
 
471
- The plugin ships with full TypeScript declarations. All types are exported from the main entry point:
351
+ 1. **NEVER** trust the SDK result for access-control decisions. The
352
+ SDK runs on the user's device and can be tampered with.
353
+ 2. The definitive verdict arrives at your backend via an HMAC-SHA256
354
+ signed webhook at the endpoint you configured in the Watchtower
355
+ dashboard.
356
+ 3. Always verify the webhook signature before acting on it. Reject
357
+ any webhook whose signature doesn't match — an attacker with the
358
+ endpoint URL but not the signing secret would otherwise be able
359
+ to forge approved verdicts.
360
+ 4. Use the webhook's pillar scores (`channelTrustScore`,
361
+ `livenessScore`, `matchSenseRiskScore`, `presenceConfidence`) to
362
+ make your actual access decision. The client-side `decision`
363
+ field is a hint, not the source of truth.
472
364
 
473
- ```typescript
474
- import {
475
- UseSense,
476
- UseSenseConfig,
477
- UseSenseResult,
478
- UseSenseError,
479
- UseSenseEvent,
480
- UseSenseDecision,
481
- UseSenseEnvironment,
482
- SessionType,
483
- ChallengePolicy,
484
- ProcessingStage,
485
- StartSessionOptions,
486
- SessionStatus,
487
- UseSenseSubscription,
488
- } from 'react-native-usesense';
489
- ```
365
+ See the native SDKs' READMEs for webhook verification examples in
366
+ Node.js, Python, and Go.
490
367
 
491
- ## API Reference
368
+ ## Full API reference
492
369
 
493
370
  ### `UseSense.initialize(config: UseSenseConfig): Promise<void>`
494
371
 
495
- Initialize the plugin. Must be called once before any other method.
372
+ Initializes the plugin. Must be called once before any other method.
373
+ Calling again with a different key or environment replaces the
374
+ previous configuration. Throws if `apiKey` is empty or the native
375
+ side fails to initialize.
496
376
 
497
- ### `UseSense.startSession(options?: StartSessionOptions): Promise<UseSenseResult>`
377
+ ### `UseSense.startVerification(request: VerificationRequest): Promise<UseSenseResult>`
498
378
 
499
- Start a verification session. Presents a full-screen native camera UI.
379
+ Runs a verification session. Presents a full-screen native camera
380
+ activity. Resolves when the session completes or rejects on failure
381
+ (including user cancellation). The returned `UseSenseResult` is
382
+ always the redacted five-field shape.
500
383
 
501
- ### `UseSense.cancelSession(): Promise<void>`
384
+ ### `UseSense.addListener(callback: (event) => void): UseSenseSubscription`
502
385
 
503
- Cancel an in-progress verification session.
386
+ Subscribes to lifecycle events. Returns a handle with a `remove()`
387
+ method. Call `addListener` **before** `startVerification` so the
388
+ subscription captures the earliest events.
504
389
 
505
- ### `UseSense.getSessionStatus(sessionId: string): Promise<SessionStatus>`
390
+ ### `UseSense.reset(): Promise<void>`
506
391
 
507
- Get the status of a verification session by ID.
392
+ Clears all event listeners and releases the native client. Call this
393
+ when you want to re-initialize with a different key, or during test
394
+ teardown. Not required during normal usage.
508
395
 
509
- ### `UseSense.addListener(callback: (event: UseSenseEvent) => void): UseSenseSubscription`
396
+ ### `UseSense.isInitialized(): Promise<boolean>`
510
397
 
511
- Subscribe to real-time events during the verification lifecycle.
398
+ Returns whether the native plugin has an active SDK client. Use this
399
+ for feature-flagging UI that should only show when the SDK is ready.
512
400
 
513
- ### `UseSense.getSdkVersion(): string`
401
+ ## Example app
514
402
 
515
- Get the native SDK version string.
403
+ See [example/](./example) for a full-featured demo app. Paste your
404
+ sandbox or production API key into the UI at runtime (persisted
405
+ across launches via AsyncStorage) and run on a physical device. The
406
+ example shows the canonical integration pattern for `initialize`,
407
+ `startVerification`, event subscription, result display, and error
408
+ handling.
516
409
 
517
410
  ## Changelog
518
411
 
519
- See [CHANGELOG.md](./CHANGELOG.md).
412
+ See [CHANGELOG.md](CHANGELOG.md).
520
413
 
521
414
  ## License
522
415
 
523
- Proprietary. See [LICENSE](./LICENSE) file.
416
+ Proprietary. See [LICENSE](LICENSE).
524
417
 
525
418
  ## Support
526
419
 
527
- - Documentation: [https://watch.usesense.ai/developer-docs](https://watchtower.usesense.ai/developer-docs)
528
- - Dashboard: [https://watchtower.usesense.ai](https://watchtower.usesense.ai)
529
- - Email: [support@usesense.ai](mailto:support@usesense.ai)
420
+ - Documentation: https://watchtower.usesense.ai/developer-docs
421
+ - Dashboard: https://watchtower.usesense.ai
422
+ - Email: support@usesense.ai
423
+ - Issues: https://github.com/qudusadeyemi/react-native-usesense/issues