> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revrag.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Flutter

> Step-by-step guide to integrate the RevRag Flutter embed SDK (voice agent, EmbedWidget, navigation-aware visibility, events, configuration, and native platform setup).

# Embed Flutter SDK

Follow this guide in order the first time you integrate. **Native microphone setup is required** on both Android and iOS — skipping it is the most common source of "the mic dialog never appears" and silent voice failures.

***

## Introduction

The **`embed_flutter`** SDK adds a **voice AI agent** to your app: a **floating action button (FAB)** backed by **LiveKit**, **navigation-aware** visibility, and a **user-context channel** to your embed backend.

**Package:** `embed_flutter` **·** **Version:** `0.0.17` **·** **Requires:** Flutter ≥ 3.0.0 / Dart ≥ 3.0.0

**What you get out of the box**

* **Realtime voice** with the agent through the FAB
* **Screen and app context** for richer conversations (route tracking via `EmbedNavigatorObserver` / `EmbedRouteListener`, optional explicit `SCREEN_STATE`)
* **Event tracking**: host-driven analytics and custom payloads via `embedEvent`, plus **agent lifecycle** signals (`embedOnAgent`)
* **Server-driven UI** for the FAB via `widget_config` from device registration
* **Advanced FAB behavior** (route **flows/groups**, show **delays** + policies, **insets**, continuity rules): covered in **[EmbedWidget advanced](/embed/integration/embed-flutter-advance)** — read it once you move past a simple `enabledRoutes` list

***

## 1. Installation

Add the package to your `pubspec.yaml`:

```yaml theme={null}
dependencies:
  embed_flutter: ^0.0.17
```

Then fetch:

```bash theme={null}
flutter pub get
```

### 1.1 Android Setup

Open `android/app/src/main/AndroidManifest.xml` and add:

```xml theme={null}
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MICROPHONE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- WAKE_LOCK keeps the CPU active during voice calls so audio is not interrupted
     when the screen turns off. Required by the underlying WebRTC (LiveKit) engine. -->
```

### 1.2 iOS Setup

**Step 1 — Info.plist**

Add the microphone usage description to your `ios/Runner/Info.plist`:

```xml theme={null}
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for voice calls.</string>
```

**Step 2 — Podfile**

The SDK requires explicit configuration in your `ios/Podfile` to enable microphone permission requests. Add the `PERMISSION_MICROPHONE=1` macro inside your `post_install` block:

```ruby theme={null}
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)',
        ## dart: PermissionGroup.microphone
        'PERMISSION_MICROPHONE=1',
        # ...other permissions...
      ]
    end
  end
end
```

<Warning>
  Without the `PERMISSION_MICROPHONE=1` macro in the Podfile, the microphone permission dialog will never appear on iOS and the permission will be reported as permanently denied — even if it has not been requested before.
</Warning>

After editing the Podfile, run:

```bash theme={null}
cd ios && pod install && cd ..
```

***

## 2. How the SDK Works (Mental Model)

Understanding these three concepts up front makes integration straightforward:

| Concept                                         | Description                                                                                                                                           |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **EmbedWidget**                                 | A full-screen overlay wrapper placed at the very top of your widget tree. It renders the floating AI agent button and manages the live voice session. |
| **EmbedNavigatorObserver / EmbedRouteListener** | Tells the SDK which screen the user is on so it knows whether to show the agent button.                                                               |
| **enabledRoutes**                               | A map of route names where the agent button should appear. Only routes listed here will show the agent.                                               |

**Order of operations every time:**

```
main()
  └─ embedInitialize(apiKey, flowName: 'main')     ← before runApp
  └─ runApp(
       EmbedWidget(                                ← outermost widget
         enabledRoutes: {
           'main': ['home', 'product', 'cart'],
         },
         child: MaterialApp / Router(
           navigatorObservers: [EmbedNavigatorObserver()],  ← required
           ...
         ),
       ),
     )

After authentication → embedEvent(USER_DATA, ...)  ← activates the agent
```

***

## 3. Initialization

Call `embedInitialize()` **before** `runApp()`.

```dart theme={null}
import 'package:flutter/material.dart';
import 'package:embed_flutter/embed_flutter.dart';

void main() {
  embedInitialize(
    'your-api-key',
    flowName: 'main',                        // identifies your agent configuration
    embedUrl: 'https://embed.revrag.ai',     // optional; defaults to revrag.ai
    onResult: (bool success, String? error) {
      // Called after the SDK prefetches the UI config from the server.
      if (!success) {
        print('Embed init failed: $error');
      }
    },
  );

  runApp(const MyApp());
}
```

**Optional: set app version** (sent with every analytics event):

```dart theme={null}
embedSetAppVersion('2.4.1');
```

***

## 4. Wrapping Your App with EmbedWidget

`EmbedWidget` must be the **outermost** widget — it wraps your `MaterialApp` / `MaterialApp.router` / `CupertinoApp`.

```dart theme={null}
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      showEmbedWidget: true,
      // List the routes where the agent button should appear
      enabledRoutes: const {
        'main': ['home', 'product_list', 'product_detail', 'cart'],
      },
      child: MaterialApp(
        navigatorObservers: [EmbedNavigatorObserver()],
        // ... rest of your app
      ),
    );
  }
}
```

