> ## 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.

# iOS Native

> iOS SDK Integration Guide - Voice-enabled AI agent with real-time communication capabilities (Swift / SwiftUI)

# iOS Integration Guide

**SDK version:** 1.0.0\
**Platform:** iOS (Swift / SwiftUI)\
**Min deployment target:** iOS 16.0\
**Build system:** Xcode 15+ / Swift Package Manager

Get your API key from [app.revrag.ai → Settings → API Keys](https://app.revrag.ai).

***

## Table of Contents

1. [Requirements](#1-requirements)
2. [How it works](#2-how-it-works)
3. [Step 1 — Add the package](#step-1--add-the-package)
4. [Step 2 — Add microphone permission](#step-2--add-microphone-permission)
5. [Step 3 — Initialize the SDK](#step-3--initialize-the-sdk)
6. [Step 4 — Identify the user](#step-4--identify-the-user)
7. [Step 5 — Add the floating button](#step-5--add-the-floating-button)
8. [Routing scenarios](#routing-scenarios)
9. [Button visibility control](#button-visibility-control)
10. [Events](#events)
11. [Analytics helpers](#analytics-helpers)
12. [Cleanup on logout](#cleanup-on-logout)
13. [Configuration reference](#configuration-reference)
14. [Troubleshooting](#troubleshooting)
15. [Pre-ship checklist](#pre-ship-checklist)

***

## 1. Requirements

| Requirement  | Value           |
| ------------ | --------------- |
| Min OS       | iOS 16.0        |
| Language     | Swift 5.9+      |
| UI framework | SwiftUI         |
| Build system | Xcode 15+ / SPM |

***

## Step 1 — Add the package

### Swift Package Manager (recommended)

In Xcode: **File → Add Package Dependencies**

Paste the URL:

```
https://github.com/RevRag-ai/embed-native
```

Select `Up to Next Major Version` from `1.0.0` → select `RevragEmbed` → click **Add Package**.

### CocoaPods alternative

```ruby theme={null}
# Podfile
pod 'RevragEmbed', '~> 1.0'
```

```bash theme={null}
pod install
open YourApp.xcworkspace
```

***

## Step 2 — Add microphone permission

<Warning>
  **Required — your app will crash at runtime without this.**

  Without `NSMicrophoneUsageDescription`, iOS terminates the process the moment the SDK requests microphone access. This step cannot be skipped.
</Warning>

Add to `Info.plist`:

```xml theme={null}
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone to talk with the AI agent.</string>
```

**In Xcode:** Target → Info tab → `+` button → `Privacy - Microphone Usage Description` → enter a description.

***

## Step 3 — Initialize the SDK

Call `EmbedSDK.shared.initialize()` **once**, as early as possible.

### SwiftUI App entry point

```swift theme={null}
// YourApp.swift
import SwiftUI
import RevragEmbed

@main
struct YourApp: App {

    init() {
        Task {
            await EmbedSDK.shared.initialize(apiKey: "YOUR_REVRAG_API_KEY")
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

### UIKit AppDelegate

```swift theme={null}
// AppDelegate.swift
import UIKit
import RevragEmbed

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        Task {
            await EmbedSDK.shared.initialize(apiKey: "YOUR_REVRAG_API_KEY")
        }
        return true
    }
}
```

<Tip>
  **What `initialize()` does:**

  1. Calls `GET /embedded-agent/initialize` with your API key
  2. Parses and stores your widget configuration (colors, agent name, avatar)
  3. Pre-warms the Lottie animation cache
  4. Installs `ClickEventTracker` for automatic rage-click detection
  5. Sets `EmbedSDK.shared.isInitialized = true` on the main thread — the button appears automatically once true

  Console logs are prefixed `[RevragEmbed]` — check them if initialization fails.
</Tip>

***

## Step 4 — Identify the user

Call this right after your login flow completes:

```swift theme={null}
EmbedSDK.shared.event(.userData, data: [
    "app_user_id": "user_123",       // required
    "name":        "Jane Doe",        // optional
    "email":       "jane@email.com"   // optional
])
```

<Warning>
  Send `USER_DATA` **before** the user taps the call button. Without `app_user_id`, the agent cannot identify the user and conversation context will not be attributed.
</Warning>

***

## Step 5 — Add the floating button

Apply the `.embedProvider()` modifier to your **root view**. This overlays the draggable button on top of all your existing content.

```swift theme={null}
// ContentView.swift
import SwiftUI
import RevragEmbed

struct ContentView: View {
    var body: some View {
        NavigationStack {
            HomeView()
        }
        .embedProvider(
            appUserId:  "user_123",
            appVersion: Bundle.main.releaseVersionNumber ?? "1.0"
        )
    }
}
```

### UIKit alternative

Add `EmbedButton` as an overlay view in your root `UIViewController`:

```swift theme={null}
import RevragEmbed
import SwiftUI

// In viewDidLoad of your root UIViewController
let embedView = UIHostingController(rootView:
    EmbedButton(appUserId: "user_123")
)
embedView.view.backgroundColor = .clear
addChild(embedView)
view.addSubview(embedView.view)
embedView.view.frame = view.bounds
embedView.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
embedView.didMove(toParent: self)
```

### `.embedProvider()` modifier props

| Parameter              | Type                          | Default         | Description                             |
| ---------------------- | ----------------------------- | --------------- | --------------------------------------- |
| `appUserId`            | `String`                      | `""`            | Identifies the logged-in user           |
| `appVersion`           | `String`                      | `"1.0.0"`       | Your app's version string               |
| `accentColor`          | `Color`                       | `#6C63FF`       | Fallback gradient color                 |
| `visibilityConfig`     | `EmbedButtonVisibilityConfig` | show everywhere | Screen allow/exclude list and groups    |
| `navigationController` | `UINavigationController?`     | `nil`           | Enables automatic UIKit screen tracking |

***

## Routing scenarios

Screen tracking lets the agent know which screen the user is on. Choose the scenario that matches your navigation setup.

***

### Scenario A — NavigationStack (most common)

Post `EmbedViewDidAppear` from `.onAppear` on each screen. The SDK listens for this notification and updates the current screen name.

```swift theme={null}
struct HomeView: View {
    var body: some View {
        List { /* content */ }
            .onAppear {
                NotificationCenter.default.post(
                    name:     NSNotification.Name("EmbedViewDidAppear"),
                    object:   nil,
                    userInfo: ["screen": "Home"]
                )
            }
    }
}

struct ProductView: View {
    let productId: String

    var body: some View {
        ScrollView { /* content */ }
            .onAppear {
                NotificationCenter.default.post(
                    name:     NSNotification.Name("EmbedViewDidAppear"),
                    object:   nil,
                    userInfo: ["screen": "ProductDetail"]
                )
            }
    }
}
```

Apply the provider to the root:

```swift theme={null}
struct ContentView: View {
    var body: some View {
        NavigationStack {
            HomeView()
        }
        .embedProvider(appUserId: "user_123", appVersion: "1.0")
    }
}
```

<Tip>
  `.onAppear` fires again when navigating back to a screen — this is expected and correct behavior.
</Tip>

***

### Scenario B — TabView with multiple stacks

Apply `.embedProvider()` **outside** the `TabView` so the button floats above all tabs. Each tab's screens post `EmbedViewDidAppear` from `.onAppear` as shown in Scenario A.

```swift theme={null}
struct ContentView: View {
    var body: some View {
        TabView {
            NavigationStack {
                HomeView()
            }
            .tabItem { Label("Home", systemImage: "house") }

            NavigationStack {
                ExploreView()
            }
            .tabItem { Label("Explore", systemImage: "magnifyingglass") }

            NavigationStack {
                ProfileView()
            }
            .tabItem { Label("Profile", systemImage: "person") }
        }
        // Provider goes here — outside TabView — so button floats over all tabs
        .embedProvider(appUserId: "user_123", appVersion: "1.0")
    }
}
```

<Warning>
  Do **not** place `.embedProvider()` inside a tab. If it's scoped to one tab the button will disappear when switching to other tabs.
</Warning>

***

### Scenario C — No NavigationStack (flat views / custom transitions)

Fire the notification manually when your view becomes visible. You can also post `EmbedViewDidDisappear` on exit.

```swift theme={null}
struct CheckoutView: View {
    var body: some View {
        VStack { /* content */ }
            .onAppear {
                NotificationCenter.default.post(
                    name:     NSNotification.Name("EmbedViewDidAppear"),
                    object:   nil,
                    userInfo: ["screen": "Checkout"]
                )
            }
            .onDisappear {
                // Optional — fires SCREEN_STATE with action: "exit"
                NotificationCenter.default.post(
                    name:     NSNotification.Name("EmbedViewDidDisappear"),
                    object:   nil,
                    userInfo: ["screen": "Checkout"]
                )
            }
    }
}
```

***

### Scenario D — UIKit UINavigationController

Pass the `UINavigationController` to `.embedProvider()` for automatic tracking:

```swift theme={null}
struct ContentView: View {
    let navController: UINavigationController

    var body: some View {
        UIKitNavigationWrapper(navController: navController)
            .embedProvider(
                appUserId:            "user_123",
                appVersion:           "1.0",
                navigationController: navController   // ← automatic tracking
            )
    }
}
```

The SDK observes `UINavigationControllerDelegate` and fires `SCREEN_STATE` events automatically — no `.onAppear` notifications needed.

***

## Button visibility control

By default the button shows on **every screen**. Use `EmbedButtonVisibilityConfig` to control this.

### Show only on specific screens

```swift theme={null}
ContentView()
    .embedProvider(
        appUserId:  "user_123",
        appVersion: "1.0",
        visibilityConfig: EmbedButtonVisibilityConfig(
            allowedScreens:  ["Home", "Product", "Cart"],
            excludedScreens: ["Login", "Splash", "Onboarding"]
        )
    )
```

### Keep button visible across a flow (e.g. checkout)

Use a group with `.continuous` continuity so the button doesn't flash between screens:

```swift theme={null}
visibilityConfig: EmbedButtonVisibilityConfig(
    groups: [
        EmbedButtonGroupConfig(
            id:          "checkout-flow",
            screens:     ["Cart", "Payment", "Confirmation"],
            continuity:  .continuous,
            delayMs:     0.5,
            delayPolicy: .oncePerGroupEntry
        )
    ]
)
```

### Visibility rules (evaluated in priority order)

```
1. Screen is in excludedScreens               →  always HIDDEN  (strongest rule)
2. allowedScreens and groups are both empty   →  always VISIBLE
3. Screen is in allowedScreens or any group   →  VISIBLE
4. Otherwise                                  →  HIDDEN
```

***

## Events

### EventKeys reference

| Key              | Value            | Auto-fired                | Host-callable | Notes                                                       |
| ---------------- | ---------------- | ------------------------- | ------------- | ----------------------------------------------------------- |
| `.userData`      | `user_data`      | No                        | **Yes**       | Send after login; must include `app_user_id`                |
| `.screenState`   | `screen_state`   | **Yes** — `EmbedProvider` | Yes           | Fired on every screen enter and exit                        |
| `.analyticsData` | `analytics_data` | **Yes** — SDK UI          | **Yes**       | SDK fires built-in events; host may also fire custom events |
| `.customEvent`   | `custom_event`   | No                        | Yes           | Free-form host-app events                                   |

### Sending events

```swift theme={null}
// User identity (required before first call)
EmbedSDK.shared.event(.userData, data: ["app_user_id": "user_123"])

// Custom analytics event
EmbedSDK.shared.event(.analyticsData, data: ["event_name": "checkout_started"])

// Manual screen state
EmbedSDK.shared.event(.screenState, data: ["screen": "CheckoutScreen", "action": "enter"])
```

### Listen for agent call events

```swift theme={null}
import RevragEmbed

class CallManager {
    private var connectedId:    UUID?
    private var disconnectedId: UUID?

    func startListening() {
        connectedId = EmbedSDK.shared.onAgent(.agentConnected) { _ in
            // e.g. pause background audio
            print("Call started")
        }
        disconnectedId = EmbedSDK.shared.onAgent(.agentDisconnected) { payload in
            let duration = (payload["metadata"] as? [String: Any])?["callDuration"] as? Int ?? 0
            print("Call lasted \(duration)s")
        }
    }

    func stopListening() {
        if let id = connectedId    { EmbedSDK.shared.offAgent(.agentConnected,    id: id) }
        if let id = disconnectedId { EmbedSDK.shared.offAgent(.agentDisconnected, id: id) }
    }
}
```

### Agent lifecycle events reference

| Event                  | Payload                                               | Use case                              |
| ---------------------- | ----------------------------------------------------- | ------------------------------------- |
| `.agentConnected`      | `timestamp`, `metadata.callDuration: 0`, `server_url` | Pause background audio, start a timer |
| `.agentDisconnected`   | `timestamp`, `metadata.callDuration` (seconds)        | Resume audio, log call length         |
| `.popupMessageVisible` | —                                                     | Track tooltip impressions             |

***

## Analytics helpers

These helpers fire `analyticsData` events with standardized payloads.

```swift theme={null}
// Track a custom event
EmbedSDK.shared.trackEvent("product_viewed", properties: ["product_id": "SKU-123"])

// Track a form interaction
EmbedSDK.shared.trackFormEvent(
    formId:    "checkout_form",
    eventType: "submit",
    formData:  ["step": "payment"]
)

// Track a rage-click
EmbedSDK.shared.trackRageClick(
    elementId:   "btn_add_to_cart",
    coordinates: CGPoint(x: 200, y: 450),
    clickCount:  5,
    elementType: "UIButton"
)

// Check microphone permission status
EmbedSDK.shared.checkMicPermission { granted in
    if !granted { self.showMicEducationAlert() }
}
```

### Events auto-fired by the SDK

| `event_name`                  | Trigger                                                            |
| ----------------------------- | ------------------------------------------------------------------ |
| `agent_tap_to_open`           | User taps the collapsed FAB                                        |
| `agent_tap_to_close`          | User taps avatar to close the card                                 |
| `agent_visible`               | `EmbedProvider` show-delay completes                               |
| `popup_message_visible`       | Inactivity nudge fires the tooltip                                 |
| `gen_tool_triggered`          | First data-channel message received                                |
| `agent_conversation_started`  | LiveKit `connect()` succeeds                                       |
| `microphone_permission_allow` | On every call start (`status: already_granted` or `newly_granted`) |

***

## Cleanup on logout

Call this when the user logs out or switches accounts to prevent stale data from leaking into the next session.

```swift theme={null}
func onUserLogout() {
    EmbedSDK.shared.clearStorageCache()
}
```

***

## Configuration reference

### `EmbedButtonVisibilityConfig`

| Field             | Type                       | Default            | Description                                     |
| ----------------- | -------------------------- | ------------------ | ----------------------------------------------- |
| `allowedScreens`  | `[String]`                 | `[]` (all screens) | Button shows only on these screens              |
| `excludedScreens` | `[String]`                 | `[]`               | Button always hidden on these screens           |
| `showDelay`       | `TimeInterval` s           | `0`                | Delay before button fades in on allowed screens |
| `groups`          | `[EmbedButtonGroupConfig]` | `[]`               | Per-group override rules                        |
| `defaultInset`    | `EmbedButtonInset?`        | SDK default        | Default edge snap position                      |

### `EmbedButtonGroupConfig`

| Field         | Type                     | Default      | Description                                                               |
| ------------- | ------------------------ | ------------ | ------------------------------------------------------------------------- |
| `id`          | `String`                 | required     | Unique group identifier                                                   |
| `screens`     | `[String]`               | required     | Screens that belong to this group                                         |
| `continuity`  | `EmbedButtonContinuity`  | `.perScreen` | `.continuous` keeps the button mounted across group screens without flash |
| `inset`       | `EmbedButtonInset?`      | `nil`        | Per-group snap position override                                          |
| `delayMs`     | `TimeInterval` s         | `0`          | Show delay for this group                                                 |
| `delayPolicy` | `EmbedButtonDelayPolicy` | `.perScreen` | Controls when the delay resets                                            |

### `EmbedButtonDelayPolicy`

| Value                | Behaviour                                          |
| -------------------- | -------------------------------------------------- |
| `.perScreen`         | Delay applies every time the screen becomes active |
| `.oncePerGroupEntry` | Delay only on the first entry into a group         |
| `.oncePerAppSession` | Delay only on the very first view of the session   |

### `EmbedButton` direct props

| Parameter     | Type               | Default             | Description                               |
| ------------- | ------------------ | ------------------- | ----------------------------------------- |
| `appUserId`   | `String`           | `""`                | User identifier passed to the voice agent |
| `accentColor` | `Color`            | `#6C63FF`           | Fallback gradient                         |
| `isVisible`   | `Bool`             | `true`              | Hides the button without unmounting it    |
| `inset`       | `EmbedButtonInset` | right=24, bottom=80 | Snap position from screen edges           |

***

## Troubleshooting

### App crashes on first call

**Error:**

```
This app has crashed because it attempted to access privacy-sensitive data
without a usage description.
```

**Fix:** Add `NSMicrophoneUsageDescription` to `Info.plist`. See [Step 2](#step-2--add-microphone-permission) above.

***

### The button never appears

**Cause A — `initialize()` not called or failed**

Check the Xcode console for `[RevragEmbed]` prefixed logs:

```
[RevragEmbed] 🚀 Stage 1 — initialize() called
[RevragEmbed] 🌐 Stage 3 — calling GET /embedded-agent/initialize …
[RevragEmbed] ❌ Initialization failed: <error message>
```

Fix: verify your API key has no leading/trailing spaces and the device has internet.

***

**Cause B — SDK not ready yet**

`initialize()` is async. The button is hidden until `isInitialized = true` and appears automatically — no action required. To observe readiness:

```swift theme={null}
print(EmbedSDK.shared.isInitialized)  // should be true
```

***

**Cause C — Screen excluded by visibility config**

Screen names are **case-sensitive** and must match exactly what you post in `EmbedViewDidAppear` notifications.

```swift theme={null}
// What you post in .onAppear:
userInfo: ["screen": "ProductDetail"]

// Must match exactly in visibilityConfig:
allowedScreens: ["Home", "ProductDetail"]   // ✅ correct
allowedScreens: ["Home", "productdetail"]   // ❌ case mismatch
allowedScreens: ["Home", "Product Detail"]  // ❌ space mismatch
```

***

### Button disappears when switching tabs

**Cause:** `.embedProvider()` is applied inside a tab instead of outside the `TabView`.

```swift theme={null}
// ❌ Wrong — provider scoped to one tab only
TabView {
    HomeView()
        .embedProvider(appUserId: "user_123")  // hidden on all other tabs
        .tabItem { ... }
}

// ✅ Correct — provider wraps the entire TabView
TabView {
    HomeView().tabItem { ... }
    ProfileView().tabItem { ... }
}
.embedProvider(appUserId: "user_123")  // visible on all tabs
```

***

### Screen tracking not working (agent doesn't know current screen)

**Cause:** `EmbedViewDidAppear` notification not posted from `.onAppear`.

```swift theme={null}
// ✅ Add this to every screen's .onAppear
.onAppear {
    NotificationCenter.default.post(
        name:     NSNotification.Name("EmbedViewDidAppear"),
        object:   nil,
        userInfo: ["screen": "YourScreenName"]
    )
}
```

***

### Background music doesn't resume after a call

The SDK calls `AVAudioSession.setActive(false, options: .notifyOthersOnDeactivation)` automatically in `endCall()`. If you observe this issue, ensure you are on SDK version ≥ 1.0 and that the session is not being deactivated before the SDK finishes cleaning up.

***

### Analytics events missing from dashboard

Check in this order:

1. `USER_DATA` was sent with a valid `app_user_id` before other events
2. `initialize()` completed successfully (`isInitialized == true`)
3. Device has internet connectivity
4. The SDK rate-limits to 5 req/s — bursts are **queued, not dropped**

***

### User identity leaks between accounts

Always call `clearStorageCache()` on logout before the next user logs in:

```swift theme={null}
func onUserLogout() {
    EmbedSDK.shared.clearStorageCache()
}
```

***

## Pre-ship checklist

### Basic setup

* [ ] Package added via Xcode SPM: `https://github.com/RevRag-ai/embed-native`
* [ ] `NSMicrophoneUsageDescription` added to `Info.plist`
* [ ] `await EmbedSDK.shared.initialize(apiKey:)` called in `App.init()` or `AppDelegate`
* [ ] `USER_DATA` event sent with `app_user_id` immediately after login
* [ ] `.embedProvider()` applied to the root view (outside `TabView` if tabs are used)
* [ ] `clearStorageCache()` called on logout

### Screen tracking

* [ ] `EmbedViewDidAppear` notification posted from every screen's `.onAppear` (SwiftUI NavigationStack)
* [ ] Or `navigationController` passed to `.embedProvider()` for automatic UIKit tracking
* [ ] Screen names in notifications match `visibilityConfig` exactly (case-sensitive)

### Visibility

* [ ] `visibilityConfig` configured if the button should not show on all screens
* [ ] `.embedProvider()` placed outside `TabView` (if applicable)

### Production readiness

* [ ] `[RevragEmbed]` logs checked — no initialization errors
* [ ] Agent event listeners started and stopped at appropriate lifecycle points
* [ ] Tested on a physical device — microphone permission dialog does not appear in Simulator

***

## Support

* Issues: [GitHub Issues](https://github.com/RevRag-ai/embed-native/issues)
* Docs: [Revrag Documentation](https://docs.revrag.ai)
* Dashboard: [app.revrag.ai](https://app.revrag.ai)
