react-native-usesense 2.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/android/build.gradle +6 -6
- package/android/src/main/java/com/usesense/reactnative/UseSenseFlowsModule.kt +72 -0
- package/android/src/main/java/com/usesense/reactnative/UseSenseModule.kt +76 -0
- 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 +55 -1
- 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 +55 -3
- 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 +43 -1
- 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 +56 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +3 -1
- package/react-native-usesense.podspec +1 -1
- package/src/flows.ts +159 -0
- package/src/index.ts +96 -0
package/android/build.gradle
CHANGED
|
@@ -36,12 +36,12 @@ dependencies {
|
|
|
36
36
|
implementation "com.facebook.react:react-native:+"
|
|
37
37
|
|
|
38
38
|
// UseSense Android SDK — published to Maven Central at
|
|
39
|
-
// central.sonatype.com/artifact/ai.usesense/sdk. Pinned to 4.
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
implementation "ai.usesense:sdk:4.
|
|
39
|
+
// central.sonatype.com/artifact/ai.usesense/sdk. Pinned to 4.3.0,
|
|
40
|
+
// the release that adds the V4 capture API + Flows runner (matches
|
|
41
|
+
// the iOS SDK 4.3.0 the plugin's podspec depends on). mavenCentral()
|
|
42
|
+
// is already in the `repositories` block above, so no custom
|
|
43
|
+
// repository declaration is needed.
|
|
44
|
+
implementation "ai.usesense:sdk:4.3.0"
|
|
45
45
|
|
|
46
46
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '2.0.0')}"
|
|
47
47
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package com.usesense.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
7
|
+
import com.facebook.react.bridge.ReactMethod
|
|
8
|
+
import com.usesense.sdk.flows.FlowError
|
|
9
|
+
import com.usesense.sdk.flows.FlowOutcome
|
|
10
|
+
import com.usesense.sdk.flows.FlowRunResult
|
|
11
|
+
import com.usesense.sdk.flows.FlowRunState
|
|
12
|
+
import com.usesense.sdk.flows.FlowsCallback
|
|
13
|
+
import com.usesense.sdk.flows.UseSenseFlows
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* React Native bridge for the UseSense Android Flows runner.
|
|
17
|
+
*
|
|
18
|
+
* Exposes a single JS-callable method, `runFlow`, that forwards into the
|
|
19
|
+
* native `UseSenseFlows.run(activity, flowRunId, sdkToken, callback)` entry
|
|
20
|
+
* point and resolves the Promise with a `FlowRunResult` map. On failure the
|
|
21
|
+
* Promise rejects with `FlowError.Code.wire` as the JS error code, mirroring
|
|
22
|
+
* the taxonomy host apps catch on every other surface.
|
|
23
|
+
*
|
|
24
|
+
* Sits alongside `UseSenseModule` (Sessions); the existing module is
|
|
25
|
+
* untouched. Sessions and Flows coexist — host apps pick one per call.
|
|
26
|
+
*/
|
|
27
|
+
class UseSenseFlowsModule(private val reactContext: ReactApplicationContext) :
|
|
28
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
29
|
+
|
|
30
|
+
override fun getName(): String = "UseSenseFlowsModule"
|
|
31
|
+
|
|
32
|
+
@ReactMethod
|
|
33
|
+
fun runFlow(flowRunId: String?, sdkToken: String?, apiBaseUrl: String?, promise: Promise) {
|
|
34
|
+
if (flowRunId.isNullOrEmpty() || sdkToken.isNullOrEmpty()) {
|
|
35
|
+
promise.reject("unknown", "flowRunId and sdkToken are required")
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
val activity = reactContext.currentActivity ?: run {
|
|
39
|
+
promise.reject("unknown", "No current activity to launch from")
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
val baseUrl = apiBaseUrl ?: "https://api.usesense.ai"
|
|
43
|
+
|
|
44
|
+
val callback = object : FlowsCallback {
|
|
45
|
+
override fun onResult(result: FlowRunResult) {
|
|
46
|
+
val map = Arguments.createMap().apply {
|
|
47
|
+
putString("flowRunId", result.flowRunId)
|
|
48
|
+
putString("state", stateWire(result.state))
|
|
49
|
+
val outcomeStr = result.outcome?.let { outcomeWire(it) }
|
|
50
|
+
if (outcomeStr != null) putString("outcome", outcomeStr)
|
|
51
|
+
else putNull("outcome")
|
|
52
|
+
}
|
|
53
|
+
promise.resolve(map)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun onError(error: FlowError) {
|
|
57
|
+
promise.reject(error.code.wire, error.message)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
UseSenseFlows.run(
|
|
62
|
+
activity = activity,
|
|
63
|
+
flowRunId = flowRunId,
|
|
64
|
+
sdkToken = sdkToken,
|
|
65
|
+
callback = callback,
|
|
66
|
+
apiBaseUrl = baseUrl,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private fun stateWire(state: FlowRunState): String = state.wire
|
|
71
|
+
private fun outcomeWire(outcome: FlowOutcome): String = outcome.wire
|
|
72
|
+
}
|
|
@@ -38,6 +38,13 @@ class UseSenseModule(private val reactContext: ReactApplicationContext) :
|
|
|
38
38
|
// responsibilities server-side. The JS API no longer exposes
|
|
39
39
|
// the field; any stale JS callers that still pass it will
|
|
40
40
|
// have the key silently ignored.
|
|
41
|
+
// v4.2: Optional on-device antispoof classifier flag.
|
|
42
|
+
val antispoofOnDeviceEnabled = configMap.hasKey("antispoofOnDeviceEnabled") &&
|
|
43
|
+
configMap.getBoolean("antispoofOnDeviceEnabled")
|
|
44
|
+
// LiveSense v4 capture flow opt-in.
|
|
45
|
+
val liveSenseV4Enabled = configMap.hasKey("liveSenseV4Enabled") &&
|
|
46
|
+
configMap.getBoolean("liveSenseV4Enabled")
|
|
47
|
+
|
|
41
48
|
val config = UseSenseConfig(
|
|
42
49
|
apiKey = apiKey,
|
|
43
50
|
environment = environment,
|
|
@@ -46,6 +53,8 @@ class UseSenseModule(private val reactContext: ReactApplicationContext) :
|
|
|
46
53
|
googleCloudProjectNumber = if (configMap.hasKey("googleCloudProjectNumber"))
|
|
47
54
|
configMap.getDouble("googleCloudProjectNumber").toLong()
|
|
48
55
|
else UseSenseConfig.DEFAULT_GOOGLE_CLOUD_PROJECT_NUMBER,
|
|
56
|
+
antispoofOnDeviceEnabled = antispoofOnDeviceEnabled,
|
|
57
|
+
liveSenseV4Enabled = liveSenseV4Enabled,
|
|
49
58
|
)
|
|
50
59
|
|
|
51
60
|
val context = reactContext.applicationContext
|
|
@@ -112,6 +121,73 @@ class UseSenseModule(private val reactContext: ReactApplicationContext) :
|
|
|
112
121
|
})
|
|
113
122
|
}
|
|
114
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Phase 1 ticket R-1: passthrough to UseSense.startV4Verification.
|
|
126
|
+
*
|
|
127
|
+
* The native SDK validates the request, launches the v4 capture
|
|
128
|
+
* activity, and resolves the promise with the opaque verdict.
|
|
129
|
+
*/
|
|
130
|
+
@ReactMethod
|
|
131
|
+
fun startV4Verification(requestMap: ReadableMap, promise: Promise) {
|
|
132
|
+
val activity = currentActivity ?: run {
|
|
133
|
+
promise.reject("NO_ACTIVITY", "No current activity available")
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
val sessionId = requestMap.getString("sessionId")
|
|
137
|
+
?: return promise.reject("INVALID_REQUEST", "sessionId is required")
|
|
138
|
+
val sessionToken = requestMap.getString("sessionToken")
|
|
139
|
+
?: return promise.reject("INVALID_REQUEST", "sessionToken is required")
|
|
140
|
+
val nonce = requestMap.getString("nonce")
|
|
141
|
+
?: return promise.reject("INVALID_REQUEST", "nonce is required")
|
|
142
|
+
val apiBaseUrl = requestMap.getString("apiBaseUrl")
|
|
143
|
+
?: return promise.reject("INVALID_REQUEST", "apiBaseUrl is required")
|
|
144
|
+
val environment = requestMap.getString("environment") ?: "production"
|
|
145
|
+
val displayName = if (requestMap.hasKey("displayName")) requestMap.getString("displayName") else null
|
|
146
|
+
val brandColor = if (requestMap.hasKey("brandPrimaryColor"))
|
|
147
|
+
runCatching { android.graphics.Color.parseColor(requestMap.getString("brandPrimaryColor")) }.getOrNull()
|
|
148
|
+
else null
|
|
149
|
+
|
|
150
|
+
val request = com.usesense.sdk.api.V4VerificationRequest(
|
|
151
|
+
sessionId = sessionId,
|
|
152
|
+
sessionToken = sessionToken,
|
|
153
|
+
nonce = nonce,
|
|
154
|
+
apiBaseUrl = apiBaseUrl,
|
|
155
|
+
environment = environment,
|
|
156
|
+
brandPrimaryColor = brandColor,
|
|
157
|
+
displayName = displayName
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
UseSense.startV4Verification(activity, request, object : com.usesense.sdk.api.V4VerificationCallback {
|
|
161
|
+
override fun onComplete(verdict: com.usesense.sdk.api.V4Verdict) {
|
|
162
|
+
val map = Arguments.createMap().apply {
|
|
163
|
+
putString("session_id", verdict.sessionId)
|
|
164
|
+
putString("verdict", verdict.verdict.name.lowercase())
|
|
165
|
+
putString("confidence", verdict.confidence.name.lowercase())
|
|
166
|
+
putString("assurance_level_achieved", verdict.assuranceLevelAchieved)
|
|
167
|
+
putString("capture_channel", "rn")
|
|
168
|
+
if (verdict.matchSenseEmbeddingId != null) {
|
|
169
|
+
putString("match_sense_embedding_id", verdict.matchSenseEmbeddingId)
|
|
170
|
+
} else {
|
|
171
|
+
putNull("match_sense_embedding_id")
|
|
172
|
+
}
|
|
173
|
+
putString("timestamp", verdict.timestamp)
|
|
174
|
+
}
|
|
175
|
+
promise.resolve(map)
|
|
176
|
+
}
|
|
177
|
+
override fun onFailure(error: Throwable) {
|
|
178
|
+
promise.reject("V4_FAILED", error.message ?: "v4 verification failed", error)
|
|
179
|
+
}
|
|
180
|
+
override fun onPhaseChange(phase: com.usesense.sdk.api.V4Phase) {
|
|
181
|
+
val map = Arguments.createMap().apply {
|
|
182
|
+
putString("type", "V4_PHASE_CHANGE")
|
|
183
|
+
putString("phase", phase.name.lowercase())
|
|
184
|
+
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
185
|
+
}
|
|
186
|
+
sendEvent("UseSenseEvent", map)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
115
191
|
@ReactMethod
|
|
116
192
|
fun subscribeToEvents() {
|
|
117
193
|
eventUnsubscribe?.invoke()
|
|
@@ -8,7 +8,12 @@ import com.facebook.react.uimanager.ViewManager
|
|
|
8
8
|
class UseSensePackage : ReactPackage {
|
|
9
9
|
|
|
10
10
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
11
|
-
return listOf(
|
|
11
|
+
return listOf(
|
|
12
|
+
UseSenseModule(reactContext),
|
|
13
|
+
// Slice 5d: Flows runner module (parallel surface to Sessions, not
|
|
14
|
+
// a replacement). Host apps pick one per call.
|
|
15
|
+
UseSenseFlowsModule(reactContext),
|
|
16
|
+
)
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
// Bridging header for the Flows runner module. Single JS-callable method:
|
|
4
|
+
// runFlow(flowRunId, sdkToken, apiBaseUrl?). The apiBaseUrl parameter is
|
|
5
|
+
// nullable from JS — pass `null` (or omit, since the JS wrapper supplies a
|
|
6
|
+
// default) and the native side falls back to https://api.usesense.ai.
|
|
7
|
+
@interface RCT_EXTERN_MODULE(UseSenseFlowsModule, NSObject)
|
|
8
|
+
|
|
9
|
+
RCT_EXTERN_METHOD(runFlow:(NSString *)flowRunId
|
|
10
|
+
sdkToken:(NSString *)sdkToken
|
|
11
|
+
apiBaseUrl:(NSString * _Nullable)apiBaseUrl
|
|
12
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
13
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
14
|
+
|
|
15
|
+
+ (BOOL)requiresMainQueueSetup
|
|
16
|
+
{
|
|
17
|
+
return NO;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
import UseSenseSDK
|
|
4
|
+
|
|
5
|
+
/// React Native bridge for the UseSense iOS Flows runner.
|
|
6
|
+
///
|
|
7
|
+
/// Exposes a single JS-callable method, `runFlow`, that forwards into the
|
|
8
|
+
/// native `UseSenseFlows.run(flowRunId:sdkToken:from:completion:)` entry
|
|
9
|
+
/// point and resolves the Promise with a `FlowRunResult` map. On failure the
|
|
10
|
+
/// Promise rejects with the SDK's `FlowError.Code.rawValue` as the JS error
|
|
11
|
+
/// code, mirroring the taxonomy host apps catch on every other surface.
|
|
12
|
+
///
|
|
13
|
+
/// Sits alongside `UseSenseModule` (Sessions); the existing module is
|
|
14
|
+
/// untouched. Sessions and Flows coexist — host apps pick one per call.
|
|
15
|
+
@objc(UseSenseFlowsModule)
|
|
16
|
+
class UseSenseFlowsModule: NSObject {
|
|
17
|
+
|
|
18
|
+
@objc
|
|
19
|
+
static func requiresMainQueueSetup() -> Bool {
|
|
20
|
+
// Presentation work happens in `runFlow` after the JS call dispatches
|
|
21
|
+
// back to main via `DispatchQueue.main`. The module itself has no
|
|
22
|
+
// initialisation that must run on the main queue.
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// `runFlow(flowRunId, sdkToken, apiBaseUrl?, resolve, reject)` — Promise.
|
|
27
|
+
/// The SDK takes care of presentation; we just resolve when the run
|
|
28
|
+
/// reaches a terminal state.
|
|
29
|
+
@objc
|
|
30
|
+
func runFlow(_ flowRunId: String,
|
|
31
|
+
sdkToken: String,
|
|
32
|
+
apiBaseUrl: String?,
|
|
33
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
34
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
35
|
+
let baseString = apiBaseUrl ?? "https://api.usesense.ai"
|
|
36
|
+
guard let baseURL = URL(string: baseString) else {
|
|
37
|
+
reject("unknown", "Invalid apiBaseUrl: \(baseString)", nil)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
DispatchQueue.main.async {
|
|
41
|
+
guard let presenter = Self.topViewController() else {
|
|
42
|
+
reject("unknown", "No view controller to present from", nil)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
UseSenseFlows.run(
|
|
46
|
+
flowRunId: flowRunId,
|
|
47
|
+
sdkToken: sdkToken,
|
|
48
|
+
apiBaseURL: baseURL,
|
|
49
|
+
from: presenter
|
|
50
|
+
) { runResult in
|
|
51
|
+
switch runResult {
|
|
52
|
+
case .success(let r):
|
|
53
|
+
resolve([
|
|
54
|
+
"flowRunId": r.flowRunId,
|
|
55
|
+
"state": Self.stateWire(r.state),
|
|
56
|
+
"outcome": r.outcome.map(Self.outcomeWire) as Any,
|
|
57
|
+
])
|
|
58
|
+
case .failure(let e):
|
|
59
|
+
reject(e.code.rawValue, e.message, nil)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// MARK: - Helpers (mirror UseSenseFlowsBridge in the Flutter plugin)
|
|
66
|
+
|
|
67
|
+
private static func stateWire(_ state: FlowRunState) -> String {
|
|
68
|
+
switch state {
|
|
69
|
+
case .pending: return "pending"
|
|
70
|
+
case .inProgress: return "in_progress"
|
|
71
|
+
case .stalled: return "stalled"
|
|
72
|
+
case .awaitingReview: return "awaiting_review"
|
|
73
|
+
case .completed: return "completed"
|
|
74
|
+
case .errored: return "errored"
|
|
75
|
+
case .abandoned: return "abandoned"
|
|
76
|
+
case .cancelled: return "cancelled"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static func outcomeWire(_ outcome: FlowOutcome) -> String {
|
|
81
|
+
switch outcome {
|
|
82
|
+
case .approve: return "APPROVE"
|
|
83
|
+
case .reject: return "REJECT"
|
|
84
|
+
case .manualReview: return "MANUAL_REVIEW"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Find the top-most view controller to present from. Mirrors the
|
|
89
|
+
/// pattern used elsewhere in the bridge layer; works across UIKit +
|
|
90
|
+
/// SceneDelegate apps and falls back to `keyWindow.rootViewController`.
|
|
91
|
+
private static func topViewController() -> UIViewController? {
|
|
92
|
+
var root: UIViewController?
|
|
93
|
+
if #available(iOS 13.0, *) {
|
|
94
|
+
for scene in UIApplication.shared.connectedScenes {
|
|
95
|
+
guard let windowScene = scene as? UIWindowScene else { continue }
|
|
96
|
+
if let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) ?? windowScene.windows.first {
|
|
97
|
+
root = keyWindow.rootViewController
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if root == nil {
|
|
103
|
+
root = UIApplication.shared.keyWindow?.rootViewController
|
|
104
|
+
}
|
|
105
|
+
var current = root
|
|
106
|
+
while let presented = current?.presentedViewController {
|
|
107
|
+
current = presented
|
|
108
|
+
}
|
|
109
|
+
return current
|
|
110
|
+
}
|
|
111
|
+
}
|
package/ios/UseSenseModule.m
CHANGED
|
@@ -20,6 +20,11 @@ RCT_EXTERN_METHOD(subscribeToEvents)
|
|
|
20
20
|
|
|
21
21
|
RCT_EXTERN_METHOD(unsubscribeFromEvents)
|
|
22
22
|
|
|
23
|
+
// LiveSense v4 passthrough (R-1)
|
|
24
|
+
RCT_EXTERN_METHOD(startV4Verification:(NSDictionary *)request
|
|
25
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
26
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
27
|
+
|
|
23
28
|
RCT_EXTERN_METHOD(getSdkVersion)
|
|
24
29
|
|
|
25
30
|
@end
|
package/ios/UseSenseModule.swift
CHANGED
|
@@ -86,11 +86,22 @@ class UseSenseModule: RCTEventEmitter {
|
|
|
86
86
|
let apiEndpoint = (config["apiEndpoint"] as? String)
|
|
87
87
|
?? UseSenseConfig.defaultEndpoint
|
|
88
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
|
+
)
|
|
98
|
+
|
|
89
99
|
let sdkConfig = UseSenseConfig(
|
|
90
100
|
apiEndpoint: apiEndpoint,
|
|
91
101
|
apiKey: apiKey,
|
|
92
102
|
environment: environment,
|
|
93
|
-
branding: brandingConfig
|
|
103
|
+
branding: brandingConfig,
|
|
104
|
+
options: sdkOptions
|
|
94
105
|
)
|
|
95
106
|
|
|
96
107
|
// Create (or replace) the client. If a previous client was
|
|
@@ -178,6 +189,49 @@ class UseSenseModule: RCTEventEmitter {
|
|
|
178
189
|
}
|
|
179
190
|
}
|
|
180
191
|
|
|
192
|
+
// MARK: - LiveSense v4 (R-1)
|
|
193
|
+
|
|
194
|
+
/// Passthrough for the v4 zoom-motion capture. Resolves with the
|
|
195
|
+
/// opaque verdict; sub-scores never cross the bridge.
|
|
196
|
+
@objc
|
|
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
|
|
208
|
+
}
|
|
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
|
+
)
|
|
221
|
+
|
|
222
|
+
let delegate = V4BridgeDelegate(resolve: resolve, reject: reject, emit: { [weak self] body in
|
|
223
|
+
guard let self = self, self.hasListeners else { return }
|
|
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)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
181
235
|
@objc
|
|
182
236
|
func reset(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
183
237
|
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
@@ -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":[]}
|