@scribeup/react-native-scribeup 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,11 +15,13 @@ The package is a thin wrapper around the native [iOS](https://github.com/ScribeU
15
15
  1. [ScribeUp (Full Screen)](#scribeup-full-screen)
16
16
  2. [ScribeUpWidget (Embeddable)](#scribeupwidget-embeddable)
17
17
  4. [API Reference](#api-reference)
18
- 5. [Example Projects](#example-projects)
19
- 6. [Troubleshooting](#troubleshooting)
20
- 7. [Author](#author)
21
- 8. [License](#license)
18
+ 5. [Migration Note (0.3.x → 0.6.0)](#migration-note-03x--060)
19
+ 6. [Example Projects](#example-projects)
20
+ 7. [Troubleshooting](#troubleshooting)
21
+ 8. [Author](#author)
22
+ 9. [License](#license)
22
23
 
24
+ ---
23
25
 
24
26
  ## Installation
25
27
 
@@ -56,11 +58,15 @@ export default function App() {
56
58
 
57
59
  const authenticatedUrl = "https://example.com/subscriptions?token=YOUR_JWT"; // Obtain from your backend (see docs)
58
60
 
59
- const handleExit = (data?: { message?: string; code?: number }) => {
60
- console.log("ScribeUp finished", data);
61
+ const handleExit = (error: { code?: number; message?: string } | null, data: Record<string, any> | null) => {
62
+ console.log("ScribeUp exited", { error, data });
61
63
  setVisible(false);
62
64
  };
63
65
 
66
+ const handleEvent = (data: Record<string, any>) => {
67
+ console.log("ScribeUp event", data);
68
+ };
69
+
64
70
  return (
65
71
  <SafeAreaView style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
66
72
  <Button title="Manage my subscriptions" onPress={() => setVisible(true)} />
@@ -71,6 +77,7 @@ export default function App() {
71
77
  url={authenticatedUrl}
72
78
  productName="Subscription Manager" // optional text in the nav-bar
73
79
  onExit={handleExit} // called on success or error
80
+ onEvent={handleEvent}
74
81
  />
75
82
  )}
76
83
  </SafeAreaView>
@@ -130,14 +137,25 @@ export default function MyComponent() {
130
137
 
131
138
  ### ScribeUp (Full Screen)
132
139
 
133
- ```
140
+ ```tsx
134
141
  <ScribeUp
135
- url: string; // required – authenticated manage-subscriptions URL
136
- productName?: string; // optional – title shown in the navigation bar
137
- onExit?: (data?) => void; // optional – called when the user exits, with optional error
138
- />
142
+ url: string; // required – authenticated manage-subscriptions URL
143
+ productName?: string; // optional – title in navigation bar
144
+ onExit?: (error: ExitError|null, data: object|null) => void;
145
+ onEvent?: (data: object) => void;
146
+ >
139
147
  ```
140
148
 
149
+ **`onExit`**
150
+ Called when the user exits the flow.
151
+ Receives two arguments:
152
+ - `error`: `{ code: number; message?: string }` or `null` on success
153
+ - `data`: structured object with optional payload
154
+
155
+ **`onEvent`**
156
+ Emitted zero or more times during the session to notify about intermediate states or actions (e.g., UI transitions, user actions).
157
+
158
+
141
159
  ### ScribeUpWidget (Embeddable)
142
160
 
143
161
  ```
@@ -152,14 +170,12 @@ export default function MyComponent() {
152
170
  - `reload()` – reloads the current page
153
171
  - `loadURL(url: string)` – loads a new URL
154
172
 
155
- ### Exit Callback
156
-
157
- `onExit` receives an object with two optional fields:
173
+ ---
158
174
 
159
- * `message` descriptive error or informational message.
160
- * `code` – numeric error code (0 on success, -1 on unknown error).
175
+ ## Migration Note (0.3.x 0.6.0)
161
176
 
162
- If both fields are undefined, the flow completed without errors.
177
+ - `onExit` now receives **two parameters**: `(error, data)` instead of a single `data` object.
178
+ - New `onEvent(data)` listener added for real-time progress updates.
163
179
 
164
180
  ---
165
181
 
@@ -204,9 +220,9 @@ The first execution may take a while – the scripts:
204
220
 
205
221
  ## Troubleshooting
206
222
 
207
- 1. **iOS: `The package '@scribeup/react-native-scribeup' doesn't seem to be linked`.**
223
+ 1. **iOS: `The package '@scribeup/react-native-scribeup' doesn't seem to be linked`.**
208
224
  Make sure you ran `pod install` after installing the package and rebuilt the app.
209
- 2. **Expo Go crashes when opening the manager.**
225
+ 2. **Expo Go crashes when opening the manager.**
210
226
  Expo Go does not include native code. Use a dev-client or production build.
211
227
  3. Still stuck? Reach us at **dev@scribeup.io** or open an issue.
212
228
 
@@ -41,7 +41,7 @@ class ScribeupModule(reactContext: ReactApplicationContext) :
41
41
  if (error != null) {
42
42
  val params = Arguments.createMap()
43
43
  params.putString("message", error.message)
44
- params.putString("code", error.code.toString())
44
+ params.putInt("code", error.code)
45
45
  currentPromise?.resolve(params)
46
46
  } else {
47
47
  currentPromise?.resolve(null)
@@ -50,8 +50,8 @@ class ScribeupModule(reactContext: ReactApplicationContext) :
50
50
  }
51
51
  } catch (e: Exception) {
52
52
  val params = Arguments.createMap()
53
- params.putString("message", e.message ?: "Unknown error")
54
- params.putString("code", "-1")
53
+ params.putString("message", e.message ?: "Unexpected Error")
54
+ params.putInt("code", 1000)
55
55
  promise.resolve(params)
56
56
  currentPromise = null
57
57
  }
@@ -5,6 +5,7 @@ import android.util.Log
5
5
  import androidx.fragment.app.FragmentActivity
6
6
  import com.facebook.react.bridge.Arguments
7
7
  import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.bridge.WritableArray
8
9
  import com.facebook.react.bridge.WritableMap
9
10
  import com.facebook.react.modules.core.DeviceEventManagerModule
10
11
  import io.scribeup.scribeupsdk.SubscriptionManager
@@ -15,12 +16,12 @@ import java.net.URL
15
16
 
16
17
  // Error codes shared between iOS and Android
17
18
  object ErrorCodes {
18
- const val UNKNOWN = -1
19
+ const val UNKNOWN = 1000
19
20
  const val INVALID_URL = 1001
20
- const val ACTIVITY_NULL = 1002
21
- const val INVALID_ACTIVITY_TYPE = 1003
22
- const val NO_ROOT_VIEW_CONTROLLER = 1004
23
- const val SDK_ERROR = 2001
21
+ const val INVALID_ENV = 1002
22
+ const val ACTIVITY_NULL = 1003
23
+ const val INVALID_ACTIVITY_TYPE = 1004
24
+ const val NO_ROOT_VIEW_CONTROLLER = 1005
24
25
  }
25
26
 
26
27
  class ScribeupModuleImpl(private val reactContext: ReactApplicationContext) {
@@ -38,7 +39,13 @@ class ScribeupModuleImpl(private val reactContext: ReactApplicationContext) {
38
39
  if (activity == null) {
39
40
  Log.e("Scribeup", "Activity is null")
40
41
  val error = SubscriptionManagerError(message = "Activity is null", code = ErrorCodes.ACTIVITY_NULL)
41
- exitCallback?.invoke(error)
42
+ val params = Arguments.createMap().apply {
43
+ putMap("error", Arguments.createMap().apply {
44
+ putInt("code", error.code)
45
+ putString("message", error.message)
46
+ })
47
+ }
48
+ sendEvent("ScribeupOnExit", params)
42
49
  return
43
50
  }
44
51
 
@@ -46,7 +53,13 @@ class ScribeupModuleImpl(private val reactContext: ReactApplicationContext) {
46
53
  if (activity !is FragmentActivity) {
47
54
  Log.e("Scribeup", "Activity is not a FragmentActivity")
48
55
  val error = SubscriptionManagerError(message = "Activity is not a FragmentActivity", code = ErrorCodes.INVALID_ACTIVITY_TYPE)
49
- exitCallback?.invoke(error)
56
+ val params = Arguments.createMap().apply {
57
+ putMap("error", Arguments.createMap().apply {
58
+ putInt("code", error.code)
59
+ putString("message", error.message)
60
+ })
61
+ }
62
+ sendEvent("ScribeupOnExit", params)
50
63
  return
51
64
  }
52
65
 
@@ -54,7 +67,13 @@ class ScribeupModuleImpl(private val reactContext: ReactApplicationContext) {
54
67
  if (!isValidUrl(url)) {
55
68
  Log.e("Scribeup", "Invalid URL: $url")
56
69
  val error = SubscriptionManagerError(message = "Invalid URL: $url", code = ErrorCodes.INVALID_URL)
57
- exitCallback?.invoke(error)
70
+ val params = Arguments.createMap().apply {
71
+ putMap("error", Arguments.createMap().apply {
72
+ putInt("code", error.code)
73
+ putString("message", error.message)
74
+ })
75
+ }
76
+ sendEvent("ScribeupOnExit", params)
58
77
  return
59
78
  }
60
79
 
@@ -64,22 +83,41 @@ class ScribeupModuleImpl(private val reactContext: ReactApplicationContext) {
64
83
  url = url,
65
84
  productName = productName,
66
85
  listener = object : SubscriptionManagerListener {
67
- override fun onExit(error: SubscriptionManagerError?) {
86
+ override fun onExit(error: SubscriptionManagerError?, data: Map<String, Any?>?) {
68
87
  val params = Arguments.createMap()
69
88
 
70
89
  if (error != null) {
71
- params.putString("message", error.message)
72
- params.putInt("code", error.code)
90
+ params.putMap("error", Arguments.createMap().apply {
91
+ putInt("code", error.code)
92
+ putString("message", error.message)
93
+ })
73
94
  }
74
95
 
75
- exitCallback?.invoke(error)
96
+ if (data != null) {
97
+ params.putMap("data", toWritableMap(data))
98
+ }
99
+
100
+ sendEvent("ScribeupOnExit", params)
101
+ }
102
+
103
+ override fun onEvent(data: Map<String, Any?>) {
104
+ val params = Arguments.createMap().apply {
105
+ putMap("data", toWritableMap(data))
106
+ }
107
+ sendEvent("ScribeupOnEvent", params)
76
108
  }
77
109
  }
78
110
  )
79
111
  } catch (e: Exception) {
80
112
  Log.e("Scribeup", "Error presenting subscription manager: ${e.message}")
81
- val error = SubscriptionManagerError(message = e.message ?: "Unknown error", code = ErrorCodes.UNKNOWN)
82
- exitCallback?.invoke(error)
113
+ val error = SubscriptionManagerError(message = e.message ?: "Unexpected Error", code = ErrorCodes.UNKNOWN)
114
+ val params = Arguments.createMap().apply {
115
+ putMap("error", Arguments.createMap().apply {
116
+ putInt("code", error.code)
117
+ putString("message", error.message)
118
+ })
119
+ }
120
+ sendEvent("ScribeupOnExit", params)
83
121
  }
84
122
  }
85
123
 
@@ -97,21 +135,56 @@ class ScribeupModuleImpl(private val reactContext: ReactApplicationContext) {
97
135
  reactContext.runOnUiQueueThread {
98
136
  try {
99
137
  val eventEmitter = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
138
+ eventEmitter.emit(eventName, params ?: Arguments.createMap())
139
+ } catch (e: Exception) {
140
+ Log.e("Scribeup", "Error emitting event: ${e.message}")
141
+ }
142
+ }
143
+ }
100
144
 
101
- val newParams = Arguments.createMap()
102
- if (params != null) {
103
- if (params.hasKey("message")) {
104
- newParams.putString("message", params.getString("message"))
105
- }
106
- if (params.hasKey("code")) {
107
- newParams.putString("code", params.getString("code"))
108
- }
145
+ // -------------------- Converters: Map/List -> Writable* --------------------
146
+
147
+ @Suppress("UNCHECKED_CAST")
148
+ private fun toWritableMap(map: Map<String, Any?>): WritableMap {
149
+ val out = Arguments.createMap()
150
+ for ((kAny, v) in map) {
151
+ val key = kAny as String
152
+ when (v) {
153
+ null -> out.putNull(key)
154
+ is String -> out.putString(key, v)
155
+ is Int -> out.putInt(key, v)
156
+ is Long -> {
157
+ if (v in Int.MIN_VALUE..Int.MAX_VALUE) out.putInt(key, v.toInt()) else out.putDouble(key, v.toDouble())
109
158
  }
159
+ is Float -> out.putDouble(key, v.toDouble())
160
+ is Double -> out.putDouble(key, v)
161
+ is Boolean -> out.putBoolean(key, v)
162
+ is Map<*, *> -> out.putMap(key, toWritableMap(v as Map<String, Any?>))
163
+ is List<*> -> out.putArray(key, toWritableArray(v))
164
+ else -> out.putString(key, v.toString())
165
+ }
166
+ }
167
+ return out
168
+ }
110
169
 
111
- eventEmitter.emit("ScribeupExitSignal", newParams)
112
- } catch (e: Exception) {
113
- Log.e("Scribeup", "Error emitting event: ${e.message}")
170
+ private fun toWritableArray(list: List<*>): WritableArray {
171
+ val out = Arguments.createArray()
172
+ for (v in list) {
173
+ when (v) {
174
+ null -> out.pushNull()
175
+ is String -> out.pushString(v)
176
+ is Int -> out.pushInt(v)
177
+ is Long -> {
178
+ if (v in Int.MIN_VALUE..Int.MAX_VALUE) out.pushInt(v.toInt()) else out.pushDouble(v.toDouble())
179
+ }
180
+ is Float -> out.pushDouble(v.toDouble())
181
+ is Double -> out.pushDouble(v)
182
+ is Boolean -> out.pushBoolean(v)
183
+ is Map<*, *> -> out.pushMap(toWritableMap(v as Map<String, Any?>))
184
+ is List<*> -> out.pushArray(toWritableArray(v))
185
+ else -> out.pushString(v.toString())
114
186
  }
115
187
  }
188
+ return out
116
189
  }
117
190
  }
@@ -4,12 +4,12 @@ import React
4
4
 
5
5
  // MARK: - Error Codes (shared between iOS and Android)
6
6
  fileprivate enum ErrorCodes {
7
- static let unknown = -1
8
- static let invalidUrl = 1001
9
- static let activityNull = 1002
10
- static let invalidActivityType = 1003
11
- static let noRootViewController = 1004
12
- static let sdkError = 2001
7
+ static let UNKNOWN = 1000
8
+ static let INVALID_URL = 1001
9
+ static let INVALID_ENV = 1002
10
+ static let ACTIVITY_NULL = 1003
11
+ static let INVALID_ACTIVITY_TYPE = 1004
12
+ static let NO_ROOT_VIEW_CONTROLLER = 1005
13
13
  }
14
14
 
15
15
  @objc(Scribeup)
@@ -24,7 +24,7 @@ class Scribeup: RCTEventEmitter {
24
24
 
25
25
  @objc
26
26
  override func supportedEvents() -> [String] {
27
- return ["ScribeupOnExit"]
27
+ return ["ScribeupOnExit", "ScribeupOnEvent"]
28
28
  }
29
29
 
30
30
  // MARK: - Properties
@@ -46,7 +46,7 @@ class Scribeup: RCTEventEmitter {
46
46
 
47
47
  DispatchQueue.main.async {
48
48
  guard let rootVC = UIApplication.shared.delegate?.window??.rootViewController else {
49
- self.rejecter?(String(ErrorCodes.noRootViewController), "Cannot find root view controller", nil)
49
+ self.rejecter?(String(ErrorCodes.NO_ROOT_VIEW_CONTROLLER), "Cannot find root view controller", nil)
50
50
  return
51
51
  }
52
52
 
@@ -60,18 +60,27 @@ class Scribeup: RCTEventEmitter {
60
60
  }
61
61
  }
62
62
 
63
- func sendExitEvent(error: SubscriptionManagerError?) {
63
+ func sendExitEvent(error: SubscriptionManagerError?, data: [String: Any]?) {
64
64
  if self.hasListeners {
65
65
  var body: [String: Any] = [:]
66
66
 
67
67
  if let error = error {
68
- body["error"] = ["code": ErrorCodes.sdkError, "message": error.message]
68
+ body["error"] = ["code": error.code, "message": error.message]
69
+ }
70
+ if let data = data {
71
+ body["data"] = data
69
72
  }
70
73
 
71
74
  self.sendEvent(withName: "ScribeupOnExit", body: body)
72
75
  }
73
76
  }
74
77
 
78
+ func sendOnEvent(data: [String: Any]) {
79
+ if self.hasListeners {
80
+ self.sendEvent(withName: "ScribeupOnEvent", body: ["data": data])
81
+ }
82
+ }
83
+
75
84
  override func startObserving() {
76
85
  self.hasListeners = true
77
86
  }
@@ -88,18 +97,24 @@ class ScribeupSubscriptionListener: NSObject, SubscriptionManagerListener {
88
97
  self.delegate = delegate
89
98
  }
90
99
 
91
- func onExit(_ error: SubscriptionManagerError?) {
100
+ func onExit(_ error: SubscriptionManagerError?, _ data: [String: Any]?) {
92
101
  if let delegate = self.delegate {
93
102
  if let error = error {
94
103
  delegate.rejecter?(String(error.code), error.message, nil)
95
104
  } else {
96
- delegate.resolver?(nil)
105
+ var payload: [String: Any] = [:]
106
+ if let data = data { payload["data"] = data }
107
+ delegate.resolver?(payload)
97
108
  }
98
109
 
99
110
  delegate.resolver = nil
100
111
  delegate.rejecter = nil
101
112
  }
102
113
 
103
- self.delegate?.sendExitEvent(error: error)
114
+ self.delegate?.sendExitEvent(error: error, data: data)
115
+ }
116
+
117
+ func onEvent(_ data: [String: Any]) {
118
+ self.delegate?.sendOnEvent(data: data)
104
119
  }
105
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scribeup/react-native-scribeup",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -52,8 +52,8 @@
52
52
  },
53
53
  "config": {
54
54
  "nativeSDKVersions": {
55
- "android": "0.8.2",
56
- "ios": "0.8.3"
55
+ "android": "0.8.5",
56
+ "ios": "0.8.5"
57
57
  }
58
58
  }
59
59
  }
package/src/ScribeUp.d.ts CHANGED
@@ -1,30 +1,46 @@
1
1
  import React from "react";
2
- /**
3
- * Data type returned when the SDK process ends
4
- */
5
- export type ExitData = {
6
- message?: string;
7
- code?: number;
8
- };
2
+
3
+ /** JSON value types allowed inside data payloads */
4
+ export type JsonValue =
5
+ | string
6
+ | number
7
+ | boolean
8
+ | null
9
+ | JsonObject
10
+ | JsonValue[];
11
+
12
+ export interface JsonObject {
13
+ [key: string]: JsonValue;
14
+ }
15
+
16
+ /** Error returned when the SDK process ends with failure */
17
+ export interface ExitError {
18
+ code: number;
19
+ message?: string;
20
+ }
21
+
9
22
  /**
10
23
  * ScribeUp component properties
11
24
  */
12
25
  export interface ScribeUpProps {
13
- url: string;
14
- productName: string;
15
- onExit: (data?: ExitData) => void;
26
+ url: string;
27
+ productName?: string;
28
+ onExit: (error: ExitError | null, data: JsonObject | null) => void;
29
+ onEvent?: (data: JsonObject) => void;
16
30
  }
31
+
17
32
  /**
18
33
  * ScribeUp Component
19
34
  *
20
35
  * React component that displays the ScribeUp SDK interface when
21
- * mounted and invokes the onExit callback when the user completes or
22
- * cancels the process.
36
+ * mounted and invokes the callbacks as the user interacts with or
37
+ * exits the flow.
23
38
  */
24
39
  declare class ScribeUp extends React.Component<ScribeUpProps> {
25
- constructor(props: ScribeUpProps);
26
- componentDidMount(): void;
27
- private present;
28
- render(): any;
40
+ constructor(props: ScribeUpProps);
41
+ componentDidMount(): void;
42
+ private present;
43
+ render(): any;
29
44
  }
45
+
30
46
  export default ScribeUp;
package/src/ScribeUp.tsx CHANGED
@@ -1,5 +1,10 @@
1
1
  import React from "react";
2
- import { NativeModules, Platform } from "react-native";
2
+ import {
3
+ NativeModules,
4
+ Platform,
5
+ NativeEventEmitter,
6
+ EmitterSubscription,
7
+ } from "react-native";
3
8
 
4
9
  const LINKING_ERROR =
5
10
  `The package 'scribeup-react-native-scribeup' doesn't seem to be linked. Make sure: \n\n` +
@@ -20,65 +25,152 @@ const Scribeup = NativeModules.Scribeup
20
25
  },
21
26
  );
22
27
 
23
- /**
24
- * Data type returned when the SDK process ends
25
- */
26
- export type ExitData = {
28
+ const EXIT_EVENT = "ScribeupOnExit";
29
+ const EVENT_NAME = "ScribeupOnEvent";
30
+
31
+ /** JSON value types allowed inside data payloads */
32
+ export type JsonValue =
33
+ | string
34
+ | number
35
+ | boolean
36
+ | null
37
+ | JsonObject
38
+ | JsonValue[];
39
+
40
+ export interface JsonObject {
41
+ [key: string]: JsonValue;
42
+ }
43
+
44
+ export type ExitError = {
45
+ code: number;
27
46
  message?: string;
28
- code?: number;
29
47
  };
30
48
 
49
+ type ExitPayload = { error?: ExitError; data?: JsonObject };
50
+ type EventPayload = { data?: JsonObject };
51
+
31
52
  /**
32
53
  * ScribeUp component properties
33
54
  */
34
55
  export interface ScribeUpProps {
35
56
  url: string;
36
- productName: string;
37
- onExit: (data?: ExitData) => void;
57
+ productName?: string;
58
+ onExit?: (error: ExitError | null, data: JsonObject | null) => void;
59
+ onEvent?: (data: JsonObject) => void;
38
60
  }
39
61
 
40
62
  /**
41
63
  * ScribeUp Component
42
64
  *
43
- * React component that displays the ScribeUp SDK interface when
44
- * mounted and invokes the onExit callback when the user completes or
45
- * cancels the process.
65
+ * Presents the native UI on mount and wires callbacks via device events.
66
+ * Safe against modules that don't implement addListener/removeListeners.
46
67
  */
47
68
  class ScribeUp extends React.Component<ScribeUpProps> {
48
- constructor(props: ScribeUpProps) {
49
- super(props);
50
- }
69
+ private emitter?: NativeEventEmitter;
70
+ private exitSub?: EmitterSubscription;
71
+ private eventSub?: EmitterSubscription;
72
+ private didExit = false;
51
73
 
52
74
  componentDidMount() {
53
- const { url, productName, onExit } = this.props;
54
- this.present(url, productName, onExit);
75
+ const { url, productName = "" } = this.props;
76
+
77
+ // Use module-backed emitter only if it implements the listener API.
78
+ const canAttach =
79
+ Scribeup &&
80
+ typeof (Scribeup as any).addListener === "function" &&
81
+ typeof (Scribeup as any).removeListeners === "function";
82
+
83
+ this.emitter = new NativeEventEmitter(canAttach ? (Scribeup as any) : undefined);
84
+
85
+ // Subscribe to onExit
86
+ this.exitSub = this.emitter.addListener(
87
+ EXIT_EVENT,
88
+ (payload?: ExitPayload) => {
89
+ if (this.didExit) return;
90
+ this.didExit = true;
91
+
92
+ const error: ExitError | null = payload?.error
93
+ ? { code: Number(payload.error.code ?? 1000), message: payload.error.message }
94
+ : null;
95
+
96
+ const data: JsonObject | null =
97
+ payload?.data && typeof payload.data === "object"
98
+ ? (payload.data as JsonObject)
99
+ : null;
100
+
101
+ this.props.onExit?.(error, data);
102
+ },
103
+ );
104
+
105
+ // Subscribe to onEvent
106
+ this.eventSub = this.emitter.addListener(
107
+ EVENT_NAME,
108
+ (payload?: EventPayload) => {
109
+ const data =
110
+ payload?.data && typeof payload.data === "object"
111
+ ? (payload.data as JsonObject)
112
+ : null;
113
+ if (data) this.props.onEvent?.(data);
114
+ },
115
+ );
116
+
117
+ // Present the native UI. Support both promise- and event-based native APIs.
118
+ this.present(url, productName);
55
119
  }
56
120
 
57
- private present(
58
- url: string,
59
- productName: string,
60
- onExit: (data?: ExitData) => void,
61
- ) {
62
- if (Scribeup && typeof Scribeup.presentWithUrl === "function") {
63
- try {
64
- Scribeup.presentWithUrl(url, productName)
121
+ componentWillUnmount() {
122
+ try {
123
+ this.exitSub?.remove();
124
+ this.eventSub?.remove();
125
+ } finally {
126
+ this.exitSub = undefined;
127
+ this.eventSub = undefined;
128
+ this.emitter = undefined;
129
+ }
130
+ }
131
+
132
+ private present(url: string, productName: string) {
133
+ if (!(Scribeup && typeof (Scribeup as any).presentWithUrl === "function")) {
134
+ throw new Error(`ScribeUp: Native module not found for ${Platform.OS}`);
135
+ }
136
+
137
+ try {
138
+ const ret = (Scribeup as any).presentWithUrl(url, productName);
139
+
140
+ // If native returns a Promise, also resolve to onExit (guarded to avoid duplicate calls).
141
+ if (ret && typeof ret.then === "function") {
142
+ ret
65
143
  .then((result: any) => {
66
- onExit(result || {});
144
+ if (this.didExit) return; // event already handled exit
145
+ this.didExit = true;
146
+
147
+ const error: ExitError | null = result?.error
148
+ ? { code: Number(result.error.code ?? 1000), message: result.error.message }
149
+ : null;
150
+
151
+ const data: JsonObject | null =
152
+ result && result.data && typeof result.data === "object"
153
+ ? (result.data as JsonObject)
154
+ : null;
155
+
156
+ this.props.onExit?.(error, data);
67
157
  })
68
- .catch((error: any) => {
69
- onExit({
70
- message: error?.message || "Unknown error",
71
- code: error?.code || -1,
72
- });
158
+ .catch((err: any) => {
159
+ if (this.didExit) return;
160
+ this.didExit = true;
161
+ this.props.onExit?.(
162
+ { code: Number(err?.code ?? 1000), message: err?.message || "Unexpected Error" },
163
+ null,
164
+ );
73
165
  });
74
- } catch (error: any) {
75
- onExit({
76
- message: error?.message || "Unknown error",
77
- code: error?.code || -1,
78
- });
79
166
  }
80
- } else {
81
- throw new Error(`ScribeUp: Native module not found for ${Platform.OS}`);
167
+ } catch (err: any) {
168
+ if (this.didExit) return;
169
+ this.didExit = true;
170
+ this.props.onExit?.(
171
+ { code: Number(err?.code ?? 1000), message: err?.message || "Unexpected Error" },
172
+ null,
173
+ );
82
174
  }
83
175
  }
84
176