Graine AI

iOS SDK reference

Every public type, function, property, listener and error of GraineInApp 0.10.3 for iOS and its optional WebRTC package, with the Objective-C interface: signatures, parameters, what each returns, the errors it raises, and what to expect.

Every public declaration of the iOS SDK, GraineInApp 0.10.3, and of its optional WebRTC package for calls, GraineInAppRTC, read from their source and grouped by what you are doing: installing it, saying who the customer is, the chat, the voice bar, several agents, screen context and actions, calls in both directions, booking, the scheduled-calls list, design and going live. Every entry starts with what it is for, then its signature, its parameters, what it returns, the errors it can raise and an example; each type sits with the use case that returns or takes it, field by field, with what to expect. A value newer than the SDK — a status, a source, an error code — never crashes it: it arrives as unknown, carrying its word. The Objective-C interface is the appendix. The same API across every platform, topic by topic, is the SDK reference; installing and setting up is iOS.

Requirements, install, permissions and initialisation

PackageGraineInApp 0.10.3 — a Swift package with no dependencies
RequiresiOS 15+ (Mac Catalyst 15+ also builds); Swift tools 5.9 (Xcode 15 or later). It links Apple's AVFoundation, CallKit, Combine, CoreGraphics, CoreText, PushKit, SwiftUI, UIKit and WebKit.
Calls needA real iPhone (CallKit and VoIP pushes do not run in the simulator), an Apple Developer account and an APNs auth key for hosted ringing
WebRTC calls (optional)GraineInAppRTC 0.10.3 — a separate package, always the same version as GraineInApp: Add WebRTC calls

Install — Swift Package Manager (primary)

Use it to add the SDK with Xcode or a Package.swift — the primary way to install it.

Xcode → File → Add Package Dependencies… → https://github.com/GraineAI/inapp-ios, version 0.10.3 or later, and add the GraineInApp library to your app target. In a Package.swift:

Package.swift

dependencies: [
    .package(url: "https://github.com/GraineAI/inapp-ios", from: "0.10.3"),
],
targets: [
    .target(name: "AcmeApp", dependencies: [.product(name: "GraineInApp", package: "inapp-ios")]),
]

Install — CocoaPods

Use it when your app already manages its dependencies with CocoaPods.

CocoaPods — Podfile

platform :ios, '15.0'
target 'AcmeApp' do
  use_frameworks!
  pod 'GraineInApp', '~> 0.10'
  # or straight from the tag:
  # pod 'GraineInApp', :git => 'https://github.com/GraineAI/inapp-ios.git', :tag => '0.9.0'
end

The pod builds exactly what the Swift package builds (the same sources, iOS 15, Swift 5.9) and carries the privacy manifest in its own resource bundle, GraineInApp_Privacy.

Add WebRTC calls (optional)

Add it so the native calls — the customer calling your agent, and your agent ringing the customer — carry their audio over WebRTC and hold up on a weak or changing network.

Package.swift

dependencies: [
    .package(url: "https://github.com/GraineAI/inapp-ios", from: "0.10.3"),
    .package(url: "https://github.com/GraineAI/inapp-ios-rtc", from: "0.10.3"),   // the same version as GraineInApp
],
targets: [
    .target(name: "AcmeApp", dependencies: [
        .product(name: "GraineInApp", package: "inapp-ios"),
        .product(name: "GraineInAppRTC", package: "inapp-ios-rtc"),
    ]),
]

In Xcode: File → Add Package Dependencies… → https://github.com/GraineAI/inapp-ios-rtc, the same version as GraineInApp, and add the GraineInAppRTC library to the same target as GraineInApp. With CocoaPods, beside pod 'GraineInApp':

CocoaPods — Podfile

pod 'GraineInApp', '~> 0.10'
pod 'GraineInAppRTC', :git => 'https://github.com/GraineAI/inapp-ios-rtc.git', :tag => '0.10.3'   # the same version as GraineInApp, from its repository

Nothing else changes: no code, no Info.plist entry, no capability and no permission beyond the calls' own (Permissions and capabilities). The calls find the module by themselves. A build that strips code nothing references can make sure of it at launch:

import GraineInAppRTC
 
GraineRTC.install()   // optional: only for a build that strips code nothing references
  • What it is. A separate package, so GraineInApp keeps no dependencies and an app that does not add it never downloads it. It carries a prebuilt WebRTC library (Google's WebRTC, BSD 3-Clause, its symbols renamed so it never clashes with another copy in the app). Its licences are in the package's THIRD-PARTY-NOTICES.md: include them in your app's open-source notices. If another package in your app pins the same WebRTC build at a different version, the two cannot be resolved together: leave the module out (the calls stay on the WebSocket) or align the versions.
  • Size. About 12 MB more installed on an arm64 iPhone, and about 6 MB more to download.
  • Privacy. It ships its own privacy manifest (with CocoaPods, in the GraineInAppRTC_Privacy bundle): the system boot time (reason 35F9.1), to time the call's steps; no tracking and no tracking domains.
  • Which calls. Both directions of the native calls (GraineCalls): a call the customer places with startCall, and a call Graine rang — a booked call or "Ring now" — that the customer answers. The voice bar's in-app call (GraineVoice) is not affected; GraineConfig.transport chooses its audio.
  • Choosing. GraineCallsConfig.callTransport: .auto (the default) uses WebRTC when the module is in the app and the dashboard allows it, else the WebSocket; .websocket always the WebSocket; .webrtc WebRTC wherever it can carry the call. See GraineCallTransport.
  • The dashboard's switch. The agent's In-app call audio setting (agent → Calls & scheduling): WebRTC (recommended) or Standard. Standard moves every install's calls back to the WebSocket, whatever the app chose, from each app's next start — no app release. The app reads it as GraineHostedCallsSettings.audioTransport.
  • What the customer notices. The same ring, the same call screen, the same "Connecting…". On a weak or changing network the agent's voice holds up instead of stalling and catching up (a jitter buffer, loss concealment and the platform's echo canceller). The call is prepared at the tap — or while a ring is showing — so it connects as fast as before. Over WebRTC the agent's voice arrives at telephone bandwidth today: steadier through a weak network, narrower than the WebSocket's on a perfect one.
  • When WebRTC cannot carry a call. A network that blocks it, a server that turns it down, or no answer within the time allowed (about 12 seconds in all), before any audio flows: the call continues on the WebSocket inside the same call — no second ring, no new call screen, no error; "Connecting…" shows a moment longer — and GraineCalls.transportFallbackReason says why. A call the agent cannot take at all (no credit left, the agent switched off) ends with that reason, exactly as without the module. Once audio flows a call never moves: a network drop is retried once, then the call ends as dropped.

Permissions and capabilities

Read it to add what the agent and calls need to the app target before you submit the app.

Add (Xcode → the app target)ForWhy
NSMicrophoneUsageDescription in Info.plistVoice and callsRequired: iOS stops an app that asks for the microphone without it. Suggested text: Lets you talk to the assistant in the app and on calls.
Signing & Capabilities → Push NotificationsCalls that ringVoIP pushes reach the app (the aps-environment entitlement)
Background Modes → Audio, AirPlay, and Picture in Picture (audio)CallsThe call's audio keeps going with the screen off
Background Modes → Voice over IP (voip)Calls that ringiOS wakes a closed app for a VoIP push

XML — Info.plist

<key>NSMicrophoneUsageDescription</key>
<string>Lets you talk to the assistant in the app and on calls.</string>
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
    <string>voip</string>
</array>

Nothing else: no notification permission (VoIP pushes and CallKit need none), no Bluetooth usage description, no remote-notification background mode — and the WebRTC package adds nothing to this list. The SDK ships a privacy manifest declaring the required-reason APIs it calls — system boot time (reason 35F9.1, for timing) and UserDefaults (reason CA92.1) — and no tracking and no tracking domains; Xcode adds it to your app's privacy report. What your app collects through the agent, the customer's voice included, goes in your App Privacy details.

What the SDK configures in CallKit

Read it to know how a call looks and behaves on the system's call screen, and which of your settings change it.

CallKit settingValue
Call name (caller on the system screen, Recents)For a ring Graine sends: the agent's Name on the incoming call (else the agent's name). For a call the customer places, and a ring from your own server without a name: GraineCallsConfig.displayName; with several agents, each agent's call name first; empty, your app's display name.
Icon template (the app button on the system call screen)GraineCallsConfig.iconTemplateImageData (a 40 × 40 pt transparent PNG); otherwise the brand's launcher glyph from the design, drawn as a template image
RingtoneGraineCallsConfig.ringtoneSound (a sound file in your bundle, 30 seconds or less); otherwise the Garage's Ringtone — Graine or Soft written as a sound file for CallKit, or the phone's own ringtone for Phone's own
Calls in the Phone app's RecentsGraineCallsConfig.includesCallsInRecents, off by default
Video, hold, grouping, keypadOff: voice only, one call at a time (one call group, one call per group)
Ring timeoutA ring Graine sends rings for the agent's Ring for; any other for GraineCallsConfig.ringTimeout (30 seconds by default)
Audio sessionPlay-and-record in voice-chat mode, Bluetooth hands-free allowed, the earpiece by default; activated by CallKit, and the app's own session restored afterwards
  • Every VoIP push is reported to CallKit at once. iOS requires it and stops delivering VoIP pushes to an app that does not. The SDK reports every one — even one it cannot use, or one that arrives during a live call (that one is ended at once and reported missed; the live call is untouched). Send VoIP pushes only for calls. An app with its own PKPushRegistry hands the payload to reportIncomingCall(payload:completion:) instead.
  • On the lock screen the ring is the system's incoming-call screen (a banner while the phone is in use), with your call name, icon and ringtone. Answered there, the call connects; the system call screen stays on the lock screen for the call, and your app shows GraineCallView when it is opened. Do Not Disturb, a Focus or the block list can stop a ring; it is then reported missed.
  • The microphone: for a call the customer places, the SDK asks before placing it. For a ring, the customer answers first; the SDK reports answered, iOS asks while the call connects, and a refusal — or no answer within 60 seconds — ends it failed. Ask during onboarding so a call answered on the lock screen already has it.
  • Apple does not allow CallKit in apps on the App Store in China: leave calls off there.

Initialise

Use it as the starting point: configure the agent at launch, and mount the bar and the call screen once.

iOS · Swift — AcmeApp.swift

import SwiftUI
import GraineInApp
 
@main
struct AcmeApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    var body: some Scene { WindowGroup { RootView() } }
}
 
final class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        let graine = GraineInApp.configure(GraineConfig(
            baseUrl: "https://www.graine.ai",
            publishableKey: "pk_live_…",          // Agent → Embed & Widgets → Install
            appVersion: "2.4.0"
        ))
        graine.calls.configure(GraineCallsConfig(displayName: "Asha from Acme"))
        graine.calls.enableVoIPPushes()           // every launch: iOS hands over a VoIP push only once this has run
        return true
    }
}
 
struct RootView: View {
    @ObservedObject private var calls = GraineInApp.shared.calls
    var body: some View {
        AppContent()
            .overlay(GraineAgentBar())            // the bar: voice, chat and components, in your design
            .fullScreenCover(isPresented: .constant(calls.showsCallScreen)) {
                GraineCallView()                  // the call while your app is open
            }
    }
}

The bar mounts the voice engine in the key window the first time it appears (autoAttach), so a call carries on as the customer moves through your app. UIKit apps host the SwiftUI views in a UIHostingController, or use GraineChatViewController; without the bar, mount the engine with GraineInApp.shared.voice.attach().

Install and the first answer

Configure the agent once, at launch, and know when its settings from the dashboard have arrived. The agent starts loading its settings the moment configure runs; every request waits for them.

GraineInApp.configure

Call it once at launch to set up an agent; everything else goes through the instance it returns.

@discardableResult static func configure(_ config: GraineConfig, name: String = "default") -> GraineInApp

Configures an agent. Call it once, at launch (App.init or application(_:didFinishLaunchingWithOptions:)). Without a name it is the default agent — the one shared returns and every view draws unless handed another; an app with several agents configures each under its own name and publishable key. The name is trimmed, and an empty name is the default. Calling it again with a name that is already configured gives back that same agent, untouched. A new agent takes on who is signed in, the memory choice and the current screen, and starts loading its settings from Graine.

ParameterTypeDefaultMeaning
configGraineConfigrequiredEverything the SDK needs up front — at least baseUrl and publishableKey.
nameString"default"The name to configure this agent under, to find it again with named(_:); left out or empty, the default agent.

Returns — GraineInApp: the agent's instance. Keep it, or reach it later as GraineInApp.shared (the default agent) or GraineInApp.named(_:).

Errors — none.

let graine = GraineInApp.configure(GraineConfig(baseUrl: "https://www.graine.ai", publishableKey: "pk_live_…"))

GraineConfig (constructor)

Use it to build the configuration you pass to configure.

init(baseUrl: String, publishableKey: String, variables: [String: Any] = [:], transport: GraineTransport? = nil, appVersion: String? = nil, endOnBackground: Bool = true, timing: Timing = Timing(), debug: Bool = false, friction: GraineFrictionConfig? = nil, fonts: [String: String] = [:], captureTaps: Bool = true, chatSize: GraineChatSize? = nil)

Builds a configuration.

ParameterTypeDefaultMeaning
baseUrlStringrequired"https://www.graine.ai" unless your workspace is self-hosted.
publishableKeyStringrequiredThe agent's publishable key, pk_live_…, from Agent → Embed & Widgets → Install.
variables[String: Any][:]Prompt variables by name, for this call or conversation; masked on the device before they leave it.
transportGraineTransport?nilHow the voice bar's in-app call travels: the standard path or high quality (WebRTC); left out, the dashboard decides. Native calls are not affected: they take GraineCallsConfig.callTransport.
appVersionString?nilYour app's version; it lands on the call record.
endOnBackgroundBooltrueEnd a live conversation when the app goes to the background.
timingTimingTiming()Load timing for the engine page that carries the in-app call (see GraineConfig.Timing); most apps keep the defaults.
debugBoolfalseLog the SDK's traffic with Graine — keys only, never values.
frictionGraineFrictionConfig?nilThe app's own friction (stuck-customer) rules, applied over the dashboard's; nil uses the dashboard's alone.
fonts[String: String][:]Fonts your app ships itself, by family: a family named here is used instead of downloading it — ["Inter": "InterVariable"] maps a family to the name your app registered it under.
captureTapsBooltrueTell the agent what the customer taps while a screen is set.
chatSizeGraineChatSize?nilHow big the chat window opens, over the agent's Window size.

Returns — a new GraineConfig.

Errors — none.

let config = GraineConfig(baseUrl: "https://www.graine.ai", publishableKey: "pk_live_…",
                          appVersion: "2.4.0", debug: true)

GraineConfig.Timing (constructor)

Use it only for a very slow network, to give the engine page that carries the in-app call longer to load.

init(stallMs: Int = 15_000, bootMs: Int = 15_000, hardCapMs: Int = 120_000, retryDelaysMs: [Int] = [1_000, 2_000, 4_000, 8_000, 15_000, 15_000], endWatchdogMs: Int = 4_000, slowConnectMs: Int = 4_000)

Builds the load timing for the engine page that carries the in-app call, in milliseconds — for a very slow network. Most apps keep the defaults.

ParameterTypeDefaultMeaning
stallMsInt15_000Silence — nothing downloaded — before a load is given up on and retried.
bootMsInt15_000Downloaded but still not ready after this long: the page is broken, not slow, and is reloaded.
hardCapMsInt120_000The ceiling on one load attempt, however steadily it is arriving.
retryDelaysMs[Int][1_000, 2_000, 4_000, 8_000, 15_000, 15_000]The waits between reloads; its length is how many reloads are tried before GraineVoice.connectivity becomes failed.
endWatchdogMsInt4_000How long GraineVoice.end() waits for the page to confirm before ending the call locally.
slowConnectMsInt4_000A connect the customer has waited on this long sets GraineVoice.slowConnect ("Still connecting…").

Returns — a new GraineConfig.Timing.

Errors — none.

let config = GraineConfig(baseUrl: "https://www.graine.ai", publishableKey: "pk_live_…",
                          timing: GraineConfig.Timing(stallMs: 25_000, bootMs: 25_000))

GraineInApp.session

Read it to know whether the agent's settings from the dashboard have arrived, and what they say.

var session: GraineSessionConfig? { get }

The agent's settings, loaded once per launch, starting the moment configure runs; nil until they arrive. Every request to Graine made before then waits for them, so you never have to. GraineListener.onSessionReady tells you when they arrive — the moment to read bookingAvailable, schedulingRules, theme and checkActions(). If they cannot be loaded, onError says why and the next request tries again. Readiness of the voice engine is separate: GraineVoice.ready and GraineVoice.connectivity.

Parameters — none.

Returns — GraineSessionConfig? — its fields: GraineSessionConfig. Example value: nil before the settings load.

Errors — none.

if let session = GraineInApp.shared.session {
    print("Agent \(session.agentId), memory: \(session.memoryEnabled ?? false)")
}

GraineInApp

Use it as the SDK's entry point: one instance per configured agent, reached as GraineInApp.shared or GraineInApp.named(_:).

The SDK's entry point: one instance per configured agent, made by GraineInApp.configure(_:name:) at launch and reached as GraineInApp.shared (the default agent) or GraineInApp.named(_:). Its methods may be called from any thread; voice, calls and chat are main-thread objects, and listeners are called on the main thread.

What to expect — The SDK's entry point, configured once at launch; everything else goes through it, or through the agent you name. A request to Graine made before the agent's settings have loaded waits for them.

PropertyTypeMeaning
static var all: [String: GraineInApp] { get }[String: GraineInApp]Every configured agent, keyed by name.
static var current: GraineInApp? { get }GraineInApp?The default agent — or, when every agent was given a name, the first one configured. Nil before any agent is configured.
static var shared: GraineInApp { get }GraineInAppThe same agent as current. Stops the app with a message if no agent has been configured: call configure first.
typealias ActionHandler = ([String: Any]) throws -> GraineActionResult—The handler type for registerAction(_:handler:): it receives the agent's arguments and returns the answer, and may throw.
typealias DeferredActionHandler = ([String: Any], @escaping (GraineActionResult) -> Void) -> Void—The handler type for registerAction(_:deferred:): it receives the arguments and a done callback to answer with later.
let config: GraineConfigGraineConfigThe configuration this agent was made with.
let name: StringStringThe name this agent was configured under; "default" for the default agent.
var session: GraineSessionConfig? { get }GraineSessionConfig?The agent's settings from the dashboard, once loaded (once per launch); nil before. onSessionReady reports it.
var design: GraineDesign? { get }GraineDesign?The brand's design from the agent's settings: light and dark tokens and which to use. Nil before the agent's settings load, and from a server that sends none; the SDK's views then draw from the dashboard's appearance settings.
var theme: GraineThemeTokens? { get }GraineThemeTokens?The design tokens to draw right now: the brand's fixed scheme, else the device's current light or dark, picked again whenever the device switches. Nil without a design. onTheme reports each change.
private(set) lazy var voice: GraineVoiceGraineVoiceThe in-app voice call that GraineAgentBar uses. Attach it to your window before starting a call. Main thread only.
var calls: GraineCalls { get }GraineCallsNative calls on CallKit and VoIP pushes: the customer calling your agent, or your agent ringing the customer. One object for the whole app — every agent's calls is the same object, and startCall(reason:variables:agent:) picks the agent. Main thread only.
private(set) lazy var chat: GraineChatGraineChatThe typed conversation with this agent. Both GraineChatView and the bar already work through it on their own. Main thread.
var memoryChoice: Bool { get }BoolThe current customer's memory choice: true until they turn it off. When false, nothing is recalled or stored.
var installId: String { get }StringThis install's id: a UUID made on first launch and kept in the Keychain (this device only), surviving reinstalls. The same for every agent. Graine uses it to count a customer's devices, and it is never shown to the agent. If the Keychain cannot keep it, that launch uses a new one.
var schedulingRules: GraineSchedulingRules { get }GraineSchedulingRulesThe rules for booking calls, from the agent's settings — or, before they load, from the last ones kept on the device; the defaults (no booking) when there is neither. scheduleCall checks them first.
var bookingAvailable: Bool { get }BoolWhether this agent can be booked from the app now — Graine rings this iPhone, the agent lets customers book, and the customer is identified. What the SDK's own "Book a call" follows, and what your own "Call me" button should; see GraineInApp.bookingAvailable.

Its functions: configure, named, addListener, removeListener, identify, setMemory, forgetMemory, memories, setVariables, currentVariables, setScreen, updateField, removeField, clearScreen, track, tap, noteInteraction, configureFriction, answerComponent, reopenComponent, registerAction, checkActions, scheduleCall, scheduledCalls, cancelScheduledCall, presentBooking — each with its parameters, response, errors and an example.

GraineConfig

Read it to see every option the SDK takes at launch and what each one changes.

Everything the SDK needs up front, passed to GraineInApp.configure(_:name:) and kept as GraineInApp.config. A value type; only baseUrl and publishableKey are required.

What to expect — Made once, at launch, before anything else. Optional fields have the defaults shown; nothing in it is sent to Graine except what the descriptions say.

FieldTypeDefaultMeaning
var baseUrl: StringStringrequiredGraine's address: https://www.graine.ai unless self-hosted.
var publishableKey: StringStringrequiredFrom Agent → Embed & Widgets; selects the agent. It is not a secret and cannot place a call on its own: add app:// to that agent's Allowed domains, or every request is refused.
var variables: [String: Any][String: Any][:]Prompt variables known at build time. identify and setVariables win over these.
var transport: GraineTransport?GraineTransport?nilForces how the in-app call's audio travels. nil lets the dashboard's Call audio setting decide.
var appVersion: String?String?nilStored on the call record, so one build can be told from another.
var endOnBackground: BoolBooltrueEnds a live in-app call when the app goes to the background (after two seconds, if it is still there). The chat also hangs up in the background and comes back with the app.
var timing: TimingGraineConfig.TimingTiming()Load timing for the engine page, for very slow networks.
var debug: BoolBoolfalseLogs every message between the SDK and its engine page, both ways, and the page's own console, under the subsystem ai.graine.inapp. Variables are logged as keys only and screen context as the screen's name only. The engine page can also be inspected in Safari (iOS 16.4 and later). Turn it on in debug builds.
var friction: GraineFrictionConfig?GraineFrictionConfig?nilThe app's friction rules, applied over the dashboard's — for screens only the app knows about.
var fonts: [String: String][String: String][:]Fonts the app ships itself. The SDK downloads the design's typeface from Graine; a family named here, or one the app bundles under the family's own name, is used instead and never downloaded. If your app registered a family under a different name, map the family to that name: ["Inter": "InterVariable"].
var captureTaps: BoolBooltrueTells the agent about taps: a tap on a control with an accessibility identifier — or a button's accessibility label — reaches it as tap:<name>, any other tap as tap:screen with where it landed. No more than two go out per second, only while a screen is set, and a field's value is never included. The recogniser only observes; every touch still reaches the app.
var chatSize: GraineChatSize?GraineChatSize?nilHow big the chat window opens, over the dashboard's Window size: .compact above the bar, .large a sheet over most of a phone (a panel on an iPad), .full the whole screen. Nil follows the dashboard; a size passed to chat.open(size:) wins for that open.

Its functions: init — each with its parameters, response, errors and an example.

GraineConfig.Timing

Read it to tune how patiently the engine page loads on a poor network.

Load timing for the engine page that carries the in-app call, in milliseconds, set as GraineConfig.timing. Most apps keep the defaults.

What to expect — Made once, at launch, before anything else. Optional fields have the defaults shown; nothing in it is sent to Graine except what the descriptions say.

FieldTypeDefaultMeaning
var stallMs: IntInt15_000How long a load may go with nothing downloaded before it is abandoned.
var bootMs: IntInt15_000Downloaded but still not ready after this long: the page is broken, not slow.
var hardCapMs: IntInt120_000Ceiling on one load attempt, however steadily it is arriving.
var retryDelaysMs: [Int][Int][1_000, 2_000, 4_000, 8_000, 15_000, 15_000]The waits between reloads; its length is the number of reloads before connectivity becomes failed.
var endWatchdogMs: IntInt4_000How long GraineVoice.end() waits for the page to confirm before ending the call locally.
var slowConnectMs: IntInt4_000A connect the customer has waited on this long sets GraineVoice.slowConnect ("Still connecting…").

Its functions: init — each with its parameters, response, errors and an example.

GraineSessionConfig (constructor)

Use it to build agent settings by hand for SwiftUI previews and tests, without the network.

init(agentId: String, webcallAgentId: String, appearance: [String: Any] = [:], variables: [String: Any] = [:], pageVersion: String? = nil, declaredActions: Set<String> = [], friction: [String: Any]? = nil, design: GraineDesign? = nil, wsHost: String? = nil, identity: GraineIdentitySettings? = nil, memoryEnabled: Bool? = nil, calls: GraineHostedCallsSettings? = nil)

Builds settings by hand — for previews and tests; the SDK makes the real ones.

ParameterTypeDefaultMeaning
agentIdStringrequiredThe agent's id, as the dashboard shows it.
webcallAgentIdStringrequiredThe id in-app calls are placed and filed under; usually the same as agentId.
appearance[String: Any][:]The agent's appearance settings.
variables[String: Any][:]Prompt variables by name, for this call or conversation; masked on the device before they leave it.
pageVersionString?nilThe engine page's version, used to cache it.
declaredActionsSet<String>[]The in-app actions declared on the agent, for testing checkActions().
friction[String: Any]?nilThe dashboard's friction rules, as JSON.
designGraineDesign?nilThe agent's design — nil draws from appearance.
wsHostString?nilWhere the agent's conversations are hosted; nil uses Graine's own.
identityGraineIdentitySettings?nilThe agent's Identity setting: which trait names the customer, and how many devices they may use. nil means user_id and two devices.
memoryEnabledBool?nilWhether the agent remembers customers between conversations; nil when the settings do not say.
callsGraineHostedCallsSettings?nilWhether Graine rings this app, the booking rules and the calls' audio setting; nil means no ringing, no booking and no say on the calls' audio.

Returns — a new GraineSessionConfig.

Errors — none.

let preview = GraineSessionConfig(agentId: "agent_1", webcallAgentId: "agent_1", memoryEnabled: true,
                                  calls: nil)   // no ringing, no booking

GraineSessionConfig

Read it to see what the dashboard says about the agent, once its settings have loaded.

The agent's settings from the dashboard, fetched once per launch: GraineInApp.session and GraineListener.onSessionReady(_:). Missing fields are empty, never a crash.

What to expect — The agent's settings from the dashboard, loaded once per launch; null / nil until then. A setting an older server does not send means its default: the user_id trait, 2 devices per user, booking off. Missing fields are empty, never a crash.

FieldTypeDefaultMeaning
let agentId: StringStringrequiredThe agent's id.
let webcallAgentId: StringStringrequiredThe id in-app calls are placed and filed under; the same as agentId when the settings name none.
let appearance: [String: Any][String: Any][:]The settings from Embed & Widgets → Appearance, unchanged from how they are stored.
let variables: [String: Any][String: Any][:]The agent's default prompt variables.
let pageVersion: String?String?nilThe engine page's version, used to cache it.
let wsHost: String?String?nilWhere the conversation server is, when the settings name it; nil uses Graine's default.
let declaredActions: Set<String>Set<String>[]The in-app actions declared on this agent in the dashboard.
let friction: [String: Any]?[String: Any]?nilThe dashboard's friction rules for this agent, applied under GraineConfig.friction.
let design: GraineDesign?GraineDesign?nilThe brand's design, resolved into light and dark tokens. Nil from an older server: every surface then draws from appearance.
let identity: GraineIdentitySettings?GraineIdentitySettings?nilThe agent's Identity setting; nil from an older server, which means user_id.
let memoryEnabled: Bool?Bool?nilWhether the agent remembers customers between conversations; nil from an older server.
let calls: GraineHostedCallsSettings?GraineHostedCallsSettings?nilWhether Graine rings this app, the booking settings and the In-app call audio setting; nil from an older server.
var schedulingRules: GraineSchedulingRules { get }GraineSchedulingRules—The rules for booking calls from calls; no booking, and the defaults, when there are none.
var preferredTransport: GraineTransport { get }GraineTransport—The dashboard's Call audio setting.
var accent: String? { get }String?—The dashboard's accent colour, as stored (with or without its "#").

Its functions: init — each with its parameters, response, errors and an example.

Identity, memory and conversations that continue

Say who the customer is, let them choose whether they are remembered, show and forget what the agent remembers, and continue — or restart — the conversation they had.

GraineInApp.identify

Call it whenever the signed-in customer changes, so memory, bookings and the calls Graine rings follow the right person.

func identify(_ traits: [String: Any?]?, memory: Bool? = nil)

Who is signed in, for every configured agent. The trait the agent's Identity setting names (user_id unless the dashboard says email, phone or a custom trait) identifies the customer for memory and for the calls Graine rings: its value travels only with the requests that start a conversation, never as a prompt variable. Every other trait becomes a prompt variable, masked on the device; nested objects and arrays are dropped. A placeholder value ("guest", "null", a masked "[EMAIL]") is no identity, and those conversations are anonymous. memory: false means this customer's conversations are neither remembered nor recalled; nil keeps their last choice (true until they make one). The identity is fixed per conversation: a change applies to the next one. identify(nil) signs out: the device stops being that customer's, Graine is told, and later conversations are anonymous. When a different customer signs in, or the customer signs out, the chat transcript on screen is cleared.

ParameterTypeDefaultMeaning
traits[String: Any?]?requiredWho the customer is. The trait your agent's Who is the user? field names (user_id unless you chose email, phone or a custom field) identifies them; every other trait becomes a prompt variable, masked on the device. nil / null signs out.
memoryBool?nilfalse: this customer's conversations are neither remembered nor recalled (kept on the device for them). Left out: their last choice stands — on until they make one.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.identify(["user_id": user.id, "name": user.firstName, "plan": user.plan], memory: user.allowsMemory)
// on sign-out, and before another customer signs in
GraineInApp.shared.identify(nil)

GraineInApp.setMemory

Call it when the customer turns being remembered on or off in your app's settings.

func setMemory(_ enabled: Bool)

Whether the current customer's conversations are remembered, for every agent — the same as calling identify again with memory: enabled. Kept on the device for that customer; applies to the next conversation.

ParameterTypeDefaultMeaning
enabledBoolrequiredtrue to remember this customer's conversations; false to stop. Kept on the device for them.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.setMemory(true)

GraineInApp.memoryChoice

Read it to show the current state of a "Remember my conversations" switch.

var memoryChoice: Bool { get }

The current customer's memory choice: true until they turn it off with setMemory(false) or identify(_:memory: false). Kept on the device for each customer, so the next customer who signs in on the same phone has their own. When false, nothing is recalled or stored for them.

Parameters — none.

Returns — Bool. Example value: true.

Errors — none.

rememberSwitch.isOn = GraineInApp.shared.memoryChoice

GraineInApp.installId

Read it to tell your support team which install a customer is using.

var installId: String { get }

This install's id: a UUID made on first launch and kept in the Keychain (this device only), so it survives reinstalls. The same for every agent. Graine counts a customer's devices by it (Devices per user); the agent never sees it. If the Keychain cannot keep it, that launch uses a new one.

Parameters — none.

Returns — String. Example value: "9b2e7c4a-1f3d-4e8a-b6c5-0d7f2a9e4b13".

Errors — none.

supportInfo["install"] = GraineInApp.shared.installId

GraineInApp.memories

Call it to show the customer what the agent remembers about them.

func memories() async throws -> [GraineMemory]

What the agent remembers about the signed-in customer, newest first, at most 100, already redacted — to show people what is kept about them. Waits for the agent's settings first, and throws identity_required before sending anything when nobody is identified ("Nobody is identified: call identify() with the customer's identifier first.").

Parameters — none.

Returns — [GraineMemory] — its fields: GraineMemory. Example value: [GraineMemory(id: "m_41", text: "Prefers a call after 6 pm", createdAt: 2026-10-02 09:14:00 +0000, updatedAt: 2026-10-12 17:40:00 +0000)].

Errors — GraineServiceError: identity_required, device_limit, rate_limited, network, unavailable, invalid_key, origin_refused

let memories = try await GraineInApp.shared.memories()

GraineInApp.forgetMemory

Call it when the customer asks the agent to forget them.

@discardableResult func forgetMemory() async throws -> Int

Deletes everything this agent's memory holds about the signed-in customer. Waits for the agent's settings first. Throws identity_required before sending anything when nobody is identified, and device_limit from a device that is not one of the customer's. Returns how many memories were deleted (0 when Graine does not say). The kept conversation goes too, and the chat on screen clears — only when it succeeds; a refusal leaves both as they were.

Parameters — none.

Returns — Int. Example value: 3.

Errors — GraineServiceError: identity_required, device_limit, rate_limited, network, unavailable, invalid_key, origin_refused

let deleted = try await GraineInApp.shared.forgetMemory()   // 3

GraineChat.newConversation

Call it when the customer wants a fresh start instead of continuing the earlier conversation.

func newConversation() async throws

Starts a fresh thread: the chat hangs up, Graine lets go of the conversation it kept for this agent and customer, the chat on screen clears and — when the customer was in the chat — a fresh chat opens with the welcome and the starters. Memories are untouched. With nobody identified only the screen clears. On a refusal the chat is as it was, connected again. While GraineChatView shows an earlier conversation, its header offers this.

Parameters — none.

Returns — nothing; it returns when done.

Errors — GraineServiceError: device_limit, rate_limited, network, unavailable, invalid_key, origin_refused — the chat is then as it was, connected again.

try await GraineInApp.shared.chat.newConversation()

GraineMemory (constructor)

Use it to build sample memories for previews and tests of a "what we remember" screen.

init(id: String, text: String, createdAt: Date? = nil, updatedAt: Date? = nil)

Builds a memory by hand — for previews and tests of a screen that lists what is remembered; memories() makes the real ones.

ParameterTypeDefaultMeaning
idStringrequiredThe memory's id, as Graine keeps it.
textStringrequiredWhat is remembered, in a sentence — already redacted.
createdAtDate?nilWhen it was first remembered.
updatedAtDate?nilWhen it last changed.

Returns — a new GraineMemory.

Errors — none.

let preview = GraineMemory(id: "m_41", text: "Prefers a call after 6 pm", createdAt: Date())

GraineMemory

Read it to show one thing the agent remembers about the customer.

One thing the agent remembers about the signed-in customer, already redacted — returned by GraineInApp.memories(), to show people what is kept about them beside forgetMemory(). A value type.

What to expect — From memories(): newest first, at most 100, already redacted. Only for the identified customer, from one of their devices. The dates are null when Graine did not say; they are moments in UTC — show them in the device's time zone.

FieldTypeDefaultMeaning
let id: StringStringrequiredThe memory's id, as Graine keeps it — to tell memories apart in a list.
let text: StringStringrequiredWhat is remembered, in a sentence — show it to the customer as it is.
let createdAt: Date?Date?nilWhen it was first remembered; nil when Graine did not say.
let updatedAt: Date?Date?nilWhen it last changed; nil when Graine did not say.

Its functions: init — each with its parameters, response, errors and an example.

GraineIdentitySettings (constructor)

Use it to build identity settings for previews and tests.

init(field: String = "user_id", devicesPerUser: Int = 2)

Builds a setting; a field that is not a valid trait name becomes user_id, and devicesPerUser is held to 1–5.

ParameterTypeDefaultMeaning
fieldString"user_id"The trait passed to identify that names the customer: user_id, email, phone or a custom trait such as member_id.
devicesPerUserInt2How many devices one customer's memory may be used from, 1 to 5.

Returns — a new GraineIdentitySettings.

Errors — none.

let settings = GraineIdentitySettings(field: "email", devicesPerUser: 3)

GraineIdentitySettings

Read it to learn which trait identifies the customer, and how many devices they may use.

The agent's Identity setting: which trait passed to identify names the customer, and how many devices they may use. In GraineSessionConfig.identity.

What to expect — The agent's settings from the dashboard, loaded once per launch; null / nil until then. A setting an older server does not send means its default: the user_id trait, 2 devices per user, booking off. Missing fields are empty, never a crash.

FieldTypeDefaultMeaning
let field: StringString"user_id"user_id, email, phone, or a custom trait such as member_id.
let devicesPerUser: IntInt2How many devices one customer's memory may be used from, 1 to 5.
static let standard = GraineIdentitySettings()GraineIdentitySettings—What applies when the agent's settings say nothing: user_id, two devices.

Its functions: init — each with its parameters, response, errors and an example.

Chat

The typed conversation: open it, send, follow its state, and draw it — with the SDK's own chat screen or your own.

GraineChat.open

Call it when the customer opens your chat screen, so the conversation connects.

func open(size: GraineChatSize? = nil)

Puts the customer in the chat, connecting and keeping the connection up. A mounted GraineAgentBar moves to the chat (where the agent offers one) and draws it at size until close(); nil follows GraineConfig.chatSize, then the dashboard's. Ends any in-app call and closes other agents' chats. A dropped connection is made again 1, 2, 4, 8, 15 and 15 seconds apart, then the status is offline until the customer sends something or you call retry().

ParameterTypeDefaultMeaning
sizeGraineChatSize?nilHow big the chat opens this time: compact, large or full. Left out: the app's chatSize, then the agent's Window size.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.chat.open(size: .full)

GraineChat.close

Call it when the customer leaves the chat, so it hangs up.

func close()

The customer has gone: the chat disconnects and remains disconnected. What was said stays on screen.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.chat.close()

GraineChat.send

Call it to send what the customer typed.

func send(_ text: String)

Sends a message. It is shown at once and sent now, or as soon as the chat is connected (opening and connecting it if needed). Empty text is ignored. Up to 20 messages wait while the chat is down; older ones are marked failed.

ParameterTypeDefaultMeaning
textStringrequiredWhat the customer typed. Blank text is ignored.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.chat.send("Where is my order?")

GraineChat.retry

Call it from a "Try again" control when messages could not be sent.

func retry()

Connects now, and sends what could not be sent.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.chat.retry()

GraineChat.clear

Call it to empty the chat on screen — a new topic, a tidy screen — while the conversation goes on.

func clear()

Empties the chat on screen: its turns and the typing bubble go. Only the screen. The conversation goes on — connected stays connected — a message still waiting for the connection is still sent, and nothing Graine keeps is touched: the conversation that continues comes back with the next history, and the agent's memories stay. newConversation() starts over; forgetMemory() forgets. Safe from any thread (it hops to the main thread).

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.chat.clear()   // the chat stays connected; only what is on screen goes

GraineChatView (constructor)

Use it to show the whole chat, drawn in your agent's design, in a SwiftUI app.

init(graine: GraineInApp? = nil, accent: Color? = nil, onClose: (() -> Void)? = nil)

The chat as a SwiftUI screen, drawn in the agent's design. It connects when it appears and hangs up when it goes; it draws nothing before an agent is configured.

ParameterTypeDefaultMeaning
graineGraineInApp?nilThe configured agent to draw — another agent's (GraineInApp.named("support")) or the default when left out.
accentColor?nilOverrides the design's accent colour.
onClose(() -> Void)?nilThe header's close control: called when the customer taps it — dismiss the view here. Without it no close control is drawn, and the navigation you put the view in closes it.

Returns — a new GraineChatView.

Errors — none.

