View as Markdownllms.txt

RevRag Flutter SDK — AI Agent Integration Master Prompt

How to use: paste this entire document into your AI coding agent (Copilot, Cursor, Claude Code, etc.) inside the Flutter app you want to integrate. The agent will inspect your app, install and configure embed_flutter, ask you the few decisions only you can make, then validate the integration end-to-end and report.

Every rule below is derived from the SDK's source (v0.2.0). Where this prompt and a generic Flutter tutorial disagree, this prompt is correct for this SDK. Flutter is materially simpler to integrate than the React Native SDK: there is no native bootstrap code (no MainApplication/AppDelegate edits, no registerGlobals, no Babel/Metro/worklets/Gesture-Handler setup) — the livekit_client plugin registers its own native side.


You are an expert Flutter integration agent. Take this application from zero → working → validated RevRag SDK integration with minimal manual work.

Prime directives

  1. Inspect first, modify second. Never assume Flutter/Dart version, navigation library, state management, or native setup. Read the project.
  2. Preserve host behavior. Merge into existing config; never blindly overwrite AndroidManifest.xml, Info.plist, Podfile, main.dart, or the root widget.
  3. Never claim success without running the validation in §16–§21 and observing the log lines named there.
  4. Ask the developer for the decisions in §13 — do not guess them.
  5. Never hide a crash or error. The SDK ships its own crash-guard that suppresses only LiveKit/WebRTC async errors (§21); everything else must surface. Report SDK-side issues you cannot fix.
  6. Do not add dependencies, permissions, or config the SDK does not require.

1. Inspect the existing application

Record each of these before changing anything:

WhatHow to find it
Flutter / Dart versionflutter --version; environment: in pubspec.yaml
NavigationMaterialApp named routes / onGenerateRoute, MaterialApp.router + GoRouter, Navigator 2.0, or a tab shell (ShellRoute / BottomNavigationBar / IndexedStack)
Where runApp mountslib/main.dart — is there an auth/login gate before the app mounts?
Root widgetthe widget passed to runApp(...); what wraps MaterialApp
Existing LiveKit / WebRTCgrep -n "livekit_client|flutter_webrtc" pubspec.yaml pubspec.lock — note versions
Existing shared peerspermission_handler, shared_preferences, path_provider, http, lottie in pubspec.yaml (conflict risk, §4/§22)
Providers above the rootany global overlay/portal/theme wrapper that sits above MaterialApp (occlusion risk, §22)
Permissions already declaredandroid/app/src/main/AndroidManifest.xml, ios/Runner/Info.plist
App version sourcea build constant, --dart-define, CI-stamped value, or package_infothe SDK does NOT auto-derive it (§10)
Platform targetsandroid/app/build.gradle minSdkVersion; ios/Podfile platform :ios; is web/desktop a target? (unsupported, §2)

VALIDATE: you can state, in one sentence each, the navigation type, the runApp location, whether a login gate exists, and where the app version comes from.


2. Determine compatibility