<Note>
  The agent button appears **only** on routes listed in `enabledRoutes`. On all other routes (e.g. splash, login, settings) it is hidden automatically.
</Note>

Alternatively, to show the agent on **every** screen except a few:

```dart theme={null}
embedEvent(
  EventKeys.CUSTOM_EVENT,
  CustomEventPayload(
    data: {
      'context': 'pricing_screen',
      'event': 'option_selected',
      'option_id': 'plan_12_months',
      'price_per_month': 249,
      'currency': 'INR',
    },
  ),
);
```

#### Available Events

| Event          | Purpose                                          | Required     | When to Use                                                    |
| -------------- | ------------------------------------------------ | ------------ | -------------------------------------------------------------- |
| `USER_DATA`    | Initialize user context and activate EmbedWidget | **Required** | After user authentication or when user ID is available         |
| `SCREEN_STATE` | Provide screen context and navigation info       | Optional     | When navigating between screens or when screen context changes |
| `CUSTOM_EVENT` | Capture bespoke user interactions                | Optional     | Whenever you need additional tracking for specific actions     |

### Basic Usage

Here's a complete example showing how to use the SDK with flow-based activation:

```dart theme={null}
import 'package:flutter/material.dart';
import 'package:embed_flutter/embed_flutter.dart';

void main() {
  embedInitialize('your-api-key', flowName: 'main');
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      showEmbedWidget: true,
      enabledRoutes: const {
        'main': ['product_list', 'product_detail', 'cart'],
      },
      child: MaterialApp(
        // ✅ Required — tracks route changes
        navigatorObservers: [EmbedNavigatorObserver()],
        initialRoute: '/',
        routes: {
          '/':              (ctx) => const SplashScreen(),
          'home':           (ctx) => const HomeScreen(),
          'product_list':   (ctx) => const ProductListScreen(),
          'product_detail': (ctx) => const ProductDetailScreen(),
          'cart':           (ctx) => const CartScreen(),
          'checkout':       (ctx) => const CheckoutScreen(),
        },
      ),
    );
  }
}
```

**Navigating between routes:**

```dart theme={null}
// Push
Navigator.pushNamed(context, 'product_list');

// Push with arguments
Navigator.pushNamed(context, 'product_detail', arguments: {'id': 42});

// Replace (useful for tabs — see section 5.5)
Navigator.pushReplacementNamed(context, 'cart');
```

`EmbedNavigatorObserver` intercepts every push, pop, and replace and shows or hides the agent button based on whether the new route is in `enabledRoutes`.

***

### 5.2 MaterialApp with onGenerateRoute

When routes carry parameters you typically use `onGenerateRoute`. Everything else is identical.

```dart theme={null}
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      showEmbedWidget: true,
      enabledRoutes: const {
        'main': ['welcome', 'personal_info', 'address', 'review'],
      },
      child: MaterialApp(
        navigatorObservers: [EmbedNavigatorObserver()], // ✅ required
        home: const SplashScreen(),
        onGenerateRoute: (settings) {
          switch (settings.name) {
            case 'welcome':
              return MaterialPageRoute(
                settings: settings,   // ← always pass settings so the observer reads the name
                builder: (_) => const WelcomeScreen(),
              );
            case 'personal_info':
              final args = settings.arguments as Map<String, dynamic>?;
              return MaterialPageRoute(
                settings: settings,
                builder: (_) => PersonalInfoScreen(data: args),
              );
            case 'address':
              return MaterialPageRoute(
                settings: settings,
                builder: (_) => const AddressScreen(),
              );
            case 'review':
              return MaterialPageRoute(
                settings: settings,
                builder: (_) => const ReviewScreen(),
              );
            default:
              return null;
          }
        },
      ),
    );
  }
}
```

<Tip>
  Always pass `settings: settings` to `MaterialPageRoute`. Without it the route name is `null` and the SDK cannot determine whether to show the agent.
</Tip>

***

### 5.3 GoRouter

GoRouter uses the `name` field of each `GoRoute` for matching. Pass the observer in `GoRouter.observers`.

```dart theme={null}
import 'package:go_router/go_router.dart';
import 'package:embed_flutter/embed_flutter.dart';

// ── Router definition ─────────────────────────────────────────────────────────
final _router = GoRouter(
  initialLocation: '/splash',
  // ✅ Add observer here
  observers: [EmbedNavigatorObserver()],
  routes: [
    GoRoute(
      path: '/splash',
      name: 'splash',
      builder: (context, state) => const SplashScreen(),
    ),
    GoRoute(
      path: '/home',
      name: 'home',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/product/:id',
      name: 'product_detail',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return ProductDetailScreen(id: id);
      },
    ),
    GoRoute(
      path: '/cart',
      name: 'cart',
      builder: (context, state) => const CartScreen(),
    ),
    GoRoute(
      path: '/payment',
      name: 'payment',
      builder: (context, state) => const PaymentScreen(),
    ),
  ],
);

// ── main ──────────────────────────────────────────────────────────────────────
void main() {
  embedInitialize('your-api-key', flowName: 'main');
  runApp(const MyApp());
}

// ── App widget ────────────────────────────────────────────────────────────────
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      showEmbedWidget: true,
      // Use the `name:` field from GoRoute — not the path
      enabledRoutes: const {
        'main': ['home', 'product_detail', 'cart', 'payment'],
      },
      child: MaterialApp.router(
        routerConfig: _router,
      ),
    );
  }
}
```

