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