react-native-usesense 1.0.0 → 2.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.
- package/README.md +267 -373
- package/android/build.gradle +7 -2
- package/android/src/main/java/com/usesense/reactnative/UseSenseFlowsModule.kt +72 -0
- package/android/src/main/java/com/usesense/reactnative/UseSenseModule.kt +82 -1
- package/android/src/main/java/com/usesense/reactnative/UseSensePackage.kt +6 -1
- package/ios/UseSenseFlowsModule.m +20 -0
- package/ios/UseSenseFlowsModule.swift +111 -0
- package/ios/UseSenseModule.m +5 -0
- package/ios/UseSenseModule.swift +277 -88
- package/ios/V4BridgeDelegate.swift +62 -0
- package/lib/commonjs/flows.js +105 -0
- package/lib/commonjs/flows.js.map +1 -0
- package/lib/commonjs/index.js +171 -98
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/flows.js +99 -0
- package/lib/module/flows.js.map +1 -0
- package/lib/module/index.js +160 -97
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/flows.d.ts +72 -0
- package/lib/typescript/src/flows.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +210 -138
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +3 -1
- package/react-native-usesense.podspec +9 -3
- package/src/flows.ts +159 -0
- package/src/index.ts +313 -159
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
63
|
+
### Android setup
|
|
54
64
|
|
|
55
|
-
The native UseSense Android SDK is pulled automatically
|
|
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
|
-
|
|
63
|
-
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
75
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
64
76
|
```
|
|
65
77
|
|
|
66
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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: '
|
|
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
|
-
//
|
|
105
|
+
// 3. Run a verification session. Presents a full-screen native
|
|
106
|
+
// camera activity.
|
|
86
107
|
try {
|
|
87
|
-
const result = await UseSense.
|
|
108
|
+
const result = await UseSense.startVerification({
|
|
88
109
|
sessionType: 'enrollment',
|
|
89
110
|
});
|
|
90
|
-
console.log('Decision:', result.decision);
|
|
91
|
-
console.log('
|
|
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
|
-
//
|
|
99
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
148
|
+
## Session types
|
|
119
149
|
|
|
120
150
|
### Enrollment
|
|
121
151
|
|
|
122
|
-
First-time face registration. The system
|
|
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.
|
|
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.
|
|
130
|
-
// Identity created. Store result.sessionId for reference
|
|
131
|
-
|
|
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
|
|
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.
|
|
177
|
+
const result = await UseSense.startVerification({
|
|
141
178
|
sessionType: 'authentication',
|
|
142
|
-
identityId: 'idn_abc123',
|
|
179
|
+
identityId: 'idn_abc123',
|
|
180
|
+
externalUserId: 'user_12345',
|
|
143
181
|
});
|
|
144
182
|
|
|
145
|
-
if (result.
|
|
146
|
-
|
|
183
|
+
if (result.isApproved) {
|
|
184
|
+
// Identity verified. Proceed with login / transaction / action.
|
|
147
185
|
}
|
|
148
186
|
```
|
|
149
187
|
|
|
150
|
-
## Handling
|
|
188
|
+
## Handling results
|
|
151
189
|
|
|
152
|
-
`UseSenseResult`
|
|
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
|
-
| `
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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,
|
|
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
|
|
219
|
+
const result = await UseSense.startVerification({
|
|
178
220
|
sessionType: 'enrollment',
|
|
179
221
|
});
|
|
180
222
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
##
|
|
243
|
+
## Events
|
|
206
244
|
|
|
207
|
-
Subscribe to real-time
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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,
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
+
**The most important section in this document.** Repeat after me:
|
|
470
350
|
|
|
471
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
|
368
|
+
## Full API reference
|
|
492
369
|
|
|
493
370
|
### `UseSense.initialize(config: UseSenseConfig): Promise<void>`
|
|
494
371
|
|
|
495
|
-
|
|
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.
|
|
377
|
+
### `UseSense.startVerification(request: VerificationRequest): Promise<UseSenseResult>`
|
|
498
378
|
|
|
499
|
-
|
|
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.
|
|
384
|
+
### `UseSense.addListener(callback: (event) => void): UseSenseSubscription`
|
|
502
385
|
|
|
503
|
-
|
|
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.
|
|
390
|
+
### `UseSense.reset(): Promise<void>`
|
|
506
391
|
|
|
507
|
-
|
|
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.
|
|
396
|
+
### `UseSense.isInitialized(): Promise<boolean>`
|
|
510
397
|
|
|
511
|
-
|
|
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
|
-
|
|
401
|
+
## Example app
|
|
514
402
|
|
|
515
|
-
|
|
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](
|
|
412
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
520
413
|
|
|
521
414
|
## License
|
|
522
415
|
|
|
523
|
-
Proprietary. See [LICENSE](
|
|
416
|
+
Proprietary. See [LICENSE](LICENSE).
|
|
524
417
|
|
|
525
418
|
## Support
|
|
526
419
|
|
|
527
|
-
- Documentation:
|
|
528
|
-
- Dashboard:
|
|
529
|
-
- Email:
|
|
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
|