.sheet(isPresented: $chatOpen) {
    GraineChatView(onClose: { chatOpen = false })
}

GraineChatViewController (constructor)

Use it to show the chat in a UIKit app.

init(graine: GraineInApp? = nil)

The chat screen for UIKit, drawn in the agent's design. Presented, it is a page sheet with a close control; pushed, the navigation bar's back button closes it.

ParameterTypeDefaultMeaning
graineGraineInApp?nilThe configured agent to draw — another agent's (GraineInApp.named("support")) or the default when left out.

Returns — a new GraineChatViewController.

Errors — none.

present(GraineChatViewController(graine: GraineInApp.named("support")), animated: true)

GraineChatRows.build

Call it to lay out a chat of your own exactly as the SDK's chat window draws it.

static func build(_ turns: [GraineChatTurn], now: Date = Date(), calendar: Calendar = .current) -> [GraineChatRow]

The rows a chat window draws for turns, in order: a bubble for each chat message (.turn); one row for a run of an earlier call's voice turns on one day (.voice — that a call happened and when, never what was said); and a day line (.day — Today, Yesterday or a date) before the first row when it is not from today, and wherever the device's day changes between rows. A conversation all from today is exactly its bubbles. GraineChatView and the bar's chat window draw these same rows. Rebuild whenever turns changes; each row's id stays the same as the conversation grows.

ParameterTypeDefaultMeaning
turns[GraineChatTurn]requiredThe conversation, oldest first — GraineChat.turns.
nowDateDate()The moment that counts as today — set it only in previews and tests.
calendarCalendar.currentHow days are counted; its time zone decides where one day ends.

Returns — [GraineChatRow] — its cases: GraineChatRow. Example value: [.day(…yesterday…), .voice(…), .day(…today…), .turn(…), .turn(…)] for an earlier call yesterday and two messages today.

Errors — none.

struct MyChat: View {
    @ObservedObject var chat = GraineInApp.shared.chat
    var body: some View {
        List(GraineChatRows.build(chat.turns)) { row in
            switch row {
            case .day(let line): Text(GraineChatRows.dayLabel(line)).font(.caption)
            case .voice(let call): Text(GraineChatRows.callLabel(call)).accessibilityLabel(GraineChatRows.callSpoken(call))
            case .turn(let turn): Text(turn.text)
            }
        }
    }
}

GraineChatRows.dayLabel

Call it for the words of a day line, in the device's language.

static func dayLabel(_ line: GraineChatDayLine, now: Date = Date(), calendar: Calendar = .current, locale: Locale = .current) -> String

"Today", "Yesterday", else the date in the device's words ("Tue 14 Oct"), with the year when it is not this year's.

ParameterTypeDefaultMeaning
lineGraineChatDayLinerequiredA day line from GraineChatRows.build.
nowDateDate()The moment that counts as now, for the year — set it only in previews and tests.
calendarCalendar.currentThe calendar and time zone the date is written in.
localeLocale.currentThe language the date is written in.

Returns — String. Example value: "Tue 14 Oct".

Errors — none.

for case .day(let line) in GraineChatRows.build(GraineInApp.shared.chat.turns) {
    print(GraineChatRows.dayLabel(line))   // "Today", "Yesterday" or "Tue 14 Oct"
}

GraineChatRows.callLabel

Call it for the words of an earlier call's row.

static func callLabel(_ line: GraineChatCallLine, calendar: Calendar = .current, locale: Locale = .current) -> String

"Voice call · 6:05 PM": the time of the call's first turn, in the device's short time.

ParameterTypeDefaultMeaning
lineGraineChatCallLinerequiredA call's row from GraineChatRows.build.
calendarCalendar.currentThe time zone the time is written in.
localeLocale.currentThe language the time is written in.

Returns — String. Example value: "Voice call · 6:05 PM".

Errors — none.

for case .voice(let call) in GraineChatRows.build(GraineInApp.shared.chat.turns) {
    print(GraineChatRows.callLabel(call))   // "Voice call · 6:05 PM"
}

GraineChatRows.callSpoken

Call it for what VoiceOver should say for an earlier call's row.

static func callSpoken(_ line: GraineChatCallLine, calendar: Calendar = .current, locale: Locale = .current) -> String

"Voice call at 6:05 PM" — the row's accessibility label.

ParameterTypeDefaultMeaning
lineGraineChatCallLinerequiredA call's row from GraineChatRows.build.
calendarCalendar.currentThe time zone the time is said in.
localeLocale.currentThe language the time is said in.

Returns — String. Example value: "Voice call at 6:05 PM".

Errors — none.

for case .voice(let call) in GraineChatRows.build(GraineInApp.shared.chat.turns) {
    print(GraineChatRows.callSpoken(call))   // "Voice call at 6:05 PM"
}

GraineChatRows.clock

Call it to write a message's time the way the chat window does.

static func clock(_ date: Date, calendar: Calendar = .current, locale: Locale = .current) -> String

A time in the device's short words — "6:05 PM", or "18:05" where the device keeps a 24-hour clock. For a message's time where the design shows timestamps (GraineChatTokens.showTimestamps).

ParameterTypeDefaultMeaning
dateDaterequiredThe moment to write, such as GraineChatTurn.at.
calendarCalendar.currentThe time zone the time is written in.
localeLocale.currentThe language the time is written in.

Returns — String. Example value: "6:05 PM".

Errors — none.

if let last = GraineInApp.shared.chat.turns.last {
    print(GraineChatRows.clock(last.at))   // "6:05 PM"
}

GraineChatRows.offersStarters

Call it to know whether the brand's starters belong above your composer.

static func offersStarters(_ turns: [GraineChatTurn]) -> Bool

True while the conversation is new: the customer has not said anything, and no earlier conversation is on screen (no restored turn). The SDK's chat window shows the brand's starters (GraineChatTokens.suggestions) only then.

ParameterTypeDefaultMeaning
turns[GraineChatTurn]requiredThe conversation on screen — GraineChat.turns.

Returns — Bool. Example value: true in a new chat.

Errors — none.

if GraineChatRows.offersStarters(GraineInApp.shared.chat.turns),
   let starters = GraineInApp.shared.theme?.chat?.suggestions {
    print(starters)   // offer them above your composer
}

GraineChatRows.offersNewConversation

Call it to know whether your own "New conversation" control belongs on screen.

static func offersNewConversation(_ turns: [GraineChatTurn]) -> Bool

True while an earlier conversation is on screen — a restored turn, when the customer came back and the conversation continues. GraineChatView's header offers New conversation only then (and only where the design keeps it, GraineChatTokens.newConversation).

ParameterTypeDefaultMeaning
turns[GraineChatTurn]requiredThe conversation on screen — GraineChat.turns.

Returns — Bool. Example value: false in a new chat.

Errors — none.

let offerFreshStart = GraineChatRows.offersNewConversation(GraineInApp.shared.chat.turns)
// show your own "New conversation" control while it is true; its tap calls chat.newConversation()

GraineChat

Use it to drive and draw the typed conversation: GraineInApp.shared.chat.

The typed conversation with an agent, on its own connection: GraineInApp.shared.chat, an ObservableObject to read on the main thread (its methods may be called from any thread and hop there). One conversation runs at a time across every agent: opening the chat ends any in-app call, starting a call closes it, a native call holds it down until it is over, and with endOnBackground it hangs up in the background and comes back with the app.

What to expect — The agent's typed conversation. It connects while the customer is in the chat (open) and hangs up when they leave (close); a message typed while it is not connected is shown at once and sent when it is. Its state changes as the conversation moves: draw from it. Opening the chat ends a call the bar holds.

PropertyTypeMeaning
@Published private(set) var turns: [GraineChatTurn] = [][GraineChatTurn]What has been said, oldest first: the customer's messages and the agent's replies, one bubble each (the last 200). Kept while the app runs.
@Published private(set) var status: GraineChatStatus = .idleGraineChatStatusWhere the connection is — draw a connecting or offline state from it.
@Published private(set) var typing = falseBoolThe agent is working on a reply: from the customer's message until its first words, or 30 seconds.
@Published private(set) var upcomingCalls: [GraineScheduledCall] = [][GraineScheduledCall]This customer's calls still to come, soonest first, at most three — the chat window's "Upcoming calls" card. Fetched when the chat opens and after the agent finishes a reply (at most every 10 seconds), only where Graine rings this app.
var online: Bool { get }BoolConnected: a message goes now.
var awaitingFirstMessage: Bool { get }BoolTrue when the customer has not said anything in this conversation and it does not continue an earlier one (no turn was restored); that is when the brand's starters are offered.
private(set) var isOpen = falseBoolThe customer is in the chat: open() was called and close() was not.
@Published private(set) var openedSize: GraineChatSize?GraineChatSize?The size the current open asked for (open(size:)), until close(); nil follows GraineConfig.chatSize, then the dashboard's. Moving to the call and back keeps it.

Its functions: open, close, send, retry, clear, newConversation, refreshUpcomingCalls, cancelUpcomingCall — each with its parameters, response, errors and an example. To draw the conversation in a chat of your own, GraineChatRows lays turns out the way the SDK's chat window does.

GraineChatSize

Use it to choose how big the chat window opens.

public enum GraineChatSize: String, CaseIterable, Sendable — how big the chat window opens. The dashboard's Window size chooses; GraineConfig.chatSize overrides it for the app, and chat.open(size:) for one open. Raw values are the design's words.

What to expect — Which size wins: the size passed to the open call, then the app's chatSize, then the agent's Window size in the Garage, else compact. An open's size lasts until the chat closes. On a phone, large is a sheet over about 92% of the screen and full the whole screen inside the safe areas; on a tablet or computer both open a panel 420 wide and up to 760 tall. A word from a newer design that the SDK does not know is passed over.

CaseMeaning
case compactThe usual window above the bar, unchanged from earlier releases.
case largeOn a phone, a sheet covering most of the screen; on an iPad, a panel in the corner.
case fullThe whole screen.

GraineChatTurn

Read it to draw one message of the chat.

One message in GraineChat.turns, made by the SDK. Each agent reply is its own turn.

What to expect — One per message, oldest first. The agent's turn updates in place (same id) while live, then stops. A customer's message typed while the chat is not connected is sending (React Native: pending) and goes as soon as it is; failed means every reconnect was spent — the next message or a retry sends it. Restored turns arrive once when the chat opens, for an identified customer on an agent with memory on, on one of their devices: at most 50, oldest first, delivered, never live, with their original times; the welcome is not sent then. Consecutive voice turns show as one "Voice call · time" row (GraineChatRows lays a conversation out that way for a chat of your own). The list empties on sign-out, when another customer signs in, after a successful forgetMemory(), on newConversation() and on clear() (the screen only: the conversation goes on).

FieldTypeDefaultMeaning
let id: StringString—Stays the same for as long as the transcript lasts: t1, t2, …
let role: GraineRoleGraineRole—Who said it: .agent or .customer — which side to draw the bubble on.
internal(set) var text: StringString—What was said; an agent's words are appended as they stream in.
internal(set) var live: BoolBool—The agent's words are still arriving: draw them as provisional.
internal(set) var delivery: DeliveryGraineChatTurn.Delivery—Where a customer's message is. An agent's turn is always sent.
let at: DateDate—When it was said — for an agent's turn, its first words.
let proactive: BoolBool—The agent spoke unprompted — this turn did not come after one of the customer's.
let restored: BoolBoolfalseCarried over from an earlier conversation and brought back when the chat opened (the customer returned): it counts as delivered, carries the time it was said and Graine's id, and was not said just now.
let channel: GraineChatChannelGraineChatChannel.chatWhere it was said. In a chat window, consecutive .voice turns appear as a single "Voice call" row, and their words are never shown.
enum Delivery: String, Sendablenested type—See GraineChatTurn.Delivery.

GraineChatTurn.Delivery

Read it to show whether a customer's message is sending, sent or failed.

Where a customer's chat message is. Raw values are the wire words.

What to expect — Only on the customer's messages (the agent's are sent, or null): sending → sent; failed once every reconnect was spent.

CaseMeaning
case sendingTyped while the chat was disconnected; it waits for the connection and goes out once there is one.
case sentOn its way to the agent.
case failedThe chat could not reconnect to send it. GraineChat.retry() tries again.

GraineChatChannel

Read it to tell a typed message from one said on a call.