**Navigating with GoRouter:**

```dart theme={null}
// By name
context.goNamed('cart');

// By path
context.go('/cart');

// Push (adds to stack)
context.pushNamed('product_detail', pathParameters: {'id': '42'});
```

#### GoRouter with `nameExtractor` (path-based matching)

If you prefer to match on URL paths instead of route names, supply a `nameExtractor` to the observer:

```dart theme={null}
EmbedNavigatorObserver(
  nameExtractor: (route) {
    final name = route.settings.name ?? '';
    if (name.startsWith('/product/')) return 'product_detail';
    if (name == '/cart') return 'cart';
    return null; // null = SDK ignores this push
  },
)
```

Then use those extracted names in `enabledRoutes` as usual.

***

### 5.4 GoRouter with ShellRoute (Bottom Tab Navigator)

`ShellRoute` keeps a persistent shell (e.g. a bottom navigation bar) while swapping child routes. The challenge is that `ShellRoute` children run inside a **nested navigator** — the top-level observer does not fire for them.

**Solution:** Wrap each tab's screen with `EmbedRouteListener`. This widget notifies the SDK of the current screen whenever the tab is displayed.

```dart theme={null}
import 'package:go_router/go_router.dart';
import 'package:embed_flutter/embed_flutter.dart';

// ── Router ────────────────────────────────────────────────────────────────────
final _router = GoRouter(
  initialLocation: '/home',
  observers: [EmbedNavigatorObserver()], // catches pushes outside the shell
  routes: [
    ShellRoute(
      builder: (context, state, child) => AppShell(child: child),
      routes: [
        GoRoute(
          path: '/home',
          name: 'home',
          builder: (context, state) => const HomeTab(),
        ),
        GoRoute(
          path: '/offers',
          name: 'offers',
          builder: (context, state) => const OffersTab(),
        ),
        GoRoute(
          path: '/profile',
          name: 'profile',
          builder: (context, state) => const ProfileTab(),
        ),
        // Sub-route inside a tab (pushed onto the nested navigator)
        GoRoute(
          path: '/offers/detail/:id',
          name: 'offer_detail',
          builder: (context, state) {
            final id = state.pathParameters['id']!;
            return OfferDetailScreen(id: id);
          },
        ),
      ],
    ),
  ],
);

// ── Shell scaffold ────────────────────────────────────────────────────────────
class AppShell extends StatelessWidget {
  final Widget child;
  const AppShell({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    final location = GoRouterState.of(context).uri.toString();
    int currentIndex = 0;
    if (location.startsWith('/offers')) currentIndex = 1;
    if (location.startsWith('/profile')) currentIndex = 2;

    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentIndex,
        onTap: (i) {
          switch (i) {
            case 0: context.go('/home');    break;
            case 1: context.go('/offers');  break;
            case 2: context.go('/profile'); break;
          }
        },
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home),        label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.local_offer), label: 'Offers'),
          BottomNavigationBarItem(icon: Icon(Icons.person),      label: 'Profile'),
        ],
      ),
    );
  }
}

// ── Tab screens ───────────────────────────────────────────────────────────────
// Wrap each tab with EmbedRouteListener so the SDK knows which tab is active.

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

  @override
  Widget build(BuildContext context) {
    return EmbedRouteListener(
      routeName: 'home', // ✅ must match the name in enabledRoutes
      child: Scaffold(
        appBar: AppBar(title: const Text('Home')),
        body: const Center(child: Text('Home content')),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return EmbedRouteListener(
      routeName: 'offers', // ✅ agent visible — listed in enabledRoutes
      child: Scaffold(
        appBar: AppBar(title: const Text('Offers')),
        body: const Center(child: Text('Browse offers')),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return EmbedRouteListener(
      routeName: 'profile', // agent hidden — not listed in enabledRoutes
      child: Scaffold(
        appBar: AppBar(title: const Text('Profile')),
        body: const Center(child: Text('Your profile')),
      ),
    );
  }
}

// ── main ──────────────────────────────────────────────────────────────────────
void main() {
  embedInitialize('your-api-key', flowName: 'main');
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      showEmbedWidget: true,
      enabledRoutes: const {
        'main': ['home', 'offers', 'offer_detail'],
        // 'profile' is intentionally excluded — agent hidden there
      },
      child: MaterialApp.router(routerConfig: _router),
    );
  }
}
```

**Key rules for ShellRoute:**

| Rule                                                               | Why                                                                |
| ------------------------------------------------------------------ | ------------------------------------------------------------------ |
| Add `EmbedNavigatorObserver()` to the top-level `GoRouter`         | Catches pushes onto the root navigator (screens outside the shell) |
| Wrap each tab's screen with `EmbedRouteListener(routeName: '...')` | Tells the SDK which tab is currently visible                       |
| The `routeName` must match what you put in `enabledRoutes`         | Otherwise the SDK cannot determine visibility                      |

***

### 5.5 MaterialApp Bottom TabNavigator (BottomNavigationBar)

When using `BottomNavigationBar` with a plain `MaterialApp`, push a new named route for each tab using `Navigator.pushReplacementNamed` so the route stack stays shallow and the observer fires on every tab switch.

