@upscopeio/react-native-sdk 2026.3.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.
Files changed (64) hide show
  1. package/README.md +291 -0
  2. package/android/build.gradle.kts +36 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/kotlin/io/upscope/reactnative/UpscopeMaskedViewManager.kt +31 -0
  5. package/android/src/main/kotlin/io/upscope/reactnative/UpscopeModule.kt +297 -0
  6. package/android/src/main/kotlin/io/upscope/reactnative/UpscopePackage.kt +31 -0
  7. package/ios/UpscopeMaskedView.swift +18 -0
  8. package/ios/UpscopeMaskedViewManager.mm +4 -0
  9. package/ios/UpscopeMaskedViewManager.swift +13 -0
  10. package/ios/UpscopeModule.mm +31 -0
  11. package/ios/UpscopeModule.swift +243 -0
  12. package/lib/commonjs/NativeUpscopeMaskedView.js +10 -0
  13. package/lib/commonjs/NativeUpscopeMaskedView.js.map +1 -0
  14. package/lib/commonjs/NativeUpscopeModule.js +9 -0
  15. package/lib/commonjs/NativeUpscopeModule.js.map +1 -0
  16. package/lib/commonjs/Upscope.js +95 -0
  17. package/lib/commonjs/Upscope.js.map +1 -0
  18. package/lib/commonjs/UpscopeMasked.js +22 -0
  19. package/lib/commonjs/UpscopeMasked.js.map +1 -0
  20. package/lib/commonjs/hooks.js +98 -0
  21. package/lib/commonjs/hooks.js.map +1 -0
  22. package/lib/commonjs/index.js +58 -0
  23. package/lib/commonjs/index.js.map +1 -0
  24. package/lib/commonjs/package.json +1 -0
  25. package/lib/commonjs/types.js +2 -0
  26. package/lib/commonjs/types.js.map +1 -0
  27. package/lib/module/NativeUpscopeMaskedView.js +5 -0
  28. package/lib/module/NativeUpscopeMaskedView.js.map +1 -0
  29. package/lib/module/NativeUpscopeModule.js +5 -0
  30. package/lib/module/NativeUpscopeModule.js.map +1 -0
  31. package/lib/module/Upscope.js +91 -0
  32. package/lib/module/Upscope.js.map +1 -0
  33. package/lib/module/UpscopeMasked.js +17 -0
  34. package/lib/module/UpscopeMasked.js.map +1 -0
  35. package/lib/module/hooks.js +89 -0
  36. package/lib/module/hooks.js.map +1 -0
  37. package/lib/module/index.js +7 -0
  38. package/lib/module/index.js.map +1 -0
  39. package/lib/module/package.json +1 -0
  40. package/lib/module/types.js +2 -0
  41. package/lib/module/types.js.map +1 -0
  42. package/lib/typescript/NativeUpscopeMaskedView.d.ts +7 -0
  43. package/lib/typescript/NativeUpscopeMaskedView.d.ts.map +1 -0
  44. package/lib/typescript/NativeUpscopeModule.d.ts +20 -0
  45. package/lib/typescript/NativeUpscopeModule.d.ts.map +1 -0
  46. package/lib/typescript/Upscope.d.ts +60 -0
  47. package/lib/typescript/Upscope.d.ts.map +1 -0
  48. package/lib/typescript/UpscopeMasked.d.ts +7 -0
  49. package/lib/typescript/UpscopeMasked.d.ts.map +1 -0
  50. package/lib/typescript/hooks.d.ts +36 -0
  51. package/lib/typescript/hooks.d.ts.map +1 -0
  52. package/lib/typescript/index.d.ts +8 -0
  53. package/lib/typescript/index.d.ts.map +1 -0
  54. package/lib/typescript/types.d.ts +79 -0
  55. package/lib/typescript/types.d.ts.map +1 -0
  56. package/package.json +94 -0
  57. package/src/NativeUpscopeMaskedView.ts +9 -0
  58. package/src/NativeUpscopeModule.ts +32 -0
  59. package/src/Upscope.ts +113 -0
  60. package/src/UpscopeMasked.tsx +19 -0
  61. package/src/hooks.ts +99 -0
  62. package/src/index.ts +34 -0
  63. package/src/types.ts +109 -0
  64. package/upscopeio-react-native-sdk.podspec +17 -0
