Upscope Flutter SDK
Flutter plugin for Upscope cobrowsing — lets support agents view and interact with your users' screens in real-time, with no extra permissions required.
Requirements
- Flutter 3.19+
- Dart SDK 3.3+
- iOS 14.0+
- Android API 26+ (Android 8.0)
Installation
flutter pub add upscopeio_flutter_sdk
Or add it manually to your pubspec.yaml:
dependencies:
upscopeio_flutter_sdk: ^2026.8.3
Then run:
flutter pub get
Quick Start
1. Initialize
Call initialize once, early in your app lifecycle (e.g. in main or your root widget's initState):
import 'package:upscopeio_flutter_sdk/upscopeio_flutter_sdk.dart';
await Upscope.instance.initialize(
UpscopeConfiguration(apiKey: 'your_api_key'),
);
By default autoConnect: true, so the SDK connects immediately after initialization.
2. Identify the visitor
await Upscope.instance.updateConnection(
uniqueId: 'user-123',
callName: 'Jane Smith',
tags: ['premium', 'mobile'],
);
Call this any time visitor identity changes (e.g. after sign-in).
3. Show a lookup code to the user
The lookup code lets a support agent start a session by entering a short code in the Upscope dashboard:
Upscope.instance.lookupCode.listen((code) {
if (code != null) {
print('Share this code with support: $code');
}
});
That's all. The agent can now initiate a cobrowsing session from the dashboard.
Core Concepts
Connection vs. Session
- Connection — persistent WebSocket link to Upscope servers. Established via
connect(), torn down viadisconnect(). Required before any session can start. - Session — an active cobrowsing window with an agent. Begins when an agent joins (or when
requestAgent()is called), ends when either party stops it.
Reactive streams
All state is exposed as Streams so you can drive your UI reactively without polling.
// Show connection status in a StreamBuilder
StreamBuilder<ConnectionState>(
stream: Upscope.instance.connectionState,
builder: (context, snapshot) {
return Text(switch (snapshot.data) {
ConnectionState.connected => 'Ready',
ConnectionState.connecting => 'Connecting…',
ConnectionState.reconnecting => 'Reconnecting…',
ConnectionState.error => 'Error',
_ => 'Offline',
});
},
);
Masking sensitive widgets
Wrap any widget in UpscopeMasked to hide it from the agent's view. The region is replaced with a black rectangle in the screen capture; your users see the real content as normal.
UpscopeMasked(
child: CreditCardField(),
)
UpscopeMasked tracks position automatically — it handles layout changes, device rotation, and scroll without any extra wiring.
Common Tasks
Listen for session lifecycle events
Upscope.instance.onSessionStarted.listen((_) {
showBanner('Screen sharing started');
});
Upscope.instance.onSessionEnded.listen((reason) {
showBanner('Session ended: $reason');
});
React to viewer changes
Upscope.instance.onViewerJoined.listen((viewer) {
print('Agent joined: ${viewer.name}');
});
Upscope.instance.onViewerCountChanged.listen((count) {
print('Viewers watching: $count');
});
Remote control and full-device sharing
Observe and revoke agent device control, and respond to full-device sharing requests:
Upscope.instance.remoteControlState.listen((state) {
print('Remote control: ${state.name}'); // inactive | pendingRequest | active
});
Upscope.instance.fullDeviceSharingState.listen((state) {
print('Full-device sharing: ${state.name}'); // inactive | pendingRequest | active
});
// Stop an in-progress agent interaction (session stays active)
await Upscope.instance.stopRemoteControl();
// Stop full-device screen sharing
await Upscope.instance.stopFullDeviceSharing();
// Approve or decline a full-device sharing request
Upscope.instance.onFullDeviceRequest.listen((request) async {
final allow = await askUser(request.agentName);
await Upscope.instance
.respondToFullDeviceRequest(request.requestId, accept: allow);
});
// Approve or decline a remote-control request
Upscope.instance.onControlRequest.listen((request) async {
final allow = await askUser(request.agentName);
await Upscope.instance
.respondToControlRequest(request.requestId, accept: allow);
});
The pendingRequest state lets you build a fully custom accept/decline UI
driven purely off the state stream: render your prompt while the state is
pendingRequest, then respond via respondToControlRequest /
respondToFullDeviceRequest. The onControlRequest / onFullDeviceRequest
events each carry the requesting agentName (nullable) alongside the
requestId.
Full-device sharing on iOS requires a Broadcast Upload Extension target — see Full device sharing (iOS).
Request an agent
Proactively ask for a support agent without waiting for them to initiate:
await Upscope.instance.requestAgent();
// Cancel if the user changes their mind
await Upscope.instance.cancelAgentRequest();
Stop a session
await Upscope.instance.stopSession();
Send a custom message to the agent
await Upscope.instance.sendCustomMessage('{"event": "checkout_started"}');
Receive messages from the agent:
Upscope.instance.onCustomMessageReceived.listen((msg) {
print('From ${msg.viewerId}: ${msg.message}');
});
Reset the connection
Clears stored identity and reconnects (useful on sign-out):
// Reset and reconnect
await Upscope.instance.reset();
// Reset without reconnecting
await Upscope.instance.reset(reconnect: false);
Handle errors
Upscope.instance.onError.listen((error) {
print('Upscope error [${error.code}]: ${error.message}');
});
Configuration Reference
All options are set once in UpscopeConfiguration passed to initialize:
| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
String |
required | Your Upscope API key |
autoConnect |
bool |
true |
Connect automatically on initialize |
requireAuthorizationForSession |
bool |
true |
Prompt user before allowing agent to view screen |
authorizationPromptTitle |
String |
'Screen Sharing Request' |
Title of the consent dialog |
authorizationPromptMessage |
String |
'An agent wants to view your screen. Do you accept?' |
Body of the consent dialog |
showTerminateButton |
bool |
true |
Show a stop-sharing button in the session banner |
showUpscopeLink |
bool? |
null |
Show the "Powered by Upscope" link (hiding requires the whitelabel feature) |
stopSessionText |
String |
'Stop Sharing' |
Label on the stop-sharing button |
endOfSessionMessage |
String? |
null |
Message shown to the user when the session ends |
allowRemoteClick |
bool? |
null |
Override server-side remote click permission |
allowRemoteScroll |
bool? |
null |
Override server-side remote scroll permission |
requireControlRequest |
bool? |
null |
Require explicit user approval before agent can interact |
region |
String? |
null |
Force a specific Upscope server region |
Full device sharing (iOS)
Full device sharing lets the agent see the entire device screen (other apps, home screen) via ReplayKit. It requires a Broadcast Upload Extension target in your app — Flutter plugins can't add extension targets, so this is a one-time manual setup (~10 minutes). Without it, the agent's "Full app sharing" button is hidden.
Android needs no setup: it uses MediaProjection, and the required foreground-service permissions are merged automatically from the SDK's manifest.
1. Add the extension target
In Xcode: File → New → Target → Broadcast Upload Extension. Name it (e.g.
YourAppBroadcast), uncheck "Include UI Extension".
2. Replace the generated SampleHandler
Open YourAppBroadcast/SampleHandler.swift (created by Xcode) and replace its
contents:
import UpscopeSDK
class SampleHandler: UpscopeSampleHandler {}
3. Add an App Group to BOTH targets
In Signing & Capabilities for your app target and the extension target,
add the App Groups capability with the same group, e.g.
group.com.yourcompany.yourapp.
4. Declare the group in the extension's Info.plist
<key>UpscopeAppGroupId</key>
<string>group.com.yourcompany.yourapp</string>
Also make sure NSExtension > RPBroadcastProcessMode is 2 (sample-buffer
mode); some Xcode template versions omit it. See the example app's
Info.plist.
5. Add the extension pod
# ios/Podfile (top level, alongside the Runner target)
target 'YourAppBroadcast' do
pod 'UpscopeSDK/BroadcastExtension'
end
Run pod install.
6. Add the keys to your iOS app's Info.plist
<key>UpscopeAppGroupId</key>
<string>group.com.yourcompany.yourapp</string>
<key>UpscopeBroadcastExtensionBundleId</key>
<string>com.yourcompany.yourapp.YourAppBroadcast</string>
UpscopeAppGroupId enables the feature (the agent's button appears);
UpscopeBroadcastExtensionBundleId preselects your extension in the iOS
picker. No Dart code is involved — the configuration lives entirely in your
iOS project. (Upgrading from a version with broadcastAppGroupId /
broadcastExtensionBundleId? Remove those arguments from your
UpscopeConfiguration — they no longer exist.)
Note: reading these keys requires an Upscope iOS SDK release with Info.plist broadcast configuration. On older native SDK versions the keys are ignored and full-device sharing stays disabled.
Notes
- The App Group ID must match in three places: both targets' entitlements,
the extension's
Info.plist, and the app'sInfo.plist(both use theUpscopeAppGroupIdkey). - Test on a physical device — ReplayKit broadcasts are unreliable on the simulator.
- Full-device capture cannot mask views. Set
disableFullScreenWhenMasked: trueto automatically end full-device sharing whenever masked content is on screen. - Add-to-app setups: the extension target must be added to the host iOS project, not the Flutter module.
- Without the extension, screen-only and visitor-app sharing still work; only full-device sharing is unavailable.
- A complete working setup is in
example/ios(RunnerBroadcasttarget).
API Reference
Upscope.instance — methods
| Method | Returns | Description |
|---|---|---|
initialize(UpscopeConfiguration) |
Future<void> |
Initialize and optionally auto-connect |
connect() |
Future<void> |
Connect to Upscope servers |
disconnect() |
Future<void> |
Disconnect from Upscope servers |
reset({bool reconnect}) |
Future<void> |
Clear identity and optionally reconnect |
updateConnection({uniqueId, callName, tags, identities, metadata}) |
Future<void> |
Update visitor identity |
stopSession() |
Future<void> |
End the current cobrowsing session |
requestAgent() |
Future<void> |
Request a support agent |
cancelAgentRequest() |
Future<void> |
Cancel a pending agent request |
getLookupCode() |
Future<void> |
Trigger a lookup code refresh |
getShortId() |
Future<String?> |
Get the persistent visitor short ID |
getWatchLink() |
Future<String?> |
Get a direct watch link for the current session |
sendCustomMessage(String) |
Future<void> |
Send a custom string to all connected agents |
stopRemoteControl() |
Future<void> |
Revoke agent device control (session continues) |
stopFullDeviceSharing() |
Future<void> |
Stop full-device screen sharing |
respondToFullDeviceRequest(String, {required bool accept}) |
Future<void> |
Approve/decline a full-device sharing request |
respondToControlRequest(String, {required bool accept}) |
Future<void> |
Approve/decline a remote-control request |
Upscope.instance — streams
| Stream | Type | Emits |
|---|---|---|
connectionState |
Stream<ConnectionState> |
Connection state changes |
sessionState |
Stream<SessionState> |
Session state changes |
lookupCode |
Stream<String?> |
Current lookup code (refreshes periodically) |
shortId |
Stream<String?> |
Persistent visitor short ID |
onSessionStarted |
Stream<String?> |
Session ID when a session begins |
onSessionEnded |
Stream<SessionEndReason> |
Reason the session ended |
onViewerJoined |
Stream<Viewer> |
Agent joined the session |
onViewerLeft |
Stream<String> |
Agent ID when an agent leaves |
onViewerCountChanged |
Stream<int> |
Current number of watching agents |
onCustomMessageReceived |
Stream<CustomMessage> |
Custom message from an agent |
remoteControlState |
Stream<RemoteControlState> |
Agent device-control state changes |
fullDeviceSharingState |
Stream<FullDeviceSharingState> |
Full-device sharing state changes |
onFullDeviceRequest |
Stream<FullDeviceRequest> |
Request ID + agent name when an agent asks for full device |
onControlRequest |
Stream<ControlRequest> |
Request ID + agent name when an agent asks for control |
onError |
Stream<UpscopeError> |
SDK-level errors |
Enums
ConnectionState — inactive, connecting, connected, reconnecting, error
SessionState — inactive, pendingRequest, active, paused, ended
SessionEndReason — userStopped, agentStopped, timeout, error
RemoteControlState — inactive, pendingRequest, active
FullDeviceSharingState — inactive, pendingRequest, active
Widgets
UpscopeMasked({required Widget child}) — Wraps a child and masks its screen region during cobrowsing. Handles position tracking, rotation, and scroll automatically. No configuration needed.
Troubleshooting
Sessions never start / connection stays inactive
- Confirm
autoConnect: true(default) or that you calledconnect()explicitly. - Verify the API key is correct for your Upscope account.
- Check that the device has internet access.
Streams receive no events
- Subscribe to streams before calling
initialize, or use aStreamControllerwithsync: falseto avoid missing the first emission.
Masked widget position is wrong after navigation
UpscopeMaskedtracks metrics changes and scroll, but not route transitions. If you push/pop routes while a masked widget is off-screen, the rect will be corrected on the next frame when the widget is visible again. This is typically transparent to the agent.
iOS build fails with missing plugin
- Ensure you have run
pod install(orflutter pub getwhich runs it for you). - The plugin class is
UpscopePluginregistered under theio.upscope.flutternamespace.
Android build fails
- Minimum supported API is 26. Check
minSdkin your app-levelbuild.gradle.
Support
- Documentation: Flutter Docs
- Email: team@upscope.io
License
Proprietary. See your agreement with Upscope for details.
Libraries
- upscopeio_flutter_sdk
- Upscope Flutter SDK — public API surface.