@upscopeio/react-native-sdk 2026.4.1 → 2026.6.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 (40) hide show
  1. package/README.md +144 -17
  2. package/android/build.gradle.kts +1 -1
  3. package/android/src/main/kotlin/io/upscope/reactnative/UpscopeModule.kt +193 -31
  4. package/ios/UpscopeModule.mm +3 -0
  5. package/ios/UpscopeModule.swift +195 -43
  6. package/lib/commonjs/NativeUpscopeModule.js.map +1 -1
  7. package/lib/commonjs/Upscope.js +55 -18
  8. package/lib/commonjs/Upscope.js.map +1 -1
  9. package/lib/commonjs/hooks.js +45 -2
  10. package/lib/commonjs/hooks.js.map +1 -1
  11. package/lib/commonjs/index.js +12 -0
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/version.js +1 -1
  14. package/lib/module/NativeUpscopeModule.js.map +1 -1
  15. package/lib/module/Upscope.js +57 -19
  16. package/lib/module/Upscope.js.map +1 -1
  17. package/lib/module/hooks.js +43 -2
  18. package/lib/module/hooks.js.map +1 -1
  19. package/lib/module/index.js +4 -4
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/version.js +1 -1
  22. package/lib/typescript/NativeUpscopeModule.d.ts +3 -0
  23. package/lib/typescript/NativeUpscopeModule.d.ts.map +1 -1
  24. package/lib/typescript/Upscope.d.ts +20 -2
  25. package/lib/typescript/Upscope.d.ts.map +1 -1
  26. package/lib/typescript/hooks.d.ts +20 -3
  27. package/lib/typescript/hooks.d.ts.map +1 -1
  28. package/lib/typescript/index.d.ts +7 -7
  29. package/lib/typescript/index.d.ts.map +1 -1
  30. package/lib/typescript/types.d.ts +64 -17
  31. package/lib/typescript/types.d.ts.map +1 -1
  32. package/lib/typescript/version.d.ts +1 -1
  33. package/package.json +1 -1
  34. package/src/NativeUpscopeModule.ts +5 -0
  35. package/src/Upscope.ts +63 -20
  36. package/src/hooks.ts +56 -3
  37. package/src/index.ts +19 -10
  38. package/src/types.ts +78 -30
  39. package/src/version.ts +1 -1
  40. package/upscopeio-react-native-sdk.podspec +1 -1
package/README.md CHANGED
@@ -17,16 +17,14 @@ npm install @upscopeio/react-native-sdk
17
17
 
18
18
  ### iOS
19
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`.
20
+ Run `pod install` — the native UpscopeSDK is linked automatically via this SDK's podspec dependency, so its version always matches the published package. No extra steps.
25
21
 
26
- 2. **Install pods** (for the RN bridge module):
27
- ```bash
28
- cd ios && pod install
29
- ```
22
+ ```bash
23
+ cd ios && pod install
24
+ ```
25
+
26
+ By default the SDK shares only your app's UI. To let agents view the visitor's
27
+ **entire device**, see [Full device sharing (iOS)](#full-device-sharing-ios).
30
28
 
31
29
  ### Android
32
30
 
@@ -118,6 +116,34 @@ function LookupCode() {
118
116
  }
119
117
  ```
120
118
 
119
+ ### `useRemoteControlState`
120
+
121
+ Returns whether an agent currently has remote control of the device.
122
+
123
+ ```tsx
124
+ import { useRemoteControlState } from '@upscopeio/react-native-sdk';
125
+
126
+ function ControlBadge() {
127
+ const state = useRemoteControlState();
128
+ // state: 'inactive' | 'active'
129
+ return <Text>Remote control: {state}</Text>;
130
+ }
131
+ ```
132
+
133
+ ### `useFullDeviceSharingState`
134
+
135
+ Returns whether full-device (broadcast) sharing is currently running.
136
+
137
+ ```tsx
138
+ import { useFullDeviceSharingState } from '@upscopeio/react-native-sdk';
139
+
140
+ function SharingBadge() {
141
+ const state = useFullDeviceSharingState();
142
+ // state: 'inactive' | 'active'
143
+ return <Text>Full-device sharing: {state}</Text>;
144
+ }
145
+ ```
146
+
121
147
  ### `useUpscope`
122
148
 