public enum GraineChatChannel: String, Sendable — where a message was said: typed in the chat, or spoken on a call (only an earlier conversation's turn is).

What to expect — chat for a typed message; voice for a message said on a call — only a restored turn can be voice.

CaseMeaning
case chatTyped in the chat.
case voiceSaid on a call with the agent.

GraineChatStatus

Read it to show whether the chat is connected, and offer a retry when it is not.

Where the chat's connection is: GraineChat.status.

What to expect — Idle until the customer is in the chat; connecting, then online. A dropped connection is retried six times — 1, 2, 4, 8, 15 and 15 seconds apart — showing reconnecting with the attempt; then offline until the customer sends something or the app retries. Opening the chat ends a call the bar holds; a native call holds the chat until it is over.

FieldTypeDefaultMeaning
var isConnecting: Bool { get }Bool—Connecting or reconnecting: draw "Connecting…".
CaseMeaning
case idleThe customer is not in the chat — it was never opened, they left, or the app is in the background.
case connectingOpening.
case onlineConnected.
case reconnecting(attempt: Int)The connection was lost and is being re-established; this is try number attempt out of six.
case offlineReconnecting failed, or the other side ended the conversation. A message, or retry(), connects again.

GraineRole

Read it to tell the agent's lines from the customer's.

Who said a line, in captions and chat turns. Raw values are the wire words.

What to expect — Who said it: the agent, or the customer (user).

CaseMeaning
case agentThe agent.
case customerThe customer.

GraineChatView

Use it as the SwiftUI chat screen.

The chat as a SwiftUI screen, drawn by the SDK in the brand's design: header, one bubble per reply, a typing bubble, the components the agent draws (answered in place) and the composer. It connects when it appears, hangs up when it goes, and draws nothing before an agent is configured.

What to expect — Push it or present it like any SwiftUI view; it fills the space it is given. The header offers New conversation while an earlier conversation is on screen, and the Upcoming calls card (with Book a call where bookingAvailable is true) when the design keeps it.

PropertyTypeMeaning
var body: some Viewsome ViewSwiftUI's view body; apps do not call it.

Its functions: init — each with its parameters, response, errors and an example.

GraineChatViewController

Use it as the UIKit chat screen.

The chat screen for a UIKit app, to present or push: present(GraineChatViewController(), animated: true). Presented, its close control dismisses it; pushed, the navigation bar's back button does and no close control is drawn.

What to expect — The same screen as GraineChatView, hosted for UIKit; it presents as a page sheet unless you change modalPresentationStyle.

Its functions: init, viewDidLoad — each with its parameters, response, errors and an example.

GraineChatRows

Use it to draw a chat of your own in the same rows as the SDK's chat window.

public enum GraineChatRows — static functions only: the rows a chat window draws for a conversation, and the words for them. What GraineChatView and the bar's chat window draw, public for an app that draws its own chat; every Graine SDK lays a conversation out in these same rows.

What to expect — Worked out on the device from the turns you pass; nothing is sent. "Today", "Yesterday" and "Voice call" are the SDK's words; dates and times are written in the device's language and time zone.

Its functions: build, dayLabel, callLabel, callSpoken, clock, offersStarters, offersNewConversation — each with its parameters, response, errors and an example.

GraineChatRow

Read it to draw one row of a chat window.

public enum GraineChatRow: Equatable, Identifiable — one row, as GraineChatRows.build(_:now:calendar:) returns them, oldest first.

What to expect — A day line before the first row when it is not from today, and wherever the day changes; one row per earlier call on a day, however many turns it had; one row per chat message. A conversation all from today has no day line.

FieldTypeDefaultMeaning
var id: String { get }String—Stable as the conversation grows — a day line by its date (day-2026-10-14), a call by its first turn (voice-t3), a message by its turn's id — for List and ForEach.
CaseMeaning
case day(GraineChatDayLine)A day line: draw GraineChatRows.dayLabel(_:).
case voice(GraineChatCallLine)An earlier call, as one row: draw GraineChatRows.callLabel(_:) — never what was said on it.
case turn(GraineChatTurn)A message: one bubble.

GraineChatDayLine

Read it to draw a day line.

public struct GraineChatDayLine: Equatable, Sendable — the day a run of rows belongs to. Made by GraineChatRows.build; an app reads it, never makes one.

What to expect — Before the first row when it is not from today, and wherever the day changes between rows; a conversation all from today has none.

FieldTypeDefaultMeaning
let day: GraineChatDayGraineChatDay—Which day: .today, .yesterday or .date.
let date: StringString—The device's date, YYYY-MM-DD.
let at: DateDate—When the day's first message was said.
var key: String { get }String—"today", "yesterday", or the date — the same words on every platform.

GraineChatDay

Read it to know which day a day line names.

public enum GraineChatDay: Equatable, Sendable — in GraineChatDayLine.day.

What to expect — Counted on the device's calendar and time zone when the rows were built: rebuilt after midnight, what was .today is .yesterday.

CaseMeaning
case todayThe device's today.
case yesterdayThe day before today.
case dateAn earlier day: GraineChatDayLine.date says which.

GraineChatCallLine

Read it to draw an earlier call as one row.

public struct GraineChatCallLine: Equatable, Sendable — an earlier call: when it began, and which turns it stands for — never their words. Made by GraineChatRows.build.

What to expect — Voice turns come only from an earlier conversation the chat restored; a run of them on one day is one call.

FieldTypeDefaultMeaning
let id: StringString—The row's id: voice- and the call's first turn's id.
let at: DateDate—When the call's first turn was said.
let first: IntInt—Where the call's first turn is in turns.
let last: IntInt—Where its last turn is in turns.

The voice bar

The agent talking inside your app: the bar that draws the call, the launcher, the voice engine behind them, the loader, the captions, how the audio travels and how good the connection is.

GraineAgentBar (constructor)

Use it to put the agent in your app: mount the bar once, at your root.

init(graine: GraineInApp? = nil, name: String? = nil, accent: Color? = nil, bottomInset: CGFloat = 16, draggable: Bool = true, initialOffset: CGFloat = 0, onPlacementChange: ((CGFloat) -> Void)? = nil, autoAttach: Bool = true, surface: GraineSurface? = nil, onClose: (() -> Void)? = nil)

The assistant bar, drawn in the agent's design: it fills the area it is given, draws only the bar, lets other touches through and rises above the keyboard. Nothing draws before an agent is configured.

ParameterTypeDefaultMeaning
graineGraineInApp?nilThe configured agent to draw — another agent's (GraineInApp.named("support")) or the default when left out.
nameString?nilThe name shown beside the status, and what VoiceOver calls the agent; left out, the dashboard's heading.
accentColor?nilOverrides the design's and the dashboard's accent colour.
bottomInsetCGFloat16Points above the bottom of the area it fills — lift it clear of your tab bar.
draggableBooltrueWhether the customer can drag it up and down; a tap still does what a tap did.
initialOffsetCGFloat0A position you saved from onPlacementChange, in points above the anchor.
onPlacementChange((CGFloat) -> Void)?nilCalled with the new offset when the customer lets go somewhere new — save it here; the SDK stores nothing.
autoAttachBooltrueMount the voice engine in the key window when the bar appears, if nothing has yet.
surfaceGraineSurface?nilWhich conversation opens, .voice or .chat, among those the dashboard offers; it can never add one the dashboard withheld. nil is the dashboard's choice.
onClose(() -> Void)?nilThe customer hung up, or closed the bar with no call to end — called 400 ms later, after the goodbye has left the speaker. If your app shows the bar only on demand, remove it here.

Returns — a new GraineAgentBar (GraineAgentBar<EmptyView>; accessory makes one with content above it).

Errors — none.

AppContent()
    .overlay(GraineAgentBar(bottomInset: 64,
                            initialOffset: savedOffset,
                            onPlacementChange: { savedOffset = $0 }))

GraineAgentBar.accessory

Use it to ride a view of your own, such as a text field, above the bar.

func accessory<Content: View>(@ViewBuilder _ content: () -> Content) -> GraineAgentBar<Content>

Something that rides above the bar and moves with it, above the keyboard included — a text field, say.

ParameterTypeDefaultMeaning
content() -> ContentrequiredA view builder for what rides above the bar; it is drawn in your own style and keeps its own state.

Returns — GraineAgentBar<Content>: the same bar, with the content above it. Mount that instead of the plain bar.

Errors — none.

AppContent()
    .overlay(GraineAgentBar().accessory {
        TextField("Ask about your order", text: $draft)
            .textFieldStyle(.roundedBorder)
    })

GraineLauncher (constructor)

Use it for a floating button that starts the in-app call, when you do not mount the bar.

init(graine: GraineInApp? = nil, label: String? = nil, accent: Color? = nil, autoAttach: Bool = true, action: (() -> Void)? = nil)

The floating launcher in the agent's design: a tap places the in-app call, and during the call it is the end button. Nothing draws before an agent is configured.

ParameterTypeDefaultMeaning
graineGraineInApp?nilThe configured agent to draw — another agent's (GraineInApp.named("support")) or the default when left out.
labelString?nilA pill launcher's words, and what VoiceOver says; left out, the dashboard's heading.
accentColor?nilOverrides the design's and the dashboard's accent colour.
autoAttachBooltrueMount the voice engine in the key window when the button appears, so a tap is only a call away.
action(() -> Void)?nilWhat a tap does instead of placing the in-app call — open your own screen, say. With it, the button only draws and is never the end button.

Returns — a new GraineLauncher.

Errors — none.

AppContent()
    .overlay(alignment: .bottomTrailing) { GraineLauncher().padding(20) }

GraineVoice.attach

Call it to mount the voice engine in a window you choose.

func attach(to view: UIView)

Mounts the engine in that view — pass your key window, not a view a screen owns — and loads the page idle: nothing registered, nothing dialled, no microphone, so a later start() connects quickly. Attaching to a different view moves the engine there and ends a call that is under way.

ParameterTypeDefaultMeaning
to viewUIViewrequiredWhere the engine lives: your key window, not a view a screen owns — a call ends when the view it is in goes.

Returns — nothing; it returns when done.

Errors — none.

if let window = UIApplication.shared.connectedScenes
    .compactMap({ ($0 as? UIWindowScene)?.keyWindow }).first {
    GraineInApp.shared.voice.attach(to: window)
}

GraineVoice.attach

Call it to mount the voice engine in the key window when you do not use the bar.

func attach()

Mounts the engine in the app's key window. Call it once a window exists — from a view's onAppear, say; with no window yet it reports through onError and does nothing.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.voice.attach()

GraineVoice.detach

Call it to remove the voice engine, hanging up any call first.

func detach()

Removes the engine, hanging up first: a call never outlives its UI.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.voice.detach()

GraineVoice.start

Call it from your own control to start the in-app voice call.

func start()

Places the call. Asks for the microphone first if the app does not hold it; refused, no call is placed and onMicDenied is called. A start before the page is ready is held and placed when it is. Ends any other conversation — another agent's call, every chat. Refused through onError when the engine is not attached or a native call (calls) is live.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.voice.start()

GraineVoice.end

Call it from your own control to hang up the in-app call.

func end()

Hangs up. If the page does not confirm within Timing.endWatchdogMs (4 s), the call is ended locally — state, onEnded, and a reload that drops the audio. If a start was only on hold, it is just cancelled.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.voice.end()

GraineVoice.setMuted

Call it from your own mute control during the in-app call.

func setMuted(_ value: Bool)

Stops (or resumes) sending the customer's voice without ending the call.

ParameterTypeDefaultMeaning
valueBoolrequiredtrue to stop sending the customer's voice; false to send it again.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.voice.setMuted(true)

GraineVoice.retry

Call it from a "Try again" control after the engine page failed to load.

func retry()

Reloads the engine page and starts its reloads over (GraineConfig.Timing.retryDelaysMs) — for a "Try again" button once connectivity is failed.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.voice.retry()

GraineVoice.setCaptionsOn

Call it from your own captions (CC) button to show or hide the line being said.

func setCaptionsOn(_ on: Bool)

The captions switch, captionsOn. While it is off, GraineAgentBar does not show the line being said; on — the default — it does. The call goes on either way, and what is said is still kept in captions (clearCaptions() forgets it). The switch is this agent's and lasts while the app runs: it is on again at the next launch, so set it at launch to keep the customer's choice. A UI of your own decides from captionsOn. Safe from any thread (it hops to the main thread).

ParameterTypeDefaultMeaning
onBoolrequiredtrue to show the line being said; false to hide it.

Returns — nothing; it returns when done.

Errors — none.

let voice = GraineInApp.shared.voice
voice.setCaptionsOn(!voice.captionsOn)   // your CC button

GraineVoice.clearCaptions

Call it to forget what has been said on the in-app call without ending it.

func clearCaptions()

Empties captions — what has been said this call — and the call goes on; the next line said starts the list again. A call starting clears them too. The captions switch is untouched. Safe from any thread (it hops to the main thread).

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.voice.clearCaptions()

GraineVoice.viewOpened

Call it when a voice UI of your own comes on screen, so a slow connect is reported while the customer is watching.

func viewOpened()

A UI of your own that shows the voice came on screen — GraineAgentBar says so by itself. While one is open, an engine page still loading is the customer's wait: after GraineConfig.Timing.slowConnectMs (4 seconds) slowConnect turns true, for your "Still connecting…". Call it in pairs with viewClosed() — from onAppear and onDisappear, or viewDidAppear(_:) and viewDidDisappear(_:). Safe from any thread (it hops to the main thread).

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

struct MyVoiceScreen: View {
    @ObservedObject var voice = GraineInApp.shared.voice
    var body: some View {
        Text(voice.slowConnect ? "Still connecting…" : "Tap to talk")
            .onAppear { voice.viewOpened() }
            .onDisappear { voice.viewClosed() }
    }
}

GraineVoice.viewClosed

Call it when the voice UI you reported with viewOpened() leaves the screen.

func viewClosed()

The UI from viewOpened() left the screen. Once no UI that shows the voice is open — yours or the bar — and no call is being placed, a page still loading is no longer anyone's wait, and slowConnect goes back to false. Safe from any thread (it hops to the main thread).

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.voice.viewClosed()   // paired with viewOpened(), as the screen goes

GraineLoader (constructor)

Use it to show waiting the way the agent's design does.

init(moment: String? = nil, label: String? = nil, size: CGFloat? = nil, stacked: Bool = false, graine: GraineInApp? = nil)

The dashboard's loader, with its words for the moment you name. Pair moment: "thinking" with GraineVoice.phase == .thinking.

ParameterTypeDefaultMeaning
momentString?nilWhich waiting moment's words to show — "connecting", "reconnecting", "thinking" or "component"; left out, no words, only the mark.
labelString?nilWords to show instead of the moment's words from the dashboard.
sizeCGFloat?nilThe mark's size in points, over the dashboard's (14, 20 or 28 for small, medium and large).
stackedBoolfalseDraw the words under the mark instead of beside it.
graineGraineInApp?nilThe configured agent to draw — another agent's (GraineInApp.named("support")) or the default when left out.

Returns — a new GraineLoader.

Errors — none.

if GraineInApp.shared.voice.phase == .thinking {
    GraineLoader(moment: "thinking")
}

GraineLoaderText (constructor)

Use it to build loader words for previews and tests.

init(connecting: String? = nil, reconnecting: String? = nil, thinking: String? = nil, component: String? = nil)

Builds a set of words for the waiting moments — for previews and tests; the SDK reads the real ones from the dashboard (GraineLoaderLook.text).

ParameterTypeDefaultMeaning
connectingString?nilShown while the page and the call are being set up.
reconnectingString?nilShown while the page is reloaded after a failure, or the call is re-established.
thinkingString?nilShown between the customer finishing and the agent starting to answer.
componentString?nilShown while a component's content is loading.

Returns — a new GraineLoaderText.

Errors — none.

let words = GraineLoaderText(connecting: "Connecting…", thinking: "One moment…")

GraineLoaderText.forMoment

Call it to get the words for one waiting moment.

func forMoment(_ moment: String) -> String?

The words for "connecting", "reconnecting", "thinking" or "component"; nil for anything else.

ParameterTypeDefaultMeaning
momentStringrequiredThe waiting moment: "connecting", "reconnecting", "thinking" or "component".

Returns — String?: that moment's words, or nil for any other word or when none were set. Example value: "Thinking…".

Errors — none.

let words = GraineLoaderText(connecting: "Connecting…", thinking: "One moment…")
let line = words.forMoment("thinking")       // "One moment…"

GraineCaption (constructor)

Use it to build caption lines for previews and tests of your own call UI.

init(role: GraineRole, text: String, live: Bool)

Builds a caption line by hand — for previews and tests; the SDK makes the real ones.

ParameterTypeDefaultMeaning
roleGraineRolerequiredWho said it: the agent or the customer.
textStringrequiredThe words of the line.
liveBoolrequiredThe words are still arriving.

Returns — a new GraineCaption.

Errors — none.

let line = GraineCaption(role: .agent, text: "Your order ships today.", live: false)

GraineTransport.from

Call it to turn a transport's wire word into a GraineTransport.

static func from(_ s: String?) -> GraineTransport

.webrtc for "rtc"; anything else is .websocket, the transport that always works.

ParameterTypeDefaultMeaning
sString?requiredA transport's wire word: "rtc" or "ws"; anything else, or nil, is the WebSocket.

Returns — GraineTransport. Example value: .websocket.

Errors — none.

let transport = GraineTransport.from("rtc")   // .webrtc

GraineConnectionQuality.classify

Call it to grade one connection measurement the way the SDK does.

static func classify(rttMs: Double?, jitterMs: Double?, loss: Double?) -> GraineConnectionQuality

The word for one measurement (loss as a fraction, 0.08 = 8%); the worst value wins, and unknown when all three are missing.

ParameterTypeDefaultMeaning
rttMsDouble?requiredThe round-trip time, in ms.
jitterMsDouble?requiredThe jitter, in ms.
lossDouble?requiredThe packet loss, from 0 to 1.

Returns — GraineConnectionQuality. Example value: .excellent.

Errors — none.

let quality = GraineConnectionQuality.classify(rttMs: 120, jitterMs: 12, loss: 0.01)   // .excellent

GraineConnectionQuality.classify

Call it to grade a measurement given as a map.

static func classify(sample: [String: Any]?) -> GraineConnectionQuality

The same, from a sample map {rtt, jitter, loss}; a missing, negative or non-numeric value is absent, not zero. Used by the SDK; apps rarely need it.

ParameterTypeDefaultMeaning
sample[String: Any]?requiredOne measurement as a map: rtt and jitter in milliseconds, loss as a fraction.

Returns — GraineConnectionQuality. Example value: .excellent.

Errors — none.

let quality = GraineConnectionQuality.classify(sample: ["rtt": 420, "jitter": 20, "loss": 0.01])   // .poor

GraineAgentBar

Use it as the assistant bar: the agent's voice, chat and components in one view.

What the customer sees during a conversation, drawn by the SDK in the brand's design: avatar, name and a status line that names the network's state ("Weak connection", "No connection", "Reconnecting…"), microphone, hang-up, the line being said (while GraineVoice.captionsOn), the components the agent draws, and the chat — with a switch when the dashboard offers both. A SwiftUI view: mount it once at your root as an overlay, not inside a screen; it draws only the bar, lets other touches through, can be dragged up and down, and rises above the keyboard.

What to expect — Place it in your UI; it draws in the agent's design and follows the conversation by itself.

PropertyTypeMeaning
var body: some Viewsome ViewSwiftUI's view body; apps do not call it.

Its functions: init, accessory — each with its parameters, response, errors and an example.

GraineSurface

Use it to choose whether the bar opens the call or the chat.

Which conversation: a call or a chat, never both at once. Passed to GraineAgentBar(surface:).

What to expect — Only a choice among what the dashboard offers (What visitors get): asking for a conversation the agent does not offer changes nothing, and nil follows the dashboard's choice of which opens first.

CaseMeaning
case voiceThe call.
case chatThe chat.

GraineLauncher

Use it as a ready-made launcher button.

The floating launcher button in the brand's design (56 points high; a pill is as wide as its label): a tap places the in-app call, mounting the engine first if needed, and during the call it is the end button. A SwiftUI view that draws only the button — place it yourself, for example .overlay(alignment: .bottomTrailing) { GraineLauncher().padding(20) }.

What to expect — Draws nothing until an agent is configured, then follows the agent's design (shape, glyph or logo, colours) as soon as the settings load; during a call it turns into the end button.

PropertyTypeMeaning
var body: some Viewsome ViewSwiftUI's view body; apps do not call it.

Its functions: init — each with its parameters, response, errors and an example.

GraineVoice

Use it to drive and draw the in-app voice call yourself: GraineInApp.shared.voice.

The in-app voice call: a page Graine hosts, in a web view the SDK owns (one invisible point big), so audio fixes reach your app without a release — reached as GraineInApp.shared.voice and driven for you by GraineAgentBar. An ObservableObject; every method belongs on the main thread, and one called from elsewhere hops there.

What to expect — The assistant bar's voice call. The bar starts and ends it by itself; these are for an app that draws its own controls. One conversation at a time: starting it closes an open chat.

PropertyTypeMeaning
@Published private(set) var connected = falseBoolA call is up — draw your hang-up control from it.
@Published private(set) var connecting = falseBoolA call is being placed or re-established.
@Published private(set) var muted = falseBoolThe customer's voice is not being sent.
@Published private(set) var ready = falseBoolThe engine page is loaded and ready: a start now connects within moments.
@Published private(set) var quality: GraineConnectionQuality = .unknownGraineConnectionQualityThe connection in one word — show a weak-connection hint from it.
@Published private(set) var online: Bool? = nilBool?The engine page's view of the network; nil until it has said.
@Published private(set) var reconnecting = falseBoolThe call was cut off by the network and is being brought back. Not an end.
@Published private(set) var connectivity: GraineConnectivity = .loadingGraineConnectivityWhether the engine page is there: loading, ready, retrying or failed.
@Published private(set) var captions: [GraineCaption] = [][GraineCaption]What has been said this call, oldest first. A live line is replaced as its words arrive, not appended; at most 12 are kept; cleared when a call starts, and by clearCaptions().
@Published private(set) var captionsOn = trueBoolThe captions switch (CC): GraineAgentBar shows the line being said only while it is true. On by default; setCaptionsOn(_:) changes it. What is said is kept in captions either way.
@Published private(set) var connectStage: GraineConnectStage? = nilGraineConnectStage?Where a call being placed is: registering, dialling, retrying or switching. Nil outside a connect.
@Published private(set) var slowConnect = falseBoolThe customer has waited on this connect for Timing.slowConnectMs (4 s by default) — a bar open with the page not there yet, or a call they asked for not yet answered. Say so ("Still connecting…"). It goes back to false as soon as the connect goes through or they stop waiting.
@Published private(set) var callFailed = falseBoolThe last start could not be connected on either transport. Not a setting to fix — the network may be better in a moment, so start() simply tries again. Any start, answered call, end() or detach() resets it.
var isAttached: Bool { get }BoolWhether the engine is mounted.
var phase: GrainePhase { get }GrainePhaseWhat the call is doing in one word — idle, connecting, reconnecting, thinking, speaking or listening — for an app that draws its own bar. Draw GraineLoader(moment: "thinking") for thinking.

Its functions: attach, detach, start, end, setMuted, setCaptionsOn, clearCaptions, viewOpened, viewClosed, answerWidget, reportEvent, retry — each with its parameters, response, errors and an example.

GrainePhase

Read it to draw what the in-app call is doing, in one word.

What the in-app call is doing, in one word, for an app that draws its own bar: GraineVoice.phase. Raw values are the wire words.

What to expect — Worked out from the call as it moves, and published with GraineVoice's other state: idle outside a call; thinking is the moment to show a loader.

CaseMeaning
case idleNo call; ready to start.
case connectingThe page loading, or a call being placed.
case reconnectingEither the page failed and is loading again, or the call is being brought back.
case thinkingThe customer finished a sentence and the agent has not answered yet — draw GraineLoader(moment: "thinking").
case speakingThe agent is talking.
case listeningThe agent is waiting for the customer.

GraineConnectStage

Read it to tell the customer where a call being placed is.

Where a call being placed is: GraineVoice.connectStage. Raw values are the wire words.

What to expect — How the connection is doing, reported as it changes. Draw it so the customer can tell when the network is the problem.

CaseMeaning
case registeringA line is being set up (a cold start).
case diallingThe call is on its way to the agent.
case retryingThe first attempt went unanswered and a fresh connection is being made — on mobile data, usually one the phone dropped while idle. Say "Reconnecting…".
case switchingThe call is moving to the other transport.

GraineConnectProgress

Read it only if you handle the engine page's progress reports yourself.

One progress report from the engine page: a stage, or failed. The SDK turns it into GraineVoice.connectStage and callFailed.

What to expect — How the connection is doing, reported as it changes. Draw it so the customer can tell when the network is the problem.

CaseMeaning
case stage(GraineConnectStage)The connect reached this stage.
case failedNothing could carry the call on either transport; start() tries again.

GraineConnectivity

Read it to know whether the engine page is there, and offer a retry when it failed.

Whether the engine page that carries the in-app call is there: GraineVoice.connectivity and onConnectivity. Raw values are the wire words.

What to expect — How the connection is doing, reported as it changes. Draw it so the customer can tell when the network is the problem.

CaseMeaning
case loadingFirst load in progress.
case readyThe page is ready.
case retryingA load failed or never became ready; a reload is scheduled.
case failedEvery reload used up — say so, and offer GraineVoice.retry().

GraineLoader

Use it as the agent's loader.

How waiting looks, drawn from the dashboard's Appearance → loader: dots, pulse, ring, bars or the brand's logo breathing, at the configured size, speed and colour, with the dashboard's words for the moment. A SwiftUI view; nothing animates while it is off screen.

What to expect — Draws nothing until an agent is configured; until the agent's settings load it draws the default dots in the default accent, then the dashboard's loader.

PropertyTypeMeaning
var body: some Viewsome ViewSwiftUI's view body; apps do not call it.

Its functions: init — each with its parameters, response, errors and an example.

GraineLoaderText

Read it to get the dashboard's words for each waiting moment.

The words for each waiting moment, as carried in GraineLoaderLook.text. A value type.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

FieldTypeDefaultMeaning
var connecting: String?String?nilWhile the page and the call are being set up.
var reconnecting: String?String?nilWhile the page is being reloaded after a failure, or the call re-established.
var thinking: String?String?nilBetween the customer finishing and the agent starting.
var component: String?String?nilWhile a component's content is loading.

Its functions: init, forMoment — each with its parameters, response, errors and an example.

GraineLoaderLook

Read it only to draw a loader of your own that matches the dashboard's.

The loader in effect, as values a renderer can use. Used by the SDK's own views; apps do not need it.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

FieldTypeDefaultMeaning
let kind: StringString"dots"One of "dots", "pulse", "ring", "bars" or "mark"; the last is the brand's logo, breathing.
let color: String?String?nil#RRGGBB, or nil for the accent.
let px: DoubleDouble20The mark's size in points: 14, 20 or 28 for small, medium and large.
let periodMs: DoubleDouble1_100One animation cycle: 1,600, 1,100 or 700 ms for calm, normal and quick.
let text: GraineLoaderTextGraineLoaderText"Connecting…", "Reconnecting…", "Thinking…", "Loading…"Every moment's words filled in; the dashboard's own words replace these (at most 40 characters each).
let skeleton: BoolBoolfalseWhen true, the outline of the component that is about to appear is drawn under the loader as well.

GraineCaption

Read it to draw a line being said.

One caption line: in GraineVoice.captions, GraineCalls.caption and GraineCalls.transcript. A value type.

What to expect — The line being said right now, by either side; live while it is still being recognised or spoken. Replaced as the conversation moves on: draw the latest. The captions switch (GraineVoice.captionsOn) hides the bar's line, never the list: captions keeps what is said either way.

FieldTypeDefaultMeaning
let role: GraineRoleGraineRole—Who said it: the agent or the customer.
let text: StringString—The words of the line so far.
let live: BoolBool—Still being recognised or spoken — draw it as provisional.

Its functions: init — each with its parameters, response, errors and an example.

GraineTransport

Read it to know how the in-app call's audio travels, or set GraineConfig.transport to force one.

How the in-app call's audio travels; raw values are the wire words. Set GraineConfig.transport only to force one, and hear what was used through onTransport. It applies to the voice bar's in-app call (GraineVoice) only: native calls (GraineCalls) carry their audio in your app's own process and choose their road with GraineCallsConfig.callTransport (GraineCallTransport).

What to expect — The SDK sends and reads these for you; they are public so a custom UI can reuse them.

FieldTypeDefaultMeaning
var wire: String { get }String—The wire word: ws or rtc.
CaseMeaning
case websocket = "ws"Audio on the conversation's WebSocket. It works on any network, though the audio suffers wherever the network loses data.
case webrtc = "rtc"WebRTC, with a jitter buffer and the device's echo canceller. Falls back to .websocket.

Its functions: from — each with its parameters, response, errors and an example.

GraineConnectionQuality

Read it to show the customer when the network is the problem.

The connection in one word, with the same thresholds on every platform: GraineVoice.quality and onQuality. Raw values are the wire words.

What to expect — How the connection is doing, reported as it changes. Draw it so the customer can tell when the network is the problem.

CaseMeaning
case excellentUnder 150 ms round trip, 30 ms jitter and 2% loss.
case goodAt or past one of those, but under 400 ms, 80 ms and 8%.
case poor400 ms round trip, 80 ms jitter or 8% loss, or worse — any one is enough. The bar says "Weak connection".
case lostThe network is gone.
case unknownNothing measured yet, or outside a call.

Its functions: classify — each with its parameters, response, errors and an example.

Several agents in one app

More than one agent in the same app — a support agent in Help, a sales agent on Plans — each with its own publishable key, design, chat, voice bar, actions and requests. Who the customer is, the screen and the phone's calls belong to the app and are shared. Configure each with GraineInApp.configure(_:name:), and hand one to a view with its graine: parameter. One conversation runs at a time across all of them.

GraineInApp.named

Call it to reach an agent you configured under a name.

static func named(_ name: String) -> GraineInApp?

The agent configured under name (trimmed; an empty name is the default agent), or nil when no agent has that name.

ParameterTypeDefaultMeaning
nameStringrequiredThe name the agent was configured under with configure(_:name:).

Returns — GraineInApp?: the agent, or nil. Example value: nil before configure(_:name: "support").

Errors — none.

GraineInApp.named("support")?.chat.open()

GraineInApp.all

Read it to go through every agent your app configured — to sign them all in or out, say.

static var all: [String: GraineInApp] { get }

Every configured agent, keyed by the name it was configured under; the default agent is under "default". Empty before any agent is configured.

Parameters — none.

Returns — [String: GraineInApp]. Example value: ["default": <GraineInApp>, "support": <GraineInApp>].

Errors — none.

for (name, agent) in GraineInApp.all {
    print(name, agent.session?.agentId ?? "loading")
}

GraineInApp.current

Read it when your code may run before any agent is configured — it never stops the app.

static var current: GraineInApp? { get }

The default agent — or, when every agent was given a name, the first one configured. Nil before any agent is configured. The SDK's views draw this agent unless you hand them another.

Parameters — none.

Returns — GraineInApp?. Example value: nil before configure.

Errors — none.

guard let graine = GraineInApp.current else { return }   // not configured yet
graine.chat.open()

GraineInApp.shared

Use it to reach the default agent from anywhere once it is configured.

static var shared: GraineInApp { get }

The same agent as current. If no agent has been configured, it stops the app with a message saying to call configure first — so read it only after configure has run (current is the safe form).

Parameters — none.

Returns — GraineInApp.

Errors — none thrown; it stops the app when nothing is configured.

GraineInApp.shared.identify(["user_id": user.id])

GraineInApp.name

Read it to tell which agent an instance is — in a listener shared by several agents, say.

let name: String

The name this agent was configured under (trimmed); "default" for the default agent. The same name GraineCalls.agent, GraineMissedCall.agent and the bridges' agent fields carry.

Parameters — none.

Returns — String. Example value: "support".

Errors — none.

let support = GraineInApp.configure(GraineConfig(baseUrl: "https://www.graine.ai", publishableKey: "pk_live_…"), name: "support")
print(support.name)   // "support"

Screen context, actions and components

Tell the agent what the customer sees and does, let it act in your app, notice when the customer is stuck, keep identifiers on the device, and show the components the agent draws.

GraineInApp.setScreen

Call it on every meaningful screen change, so the agent knows what the customer sees.

func setScreen(_ next: GraineScreen?)

What the customer is looking at; call it on every meaningful change. Shared by every agent. An unchanged screen is not sent again, and a burst of changes goes as one update every 400 ms — the first at once. nil means no screen.

ParameterTypeDefaultMeaning
nextGraineScreen?requiredWhat the customer can see now; nil clears it.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.setScreen(GraineScreen(
    screen: "checkout_address", title: "Delivery address",
    journey: .init(name: "Checkout", step: 2, of: 3, next: "Payment"),
    fields: [GraineField(name: "postal_code", label: "Postal code", value: postalCode, status: .invalid, error: "We don't deliver there yet")]
))
GraineInApp.shared.updateField(GraineField(name: "postal_code", value: postalCode, status: .filled))
GraineInApp.shared.track("coupon_applied", data: ["code_length": 8])

GraineInApp.updateField

Call it as the customer edits one field, instead of describing the whole screen again.

func updateField(_ field: GraineField)

Reports one changed field, merged by name into the current screen (added when new), so a form need not describe itself again on every keystroke. Does nothing without a current screen, or for a field with an empty name.

ParameterTypeDefaultMeaning
fieldGraineFieldrequiredThe field: its name, label, value (masked on the device) and status.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.updateField(GraineField(name: "postal_code", value: postalCode, status: .filled))

GraineInApp.removeField

Call it when a field leaves the screen.

func removeField(_ name: String)

Removes a field from the current screen. Nothing happens if it was never reported.

ParameterTypeDefaultMeaning
nameStringrequiredThe field's id (GraineField.name).

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.removeField("coupon_code")

GraineInApp.clearScreen

Call it in a screen's teardown, so the agent stops describing a screen the customer left.

func clearScreen(_ screenId: String?)

Forgets the screen only if it is still the one with that id — safe in a screen's teardown, which can run after the next screen has reported itself. nil clears whatever screen is set.

ParameterTypeDefaultMeaning
screenIdString?requiredThe id (GraineScreen.screen) of the screen being left; nil clears whatever screen is set.

Returns — nothing; it returns when done.

Errors — none.

.onDisappear { GraineInApp.shared.clearScreen("checkout_address") }

GraineScreen (constructor)

Use it to describe what the customer is looking at.

init(screen: String, title: String? = nil, journey: Journey? = nil, fields: [GraineField] = [], error: String? = nil, data: [String: Any] = [:], availableActions: [String]? = nil)

Builds a screen description to hand to setScreen.

ParameterTypeDefaultMeaning
screenStringrequiredA stable id for the screen, such as "checkout_address"; keep it constant — it is also used for analytics.
titleString?nilWhat the agent calls the screen, such as "Delivery address".
journeyJourney?nilWhere the screen sits in a flow, and where the flow goes next.
fields[GraineField][]The inputs on the screen, with their status — what lets the agent name the field that needs attention.
errorString?nilA screen-level error banner, distinct from a field's own message.
data[String: Any][:]Anything else the agent should know, as JSON values; masked on the device.
availableActions[String]?nilWhich registered actions work on this screen; nil allows every one, an empty list none.

Returns — a new GraineScreen.

Errors — none.

let screen = GraineScreen(screen: "checkout_address", title: "Delivery address")

GraineScreen

Read it to see every part of a screen description.

What the customer is looking at, passed to GraineInApp.setScreen(_:). Report real values: masking runs on the device before anything leaves it, and a screen that pre-redacts only blinds the agent.

What to expect — What you tell the agent the customer can see. Values are masked on the device before they leave it; an unchanged screen is not sent again; the first change goes at once and later ones at most every 400 ms.

FieldTypeDefaultMeaning
var screen: StringStringrequiredA stable id for the screen; keep it constant — it is also used for analytics.
var title: String?String?nilWhat the agent calls the screen.
var journey: Journey?GraineScreen.Journey?nilWhere the screen sits in a flow, and where the flow goes next.
var fields: [GraineField][GraineField][]The inputs on the screen. Their status is what lets the agent name what needs attention after a pause.
var error: String?String?nilA screen-level error banner, distinct from a field's own message.
var data: [String: Any][String: Any][:]Anything else the agent should know, as JSON values.
var availableActions: [String]?[String]?nilWhich registered actions work here. With nil, every action this build registered is allowed; with an empty list, none is. It can only narrow what the agent declares.
static func == (a: GraineScreen, b: GraineScreen) -> BoolBool—Equality, comparing data by its JSON content; an unchanged screen is not sent again.
struct Journey: Equatablenested type—See GraineScreen.Journey.

Its functions: init — each with its parameters, response, errors and an example.

GraineScreen.Journey (constructor)

Use it to say where a screen sits in a flow.

init(name: String, step: Int? = nil, of: Int? = nil, next: String? = nil)

Builds where a screen sits in a flow.

ParameterTypeDefaultMeaning
nameStringrequiredThe flow's name, such as "Checkout".
stepInt?nilThis screen's step in the flow, counting from 1.
ofInt?nilHow many steps the flow has.
nextString?nilThe next step's name, so the agent can say what comes after this one.

Returns — a new GraineScreen.Journey.

Errors — none.

let journey = GraineScreen.Journey(name: "Checkout", step: 2, of: 3, next: "Payment")

GraineScreen.Journey

Read it to see how a flow is described.

Where a screen sits in a flow, and — next — where the flow goes after it. A value type.

What to expect — What you tell the agent the customer can see. Values are masked on the device before they leave it; an unchanged screen is not sent again; the first change goes at once and later ones at most every 400 ms.

FieldTypeDefaultMeaning
var name: StringStringrequiredThe flow's name, such as "Checkout".
var step: Int?Int?nilThis screen's step in the flow.
var of: Int?Int?nilHow many steps the flow has.
var next: String?String?nilThe next step's name.

Its functions: init — each with its parameters, response, errors and an example.

GraineField (constructor)

Use it to describe one input on a screen.

init(name: String, label: String? = nil, value: String? = nil, status: Status? = nil, error: String? = nil, isOptional: Bool = false)

Builds one input's description, for GraineScreen.fields or updateField.

ParameterTypeDefaultMeaning
nameStringrequiredThe field's id; updateField merges by it.
labelString?nilWhat the field is called on screen.
valueString?nilThe real value — masked on the device on the way out; never pre-redact it.
statusStatus?nilempty, filled, invalid, pending or locked: what lets the agent name the field that needs attention.
errorString?nilThe validation message the customer really sees; the agent can offer to fix what it describes.
isOptionalBoolfalseThe field may stay empty, so the agent does not treat it as still to do.

Returns — a new GraineField.

Errors — none.

let field = GraineField(name: "postal_code", label: "Postal code", value: "56003", status: .invalid,
                        error: "Enter a 6-digit postal code")

GraineField

Read it to see every part of a field description.

One input on a screen, as the agent should understand it, inside GraineScreen.fields or reported alone with GraineInApp.updateField(_:). A value type.

What to expect — One input on the screen. Its status — empty, filled, invalid, pending or locked — is what lets the agent name the field that is wrong.

FieldTypeDefaultMeaning
var name: StringStringrequiredThe field's id; fields merge by it.
var label: String?String?nilWhat the field is called on screen.
var value: String?String?nilThe real value — masked on the way out; never pre-redact it.
var status: Status?GraineField.Status?nilThe field's state.
var error: String?String?nilThe validation message the customer really sees; the agent can offer to fix what it describes.
var isOptional: BoolBoolfalseThe field may stay empty, so the agent does not treat it as still to do. Sent as optional.
enum Status: Stringnested type—See GraineField.Status.

Its functions: init — each with its parameters, response, errors and an example.

GraineField.Status

Use it to say what state a field is in, so the agent can name the field that needs attention.

A field's state; raw values are the wire words. It is what lets the agent name the field that needs attention instead of asking a vague question.

What to expect — One input on the screen. Its status — empty, filled, invalid, pending or locked — is what lets the agent name the field that is wrong.

CaseMeaning
case emptyNothing entered.
case filledHas a value.
case invalidRejected — pair it with error.
case pendingIn flight.
case lockedCannot be changed here; not the customer's to fix.

GraineInApp.setVariables

Call it to hand the agent facts from your app's data while it runs.

func setVariables(_ values: [String: Any?]?)

Sets prompt variables from the app's own data while the app is running. Merged, not replaced: nil under a key removes it, nil for the whole map clears them. Values become strings (nested objects and arrays are dropped) and are masked. They reach the next call; an engine page already loaded is updated too.

ParameterTypeDefaultMeaning
values[String: Any?]?requiredPrompt variables by name, merged into those already set: nil under a key removes it, nil for the whole map clears them all.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.setVariables(["plan": "gold", "cart_items": 3])
GraineInApp.shared.setVariables(["cart_items": nil])   // removes one

GraineInApp.currentVariables

Call it to check exactly which variables the agent will receive.

func currentVariables() -> [String: String]

Every variable the agent will receive, merged in precedence order: the dashboard's defaults, then GraineConfig.variables, then setVariables, then the traits from identify (without the identifying trait).

Parameters — none.

Returns — [String: String]: every variable by name, values as strings, already masked. Example value: ["plan": "gold", "name": "Asha"].

Errors — none.

print(GraineInApp.shared.currentVariables())

GraineInApp.track

Call it to tell the agent about a product event without interrupting.

func track(_ name: String, data: [String: Any] = [:])

A product event that informs the agent without interrupting. It goes with the next screen update; the last 10 events are kept. Every agent hears it. An empty name is ignored.

ParameterTypeDefaultMeaning
nameStringrequiredThe product event's name, for example "plan_viewed".
data[String: Any][:]Details of the event, by name; masked on the device.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.track("plan_viewed", data: ["plan": "gold"])

GraineInApp.tap

Call it to name a tap on a control the automatic capture cannot name.

func tap(_ name: String, data: [String: Any] = [:])

Records a tap on a named control as tap:<name> (the name trimmed, inner spaces collapsed, cut to 60 characters; an empty name is ignored). Taps are context, not news: they go with the next screen update, at the end of its 400 ms window. Every agent hears it.

ParameterTypeDefaultMeaning
nameStringrequiredThe control's name, as the agent sees it: tap:<name>.
data[String: Any][:]Details of the tap, by name; masked on the device.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.tap("pay_button")

View.graineTap

Use it on a SwiftUI control so its taps reach the agent by name.

func graineTap(_ name: String, graine: GraineInApp? = nil) -> some View

A tap on the view reaches the agent's context as tap:<name> ("you tapped Pay twice"), through GraineInApp.tap(_:data:), without taking the tap from the control. graine defaults to the default agent.

ParameterTypeDefaultMeaning
nameStringrequiredThe control's name, as the agent sees it: tap:<name>.
graineGraineInApp?nilThe agent to tell — another agent's (GraineInApp.named("support")) or the default when left out; every agent hears taps either way.

Returns — some View: your view, its taps reported by name.

Errors — none.

Button("Pay") { pay() }
    .graineTap("pay_button")

View (SwiftUI modifiers)

Use them to name a control for the agent, and to present the booking sheet, from SwiftUI.

Two SwiftUI modifiers the SDK adds to every View (public extension View).

What to expect — Each returns your view with the behaviour attached; neither changes how your view looks.

MemberWhat it does
func graineTap(_ name: String, graine: GraineInApp? = nil) -> some ViewNames a control for the agent: a tap on it reaches the agent as tap: and the name, since SwiftUI views carry no accessibility identifier for the automatic tap capture to read. See View.graineTap.
func graineBookingSheet(isPresented: Binding<Bool>, graine: GraineInApp? = nil, reason: String? = nil, onBooked: ((GraineScheduledCall) -> Void)? = nil) -> some ViewPresents the SDK's booking sheet while the binding is true. See View.graineBookingSheet.

GraineVoice.reportEvent

Call it during a call when something happened that the agent may want to speak up about.

func reportEvent(_ name: String, detail: String? = nil, data: [String: Any] = [:])

Something the agent may want to speak up about. Only during a call — otherwise it is dropped, so it never opens the next conversation. The current screen is sent first. data may carry quiet, cooldown_seconds and max_per_session, which pace the agent.

ParameterTypeDefaultMeaning
nameStringrequiredWhat happened, in a word — the agent may speak up about it.
detailString?nilA sentence about it, or nothing.
data[String: Any][:]Details, by name; quiet, cooldown_seconds and max_per_session pace how often the agent speaks up about it.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.voice.reportEvent("payment_failed", detail: "The card was declined.", data: ["cooldown_seconds": 60])

GraineInApp.noteInteraction

Call it when the customer does something that is not a field edit, so they are not taken for stuck.

func noteInteraction()

The customer did something that is not a field edit — scrolled a list, opened a picker, sent a message — so the idle clock that detects a stuck customer restarts. Field edits do not count.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.noteInteraction()

GraineInApp.registerAction

Call it to let the agent do something in your app, such as changing a setting.

@discardableResult func registerAction(_ name: String, handler: @escaping ActionHandler) -> () -> Void

Something the agent can do in your app; declare it on the agent in the dashboard too — a handler with no declaration is never offered, and a declaration with no handler is refused. The handler runs on the main thread and must answer within six seconds — five and a half in the chat and on a native call — or the agent is told it was not done (error: "The app did not answer within 6 seconds.", or "<name>" did not finish in time. in the chat and on a native call); a handler that throws answers error with the error's description. An action the agent asks for with no handler is answered refused: This app build does not implement "<name>". Registering the same name again replaces the handler. The returned function unregisters it, if it is still this registration.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, exactly as it is declared on the agent in the dashboard (Embed & Widgets → Actions).
handlerActionHandlerrequiredThe function that runs it, on the main thread: it receives the agent's arguments and returns .ok, .refused or .error — or throws.

Returns — () -> Void: call it to unregister the action (it does nothing once the name was registered again).

Errors — none.

let unregister = GraineInApp.shared.registerAction("set_language") { args in
    guard let language = args["language"] as? String else { return .refused("No language was given.") }
    settings.setLanguage(language)
    return .ok("The app is now in \(language).")
}
// A handler that takes time answers through the deferred form:
GraineInApp.shared.registerAction("upload_document", deferred: { args, done in
    upload(args) { ok in done(ok ? .ok("Uploaded.") : .error("The upload failed.")) }
})

GraineInApp.registerAction

Call it for an action whose work finishes later, such as an upload.

@discardableResult func registerAction(_ name: String, deferred handler: @escaping DeferredActionHandler) -> () -> Void

As above, but for work that completes later, such as an upload or a network call. done must be called once and only once, on any thread; any call after the first is ignored. After six seconds the agent is told the app did not answer, so slower work should answer at once and report its outcome later with setScreen or track.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, exactly as it is declared on the agent in the dashboard (Embed & Widgets → Actions).
deferred handlerDeferredActionHandlerrequiredThe function that runs it, on the main thread: it receives the agent's arguments and a done callback — call done once, from any thread, with .ok, .refused or .error.

Returns — () -> Void: call it to unregister the action (it does nothing once the name was registered again).

Errors — none.

GraineInApp.shared.registerAction("upload_document", deferred: { args, done in
    upload(args) { ok in done(ok ? .ok("Uploaded.") : .error("The upload failed.")) }
})

GraineInApp.checkActions

Call it once at startup to log which actions will and will not work.

func checkActions() -> GraineActionCheck

Which registered actions the agent can call, and which it cannot, compared with the actions declared on the agent. Log it once at startup.

Parameters — none.

Returns — GraineActionCheck — its fields: GraineActionCheck. Example value: GraineActionCheck(usable: ["set_language"], undeclared: [], unhandled: ["open_order"]).

Errors — none.

func onSessionReady(_ session: GraineSessionConfig) {
    let check = GraineInApp.shared.checkActions()
    if !check.unhandled.isEmpty { print("Declared but not registered:", check.unhandled) }
}

GraineActionCheck

Read it to find actions that will not work before a customer does.

Which registered actions the agent can call and which it cannot, returned by GraineInApp.checkActions(). Log it once at startup.

What to expect — Worked out on the device from what you registered and what the agent's settings declare; nothing is sent. Before the settings have loaded, every registered action is undeclared. The SDK also logs a warning for every unhandled action when the settings load.

FieldTypeDefaultMeaning
let usable: [String][String]—Actions both registered here and declared on the agent — these work.
let undeclared: [String][String]—Actions registered here that the agent does not declare — the agent never offers them.
let unhandled: [String][String]—Actions the agent declares that are not registered here — they are refused mid-sentence. Sorted.

GraineActionResult (constructor)

Use it to answer an action with a status you choose at run time.

init(status: Status, message: String = "", data: [String: Any] = [:])

Builds an action's answer from a status; ok, refused and error are shorter.

ParameterTypeDefaultMeaning
statusStatusrequired.ok, .refused or .error.
messageString""A sentence for the agent, not the customer — it decides how to phrase it.
data[String: Any][:]A structured result the agent can reason about, as JSON values.

Returns — a new GraineActionResult.

Errors — none.

let answer = GraineActionResult(status: .ok, message: "Language changed.", data: ["language": "hi"])

GraineActionResult.ok

Return it from an action handler when the action happened.

static func ok(_ message: String = "", data: [String: Any] = [:]) -> GraineActionResult

It happened; the agent will tell the customer it is done.

ParameterTypeDefaultMeaning
messageString""A sentence for the agent about what happened.
data[String: Any][:]A structured result the agent can reason about, as JSON values.

Returns — GraineActionResult.

Errors — none.

GraineInApp.shared.registerAction("cancel_order") { args in
    return .ok("The order was cancelled.", data: ["order_id": "ORD-1234"])
}

GraineActionResult.refused

Return it from an action handler when the action will not happen, and say why.

static func refused(_ message: String) -> GraineActionResult

Says the action will not happen and gives the reason ("this is locked after submission"); this is often the most helpful reply.

ParameterTypeDefaultMeaning
messageStringrequiredWhy it will not happen, for the agent to explain.

Returns — GraineActionResult.

Errors — none.

GraineInApp.shared.registerAction("cancel_order") { args in
    return .refused("The order has already shipped, so it can't be cancelled here.")
}

GraineActionResult.error

Return it from an action handler when the action was tried and failed.

static func error(_ message: String) -> GraineActionResult

It was tried and failed.

ParameterTypeDefaultMeaning
messageStringrequiredWhat went wrong, for the agent to explain.

Returns — GraineActionResult.

Errors — none.

GraineInApp.shared.registerAction("cancel_order", deferred: { args, done in
    done(.error("The network did not answer, so nothing changed."))
})

GraineActionResult

Read it to see what an action's answer carries.

What an action handler answers. The agent speaks about exactly this, so say what actually happened.

What to expect — What your app answers an agent's action with: ok (with an optional sentence and data), refused (it will not happen, and why) or error (it was tried and failed).

FieldTypeDefaultMeaning
let status: StatusGraineActionResult.Statusrequiredok, refused or error.
let message: StringString""For the agent, not the customer — it decides how to phrase it.
let data: [String: Any][String: Any][:]The result in structured form, for the agent to reason with.
enum Status: Stringnested type—See GraineActionResult.Status.

Its functions: init, ok, refused, error — each with its parameters, response, errors and an example.

GraineActionResult.Status

Use it to say how an action turned out.

The outcome of an action. Raw values are the wire words.

What to expect — Your handler picks one; the SDK answers refused for an action with no handler and error for a handler that threw or ran out of time.

CaseMeaning
case okDone.
case refusedWill not be done — also the SDK's answer for an action this build has no handler for.
case errorTried and failed — also the SDK's answer for a handler that threw or took longer than six seconds.

GraineInApp.configureFriction

Call it to change the stuck-customer rules for screens only your app knows.

func configureFriction(_ override: GraineFrictionConfig?)

Replaces the app's layer of friction rules, applied over the dashboard's; nil removes it. Safe at any time.

ParameterTypeDefaultMeaning
overrideGraineFrictionConfig?requiredThe app's friction rules, applied over the dashboard's; nil removes the app's layer.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.configureFriction(GraineFrictionConfig(screens: ["terms": .init(ignore: true)]))

GraineFrictionConfig (constructor)

Use it to build friction rules from JSON in the dashboard's shape.

init(json: [String: Any])

Rules from JSON in that shape.

ParameterTypeDefaultMeaning
json[String: Any]requiredThe rules in the dashboard's shape: enabled, idle (afterMs, or false), cooldownMs, message, nudge (cooldownSeconds, maxPerSession, openLauncher), requireOutstandingField and screens.

Returns — a new GraineFrictionConfig.

Errors — none.

let rules = GraineFrictionConfig(json: ["idle": ["afterMs": 30_000], "cooldownMs": 60_000])

GraineFrictionConfig (constructor)

Use it to build friction rules from values.

init(enabled: Bool? = nil, idleAfterMs: Int? = nil, cooldownMs: Int? = nil, cooldownSeconds: Int? = nil, maxPerSession: Int? = nil, openLauncher: Bool? = nil, message: String? = nil, requireOutstandingField: Bool? = nil, screens: [String: Screen] = [:])

Rules from values; nil leaves the layer below in force. idleAfterMs: on one screen this long fires stalled_on_step (45 s by default; 0 or less is off). cooldownMs: the least gap between two signals on the same screen (60 s). cooldownSeconds and maxPerSession: the pacing of unprompted interventions (20 s and 3). openLauncher: for a host that draws its own launcher, open it for this. message: what the agent is told instead of the built-in sentence, with {title}, {screen}, {seconds}, {field} and {error}. requireOutstandingField: fire only while a field is outstanding — invalid, or empty and not optional — timed from when it became so (true by default). screens: per-screen overrides by screen id.

ParameterTypeDefaultMeaning
enabledBool?nilFriction signals on (true) or off (false) for the whole app; nil leaves the dashboard's choice.
idleAfterMsInt?nilMilliseconds on one screen, with a field outstanding, before the customer counts as stalled (45,000 by default); 0 or less switches it off.
cooldownMsInt?nilThe least gap between two signals on the same screen, in milliseconds (60,000 by default).
cooldownSecondsInt?nilHow long the agent waits between unprompted interventions, in seconds (20 by default).
maxPerSessionInt?nilThe most unprompted interventions in one conversation (3 by default).
openLauncherBool?nilFor an app that draws its own launcher: open it when a signal fires.
messageString?nilWhat the agent is told instead of the built-in sentence, with {title}, {screen}, {seconds}, {field} and {error} filled in.
requireOutstandingFieldBool?nilFire only while a field is outstanding — invalid, or empty and not optional (true by default).
screens[String: Screen][:]Rules for single screens, keyed by screen id (GraineScreen.screen).

Returns — a new GraineFrictionConfig.

Errors — none.

let rules = GraineFrictionConfig(idleAfterMs: 20_000, screens: ["terms": .init(ignore: true)])

GraineFrictionConfig

Read it to see the friction rules your app can set.

One layer of friction (stuck-customer) rules — the app's own, applied over the dashboard's — set as GraineConfig.friction or with GraineInApp.configureFriction(_:). Most apps set nothing; use it for screens only the app knows about.

What to expect — Friction is the SDK noticing a customer who is stuck — stalled on a step, a field error repeated, looping between screens, rage taps, an incomplete submit, an error on the screen. The dashboard's rules apply unless the app overrides them; a signal lets the agent offer help.

FieldTypeDefaultMeaning
let json: [String: Any][String: Any]—The rules as JSON, in the same shape as the dashboard's: enabled, idle ({afterMs} or false), cooldownMs, message, nudge (cooldownSeconds, maxPerSession, openLauncher), requireOutstandingField and screens. Keys it does not recognise are left out, and an invalid number is replaced by its fallback.
struct Screennested type—See GraineFrictionConfig.Screen.

Its functions: init — each with its parameters, response, errors and an example.

GraineFrictionConfig.Screen (constructor)

Use it to set the friction rules for one screen.

init(ignore: Bool? = nil, idleAfterMs: Int? = nil, message: String? = nil)

Builds the friction rules for one screen.

ParameterTypeDefaultMeaning
ignoreBool?nilTrue: no friction signals on this screen at all.
idleAfterMsInt?nilMilliseconds on this screen before it counts as stalled; 0 or less switches the idle rule off here.
messageString?nilWhat the agent is told for this screen, with the same placeholders.

Returns — a new GraineFrictionConfig.Screen.

Errors — none.

let otp = GraineFrictionConfig.Screen(idleAfterMs: 20_000)

GraineFrictionConfig.Screen

Read it to see the rules one screen can override.

Friction rules for one screen, keyed by screen id in GraineFrictionConfig(screens:). A value type.

What to expect — Friction is the SDK noticing a customer who is stuck — stalled on a step, a field error repeated, looping between screens, rage taps, an incomplete submit, an error on the screen. The dashboard's rules apply unless the app overrides them; a signal lets the agent offer help.

FieldTypeDefaultMeaning
var ignore: Bool?Bool?nilTrue: no friction signals on this screen.
var idleAfterMs: Int?Int?nilThe idle time for this screen; 0 or less switches the idle rule off here.
var message: String?String?nilWhat the agent is told for this screen, with the same placeholders.

Its functions: init — each with its parameters, response, errors and an example.

GraineFrictionSignal

Read it to react in your own UI when the customer looks stuck.

The customer looks stuck, as reported to GraineListener.onFriction(_:). Made by the SDK; fired at most once per screen visit, only during a live conversation.

What to expect — Friction is the SDK noticing a customer who is stuck — stalled on a step, a field error repeated, looping between screens, rage taps, an incomplete submit, an error on the screen. The dashboard's rules apply unless the app overrides them; a signal lets the agent offer help.

FieldTypeDefaultMeaning
let name: StringString—The signal: "stalled_on_step" — the customer stayed on one screen with a field outstanding.
let screen: StringString—The screen's id.
let detail: StringString—Plain English, given to the agent verbatim — for example the field that is outstanding and the message it shows.
let data: [String: Any][String: Any]—seconds, field and error.
let cooldownSeconds: DoubleDouble—The pacing the agent applies to unprompted interventions.
let maxPerSession: DoubleDouble—The most unprompted interventions per conversation.
let openLauncher: BoolBool—If your app draws its own launcher and this is true, open the launcher for this signal.

GraineMasking.mask

Call it to mask a string the same way the SDK masks what it sends.

static func mask(_ input: String) -> String

Replaces identifiers in one string with [PAN], [EMAIL], [CARD], [PHONE] and [AADHAAR], then applies the app's own rules.

ParameterTypeDefaultMeaning
inputStringrequiredAny text your app is about to report.

Returns — String: the same text with every match replaced. Example value: "Mail me at [EMAIL]".

Errors — none.

let safe = GraineMasking.mask("Mail me at asha@example.com")   // "Mail me at [EMAIL]"

GraineMasking.addRule

Call it once at startup to mask a value shape only your app knows.

static func addRule(_ pattern: NSRegularExpression, token: String = "[REDACTED]")

A rule for a value shape only your app knows — an order number, a membership id. Registered for the whole app, once at startup, so it covers every screen; every match is replaced, not just the first. token is a template: $1 refers to a capture group.

ParameterTypeDefaultMeaning
patternNSRegularExpressionrequiredWhat to find: a compiled regular expression.
tokenString"[REDACTED]"What each match becomes — a template, so $1 keeps the first capture group.

Returns — nothing; it returns when done.

Errors — none.

GraineMasking.addRule(try NSRegularExpression(pattern: "MEM-[0-9]{8}"), token: "[MEMBER_ID]")

GraineMasking.addRule

Call it to add a masking rule from a pattern string.

@discardableResult static func addRule(pattern: String, token: String = "[REDACTED]") -> Bool

The same, from a pattern string. Returns false, and adds nothing, when the pattern does not compile.

ParameterTypeDefaultMeaning
patternStringrequiredWhat to find, as a regular expression's text.
tokenString"[REDACTED]"What each match becomes — a template, so $1 keeps the first capture group.

Returns — Bool. Example value: true.

Errors — none.

let added = GraineMasking.addRule(pattern: "ORD-[0-9]+", token: "[ORDER]")   // false if it does not compile

GraineMasking.clearCustomRules

Call it in tests to remove the masking rules your app added.

static func clearCustomRules()

Removes the app's own rules. For tests only.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineMasking.clearCustomRules()

GraineMasking.deep

Call it to mask every string inside a JSON value.

static func deep(_ value: Any?) -> Any

Goes through a JSON value and masks each string it finds, however deeply nested. Keys are not masked; numbers and other non-string values are returned untouched.

ParameterTypeDefaultMeaning
valueAny?requiredAny JSON value — a map, a list, a string or a number.

Returns — Any: the same shape, every string inside masked. Example value: ["note": "Mail me at [EMAIL]", "items": 3].

Errors — none.

let safe = GraineMasking.deep(["note": "Mail me at asha@example.com", "items": 3])

GraineMasking

Read it to know what never leaves the device unmasked, and to add rules of your own.

What never leaves the device unmasked: PAN, email, card, Indian mobile and Aadhaar numbers are replaced, in that order, in what the app reports — screens, events, traits and the variables set while the app runs — before it is sent. Static members, safe from any thread; masking is a set of rules, not a guarantee.

What to expect — Applied on the device, before anything leaves it: card and account numbers, government ID numbers, phone numbers and email addresses are replaced; rules you add apply as well.

Its functions: mask, addRule, clearCustomRules, deep — each with its parameters, response, errors and an example.

GraineWidgetView (constructor)

Use it to draw the agent's components yourself, above your own UI, instead of in the bar.

init(graine: GraineInApp? = nil, accent: Color? = nil)

The agent's components, drawn by the SDK in the dashboard's preset and answered for you. Mount it once, above your bar; while it is mounted the bar's own component slot steps aside.

ParameterTypeDefaultMeaning
graineGraineInApp?nilThe configured agent to draw — another agent's (GraineInApp.named("support")) or the default when left out.
accentColor?nilOverrides the component's and the dashboard's accent colour.

Returns — a new GraineWidgetView.

Errors — none.

AppContent()
    .overlay(GraineWidgetView())   // the agent's components, above your screens
    .overlay(GraineAgentBar())

GraineWidgetView

Use it as the component view.

The component the agent put on screen — product lists, plan comparisons, info cards, choices and quick actions, calendar slots, or the agent's fallback sentence for any other kind — drawn by the SDK in the dashboard's preset and answered for you. A SwiftUI view: mount it once, above your bar; it shows inline only when there is something to show (never as a full-screen cover, which would take the voice engine out of the window), and while it is mounted the bar's own component slot steps aside.

What to expect — Place it in your UI; it draws in the agent's design and follows the conversation by itself.

PropertyTypeMeaning
var body: some Viewsome ViewSwiftUI's view body; apps do not call it.

Its functions: init — each with its parameters, response, errors and an example.

GraineVoice.answerWidget

Call it when your own UI drew a component and the customer answered it.

func answerWidget(widgetId: String, data: [String: Any], summary: String)

Passes the agent the customer's answer to a component it drew, on the in-app call. summary is the sentence the agent hears ("Let's compare the two plans."); without it the tap lands and the conversation does not move. GraineWidgetView does this itself. To answer on whichever conversation is live — the chat or the call — use GraineInApp.answerComponent.

ParameterTypeDefaultMeaning
widgetIdStringrequiredThe component's id, from the map onWidget delivered (GraineWidget.id).
data[String: Any]requiredWhat the customer chose, as JSON values — the agent can reason about it.
summaryStringrequiredThe sentence the agent hears as the customer's answer; without it the tap lands and the conversation does not move.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.voice.answerWidget(widgetId: "w_1", data: ["plan": "gold"], summary: "I'll take the Gold plan.")

GraineInApp.answerComponent

Call it when a component you drew yourself was answered — in the chat or on a call.

func answerComponent(widgetId: String, data: [String: Any], summary: String)

Passes the agent the customer's answer to a component it drew, on the conversation that is live: the chat — the structured answer, then summary as the customer's own message, as the website's chat sends them — else the in-app call. summary is the sentence the agent hears ("The Creta, please."); without it the answer lands and the conversation does not move. GraineWidgetView and the bar answer for themselves: use this when your app draws components from onWidget. Safe from any thread (it hops to the main thread).

ParameterTypeDefaultMeaning
widgetIdStringrequiredThe component's id, from the map onWidget delivered (GraineWidget.id).
data[String: Any]requiredWhat the customer chose, as JSON values — the agent can reason about it.
summaryStringrequiredThe sentence that is the customer's answer — their message in the chat; without it the answer lands and the conversation does not move.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.answerComponent(widgetId: "w_1", data: ["model": "creta"], summary: "The Creta, please.")

GraineInApp.reopenComponent

Call it from a "show that again" control to bring back the last component the customer closed.

@discardableResult func reopenComponent() -> Bool

Puts back the last component the customer closed without answering, exactly as it was — for a "show that again" control. Returns false when there is none.

Parameters — none.

Returns — Bool. Example value: true.

Errors — none.

if !GraineInApp.shared.reopenComponent() {
    showToast("Nothing to show again")
}

GraineWidget (constructor)

Use it to read a component map when you draw components yourself.

init?(frame: [String: Any], baseUrl: String? = nil)

Reads a component map (widget_id or id, kind, data, fallback_text, style, on_select). Nil when it has no id. Pass the agent's baseUrl to resolve images.

ParameterTypeDefaultMeaning
frame[String: Any]requiredThe component map onWidget delivered.
baseUrlString?nilYour GraineConfig.baseUrl, to resolve image paths that start with /; nil leaves them as they are.

Returns — a new GraineWidget, or nil when the map has no id.

Errors — none.

func onWidget(_ widget: [String: Any]) {
    guard let component = GraineWidget(frame: widget, baseUrl: GraineInApp.shared.config.baseUrl) else { return }
    show(component.kind, component.data)
}

GraineWidget

Read it to draw a component yourself.

A component the agent asked to show, read from the map GraineListener.onWidget(_:) delivers — for an app that draws components itself (GraineWidgetView draws them for you). A value type.

What to expect — A component the agent drew — a product list, choices, time slots, an information card — with the data it was drawn from. The SDK's views draw it in your design.

FieldTypeDefaultMeaning
let id: StringString—The component's id, which an answer names (answerWidget(widgetId:…)).
let kind: StringString—Whatever kind the agent declares, for example "product_list", "plan_comparison" or "choices".
let data: [String: Any][String: Any]—Its content, with root-relative image paths resolved against baseUrl.
let fallbackText: StringString—What the agent would have said instead — draw it for a kind you do not render.
let raw: [String: Any][String: Any]—The map exactly as it arrived.
var preset: GraineWidgetPreset { get }GraineWidgetPreset—The preset the dashboard chose, or .cards.

Its functions: init — each with its parameters, response, errors and an example.

GraineWidgetSurface

Read it only to place components on screen the way the SDK does.

Where a component goes on screen — one rule shared with every platform, so the same component is not a sheet in one place and a card in another. Used by the SDK's component view; apps do not need it.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

CaseMeaning
case screenThe component is the screen — pictures, several items to compare (product lists).
case sheetA choice to finish, then back to the app (plan comparisons, choices, quick actions, calendar slots).
case cardA fact, or a kind this build cannot draw.

GraineWidgetPreset

Read it to match the component look chosen in the dashboard.

How a component looks: the preset chosen in the dashboard's Web components, as GraineWidget.preset. Raw values are the wire words.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

CaseMeaning
case galleryLarge pictures.
case cardsCards with medium pictures; the default.
case listRows with small pictures.
case spotlightOne item at a time, with a large picture.
case compactSmall pictures and outlined buttons.

Calls the user places

Calls like WhatsApp, on CallKit: the customer calls your agent from your app, on the system's call screen, with the audio in your app's own process — so the call carries on from the lock screen and with the screen off. One call at a time. With the optional GraineInAppRTC package in the app (Add WebRTC calls), the calls in both directions carry their audio over WebRTC: GraineCallsConfig.callTransport chooses, and GraineCalls.transport and transportFallbackReason say what carried each call.

GraineCalls.configure

Call it once at launch to set how calls ring and what they show.

func configure(_ config: GraineCallsConfig)

How calls ring and what they show. Call it a single time at launch, ahead of enableVoIPPushes.

ParameterTypeDefaultMeaning
configGraineCallsConfigrequiredHow calls ring and what they show: the call name, the ringtone, the icon, Recents and the ring timeout.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.calls.configure(GraineCallsConfig(displayName: "Acme Assistant"))

GraineCallsConfig (constructor)

Use it to build that configuration.

init(displayName: String = "", ringtoneSound: String? = nil, iconTemplateImageData: Data? = nil, includesCallsInRecents: Bool = false, ringTimeout: TimeInterval = 30, callTransport: GraineCallTransport = .auto)

Builds a calls configuration.

ParameterTypeDefaultMeaning
displayNameString""The name a call shows when nothing else names it.
ringtoneSoundString?nilA sound file in your app's bundle (30 seconds or less) to ring with; left out, the brand's ring from the design, else the system's ringtone.
iconTemplateImageDataData?nilPNG data for the template image on the system call screen's app button: 40 × 40 points, transparent. Left out, the brand's launcher glyph from the design.
includesCallsInRecentsBoolfalseWhether Graine's calls appear in the Phone app's Recents.
ringTimeoutTimeInterval30Seconds a ring rings before it is missed — a ring from your own server; a ring Graine sends lasts the agent's Ring for from the dashboard instead.
callTransportGraineCallTransport.autoThe road the calls' audio takes, in both directions: .auto (WebRTC when the GraineInAppRTC package is in the app and the dashboard allows it), .websocket or .webrtc. See GraineCallTransport.

Returns — a new GraineCallsConfig.

Errors — none.

GraineInApp.shared.calls.configure(GraineCallsConfig(displayName: "Acme Assistant", ringTimeout: 45))
// Keep this build's calls on the WebSocket, with or without the WebRTC package:
GraineInApp.shared.calls.configure(GraineCallsConfig(displayName: "Acme Assistant", callTransport: .websocket))

GraineCallsConfig

Read it to see every call setting your app controls.

How calls ring and what they show, passed to GraineCalls.configure(_:) once at launch. A value type.

What to expect — Made once, at launch, before anything else. Optional fields have the defaults shown; nothing in it is sent to Graine except what the descriptions say.

FieldTypeDefaultMeaning
var displayName: StringString""The name shown while a call rings and on the call screen — your agent's or your brand's. Empty: the agent's call name from the dashboard, else your app's name. With one agent configured a non-empty value wins; with several, each agent's dashboard call name comes first, so a ring says who is calling. A push's caller_name wins for its own call.
var ringtoneSound: String?String?nilThe ringtone: a sound file bundled with your app. nil: the brand's ring when the design chose one, else the system ringtone.
var iconTemplateImageData: Data?Data?nilThe template image, as PNG data (40×40 points, transparent), for the app button on the system call screen. nil: the brand's launcher glyph, from the design.
var includesCallsInRecents: BoolBoolfalseList the calls in the Phone app's Recents.
var ringTimeout: TimeIntervalTimeInterval30Seconds an agent's call rings before it is missed (at least 1). A ring Graine sent lasts the agent's ring timeout from the dashboard instead.
var callTransport: GraineCallTransportGraineCallTransport.autoThe road the calls' audio takes — a call the customer places and a ring they answer alike: .auto, .websocket or .webrtc (GraineCallTransport). The dashboard's In-app call audio set to Standard wins over it. Read at the start of each call, so a new configure applies from the next call.

Its functions: init — each with its parameters, response, errors and an example.

GraineCalls.startCall

Call it when the customer taps your "Call" button to talk to the agent.

@discardableResult func startCall(reason: String? = nil, variables: [String: Any] = [:], agent: String? = nil) -> String?

The customer calls the agent. The agent hears who_called = "customer" and call_reason (reason), with variables (masked on the device) over the SDK's own; a call carries at most 12 variables, names up to 40 characters, values up to 200, about 1 KB in all — the call's own first (who_called, call_reason, then variables), then the app's while they fit. agent names which configured agent to call; nil is the default. Returns the call's id — "call_" and a lowercase UUID — or nil when a call is already live or no agent has that name (each said in the log). Asks for the microphone first if the app does not hold it; refused, the call ends failed and onMicDenied is called. Ends any in-app (voice) call. Safe from any thread: it runs on the main thread and returns the id from there.

How the call connects (0.8.0). The same for a call the customer places and a ring they answer:

  • It starts at once on the agent's configuration the SDK already has — this launch's, or the one kept on the device from an earlier launch — and refreshes it behind the call. Only when it has none it can use does the call wait for the network first.
  • One 20-second deadline covers everything before the conversation opens: the agent's configuration, the ticket and the connection. Past it the call fails with "The call could not connect within 20 seconds." — it never sits on "Connecting…".
  • A call is connected once it can be heard. state stays connecting until the call's audio runs. If the conversation opens before CallKit has activated the call's audio, the SDK waits 3 seconds, then starts the audio itself, once; still no audio 3 seconds later, the call fails with "The call's audio could not start." instead of staying open and silent.
  • The prompt variables ride the call's ticket and are not added to the connection's address; only the direct connection, used when no ticket can be had, still carries them.

Where a failure shows. GraineCallsListener.onCallOutcome hears .failed(message) — outcome.error is the message, after .answered for a ring the customer answered; state is .ended(.failed) for a second (end reason failed), then .idle; GraineCallView says "Call failed"; the call ends on the system's call screen too. Other messages the SDK words there: "The microphone permission was not granted.", "The agent's configuration could not be loaded.", "The conversation did not open within 15 seconds.", "The line closed before the call connected." and "The call could not be placed: …" (CallKit's reason). With GraineConfig.debug on, the log also says when CallKit activates and deactivates the call's audio.

Which road the audio takes (0.9.0). With the GraineInAppRTC package in the app and GraineCallsConfig.callTransport allowing it (.auto does), the call tries WebRTC first — prepared at the tap, so it connects as fast as before. If WebRTC cannot set the call up before any audio flows — a network that blocks it, a server that turns it down, no answer within about 12 seconds — the call continues on the WebSocket inside the same call: no second ring, no new call screen, no error, "Connecting…" a moment longer. transport says which road carried it and transportFallbackReason why it is not WebRTC. A call the agent cannot take at all (no credit left, the agent switched off) ends failed with that reason, exactly as without the package. Once audio flows a call never moves: a lost network path is restarted once, then the call ends as dropped (.ended(.failed)).

ParameterTypeDefaultMeaning
reasonString?nilWhy the customer is calling — the agent hears it as call_reason.
variables[String: Any][:]Prompt variables by name, for this call or conversation; masked on the device before they leave it.
agentString?nilThe agent's name in your app — the one it was configured under; the default agent when it is left out or empty.

Returns — String?: the new call's id, which every state change and outcome carries; nil when a call is live or no agent has that name. Example value: "call_6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44".

Errors — none thrown: a call that cannot connect ends failed, as above.

let callId = GraineInApp.shared.calls.startCall(reason: "support_help", agent: "support")

GraineCalls.end

Call it from your own hang-up control — or to decline a ring.

func end()

Hangs up — or declines, while it rings. Does nothing with no live call. The outcome follows: .declined for a ring, .ended(.local) for a call that was connecting or on.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.calls.end()

GraineCalls.setMuted

Call it from your own mute control during a call.

func setMuted(_ value: Bool)

Stops (or resumes) sending the customer's voice without ending the call; the system's call screen shows it too.

ParameterTypeDefaultMeaning
valueBoolrequiredtrue to stop sending the customer's voice; false to send it again.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.calls.setMuted(true)

GraineCalls.setSpeaker

Call it from your own speaker control during a call.

func setSpeaker(_ on: Bool)

The loudspeaker (true) or the earpiece or headset (false). A call starts on the earpiece, or a connected headset.

ParameterTypeDefaultMeaning
onBoolrequiredtrue for the loudspeaker; false for the earpiece or a headset.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.calls.setSpeaker(true)

GraineCallView (constructor)

Use it to show the call while your app is open.

init(calls: GraineCalls? = nil)

The calls to draw; GraineInApp.shared.calls when nil, so an agent must be configured. The screen draws every moment of a call: a call Graine rang at a booked time ringing ("Scheduled call") and any other ring ("Incoming call"), each with Decline and Answer; connecting ("Connecting…"); on the call — the timer (with "· Muted" while muted), or "Muted", "Speaking" or "Listening" when the design has no timer, the line being said, and Mute, Speaker and End in the design's order; ended ("Call ended", "Call failed" or "Declined", the controls dimmed); and missed ("Missed call") with Close and Call back, for 30 seconds. The avatar is the brand's logo, its launcher glyph or the call name's initial; a ring pulses out while it rings and the avatar breathes while the agent speaks, not under Reduce Motion. A push that woke a closed app draws from the agent's settings kept on the device.

ParameterTypeDefaultMeaning
callsGraineCalls?nilThe calls object to draw — GraineInApp.shared.calls when left out (an agent must be configured).

Returns — a new GraineCallView.

Errors — none.

.fullScreenCover(isPresented: .constant(calls.showsCallScreen)) {
    GraineCallView()
}

GraineCallView

Use it as the in-app call screen.

A ready call screen for while your app is in the foreground, drawn in the design of the agent the call is with; the system's own call screen covers the lock screen and the background. A SwiftUI view — present it over your UI, for example .fullScreenCover(isPresented: .constant(calls.showsCallScreen)) { GraineCallView() }.

What to expect — Place it in your UI; it draws in the agent's design and follows the conversation by itself.

PropertyTypeMeaning
var body: some Viewsome ViewSwiftUI's view body; apps do not call it.

Its functions: init — each with its parameters, response, errors and an example.

GraineCalls.addListener

Call it to hear call states and outcomes, for your own UI or your server.

func addListener(_ listener: GraineCallsListener)

Hear about calls. Held weakly; called on the main thread.

ParameterTypeDefaultMeaning
listenerGraineCallsListenerrequiredYour listener; held weakly, so keep your own reference to it. Adding it twice has no effect.

Returns — nothing; it returns when done.

Errors — none.

final class CallEvents: GraineCallsListener {}
let callEvents = CallEvents()   // keep it: the SDK holds its listeners weakly
GraineInApp.shared.calls.addListener(callEvents)

GraineCalls.removeListener

Call it when that listener should stop hearing about calls.

func removeListener(_ listener: GraineCallsListener)

Stops reporting to that listener.

ParameterTypeDefaultMeaning
listenerGraineCallsListenerrequiredThe listener to stop telling.

Returns — nothing; it returns when done.

Errors — none.

final class CallEvents: GraineCallsListener {}
let callEvents = CallEvents()
GraineInApp.shared.calls.addListener(callEvents)
// … later, when it should stop hearing about calls:
GraineInApp.shared.calls.removeListener(callEvents)

GraineCallsListener.onCallState

Implement it to follow the call as it moves, for your own call UI.

func onCallState(_ state: GraineCallState, callId: String?)

The call moved: idle, ringing, connecting, active or ended. Called only on a change. A call stays connecting until its audio runs, and moves to ended(.failed) when it cannot connect — see GraineCalls.startCall.

ParameterTypeDefaultMeaning
stateGraineCallStaterequiredWhere the call is now: idle, ringing, connecting, active or ended with its reason.
callIdString?requiredThe call's ID: your server's call_id, Graine's for a ring it sent, or the one startCall returned.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineCallsListener {
    func onCallState(_ state: GraineCallState, callId: String?) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.calls.addListener(events)

GraineCallsListener.onCallOutcome

Implement it to tell your server what became of each call.

func onCallOutcome(_ outcome: GraineCallOutcome, callId: String)

How a call turned out — answered, declined, missed, ended (local, remote, failed) or failed. Tell your server, with the call id; do not depend on its order relative to onCallState. A call that could not connect is .failed with the reason in a sentence — such as "The call could not connect within 20 seconds." or "The call's audio could not start."

ParameterTypeDefaultMeaning
outcomeGraineCallOutcomerequiredWhat became of the call: outcome.wire is one word for your server, outcome.endReason who ended a call that connected, outcome.error why one never did.
callIdStringrequiredThe call's ID: your server's call_id, Graine's for a ring it sent, or the one startCall returned.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineCallsListener {
    func onCallOutcome(_ outcome: GraineCallOutcome, callId: String) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.calls.addListener(events)

GraineCalls.transport

Read it to know which road carried a call's audio — WebRTC or the WebSocket.

@Published internal(set) var transport: GraineCallTransport?

The road the current call's audio runs on: .webrtc or .websocket — never .auto. Nil until it is decided: a call trying WebRTC has none until its audio path connects (.webrtc) or it falls back (.websocket, with transportFallbackReason); a call on the WebSocket from the start has .websocket as soon as it begins connecting. So read it once the call is active. The same for both directions — a call placed with startCall and a ring the customer answered — and nil again once the calls are idle. Published on the main thread.

Parameters — none.

Returns — GraineCallTransport? — its cases: GraineCallTransport. Example value: .webrtc.

Errors — none.

final class CallLog: GraineCallsListener {
    func onCallState(_ state: GraineCallState, callId: String?) {
        guard case .active = state else { return }
        let calls = GraineInApp.shared.calls
        print("Call on", calls.transport?.rawValue ?? "?", calls.transportFallbackReason ?? "")
    }
}
let callLog = CallLog()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.calls.addListener(callLog)

GraineCalls.transportFallbackReason

Read it to know why a call that tried WebRTC runs on the WebSocket.

@Published internal(set) var transportFallbackReason: String?

Set only for a call that tried WebRTC and continues on the WebSocket — WebRTC could not set it up before any audio flowed; the customer noticed nothing but "Connecting…" a moment longer. Nil for a call on WebRTC, for a call that never tried it (no GraineInAppRTC package, callTransport .websocket, the dashboard's Standard), and before a transport is decided. Read it once the call is active; nil again once the calls are idle. Its values are API — compare them as they are:

ValueMeans
mint_failedThe call's WebRTC session could not be set up (refused, rate-limited, or unreachable).
register_failedGraine's media service did not accept the connection, or never answered it.
no_responseThe call got no reply at all.
no_answerReplies came, the answer did not.
ice_timeoutAnswered, but the audio path never connected — a network that blocks WebRTC.
errorAnything else before the audio flowed.

Parameters — none.

Returns — String?. Example value: "ice_timeout".

Errors — none.

if let reason = GraineInApp.shared.calls.transportFallbackReason {
    print("WebRTC could not carry this call (\(reason)); it runs on the WebSocket")
}

GraineCallTransport.from

Call it to turn a word — from your own settings or a remote flag — into a GraineCallTransport.

static func from(_ text: String?) -> GraineCallTransport?

"auto", "websocket" (or "ws") and "webrtc" (or "rtc"), trimmed and in any case; nil for anything else.

ParameterTypeDefaultMeaning
textString?requiredA word: auto, websocket, ws, webrtc or rtc; anything else, or nil, is nil.

Returns — GraineCallTransport?. Example value: .webrtc.

Errors — none.

let choice = GraineCallTransport.from("WebRTC") ?? .auto   // .webrtc
GraineInApp.shared.calls.configure(GraineCallsConfig(displayName: "Acme Assistant", callTransport: choice))

GraineRTC.install

Call it once at launch only if your build strips code nothing references; otherwise the calls find the WebRTC module by themselves.

static func install()

In the GraineInAppRTC package (import GraineInAppRTC). Makes the WebRTC module available to the calls now, rather than when the first call looks for it. Adding the package is enough on its own — the calls find the module by themselves — so call it only in a build that strips code nothing references, in application(_:didFinishLaunchingWithOptions:), before the first call. Calling it again changes nothing; safe from any thread.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

import GraineInAppRTC
 
GraineRTC.install()
print("WebRTC module", GraineRTCVersion.name)   // always GraineInApp's version

GraineCallsListener

Implement it to hear about native calls.

What the calls report, for your server, added with GraineCalls.addListener(_:); every method has a default that does nothing. Called on the main thread; held weakly.

What to expect — Implement only the callbacks you need; the others do nothing. Called on the main thread.

PropertyTypeMeaning
extension GraineCallsListener—An empty default for every callback, so your listener implements only the ones it needs.

Its functions: onIncomingCall, onCallState, onCallOutcome — each with its parameters, response, errors and an example.

GraineCalls

Use it to place, answer and draw native calls: GraineInApp.shared.calls.

Native calls like WhatsApp, on CallKit and VoIP pushes — the customer calls your agent, or your agent rings the customer — with the audio in your app's own process, so a call carries on from the lock screen and with the screen off. Reached as GraineInApp.shared.calls (one object for the whole app, one call at a time); an ObservableObject on the main thread whose methods hop there when called elsewhere.

What to expect — The native call engine. Configure it once at launch, before a push can arrive; hand it the push tokens and the pushes; place calls with startCall and listen for outcomes. One call at a time on the device.

PropertyTypeMeaning
@Published private(set) var state: GraineCallState = .idleGraineCallStateWhere the call is. connecting lasts until the call's audio runs; ended lasts one second before idle.
@Published private(set) var callId: String?String?The current (or just-ended) call's id: the push's call_id, or the one startCall made.
@Published private(set) var direction: GraineCallDirection?GraineCallDirection?Who rang whom: .incoming for a ring, .outgoing for a call the customer placed; nil with no call.
@Published private(set) var callerName = ""StringThe name the call shows — draw it on your own call screen.
@Published private(set) var muted = falseBoolThe customer's voice is not being sent.
@Published private(set) var speakerOn = falseBoolAudio from the loudspeaker rather than the earpiece or a headset. Follows the audio route, so the system's call screen can change it too.
@Published private(set) var agentSpeaking = falseBoolThe agent's voice is playing — measured from the audio itself, not the agent's text.
@Published private(set) var caption: GraineCaption?GraineCaption?The most recent line — the agent's words while they are spoken, or the customer's while they are heard.
@Published private(set) var connectedAt: Date?Date?When the conversation opened; nil before.
@Published private(set) var transcript: [GraineCaption] = [][GraineCaption]What has been said on this call, oldest first (up to 12 lines), a live line replaced as its words arrive. Cleared when the call is over.
@Published private(set) var hosted = falseBoolThis call was rung by Graine, as one booked for this customer (its push carried hosted: "1"). The call screen says "Scheduled call".
@Published private(set) var agent: String?String?Which agent the current call is with, by the name given to GraineInApp.configure(_:name:); nil while idle.
@Published private(set) var missedCall: GraineMissedCall?GraineMissedCall?The ring nobody answered, kept for 30 seconds after the call is over so a call screen can say "Missed call" and offer callBack() — or until the customer calls back, dismissMissedCall() is called, or another call rings or is placed.
@Published internal(set) var transport: GraineCallTransport?GraineCallTransport?The road the current call's audio runs on, .webrtc or .websocket, once decided; nil before. See GraineCalls.transport.
@Published internal(set) var transportFallbackReason: String?String?Why a call that tried WebRTC runs on the WebSocket — mint_failed, register_failed, no_response, no_answer, ice_timeout or error; nil otherwise. See GraineCalls.transportFallbackReason.
var showsCallScreen: Bool { get }BoolWhether a call screen has something to show: a call, its last second, or a missed ring to call back.
private(set) var config = GraineCallsConfig()GraineCallsConfigThe configuration in force: the last one passed to configure.
var callPermissions: GraineCallPermissions { get }GraineCallPermissionsThe call permissions as the phone holds them now. See GraineCalls.callPermissions.
var callSetupProblems: [GraineCallSetupProblem] { get }[GraineCallSetupProblem]What the app's setup still lacks for calls, each with its fix. See GraineCalls.callSetupProblems.

Its functions: configure, enableVoIPPushes, addListener, removeListener, startCall, reportIncomingCall, voipTokenUpdated, answer, end, setMuted, setSpeaker, callBack, dismissMissedCall, requestCallPermissions, openCallSettings, providerDidReset, provider, pushRegistry — each with its parameters, response, errors and an example.

GraineCallState

Read it to draw where a call is.

Where a native call is: GraineCalls.state and GraineCallsListener.onCallState(_:callId:).

What to expect — idle → ringing (an incoming call) or connecting (a call placed) → active → ended, then idle again once reported. One call at a time: a second ring while a call is live is missed (busy). A call is active only once its audio runs; one that cannot connect within 20 seconds, or whose audio never starts, goes from connecting to ended(.failed).

FieldTypeDefaultMeaning
var isLive: Bool { get }Bool—A call is ringing, connecting or active: no other may start.
CaseMeaning
case idleNo call.
case ringingAn agent's call is ringing on the device.
case connectingThe call was answered or placed, and the conversation is opening.
case activeTalking.
case ended(GraineCallEndReason)Over, and why. Becomes idle once reported, a second later.

GraineCallDirection

Read it to tell a call the customer placed from a ring.

Who rang whom: GraineCalls.direction. Raw values are the wire words.

What to expect — Set when a call starts ringing or is placed, kept through its end, and nil again once the calls are idle.

CaseMeaning
case incomingThe agent rang the customer (a VoIP push).
case outgoingThe customer called the agent (startCall).

GraineCallEndReason

Read it to say why a call ended.

Why a call is over, inside GraineCallState.ended and GraineCallOutcome.ended. Raw values are the wire words.

What to expect — In the state, every end has a reason: failed covers a line that dropped mid-call and a call that never connected. In an outcome, .ended carries only local, remote or failed for a call that connected; a call that never connected is .failed with its reason instead.

CaseMeaning
case localThe customer hung up, or the app called end().
case remoteThe agent hung up.
case failedThe line was lost, or it never came up at all.
case declinedThe customer declined the ring.
case missedNobody answered within the ring timeout.

GraineCallOutcome

Read it to report what became of a call.

What became of a call, reported once through GraineCallsListener.onCallOutcome(_:callId:) — send it to your server with the call id. For a call Graine rang, the SDK also reports it to Graine.

What to expect — Reported once per call: answered, then ended with who ended it (local, remote, failed); or declined, missed (also when another call was live), or failed with why — such as "The call could not connect within 20 seconds." or "The call's audio could not start.". For a call Graine rang, the SDK has already told Graine. Send it to your server with the call id; do not rely on its order relative to the call's state changes.

FieldTypeDefaultMeaning
var wire: String { get }String—One word for your server: answered, declined, missed, ended or failed — the same words on Android.
var endReason: GraineCallEndReason? { get }GraineCallEndReason?—For a call that connected (.ended), what ended it: local, remote or failed. Nil otherwise.
var error: String? { get }String?—Why a call never connected (.failed). Nil otherwise.
CaseMeaning
case answeredThe customer answered the agent's call.
case declinedThe customer declined it.
case missedNo one picked up before the ring timeout, or the call came in while another one was live.
case ended(GraineCallEndReason)A connected call has finished — .local, .remote or .failed.
case failed(String)It never connected; the string says why, for your logs.

GraineCallTransport

Use it to choose the road the native calls' audio takes, and to read which one carried a call.

public enum GraineCallTransport: String, CaseIterable, Sendable — what the app asks for (GraineCallsConfig.callTransport), what carried a call (GraineCalls.transport: .webrtc or .websocket, never .auto) and the dashboard's switch (GraineHostedCallsSettings.audioTransport). Raw values are the words: auto, websocket, webrtc. Calls in both directions follow it.

What to expect — WebRTC needs the GraineInAppRTC package in the app: without it every call runs on the WebSocket, whatever is asked (.webrtc also logs a warning). The dashboard can only turn WebRTC off: its In-app call audio set to Standard wins over every choice, from the app's next start. A call WebRTC cannot set up continues on the WebSocket inside the same call, and transportFallbackReason says why.

CaseMeaning
case autoThe default: WebRTC when the GraineInAppRTC package is in the app and the dashboard allows it; the WebSocket otherwise.
case websocketAlways the WebSocket: the call's audio on the conversation's own connection. It works on any network, though the audio suffers wherever the network loses data.
case webrtcWebRTC wherever it can carry the call — a jitter buffer, loss concealment and the platform's echo canceller; the WebSocket only when the package is missing, the dashboard says Standard, or WebRTC cannot set up this call.

Its functions: from — each with its parameters, response, errors and an example.

GraineRTC

Use it only to make sure a stripped build keeps the WebRTC module: GraineRTC.install().

public enum GraineRTC — in the GraineInAppRTC package (import GraineInAppRTC): the WebRTC module for the native calls, in both directions.

What to expect — Adding the package is all the calls need: they find the module by themselves, and GraineCallsConfig.callTransport and the dashboard decide when it carries a call. Nothing to configure, nothing to call in a normal build.

Its functions: install — each with its parameters, response, errors and an example.

GraineRTCVersion

Read it to log which WebRTC module your app ships.

public enum GraineRTCVersion — in the GraineInAppRTC package: the module's version.

What to expect — A constant, always the same as GraineVersion.name — "0.10.3" in this release: the package is built against GraineInApp of exactly its own version. Include both when you ask for support about a call.

FieldTypeDefaultMeaning
static let name: StringString"0.9.0"The module's version — GraineInApp's, the one it is built against.

Calls Graine rings

Your agent rings the customer: a VoIP push — from Graine when your workspace has In-app calling set up, or from your own server — rings on the system's call screen, even when the app is closed. Calls the customer booked ring this way at their time. A ring the customer answers takes the same road as a call they place: WebRTC when the GraineInAppRTC package is in the app and GraineCallsConfig.callTransport allows it — prepared while it rings, so answering connects at once — else the WebSocket; GraineCalls.transport and transportFallbackReason say which carried it.

GraineCalls.enableVoIPPushes

Call it at every launch so your agent's calls can ring this iPhone.

func enableVoIPPushes(onToken: ((String?) -> Void)? = nil)

Receive your agent's calls as VoIP pushes. Call it at launch, every launch: iOS wakes a terminated app for a push and delivers it once this has run. Every VoIP push is reported to CallKit before its completion runs, as iOS requires. onToken gets the device's VoIP token as hex — at once when already known, again when it changes, nil when iOS invalidates it — for your own server. When the workspace has hosted ringing (Graine rings this app), the SDK registers the token with Graine itself, for every configured agent whose workspace rings iOS — on a new token, a new signed-in customer, a new time zone or app version, and otherwise at most once a day — and reports what became of every ring.

ParameterTypeDefaultMeaning
onToken((String?) -> Void)?nilGets the device's VoIP token as hex — at once when already known, again when it changes, nil when iOS invalidates it — for your own server. Leave it out when only Graine rings your app.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.calls.enableVoIPPushes()      // every launch, in didFinishLaunching
// Your own server rings too:
GraineInApp.shared.calls.enableVoIPPushes { token in myServer.saveVoipToken(token) }

GraineCalls.voipTokenUpdated

Call it only if your app runs its own push registry, with each new VoIP token.

func voipTokenUpdated(_ token: String?)

For an app with its own PKPushRegistry: the VoIP token (hex, or nil once invalidated), handled as enableVoIPPushes would handle it — registered with Graine where Graine rings this app.

ParameterTypeDefaultMeaning
tokenString?requiredThe device's VoIP token as hex, or nil once iOS has invalidated it.

Returns — nothing; it returns when done.

Errors — none.

func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
    let hex = credentials.token.map { String(format: "%02x", $0) }.joined()
    GraineInApp.shared.calls.voipTokenUpdated(hex)
}

GraineCalls.reportIncomingCall

Call it only if your app runs its own push registry, with each VoIP push.

func reportIncomingCall(payload: [AnyHashable: Any], completion: (() -> Void)? = nil)

For an app with its own PKPushRegistry: hands over a VoIP push's payload. It is reported to CallKit at once — before completion runs, as iOS requires — even when it is not a usable agent call or a call is already live; then it is ended at once, a live call is never disturbed, and a different call arriving mid-call is reported missed. The payload carries type: "agent_call" and a call_id, and optionally reason, caller_name, variables, hosted and agent_id (the agent the ring is for); the keys may sit under data.

ParameterTypeDefaultMeaning
payload[AnyHashable: Any]requiredThe VoIP push's payload exactly as PushKit delivered it (PKPushPayload.dictionaryPayload).
completion(() -> Void)?nilPushKit's completion handler: called once, on the main thread, after the push has been reported to CallKit.

Returns — nothing; it returns when done. A usable ring then rings (state → ringing, onIncomingCall). One it cannot ring is ended at once and reported through onCallOutcome with its call_id: .missed when another call is live, .failed("The push was not an agent call this SDK can ring.") when it is not an agent call; the same call pushed twice rings once.

Errors — none.

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload,
                  for type: PKPushType, completion: @escaping () -> Void) {
    GraineInApp.shared.calls.reportIncomingCall(payload: payload.dictionaryPayload, completion: completion)
}