package/README.md ADDED
@@ -0,0 +1,291 @@
1
+ # @upscopeio/react-native-sdk
2
+
3
+ React Native SDK for Upscope cobrowsing — screen sharing, agent annotations, and remote control for iOS and Android.
4
+
5
+ ## Requirements
6
+
7
+ - React Native 0.76+
8
+ - iOS 14+
9
+ - Android API 26+ (Android 8.0 Oreo)
10
+ - New Architecture (Fabric + TurboModules) required
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @upscopeio/react-native-sdk
16
+ ```
17
+
18
+ ### iOS
19
+
20
+ 1. **Add UpscopeSDK via SPM** — In Xcode, go to your project > Package Dependencies > `+` and add:
21
+ ```
22
+ https://github.com/upscopeio/cobrowsing-ios
23
+ ```
24
+ Set version to `~> 1.0`.
25
+
26
+ 2. **Install pods** (for the RN bridge module):
27
+ ```bash
28
+ cd ios && pod install
29
+ ```
30
+
31
+ ### Android
32
+
33
+ No extra steps — the native module and Upscope Android SDK are linked automatically via autolinking.
34
+
35
+ ## Quick Start
36
+
37
+ Initialize in your app entry point (before rendering any components that use Upscope hooks):
38
+
39
+ ```tsx
40
+ import Upscope from '@upscopeio/react-native-sdk';
41
+
42
+ // App.tsx — call once, as early as possible
43
+ Upscope.initialize({
44
+ apiKey: 'your-api-key',
45
+ autoConnect: true,
46
+ requireAuthorizationForSession: true,
47
+ });
48
+
49
+ // Identify the visitor
50
+ Upscope.updateConnection({
51
+ uniqueId: 'user-123',
52
+ callName: 'Jane Doe',
53
+ tags: ['support'],
54
+ identities: ['jane@example.com'],
55
+ metadata: { plan: 'premium' },
56
+ });
57
+ ```
58
+
59
+ If `autoConnect` is `false`, call `Upscope.connect()` manually after initialization.
60
+
61
+ ## Hooks
62
+
63
+ All hooks are reactive — they re-render the component when the underlying state changes.
64
+
65
+ ### `useConnectionState`
66
+
67
+ Returns the current WebSocket connection state.
68
+
69
+ ```tsx
70
+ import { useConnectionState } from '@upscopeio/react-native-sdk';
71
+
72
+ function StatusBadge() {
73
+ const state = useConnectionState();
74
+ // state: 'inactive' | 'connecting' | 'connected' | 'reconnecting' | 'error'
75
+ return <Text>Connection: {state}</Text>;
76
+ }
77
+ ```
78
+
79
+ ### `useSessionState`
80
+
81
+ Returns the current co-browsing session state.
82
+
83
+ ```tsx
84
+ import { useSessionState } from '@upscopeio/react-native-sdk';
85
+
86
+ function SessionBanner() {
87
+ const session = useSessionState();
88
+ // session: 'inactive' | 'pendingRequest' | 'active' | 'paused' | 'ended'
89
+ if (session !== 'active') return null;
90
+ return <Text>Session in progress</Text>;
91
+ }
92
+ ```
93
+
94
+ ### `useShortId`
95
+
96
+ Returns the SDK short ID (used to look up the visitor in the Upscope dashboard), or `null` until assigned.
97
+
98
+ ```tsx
99
+ import { useShortId } from '@upscopeio/react-native-sdk';
100
+
101
+ function VisitorId() {
102
+ const shortId = useShortId();
103
+ return <Text>Short ID: {shortId ?? 'pending...'}</Text>;
104
+ }
105
+ ```
106
+
107
+ ### `useLookupCode`
108
+
109
+ Returns the current lookup code shown to the visitor for phone-based agent lookup, or `null` until emitted.
110
+
111
+ ```tsx
112
+ import { useLookupCode } from '@upscopeio/react-native-sdk';
113
+
114
+ function LookupCode() {
115
+ const code = useLookupCode();
116
+ return <Text>Your code: {code ?? '—'}</Text>;
117
+ }
118
+ ```
119
+
120
+ ### `useUpscope`
121
+
122
+ Returns a stable object of imperative action methods — safe to destructure and pass as deps.
123
+
124
+ ```tsx
125
+ import { useUpscope } from '@upscopeio/react-native-sdk';
126
+
127
+ function Controls() {
128
+ const { connect, disconnect, stopSession, sendCustomMessage } = useUpscope();
129
+ return (
130
+ <>
131
+ <Button title="Connect" onPress={connect} />
132
+ <Button title="Disconnect" onPress={disconnect} />
133
+ <Button title="End Session" onPress={stopSession} />
134
+ </>
135
+ );
136
+ }
137
+ ```
138
+
139
+ ## Events
140
+
141
+ Subscribe to native events imperatively via `Upscope.addListener`. Always call `.remove()` on cleanup.
142
+
143
+ ```tsx
144
+ import Upscope from '@upscopeio/react-native-sdk';
145
+ import { useEffect } from 'react';
146
+
147
+ useEffect(() => {
148
+ const sub = Upscope.addListener('sessionStarted', ({ agentName }) => {
149
+ console.log(`Session started with ${agentName}`);
150
+ });
151
+ return () => sub.remove();
152
+ }, []);
153
+ ```
154
+
155
+ ### Event names
156
+
157
+ | Event name | Payload type | Description |
158
+ | ------------------------- | ----------------------------- | ---------------------------------------- |
159
+ | `connectionStateChanged` | `ConnectionStateChangedEvent` | Connection state transitioned |
160
+ | `sessionStarted` | `SessionStartedEvent` | Agent joined and session is active |
161
+ | `sessionEnded` | `SessionEndedEvent` | Session ended (includes reason) |
162
+ | `customMessageReceived` | `CustomMessageEvent` | Arbitrary string from an observer |
163
+ | `error` | `UpscopeError` | Non-fatal SDK error |
164
+ | `observerJoined` | `Observer` | New observer connected |
165
+ | `observerLeft` | `ObserverLeftEvent` | Observer disconnected |
166
+ | `observerCountChanged` | `ObserverCountChangedEvent` | Total observer count changed |
167
+ | `shortIdChanged` | `ShortIdChangedEvent` | Short ID assigned or rotated |
168
+ | `lookupCodeChanged` | `LookupCodeChangedEvent` | Lookup code issued or changed |
169
+
170
+ ## View Masking
171
+
172
+ Wrap any sensitive content in `UpscopeMasked` to hide it from the agent's view during a session. The masked region appears as a solid block in the screen share.
173
+
174
+ ```tsx
175
+ import { UpscopeMasked } from '@upscopeio/react-native-sdk';
176
+
177
+ function PaymentForm() {
178
+ return (
179
+ <View>
180
+ <Text>Card number</Text>
181
+ <UpscopeMasked>
182
+ <TextInput secureTextEntry placeholder="•••• •••• •••• ••••" />
183
+ </UpscopeMasked>
184
+ </View>
185
+ );
186
+ }
187
+ ```
188
+
189
+ `UpscopeMasked` accepts all standard `ViewProps` in addition to `children`.
190
+
191
+ ## Configuration
192
+
193
+ Pass a `UpscopeConfig` object to `Upscope.initialize()`:
194
+
195
+ ```ts
196
+ interface UpscopeConfig {
197
+ apiKey: string; // Required. Your Upscope API key.
198
+ requireAuthorizationForSession?: boolean; // Show a prompt before allowing a session. Default: true.
199
+ autoConnect?: boolean; // Connect on initialize(). Default: false.
200
+ region?: string; // Override the server region.
201
+ showBanner?: boolean; // Show the native in-session banner.
202
+ showTerminateButton?: boolean; // Show an end-session button in the banner.
203
+ authorizationPromptTitle?: string; // Custom title for the session request dialog.
204
+ authorizationPromptMessage?: string; // Custom message for the session request dialog.
205
+ endOfSessionMessage?: string; // Message shown when a session ends.
206
+ stopSessionText?: string; // Label for the terminate-session button.
207
+ allowRemoteClick?: boolean; // Allow agent to send tap events. Default: true.
208
+ allowRemoteScroll?: boolean; // Allow agent to send scroll events. Default: true.
209
+ requireControlRequest?: boolean; // Require visitor approval before remote control starts.
210
+ }
211
+ ```
212
+
213
+ ## API Reference
214
+
215
+ ### Initialization
216
+
217
+ | Method | Description |
218
+ | ----------------------------------- | --------------------------------------------------- |
219
+ | `initialize(config)` | Initialize the SDK. Call once before everything else. |
220
+ | `connect()` | Open the WebSocket connection. |
221
+ | `disconnect()` | Close the WebSocket connection. |
222
+ | `reset(reconnect?)` | Reset connection state. Reconnects by default. |
223
+
224
+ ### Visitor Identity
225
+
226
+ | Method | Description |
227
+ | ----------------------------------- | --------------------------------------------------- |
228
+ | `updateConnection(params)` | Set/update `uniqueId`, `callName`, `tags`, `identities`, `metadata`. |
229
+
230
+ ### Session
231
+
232
+ | Method | Description |
233
+ | ----------------------------------- | --------------------------------------------------- |
234
+ | `stopSession()` | Terminate the active session. |
235
+ | `requestAgent()` | Request that an agent join. |
236
+ | `cancelAgentRequest()` | Cancel a pending agent-join request. |
237
+
238
+ ### Data
239
+
240
+ | Method | Returns | Description |
241
+ | ----------------------------------- | -------------------- | ------------------------------------- |
242
+ | `getShortId()` | `Promise<string \| null>` | Resolve the current short ID. |
243
+ | `getWatchLink()` | `Promise<string \| null>` | Resolve the session watch link. |
244
+ | `getLookupCode()` | `void` | Trigger a `lookupCodeChanged` event. |
245
+
246
+ ### Messaging
247
+
248
+ | Method | Description |
249
+ | ----------------------------------- | --------------------------------------------------- |
250
+ | `sendCustomMessage(message)` | Send a string to all connected observers. |
251
+
252
+ ### Events
253
+
254
+ | Method | Description |
255
+ | ----------------------------------- | --------------------------------------------------- |
256
+ | `addListener(eventName, callback)` | Subscribe to an event. Returns `{ remove() }`. |
257
+
258
+ ## Example App
259
+
260
+ A full-featured example app is included in `example/`. It demonstrates initialization, connection, visitor identification, view masking, and event logging.
261
+
262
+ ### Running the example
263
+
264
+ ```bash
265
+ # Install dependencies
266
+ cd example
267
+ npm install
268
+
269
+ # iOS
270
+ cd ios && pod install && cd ..
271
+ npm run ios
272
+
273
+ # Android
274
+ npm run android
275
+ ```
276
+
277
+ The app prompts for an API key on launch, then shows all SDK features in a scrollable demo screen.
278
+
279
+ ## Troubleshooting
280
+
281
+ **"SDK not initialized"** — `Upscope.initialize()` must be called before `connect()` or any hook mounts.
282
+
283
+ **Session prompt never appears** — Ensure `requireAuthorizationForSession: true` (default). On Android, the app must be in the foreground.
284
+
285
+ **Masked views visible to agent** — `UpscopeMasked` must be a direct native view wrapper. Confirm autolinking ran and `pod install` completed on iOS.
286
+
287
+ **TypeScript errors on event payloads** — Import the specific payload type from `@upscopeio/react-native-sdk`, e.g. `import type { SessionStartedEvent } from '@upscopeio/react-native-sdk'`.
288
+
289
+ ## License
290
+
291
+ MIT
@@ -0,0 +1,36 @@
1
+ plugins {
2
+ id("com.android.library")
3
+ id("org.jetbrains.kotlin.android")
4
+ id("com.facebook.react")
5
+ }
6
+
7
+ android {
8
+ namespace = "io.upscope.reactnative"
9
+ compileSdk = 36
10
+
11
+ defaultConfig {
12
+ minSdk = 26
13
+ }
14
+
15
+ compileOptions {
16
+ sourceCompatibility = JavaVersion.VERSION_17
17
+ targetCompatibility = JavaVersion.VERSION_17
18
+ }
19
+
20
+ kotlin {
21
+ compilerOptions {
22
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
23
+ }
24
+ }
25
+
26
+ sourceSets {
27
+ named("main") {
28
+ java.srcDirs("src/main/kotlin")
29
+ }
30
+ }
31
+ }
32
+
33
+ dependencies {
34
+ implementation("com.facebook.react:react-android")
35
+ implementation("io.github.upscopeio:upscope-android-sdk:2026.3.6")
36
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,31 @@
1
+ package io.upscope.reactnative
2
+
3
+ import android.widget.FrameLayout
4
+ import com.facebook.react.module.annotations.ReactModule
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.ViewGroupManager
7
+ import io.upscope.sdk.Upscope
8
+
9
+ @ReactModule(name = UpscopeMaskedViewManager.NAME)
10
+ class UpscopeMaskedViewManager : ViewGroupManager<FrameLayout>() {
11
+
12
+ companion object {
13
+ const val NAME = "UpscopeMaskedView"
14
+ }
15
+
16
+ override fun getName(): String = NAME
17
+
18
+ override fun createViewInstance(reactContext: ThemedReactContext): FrameLayout {
19
+ val view = FrameLayout(reactContext)
20
+ view.addOnAttachStateChangeListener(object : android.view.View.OnAttachStateChangeListener {
21
+ override fun onViewAttachedToWindow(v: android.view.View) {
22
+ Upscope.addMaskedView(v)
23
+ }
24
+
25
+ override fun onViewDetachedFromWindow(v: android.view.View) {
26
+ Upscope.removeMaskedView(v)
27
+ }
28
+ })
29
+ return view
30
+ }
31
+ }
@@ -0,0 +1,297 @@
1
+ package io.upscope.reactnative
2
+
3
+ import android.app.AlertDialog
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.Promise
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.facebook.react.bridge.ReadableMap
8
+ import com.facebook.react.bridge.WritableMap
9
+ import com.facebook.react.module.annotations.ReactModule
10
+ import com.facebook.react.modules.core.DeviceEventManagerModule
11
+ import io.upscope.sdk.Cancellable
12
+ import io.upscope.sdk.ConnectionState
13
+ import io.upscope.sdk.Observer
14
+ import io.upscope.sdk.SessionEndReason
15
+ import io.upscope.sdk.SessionRequestResponse
16
+ import io.upscope.sdk.Upscope
17
+ import io.upscope.sdk.UpscopeConfiguration
18
+ import io.upscope.sdk.UpscopeError
19
+ import io.upscope.sdk.UpscopeListener
20
+ import kotlinx.coroutines.CoroutineScope
21
+ import kotlinx.coroutines.Dispatchers
22
+ import kotlinx.coroutines.Job
23
+ import kotlinx.coroutines.launch
24
+
25
+ @ReactModule(name = UpscopeModule.NAME)
26
+ class UpscopeModule(
27
+ private val reactContext: ReactApplicationContext
28
+ ) : NativeUpscopeModuleSpec(reactContext) {
29
+
30
+ companion object {
31
+ const val NAME = "UpscopeModule"
32
+ }
33
+
34
+ override fun getName(): String = NAME
35
+
36
+ private var listenerCount = 0
37
+ private var shortIdJob: Job? = null
38
+ private var lookupCodeJob: Job? = null
39
+
40
+ private val upscopeListener = object : UpscopeListener {
41
+ override fun onConnectionStateChanged(state: ConnectionState) {
42
+ val params = Arguments.createMap().apply {
43
+ putString("state", serializeConnectionState(state))
44
+ if (state is ConnectionState.Error) {
45
+ putMap("error", serializeError(state.error))
46
+ }
47
+ }
48
+ sendEvent("connectionStateChanged", params)
49
+ }
50
+
51
+ override fun onSessionStarted(agentName: String?) {
52
+ val params = Arguments.createMap().apply {
53
+ putString("agentName", agentName)
54
+ }
55
+ sendEvent("sessionStarted", params)
56
+ }
57
+
58
+ override fun onSessionEnded(reason: SessionEndReason) {
59
+ val params = Arguments.createMap().apply {
60
+ putString("reason", serializeEndReason(reason))
61
+ if (reason is SessionEndReason.Error) {
62
+ putMap("error", serializeError(reason.error))
63
+ }
64
+ }
65
+ sendEvent("sessionEnded", params)
66
+ }
67
+
68
+ override fun onCustomMessageReceived(message: String, observerId: String) {
69
+ val params = Arguments.createMap().apply {
70
+ putString("message", message)
71
+ putString("observerId", observerId)
72
+ }
73
+ sendEvent("customMessageReceived", params)
74
+ }
75
+
76
+ override fun onError(error: UpscopeError) {
77
+ sendEvent("error", serializeError(error))
78
+ }
79
+
80
+ override fun onObserverJoined(observer: Observer) {
81
+ sendEvent("observerJoined", serializeObserver(observer))
82
+ }
83
+
84
+ override fun onObserverLeft(observerId: String) {
85
+ val params = Arguments.createMap().apply {
86
+ putString("observerId", observerId)
87
+ }
88
+ sendEvent("observerLeft", params)
89
+ }
90
+
91
+ override fun onObserverCountChanged(count: Int) {
92
+ val params = Arguments.createMap().apply {
93
+ putInt("count", count)
94
+ }
95
+ sendEvent("observerCountChanged", params)
96
+ }
97
+ }
98
+
99
+ override fun initialize(config: ReadableMap) {
100
+ val apiKey = config.getString("apiKey")
101
+ ?: throw IllegalArgumentException("apiKey is required")
102
+
103
+ val builder = UpscopeConfiguration.Builder(apiKey)
104
+ .wrapper("react-native")
105
+
106
+ if (config.hasKey("requireAuthorizationForSession")) {
107
+ builder.requireAuthorizationForSession(config.getBoolean("requireAuthorizationForSession"))
108
+ }
109
+ if (config.hasKey("autoConnect")) {
110
+ builder.autoConnect(config.getBoolean("autoConnect"))
111
+ }
112
+ if (config.hasKey("region")) {
113
+ builder.region(checkNotNull(config.getString("region")))
114
+ }
115
+ if (config.hasKey("showTerminateButton")) {
116
+ builder.showTerminateButton(config.getBoolean("showTerminateButton"))
117
+ }
118
+ if (config.hasKey("authorizationPromptTitle")) {
119
+ builder.authorizationPromptTitle(
120
+ checkNotNull(config.getString("authorizationPromptTitle"))
121
+ )
122
+ }
123
+ if (config.hasKey("authorizationPromptMessage")) {
124
+ builder.authorizationPromptMessage(
125
+ checkNotNull(config.getString("authorizationPromptMessage"))
126
+ )
127
+ }
128
+ if (config.hasKey("endOfSessionMessage")) {
129
+ builder.endOfSessionMessage(checkNotNull(config.getString("endOfSessionMessage")))
130
+ }
131
+ if (config.hasKey("stopSessionText")) {
132
+ builder.stopSessionText(checkNotNull(config.getString("stopSessionText")))
133
+ }
134
+ if (config.hasKey("allowRemoteClick")) {
135
+ builder.allowRemoteClick(config.getBoolean("allowRemoteClick"))
136
+ }
137
+ if (config.hasKey("allowRemoteScroll")) {
138
+ builder.allowRemoteScroll(config.getBoolean("allowRemoteScroll"))
139
+ }
140
+ if (config.hasKey("requireControlRequest")) {
141
+ builder.requireControlRequest(config.getBoolean("requireControlRequest"))
142
+ }
143
+
144
+ val promptTitle = config.getString("authorizationPromptTitle") ?: "Screen Sharing Request"
145
+ val promptMessage = config.getString("authorizationPromptMessage")
146
+ ?: "An agent wants to view your screen. Do you accept?"
147
+ val yesLabel = config.getString("translationYes") ?: "Yes"
148
+ val noLabel = config.getString("translationNo") ?: "No"
149
+
150
+ builder.onSessionRequest { response: SessionRequestResponse, agentName: String? ->
151
+ val activity = reactContext.currentActivity
152
+ if (activity == null) {
153
+ response.reject()
154
+ return@onSessionRequest null
155
+ }
156
+
157
+ val appLabel = activity.packageManager
158
+ .getApplicationLabel(activity.applicationInfo)
159
+ .toString()
160
+
161
+ fun String.fillTemplates() = this
162
+ .replace("{%agentName%}", agentName ?: "")
163
+ .replace("{%currentDomain%}", appLabel)
164
+
165
+ val dialog = AlertDialog.Builder(activity)
166
+ .setTitle(promptTitle.fillTemplates())
167
+ .setMessage(promptMessage.fillTemplates())
168
+ .setCancelable(false)
169
+ .setPositiveButton(yesLabel) { _, _ -> response.accept() }
170
+ .setNegativeButton(noLabel) { _, _ -> response.reject() }
171
+ .create()
172
+
173
+ dialog.show()
174
+ Cancellable { dialog.dismiss() }
175
+ }
176
+
177
+ val configuration = builder.build()
178
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
179
+ Upscope.initialize(reactContext.applicationContext, configuration)
180
+ Upscope.listener = upscopeListener
181
+ reactContext.currentActivity?.let { Upscope.setCurrentActivity(it) }
182
+ }
183
+ }
184
+
185
+ override fun connect() { Upscope.connect() }
186
+
187
+ override fun disconnect() { Upscope.disconnect() }
188
+
189
+ override fun reset(reconnect: Boolean) { Upscope.reset(reconnect) }
190
+
191
+ override fun updateConnection(params: ReadableMap) {
192
+ Upscope.updateConnection(
193
+ uniqueId = if (params.hasKey("uniqueId")) params.getString("uniqueId") else null,
194
+ callName = if (params.hasKey("callName")) params.getString("callName") else null,
195
+ tags = if (params.hasKey("tags")) params.getArray("tags")?.toArrayList()?.filterIsInstance<String>() else null,
196
+ identities = if (params.hasKey("identities")) params.getArray("identities")?.toArrayList()?.filterIsInstance<String>() else null,
197
+ metadata = if (params.hasKey("metadata")) {
198
+ val map = params.getMap("metadata")
199
+ val result = mutableMapOf<String, String>()
200
+ map?.entryIterator?.forEach { entry ->
201
+ result[entry.key] = entry.value.toString()
202
+ }
203
+ result
204
+ } else null
205
+ )
206
+ }
207
+
208
+ override fun stopSession() { Upscope.stopSession() }
209
+
210
+ override fun requestAgent() { Upscope.requestAgent() }
211
+
212
+ override fun cancelAgentRequest() { Upscope.cancelAgentRequest() }
213
+
214
+ override fun getLookupCode() { Upscope.getLookupCode() }
215
+
216
+ override fun getShortId(promise: Promise) { promise.resolve(Upscope.getShortId()) }
217
+
218
+ override fun getWatchLink(promise: Promise) { promise.resolve(Upscope.getWatchLink()) }
219
+
220
+ override fun sendCustomMessage(message: String) { Upscope.sendCustomMessage(message) }
221
+
222
+ /**
223
+ * Called by RN when the JS side adds an event listener. Starts collecting Flow emissions
224
+ * for shortId and lookupCode on the first listener so we only run these coroutines when
225
+ * something is actually subscribed.
226
+ */
227
+ override fun addListener(eventName: String) {
228
+ listenerCount++
229
+ if (listenerCount == 1) {
230
+ val scope = CoroutineScope(Dispatchers.Main)
231
+ shortIdJob = scope.launch {
232
+ Upscope.shortIdFlow.collect { shortId ->
233
+ val p = Arguments.createMap().apply { putString("shortId", shortId) }
234
+ sendEvent("shortIdChanged", p)
235
+ }
236
+ }
237
+ lookupCodeJob = scope.launch {
238
+ Upscope.lookupCodeFlow.collect { code ->
239
+ val p = Arguments.createMap().apply { putString("lookupCode", code) }
240
+ sendEvent("lookupCodeChanged", p)
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Called by RN when JS listeners are removed. Cancels Flow collection jobs when the
248
+ * subscriber count drops to zero to avoid unnecessary work.
249
+ */
250
+ override fun removeListeners(count: Double) {
251
+ listenerCount -= count.toInt()
252
+ if (listenerCount <= 0) {
253
+ listenerCount = 0
254
+ shortIdJob?.cancel()
255
+ lookupCodeJob?.cancel()
256
+ shortIdJob = null
257
+ lookupCodeJob = null
258
+ }
259
+ }
260
+
261
+ private fun sendEvent(eventName: String, params: WritableMap) {
262
+ if (listenerCount <= 0) return
263
+ reactContext
264
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
265
+ .emit(eventName, params)
266
+ }
267
+
268
+ private fun serializeConnectionState(state: ConnectionState): String = when (state) {
269
+ is ConnectionState.Inactive -> "inactive"
270
+ is ConnectionState.Connecting -> "connecting"
271
+ is ConnectionState.Connected -> "connected"
272
+ is ConnectionState.Reconnecting -> "reconnecting"
273
+ is ConnectionState.Error -> "error"
274
+ }
275
+
276
+ private fun serializeEndReason(reason: SessionEndReason): String = when (reason) {
277
+ is SessionEndReason.UserStopped -> "userStopped"
278
+ is SessionEndReason.AgentStopped -> "agentStopped"
279
+ is SessionEndReason.Timeout -> "timeout"
280
+ is SessionEndReason.Error -> "error"
281
+ }
282
+
283
+ private fun serializeError(error: UpscopeError): WritableMap = Arguments.createMap().apply {
284
+ putString("code", error.code)
285
+ putString("message", error.message)
286
+ }
287
+
288
+ private fun serializeObserver(observer: Observer): WritableMap = Arguments.createMap().apply {
289
+ putString("id", observer.id)
290
+ putString("name", observer.name)
291
+ putDouble("screenWidth", observer.screenWidth)
292
+ putDouble("screenHeight", observer.screenHeight)
293
+ putDouble("windowWidth", observer.windowWidth)
294
+ putDouble("windowHeight", observer.windowHeight)
295
+ putBoolean("hasFocus", observer.hasFocus)
296
+ }
297
+ }
@@ -0,0 +1,31 @@
1
+ package io.upscope.reactnative
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+
10
+ class UpscopePackage : BaseReactPackage() {
11
+
12
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
13
+ if (name == UpscopeModule.NAME) UpscopeModule(reactContext) else null
14
+
15
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
16
+ mapOf(
17
+ UpscopeModule.NAME to ReactModuleInfo(
18
+ name = UpscopeModule.NAME,
19
+ className = UpscopeModule.NAME,
20
+ canOverrideExistingModule = false,
21
+ needsEagerInit = false,
22
+ isCxxModule = false,
23
+ isTurboModule = true
24
+ )
25
+ )
26
+ }
27
+
28
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
29
+ return listOf(UpscopeMaskedViewManager())
30
+ }
31
+ }