123
149
  Returns a stable object of imperative action methods — safe to destructure and pass as deps.
@@ -158,15 +184,19 @@ useEffect(() => {
158
184
  | Event name | Payload type | Description |
159
185
  | ------------------------- | ----------------------------- | ---------------------------------------- |
160
186
  | `connectionStateChanged` | `ConnectionStateChangedEvent` | Connection state transitioned |
187
+ | `sessionStateChanged` | `SessionStateChangedEvent` | Session state transitioned (current-value)|
161
188
  | `sessionStarted` | `SessionStartedEvent` | Agent joined and session is active |
162
189
  | `sessionEnded` | `SessionEndedEvent` | Session ended (includes reason) |
163
- | `customMessageReceived` | `CustomMessageEvent` | Arbitrary string from an observer |
190
+ | `customMessageReceived` | `CustomMessageEvent` | Arbitrary string from a viewer |
164
191
  | `error` | `UpscopeError` | Non-fatal SDK error |
165
- | `observerJoined` | `Observer` | New observer connected |
166
- | `observerLeft` | `ObserverLeftEvent` | Observer disconnected |
167
- | `observerCountChanged` | `ObserverCountChangedEvent` | Total observer count changed |
192
+ | `viewerJoined` | `Viewer` | New viewer connected |
193
+ | `viewerLeft` | `ViewerLeftEvent` | Viewer disconnected |
194
+ | `viewerCountChanged` | `ViewerCountChangedEvent` | Total viewer count changed |
168
195
  | `shortIdChanged` | `ShortIdChangedEvent` | Short ID assigned or rotated |
169
196
  | `lookupCodeChanged` | `LookupCodeChangedEvent` | Lookup code issued or changed |
197
+ | `remoteControlStateChanged` | `RemoteControlStateChangedEvent` | Agent gained or lost remote control |
198
+ | `fullDeviceSharingStateChanged` | `FullDeviceSharingStateChangedEvent` | Full-device sharing started or stopped |
199
+ | `fullDeviceRequest` | `FullDeviceRequestEvent` | Agent requested full-device sharing — answer with `respondToFullDeviceRequest` |
170
200
 
171
201
  ## View Masking
172
202
 
@@ -199,18 +229,112 @@ interface UpscopeConfig {
199
229
  requireAuthorizationForSession?: boolean; // Show a prompt before allowing a session. Default: true.
200
230
  autoConnect?: boolean; // Connect on initialize(). Default: false.
201
231
  region?: string; // Override the server region.
202
- showBanner?: boolean; // Show the native in-session banner.
203
- showTerminateButton?: boolean; // Show an end-session button in the banner.
232
+ showTerminateButton?: boolean; // Show an end-session button during a session.
204
233
  authorizationPromptTitle?: string; // Custom title for the session request dialog.
205
234
  authorizationPromptMessage?: string; // Custom message for the session request dialog.
206
235
  endOfSessionMessage?: string; // Message shown when a session ends.
207
- stopSessionText?: string; // Label for the terminate-session button.
236
+ stopSessionText?: LocalizedString; // Label for the terminate-session button.
208
237
  allowRemoteClick?: boolean; // Allow agent to send tap events. Default: true.
209
238
  allowRemoteScroll?: boolean; // Allow agent to send scroll events. Default: true.
210
239
  requireControlRequest?: boolean; // Require visitor approval before remote control starts.
240
+ broadcastAppGroupId?: string; // iOS only. App Group shared with the Broadcast Upload Extension (enables full-device sharing).
241
+ broadcastExtensionBundleId?: string; // iOS only. Bundle id of the Broadcast Upload Extension.
211
242
  }
243
+
244
+ // Translation fields accept either a plain string or a language-keyed map.
245
+ // Keys are BCP-47 language tags; the native SDK picks the best match for the
246
+ // device's preferred languages.
247
+ type LocalizedString = string | { [language: string]: string };
212
248
  ```
213
249
 
250
+ Example:
251
+
252
+ ```ts
253
+ Upscope.initialize({
254
+ apiKey: "…",
255
+ stopSessionText: { en: "Stop", it: "Ferma", "pt-PT": "Parar" },
256
+ });
257
+ ```
258
+
259
+ ## Full device sharing (iOS)
260
+
261
+ Full device sharing lets the agent see the entire device screen (other apps,
262
+ home screen) via ReplayKit. It requires a Broadcast Upload Extension target in
263
+ your app — npm packages can't add app extension targets, so this is a one-time
264
+ manual setup (~10 minutes). Without it, the agent's "Full app sharing" button
265
+ is hidden.
266
+
267
+ > Android needs no setup: it uses MediaProjection, and the required
268
+ > foreground-service permissions are merged automatically from the SDK's manifest.
269
+
270
+ ### 1. Add the extension target
271
+
272
+ In Xcode: **File → New → Target → Broadcast Upload Extension**. Name it (e.g.
273
+ `YourAppBroadcast`), uncheck "Include UI Extension".
274
+
275
+ ### 2. Replace the generated SampleHandler
276
+
277
+ ```swift
278
+ import UpscopeSDK
279
+
280
+ class SampleHandler: UpscopeSampleHandler {}
281
+ ```
282
+
283
+ ### 3. Add an App Group to BOTH targets
284
+
285
+ In **Signing & Capabilities** for your app target *and* the extension target,
286
+ add the **App Groups** capability with the same group, e.g.
287
+ `group.com.yourcompany.yourapp`.
288
+
289
+ ### 4. Declare the group in the extension's Info.plist
290
+
291
+ ```xml
292
+ <key>UpscopeAppGroupId</key>
293
+ <string>group.com.yourcompany.yourapp</string>
294
+ ```
295
+
296
+ Also make sure `NSExtension > RPBroadcastProcessMode` is `2` (sample-buffer
297
+ mode); some Xcode template versions omit it. See the example app's
298
+ [`Info.plist`](example/ios/UpscopeExampleBroadcast/Info.plist).
299
+
300
+ ### 5. Add the extension pod
301
+
302
+ ```ruby
303
+ # Podfile (top level, alongside your app target)
304
+ target 'YourAppBroadcast' do
305
+ pod 'UpscopeSDK/BroadcastExtension'
306
+ end
307
+ ```
308
+
309
+ Run `pod install`.
310
+
311
+ ### 6. Pass the keys at initialization
312
+
313
+ ```ts
314
+ Upscope.initialize({
315
+ apiKey: 'your-api-key',
316
+ broadcastAppGroupId: 'group.com.yourcompany.yourapp',
317
+ broadcastExtensionBundleId: 'com.yourcompany.yourapp.YourAppBroadcast',
318
+ });
319
+ ```
320
+
321
+ `broadcastAppGroupId` enables the feature (the agent's button appears);
322
+ `broadcastExtensionBundleId` preselects your extension in the iOS picker.
323
+
324
+ ### Notes
325
+
326
+ - The App Group ID must match in **three** places: both targets' entitlements,
327
+ the extension's `Info.plist` (`UpscopeAppGroupId`), and `broadcastAppGroupId`.
328
+ - Test on a **physical device** — ReplayKit broadcasts are unreliable on the
329
+ simulator.
330
+ - Full-device capture cannot mask views. Set
331
+ `disableFullScreenWhenMasked: true` to automatically end full-device sharing
332
+ whenever masked content is on screen.
333
+ - Expo managed workflow is not supported (extension targets require a config
334
+ plugin; not currently provided).
335
+ - A complete working setup is in
336
+ [`example/ios`](example/ios) (`UpscopeExampleBroadcast` target).
337
+
214
338
  ## API Reference
215
339
 
216
340
  ### Initialization
@@ -235,6 +359,9 @@ interface UpscopeConfig {
235
359
  | `stopSession()` | Terminate the active session. |
236
360
  | `requestAgent()` | Request that an agent join. |
237
361
  | `cancelAgentRequest()` | Cancel a pending agent-join request. |
362
+ | `stopRemoteControl()` | Revoke the agent's remote control, keep the session.|
363
+ | `stopFullDeviceSharing()` | Stop full-device sharing, revert to in-app, keep the session. |
364
+ | `respondToFullDeviceRequest(id, accept)` | Answer a `fullDeviceRequest` event. |
238
365
 
239
366
  ### Data
240
367
 
@@ -248,7 +375,7 @@ interface UpscopeConfig {
248
375
 
249
376
  | Method | Description |
250
377
  | ----------------------------------- | --------------------------------------------------- |
251
- | `sendCustomMessage(message)` | Send a string to all connected observers. |
378
+ | `sendCustomMessage(message)` | Send a string to all connected viewers. |
252
379
 
253
380
  ### Events
254
381
 
@@ -32,5 +32,5 @@ android {
32
32
 
33
33
  dependencies {
34
34
  implementation("com.facebook.react:react-android")
35
- implementation("io.github.upscopeio:upscope-android-sdk:2026.4.6")
35
+ implementation("io.github.upscopeio:upscope-android-sdk:2026.5.1")
36
36
  }
@@ -1,19 +1,28 @@
1
1
  package io.upscope.reactnative
2
2
 
3
3
  import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.LifecycleEventListener
4
5
  import com.facebook.react.bridge.Promise
5
6
  import com.facebook.react.bridge.ReactApplicationContext
6
7
  import com.facebook.react.bridge.ReadableMap
8
+ import com.facebook.react.bridge.ReadableType
7
9
  import com.facebook.react.bridge.WritableMap
8
10
  import com.facebook.react.module.annotations.ReactModule
9
11
  import com.facebook.react.modules.core.DeviceEventManagerModule
12
+ import io.upscope.sdk.Cancellable
10
13
  import io.upscope.sdk.ConnectionState
11
- import io.upscope.sdk.Observer
14
+ import io.upscope.sdk.FullDeviceSharingState
15
+ import io.upscope.sdk.Viewer
16
+ import io.upscope.sdk.RemoteControlState
12
17
  import io.upscope.sdk.SessionEndReason
18
+ import io.upscope.sdk.SessionRequestResponse
19
+ import io.upscope.sdk.SessionState
13
20
  import io.upscope.sdk.Upscope
14
21
  import io.upscope.sdk.UpscopeConfiguration
15
22
  import io.upscope.sdk.UpscopeError
16
23
  import io.upscope.sdk.UpscopeListener
24
+ import java.util.UUID
25
+ import java.util.concurrent.ConcurrentHashMap
17
26
  import kotlinx.coroutines.CoroutineScope
18
27
  import kotlinx.coroutines.Dispatchers
19
28
  import kotlinx.coroutines.Job
@@ -33,6 +42,28 @@ class UpscopeModule(
33
42
  private var listenerCount = 0
34
43
  private var shortIdJob: Job? = null
35
44
  private var lookupCodeJob: Job? = null
45
+ private var sessionStateJob: Job? = null
46
+ private var remoteControlStateJob: Job? = null
47
+ private var fullDeviceSharingStateJob: Job? = null
48
+
49
+ /** Pending full-device requests awaiting a JS response, keyed by request id. */
50
+ private val pendingFullDeviceRequests = ConcurrentHashMap<String, SessionRequestResponse>()
51
+
52
+ /**
53
+ * Refreshes the SDK's current activity whenever the host resumes. In-app
54
+ * capture needs a non-null current activity to produce frames; registering
55
+ * once at init can race RN startup or go stale after activity changes
56
+ * (Bug #5 — "Waiting for first frame").
57
+ */
58
+ private val lifecycleListener = object : LifecycleEventListener {
59
+ override fun onHostResume() {
60
+ reactContext.currentActivity?.let { Upscope.setCurrentActivity(it) }
61
+ }
62
+
63
+ override fun onHostPause() {}
64
+
65
+ override fun onHostDestroy() {}
66
+ }
36
67
 
37
68
  private val upscopeListener = object : UpscopeListener {
38
69
  override fun onConnectionStateChanged(state: ConnectionState) {
@@ -62,10 +93,10 @@ class UpscopeModule(
62
93
  sendEvent("sessionEnded", params)
63
94
  }
64
95
 
65
- override fun onCustomMessageReceived(message: String, observerId: String) {
96
+ override fun onCustomMessageReceived(message: String, viewerId: String) {
66
97
  val params = Arguments.createMap().apply {
67
98
  putString("message", message)
68
- putString("observerId", observerId)
99
+ putString("viewerId", viewerId)
69
100
  }
70
101
  sendEvent("customMessageReceived", params)
71
102
  }
@@ -74,23 +105,31 @@ class UpscopeModule(
74
105
  sendEvent("error", serializeError(error))
75
106
  }
76
107
 
77
- override fun onObserverJoined(observer: Observer) {
78
- sendEvent("observerJoined", serializeObserver(observer))
108
+ override fun onViewerJoined(viewer: Viewer) {
109
+ sendEvent("viewerJoined", serializeViewer(viewer))
79
110
  }
80
111
 
81
- override fun onObserverLeft(observerId: String) {
112
+ override fun onViewerLeft(viewerId: String) {
82
113
  val params = Arguments.createMap().apply {
83
- putString("observerId", observerId)
114
+ putString("viewerId", viewerId)
84
115
  }
85
- sendEvent("observerLeft", params)
116
+ sendEvent("viewerLeft", params)
86
117
  }
87
118
 
88
- override fun onObserverCountChanged(count: Int) {
119
+ override fun onViewerCountChanged(count: Int) {
89
120
  val params = Arguments.createMap().apply {
90
121
  putInt("count", count)
91
122
  }
92
- sendEvent("observerCountChanged", params)
123
+ sendEvent("viewerCountChanged", params)
93
124
  }
125
+
126
+ // Remote-control and full-device-sharing state are emitted from their
127
+ // StateFlows in addListener() (current-value, so fresh subscriptions get
128
+ // the real state). The listener overrides would duplicate those events.
129
+ }
130
+
131
+ init {
132
+ reactContext.addLifecycleEventListener(lifecycleListener)
94
133
  }
95
134
 
96
135
  override fun initialize(config: ReadableMap) {
@@ -112,6 +151,12 @@ class UpscopeModule(
112
151
  if (config.hasKey("region")) {
113
152
  builder.region(checkNotNull(config.getString("region")))
114
153
  }
154
+ if (config.hasKey("baseDomain")) {
155
+ builder.baseDomain(checkNotNull(config.getString("baseDomain")))
156
+ }
157
+ if (config.hasKey("disableFullScreenWhenMasked")) {
158
+ builder.disableFullScreenWhenMasked(config.getBoolean("disableFullScreenWhenMasked"))
159
+ }
115
160
  if (config.hasKey("showTerminateButton")) {
116
161
  builder.showTerminateButton(config.getBoolean("showTerminateButton"))
117
162
  }
@@ -128,9 +173,12 @@ class UpscopeModule(
128
173
  if (config.hasKey("endOfSessionMessage")) {
129
174
  builder.endOfSessionMessage(checkNotNull(config.getString("endOfSessionMessage")))
130
175
  }
131
- if (config.hasKey("stopSessionText")) {
132
- builder.stopSessionText(checkNotNull(config.getString("stopSessionText")))
133
- }
176
+ applyLocalized(
177
+ config,
178
+ "stopSessionText",
179
+ setString = { builder.stopSessionText(it) },
180
+ setMap = { builder.stopSessionText(it) },
181
+ )
134
182
  if (config.hasKey("allowRemoteClick")) {
135
183
  builder.allowRemoteClick(config.getBoolean("allowRemoteClick"))
136
184
  }
@@ -155,14 +203,35 @@ class UpscopeModule(
155
203
  if (config.hasKey("lookupCodeKeyMessage")) {
156
204
  builder.lookupCodeKeyMessage(checkNotNull(config.getString("lookupCodeKeyMessage")))
157
205
  }
158
- if (config.hasKey("translationsYes")) {
159
- builder.translationsYes(checkNotNull(config.getString("translationsYes")))
160
- }
161
- if (config.hasKey("translationsNo")) {
162
- builder.translationsNo(checkNotNull(config.getString("translationsNo")))
163
- }
164
- if (config.hasKey("translationsOk")) {
165
- builder.translationsOk(checkNotNull(config.getString("translationsOk")))
206
+ applyLocalized(
207
+ config,
208
+ "translationsYes",
209
+ setString = { builder.translationsYes(it) },
210
+ setMap = { builder.translationsYes(it) },
211
+ )
212
+ applyLocalized(
213
+ config,
214
+ "translationsNo",
215
+ setString = { builder.translationsNo(it) },
216
+ setMap = { builder.translationsNo(it) },
217
+ )
218
+ applyLocalized(
219
+ config,
220
+ "translationsOk",
221
+ setString = { builder.translationsOk(it) },
222
+ setMap = { builder.translationsOk(it) },
223
+ )
224
+
225
+ // Bridge the native full-device request hook to the JS event layer:
226
+ // store the response keyed by a generated id, emit `fullDeviceRequest`,
227
+ // and return a Cancellable that clears the entry if the SDK cancels the
228
+ // request externally. JS answers via respondToFullDeviceRequest(id, accept).
229
+ builder.onFullDeviceRequest { response ->
230
+ val requestId = UUID.randomUUID().toString()
231
+ pendingFullDeviceRequests[requestId] = response
232
+ val params = Arguments.createMap().apply { putString("requestId", requestId) }
233
+ sendEvent("fullDeviceRequest", params)
234
+ Cancellable { pendingFullDeviceRequests.remove(requestId) }
166
235
  }
167
236
 
168
237
  val configuration = builder.build()
@@ -203,6 +272,18 @@ class UpscopeModule(
203
272
 
204
273
  override fun cancelAgentRequest() { Upscope.cancelAgentRequest() }
205
274
 
275
+ override fun stopRemoteControl() { Upscope.stopRemoteControl() }
276
+
277
+ override fun stopFullDeviceSharing() { Upscope.stopFullDeviceSharing() }
278
+
279
+ override fun respondToFullDeviceRequest(requestId: String, accept: Boolean) {
280
+ val response = pendingFullDeviceRequests.remove(requestId)
281
+ if (response == null) {
282
+ return
283
+ }
284
+ if (accept) response.accept() else response.reject()
285
+ }
286
+
206
287
  override fun getLookupCode() { Upscope.getLookupCode() }
207
288
 
208
289
  override fun getShortId(promise: Promise) { promise.resolve(Upscope.getShortId()) }
@@ -213,8 +294,9 @@ class UpscopeModule(
213
294
 
214
295
  /**
215
296
  * Called by RN when the JS side adds an event listener. Starts collecting Flow emissions
216
- * for shortId and lookupCode on the first listener so we only run these coroutines when
217
- * something is actually subscribed.
297
+ * on the first listener so we only run these coroutines when something is actually
298
+ * subscribed. State flows are current-value (StateFlow), so a fresh subscription
299
+ * immediately receives the real current state — the fix for state stuck on remount.
218
300
  */
219
301
  override fun addListener(eventName: String) {
220
302
  listenerCount++
@@ -232,6 +314,30 @@ class UpscopeModule(
232
314
  sendEvent("lookupCodeChanged", p)
233
315
  }
234
316
  }
317
+ sessionStateJob = scope.launch {
318
+ Upscope.sessionStateFlow.collect { state ->
319
+ val p = Arguments.createMap().apply {
320
+ putString("state", serializeSessionState(state))
321
+ }
322
+ sendEvent("sessionStateChanged", p)
323
+ }
324
+ }
325
+ remoteControlStateJob = scope.launch {
326
+ Upscope.remoteControlStateFlow.collect { state ->
327
+ val p = Arguments.createMap().apply {
328
+ putString("state", serializeRemoteControlState(state))
329
+ }
330
+ sendEvent("remoteControlStateChanged", p)
331
+ }
332
+ }
333
+ fullDeviceSharingStateJob = scope.launch {
334
+ Upscope.fullDeviceSharingStateFlow.collect { state ->
335
+ val p = Arguments.createMap().apply {
336
+ putString("state", serializeFullDeviceSharingState(state))
337
+ }
338
+ sendEvent("fullDeviceSharingStateChanged", p)
339
+ }
340
+ }
235
341
  }
236
342
  }
237
343
 
@@ -245,8 +351,45 @@ class UpscopeModule(
245
351
  listenerCount = 0
246
352
  shortIdJob?.cancel()
247
353
  lookupCodeJob?.cancel()
354
+ sessionStateJob?.cancel()
355
+ remoteControlStateJob?.cancel()
356
+ fullDeviceSharingStateJob?.cancel()
248
357
  shortIdJob = null
249
358
  lookupCodeJob = null
359
+ sessionStateJob = null
360
+ remoteControlStateJob = null
361
+ fullDeviceSharingStateJob = null
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Applies a translation field that may arrive from JS as either a plain string or a
367
+ * language-keyed map. The native SDK exposes a [String] and a [Map] builder overload
368
+ * for each such field; we dispatch to whichever matches the runtime value and silently
369
+ * skip any other type so the server-provided default remains in effect.
370
+ */
371
+ private fun applyLocalized(
372
+ config: ReadableMap,
373
+ key: String,
374
+ setString: (String) -> Unit,
375
+ setMap: (Map<String, String>) -> Unit,
376
+ ) {
377
+ if (!config.hasKey(key)) return
378
+ when (config.getType(key)) {
379
+ ReadableType.String -> config.getString(key)?.let(setString)
380
+ ReadableType.Map -> {
381
+ val map = config.getMap(key) ?: return
382
+ val entries = mutableMapOf<String, String>()
383
+ val iter = map.keySetIterator()
384
+ while (iter.hasNextKey()) {
385
+ val entryKey = iter.nextKey()
386
+ if (map.getType(entryKey) == ReadableType.String) {
387
+ map.getString(entryKey)?.let { entries[entryKey] = it }
388
+ }
389
+ }
390
+ setMap(entries)
391
+ }
392
+ else -> Unit
250
393
  }
251
394
  }
252
395
 
@@ -265,6 +408,24 @@ class UpscopeModule(
265
408
  is ConnectionState.Error -> "error"
266
409
  }
267
410
 
411
+ private fun serializeSessionState(state: SessionState): String = when (state) {
412
+ SessionState.INACTIVE -> "inactive"
413
+ SessionState.PENDING_REQUEST -> "pendingRequest"
414
+ SessionState.ACTIVE -> "active"
415
+ SessionState.PAUSED -> "paused"
416
+ SessionState.ENDED -> "ended"
417
+ }
418
+
419
+ private fun serializeRemoteControlState(state: RemoteControlState): String = when (state) {
420
+ RemoteControlState.INACTIVE -> "inactive"
421
+ RemoteControlState.ACTIVE -> "active"
422
+ }
423
+
424
+ private fun serializeFullDeviceSharingState(state: FullDeviceSharingState): String = when (state) {
425
+ FullDeviceSharingState.INACTIVE -> "inactive"
426
+ FullDeviceSharingState.ACTIVE -> "active"
427
+ }
428
+
268
429
  private fun serializeEndReason(reason: SessionEndReason): String = when (reason) {
269
430
  is SessionEndReason.UserStopped -> "userStopped"
270
431
  is SessionEndReason.AgentStopped -> "agentStopped"
@@ -277,13 +438,14 @@ class UpscopeModule(
277
438
  putString("message", error.message)
278
439
  }
279
440
 
280
- private fun serializeObserver(observer: Observer): WritableMap = Arguments.createMap().apply {
281
- putString("id", observer.id)
282
- putString("name", observer.name)
283
- putDouble("screenWidth", observer.screenWidth)
284
- putDouble("screenHeight", observer.screenHeight)
285
- putDouble("windowWidth", observer.windowWidth)
286
- putDouble("windowHeight", observer.windowHeight)
287
- putBoolean("hasFocus", observer.hasFocus)
441
+ private fun serializeViewer(viewer: Viewer): WritableMap = Arguments.createMap().apply {
442
+ putString("id", viewer.id)
443
+ putString("name", viewer.name)
444
+ putDouble("screenWidth", viewer.screenWidth)
445
+ putDouble("screenHeight", viewer.screenHeight)
446
+ putDouble("screenScale", viewer.screenScale)
447
+ putDouble("windowWidth", viewer.windowWidth)
448
+ putDouble("windowHeight", viewer.windowHeight)
449
+ putBoolean("hasFocus", viewer.hasFocus)
288
450
  }
289
451
  }
@@ -13,6 +13,9 @@ RCT_EXTERN_METHOD(updateConnection:(NSDictionary *)params)
13
13
  RCT_EXTERN_METHOD(stopSession)
14
14
  RCT_EXTERN_METHOD(requestAgent)
15
15
  RCT_EXTERN_METHOD(cancelAgentRequest)
16
+ RCT_EXTERN_METHOD(stopRemoteControl)
17
+ RCT_EXTERN_METHOD(stopFullDeviceSharing)
18
+ RCT_EXTERN_METHOD(respondToFullDeviceRequest:(NSString *)requestId accept:(BOOL)accept)
16
19
  RCT_EXTERN_METHOD(getLookupCode)
17
20
  RCT_EXTERN_METHOD(getShortId:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
18
21
  RCT_EXTERN_METHOD(getWatchLink:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)