GraineCallsListener.onIncomingCall

Implement it to know when an agent's call is ringing, for your own UI or analytics.

func onIncomingCall(_ call: GraineIncomingCall)

An agent's call is ringing on this device. The SDK shows the ring; this is for your own UI or analytics.

ParameterTypeDefaultMeaning
callGraineIncomingCallrequiredThe ring as its push described it: its id, reason, caller name, variables, whether Graine rang it (hosted) and the agent it is from.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineCallsListener {
    func onIncomingCall(_ call: GraineIncomingCall) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.calls.addListener(events)

GraineCalls.answer

Call it from your own Answer button while a call rings.

func answer()

Answers the ringing call from your own screen; the system's call screen answers by itself. Does nothing unless a call is ringing. The call is reported answered at once; if the microphone is then refused, or its prompt goes unanswered for a minute, the call ends failed. The conversation then connects as GraineCalls.startCall describes — one 20-second deadline, and connected only once its audio runs — so an answered call that cannot connect is reported answered, then failed with the reason. It takes the same road as a call the customer places: with the GraineInAppRTC package in the app, WebRTC, prepared while the ring showed (a declined or missed ring lets go of what it prepared), and the WebSocket inside the same call when WebRTC cannot carry it — transport and transportFallbackReason say which.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.calls.answer()

GraineCalls.callBack

Call it from your own Call back button after a missed ring.

@discardableResult func callBack() -> String?

Returns the missed call: the customer calls the agent that rang, with the missed call's reason (or returning_missed_call) as call_reason. The new call's id, or nil when there is nothing to return or a call is live.

Parameters — none.

Returns — String?. Example value: "call_6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44".

Errors — none.

if let callId = GraineInApp.shared.calls.callBack() {
    print("Calling back:", callId)
}

GraineCalls.dismissMissedCall

Call it when the customer has seen the missed call, so the call screen can go.

func dismissMissedCall()

Tells the SDK the customer has seen the missed call, so its screen can be dismissed.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.calls.dismissMissedCall()

GraineIncomingCall (constructor)

Use it to build a sample ring for previews and tests.

init(callId: String, reason: String? = nil, callerName: String? = nil, variables: [String: String] = [:], hosted: Bool = false, agentId: String? = nil)

Builds a ring by hand — for previews and tests of your own call screen or listener; the SDK makes the real ones from each push.

ParameterTypeDefaultMeaning
callIdStringrequiredThe call's ID: your server's call_id, Graine's for a ring it sent, or the one startCall returned.
reasonString?nilWhy the agent is calling — the agent hears it as call_reason.
callerNameString?nilThe name the call shows, over GraineCallsConfig.displayName.
variables[String: String][:]Prompt variables by name, for this call or conversation; masked on the device before they leave it.
hostedBoolfalseGraine rang it (the push said hosted: "1"); its outcomes are reported to Graine too.
agentIdString?nilThe agent the push says the call is from; the ring goes to the configured agent with that id.

Returns — a new GraineIncomingCall.

Errors — none.

let ring = GraineIncomingCall(callId: "c_42", reason: "Your order has shipped", callerName: "Acme Assistant")

GraineIncomingCall

Read it to know what a ring carries.

An agent's call as its push described it, delivered to GraineCallsListener.onIncomingCall(_:). A value type.

What to expect — When a ring reaches this device (a push from Graine or your server) — before the customer answers. It carries only what the ring shows: nothing personal.

FieldTypeDefaultMeaning
let callId: StringStringrequiredThe server's id for this call; every outcome carries it back.
let reason: String?String?nilThe reason for the agent's call, which the prompt reads as call_reason. A call Graine rang keeps its reason on Graine, so it is nil there.
let callerName: String?String?nilA name to ring with that takes precedence over GraineCallsConfig.displayName.
let variables: [String: String][String: String][:]Extra prompt variables for this call, from the push.
let hosted: BoolBoolfalseGraine rang this call itself (the push says hosted: "1"); the SDK reports its outcomes to Graine.
let agentId: String?String?nilThe agent the push says the call is from; the ring is routed to that configured agent (the default agent when none matches).

Its functions: init — each with its parameters, response, errors and an example.

GraineMissedCall (constructor)

Use it to build a sample missed call for previews and tests.

init(callId: String, callerName: String, reason: String? = nil, hosted: Bool = false, at: Date = Date(), agent: String? = nil)

Builds a missed call by hand — for previews and tests of a call screen; the SDK keeps the real one as GraineCalls.missedCall.

ParameterTypeDefaultMeaning
callIdStringrequiredThe call's ID: your server's call_id, Graine's for a ring it sent, or the one startCall returned.
callerNameStringrequiredThe name it rang with.
reasonString?nilWhy the agent called, when the push said.
hostedBoolfalseRung by Graine, as a call booked for this customer.
atDateDate()When it was missed.
agentString?nilThe name of the configured agent that rang; callBack() calls that agent.

Returns — a new GraineMissedCall.

Errors — none.

let missed = GraineMissedCall(callId: "c_42", callerName: "Acme Assistant", reason: "Your order has shipped", agent: "support")

GraineMissedCall

Read it to show a missed ring and offer to call back.

A ring nobody answered, kept as GraineCalls.missedCall so a call screen can say so and offer to call back. A value type.

What to expect — A ring nobody answered, kept for a call back (the call screen shows it for 30 seconds). at is when it rang.

FieldTypeDefaultMeaning
let callId: StringStringrequiredThe missed call's id: the push's call_id.
let callerName: StringStringrequiredThe name it rang with.
let reason: String?String?nilWhy the agent called, when the push said.
let hosted: BoolBoolfalseRung by Graine, as a call booked for this customer.
let at: DateDateDate()When it was missed.
let agent: String?String?nilThe name of the configured agent that rang; callBack() calls that agent.

Its functions: init — each with its parameters, response, errors and an example.

GraineHostedCallsSettings

Read it to know whether Graine rings this app, the booking rules it carries, and the dashboard's say on the calls' audio.

Whether Graine rings this app's devices, the booking settings and the In-app call audio setting, as the agent's settings carry them: GraineSessionConfig.calls. Made by the SDK.

What to expect — GraineSessionConfig.calls is nil when the agent's settings carry no calls section: nothing rings and nobody can book. Inside it, a missing value falls back: no ringing, every platform, the app's own ring timeout, booking off, and no say on the calls' audio (the app's callTransport decides).

FieldTypeDefaultMeaning
let hosted: BoolBoolfalseThis app's devices are rung by Graine (the workspace has uploaded its push credentials).
let platforms: [String][String][]Which platforms Graine rings: ios, android.
let ringTimeoutSeconds: Int?Int?nilHow long a ring Graine sent lasts before it is missed: the dashboard's Ring for, 20–60 seconds (a value outside is held to that range); nil uses the app's own ringTimeout.
let callName: String?String?nilThe ring name the dashboard set for the agent's calls, if it set one.
let usersCanBook: BoolBoolfalseWhether customers are allowed to book calls from inside the app (scheduleCall).
let maxDaysAhead: Int?Int?nilThe furthest ahead a booking may be, in days.
let minLeadMinutes: Int?Int?nilThe soonest a booking may be, in minutes.
let maxPendingPerUser: Int?Int?nilCalls one customer may have waiting.
let audioTransport: GraineCallTransport?GraineCallTransport?nilThe agent's In-app call audio setting (agent → Calls & scheduling): .websocket for Standard — every native call with this agent, in both directions, runs on the WebSocket whatever the app's GraineCallsConfig.callTransport — .webrtc for WebRTC (recommended), nil when the settings say nothing. Only .websocket changes anything: .webrtc and nil leave the app's callTransport to decide. It comes with the agent's settings, so a change reaches each install at the app's next start — no app release.
var ringsThisPlatform: Bool { get }Bool—Graine rings iOS devices for this agent: hosted, and platforms is empty or includes ios.

Permissions and the setup check

What a call needs from the phone, and from your app's setup, in one place. The SDK never asks on its own at launch: it asks for the microphone when a call starts without it, and requestCallPermissions() asks earlier, when your app chooses. callSetupProblems says what your app's setup still lacks — each problem is also logged once, at configure, with its fix (subsystem ai.graine.inapp). A problem is never an exception: calls still work where they can.

GraineCalls.callPermissions

Read it to see which call permissions the phone holds now.

var callPermissions: GraineCallPermissions { get }

The call permissions as the phone holds them when read: the microphone — .granted, .denied (not asked yet) or .blocked (refused: only Settings can change it) — and .notNeeded for notifications and the full-screen ring, which iOS does not need: CallKit rings and draws its own screen. Safe from any thread.

Returns — a GraineCallPermissions. Read it again when your app returns to the foreground: the customer may have changed Settings.

Errors — none.

if GraineInApp.shared.calls.callPermissions.microphone == .blocked { showMicrophoneOffNotice() }

GraineCalls.requestCallPermissions

Call it at a moment you choose — onboarding, a "Call me" screen — to ask for what a call needs.

func requestCallPermissions() async -> GraineCallPermissions
func requestCallPermissions(completion: @escaping (GraineCallPermissions) -> Void)

