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.
@@ -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.2.1
40
- // because that's the first release with the vanniktech-based
41
- // Maven Central artifact shape and the terminal-screen centering
42
- // fix. mavenCentral() is already in the `repositories` block above,
43
- // so no custom repository declaration is needed.
44
- implementation "ai.usesense:sdk:4.2.1"
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(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
@@ -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":[]}