@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.
- package/README.md +291 -0
- package/android/build.gradle.kts +36 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/kotlin/io/upscope/reactnative/UpscopeMaskedViewManager.kt +31 -0
- package/android/src/main/kotlin/io/upscope/reactnative/UpscopeModule.kt +297 -0
- package/android/src/main/kotlin/io/upscope/reactnative/UpscopePackage.kt +31 -0
- package/ios/UpscopeMaskedView.swift +18 -0
- package/ios/UpscopeMaskedViewManager.mm +4 -0
- package/ios/UpscopeMaskedViewManager.swift +13 -0
- package/ios/UpscopeModule.mm +31 -0
- package/ios/UpscopeModule.swift +243 -0
- package/lib/commonjs/NativeUpscopeMaskedView.js +10 -0
- package/lib/commonjs/NativeUpscopeMaskedView.js.map +1 -0
- package/lib/commonjs/NativeUpscopeModule.js +9 -0
- package/lib/commonjs/NativeUpscopeModule.js.map +1 -0
- package/lib/commonjs/Upscope.js +95 -0
- package/lib/commonjs/Upscope.js.map +1 -0
- package/lib/commonjs/UpscopeMasked.js +22 -0
- package/lib/commonjs/UpscopeMasked.js.map +1 -0
- package/lib/commonjs/hooks.js +98 -0
- package/lib/commonjs/hooks.js.map +1 -0
- package/lib/commonjs/index.js +58 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/NativeUpscopeMaskedView.js +5 -0
- package/lib/module/NativeUpscopeMaskedView.js.map +1 -0
- package/lib/module/NativeUpscopeModule.js +5 -0
- package/lib/module/NativeUpscopeModule.js.map +1 -0
- package/lib/module/Upscope.js +91 -0
- package/lib/module/Upscope.js.map +1 -0
- package/lib/module/UpscopeMasked.js +17 -0
- package/lib/module/UpscopeMasked.js.map +1 -0
- package/lib/module/hooks.js +89 -0
- package/lib/module/hooks.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/NativeUpscopeMaskedView.d.ts +7 -0
- package/lib/typescript/NativeUpscopeMaskedView.d.ts.map +1 -0
- package/lib/typescript/NativeUpscopeModule.d.ts +20 -0
- package/lib/typescript/NativeUpscopeModule.d.ts.map +1 -0
- package/lib/typescript/Upscope.d.ts +60 -0
- package/lib/typescript/Upscope.d.ts.map +1 -0
- package/lib/typescript/UpscopeMasked.d.ts +7 -0
- package/lib/typescript/UpscopeMasked.d.ts.map +1 -0
- package/lib/typescript/hooks.d.ts +36 -0
- package/lib/typescript/hooks.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +8 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +79 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/package.json +94 -0
- package/src/NativeUpscopeMaskedView.ts +9 -0
- package/src/NativeUpscopeModule.ts +32 -0
- package/src/Upscope.ts +113 -0
- package/src/UpscopeMasked.tsx +19 -0
- package/src/hooks.ts +99 -0
- package/src/index.ts +34 -0
- package/src/types.ts +109 -0
- 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,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
|
+
}
|