```dart theme={null}
// ── main.dart ─────────────────────────────────────────────────────────────────
void main() {
  embedInitialize('your-api-key', flowName: 'main');
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      showEmbedWidget: true,
      // Agent appears only on the Plans and Exclusive tabs
      enabledRoutes: const {
        'main': ['plans_tab', 'exclusive_tab'],
      },
      child: MaterialApp(
        navigatorObservers: [EmbedNavigatorObserver()], // ✅ required
        home: const HomeTab(),
        onGenerateRoute: (settings) {
          switch (settings.name) {
            case 'home_tab':
              return MaterialPageRoute(settings: settings, builder: (_) => const HomeTab());
            case 'plans_tab':
              return MaterialPageRoute(settings: settings, builder: (_) => const PlansTab());
            case 'exclusive_tab':
              return MaterialPageRoute(settings: settings, builder: (_) => const ExclusiveTab());
            case 'profile_tab':
              return MaterialPageRoute(settings: settings, builder: (_) => const ProfileTab());
            default:
              return null;
          }
        },
      ),
    );
  }
}

// ── Shared bottom nav scaffold ────────────────────────────────────────────────
class AppNavBar extends StatelessWidget {
  final Widget child;
  final int currentIndex;

  const AppNavBar({super.key, required this.child, required this.currentIndex});

  static const _routes = ['home_tab', 'plans_tab', 'exclusive_tab', 'profile_tab'];

  void _onTap(BuildContext context, int index) {
    if (index == currentIndex) return;
    // pushReplacementNamed so the back button doesn't cycle through tab history
    // and EmbedNavigatorObserver fires didReplace → SDK updates its route state
    Navigator.pushReplacementNamed(context, _routes[index]);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentIndex,
        type: BottomNavigationBarType.fixed,
        selectedItemColor: Colors.blue[700],
        unselectedItemColor: Colors.grey[600],
        onTap: (i) => _onTap(context, i),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home),        label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.description), label: 'Plans'),
          BottomNavigationBarItem(icon: Icon(Icons.star),        label: 'Exclusive'),
          BottomNavigationBarItem(icon: Icon(Icons.person),      label: 'Profile'),
        ],
      ),
    );
  }
}

// ── Tab screens ───────────────────────────────────────────────────────────────
class HomeTab extends StatelessWidget {
  const HomeTab({super.key});

  @override
  Widget build(BuildContext context) {
    return AppNavBar(
      currentIndex: 0,
      // Agent hidden — 'home_tab' is not in enabledRoutes
      child: Scaffold(
        appBar: AppBar(title: const Text('Home')),
        body: const Center(child: Text('Home content')),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return AppNavBar(
      currentIndex: 1,
      // ✅ Agent visible — 'plans_tab' is in enabledRoutes
      child: Scaffold(
        appBar: AppBar(title: const Text('Plans')),
        body: const Center(child: Text('Browse plans')),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return AppNavBar(
      currentIndex: 2,
      // ✅ Agent visible — 'exclusive_tab' is in enabledRoutes
      child: Scaffold(
        appBar: AppBar(title: const Text('Exclusive')),
        body: const Center(child: Text('Exclusive deals')),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return AppNavBar(
      currentIndex: 3,
      // Agent hidden — 'profile_tab' is not in enabledRoutes
      child: Scaffold(
        appBar: AppBar(title: const Text('Profile')),
        body: const Center(child: Text('Your profile')),
      ),
    );
  }
}
```

**How it works:**

* `Navigator.pushReplacementNamed` fires `didReplace` on `EmbedNavigatorObserver`, which updates the current route in the SDK.
* The SDK checks the new route name against `enabledRoutes` and shows or hides the agent button — no extra code needed.

***

## 6. Sending Events

### 6.1 USER\_DATA (Required)

This is the **only required event**. Send it after you have a logged-in user ID. It activates voice features and associates all future analytics with that user.

```dart theme={null}
embedEvent(
  EventKeys.USER_DATA,
  UserEventPayload(
    app_user_id: 'user_12345',     // required — your unique user identifier
    data: {
      'name':  'Priya Sharma',     // optional extra fields sent to the agent
      'email': 'priya@example.com',
      'plan':  'premium',
    },
  ),
);
```

**When to call it:**

| Scenario            | When to call                                          |
| ------------------- | ----------------------------------------------------- |
| Auth flow           | Immediately after `signIn()` / token refresh succeeds |
| No auth (demo apps) | In `initState` of your first meaningful screen        |

<Note>
  The SDK queues this event internally if the server config hasn't loaded yet and flushes it automatically — you do not need to wait or retry.
</Note>

***

### 6.2 SCREEN\_STATE (Optional)

Sends additional context about the current screen to the agent. Useful when you want the agent to know which step of a multi-step form the user is on, or when multiple routes share one screen widget.

```dart theme={null}
embedEvent(
  EventKeys.SCREEN_STATE,
  ScreenEventPayload(
    screen: 'loan_application_step_2',
    data: {
      'step':        2,
      'total_steps': 5,
      'category':    'home_loan',
    },
  ),
);
```

Call it in `initState` or whenever meaningful context changes (e.g. a stepper advances).

***

### 6.3 CUSTOM\_EVENT (Optional)

Captures any interaction you want to relay to the agent — button taps, option selections, modal opens, etc.

