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.
@@ -35,8 +35,13 @@ repositories {
35
35
  dependencies {
36
36
  implementation "com.facebook.react:react-native:+"
37
37
 
38
- // UseSense Android SDK
39
- implementation "ai.usesense:sdk:1.0.0"
38
+ // UseSense Android SDK — published to Maven Central at
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"
40
45
 
41
46
  implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '2.0.0')}"
42
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
+ }
@@ -32,15 +32,29 @@ class UseSenseModule(private val reactContext: ReactApplicationContext) :
32
32
  else -> UseSenseEnvironment.AUTO
33
33
  }
34
34
 
35
+ // `gatewayKey` is intentionally NOT passed to UseSenseConfig.
36
+ // It existed on the v1.x Android SDK but was removed in v4.0
37
+ // when the Cloudflare Worker proxy took over gateway
38
+ // responsibilities server-side. The JS API no longer exposes
39
+ // the field; any stale JS callers that still pass it will
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
+
35
48
  val config = UseSenseConfig(
36
49
  apiKey = apiKey,
37
50
  environment = environment,
38
51
  baseUrl = configMap.getString("baseUrl") ?: UseSenseConfig.DEFAULT_BASE_URL,
39
- gatewayKey = configMap.getString("gatewayKey"),
40
52
  branding = branding,
41
53
  googleCloudProjectNumber = if (configMap.hasKey("googleCloudProjectNumber"))
42
54
  configMap.getDouble("googleCloudProjectNumber").toLong()
43
55
  else UseSenseConfig.DEFAULT_GOOGLE_CLOUD_PROJECT_NUMBER,
56
+ antispoofOnDeviceEnabled = antispoofOnDeviceEnabled,
57
+ liveSenseV4Enabled = liveSenseV4Enabled,
44
58
  )
45
59
 
46
60
  val context = reactContext.applicationContext
@@ -107,6 +121,73 @@ class UseSenseModule(private val reactContext: ReactApplicationContext) :
107
121
  })
108
122
  }
109
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
+
110
191
  @ReactMethod
111
192
  fun subscribeToEvents() {
112
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(UseSenseModule(reactContext))
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
+ }
@@ -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