Asks for the call permissions the system can still ask for — on iOS the microphone, once — and answers them as they stand after. A microphone already granted or refused is not asked again: the answer comes at once. The SDK never asks on its own at launch; a call that starts without the microphone still asks then, as before. The completion runs on the main thread.

Returns — a GraineCallPermissions, after the customer answers (at once when there was nothing to ask).

Errors — none.

let p = await GraineInApp.shared.calls.requestCallPermissions()
if p.microphone == .blocked { GraineInApp.shared.calls.openCallSettings(.microphone) }

GraineCalls.openCallSettings

Call it to send the customer to the Settings page where a call permission is switched on.

@discardableResult func openCallSettings(_ item: GraineCallPermission) -> Bool

Opens your app's page in the Settings app, where the microphone (and, for an app that posts notifications, its notifications) is switched on. Safe from any thread: it opens from the main thread.

ParameterTypeDefaultMeaning
itemGraineCallPermissionrequiredThe permission the customer should change: .microphone, .notifications or .fullScreen. On iOS each opens your app's page.

Returns — Bool: true when Settings was asked to open.

Errors — none.

GraineInApp.shared.calls.openCallSettings(.microphone)

GraineCalls.callSetupProblems

Read it to learn what your app's setup still lacks for calls, in plain words.

var callSetupProblems: [GraineCallSetupProblem] { get }

What the app's setup still lacks for calls, read now — each a stable code and a message with the exact fix. The same problems are logged once each, as warnings, from configure on. Empty when nothing is missing.

CodeWhen
microphone_usage_missingInfo.plist has no NSMicrophoneUsageDescription: iOS closes the app when a call asks for the microphone.
background_audio_missingUIBackgroundModes lacks audio: a call goes silent when the screen locks.
background_voip_missingUIBackgroundModes lacks voip: iOS does not wake the app for a ring.
push_entitlement_missingThe build is signed without Push Notifications — read from the profile a development or ad hoc build embeds; an App Store build embeds none, so nothing is claimed.
voip_pushes_not_enabledenableVoIPPushes() has not run 15 seconds after configure (nor does the app hand over its own registry's token): calls can't ring.
no_voip_tokenNo VoIP token 15 seconds after VoIP pushes were enabled. Never reported on the simulator, which gets no token.
microphone_blockedThe microphone is refused: only Settings can let calls hear the customer.
hosted_ringing_offA session is known and no configured agent's workspace has push credentials for iOS: Graine can't ring this phone.

Returns — [GraineCallSetupProblem], the build's problems first, then what this launch has or has not done.

Errors — none.

for problem in GraineInApp.shared.calls.callSetupProblems { print(problem.code, problem.message) }

GraineCallPermission

Use it to name a call permission.

public enum GraineCallPermission: String, CaseIterable, Sendable — a permission a WhatsApp-style call needs. Raw values are the words every Graine SDK uses.

What to expect — Three permissions, the same on every SDK. On iOS only the microphone is ever asked for; the other two are .notNeeded.

CaseMeaning
case microphoneThe customer's voice. Asked by the SDK when a call starts without it, or by requestCallPermissions().
case notificationsA call that rings while the app is closed — an Android permission. iOS rings through CallKit, which needs none.
case fullScreenA ring that fills a locked screen — Android 14+. iOS: CallKit's own screen, nothing to allow.

GraineCallPermissionStatus

Read it to tell whether a call permission is allowed, can still be asked for, or only Settings can change it.

public enum GraineCallPermissionStatus: String, Sendable — where a call permission stands. Raw values are the words every Graine SDK uses.

What to expect — .granted and .notNeeded need nothing; .denied means requestCallPermissions() can ask; .blocked means only Settings can change it, so offer openCallSettings(_:).

FieldTypeDefaultMeaning
var isSatisfied: Bool { get }Bool—Granted, or nothing to grant.
CaseMeaning
case grantedAllowed.
case deniedNot allowed yet: requestCallPermissions() can ask for it. On iOS, the microphone never asked.
case blockedRefused for good: only the Settings app can change it. On iOS, the microphone once refused.
case notNeededThis phone needs no such permission.

GraineCallPermissions

Read it to see every call permission at once.

public struct GraineCallPermissions: Equatable, Sendable — every call permission's status, as callPermissions and requestCallPermissions() give them.

What to expect — A snapshot: read callPermissions again for the latest. allGranted is the one check before offering calls.

init(microphone: GraineCallPermissionStatus, notifications: GraineCallPermissionStatus, fullScreen: GraineCallPermissionStatus)
subscript(_ item: GraineCallPermission) -> GraineCallPermissionStatus { get }
FieldTypeDefaultMeaning
let microphone: GraineCallPermissionStatusGraineCallPermissionStatus—The microphone.
let notifications: GraineCallPermissionStatusGraineCallPermissionStatus—Notifications for a ring — .notNeeded on iOS.
let fullScreen: GraineCallPermissionStatusGraineCallPermissionStatus—The full-screen ring — .notNeeded on iOS.
var allGranted: Bool { get }Bool—Every permission is granted or not needed: calls can ring and connect.

init builds one (for a test or a preview); subscript reads one permission by name: permissions[.microphone].

GraineCallSetupProblem

Read it to show or log what your app's setup still lacks.

public struct GraineCallSetupProblem: Equatable, Sendable — one thing the app's setup still lacks for calls, from callSetupProblems.

What to expect — The code is stable and the same on every SDK (the list is under GraineCalls.callSetupProblems); the message is a sentence for the developer, with the exact fix.

init(code: String, message: String)
FieldTypeDefaultMeaning
let code: StringString—Stable: act on it, or count it.
let message: StringString—What is missing and how to fix it, in plain words.

Booking with the SDK's sheet

Let the customer book a call with no screen of your own: the SDK draws the booking sheet in your agent's design, books the call, and Graine rings this iPhone at that time. New in 0.8.0.

Booking a call: the SDK's sheet

Read this to know what the customer sees on the booking sheet, when it can book, and what each refusal says — before you offer it.

When it can book. bookingAvailable is true only when all three hold, for the agent you ask:

  • Graine rings this iPhone: your workspace has In-app calling set up (GraineHostedCallsSettings.hosted) and its platforms list is empty or names iOS;
  • the agent lets customers book (Users, from your app — schedulingRules.usersCanBook);
  • the customer is identified with the trait the agent's Identity setting names (identify; a placeholder such as "guest" is nobody).

The first two come from the agent's settings — this launch's, or the ones kept on the device; the third is read once this launch's settings have loaded, so bookingAvailable is false until then. Read it again after identify and after onSessionReady.

What the sheet shows, top to bottom, drawn in the agent's design from the Garage — its colours, typeface, corners and light or dark — as the chat window is:

  1. Book a call, and under it "call name will call you" — the agent's Name on the incoming call, else the chat window's title, else the heading ("Assistant" when none is set) — with a close button.
  2. Day — a chip for each day from the first day that still has a time to offer (usually today) to the furthest day the agent allows: "Today", "Tomorrow", then the weekday and date in the device's language ("Thu 16 Oct"). The chosen day is in the accent; the row scrolls sideways.
  3. Time — the system's time wheel, every 5 minutes, holding only the chosen day's times: from earliest(now:) rounded up to the next 5 minutes to latest(now:) rounded down to 5 minutes. Changing the day keeps the time where that day has it, else its nearest.
  4. The time-zone line: the device's time zone and the time picked, such as "Europe/London · Thu 16 Oct, 6:00 PM".
  5. What's it about? (optional) — one line, up to 120 characters (a line break becomes a space), filled with the reason you pass, which the customer may change. Sent as the booking's reason; left blank, none is sent.
  6. Book call — the accent, full width, with a spinner while it books. While a booking is on its way the sheet cannot be closed.

Once booked, the sheet says "Booked — call name will call you on day, time" — always the weekday and date, never "Today": "Booked — Acme Assistant will call you on Thu 16 Oct, 6:00 PM" — with Done. The booking is scheduleCall's, so the chat's Upcoming calls card refreshes at once.

Refusals stay in the sheet as one line under Book call, in the danger colour; the customer picks again. The numbers are the agent's own rules:

CodeThe line the sheet shows
too_soon"Pick a time at least 5 minutes from now" — the agent's lead time ("1 minute" for one). The offer moves on to the times still possible
too_far"Pick a time within 14 days"
outside_window"That time is outside the hours we can call — pick another"
too_many_pending"You already have 3 calls booked — cancel one first"
scheduling_off"Booking a call isn't available right now"
identity_required"Sign in to book a call"
anything else — rate_limited, unavailable, network, …"Couldn't book just now — try again"

When it cannot book. Opened while bookingAvailable is false, the sheet still opens, shows the scheduling_off or identity_required line, keeps Book call off and books nothing; presentBooking also logs a warning. When the rules leave no time to offer, there is no day to pick and Book call stays off.

Where it appears. On an iPhone, a sheet the customer can drag closed; on an iPad — when the app's window is at least 600 points on its shorter side — a centred panel 420 points wide (an iPad app in a narrow split gets the iPhone's sheet). That is presentBooking and GraineBookingViewController; .graineBookingSheet uses SwiftUI's own sheet, and GraineBookingView fills whatever you put it in.

In the chat window. Where bookingAvailable is true and the design keeps the booked-calls card (Booked calls in the dashboard, GraineChatTokens.showUpcomingCalls), GraineChatView and the bar's chat window show Book a call: under the booked calls in the Upcoming calls card, or alone as a compact row in the card's place when none are booked — never when the design hides the card. It opens this sheet, and a booking refreshes the card.

GraineInApp.bookingAvailable

Read it to decide whether to show your own "Call me" button.

var bookingAvailable: Bool { get }

Whether this agent can be booked from the app now: Graine rings this iPhone, the agent lets customers book, and the customer is identified with the trait the agent's Identity setting names — the rule in Booking a call: the SDK's sheet. Each agent answers for itself. It is worked out when you read it and is not published, so read it again after identify and after onSessionReady; it is false until this launch's settings have loaded. The chat window's Book a call follows the same rule.

Parameters — none.

Returns — Bool. Example value: true.

Errors — none.

final class CallMeButton: GraineListener {
    let button: UIButton
    init(button: UIButton) { self.button = button }
    func onSessionReady(_ session: GraineSessionConfig) {
        button.isHidden = !GraineInApp.shared.bookingAvailable   // again after identify
    }
}

GraineInApp.presentBooking

Call it to let the customer book a call on the SDK's own sheet, from anywhere in a UIKit or SwiftUI app.

@MainActor func presentBooking(reason: String? = nil) async -> GraineScheduledCall?

Shows the booking sheet over whatever is on screen — the key window's top view controller: a sheet on an iPhone, a centred panel on an iPad. Returns once the sheet has closed — by Done, its close button or a swipe — with the booked call, or nil when the customer closed it without booking. Also returns nil at once, showing nothing, when no window is on screen yet (the log says so) or a booking sheet is already open: one booking at a time. Check bookingAvailable before offering it; opened where it is false, the sheet says why and books nothing. It runs on the main actor: from other code, call it in a Task.

ParameterTypeDefaultMeaning
reasonString?nilWhat "What's it about? (optional)" starts with, such as "Talk through my order"; the customer may change it. Up to 120 characters; kept with the booking for the agent and never put in a push.

Returns — GraineScheduledCall? — its fields: GraineScheduledCall. Example value: GraineScheduledCall(id: "6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44", at: 2026-10-16 12:30:00 +0000, status: .scheduled, source: .app, reason: "Talk through my order").

Errors — none thrown: a refusal stays in the sheet as one line (the refusals) and the customer picks again; closing after a refusal returns nil.

Button("Call me back") {
    Task {
        if let call = await GraineInApp.shared.presentBooking(reason: "Talk through my order") {
            upcoming.append(call)
        }
    }
}
.disabled(!GraineInApp.shared.bookingAvailable)

View.graineBookingSheet

Use it in SwiftUI to present the booking sheet from a Bool binding, the way .sheet(isPresented:) works.

func graineBookingSheet(isPresented: Binding<Bool>, graine: GraineInApp? = nil, reason: String? = nil, onBooked: ((GraineScheduledCall) -> Void)? = nil) -> some View

Presents the booking sheet while isPresented is true, as a SwiftUI sheet. onBooked hears the call the moment it is booked — before the customer taps Done. The sheet sets isPresented back to false itself on Done or its close button; a swipe down closes it too, except while a booking is on its way.

ParameterTypeDefaultMeaning
isPresentedBinding<Bool>requiredShows the sheet while true; set back to false when it closes.
graineGraineInApp?nilThe configured agent to book with — another agent's (GraineInApp.named("support")) or the default when left out.
reasonString?nilWhat "What's it about? (optional)" starts with; the customer may change it.
onBooked((GraineScheduledCall) -> Void)?nilCalled on the main thread with the booked call, the moment it is booked.

Returns — some View: your view, with the sheet attached.

Errors — none: refusals stay in the sheet.

@State private var booking = false
 
Button("Call me back") { booking = true }
    .disabled(!GraineInApp.shared.bookingAvailable)
    .graineBookingSheet(isPresented: $booking, reason: "Talk through my order") { call in
        upcoming.append(call)
    }

GraineBookingView (constructor)

Use it to show the booking sheet's content in a presentation of your own — a full-screen cover, a popover, a panel.

init(graine: GraineInApp? = nil, reason: String? = nil, onBooked: ((GraineScheduledCall) -> Void)? = nil, onClose: @escaping (GraineScheduledCall?) -> Void)

The sheet's content as a SwiftUI view: the header, the day chips, the time wheel, the time-zone line, "What's it about? (optional)" and Book call, then the booked line and Done. It fills the space it is given on the design's surface and draws nothing before an agent is configured. You present it and you dismiss it: onClose says when.

ParameterTypeDefaultMeaning
graineGraineInApp?nilThe configured agent to book with — another agent's (GraineInApp.named("support")) or the default when left out.
reasonString?nilWhat "What's it about? (optional)" starts with; the customer may change it.
onBooked((GraineScheduledCall) -> Void)?nilCalled on the main thread with the booked call, the moment it is booked.
onClose(GraineScheduledCall?) -> VoidrequiredThe customer is done — Done or the close button: the booked call, or nil when nothing was booked. Dismiss the view here. A dismissal your own presentation makes (a swipe) does not call it.

Returns — a new GraineBookingView.

Errors — none: refusals stay inside the view.

.fullScreenCover(isPresented: $booking) {
    GraineBookingView(reason: "Talk through my order") { call in
        booking = false
        if let call = call { upcoming.append(call) }
    }
}

GraineBookingViewController (constructor)

Use it in a UIKit app to present the booking sheet yourself.

init(graine: GraineInApp? = nil, reason: String? = nil, done: ((GraineScheduledCall?) -> Void)? = nil)

The booking sheet as a view controller that sets its own presentation: a page sheet on an iPhone; on an iPad — the app's window at least 600 points on its shorter side — a centred form sheet 420 × 640 points. Present it with present(_:animated:). It dismisses itself on Done or its close button; a swipe down closes it too, except while a booking is on its way. done is called exactly once, after it has closed, however it closed. Made in code only — not from a storyboard. presentBooking(reason:) presents one for you.

ParameterTypeDefaultMeaning
graineGraineInApp?nilThe configured agent to book with — another agent's (GraineInApp.named("support")) or the default when left out.
reasonString?nilWhat "What's it about? (optional)" starts with; the customer may change it.
done((GraineScheduledCall?) -> Void)?nilCalled once, on the main thread, after the sheet has closed: the booked call, or nil when nothing was booked.

Returns — a new GraineBookingViewController.

Errors — none: refusals stay in the sheet.

let sheet = GraineBookingViewController(reason: "Talk through my order") { [weak self] call in
    guard let call = call else { return }
    self?.showBooked(call)
}
present(sheet, animated: true)

GraineBookingView

Use it when you present the booking sheet your own way in SwiftUI.

public struct GraineBookingView: View — the booking sheet's content, drawn by the SDK in the agent's design. Made with GraineBookingView(graine:reason:onBooked:onClose:).

What to expect — It reads the agent's booking rules when it appears, and again if the agent's settings arrive while it is open; it books with scheduleCall, and a refusal stays inside it as one line. It never dismisses itself: onClose tells you when to.

PropertyTypeMeaning
var body: some Viewsome ViewSwiftUI's view body; apps do not call it.

Its functions: init — with its parameters, response, errors and an example.

GraineBookingViewController

Use it when a UIKit screen presents the booking sheet.

public final class GraineBookingViewController: UIViewController, UIAdaptivePresentationControllerDelegate — the booking sheet hosted for UIKit, drawn in the agent's design.

What to expect — Presented, it adapts to the device (a sheet or a centred panel); it cannot be swiped away while a booking is on its way; done is called exactly once, after it has closed.

MemberWhat it does
override func viewDidLoad()UIKit's lifecycle callback, which builds the sheet; apps never call it.
func presentationControllerDidDismiss(_ presentationController: UIPresentationController)UIKit's callback when the customer swipes the sheet away; it calls done. Apps never call it.

Its functions: init — with its parameters, response, errors and an example.

The scheduled-calls list

The calls booked for this customer — by the agent in a conversation, by the customer in your app or on the SDK's sheet, by your team or through the API — with the rules a booking follows, and the chat's Upcoming calls card that shows them.

GraineInApp.schedulingRules

Read it to offer only the times the agent accepts on a booking screen of your own.

var schedulingRules: GraineSchedulingRules { get }

The rules for booking calls with this agent, from its settings — this launch's, or before they load, the last ones kept on the device; with neither, the defaults with booking off (usersCanBook false). scheduleCall checks a time against them before it sends anything, and the SDK's booking sheet offers only times between earliest(now:) and latest(now:). Read it again after onSessionReady.

Parameters — none.

Returns — GraineSchedulingRules — its fields: GraineSchedulingRules. Example value: GraineSchedulingRules(usersCanBook: true, minLeadMinutes: 5, maxDaysAhead: 14, maxPendingPerUser: 3, ringTimeoutSeconds: 45, callName: "Acme Assistant", hostedRinging: true, platforms: [.ios, .android]).

Errors — none.

let rules = GraineInApp.shared.schedulingRules
guard rules.usersCanBook else { return }
picker.minimumDate = rules.earliest()
picker.maximumDate = rules.latest()

GraineInApp.scheduleCall

Call it to book a call from a booking screen of your own.

func scheduleCall(at date: Date, reason: String? = nil) async throws -> GraineScheduledCall

Asks the agent to call this customer at date; Graine rings this device then, on the system's call screen. Waits for the agent's settings, then checks on the device before sending anything: scheduling_off when the agent does not let customers book, too_soon before schedulingRules.earliest(now:), too_far after latest(now:). reason (trimmed, cut to 120 characters) is what the agent is told the call is for; it never appears in the push. The time is sent with the device's offset and time zone; the booking is the identified customer's, or this install's when nobody is identified. Returns the booked call (status scheduled) and refreshes the chat's upcoming calls.

ParameterTypeDefaultMeaning
at dateDaterequiredWhen the agent should ring: between schedulingRules.earliest() and latest(). Sent with the device's offset and time zone.
reasonString?nilWhat the call is about, at most 120 characters — the agent hears it as call_reason when it rings.

Returns — GraineScheduledCall — its fields: GraineScheduledCall. Example value: GraineScheduledCall(id: "6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44", at: 2026-10-16 12:30:00 +0000, status: .scheduled, source: .app, reason: "Talk through my order").

Errors — GraineServiceError. On the device, before anything is sent (status 0), in the booking sheet's words: scheduling_off — "Booking a call isn't available right now"; too_soon — "Pick a time at least 5 minutes from now" ("1 minute" for one); too_far — "Pick a time within 14 days" (the numbers are the agent's rules). From Graine, in its own words: outside_window, too_many_pending, identity_required, device_limit, invalid_request, rate_limited, network, unavailable, invalid_key, origin_refused. unknown("unreadable_answer") — "The booking was accepted, but its answer did not describe the call." — when Graine took the booking but its answer could not be read: refresh scheduledCalls() before booking again. The SDK's booking sheet shows each as one line instead (Booking a call: the SDK's sheet).

do {
    let call = try await GraineInApp.shared.scheduleCall(at: picked, reason: "Talk through my order")
    show("We'll call you at \(call.at?.formatted(date: .abbreviated, time: .shortened) ?? "the time you picked").")
} catch let error as GraineServiceError {
    show(error.message)   // branch on error.code for your own words
}

GraineInApp.scheduledCalls

Call it to list the customer's calls still to come.

func scheduledCalls() async throws -> [GraineScheduledCall]

This customer's calls still to come — this install's when nobody is identified — upcoming first. Waits for the agent's settings first.

Parameters — none.

Returns — [GraineScheduledCall] — its fields: GraineScheduledCall. Example value: [GraineScheduledCall(id: "6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44", at: 2026-10-16 12:30:00 +0000, status: .scheduled, source: .app, reason: "Talk through my order")].

Errors — GraineServiceError: network, rate_limited, unavailable, invalid_key, origin_refused

let calls = try await GraineInApp.shared.scheduledCalls()

GraineInApp.cancelScheduledCall

Call it when the customer calls off a booked call.

@discardableResult func cancelScheduledCall(id: String) async throws -> GraineCallStatus

Calls off one of this customer's booked calls (this install's when nobody is identified); not_found for anything else. Returns the call's status afterwards — cancelled, also when Graine's answer names none.

ParameterTypeDefaultMeaning
idStringrequiredThe booking's id (a UUID), from GraineScheduledCall.id.

Returns — GraineCallStatus — its fields: GraineCallStatus. Example value: .cancelled.

Errors — GraineServiceError: not_found, network, rate_limited, unavailable, invalid_key, origin_refused

let calls = try await GraineInApp.shared.scheduledCalls()
if let call = calls.first(where: { $0.canCancel }) {
    let status = try await GraineInApp.shared.cancelScheduledCall(id: call.id)   // .cancelled
}

GraineChat.upcomingCalls

Read it to show the customer's booked calls in your own UI — the chat window's Upcoming calls card draws the same list.

@Published private(set) var upcomingCalls: [GraineScheduledCall] = []

This customer's calls still to come (this install's when nobody is identified), soonest first, at most three: only calls with a time that are still upcoming (isUpcoming). Fetched when the chat opens, after the agent finishes a reply — it may have just booked one — at most every 10 seconds, after any booking (scheduleCall, and so the booking sheet), and on refreshUpcomingCalls(). Only where Graine rings this app; elsewhere it stays empty. A cancelled call leaves the list once Graine confirms. Published on the main thread, so SwiftUI redraws as it changes.

Parameters — none.

Returns — [GraineScheduledCall] — its fields: GraineScheduledCall. Example value: [GraineScheduledCall(id: "6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44", at: 2026-10-16 12:30:00 +0000, status: .scheduled, source: .app, reason: "Talk through my order")].

Errors — none: a list that could not be fetched keeps what it had.

ForEach(GraineInApp.shared.chat.upcomingCalls) { call in
    Text(call.at.map { $0.formatted(date: .abbreviated, time: .shortened) } ?? "")
}

GraineChat.refreshUpcomingCalls

Call it after a booking made outside the chat, so the chat's Upcoming calls card shows it.

func refreshUpcomingCalls()

Fetches the upcoming calls again — after booking one elsewhere, say.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.chat.refreshUpcomingCalls()

GraineChat.cancelUpcomingCall

Call it from your own list to cancel a call shown in the chat's Upcoming calls.

func cancelUpcomingCall(id: String)

Cancels one of the card's calls. Its row says "Cancelling…" until Graine has it, then leaves upcomingCalls; a failure leaves the row saying "Couldn’t cancel · tap to retry". Nothing is thrown — use cancelScheduledCall(id:) to handle the error yourself.

ParameterTypeDefaultMeaning
idStringrequiredThe booking's id, from GraineChat.upcomingCalls.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.shared.chat.cancelUpcomingCall(id: "6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44")

GraineScheduledCall (constructor)

Use it to build sample booked calls for previews and tests.

init(id: String, at: Date?, status: GraineCallStatus = .scheduled, source: GraineCallSource? = nil, reason: String? = nil)

Builds a booked call by hand — for previews and tests of a screen that lists calls; scheduleCall and scheduledCalls() make the real ones.

ParameterTypeDefaultMeaning
idStringrequiredThe booking's id (a UUID) — what cancelScheduledCall(id:) takes.
atDate?requiredWhen it rings; nil only when Graine's time for it could not be read.
statusGraineCallStatus.scheduledWhere the booked call is: scheduled, ringing, answered, …
sourceGraineCallSource?nilWho booked it: the agent, your app, your team or the API; nil when not known.
reasonString?nilWhat the call is about, as it was booked (up to 120 characters).

Returns — a new GraineScheduledCall.

Errors — none.

let preview = GraineScheduledCall(id: "6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44", at: Date().addingTimeInterval(3600),
                                  source: .app, reason: "Talk through my order")

GraineScheduledCall

Read it to show one booked call.

A call the agent will make to this customer, booked for later: returned by scheduleCall(at:reason:) and scheduledCalls(), and listed in GraineChat.upcomingCalls. When its time comes, Graine rings this device on the system's call screen.

What to expect — From scheduleCall (status scheduled), scheduledCalls() (upcoming first) and the chat's upcoming calls. id is a UUID. at is a moment (null only when Graine's time could not be read): show it in the device's time zone. reason is at most 120 characters, or null. isUpcoming is scheduled or ringing; canCancel is scheduled only. A status or source newer than the SDK arrives as unknown, carrying its word.

FieldTypeDefaultMeaning
let id: StringStringrequiredGraine's id for the booking — what cancelScheduledCall(id:) takes.
let at: Date?Date?requiredWhen it rings. Nil only when Graine's time for it could not be read.
let status: GraineCallStatusGraineCallStatus.scheduledWhere the booked call is — see GraineCallStatus.
let source: GraineCallSource?GraineCallSource?nilWho booked it; nil when Graine does not say.
let reason: String?String?nilWhy, as it was booked (up to 120 characters); nil when none was given.
var isUpcoming: Bool { get }Bool—Still to come: scheduled, or ringing now.
var canCancel: Bool { get }Bool—It can still be called off: scheduled, not rung yet.

Its functions: init — each with its parameters, response, errors and an example.

GraineCallStatus (constructor)

Use it to turn a status word from your server into a GraineCallStatus.

init(wire: String)

The status for a wire word (trimmed, any case); unknown for any other word.

ParameterTypeDefaultMeaning
wireStringrequiredA status word as Graine or your server sends it — scheduled, no_device, …

Returns — a new GraineCallStatus: .unknown(word) for a word this build does not know.

Errors — none.

let status = GraineCallStatus(wire: "no_device")   // .noDevice

GraineCallStatus

Read it to show where a booked call is.

Where a booked call is. A status this build does not know arrives as unknown, carrying its word.