```dart theme={null}
embedEvent(
  EventKeys.CUSTOM_EVENT,
  CustomEventPayload(
    data: {
      'context': 'plan_selection',
      'action':  'plan_selected',
      'plan_id': 'gold_12m',
      'price':   1499,
      'currency': 'INR',
    },
  ),
);
```

***

### 6.4 ANALYTICS\_DATA (Optional)

Like `CUSTOM_EVENT` but requires a named event identifier. Use this for conversion events, funnel steps, or any event you need to categorise by a fixed name on the backend.

```dart theme={null}
embedEvent(
  EventKeys.ANALYTICS_DATA,
  AnalyticsDataEventPayload(
    event_name: 'payment_completed',
    data: {
      'amount':         4999,
      'currency':       'INR',
      'payment_method': 'upi',
      'transaction_id': 'txn_abc123',
    },
  ),
);
```

**Event reference table:**

| Event            | Class                       | `event_name` required? | Typical use                                   |
| ---------------- | --------------------------- | ---------------------- | --------------------------------------------- |
| `USER_DATA`      | `UserEventPayload`          | —                      | Identifying the user and activating the agent |
| `SCREEN_STATE`   | `ScreenEventPayload`        | —                      | Providing screen or step context to the agent |
| `CUSTOM_EVENT`   | `CustomEventPayload`        | No                     | Ad-hoc interactions and selections            |
| `ANALYTICS_DATA` | `AnalyticsDataEventPayload` | **Yes**                | Named funnel and conversion events            |

***

## 7. EmbedWidget Parameters Reference

| Parameter                   | Type                        | Default                | Description                                                                     |
| --------------------------- | --------------------------- | ---------------------- | ------------------------------------------------------------------------------- |
| `child`                     | `Widget`                    | —                      | **Required.** Your `MaterialApp` / `MaterialApp.router`                         |
| `showEmbedWidget`           | `bool`                      | `true`                 | Master visibility toggle — set to `false` to hide the agent everywhere          |
| `enabledRoutes`             | `Map<String, List<String>>` | `{}`                   | Route names where the agent button should appear                                |
| `showOnAllRoutes`           | `bool`                      | `false`                | Show on every route. Use with `disabledRoutes` to exclude specific screens      |
| `disabledRoutes`            | `List<String>`              | `[]`                   | Routes where the agent is always hidden (takes priority over `showOnAllRoutes`) |
| `routeMatchMode`            | `RouteMatchMode`            | `exact`                | How route names are compared — see section 7.1                                  |
| `apiKey`                    | `String?`                   | from `embedInitialize` | Override API key directly on the widget                                         |
| `embedUrl`                  | `String?`                   | from `embedInitialize` | Override server base URL                                                        |
| `rightPadding`              | `double?`                   | server config          | FAB distance from the right edge in logical pixels                              |
| `bottomPadding`             | `double?`                   | server config          | FAB distance from the bottom edge in logical pixels                             |
| `onPermissionStatusChanged` | `void Function(bool)?`      | —                      | Called after the mic permission dialog — `true` = granted, `false` = denied     |

<Note>
  For per-flow visibility props — `embedButtonDelayMs`, `groupDelays`,
  `groupContinuity`, and `groupInsets` — see **[EmbedWidget advanced](/embed/integration/embed-flutter-advance)**.
</Note>

### 7.1 Route Match Modes

By default route names must match **exactly**. Use `routeMatchMode` to relax this:

```dart theme={null}
EmbedWidget(
  routeMatchMode: RouteMatchMode.startsWith,
  enabledRoutes: const {
    'main': ['/product'],  // matches /product, /product/detail, /product/42, etc.
  },
  child: ...,
)
```

| Mode                        | Behaviour                           | Example pattern | Matches                                              |
| --------------------------- | ----------------------------------- | --------------- | ---------------------------------------------------- |
| `RouteMatchMode.exact`      | Exact string match (default)        | `'cart'`        | only `'cart'`                                        |
| `RouteMatchMode.startsWith` | Route starts with the pattern       | `'/product'`    | `/product`, `/product/detail`, `/product/42`         |
| `RouteMatchMode.contains`   | Route contains the pattern anywhere | `'product'`     | `'product_list'`, `'new_product'`, `'/v2/product/3'` |

<Note>
  Leading slashes are normalised — `'home'` and `'/home'` are treated identically.
</Note>

***

## 8. Advanced: Widget Visibility and Positioning

This section covers the common cases — custom position, show-on-all, and the mic
callback. For **route flows/groups**, **per-flow show delays + policies**,
**continuity**, and **per-flow insets**, see the companion guide:

<CardGroup cols={1}>
  <Card title="EmbedWidget advanced — FAB visibility (flows, delays, continuity, insets)" href="/embed/integration/embed-flutter-advance" horizontal>
    **Important for production UX.** Read it once you move past a simple
    `enabledRoutes` list and need the FAB to behave differently per flow or screen.
  </Card>
</CardGroup>

### Custom FAB Position

Override the default floating button position with `rightPadding` and `bottomPadding`:

```dart theme={null}
EmbedWidget(
  showEmbedWidget: true,
  enabledRoutes: const {
    'main': ['home', 'plans', 'cart'],
  },
  rightPadding: 20.0,    // 20px from the right edge
  bottomPadding: 100.0,  // 100px from the bottom edge
  child: MaterialApp(
    navigatorObservers: [EmbedNavigatorObserver()],
    home: const HomeScreen(),
  ),
)
```

