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.
@@ -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 initialize(_ config: NSDictionary) {
24
- guard let apiKey = config["apiKey"] as? String else {
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: UseSenseEnvironment = {
29
- switch config["environment"] as? String {
30
- case "production": return .production
31
- case "sandbox": return .sandbox
32
- default: return .sandbox
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 useSenseConfig = UseSenseConfig(
99
+ let sdkConfig = UseSenseConfig(
100
+ apiEndpoint: apiEndpoint,
37
101
  apiKey: apiKey,
38
102
  environment: environment,
39
- organizationId: config["organizationId"] as? String,
40
- sessionType: (config["sessionType"] as? String) == "authentication" ? .authentication : .enrollment,
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
- UseSense.initialize(config: useSenseConfig)
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 startSession(_ options: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
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 viewController = RCTPresentedViewController() else {
60
- reject("no_view_controller", "No view controller available", nil)
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 sessionType: SessionType = (options["sessionType"] as? String) == "authentication" ? .authentication : .enrollment
65
- let identityId = options["identityId"] as? String
66
-
67
- UseSense.startSession(
68
- from: viewController,
69
- sessionType: sessionType,
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 useSenseResult):
74
- let resultDict: [String: Any] = [
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
- reject(error.code, error.message, nil)
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
- @objc
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 getSessionStatus(_ sessionId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
103
- UseSense.getSessionStatus(sessionId: sessionId) { result in
104
- switch result {
105
- case .success(let status):
106
- var dict: [String: Any] = ["status": status.status]
107
- if let r = status.result {
108
- dict["result"] = [
109
- "sessionId": r.sessionId,
110
- "decision": r.decision.rawValue,
111
- "channelTrustScore": r.channelTrustScore,
112
- "livenessScore": r.livenessScore,
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
- @objc
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: event.toDictionary())
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 unsubscribeFromEvents() {
136
- UseSense.removeEventListener()
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 getSdkVersion() -> String {
141
- return UseSense.sdkVersion
246
+ func isInitialized(_ resolve: @escaping RCTPromiseResolveBlock,
247
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
248
+ resolve(client != nil)
142
249
  }
143
250
 
144
- @objc
145
- override static func requiresMainQueueSetup() -> Bool {
146
- return false
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":[]}