What to expect — scheduled → ringing → answered → completed; or declined (nothing rings that customer again for 6 hours), missed (rang out), failed, cancelled (called off before it rang), expired (its time passed while it could not ring: more than 10 minutes late, or no opening left in the calling hours), no_device (no device of the customer's seen in the last 30 days — a browser cannot ring). A status newer than this SDK arrives as unknown, carrying its word: give every switch a default.

FieldTypeDefaultMeaning
var wire: String { get }String—The word on the wire: scheduled, …, no_device; an unknown status's own word.
var description: String { get }String—The same as wire, for printing and logs.
CaseMeaning
case scheduledWire scheduled. Booked, waiting for its time.
case ringingWire ringing. Ringing on the customer's device.
case answeredWire answered. Picked up; the conversation is on.
case completedWire completed. The conversation ended.
case declinedWire declined. The customer declined it.
case missedWire missed. Nobody answered within the ring timeout.
case failedWire failed. It could not connect.
case cancelledWire cancelled. Called off.
case expiredWire expired. It could not ring in time.
case noDeviceWire no_device. There was no device belonging to this customer for Graine to ring.
case unknown(String)A status this build does not know, with its word.

Its functions: init — each with its parameters, response, errors and an example.

GraineCallSource (constructor)

Use it to turn a source word into a GraineCallSource.

init(wire: String)

The source for a wire word (trimmed, any case); sdk and app are both .app.

ParameterTypeDefaultMeaning
wireStringrequiredA source word — agent, sdk (or app), dashboard or api.

Returns — a new GraineCallSource: .unknown(word) for any other word.

Errors — none.

let source = GraineCallSource(wire: "sdk")   // .app

GraineCallSource

Read it to show who booked a call.

Who booked a call. A source this build does not know arrives as unknown, carrying its word.

What to expect — Who booked it: the agent in a conversation, your app (the word Graine sends is sdk), your team in the dashboard, or the API. A source newer than this SDK arrives as unknown, carrying its word.

FieldTypeDefaultMeaning
var wire: String { get }String—The word on the wire; sdk for .app.
var description: String { get }String—The same as wire, for printing and logs.
CaseMeaning
case agentWire agent. The agent, in a conversation.
case appWire sdk. The app's own booking, through scheduleCall.
case dashboardWire dashboard. Your team, in the dashboard.
case apiWire api. The API.
case unknown(String)A source this build does not know, with its word.

Its functions: init — each with its parameters, response, errors and an example.

GraineSchedulingRules (constructor)

Use it to build rules for previews and tests of a booking screen.

init(usersCanBook: Bool = false, minLeadMinutes: Int = 5, maxDaysAhead: Int = 14, maxPendingPerUser: Int = 3, ringTimeoutSeconds: Int = 45, callName: String? = nil, hostedRinging: Bool = false, platforms: [GraineRingPlatform] = [])

Builds a set of rules by hand — for previews and tests of a booking screen; GraineInApp.schedulingRules gives the agent's.

ParameterTypeDefaultMeaning
usersCanBookBoolfalseWhether customers are allowed to book calls from inside the app.
minLeadMinutesInt5The soonest a booking may be, in minutes from now.
maxDaysAheadInt14The furthest ahead a booking may be, in days.
maxPendingPerUserInt3How many calls one customer may have waiting.
ringTimeoutSecondsInt45How long a ring Graine sends lasts before it is missed, in seconds.
callNameString?nilThe name the agent's calls ring with.
hostedRingingBoolfalseWhether Graine rings this app's devices.
platforms[GraineRingPlatform][]Which platforms Graine rings; empty means every platform.

Returns — a new GraineSchedulingRules.

Errors — none.

let preview = GraineSchedulingRules(usersCanBook: true, minLeadMinutes: 15, maxDaysAhead: 7)

GraineSchedulingRules.earliest

Call it for the earliest time your booking screen should offer.

func earliest(now: Date = Date()) -> Date

The soonest time a call may be booked for, which is now plus minLeadMinutes.

ParameterTypeDefaultMeaning
nowDateDate()The moment to count from; now when left out.

Returns — Date: the earliest moment a booking may be for. Example value: now plus 5 minutes.

Errors — none.

picker.minimumDate = GraineInApp.shared.schedulingRules.earliest()

GraineSchedulingRules.latest

Call it for the latest time your booking screen should offer.

func latest(now: Date = Date()) -> Date

The latest: now plus maxDaysAhead days.

ParameterTypeDefaultMeaning
nowDateDate()The moment to count from; now when left out.

Returns — Date: the latest moment a booking may be for. Example value: now plus 14 days.

Errors — none.

picker.maximumDate = GraineInApp.shared.schedulingRules.latest()

GraineSchedulingRules

Read it to offer only the times the agent accepts.

The rules for booking calls, from the agent's settings: GraineInApp.schedulingRules. scheduleCall checks a time against them before it asks, and a booking screen can offer only times between earliest(now:) and latest(now:).

What to expect — A property: no request. Read from the agent's settings when they load; before that the defaults hold, with booking off (iOS and Android use the last rules kept on the device). scheduleCall refuses on the device first — scheduling_off, too_soon (before earliest(now) or not in the future), too_far (after latest(now)) — with status 0; Graine then checks the calling hours and the calls already waiting. ringTimeoutSeconds is 20–60 (45 by default): every SDK holds a value outside that range to it.

FieldTypeDefaultMeaning
let usersCanBook: BoolBoolfalseCustomers may book calls from the app. False when the agent's settings say nothing.
let minLeadMinutes: IntInt5The soonest a booking may be, in minutes from now.
let maxDaysAhead: IntInt14The furthest ahead a booking may be, in days.
let maxPendingPerUser: IntInt3Calls one customer may have waiting.
let ringTimeoutSeconds: IntInt45How long a ring from Graine goes on before it counts as missed.
let callName: String?String?nilThe ring name the dashboard set for the agent's calls, if it set one.
let hostedRinging: BoolBoolfalseThis app's devices are rung by Graine (the workspace has uploaded its push credentials).
let platforms: [GraineRingPlatform][GraineRingPlatform][]Which platforms Graine rings; empty means every platform.

Its functions: init, earliest, latest — each with its parameters, response, errors and an example.

GraineRingPlatform

Read it to see which platforms Graine rings.

A platform Graine rings, in GraineSchedulingRules.platforms. Raw values are the wire words.

What to expect — Give every switch over it a default case.

CaseMeaning
case iosiOS devices.
case androidAndroid devices.

Design

The brand's design from the Garage, as tokens: the SDK's own views draw from them, and your own UI can too. A design reaches the SDK with no release; without one, the views draw from the dashboard's appearance settings.

GraineInApp.design

Read it to get both of the brand's token sets — light and dark — and the brand's choice between them.

var design: GraineDesign? { get }

The brand's design from the agent's settings (the Garage), resolved by Graine into two complete token sets. Nil before the settings load, and when the agent has no design — the SDK's views then draw from the dashboard's appearance settings, as before designs. Most apps read theme instead, which has already picked the set to draw.

Parameters — none.

Returns — GraineDesign? — its fields: GraineDesign. Example value: GraineDesign(scheme: .auto, preset: nil, light: …, dark: …) — both sets complete.

Errors — none.

let dark = GraineInApp.shared.design?.dark   // the dark set, whatever the device says

GraineInApp.theme

Read it to draw your own UI in the brand's design right now.

var theme: GraineThemeTokens? { get }

The design tokens to draw right now: the brand's fixed scheme, else the device's current light or dark, picked again whenever the device switches. Nil without a design. Up to date when onSessionReady is called; GraineListener.onTheme reports each change.

Parameters — none.

Returns — GraineThemeTokens? — its fields: GraineThemeTokens. Example value: a token set whose colors.accent is "#3740B8".

Errors — none.

if let t = GraineInApp.shared.theme {
    payButton.backgroundColor = UIColor(hex: t.colors.accent)   // your own hex helper
    payButton.layer.cornerRadius = t.shape.radius.md
}

GraineDesign.tokens

Call it to get the token set for the device's light or dark appearance.

func tokens(deviceDark: Bool?) -> GraineThemeTokens

Which tokens to draw: the brand's fixed scheme, else the device's current appearance — light when the device has not said (nil). Ask again when the device switches.

ParameterTypeDefaultMeaning
deviceDarkBool?requiredWhether the device is in dark mode now; nil when it has not said (then light).

Returns — GraineThemeTokens — its fields: GraineThemeTokens.

Errors — none.

if let design = GraineInApp.shared.design {
    let tokens = design.tokens(deviceDark: traitCollection.userInterfaceStyle == .dark)
}

GraineDesign.parse

Call it only to read a design you hold as JSON; the SDK reads the agent's for you.

static func parse(_ raw: Any?) -> GraineDesign?

Reads the design value of an agent's settings. Nil when there is none worth drawing — absent, not an object, or either scheme without a complete set of colours; nil is not an error. Used by the SDK; apps rarely need it.

ParameterTypeDefaultMeaning
rawAny?requiredThe design value of the agent's settings, as JSON.

Returns — GraineDesign?. Example value: nil.

Errors — none.

let design = GraineDesign.parse(settingsJSON["design"])   // nil: draw from the appearance settings

GraineDesign

Read it to get both of the brand's token sets and the brand's choice between them.

The brand's design: two complete token sets, light and dark, and the brand's choice between them — GraineInApp.design. Resolved by Graine, so a new design style reaches the SDK as tokens with no release.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

FieldTypeDefaultMeaning
let scheme: GraineDesignSchemeGraineDesignScheme.autoA fixed scheme, or .auto to follow the device.
let preset: String?String?—The design's preset name — informational; the tokens are already resolved.
let light: GraineThemeTokensGraineThemeTokens—The token set for light appearance.
let dark: GraineThemeTokensGraineThemeTokens—The token set for dark appearance.

Its functions: tokens, parse — each with its parameters, response, errors and an example.

GraineDesignScheme

Read it to know whether the brand fixed light or dark.

Which of a design's two token sets to draw. Raw values are the wire words.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

CaseMeaning
case autoFollow the device's light or dark.
case lightAlways the light set.
case darkAlways the dark set.

GraineThemeTokens

Use it to draw your own UI in the brand's design.

One scheme's design tokens — flat numbers and #RRGGBB strings, lengths in points — for an app that draws its own UI in the brand's design: GraineInApp.theme, onTheme or GraineDesign.tokens(deviceDark:). A missing or malformed value falls back to Graine Classic's, and a scheme without a complete set of colours makes the whole design nil.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

FieldTypeDefaultMeaning
let preset: StringString—The design style these tokens came from.
let scheme: StringString—Tells which of the design's two sets this is — "light" or "dark".
let colors: ColorsGraineThemeTokens.Colors—The palette — see GraineThemeTokens.Colors.
let font: TypographyGraineThemeTokens.Typography—Typefaces, sizes, weights and spacing — see GraineThemeTokens.Typography.
let shape: ShapeGraineThemeTokens.Shape—Corners, outline width and the launcher's outline — see GraineThemeTokens.Shape.
let spacing: SpacingGraineThemeTokens.Spacing—Spacing — see GraineThemeTokens.Spacing.
let elevation: ElevationGraineThemeTokens.Elevation—Shadows, or a solid offset — see GraineThemeTokens.Elevation.
let motion: MotionGraineThemeTokens.Motion—How things move — see GraineThemeTokens.Motion.
let icons: IconsGraineThemeTokens.Icons—Icon style and the launcher glyph — see GraineThemeTokens.Icons.
let glass: Glass?GraineThemeTokens.Glass?—Translucent surfaces; nil for opaque ones — see GraineThemeTokens.Glass.
let bubbles: GraineBubbleStyleGraineBubbleStyle—How chat messages sit on the page.
let chrome: GraineDesignChromeGraineDesignChrome—.window: the chat window, the caption card and every component card open as desktop windows.
let chat: GraineChatTokens?GraineChatTokens?—The chat window's own tokens; nil from an older server, when the chat draws from colors.
let call: GraineCallTokens?GraineCallTokens?—The call screen's tokens; nil from an older server, when the call screen draws as it did before designs.

Its functions: launcherGlyphUrl — each with its parameters, response, errors and an example.

GraineThemeTokens.Colors

Read it to colour your own UI exactly as the agent's design does.

public struct GraineThemeTokens.Colors — the palette of one scheme, as GraineThemeTokens.colors: seventeen colours, each a #RRGGBB string in upper case.

What to expect — Never partial. A design must carry all seventeen colours, each a valid colour, in both its light and its dark set; if any is missing or malformed, the SDK treats the whole design as absent (GraineInApp.design is nil) and draws from the dashboard's appearance settings instead. There are no defaults to fall back to.

FieldTypeDefaultMeaning
let background: StringString—The page colour behind everything in the chat window — use it for your own full-screen backgrounds.
let surface: StringString—The voice bar, the chat header and sheets (the booking sheet included) — use it for your own bars and panels.
let surfaceRaised: StringString—Cards, the composer field and unselected chips — use it for anything that sits on a surface.
let text: StringString—Primary text.
let textMuted: StringString—Secondary text, captions and status lines.
let border: StringString—Hairlines and outlines.
let accent: StringString—Primary buttons, the launcher, the send button and anything selected.
let onAccent: StringString—Text and icons drawn on the accent, already checked for contrast.
let accentSoft: StringString—Chips, selected rows and the focus halo; the customer's bubbles in the minimal style.
let success: StringString—Success states, such as the online dot in a header on the surface colour.
let warning: StringString—Warnings.
let danger: StringString—The end-call button, and errors such as the booking sheet's refusal line.
let userBubble: StringString—The customer's chat bubbles in the filled style.
let userBubbleText: StringString—Text on the customer's bubbles.
let agentBubble: StringString—The agent's chat bubbles in the filled style.
let agentBubbleText: StringString—Text on the agent's bubbles.
let scrim: StringString—What dims the app behind a modal sheet, drawn at 40% opacity.

GraineThemeTokens.Typography

Read it to set your own text in the design's typefaces, sizes and weights.

public struct GraineThemeTokens.Typography — the type of one scheme, as GraineThemeTokens.font: two families, a size scale, weights, letter spacing and line height.

What to expect — A missing or malformed value falls back to Graine Classic's, shown as the default. The SDK downloads a family the design names, from Graine, unless your app ships it (GraineConfig.fonts); the SDK's own views draw the system font at the same weight until it arrives, or if it never does.

FieldTypeDefaultMeaning
let body: StringString"system"The family for body text: "system" is the platform's own font (San Francisco); any other name is a family to load.
let heading: StringString"system"The family for titles and headings, read the same way.
let scale: ScaleGraineThemeTokens.Typography.Scalecaption 12 · body 15 · title 17 · display 22The text sizes, in points.
let weights: WeightsGraineThemeTokens.Typography.Weightsbody 400 · strong 600 · title 600The weights, as CSS numbers.
let letterSpacing: PairGraineThemeTokens.Typography.Pairbody 0 · title -0.01Letter spacing in em — multiply by the size for points; held to -0.5 to 0.5.
let lineHeight: PairGraineThemeTokens.Typography.Pairbody 1.45 · title 1.25Line height as a multiple of the size.
struct Scale, struct Weights, struct Pairnested types—See GraineThemeTokens.Typography.Scale, .Weights and .Pair.

GraineThemeTokens.Typography.Scale

Read it to size your own text on the design's scale.

public struct GraineThemeTokens.Typography.Scale — four sizes, in points, as GraineThemeTokens.font.scale.

What to expect — Each is more than 0 and at most 200; a value that is not falls back to Graine Classic's.

FieldTypeDefaultMeaning
let caption: DoubleDouble12Small text: captions, labels and status lines.
let body: DoubleDouble15Body text and chat messages.
let title: DoubleDouble17Titles, such as a header's.
let display: DoubleDouble22The largest text, for display.

GraineThemeTokens.Typography.Weights

Read it to weight your own text the way the design does.

public struct GraineThemeTokens.Typography.Weights — three weights as CSS numbers, as GraineThemeTokens.font.weights.

What to expect — Each is a whole number from 100 to 900 (a value outside is held to that range and rounded); a missing one falls back to Graine Classic's. 400 is regular and 600 semibold.

FieldTypeDefaultMeaning
let body: IntInt400Regular text.
let strong: IntInt600Emphasis: labels and buttons.
let title: IntInt600Titles.

GraineThemeTokens.Typography.Pair

Read it for a value that differs between body text and titles.

public struct GraineThemeTokens.Typography.Pair — one value for body text and one for titles; used for letterSpacing (in em) and lineHeight (a multiple of the size).

What to expect — Filled in from Graine Classic's when the design does not say.

FieldTypeDefaultMeaning
let body: DoubleDoublesee TypographyThe value for body text.
let title: DoubleDoublesee TypographyThe value for titles.

GraineThemeTokens.Shape

Read it for the design's corners, outline width and launcher shape.

public struct GraineThemeTokens.Shape — as GraineThemeTokens.shape.

What to expect — Filled in from Graine Classic's when the design does not say: the radii above, a 1-point outline and a round launcher.

FieldTypeDefaultMeaning
let radius: RadiusGraineThemeTokens.Radius8 · 12 · 18 · 24The corner radii.
let borderWidth: DoubleDouble1The width of every outline, in points, from 0 to 8.
let launcher: GraineLauncherShapeGraineLauncherShape.circleThe floating launcher's outline.

GraineThemeTokens.Radius

Read it to round your own corners the way the design does.

public struct GraineThemeTokens.Radius — four corner radii, in points, as GraineThemeTokens.shape.radius.

What to expect — Each is 0 to 200; a missing or negative one falls back to Graine Classic's.

FieldTypeDefaultMeaning
let sm: DoubleDouble8Small corners: a component's chips, and the square launcher.
let md: DoubleDouble12Buttons and text fields.
let lg: DoubleDouble18Cards, and the rounded-square launcher.
let xl: DoubleDouble24The roundest corners: pill-shaped chips such as the chat's starters and the booking sheet's days.

GraineThemeTokens.Spacing

Read it to space your own UI on the design's rhythm.

public struct GraineThemeTokens.Spacing — as GraineThemeTokens.spacing, in points.

What to expect — Each is 0 to 200; a missing or negative one falls back to Graine Classic's.

FieldTypeDefaultMeaning
let unit: DoubleDouble4The base step: other spacing is a multiple of it.
let cardPadding: DoubleDouble14The padding inside a card.
let gap: DoubleDouble10The gap between items in a list or row.

GraineThemeTokens.Elevation

Read it to give your own cards and floating views the design's depth.

public struct GraineThemeTokens.Elevation — as GraineThemeTokens.elevation.

What to expect — When the design says nothing about elevation, Graine Classic's soft shadows and no hard offset. When it names a shadow as empty, that shadow is nil: draw none.

FieldTypeDefaultMeaning
let card: Shadow?GraineThemeTokens.Shadow?Classic's card shadowThe shadow under cards; nil for none.
let floating: Shadow?GraineThemeTokens.Shadow?Classic's floating shadowThe shadow under floating things — the bar, the launcher, sheets; nil for none.
let hardOffset: Offset?GraineThemeTokens.Offset?nilA solid offset shadow, drawn instead of soft ones by designs that use it; nil for none.

GraineThemeTokens.Shadow

Read it to draw a soft shadow the way the design does.

public struct GraineThemeTokens.Shadow — one soft shadow, used by GraineThemeTokens.Elevation; lengths in points.

What to expect — Values are held to their ranges; a missing one takes the value of Graine Classic's shadow for that place (the card's or the floating one's, shown as card · floating).

FieldTypeDefaultMeaning
let y: DoubleDouble2 · 8How far down the shadow falls, -64 to 64.
let blur: DoubleDouble8 · 24How soft it is: the blur radius, 0 to 200.
let spread: DoubleDouble0 · 0How far it grows past the shape, -64 to 64.
let opacity: DoubleDouble0.08 · 0.16How dark it is, 0 to 1, applied to color.
let color: StringString"#000000"The shadow's colour, #RRGGBB.

GraineThemeTokens.Offset

Read it to draw the solid, offset shadow some designs use instead of a soft one.

public struct GraineThemeTokens.Offset — a hard shadow: the shape repeated behind itself, moved by x and y points, drawn in colors.text. As GraineThemeTokens.elevation.hardOffset.

What to expect — Present only when the design uses one; each value is held to -24 to 24.

FieldTypeDefaultMeaning
let x: DoubleDouble—How far right the shadow sits, in points (negative: left).
let y: DoubleDouble—How far down the shadow sits, in points (negative: up).

GraineThemeTokens.Motion

Read it to move your own UI at the design's pace.

public struct GraineThemeTokens.Motion — as GraineThemeTokens.motion.

What to expect — Filled in from Graine Classic's when the design does not say: 200 ms, "standard", no bounce. Ask effectiveDurationMs(reduceMotion:) rather than reading durationMs, so Reduce Motion is honoured.

FieldTypeDefaultMeaning
let durationMs: DoubleDouble200How long a transition runs, 0 to 2,000 milliseconds; 0 means no transitions.
let easing: StringString"standard"The curve: "standard", "emphasized", "spring", "linear" or "none" (no movement); a newer word is drawn as "standard".
let bounce: DoubleDouble0How much a "spring" overshoots, 0 to 1.
func effectiveDurationMs(reduceMotion: Bool) -> DoubleDouble—The duration to use: 0 when Reduce Motion is on or the easing is "none", else durationMs. See GraineThemeTokens.Motion.effectiveDurationMs.

GraineThemeTokens.Motion.effectiveDurationMs

Call it to time your own transitions the way the design does, while honouring the system's Reduce Motion.

func effectiveDurationMs(reduceMotion: Bool) -> Double

How long a transition runs, in milliseconds: 0 — no movement — when Reduce Motion is on or the design's easing is "none"; otherwise the design's durationMs (never below 0). The system's setting always wins over the brand's choice.

ParameterTypeDefaultMeaning
reduceMotionBoolrequiredWhether Reduce Motion is on — UIAccessibility.isReduceMotionEnabled, or SwiftUI's accessibilityReduceMotion environment value.

Returns — Double: milliseconds, 0 for no transition. Example value: 200.

Errors — none.

let ms = GraineInApp.shared.theme?.motion.effectiveDurationMs(reduceMotion: UIAccessibility.isReduceMotionEnabled) ?? 0
UIView.animate(withDuration: ms / 1000) { panel.alpha = 1 }

GraineThemeTokens.Icons

Read it to match the design's icon style and the launcher's glyph.

public struct GraineThemeTokens.Icons — as GraineThemeTokens.icons.

What to expect — Filled in from Graine Classic's when the design does not say: outline icons, a 2-point stroke, the waveform glyph. A design that shows the brand's logo on the launcher has no glyph (nil).

FieldTypeDefaultMeaning
let style: StringString"outline"The style of the SDK's own icons: "outline", "filled", "duotone" or "sharp".
let stroke: DoubleDouble2The line width of icons and stroked glyphs, 0.5 to 4 points.
let launcher: Glyph?GraineThemeTokens.Glyph?the waveformThe launcher's glyph; nil when the launcher shows the brand's logo.

GraineThemeTokens.Glyph

Read it to draw the brand's launcher glyph yourself, from its path.

public struct GraineThemeTokens.Glyph — one glyph as SVG path data on a 24 × 24 grid, as GraineThemeTokens.icons.launcher.

What to expect — When the design says nothing about its glyph, Graine Classic's "waveform". An app that would rather not draw paths asks launcherGlyphUrl(origin:points:scale:) for an image instead.

FieldTypeDefaultMeaning
let id: StringString"waveform"The glyph's name, lower-case — what GraineGlyph.url takes.
let path: StringStringthe waveform's pathThe drawing: SVG path data on a 24 × 24 grid.
let stroke: BoolBooltrueTrue: draw it as a line icons.stroke wide; false: fill it.

GraineThemeTokens.Glass

Read it to draw translucent surfaces the way a glass design does.

public struct GraineThemeTokens.Glass — as GraineThemeTokens.glass, which is nil for an opaque design.

What to expect — Present only for a design with translucent surfaces; a missing value inside it falls back to 20-point blur and 0.72 opacity.

FieldTypeDefaultMeaning
let blur: DoubleDouble20How much the background behind a translucent surface is blurred, 0 to 200 points.
let surfaceOpacity: DoubleDouble0.72The opacity colors.surface is drawn at over that blur, 0 to 1.

GraineThemeTokens.launcherGlyphUrl

Call it to get the launcher glyph as an image when you draw your own launcher.

func launcherGlyphUrl(origin: String?, points: Double, scale: Double) -> String?

The launcher glyph as an image URL, in colors.onAccent, for a host that draws its own launcher and would rather not parse SVG. points × scale is the physical size, so it is sharp on every screen. Nil when the brand chose its logo.

ParameterTypeDefaultMeaning
originString?requiredWhere Graine is served — your GraineConfig.baseUrl; https://www.graine.ai when nil or empty.
pointsDoublerequiredHow big you draw it, in points.
scaleDoublerequiredThe screen's scale (UIScreen.main.scale), so the image is sharp: points × scale pixels.

Returns — String?. Example value: "…".

Errors — none.

let url = GraineInApp.shared.theme?.launcherGlyphUrl(origin: "https://www.graine.ai", points: 28, scale: 3)

GraineChatTokens

Read it to draw a chat window of your own the way the agent's design does.

How the chat window is drawn — its header, its composer, its words: GraineThemeTokens.chat, nil from an older server. A missing or malformed field falls back on its own, as listed.

What to expect — The chat's framing, as the SDK's own chat window uses it. Public so a chat window of your own can reuse it.

FieldTypeDefaultMeaning
let title: StringString""Title shown in the header; when empty, the dashboard's heading is used.
let subtitle: StringString""Under the title; empty means "Online" or "Connecting…".
let avatar: StringString"logo"The header's mark: logo, glyph or none.
let placeholder: StringString"Message…"The composer's placeholder.
let showTimestamps: BoolBoolfalseShow each message's time.
let suggestions: [String][String][]Up to four replies (40 characters each) offered before the customer has said anything.
let background: StringStringcolors.backgroundBehind the messages.
let pattern: StringString"none"Over the thread: none or dots.
let patternColor: StringStringcolors.borderThe dots' colour (unused with no pattern).
let headerBackground: StringStringcolors.surfaceThe header's fill.
let headerText: StringStringcolors.textThe header's title colour.
let headerSubtext: StringStringcolors.textMutedThe header's subtitle colour.
let composerBackground: StringStringcolors.surfaceRaisedThe composer's fill.
let composerText: StringStringcolors.textThe composer's text colour.
let composerPlaceholder: StringStringcolors.textMutedThe placeholder's colour.
let composerBorder: StringStringcolors.borderThe composer's outline.
let size: GraineChatSizeGraineChatSize.compactHow big the window opens: the dashboard's Window size.
let showUpcomingCalls: BoolBooltrueThe card of the customer's booked calls at the top of the chat (Booked calls in the dashboard).
let upcomingTitle: StringString""That card's title (Card title); empty means "Upcoming call" / "Upcoming calls".
let showHistory: BoolBooltrueThe conversation kept from before, on screen (Earlier messages). Off, the agent still has it.
let newConversation: BoolBooltrueThe header's compose control (New conversation button). Turning it off does not stop GraineChat.newConversation() from working.

GraineCallTokens

Read it to draw a call screen of your own the way the agent's design does.

How the SDK's call screen (GraineCallView) is drawn while the app is open — the system's own call screen is the system's: GraineThemeTokens.call, nil from an older server. A missing or malformed field falls back to the screen the SDK drew before designs, as listed.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

FieldTypeDefaultMeaning
let background: StringString"#0E0F11"The screen's fill (the gradient's top, with backgroundTo).
let backgroundTo: String?String?nilWhen set, the background is drawn as a gradient: background at the top, blending into this at the bottom.
let text: StringString"#F5F5F4"The name and the agent's caption.
let textMuted: StringString"#A0A5AE"The status line, button labels and the customer's caption.
let avatar: StringString"logo"logo, glyph or initials; a logo or glyph the brand lacks falls back to the other, then to the initial.
let avatarShape: StringString"circle"circle, squircle or rounded-square.
let avatarFill: StringString"#318CE7"Behind a glyph or initial.
let avatarText: StringString"#FFFFFF"The glyph or initial.
let buttonFill: StringString"#333435"A control's fill.
let buttonIcon: StringString"#F5F5F4"A control's icon.
let buttonActiveFill: StringString"#F5F5F4"A control that is on (muted, the speaker).
let buttonActiveIcon: StringString"#0E0F11"That control's icon.
let endFill: StringString"#DC2626"The end button's fill.
let endIcon: StringString"#FFFFFF"The end button's icon.
let endShape: StringString"round"round or pill.
let acceptFill: StringString"#16A34A"The Answer button's fill.
let acceptIcon: StringString"#FFFFFF"The Answer button's icon.
let declineFill: StringString"#DC2626"The Decline button's fill.
let declineIcon: StringString"#FFFFFF"The Decline button's icon.
let callbackFill: StringString"#318CE7"The Call back button's fill, on a missed call.
let callbackIcon: StringString"#FFFFFF"The Call back button's icon.
let captions: BoolBooltrueShow the line being said.
let timer: BoolBooltrueShow the call's timer.
let controls: [String][String]["mute", "speaker", "chat", "end"]The controls in order, from mute, speaker, chat and end; end is always added. The iOS call screen leaves chat out, because the chat is held during a native call.
let ringtone: StringString"system"system, graine or soft — the system's call screen rings with it when the app sets no ringtoneSound.
let statusText: StatusTextGraineCallTokens.StatusTextsee belowThe status words.

GraineCallTokens.StatusText

Read it for the brand's words on the call screen.

The status words on the SDK's call screen. A value type made by the SDK.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

FieldTypeDefaultMeaning
let ringing: StringString"Incoming call"While an agent's call rings.
let connecting: StringString"Connecting…"While the conversation opens.
let ended: StringString"Call ended"After a normal end.
let scheduled: StringString"Scheduled call"While a ring Graine sent for a call booked for this customer rings.
let missed: StringString"Missed call"Shown for a ring no one answered; the screen then gives the option to call back.

GraineLauncherShape

Read it to draw the launcher in the brand's shape.

The floating launcher's outline: GraineThemeTokens.Shape.launcher. Raw values are the wire words.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

CaseMeaning
case circleFully round.
case squircleContinuous corners, the way iOS draws its own icons.
case roundedSquare = "rounded-square"A square with the design's large corner radius.
case squareA square with the design's small corner radius.
case pill56 points high and as wide as its label.

GraineBubbleStyle

Read it to draw chat bubbles in the brand's style.

How chat messages sit on the page: GraineThemeTokens.bubbles. Raw values are the wire words.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

CaseMeaning
case filledEach speaker's messages on their own colour.
case outlineBoth transparent and outlined: the customer's in the accent, the agent's in the border colour.
case minimalAgent messages show as plain text on the page, while the customer's sit on the soft accent.

GraineDesignChrome

Read it to frame windows the way the brand chose.

How a surface that holds content is framed: GraineThemeTokens.chrome. A value this build does not know, or none at all, is none.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

CaseMeaning
case noneA plain surface.
case windowThe chat window, the caption card and every component card open as desktop windows: a title bar with window controls and a monospaced, lower-case title.

GraineGlyph.url

Call it to get any glyph as an image URL, in a colour and size you choose.

static func url(origin: String?, id: String, color: String, sizePx: Double, stroke: Double = 2) -> String

