upscopeio_flutter_sdk 2026.8.3 copy "upscopeio_flutter_sdk: ^2026.8.3" to clipboard
upscopeio_flutter_sdk: ^2026.8.3 copied to clipboard

Flutter SDK for Upscope cobrowsing — screen sharing, annotations, and remote control

example/lib/main.dart

import 'dart:async';
import 'dart:developer' as dev;

import 'package:flutter/material.dart' hide ConnectionState;
import 'package:upscopeio_flutter_sdk/upscopeio_flutter_sdk.dart';

void main() {
  UpscopeMethodChannel.register();
  runApp(const UpscopeExampleApp());
}

// ---------------------------------------------------------------------------
// App root
// ---------------------------------------------------------------------------

class UpscopeExampleApp extends StatelessWidget {
  const UpscopeExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Upscope SDK Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF5C6BC0)),
        useMaterial3: true,
      ),
      home: const DemoScreen(),
    );
  }
}

// ---------------------------------------------------------------------------
// Top-level screen — owns the event log and coordinates child sections
// ---------------------------------------------------------------------------

class DemoScreen extends StatefulWidget {
  const DemoScreen({super.key});

  @override
  State<DemoScreen> createState() => _DemoScreenState();
}

class _DemoScreenState extends State<DemoScreen> {
  static const _maxLogEntries = 20;

  final _upscope = Upscope.instance;
  final _eventLog = <String>[];
  final _subscriptions = <StreamSubscription>[];

  StreamSubscription<FullDeviceRequest>? _fullDeviceRequestSub;
  StreamSubscription<SessionRequest>? _sessionRequestSub;

  @override
  void initState() {
    super.initState();
    _subscribeToEvents();
    _subscribeToFullDeviceRequests();
    _subscribeToSessionRequests();
  }

  /// Full-device requests are handled here (not in a child section) because the
  /// dialog needs a navigator context that outlives any single section.
  void _subscribeToFullDeviceRequests() {
    _fullDeviceRequestSub = _upscope.onFullDeviceRequest.listen((request) {
      _promptFullDeviceRequest(request);
    });
  }

