ringdesk_flutter_sdk 0.0.1
ringdesk_flutter_sdk: ^0.0.1 copied to clipboard
Drop-in in-app voice support, screen sharing, and overlay widgets for Flutter applications.
RingDesk Flutter SDK #
RingDesk adds in-app voice support and consent-based screen sharing to an existing Flutter app through one wrapper widget. The SDK owns enrollment, permissions, secure token storage, WebRTC, call history, customer profile handling, call controls, overlays, retries, and cleanup.
The default customer surface is deliberately white-label and minimal. It displays only call controls and never displays SDK marketing, customer profile fields, call history, or a shared-screen preview.
Call audio matches the RingDesk customer app: ringing loops until connection, transfer audio leads back into ringing, and server-selected busy, unavailable, or call-ended prompts play without exposing technical errors in the mini UI. Prompt media is accepted only over HTTPS.
Install #
RingDesk is currently distributed to design partners as source or through an
issued private package reference. Place the issued ringdesk_flutter_sdk
directory beside the host application and add:
dependencies:
ringdesk_flutter_sdk:
path: ../ringdesk_flutter_sdk
Partners issued access to a private Git package can instead pin the immutable tag or commit provided with their release:
dependencies:
ringdesk_flutter_sdk:
git:
url: <issued repository URL>
ref: <issued tag or commit>
Only after the package is published on pub.dev should public installations use
flutter pub add ringdesk_flutter_sdk. Package metadata remains checked with
dart pub publish --dry-run so that future publication does not require an
integration change.
Requirements: Flutter 3.38 or newer, Dart 3.12.2 or newer, Android API 24 or newer, or iOS 13 or newer. Create an application in the developer portal, then copy the workspace Developer ID and application App code. These are public identifiers, not credentials.
Host integration #
Initialize RingDesk once with the signed-in customer, then wrap the existing root widget. Customer profile data is always supplied programmatically. The minimal integration remains within 15 Dart lines:
import 'package:ringdesk_flutter_sdk/ringdesk_flutter_sdk.dart';
void main() {
RingDesk.initialize(
developerId: const String.fromEnvironment('RINGDESK_DEVELOPER_ID'),
appCode: const String.fromEnvironment('RINGDESK_APP_CODE'),
customer: const RingDeskCustomer(
name: 'Signed-in customer',
customerId: 'auth-user-42',
),
);
runApp(const RingDeskOverlay(child: MyApp()));
}
Run the app with the two public backend identifiers:
flutter run \
--dart-define=RINGDESK_DEVELOPER_ID=YOUR_DEVELOPER_ID \
--dart-define=RINGDESK_APP_CODE=YOUR_APP_CODE
The host should populate RingDeskCustomer from its signed-in user. name and
a stable, non-empty, opaque customerId are required before enrollment. Use an
internal authentication subject or similarly stable identifier; do not derive
it from mutable or directly identifying fields such as name, email, or phone.
phone, email, and preferredLanguage are optional. If the developer portal
defines required customer fields, pass them by key:
customer: const RingDeskCustomer(
name: 'Signed-in customer',
customerId: 'auth-user-42',
customFields: {'order_id': 'ORDER-1042'},
),
If customer identity is only available after login, initialize the SDK configuration first and retain a controller. It requests normal call permissions by default, waits without enrolling, and resumes when setCustomer receives the signed-in profile:
RingDesk.initialize(
developerId: developerId,
appCode: appCode,
);
final support = RingDeskController.initialized();
RingDeskOverlay(
controller: support,
child: const MyApp(),
);
await support.setCustomer(
RingDeskCustomer(name: signedInUser.name, customerId: signedInUser.id),
);
When the host user signs out, clear the linked customer before presenting the next login:
await support.signOut(); // `clearCustomer()` is an equivalent alias.
This ends any active or in-flight support call, invalidates pending enrollment,
revokes the old server link when reachable, and removes that customer's scoped
installation, cached profile, and capability from secure storage. If revocation
is temporarily offline, the SDK securely queues a retry while keeping the old
link and history unavailable to the next signed-in customer. The same controller
can later resume with setCustomer after the next user signs in.
Hosts that need to present their own permission explanation can defer the microphone/notification request:
RingDesk.initialize(
developerId: developerId,
appCode: appCode,
permissionPolicy: RingDeskPermissionPolicy.deferred,
);
final support = RingDeskController.initialized();
final ready = await support.requestNecessaryPermissions();
requestNecessaryPermissions never requests or starts screen capture. App-only and entire-screen capture always remain user-triggered and display the operating-system consent surface where required.
The SDK validates the profile, enrolls the installation, saves the profile automatically, and only then enables calling. Customer PII and the short-lived capability are cached through Android Keystore or iOS Keychain and are never logged.
Backend linking remains restricted to the public developerId and appCode. Never put an admin token, API secret, or backend credential in a host app.
Integration types #
All three integration types use the same RingDeskController and call engine, so a host can move from the default UI to a fully custom UI without replacing enrollment, media, signaling, permissions, or cleanup logic.
On iOS and Android, system-call integration is enabled by default. The SDK registers the outgoing support call with CallKit/Android Telecom, keeps the native call state synchronized with WebRTC, and restores the Flutter overlay when the customer returns to the host app. This provides the operating-system ongoing-call surface while the host app is minimized; iOS also includes the call in Phone Recents and uses the native ongoing-call indicator/Dynamic Island where the device supports it.
1. Simple draggable overlay #
RingDeskOverlay is the default RingDeskIntegrationType.overlay integration. During a call it shows only a compact icon bar containing mute, audio output, screen sharing, and hang-up. It contains no launcher, timer, support-app pages, or local screen preview by default.
The surface is draggable by default. Set behavior: RingDeskOverlayBehavior.fixed to keep it in its configured corner.
Start calls from the host app's own button by sharing an overlay controller:
final supportOverlay = RingDeskOverlayController();
RingDeskOverlay(
overlayController: supportOverlay,
child: const MyApp(),
);
RingDeskSupportButton(
overlayController: supportOverlay,
label: 'Support',
);
RingDeskSupportButton starts the call; pressing it again minimizes the controls, and pressing it during an active minimized call restores them. The call remains active while the controls are minimized and the surface closes automatically when the call ends.
Configure the control bar without changing the call engine:
RingDeskOverlay(
behavior: RingDeskOverlayBehavior.fixed,
controls: const RingDeskMinimalOverlayConfig(
showMute: true,
showAudioOutput: true,
initialAudioOutput: RingDeskAudioOutput.earpiece,
screenShareControl: RingDeskScreenShareControl.agentRequestOnly,
),
child: const MyApp(),
);
screenShareControl supports always, agentRequestOnly, and hidden. A share-control tap uses the agent-requested scope when present, otherwise the first supported configured scope, and then opens only the operating-system capture consent. Hidden sharing automatically declines agent share requests. Hang-up is intentionally always present when the strip is visible.
2. Embedded full integration #
Use RingDeskSupportView (RingDeskIntegrationType.embedded) only when an application intentionally wants the complete built-in calling experience. It includes calling, customer profile editing, required custom fields, typed call history, active-call controls, screen-share preview, and annotations.
final support = RingDeskController.initialized();
RingDeskSupportView(
controller: support,
compact: false,
screenShare: RingDeskScreenShareOption.both,
);
The view initializes the controller automatically. A host-owned controller must be disposed by its owning State.dispose method.
3. Headless or custom widgets #
Use RingDeskBuilder (RingDeskIntegrationType.headless) for completely custom UI, including the future customer Flutter app integration. The public controller exposes application/profile data, readiness, availability, call history, current call state, screen-share state, annotations, and all call actions.
RingDeskBuilder(
controller: support,
builder: (context, sdk) => FilledButton(
onPressed: sdk.phase == RingDeskSdkPhase.ready
? sdk.startCall
: null,
child: Text(sdk.call?.active == true ? 'Call in progress' : 'Get help'),
),
);
For a custom floating panel while retaining dragging/minimizing, pass panelBuilder and/or launcherBuilder to RingDeskOverlay. RingDeskMinimalCallControls, RingDeskScreenSharePreview, and RingDeskCallHistoryView can also be composed independently. These opt-in widgets do not change media, signaling, consent, or cleanup behavior.
Set showLauncher: true only when the host explicitly wants the SDK-provided call launcher in addition to the control bar.
Applications that already own their complete support UI can use the public call engine directly. Build a validated session from the enrollment returned by the backend, initialize it once, and bind the controller's ChangeNotifier state to the existing widgets:
final call = RingDeskCallController.fromSession(
session: RingDeskCallSession(
apiBaseUri: apiBaseUri,
linkId: enrollment.linkId,
accessToken: enrollment.accessToken,
application: application,
customer: customer,
),
);
await call.initialize();
await call.start();
The controller exposes the remote renderer, annotations, elapsed time, audio routes, screen-share requests, the effective screenProfile, and call actions. The host owns this controller and must call dispose() with its widget lifecycle. This is the integration used by the repository customer Flutter app; its existing pages and visual design stay host-owned while signaling, media, permissions, prompts, and teardown come from the SDK.
Variants #
When the optional launcher is enabled, choose its treatment without changing the call flow:
RingDeskVariant.professional: labeled support pill for customer-facing apps.RingDeskVariant.compact: compact square control for dense interfaces.RingDeskVariant.minimal: icon-only circular control.
The overlay can be placed in any corner with RingDeskOverlayPosition. All variants use 48-point-or-larger controls, semantic labels, safe-area insets, bounded panel width, and responsive text wrapping.
Themes #
Set themeMode on RingDeskOverlay:
RingDeskThemeMode.hostteamsLight,teamsDark, orteamsSystemwebexLight,webexDark, orwebexSystem
Pass customTheme: ThemeData(...) for complete control. The host theme is inherited when the SDK wrapper is built below the host MaterialApp, normally through its builder:
MaterialApp(
builder: (context, child) => RingDeskOverlay(
themeMode: RingDeskThemeMode.host,
child: child!,
),
home: const HomePage(),
);
Teams/Webex system variants follow platform brightness automatically. Presets use semantic Material colors, so error, success, focus, disabled, and high-contrast behavior remains consistent.
Screen sharing #
Use RingDeskScreenShareOption.hostAppOnly, entireScreenOnly, or both. These
options describe the scope RingDesk requests; the operating-system capture
surface remains authoritative and the customer must approve the source. When
both is selected, the customer sees two requested-scope choices:
RingDeskScreenShareMode.hostApp: asks the operating system for app-only sharing where the platform supports it.RingDeskScreenShareMode.entireScreen: asks for full-display sharing, followed by explicit operating-system consent.
The minimal overlay never displays a copy of the shared screen. When app-only scope was requested, it draws a two-logical-pixel red-orange outline around the host application and renders support-agent annotations without intercepting host touches. The outline reports the requested mode; it is not proof that the operating system selected a particular source. Entire-screen sharing relies on the operating system's privacy indicator and Android foreground notification outside the host app; the active share control remains highlighted when the customer returns. The SDK does not request cross-app floating-window permission.
Platform behavior:
| Platform | Host app only | Entire screen |
|---|---|---|
| Android 14+ | Requests the single-app capture path; the OS picker confirms the source | Requests full-display capture; OS consent remains authoritative |
| Android 8–13 | Returns a clear unsupported error to avoid oversharing | Supported through MediaProjection |
| iOS 13+ | Requests in-app ReplayKit capture | Requests a ReplayKit Broadcast Upload Extension |
iOS whole-device capture requires the host target to define RTCScreenSharingExtension and include a ReplayKit Broadcast Upload Extension/App Group. The working reference is under customer_flutter/ios/BroadcastExtension in this repository. Without it, the SDK disables the operation with a configuration error rather than starting the wrong capture scope.
Screen-capture consent cannot be granted silently or persisted by an SDK. By default, RingDesk requests microphone and Android notification permissions on the first rendered frame; hosts can defer them with RingDeskPermissionPolicy.deferred. The OS capture consent sheet appears only after the customer chooses a requested scope. An agent request can reveal the consent chooser but can never start capture or override the source the operating system and customer approve.
The backend selects the capture profile for each call. The SDK treats that selection as policy, ignores client-supplied numeric overrides, and applies the following maximum RTP frame-rate and bitrate constraints on every screen sender:
| Profile | Maximum resolution | Maximum FPS | Maximum bitrate |
|---|---|---|---|
hd_readability |
1280×720 | 10 | 1.2 Mbps |
full_hd_detail |
1920×1080 | 15 | 2.4 Mbps |
hd_motion |
1280×720 | 24 | 2.0 Mbps |
full_hd_motion |
1920×1080 | 24 | 3.2 Mbps |
hd_ultra_motion |
1280×720 | 30 | 2.4 Mbps |
The SDK installs the conservative plan encoding before SDP negotiation, applies the selected profile again when capture starts, and reapplies it if encoded-frame cadence exceeds the ceiling. Telemetry uses cumulative encoded-frame deltas instead of ReplayKit's noisy instantaneous gauge, so normal app activity cannot falsely terminate a share. Quota exhaustion remains the only server-side usage condition that stops capture.
Whitelisted application files #
Growth and Scale applications can expose only explicit application-owned folders to the connected support person. Resolve platform directories in the host app, then pass them during initialization:
RingDesk.initialize(
developerId: developerId,
appCode: appCode,
customer: customer,
fileAccessRoots: [
RingDeskFileAccessRoot(
id: 'logs',
label: 'Application logs',
path: logsDirectory.path,
),
RingDeskFileAccessRoot(
id: 'support',
label: 'Application support',
path: supportDirectory.path,
allowUploads: true,
),
],
);
Files travel over the call's ordered WebRTC data channel. Paths are virtualized, symbolic links outside a root are rejected, names and sizes are bounded, uploads require an explicit writable root, and file bytes use the same quota as screen sharing.
Native configuration #
Android call, notification, microphone, and media-projection declarations are merged from plugin dependencies automatically. The SDK asks for runtime microphone and Android notification permission itself. An active call is represented by an ongoing notification and a self-managed Telecom call, so system interruptions cannot create an untracked parallel audio session.
iOS requires one host-owned usage string plus audio and VoIP background modes because App Store bundles do not allow a plugin to inject application privacy copy or target capabilities:
<key>NSMicrophoneUsageDescription</key>
<string>Voice support uses the microphone during a support call.</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>voip</string>
</array>
CallKit supplies the system ongoing-call indicator, lock-screen controls, Dynamic Island presentation on supported iPhones, and the Phone Recents entry. Native end, mute, and hold actions are matched to the current backend call ID before changing media. Holding pauses outgoing microphone/screen tracks and incoming audio until the operating system resumes the call.
Apps that already own a complete native calling layer can pass a custom RingDeskSystemCallAdapter to RingDeskController/RingDeskCallController.fromSession. To opt out explicitly, set systemCallIntegration: RingDeskSystemCallIntegration.disabled in RingDeskConfig; doing so removes the system ongoing-call experience and should only be used when the host replaces it.
Built-in edge handling #
- Invalid or mismatched developer/app identifiers fail before media starts.
- Missing or malformed initialized customer data fails before media starts.
- Required application custom fields are checked and saved before calling is enabled.
- The default overlay never renders customer profile or history data; full/custom integrations opt into those widgets explicitly.
- Customer details can be updated through the public controller or built-in profile form when no call is active.
- Call history is authenticated, typed, bounded to the backend's 25-entry window, and refreshed after calls end.
- Microphone denial blocks calling with a retry path; notification denial remains nonfatal.
- Offline initialization can use the last matching Keychain/Keystore enrollment.
- Network and signaling calls have 15-second timeouts and 512 KiB response limits.
- ICE queues, data-channel messages, annotations, and stroke points are bounded.
- A process-wide call owner, native single-call groups, and the backend's atomic agent claim prevent parallel support calls; native cellular call waiting can hold or end the SDK call through CallKit/Telecom.
- Duplicate call/share starts are ignored; abandoned capture streams and foreground services are stopped.
- Hidden screen-sharing policy declines remote requests instead of silently granting or exposing capture.
- Android versions that cannot guarantee app-only capture never fall back to full-screen capture.
- iOS whole-screen sharing checks extension configuration before opening ReplayKit.
- Backend screen-share quota responses stop capture immediately.
- Transient or platform-reported FPS spikes reapply sender limits without terminating capture.
- App detach, remote hang-up, capture cancellation, and call teardown release tracks, peer connections, audio routes, timers, and secure foreground execution.
Verification #
flutter analyze
flutter test
flutter build apk --debug
flutter build ios --debug --no-codesign
dart pub publish --dry-run
Physical-device validation is still required for microphone routing, Android MediaProjection, and iOS ReplayKit before production release.
Before releasing a host app, complete the Play Console Data safety and foreground-service declarations or the App Store Connect privacy disclosures as applicable. The host remains responsible for its privacy policy, permission explanations, recording consent, retention, and account-deletion flow. See the repository STORE_RELEASE_CHECKLIST.md for the tested release procedure.