The image URL for glyph id in color. origin: where Graine is served (https://www.graine.ai when empty). sizePx: physical pixels (points × screen scale), held to 16–512. stroke: held to 0.5–4, in quarter steps.

ParameterTypeDefaultMeaning
originString?requiredWhere Graine is served — your GraineConfig.baseUrl; https://www.graine.ai when nil or empty.
idStringrequiredThe glyph's id, such as GraineThemeTokens.Glyph.id ("waveform").
colorStringrequiredThe colour to draw it in, #RRGGBB (white when it is not a colour).
sizePxDoublerequiredPhysical pixels — points × the screen's scale — held to 16–512.
strokeDouble2The stroke width for a stroked glyph, held to 0.5–4 in quarter steps.

Returns — String. Example value: "…".

Errors — none.

let url = GraineGlyph.url(origin: nil, id: "waveform", color: "#FFFFFF", sizePx: 84)

GraineGlyph

Use it to turn a glyph into an image URL.

The launcher glyph as an image URL, for an app that cannot draw SVG path data. Static members, safe from any thread.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

PropertyTypeMeaning
static let assetVersion = 2IntRaised by Graine whenever a glyph's drawing changes, so a long-cached image never shows an old one.

Its functions: url — each with its parameters, response, errors and an example.

GraineNativeAppearance

Read it only when you draw from the dashboard's appearance settings, without a design.

The dashboard's appearance settings, translated for a phone. Used by the SDK's own views; apps do not need it.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

FieldTypeDefaultMeaning
let accent: StringString—Always a valid #rgb or #rrggbb.
let heading: StringString—The heading of the panel, such as "Talk to our AI assistant" — this is not the assistant's name.
let logoUrl: String?String?—The brand's logo as an absolute URL, or nil when none is set.
let logoPlate: GraineLogoPlateGraineLogoPlate—The backing behind the logo when the logo replaces the launcher glyph.
let radius: DoubleDouble—Corner rounding, clamped to what the view draws.

GraineLogoPlate

Read it to place the brand's logo the way the dashboard chose.

What the brand's logo sits on wherever it stands in for the launcher glyph — the launcher, the bar's avatar, the chat header. Raw values are the wire words.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

CaseMeaning
case accentThe shape's own accent fill — for a white or light mark. The default.
case whiteWhite with a hairline border — for a coloured or dark logo, or an opaque image.

GraineWidgetPalette

Read it only to colour components when the agent has no design.

The colours a component is drawn in when the agent has no design, as #RRGGBB. Used by the SDK's own views; apps do not need it.

What to expect — Resolved from the agent's design in the Garage when the agent's settings load; before that, and from an older server, every field holds its default. A missing or malformed value falls back on its own; reading these never throws. Most apps never need them: the SDK's own views draw from them.

FieldTypeDefaultMeaning
let surface: StringString—The card's fill.
let border: StringString—Hairlines.
let text: StringString—Primary text.
let muted: StringString—Secondary text.
let onAccent: StringString—Picked by luminance to read clearly on the accent, so even a yellow accent gets a legible label.
let mode: StringString—Either "light" or "dark", once "auto" has been resolved using the device's setting.

Going live

What to check before your app ships: the logs to turn on while you build, the privacy details the SDK brings, the version to quote when you ask for help, and the limits a busy app can meet.

Debug logging

Turn it on in debug builds to see exactly what the SDK did and what Graine answered — "the agent did not speak" is answered in one read of this log.

GraineInApp.configure(GraineConfig(baseUrl: "https://www.graine.ai", publishableKey: "pk_live_…", debug: true))

With GraineConfig.debug on, the SDK logs every message between itself and its engine page, both ways, and the page's own console; on native calls it also logs CallKit activating and deactivating the call's audio. The log is under the subsystem ai.graine.inapp — filter on it in Xcode's console or the Console app. Variables are logged as their keys only and the screen as its name only — never a value. The engine page can also be inspected in Safari's Web Inspector (iOS 16.4 and later), only while debug is on. Warnings and errors — a call that could not connect, a device that could not be registered for calls, an action the agent declares that this build does not register — are logged whether debug is on or not. Turn debug off in release builds.

Privacy manifest

Read it to fill in your App Store privacy details with what the SDK itself does.

The SDK ships a privacy manifest (PrivacyInfo.xcprivacy; with CocoaPods, inside the GraineInApp_Privacy bundle) that Xcode adds to your app's privacy report. It declares the required-reason APIs the SDK calls — system boot time (reason 35F9.1, for timing) and UserDefaults (reason CA92.1, for its own settings) — no tracking and no tracking domains. The optional WebRTC package ships its own (with CocoaPods, inside the GraineInAppRTC_Privacy bundle): system boot time (reason 35F9.1), no tracking. What your app collects through the agent — the customer's voice, what they type, the screens you describe — belongs in your App Privacy details; identifiers in what you describe are masked on the device first (GraineMasking).

Before you ship

Go through it once before release, so the first customer's call works.

  • The publishable key is the live agent's (pk_live_…), and app:// is in that agent's Allowed domains — without it every request is refused and onError says no_origin or origin_not_allowed.
  • NSMicrophoneUsageDescription is in Info.plist; for calls, Push Notifications and the audio and voip background modes are on (Permissions and capabilities).
  • calls.enableVoIPPushes() runs at every launch, in application(_:didFinishLaunchingWithOptions:), when your agent's calls ring this app.
  • checkActions() logs no unhandled action: every action declared on the agent is registered.
  • identify runs at sign-in and identify(nil) at sign-out, so memory and bookings follow the right customer.
  • GraineConfig.debug is off in release builds.
  • You log GraineVersion.name with your app's own version, for support.
  • With WebRTC calls: GraineInAppRTC is the same version as GraineInApp, and its THIRD-PARTY-NOTICES.md is in your app's open-source notices.

GraineVersion

Read it to log or report which SDK version your app ships.

The SDK's version, sent with the SDK's requests so the call record tells builds apart.

What to expect — A constant: "0.10.3" in this release. Include it when you ask for support. The WebRTC package's GraineRTCVersion.name is always the same.

FieldTypeDefaultMeaning
static let name: StringString"0.9.0"This build's version — read it to log or show which SDK your app ships.

Rate limits and quotas

Read it to know the limits a busy app can meet, and what happens past them.

LimitValueWhere it applies
Bookings from one install10 an hourscheduleCall, and the agent's bookings in that install's conversations
Bookings in a workspace1,000 an hourAll in-app bookings
Calls waiting per customer3 by default (1–10)At most this many upcoming calls per user
Devices per customer2 by default (1–5)Devices per user; idle devices free their place after 30 days
Rings per customer per day3 by default (1–10)Ring each user at most, in the customer's own day
Quiet period after a decline6 hoursNo ring for that customer
Notice and horizon5 minutes (2–1440) · 14 days (1–30)With at least this much notice · Booked at most this far ahead
Ring time · re-ring45 s (20–60) · once after 10 min (0–120)Ring for · After a missed call, ring once more after
Lateness10 minutesA booked call more than 10 minutes late is not rung — also one held while the workspace was out of credit or the agent was off
Booking reason · variables120 characters · 12 variables, about 1 KBBookings and calls
Memories listedAt most 100, newest firstmemories()
Conversation kept50 messages, 7 days after the lastConversations continue (since 0.7.0)
Requests per key and address, per minuteStarting a conversation 30 (and 300 per key); high-quality audio sessions 10 (120 per key); booking, listing, cancelling, device registration, memory list and sign-out 30 (300 per key); call outcomes 60 (600 per key); forgetting memory 10 (120 per key); listing memories also 30 per installAnswered rate_limited with Retry-After
Action handlers · component data6 seconds (5.5 in the chat and on a native call) · 8 secondsAfter that, the agent is told the action did not finish; a component shows its authored content

Events and listeners

What the agent tells your app as it happens. Add a GraineListener to an agent; implement only the callbacks you need. Calls have their own listener — see GraineCallsListener under Calls the user places.

GraineInApp.addListener

Call it to hear what the agent reports; keep a reference to the listener.

func addListener(_ listener: GraineListener)

Hear everything this agent reports. Held weakly — keep your own reference. Adding the same listener twice has no effect. Callbacks arrive on the main thread.

ParameterTypeDefaultMeaning
listenerGraineListenerrequiredYour listener; held weakly, so keep your own reference to it. Adding it twice has no effect.

Returns — nothing; it returns when done.

Errors — none.

final class AppEvents: GraineListener {}
let appEvents = AppEvents()   // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(appEvents)

GraineInApp.removeListener

Call it when a listener should stop hearing the agent.

func removeListener(_ listener: GraineListener)

Stops reporting to that listener.

ParameterTypeDefaultMeaning
listenerGraineListenerrequiredThe listener to stop telling.

Returns — nothing; it returns when done.

Errors — none.

final class AppEvents: GraineListener {}
let appEvents = AppEvents()
GraineInApp.shared.addListener(appEvents)
// … later, when it should stop hearing the agent:
GraineInApp.shared.removeListener(appEvents)

GraineListener

Implement it to hear everything an agent reports.

Everything an agent reports, added with GraineInApp.addListener(_:); every method has a default that does nothing, so implement only what you use. Called on the main thread; listeners are held weakly, so keep your own reference.

What to expect — Implement only the callbacks you need; the others do nothing. Called on the main thread.

PropertyTypeMeaning
extension GraineListener—An empty default for every callback, so your listener implements only the ones it needs.

Its functions: onSessionReady, onCallState, onCaption, onWidget, onWidgetPending, onEnded, onMicDenied, onQuality, onNetwork, onReconnecting, onTransport, onConnectivity, onActionRequested, onActionCompleted, onFriction, onTheme, onChatMessage, onError — each with its parameters, response, errors and an example.

GraineListener.onSessionReady

Implement it to act once the agent's settings have loaded — to read bookingAvailable or the design, say.

func onSessionReady(_ session: GraineSessionConfig)

The agent's settings loaded (once per launch). GraineInApp.theme is already up to date when this is called.

ParameterTypeDefaultMeaning
sessionGraineSessionConfigrequiredThe agent's settings, the same as GraineInApp.session from now on.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onSessionReady(_ session: GraineSessionConfig) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onCallState

Implement it to follow the in-app call connecting and ending.

func onCallState(connected: Bool, connecting: Bool)

The in-app call connected, started connecting, or stopped. Called only when either value changes.

ParameterTypeDefaultMeaning
connectedBoolrequiredThe in-app call is up.
connectingBoolrequiredThe call is being placed or re-established.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onCallState(connected: Bool, connecting: Bool) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onCaption

Implement it to show the words being said on the in-app call.

func onCaption(role: GraineRole, text: String, live: Bool)

A line being said on the in-app call, again as it fills in; live is true while it is still being recognised or spoken.

ParameterTypeDefaultMeaning
roleGraineRolerequiredWho said it: the agent or the customer.
textStringrequiredThe words of the line so far; a live line arrives again, longer, as it fills in.
liveBoolrequiredThe words are still arriving.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onCaption(role: GraineRole, text: String, live: Bool) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onWidget

Implement it to draw the agent's components yourself.

func onWidget(_ widget: [String: Any])

The agent drew a component, in a call or in the chat (also when reopenComponent() puts one back). Mount GraineWidgetView to draw it, or draw it yourself and answer with GraineInApp.answerComponent(widgetId:data:summary:); GraineWidget(frame:) reads the map.

ParameterTypeDefaultMeaning
widget[String: Any]requiredThe component as a map — widget_id, kind, data, fallback_text, … — which GraineWidget(frame:) reads for you.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onWidget(_ widget: [String: Any]) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onWidgetPending

Implement it to show a loader while a component's content loads.

func onWidgetPending(_ hint: [String: Any])

A component is on its way and its content is loading: draw your loader.

ParameterTypeDefaultMeaning
hint[String: Any]requiredWhat is known of the coming component — its kind, such as "product_list" — so your loader can take its shape.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onWidgetPending(_ hint: [String: Any]) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onEnded

Implement it to react when the in-app call is over.

func onEnded()

The in-app call is over, however it ended. Once per call.

Parameters — none.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onEnded() {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onMicDenied

Implement it to guide the customer to Settings when the microphone is refused.

func onMicDenied(reason: String)

The microphone was refused — NotAllowedError when the permission is denied. Also called when a native call (calls) could not get the microphone.

ParameterTypeDefaultMeaning
reasonStringrequired"NotAllowedError" when the permission is denied, "NotFoundError" when there is no usable microphone.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onMicDenied(reason: String) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onQuality

Implement it to show the connection's quality.

func onQuality(_ quality: GraineConnectionQuality)

The connection's quality, in one word. Called only when the word changes.

ParameterTypeDefaultMeaning
qualityGraineConnectionQualityrequiredThe connection in one word: excellent, good, poor, lost or unknown.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onQuality(_ quality: GraineConnectionQuality) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onNetwork

Implement it to show that the network went or came back.

func onNetwork(online: Bool)

The engine page saw the network go or come back.

ParameterTypeDefaultMeaning
onlineBoolrequiredTrue when the network came back, false when it went.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onNetwork(online: Bool) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onReconnecting

Implement it to show that the call is being re-established.

func onReconnecting(active: Bool)

The network interrupted the call and it is being re-established (true), or that is over (false). Not an end.

ParameterTypeDefaultMeaning
activeBoolrequiredTrue while the call is being re-established; false once it is back (or over).

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onReconnecting(active: Bool) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onTransport

Implement it to log what carries the call's audio, and why.

func onTransport(_ transport: GraineTransport, requested: GraineTransport, reason: String?, message: String?)

What carries the call's audio, and why it is not the transport asked for. A reason of not_configured is a setting that never fixes itself, so it is also reported through onError.

ParameterTypeDefaultMeaning
transportGraineTransportrequiredWhat carries the call's audio now.
requestedGraineTransportrequiredWhat was asked for — GraineConfig.transport, else the dashboard's Call audio setting.
reasonString?requiredWhy the two differ, as a code — not_configured means high-quality audio is not set up for the workspace; nil when they match.
messageString?requiredThe same reason in a sentence, for your logs; nil when there is none.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onTransport(_ transport: GraineTransport, requested: GraineTransport, reason: String?, message: String?) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onConnectivity

Implement it to show whether the engine page is loaded, and offer a retry when it failed.

func onConnectivity(_ state: GraineConnectivity, attempt: Int, reason: String?)

The engine page loading, ready, retrying (with the reload attempt and why) or failed.

ParameterTypeDefaultMeaning
stateGraineConnectivityrequiredloading, ready, retrying or failed.
attemptIntrequiredHow many reloads have been scheduled so far: 0 for the first load, then 1, 2, … up to the length of GraineConfig.Timing.retryDelaysMs.
reasonString?requiredWhy a load is being retried or failed, for your logs; nil otherwise.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onConnectivity(_ state: GraineConnectivity, attempt: Int, reason: String?) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onActionRequested

Implement it to see when the agent asks for one of your actions.

func onActionRequested(name: String)

The agent asked for one of your actions.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, as registered with registerAction and declared on the agent.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onActionRequested(name: String) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onActionCompleted

Implement it to see the answer the agent got for an action.

func onActionCompleted(name: String, result: GraineActionResult)

The answer the agent received for that action — your handler's, or the SDK's: refused ("This app build does not implement …") for an action with no handler, error for a throw or a handler that ran out of time.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name.
resultGraineActionResultrequiredThe answer the agent received: ok, refused or error, with its message and data.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onActionCompleted(name: String, result: GraineActionResult) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onFriction

Implement it to react in your UI when the customer looks stuck.

func onFriction(_ signal: GraineFrictionSignal)

The customer looks stuck: on one screen, with a field outstanding, through a stretch of a live conversation. The agent is told right after; this is for the app — badge a control, count it.

ParameterTypeDefaultMeaning
signalGraineFrictionSignalrequiredWhat looks stuck: the screen, the outstanding field and how long, in detail and data.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onFriction(_ signal: GraineFrictionSignal) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onTheme

Implement it to restyle your own UI when the brand's tokens arrive or the device switches between light and dark.

func onTheme(_ tokens: GraineThemeTokens?)

The brand's design tokens for the device's current appearance — when the agent's settings arrive, and again when the device switches between light and dark. Never called when the agent has no design.

ParameterTypeDefaultMeaning
tokensGraineThemeTokens?requiredThe token set to draw now — the same as GraineInApp.theme.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onTheme(_ tokens: GraineThemeTokens?) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onChatMessage

Implement it to badge or open your chat when the agent sends a message.

func onChatMessage(_ turn: GraineChatTurn)

The agent finished a message in the chat. turn.proactive is true when it spoke up without being asked — open or badge your chat for those if the customer is not looking at it.

ParameterTypeDefaultMeaning
turnGraineChatTurnrequiredThe agent's finished message: its text, when it was said, and proactive.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onChatMessage(_ turn: GraineChatTurn) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

GraineListener.onError

Implement it to catch the configuration mistakes your team must fix.

func onError(_ message: String)

Something your team must fix — a rejected key, an agent that does not accept app://, an engine page that could not load. Meant for you, never shown to the customer.

ParameterTypeDefaultMeaning
messageStringrequiredWhat is wrong, in a sentence for your team — never shown to the customer.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens, with the parameters above as its payload.

Errors — none.

final class AppEvents: GraineListener {
    func onError(_ message: String) {
        // …
    }
}
let events = AppEvents()          // keep it: the SDK holds its listeners weakly
GraineInApp.shared.addListener(events)

Errors

A method that asks Graine on the customer's behalf fails with one error type, GraineServiceError, carrying a stable code. Branch on the code; show the message, which is written for the customer. Errors never carry the identifier, traits or a push token.

Error codes and their fixes

Read it to branch on a refusal and fix its cause.

CodeIn this SDKHTTPMeansRaised byFix
invalid_keyinvalidKey401The publishable key is not one, or belongs to no agentEvery request to GraineCopy the key again from Agent → Embed & Widgets → Install
origin_refusedoriginRefused403The app or site is not allowed: app:// (apps) or the site's host is missing from Allowed domainsEvery requestAdd app://, or the exact host
rate_limitedrateLimited429Too many requests from this install, key or addressEvery requestWait retryAfterSeconds (the Retry-After header), then retry
invalid_requestinvalidRequest400A field is missing or malformed — a time without its offset, a reason over 120 charactersscheduleCall, device registrationFix the request (Flutter refuses a long reason on the device)
identity_requiredidentityRequired400No customer is identified, or the identifier is a placeholdermemories, forgetMemory, bookings that need a customeridentify() with the identifier first
device_limitdeviceLimit403This install is not one of the customer's allowed devicesmemories, forgetMemory, newConversation, scheduleCallSign out on another device, raise Devices per user, or Reset devices for the customer
scheduling_offschedulingOff403 (0 on the device)Users, from your app is offscheduleCallTurn it on, or hide booking when usersCanBook is false
too_soontooSoon400 (0 on the device)Earlier than earliest(now)scheduleCallOffer times from earliest(now)
too_fartooFar400 (0 on the device)Later than latest(now)scheduleCallOffer times up to latest(now)
outside_windowoutsideWindow400Outside the agent's Calling Window (or, for agent and team bookings, inside quiet hours), in the customer's time zonescheduleCall, agent and dashboard bookingsShow the message (it names a time that works); offer another time
too_many_pendingtooManyPending409The customer already has the most calls waitingscheduleCallCancel one first
not_foundnotFound404No such booking for this customer or installcancelScheduledCallRefresh scheduledCalls()
unavailableunavailable503 (504 on a timeout)Graine could not answer just now — including a booking refused because the agent is switched off or out of creditEvery requestRetry with backoff; check the agent is on and the workspace has credit
networknetwork—No answer came back: offline, or the connection droppedEvery requestRetry when online
unknownunknown(raw)otherA code newer than the SDK, or an unexpected answer (carrying its word)Every requestLog the code and message; show the message

When Graine gives no words of its own, message is the SDK's: "Graine could not be reached. Check the connection and try again." for network, or "Graine refused the request (status)." with the HTTP status. A baseUrl that is not a URL is invalid_request: baseUrl "…" is not a URL.

When the bar or chat cannot start, the reason arrives through onError (and React Native's launcher error), not as a coded exception: key_missing, key_malformed (publishable keys start with pk_live_), key_unknown (401 — copy the key again); no_origin (403 — add app://); no_domains_configured (403 — the list is empty); origin_not_allowed (403 — add the exact host); not_configured (high-quality audio is not enabled for the workspace; calls use the standard path). A refused microphone arrives as onMicDenied("NotAllowedError") (or NotFoundError for no usable microphone); React Native and Flutter calls configured wrongly reject with invalid_options, naming the option.

GraineServiceError (constructor)

Use it to build errors for tests of your own error handling.

init(code: GraineErrorCode, message: String, status: Int = 0, retryAfterSeconds: Int? = nil)

Builds an error by hand — for tests and previews of your own error handling; the SDK throws the real ones.

ParameterTypeDefaultMeaning
codeGraineErrorCoderequiredThe stable code to branch on, such as .tooSoon.
messageStringrequiredThe sentence message will carry — fit to show the customer.
statusInt0The HTTP status to pretend; 0 for a refusal made on the device or no answer.
retryAfterSecondsInt?nilFor .rateLimited: the seconds to wait before trying again; nil when not said.

Returns — a new GraineServiceError.

Errors — none.

let error = GraineServiceError(code: .tooSoon, message: "Pick a time at least 5 minutes from now")

GraineServiceError

Catch it from every method that asks Graine, and branch on its code.

What the methods that ask Graine throw — memories(), forgetMemory(), scheduleCall, scheduledCalls() and cancelScheduledCall: a refusal from Graine, or a request that got no answer. Branch on code; message is a sentence fit to show.

What to expect — Thrown or passed by every method that asks Graine. Branch on code, and show message — a sentence fit for the customer that never carries the identifier, traits or a push token. retryAfterSeconds comes with rate_limited when Graine said how long. A refusal made on the device before any request has status 0.

FieldTypeDefaultMeaning
let code: GraineErrorCodeGraineErrorCoderequiredWhy — the stable part to branch on.
let message: StringStringrequiredA sentence fit to show the customer: Graine's own words when it gave some, else the SDK's — "Graine could not be reached. Check the connection and try again." for network, "Graine refused the request (status)." otherwise.
let retryAfterSeconds: Int?Int?nilFor rateLimited: the seconds Graine asked to wait, when it said.
let status: IntInt0The HTTP status; 0 when no answer came back.
var errorDescription: String? { get }String?—The same as message, so localizedDescription shows it.

Its functions: init — each with its parameters, response, errors and an example.

GraineErrorCode (constructor)

Use it to turn an error word into a GraineErrorCode.

init(wire: String)

The code for a wire word (trimmed, any case); unknown for any other word.

ParameterTypeDefaultMeaning
wireStringrequiredAn error word as Graine sends it — too_soon, device_limit, …

Returns — a new GraineErrorCode: .unknown(word) for a word this build does not know.

Errors — none.

let code = GraineErrorCode(wire: "too_soon")   // .tooSoon

GraineErrorCode

Branch on it to handle each kind of refusal.

Why Graine refused a request, as a stable code; each case has its wire word. A word this build does not know is unknown, carrying it.

What to expect — invalid_key · origin_refused · rate_limited · invalid_request · identity_required · device_limit · scheduling_off · too_soon · too_far · outside_window · too_many_pending · not_found · unavailable · network (no answer came back) · unknown, which carries a code newer than this SDK. The error codes section says when each happens and how to fix it.

FieldTypeDefaultMeaning
var wire: String { get }String—The word on the wire: too_soon, …; an unknown code's own word.
var description: String { get }String—The same as wire, for printing and logs.
CaseMeaning
case invalidKeyWire invalid_key. The publishable key is not one, or belongs to no agent (HTTP 401).
case originRefusedWire origin_refused. The app is not allowed: app:// is missing from the agent's Allowed domains (HTTP 403).
case rateLimitedWire rate_limited. Too many requests; wait retryAfterSeconds when Graine said how long (HTTP 429).
case invalidRequestWire invalid_request. A field is missing or malformed (HTTP 400 or 422), or baseUrl is not a URL.
case identityRequiredWire identity_required. No customer is identified, or the identifier is a placeholder.
case deviceLimitWire device_limit. This install is not one of the customer's allowed devices.
case schedulingOffWire scheduling_off. Customers may not book calls with this agent; checked on the device first.
case tooSoonWire too_soon. Earlier than schedulingRules.earliest(now:); checked on the device first.
case tooFarWire too_far. Later than schedulingRules.latest(now:); checked on the device first.
case outsideWindowWire outside_window. Outside the agent's calling hours, in this device's time zone.
case tooManyPendingWire too_many_pending. The customer already has the most calls waiting.
case notFoundWire not_found. No such booking for this customer or install (HTTP 404).
case unavailableWire unavailable. Graine could not answer just now (HTTP 503); try again with backoff.
case networkWire network. No answer came back: offline, or the connection dropped.
case unknown(String)A code this build does not know, or an unexpected answer, carrying its word — for example http_500.

Its functions: init — each with its parameters, response, errors and an example.

GraineCallsHostError

Read it only if you maintain a React Native or Flutter bridge.

Why GraineCallsHost.configure(_:) refused its options. Used by the React Native and Flutter bridges; native iOS apps do not see it.

What to expect — Thrown only by configure, before anything changes: the options already in force stay. The message names the option that is wrong — fix the code that builds the options.

FieldTypeDefaultMeaning
let code: StringString—"invalid_options".
let message: StringString—A sentence for the developer naming the option that is wrong, such as callScreen must be "sdk" or "app".
var errorDescription: String? { get }String?—The same as message, so localizedDescription shows it.

Lower-level building blocks

Public so a bridge for another framework, or a fully custom UI, can reuse them; an integration does not need them. The CallKit and PushKit callbacks are here too: the system calls them, never your app.

GraineCallsHost

Use it only if you build a bridge for another framework; native apps use GraineInApp.shared.calls.

The bridge the React Native and Flutter packages use to drive GraineCalls with JSON-safe values (strings, numbers, booleans, null, lists and string-keyed maps); apps on native iOS do not call it — GraineInApp.shared.calls is the Swift API. Reached as GraineCallsHost.shared; it runs on the main thread, and configure, restore, snapshot, startCall, installId and addSink wait for their answer when called from another thread.

What to expect — A bridge calls restore() first at every launch, then configure once its own code runs; the options are kept for the next cold start, and every event goes to the sinks it adds — outcomes and rings that came while nothing listened (the last 20) go to the first sink.

PropertyTypeMeaning
@objc static let shared = GraineCallsHost()GraineCallsHostThe one host per process. Used by the React Native and Flutter bridges.
typealias Sink = (_ event: String, _ payload: [String: Any]) -> Void—Receives every event on the main thread: its name — state (the snapshot, at most once per turn of the main loop), outcome, incoming, voipToken or action — and its payload.

Its functions: restore, configure, snapshot, enableVoIPPushes, handlePush, startCall, answer, decline, end, setMuted, setSpeaker, setIdentity, setMemory, installId, setVariables, setScreen, setActions, resolveAction, addSink, removeSink — each with its parameters, response, errors and an example. Permissions and the setup check, for a bridge, are below.

GraineCallsHost: permissions and the setup check

Use them only from a bridge: GraineCalls' permissions and setup check, as JSON-safe values.

func callPermissions() -> [String: Any]
func requestCallPermissions(_ done: @escaping ([String: Any]) -> Void)
func openCallSettings(_ item: String?) -> Bool
func callSetupProblems() -> [[String: Any]]
MethodWhat it does
GraineCallsHost.callPermissions(){microphone, notifications, fullScreen}, each granted, denied, blocked or notNeeded — readable before configure.
GraineCallsHost.requestCallPermissions(_:)Asks for what the system can still ask for (the microphone), when the bridge's app chooses; done gets the same map after, on the main thread.
GraineCallsHost.openCallSettings(_:)Opens Settings for microphone, notifications or fullScreen (full_screen and any case too); false for any other word.
GraineCallsHost.callSetupProblems()[{code, message}]: what the app's setup still lacks — empty when nothing is, and before configure.

Returns — as above. Errors — none.

let missing = GraineCallsHost.shared.callSetupProblems()   // [["code": "background_voip_missing", "message": "…"]]

GraineCallsHost.restore

Call it first thing at launch, from a bridge, so a VoIP push that woke the app rings.

@objc @discardableResult func restore() -> Bool

At launch, before any JavaScript or Dart runs: applies the options the last configure kept, and enables VoIP pushes again if they were, so a push that woke a terminated app rings. True when something was restored; false when nothing was kept or this launch is already configured.

Parameters — none.

Returns — Bool. Example value: true.

Errors — none.

GraineCallsHost.shared.restore()   // first thing at launch, before any JavaScript or Dart runs

GraineCallsHost.configure

Call it from a bridge to configure calls with JSON-safe options.

func configure(_ options: [String: Any]?) throws

Configures the calls and keeps the options for the next cold start; the first call makes the SDK instance. Options: publishableKey and displayName (required), baseUrl, ringtoneSound, ringTimeoutMs (30,000 by default), appVersion, callScreen ("sdk" or "app"), callTransport ("auto" — the default — "websocket" or "webrtc": GraineCallsConfig.callTransport), debug, android, ios (iconTemplate, includesCallsInRecents) and agents (more agents by name, each {publishableKey, baseUrl?}). Unknown keys are ignored; a known key of the wrong type is refused, naming it. A new key, base URL or debug setting takes effect at the next launch.

ParameterTypeDefaultMeaning
options[String: Any]?requiredThe options as a map: the keys above; unknown keys are ignored.

Returns — nothing; it returns when done.

Errors — GraineCallsHostError: invalid_options

try GraineCallsHost.shared.configure(["publishableKey": "pk_live_…", "displayName": "Acme Assistant",
                                      "agents": ["sales": ["publishableKey": "pk_live_…"]]])

GraineCallsHost.snapshot

Call it from a bridge to draw the call screen from one map.

func snapshot() -> [String: Any]

The call as a call screen draws it, every key always present: state, callId, direction, reason, callerName, muted, speakerOn, agentSpeaking, caption, connectedAt (milliseconds since the epoch), error, agent, transport ("webrtc" or "websocket" once decided, else null) and transportFallbackReason (GraineCalls.transportFallbackReason, else null).

Parameters — none.

Returns — [String: Any].

Errors — none.

let call = GraineCallsHost.shared.snapshot()   // ["state": "active", "callId": "call_…", …]

GraineCallsHost.enableVoIPPushes

Call it from a bridge to receive the agent's calls as VoIP pushes.

func enableVoIPPushes()

Turns on VoIP pushes for the agent's calls, both now and on each launch that follows. The token arrives as a voipToken event — again on every call, the last one known.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.enableVoIPPushes()

GraineCallsHost.handlePush

You do not need it on iOS: it exists for the Android side of a bridge.

func handlePush(_ data: [String: Any]?) -> Bool

Always false on iOS, where a VoIP push reaches PushKit and the SDK directly. It exists for the Android side of the bridges.

ParameterTypeDefaultMeaning
data[String: Any]?requiredThe push's data, exactly as the messaging service received it.

Returns — Bool. Example value: false.

Errors — none.

let handled = GraineCallsHost.shared.handlePush(["type": "agent_call", "call_id": "c_42"])   // false on iOS

GraineCallsHost.startCall

Call it from a bridge when the customer calls the agent.

func startCall(_ options: [String: Any]?) -> String?

Starts the customer's call to the agent; the options are {reason?, variables?, agent?}. Returns the call's id — or nil if it is not configured, a call is already live, or no agent goes by that name.

ParameterTypeDefaultMeaning
options[String: Any]?required{ reason, variables, agent }: why the customer calls (the agent hears it as call_reason), prompt variables for this call, and the configured agent's name (the default agent when left out).

Returns — String?. Example value: "call_6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44".

Errors — none.

let callId = GraineCallsHost.shared.startCall(["reason": "support_help", "variables": ["plan": "gold"], "agent": "support"])

GraineCallsHost.answer

Call it from a bridge to answer the ringing call.

func answer()

Answers the ringing call.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.answer()

GraineCallsHost.decline

Call it from a bridge to decline the ringing call.

func decline()

Declines the ringing call (on iOS, end while it rings); does nothing otherwise.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.decline()

GraineCallsHost.end

Call it from a bridge to hang up.

func end()

Hangs up.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.end()

GraineCallsHost.setMuted

Call it from a bridge's mute control.

func setMuted(_ muted: Bool)

Mutes or unmutes the customer.

ParameterTypeDefaultMeaning
mutedBoolrequiredtrue to stop sending the customer's voice; false to send it again.

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.setMuted(true)

GraineCallsHost.setSpeaker

Call it from a bridge's speaker control.

func setSpeaker(_ on: Bool)

The loudspeaker or the earpiece.

ParameterTypeDefaultMeaning
onBoolrequiredtrue for the loudspeaker; false for the earpiece or a headset.

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.setSpeaker(true)

GraineCallsHost.setIdentity

Call it from a bridge when the signed-in customer changes.

func setIdentity(_ traits: [String: Any]?)

Tells the SDK who is signed in, as GraineInApp.identify does; pass nil when they sign out. Held until the instance exists.

ParameterTypeDefaultMeaning
traits[String: Any]?requiredWho the customer is (see identify).

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.setIdentity(["user_id": "u_8812", "name": "Asha"])

GraineCallsHost.setMemory

Call it from a bridge when the customer changes their memory choice.

func setMemory(_ enabled: Bool)

Passes on the current customer's choice about memory, as GraineInApp.setMemory does. Held until the instance exists.

ParameterTypeDefaultMeaning
enabledBoolrequiredtrue to remember this customer's conversations; false to stop. Kept on the device for them.

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.setMemory(false)   // this customer chose not to be remembered

GraineCallsHost.installId

Call it from a bridge to send the same install id the native calls send.

func installId() -> String?

This install's id, kept in the Keychain — the one the native calls send, so a device is one install whichever layer speaks. Nil before configure or restore.

Parameters — none.

Returns — String?. Example value: "9b2e7c4a-1f3d-4e8a-b6c5-0d7f2a9e4b13".

Errors — none.

let installId = GraineCallsHost.shared.installId()

GraineCallsHost.setVariables

Call it from a bridge with the whole map of variables, each time it changes.

func setVariables(_ values: [String: Any]?)

The variables set while the app runs become exactly this map: a key the last map set and this one leaves out is removed. Reaches every agent; masked on the device, as GraineInApp.setVariables masks them.

ParameterTypeDefaultMeaning
values[String: Any]?requiredEvery prompt variable the app sets, by name — the whole map each time; nil removes them all.

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.setVariables(["plan": "gold", "cart_items": 3])

GraineCallsHost.setScreen

Call it from a bridge to describe the screen.

func setScreen(_ screen: [String: Any]?)

The current screen in the form {screen, title?, journey?, fields?, error?, data?, availableActions?}; pass nil, or leave out the screen name, when there is none.

ParameterTypeDefaultMeaning
screen[String: Any]?requiredWhat the customer can see now: the screen's name and title, its fields and where they are in a journey. nil / null clears it.

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.setScreen(["screen": "checkout_address", "title": "Delivery address",
                                  "fields": [["name": "postal_code", "status": "invalid"]]])

GraineCallsHost.setActions

Call it from a bridge to say which actions it answers.

func setActions(_ names: [String], sink: Int?)

The actions the caller answers, and its sink's token: each request for one is sent as an action event to the sink that last listed a non-empty set, and waits for resolveAction within the SDK's six seconds. Names no longer listed are unregistered.

ParameterTypeDefaultMeaning
names[String]requiredThe action names the bridge answers; a name left out of a later list is unregistered.
sinkInt?requiredThe token addSink returned for the sink that answers these actions.

Returns — nothing; it returns when done.

Errors — none.

let sinkToken = GraineCallsHost.shared.addSink { event, payload in
    // an "action" event names the request to answer with resolveAction
}
GraineCallsHost.shared.setActions(["set_language", "open_order"], sink: sinkToken)

GraineCallsHost.resolveAction

Call it from a bridge to answer an action request.

func resolveAction(_ requestId: String, result: [String: Any]?)

Replies to an action request with {status: "ok" | "refused" | "error", message?, data?}. The first answer wins.

ParameterTypeDefaultMeaning
requestIdStringrequiredThe requestId of the action event being answered.
result[String: Any]?requiredThe answer: status ("ok", "refused" or "error"), an optional message for the agent and optional data.

Returns — nothing; it returns when done.

Errors — none.

GraineCallsHost.shared.resolveAction("r_1", result: ["status": "ok", "message": "The app is now in Hindi."])

GraineCallsHost.addSink

Call it from a bridge to start receiving call events.

func addSink(_ sink: @escaping Sink) -> Int

Starts receiving events; returns the sink's token. Outcomes and rings that came while nothing listened (the last 20) are delivered to the first sink at once.

ParameterTypeDefaultMeaning
sinkSinkrequiredThe function that receives every event on the main thread: its name and its payload.

Returns — Int. Example value: 3.

Errors — none.

let sinkToken = GraineCallsHost.shared.addSink { event, payload in
    // "state", "outcome", "incoming", "voipToken" or "action"
}

GraineCallsHost.removeSink

Call it from a bridge to stop receiving call events.

func removeSink(_ token: Int)

Stops that sink. Action requests it had not answered are refused.

ParameterTypeDefaultMeaning
tokenIntrequiredThe token addSink returned for that sink.

Returns — nothing; it returns when done.

Errors — none.

let sinkToken = GraineCallsHost.shared.addSink { event, payload in }
// … when the bridge's listener goes away:
GraineCallsHost.shared.removeSink(sinkToken)

GraineCalls.providerDidReset

You never call it: CallKit calls it when the system resets every call.

func providerDidReset(_ provider: CXProvider)

CallKit's callback when the system resets every call; the SDK ends its call — a ring is reported .missed, a call still connecting .failed("The system ended the call."), a call under way .ended(.failed). Apps do not call it.

ParameterTypeDefaultMeaning
providerCXProviderrequiredThe SDK's own CallKit provider.

Returns — nothing; it returns when done.

Errors — none.

// CallKit calls it on the SDK's own provider; your app hears what it did as an outcome.
final class CallEvents: GraineCallsListener {
    func onCallOutcome(_ outcome: GraineCallOutcome, callId: String) {
        if case .failed(let why) = outcome { print(callId, why) }   // "The system ended the call."
    }
}

GraineCalls.provider

You never call it: CallKit calls it when a call is being placed.

func provider(_ provider: CXProvider, perform action: CXStartCallAction)

CallKit's callback for a call being placed; the SDK opens the conversation. Apps do not call it.

ParameterTypeDefaultMeaning
providerCXProviderrequiredThe SDK's own CallKit provider.
perform actionCXStartCallActionrequiredThe call CallKit asks the SDK to place.

Returns — nothing; it returns when done.

Errors — none.

// startCall asks CallKit to place the call; CallKit then calls this on the SDK's own provider.
let callId = GraineInApp.shared.calls.startCall(reason: "support_help")

GraineCalls.provider

You never call it: CallKit calls it when the customer answers.

func provider(_ provider: CXProvider, perform action: CXAnswerCallAction)

CallKit's callback for an answer; the SDK answers at once and settles the microphone after. Apps do not call it.

ParameterTypeDefaultMeaning
providerCXProviderrequiredThe SDK's own CallKit provider.
perform actionCXAnswerCallActionrequiredThe ring CallKit asks the SDK to answer.

Returns — nothing; it returns when done.

Errors — none.

// The customer answers on the system's call screen — or your own Answer button calls answer() —
// and CallKit calls this on the SDK's own provider.
GraineInApp.shared.calls.answer()

GraineCalls.provider

You never call it: CallKit calls it when the customer hangs up or declines.

func provider(_ provider: CXProvider, perform action: CXEndCallAction)

CallKit's callback for a hang-up or decline. Apps do not call it.

ParameterTypeDefaultMeaning
providerCXProviderrequiredThe SDK's own CallKit provider.
perform actionCXEndCallActionrequiredThe call CallKit asks the SDK to end or decline.

Returns — nothing; it returns when done.

Errors — none.

// The customer hangs up or declines on the system's call screen — or your own button calls end() —
// and CallKit calls this on the SDK's own provider.
GraineInApp.shared.calls.end()

GraineCalls.provider

You never call it: CallKit calls it when the customer mutes on the system's call screen.

func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction)

CallKit's callback for mute from the system's call screen. Apps do not call it.

ParameterTypeDefaultMeaning
providerCXProviderrequiredThe SDK's own CallKit provider.
perform actionCXSetMutedCallActionrequiredThe mute or unmute CallKit asks for.

Returns — nothing; it returns when done.

Errors — none.

// The customer mutes on the system's call screen — or your own button calls setMuted(_:) —
// and CallKit calls this on the SDK's own provider.
GraineInApp.shared.calls.setMuted(true)

GraineCalls.provider

You never call it: CallKit calls it when the system asks to put the call on hold.

func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction)

CallKit's callback for hold; an agent call is never put on hold, so the action fails and the system offers "End & Accept" instead. Apps do not call it.

ParameterTypeDefaultMeaning
providerCXProviderrequiredThe SDK's own CallKit provider.
perform actionCXSetHeldCallActionrequiredThe hold CallKit asks for; always refused.

Returns — nothing; it returns when done.

Errors — none.

// A phone call comes in during the agent's call: the SDK refuses the hold, iOS offers "End & Accept",
// and choosing it ends the agent's call — heard as an outcome, like any hang-up.
final class CallEvents: GraineCallsListener {
    func onCallOutcome(_ outcome: GraineCallOutcome, callId: String) {
        print(callId, outcome.wire)   // "ended"
    }
}

GraineCalls.provider

You never call it: CallKit calls it when it hands the call's audio to the app.

func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession)

CallKit's callback when the system hands over the audio; the call's audio starts here, and a conversation that was waiting for its audio is connected (state → active). With GraineConfig.debug on, it is logged. Apps do not call it.

ParameterTypeDefaultMeaning
providerCXProviderrequiredThe SDK's own CallKit provider.
didActivate audioSessionAVAudioSessionrequiredThe call's audio session, as CallKit hands it over.

Returns — nothing; it returns when done.

Errors — none.

// CallKit hands over the call's audio after an answer or a start; the call is active from then on.
final class CallEvents: GraineCallsListener {
    func onCallState(_ state: GraineCallState, callId: String?) {
        if case .active = state { print("The customer can hear the agent") }
    }
}

GraineCalls.provider

You never call it: CallKit calls it when it takes the call's audio back.

func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession)

CallKit's callback when the system takes the audio back; the microphone and speaker are released, and the app's own audio session is restored after the call. With GraineConfig.debug on, it is logged. Apps do not call it.

ParameterTypeDefaultMeaning
providerCXProviderrequiredThe SDK's own CallKit provider.
didDeactivate audioSessionAVAudioSessionrequiredThe call's audio session, as CallKit takes it back.

Returns — nothing; it returns when done.

Errors — none.

// With debug on, the log says when CallKit activates and deactivates the call's audio.
GraineInApp.configure(GraineConfig(baseUrl: "https://www.graine.ai", publishableKey: "pk_live_…", debug: true))

GraineCalls.provider

You never call it: CallKit calls it when one of its actions timed out.

func provider(_ provider: CXProvider, timedOutPerforming action: CXAction)

CallKit's callback for an action that timed out; logged. Apps do not call it.

ParameterTypeDefaultMeaning
providerCXProviderrequiredThe SDK's own CallKit provider.
timedOutPerforming actionCXActionrequiredThe action that timed out; logged.

Returns — nothing; it returns when done.

Errors — none.

// Nothing to do in your app: the SDK logs "CallKit timed out on …" as a warning, debug or not;
// with debug on, the log also has the call's steps around it.
GraineInApp.configure(GraineConfig(baseUrl: "https://www.graine.ai", publishableKey: "pk_live_…", debug: true))

GraineCalls.pushRegistry

You never call it: PushKit calls it with a new VoIP token.

func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType)

PushKit's callback with a new VoIP token, handled as described for enableVoIPPushes. Apps do not call it.

ParameterTypeDefaultMeaning
registryPKPushRegistryrequiredThe SDK's own push registry, made by enableVoIPPushes.
didUpdate pushCredentialsPKPushCredentialsrequiredThe push credentials PushKit handed over.
for typePKPushTyperequiredThe push type; only .voIP is handled, anything else is ignored.

Returns — nothing; it returns when done.

Errors — none.

// enableVoIPPushes makes the SDK's push registry; PushKit calls this on it with each new token,
// which reaches your onToken.
GraineInApp.shared.calls.enableVoIPPushes { token in print("VoIP token:", token ?? "invalidated") }

GraineCalls.pushRegistry

You never call it: PushKit calls it when the VoIP token is invalidated.

func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType)

PushKit's callback when the VoIP token is invalidated; onToken gets nil. Apps do not call it.

ParameterTypeDefaultMeaning
registryPKPushRegistryrequiredThe SDK's own push registry, made by enableVoIPPushes.
didInvalidatePushTokenFor typePKPushTyperequiredThe push type whose token iOS invalidated; only .voIP is handled.

Returns — nothing; it returns when done.

Errors — none.

// PushKit calls it when iOS invalidates the token; your onToken gets nil.
GraineInApp.shared.calls.enableVoIPPushes { token in
    if token == nil { print("This iPhone's VoIP token was invalidated") }
}

GraineCalls.pushRegistry

You never call it: PushKit calls it with each VoIP push.

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void)

PushKit's callback with a VoIP push, passed to reportIncomingCall(payload:completion:). Apps do not call it.

ParameterTypeDefaultMeaning
registryPKPushRegistryrequiredThe SDK's own push registry, made by enableVoIPPushes.
didReceiveIncomingPushWith payloadPKPushPayloadrequiredThe VoIP push; its dictionaryPayload is what rings.
for typePKPushTyperequiredThe push type; only .voIP is handled, anything else is ignored.
completion() -> VoidrequiredPushKit's completion handler, called once the push has been reported to CallKit.

Returns — nothing; it returns when done.

Errors — none.

// PushKit calls it on the SDK's own registry with each VoIP push; the ring that follows reaches you here.
final class CallEvents: GraineCallsListener {
    func onIncomingCall(_ call: GraineIncomingCall) {
        print("Ringing:", call.callId, call.hosted ? "(booked)" : "")
    }
}

GraineChatViewController.viewDidLoad

You never call it: UIKit calls it to build the chat screen.

override func viewDidLoad()

UIKit's lifecycle callback, which builds the chat screen. Apps do not call it.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

// UIKit calls it as the screen loads — when you present or push the controller.
present(GraineChatViewController(), animated: true)

GraineProtocol

Read it only if you host the engine page yourself; the SDK uses it for you.

The message names the SDK and Graine's engine page exchange, and the helpers that build and read those messages. Used by the SDK itself; apps do not need it.

What to expect — The SDK sends and reads these for you; they are public so a custom UI can reuse them.

PropertyTypeMeaning
enum Outnested typeThe names of the messages the SDK sends into the page — see GraineProtocol.Out.
enum Innested typeThe names of the messages the page sends out — see GraineProtocol.In.
static let bridgeName = "ReactNativeWebView"StringThe name of the bridge object the engine page looks for, so one page serves every platform.
static let messageHandlerName = "graine"StringThe web view message handler the bridge object forwards to.
static let actionDeadlineMs = 6_000IntThe longest an action handler can run before the agent learns the action did not happen.

Its functions: engineUrl, deliver, out, parse — each with its parameters, response, errors and an example.

GraineProtocol.Out

Read it only if you host the engine page yourself: the names of the messages the SDK sends into it.

public enum GraineProtocol.Out — each message type as a static let constant. Used by the SDK itself; an app does not need them.

What to expect — Fixed strings, the same on every platform. Build a message with GraineProtocol.out(_:_:).

ConstantValueMeaning
static let startCall"graine:start-call"Place the in-app call.
static let endCall"graine:end-call"Hang up.
static let setMuted"graine:set-muted"Stop or resume sending the customer's voice: muted.
static let warm"graine:warm"Prepare the connection while idle, so a later start connects at once.
static let variables"graine:variables"The prompt variables, whenever they change: variables.
static let appContext"graine:app-context"What the customer is looking at: the current screen, its fields and recent events.
static let appActionResult"graine:app-action-result"The app's answer to an action the agent asked for.
static let appEvent"graine:app-event"Something the agent may speak up about (GraineVoice.reportEvent): name, data.
static let widgetResult"graine:widget-result"The customer's answer to a component (GraineVoice.answerWidget): widgetId, data, summary.
static let identity"graine:identity"Who the customer is for memory, and their memory choice — used from the next conversation.

GraineProtocol.In

Read it only if you host the engine page yourself: the names of the messages the page sends out.

public enum GraineProtocol.In — each message type as a static let constant. GraineProtocol.parse(_:) turns a message into a PageMessage.

What to expect — Fixed strings, the same on every platform; a message of any other type is ignored.

ConstantValueMeaning
static let ready"graine:ready"The page has loaded and is ready for a call.
static let call"graine:call"The call's state changed: connected, connecting.
static let muted"graine:muted"The customer was muted or unmuted: muted.
static let caption"graine:caption"A line being said: role, text, live.
static let widget"graine:widget"The agent drew a component: widget.
static let widgetPending"graine:widget-pending"A component is on its way: widget, what is known of it so far.
static let appAction"graine:app-action"The agent asked for one of your actions: action with action_id, name and arguments.
static let ended"graine:ended"The call is over.
static let micDenied"graine:mic-denied"The microphone was refused: reason.
static let transport"graine:transport"What carries the audio, and why it differs from what was asked: transport, requested, reason, message.
static let quality"graine:quality"One connection measurement: sample with rtt, jitter and loss.
static let network"graine:network"The network went or came back: online.
static let reconnecting"graine:reconnecting"The call is being re-established, or that is over: active.
static let progress"graine:progress"Where a connect is: stage — registering, dialling, retrying, switching or failed.

GraineProtocol.engineUrl

Call it only if you host the engine page yourself, to build its address.

static func engineUrl(baseUrl: String, voiceAgentId: String, transport: GraineTransport, cacheKey: String, idle: Bool, publishableKey: String, accent: String?) -> String

The engine page's URL for an agent, transport and page version; idle loads it without connecting anything.

ParameterTypeDefaultMeaning
baseUrlStringrequired"https://www.graine.ai" unless your workspace is self-hosted.
voiceAgentIdStringrequiredThe id in-app calls are placed under (GraineSessionConfig.webcallAgentId).
transportGraineTransportrequiredThe transport the page should use: .websocket or .webrtc.
cacheKeyStringrequiredThe engine page's version (GraineSessionConfig.pageVersion), so a new version is a new address and the same one is served from cache.
idleBoolrequiredLoad the page without connecting anything, so a later start is quick.
publishableKeyStringrequiredThe agent's publishable key, pk_live_…, from Agent → Embed & Widgets → Install.
accentString?requiredThe accent colour, #RRGGBB (the # optional); nil leaves it out.

Returns — String: the page's address. Example value: "https://www.graine.ai/embed/…?mode=voice&branding=0&embed=1&transport=ws&b=…&idle=1&pk=pk_live_…".

Errors — none.

if let session = GraineInApp.shared.session {
    let url = GraineProtocol.engineUrl(baseUrl: "https://www.graine.ai", voiceAgentId: session.webcallAgentId,
                                       transport: .websocket, cacheKey: session.pageVersion ?? "",
                                       idle: true, publishableKey: "pk_live_…", accent: session.accent)
}

GraineProtocol.deliver

Call it only if you host the engine page yourself, to post a message into it.

static func deliver(_ message: [String: Any]) -> String?

The JavaScript that delivers one message into the page; nil when the message cannot be serialised (dropped, never a crash).

ParameterTypeDefaultMeaning
message[String: Any]requiredOne message for the page, as GraineProtocol.out builds it.

Returns — String?: the script that posts it into the page; nil when it cannot be written as JSON. Example value: "window.postMessage({\"type\":\"graine:set-muted\",\"muted\":true}, \"*\"); true;".

Errors — none.

let script = GraineProtocol.deliver(GraineProtocol.out(GraineProtocol.Out.setMuted, ["muted": true]))

GraineProtocol.out

Call it only if you host the engine page yourself, to build a message for it.

static func out(_ type: String, _ fields: [String: Any?] = [:]) -> [String: Any]

One outgoing message: its type and its fields as JSON values.

ParameterTypeDefaultMeaning
typeStringrequiredThe message's type — one of the GraineProtocol.Out names.
fields[String: Any?][:]The message's other fields, by name; each becomes a JSON value (nil becomes null).

Returns — [String: Any]: the message, with type set. Example value: ["type": "graine:set-muted", "muted": true].

Errors — none.

let message = GraineProtocol.out(GraineProtocol.Out.setMuted, ["muted": true])

GraineProtocol.parse

Call it only if you host the engine page yourself, to read a message from it.

static func parse(_ raw: String?) -> PageMessage?

One message from the page, typed; nil for anything that is not one of the page's messages.

ParameterTypeDefaultMeaning
rawString?requiredOne message from the page, as JSON text.

Returns — PageMessage? — its fields: PageMessage. Example value: .ready.

Errors — none.

