returningai_widget 0.1.0
returningai_widget: ^0.1.0 copied to clipboard
Official Flutter SDK for embedding ReturningAI widgets on Android and iOS.
returningai_widget #
Official Flutter SDK for embedding ReturningAI widgets on Android and iOS.
Requirements #
- Flutter 3.38 or later
- Dart 3.10 or later
- Android API 24 or later
- iOS 13 or later
- An HTTPS client origin allowlisted in ReturningAI
- A customer-owned backend that returns short-lived ReturningAI embed tokens
Supported hosts matrix (all platforms):
docs/guides/client-integration.md
(Supported hosts). Production embed steps (token mint, custom bundle, origin
allowlist, session-expired): same guide. The gallery is a public-embed demo, not
that recipe.
Install #
dependencies:
returningai_widget: ^0.1.0
Android apps must have internet permission in android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
Basic Usage #
final controller = ReturningAIWidgetController();
ReturningAIWidget(
controller: controller,
configuration: ReturningAIWidgetConfiguration(
widget: ReturningAIWidgetDescriptor.custom(widgetId: 'YOUR_WIDGET_ID'),
content: ReturningAIWidgetContent.bundle(
Uri.parse('https://prod-widgets.returning.ai/custom-widget/bundle/widget.js'),
),
clientOrigin: Uri.parse('https://mobile-client.example'),
),
embedTokenProvider: () async {
return backend.fetchReturningAIEmbedToken();
},
onEvent: (event) {
switch (event) {
case ReturningAIWidgetReady():
debugPrint('ReturningAI widget ready');
case ReturningAIWidgetSessionExpired():
debugPrint('ReturningAI session expired; next auth re-mints');
case ReturningAIWidgetHeightChanged(:final height):
debugPrint('ReturningAI widget height: $height');
case ReturningAIWidgetErrorEvent(:final error):
debugPrint(error.toString());
default:
break;
}
},
hostCallbacks: {
'callbackFieldOptions': (request) async {
return ['Option A', 'Option B'];
},
'milestoneCtaClick': (request) async {
return null;
},
},
onExternalNavigation: (uri) {
appRouter.openExternal(uri);
},
);
clientOrigin is the exact HTTPS origin allowlisted for the widget in ReturningAI. It is sent as the native package authentication request's Origin header. It is not a Flutter route or app URL.
Web SDK to Flutter #
The Flutter SDK wraps the same web widgets used by the browser SDK. A web snippet like:
<rai-custom-widget
widget-id="YOUR_WIDGET_ID"
domain-key="SGTR"
data-email="user@example.com"
bundle-url="https://sgtr-eks-widgets.genesiv.org/custom-widget/bundle/store/widget.js"
></rai-custom-widget>
maps to:
ReturningAIWidget(
configuration: ReturningAIWidgetConfiguration.customBundle(
widgetId: 'YOUR_WIDGET_ID',
bundleUrl: Uri.parse(
'https://sgtr-eks-widgets.genesiv.org/custom-widget/bundle/store/widget.js',
),
clientOrigin: Uri.parse('https://mobile-client.example'),
domainKey: ReturningAIWidgetDomainKey.sgtr,
userIdentifiers: const <String, String>{
'data-email': 'user@example.com',
},
),
);
For Access Key Embed snippets, do not pass embed-token into HTML. Keep token minting in your backend and provide it to Flutter:
ReturningAIWidget(
configuration: ReturningAIWidgetConfiguration.customBundle(
widgetId: 'YOUR_WIDGET_ID',
bundleUrl: Uri.parse(
'https://adss-prod-widgets.returning.ai/custom-widget/bundle/store/widget.js',
),
clientOrigin: Uri.parse('https://mobile-client.example'),
endpoints: ReturningAIWidgetEndpoints.adssProduction,
),
embedTokenProvider: backend.fetchReturningAIEmbedToken,
);
userIdentifiers and embed tokens stay in Dart authentication request bodies. The generated WebView document receives only auth-url="rai-native-auth://token" and the short-lived access token returned by the serverless exchange.
Security #
Do not place ReturningAI access IDs or access keys in a mobile app. The SDK accepts only an embedTokenProvider, calls it when the widget runtime requests authentication, and exchanges the returned embed token for a short-lived access token.
The embed token is never written into generated HTML, WebView globals, JavaScript source, bridge traffic, URLs, events, logs, or public error strings. The WebView receives only:
{ "token": "short-lived-access-token", "expiresIn": 300 }
Production runtime, content, endpoint, and navigation URLs must use HTTPS. debug: true permits explicit localhost HTTP URLs for local development only.
The default web runtime is https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js. Pass runtimeUrl on the primary constructor or on customBundle to pin a specific IIFE (Loyalty samples use unpkg latest; production hosts should pin). See docs/guides/client-integration.md.
Pass Dart locale (for example th). The shell writes both locale and language on the custom element so 1.8.2+ runtimes see the canonical language attribute. There is no second Dart field.
rai-session-expired is typed as ReturningAIWidgetSessionExpired. The SDK clears its access-token cache and does not reload the WebView. The host embedTokenProvider should return a freshly minted embed token on the next auth request.
Default callbackNames are callbackFieldOptions, storePurchaseSuccess, and milestoneCtaClick. The shell registers those names; the host still has to supply a hostCallbacks entry for any callback it wants to handle. Missing handlers fail closed with a sanitized error. milestoneCtaClick may return null.
Widgets #
Flutter hosts custom product embeds only (rai-custom-widget). Use ReturningAIWidgetDescriptor.custom or the ReturningAIWidgetConfiguration.customBundle factory.
ReturningAIWidgetDescriptor.custom(widgetId: 'WIDGET_ID');
Custom widget-id is the URL-safe base64 encoding of the CustomWidget Mongo _id. Do not pass the raw Mongo id. Gallery Store, Socials, Currency, Milestones, Streaks, and Mini Games product embeds are custom widgets (rai-custom-widget + a catalog bundle-url). Currency uses /custom-widget/bundle/earning-overview/widget.js. Custom Milestones uses /custom-widget/bundle/milestones/widget.js. Custom Mini Games uses /custom-widget/bundle/mini-games/widget.js. First-class web elements (<rai-store-widget>, <rai-milestone-widget>, and the rest) are not Flutter APIs.
Content can be a JavaScript bundle or a hosted widget page:
ReturningAIWidgetContent.bundle(Uri.parse('https://.../widget.js'));
ReturningAIWidgetContent.page(Uri.parse('https://.../widget.html'));
Branded Endpoints #
Use domainKey for standard web SDK environments:
ReturningAIWidgetConfiguration.customBundle(
widgetId: 'YOUR_WIDGET_ID',
bundleUrl: Uri.parse('https://sgtr-eks-widgets.genesiv.org/custom-widget/bundle/store/widget.js'),
clientOrigin: Uri.parse('https://mobile-client.example'),
domainKey: ReturningAIWidgetDomainKey.sgtr,
);
Use ReturningAIWidgetEndpoints for branded environments that need explicit web runtime globals:
endpoints: ReturningAIWidgetEndpoints.adssProduction
The built-in endpoint presets are production, sgtr, staging, local, and adssProduction. local requires debug: true because it uses localhost HTTP.
Events and Callbacks #
Events are typed Dart classes: loading, mounted, ready, logout, height change, external navigation, error, and custom events.
Callbacks are allowlisted by the hostCallbacks map keys. Payloads and returned values must be JSON-compatible: null, bool, num, String, List, or string-keyed Map. Missing callbacks, invalid payloads, callback timeouts, and thrown errors reject the JavaScript promise with sanitized errors.
Layout and Lifecycle #
By default, the WebView fills the constraints supplied by its Flutter parent. Enable automatic height when the web widget emits rai-height-change:
display: const ReturningAIWidgetDisplayOptions(autoHeight: true)
Use ReturningAIWidgetController for imperative operations:
await controller.reload();
await controller.retry();
await controller.updateTheme(ReturningAIWidgetTheme.dark);
await controller.invalidate();
The SDK isolates every mounted widget session. Multiple widgets do not share tokens, pending bridge requests, callbacks, or lifecycle state.
There is no default Flutter loading overlay; the web widget paints its own loader. When display.backgroundColor is transparent, the shell and WebView fill from the resolved theme (dark #111827, light #ffffff).
Retry and configuration changes reuse the existing WebView. Authentication failures and rai-error cancel the ready timer and keep that WebView. The ready timeout overlays an error on the WebView instead of unmounting it.
Off-allowlist HTTPS subframe navigations are blocked without calling onError. Main-frame unknown schemes still emit navigationRejected. Off-allowlist main-frame HTTPS is delegated to onExternalNavigation. Add hosts with allowedNavigationHosts when a page must stay inside the WebView.
ReturningAIWidgetTheme.system follows the current platform brightness and
updates when that brightness changes. HTTP failures from images, iframes, and
other subresources are reported without replacing an otherwise healthy widget;
main-frame resource failures and the ready timeout remain terminal overlays.
Testing #
From packages/flutter:
flutter pub get
dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test
flutter pub publish --dry-run
flutter test includes Dart CPU budgets in test/benchmark/sdk_hot_path_benchmark_test.dart (HTML render, bridge, navigation, coordinator start). Those catch catastrophic regressions; they do not measure device WebView time-to-interactive. Elapsed times print in the test log.
WebView cold-start and reload time-to-ready live in the gallery loopback fixture:
cd examples/flutter/loyalty_app
flutter test integration_test/webview_speed_benchmark_test.dart -d <device-id>
To upload this package to pub.dev, follow docs/publishing/flutter.md.
Examples #
Runnable Flutter apps are kept outside the publishable package under the
repository root. Start with
examples/flutter/loyalty_app for
Store, Socials, Currency, Milestones, Streaks, and Mini Games public embed examples that reuse this package by path.
Loyalty App run, package tests, and benchmark commands: examples/flutter/TESTING.md.