### Show on All Routes with Exclusions

When you want the agent everywhere except a handful of screens:

```dart theme={null}
EmbedWidget(
  showEmbedWidget: true,
  showOnAllRoutes: true,
  disabledRoutes: const ['splash', 'login', 'otp_verification'],
  child: ...,
)
```

### Microphone Permission Callback

Handle the case where the user denies microphone access when starting a call:

```dart theme={null}
final _messengerKey = GlobalKey<ScaffoldMessengerState>();

EmbedWidget(
  showEmbedWidget: true,
  enabledRoutes: const {
    'main': ['home', 'dashboard'],
  },
  onPermissionStatusChanged: (bool granted) {
    if (!granted) {
      _messengerKey.currentState?.showSnackBar(
        const SnackBar(
          content: Text(
            'Microphone access is required for voice calls. Please enable it in Settings.',
          ),
        ),
      );
    }
  },
  child: MaterialApp(
    scaffoldMessengerKey: _messengerKey,
    navigatorObservers: [EmbedNavigatorObserver()],
    home: const HomeScreen(),
  ),
)
```

**When is `onPermissionStatusChanged` called?**

* After the user taps the agent button and the permission dialog is shown.
* Before the call connection is established.
* `true` = permission granted, the call will proceed.
* `false` = permission denied or permanently denied.

***

## 9. Advanced: Listening to SDK Events

Subscribe to events emitted by the agent from anywhere in your app.

### Agent lifecycle events

```dart theme={null}
import 'package:embed_flutter/embed_flutter.dart';

class MyScreen extends StatefulWidget { ... }

class _MyScreenState extends State<MyScreen> {
  late final String _handle;

  @override
  void initState() {
    super.initState();
    _handle = embedOnAgent((event) {
      final type = event['type'] as String;
      switch (type) {
        case SdkEventName.agentConversationStarted:
          print('Voice call started');
          break;
        case SdkEventName.agentConversationEnded:
          print('Voice call ended');
          break;
        case SdkEventName.microphonePermissionAllowed:
          print('Mic granted');
          break;
        case SdkEventName.microphonePermissionDenied:
          print('Mic denied');
          break;
      }
    });
  }

  @override
  void dispose() {
    embedOffAgent(_handle); // always unsubscribe to avoid memory leaks
    super.dispose();
  }
}
```

**Available `SdkEventName` constants:**

| Constant                                   | Value                             | Fired when                  |
| ------------------------------------------ | --------------------------------- | --------------------------- |
| `SdkEventName.agentVisible`                | `'agent_visible'`                 | Agent FAB appears on screen |
| `SdkEventName.agentConversationStarted`    | `'agent_conversation_started'`    | User starts a voice call    |
| `SdkEventName.agentConversationEnded`      | `'agent_conversation_ended'`      | Voice call ends             |
| `SdkEventName.popupMessageVisible`         | `'popup_message_visible'`         | Inactivity popup is shown   |
| `SdkEventName.agentTapTopOpen`             | `'agent_tap_top_open'`            | Agent widget expanded       |
| `SdkEventName.agentTapTopClose`            | `'agent_tap_top_close'`           | Agent widget collapsed      |
| `SdkEventName.genToolTriggered`            | `'gen_tool_triggered'`            | Agent uses an AI tool       |
| `SdkEventName.microphonePermissionAllowed` | `'microphone_permission_allowed'` | Mic permission granted      |
| `SdkEventName.microphonePermissionDenied`  | `'microphone_permission_denied'`  | Mic permission denied       |

***

## 10. Utility Functions

| Function                                                  | Description                                                  |
| --------------------------------------------------------- | ------------------------------------------------------------ |
| `embedInitialize(apiKey, {flowName, embedUrl, onResult})` | Initialize the SDK before `runApp`                           |
| `embedSetAppVersion(String version)`                      | Tag all events with an app version string                    |
| `embedEvent(EventKeys, EventPayload)`                     | Send a typed event to the agent                              |
| `embedTrackEvent(String key, EventPayload)`               | Send an event using a raw string key                         |
| `embedClearStorageCache()`                                | Wipe all persisted SDK data — call on logout                 |
| `embedOnAgent(handler)` → `String`                        | Subscribe to agent lifecycle events                          |
| `embedOffAgent(String handle)`                            | Unsubscribe using the handle from `embedOnAgent`             |
| `EmbedWidget.isActive`                                    | `true` when the agent FAB is visible and the session is live |
| `EmbedWidget.isLive`                                      | `true` when the server marks the agent as live               |
| `EmbedWidget.forceCleanup()`                              | Force-stop the agent and release resources                   |

## 11. Complete End-to-End Example

This example covers everything: GoRouter with a bottom tab shell, `EmbedRouteListener` for tabs, `USER_DATA` after login, `SCREEN_STATE` for sub-screen context, and `CUSTOM_EVENT` / `ANALYTICS_DATA` for interactions.

