# Flutter Copilot Integration > Master prompt for an AI coding agent (Copilot / Cursor / Claude Code) to integrate embed_flutter end-to-end. URL: /embed/llmText/flutter-copilotintegration Markdown: /embed/llmText/flutter-copilotintegration.md # RevRag Flutter SDK — AI Agent Integration Master Prompt [#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 [#1-inspect-the-existing-application] Record each of these before changing anything: | What | How to find it | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flutter / Dart version | `flutter --version`; `environment:` in `pubspec.yaml` | | Navigation | `MaterialApp` named routes / `onGenerateRoute`, `MaterialApp.router` + `GoRouter`, `Navigator 2.0`, or a tab shell (`ShellRoute` / `BottomNavigationBar` / `IndexedStack`) | | Where `runApp` mounts | `lib/main.dart` — is there an auth/login gate before the app mounts? | | Root widget | the widget passed to `runApp(...)`; what wraps `MaterialApp` | | **Existing LiveKit / WebRTC** | `grep -n "livekit_client\|flutter_webrtc" pubspec.yaml pubspec.lock` — note versions | | Existing shared peers | `permission_handler`, `shared_preferences`, `path_provider`, `http`, `lottie` in `pubspec.yaml` (conflict risk, §4/§22) | | Providers above the root | any global overlay/portal/theme wrapper that sits above `MaterialApp` (occlusion risk, §22) | | Permissions already declared | `android/app/src/main/AndroidManifest.xml`, `ios/Runner/Info.plist` | | App version source | a build constant, `--dart-define`, CI-stamped value, or `package_info` — **the SDK does NOT auto-derive it** (§10) | | Platform targets | `android/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 [#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. | Target | Floor | Notes | | ------------- | --------------------------------- | --------------------------------------- | | Flutter | **≥ 3.0** | Dart-3 baseline. | | Dart | **3.0 – 3.x** | The SDK is Dart-3 only. | | Android | **`minSdk 21`**, `compileSdk 34+` | Non-negotiable WebRTC floor. | | iOS | **13.0** | `platform :ios, '13.0'` in the Podfile. | | Web / desktop | **unsupported** | Android + iOS only. | If any target is below floor: explain, raise it, re-validate. **Do not upgrade/downgrade unrelated dependencies.** *** ## 3. Install the SDK [#3-install-the-sdk] ```yaml # pubspec.yaml dependencies: embed_flutter: ^0.2.0 ``` ```bash 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 [#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: ```bash 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 [#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 ``, above ``: ```xml ``` 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 [#6-ios-native-integration] 1. **`ios/Runner/Info.plist`** — `NSMicrophoneUsageDescription` is **mandatory** (iOS kills the app on a mic access without it): ```xml NSMicrophoneUsageDescription This app needs microphone access for voice calls. ``` 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: ```ruby 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 [#7-build--analyzer-validation] ```bash 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) [#8-sdk-initialization-exact-contract] Call `embedInitialize` **once, before `runApp`.** All parameters are **named**; `apiKey` and `appVersion` are **required**: ```dart 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 `apiKey`** — `embedInitialize('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): ```dart 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 [#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=` then `[ApiService] ⚠️ /initialize returned non-200 () — 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 [#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=` 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=`, and outbound event payloads carry the same value in the `sdk` block. *** ## 11. USER\_DATA validation [#11-user_data-validation] The agent stays hidden and no call can connect until the SDK knows the user. **After login**, send: ```dart 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` and never throws. Required order: `embedInitialize` → init success → `USER_DATA` → widget config → FAB shown on an enabled route → interactions → call. *** ## 12. Widget configuration validation [#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). | Case | Expected | Observe | | --------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- | | A. config exists | FAB 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 fails | no FAB, app fully functional | `onResult(false, …)`; `[ApiService] ⚠️ /initialize returned non-200` | | D. delayed config | app must not assume config is ready; FAB appears when it lands | on 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) [#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) [#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) => …)`. * **Matching** — `routeMatchMode` 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 [#15-continuous-vs-per-screen--what-to-verify] Host-side visibility props (RN parity, single-agent): | Prop | Effect | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `enabledRoutes: List` | Routes the FAB shows on (default timing). | | `showOnAllRoutes: bool` | Show everywhere; pair with `disabledRoutes`. | | `disabledRoutes: List` | Always hidden; **the call ends when the FAB hides here.** | | `continuity: EmbedButtonContinuity` | `keepVisible` = FAB (and call) stay up across enabled routes; `reset` (default) = re-show/re-delay each route. | | `embedButtonDelayMs` / `buttonDelay` | Pre-show delay + `EmbedButtonDelayPolicy` (`perScreen` / `oncePerGroupEntry` / `oncePerAppSession`). | | `buttonVisibility: EmbedButtonVisibilityConfig` | **Visibility 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 [#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 …` → `ROOM CONNECTED — call is live` → `agent_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 [#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 bytes, over the …`. *** ## 18. Event / click-capture validation [#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: ```dart Semantics(identifier: 'submit_interest', button: true, child: MyButton(onTap: submit)); ``` **Explicit context.** Send app context so the planner disambiguates screens/actions: ```dart 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 [#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): ```dart 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() invoked` → `ROOM CONNECTED — call is live` → `agent_conversation_started` → `isCallActive` true → `endCall()` → `agent_conversation_ended`. Then a second call starts cleanly. *** ## 20. Error / failure conditions to test explicitly [#20-error--failure-conditions-to-test-explicitly] | Condition | Expected (correct) behavior | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Invalid `apiKey` | Non-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 sent | FAB never shows; token fetch skipped (`token request skipped: no app_user_id yet`); events queue then drop after 60 s. | | Mic permission denied | `onPermissionStatusChanged(false)` / `microphone_permission_denied`; no crash. | | Backend `widget_config` = `null`/`{}` | SDK uses default styling; FAB still works. | | Wrong-typed backend config field | Coerced to a default; no `CastError`. | | Navigate to `disabledRoutes` mid-call | Call ends cleanly. | | Bad `embedEvent` payload type | Returns `false` (does not throw). | | Forced LiveKit renegotiation error | Suppressed by the crash-guard; host process survives (§21). | *** ## 21. Crash visibility — grep for these [#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 yet` — `USER_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) [#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 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 [#23-final-checklist--report] ```text [ ] 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** ```text 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).*