# Install with AI
Source: https://docs.clariodesk.com/claude-code
Paste one prompt into Claude Code, Cursor, or Codex and your agent integrates ClarioDesk end to end — it detects your stack, asks the right questions, and verifies with a real message.
The fastest way to integrate ClarioDesk is to not write the integration
yourself. Copy the prompt below into your AI coding agent (Claude Code, Cursor,
Codex, or any agent that can edit your project), replace the placeholder with
your app's publishable key, and let it work.
The prompt makes the agent:
* **detect your framework** — Flutter, bare React Native, or Expo (including
the Expo Go evaluation lane),
* **ask you the decisions that are actually yours** — prebuilt chat UI vs.
building screens in your own design system, where support opens from, push
now or later, attachments,
* **consult these docs instead of guessing** — every step links the exact
reference page, and the [rules for AI agents](/concepts/agent-rules) are
embedded,
* **verify the result** — it finishes by round-tripping a real message to your
dashboard inbox, not by declaring success.
Your `pk_live_` key is **publishable** — it's safe to paste into a prompt and
ship in your app binary. It can only register devices; it can't read tickets.
Get it at [dashboard.clariodesk.com](https://dashboard.clariodesk.com) → your app → API key.
When you create a new app, the dashboard shows this same prompt with your key
already filled in.
## The prompt
````markdown theme={null}
# Add ClarioDesk In-App Support
Integrate the ClarioDesk SDK so users can contact support and report bugs from
inside this app, and replies from the support dashboard stream back live.
Your publishable API key for this integration: `pk_live_YOUR_KEY`
## Quick setup
Before running any commands or editing any files, present the user with this
checklist:
```
Here's what I'll do to add ClarioDesk in-app support.
1. Detect your framework (Flutter, bare React Native, Expo, or native iOS/Swift)
2. Ask a few questions about how you want support to work
3. Install the SDK and initialize it with your API key
4. Add a support entry point (prebuilt chat UI, or headless in your own UI)
5. Verify it works by sending a real test message
Shall I proceed?
```
## Step 1: Detect the stack
Inspect the project root:
- `pubspec.yaml` present → **Flutter**.
- `package.json` with an `expo` dependency (or `app.json`/`app.config.*` with an
`expo` key) → **Expo**. Then determine the lane: if the project uses a dev
client / EAS builds (`expo-dev-client` dependency, `eas.json`) it gets the
full experience; if the user only runs Expo Go, everything works for
evaluation but the device key is a software fallback and remote push is
unavailable — say so, and recommend a dev build before shipping.
- `package.json` with `react-native` but no `expo` → **bare React Native**
(old vs. New Architecture doesn't matter — the SDK supports both; don't
branch on it).
- An `.xcodeproj`/`.xcworkspace` (or a `Package.swift` app target) with no
Flutter/RN markers → **native iOS (Swift)**. Requires iOS 16+; if the
deployment target is lower, say so before proceeding.
- Empty or non-mobile directory → ask which framework the user wants; offer to
scaffold (`flutter create` / `npx create-expo-app`) and continue.
## Step 2: Ask the integration decisions
Ask the user these questions before writing code. Do not assume answers you
cannot infer from the codebase; if the user has no preference, use the marked
default.
1. **Prebuilt UI or your own?** The prebuilt chat UI is themeable and ships a
complete inbox → thread → new-ticket → bug-report flow with one call
*(default)*. Alternatively, integrate headless and render support screens in
the app's own design system — more work, fully native look.
2. **Where does support open from?** A settings row, a "Help" button, a menu
item — find where the app keeps such actions and propose a spot.
3. **Push notifications for agent replies — now or later?** Later is fine and
is the default; it's additive. (Requires Firebase setup on the host app;
Expo Go can't do remote push at all.)
4. **Attachments (screenshots/photos on messages) — enabled?** Default yes for
the prebuilt UI if the app already has photo permissions, otherwise ask —
iOS requires photo-library/camera usage strings in Info.plist.
## Step 3: Install and initialize
### Flutter
```bash
flutter pub add clariodesk
```
Initialize at app start, before `runApp`:
```dart
import 'package:clariodesk/clariodesk.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
// First launch generates a hardware-backed device key and registers it.
// Later launches reuse it — no network round-trip.
await ClarioDesk.init(apiKey: 'pk_live_YOUR_KEY');
runApp(const MyApp());
}
```
Platform floors: iOS 13+, Android API 23+.
### React Native (bare and Expo)
```bash
npm install @clariodesk/react-native react-native-get-random-values
```
(Substitute the project's package manager: pnpm/yarn/bun per the lockfile.)
Secure storage peer — pick one:
```bash
npx expo install expo-secure-store # Expo
npm install react-native-keychain # bare RN
```
**Expo**: add the config plugin to app.json, then make a dev build:
```jsonc
{ "expo": { "plugins": ["@clariodesk/react-native"] } }
```
```bash
npx expo prebuild && npx expo run:ios
```
(Skip prebuild for Expo Go evaluation — the SDK transparently uses a software
key there.)
**Bare RN**: run `npx install-expo-modules@latest` once (the device-key native
module is an Expo Module), then `cd ios && pod install`.
Initialize — and note the RNG import **must be the first import in the app
entry file** (index.js), before anything else:
```ts
// index.js — very first line:
import 'react-native-get-random-values';
import { ClarioDesk } from '@clariodesk/react-native';
await ClarioDesk.init({ apiKey: 'pk_live_YOUR_KEY' });
```
### Native iOS (Swift)
Add the package via Swift Package Manager — in Xcode, File → Add Package
Dependencies… with the URL, or in Package.swift:
```swift
.package(url: "https://github.com/clariodesk/clariodesk-swift", from: "0.1.1")
```
Link the products the app needs: `ClarioDesk` (headless core) and — for the
prebuilt UI lane — `ClarioDeskUI`. A headless host links only `ClarioDesk`;
Swift dead-strips the UI. Requires iOS 16+.
Initialize at app start (fire-and-forget, no await — AppDelegate or the
`App` initializer):
```swift
import ClarioDesk
ClarioDesk.initialize(.init(apiKey: "pk_live_YOUR_KEY"))
```
The device key is CryptoKit/Secure Enclave-backed (software fallback on the
simulator). Note: an **unsigned** host can't write the Keychain
(`errSecMissingEntitlement -34018`) — real signed apps are unaffected.
### Wire identity (all platforms)
First check whether Dashboard → Verified identity is enabled for this app.
Without it, `identify()` is label-only metadata and should be called in two
places; with it, use the configured token provider and the ordered lifecycle in
`references/identity-and-keys.md` instead.
1. **On every app launch**, hydrated from the app's persisted session. This is
the step integrations most often miss: existing logged-in users never pass
through the login screen again, so a login-only hook leaves them unlabeled.
2. **Right after a fresh login/signup.**
```dart
// Flutter
await ClarioDesk.identify(externalId: user.id, email: user.email, traits: {'plan': user.plan});
```
```ts
// React Native
await ClarioDesk.identify({ externalId: user.id, email: user.email });
```
```swift
// Swift
try await ClarioDesk.identify(externalId: user.id, email: user.email)
```
In label-only mode, acknowledged `reset()` → `init()` can isolate a shared
device. In Verified identity mode, logout/account switch must await
`clearIdentity()` before changing the host user; `reset()` is reserved for
removing this installation.
If the app has no auth, skip `identify()` entirely — anonymous tickets work;
the dashboard shows the device with an "Unverified device" badge.
## Step 4: Add the support UI
### Prebuilt UI lane (default)
**Flutter** — pass a theme at init so the modal matches the app, then open it
from the entry point chosen in Step 2:
```dart
await ClarioDesk.init(
apiKey: 'pk_live_YOUR_KEY',
theme: ClarioDeskTheme(
primaryColor: /* app accent */,
backgroundColor: /* app background */,
surfaceColor: /* cards/bubbles */,
textColor: /* primary text */,
mutedTextColor: /* secondary text */,
appBar: ClarioDeskAppBarTheme(title: 'Support'),
),
);
// From the entry point:
ClarioDesk.openInbox(context);
```
Pull the actual colors from the app's existing theme/design tokens — do not
invent a palette. The theme surface is deliberately small (colors + app bar
only); if the user wants more control than that, they want the headless lane.
**React Native** — wrap the app once, open from anywhere:
```tsx
import { ClarioDeskProvider, openInbox } from '@clariodesk/react-native/ui';
;
// From the entry point:
openInbox(); // also: openTicket(id), openNewConversation(), openBugReport()
```
**Swift** — one call from SwiftUI or UIKit; the modal presents on the
top-most view controller:
```swift
import ClarioDeskUI
// From the entry point:
ClarioDeskWidgets.openInbox(theme: ClarioDeskTheme(primary: /* app accent */))
// also: openBugReport()
```
The theme is 13 optional tokens — unset ones derive light/dark from the
system; pull any you do set from the app's existing design tokens.
The prebuilt UI presents in a self-owned modal — it never touches the app's
navigation (GoRouter/Navigator 2.0/react-navigation/SwiftUI NavigationStack
all unaffected).
If attachments were enabled in Step 2 (React Native): install the picker peers
(`npx expo install expo-image-picker expo-document-picker expo-image-manipulator`
for Expo; `npm install react-native-image-picker @react-native-documents/picker`
for bare RN), pass a `picker` to `ClarioDeskProvider`, and add the iOS usage
strings (`NSPhotoLibraryUsageDescription`, plus camera/microphone if camera
capture is on) to Info.plist.
### Headless lane (custom UI in the app's design system)
Build the support screens with the app's own components on top of the SDK's
methods and live subscriptions. Follow the API exactly as documented — do not
guess method names:
- Flutter: https://docs.clariodesk.com/flutter/api-reference
(`createTicket`, `sendMessage`, `ticketsStream()`, `messagesStream(id)`, …)
- React Native: https://docs.clariodesk.com/react-native/api-reference — use
the hooks (`useTickets`, `useMessages`, `useConnection`, …) from
`@clariodesk/react-native/hooks`.
- Swift: https://docs.clariodesk.com/swift/api-reference — replay-last
`AsyncStream` reads (`for try await tickets in ClarioDesk.tickets`) from
SwiftUI, or the closure variants (`subscribeTickets { … }`) from UIKit.
Match the app's existing screens: reuse its list items, buttons, spacing, and
color tokens so the support flow looks native to the app. Render optimistic
state: a message with `pending: true` shows "sending…", `failed: true` gets
tap-to-retry via `retryMessage`.
## Step 5: Push notifications (only if chosen in Step 2)
**React Native** — push is a deliberately separate package (Firebase's native
code must not autolink into apps that didn't opt in):
```bash
npm install @clariodesk/react-native-push @react-native-firebase/app @react-native-firebase/messaging
```
```ts
import { FirebaseTokenProvider } from '@clariodesk/react-native-push';
await ClarioDesk.init({ apiKey: 'pk_live_YOUR_KEY', pushTokenProvider: new FirebaseTokenProvider() });
```
Register the background handler in index.js at startup (the #1 push
setup-failure cause), and route notification taps with
`ClarioDesk.openTicketFromPush(data)`. Full guide:
https://docs.clariodesk.com/react-native/push
**Flutter** — the SDK takes an FCM token through a `PushTokenProvider` seam
(it never imports firebase_messaging itself). Follow
https://docs.clariodesk.com/concepts/push-notifications for the wiring.
**Swift** — APNs-direct, **no Firebase**. Forward the raw APNs token from the
AppDelegate — that's the whole wiring:
```swift
func application(_ app: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
ClarioDesk.setAPNSDeviceToken(token)
}
```
Full guide: https://docs.clariodesk.com/swift/push
Every platform also needs the app's push credentials uploaded once in the
ClarioDesk dashboard (app settings → push): FCM service account for
Flutter/React Native, an APNs `.p8` key for Swift.
## Step 6: Verify
Run the app (`flutter run` / `npx expo run:ios` / `npx react-native run-ios`
/ Xcode ⌘R). Then, as the user would:
1. Open the support entry point and send a message ("test from ClarioDesk
setup").
2. Confirm the message appears in the dashboard inbox at
https://dashboard.clariodesk.com — if the app-setup page is open, its
"Watching for devices" check flips as soon as the app registered.
3. Reply from the dashboard and confirm the reply streams into the app live
(no restart).
If registration or realtime fails, check the troubleshooting guides:
https://docs.clariodesk.com/integrate-flutter /
https://docs.clariodesk.com/integrate-react-native /
https://docs.clariodesk.com/integrate-swift
## When unsure, use the docs
Never guess an API. Every page below is also available as plain markdown for
you to fetch, and https://docs.clariodesk.com/llms.txt indexes everything.
| Topic | URL |
|---|---|
| Quickstart (all platforms) | https://docs.clariodesk.com/quickstart |
| Flutter integration walkthrough | https://docs.clariodesk.com/integrate-flutter |
| Flutter install / lifecycle / prebuilt UI / API | https://docs.clariodesk.com/flutter/installation · /flutter/lifecycle · /flutter/prebuilt-ui · /flutter/api-reference |
| React Native integration walkthrough | https://docs.clariodesk.com/integrate-react-native |
| RN install / lifecycle / prebuilt UI / push / API | https://docs.clariodesk.com/react-native/installation · /react-native/lifecycle · /react-native/prebuilt-ui · /react-native/push · /react-native/api-reference |
| Swift integration walkthrough | https://docs.clariodesk.com/integrate-swift |
| Swift install / lifecycle / prebuilt UI / push / API | https://docs.clariodesk.com/swift/installation · /swift/lifecycle · /swift/prebuilt-ui · /swift/push · /swift/api-reference |
| How auth works (device-is-identity) | https://docs.clariodesk.com/concepts/authentication |
| Identity lifecycle (launch/login/logout/switch) | https://docs.clariodesk.com/concepts/identity-lifecycle |
| Attachments / realtime / CSAT | https://docs.clariodesk.com/concepts/attachments · /concepts/realtime · /concepts/csat |
| Rules for AI agents | https://docs.clariodesk.com/concepts/agent-rules |
## Critical rules
- `pk_live_YOUR_KEY` is a **publishable** key — safe to ship in the app
binary. It can only register new devices; it cannot read tickets or
impersonate users. There is no secret key and no `appId` — the key encodes
the app.
- The SDK's device key always authenticates requests. Do not invent additional
auth in label-only mode. If Verified identity is enabled, use exactly its
configured host-token/OIDC profile and never ship the signing secret.
- Label-only mode identifies on every launch. Verified mode uses a fresh proof
on session restore/login and `clearIdentity()` before logout/switch;
`reset()` deprovisions only this installation.
- Don't mix modes: either the prebuilt UI owns the support screens, or the
app's custom screens do — not both for the same flow.
- React Native: `import 'react-native-get-random-values'` must be the **first
import** in the entry file.
- React Native push lives in `@clariodesk/react-native-push` — never add
Firebase packages to an app that didn't opt into push.
- Native iOS (Swift) push is APNs-direct — never add Firebase to a Swift
host; the raw APNs token is all the SDK needs.
- Expo Go is for evaluation only (software device key, no remote push) — ship
with a dev build.
- Do not read or print the contents of the user's `.env` or credential files;
the only credential this integration needs is the publishable key above.
## After setup
Tell the user to send themselves a first support message from the app, then
reply to it from the dashboard inbox (https://dashboard.clariodesk.com) and
watch it arrive in-app live. Suggest exploring next: push notifications for agent
replies, attachments, and CSAT ratings — each is an additive follow-up
(https://docs.clariodesk.com).
````
## Connect the docs MCP server
These docs also run a hosted MCP server at `https://docs.clariodesk.com/mcp`
(tools: `search_clario_desk` for search, `query_docs_filesystem_clario_desk`
for reading pages). With it connected, your agent retrieves live ClarioDesk
docs mid-task instead of guessing.
```bash Claude Code theme={null}
claude mcp add --transport http clariodesk-docs https://docs.clariodesk.com/mcp
```
```json Cursor (.cursor/mcp.json) theme={null}
{
"mcpServers": {
"clariodesk-docs": { "url": "https://docs.clariodesk.com/mcp" }
}
}
```
```json VS Code (.vscode/mcp.json) theme={null}
{
"servers": {
"clariodesk-docs": { "type": "http", "url": "https://docs.clariodesk.com/mcp" }
}
}
```
## Working docs-first instead?
If you'd rather have your agent research as it goes, point it at the
machine-readable docs:
* [`llms.txt`](https://docs.clariodesk.com/llms.txt) — index of every page;
[`llms-full.txt`](https://docs.clariodesk.com/llms-full.txt) — the full docs
in one file.
* The distilled [rules for AI agents](/concepts/agent-rules).
* Every page here can be pulled as markdown by appending `.md` to its URL, or
through the docs MCP server above.
# Rules for AI agents
Source: https://docs.clariodesk.com/concepts/agent-rules
Critical rules for integrating the ClarioDesk SDK with an AI coding agent — key handling, identity, package boundaries, and the mistakes to avoid.
If you are an AI coding agent integrating ClarioDesk into an app, these rules
override anything you might assume from other SDKs. They exist because each one
is a mistake agents actually make.
For a complete guided integration, use the [install prompt](/claude-code) — it
walks the whole flow including the questions to ask the developer. This page is
the distilled rule set to keep in context while you work.
## The rules
1. **The `pk_live_…` key is publishable — safe to ship in the app binary.** It
only authorizes a fresh install to register a device; it cannot read tickets
or impersonate users. There is no secret key to hide, no `appId` parameter,
and no server-side environment variable — the key encodes the app. Do not
move it to a backend, do not build a key-proxy endpoint.
2. **Never build auth for ClarioDesk.** There are no JWTs, no refresh tokens,
no session endpoints. The SDK generates a non-extractable hardware-backed
device key on first launch and signs every request with it
(device-is-identity — see [Authentication](/concepts/authentication)).
`identify({ externalId, email, traits })` is **metadata only** and grants no
access.
3. **Call `identify()` on every app launch for logged-in users**, hydrated from
the app's persisted session — not only in the login flow. Users who were
already signed in before the SDK was added never pass through login again;
a login-only hook leaves them unlabeled. `identify()` is idempotent, so
repeated calls are correct, not wasteful. See
[Identity lifecycle](/concepts/identity-lifecycle).
4. **`reset()` on logout; never rebind a device.** Account switch =
`reset()` → `init()` → `identify()`. Each device row carries an independent
ticket history — rebinding one device across users is always wrong.
5. **Two modes, one install — don't mix them.** Either the prebuilt UI
(Flutter: `ClarioDesk.openInbox(context)`; React Native:
`@clariodesk/react-native/ui`) owns the support screens, or the app's custom
screens built on the headless API do. Don't hand-roll one screen inside the
prebuilt flow.
6. **React Native: `import 'react-native-get-random-values'` must be the first
import in the entry file** — before any other import, or key generation
fails at runtime.
7. **Push is a separate package on purpose.** React Native push lives in
`@clariodesk/react-native-push` because Firebase's native code autolinks
into every host that installs it — never add it (or any
`@react-native-firebase/*` package) to an app that didn't opt into push.
Flutter receives FCM tokens through the `PushTokenProvider` seam; the SDK
never imports `firebase_messaging`.
8. **Expo Go is an evaluation lane, not a shipping lane.** The SDK
transparently falls back to a software P-256 key there (no native modules)
and remote push is unavailable. Everything else works. Recommend a dev
build (`npx expo prebuild && npx expo run:ios`) before release.
9. **Attachments need iOS usage strings.** Enabling the picker means the host
app must declare `NSPhotoLibraryUsageDescription` (plus camera/microphone
variants if capture is enabled) in Info.plist.
10. **Never guess an API name.** The full surface is small and documented —
Flutter: [API reference](/flutter/api-reference); React Native:
[API reference](/react-native/api-reference). Machine-readable index:
[`llms.txt`](https://docs.clariodesk.com/llms.txt) /
[`llms-full.txt`](https://docs.clariodesk.com/llms-full.txt); any page is
fetchable as markdown by appending `.md` to its URL. If the ClarioDesk
docs MCP server is connected (`https://docs.clariodesk.com/mcp` — see
[Install with AI](/claude-code)), prefer its `search_clario_desk` tool
over web fetches.
## Quick facts
| Fact | Flutter | React Native |
| -------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| Package | `clariodesk` (pub.dev) | `@clariodesk/react-native` (+ `/ui`, `/hooks` subpaths; `@clariodesk/react-native-push` separate) |
| Install | `flutter pub add clariodesk` | `npm install @clariodesk/react-native react-native-get-random-values` + secure-storage peer |
| Init | `await ClarioDesk.init(apiKey: 'pk_live_…')` | `await ClarioDesk.init({ apiKey: 'pk_live_…' })` |
| Prebuilt UI | theme at `init`, `ClarioDesk.openInbox(context)` | `` + `openInbox()` |
| Reactive reads | `ticketsStream()` / `messagesStream(id)` | `subscribe*` fns or hooks from `…/hooks` |
| Platform floor | iOS 13+, Android API 23+ | bare RN (old + New Arch), Expo dev build, Expo Go (degraded) |
## Integration flow
Detect the stack → ask the developer the four decisions (prebuilt vs. custom
UI, entry point, push, attachments) → install → `init` + `identify` wiring →
UI → **verify by round-tripping a real message** to the dashboard inbox. The
[install prompt](/claude-code) encodes this flow end to end.
# Architecture
Source: https://docs.clariodesk.com/concepts/architecture
The model behind all three SDKs. Read this once and the rest of the API explains itself.
All three SDKs are a thin client over the ClarioDesk backend. They own four things
(a device identity, a local cache, a realtime connection, and an optional set of
prebuilt screens) and expose a small, identical public API on top.
## The contract is tiny
The SDK keeps all of its state internal and private. Your app's state
management (Riverpod, Bloc, Provider, Redux, Zustand, Context, MobX) is never
involved. There are only ever a handful of calls from your app into the SDK:
1. `init(apiKey)` once at app start.
2. `identify({ externalId, email, traits })` when the user logs in or changes.
3. `handlePushPayload(...)` / `openTicketFromPush(...)` when a push fires.
4. `reset()` on logout or user switch.
> That's the entire contract. Your app sends identity and push events in; reads
> come back to you as live streams (Flutter), subscriptions/hooks (React
> Native), or `AsyncStream`s (Swift), not as state you have to wire.
## Headless vs prebuilt
Public methods + live reads only. You build the UI in your own design
system. This is the primary integration mode.
Inbox, ticket detail, new ticket, and bug report, presented in a
self-owned modal. Limited theming (colors + app bar). Minutes to integrate.
Both modes ship in the same package. The prebuilt screens are just a
consumer of the same headless methods.
## Routing: the sandbox model
The prebuilt UI never touches your router (GoRouter, Navigator 2.0, React
Navigation, Expo Router). Your app pushes one route (a full-screen modal), and
the SDK owns a private nested navigator inside it for all internal navigation
(inbox → ticket detail → compose → image viewer). Control returns to your app
when the user dismisses the modal.
## Data model
Three objects you'll touch:
| Object | What it is |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Ticket** | One support conversation. Has a status (`new` → `open` → `resolved` → `closed`), an unread indicator, and a last-message preview. |
| **Message** | One entry in a ticket thread. Carries `body`, an author type (end user / agent / system), a timestamp, and any `attachments`. |
| **Attachment** | A photo, video, or file bound to a message. Rendered via presigned URLs. |
## Where to go next
Why there are no JWTs and the publishable key is safe to ship.
The four host events that touch the SDK: login, launch, logout, switch.
# Attachments
Source: https://docs.clariodesk.com/concepts/attachments
Photos, videos, and files on any message, both directions.
Any message can carry attachments, on both `createTicket` and `sendMessage`,
in both directions (the user sends; agent-sent media renders in the thread).
## Limits
| Rule | Value |
| ------------ | --------------- |
| Per message | ≤ 4 attachments |
| Image | ≤ 10 MB |
| Video / file | ≤ 50 MB |
## How upload works
The SDK runs a two-phase upload so bytes go straight to storage, never through
the ClarioDesk API:
The SDK requests a presigned upload target for each file.
Images are normalized to JPEG client-side (this strips HEIC/EXIF and caps
dimensions). The SDK then PUTs the bytes directly to Cloudflare R2, or to
Cloudflare Stream for video, which transcodes to universal H.264 and
produces a free poster frame.
On message send, the SDK binds the uploaded ids to the message.
`Message.attachments` then carries the rendered payloads.
## Progress
Each upload exposes per-tile progress so you can render a progress ring:
```dart Flutter theme={null}
// ValueListenable per upload
final progress = ClarioDesk.attachmentUploadProgress(localId);
```
```ts React Native theme={null}
const progress = ClarioDesk.attachmentUploadProgress(stubId);
```
## Sending
```dart Flutter theme={null}
await ClarioDesk.sendMessage(
ticketId,
'see screenshot',
attachments: [OutgoingAttachment.file(path)],
);
```
```ts React Native theme={null}
await ClarioDesk.sendMessage(ticketId, 'see screenshot', {
attachments: [{ uri, mime: 'image/jpeg', kind: 'image' }],
});
```
In React Native, pass a `MediaPicker` / `ImageNormalizer` via
`init({ attachments: { picker, normalizer } })`, or build and upload
`OutgoingAttachment`s yourself.
## Reading
Presigned GET URLs expire. If an image fails to load because its URL aged out,
re-presign it:
```text theme={null}
refreshAttachmentUrl(attachmentId)
```
Video playback is external in v1 on Flutter and React Native. An in-app HLS
player would ship a heavy native dependency to every host, so video uses
external/system playback there. The Swift SDK plays video in-app via
`AVPlayer` (free and native on iOS). The in-app image viewer is a
lightweight, zero-dependency zoomable view on all three.
## iOS permission strings
The native media picker requires usage strings in your iOS app's `Info.plist`:
`NSPhotoLibraryUsageDescription`, `NSCameraUsageDescription`, and
`NSMicrophoneUsageDescription` (for video). The React Native Expo config plugin
can add these for you. See the [RN installation guide](/react-native/installation).
# Authentication
Source: https://docs.clariodesk.com/concepts/authentication
Device-signed SDK auth, plus optional verified cross-device identity.
ClarioDesk does not use bearer tokens for its base SDK authentication. Identity is the device,
proven by a non-extractable hardware keypair. This removes an entire class of
setup and makes a leaked publishable key a non-event. Apps that need
cross-device continuity can separately enable [Verified identity](/concepts/verified-identity)
with a host token or pinned OIDC provider.
## How it works
On first launch the SDK generates an ECDSA P-256 keypair inside the device's
secure hardware:
* **iOS:** Secure Enclave when present (most iPhones since the 5s), software
Keychain otherwise. Requires iOS 13+.
* **Android:** StrongBox when supported (Pixel 3+, recent Samsung), TEE-backed
Keystore otherwise. Requires API 23+ (Marshmallow).
The private key is never extractable. The SDK registers the public key with the
backend via a challenge-response handshake, then signs every authenticated
request (and the realtime handshake) with the private key. The backend records
which tier produced the key: `secure_enclave`, `tee`, `strongbox`, or
`software`.
**React Native + Expo Go:** native modules can't load in Expo Go, so the SDK
transparently falls back to a software ECDSA P-256 key. You can evaluate the
entire flow in Expo Go and ship hardware-backed with no code change. The backend
records the `software` attestation either way.
## The publishable key
You ship one publishable key (`pk_live_…`) in your app binary. Its only
capability is letting a fresh install register a device. It cannot read tickets,
list users, or impersonate anyone.
If your key leaks, an attacker can register throwaway devices (rate-limited and
bounded), but they cannot touch any existing user's data, because every
authenticated call is signed by a private key that never left the original
device's secure hardware.
> You ship the key and your users are secure by default. There's no separate
> "secure mode" to enable.
## Label-only identity
`identify({ externalId, email, traits })` attaches your user's identity to the
device row. It is pure metadata: it grants no access, because access is already
established by the device key. If you never call `identify()`, tickets still
work; agents just see an "Unverified device" badge instead of an email.
This is why `identify()` is safe to call unconditionally on every launch. See
[Identity & lifecycle](/concepts/identity-lifecycle).
When Verified identity is enabled, `identityToken` is an authorization proof,
not metadata. Invalid proofs fail closed, logout uses `clearIdentity()`, and
normal device reset is no longer the account-switch primitive.
# CSAT
Source: https://docs.clariodesk.com/concepts/csat
An inline satisfaction rating when an agent resolves a ticket.
When an agent resolves a ticket, ClarioDesk can prompt the end user for a quick
satisfaction rating. It uses the existing resolve flow: there's no separate
screen or modal, and the rating itself is the confirmation.
## The flow
1. An agent marks a ticket resolved.
2. The SDK surfaces an inline five-face rating prompt (😔 → 😄) in the thread.
3. The user taps a face (and can add an optional comment).
4. A rating of 3 or higher advances the ticket `resolved → closed`. Reopening
stays a separate action — an unhappy user just replies in the thread, which
reopens the ticket as usual.
The rating is cached on the ticket, so you can render the resolved state
correctly without a second fetch.
## Submitting a rating
If you build your own UI, submit the rating with one call:
```dart Flutter theme={null}
await ClarioDesk.submitCsat(ticketId, score: 5, comment: 'Fast fix, thanks!');
```
```ts React Native theme={null}
await ClarioDesk.submitCsat(ticketId, { score: 5, comment: 'Fast fix, thanks!' });
```
```swift Swift theme={null}
try await ClarioDesk.submitCsat(ticketId: ticket.id, score: 5, comment: "Fast fix, thanks!")
```
`score` is a 5-point scale, low to high: `1` (😔) … `5` (😄).
If you use the [prebuilt UI](/flutter/prebuilt-ui), the CSAT prompt is rendered
for you on resolve, so you don't need to call `submitCsat` yourself.
# Identity & lifecycle
Source: https://docs.clariodesk.com/concepts/identity-lifecycle
The four host events that touch the SDK: wire each one and you're done.
Four moments in your app's lifecycle interact with the SDK. Wire all four and
the integration is complete. The code below is platform-agnostic; see the
per-SDK lifecycle guides for exact syntax
([Flutter](/flutter/lifecycle), [React Native](/react-native/lifecycle),
[Swift](/swift/lifecycle)).
## 1. App launch (every time)
The most common integration miss: users who installed your app before you added
ClarioDesk never go through login again, so an "identify on login" hook alone
leaves them as unlabeled devices.
Hydrate from your own persisted session on launch and identify
**unconditionally**. `identify()` is idempotent (same values = no-op write) and
cheap (one signed POST, roughly 5 to 25 ms).
```dart Flutter theme={null}
await ClarioDesk.init(apiKey: 'pk_live_…');
final user = await yourAuth.currentUser();
if (user != null) {
await ClarioDesk.identify(externalId: user.id, email: user.email);
}
```
```ts React Native theme={null}
await ClarioDesk.init({ apiKey: 'pk_live_…' });
const user = await yourAuth.currentUser();
if (user) await ClarioDesk.identify({ externalId: user.id, email: user.email });
```
## 2. Fresh login or signup
Right after your auth succeeds, call `identify()`. This overwrites the label on
the existing device row, without registering a new device or generating a new
key. The same device now carries the new user's metadata.
## 3. Logout
With [Verified identity](/concepts/verified-identity), call
`clearIdentity()` before signing the user out of your own app.
```dart Flutter theme={null}
await ClarioDesk.clearIdentity();
await yourAuth.signOut();
```
```ts React Native theme={null}
await ClarioDesk.clearIdentity();
await yourAuth.signOut();
```
`clearIdentity()` preserves the device key, advances the signed identity epoch,
and prevents old-account cache/network work from becoming visible to the next
host user. In label-only mode, `reset()` remains the stronger shared-device
isolation option.
## 4. User switch (A → B without restart)
Create and await the `clearIdentity()` intent, switch the host session, then
verified-identify user B with a fresh proof.
Never activate user B before the clear intent exists, and never fall back to a
label write after proof failure. `reset()` removes this installation; customer
erasure is a separate owner/admin dashboard action.
## What if I never call `identify()`?
Tickets still work. The device row exists and agents see a device id but no
email, and the dashboard shows an "Unverified device" badge. Useful for
anonymous-feedback flows; otherwise, wire case 1 above.
# Push notifications
Source: https://docs.clariodesk.com/concepts/push-notifications
Deep-link straight to the right ticket when the user taps a notification.
When an agent replies (or a ticket's status changes) while the user is away,
ClarioDesk can fire an OS push notification. On tap, the SDK opens itself
directly to the relevant ticket, with no host-side routing logic required.
## How it's wired
Push goes through a `PushTokenProvider` seam rather than importing a messaging
library directly. Your app owns the Firebase/FCM plugin; the SDK just asks it
for a token. This keeps Firebase's native code out of hosts that don't want
push.
* **React Native:** push lives in a separate package,
`@clariodesk/react-native-push`, so Firebase never autolinks into a headless
host's binary. See the [RN push guide](/react-native/push).
* **Flutter:** the `PushTokenProvider` abstraction lets you hand the SDK an FCM
token without the SDK depending on `firebase_messaging`.
* **Swift (native iOS):** no Firebase at all — forward the raw APNs token with
`ClarioDesk.setAPNSDeviceToken(token)` from your AppDelegate. See the
[Swift push guide](/swift/push).
## Delivery
End-user push goes out via FCM HTTP v1. iOS is reached through FCM → APNs.
Each customer uploads their own service-account JSON per app in the dashboard,
so notifications come from your own Firebase project.
Native Swift hosts skip Firebase entirely: upload an APNs auth key (`.p8`)
instead, and delivery goes straight to APNs from your own Apple credentials.
The backend is presence-aware: if the user is actively connected, the push
is suppressed (they're already seeing the reply live).
## Handling a tap
Route ClarioDesk messages in your shared push handler so they don't collide with
your app's own notifications, then deep-link on tap:
```dart Flutter theme={null}
// In your push handler:
ClarioDesk.handlePushPayload(payload);
```
```ts React Native theme={null}
// Distinguish ours in a shared background handler:
if (ClarioDesk.isClarioMessage(msg.data ?? {})) return;
// On notification tap:
ClarioDesk.openTicketFromPush(data); // or the usePushOpened() hook
```
**React Native:** registering the background message handler in `index.js` at
startup is the #1 push setup-failure cause. Make sure it runs at app entry, not
inside a component.
See the [RN push guide](/react-native/push) for the full Firebase wiring.
# Realtime
Source: https://docs.clariodesk.com/concepts/realtime
Agent replies, status changes, presence, and read receipts stream to the device live.
Reads in ClarioDesk are live, so you don't poll the backend. The SDK holds a
realtime connection and pushes updates into your streams (Flutter),
subscriptions and hooks (React Native), or `AsyncStream`s (Swift) as they
happen.
## Transport
The SDK connects over Centrifugo by default and falls back to SSE (Server-Sent
Events) automatically when Centrifugo isn't reachable. The connection handshake
is signed by the device key, same as every other authenticated request. You
don't configure any of this. It's on by default.
## Connection state
You can observe the connection so your UI can show an offline banner or a
reconnecting spinner. The state machine is:
| State | Meaning |
| -------------- | ------------------------------------------------- |
| `connecting` | First connection attempt in progress. |
| `connected` | Live; updates are streaming. |
| `reconnecting` | Dropped; automatically retrying with backoff. |
| `disconnected` | Not connected (e.g. app backgrounded). |
| `fatal` | Unrecoverable (e.g. device revoked); won't retry. |
```dart Flutter theme={null}
ClarioDesk.connectionStream().listen((state) {
// 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'fatal'
});
```
```ts React Native theme={null}
const conn = useConnection(); // or ClarioDesk.subscribeConnection(cb)
```
## What streams live
* **Messages:** agent replies appear in the open thread instantly.
* **Tickets:** status changes and unread indicators update in the inbox.
* **Typing:** whether a support agent is currently typing.
* **Agent presence:** whether an agent is online.
* **Read receipts:** when the agent has seen the user's message.
## Optimistic sends & the offline outbox
Writes are optimistic. A message you send appears immediately as pending
("sending…"). On success it settles; on rejection it's marked failed so you can
render tap-to-retry. A send made while offline stays pending and auto-flushes on
reconnect via a durable outbox, so nothing is lost if the network drops
mid-send.
See the per-SDK API references for the exact flags and retry method
([Flutter](/flutter/api-reference), [React Native](/react-native/api-reference),
[Swift](/swift/api-reference)).
# Verified identity
Source: https://docs.clariodesk.com/concepts/verified-identity
Secure cross-device customer continuity with host tokens or a pinned OIDC provider.
Verified identity is implemented but release-gated. Enable it only for a
controlled staging app until your ClarioDesk rollout contact confirms the
production gates are complete.
By default, a ClarioDesk device key authenticates one installation and
`identify({ externalId, … })` is an unverified label. Verified identity adds a
cryptographic proof so multiple devices with the same proven `(issuer, sub)`
share one canonical customer and ticket history.
## Choose an assurance mode
* **Host token (recommended for apps with a backend):** your authenticated
backend mints a short-lived HS256 or ES256 JWT. The default profile binds it
to `ClarioDesk.deviceThumbprint` through `cnf.jkt`.
* **OIDC:** ClarioDesk verifies a pinned Firebase, Supabase, Auth0, Clerk, or
custom OIDC profile. These proofs are bearer assurance, so access expires
with a short lease and the SDK re-verifies.
The dashboard mode selects the verifier. Token headers never select a provider.
Mode, issuer, audience, and the meaning of `sub` lock after first verified use.
## Lifecycle
```text theme={null}
session restore/login → verified identify(token)
logout/switch → clearIdentity() before changing host user
remove this install → reset() (acknowledged device revoke)
delete the customer → Dashboard customer erase
```
An invalid token never falls back to a label write. The SDK quarantines the
old identity epoch and private work remains suspended until a valid identify or
clear succeeds.
## Host-token claims
Use a stable opaque user ID—not email—as `sub`. Include exact `iss`, `aud`,
`iat`, `exp`, unique `jti`, and the RFC 7638 device thumbprint:
```json theme={null}
{
"iss": "clariodesk-identity",
"aud": "your-app-id",
"sub": "usr_123",
"iat": 1784700000,
"exp": 1784703600,
"jti": "unique-token-id",
"cnf": { "jkt": "device-thumbprint" }
}
```
The token endpoint must authenticate the host session, derive `sub` from that
session, validate the submitted thumbprint, use CSRF protection where needed,
and rate-limit requests. Never ship the identity secret in an app or browser.
## Browser origins
Browser registration is disabled until an owner/admin adds exact origins to
Dashboard → App → Settings → API keys. Wildcards are not supported. Every web
device is pinned to its registration origin on all signed requests.
`reset()` is not logout or account deletion. Normal logout uses
`clearIdentity()` so the device key survives and ordered account switching stays
safe.
# Flutter API reference
Source: https://docs.clariodesk.com/flutter/api-reference
The headless public surface of the Flutter SDK.
All methods are static on `ClarioDesk`. Reads are exposed as `Stream`s that
auto-prime and then live-update.
## Lifecycle
### `init`
```dart theme={null}
Future ClarioDesk.init({
required String apiKey,
ClarioDeskTheme? theme,
IdentityTokenProvider? identityTokenProvider,
})
```
Call once at app start. Generates the hardware key + registers the device on
first launch; reuses it afterward. Pass a `theme` only if you use the
[prebuilt UI](/flutter/prebuilt-ui).
### `identify`
```dart theme={null}
Future ClarioDesk.identify({
String? externalId,
String? name,
String? email,
Map traits,
String? identityToken,
IdentityMode? identityMode,
})
```
Without a token, attach unverified display metadata. With a token/provider,
perform [Verified identity](/concepts/verified-identity). Proof failures never
fall back to labels.
```dart theme={null}
Future get ClarioDesk.deviceThumbprint
Future ClarioDesk.clearIdentity()
```
Use `clearIdentity()` before Verified-identity logout/account switch.
### `reset`
```dart theme={null}
Future ClarioDesk.reset()
```
Acknowledged one-installation revoke, followed by hardware-key/cache wipe. It
is not logout or customer erase. The next `init()` registers a fresh device.
## Tickets & messages
### `createTicket`
```dart theme={null}
Future ClarioDesk.createTicket({
required String subject,
required String body,
List attachments,
})
```
### `sendMessage`
```dart theme={null}
Future ClarioDesk.sendMessage(
String ticketId,
String body, {
List attachments,
})
```
Optimistic: the message appears as pending immediately and settles on success.
### `ticketsStream`
```dart theme={null}
Stream> ClarioDesk.ticketsStream()
```
Reactive inbox. Auto-primes from cache, then streams live updates.
### `messagesStream`
```dart theme={null}
Stream> ClarioDesk.messagesStream(String ticketId)
```
Reactive thread for one ticket.
## CSAT
### `submitCsat`
```dart theme={null}
Future ClarioDesk.submitCsat(
String ticketId, {
required int score, // 1 (😔) … 5 (😄)
String? comment,
})
```
See [CSAT](/concepts/csat).
## Attachments
### `attachmentUploadProgress`
```dart theme={null}
ValueListenable ClarioDesk.attachmentUploadProgress(String localId)
```
Per-upload progress, from `0.0` to `1.0`.
### `refreshAttachmentUrl`
```dart theme={null}
Future ClarioDesk.refreshAttachmentUrl(String attachmentId)
```
Re-presign an expired GET URL. See [Attachments](/concepts/attachments).
## Push
### `handlePushPayload`
```dart theme={null}
void ClarioDesk.handlePushPayload(Map payload)
```
Call from your push handler to deep-link to the relevant ticket. See
[Push notifications](/concepts/push-notifications).
## Connection
### `connectionStream`
```dart theme={null}
Stream ClarioDesk.connectionStream()
```
Observe realtime connection state: `connecting`, `connected`, `reconnecting`,
`disconnected`, `fatal`. See [Realtime](/concepts/realtime).
The React Native SDK mirrors this surface 1:1 with the same method names and
semantics, and the Swift SDK implements the same contract in Swift idioms. If
a capability behaves differently across the SDKs, it's a bug. See the
[RN API reference](/react-native/api-reference) and the
[Swift API reference](/swift/api-reference).
# Flutter installation
Source: https://docs.clariodesk.com/flutter/installation
Add the package, set platform minimums, and configure iOS permissions.
## Add the package
```bash theme={null}
flutter pub add clariodesk
```
Or in `pubspec.yaml`:
```yaml theme={null}
dependencies:
clariodesk: ^0.1.3
```
## Platform requirements
| Platform | Minimum | Hardware key |
| -------- | -------------------- | --------------------------------------------------- |
| iOS | 13.0 | Secure Enclave when present, else software Keychain |
| Android | API 23 (Marshmallow) | StrongBox when supported, else TEE Keystore |
### Android
Set `minSdkVersion 23` (or higher) in `android/app/build.gradle`:
```gradle theme={null}
android {
defaultConfig {
minSdkVersion 23
}
}
```
### iOS
Set the platform in `ios/Podfile`:
```ruby theme={null}
platform :ios, '13.0'
```
If you let users attach photos, videos, or files (see
[Attachments](/concepts/attachments)), add the usage strings to
`ios/Runner/Info.plist`:
```xml theme={null}
NSPhotoLibraryUsageDescription
Attach screenshots to your support messages.
NSCameraUsageDescription
Take a photo to attach to your support messages.
NSMicrophoneUsageDescription
Record video to attach to your support messages.
```
## Verify
```dart theme={null}
await ClarioDesk.init(apiKey: 'pk_live_…');
// init completing without throwing means the device registered (or reused an
// existing key). Next, wire identity. See the lifecycle guide.
```
Wire the four host events that touch the SDK.
# Flutter lifecycle
Source: https://docs.clariodesk.com/flutter/lifecycle
The four host events that touch the SDK: wire each one and you're done.
Four host events interact with the SDK. The concepts are the same across both
SDKs ([read the shared overview](/concepts/identity-lifecycle)); this page gives
the exact Flutter code.
## 1. App launch (every time, including already-logged-in users)
The most common miss: existing users who installed your app before you added
ClarioDesk never hit the login flow again. Hydrate from your own session on
launch and identify **unconditionally**. `identify()` is idempotent and cheap.
```dart theme={null}
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await ClarioDesk.init(apiKey: 'pk_live_…');
final user = await yourAuth.currentUser(); // Firebase, Supabase, your store…
if (user != null) {
await ClarioDesk.identify(
externalId: user.id,
email: user.email,
traits: {'plan': user.plan},
);
}
runApp(const MyApp());
}
```
## 2. Fresh login or signup
Right after your auth succeeds:
```dart theme={null}
await yourAuth.signIn(email, password);
await ClarioDesk.identify(
externalId: user.id,
email: user.email,
traits: {'plan': user.plan},
);
```
This overwrites the label on the existing device row. The same device and
hardware key are reused; nothing new is registered.
## 3. Logout
```dart theme={null}
await ClarioDesk.clearIdentity(); // Verified identity
await yourAuth.signOut();
```
This preserves the device key while changing the signed identity epoch. In
label-only mode, `reset()` is still available for full installation isolation.
## 4. User switch (A → B without app restart)
```dart theme={null}
await ClarioDesk.clearIdentity();
await yourAuth.switchTo(userB);
await ClarioDesk.identify(identityMode: IdentityMode.host);
```
Await clear before activating user B. `reset()` deprovisions this installation;
it is not logout or customer erase.
## What if I never call `identify()`?
Tickets still work; agents see a device id but no email, and the dashboard shows
an "Unverified device" badge. This is fine for anonymous feedback; otherwise,
wire case 1.
# Flutter prebuilt UI
Source: https://docs.clariodesk.com/flutter/prebuilt-ui
Present a themeable support inbox with one call. No UI to build.
If you don't need custom UI, the prebuilt screens get you a working support
experience in minutes. They render inside a self-owned modal with a private
nested navigator, so your router (GoRouter, Navigator 2.0) is never touched.
## Open a screen
The host pushes one route; the SDK owns everything inside it.
```dart theme={null}
ClarioDesk.openInbox(context);
```
From there, the SDK handles inbox → ticket detail → compose → image viewer
internally. Control returns to your app when the user dismisses the modal.
## The four screens
| Screen | What it does |
| ----------------- | ------------------------------------------ |
| **Support inbox** | The user's list of tickets. |
| **Ticket detail** | The message thread, with a reply composer. |
| **New ticket** | Subject + body form. |
| **Bug report** | Title + description + optional screenshot. |
## Theming
The prebuilt UI takes a single typed theme passed at `init`. Customization is
deliberately small: colors and the app bar only. Anything beyond this means
building your own UI with the [headless methods](/flutter/api-reference).
```dart theme={null}
await ClarioDesk.init(
apiKey: 'pk_live_…',
theme: ClarioDeskTheme(
primaryColor: const Color(0xFFD4FF00),
backgroundColor: const Color(0xFF0D0E0C),
surfaceColor: const Color(0xFF1A1A1A),
textColor: const Color(0xFFFAFAF7),
mutedTextColor: const Color(0xFF8B8B82),
appBar: ClarioDeskAppBarTheme(
backgroundColor: const Color(0xFF0D0E0C),
foregroundColor: const Color(0xFFFAFAF7),
title: 'Support',
),
),
);
```
| Property | Notes |
| ------------------------ | ------------------------------------- |
| `primaryColor` | Buttons, links, accents. |
| `backgroundColor` | Screen background. |
| `surfaceColor` | Cards, message bubbles, input fields. |
| `textColor` | Primary text. |
| `mutedTextColor` | Secondary text, timestamps. |
| `appBar.backgroundColor` | App bar background. |
| `appBar.foregroundColor` | App bar title + icons. |
| `appBar.title` | Override the default "Support" title. |
No font, layout, spacing, or radius customization in v1. That's "build your own
UI" territory. The point of the theme is to keep the prebuilt screens from
looking out of place, not to be a full design system.
# Flutter quickstart
Source: https://docs.clariodesk.com/flutter/quickstart
In-app support for Flutter apps: drop the SDK in, get a live ticket UI, with no backend code on your side.
The ClarioDesk Flutter SDK (`clariodesk` on pub.dev) gives you a hardware-bound
device identity and a live ticket thread with a few method calls.
## Install
```bash theme={null}
flutter pub add clariodesk
```
Requires Flutter ≥ 3.41 / Dart ≥ 3.11, iOS 13+, Android API 23+.
## Initialize
The first `init` generates the hardware key and registers the device with the
backend. Subsequent launches reuse the existing key with no network call.
```dart theme={null}
import 'package:clariodesk/clariodesk.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await ClarioDesk.init(apiKey: 'pk_live_…');
runApp(const MyApp());
}
```
## Identify the user
Call this after your own auth knows who the user is. It's optional, pure
metadata. See [lifecycle](/flutter/lifecycle) for the four events you should
wire (especially case 1, which covers users who were logged in before you
installed the SDK).
```dart theme={null}
await ClarioDesk.identify(
externalId: hostUser.id,
email: hostUser.email,
traits: {'plan': 'pro'},
);
```
## File a ticket
```dart theme={null}
final t = await ClarioDesk.createTicket(
subject: 'Upload broken',
body: 'Tapping upload does nothing.',
);
```
## Stream the inbox & a thread
Reactive reads: both auto-prime, then live-update.
```dart theme={null}
// Inbox
StreamBuilder>(
stream: ClarioDesk.ticketsStream(),
builder: (_, snap) {
final tickets = snap.data ?? const [];
return ListView(/* render tickets */);
},
);
// A single thread
StreamBuilder>(
stream: ClarioDesk.messagesStream(t.id),
builder: (_, snap) => /* render messages */,
);
```
## Send a reply
```dart theme={null}
await ClarioDesk.sendMessage(t.id, 'Still happening on 1.4.2');
```
## Run the example app
```bash theme={null}
cd example
flutter run --dart-define=CLARIODESK_API_KEY=pk_live_…
```
Wire login, launch, logout, and user-switch.
Skip building the UI. Open a themeable inbox in one call.
# ClarioDesk SDKs
Source: https://docs.clariodesk.com/index
Drop-in in-app support and bug reporting for Flutter, React Native, and native iOS (Swift), with no backend code on your side.
ClarioDesk gives your users a live support inbox inside your app. Drop the SDK
in, get a hardware-bound device identity and a real ticket thread that streams
agent replies in real time, without writing any backend, auth, or realtime code
yourself.
The **Flutter**, **React Native**, and **Swift** SDKs implement the same
contract: identical bytes on the wire, the same feature set and theme surface,
expressed in each platform's idioms. If a capability behaves differently
across the three platforms, treat it as a bug.
Add `clariodesk` to your `pubspec.yaml` and ship support in minutes.
`@clariodesk/react-native` for bare RN and Expo (dev build + Expo Go).
Native iOS via SPM: actor engine, `AsyncStream` reads, SwiftUI prebuilt UI,
APNs push with no Firebase.
## Two ways to integrate
Promise-based methods plus live subscriptions/streams. You render the UI in
your own design system. This is the primary integration mode.
A themeable drop-in chat you present with one call: inbox, ticket detail,
new ticket, and bug report screens.
## What you get out of the box
* **Device-is-identity auth:** a non-extractable hardware keypair (Secure
Enclave / Android Keystore) signs every request. No JWTs, refresh tokens, or
identity-verification backend to build. [How auth works →](/concepts/authentication)
* **Live updates:** agent replies, status changes, presence, and read receipts
stream over Centrifugo (falling back to SSE). [Realtime →](/concepts/realtime)
* **Attachments:** photos, videos, and files on any message, both directions.
[Attachments →](/concepts/attachments)
* **Push notifications:** deep-link straight to the right ticket on tap.
[Push →](/concepts/push-notifications)
* **CSAT:** an inline satisfaction rating when an agent resolves a ticket.
[CSAT →](/concepts/csat)
## Next steps
The shortest path from API key to a working ticket.
The model behind the SDKs. Read it once and the API explains itself.
# Integrating into your Flutter app
Source: https://docs.clariodesk.com/integrate-flutter
How the SDK behaves from first launch to logout, and how to wire it into an app that already has users.
ClarioDesk is built to drop into an app you have already shipped. This page walks
the whole integration, from the very first launch to logout, explains what the
SDK does at each step, and covers the case most teams get subtly wrong: an app
that already has signed-in users from before ClarioDesk existed.
If you just want the fastest path to a working ticket, start with the
[quickstart](/quickstart). This page is the "why and when" behind those calls.
## The lifecycle at a glance
```mermaid theme={null}
flowchart TD
A[App launches] --> B["ClarioDesk.init(apiKey)"]
B --> C{First launch ever?}
C -->|Yes| D["Generate hardware key,
register device"]
C -->|No| E["Reuse stored key
(no network)"]
D --> F{Is a user signed in?}
E --> F
F -->|Yes| G["identify(externalId, email)"]
F -->|No| H["Anonymous device"]
G --> I["Live use:
streams + optimistic sends"]
H --> I
I --> J{Logout or switch user?}
J -->|Yes| K["reset()"]
K --> A
J -->|No| I
```
Three calls drive everything above: `init` once at launch, `identify` when you
know who the user is, and `reset` when they leave. The SDK owns the rest: the
device key, the local cache, and the realtime connection.
## The mental model
The SDK is a thin client that owns four things: a device identity, a local
cache, a realtime connection, and (optionally) a set of prebuilt screens. Your
app never holds SDK state. You make a few calls in, and reads come back to you as
live `Stream`s. See [Architecture](/concepts/architecture) for the full picture.
## 1. The first launch
`ClarioDesk.init()` is the first thing to run, before any support UI.
```dart theme={null}
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await ClarioDesk.init(apiKey: 'pk_live_…');
runApp(const MyApp());
}
```
On the very first launch, `init` generates a non-extractable hardware keypair
(Secure Enclave, or the Android Keystore) and registers the device with the
backend. That keypair is the device's identity from then on, and it signs every
request. On every later launch, `init` finds the stored key and reuses it with no
network round-trip. See [Authentication](/concepts/authentication) for how the
keypair stands in for tokens.
## 2. Identify your user
Once your own auth knows who the user is, hand that down to ClarioDesk:
```dart theme={null}
await ClarioDesk.identify(
externalId: user.id,
email: user.email,
traits: {'plan': user.plan},
);
```
`identify` is pure metadata. It does not grant access (the device key already
did that), so it is safe to call often, and it is idempotent: the same values
twice is a no-op.
### When to call it
The right moment depends on the user's state when ClarioDesk first runs. Each row
below is a real case worth handling:
| Scenario | What to do |
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| New user signs in or signs up | Call `identify` right after your auth succeeds. |
| User was already signed in before you added (or updated to) the SDK | Call `identify` on launch from your persisted session. They never pass through login again, so a login-only hook misses them. |
| No user yet (anonymous) | Skip `identify`. Tickets still work; the device shows as unverified. |
| Anonymous user later signs in | Call `identify` at that point. The anonymous device and its existing tickets carry forward to the identified user. |
| Returning signed-in user (relaunch) | Call `identify` again on launch. It is idempotent, so this is a cheap no-op. |
| The user's email or traits changed | Call `identify` again with the new values to update the device row. |
One habit covers the whole table: identify on every launch whenever you have a
session, and again right after a fresh login.
### The existing-user trap
This is the case most integrations get wrong, so it is worth slowing down on.
If you only call `identify` from your login handler, you label the users who sign
in *after* you ship and nobody else. Everyone who was already logged in when you
released the update never visits your login screen again, so they stay unlabeled
devices, and your agents see "Unverified device" instead of an email.
The fix is to identify from your persisted session on every launch, not from the
login event:
```dart Identify on launch (correct) theme={null}
await ClarioDesk.init(apiKey: 'pk_live_…');
// Read the session your app already persisted, then identify on every launch.
final user = await yourAuth.currentUser();
if (user != null) {
await ClarioDesk.identify(externalId: user.id, email: user.email);
}
```
```dart Identify only on login (misses existing users) theme={null}
// Runs only for users who sign in AFTER this build ships.
onLoginSuccess((user) {
ClarioDesk.identify(externalId: user.id, email: user.email);
});
```
Wire `identify` into your launch path, not just your login path. An
"identify on login" hook silently skips every user who was already signed in
before you added the SDK.
Skipping `identify` entirely is fine for anonymous feedback. Tickets still work;
they just arrive without an email. When the user signs in later, call `identify`
then and the existing device, and its ticket history, carry forward.
## 3. Live by default
After `init` (and `identify`, if you have a user), reads are live. You do not
poll. Subscribe to a stream and the SDK primes it from cache, then pushes updates
as the agent replies.
```dart theme={null}
StreamBuilder>(
stream: ClarioDesk.ticketsStream(),
builder: (_, snap) => /* render the inbox */,
);
```
Writes are optimistic: a message appears immediately as pending and settles when
the backend confirms. A send made while offline waits in a durable outbox and
flushes on reconnect. Watch the realtime link with `ClarioDesk.connectionStream()`
to show an offline banner. See [Realtime](/concepts/realtime) for the connection
states.
## 4. Logout and user switching
When the user signs out, call `reset` before clearing your own session:
```dart theme={null}
await ClarioDesk.reset();
await yourAuth.signOut();
```
`reset` revokes the device server-side (best effort), wipes the hardware key, and
clears local caches. The next `init` registers a brand-new device. For a user
switch on the same install (A to B without a restart), do the same and then
identify B:
```dart theme={null}
await ClarioDesk.reset();
await ClarioDesk.init(apiKey: 'pk_live_…');
await ClarioDesk.identify(externalId: userB.id, email: userB.email);
```
Don't rebind one device to a second user. `reset` plus a fresh device is the
correct boundary, because each device row carries its own ticket history.
## 5. Relaunches and reinstalls
| After this | Device identity | Ticket history |
| ---------------------------- | ------------------------------------------------------------------- | ------------------------------ |
| A normal relaunch | Same device; key reused with no network | Visible to the user |
| `reset()` (logout or switch) | Wiped; next `init()` makes a fresh device | Detached from this device |
| An uninstall and reinstall | The stored key may be gone, so `init()` can register a fresh device | Starts clean on the new device |
The takeaway: identity is anchored to the hardware key on a single install. It is
not a cloud account that follows the user between devices, so always re-`identify`
on a fresh device to reattach their email and traits.
## Your integration checklist
For an app already in production:
* [ ] `init` runs at app start, before any support screen.
* [ ] `identify` runs on launch from your persisted session, not only on login.
* [ ] `reset` runs on logout and on user switch.
* [ ] Tickets render from `ticketsStream()` / `messagesStream()` (or the prebuilt UI).
* [ ] Optional: `connectionStream()` drives an offline indicator.
## Where to go next
The same four events as a tight code reference.
Every method, stream, and type.
Why the device key replaces tokens.
Live reads, optimistic sends, and the offline outbox.
# Integrating into your React Native app
Source: https://docs.clariodesk.com/integrate-react-native
How the SDK behaves from first launch to logout, and how to wire it into an app that already has users.
ClarioDesk is built to drop into an app you have already shipped, on bare React
Native or Expo. This page walks the whole integration, from the very first launch
to logout, explains what the SDK does at each step, and covers the case most
teams get subtly wrong: an app that already has signed-in users from before
ClarioDesk existed.
If you just want the fastest path to a working ticket, start with the
[quickstart](/quickstart). This page is the "why and when" behind those calls.
## The lifecycle at a glance
```mermaid theme={null}
flowchart TD
A[App launches] --> B["ClarioDesk.init({ apiKey })"]
B --> C{First launch ever?}
C -->|Yes| D["Generate device key,
register device"]
C -->|No| E["Reuse stored key
(no network)"]
D --> F{Is a user signed in?}
E --> F
F -->|Yes| G["identify({ externalId, email })"]
F -->|No| H["Anonymous device"]
G --> I["Live use:
subscriptions + optimistic sends"]
H --> I
I --> J{Logout or switch user?}
J -->|Yes| K["reset()"]
K --> A
J -->|No| I
```
Three calls drive everything above: `init` once at launch, `identify` when you
know who the user is, and `reset` when they leave. The SDK owns the rest: the
device key, the local cache, and the realtime connection (Centrifugo by default,
falling back to SSE).
## The mental model
The SDK is a thin client that owns four things: a device identity, a local
cache, a realtime connection, and (optionally) a set of prebuilt screens. Your
app never holds SDK state. You make a few calls in, and reads come back to you as
subscriptions, or the matching React hooks. See
[Architecture](/concepts/architecture) for the full picture.
## 1. The first launch
`ClarioDesk.init()` runs once at app start. Import the secure RNG polyfill at the
very top of `index.js`, before anything else, then init:
```ts theme={null}
// index.js, before any other import
import 'react-native-get-random-values';
import { ClarioDesk } from '@clariodesk/react-native';
await ClarioDesk.init({ apiKey: 'pk_live_…' });
```
On the very first launch, `init` generates a non-extractable hardware keypair
(Secure Enclave or the Android Keystore on a dev build; a software P-256 key in
Expo Go) and registers the device with the backend. That keypair is the device's
identity from then on, and it signs every request. On every later launch, `init`
finds the stored key and reuses it with no network round-trip. See
[Authentication](/concepts/authentication) for how the keypair stands in for
tokens, and [installation](/react-native/installation) for the secure-storage
peer dependency.
## 2. Identify your user
Once your own auth knows who the user is, hand that down to ClarioDesk:
```ts theme={null}
await ClarioDesk.identify({
externalId: user.id,
email: user.email,
traits: { plan: user.plan },
});
```
`identify` is pure metadata. It does not grant access (the device key already
did that), so it is safe to call often, and it is idempotent: the same values
twice is a no-op.
### When to call it
The right moment depends on the user's state when ClarioDesk first runs. Each row
below is a real case worth handling:
| Scenario | What to do |
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| New user signs in or signs up | Call `identify` right after your auth succeeds. |
| User was already signed in before you added (or updated to) the SDK | Call `identify` on launch from your persisted session. They never pass through login again, so a login-only hook misses them. |
| No user yet (anonymous) | Skip `identify`. Tickets still work; the device shows as unverified. |
| Anonymous user later signs in | Call `identify` at that point. The anonymous device and its existing tickets carry forward to the identified user. |
| Returning signed-in user (relaunch) | Call `identify` again on launch. It is idempotent, so this is a cheap no-op. |
| The user's email or traits changed | Call `identify` again with the new values to update the device row. |
One habit covers the whole table: identify on every launch whenever you have a
session, and again right after a fresh login.
### The existing-user trap
This is the case most integrations get wrong, so it is worth slowing down on.
If you only call `identify` from your login handler, you label the users who sign
in *after* you ship and nobody else. Everyone who was already logged in when you
released the update never visits your login screen again, so they stay unlabeled
devices, and your agents see "Unverified device" instead of an email.
The fix is to identify from your persisted session on every launch, not from the
login event:
```ts Identify on launch (correct) theme={null}
await ClarioDesk.init({ apiKey: 'pk_live_…' });
// Read the session your app already persisted, then identify on every launch.
const user = await yourAuth.currentUser();
if (user) {
await ClarioDesk.identify({ externalId: user.id, email: user.email });
}
```
```ts Identify only on login (misses existing users) theme={null}
// Runs only for users who sign in AFTER this build ships.
onLoginSuccess((user) => {
ClarioDesk.identify({ externalId: user.id, email: user.email });
});
```
Wire `identify` into your launch path, not just your login path. An
"identify on login" hook silently skips every user who was already signed in
before you added the SDK.
Skipping `identify` entirely is fine for anonymous feedback. Tickets still work;
they just arrive without an email. When the user signs in later, call `identify`
then and the existing device, and its ticket history, carry forward.
## 3. Live by default
After `init` (and `identify`, if you have a user), reads are live. You do not
poll. A hook primes from cache, then re-renders as the agent replies.
```tsx theme={null}
import { useTickets } from '@clariodesk/react-native/hooks';
const { tickets, loading } = useTickets(); // or ClarioDesk.subscribeTickets(cb)
```
Writes are optimistic: a message appears immediately as pending and settles when
the backend confirms (`failed: true` means render tap-to-retry). A send made
while offline waits in a durable outbox and flushes on reconnect. Watch the
realtime link with `useConnection()` to show an offline banner. See
[Realtime](/concepts/realtime) for the connection states.
## 4. Logout and user switching
When the user signs out, call `reset` before clearing your own session:
```ts theme={null}
await ClarioDesk.reset();
await yourAuth.signOut();
```
`reset` revokes the device server-side (best effort), wipes the device key, and
clears local caches. The next `init` registers a brand-new device. For a user
switch on the same install (A to B without a restart), do the same and then
identify B:
```ts theme={null}
await ClarioDesk.reset();
await ClarioDesk.init({ apiKey: 'pk_live_…' });
await ClarioDesk.identify({ externalId: userB.id, email: userB.email });
```
Don't rebind one device to a second user. `reset` plus a fresh device is the
correct boundary, because each device row carries its own ticket history.
## 5. Relaunches and reinstalls
| After this | Device identity | Ticket history |
| ---------------------------- | ------------------------------------------------------------------- | ------------------------------ |
| A normal relaunch | Same device; key reused with no network | Visible to the user |
| `reset()` (logout or switch) | Wiped; next `init()` makes a fresh device | Detached from this device |
| An uninstall and reinstall | The stored key may be gone, so `init()` can register a fresh device | Starts clean on the new device |
The takeaway: identity is anchored to the device key on a single install. It is
not a cloud account that follows the user between devices, so always re-`identify`
on a fresh device to reattach their email and traits.
## Your integration checklist
For an app already in production:
* [ ] `react-native-get-random-values` is imported first, at the top of `index.js`.
* [ ] `init` runs at app start, before any support screen.
* [ ] `identify` runs on launch from your persisted session, not only on login.
* [ ] `reset` runs on logout and on user switch.
* [ ] Tickets render from `useTickets()` / `useMessages()` (or the prebuilt UI).
* [ ] Optional: `useConnection()` drives an offline indicator.
## Where to go next
The same four events as a tight code reference.
Every method, subscription, hook, and type.
Why the device key replaces tokens.
Peer deps, the config plugin, dev build vs Expo Go.
# Integrating into your Swift app
Source: https://docs.clariodesk.com/integrate-swift
How the SDK behaves from first launch to logout, and how to wire it into an app that already has users.
ClarioDesk is built to drop into an app you have already shipped. This page walks
the whole integration, from the very first launch to logout, explains what the
SDK does at each step, and covers the case most teams get subtly wrong: an app
that already has signed-in users from before ClarioDesk existed.
If you just want the fastest path to a working ticket, start with the
[quickstart](/quickstart). This page is the "why and when" behind those calls.
## The lifecycle at a glance
```mermaid theme={null}
flowchart TD
A[App launches] --> B["ClarioDesk.initialize(.init(apiKey:))"]
B --> C{First launch ever?}
C -->|Yes| D["Generate Secure Enclave key,
register device"]
C -->|No| E["Reuse stored key
(no network)"]
D --> F{Is a user signed in?}
E --> F
F -->|Yes| G["identify(externalId:email:)"]
F -->|No| H["Anonymous device"]
G --> I["Live use:
AsyncStreams + optimistic sends"]
H --> I
I --> J{Logout or switch user?}
J -->|Yes| K["reset()"]
K --> A
J -->|No| I
```
Three calls drive everything above: `initialize` once at launch, `identify` when
you know who the user is, and `reset` when they leave. The SDK owns the rest:
the device key, the local cache, and the realtime connection (Centrifugo by
default, falling back to SSE).
## The mental model
The SDK is a thin client that owns four things: a device identity, a local
cache, a realtime connection, and (optionally) a set of prebuilt screens. Your
app never holds SDK state. You make a few calls in, and reads come back to you
as replay-last `AsyncStream`s (or closure subscriptions for UIKit). See
[Architecture](/concepts/architecture) for the full picture.
## 1. The first launch
`ClarioDesk.initialize()` runs once at app start — synchronous and
fire-and-forget, so it drops into `App.init` or your AppDelegate with no
`await`:
```swift theme={null}
import ClarioDesk
@main
struct MyApp: App {
init() {
ClarioDesk.initialize(.init(apiKey: "pk_live_…"))
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
```
On the very first launch, the SDK generates a non-extractable hardware keypair
in the Secure Enclave (software P-256 fallback where the SEP is unavailable,
e.g. the simulator) and registers the device with the backend. That keypair is
the device's identity from then on, and it signs every request. On every later
launch, the stored key is reused with no network round-trip. Every subsequent
SDK call awaits the shared bootstrap internally, so nothing races the
initialization. See [Authentication](/concepts/authentication) for how the
keypair stands in for tokens.
## 2. Identify your user
Once your own auth knows who the user is, hand that down to ClarioDesk:
```swift theme={null}
try await ClarioDesk.identify(
externalId: user.id,
email: user.email,
traits: ["plan": user.plan]
)
```
`identify` is pure metadata. It does not grant access (the device key already
did that), so it is safe to call often, and it is idempotent: the same values
twice is a no-op.
### When to call it
The right moment depends on the user's state when ClarioDesk first runs. Each row
below is a real case worth handling:
| Scenario | What to do |
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| New user signs in or signs up | Call `identify` right after your auth succeeds. |
| User was already signed in before you added (or updated to) the SDK | Call `identify` on launch from your persisted session. They never pass through login again, so a login-only hook misses them. |
| No user yet (anonymous) | Skip `identify`. Tickets still work; the device shows as unverified. |
| Anonymous user later signs in | Call `identify` at that point. The anonymous device and its existing tickets carry forward to the identified user. |
| Returning signed-in user (relaunch) | Call `identify` again on launch. It is idempotent, so this is a cheap no-op. |
| The user's email or traits changed | Call `identify` again with the new values to update the device row. |
One habit covers the whole table: identify on every launch whenever you have a
session, and again right after a fresh login.
### The existing-user trap
This is the case most integrations get wrong, so it is worth slowing down on.
If you only call `identify` from your login handler, you label the users who sign
in *after* you ship and nobody else. Everyone who was already logged in when you
released the update never visits your login screen again, so they stay unlabeled
devices, and your agents see "Unverified device" instead of an email.
The fix is to identify from your persisted session on every launch, not from the
login event:
```swift Identify on launch (correct) theme={null}
ClarioDesk.initialize(.init(apiKey: "pk_live_…"))
// Read the session your app already persisted, then identify on every launch.
if let user = await yourAuth.currentUser() {
try await ClarioDesk.identify(externalId: user.id, email: user.email)
}
```
```swift Identify only on login (misses existing users) theme={null}
// Runs only for users who sign in AFTER this build ships.
onLoginSuccess { user in
Task { try await ClarioDesk.identify(externalId: user.id, email: user.email) }
}
```
Wire `identify` into your launch path, not just your login path. An
"identify on login" hook silently skips every user who was already signed in
before you added the SDK.
Skipping `identify` entirely is fine for anonymous feedback. Tickets still work;
they just arrive without an email. When the user signs in later, call `identify`
then and the existing device, and its ticket history, carry forward.
## 3. Live by default
After `initialize` (and `identify`, if you have a user), reads are live. You do
not poll. Iterate a stream and the SDK primes it from cache, then pushes updates
as the agent replies.
```swift theme={null}
.task {
do {
for try await tickets in ClarioDesk.tickets { self.tickets = tickets }
} catch { /* render a retry state */ }
}
```
Writes are optimistic: a message appears immediately as pending and settles when
the backend confirms (`failed == true` means render tap-to-retry via
`retryMessage`). A send made while offline waits in a durable Keychain outbox
and flushes on reconnect. Watch the realtime link with `ClarioDesk.connection`
to show an offline banner. See [Realtime](/concepts/realtime) for the
connection states.
## 4. Logout and user switching
When the user signs out, call `reset` before clearing your own session:
```swift theme={null}
await ClarioDesk.reset()
await yourAuth.signOut()
```
`reset` revokes the device server-side (best effort), wipes the hardware key,
and clears local caches. The next `initialize` registers a brand-new device. For
a user switch on the same install (A to B without a restart), do the same and
then identify B:
```swift theme={null}
await ClarioDesk.reset()
ClarioDesk.initialize(.init(apiKey: "pk_live_…"))
try await ClarioDesk.identify(externalId: userB.id, email: userB.email)
```
Don't rebind one device to a second user. `reset` plus a fresh device is the
correct boundary, because each device row carries its own ticket history.
## 5. Relaunches and reinstalls
| After this | Device identity | Ticket history |
| ---------------------------- | ------------------------------------------------------------------------- | ------------------------------ |
| A normal relaunch | Same device; key reused with no network | Visible to the user |
| `reset()` (logout or switch) | Wiped; the next `initialize()` makes a fresh device | Detached from this device |
| An uninstall and reinstall | The stored key may be gone, so `initialize()` can register a fresh device | Starts clean on the new device |
The takeaway: identity is anchored to the hardware key on a single install. It is
not a cloud account that follows the user between devices, so always re-`identify`
on a fresh device to reattach their email and traits.
## Your integration checklist
For an app already in production:
* [ ] `initialize` runs at app start, before any support screen.
* [ ] `identify` runs on launch from your persisted session, not only on login.
* [ ] `reset` runs on logout and on user switch.
* [ ] Tickets render from `ClarioDesk.tickets` / `ClarioDesk.messages(ticketId:)` (or the prebuilt UI).
* [ ] Optional: `ClarioDesk.connection` drives an offline indicator.
## Where to go next
The same four events as a tight code reference.
Every method, stream, and type.
Why the device key replaces tokens.
APNs-direct push: one line in your AppDelegate.
# Quickstart
Source: https://docs.clariodesk.com/quickstart
From API key to a working support ticket in about five minutes.
This is the shortest path on any platform: install, initialize, identify,
and open a ticket. Pick your framework below. The steps map 1:1.
In the [ClarioDesk dashboard](https://dashboard.clariodesk.com), create an app and
copy its publishable key, which starts with `pk_live_`. This key only
authorizes a fresh install to register a device; it can't read tickets or
impersonate users, so it's safe to ship in your app binary.
```bash Flutter theme={null}
flutter pub add clariodesk
```
```bash React Native theme={null}
npm install @clariodesk/react-native react-native-get-random-values
# then pick secure storage for your stack:
npx expo install expo-secure-store # Expo
npm install react-native-keychain # bare RN
```
```swift Swift theme={null}
// Xcode: File → Add Package Dependencies…, or in Package.swift:
.package(url: "https://github.com/clariodesk/clariodesk-swift", from: "0.1.1")
// Products: ClarioDesk (headless) and/or ClarioDeskUI (prebuilt screens)
```
The first call generates the hardware device key and registers it. Later
launches reuse it with no network round-trip.
```dart Flutter theme={null}
import 'package:clariodesk/clariodesk.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await ClarioDesk.init(apiKey: 'pk_live_…');
runApp(const MyApp());
}
```
```ts React Native theme={null}
// index.js: the very first import, before anything else
import 'react-native-get-random-values';
import { ClarioDesk } from '@clariodesk/react-native';
await ClarioDesk.init({ apiKey: 'pk_live_…' });
```
```swift Swift theme={null}
import ClarioDesk
// App.init or AppDelegate — fire-and-forget, no await:
ClarioDesk.initialize(.init(apiKey: "pk_live_…"))
```
Call `identify()` once your own auth knows who the user is. It's metadata
only: the device key is the real identity, so this grants no access. It's
idempotent, so call it on every launch for logged-in users.
```dart Flutter theme={null}
await ClarioDesk.identify(
externalId: user.id,
email: user.email,
traits: {'plan': 'pro'},
);
```
```ts React Native theme={null}
await ClarioDesk.identify({ externalId: user.id, email: user.email });
```
```swift Swift theme={null}
try await ClarioDesk.identify(
externalId: user.id,
email: user.email,
traits: ["plan": "pro"]
)
```
**Already have signed-in users?** When you add ClarioDesk to an app that is
already in use, identify those users on launch from your saved session, not
only on your login screen. Existing users rarely pass through login again,
so a login-only call leaves them unidentified. Full walkthrough:
[Flutter](/integrate-flutter), [React Native](/integrate-react-native), or
[Swift](/integrate-swift).
```dart Flutter theme={null}
final t = await ClarioDesk.createTicket(
subject: 'Upload broken',
body: 'Tapping upload does nothing.',
);
// Reactive thread: primes, then live updates.
StreamBuilder>(
stream: ClarioDesk.messagesStream(t.id),
builder: (_, snap) => /* render messages */,
);
```
```ts React Native theme={null}
const { ticket } = await ClarioDesk.createTicket({
subject: '',
body: 'Tapping upload does nothing.',
});
// Reactive thread: subscribe returns an unsubscribe fn.
const off = ClarioDesk.subscribeMessages(ticket.id, (messages) =>
render(messages),
);
```
```swift Swift theme={null}
let (ticket, _) = try await ClarioDesk.createTicket(
subject: "Upload broken",
body: "Tapping upload does nothing."
)
// Reactive thread: replay-last AsyncStream, primes then live-updates.
for try await messages in ClarioDesk.messages(ticketId: ticket.id) {
render(messages)
}
```
That's a complete loop: a real ticket lands in your dashboard inbox, and the
agent's reply streams back to the device live.
Use the prebuilt chat instead: one call opens a themeable inbox. See the
prebuilt UI guide for [Flutter](/flutter/prebuilt-ui),
[React Native](/react-native/prebuilt-ui), or [Swift](/swift/prebuilt-ui).
# React Native API reference
Source: https://docs.clariodesk.com/react-native/api-reference
The headless public surface of the React Native SDK.
All methods live on the `ClarioDesk` singleton. `subscribe*` methods return an
unsubscribe function; the React `hooks` mirror each subscription.
```ts theme={null}
import { ClarioDesk } from '@clariodesk/react-native';
```
## Lifecycle
```ts theme={null}
ClarioDesk.init({ apiKey, identityTokenProvider?, pushTokenProvider?, attachments? }): Promise
ClarioDesk.identify({ externalId?, name?, email?, traits?, identityToken?, identityMode? }): Promise
ClarioDesk.deviceThumbprint(): Promise
ClarioDesk.clearIdentity(): Promise
ClarioDesk.reset(): Promise
ClarioDesk.isInitialized(): boolean
ClarioDesk.isIdentified(): boolean
```
Without a token, `identify` writes unverified display labels. With a token or
provider it performs [Verified identity](/concepts/verified-identity). Use
`clearIdentity()` before verified logout/switch; `reset()` deprovisions only
this installation.
## Tickets & messages
```ts theme={null}
ClarioDesk.createTicket({ subject, body, attachments? }): Promise<{ ticket: Ticket }>
ClarioDesk.sendMessage(ticketId, body, { attachments? }?): Promise
ClarioDesk.retryMessage(message): Promise // for failed optimistic sends
ClarioDesk.markRead(ticketId): Promise
ClarioDesk.setTyping(ticketId, isTyping): void
ClarioDesk.fetchTickets(): Promise // one-shot
ClarioDesk.fetchTicket(ticketId): Promise // one-shot
```
A message with `pending: true` is optimistic; `failed: true` means rejected, so
render tap-to-retry with `retryMessage`. Offline sends stay pending and
auto-flush on reconnect.
## Live subscriptions
Each returns an unsubscribe function.
```ts theme={null}
ClarioDesk.subscribeTickets(cb): () => void
ClarioDesk.subscribeMessages(ticketId, cb): () => void
ClarioDesk.subscribeTyping(ticketId, cb): () => void
ClarioDesk.subscribeAgentPresence(cb): () => void
ClarioDesk.subscribeConnection(cb): () => void // connecting | connected | reconnecting | disconnected | fatal
ClarioDesk.subscribeSync(cb): () => void
ClarioDesk.subscribeState(cb): () => void
ClarioDesk.subscribePushOpened(cb): () => void
```
### Hooks
The same data, idiomatic for React (separate entry, no React dep in core):
```ts theme={null}
import {
useTickets, useMessages, useTyping, useAgentPresence,
useConnection, useSync, usePushOpened,
} from '@clariodesk/react-native/hooks';
```
## CSAT
```ts theme={null}
ClarioDesk.submitCsat(ticketId, { score, comment? }): Promise
// score: 1 (😔) … 5 (😄)
```
## Push
```ts theme={null}
ClarioDesk.registerPushToken(token): Promise
ClarioDesk.isClarioMessage(data): boolean
ClarioDesk.parsePushPayload(data): ParsedPush | null
ClarioDesk.openTicketFromPush(data): void
ClarioDesk.getNotificationPreferences(): Promise
ClarioDesk.setNotificationPreferences(prefs): Promise
```
See the [push guide](/react-native/push).
## Attachments
```ts theme={null}
ClarioDesk.attachmentUploadProgress(stubId): Subscribable
ClarioDesk.refreshAttachmentUrl(attachmentId): Promise
```
## Types
All types are exported: `Ticket`, `Message`, `Attachment`, `OutgoingAttachment`,
`readStateOf`, `ClarioDeskError`, and the enums above.
This surface mirrors the [Flutter SDK](/flutter/api-reference) 1:1. Same method
names, same semantics. The [Swift SDK](/swift/api-reference) implements the
same contract in Swift idioms. If a capability behaves differently across the
SDKs, it's a bug.
# React Native installation
Source: https://docs.clariodesk.com/react-native/installation
Bare RN and Expo (dev build + Expo Go), the config plugin, and the peer-dependency matrix.
## Compatibility
The SDK supports the full surface below: every target can open a chat, and none
of them crash.
| Capability | Bare RN (old + New Arch) | Expo dev build / EAS | Expo Go |
| --------------------------- | --------------------------- | --------------------- | -------------------------- |
| Device key | ✅ Secure Enclave / Keystore | ✅ (via config plugin) | ⚠️ software P-256 fallback |
| Secure storage | ✅ `react-native-keychain` | ✅ `expo-secure-store` | ✅ `expo-secure-store` |
| Realtime (Centrifugo + SSE) | ✅ | ✅ | ✅ |
| Push | ✅ (host Firebase) | ✅ | ⛔ remote push N/A in Go |
| Attachments | ✅ | ✅ | ✅ (Expo pickers) |
| Prebuilt UI | ✅ | ✅ | ✅ |
The hardware key needs a dev build (Expo Go can't load native modules); the SDK
transparently falls back to a software ECDSA P-256 key, so you can evaluate the
whole flow in Expo Go and ship hardware-backed with no code change.
## Core install
```sh theme={null}
npm install @clariodesk/react-native
npm install react-native-get-random-values # secure RNG (required)
```
Import `react-native-get-random-values` once, at the very top of `index.js`,
before any other import. The crypto layer depends on it being registered first.
## Peer dependencies
Install only what your stack and features need:
```sh theme={null}
# secure storage (required, pick one)
npx expo install expo-secure-store # Expo
npm install react-native-keychain # bare RN
# attachments (optional, only if you attach files)
npx expo install expo-image-picker expo-document-picker expo-image-manipulator # Expo
npm install react-native-image-picker @react-native-documents/picker # bare RN
# connectivity-reactive reconnect (optional)
npm install @react-native-community/netinfo
```
## Expo
Add the config plugin, then make a dev build (or run in Expo Go for the software
tier):
```jsonc theme={null}
{ "expo": { "plugins": ["@clariodesk/react-native"] } }
// parameterize: ["@clariodesk/react-native", { "enableCamera": false, "enablePush": true }]
```
```sh theme={null}
npx expo prebuild && npx expo run:ios
```
The config plugin wires the native module and adds the iOS photo/camera/mic
usage strings needed for attachments.
## Bare React Native
Autolinking handles the native module. Run the Expo modules installer once
(the device-key module is authored with the Expo Modules API), then install pods:
```sh theme={null}
npx install-expo-modules@latest
cd ios && pod install
```
## Push (separate package)
Push lives in `@clariodesk/react-native-push` so Firebase never autolinks into a
headless host. See the [push guide](/react-native/push).
Wire login, launch, logout, and user-switch.
# React Native lifecycle
Source: https://docs.clariodesk.com/react-native/lifecycle
The four host events that touch the SDK, in React Native.
Four host events interact with the SDK. The concepts are shared across both
SDKs ([read the overview](/concepts/identity-lifecycle)); this page gives the
exact React Native code.
## 1. App launch (every time)
Identify **unconditionally** on launch so users who installed before you added
ClarioDesk aren't left as unlabeled devices. `identify()` is idempotent and
cheap.
```ts theme={null}
import 'react-native-get-random-values';
import { ClarioDesk } from '@clariodesk/react-native';
await ClarioDesk.init({ apiKey: 'pk_live_…' });
const user = await yourAuth.currentUser();
if (user) {
await ClarioDesk.identify({
externalId: user.id,
email: user.email,
traits: { plan: user.plan },
});
}
```
## 2. Fresh login or signup
```ts theme={null}
await yourAuth.signIn(email, password);
await ClarioDesk.identify({ externalId: user.id, email: user.email });
```
This overwrites the label on the existing device row. The same device and key
are reused.
## 3. Logout
```ts theme={null}
await ClarioDesk.clearIdentity(); // Verified identity
await yourAuth.signOut();
```
This preserves the device key while changing the signed identity epoch. In
label-only mode, `reset()` is still available for full installation isolation.
## 4. User switch (A → B without restart)
```ts theme={null}
await ClarioDesk.clearIdentity();
await yourAuth.switchTo(userB);
await ClarioDesk.identify({ identityMode: 'host' });
```
Await clear before activating user B. `reset()` deprovisions this installation;
it is not logout or customer erase.
## Checking state
```ts theme={null}
ClarioDesk.isInitialized(); // boolean
ClarioDesk.isIdentified(); // boolean
```
# React Native prebuilt UI
Source: https://docs.clariodesk.com/react-native/prebuilt-ui
A themeable drop-in chat: present it with one call, zero mandatory native deps.
`@clariodesk/react-native/ui` is a pure-JS prebuilt chat. It presents its own
screens in a self-owned modal (no react-navigation), derives light/dark from
your theme, and ships zero native dependencies beyond the core.
## Wrap your app
```tsx theme={null}
import { ClarioDeskProvider } from '@clariodesk/react-native/ui';
;
```
## Open from anywhere
```tsx theme={null}
import { openInbox, openTicket, openNewConversation, openBugReport } from '@clariodesk/react-native/ui';
openInbox(); // the user's tickets
openTicket(id); // a single thread
openNewConversation(); // new ticket form
openBugReport(); // bug report form
```
## Theming
The provider takes a small theme and derives light/dark automatically. It also
exposes \~50 overridable `strings` for localization and copy tweaks.
```tsx theme={null}
```
| Theme field | Notes |
| -------------- | -------------------------------------------- |
| `primary` | Accent for buttons, links, the active state. |
| `cornerRadius` | Bubble / card radius. |
| `mode` | `'light'`, `'dark'`, or `'system'`. |
## Enabling attachments
The attach button is off until you give the provider a `picker`:
```tsx theme={null}
import { ClarioDeskProvider } from '@clariodesk/react-native/ui';
import { expoPicker } from '@clariodesk/react-native/expo'; // or your own MediaPicker
```
See [Attachments](/concepts/attachments) for limits and the upload flow.
# React Native push
Source: https://docs.clariodesk.com/react-native/push
Firebase-backed push via a separate package, so Firebase never autolinks into headless hosts.
Push is a separate package (`@clariodesk/react-native-push`), so Firebase's
native code never autolinks into a host that doesn't want it. Your app owns the
messaging plugin; the SDK receives tokens through a `PushTokenProvider` seam.
## Install
```sh theme={null}
npm install @clariodesk/react-native-push \
@react-native-firebase/app @react-native-firebase/messaging
```
## Wire the token provider
```ts theme={null}
import { ClarioDesk } from '@clariodesk/react-native';
import { FirebaseTokenProvider } from '@clariodesk/react-native-push';
await ClarioDesk.init({
apiKey: 'pk_live_…',
pushTokenProvider: new FirebaseTokenProvider(),
});
```
## Route messages in your background handler
In your shared background/foreground handler, hand ClarioDesk messages back to
the SDK so they don't collide with your app's own notifications:
```ts theme={null}
import messaging from '@react-native-firebase/messaging';
import { ClarioDesk } from '@clariodesk/react-native';
messaging().setBackgroundMessageHandler(async (msg) => {
if (ClarioDesk.isClarioMessage(msg.data ?? {})) return; // ours, the SDK handles it
/* your app's own handling */
});
```
Registering the background handler in `index.js` at startup is the #1 push
setup-failure cause. It must run at app entry, not inside a component, or
background messages are silently dropped.
## Deep-link on tap
On a notification tap, navigate straight to the ticket:
```ts theme={null}
// Imperative
ClarioDesk.openTicketFromPush(data);
// …or via the hook
import { usePushOpened } from '@clariodesk/react-native/hooks';
usePushOpened((ticketId) => navigateToSupport(ticketId));
```
## Preferences
```ts theme={null}
const prefs = await ClarioDesk.getNotificationPreferences();
await ClarioDesk.setNotificationPreferences({ ...prefs, agentReplies: true });
```
Remote push is not available in Expo Go. Use a dev build / EAS for push. The
backend is presence-aware and suppresses a push when the user is already
connected live. See the [push concept page](/concepts/push-notifications).
# React Native quickstart
Source: https://docs.clariodesk.com/react-native/quickstart
Embed support in your RN app with a few method calls, on bare RN or Expo.
`@clariodesk/react-native` mirrors the [Flutter SDK](/flutter/quickstart) 1:1:
device-is-identity auth, live updates over Centrifugo/SSE, presence, read
receipts, typing, push, attachments, and CSAT. Two integration modes, one
install: headless methods, or the prebuilt `@clariodesk/react-native/ui` chat.
## Install
```sh theme={null}
npm install @clariodesk/react-native
# secure RNG (required); import once at app entry (see below)
npm install react-native-get-random-values
```
Then pick the secure-storage peer for your stack:
```sh theme={null}
npx expo install expo-secure-store # Expo
npm install react-native-keychain # bare RN
```
See [installation](/react-native/installation) for the Expo config plugin,
attachment pickers, and the full peer-dependency matrix.
## Headless quick start
```ts theme={null}
// At the very top of index.js, before anything else:
import 'react-native-get-random-values';
import { ClarioDesk } from '@clariodesk/react-native';
// 1. Once at app start. First launch generates the device key + registers;
// later launches reuse it. Realtime defaults to Centrifugo, falls back to SSE.
await ClarioDesk.init({ apiKey: 'pk_live_…' });
// 2. After your own auth knows the user (metadata only; the device key is the
// real identity, so this grants no access).
await ClarioDesk.identify({ externalId: user.id, email: user.email });
// 3. Writes are imperative + optimistic.
const { ticket } = await ClarioDesk.createTicket({ subject: '', body: 'Upload is broken' });
await ClarioDesk.sendMessage(ticket.id, 'Still happening on 1.4.2');
// 4. Reads are live. subscribe* returns an unsubscribe function.
const off = ClarioDesk.subscribeMessages(ticket.id, (messages) => render(messages));
// 5. On sign-out / user switch.
await ClarioDesk.reset();
```
## React hooks
Idiomatic state binding (not UI) lives in a separate entry so the core has no
React dependency:
```tsx theme={null}
import {
useTickets, useMessages, useTyping, useAgentPresence,
useConnection, useSync, usePushOpened,
} from '@clariodesk/react-native/hooks';
const { tickets, loading } = useTickets();
const { messages } = useMessages(ticketId);
const supportTyping = useTyping(ticketId);
const agentOnline = useAgentPresence();
const conn = useConnection(); // 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'fatal'
```
A message with `pending: true` is optimistic ("sending…"); `failed: true` means
the send was rejected, so render tap-to-retry with `ClarioDesk.retryMessage(m)`.
An offline send stays `pending` and auto-flushes on reconnect (durable outbox).
Config plugin, dev build vs Expo Go, attachment pickers.
Drop-in themeable chat: `@clariodesk/react-native/ui`.
# Swift API reference
Source: https://docs.clariodesk.com/swift/api-reference
The headless public surface of the Swift SDK.
All methods are static on `ClarioDesk`. Reads are replay-last `AsyncStream`s
(a new iterator immediately receives the current value, then live updates),
with closure-based `subscribe*` variants for UIKit hosts that return a
cancellable `ClarioSubscription`.
```swift theme={null}
import ClarioDesk
```
## Lifecycle
### `initialize`
```swift theme={null}
ClarioDesk.initialize(_ options: ClarioDeskOptions)
```
Synchronous, fire-and-forget, idempotent — call from `App.init` or
`application(_:didFinishLaunchingWithOptions:)` with no `await`. Every
subsequent method awaits the shared bootstrap internally, so nothing races.
First launch generates the Secure Enclave device key and registers the device;
later launches reuse it.
`ClarioDeskOptions` — only `apiKey` is required:
```swift theme={null}
ClarioDeskOptions(
apiKey: String,
baseURL: URL? = nil, // default https://api.clariodesk.com
appVersion: String? = nil, // default: CFBundleShortVersionString
experimentalCentrifugo: Bool = false, // deprecated/ignored; grant-SSE only
pushTokenProvider: (any PushTokenProvider)? = nil,
identityTokenProvider: (any IdentityTokenProvider)? = nil,
onDiagnostic: ClarioDiagnosticHandler? = nil, // non-fatal event receipts
logCollector: (@Sendable () async -> String?)? = nil, // bug-report logs
screenNameProvider: (@Sendable () -> String?)? = nil // client_context.screen_name
)
```
### `identify`
```swift theme={null}
try await ClarioDesk.identify(
externalId: String? = nil,
name: String? = nil,
email: String? = nil,
traits: [String: any Sendable]? = nil,
identityToken: String? = nil,
identityMode: IdentityMode? = nil
)
```
Without a token, attach unverified display metadata. With a token/provider,
perform [Verified identity](/concepts/verified-identity). Proof failures never
fall back to labels.
```swift theme={null}
let thumbprint = try await ClarioDesk.deviceThumbprint()
try await ClarioDesk.clearIdentity()
```
Use `clearIdentity()` before Verified-identity logout/account switch.
### `reset`
```swift theme={null}
try await ClarioDesk.reset()
```
Acknowledged one-installation revoke, then wipe the hardware key, caches, and
outbox. It is not logout or customer erase. The next `initialize` registers a
fresh device.
### State & diagnostics
```swift theme={null}
ClarioDesk.isInitialized // Bool
ClarioDesk.isIdentified // Bool
ClarioDesk.isAvailable // Bool — false after a terminal server state
// (device revoked / key expired / app suspended)
await ClarioDesk.diagnostics() // ClarioDeskDiagnostics snapshot; never throws
```
## Tickets & messages
### `createTicket`
```swift theme={null}
let (ticket, message) = try await ClarioDesk.createTicket(
subject: String,
body: String,
type: TicketType = .ticket, // .ticket | .bug
attachments: [OutgoingAttachment] = []
)
```
### `sendMessage`
```swift theme={null}
let message = try await ClarioDesk.sendMessage(
ticketId: String,
body: String,
attachments: [OutgoingAttachment] = []
)
```
Optimistic: the message appears as pending immediately and settles on success.
An offline send waits in the durable Keychain outbox and auto-flushes on
reconnect.
### `retryMessage`
```swift theme={null}
try await ClarioDesk.retryMessage(message)
```
Re-send a message whose optimistic write failed (`message.failed == true`).
Tap-to-retry only; never auto-retried.
### One-shot fetches
```swift theme={null}
try await ClarioDesk.fetchTickets() // [Ticket]
try await ClarioDesk.fetchTicket(id:) // (ticket: Ticket, messages: [Message])
```
### Read & typing
```swift theme={null}
await ClarioDesk.markRead(ticketId:) // best-effort, never throws
await ClarioDesk.setTyping(ticketId:, true) // best-effort; throttle to ~1/3s
```
## Bug reports
### `submitBugReport`
```swift theme={null}
let (ticket, message) = try await ClarioDesk.submitBugReport(
description: String,
screenshot: OutgoingAttachment? = nil,
attachments: [OutgoingAttachment] = [] // cap: 4 total per message
)
```
A thin wrapper over ticket-create with `type: .bug`: the subject is derived
from the description (first line, ≤60 chars — the user never types a title),
the `logCollector` output (if wired in options) is attached as
`clariodesk-logs.txt`, and the optional screenshot rides along. One-shot — no
outbox; surface failure with tap-to-retry.
The prebuilt `ClarioDeskWidgets.openBugReport()` auto-captures a screenshot
and calls this for you (see [prebuilt UI](/swift/prebuilt-ui), including
opt-in shake-to-report); headless hosts call it directly.
## CSAT
### `submitCsat`
```swift theme={null}
try await ClarioDesk.submitCsat(
ticketId: String,
score: Int, // 1 (😔) … 5 (😄)
comment: String? = nil
)
```
See [CSAT](/concepts/csat). The rating is cached on the `Ticket`
(`csatScore` / `csatComment` / `hasCsat`).
## Attachments
```swift theme={null}
// A file the host picked; the SDK signs, uploads, and binds it.
OutgoingAttachment(
path: String, mime: String, kind: AttachmentKind, // .image | .video | .file
filename: String? = nil, width: Int? = nil, height: Int? = nil
)
// Live upload progress (0…1) for an optimistic stub, by its localId:
ClarioDesk.attachmentUploadProgress(localId:) // AsyncStream?
// Re-presign an expired GET URL:
try await ClarioDesk.refreshAttachmentUrl(id:) // Attachment
```
See [Attachments](/concepts/attachments) for limits and the upload flow.
## Push
```swift theme={null}
ClarioDesk.setAPNSDeviceToken(token) // AppDelegate token forward
await ClarioDesk.registerPushToken(token) // direct registration (FCM bridge)
ClarioDesk.isClarioMessage(userInfo) // Bool
ClarioDesk.parsePushPayload(userInfo) // ClarioPushPayload?
ClarioDesk.openTicketFromPush(userInfo) // parses + emits on pushOpened
try await ClarioDesk.getNotificationPreferences()
try await ClarioDesk.setNotificationPreferences(
pushEnabled: Bool? = nil,
quietHoursStart: String? = nil, // "HH:MM"
quietHoursEnd: String? = nil,
clearQuietHours: Bool = false,
timezone: String? = nil
)
```
See the [push guide](/swift/push).
## Reactive reads
Streams that can carry a load failure are throwing; the rest are plain
`AsyncStream`s. Subscribing before `initialize` throws `.notInitialized` on
first await.
```swift theme={null}
ClarioDesk.tickets // AsyncThrowingStream<[Ticket], Error>
ClarioDesk.messages(ticketId:) // AsyncThrowingStream<[Message], Error>
ClarioDesk.typing(ticketId:) // AsyncStream
ClarioDesk.agentPresence // AsyncStream
ClarioDesk.connection // AsyncStream
// connecting | connected | reconnecting
// | disconnected | fatal
ClarioDesk.sync(ticketId:) // AsyncStream
// loading | live | recovering
// | catchingUp | stale | error
ClarioDesk.availability // AsyncStream — isAvailable, live
ClarioDesk.pushOpened // AsyncStream — not replayed
```
### Closure subscriptions (UIKit)
Same semantics; each returns a `ClarioSubscription` — call `.cancel()` when
done.
```swift theme={null}
ClarioDesk.subscribeTickets { result in … }
ClarioDesk.subscribeMessages(ticketId:) { result in … }
ClarioDesk.subscribeTyping(ticketId:) { isTyping in … }
ClarioDesk.subscribeAgentPresence { online in … }
ClarioDesk.subscribeConnection { state in … }
ClarioDesk.subscribeSync(ticketId:) { state in … }
ClarioDesk.subscribeAvailability { available in … }
ClarioDesk.subscribePushOpened { ticketId in … }
```
## Types
All models are `Sendable` value types: `Ticket`, `Message`, `Attachment`,
`OutgoingAttachment`, `NotificationPreferences`, `ClarioPushPayload`,
`ClarioDeskError`, and the enums above (`TicketType`, `TicketStatus`,
`MessageAuthor`, `AttachmentKind`, `MessageReadState`).
This SDK implements the same wire contract as the
[Flutter](/flutter/api-reference) and
[React Native](/react-native/api-reference) SDKs — identical bytes on the
wire and the same state machines, expressed in Swift idioms (`initialize`
instead of `init`, tuples for create results, `AsyncStream` reads).
# Swift installation
Source: https://docs.clariodesk.com/swift/installation
Add the SPM package, pick your products, and check the host prerequisites.
## Add the package
In Xcode: **File → Add Package Dependencies…** and enter
`https://github.com/clariodesk/clariodesk-swift`.
Or in `Package.swift`:
```swift theme={null}
dependencies: [
.package(url: "https://github.com/clariodesk/clariodesk-swift", from: "0.1.1"),
],
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "ClarioDesk", package: "clariodesk-swift"),
// Only if you use the prebuilt screens:
.product(name: "ClarioDeskUI", package: "clariodesk-swift"),
]
),
]
```
## The two products
| Product | What it is |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `ClarioDesk` | The headless core: the `ClarioDesk` facade, `AsyncStream` reads, device key, realtime, outbox. One third-party dependency (SwiftCentrifuge). |
| `ClarioDeskUI` | The prebuilt SwiftUI screens (`ClarioDeskWidgets`), theme, and strings. Depends on the core. |
A headless host links only `ClarioDesk` and Swift dead-strips the UI — you
never ship screens you don't present.
## Platform requirements
| Requirement | Value |
| ------------ | ------------------------------------------------------------------------------------------- |
| iOS | 16.0+ |
| Swift | Package is Swift 6; consumable from Swift 5.x hosts |
| Hardware key | Secure Enclave (P-256); software fallback where the SEP is unavailable (e.g. the simulator) |
## Host prerequisites
### Keychain
The SDK stores the device id, device-key blob, and offline outbox in the
Keychain. Any code-signed app has this by default — which is every real app.
An **unsigned** build can't write the Keychain (`errSecMissingEntitlement`,
OSStatus `-34018`). This only bites ad-hoc/CI builds without a signing
identity; add a `keychain-access-groups` entitlement there (the package's
example app shows how).
### Camera usage string
Add `NSCameraUsageDescription` to your `Info.plist` **only** if you use the
prebuilt UI's camera attach affordance:
```xml theme={null}
NSCameraUsageDescription
Take a photo to attach to your support messages.
```
The photo picker (`PhotosPicker`) and the document picker need **no** usage
strings on iOS 16+.
### Push
The standard APNs capability plus a one-line token forward — see the
[push guide](/swift/push). No Firebase.
## Verify
```swift theme={null}
import ClarioDesk
ClarioDesk.initialize(.init(apiKey: "pk_live_…"))
// Fire-and-forget: every subsequent call awaits the shared bootstrap, so
// nothing races. Next, wire identity. See the lifecycle guide.
```
Wire the four host events that touch the SDK.
# Swift lifecycle
Source: https://docs.clariodesk.com/swift/lifecycle
The four host events that touch the SDK: wire each one and you're done.
Four host events interact with the SDK. The concepts are shared across the
SDKs ([read the overview](/concepts/identity-lifecycle)); this page gives the
exact Swift code.
## 1. App launch (every time, including already-logged-in users)
The most common miss: existing users who installed your app before you added
ClarioDesk never hit the login flow again. Hydrate from your own session on
launch and identify **unconditionally**. `identify()` is idempotent and cheap.
```swift theme={null}
import ClarioDesk
@main
struct MyApp: App {
init() {
ClarioDesk.initialize(.init(apiKey: "pk_live_…"))
}
var body: some Scene {
WindowGroup {
ContentView()
.task {
if let user = await yourAuth.currentUser() {
try? await ClarioDesk.identify(
externalId: user.id,
email: user.email,
traits: ["plan": user.plan]
)
}
}
}
}
}
```
## 2. Fresh login or signup
Right after your auth succeeds:
```swift theme={null}
try await yourAuth.signIn(email, password)
try await ClarioDesk.identify(externalId: user.id, email: user.email)
```
This overwrites the label on the existing device row. The same device and
hardware key are reused; nothing new is registered.
## 3. Logout
```swift theme={null}
try await ClarioDesk.clearIdentity() // Verified identity
await yourAuth.signOut()
```
This preserves the device key while changing the signed identity epoch. In
label-only mode, `reset()` remains available for full installation isolation.
## 4. User switch (A → B without app restart)
```swift theme={null}
try await ClarioDesk.clearIdentity()
await yourAuth.switchTo(userB)
try await ClarioDesk.identify(identityMode: .host)
```
Await clear before activating user B. `reset()` deprovisions this installation;
it is not logout or customer erase.
## Checking state
```swift theme={null}
ClarioDesk.isInitialized // Bool
ClarioDesk.isIdentified // Bool
ClarioDesk.isAvailable // Bool — false once the server declared a terminal
// state (device revoked / key expired / app
// suspended); use it to hide your support entry point
```
## What if I never call `identify()`?
Tickets still work; agents see a device id but no email, and the dashboard shows
an "Unverified device" badge. This is fine for anonymous feedback; otherwise,
wire case 1.
# Swift prebuilt UI
Source: https://docs.clariodesk.com/swift/prebuilt-ui
A themeable SwiftUI support UI you present with one call, from SwiftUI or UIKit.
`ClarioDeskUI` is a drop-in support UI built in SwiftUI. One static call
presents it as a self-owned modal on the top-most view controller, so it works
identically from SwiftUI and UIKit hosts with zero setup — your navigation
stack is never touched. On iPhone it's full-screen; on iPad (regular width)
it's a centered form-sheet card.
## Open a screen
```swift theme={null}
import ClarioDeskUI
ClarioDeskWidgets.openInbox()
```
From there, the SDK handles inbox → thread → composer → attachment viewer
internally. Control returns to your app when the user dismisses the modal.
All entry points:
```swift theme={null}
ClarioDeskWidgets.openInbox() // the user's conversations
ClarioDeskWidgets.openNewTicket() // straight into the composer
ClarioDeskWidgets.openBugReport() // bug report form (auto-screenshot)
ClarioDeskWidgets.openConversation(ticketId) // one thread — the push deep-link target
```
Each takes optional `theme:`, `strings:`, and `from:` (an explicit presenting
`UIViewController`; omitted, the SDK finds the top-most one itself).
## The four screens
| Screen | What it does |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Inbox** | The user's list of conversations, with whose-turn chips. |
| **Thread** | The message thread, with a reply composer, typing, read receipts, and the CSAT prompt on resolve. |
| **New conversation** | Subject + body form. |
| **Bug report** | Description + a removable screenshot tile, captured **before** the modal presents so the SDK's own UI is never in frame. |
Plus an attachment viewer: zoomable images (with save), and **in-app HLS video
via `AVPlayer`** — a deliberate upward divergence from the Flutter/RN SDKs,
where in-app video playback would ship an ExoPlayer-weight dependency to every
host. AVPlayer is free and native on iOS.
## Theming
The theme is 13 optional tokens. Every unset token derives from sensible
light/dark defaults keyed off the system color scheme, so a zero-config
install already matches the host. Token names are 1:1 with the Flutter theme.
```swift theme={null}
let theme = ClarioDeskTheme(
primary: Color(red: 0.83, green: 1.0, blue: 0.0),
background: Color(white: 0.05),
primaryText: .white,
cornerRadius: 18
)
ClarioDeskWidgets.openInbox(theme: theme)
```
| Token | Notes |
| --------------------- | ------------------------------------------------------------ |
| `primary` | Accent — send button, links, unread dot, own-message bubble. |
| `onPrimary` | Text/icon drawn on `primary`. |
| `background` | Screen background. |
| `primaryText` | Body text. |
| `secondaryText` | Timestamps and metadata. |
| `componentBackground` | Composer field + received-message bubbles. |
| `componentBorder` | Dividers and input borders. |
| `placeholderText` | Composer placeholder. |
| `appBarBackground` | App bar background (defaults to `background`). |
| `appBarForeground` | App bar title + icons (defaults to `primaryText`). |
| `error` | Failed sends, bug-report accents. |
| `cornerRadius` | Bubbles, inputs, cards. Default 14. |
| `borderWidth` | Inputs and dividers. Default 1. |
The UI carries a WCAG 4.5:1 contrast guard: if a theme color would make text
unreadable on its background (a pastel accent, say), the SDK falls back to
black/white for the on-color instead of shipping unreadable text.
## Strings
\~50 user-facing strings are overridable for copy tweaks and localization —
English defaults, and no agent jargon (never "ticket" or raw statuses).
Field names are 1:1 with the Flutter `ClarioDeskStrings`.
```swift theme={null}
ClarioDeskWidgets.openInbox(
strings: ClarioDeskStrings(
inboxTitle: "Support",
newConversationCta: "New conversation"
)
)
```
## Shake to report a bug
Opt-in: a deliberate shake captures a screenshot and opens the bug-report
form. Foreground-only, debounced (5 s cooldown), suppressed while the support
modal is already up, and needs no `Info.plist` key.
```swift theme={null}
ClarioDeskWidgets.enableShakeReport() // idempotent; call once at launch
ClarioDeskWidgets.disableShakeReport()
```
Shake detection uses the CoreMotion accelerometer, so it does **not** fire on
the iOS Simulator (`⌃⌘Z` sends a UIKit motion event, not an accelerometer
sample). Verify on a physical device.
## Headless instead?
Everything the prebuilt screens do rides the same public methods — see the
[API reference](/swift/api-reference) to build the UI in your own design
system.
# Swift push
Source: https://docs.clariodesk.com/swift/push
APNs-direct push with zero Firebase: forward the token from your AppDelegate and you're done.
On native iOS, ClarioDesk delivers push straight through APNs. There is no
Firebase SDK to add, no google-services plist, no third-party messaging
dependency — the wiring is one line in your AppDelegate.
## Wire the token
Request notification permission and register for remote notifications as you
normally would, then forward the raw APNs token:
```swift theme={null}
import ClarioDesk
func application(
_ app: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken token: Data
) {
ClarioDesk.setAPNSDeviceToken(token)
}
```
That's the whole client side. The SDK registers the token with the backend and
re-registers when iOS rotates it.
## Upload your APNs key once
In the [dashboard](https://dashboard.clariodesk.com), open your app's push settings
and upload the APNs auth key (the `.p8` file from your Apple Developer
account, with its key id and team id). Delivery then goes ClarioDesk → APNs →
device, from your own Apple credentials.
## Deep-link on tap
Route ClarioDesk notifications in your tap handler, then open the thread:
```swift theme={null}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
if let payload = ClarioDesk.openTicketFromPush(userInfo) {
// Prebuilt UI: present straight into the thread.
ClarioDeskWidgets.openConversation(payload.ticketId)
}
completionHandler()
}
```
`openTicketFromPush` parses the payload, returns it (or `nil` if the
notification isn't ours), and emits the ticket id on `ClarioDesk.pushOpened` —
if the support modal is already presented, it navigates itself. Headless hosts
route with the returned payload or subscribe:
```swift theme={null}
ClarioDesk.subscribePushOpened { ticketId in
navigateToSupport(ticketId)
}
```
In a shared handler, distinguish ours without consuming:
```swift theme={null}
if ClarioDesk.isClarioMessage(userInfo) { /* ours */ }
let payload = ClarioDesk.parsePushPayload(userInfo) // nil if not ours
```
## Preferences
```swift theme={null}
let prefs = try await ClarioDesk.getNotificationPreferences()
_ = try await ClarioDesk.setNotificationPreferences(
pushEnabled: true,
quietHoursStart: "22:00",
quietHoursEnd: "08:00",
timezone: "America/New_York"
)
```
## Already shipping Firebase?
A host that already runs Firebase Messaging can hand the SDK FCM tokens
instead: implement the `PushTokenProvider` protocol against
`Messaging.messaging()` and pass it in `ClarioDeskOptions`, or call
`ClarioDesk.registerPushToken(token)` from your own messaging callback. The
SDK itself never imports Firebase either way.
The backend is presence-aware: when the user is actively connected over
realtime, the push is suppressed — they're already seeing the reply live. See
the [push concept page](/concepts/push-notifications).
# Swift quickstart
Source: https://docs.clariodesk.com/swift/quickstart
Native in-app support for iOS apps: one SPM package, a live ticket thread, no backend code on your side.
The ClarioDesk Swift SDK is the native iOS expression of the same contract as
the [Flutter](/flutter/quickstart) and [React Native](/react-native/quickstart)
SDKs: device-is-identity auth, live updates over Centrifugo/SSE, attachments,
CSAT, bug reports, and push — as an actor engine with `AsyncStream` reads and a
SwiftUI prebuilt UI.
## Install
Add the package in Xcode (**File → Add Package Dependencies…**) or in
`Package.swift`:
```swift theme={null}
.package(url: "https://github.com/clariodesk/clariodesk-swift", from: "0.1.1")
```
Link the `ClarioDesk` product (headless), and `ClarioDeskUI` too if you want
the prebuilt screens. Requires iOS 16+; the package is Swift 6 and consumable
from Swift 5.x hosts.
## Initialize
`initialize` is synchronous and fire-and-forget — no `await`. The first launch
generates the Secure Enclave device key and registers the device; later
launches reuse the stored key with no network call.
```swift theme={null}
import ClarioDesk
@main
struct MyApp: App {
init() {
ClarioDesk.initialize(.init(apiKey: "pk_live_…"))
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
```
(UIKit hosts call the same line from
`application(_:didFinishLaunchingWithOptions:)`.)
## Identify the user
Call this after your own auth knows who the user is. It's optional, pure
metadata. See [lifecycle](/swift/lifecycle) for the four events you should
wire (especially case 1, which covers users who were logged in before you
installed the SDK).
```swift theme={null}
try await ClarioDesk.identify(
externalId: user.id,
email: user.email,
traits: ["plan": "pro"]
)
```
## File a ticket
```swift theme={null}
let (ticket, _) = try await ClarioDesk.createTicket(
subject: "Upload broken",
body: "Tapping upload does nothing."
)
```
## Stream the inbox & a thread
Reactive reads are replay-last `AsyncStream`s: a new iterator immediately gets
the current value, then live updates.
```swift theme={null}
struct InboxView: View {
@State private var tickets: [Ticket] = []
var body: some View {
List(tickets) { ticket in
Text(ticket.subject)
}
.task {
do {
for try await latest in ClarioDesk.tickets { tickets = latest }
} catch { /* render a retry state */ }
}
}
}
// A single thread:
for try await messages in ClarioDesk.messages(ticketId: ticket.id) {
render(messages)
}
```
UIKit hosts use the closure variants instead of `for await`:
```swift theme={null}
let sub = ClarioDesk.subscribeTickets { result in
if case .success(let tickets) = result { render(tickets) }
}
// sub.cancel() when done
```
## Send a reply
```swift theme={null}
_ = try await ClarioDesk.sendMessage(
ticketId: ticket.id,
body: "Still happening on 1.4.2"
)
```
## Run the example app
The package repo ships `Examples/SwiftUIExample`, a runnable SwiftUI host:
```bash theme={null}
cd Examples/SwiftUIExample
xcodegen generate
xcodebuild -scheme ClarioDeskSwiftUIExample \
-destination 'generic/platform=iOS Simulator' build
```
Wire login, launch, logout, and user-switch.
Skip building the UI. Open a themeable inbox in one call.