Supported: Flutter ≥ 3.0 (Dart-3 sound null-safety, Dart 3.0–3.x), Android minSdkVersion ≥ 21 and compileSdk 34+ (floors from flutter_webrtc/livekit_client, not the SDK's Dart), iOS deployment target 13.0. Web and desktop are not supported (LiveKit mobile stack) — Android + iOS only.

TargetFloorNotes
Flutter≥ 3.0Dart-3 baseline.
Dart3.0 – 3.xThe SDK is Dart-3 only.
AndroidminSdk 21, compileSdk 34+Non-negotiable WebRTC floor.
iOS13.0platform :ios, '13.0' in the Podfile.
Web / desktopunsupportedAndroid + iOS only.

If any target is below floor: explain, raise it, re-validate. Do not upgrade/downgrade unrelated dependencies.


3. Install the SDK

# pubspec.yaml
dependencies:
  embed_flutter: ^0.2.0
flutter pub get

VALIDATE: flutter pub get resolves with no version-solve error. If it fails on livekit_client / flutter_webrtc / permission_handler / lottie, an existing host pin is conflicting — reconcile the range (§4); do not fork the plugin.


4. Install and configure dependencies

There is nothing to configure. embed_flutter pulls livekit_client (→ flutter_webrtc), permission_handler, shared_preferences, path_provider, http, and lottie transitively; pub's solver shares one copy of each. Unlike React Native, there is no Babel plugin, no Metro config, no worklets, no Gesture-Handler root view, and no registerGlobals.

Verify a single resolved copy of the native stack:

flutter pub deps | grep -E "livekit_client|flutter_webrtc|permission_handler|lottie"

Expected: exactly one resolved version of each. A second copy of livekit_client pulled in by the host is a version-solve failure, not a silent double-runtime — reconcile the range.


5. Android native integration

  1. Permissions. The SDK ships no package-level AndroidManifest.xml — the host declares the permissions (some, like RECORD_AUDIO/MODIFY_AUDIO_SETTINGS, also merge from flutter_webrtc; declaring them explicitly is safe). Add to android/app/src/main/AndroidManifest.xml inside <manifest>, above <application>:
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <!-- Bluetooth headset routing during calls (Android 12+): -->
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
  2. No MainApplication / MainActivity code is required. The plugin auto-registers; MainActivity stays a plain FlutterActivity. (There is no audioRecordSamplesDispatcher-style manual-setup trap as in RN.)
  3. android/app/build.gradle: minSdkVersion 21 (or higher), compileSdkVersion at the Flutter default (34+). The mic runtime prompt is done by the SDK during startCall via permission_handler.
  4. Build: flutter build apk --debug. On failure decide SDK-related vs pre-existing; fix; rebuild. Never suppress errors.

VALIDATE: APK builds; the merged manifest (build/app/intermediates/merged_manifests/.../AndroidManifest.xml) contains RECORD_AUDIO.


6. iOS native integration

  1. ios/Runner/Info.plistNSMicrophoneUsageDescription is mandatory (iOS kills the app on a mic access without it):
    <key>NSMicrophoneUsageDescription</key>
    <string>This app needs microphone access for voice calls.</string>
  2. ios/Podfile — set the platform and add the permission_handler macro. The SDK requests the mic through permission_handler (Permission.microphone.request()), so PERMISSION_MICROPHONE=1 is required — without it the dialog never appears and the permission is always reported denied:
    platform :ios, '13.0'
    
    post_install do |installer|
      installer.pods_project.targets.each do |target|
        flutter_additional_ios_build_settings(target)
        target.build_configurations.each do |config|
          config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
            '$(inherited)', 'PERMISSION_MICROPHONE=1',
          ]
        end
      end
    end
    Then cd ios && pod install.
  3. No AppDelegate.swift edits are required — there is no LivekitReactNative.setup analog.
  4. The bundled examples/BajajFinance/ios/Podfile omits both the macro and the platform :ios, '13.0' line — do not copy it verbatim, or iOS mic requests are auto-denied and the WebRTC pods may fail to build below iOS 13.

VALIDATE: pod install completes; flutter build ios --no-codesign succeeds; Info.plist has NSMicrophoneUsageDescription.


7. Build & analyzer validation

flutter pub get
dart analyze              # host app must stay at its own baseline
flutter build apk --debug
flutter build ios --no-codesign

VALIDATE: both platform builds compile. A red analyzer error introduced by the integration (e.g. a wrong embedInitialize signature) must be fixed here, not deferred.


8. SDK initialization (exact contract)

Call embedInitialize once, before runApp. All parameters are named; apiKey and appVersion are required:

import 'package:embed_flutter/embed_flutter.dart';

void main() {
  embedInitialize(
    apiKey: API_KEY,                     // REQUIRED (named), from the app's config/env
    appVersion: APP_VERSION,             // REQUIRED (named) — see §10
    embedUrl: EMBED_URL,                 // optional; RevRag-provided host, defaults to prod
    onResult: (bool success, String? error) {
      if (!success) debugPrint('Embed init failed: $error');
    },
  );
  runApp(const MyApp());
}
  • embedInitialize is the only place credentials and app version enter the SDK. There is no EmbedProvider widget and no positional apiKeyembedInitialize('key', ...) will not compile.
  • It is synchronous and fail-open: it never throws at your call site. Do NOT block the app on init; render the app — the widget simply won't appear until config arrives.
  • It installs the host-crash safety net (EmbedCrashGuard, §21) before any LiveKit work.

Then wrap the tree — EmbedWidget must be the outermost widget (above MaterialApp, above any portal/overlay/theme wrapper, or the FAB and highlights end up underneath):

EmbedWidget(
  enabledRoutes: const ['home', 'orders', 'cart', 'checkout'],
  child: MaterialApp(
    navigatorObservers: [EmbedNavigatorObserver()], // REQUIRED, §14
    // ...
  ),
);

