# iOS

Native Swift chat SDK with a SwiftUI chat screen: AI chatbot, live-agent handoff, attachments and typing indicators. iOS 14+, Swift 5.9.

## Install

Swift PM (note the lowercase package name):

```swift
.package(url: "https://github.com/talktothelaw/new-instance-livechat.git", from: "0.5.0")
// target dependency:
.product(name: "LiveAndAiChat", package: "liveandaichat-ios")
```

CocoaPods: `pod 'LiveAndAiChat', '~> 0.5.0'`

## Quick start

Your backend mints a customer token (see [Session tokens (REST API)](/session-tokens-rest-api)) and hands it to the app:

```swift
import LiveAndAiChat

let chat = try LiveAndAiChat.Builder()
    .config(try LiveAndAiChatConfig(customerToken: tokenFromYourBackend))
    .build()

chat.present(from: viewController)
```

UIKit hosts call `present(from:)`, which presents the screen and wires the built-in document picker. SwiftUI hosts embed `ChatScreen(sdk:onClose:onPickFile:)` directly. Unlike Android, `openChat()` presents no UI on iOS: it flips state and starts `initialize()`; the host presents the screen.

## Configuration

| Field | Default | Notes |
|-------|---------|-------|
| `customerToken` | none | Customer token minted by your backend. Selects customer token mode (recommended) |
| `apiKey` | none | The full `keyId:secret`, sent whole. Selects key mode, trusted internal apps only |
| `baseUrl` | `https://service.newinstance.cloud` | Release builds reject overrides |
| `gqlPath` / `ssePath` / `wsPath` | `/service` / `/graphql/stream` / `/graphql/ws` | Transport paths |
| `transport` / `initialMessage` | inherited / none | Overrides |
| `allowRemoteAttachmentUrls` | false | Opt-in: lets `attach(_:)` download and upload http(s) sources |
| `theme` | dashboard theme | `ChatThemeOverride(...)` local theme, ranked below the dashboard theme and above the built-in one. See [Theming](/theming) |

The config initialiser throws `LiveAndAiChatError` with code `E_CONFIG` for both credentials, neither, or a bare key ID, and `ChatUser` requires a non-empty `customerName`. The raw key and token are not readable from the config, and its descriptions redact them.

## Auth modes

**Customer token mode (recommended).** `LiveAndAiChatConfig(customerToken:)`. The SDK sends `Authorization: Bearer <token>` and the server derives the customer from the signed claims, so `setUser` fields are not sent. When a fresh token is due, call `try chat.setUserToken(freshTokenFromYourBackend)`: it swaps the credential for every later request and reconnects without destroying the SDK. A token for the same customer keeps the conversation; the SDK compares the token's subject, not the token string.

**Key mode (trusted internal apps only).** This ships your secret inside the IPA, so never use it in an app distributed to the public.

```swift
let chat = try LiveAndAiChat.Builder()
    .config(try LiveAndAiChatConfig(apiKey: "sk_live_abc123:YOUR_SECRET_KEY"))
    .build()
chat.setUser(ChatUser(customerName: "Ada", customerId: "usr_123", customerEmail: "ada@example.com"))
```

The SDK sends the full key in `x-api-key`. `setUserToken(_:)` throws `E_CONFIG` in key mode. See [Authentication and identity](/authentication-and-identity).

## API surface

`initialize`, `openChat`, `closeChat` (now also dismisses the presented chat screen), `sendMessage`, `setUser(_:)` (key mode identity), `setUserToken(_:)` (swap the customer token; throws in key mode), `attachFile(data:name:mimeType:previewUri:)` (returns the queue-item id), `attach(_:previewUri:)`, `removeAttachment` (cancels an in-flight upload), `clearAttachments`, `updateDraft(_:)`, `retryMessage`, `requestHandoff(reason:)`, `sendTypingStart` / `sendTypingStop`, `destroy`, `addDelegate` / `removeDelegate`; `LiveAndAiChat.current()` returns the most recently built instance. `present(from:)` now presents over whatever is topmost instead of requiring a modal-free host.

State: `@Published` `orgConfig`, `connectionState` and `lifecycle`, plus derived `flowState`, `messages`, `conversation`, `assignment`, `agentTyping`, `unreadCount`, `widgetOpen`, `draftText`. Delegate callbacks (all optional): `didReceiveMessage`, `didSendMessage`, `agentTypingDidChange`, `connectionStateDidChange`, `didEncounterError`, `unreadCountDidChange`, `attachmentDidUpdate`, `chatDidClose`. `lifecycle == .failed` means call `initialize()` again.

## Errors

`didEncounterError(_:)` is the channel for "why is my chat not working". Nothing on it reaches the chat interface, and it never carries the API key, the identity token, or customer data.

`LiveAndAiChatError` carries `type` (`.network`, `.validation`, `.auth`, `.system`), `message`, `recoverable`, and `code` (the raw GraphQL or transport code). Call `sdkCode(usingIdentityToken:)` to normalise onto the `ChatErrorCode` vocabulary shared with every other chat SDK:

```swift
func didEncounterError(_ error: LiveAndAiChatError) {
    switch error.sdkCode(usingIdentityToken: true) {
    case ChatErrorCode.invalidIdentityToken: refreshChatToken()
    case ChatErrorCode.invalidPublicKey: reportMisconfiguration()
    default: break
    }
}
```

Pass `usingIdentityToken: true` when the SDK is in customer token mode: an auth failure cannot otherwise tell a rejected key from a rejected token. After an auth failure the SDK does not reconnect with the same credential; a later `setUserToken(_:)` restarts it. The full code list is in **Authentication and identity -> Diagnosing problems**.

## Programmatic attachments

`attach(AttachmentRequest(source:name:mimeType:declaredSize:metadata:))` accepts `.base64`, `.dataUri`, `.filePath` (security-scoped picker URLs handled), `.bytes`, or `.remoteUrl` (config-gated); `AttachmentSource.detect(_:)` safely classifies strings. Validation runs before anything is queued: base64 must decode, files must be readable, non-empty and at most 25 MB, MIME must be png/jpeg/webp/gif/pdf, a wrong `declaredSize` is rejected as corruption, and the extension must agree with the MIME type. Uploads run off the main thread; progress and results stream through the attachment queue and `attachmentDidUpdate` (statuses queued, uploading, uploaded, failed, cancelled). A message is never sent with an attachment that has not finished uploading, and attachment contents are never logged.

## The chat close event

`chatDidClose` fires exactly once per chat presentation: the in-chat X (`closeButton`), a dismissal gesture (`gesture`), `closeChat()` (`programmatic`), `destroy()` (`sessionEnded`), or the host dismissing the screen (`hostNavigation`). The event carries reason, initiator (user/host/sdk), timestamp, conversationId, assignmentId, channel, previousState, unreadCount, hasDraft, pendingAttachmentCount and SDK metadata; never message contents, credentials or attachment data.

## Platform notes

- No Info.plist usage keys are required: attachments use the system document picker. Add a Photos usage description only if your app saves chat images.
- UI strings are inline English; there is no localisation override today, and no push support.
- `destroy()` makes the instance unusable (a later call trips a precondition); build a new instance to start again.
- `theme` and `sdkCode(usingIdentityToken:)` require 0.3.0. The two exclusive auth modes and `setUserToken(_:)` require 0.4.0, which also removes `ChatUser.customerToken` and `ChatUser.fromToken`.
- Steps from your connected MCP servers (forms and secure pages) require 0.5.0; see [MCP servers](/mcp-servers).
