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/ios/UseSenseModule.swift
CHANGED
|
@@ -2,11 +2,33 @@ import Foundation
|
|
|
2
2
|
import React
|
|
3
3
|
import UseSenseSDK
|
|
4
4
|
|
|
5
|
+
/// React Native bridge for the UseSense iOS SDK.
|
|
6
|
+
///
|
|
7
|
+
/// Exposes five JS-callable methods — `initialize`, `startVerification`,
|
|
8
|
+
/// `reset`, `isInitialized`, plus RCTEventEmitter `supportedEvents`/
|
|
9
|
+
/// `startObserving`/`stopObserving` — that mirror the native SDK's
|
|
10
|
+
/// public API without leaking any internal scoring data through the
|
|
11
|
+
/// bridge.
|
|
12
|
+
///
|
|
13
|
+
/// The result payload returned from `startVerification` is the
|
|
14
|
+
/// five-field redacted shape defined by the native SDK's
|
|
15
|
+
/// `RedactedDecisionObject`: `sessionId`, `sessionType`, `identityId`,
|
|
16
|
+
/// `decision` (`APPROVE` / `REJECT` / `MANUAL_REVIEW`), `timestamp`.
|
|
17
|
+
/// Pillar scores (channel trust, liveness, matchsense risk) are
|
|
18
|
+
/// intentionally NOT exposed — integrators must consume those via
|
|
19
|
+
/// the signed webhook delivered to their backend, never from the
|
|
20
|
+
/// client. Matches the behaviour of the native Android bridge at
|
|
21
|
+
/// `android/src/main/java/com/usesense/reactnative/UseSenseModule.kt`
|
|
22
|
+
/// and the Flutter plugin at `ios/Classes/UseSenseFlutterPlugin.swift`.
|
|
5
23
|
@objc(UseSenseModule)
|
|
6
24
|
class UseSenseModule: RCTEventEmitter {
|
|
7
25
|
|
|
26
|
+
private var client: UseSense?
|
|
27
|
+
private var eventUnsubscribe: (() -> Void)?
|
|
8
28
|
private var hasListeners = false
|
|
9
29
|
|
|
30
|
+
// MARK: - RCTEventEmitter
|
|
31
|
+
|
|
10
32
|
override func supportedEvents() -> [String]! {
|
|
11
33
|
return ["UseSenseEvent"]
|
|
12
34
|
}
|
|
@@ -20,129 +42,296 @@ class UseSenseModule: RCTEventEmitter {
|
|
|
20
42
|
}
|
|
21
43
|
|
|
22
44
|
@objc
|
|
23
|
-
func
|
|
24
|
-
|
|
45
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MARK: - Module methods
|
|
50
|
+
|
|
51
|
+
@objc
|
|
52
|
+
func initialize(_ config: NSDictionary,
|
|
53
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
54
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
55
|
+
guard let apiKey = config["apiKey"] as? String, !apiKey.isEmpty else {
|
|
56
|
+
reject("invalid_config", "apiKey is required", nil)
|
|
25
57
|
return
|
|
26
58
|
}
|
|
27
59
|
|
|
28
|
-
let environment:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
60
|
+
let environment: Environment
|
|
61
|
+
switch config["environment"] as? String {
|
|
62
|
+
case "sandbox":
|
|
63
|
+
environment = .sandbox
|
|
64
|
+
case "production":
|
|
65
|
+
environment = .production
|
|
66
|
+
case "auto", nil:
|
|
67
|
+
environment = .auto
|
|
68
|
+
default:
|
|
69
|
+
environment = .auto
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var brandingConfig: BrandingConfig? = nil
|
|
73
|
+
if let b = config["branding"] as? [String: Any] {
|
|
74
|
+
// iOS BrandingConfig accepts logoUrl, primaryColor, buttonRadius,
|
|
75
|
+
// fontFamily. Android-only fields (displayName, redirectUrl) are
|
|
76
|
+
// accepted by the JS API for cross-platform consistency but
|
|
77
|
+
// ignored here since the iOS SDK doesn't support them.
|
|
78
|
+
brandingConfig = BrandingConfig(
|
|
79
|
+
logoUrl: b["logoUrl"] as? String,
|
|
80
|
+
primaryColor: (b["primaryColor"] as? String) ?? "#4F7CFF",
|
|
81
|
+
buttonRadius: CGFloat((b["buttonRadius"] as? Int) ?? 10),
|
|
82
|
+
fontFamily: b["fontFamily"] as? String
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let apiEndpoint = (config["apiEndpoint"] as? String)
|
|
87
|
+
?? UseSenseConfig.defaultEndpoint
|
|
88
|
+
|
|
89
|
+
// v4.2: Optional on-device antispoof classifier flag (defaults to false;
|
|
90
|
+
// watchtower runs the classifier server-side when off).
|
|
91
|
+
let antispoofOnDeviceEnabled = (config["antispoofOnDeviceEnabled"] as? Bool) ?? false
|
|
92
|
+
// LiveSense v4 capture flow opt-in (zoom phase + frame phase tags).
|
|
93
|
+
let liveSenseV4Enabled = (config["liveSenseV4Enabled"] as? Bool) ?? false
|
|
94
|
+
let sdkOptions = SDKOptions(
|
|
95
|
+
antispoofOnDeviceEnabled: antispoofOnDeviceEnabled,
|
|
96
|
+
liveSenseV4Enabled: liveSenseV4Enabled
|
|
97
|
+
)
|
|
35
98
|
|
|
36
|
-
let
|
|
99
|
+
let sdkConfig = UseSenseConfig(
|
|
100
|
+
apiEndpoint: apiEndpoint,
|
|
37
101
|
apiKey: apiKey,
|
|
38
102
|
environment: environment,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
challengePolicy: {
|
|
42
|
-
switch config["challengePolicy"] as? String {
|
|
43
|
-
case "enhanced": return .enhanced
|
|
44
|
-
case "adaptive": return .adaptive
|
|
45
|
-
default: return .standard
|
|
46
|
-
}
|
|
47
|
-
}(),
|
|
48
|
-
enableAudio: config["enableAudio"] as? Bool ?? false,
|
|
49
|
-
timeout: config["timeout"] as? TimeInterval ?? 60000,
|
|
50
|
-
metadata: config["metadata"] as? [String: String]
|
|
103
|
+
branding: brandingConfig,
|
|
104
|
+
options: sdkOptions
|
|
51
105
|
)
|
|
52
106
|
|
|
53
|
-
|
|
107
|
+
// Create (or replace) the client. If a previous client was
|
|
108
|
+
// active, its event subscription is disposed before we
|
|
109
|
+
// replace it so we don't accumulate stale listeners on
|
|
110
|
+
// repeated initialize() calls.
|
|
111
|
+
eventUnsubscribe?()
|
|
112
|
+
eventUnsubscribe = nil
|
|
113
|
+
|
|
114
|
+
let newClient = UseSense(config: sdkConfig)
|
|
115
|
+
eventUnsubscribe = newClient.onEvent { [weak self] event in
|
|
116
|
+
self?.emitEvent(event)
|
|
117
|
+
}
|
|
118
|
+
client = newClient
|
|
119
|
+
|
|
120
|
+
resolve(nil)
|
|
54
121
|
}
|
|
55
122
|
|
|
56
123
|
@objc
|
|
57
|
-
func
|
|
124
|
+
func startVerification(_ request: NSDictionary,
|
|
125
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
126
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
127
|
+
guard let client = client else {
|
|
128
|
+
reject("sdk_not_initialized",
|
|
129
|
+
"UseSense SDK is not initialized. Call initialize() first.",
|
|
130
|
+
nil)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let sessionType: SessionType = (request["sessionType"] as? String) == "authentication"
|
|
135
|
+
? .authentication
|
|
136
|
+
: .enrollment
|
|
137
|
+
|
|
138
|
+
// The native iOS `VerificationRequest` takes an `AnyCodableValue`
|
|
139
|
+
// map for metadata. RN can only pass plain JSON so we coerce
|
|
140
|
+
// every value to a string — matches what the Flutter plugin
|
|
141
|
+
// does over the Pigeon channel.
|
|
142
|
+
let rawMetadata = request["metadata"] as? [String: Any]
|
|
143
|
+
let metadata: [String: AnyCodableValue]? = rawMetadata?.mapValues { value in
|
|
144
|
+
if let s = value as? String { return .string(s) }
|
|
145
|
+
if let b = value as? Bool { return .bool(b) }
|
|
146
|
+
if let i = value as? Int { return .int(i) }
|
|
147
|
+
if let d = value as? Double { return .double(d) }
|
|
148
|
+
return .string(String(describing: value))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let nativeRequest = VerificationRequest(
|
|
152
|
+
sessionType: sessionType,
|
|
153
|
+
externalUserId: request["externalUserId"] as? String,
|
|
154
|
+
identityId: request["identityId"] as? String,
|
|
155
|
+
metadata: metadata
|
|
156
|
+
)
|
|
157
|
+
|
|
58
158
|
DispatchQueue.main.async {
|
|
59
|
-
guard let
|
|
60
|
-
reject("no_view_controller",
|
|
159
|
+
guard let rootVC = Self.topmostViewController() else {
|
|
160
|
+
reject("no_view_controller",
|
|
161
|
+
"No view controller available to present the camera UI.",
|
|
162
|
+
nil)
|
|
61
163
|
return
|
|
62
164
|
}
|
|
63
165
|
|
|
64
|
-
let
|
|
65
|
-
let
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
identityId: identityId
|
|
71
|
-
) { result in
|
|
166
|
+
let session = client.startVerification(request: nativeRequest)
|
|
167
|
+
let vc = UseSenseViewController(session: session) { [weak rootVC] result in
|
|
168
|
+
// The view controller dismisses itself on the main
|
|
169
|
+
// thread before invoking this closure. We're safe to
|
|
170
|
+
// resolve/reject directly without hopping.
|
|
171
|
+
_ = rootVC
|
|
72
172
|
switch result {
|
|
73
|
-
case .success(let
|
|
74
|
-
|
|
75
|
-
"sessionId": useSenseResult.sessionId,
|
|
76
|
-
"decision": useSenseResult.decision.rawValue,
|
|
77
|
-
"channelTrustScore": useSenseResult.channelTrustScore,
|
|
78
|
-
"livenessScore": useSenseResult.livenessScore,
|
|
79
|
-
"matchSenseRiskScore": useSenseResult.matchSenseRiskScore,
|
|
80
|
-
"presenceConfidence": useSenseResult.presenceConfidence,
|
|
81
|
-
"reasons": useSenseResult.reasons,
|
|
82
|
-
"ruleTriggered": useSenseResult.ruleTriggered as Any,
|
|
83
|
-
"recommendedAction": useSenseResult.recommendedAction as Any,
|
|
84
|
-
"sessionSignature": useSenseResult.sessionSignature,
|
|
85
|
-
]
|
|
86
|
-
resolve(resultDict)
|
|
87
|
-
|
|
173
|
+
case .success(let decision):
|
|
174
|
+
resolve(Self.redactedResultDict(decision))
|
|
88
175
|
case .failure(let error):
|
|
89
|
-
|
|
176
|
+
if error.code == .userCancelled {
|
|
177
|
+
reject("session_cancelled",
|
|
178
|
+
"User cancelled the verification session.",
|
|
179
|
+
nil)
|
|
180
|
+
} else {
|
|
181
|
+
reject(Self.bridgeErrorCode(error.code),
|
|
182
|
+
error.message,
|
|
183
|
+
error)
|
|
184
|
+
}
|
|
90
185
|
}
|
|
91
186
|
}
|
|
187
|
+
vc.modalPresentationStyle = .fullScreen
|
|
188
|
+
rootVC.present(vc, animated: true)
|
|
92
189
|
}
|
|
93
190
|
}
|
|
94
191
|
|
|
95
|
-
|
|
96
|
-
func cancelSession(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
97
|
-
UseSense.cancelSession()
|
|
98
|
-
resolve(nil)
|
|
99
|
-
}
|
|
192
|
+
// MARK: - LiveSense v4 (R-1)
|
|
100
193
|
|
|
194
|
+
/// Passthrough for the v4 zoom-motion capture. Resolves with the
|
|
195
|
+
/// opaque verdict; sub-scores never cross the bridge.
|
|
101
196
|
@objc
|
|
102
|
-
func
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"matchSenseRiskScore": r.matchSenseRiskScore,
|
|
114
|
-
"presenceConfidence": r.presenceConfidence,
|
|
115
|
-
"reasons": r.reasons,
|
|
116
|
-
"sessionSignature": r.sessionSignature,
|
|
117
|
-
]
|
|
118
|
-
}
|
|
119
|
-
resolve(dict)
|
|
120
|
-
case .failure(let error):
|
|
121
|
-
reject(error.code, error.message, nil)
|
|
122
|
-
}
|
|
197
|
+
func startV4Verification(_ request: NSDictionary,
|
|
198
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
199
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
200
|
+
guard let sessionIdStr = request["sessionId"] as? String,
|
|
201
|
+
let sessionId = UUID(uuidString: sessionIdStr),
|
|
202
|
+
let sessionToken = request["sessionToken"] as? String,
|
|
203
|
+
let nonce = request["nonce"] as? String,
|
|
204
|
+
let apiBaseStr = request["apiBaseUrl"] as? String,
|
|
205
|
+
let apiBaseURL = URL(string: apiBaseStr) else {
|
|
206
|
+
reject("INVALID_REQUEST", "sessionId, sessionToken, nonce, apiBaseUrl required", nil)
|
|
207
|
+
return
|
|
123
208
|
}
|
|
124
|
-
|
|
209
|
+
let environment = request["environment"] as? String ?? "production"
|
|
210
|
+
let displayName = request["displayName"] as? String
|
|
211
|
+
let client = UseSense(config: UseSenseConfig(apiKey: "v4-bridge"))
|
|
212
|
+
let config = LiveSenseV4Config(
|
|
213
|
+
sessionId: sessionId,
|
|
214
|
+
sessionToken: sessionToken,
|
|
215
|
+
nonce: nonce,
|
|
216
|
+
apiBaseURL: apiBaseURL,
|
|
217
|
+
environment: environment,
|
|
218
|
+
brandPrimaryColor: nil,
|
|
219
|
+
displayName: displayName
|
|
220
|
+
)
|
|
125
221
|
|
|
126
|
-
|
|
127
|
-
func subscribeToEvents() {
|
|
128
|
-
UseSense.onEvent { [weak self] event in
|
|
222
|
+
let delegate = V4BridgeDelegate(resolve: resolve, reject: reject, emit: { [weak self] body in
|
|
129
223
|
guard let self = self, self.hasListeners else { return }
|
|
130
|
-
self.sendEvent(withName: "UseSenseEvent", body:
|
|
224
|
+
DispatchQueue.main.async { self.sendEvent(withName: "UseSenseEvent", body: body) }
|
|
225
|
+
})
|
|
226
|
+
let session = client.startV4Session(config: config, delegate: delegate)
|
|
227
|
+
delegate.retain(session)
|
|
228
|
+
do {
|
|
229
|
+
try session.start()
|
|
230
|
+
} catch {
|
|
231
|
+
reject("V4_FAILED", error.localizedDescription, error)
|
|
131
232
|
}
|
|
132
233
|
}
|
|
133
234
|
|
|
134
235
|
@objc
|
|
135
|
-
func
|
|
136
|
-
|
|
236
|
+
func reset(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
237
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
238
|
+
eventUnsubscribe?()
|
|
239
|
+
eventUnsubscribe = nil
|
|
240
|
+
client?.reset()
|
|
241
|
+
client = nil
|
|
242
|
+
resolve(nil)
|
|
137
243
|
}
|
|
138
244
|
|
|
139
245
|
@objc
|
|
140
|
-
func
|
|
141
|
-
|
|
246
|
+
func isInitialized(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
247
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
248
|
+
resolve(client != nil)
|
|
142
249
|
}
|
|
143
250
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
251
|
+
// MARK: - Private helpers
|
|
252
|
+
|
|
253
|
+
private func emitEvent(_ event: UseSenseEvent) {
|
|
254
|
+
guard hasListeners else { return }
|
|
255
|
+
let eventTypeString = Self.eventTypeString(event.type)
|
|
256
|
+
let body: [String: Any] = [
|
|
257
|
+
"type": eventTypeString,
|
|
258
|
+
"timestamp": Int64(event.timestamp.timeIntervalSince1970 * 1000),
|
|
259
|
+
"data": event.data ?? [:]
|
|
260
|
+
]
|
|
261
|
+
DispatchQueue.main.async { [weak self] in
|
|
262
|
+
self?.sendEvent(withName: "UseSenseEvent", body: body)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Maps the SDK's `RedactedDecisionObject` into the exact five-field
|
|
267
|
+
/// dict the JS API exposes. Adding or returning anything beyond
|
|
268
|
+
/// these five fields would leak internal scoring data through the
|
|
269
|
+
/// bridge — do NOT add channel/liveness/matchsense scores here.
|
|
270
|
+
private static func redactedResultDict(_ decision: RedactedDecisionObject) -> [String: Any] {
|
|
271
|
+
return [
|
|
272
|
+
"sessionId": decision.sessionId,
|
|
273
|
+
"sessionType": decision.sessionType as Any,
|
|
274
|
+
"identityId": decision.identityId as Any,
|
|
275
|
+
"decision": decision.decision,
|
|
276
|
+
"timestamp": decision.timestamp,
|
|
277
|
+
// Convenience booleans computed on the native side so the
|
|
278
|
+
// JS consumer doesn't have to string-compare decision codes.
|
|
279
|
+
"isApproved": decision.isApproved,
|
|
280
|
+
"isRejected": decision.isRejected,
|
|
281
|
+
"isPendingReview": decision.isPendingReview
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private static func bridgeErrorCode(_ code: UseSenseErrorCode) -> String {
|
|
286
|
+
// Return the raw error code string (e.g. "NETWORK_ERROR"). JS
|
|
287
|
+
// consumers can key off this via UseSenseError.code. Matches the
|
|
288
|
+
// string-valued error codes the Flutter plugin emits.
|
|
289
|
+
return code.rawValue
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private static func eventTypeString(_ type: UseSenseEventType) -> String {
|
|
293
|
+
// RN bridges typically emit UPPER_SNAKE_CASE event type strings.
|
|
294
|
+
// Map the SDK's enum cases to the same canonical form the
|
|
295
|
+
// Android bridge uses (event.type.name) so JS consumers can
|
|
296
|
+
// switch on a single constant set regardless of platform.
|
|
297
|
+
switch type {
|
|
298
|
+
case .sessionCreated: return "SESSION_CREATED"
|
|
299
|
+
case .permissionsRequested: return "PERMISSIONS_REQUESTED"
|
|
300
|
+
case .permissionsGranted: return "PERMISSIONS_GRANTED"
|
|
301
|
+
case .permissionsDenied: return "PERMISSIONS_DENIED"
|
|
302
|
+
case .captureStarted: return "CAPTURE_STARTED"
|
|
303
|
+
case .frameCaptured: return "FRAME_CAPTURED"
|
|
304
|
+
case .captureCompleted: return "CAPTURE_COMPLETED"
|
|
305
|
+
case .audioRecordStarted: return "AUDIO_RECORD_STARTED"
|
|
306
|
+
case .audioRecordCompleted: return "AUDIO_RECORD_COMPLETED"
|
|
307
|
+
case .challengeStarted: return "CHALLENGE_STARTED"
|
|
308
|
+
case .challengeCompleted: return "CHALLENGE_COMPLETED"
|
|
309
|
+
case .uploadStarted: return "UPLOAD_STARTED"
|
|
310
|
+
case .uploadProgress: return "UPLOAD_PROGRESS"
|
|
311
|
+
case .uploadCompleted: return "UPLOAD_COMPLETED"
|
|
312
|
+
case .completeStarted: return "COMPLETE_STARTED"
|
|
313
|
+
case .decisionReceived: return "DECISION_RECEIVED"
|
|
314
|
+
case .imageQualityCheck: return "IMAGE_QUALITY_CHECK"
|
|
315
|
+
case .error: return "ERROR"
|
|
316
|
+
@unknown default: return "UNKNOWN"
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// Walks the currently-presented view controller chain and returns
|
|
321
|
+
/// the topmost one — which is the correct presenter for a new
|
|
322
|
+
/// full-screen modal. `RCTPresentedViewController()` returns the
|
|
323
|
+
/// currently-displayed VC which may itself be a modal, so we
|
|
324
|
+
/// traverse to the leaf to avoid "already presenting" errors.
|
|
325
|
+
private static func topmostViewController() -> UIViewController? {
|
|
326
|
+
guard var top = UIApplication.shared.connectedScenes
|
|
327
|
+
.compactMap({ ($0 as? UIWindowScene)?.keyWindow })
|
|
328
|
+
.first?
|
|
329
|
+
.rootViewController else {
|
|
330
|
+
return RCTPresentedViewController()
|
|
331
|
+
}
|
|
332
|
+
while let presented = top.presentedViewController {
|
|
333
|
+
top = presented
|
|
334
|
+
}
|
|
335
|
+
return top
|
|
147
336
|
}
|
|
148
337
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//
|
|
2
|
+
// V4BridgeDelegate.swift
|
|
3
|
+
// react-native-usesense
|
|
4
|
+
//
|
|
5
|
+
// Phase 1 ticket R-1. Adapter that translates LiveSenseV4Delegate
|
|
6
|
+
// callbacks into React Native promise resolve/reject + bridge events.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
import UseSenseSDK
|
|
11
|
+
|
|
12
|
+
final class V4BridgeDelegate: NSObject, LiveSenseV4Delegate {
|
|
13
|
+
private let resolve: (Any?) -> Void
|
|
14
|
+
private let reject: (String, String?, Error?) -> Void
|
|
15
|
+
private let emit: ([String: Any]) -> Void
|
|
16
|
+
private var strongSession: LiveSenseV4Session?
|
|
17
|
+
private var didFinish = false
|
|
18
|
+
|
|
19
|
+
init(resolve: @escaping (Any?) -> Void,
|
|
20
|
+
reject: @escaping (String, String?, Error?) -> Void,
|
|
21
|
+
emit: @escaping ([String: Any]) -> Void) {
|
|
22
|
+
self.resolve = resolve
|
|
23
|
+
self.reject = reject
|
|
24
|
+
self.emit = emit
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func retain(_ session: LiveSenseV4Session) {
|
|
28
|
+
strongSession = session
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func sessionDidComplete(verdict: V4Verdict) {
|
|
32
|
+
if didFinish { return }
|
|
33
|
+
didFinish = true
|
|
34
|
+
let body: [String: Any?] = [
|
|
35
|
+
"session_id": verdict.sessionId,
|
|
36
|
+
"verdict": verdict.verdict.rawValue,
|
|
37
|
+
"confidence": verdict.confidence.rawValue,
|
|
38
|
+
"assurance_level_achieved": verdict.assuranceLevelAchieved,
|
|
39
|
+
"capture_channel": "rn",
|
|
40
|
+
"match_sense_embedding_id": verdict.matchSenseEmbeddingId,
|
|
41
|
+
"timestamp": verdict.timestamp
|
|
42
|
+
]
|
|
43
|
+
resolve(body.compactMapValues { $0 })
|
|
44
|
+
strongSession = nil
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func sessionDidFail(error: Error) {
|
|
48
|
+
if didFinish { return }
|
|
49
|
+
didFinish = true
|
|
50
|
+
reject("V4_FAILED", error.localizedDescription, error)
|
|
51
|
+
strongSession = nil
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func sessionPhaseDidChange(phase: LiveSenseV4Phase) {
|
|
55
|
+
let body: [String: Any] = [
|
|
56
|
+
"type": "V4_PHASE_CHANGE",
|
|
57
|
+
"phase": phase.rawValue,
|
|
58
|
+
"timestamp": Int64(Date().timeIntervalSince1970 * 1000)
|
|
59
|
+
]
|
|
60
|
+
emit(body)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.UseSenseFlows = exports.FlowError = void 0;
|
|
7
|
+
var _reactNative = require("react-native");
|
|
8
|
+
/**
|
|
9
|
+
* React Native wrapper for the UseSense Flows runner.
|
|
10
|
+
*
|
|
11
|
+
* Coexists with the existing Sessions API (`UseSense.startSession`, etc.);
|
|
12
|
+
* this is a parallel surface, not a replacement. See `guides/flows/sessions-vs-flows`
|
|
13
|
+
* in the API docs for when to use which.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
*
|
|
17
|
+
* import { UseSenseFlows, FlowError } from 'react-native-usesense';
|
|
18
|
+
*
|
|
19
|
+
* try {
|
|
20
|
+
* const result = await UseSenseFlows.runFlow({ flowRunId, sdkToken });
|
|
21
|
+
* console.log(result.outcome); // 'APPROVE' | 'REJECT' | 'MANUAL_REVIEW' | null
|
|
22
|
+
* } catch (e) {
|
|
23
|
+
* if (e instanceof FlowError) {
|
|
24
|
+
* // e.code: 'token_expired' | 'token_invalid' | ...
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const LINKING_ERROR = `react-native-usesense (flows): the native module could not be found.\n\n` + _reactNative.Platform.select({
|
|
30
|
+
ios: `iOS: run \`cd ios && pod install\` and rebuild the app.`,
|
|
31
|
+
android: `Android: rebuild the app after installing the package.`,
|
|
32
|
+
default: ''
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Lazy lookup so this module doesn't throw at import time on platforms that
|
|
36
|
+
// haven't been re-linked yet. The Sessions module mirrors this pattern.
|
|
37
|
+
function nativeModule() {
|
|
38
|
+
const m = _reactNative.NativeModules.UseSenseFlowsModule;
|
|
39
|
+
if (!m) throw new Error(LINKING_ERROR);
|
|
40
|
+
return m;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Server-driven flow run state. Host apps see only the terminal ones via
|
|
45
|
+
* `FlowRunResult`; intermediate states drive the runner internally.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** Terminal outcome of a flow run. */
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Uniform error taxonomy across every SDK. See `guides/flows/errors` in the
|
|
52
|
+
* API docs for per-code recovery patterns.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
const FLOW_ERROR_CODES = ['token_expired', 'token_invalid', 'network_unavailable', 'permission_denied', 'provider_unavailable', 'cancelled', 'unsupported_action', 'invalid_input', 'unknown'];
|
|
56
|
+
function normaliseCode(code) {
|
|
57
|
+
return typeof code === 'string' && FLOW_ERROR_CODES.includes(code) ? code : 'unknown';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Result returned to the host app's `runFlow` promise on terminal state. */
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Typed error thrown by `runFlow` when the native runner reports a failure.
|
|
64
|
+
* Mirrors the web / iOS / Android / Flutter SDKs so host apps catch one
|
|
65
|
+
* taxonomy regardless of platform.
|
|
66
|
+
*/
|
|
67
|
+
class FlowError extends Error {
|
|
68
|
+
constructor(code, message) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = 'FlowError';
|
|
71
|
+
this.code = code;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Options for `UseSenseFlows.runFlow`. */
|
|
76
|
+
exports.FlowError = FlowError;
|
|
77
|
+
/**
|
|
78
|
+
* Public entry point for the Flows runner. Sessions APIs are untouched;
|
|
79
|
+
* this is a parallel namespace.
|
|
80
|
+
*/
|
|
81
|
+
const UseSenseFlows = exports.UseSenseFlows = {
|
|
82
|
+
/**
|
|
83
|
+
* Run an operator-authored Flow inside the host app. Resolves with a
|
|
84
|
+
* [FlowRunResult] when the run reaches a terminal state (completed,
|
|
85
|
+
* cancelled, errored, abandoned). Rejects with a [FlowError] on transport
|
|
86
|
+
* / token / unsupported-action faults.
|
|
87
|
+
*/
|
|
88
|
+
async runFlow(options) {
|
|
89
|
+
try {
|
|
90
|
+
const native = await nativeModule().runFlow(options.flowRunId, options.sdkToken, options.apiBaseUrl ?? null);
|
|
91
|
+
return {
|
|
92
|
+
flowRunId: native.flowRunId,
|
|
93
|
+
state: native.state,
|
|
94
|
+
outcome: native.outcome
|
|
95
|
+
};
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// RN bridge errors are surfaced as `Error` with `.code` and `.message`
|
|
98
|
+
// properties when the native side calls `reject(code, message, null)`.
|
|
99
|
+
const code = normaliseCode(e.code);
|
|
100
|
+
const message = e.message ?? 'Flow failed';
|
|
101
|
+
throw new FlowError(code, message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=flows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["_reactNative","require","LINKING_ERROR","Platform","select","ios","android","default","nativeModule","m","NativeModules","UseSenseFlowsModule","Error","FLOW_ERROR_CODES","normaliseCode","code","includes","FlowError","constructor","message","name","exports","UseSenseFlows","runFlow","options","native","flowRunId","sdkToken","apiBaseUrl","state","outcome","e"],"sourceRoot":"../../src","sources":["flows.ts"],"mappings":";;;;;;AAqBA,IAAAA,YAAA,GAAAC,OAAA;AArBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA,MAAMC,aAAa,GACjB,0EAA0E,GAC1EC,qBAAQ,CAACC,MAAM,CAAC;EACdC,GAAG,EAAE,yDAAyD;EAC9DC,OAAO,EAAE,wDAAwD;EACjEC,OAAO,EAAE;AACX,CAAC,CAAC;;AAEJ;AACA;AACA,SAASC,YAAYA,CAAA,EAAG;EACtB,MAAMC,CAAC,GAAGC,0BAAa,CAACC,mBAAmB;EAC3C,IAAI,CAACF,CAAC,EAAE,MAAM,IAAIG,KAAK,CAACV,aAAa,CAAC;EACtC,OAAOO,CAAC;AACV;;AAEA;AACA;AACA;AACA;;AAWA;;AAGA;AACA;AACA;AACA;;AAeA,MAAMI,gBAA0C,GAAG,CACjD,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,EACX,oBAAoB,EACpB,eAAe,EACf,SAAS,CACD;AAEV,SAASC,aAAaA,CAACC,IAAa,EAAiB;EACnD,OAAO,OAAOA,IAAI,KAAK,QAAQ,IAAKF,gBAAgB,CAAuBG,QAAQ,CAACD,IAAI,CAAC,GACpFA,IAAI,GACL,SAAS;AACf;;AAEA;;AAaA;AACA;AACA;AACA;AACA;AACO,MAAME,SAAS,SAASL,KAAK,CAAC;EAEnCM,WAAWA,CAACH,IAAmB,EAAEI,OAAe,EAAE;IAChD,KAAK,CAACA,OAAO,CAAC;IACd,IAAI,CAACC,IAAI,GAAG,WAAW;IACvB,IAAI,CAACL,IAAI,GAAGA,IAAI;EAClB;AACF;;AAEA;AAAAM,OAAA,CAAAJ,SAAA,GAAAA,SAAA;AAQA;AACA;AACA;AACA;AACO,MAAMK,aAAa,GAAAD,OAAA,CAAAC,aAAA,GAAG;EAC3B;AACF;AACA;AACA;AACA;AACA;EACE,MAAMC,OAAOA,CAACC,OAAuB,EAA0B;IAC7D,IAAI;MACF,MAAMC,MAAM,GAAG,MAAMjB,YAAY,CAAC,CAAC,CAACe,OAAO,CACzCC,OAAO,CAACE,SAAS,EACjBF,OAAO,CAACG,QAAQ,EAChBH,OAAO,CAACI,UAAU,IAAI,IACxB,CAAC;MACD,OAAO;QACLF,SAAS,EAAED,MAAM,CAACC,SAAS;QAC3BG,KAAK,EAAEJ,MAAM,CAACI,KAAK;QACnBC,OAAO,EAAEL,MAAM,CAACK;MAClB,CAAC;IACH,CAAC,CAAC,OAAOC,CAAC,EAAE;MACV;MACA;MACA,MAAMhB,IAAI,GAAGD,aAAa,CAAEiB,CAAC,CAAuBhB,IAAI,CAAC;MACzD,MAAMI,OAAO,GAAIY,CAAC,CAA0BZ,OAAO,IAAI,aAAa;MACpE,MAAM,IAAIF,SAAS,CAACF,IAAI,EAAEI,OAAO,CAAC;IACpC;EACF;AACF,CAAC","ignoreList":[]}
|