@upscopeio/react-native-sdk 2026.3.0 → 2026.4.2
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 +137 -4
- package/android/build.gradle.kts +1 -1
- package/android/src/main/kotlin/io/upscope/reactnative/UpscopeModule.kt +209 -55
- package/ios/UpscopeModule.mm +3 -0
- package/ios/UpscopeModule.swift +207 -45
- package/lib/commonjs/NativeUpscopeModule.js.map +1 -1
- package/lib/commonjs/Upscope.js +59 -18
- package/lib/commonjs/Upscope.js.map +1 -1
- package/lib/commonjs/hooks.js +45 -2
- package/lib/commonjs/hooks.js.map +1 -1
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/version.js +9 -0
- package/lib/commonjs/version.js.map +1 -0
- package/lib/module/NativeUpscopeModule.js.map +1 -1
- package/lib/module/Upscope.js +60 -18
- package/lib/module/Upscope.js.map +1 -1
- package/lib/module/hooks.js +43 -2
- package/lib/module/hooks.js.map +1 -1
- package/lib/module/index.js +4 -4
- package/lib/module/index.js.map +1 -1
- package/lib/module/version.js +5 -0
- package/lib/module/version.js.map +1 -0
- package/lib/typescript/NativeUpscopeModule.d.ts +3 -0
- package/lib/typescript/NativeUpscopeModule.d.ts.map +1 -1
- package/lib/typescript/Upscope.d.ts +20 -2
- package/lib/typescript/Upscope.d.ts.map +1 -1
- package/lib/typescript/hooks.d.ts +20 -3
- package/lib/typescript/hooks.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +7 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +70 -14
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +2 -0
- package/lib/typescript/version.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/NativeUpscopeModule.ts +5 -0
- package/src/Upscope.ts +66 -19
- package/src/hooks.ts +56 -3
- package/src/index.ts +19 -10
- package/src/types.ts +84 -27
- package/src/version.ts +2 -0
- package/upscopeio-react-native-sdk.podspec +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,9 @@ npm install @upscopeio/react-native-sdk
|
|
|
28
28
|
cd ios && pod install
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
By default the SDK shares only your app's UI. To let agents view the visitor's
|
|
32
|
+
**entire device**, see [Full device sharing (iOS)](#full-device-sharing-ios).
|
|
33
|
+
|
|
31
34
|
### Android
|
|
32
35
|
|
|
33
36
|
No extra steps — the native module and Upscope Android SDK are linked automatically via autolinking.
|
|
@@ -52,6 +55,7 @@ Upscope.updateConnection({
|
|
|
52
55
|
callName: 'Jane Doe',
|
|
53
56
|
tags: ['support'],
|
|
54
57
|
identities: ['jane@example.com'],
|
|
58
|
+
integrationIds: ['intercom:user123'],
|
|
55
59
|
metadata: { plan: 'premium' },
|
|
56
60
|
});
|
|
57
61
|
```
|
|
@@ -117,6 +121,34 @@ function LookupCode() {
|
|
|
117
121
|
}
|
|
118
122
|
```
|
|
119
123
|
|
|
124
|
+
### `useRemoteControlState`
|
|
125
|
+
|
|
126
|
+
Returns whether an agent currently has remote control of the device.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { useRemoteControlState } from '@upscopeio/react-native-sdk';
|
|
130
|
+
|
|
131
|
+
function ControlBadge() {
|
|
132
|
+
const state = useRemoteControlState();
|
|
133
|
+
// state: 'inactive' | 'active'
|
|
134
|
+
return <Text>Remote control: {state}</Text>;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `useFullDeviceSharingState`
|
|
139
|
+
|
|
140
|
+
Returns whether full-device (broadcast) sharing is currently running.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { useFullDeviceSharingState } from '@upscopeio/react-native-sdk';
|
|
144
|
+
|
|
145
|
+
function SharingBadge() {
|
|
146
|
+
const state = useFullDeviceSharingState();
|
|
147
|
+
// state: 'inactive' | 'active'
|
|
148
|
+
return <Text>Full-device sharing: {state}</Text>;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
120
152
|
### `useUpscope`
|
|
121
153
|
|
|
122
154
|
Returns a stable object of imperative action methods — safe to destructure and pass as deps.
|
|
@@ -157,6 +189,7 @@ useEffect(() => {
|
|
|
157
189
|
| Event name | Payload type | Description |
|
|
158
190
|
| ------------------------- | ----------------------------- | ---------------------------------------- |
|
|
159
191
|
| `connectionStateChanged` | `ConnectionStateChangedEvent` | Connection state transitioned |
|
|
192
|
+
| `sessionStateChanged` | `SessionStateChangedEvent` | Session state transitioned (current-value)|
|
|
160
193
|
| `sessionStarted` | `SessionStartedEvent` | Agent joined and session is active |
|
|
161
194
|
| `sessionEnded` | `SessionEndedEvent` | Session ended (includes reason) |
|
|
162
195
|
| `customMessageReceived` | `CustomMessageEvent` | Arbitrary string from an observer |
|
|
@@ -166,6 +199,9 @@ useEffect(() => {
|
|
|
166
199
|
| `observerCountChanged` | `ObserverCountChangedEvent` | Total observer count changed |
|
|
167
200
|
| `shortIdChanged` | `ShortIdChangedEvent` | Short ID assigned or rotated |
|
|
168
201
|
| `lookupCodeChanged` | `LookupCodeChangedEvent` | Lookup code issued or changed |
|
|
202
|
+
| `remoteControlStateChanged` | `RemoteControlStateChangedEvent` | Agent gained or lost remote control |
|
|
203
|
+
| `fullDeviceSharingStateChanged` | `FullDeviceSharingStateChangedEvent` | Full-device sharing started or stopped |
|
|
204
|
+
| `fullDeviceRequest` | `FullDeviceRequestEvent` | Agent requested full-device sharing — answer with `respondToFullDeviceRequest` |
|
|
169
205
|
|
|
170
206
|
## View Masking
|
|
171
207
|
|
|
@@ -198,18 +234,112 @@ interface UpscopeConfig {
|
|
|
198
234
|
requireAuthorizationForSession?: boolean; // Show a prompt before allowing a session. Default: true.
|
|
199
235
|
autoConnect?: boolean; // Connect on initialize(). Default: false.
|
|
200
236
|
region?: string; // Override the server region.
|
|
201
|
-
|
|
202
|
-
showTerminateButton?: boolean; // Show an end-session button in the banner.
|
|
237
|
+
showTerminateButton?: boolean; // Show an end-session button during a session.
|
|
203
238
|
authorizationPromptTitle?: string; // Custom title for the session request dialog.
|
|
204
239
|
authorizationPromptMessage?: string; // Custom message for the session request dialog.
|
|
205
240
|
endOfSessionMessage?: string; // Message shown when a session ends.
|
|
206
|
-
stopSessionText?:
|
|
241
|
+
stopSessionText?: LocalizedString; // Label for the terminate-session button.
|
|
207
242
|
allowRemoteClick?: boolean; // Allow agent to send tap events. Default: true.
|
|
208
243
|
allowRemoteScroll?: boolean; // Allow agent to send scroll events. Default: true.
|
|
209
244
|
requireControlRequest?: boolean; // Require visitor approval before remote control starts.
|
|
245
|
+
broadcastAppGroupId?: string; // iOS only. App Group shared with the Broadcast Upload Extension (enables full-device sharing).
|
|
246
|
+
broadcastExtensionBundleId?: string; // iOS only. Bundle id of the Broadcast Upload Extension.
|
|
210
247
|
}
|
|
248
|
+
|
|
249
|
+
// Translation fields accept either a plain string or a language-keyed map.
|
|
250
|
+
// Keys are BCP-47 language tags; the native SDK picks the best match for the
|
|
251
|
+
// device's preferred languages.
|
|
252
|
+
type LocalizedString = string | { [language: string]: string };
|
|
211
253
|
```
|
|
212
254
|
|
|
255
|
+
Example:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
Upscope.initialize({
|
|
259
|
+
apiKey: "…",
|
|
260
|
+
stopSessionText: { en: "Stop", it: "Ferma", "pt-PT": "Parar" },
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Full device sharing (iOS)
|
|
265
|
+
|
|
266
|
+
Full device sharing lets the agent see the entire device screen (other apps,
|
|
267
|
+
home screen) via ReplayKit. It requires a Broadcast Upload Extension target in
|
|
268
|
+
your app — npm packages can't add app extension targets, so this is a one-time
|
|
269
|
+
manual setup (~10 minutes). Without it, the agent's "Full app sharing" button
|
|
270
|
+
is hidden.
|
|
271
|
+
|
|
272
|
+
> Android needs no setup: it uses MediaProjection, and the required
|
|
273
|
+
> foreground-service permissions are merged automatically from the SDK's manifest.
|
|
274
|
+
|
|
275
|
+
### 1. Add the extension target
|
|
276
|
+
|
|
277
|
+
In Xcode: **File → New → Target → Broadcast Upload Extension**. Name it (e.g.
|
|
278
|
+
`YourAppBroadcast`), uncheck "Include UI Extension".
|
|
279
|
+
|
|
280
|
+
### 2. Replace the generated SampleHandler
|
|
281
|
+
|
|
282
|
+
```swift
|
|
283
|
+
import UpscopeSDK
|
|
284
|
+
|
|
285
|
+
class SampleHandler: UpscopeSampleHandler {}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### 3. Add an App Group to BOTH targets
|
|
289
|
+
|
|
290
|
+
In **Signing & Capabilities** for your app target *and* the extension target,
|
|
291
|
+
add the **App Groups** capability with the same group, e.g.
|
|
292
|
+
`group.com.yourcompany.yourapp`.
|
|
293
|
+
|
|
294
|
+
### 4. Declare the group in the extension's Info.plist
|
|
295
|
+
|
|
296
|
+
```xml
|
|
297
|
+
<key>UpscopeAppGroupId</key>
|
|
298
|
+
<string>group.com.yourcompany.yourapp</string>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Also make sure `NSExtension > RPBroadcastProcessMode` is `2` (sample-buffer
|
|
302
|
+
mode); some Xcode template versions omit it. See the example app's
|
|
303
|
+
[`Info.plist`](example/ios/UpscopeExampleBroadcast/Info.plist).
|
|
304
|
+
|
|
305
|
+
### 5. Add the extension pod
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
# Podfile (top level, alongside your app target)
|
|
309
|
+
target 'YourAppBroadcast' do
|
|
310
|
+
pod 'UpscopeSDK/BroadcastExtension'
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Run `pod install`.
|
|
315
|
+
|
|
316
|
+
### 6. Pass the keys at initialization
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
Upscope.initialize({
|
|
320
|
+
apiKey: 'your-api-key',
|
|
321
|
+
broadcastAppGroupId: 'group.com.yourcompany.yourapp',
|
|
322
|
+
broadcastExtensionBundleId: 'com.yourcompany.yourapp.YourAppBroadcast',
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`broadcastAppGroupId` enables the feature (the agent's button appears);
|
|
327
|
+
`broadcastExtensionBundleId` preselects your extension in the iOS picker.
|
|
328
|
+
|
|
329
|
+
### Notes
|
|
330
|
+
|
|
331
|
+
- The App Group ID must match in **three** places: both targets' entitlements,
|
|
332
|
+
the extension's `Info.plist` (`UpscopeAppGroupId`), and `broadcastAppGroupId`.
|
|
333
|
+
- Test on a **physical device** — ReplayKit broadcasts are unreliable on the
|
|
334
|
+
simulator.
|
|
335
|
+
- Full-device capture cannot mask views. Set
|
|
336
|
+
`disableFullScreenWhenMasked: true` to automatically end full-device sharing
|
|
337
|
+
whenever masked content is on screen.
|
|
338
|
+
- Expo managed workflow is not supported (extension targets require a config
|
|
339
|
+
plugin; not currently provided).
|
|
340
|
+
- A complete working setup is in
|
|
341
|
+
[`example/ios`](example/ios) (`UpscopeExampleBroadcast` target).
|
|
342
|
+
|
|
213
343
|
## API Reference
|
|
214
344
|
|
|
215
345
|
### Initialization
|
|
@@ -225,7 +355,7 @@ interface UpscopeConfig {
|
|
|
225
355
|
|
|
226
356
|
| Method | Description |
|
|
227
357
|
| ----------------------------------- | --------------------------------------------------- |
|
|
228
|
-
| `updateConnection(params)` | Set/update `uniqueId`, `callName`, `tags`, `identities`, `metadata`. |
|
|
358
|
+
| `updateConnection(params)` | Set/update `uniqueId`, `callName`, `tags`, `identities`, `integrationIds`, `metadata`. |
|
|
229
359
|
|
|
230
360
|
### Session
|
|
231
361
|
|
|
@@ -234,6 +364,9 @@ interface UpscopeConfig {
|
|
|
234
364
|
| `stopSession()` | Terminate the active session. |
|
|
235
365
|
| `requestAgent()` | Request that an agent join. |
|
|
236
366
|
| `cancelAgentRequest()` | Cancel a pending agent-join request. |
|
|
367
|
+
| `stopRemoteControl()` | Revoke the agent's remote control, keep the session.|
|
|
368
|
+
| `stopFullDeviceSharing()` | Stop full-device sharing, revert to in-app, keep the session. |
|
|
369
|
+
| `respondToFullDeviceRequest(id, accept)` | Answer a `fullDeviceRequest` event. |
|
|
237
370
|
|
|
238
371
|
### Data
|
|
239
372
|
|
package/android/build.gradle.kts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
package io.upscope.reactnative
|
|
2
2
|
|
|
3
|
-
import android.app.AlertDialog
|
|
4
3
|
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
5
5
|
import com.facebook.react.bridge.Promise
|
|
6
6
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
7
|
import com.facebook.react.bridge.ReadableMap
|
|
8
|
+
import com.facebook.react.bridge.ReadableType
|
|
8
9
|
import com.facebook.react.bridge.WritableMap
|
|
9
10
|
import com.facebook.react.module.annotations.ReactModule
|
|
10
11
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
11
12
|
import io.upscope.sdk.Cancellable
|
|
12
13
|
import io.upscope.sdk.ConnectionState
|
|
13
|
-
import io.upscope.sdk.
|
|
14
|
+
import io.upscope.sdk.FullDeviceSharingState
|
|
15
|
+
import io.upscope.sdk.Viewer
|
|
16
|
+
import io.upscope.sdk.RemoteControlState
|
|
14
17
|
import io.upscope.sdk.SessionEndReason
|
|
15
18
|
import io.upscope.sdk.SessionRequestResponse
|
|
19
|
+
import io.upscope.sdk.SessionState
|
|
16
20
|
import io.upscope.sdk.Upscope
|
|
17
21
|
import io.upscope.sdk.UpscopeConfiguration
|
|
18
22
|
import io.upscope.sdk.UpscopeError
|
|
19
23
|
import io.upscope.sdk.UpscopeListener
|
|
24
|
+
import java.util.UUID
|
|
25
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
20
26
|
import kotlinx.coroutines.CoroutineScope
|
|
21
27
|
import kotlinx.coroutines.Dispatchers
|
|
22
28
|
import kotlinx.coroutines.Job
|
|
@@ -36,6 +42,28 @@ class UpscopeModule(
|
|
|
36
42
|
private var listenerCount = 0
|
|
37
43
|
private var shortIdJob: Job? = null
|
|
38
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
|
+
}
|
|
39
67
|
|
|
40
68
|
private val upscopeListener = object : UpscopeListener {
|
|
41
69
|
override fun onConnectionStateChanged(state: ConnectionState) {
|
|
@@ -65,10 +93,10 @@ class UpscopeModule(
|
|
|
65
93
|
sendEvent("sessionEnded", params)
|
|
66
94
|
}
|
|
67
95
|
|
|
68
|
-
override fun onCustomMessageReceived(message: String,
|
|
96
|
+
override fun onCustomMessageReceived(message: String, viewerId: String) {
|
|
69
97
|
val params = Arguments.createMap().apply {
|
|
70
98
|
putString("message", message)
|
|
71
|
-
putString("
|
|
99
|
+
putString("viewerId", viewerId)
|
|
72
100
|
}
|
|
73
101
|
sendEvent("customMessageReceived", params)
|
|
74
102
|
}
|
|
@@ -77,23 +105,31 @@ class UpscopeModule(
|
|
|
77
105
|
sendEvent("error", serializeError(error))
|
|
78
106
|
}
|
|
79
107
|
|
|
80
|
-
override fun
|
|
81
|
-
sendEvent("
|
|
108
|
+
override fun onViewerJoined(viewer: Viewer) {
|
|
109
|
+
sendEvent("viewerJoined", serializeViewer(viewer))
|
|
82
110
|
}
|
|
83
111
|
|
|
84
|
-
override fun
|
|
112
|
+
override fun onViewerLeft(viewerId: String) {
|
|
85
113
|
val params = Arguments.createMap().apply {
|
|
86
|
-
putString("
|
|
114
|
+
putString("viewerId", viewerId)
|
|
87
115
|
}
|
|
88
|
-
sendEvent("
|
|
116
|
+
sendEvent("viewerLeft", params)
|
|
89
117
|
}
|
|
90
118
|
|
|
91
|
-
override fun
|
|
119
|
+
override fun onViewerCountChanged(count: Int) {
|
|
92
120
|
val params = Arguments.createMap().apply {
|
|
93
121
|
putInt("count", count)
|
|
94
122
|
}
|
|
95
|
-
sendEvent("
|
|
123
|
+
sendEvent("viewerCountChanged", params)
|
|
96
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)
|
|
97
133
|
}
|
|
98
134
|
|
|
99
135
|
override fun initialize(config: ReadableMap) {
|
|
@@ -103,6 +139,9 @@ class UpscopeModule(
|
|
|
103
139
|
val builder = UpscopeConfiguration.Builder(apiKey)
|
|
104
140
|
.wrapper("react-native")
|
|
105
141
|
|
|
142
|
+
if (config.hasKey("wrapperVersion")) {
|
|
143
|
+
builder.wrapperVersion(checkNotNull(config.getString("wrapperVersion")))
|
|
144
|
+
}
|
|
106
145
|
if (config.hasKey("requireAuthorizationForSession")) {
|
|
107
146
|
builder.requireAuthorizationForSession(config.getBoolean("requireAuthorizationForSession"))
|
|
108
147
|
}
|
|
@@ -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
|
-
|
|
132
|
-
|
|
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
|
}
|
|
@@ -140,38 +188,50 @@ class UpscopeModule(
|
|
|
140
188
|
if (config.hasKey("requireControlRequest")) {
|
|
141
189
|
builder.requireControlRequest(config.getBoolean("requireControlRequest"))
|
|
142
190
|
}
|
|
191
|
+
if (config.hasKey("controlRequestTitle")) {
|
|
192
|
+
builder.controlRequestTitle(checkNotNull(config.getString("controlRequestTitle")))
|
|
193
|
+
}
|
|
194
|
+
if (config.hasKey("controlRequestMessage")) {
|
|
195
|
+
builder.controlRequestMessage(checkNotNull(config.getString("controlRequestMessage")))
|
|
196
|
+
}
|
|
197
|
+
if (config.hasKey("enableLookupCodeOnShake")) {
|
|
198
|
+
builder.enableLookupCodeOnShake(config.getBoolean("enableLookupCodeOnShake"))
|
|
199
|
+
}
|
|
200
|
+
if (config.hasKey("lookupCodeKeyTitle")) {
|
|
201
|
+
builder.lookupCodeKeyTitle(checkNotNull(config.getString("lookupCodeKeyTitle")))
|
|
202
|
+
}
|
|
203
|
+
if (config.hasKey("lookupCodeKeyMessage")) {
|
|
204
|
+
builder.lookupCodeKeyMessage(checkNotNull(config.getString("lookupCodeKeyMessage")))
|
|
205
|
+
}
|
|
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
|
+
)
|
|
143
224
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
val
|
|
152
|
-
|
|
153
|
-
|
|
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() }
|
|
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) }
|
|
175
235
|
}
|
|
176
236
|
|
|
177
237
|
val configuration = builder.build()
|
|
@@ -194,6 +254,7 @@ class UpscopeModule(
|
|
|
194
254
|
callName = if (params.hasKey("callName")) params.getString("callName") else null,
|
|
195
255
|
tags = if (params.hasKey("tags")) params.getArray("tags")?.toArrayList()?.filterIsInstance<String>() else null,
|
|
196
256
|
identities = if (params.hasKey("identities")) params.getArray("identities")?.toArrayList()?.filterIsInstance<String>() else null,
|
|
257
|
+
integrationIds = if (params.hasKey("integrationIds")) params.getArray("integrationIds")?.toArrayList()?.filterIsInstance<String>() else null,
|
|
197
258
|
metadata = if (params.hasKey("metadata")) {
|
|
198
259
|
val map = params.getMap("metadata")
|
|
199
260
|
val result = mutableMapOf<String, String>()
|
|
@@ -211,6 +272,18 @@ class UpscopeModule(
|
|
|
211
272
|
|
|
212
273
|
override fun cancelAgentRequest() { Upscope.cancelAgentRequest() }
|
|
213
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
|
+
|
|
214
287
|
override fun getLookupCode() { Upscope.getLookupCode() }
|
|
215
288
|
|
|
216
289
|
override fun getShortId(promise: Promise) { promise.resolve(Upscope.getShortId()) }
|
|
@@ -221,8 +294,9 @@ class UpscopeModule(
|
|
|
221
294
|
|
|
222
295
|
/**
|
|
223
296
|
* Called by RN when the JS side adds an event listener. Starts collecting Flow emissions
|
|
224
|
-
*
|
|
225
|
-
*
|
|
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.
|
|
226
300
|
*/
|
|
227
301
|
override fun addListener(eventName: String) {
|
|
228
302
|
listenerCount++
|
|
@@ -240,6 +314,30 @@ class UpscopeModule(
|
|
|
240
314
|
sendEvent("lookupCodeChanged", p)
|
|
241
315
|
}
|
|
242
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
|
+
}
|
|
243
341
|
}
|
|
244
342
|
}
|
|
245
343
|
|
|
@@ -253,8 +351,45 @@ class UpscopeModule(
|
|
|
253
351
|
listenerCount = 0
|
|
254
352
|
shortIdJob?.cancel()
|
|
255
353
|
lookupCodeJob?.cancel()
|
|
354
|
+
sessionStateJob?.cancel()
|
|
355
|
+
remoteControlStateJob?.cancel()
|
|
356
|
+
fullDeviceSharingStateJob?.cancel()
|
|
256
357
|
shortIdJob = null
|
|
257
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
|
|
258
393
|
}
|
|
259
394
|
}
|
|
260
395
|
|
|
@@ -273,6 +408,24 @@ class UpscopeModule(
|
|
|
273
408
|
is ConnectionState.Error -> "error"
|
|
274
409
|
}
|
|
275
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
|
+
|
|
276
429
|
private fun serializeEndReason(reason: SessionEndReason): String = when (reason) {
|
|
277
430
|
is SessionEndReason.UserStopped -> "userStopped"
|
|
278
431
|
is SessionEndReason.AgentStopped -> "agentStopped"
|
|
@@ -285,13 +438,14 @@ class UpscopeModule(
|
|
|
285
438
|
putString("message", error.message)
|
|
286
439
|
}
|
|
287
440
|
|
|
288
|
-
private fun
|
|
289
|
-
putString("id",
|
|
290
|
-
putString("name",
|
|
291
|
-
putDouble("screenWidth",
|
|
292
|
-
putDouble("screenHeight",
|
|
293
|
-
putDouble("
|
|
294
|
-
putDouble("
|
|
295
|
-
|
|
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)
|
|
296
450
|
}
|
|
297
451
|
}
|
package/ios/UpscopeModule.mm
CHANGED
|
@@ -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)
|