```dart theme={null}
// pubspec.yaml dependencies:
//   flutter: sdk: flutter
//   embed_flutter: ^0.0.17
//   go_router: ^14.0.0

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:embed_flutter/embed_flutter.dart';

// ════════════════════════════════════════════════════════════════════════════════
// main.dart
// ════════════════════════════════════════════════════════════════════════════════

void main() {
  embedInitialize('your-api-key', flowName: 'main');
  embedSetAppVersion('1.0.0'); // optional
  runApp(const MyApp());
}

// ════════════════════════════════════════════════════════════════════════════════
// Router
// ════════════════════════════════════════════════════════════════════════════════

final _router = GoRouter(
  initialLocation: '/login',
  observers: [EmbedNavigatorObserver()],
  routes: [
    GoRoute(
      path: '/login',
      name: 'login',
      builder: (_, __) => const LoginScreen(),
    ),
    // Persistent tab shell
    ShellRoute(
      builder: (_, __, child) => MainShell(child: child),
      routes: [
        GoRoute(
          path: '/home',
          name: 'home',
          builder: (_, __) => const HomeTab(),
        ),
        GoRoute(
          path: '/plans',
          name: 'plans',
          builder: (_, __) => const PlansTab(),
          routes: [
            GoRoute(
              path: 'detail/:id',
              name: 'plan_detail',
              builder: (_, s) => PlanDetailScreen(id: s.pathParameters['id']!),
            ),
          ],
        ),
        GoRoute(
          path: '/profile',
          name: 'profile',
          builder: (_, __) => const ProfileTab(),
        ),
      ],
    ),
    // Full-screen checkout screens outside the tab shell
    GoRoute(
      path: '/cart',
      name: 'cart',
      builder: (_, __) => const CartScreen(),
    ),
    GoRoute(
      path: '/payment',
      name: 'payment',
      builder: (_, __) => const PaymentScreen(),
    ),
    GoRoute(
      path: '/confirmation',
      name: 'confirmation',
      builder: (_, __) => const ConfirmationScreen(),
    ),
  ],
);

// ════════════════════════════════════════════════════════════════════════════════
// Root app widget
// ════════════════════════════════════════════════════════════════════════════════

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

  @override
  Widget build(BuildContext context) {
    return EmbedWidget(
      showEmbedWidget: true,
      enabledRoutes: const {
        // Agent appears on these screens; hidden on login and profile
        'main': ['home', 'plans', 'plan_detail', 'cart', 'payment', 'confirmation'],
      },
      rightPadding: 16,
      bottomPadding: 100,
      onPermissionStatusChanged: (granted) {
        if (!granted) debugPrint('Mic permission denied');
      },
      child: MaterialApp.router(
        routerConfig: _router,
        theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue),
      ),
    );
  }
}

// ════════════════════════════════════════════════════════════════════════════════
// Shell scaffold
// ════════════════════════════════════════════════════════════════════════════════

class MainShell extends StatelessWidget {
  final Widget child;
  const MainShell({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    final location = GoRouterState.of(context).uri.toString();
    int idx = 0;
    if (location.startsWith('/plans'))   idx = 1;
    if (location.startsWith('/profile')) idx = 2;

    return Scaffold(
      body: child,
      bottomNavigationBar: NavigationBar(
        selectedIndex: idx,
        onDestinationSelected: (i) {
          switch (i) {
            case 0: context.go('/home');    break;
            case 1: context.go('/plans');   break;
            case 2: context.go('/profile'); break;
          }
        },
        destinations: const [
          NavigationDestination(icon: Icon(Icons.home),        label: 'Home'),
          NavigationDestination(icon: Icon(Icons.description), label: 'Plans'),
          NavigationDestination(icon: Icon(Icons.person),      label: 'Profile'),
        ],
      ),
    );
  }
}

// ════════════════════════════════════════════════════════════════════════════════
// Login screen — identify the user after sign-in
// ════════════════════════════════════════════════════════════════════════════════

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

  void _login(BuildContext context) {
    // Your auth logic here ...

    // Identify the user to the SDK after successful authentication
    embedEvent(
      EventKeys.USER_DATA,
      UserEventPayload(
        app_user_id: 'user_42',
        data: {
          'name':  'Rahul Verma',
          'email': 'rahul@example.com',
          'plan':  'free',
        },
      ),
    );

    context.go('/home');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => _login(context),
          child: const Text('Sign In'),
        ),
      ),
    );
  }
}

// ════════════════════════════════════════════════════════════════════════════════
// Tab screens — each wrapped with EmbedRouteListener
// ════════════════════════════════════════════════════════════════════════════════

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

  @override
  Widget build(BuildContext context) {
    return EmbedRouteListener(
      routeName: 'home', // ✅ in enabledRoutes — agent visible
      child: Scaffold(
        appBar: AppBar(title: const Text('Home')),
        body: Center(
          child: ElevatedButton(
            onPressed: () => context.go('/cart'),
            child: const Text('Go to Cart'),
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return EmbedRouteListener(
      routeName: 'plans', // ✅ in enabledRoutes — agent visible
      child: Scaffold(
        appBar: AppBar(title: const Text('Plans')),
        body: ListView(
          children: [
            ListTile(
              title: const Text('Gold Plan — ₹999/mo'),
              onTap: () {
                // Send context so the agent knows which plan is being viewed
                embedEvent(
                  EventKeys.SCREEN_STATE,
                  ScreenEventPayload(
                    screen: 'plan_detail',
                    data: {'plan_id': 'gold', 'price': 999},
                  ),
                );
                context.pushNamed('plan_detail', pathParameters: {'id': 'gold'});
              },
            ),
            ListTile(
              title: const Text('Platinum Plan — ₹1999/mo'),
              onTap: () {
                embedEvent(
                  EventKeys.SCREEN_STATE,
                  ScreenEventPayload(
                    screen: 'plan_detail',
                    data: {'plan_id': 'platinum', 'price': 1999},
                  ),
                );
                context.pushNamed('plan_detail', pathParameters: {'id': 'platinum'});
              },
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return EmbedRouteListener(
      routeName: 'profile', // not in enabledRoutes — agent hidden
      child: Scaffold(
        appBar: AppBar(title: const Text('Profile')),
        body: const Center(child: Text('Your profile')),
      ),
    );
  }
}

// ════════════════════════════════════════════════════════════════════════════════
// Plan detail screen
// ════════════════════════════════════════════════════════════════════════════════

class PlanDetailScreen extends StatelessWidget {
  final String id;
  const PlanDetailScreen({super.key, required this.id});

  @override
  Widget build(BuildContext context) {
    // plan_detail is a top-level GoRoute so EmbedNavigatorObserver catches the push
    return Scaffold(
      appBar: AppBar(title: Text('Plan: $id')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Plan ID: $id', style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () {
                // Inform the agent the user selected a plan
                embedEvent(
                  EventKeys.CUSTOM_EVENT,
                  CustomEventPayload(
                    data: {'action': 'plan_selected', 'plan_id': id},
                  ),
                );
                context.go('/cart');
              },
              child: const Text('Add to Cart'),
            ),
          ],
        ),
      ),
    );
  }
}

// ════════════════════════════════════════════════════════════════════════════════
// Checkout screens
// ════════════════════════════════════════════════════════════════════════════════

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Cart')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => context.go('/payment'),
          child: const Text('Proceed to Payment'),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Payment')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // Track the conversion event
            embedEvent(
              EventKeys.ANALYTICS_DATA,
              AnalyticsDataEventPayload(
                event_name: 'payment_completed',
                data: {'amount': 999, 'currency': 'INR', 'method': 'upi'},
              ),
            );
            context.go('/confirmation');
          },
          child: const Text('Pay ₹999'),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Confirmed!')),
      body: const Center(child: Text('Your order is placed.')),
    );
  }
}
```