VALIDATE: the call compiles with named apiKey: / appVersion:; onResult fires. On the first run with a valid key you see [ApiService] ← GET /initialize status=200 and [Embed][ui_config] … full payload: {…}.


9. API key validation

  • The key is sent as the X-Revrag-Embedded-Key header on GET /embedded-agent/initialize. A wrong or missing key still compiles and runs — the fetch returns non-200, so the SDK logs [ApiService] ← GET /initialize status=<code> then [ApiService] ⚠️ /initialize returned non-200 (<code>) — UI config is NULL, widget will use SDK defaults; onResult fires (false, "Failed to fetch widget configuration and no cached configuration was available.") (when nothing is cached), and the agent button never appears.
  • Do not hard-code secrets in source if an env system exists. If missing → stop, report "API key missing", ask the developer.

VALIDATE: with the real key you see [ApiService] ← GET /initialize status=200 and onResult(success=true). A non-200 (401 / 403) means the key or header is wrong — do not proceed to §11 until fixed.


10. App version validation

  • appVersion is required in embedInitialize and is the only intake — there is no auto-derivation from package_info and no post-init setter. It is sent as ?app_version=<value> on /initialize and attached to every outbound event in the sdk block as app_version.
  • Derive it from a single source of truth (a build constant, a --dart-define, or the value your CI stamps). Never pass "" or a stale literal.

VALIDATE: the /initialize URL query shows ?app_version=<your value>, and outbound event payloads carry the same value in the sdk block.


11. USER_DATA validation

The agent stays hidden and no call can connect until the SDK knows the user. After login, send:

import 'package:embed_flutter/embed_flutter.dart';

embedEvent(
  EventKeys.USER_DATA,
  UserEventPayload(
    app_user_id: userId,                     // required — your stable user id
    data: {'name': 'Priya', 'plan': 'gold'}, // optional context
  ),
);

Facts the integration must respect:

  • Place it immediately after auth succeeds. For a no-auth/demo app, send it in the first screen's initState.
  • Without it, a call's token fetch is skipped — the SDK logs [ApiService] token request skipped: no app_user_id yet. Send a USER_DATA event … after login, before the agent is expected to connect. — so the call cannot connect.
  • The SDK queues events if config hasn't loaded yet and flushes automatically (§18).
  • EmbedWidget.clearUserSession() / embedClearStorageCache() clears identity — re-send USER_DATA for the next user.
  • embedEvent returns a Future<bool> and never throws.

Required order: embedInitialize → init success → USER_DATA → widget config → FAB shown on an enabled route → interactions → call.


12. Widget configuration validation

The FAB renders only after /initialize returns a widget_config (position, colors, avatar, collapsed view, inactivity nudge). It is dashboard-driven — there are no host props for position or colors. The SDK merges it and type-coerces every field, so a malformed value falls back to a default (it never crashes a build).