  Future<void> _promptFullDeviceRequest(FullDeviceRequest request) async {
    final agent = request.agentName ?? 'An agent';
    final accept = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Full-device sharing requested'),
        content: Text(
          '$agent is requesting to share your entire device screen. Allow?',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(ctx).pop(false),
            child: const Text('Decline'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(ctx).pop(true),
            child: const Text('Allow'),
          ),
        ],
      ),
    );
    await _upscope.respondToFullDeviceRequest(request.requestId,
        accept: accept ?? false);
  }

  /// Session-start requests are handled here (not in a child section) because
  /// the dialog needs a navigator context that outlives any single section.
  void _subscribeToSessionRequests() {
    _sessionRequestSub = _upscope.onSessionRequest.listen((request) {
      _promptSessionRequest(request);
    });
  }

  Future<void> _promptSessionRequest(SessionRequest request) async {
    final agent = request.agentName ?? 'An agent';
    final accept = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Session requested'),
        content: Text(
          '$agent is requesting to start a cobrowsing session. Allow?',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(ctx).pop(false),
            child: const Text('Deny'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(ctx).pop(true),
            child: const Text('Allow'),
          ),
        ],
      ),
    );
    await _upscope.respondToSessionRequest(request.requestId,
        accept: accept ?? false);
  }

  void _subscribeToEvents() {
    void log(String msg) {
      dev.log(msg, name: 'UpscopeDemo');
      if (!mounted) return;
      setState(() {
        _eventLog.insert(0, '[${_timestamp()}] $msg');
        if (_eventLog.length > _maxLogEntries) {
          _eventLog.removeLast();
        }
      });
    }

    _subscriptions.addAll([
      _upscope.connectionState.listen(
        (s) => log('connectionState → ${s.name}'),
      ),
      _upscope.sessionState.listen((s) => log('sessionState → ${s.name}')),
      _upscope.shortId.listen((id) => log('shortId → ${id ?? 'null'}')),
      _upscope.lookupCode.listen(
        (code) => log('lookupCode → ${code ?? 'null'}'),
      ),
      _upscope.onSessionStarted.listen((id) => log('sessionStarted id=$id')),
      _upscope.onSessionEnded.listen(
        (reason) => log('sessionEnded reason=${reason.name}'),
      ),
      _upscope.onViewerJoined.listen(
        (v) => log('viewerJoined id=${v.id} name=${v.name}'),
      ),
      _upscope.onViewerLeft.listen((id) => log('viewerLeft id=$id')),
      _upscope.onViewerCountChanged.listen((n) => log('viewerCount=$n')),
      _upscope.onCustomMessageReceived.listen(
        (m) => log('customMessage from=${m.viewerId} msg=${m.message}'),
      ),
      _upscope.remoteControlState.listen(
        (s) => log('remoteControlState → ${s.name}'),
      ),
      _upscope.fullDeviceSharingState.listen(
        (s) => log('fullDeviceSharingState → ${s.name}'),
      ),
      _upscope.onError.listen((e) => log('error ${e.code}: ${e.message}')),
    ]);
  }

  @override
  void dispose() {
    _fullDeviceRequestSub?.cancel();
    _sessionRequestSub?.cancel();
    for (final sub in _subscriptions) {
      sub.cancel();
    }
    super.dispose();
  }

  String _timestamp() {
    final now = DateTime.now();
    return '${now.hour.toString().padLeft(2, '0')}:'
        '${now.minute.toString().padLeft(2, '0')}:'
        '${now.second.toString().padLeft(2, '0')}';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Upscope SDK Demo'), centerTitle: true),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          InitSection(upscope: _upscope),
          const SizedBox(height: 16),
          StatusSection(upscope: _upscope),
          const SizedBox(height: 16),
          ControlSection(upscope: _upscope),
          const SizedBox(height: 16),
          RemoteControlSection(upscope: _upscope),
          const SizedBox(height: 16),
          MaskedFieldSection(upscope: _upscope),
          const SizedBox(height: 16),
          const MaskingZOrderSection(),
          const SizedBox(height: 16),
          EventLogSection(events: _eventLog),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Init section: API key field + Initialize button
// ---------------------------------------------------------------------------

class InitSection extends StatefulWidget {
  final Upscope upscope;

  const InitSection({super.key, required this.upscope});

  @override
  State<InitSection> createState() => _InitSectionState();
}

class _InitSectionState extends State<InitSection> {
  final _apiKeyController = TextEditingController();
  bool _initialized = false;
  bool _initializing = false;

  @override
  void dispose() {
    _apiKeyController.dispose();
    super.dispose();
  }

  Future<void> _initialize() async {
    final apiKey = 'cEoQfasmRo';
    if (apiKey.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Enter your Upscope API key first'),
          behavior: SnackBarBehavior.floating,
        ),
      );
      return;
    }

    setState(() => _initializing = true);
    try {
      await widget.upscope.initialize(
        UpscopeConfiguration(
          apiKey: apiKey,
          requireAuthorizationForSession: true,
          autoConnect: true,
          // The example handles session and full-device requests with its own
          // dialogs (see _promptSessionRequest / _promptFullDeviceRequest), so
          // opt out of the native flows.
          customSessionRequestUI: true,
          customFullDeviceRequestUI: true,
          // iOS full-device sharing is configured in ios/Runner/Info.plist
          // (UpscopeAppGroupId / UpscopeBroadcastExtensionBundleId).
        ),
      );
      if (mounted) setState(() => _initialized = true);
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Init failed: $e'),
            behavior: SnackBarBehavior.floating,
          ),
        );
      }
    } finally {
      if (mounted) setState(() => _initializing = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return _SectionCard(
      title: 'Initialize',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          TextField(
            controller: _apiKeyController,
            enabled: !_initialized && !_initializing,
            decoration: const InputDecoration(
              labelText: 'Upscope API Key',
              hintText: 'Paste your API key here',
              border: OutlineInputBorder(),
              prefixIcon: Icon(Icons.vpn_key),
            ),
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Icon(
                _initialized ? Icons.check_circle : Icons.hourglass_empty,
                color: _initialized ? cs.tertiary : cs.outline,
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Text(
                  _initializing
                      ? 'Initializing…'
                      : _initialized
                      ? 'SDK initialized'
                      : 'Not initialized',
                ),
              ),
              FilledButton(
                onPressed: (_initialized || _initializing) ? null : _initialize,
                child: const Text('Initialize'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Status section: connection, session, shortId streams
// ---------------------------------------------------------------------------

class StatusSection extends StatefulWidget {
  final Upscope upscope;

  const StatusSection({super.key, required this.upscope});

  @override
  State<StatusSection> createState() => _StatusSectionState();
}

class _StatusSectionState extends State<StatusSection> {
  final _subs = <StreamSubscription>[];
  String? _shortId;
  String? _lookupCode;

  @override
  void initState() {
    super.initState();
    _subs.addAll([
      widget.upscope.shortId.listen((id) {
        if (mounted) setState(() => _shortId = id);
      }),
      widget.upscope.lookupCode.listen((code) {
        if (mounted) setState(() => _lookupCode = code);
      }),
      widget.upscope.connectionState.listen((state) {
        if (state == ConnectionState.connected) _fetchValues();
      }),
    ]);
  }

  Future<void> _fetchValues() async {
    final id = await widget.upscope.getShortId();
    if (mounted && id != null) setState(() => _shortId = id);
  }

  @override
  void dispose() {
    for (final sub in _subs) {
      sub.cancel();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return _SectionCard(
      title: 'Status',
      child: Column(
        children: [
          _StreamRow<ConnectionState>(
            label: 'Connection',
            stream: widget.upscope.connectionState,
            format: (s) => s.name,
            colorFor: _connectionColor,
          ),
          const SizedBox(height: 8),
          _StreamRow<SessionState>(
            label: 'Session',
            stream: widget.upscope.sessionState,
            format: (s) => s.name,
            colorFor: _sessionColor,
          ),
          const SizedBox(height: 8),
          _LabelValue(label: 'Short ID', value: _shortId),
          const SizedBox(height: 8),
          _LabelValue(label: 'Lookup Code', value: _lookupCode),
        ],
      ),
    );
  }

  Color _connectionColor(ConnectionState state, BuildContext ctx) {
    final cs = Theme.of(ctx).colorScheme;
    return switch (state) {
      ConnectionState.connected => cs.tertiary,
      ConnectionState.connecting ||
      ConnectionState.reconnecting => cs.secondary,
      ConnectionState.error => cs.error,
      ConnectionState.inactive => cs.outline,
    };
  }

  Color _sessionColor(SessionState state, BuildContext ctx) {
    final cs = Theme.of(ctx).colorScheme;
    return switch (state) {
      SessionState.active => cs.tertiary,
      SessionState.pendingRequest => cs.secondary,
      SessionState.paused => cs.secondary,
      SessionState.ended || SessionState.inactive => cs.outline,
    };
  }
}

// ---------------------------------------------------------------------------
// Control section: connect/disconnect, lookup code, stop session
// ---------------------------------------------------------------------------

class ControlSection extends StatefulWidget {
  final Upscope upscope;

  const ControlSection({super.key, required this.upscope});

  @override
  State<ControlSection> createState() => _ControlSectionState();
}

class _ControlSectionState extends State<ControlSection> {
  Future<void> _run(Future<void> Function() action, String label) async {
    try {
      await action();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('$label failed: $e'),
            behavior: SnackBarBehavior.floating,
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return _SectionCard(
      title: 'Controls',
      child: Wrap(
        spacing: 8,
        runSpacing: 8,
        children: [
          OutlinedButton(
            onPressed: () => _run(widget.upscope.connect, 'Connect'),
            child: const Text('Connect'),
          ),
          OutlinedButton(
            onPressed: () => _run(widget.upscope.disconnect, 'Disconnect'),
            child: const Text('Disconnect'),
          ),
          OutlinedButton(
            onPressed: () => _run(() => widget.upscope.reset(), 'Reset'),
            child: const Text('Reset'),
          ),
          OutlinedButton(
            onPressed: () => _run(widget.upscope.getLookupCode, 'Lookup code'),
            child: const Text('Get Lookup Code'),
          ),
          OutlinedButton(
            onPressed: () => _run(widget.upscope.stopSession, 'Stop session'),
            child: const Text('Stop Session'),
          ),
          OutlinedButton(
            onPressed: () => _run(widget.upscope.requestAgent, 'Request agent'),
            child: const Text('Request Agent'),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Remote control section: device-control state + stop
// ---------------------------------------------------------------------------

class RemoteControlSection extends StatelessWidget {
  final Upscope upscope;

  const RemoteControlSection({super.key, required this.upscope});

  Future<void> _run(
    BuildContext context,
    Future<void> Function() action,
    String label,
  ) async {
    try {
      await action();
    } catch (e) {
      if (context.mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('$label failed: $e'),
            behavior: SnackBarBehavior.floating,
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return _SectionCard(
      title: 'Remote Control',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          _StreamRow<RemoteControlState>(
            label: 'Control',
            stream: upscope.remoteControlState,
            format: (s) => s.name,
            colorFor: (s, ctx) => s == RemoteControlState.active
                ? Theme.of(ctx).colorScheme.tertiary
                : Theme.of(ctx).colorScheme.outline,
          ),
          const SizedBox(height: 12),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              OutlinedButton(
                onPressed: () => _run(
                  context,
                  upscope.stopRemoteControl,
                  'Stop remote control',
                ),
                child: const Text('Stop Remote Control'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Masked field section: demonstrates UpscopeMasked
// ---------------------------------------------------------------------------

class MaskedFieldSection extends StatefulWidget {
  final Upscope upscope;

  const MaskedFieldSection({super.key, required this.upscope});

  @override
  State<MaskedFieldSection> createState() => _MaskedFieldSectionState();
}

class _MaskedFieldSectionState extends State<MaskedFieldSection> {
  final _cardController = TextEditingController();

  @override
  void dispose() {
    _cardController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return _SectionCard(
      title: 'Masked Widget Demo',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'The field below is wrapped in UpscopeMasked — '
            'its region is hidden from cobrowsing observers.',
            style: Theme.of(context).textTheme.bodySmall,
          ),
          const SizedBox(height: 12),
          UpscopeMasked(
            child: TextField(
              controller: _cardController,
              decoration: const InputDecoration(
                labelText: 'Card Number (masked)',
                hintText: '•••• •••• •••• ••••',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.credit_card),
              ),
              keyboardType: TextInputType.number,
              obscureText: true,
            ),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Masking z-order repro: masked widget partially covered by an opaque overlay
// painted above it. The viewer must show the overlay on top of the mask.
// ---------------------------------------------------------------------------

class MaskingZOrderSection extends StatelessWidget {
  const MaskingZOrderSection({super.key});

  @override
  Widget build(BuildContext context) {
    return _SectionCard(
      title: 'Masking z-order repro',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Agent should see a black box where the red box is, with the '
            'white overlay strip visible on top (matching this screen).',
            style: Theme.of(context).textTheme.bodySmall,
          ),
          const SizedBox(height: 12),
          SizedBox(
            height: 140,
            child: Stack(
              alignment: Alignment.center,
              children: [
                UpscopeMasked(
                  child: Container(
                    width: 200,
                    height: 100,
                    color: const Color(0xFFFF0000),
                  ),
                ),
                Container(
                  width: 220,
                  height: 50,
                  color: const Color(0xFFFFFFFF),
                  alignment: Alignment.center,
                  child: const Text(
                    'Overlay — must stay visible to agent',
                    style: TextStyle(color: Color(0xFF000000), fontSize: 12),
                    textAlign: TextAlign.center,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Event log section
// ---------------------------------------------------------------------------

class EventLogSection extends StatelessWidget {
  final List<String> events;

  const EventLogSection({super.key, required this.events});

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    final cs = Theme.of(context).colorScheme;

    return _SectionCard(
      title: 'Event Log',
      child: events.isEmpty
          ? Text(
              'No events yet. Initialize the SDK to start.',
              style: textTheme.bodySmall?.copyWith(color: cs.outline),
            )
          : Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: events
                  .map(
                    (e) => Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(
                        e,
                        style: textTheme.bodySmall?.copyWith(
                          fontFamily: 'monospace',
                          color: cs.onSurfaceVariant,
                        ),
                      ),
                    ),
                  )
                  .toList(),
            ),
    );
  }
}

// ---------------------------------------------------------------------------
// Shared composable primitives
// ---------------------------------------------------------------------------

/// Generic card wrapper used by every section.
class _SectionCard extends StatelessWidget {
  final String title;
  final Widget child;

  const _SectionCard({required this.title, required this.child});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: Theme.of(context).textTheme.titleSmall?.copyWith(
                color: Theme.of(context).colorScheme.primary,
                fontWeight: FontWeight.w600,
              ),
            ),
            const SizedBox(height: 12),
            child,
          ],
        ),
      ),
    );
  }
}

/// A labeled row displaying a pre-resolved value (no stream subscription).
class _LabelValue extends StatelessWidget {
  final String label;
  final String? value;

  const _LabelValue({required this.label, this.value});

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Row(
      children: [
        SizedBox(
          width: 96,
          child: Text(
            label,
            style: Theme.of(
              context,
            ).textTheme.bodySmall?.copyWith(color: cs.outline),
          ),
        ),
        Expanded(
          child: Text(
            value ?? '—',
            style: Theme.of(context).textTheme.bodyMedium?.copyWith(
              color: cs.onSurface,
              fontWeight: FontWeight.w600,
            ),
          ),
        ),
      ],
    );
  }
}

/// A labeled row that subscribes to a stream and displays its latest value.
class _StreamRow<T> extends StatelessWidget {
  final String label;
  final Stream<T> stream;
  final String Function(T) format;
  final Color Function(T, BuildContext)? colorFor;

  const _StreamRow({
    required this.label,
    required this.stream,
    required this.format,
    this.colorFor,
  });

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Row(
      children: [
        SizedBox(
          width: 96,
          child: Text(
            label,
            style: Theme.of(
              context,
            ).textTheme.bodySmall?.copyWith(color: cs.outline),
          ),
        ),
        Expanded(
          child: StreamBuilder<T>(
            stream: stream,
            builder: (ctx, snap) {
              final value = snap.data;
              final text = value != null ? format(value) : '—';
              final color = value != null && colorFor != null
                  ? colorFor!(value, ctx)
                  : cs.outline;
              return Text(
                text,
                style: Theme.of(ctx).textTheme.bodyMedium?.copyWith(
                  color: color,
                  fontWeight: FontWeight.w600,
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}
0
likes
150
points
542
downloads

Documentation

API reference

Publisher

verified publisherupscope.io

Weekly Downloads

Flutter SDK for Upscope cobrowsing — screen sharing, annotations, and remote control

Homepage

License

unknown (license)

Dependencies

flutter

More

Packages that depend on upscopeio_flutter_sdk

Packages that implement upscopeio_flutter_sdk