***

## 12. Troubleshooting

### Agent button not visible

| Check                                       | Fix                                                                              |
| ------------------------------------------- | -------------------------------------------------------------------------------- |
| `showEmbedWidget` is `false`                | Set it to `true`                                                                 |
| Route name is not listed in `enabledRoutes` | Add it under the `'main'` key                                                    |
| `EmbedNavigatorObserver` is missing         | Add it to `navigatorObservers` on `MaterialApp` or `observers` on `GoRouter`     |
| `USER_DATA` event not sent                  | The agent requires user identity. Call `embedEvent(USER_DATA, ...)` after login. |
| Invalid API key                             | Verify the key passed to `embedInitialize`                                       |
| Network error on startup                    | Check the `onResult` callback in `embedInitialize` for the error message         |

### Agent not showing on a specific tab (ShellRoute / BottomNavigationBar)

| Check                                                             | Fix                                                                                |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Tab screen not wrapped with `EmbedRouteListener`                  | Wrap each tab body with `EmbedRouteListener(routeName: '...')`                     |
| `pushReplacementNamed` not used for tab switches (MaterialApp)    | Switch to `pushReplacementNamed` so `didReplace` fires on `EmbedNavigatorObserver` |
| Tab route name not in `enabledRoutes`                             | Add it to the `'main'` list                                                        |
| `routeName` in `EmbedRouteListener` doesn't match `enabledRoutes` | Both must be identical strings                                                     |

### iOS microphone permission never requested

| Check                                                    | Fix                                                       |
| -------------------------------------------------------- | --------------------------------------------------------- |
| `NSMicrophoneUsageDescription` missing from `Info.plist` | Add the key with a usage description string               |
| `PERMISSION_MICROPHONE=1` missing from Podfile           | Add the macro inside `post_install` and run `pod install` |

### GoRouter routes not tracked

| Check                                                        | Fix                                                                                          |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| Observer missing                                             | Add `EmbedNavigatorObserver()` to `GoRouter.observers`                                       |
| Using path strings in `enabledRoutes` instead of route names | Use the `name:` field value of `GoRoute`, not the `path:` string                             |
| `nameExtractor` needed                                       | If GoRoute `name` values differ from what you want to match, supply a custom `nameExtractor` |

### Clearing SDK data on logout

```dart theme={null}
Future<void> onLogout() async {
  await embedClearStorageCache();
  // Navigate to login screen
}
```

***

## Related documentation

<CardGroup cols={1}>
  <Card title="EmbedWidget advanced — FAB visibility" href="/embed/integration/embed-flutter-advance" horizontal>
    Route flows/groups, per-flow show delays and policies, continuity, and
    per-flow insets. The companion guide for production-grade FAB behavior.
  </Card>
</CardGroup>

***

## Support

* **Docs:** [https://docs.revrag.ai](https://docs.revrag.ai/)
* **Email:** [contact@revrag.ai](mailto:contact@revrag.ai)

**Last updated:** April 2026 · **Flutter:** 3.0.0+ · **`embed_flutter`:** 0.0.17