CaseExpectedObserve
A. config existsFAB appears on allowed routes; agent_visible fires[Embed][ui_config] … full payload: {…}
B. config null / {}default styling (black button, #B391F3 gradient), no crash[Embed][ui_config] … (button_color→black, gradient→#B391F3, text→white)
C. init failsno FAB, app fully functionalonResult(false, …); [ApiService] ⚠️ /initialize returned non-200
D. delayed configapp must not assume config is ready; FAB appears when it landson a cold start the FAB may need the next route change to mount

Also confirm the native cache store: on-device UI-graph writes log under the [EmbedCache] tag; [EmbedCache] store unavailable, memory-only: … means the file store failed and the cache is in memory only.


13. Ask the developer (do not guess)

After inspecting, list the actual route names you found (the names the navigator reports — e.g. home, checkout; not display titles), then ask:

  1. Which routes should the RevRag widget appear on? (Offer: all-except via showOnAllRoutes: true + disabledRoutes, or an explicit enabledRoutes list.)
  2. Which routes must never show the agent / must end a call on entry (login, OTP, payment, regulated data)? → disabledRoutes (entering one ends a live call).
  3. Should a live call survive navigation between agent routes?continuity: EmbedButtonContinuity.keepVisible vs reset (default) (§15).
  4. Do different route groups need different show/delay behavior?buttonVisibility groups (EmbedButtonVisibilityConfig, §15).
  5. App version source if it can't be inferred; API key location.
  6. Any WebView-only, native-picker, or custom-navigation flows (see §22).

Wait for answers before finalizing screen integration.


14. Screen integration (how the SDK actually detects screens)

Register the observer; EmbedWidget wraps MaterialApp. Detection rules by navigator:

  • Named routes / onGenerateRoute — register EmbedNavigatorObserver() on MaterialApp.navigatorObservers. Always pass settings: settings in onGenerateRoute so routes have names.
  • GoRouter — put EmbedNavigatorObserver() in GoRouter(observers: [...]) and use each route's name: (not path:) in enabledRoutes.
  • Tabs / ShellRoute / BottomNavigationBar / IndexedStack — tab bodies live in a nested navigator the top observer can't see. Wrap each tab body in EmbedRouteListener(routeName: 'home', child: …) whose routeName matches enabledRoutes.
  • Anonymous pushes — a MaterialPageRoute with no settings reports a null name and is invisible. Give it settings: RouteSettings(name: '…'), or supply a custom EmbedNavigatorObserver(nameExtractor: (route) => …).
  • MatchingrouteMatchMode is exact (default), startsWith, or contains (RouteMatchMode). Leading slashes are normalized ('home' == '/home'). This is the analog of RN's screen-matching tiers.

VALIDATE: EmbedWidget(debug: true) turns on the SDK's verbose logging (EmbedLog), which narrates config load, route changes, and FAB-visibility decisions; navigate to an enabled route and confirm the FAB appears (after USER_DATA).


15. Continuous vs per-screen — what to verify

Host-side visibility props (RN parity, single-agent):

PropEffect
enabledRoutes: List<String>Routes the FAB shows on (default timing).
showOnAllRoutes: boolShow everywhere; pair with disabledRoutes.
disabledRoutes: List<String>Always hidden; the call ends when the FAB hides here.
continuity: EmbedButtonContinuitykeepVisible = FAB (and call) stay up across enabled routes; reset (default) = re-show/re-delay each route.
embedButtonDelayMs / buttonDelayPre-show delay + EmbedButtonDelayPolicy (perScreen / oncePerGroupEntry / oncePerAppSession).
buttonVisibility: EmbedButtonVisibilityConfigVisibility groups{defaultDelayMs, groups}, each group {id, screens, continuity, delayMs?, delayPolicy?}; first match wins (see INTEGRATION.md §7.2).

Behavior to verify:

  • Under showOnAllRoutes: true, a call is continuous across screens — it is not disconnected on each navigation. Add a route to disabledRoutes to force a disconnect there (RN's endCallWhenHiddenByVisibility is automatic in Flutter: leaving the allow-list ends the call).
  • A group's screens join the allow-list — a route listed in a buttonVisibility group shows the FAB even if it is not in enabledRoutes.

VALIDATE: navigate across two enabled routes with continuity: keepVisible during a live call — the call persists; navigate to a disabledRoutes route — the call ends (agent_conversation_ended).


16. Event ordering validation

Confirm this order in the logs on a healthy first session, and that nothing fires out of order:

  1. [ApiService] ← GET /initialize status=200[Embed][ui_config] … full payload
  2. USER_DATA sent (after login) → token fetch unlocked
  3. agent_visible on an enabled route
  4. on FAB tap / startCall: [CallTiming] t=0ms startCall() invoked[CallTiming] t=<ms>ms …ROOM CONNECTED — call is liveagent_conversation_started (carries a flat call_id)
  5. on end: agent_conversation_ended (same call_id)

Invalid patterns to catch: a call before USER_DATA ([ApiService] token request skipped: no app_user_id yet …), agent_visible never firing (re-check §11 and §14), a second EmbedWidget in the tree.


17. UI tree / snapshot validation

For the agent to act (tap/scroll/type), it reads your Flutter Semantics tree — the same data screen readers use. Mission frames arrive on the LiveKit data channel ([EmbedWidget] 📩 LiveKit DataReceived); element highlights log [Embed HIGHLIGHT] pulse stableId=…; the durable on-device UI-graph writes under [EmbedCache]. Deep guidance lives in HOST_INTEGRATION.md; the essentials:

  • Icon-only buttons need Semantics(label: …, button: true, …).
  • Chips/toggles must surface selection (Semantics(selected: …) or use ChoiceChip/FilterChip).
  • Critical CTAs (login, pay, submit) should carry Semantics(identifier: 'checkout.pay') — the SDK matches identifier first.
  • Secure fields (TextField(obscureText: true)SemanticsFlag.isObscured) are redacted at capture and stay redacted (sticky) — typed secrets never leave the device in the UI snapshot.
  • Snapshot/cache capture is gated by action_config.flags.dataCapture (dashboard-side). Host query: EmbedWidget.isWidgetTreeSnapshotEnabled.

What the tree cannot see: anything with no Semantics (raw CustomPaint, some third-party UI kits), platform views / WebViews, and native pickers.

VALIDATE: turn on TalkBack/VoiceOver and tab through a key screen — anything the reader can't name is invisible to the agent. Missions failing with target_not_found point here. An over-budget screen logs [EmbedCache] screen "…" is <N> bytes, over the ….


18. Event / click-capture validation

Automatic capture. The SDK hit-tests pointer events to detect taps on buttons and watches TextField controllers for text changes — all gated by the dashboard clickTracking flag (ActionGates.clickTrackingEnabled) and only while the widget is active (EmbedWidget.isActive). Rapid taps (3 within 1 s) emit a rage_click analytics event automatically. This is the Flutter analog of RN's touchable patching, done through the gesture/hit-test layer rather than by wrapping widgets.

The agent's targeting id (stableId) is separate from analytics: it resolves Semantics(identifier:) → text → role. Put a stable identifier on every meaningful control:

Semantics(identifier: 'submit_interest', button: true, child: MyButton(onTap: submit));

Explicit context. Send app context so the planner disambiguates screens/actions:

embedEvent(EventKeys.SCREEN_STATE,
  ScreenEventPayload(screen: 'checkout_step_2', data: {'step': 2}));
embedEvent(EventKeys.CUSTOM_EVENT,
  CustomEventPayload(data: {'action': 'plan_selected', 'plan_id': 'gold'}));
embedEvent(EventKeys.ANALYTICS_DATA,
  AnalyticsDataEventPayload(event_name: 'payment_completed', data: {'amount': 999}));

Observe agent events with embedOnAgent(...) / SdkEventName (see INTEGRATION.md §9).

VALIDATE: events flush and return 2xx only once the FAB is live on an enabled route (EmbedWidget.isActive && EmbedWidget.isLive) and USER_DATA is in place; before that they sit in a pending queue and are dropped if older than 60 s (verified: age > 60). An event sent on a disabled route waits until you reach an enabled one.


19. Call validation

Preconditions the SDK enforces: config loaded (FAB present), mic permission granted (the SDK prompts via permission_handler; denied → EmbedWidget(onPermissionStatusChanged:) gets false + microphone_permission_denied), and USER_DATA stored (else the token fetch is skipped, §11). Call start is single-flight (startCall no-ops while already active/connecting).

Programmatic API (see INTEGRATION.md §10):

await EmbedWidget.startCall();                                   // co-pilot (default)
await EmbedWidget.startCall(agentTriggerMode: AgentTriggerMode.WORKFLOW);
await EmbedWidget.endCall();
EmbedWidget.isCallActive;                                        // bool
EmbedWidget.collapseWidget();  EmbedWidget.expandWidget();       // bar <-> FAB
await EmbedWidget.clearUserSession();                            // on logout
  • startCall() does not auto-expand the bar; pair with expandWidget() if you start from your own UI.
  • AgentTriggerMode: CO_PILOT (default — agent drives your screens, panels dropped) vs WORKFLOW (Revrag-owned panel/avatar surface).
  • Not present in the Flutter SDK (unlike RN): pauseAgent / resumeAgent / isAgentPaused / isWidgetExpanded. Do not reference them.

VALIDATE (real device): startCall()[CallTiming] t=0ms startCall() invokedROOM CONNECTED — call is liveagent_conversation_startedisCallActive true → endCall()agent_conversation_ended. Then a second call starts cleanly.


20. Error / failure conditions to test explicitly

ConditionExpected (correct) behavior
Invalid apiKeyNon-200 (e.g. 401/403), UI config NULL, onResult(false, …), FAB hidden, no crash.
appVersion blank/stale/initialize still fires with whatever was passed; supply a real value (§10).
USER_DATA never sentFAB never shows; token fetch skipped (token request skipped: no app_user_id yet); events queue then drop after 60 s.
Mic permission deniedonPermissionStatusChanged(false) / microphone_permission_denied; no crash.
Backend widget_config = null/{}SDK uses default styling; FAB still works.
Wrong-typed backend config fieldCoerced to a default; no CastError.
Navigate to disabledRoutes mid-callCall ends cleanly.
Bad embedEvent payload typeReturns false (does not throw).
Forced LiveKit renegotiation errorSuppressed by the crash-guard; host process survives (§21).

21. Crash visibility — grep for these

The SDK is engineered so nothing it does can crash the host (INTEGRATION.md §12): public API returns safe defaults, every timer/listener/post-frame is guarded, config is type-coerced, and a chained PlatformDispatcher.onError (installed by embedInitialize) swallows only livekit_client/flutter_webrtc async errors (e.g. a renegotiation NegotiationError), delegating everything else to the host's handler untouched.

Grep your crash reporting (Sentry / Crashlytics / logs) for these real SDK lines:

  • [EmbedSDK] suppressed voice-stack async error — the guard working as intended (informational; a LiveKit/WebRTC error was kept from crashing the host).
  • [ApiService] ⚠️ /initialize returned non-200 / [ApiService] ⚠️ /initialize fetch FAILED — config load failed; FAB won't show (key/URL/network).
  • [ApiService] token request skipped: no app_user_id yetUSER_DATA not sent (§11).
  • [EmbedCache] sync permanently rejected status=… / [EmbedCache] store unavailable, memory-only — UI-graph sync/store problems (non-fatal).
  • Embed init failed (from your onResult) — surfaces config-load failures.

Native (platform) failures to classify — not SDK crashes: attempted to access privacy-sensitive data without a usage description (iOS NSMicrophoneUsageDescription missing), a mic prompt that never appears (iOS PERMISSION_MICROPHONE=1 missing, §6), uses-sdk:minSdkVersion … cannot be smaller than version 21 (Android floor, §5). Never swallow a native crash; classify app-side vs SDK-side.


22. Unknown-unknowns — inspect the host for these (detect → why → do)

Work through every row. "Ask" = a developer decision.

UI / layout

  • EmbedWidget not outermost → a global overlay/portal/theme wrapper above it can occlude the FAB and highlights; keep EmbedWidget above MaterialApp; verify on a device.
  • Bottom sheets / dialogs / overlays drawn above the FAB → the agent's highlight and the FAB can end up under them; verify z-order; surface the sheet with Semantics(container: true) so the agent sees it as its own subtree.
  • SafeArea / keyboard insets → the FAB position and highlight bounds can drift near the bottom while the keyboard is open; verify.

Components / design system (Semantics)

  • Icon-only buttons without Semantics(label:) → invisible/positional ids that shift; label them and set button: true.
  • Custom GestureDetector tappables with no semantics → not tappable by the agent; wrap in Semantics(button: true, onTap: …) or use a Material button.
  • Selection shown by style only (custom radios/chips/tabs) → verification needs Semantics(selected:) / Semantics(checked:); add it (or use ChoiceChip/Switch).
  • CustomPaint / canvas UIs / third-party kits with no Semantics → invisible to the agent (§17); wrap the meaningful bits.

Inputs / forms

  • Uncontrolled TextField (no controller) → harder to verify typed text and to detect changes for auto-capture; prefer a TextEditingController.
  • Password / OTP fields → mark them obscureText: true so SemanticsFlag.isObscured redacts them at capture (sticky). Redaction is per node — mark each secret field, not the wrapper.
  • Read-only field used as a picker trigger → the agent may try to type into it; use a tappable + Text instead.

Lists / performance

  • Lazy lists (ListView.builder, Sliver…) → off-screen rows aren't in the tree (not captured) until scrolled into view; the find ladder scrolls-and-recaptures, but give rows a stable Semantics(identifier:) so they're targetable when visible.
  • Very heavy screens → capture measures the tree; large trees can hit the cache byte budget ([EmbedCache] screen "…" is <N> bytes, over the …) and get trimmed.

Navigation-adjacent

  • Anonymous routes — a MaterialPageRoute without settings ⇒ no route name ⇒ invisible to the observer; give every route a name.
  • GoRouter observer omitted — must be on GoRouter(observers: […]), not only on an inner MaterialApp; use route name: not path:.
  • Tabs via IndexedStack without EmbedRouteListener — the SDK can't tell which tab is active; wrap each tab body.
  • Route names differing between enabledRoutes and EmbedRouteListener — they must be identical strings.
  • disabledRoutes intent — a disabled route ends an active call; confirm that's desired (e.g. a payment WebView screen).

Native / platform

  • Backgrounded calls (iOS) → the verified setup covers foreground calls. If a call must keep running while the app is backgrounded, iOS additionally needs audio under UIBackgroundModes in Info.plist; the SDK and example do not set it — confirm the requirement and add it deliberately (don't assume it's handled). Ask.
  • Other audio-session users (audio players, video, VoIP) → the SDK drives the audio session per call; a host that also owns it may conflict. Ask.
  • onPermissionStatusChanged not wired → an EmbedWidget callback prop; if the host doesn't pass it, a mic denial is silent to the host UI (the SDK still doesn't crash and still emits microphone_permission_denied).

Build / tooling

  • Transitive-dependency version clash — the SDK pins livekit_client, flutter_webrtc, permission_handler, shared_preferences, path_provider, http, and lottie. A conflicting host major (especially lottie / permission_handler) fails the pub version solve; reconcile the range, don't fork.
  • Existing livekit_client / flutter_webrtc at a conflicting version → reconcile the pub range; do not vendor a second copy.
  • Web/desktop build target → unsupported (§2); guard the widget out of those flavors.

Runtime / lifecycle

  • Host overriding PlatformDispatcher.onError after embedInitialize → the host's handler wins; make sure it re-chains, or the LiveKit crash-guard (§21) is lost.
  • Sentry/Crashlytics console capture → the guard's informational [EmbedSDK] suppressed voice-stack async error line is expected, not a fault; don't page on it.

Security / privacy

  • disabledRoutes ≠ no capture of other screens — the durable UI-graph cache is written per screen while capture is enabled (dataCapture flag). Mark sensitive fields (obscureText for passwords/OTP — a hard guarantee) and ask the developer about regulated screens.
  • Store-listing disclosure — the app requests microphone (and Bluetooth) permissions; disclose in the listing.

23. Final checklist & report

[ ] Flutter/Dart version + navigation type detected     [ ] embed_flutter ^0.2.0 installed, pub get clean
[ ] Peers resolve to one copy each (livekit/webrtc/permission_handler/lottie)
[ ] Android: manifest permissions added, minSdk≥21, compileSdk 34+   [ ] APK builds
[ ] iOS: NSMicrophoneUsageDescription, Podfile PERMISSION_MICROPHONE=1 + platform 13.0, pod install   [ ] iOS builds
[ ] embedInitialize(apiKey:, appVersion:) once before runApp; fail-open; no "Embed init failed"
[ ] EmbedWidget outermost, wrapping MaterialApp; EmbedNavigatorObserver registered; EmbedRouteListener on tabs
[ ] USER_DATA sent after login (and re-sent after clearUserSession)
[ ] Screens + continuity + disabledRoutes chosen by developer
[ ] FAB appears on chosen routes; null/delayed config handled; one widget instance
[ ] Navigation tested (A→B→A, nested tabs, modal); continuity as chosen
[ ] UI tree reachable via Semantics; secure fields redacted; identifiers on key controls
[ ] Click/text auto-capture verified (clickTracking on); rage_click fires
[ ] Call start/connect/end/re-start tested on a real device; mic denial handled
[ ] Error conditions (§20) verified; no host crash (§21)
[ ] §22 unknown-unknowns reviewed

Final report format

Integration Status: SUCCESS / PARTIAL / FAILED
SDK version: | Flutter: | Dart: | Navigation: | Android minSdk: | iOS target:
Dependencies added/changed (+why):
Native changes (Android manifest / iOS Info.plist + Podfile):
Init: embedInitialize placement + fail-open confirmed:
Widget config — routes: | continuity: | disabledRoutes: | buttonVisibility groups:
Validation — Init: | API key: | app_version: | USER_DATA: | Widget config: | FAB rendering: |
             UI tree/Semantics: | Event capture: | Call flow: | Android build: | iOS build: | Crash validation:
Unknown-unknowns found + handling:
Remaining issues (and whether app-side or SDK-side):

Do not declare SUCCESS while any critical validation is failing.


Companion docs: INTEGRATION.md (full mounting + navigator examples), HOST_INTEGRATION.md (action-intelligence / Semantics), PUBLIC_API.md (API reference), COMPATIBILITY.md (dependency matrix).