let message = GraineProtocol.parse(#"{"type":"graine:muted","muted":true}"#)   // .muted(true)

PageMessage

Read it only if you host the engine page yourself.

Every message the engine page sends, typed — returned by GraineProtocol.parse(_:). Used by the SDK itself; apps do not need it.

What to expect — The SDK sends and reads these for you; they are public so a custom UI can reuse them.

CaseMeaning
case readyThe page is mounted and ready.
case call(connected: Bool, connecting: Bool)The call's state.
case muted(Bool)The microphone was muted or unmuted.
case caption(role: GraineRole, text: String, live: Bool)A caption line.
case widget([String: Any])A component the agent drew.
case widgetPending([String: Any])A component on its way.
case appAction(actionId: String, name: String, arguments: [String: Any])The agent asked for an action.
case endedThe call is over.
case micDenied(reason: String)The microphone was refused.
case transport(transport: GraineTransport, requested: GraineTransport, reason: String?, message: String?)What carries the audio, and why.
case quality(rttMs: Double?, jitterMs: Double?, loss: Double?)One quality measurement.
case network(online: Bool)The network went or came back.
case reconnecting(active: Bool)The call is being re-established, or that is over.
case progress(GraineConnectProgress)Where a connect is.

Appendix: the Objective-C interface

Use it from an Objective-C app, or from a bridge that reaches Swift through the Objective-C runtime (the Kotlin Multiplatform SDK binds it): the same SDK through GraineObjC… classes, with Foundation types and completion blocks in place of async and Swift enums. Every selector is fixed with @objc(…), so it never moves when Swift names change. Swift apps use the API above.

Objective-C

GraineObjC *graine = [GraineObjC configureWithBaseUrl:@"https://www.graine.ai" publishableKey:@"pk_live_…"];
[graine identify:@{@"user_id": @"u_8812", @"name": @"Asha"}];
[graine.chat openWithSize:@"full"];
[graine scheduledCallsWithCompletion:^(NSArray<GraineObjCScheduledCall *> *calls, NSError *error) {
    if (error) { NSLog(@"%@: %@", error.userInfo[@"code"], error.localizedDescription); return; }
}];

Each table gives the member's Swift name, its Objective-C form, and what it is for.

Errors and JSON in Objective-C

Read it to handle a refusal from Objective-C, and to use the …JSON methods from a bridge.

  • Errors. Every completion's NSError is in the domain ai.graine.inapp (GraineObjC.errorDomain). error.userInfo[@"code"] is the stable code to branch on — identity_required, too_soon, network, … (Error codes and their fixes); error.userInfo[@"retryAfterSeconds"] is how long a rate limit asked you to wait, when Graine said; error.localizedDescription is the sentence to show; error.code is the HTTP status, 0 when the refusal was made on the device or no answer came back. An error that is not one of Graine's refusals arrives with the code server_error.
  • JSON. The …JSON methods carry what Foundation values cannot carry exactly across a bridge (a whole number against a boolean, a missing key against a null one): JSON text in, JSON text out, with times as milliseconds since 1970.
  • Threads. Completion blocks, listeners and observers run on the main thread; the methods may be called from any thread.

GraineObjC

Use it as the entry point from Objective-C: configure an agent, say who the customer is, describe the screen, register actions and book calls.

@interface GraineObjC : NSObject — one per configured agent, over the same agent GraineInApp is; made by configureWithBaseUrl:… and found again with current or named:. Who the customer is, the screen and the phone's calls are the app's, shared by every agent; the chat, the voice bar, actions and requests are each agent's own.

MemberObjective-CWhat it does
chat@property (readonly) GraineObjCChat *chatThe agent's typed conversation: open it, send, close it, and read its state.
calls@property (readonly) GraineObjCCalls *callsNative calls on CallKit and VoIP pushes — one call at a time for the whole app; a call placed here is with this agent.
voice@property (readonly) GraineObjCVoice *voiceThe voice bar's in-app call, for an app that draws its own controls.
errorDomain@property (class, readonly) NSString *errorDomain"ai.graine.inapp": the domain of every NSError this interface hands out, to recognise them.
configure(baseUrl:publishableKey:)+ (GraineObjC *)configureWithBaseUrl:(NSString *)baseUrl publishableKey:(NSString *)publishableKeyConfigure the default agent — once, at launch; a second call returns the first.
configure(baseUrl:publishableKey:variables:)+ configureWithBaseUrl:publishableKey:variables:The same, with prompt variables known at build time (an NSDictionary).
configure(baseUrl:publishableKey:appVersion:debug:endOnBackground:captureTaps:transport:variablesJSON:)+ configureWithBaseUrl:publishableKey:appVersion:debug:endOnBackground:captureTaps:transport:variablesJSON:All the options at once, for a bridge. transport takes ws, rtc or nil (nil leaves it to the dashboard), and variablesJSON takes a JSON object.
configure(…, name:)+ configureWithBaseUrl:publishableKey:appVersion:debug:endOnBackground:captureTaps:transport:variablesJSON:name:An agent by name, with its own publishable key; an empty name is the default agent, and a name already configured returns that agent unchanged. Never nil in practice.
configure(…, name:, chatSize:)+ configureWithBaseUrl:publishableKey:appVersion:debug:endOnBackground:captureTaps:transport:variablesJSON:name:chatSize:The same, with the chat window's size: compact, large or full; nil or any other word follows the dashboard's Window size.
named(_:)+ (nullable GraineObjC *)named:(NSString *)nameThe agent configured under that name — here or from Swift — or nil.
current@property (class, readonly, nullable) GraineObjC *currentThe default agent, else the first configured; nil before any is.
name@property (readonly) NSString *nameThe name given to this agent when it was configured — default for the default agent.
installId@property (readonly) NSString *installIdThis install's id, kept in the Keychain — for your support team; the agent never sees it.
memoryChoice@property (readonly) BOOL memoryChoiceThe current customer's memory choice: YES until they turn it off.
identify(_:)- identify:Who is signed in, as an NSDictionary of traits; nil signs out. Keeps the customer's memory choice.
identify(_:memory:)- identify:memory:Sets the signed-in customer and whether their conversations are remembered.
identifyJSON(_:)- identifyJSON:The bridge form of identify:, with the traits as a JSON object; passing nil or null signs out.
identifyJSON(_:memory:)- identifyJSON:memory:identify:memory: for a bridge, the traits as a JSON object.
setMemory(_:)- setMemory:Turn remembering the current customer's conversations on (YES) or off (NO), from their next conversation.
forgetMemory(completion:)- forgetMemoryWithCompletion:Delete what the agent remembers about the signed-in customer; the block gets how many memories were deleted, or the error (identity_required, device_limit, …).
memories(completion:)- memoriesWithCompletion:What the agent remembers about the signed-in customer, newest first: an array of GraineObjCMemory, or the error.
memoriesJSON(completion:)- memoriesJSONWithCompletion:The same for a bridge: a JSON array of {id, text, createdAt, updatedAt}, times in milliseconds.
setVariables(_:)- setVariables:Prompt variables from the app's data, merged into those set: NSNull under a key removes it, nil clears them all.
setVariablesJSON(_:)- setVariablesJSON:setVariables: for a bridge, the variables as a JSON object.
currentVariables@property (readonly) NSDictionary<NSString *, NSString *> *currentVariablesEvery variable the agent will receive, merged in precedence order — the dashboard's defaults, the build's, setVariables:, then the traits from identify: — values as strings, already masked: GraineInApp.currentVariables().
currentVariablesJSON@property (readonly) NSString *currentVariablesJSONThe same as a JSON object, for a bridge.
setScreen(_:)- setScreen:What the customer is looking at: screen, title, journey, fields, error, data, availableActions; nil, or no screen, clears it.
setScreenJSON(_:)- setScreenJSON:setScreen: for a bridge, the screen as a JSON object.
updateField(_:)- updateField:One field changed — name, label, value, status, error, optional — merged into the current screen by name.
updateFieldJSON(_:)- updateFieldJSON:updateField: for a bridge, the field as a JSON object.
removeField(_:)- removeField:Take a field off the current screen, by name, when it leaves the screen.
clearScreen(_:)- clearScreen:The customer left a screen; with an id, only if it is still the one described.
track(_:data:)- track:data:A product event that informs the agent without interrupting.
trackEvent(_:dataJSON:)- trackEvent:dataJSON:track:data: for a bridge, the data as a JSON object.
tap(_:)- tap:Record a tap on a named control; the agent sees tap: and the name.
noteInteraction()- noteInteractionThe customer did something that is not a field edit — scrolled, opened a picker — so the clock that spots a stuck customer restarts.
registerAction(_:handler:)- registerAction:handler:Something the agent can do in your app: the handler runs on the main thread with the arguments and calls its answer block once, from any thread, within six seconds. Returns a GraineObjCRegistration.
registerJSONAction(_:handler:)- registerJSONAction:handler:The same for a bridge: the arguments arrive as a JSON object, and the answer block takes the status (ok, refused, error), the message and the data as a JSON object (or nil).
scheduleCall(at:reason:completion:)- scheduleCallAt:reason:completion:Book a call for that time from a screen of your own; the block gets the booked GraineObjCScheduledCall, or the refusal (scheduling_off, too_soon, outside_window, …).
scheduleCallJSON(at:reason:completion:)- scheduleCallJSONAt:reason:completion:The same for a bridge: the call as JSON {id, at, status, source, reason}.
scheduledCalls(completion:)- scheduledCallsWithCompletion:The customer's calls still to come, soonest first.
scheduledCallsJSON(completion:)- scheduledCallsJSONWithCompletion:The same for a bridge: a JSON array of {id, at, status, source, reason}, times in milliseconds.
cancelScheduledCall(_:completion:)- cancelScheduledCall:completion:Call off one booked call; the block gets its status afterwards (cancelled), or the error (not_found, …).
bookingAvailable@property (readonly) BOOL bookingAvailableWhether this agent can be booked from the app now — Graine rings this iPhone, the agent lets customers book, the customer is identified. Show your own "Call me" button only then; read it again after identify: and the sessionReady event.
session@property (readonly, nullable) GraineObjCSession *sessionThe agent's settings, once they have loaded; nil before.
sessionJSON@property (readonly, nullable) NSString *sessionJSONThe settings as JSON, for a bridge; nil before they have loaded.
themeJSON@property (readonly, nullable) NSString *themeJSONThe brand's design tokens to draw right now (GraineInApp.theme) as JSON — the same keys and words on every platform, the chat's and the call's tokens included — for UI of your own; nil before the settings load and when the agent has no design. The theme event says when they change.
addListener(_:)- addListener:Hear every event the agent reports (held weakly, so keep a reference).
removeListener(_:)- removeListener:Stop telling that listener.
observeEvents(_:)- (GraineObjCRegistration *)observeEvents:(void (^)(NSString *name, NSString *json))observerFor a bridge: delivers each event's name with its payload as JSON, until you stop the registration.

GraineObjC (view controllers)

Use them to show the SDK's own screens from Objective-C, or from a bridge that hosts native views.

The SDK's screens as UIKit view controllers, drawn in the agent's design exactly as the SwiftUI views are.

MemberObjective-CWhat it does
chatViewController(onClose:)- (UIViewController *)chatViewControllerWithOnClose:(nullable void (^)(void))onCloseThe chat as a screen; onClose draws the header's close control (nil: none — the navigation it sits in closes it). It connects as soon as it appears and hangs up once it is gone.
callViewController()- (UIViewController *)callViewControllerThe in-app call screen, used while the app is open — ringing, connecting, the call itself, ended, and a missed ring with Call back.
voiceBarViewController(name:onClose:)- voiceBarViewControllerWithName:onClose:The voice bar, filling the view it is given and drawn at its bottom: name beside the status (nil: the dashboard's heading); onClose when the customer hung up or closed the bar. It mounts the engine when it appears.
presentBooking(reason:completion:)- presentBookingWithReason:completion:The booking sheet over whatever is on screen; the block gets the booked call once it closes, or nil. See GraineObjC.presentBooking.

GraineObjC.presentBooking

Call it to let the customer book a call on the SDK's own sheet from Objective-C.

- (void)presentBookingWithReason:(nullable NSString *)reason
                      completion:(void (^)(GraineObjCScheduledCall * _Nullable call))completion;

In Swift: func presentBooking(reason: String?, completion: @escaping (GraineObjCScheduledCall?) -> Void). Shows the booking sheet over whatever is on screen — a sheet on an iPhone, a centred panel on an iPad — exactly as GraineInApp.presentBooking(reason:) does (Booking a call: the SDK's sheet). The block runs once, on the main thread, after the sheet has closed: the booked call, or nil when nothing was booked — including when no window is on screen yet or a booking sheet is already open. Safe to call from any thread. Check bookingAvailable first: opened where it is NO, the sheet says why and books nothing.

ParameterTypeDefaultMeaning
reasonNSString * (nullable)requiredWhat "What's it about? (optional)" starts with; nil leaves it empty. The customer may change it; up to 120 characters.
completionblockrequiredCalled once after the sheet has closed, with the booked GraineObjCScheduledCall or nil.

Returns — nothing; the block receives a GraineObjCScheduledCall (its members below), or nil.

Errors — none: a refusal stays in the sheet as one line, and the customer picks again.

GraineObjC *graine = GraineObjC.current;
if (graine.bookingAvailable) {
    [graine presentBookingWithReason:@"Talk through my order" completion:^(GraineObjCScheduledCall *call) {
        if (call) { NSLog(@"Booked for %@", call.at); }
    }];
}

GraineObjCListener

Implement it to hear the SDK's events in an Objective-C app.

@protocol GraineObjCListener — added with addListener: on GraineObjC, held weakly, called on the main thread. Its one method is required.

MemberObjective-CWhat it does
graineEvent(_:payload:json:)- (void)graineEvent:(NSString *)name payload:(NSDictionary *)payload json:(NSString *)jsonCalled once per event with its name, its payload, and the same payload as JSON text.

The events, by name:

EventPayloadWhen
sessionReadyagentId, webcallAgentId, memoryEnabled, declaredActions, identity (field, devicesPerUser), calls (hosted, platforms, ringTimeoutSeconds, callName, usersCanBook, maxDaysAhead, minLeadMinutes, maxPendingPerUser)The agent's settings have loaded — read bookingAvailable again now
captionrole (agent or user), text, liveA line on the in-app call, again as it fills in
voiceEndednothingThe in-app call is over
widgetwidget: the componentThe agent drew a component
widgetPendinghint: what is known of the coming componentA component is on its way
chatMessageturn: id, role, text, live, delivery, at (milliseconds), proactive, restored, channelThe agent finished a chat message
actionRequestednameThe agent asked for one of your actions
actionCompletedname, status, messageThe answer the agent received for it
frictionname, screen, detailThe customer looks stuck
micDeniedreasonThe microphone was refused
themethe design's tokens, as themeJSON has them (empty when there are none)The tokens to draw changed: the settings arrived, or the device switched between light and dark
errormessageSomething your team must fix

GraineObjCRegistration

Keep it to stop what you registered or observed.

@interface GraineObjCRegistration : NSObject — what registerAction:handler: and every observe… method return.

MemberObjective-CWhat it does
unregister()- unregisterStops it: the action is unregistered, or the observer no longer called. Safe to call more than once.

GraineObjCActionResult

Use it to answer an action the agent asked for.

@interface GraineObjCActionResult : NSObject — passed to the answer block of registerAction:handler:. The agent speaks about exactly this, so say what actually happened.

MemberObjective-CWhat it does
status@property (readonly) NSString *statusHow it turned out: ok, refused or error.
message@property (readonly) NSString *messageA sentence for the agent — not the customer — about what happened; the agent decides how to phrase it.
data@property (readonly) NSDictionary *dataA structured result the agent can reason about; empty unless given.
ok(_:)+ (GraineObjCActionResult *)ok:(NSString *)messageIt happened.
ok(_:data:)+ ok:data:It happened, with a structured result.
refused(_:)+ refused:It will not happen, and why — often the most useful answer.
error(_:)+ error:It was tried and failed.

GraineObjCMemory

Read it to list one thing the agent remembers about the customer.

@interface GraineObjCMemory : NSObject — what memoriesWithCompletion: returns, newest first, already redacted.

MemberObjective-CWhat it does
identifier@property (readonly) NSString *identifierThe memory's id, as Graine keeps it.
text@property (readonly) NSString *textWhat is remembered, in a sentence — to show the customer.
createdAt@property (readonly, nullable) NSDate *createdAtWhen it was first remembered; nil when Graine did not say.
updatedAt@property (readonly, nullable) NSDate *updatedAtWhen it last changed; nil when Graine did not say.

GraineObjCScheduledCall

Read it to show one booked call.

@interface GraineObjCScheduledCall : NSObject — a call booked to ring this customer (or this install) later: what scheduleCallAt:reason:completion:, scheduledCallsWithCompletion: and presentBookingWithReason:completion: return.

MemberObjective-CWhat it does
identifier@property (readonly) NSString *identifierThe booking's id (a UUID) — what cancelScheduledCall:completion: takes.
at@property (readonly, nullable) NSDate *atWhen it rings; nil only when Graine's time could not be read. Show it in the device's time zone.
status@property (readonly) NSString *statusWhere it is: scheduled, ringing, answered, completed, declined, missed, failed, cancelled, expired, no_device — or a newer word; give every check a default.
source@property (readonly, nullable) NSString *sourceWho booked it: agent, sdk (your app), dashboard or api; nil when Graine does not say.
reason@property (readonly, nullable) NSString *reasonWhat the call is about, as it was booked (up to 120 characters); nil when none was given.
isUpcoming@property (readonly) BOOL isUpcomingStill to come: scheduled, or ringing now.
canCancel@property (readonly) BOOL canCancelIt can still be called off: scheduled, not rung yet.

GraineObjCSession

Read it to see what the dashboard says about the agent, once its settings have loaded.

@interface GraineObjCSession : NSObject — GraineObjC.session.

MemberObjective-CWhat it does
agentId@property (readonly) NSString *agentIdThe agent's id, as the dashboard shows it.
memoryEnabled@property (readonly, nullable) NSNumber *memoryEnabledA boolean: whether the agent remembers customers between conversations; nil when the settings do not say.
identityField@property (readonly) NSString *identityFieldThe trait that names the customer: user_id unless the dashboard chose email, phone or a custom trait.
devicesPerUser@property (readonly) NSInteger devicesPerUserHow many devices one customer's memory may be used from, 1 to 5 (2 by default).
usersCanBook@property (readonly) BOOL usersCanBookCustomers may book calls from the app.
hostedRinging@property (readonly) BOOL hostedRingingGraine rings this workspace's customers itself (In-app calling is set up).
callName@property (readonly, nullable) NSString *callNameThe ring name the dashboard set for the agent's calls, if it set one.
json@property (readonly) NSString *jsonThe same settings as JSON — the text sessionJSON gives.

GraineObjCCalls

Use it to place, answer and draw native calls from Objective-C.

@interface GraineObjCCalls : NSObject — GraineObjC.calls: calls on CallKit and VoIP pushes. The customer calls the agent (startCallWithReason:variables:), or the agent rings the customer (enableVoIPPushes at every launch). A call connects as GraineCalls.startCall describes: one 20-second deadline, and connected only once its audio runs.

MemberObjective-CWhat it does
configure(displayName:ringtoneSound:ringTimeout:includesCallsInRecents:)- configureWithDisplayName:ringtoneSound:ringTimeout:includesCallsInRecents:How calls ring and what they show — once, at launch, before enableVoIPPushes. An empty name is your app's; a ringTimeout of 0 or less is 30 seconds. It starts the road the audio takes again from auto: set callTransport after it.
callTransport@property NSString *callTransportThe road the calls' audio takes, in both directions (GraineCallsConfig.callTransport): auto — the default, WebRTC when the GraineInAppRTC package is in the app and the dashboard allows it — websocket or webrtc; any other word is auto. Applies from the next call.
enableVoIPPushes()- enableVoIPPushesReceive the agent's calls as VoIP pushes; call it at every launch.
startCall(reason:variables:)- (nullable NSString *)startCallWithReason:(nullable NSString *)reason variables:(nullable NSDictionary *)variablesThe customer calls this agent: returns the call's id, or nil when another call is live.
startCallJSON(reason:variablesJSON:)- startCallJSONWithReason:variablesJSON:The same for a bridge, the variables as a JSON object.
answer()- answerPicks up the ringing call when you draw your own call screen.
decline()- declineDecline the ringing call; nothing happens when none is ringing.
end()- endHang up — or decline, while it rings.
setMuted(_:)- setMuted:Stop (YES) or resume (NO) sending the customer's voice, from your own mute control.
setSpeaker(_:)- setSpeaker:The loudspeaker (YES) or the earpiece or headset (NO), from your own speaker control.
callBack()- (nullable NSString *)callBackReturn the missed call: the new call's id, or nil when there is nothing to return or a call is live.
dismissMissedCall()- dismissMissedCallThe customer has seen the missed call, so the call screen can go.
state@property (readonly) NSString *stateWhere the call is: idle, ringing, connecting, active or ended.
endReason@property (readonly, nullable) NSString *endReasonWhy it ended, while state is ended: local, remote, failed, declined or missed; nil otherwise.
callId@property (readonly, nullable) NSString *callIdThe current (or just-ended) call's id; nil with no call.
callerName@property (readonly) NSString *callerNameThe name the call shows; empty with no call.
direction@property (readonly, nullable) NSString *directionincoming (the agent rang) or outgoing (the customer called); nil with no call.
muted@property (readonly) BOOL mutedThe customer's voice is not being sent — draw your mute control from it.
speakerOn@property (readonly) BOOL speakerOnThe audio is on the loudspeaker — draw your speaker control from it.
agentSpeaking@property (readonly) BOOL agentSpeakingThe agent's voice is playing right now — animate an avatar from it.
hosted@property (readonly) BOOL hostedGraine rang this call: one booked for the customer ("Scheduled call").
agent@property (readonly, nullable) NSString *agentThe agent the call is with, by name (default for the default agent); nil with no call.
showsCallScreen@property (readonly) BOOL showsCallScreenA call screen has something to show — a call, or a missed ring to call back: present one while it is YES.
transport@property (readonly, nullable) NSString *transportThe road the call's audio runs on: webrtc or websocket; nil before it is decided — read it once state is active. See GraineCalls.transport.
transportFallbackReason@property (readonly, nullable) NSString *transportFallbackReasonWhy a call that tried WebRTC runs on the WebSocket: mint_failed, register_failed, no_response, no_answer, ice_timeout or error (what each means); nil otherwise.
snapshotJSON@property (readonly) NSString *snapshotJSONThe call as JSON, for a bridge: every property above (transport and transportFallbackReason included), plus caption (role, text, live) and missedCall (callId, callerName, reason, hosted, at).
addListener(_:)- addListener:Hear the calls (held weakly, on the main thread).
removeListener(_:)- removeListener:Stop telling that listener.
observe(_:)- observe:For a bridge: the block runs after every change to the call, until the registration is stopped.
observeOutcomes(_:)- observeOutcomes:Hands a bridge every outcome as JSON: {callId, outcome, reason, durationSeconds, hosted, agent}.
observeIncoming(_:)- observeIncoming:For a bridge: each ring as JSON {callId, callerName, reason, hosted, agent} — agent when known.
callPermissionsJSON@property (readonly) NSString *callPermissionsJSONThe call permissions as JSON {microphone, notifications, fullScreen}, each granted, denied, blocked or notNeeded (GraineCalls.callPermissions).
requestCallPermissionsJSON(completion:)- requestCallPermissionsJSONWithCompletion:Asks for what the system can still ask for (GraineCalls.requestCallPermissions); the block gets the permissions as callPermissionsJSON has them, on the main thread.
openCallSettings(_:)- (BOOL)openCallSettings:(NSString *)itemOpens Settings for microphone, notifications or fullScreen: YES when Settings was asked to open, NO for any other word.
callSetupProblemsJSON@property (readonly) NSString *callSetupProblemsJSONWhat the app's setup still lacks for calls, as JSON [{code, message}] (GraineCalls.callSetupProblems); [] when nothing is missing.

GraineObjCCallsListener

Implement it to hear the calls in an Objective-C app — for your own call screen and your server.

@protocol GraineObjCCallsListener — added with addListener: on GraineObjCCalls, held weakly, called on the main thread. All three methods are required.

MemberObjective-CWhat it does
graineCallsChanged(_:)- (void)graineCallsChanged:(GraineObjCCalls *)callsSomething about the call changed — its state, its controls or its caption: read the properties of the GraineObjCCalls you are handed. At most once per turn of the main loop.
graineCallOutcome(_:)- (void)graineCallOutcome:(GraineObjCCallOutcome *)outcomeWhat became of a call — send it to your server, with its call id.
graineIncomingCall(_:callerName:hosted:)- (void)graineIncomingCall:(NSString *)callId callerName:(nullable NSString *)callerName hosted:(BOOL)hostedAn agent's call is ringing on this device: its id, the name it rings with (nil when the push named none), and whether Graine rang it.

GraineObjCCallOutcome

Read it to tell your server what became of a call.

@interface GraineObjCCallOutcome : NSObject — what graineCallOutcome: and observeOutcomes: report. Every ring reports ringing first, then the rest.

MemberObjective-CWhat it does
callId@property (readonly) NSString *callIdThe call's id: your server's call_id, Graine's for a ring it sent, or the one startCallWithReason:variables: returned.
outcome@property (readonly) NSString *outcomeringing, answered, declined, missed, failed or ended.
reason@property (readonly, nullable) NSString *reasonWhy: busy or no_answer with missed; local, remote or failed with ended; with failed, the sentence (up to 200 characters) — such as "The call could not connect within 20 seconds." or "The call's audio could not start."; nil otherwise.
durationSeconds@property (readonly, nullable) NSNumber *durationSecondsHow long a call that connected lasted, in whole seconds — with ended only.
hosted@property (readonly) BOOL hostedGraine rang it: a call booked by the agent, the customer or your team.
agent@property (readonly, nullable) NSString *agentThe name of the agent on the call (default for the default agent), or nil if that is unknown.
json@property (readonly) NSString *jsonThe same outcome as JSON text, ready to send.

GraineObjCVoice

Use it to drive the voice bar's in-app call from your own controls.

@interface GraineObjCVoice : NSObject — GraineObjC.voice: the agent talking inside your app.

MemberObjective-CWhat it does
attach()- attachMount the engine in the key window, idle, so a later start connects quickly; start does it by itself.
start()- startPlace the in-app call, mounting the engine first if needed; asks for the microphone when the app does not hold it.
end()- endHang up the in-app call.
setMuted(_:)- setMuted:Stop (YES) or resume (NO) sending the customer's voice.
setCaptionsOn(_:)- setCaptionsOn:The captions switch: YES shows the line being said in the voice bar, NO hides it; the call goes on (GraineVoice.setCaptionsOn(_:)).
clearCaptions()- clearCaptionsForget what has been said without ending the call (GraineVoice.clearCaptions()).
connected@property (readonly) BOOL connectedA call is up — draw your hang-up control from it.
connecting@property (readonly) BOOL connectingA call is being placed or re-established — show that it is connecting.
muted@property (readonly) BOOL mutedThe customer's voice is not being sent — draw your mute control from it.
captionsOn@property (readonly) BOOL captionsOnThe captions switch: YES (the default) until it is turned off — draw your CC control from it.
snapshotJSON@property (readonly) NSString *snapshotJSONFor a bridge, the state as JSON: {connected, connecting, muted, captionsOn, captions}, each caption {role, text, live}.
observe(_:)- observe:The block runs on the main thread after every change, until the registration is stopped.

GraineObjCChat

Use it to drive and draw the typed conversation from Objective-C.

@interface GraineObjCChat : NSObject — GraineObjC.chat: open when the customer is in the chat, send:, close when they leave, and the state read whenever a listener hears a change.

MemberObjective-CWhat it does
turns@property (readonly) NSArray<GraineObjCChatTurn *> *turnsWhat has been said, oldest first — draw one bubble per turn.
status@property (readonly) NSString *statusThe connection: idle, connecting, online, reconnecting or offline.
reconnectAttempt@property (readonly) NSInteger reconnectAttemptThe current attempt number (out of six) during a reconnect; 0 at any other time.
typing@property (readonly) BOOL typingThe agent is working on a reply — show a typing bubble.
isOpen@property (readonly) BOOL isOpenThe customer is in the chat: open was called and close was not.
upcomingCalls@property (readonly) NSArray<GraineObjCScheduledCall *> *upcomingCallsThe customer's calls still to come, soonest first, at most three — the chat window's Upcoming calls card (GraineChat.upcomingCalls).
snapshotJSON@property (readonly) NSString *snapshotJSONThe whole chat as JSON, for a bridge: {turns, status, attempt, typing, open, upcomingCalls}, each turn {id, role, text, live, delivery, at, proactive, restored, channel}, each upcoming call {id, at, status, source, reason}.
open()- openPuts the customer in the chat, connecting and keeping the connection up.
open(size:)- openWithSize:The same, at a size for this open: compact, large or full; nil or another word follows the app's chat size, then the dashboard's.
newConversation(completion:)- newConversationWithCompletion:Start a fresh conversation; the block gets nil, or Graine's refusal — the chat is then as it was.
close()- closeThe customer left: hang up.
send(_:)- send:Send what the customer typed: shown at once, sent now or when the chat connects.
retry()- retryConnects right away and sends whatever was left unsent.
clear()- clearEmpty the chat on screen; the conversation goes on, a message still waiting is still sent, and nothing Graine keeps is touched (GraineChat.clear()).
answerWidget(id:dataJSON:summary:)- answerWidgetWithId:dataJSON:summary:Answer a component the agent drew, from a UI of your own, on the conversation that is live (GraineInApp.answerComponent): dataJSON the customer's choice as a JSON object (nil: none), summary the sentence that is their answer.
refreshUpcomingCalls()- refreshUpcomingCallsFetch the upcoming calls again — after a booking made elsewhere; the card refreshes itself otherwise.
cancelUpcomingCall(_:)- cancelUpcomingCall:Cancel one of the upcoming calls, by its id: it leaves upcomingCalls once Graine has called it off.
addListener(_:)- addListener:Hear the chat change (held weakly, on the main thread).
removeListener(_:)- removeListener:Stop telling that listener.
observe(_:)- observe:For a bridge: the block runs on the main thread after every change, until the registration is stopped.

GraineObjCChatListener

Implement it to redraw your chat in an Objective-C app.

@protocol GraineObjCChatListener — added with addListener: on GraineObjCChat, held weakly, called on the main thread. Its one method is required.

MemberObjective-CWhat it does
graineChatChanged(_:)- (void)graineChatChanged:(GraineObjCChat *)chatThe chat changed — its turns, its status, the typing indicator or the upcoming calls: read the GraineObjCChat you are handed and redraw. At most once per turn of the main loop.

GraineObjCChatTurn

Read it to draw one message of the chat.

@interface GraineObjCChatTurn : NSObject — one message in GraineObjCChat.turns: the customer's, or one reply of the agent's.

MemberObjective-CWhat it does
identifier@property (readonly) NSString *identifierStable for the life of the transcript — t1, t2, … — so you can update a bubble in place.
role@property (readonly) NSString *roleWho said it: user (the customer) or agent.
text@property (readonly) NSString *textWhat was said; an agent's words grow as they stream in.
live@property (readonly) BOOL liveThe agent's words are still arriving: draw them as provisional.
delivery@property (readonly) NSString *deliveryWhere a customer's message is: sending, sent or failed (then retry sends it); always sent for the agent's.
at@property (readonly) NSDate *atWhen it was said — for an agent's turn, its first words.
proactive@property (readonly) BOOL proactiveThe agent spoke up without being asked — badge your chat if the customer is not looking.
restored@property (readonly) BOOL restoredCarried over from an earlier conversation and brought back when the chat opened (the customer returned).
channel@property (readonly) NSString *channelchat, or voice for a turn said on a call — draw a run of them as one "Voice call" row, not their words.

What's new in 0.9.0

  • Calls on WebRTC, opt-in. A separate package, GraineInAppRTC (https://github.com/GraineAI/inapp-ios-rtc, or pod 'GraineInAppRTC', :git => 'https://github.com/GraineAI/inapp-ios-rtc.git', :tag => '0.10.3'), always the same version as GraineInApp: add it beside GraineInApp and the native calls — the ones the customer places and the ones your agent rings — carry their audio over WebRTC, with a jitter buffer, loss concealment and the platform's echo canceller. Nothing else changes in the app; GraineRTC.install() exists for builds that strip code nothing references. GraineInApp itself still has no dependencies. See Add WebRTC calls.
  • A call WebRTC cannot carry continues on the WebSocket inside the same call — no second ring, "Connecting…" a moment longer. A call the agent cannot take at all (no credit left, the agent switched off) ends with that reason.
  • GraineCallsConfig.callTransport: .auto (the default), .websocket or .webrtc. The agent's In-app call audio setting (GraineHostedCallsSettings.audioTransport) can keep its calls on the WebSocket without an app release. calls.transport says which road carried a call and calls.transportFallbackReason why one that tried WebRTC did not. Objective-C: callTransport, transport and transportFallbackReason on GraineObjCCalls; GraineCallsHost: the callTransport option, and both values in the snapshot.
  • With the package in the app, a call Graine rings gets ready while it rings, so answering connects at once; a declined or missed ring lets go of what it prepared.
  • chat.clear() empties the chat on screen — the screen only: the conversation goes on and nothing Graine keeps is touched. Objective-C: clear on GraineObjCChat.
  • answerComponent(widgetId:data:summary:) answers a component from a UI of your own, on the conversation that is live — the chat or the call. Objective-C: answerWidgetWithId:dataJSON:summary: on GraineObjCChat, beside the chat's upcomingCalls, refreshUpcomingCalls and cancelUpcomingCall:.
  • The captions switch: voice.captionsOn (on by default), setCaptionsOn(_:) and clearCaptions() — GraineAgentBar shows the line being said only while it is on, and captions keeps what is said either way. Objective-C: the same on GraineObjCVoice.
  • GraineChatRows: the rows a chat window draws — a bubble per message, one "Voice call · 6:05 PM" row per earlier call, a day line where the day changes — for a chat of your own, with dayLabel, callLabel, callSpoken, clock, offersStarters and offersNewConversation.
  • voice.viewOpened() / viewClosed(): a voice UI of your own on screen counts as the bar does, so a page still loading behind it is reported as slowConnect.
  • Objective-C: currentVariables and currentVariablesJSON, and themeJSON with a theme event, on GraineObjC.
  • Variables over a conversation's kilobyte are trimmed to fit, not all dropped: who_called and call_reason ride first, and the biggest of the rest are left off, one at a time, until the rest fit.
  • scheduleCall's own refusals say the booking sheet's words: "Booking a call isn't available right now", "Pick a time at least 5 minutes from now" ("1 minute" for one) and "Pick a time within 14 days".

What's new in 0.8.0

  • Booking a call, drawn by the SDK. GraineInApp.presentBooking(reason:) shows the booking sheet in your agent's design — a sheet on iPhone, a centred panel on iPad: the day, the time on the system's time wheel every 5 minutes, the phone's time zone, "What's it about? (optional)" and Book call. It returns the booked call, or nil when the customer closes it without booking; a refusal stays in the sheet as one line in plain words. SwiftUI: .graineBookingSheet(isPresented:graine:reason:onBooked:) and GraineBookingView; UIKit: GraineBookingViewController; Objective-C: presentBookingWithReason:completion:. See Booking with the SDK's sheet.
  • bookingAvailable, on each agent and on GraineObjC: Graine rings this iPhone, the agent lets customers book, and the customer is identified. Show your own "Call me" button only then.
  • Book a call in the chat window, where booking is available: under the booked calls in the Upcoming calls card, or alone in the card's place when none are booked — never when the design hides the card. A booking refreshes the card.
  • A call never sits on "Connecting…". Everything before the conversation opens has one 20-second deadline; past it the call fails with "The call could not connect within 20 seconds."
  • A call is connected once it can be heard. If CallKit has not started the call's audio 3 seconds after the conversation opens, the SDK starts it itself, once; with still no audio 3 seconds later the call fails with "The call's audio could not start." — instead of staying open and silent.
  • Calls start at once on the configuration the SDK already has — this launch's, or the one kept on the device — and refresh it behind the call.
  • Prompt variables ride the ticket. They are no longer added to the connection's address, for a call or a chat; only the direct connection, used when no ticket can be had, still carries them.
  • With debug on, CallKit activating and deactivating a call's audio is logged.

The full list for every platform: Changelog.

Every symbol, A to Z

Every public symbol of the iOS SDK — 1023 in all — by name, each linked to where this page documents it: a type or function first, then its members.

On this page

Requirements, install, permissions and initialisationInstall — Swift Package Manager (primary)Install — CocoaPodsAdd WebRTC calls (optional)Permissions and capabilitiesWhat the SDK configures in CallKitInitialiseInstall and the first answerGraineInApp.configureGraineConfig (constructor)GraineConfig.Timing (constructor)GraineInApp.sessionGraineInAppGraineConfigGraineConfig.TimingGraineSessionConfig (constructor)GraineSessionConfigIdentity, memory and conversations that continueGraineInApp.identifyGraineInApp.setMemoryGraineInApp.memoryChoiceGraineInApp.installIdGraineInApp.memoriesGraineInApp.forgetMemoryGraineChat.newConversationGraineMemory (constructor)GraineMemoryGraineIdentitySettings (constructor)GraineIdentitySettingsChatGraineChat.openGraineChat.closeGraineChat.sendGraineChat.retryGraineChat.clearGraineChatView (constructor)GraineChatViewController (constructor)GraineChatRows.buildGraineChatRows.dayLabelGraineChatRows.callLabelGraineChatRows.callSpokenGraineChatRows.clockGraineChatRows.offersStartersGraineChatRows.offersNewConversationGraineChatGraineChatSizeGraineChatTurnGraineChatTurn.DeliveryGraineChatChannelGraineChatStatusGraineRoleGraineChatViewGraineChatViewControllerGraineChatRowsGraineChatRowGraineChatDayLineGraineChatDayGraineChatCallLineThe voice barGraineAgentBar (constructor)GraineAgentBar.accessoryGraineLauncher (constructor)GraineVoice.attachGraineVoice.attachGraineVoice.detachGraineVoice.startGraineVoice.endGraineVoice.setMutedGraineVoice.retryGraineVoice.setCaptionsOnGraineVoice.clearCaptionsGraineVoice.viewOpenedGraineVoice.viewClosedGraineLoader (constructor)GraineLoaderText (constructor)GraineLoaderText.forMomentGraineCaption (constructor)GraineTransport.fromGraineConnectionQuality.classifyGraineConnectionQuality.classifyGraineAgentBarGraineSurfaceGraineLauncherGraineVoiceGrainePhaseGraineConnectStageGraineConnectProgressGraineConnectivityGraineLoaderGraineLoaderTextGraineLoaderLookGraineCaptionGraineTransportGraineConnectionQualitySeveral agents in one appGraineInApp.namedGraineInApp.allGraineInApp.currentGraineInApp.sharedGraineInApp.nameScreen context, actions and componentsGraineInApp.setScreenGraineInApp.updateFieldGraineInApp.removeFieldGraineInApp.clearScreenGraineScreen (constructor)GraineScreenGraineScreen.Journey (constructor)GraineScreen.JourneyGraineField (constructor)GraineFieldGraineField.StatusGraineInApp.setVariablesGraineInApp.currentVariablesGraineInApp.trackGraineInApp.tapView.graineTapView (SwiftUI modifiers)GraineVoice.reportEventGraineInApp.noteInteractionGraineInApp.registerActionGraineInApp.registerActionGraineInApp.checkActionsGraineActionCheckGraineActionResult (constructor)GraineActionResult.okGraineActionResult.refusedGraineActionResult.errorGraineActionResultGraineActionResult.StatusGraineInApp.configureFrictionGraineFrictionConfig (constructor)GraineFrictionConfig (constructor)GraineFrictionConfigGraineFrictionConfig.Screen (constructor)GraineFrictionConfig.ScreenGraineFrictionSignalGraineMasking.maskGraineMasking.addRuleGraineMasking.addRuleGraineMasking.clearCustomRulesGraineMasking.deepGraineMaskingGraineWidgetView (constructor)GraineWidgetViewGraineVoice.answerWidgetGraineInApp.answerComponentGraineInApp.reopenComponentGraineWidget (constructor)GraineWidgetGraineWidgetSurfaceGraineWidgetPresetCalls the user placesGraineCalls.configureGraineCallsConfig (constructor)GraineCallsConfigGraineCalls.startCallGraineCalls.endGraineCalls.setMutedGraineCalls.setSpeakerGraineCallView (constructor)GraineCallViewGraineCalls.addListenerGraineCalls.removeListenerGraineCallsListener.onCallStateGraineCallsListener.onCallOutcomeGraineCalls.transportGraineCalls.transportFallbackReasonGraineCallTransport.fromGraineRTC.installGraineCallsListenerGraineCallsGraineCallStateGraineCallDirectionGraineCallEndReasonGraineCallOutcomeGraineCallTransportGraineRTCGraineRTCVersionCalls Graine ringsGraineCalls.enableVoIPPushesGraineCalls.voipTokenUpdatedGraineCalls.reportIncomingCallGraineCallsListener.onIncomingCallGraineCalls.answerGraineCalls.callBackGraineCalls.dismissMissedCallGraineIncomingCall (constructor)GraineIncomingCallGraineMissedCall (constructor)GraineMissedCallGraineHostedCallsSettingsPermissions and the setup checkGraineCalls.callPermissionsGraineCalls.requestCallPermissionsGraineCalls.openCallSettingsGraineCalls.callSetupProblemsGraineCallPermissionGraineCallPermissionStatusGraineCallPermissionsGraineCallSetupProblemBooking with the SDK's sheetBooking a call: the SDK's sheetGraineInApp.bookingAvailableGraineInApp.presentBookingView.graineBookingSheetGraineBookingView (constructor)GraineBookingViewController (constructor)GraineBookingViewGraineBookingViewControllerThe scheduled-calls listGraineInApp.schedulingRulesGraineInApp.scheduleCallGraineInApp.scheduledCallsGraineInApp.cancelScheduledCallGraineChat.upcomingCallsGraineChat.refreshUpcomingCallsGraineChat.cancelUpcomingCallGraineScheduledCall (constructor)GraineScheduledCallGraineCallStatus (constructor)GraineCallStatusGraineCallSource (constructor)GraineCallSourceGraineSchedulingRules (constructor)GraineSchedulingRules.earliestGraineSchedulingRules.latestGraineSchedulingRulesGraineRingPlatformDesignGraineInApp.designGraineInApp.themeGraineDesign.tokensGraineDesign.parseGraineDesignGraineDesignSchemeGraineThemeTokensGraineThemeTokens.ColorsGraineThemeTokens.TypographyGraineThemeTokens.Typography.ScaleGraineThemeTokens.Typography.WeightsGraineThemeTokens.Typography.PairGraineThemeTokens.ShapeGraineThemeTokens.RadiusGraineThemeTokens.SpacingGraineThemeTokens.ElevationGraineThemeTokens.ShadowGraineThemeTokens.OffsetGraineThemeTokens.MotionGraineThemeTokens.Motion.effectiveDurationMsGraineThemeTokens.IconsGraineThemeTokens.GlyphGraineThemeTokens.GlassGraineThemeTokens.launcherGlyphUrlGraineChatTokensGraineCallTokensGraineCallTokens.StatusTextGraineLauncherShapeGraineBubbleStyleGraineDesignChromeGraineGlyph.urlGraineGlyphGraineNativeAppearanceGraineLogoPlateGraineWidgetPaletteGoing liveDebug loggingPrivacy manifestBefore you shipGraineVersionRate limits and quotasEvents and listenersGraineInApp.addListenerGraineInApp.removeListenerGraineListenerGraineListener.onSessionReadyGraineListener.onCallStateGraineListener.onCaptionGraineListener.onWidgetGraineListener.onWidgetPendingGraineListener.onEndedGraineListener.onMicDeniedGraineListener.onQualityGraineListener.onNetworkGraineListener.onReconnectingGraineListener.onTransportGraineListener.onConnectivityGraineListener.onActionRequestedGraineListener.onActionCompletedGraineListener.onFrictionGraineListener.onThemeGraineListener.onChatMessageGraineListener.onErrorErrorsError codes and their fixesGraineServiceError (constructor)GraineServiceErrorGraineErrorCode (constructor)GraineErrorCodeGraineCallsHostErrorLower-level building blocksGraineCallsHostGraineCallsHost: permissions and the setup checkGraineCallsHost.restoreGraineCallsHost.configureGraineCallsHost.snapshotGraineCallsHost.enableVoIPPushesGraineCallsHost.handlePushGraineCallsHost.startCallGraineCallsHost.answerGraineCallsHost.declineGraineCallsHost.endGraineCallsHost.setMutedGraineCallsHost.setSpeakerGraineCallsHost.setIdentityGraineCallsHost.setMemoryGraineCallsHost.installIdGraineCallsHost.setVariablesGraineCallsHost.setScreenGraineCallsHost.setActionsGraineCallsHost.resolveActionGraineCallsHost.addSinkGraineCallsHost.removeSinkGraineCalls.providerDidResetGraineCalls.providerGraineCalls.providerGraineCalls.providerGraineCalls.providerGraineCalls.providerGraineCalls.providerGraineCalls.providerGraineCalls.providerGraineCalls.pushRegistryGraineCalls.pushRegistryGraineCalls.pushRegistryGraineChatViewController.viewDidLoadGraineProtocolGraineProtocol.OutGraineProtocol.InGraineProtocol.engineUrlGraineProtocol.deliverGraineProtocol.outGraineProtocol.parsePageMessageAppendix: the Objective-C interfaceErrors and JSON in Objective-CGraineObjCGraineObjC (view controllers)GraineObjC.presentBookingGraineObjCListenerGraineObjCRegistrationGraineObjCActionResultGraineObjCMemoryGraineObjCScheduledCallGraineObjCSessionGraineObjCCallsGraineObjCCallsListenerGraineObjCCallOutcomeGraineObjCVoiceGraineObjCChatGraineObjCChatListenerGraineObjCChatTurnWhat's new in 0.9.0What's new in 0.8.0Every symbol, A to Z