Graine AI

Flutter SDK reference

Every public class, function, property, stream and error of graine_inapp 0.10.3 for Flutter, grouped by what you are building: what each is for, its signature, parameters, what it returns, the errors it raises, and what to expect.

Every public declaration the Flutter package, graine_inapp 0.10.3, exports, read from its source and grouped by what you are doing: installing, identifying the customer, the chat, the voice bar, several agents, screen context and components, calls in both directions, booking, design, going live, events and errors. Each 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 beside the functions that use it, field by field, with what to expect. The same API across every platform, topic by topic, is the SDK reference; installing and setting up is Flutter.

Requirements, install, permissions and initialisation

Packagegraine_inapp 0.10.3 on pub.dev (Android and iOS)
RequiresFlutter 3.19+ and Dart 3.3+; iOS 15+ (its pod, Swift 5.9); Android 7 / API 24+ (Java 17, compileSdk 36). Depends on http and webview_flutter (4.8+) only.
Native codeThe package carries the native call engine (the iOS and Android SDKs' call code), so adding it or updating it needs a new store build

YAML — pubspec.yaml

dependencies:
  graine_inapp: ^0.10.3
  permission_handler: ^11.0.0     # or whatever your app already uses to ask for permissions
  • Calls that ring on Android: add Firebase Cloud Messaging to the Android app — google-services.json in android/app, the Google services Gradle plugin, and com.google.firebase:firebase-messaging in android/app/build.gradle.kts — and the SDK registers for pushes itself: its own messaging service rings Graine's calls natively, with no Dart. An app that uses firebase_messaging has its messaging service instead, which receives every message: it forwards them to handlePush (below).

  • Android: nothing to add to your manifest — the package's own merges into the app: the permissions (internet, microphone, notifications, the full-screen call notification and the microphone's foreground service), the call screen, the microphone service and the call receiver. Set minSdk 24 or higher, and ask for the notification permission on Android 13+ yourself (for example Permission.notification.request()), so a ring can show.

  • iOS: on the Runner target add NSMicrophoneUsageDescription to Info.plist (for example "The assistant listens when you talk to it."), the Push Notifications capability, and Background Modes → Audio, AirPlay, and Picture in Picture and Voice over IP. CallKit and VoIP pushes need a real iPhone: the simulator cannot ring.

  • Microphone: for the bar's voice, your app asks, in GraineConfig.requestMicrophone, which the SDK calls before every call (left out, the SDK assumes you asked). For native calls, the SDK asks.

Flutter · Dart — lib/main.dart

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:graine_inapp/graine_inapp.dart';
import 'package:permission_handler/permission_handler.dart';
 
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  GraineInApp.init(GraineConfig(
    baseUrl: 'https://www.graine.ai',
    publishableKey: 'pk_live_…',                       // Agent → Embed & Widgets → Install
    requestMicrophone: () async => (await Permission.microphone.request()).isGranted,
  ));
  await GraineCalls.instance.configure(const GraineCallsOptions(
    publishableKey: 'pk_live_…',
    displayName: 'Asha from Acme',
  ));
  GraineCalls.instance.attach();                       // identity, variables, the screen and actions reach the call
  runApp(MaterialApp(
    // Once, above the navigator: a call outlives every route.
    builder: (context, child) => GraineEngine(child: Stack(children: [child!, const GraineAgentBar()])),
    home: const HomeScreen(),
  ));
}

WebRTC calls

Turn it on to carry the native calls' audio over WebRTC instead of the WebSocket — in both directions: the customer calling the agent (startCall, your "Call" button) and the customer answering a call Graine rang (a booked call, or "Ring now"). Off unless the app is built with it. New in 0.9.0.

iOS — at the top of ios/Podfile:

ENV['GRAINE_WEBRTC'] = '1'

Android — in android/gradle.properties:

graineWebRtc=true

On a build server, GRAINE_WEBRTC=1 in the build's environment turns it on for both. Then rebuild — it is native code, so it reaches customers with your next store build. flutter build runs pod install when the Podfile changes; with only the environment variable, run pod install in ios/ too.

  • Nothing else changes. No code to write, no Info.plist entry, no capability: the same ring, the same call screen, the same "Connecting…", the same outcomes. GraineCallsOptions(callTransport:) chooses how calls carry their audio, and GraineCallTransport.auto, the default, takes WebRTC in a build that has it — see GraineCallTransport.
  • What the customer notices. 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. The agent's voice arrives at telephone bandwidth over WebRTC today: steadier through a weak network, narrower than the WebSocket's on a perfect one.
  • When WebRTC cannot set up a call — a network that blocks it, a server that turns it down, no answer within the time allowed (about 12 seconds in all) — the call continues on the WebSocket inside the same call: no second ring, no new call screen, no error; "Connecting…" shows a moment longer. A call the agent cannot take at all (no credit left, the agent switched off) ends with that reason, exactly as without WebRTC. Once audio flows, a call never moves: a network drop is retried once, then the call ends as dropped. The snapshot says which one carried each call, and why: GraineCallSnapshot.transport and GraineCallSnapshot.transportFallbackReason.
  • 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 voice bar's own in-app call has its own setting: GraineConfig.transport and the agent's Call audio.)
  • Size. A prebuilt WebRTC library — Google's WebRTC, its symbols renamed so it never clashes with another copy in the app (flutter_webrtc, a video-calling SDK). On an iPhone (arm64), about 12 MB more installed and about 6 MB more to download. On Android, per phone (an app bundle delivers one ABI): about 7.4 MB installed on 64-bit ARM (about 2.5–3.3 MB to download), 4.3 MB on 32-bit ARM, 8.2–8.4 MB on x86, and about 0.5 MB of classes.
  • One version of the library. CocoaPods installs one version of this WebRTC build: if another plugin in the app pins it at a different version, the two cannot be installed together. Leave WebRTC off (calls stay on the WebSocket) or align the versions.
  • Permissions. Android: the plugin adds ACCESS_NETWORK_STATE to your manifest — a normal permission, granted at install: nothing is asked — and its R8 rules ship inside it. iOS: nothing new.
  • Licences. WebRTC is BSD 3-Clause, with Google's patent grant: list it in your app's open-source licences.
await GraineCalls.instance.configure(const GraineCallsOptions(
  publishableKey: 'pk_live_…',
  displayName: 'Asha from Acme',
  callTransport: GraineCallTransport.auto,   // the default: WebRTC in a build that has it
));

Which one carried a call, read once it is active:

GraineCalls.instance.addListener(() {
  final call = GraineCalls.instance.value;
  if (call.status != GraineNativeCallStatus.active) return;
  if (call.transport == GraineCallTransport.websocket && call.transportFallbackReason != null) {
    debugPrint('WebRTC could not carry this call (${call.transportFallbackReason}); it runs on the WebSocket.');
  }
});

Install and the first answer

Configure each agent once, at launch, with its publishable key; place GraineEngine once, above your navigator; the agent's settings (its session: design, identity, memory and booking rules) then arrive by themselves. Everything else on this page goes through the agent init returns. Readiness: loadSession() completes when the settings are in, and GraineConnectivity says whether the page that carries the voice call is there.

GraineInApp.init

Call it once at launch, before runApp, to configure an agent with its publishable key — every other call on this page goes through the agent it returns.

static GraineInApp init(GraineConfig config, {String name = defaultName})

Configures an agent and starts fetching its configuration at once. Call it before runApp or in your first widget; it initialises the Flutter binding itself. Without a name it is the default agent. The name is trimmed, and an empty name is the default agent. A later call with a name already configured returns that agent unchanged (the new config is not applied).

ParameterTypeDefaultMeaning
configGraineConfigrequiredThe configuration (see its type).
nameStringdefaultNameThe agent's name in your app — the one it was configured under; the default agent when it is left out or empty.

Returns — a new GraineInApp.

Errors — none.

GraineInApp.init(GraineConfig(baseUrl: 'https://www.graine.ai', publishableKey: 'pk_live_…'));

GraineInApp

Use it to reach an agent you configured — its call, chat, components, session and design — and to make every request your app sends Graine on the customer's behalf.

The in-app agent: one instance per publishable key, configured with GraineInApp.init(config) before runApp and reached afterwards as GraineInApp.instance (or GraineInApp.named('support') for an agent configured under a name). Accessing instance before init throws StateError. It is a WidgetsBindingObserver. Use it on the main (UI) isolate only.

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 const String defaultName = 'default'StringThe name of the default agent: the one init configures when given no name, which instance returns and every view draws unless handed another.
static Map<String, GraineInApp> get allMap<String, GraineInApp>All the agents you have configured, keyed by name and listed in configuration order. Unmodifiable.
static GraineInApp get instanceGraineInAppThe default agent: the one configured without a name, or, in an app that named every agent, the first one configured.
static GraineInApp? get maybeInstanceGraineInApp?The default agent, or null before init. Use it to ask whether the SDK has been configured.
@visibleForTesting static GraineInApp forTesting(GraineConfig config, {http.Client? httpClient, int Function()? clock, bool install = false, String name = defaultName, Directory? fontCacheDir, Future<void> Function(String name, Uint8List bytes)? fontRegistrar, GraineChatConnector? chatConnector})GraineInAppFor tests only: an instance with an injected HTTP client, clock, font cache directory, font registrar and chat connector, and no lifecycle observer. With install: true it joins the app's agents under name; otherwise it shares nothing with them.
@visibleForTesting static void resetForTesting()nothingFor tests only: forgets every agent and what they share, disposing each.
final GraineConfig configGraineConfigThe configuration this agent was initialised with.
final String nameStringThe name this agent was configured under; defaultName for the default agent.
late final GraineVoice voiceGraineVoiceThis agent's voice call. GraineEngine attaches it; voice.start() places a call.
late final GraineComponents componentsGraineComponentsThe components the agent draws (from the call or the chat), as state a GraineWidgetView renders.
GraineChatSession get chatGraineChatSessionThis agent's text chat, on its own connection; created on first use. GraineAgentBar and GraineChat open it while the customer is in it and close it when they leave; an app drawing its own chat calls open() and close() itself. Opening it also ends a call with any other agent and closes that agent's chat.
Future<String> get installIdFuture<String>This install's id: a UUID made on the app's first launch and kept by the platform (the Keychain on iOS, surviving reinstalls; storage excluded from backups on Android). The native call engine sends this same id, and every agent shares it. Graine uses it to count a user's devices, and it is never shown to the agent. Where there is no platform side (a test, a desktop build) it is a random UUID kept in memory for the run.
bool get bookingAvailableboolWhether this customer can book a call from here — show your own "Call me" button only then. See bookingAvailable.
GraineErrorCode? get bookingBlockerGraineErrorCode?Why booking is not available (schedulingOff or identityRequired), or null when it is.
String? get deviceTimezoneString?The device's time zone (IANA, "Asia/Kolkata") once the SDK has read it; null before then.
ValueListenable<bool> get bookingListenableValueListenable<bool>Exposes bookingAvailable as a listenable, so a widget can redraw whenever it changes.
GraineSchedulingRules get schedulingRulesGraineSchedulingRulesWhat the agent lets users book, and how Graine rings, from the agent's configuration. No request. Before the configuration loads, and from a server that says nothing, the defaults — which allow no booking.
FrictionRules get frictionRulesFrictionRulesThe friction rules in force now: the defaults, the dashboard's, then the app's.
Map<String, Object?>? get pendingWidgetMap<String, Object?>?The component the agent drew that is still waiting to be answered, as Graine sent it, so a view mounted later can still draw it. Answering or dismissing it clears it, and so does the start of a new call.
SessionConfig? get sessionSessionConfig?What the dashboard says about this agent; null until it has loaded.
ValueListenable<SessionConfig?> get sessionListenableValueListenable<SessionConfig?>The session, for a widget that rebuilds when it arrives.
ResolvedDesign? get designResolvedDesign?The design the brand chose in the dashboard, with both token sets. It is null before the session has loaded, and also when the server is too old to send designs.
ThemeTokens? get tokensThemeTokens?The tokens to use at this moment: the brand's fixed scheme if it has one, otherwise the device's light or dark set, chosen again whenever the device switches between them. Null without a design. For an app drawing its own UI; the SDK's widgets pick for themselves.
ValueListenable<ThemeTokens?> get tokensListenableValueListenable<ThemeTokens?>The tokens, for a widget that rebuilds when they change.
@override void didChangePlatformBrightness()nothingLifecycle callback: the device switched between light and dark, so the tokens are picked again. Called by Flutter; apps do not call it.
ValueListenable<Map<String, String>> get fontFacesValueListenable<Map<String, String>>The design's typefaces the SDK has loaded: family|weight (the design's family at the weight it really has) → the family name the face is registered under, for example "DM Sans|700" → "Graine-DMSans-700". Empty until one loads, and for "system", which is never fetched. To draw your own UI in the brand's typeface, set fontFamily to the registered name with FontWeight.normal: a loaded face is its weight.
bool get foregroundboolTrue when the app's most recent lifecycle report put it in the foreground.
@override void didChangeAppLifecycleState(AppLifecycleState state)nothingLifecycle callback: resumed is the foreground; inactive is ignored (iOS reports it for the app switcher, a pulled-down notification and the microphone permission dialog); anything else is the background. Called by Flutter; apps do not call it.
@visibleForTesting void setForegroundForTesting(bool foreground)nothingFor tests only: what the lifecycle callback calls.
@visibleForTesting void dispose()nothingFor tests only: stops every timer, detaches the engine, closes the chat and removes the agent from the app's agents. An app keeps its agents for its lifetime.

Its functions: init, loadSession, identify, installId, setMemory, memoryEnabled, memories, forgetMemory, named, all, instance, maybeInstance, defaultName, name, setVariables, currentVariables, setScreen, updateField, removeField, clearScreen, track, tap, captureTap, noteInteraction, registerAction, checkActions, executeAction, configureFriction, clearPendingWidget, bookingAvailable, bookingBlocker, bookingListenable, deviceTimezone, schedulingRules, scheduleCall, scheduledCalls, cancelScheduledCall, loadDesignFonts, addListener, removeListener — each with its parameters, what it returns, the errors it raises and an example.

GraineInApp.loadSession

Call it when you need the agent's settings (design, identity, memory, booking rules) before the SDK has fetched them on its own, or to try again after a fetch failed.

Future<SessionConfig?> loadSession()

Fetches the agent's configuration once; concurrent callers share one request, and a failure is retried by the next caller. Completes with null on failure, which is also reported through GraineListener.onError. init calls it.

Parameters — none.

Returns — Future<SessionConfig?> — its fields: SessionConfig. Example value: null.

Errors — none.

final result = await GraineInApp.instance.loadSession();

GraineConfig

Use it to say, once, which agent the app talks to and how: the publishable key, your build's prompt variables, fonts, timing and switches.

Everything the SDK needs up front, passed to GraineInApp.init. Immutable.

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
final String baseUrlStringrequired"https://www.graine.ai" unless self-hosted.
final String publishableKeyStringrequiredFrom 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.
final Map<String, Object?> variablesMap<String, Object?>emptyPrompt variables known at build time. The dashboard's defaults sit under them; setVariables and identify win over them. Unlike runtime variables and traits, the SDK does not mask these.
final Transport? transportTransport?nullForces a transport. Null lets the dashboard's Call audio setting decide.
final String? appVersionString?nullLands on the call record and in the screen context, to tell builds apart.
final bool endOnBackgroundbooltrueEnds a live call, and hangs up a connected chat, once the app has been in the background for two seconds (a moment away, such as a permission dialog, does not count). The chat reconnects when the app returns, if the customer is still in it. You want it on.
final GraineTiming timingGraineTimingGraineTiming()The page-load watchdog, retry ladder, end-call watchdog and "Still connecting…" threshold.
final bool debugboolfalseLogs, prefixed "[Graine]", every message across the engine bridge both ways, the chat's frames and the engine page's console. Outgoing variables and screen context are logged by key names and screen id only; incoming messages are logged as received, cut to 600 characters. Turn it on in debug builds.
final FrictionConfig? frictionFrictionConfig?nullThis app's layer of friction rules, over the dashboard's. Most apps set nothing here.
final Future<bool> Function()? requestMicrophoneFuture<bool> Function()?nullMakes sure the app holds the microphone before a call opens; this package has no permission plugin of its own, so pass the one the app already uses (for example () async => (await Permission.microphone.request()).isGranted). Called by GraineVoice.start() — and so by the bar's microphone button and GraineLauncher — before the call is placed. false: no call is opened and onMicDenied("NotAllowedError") is reported. If it throws, the error is logged and the call is attempted anyway. Null: the SDK assumes the app asked already; if it did not, the engine reports the refusal.
final List<String> fontsList<String>emptyThe font families the app bundles itself, by the family name its pubspec declares. The SDK fetches the brand's typeface by itself; a family listed here is the app's own copy, wins over the download and is never fetched. Empty: the SDK fetches every family the design uses, drawing the platform font until each arrives.
final bool captureTapsbooltrueTells the agent what the customer taps while a screen is set: a tap on a control with an accessibility label (Semantics(label: …)) as "tap:<label>", anywhere else as "tap:screen" with the position — in the recent events, at most two a second, masked like everything else. GraineEngine (given a child) sees the taps.
final GraineChatSize? chatSizeGraineChatSize?nullHow 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 a tablet), full the whole screen. Null follows the dashboard; a size passed to GraineChat.open wins for that open.

Its functions: GraineConfig — each with its parameters, what it returns, the errors it raises and an example.

GraineConfig.GraineConfig

Use it to build the configuration you pass to GraineInApp.init.

const GraineConfig({required this.baseUrl, required this.publishableKey, this.variables = const <String, Object?>{}, this.transport, this.appVersion, this.endOnBackground = true, this.timing = const GraineTiming(), this.debug = false, this.friction, this.requestMicrophone, this.fonts = const <String>[], this.captureTaps = true, this.chatSize})

The constructor.

ParameterTypeDefaultMeaning
baseUrlStringrequired"https://www.graine.ai" unless self-hosted.
publishableKeyStringrequiredFrom 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.
variablesMap<String, Object?>const <String, Object?>{}Prompt variables known at build time. The dashboard's defaults sit under them; setVariables and identify win over them. Unlike runtime variables and traits, the SDK does not mask these.
transportTransport?optionalForces a transport. Null lets the dashboard's Call audio setting decide.
appVersionString?optionalLands on the call record and in the screen context, to tell builds apart.
endOnBackgroundbooltrueEnds a live call, and hangs up a connected chat, once the app has been in the background for two seconds (a moment away, such as a permission dialog, does not count). The chat reconnects when the app returns, if the customer is still in it. You want it on.
timingGraineTimingconst GraineTiming()The page-load watchdog, retry ladder, end-call watchdog and "Still connecting…" threshold.
debugboolfalseLogs, prefixed "[Graine]", every message across the engine bridge both ways, the chat's frames and the engine page's console. Outgoing variables and screen context are logged by key names and screen id only; incoming messages are logged as received, cut to 600 characters. Turn it on in debug builds.
frictionFrictionConfig?optionalThis app's layer of friction rules, over the dashboard's. Most apps set nothing here.
requestMicrophoneFuture<bool> Function()?optionalAsks for the microphone before a call opens and answers whether the app holds it — pass the permission call your app already uses, for example () async => (await Permission.microphone.request()).isGranted. Null: the SDK assumes you asked.
fontsList<String>const <String>[]The font families the app bundles itself, by the family name its pubspec declares. The SDK fetches the brand's typeface by itself; a family listed here is the app's own copy, wins over the download and is never fetched. Empty: the SDK fetches every family the design uses, drawing the platform font until each arrives.
captureTapsbooltrueTells the agent what the customer taps while a screen is set: a tap on a control with an accessibility label (Semantics(label: …)) as "tap:<label>", anywhere else as "tap:screen" with the position — in the recent events, at most two a second, masked like everything else. GraineEngine (given a child) sees the taps.
chatSizeGraineChatSize?optionalHow 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 a tablet), full the whole screen. Null follows the dashboard; a size passed to GraineChat.open wins for that open.

Returns — a new GraineConfig.

Errors — none.

final result = GraineConfig(baseUrl: 'https://www.graine.ai', publishableKey: 'pk_live_…');

GraineTiming

Use it to change how long the SDK waits for the voice engine and a connect before it retries or says so — only for an app used on very slow networks.

Load and connect timing, for an app used on very slow networks. Passed as GraineConfig.timing; all values in milliseconds.

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
final int stallMsint15000Silence — nothing downloaded — before a page load is given up on and retried.
final int bootMsint15000Downloaded but still not ready after this long: the page is broken, not slow.
final int hardCapMsint120000The ceiling on one load attempt, however steadily it is arriving.
final List<int> retryDelaysMsList<int>1, 2, 4, 8, 15, 15 sThe whole retry ladder, each delay jittered by ±25%. The SDK reloads once per entry in the list; after the last one it stops trying and reports GraineConnectivity.failed. The chat's reconnect backoff uses the same ladder.
final int endWatchdogMsint4000The time the SDK waits for an end-call to be confirmed before it tears the call down on the device itself.
final int slowConnectMsint4000How long a connect the customer is waiting on runs before slowConnect turns true ("Still connecting…").

Its functions: GraineTiming — each with its parameters, what it returns, the errors it raises and an example.

GraineTiming.GraineTiming

Use it to build the timing you pass as GraineConfig(timing:).

const GraineTiming({this.stallMs = 15000, this.bootMs = 15000, this.hardCapMs = 120000, this.retryDelaysMs = const <int>[1000, 2000, 4000, 8000, 15000, 15000], this.endWatchdogMs = 4000, this.slowConnectMs = 4000})

The constructor.

ParameterTypeDefaultMeaning
stallMsint15000Silence — nothing downloaded — before a page load is given up on and retried.
bootMsint15000Downloaded but still not ready after this long: the page is broken, not slow.
hardCapMsint120000The ceiling on one load attempt, however steadily it is arriving.
retryDelaysMsList<int>const <int>[1000, 2000, 4000, 8000, 15000, 15000]The whole retry ladder, each delay jittered by ±25%. The SDK reloads once per entry in the list; after the last one it stops trying and reports GraineConnectivity.failed. The chat's reconnect backoff uses the same ladder.
endWatchdogMsint4000The time the SDK waits for an end-call to be confirmed before it tears the call down on the device itself.
slowConnectMsint4000How long a connect the customer is waiting on runs before slowConnect turns true ("Still connecting…").

Returns — a new GraineTiming.

Errors — none.

final result = GraineTiming();

GraineEngine

Use it to carry an agent's voice call: place it once near the root, above your navigator, so a call outlives every screen and starts at once.

class GraineEngine extends StatefulWidget

What to expect — Place it once, near the root, above your navigator — one per agent the customer can call. It is one invisible pixel; it loads the agent's page idle at mount, so a tap starts a call at once, and it tells the agent what the customer taps while a screen is set (GraineConfig.captureTaps).

PropertyTypeMeaning
final Widget? childWidget?Your app — usually the navigator's child in MaterialApp.builder. The engine itself is one invisible pixel beside it.
final GraineVoice? voiceGraineVoice?The call to attach.
final GraineInApp? appGraineInApp?The agent whose calls this engine carries.

Its functions: GraineEngine — each with its parameters, what it returns, the errors it raises and an example.

GraineEngine.GraineEngine

Use it to place the engine for the default agent — or, with app:, for another agent the customer can call.

const GraineEngine({super.key, this.child, this.voice, this.app})
ParameterTypeDefaultMeaning
keyKey?optionalThe widget's key, as for any Flutter widget.
childWidget?optionalYour app — usually the navigator's child in MaterialApp.builder. The engine itself is one invisible pixel beside it.
voiceGraineVoice?optionalThe call to attach.
appGraineInApp?optionalThe agent whose calls this engine carries.

Returns — a new GraineEngine.

Errors — none.

MaterialApp(
  // Once, above the navigator: a call outlives every route.
  builder: (context, child) => GraineEngine(child: Stack(children: [child!, const GraineAgentBar()])),
  home: const HomeScreen(),
);

SessionConfig

Read it to see what the dashboard says about this agent — its appearance, design, actions, identity, memory and call settings — once the session has arrived.

class SessionConfig

What to expect — Fetched once per launch when the agent is configured, and kept for the run; null until it arrives (GraineInApp.session). A setting the server does not send is null, and the SDK then behaves as before that setting existed.

PropertyTypeMeaning
final String agentIdStringThe agent's id in Graine — the one the publishable key selects.
final String webcallAgentIdStringThe id calls are placed and filed under. Place calls with it, not agentId — or their transcripts land under an agent nobody watches.
final Map<String, Object?> appearanceMap<String, Object?>The settings from Embed & Widgets → Appearance, unchanged from how they are stored.
final Map<String, Object?> variablesMap<String, Object?>The dashboard's variable defaults.
final String? pageVersionString?The id of the deployment, which the engine page is cached under.
final Set<String> declaredActionsSet<String>The in-app actions the dashboard declares.
final Map<String, Object?>? frictionMap<String, Object?>?The dashboard's friction rules, as served.
final ResolvedDesign? designResolvedDesign?The design the brand chose, already resolved into light and dark tokens. Null for an agent with no design: every surface then draws from appearance.
final String? wsHostString?The host the chat socket connects to when it has no ticket.
final IdentitySetting? identityIdentitySetting?Which trait identifies a customer. Null when the settings do not say, which means user_id (see identitySetting).
final MemorySetting? memoryMemorySetting?The agent's memory setting, or null when an older server leaves it out.
final CallsSetting? callsCallsSetting?Hosted ringing and the booking rules for users, or null when an older server leaves it out.
IdentitySetting get identitySettingIdentitySettingThe identity setting, or the one that applies when the server does not send it.
Transport get preferredTransportTransportThe dashboard's Call audio setting.
String? get accentString?The accent colour saved in the dashboard, unchanged — the leading "#" may be missing.

Its functions: SessionConfig, fromJson, errorFor — each with its parameters, what it returns, the errors it raises and an example.

SessionConfig.SessionConfig

Use it to build a session by hand for a test or a preview; the SDK fetches the real one.

const SessionConfig({ required this.agentId, required this.webcallAgentId, this.appearance = const <String, Object?>{}, this.variables = const <String, Object?>{}, this.pageVersion, this.declaredActions = const <String>{}, this.friction, this.design, this.wsHost, this.identity, this.memory, this.calls})
ParameterTypeDefaultMeaning
agentIdStringrequiredThe agent's id in Graine — the one the publishable key selects.
webcallAgentIdStringrequiredThe id calls are placed and filed under. Place calls with it, not agentId — or their transcripts land under an agent nobody watches.
appearanceMap<String, Object?>const <String, Object?>{}The settings from Embed & Widgets → Appearance, unchanged from how they are stored.
variablesMap<String, Object?>const <String, Object?>{}The dashboard's variable defaults.
pageVersionString?optionalThe id of the deployment, which the engine page is cached under.
declaredActionsSet<String>const <String>{}The in-app actions the dashboard declares.
frictionMap<String, Object?>?optionalThe dashboard's friction rules, as served.
designResolvedDesign?optionalThe design the brand chose, already resolved into light and dark tokens. Null for an agent with no design: every surface then draws from appearance.
wsHostString?optionalThe host the chat socket connects to when it has no ticket.
identityIdentitySetting?optionalWhich trait identifies a customer. Null when the settings do not say, which means user_id (see identitySetting).
memoryMemorySetting?optionalThe agent's memory setting, or null when an older server leaves it out.
callsCallsSetting?optionalHosted ringing and the booking rules for users, or null when an older server leaves it out.

Returns — a new SessionConfig.

Errors — none.

const session = SessionConfig(agentId: 'agent_123', webcallAgentId: 'agent_123_web');

SessionConfig.fromJson

Use it to read a session from the JSON Graine sends — for a session you stored, or a test; the SDK does this itself when it loads.

static SessionConfig fromJson(Map<String, Object?> data)

Reads the session as Graine sends it, the same way every Graine SDK reads it; anything missing takes its default.

ParameterTypeDefaultMeaning
dataMap<String, Object?>requiredThe session as Graine sent it, decoded from JSON.

Returns — SessionConfig.

Errors — none.

import 'dart:convert';
 
const stored = '{"agentId": "agent_123", "webcallAgentId": "agent_123_web"}';   // a session your app kept
final session = SessionConfig.fromJson(jsonDecode(stored) as Map<String, Object?>);

SessionConfig.errorFor

Use it to turn a refused session request into the sentence that tells whoever has to fix it what to change.

static String? errorFor(int status, Map<String, Object?> body)

For a refused session request, a sentence aimed at the person who has to fix it; for a 2xx response, null.

ParameterTypeDefaultMeaning
statusintrequiredThe HTTP status of the session request.
bodyMap<String, Object?>requiredIts body, decoded from JSON ({} when it was not JSON).

Returns — String?. Example value: 'Publishable key was rejected. Check it against Embed & Widgets in the dashboard.'.

Errors — none.

final sentence = SessionConfig.errorFor(401, const <String, Object?>{});

GraineConnectivity

Read it to know whether the page that carries the voice call is there — loading, ready, retrying or failed — and to offer a retry when it failed.

enum GraineConnectivity { loading, ready, retrying, failed }

What to expect — loading at launch and after retry(); ready once the page has loaded; retrying while a failed load waits for its next attempt (the bar says "Reconnecting…"); failed once every attempt in GraineTiming.retryDelaysMs is spent, or the agent's settings could not be loaded (the bar says "Couldn't load · tap to retry"). onConnectivity reports every change. A page that is carrying a call is never reloaded.

CaseMeaning
loadingThe page that carries the call is loading.
readyIt has loaded: a call can start at once.
retryingA load failed; the next attempt is scheduled.
failedEvery attempt failed. Offer "Try again" (retry()).

Identity, memory and conversations that continue

Tell the agent who the customer is, let them choose whether they are remembered, show them what is remembered and delete it, and start a conversation over. An identified customer who comes back — later today, or on their other phone — finds the chat where they left it, and the agent picks up from there. Nothing here is needed for an anonymous customer: they simply start fresh every time.

GraineInApp.identify

Call it when the customer signs in — and with null when they sign out — so the agent knows who it is talking to and can remember them between conversations.

void identify(Map<String, Object?>? traits, {bool? memory})

Who is signed in. Traits become prompt variables, masked on the device, and land on the call record — except the one trait the agent's Identity setting names (user_id unless it names email, phone or a custom trait), which is kept raw and sent only where Graine needs it to recognise the user, never as a prompt variable or in the screen context. A placeholder (such as "guest", "null" or an already-masked "[EMAIL]"), null, a boolean or a fraction identifies nobody: that conversation is anonymous, with no memory. memory: false: this user's conversations are neither remembered nor recalled; the choice is kept on the device for this user, and when left out their last choice stands (on until they make one). A conversation keeps the identity it started with; a change applies from the next conversation. null signs out: Graine is told this device is no longer the user's, a chat open for them ends and its transcript is cleared, and later conversations are anonymous. A different user signing in without a sign-out first signs the previous one out the same way. One user for the whole app: every agent the app configured hears it (each keeps the trait its own Identity setting names), and the native call engine receives it too.

ParameterTypeDefaultMeaning
traitsMap<String, Object?>?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?optionalfalse: 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.instance.identify({'user_id': user.id, 'name': user.firstName, 'plan': user.plan}, memory: user.allowsMemory);
// on sign-out
GraineInApp.instance.identify(null);

GraineInApp.installId

Read it when you need this install's id — the one the SDK and the native call engine send Graine for this device, the same for every agent.

Future<String> get installId

This install's id: a UUID made on the app's first launch and kept by the platform — in the Keychain on iOS, where it survives a reinstall, and in storage excluded from backups on Android. The native call engine sends the same id, and every agent shares it. Graine counts a customer's devices by it (Devices per user in the dashboard); identify ties it to the customer, and identify(null) lets it go.

Parameters — none.

Returns — Future<String> — a lower-case UUID. Example value: '6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44'.

Errors — none.

final id = await GraineInApp.instance.installId;

GraineInApp.setMemory

Call it when the customer turns "remember me" on or off in your app's settings.

void setMemory(bool enabled)

Turns remembering on or off for the current user's conversations; it does what identify(currentTraits, memory: enabled) does. Kept on the device for that user (for an anonymous user, for this run only). Applies to every agent and reaches the native call engine.

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.instance.setMemory(true);

GraineInApp.memoryEnabled

Call it to read the customer's memory choice — to show your settings switch in the right position.

Future<bool> memoryEnabled()

The current user's memory choice: true unless they turned it off. Waits for the agent's configuration and the choices kept on the device.

Parameters — none.

Returns — Future<bool>. Example value: true.

Errors — none.

final result = await GraineInApp.instance.memoryEnabled();

GraineInApp.memories

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

Future<List<GraineMemory>> memories()

What this agent remembers about the identified user, already redacted, in Graine's order: newest first, at most 100. For a "what you remember about me" screen. Only from a device that is theirs.

Parameters — none.

Returns — Future<List<GraineMemory>> — its fields: GraineMemory. Example value: [GraineMemory(id: 'm_41', text: 'Prefers a call after 6 pm', createdAt: 2026-10-02 09:14:00.000Z, updatedAt: 2026-10-12 17:40:00.000Z)].

Errors — GraineException: identityRequired (checked on the device first), deviceLimit, rateLimited, unavailable, invalidKey, originRefused, network, unknown

final result = await GraineInApp.instance.memories();

GraineInApp.forgetMemory

Call it when the customer asks the agent to forget them: it deletes everything the agent remembers about the signed-in customer.

Future<int> forgetMemory()

Deletes everything this agent's memory holds about the identified user. Allowed only from a device that is theirs. Completes with the number deleted (0 when Graine does not say).

Parameters — none.

Returns — Future<int> — how many memories were deleted. Example value: 3.

Errors — GraineException: identityRequired (checked on the device first), deviceLimit, rateLimited, unavailable, invalidKey, originRefused, network, unknown

final result = await GraineInApp.instance.forgetMemory();

GraineChatSession.newConversation

Call it when the customer wants to start over: a fresh conversation with the welcome and the starters, while what the agent remembers about them stays.

Future<void> newConversation()

Starts a new conversation — a fresh thread, as a support chat offers one.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

await GraineInApp.instance.chat.newConversation();

GraineMemory

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

class GraineMemory

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.

PropertyTypeMeaning
final String idStringGraine's id for this memory. Use it as the key of a list row.
final String textStringWhat is remembered, in a sentence.
final DateTime? createdAtDateTime?The UTC time the memory was first stored, or null if the server leaves it out.
final DateTime? updatedAtDateTime?The UTC time of its latest change, or null if the server leaves it out.

Its functions: GraineMemory, fromJson, listFrom, toJson — each with its parameters, what it returns, the errors it raises and an example.

GraineMemory.GraineMemory

Use it to build a memory by hand for a test or a preview; memories() returns the real ones.

const GraineMemory({required this.id, required this.text, this.createdAt, this.updatedAt})
ParameterTypeDefaultMeaning
idStringrequiredGraine's id for this memory. Use it as the key of a list row.
textStringrequiredWhat is remembered, in a sentence.
createdAtDateTime?optionalThe UTC time the memory was first stored, or null if the server leaves it out.
updatedAtDateTime?optionalThe UTC time of its latest change, or null if the server leaves it out.

Returns — a new GraineMemory.

Errors — none.

const memory = GraineMemory(id: 'm_1', text: 'Prefers calls in the evening');

GraineMemory.fromJson

Use it to read one memory from JSON you stored; memories() already returns them read.

static GraineMemory? fromJson(Object? raw)

Returns null for anything other than a memory that has an id and some text.

ParameterTypeDefaultMeaning
rawObject?requiredThe value as decoded from JSON; anything that is not the expected object reads as null.

Returns — GraineMemory?. Example value: GraineMemory(id: 'm_41', text: 'Prefers a call after 6 pm', createdAt: 2026-10-02 09:14:00.000Z, updatedAt: 2026-10-12 17:40:00.000Z).

Errors — none.

final memory = GraineMemory.fromJson({'id': 'mem_1', 'text': 'Prefers a call after 6 pm', 'createdAt': '2026-10-01T09:30:00Z'});

GraineMemory.listFrom

Use it to read a list of memories from an answer you stored or forwarded, newest first.

static List<GraineMemory> listFrom(Object? body)

{memories: [...]} (or a bare list), in Graine's order — newest first. Entries that are not memories are skipped.

ParameterTypeDefaultMeaning
bodyObject?requiredThe answer's body as decoded: {memories: [...]} or a bare list.

Returns — List<GraineMemory>. Example value: [GraineMemory(id: 'm_41', text: 'Prefers a call after 6 pm', createdAt: 2026-10-02 09:14:00.000Z, updatedAt: 2026-10-12 17:40:00.000Z)].

Errors — none.

final memories = GraineMemory.listFrom({'memories': [{'id': 'mem_1', 'text': 'Prefers a call after 6 pm'}]});

GraineMemory.toJson

Use it to turn a memory into JSON — to cache it or hand it to your own server.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

for (final memory in await GraineInApp.instance.memories()) {
  debugPrint(memory.toJson().toString());
}

IdentitySetting

Read it to know which trait identifies a customer for this agent (user_id unless the dashboard names another) and how many devices each customer may use.

class IdentitySetting

What to expect — Part of the session; null until it arrives, and from an agent that does not say — which means user_id and 2 devices.

PropertyTypeMeaning
final String fieldStringThe trait Graine uses to recognise a user: user_id, email, phone, or the name of a custom trait.
final int devicesPerUserintThe most devices a single user can have at once, from 1 to 5.
static const IdentitySetting standardIdentitySettingThe setting assumed when an older server sends none: identify by the user_id trait.

Its functions: IdentitySetting, fromJson — each with its parameters, what it returns, the errors it raises and an example.

IdentitySetting.IdentitySetting

Use it to build the identity setting by hand for a test; the session carries the real one.

const IdentitySetting({this.field = Identity.defaultField, this.devicesPerUser = 2})
ParameterTypeDefaultMeaning
fieldStringIdentity.defaultFieldThe trait Graine uses to recognise a user: user_id, email, phone, or the name of a custom trait.
devicesPerUserint2The most devices a single user can have at once, from 1 to 5.

Returns — a new IdentitySetting.

Errors — none.

final result = IdentitySetting();

IdentitySetting.fromJson

Use it to read the identity setting from the session's JSON.

static IdentitySetting? fromJson(Object? raw)

Null when raw is not an object — the settings did not say.

ParameterTypeDefaultMeaning
rawObject?requiredThe value as decoded from JSON; anything that is not the expected object reads as null.

Returns — IdentitySetting?. Example value: null.

Errors — none.

final identity = IdentitySetting.fromJson({'field': 'email', 'devicesPerUser': 2});

MemorySetting

Read it to know whether the agent remembers identified customers between conversations.

class MemorySetting

What to expect — Part of the session; null until it arrives, and from an agent that does not say — which means memory off.

PropertyTypeMeaning
final bool enabledboolTrue when the agent carries its memory of identified users from one conversation into the next.

Its functions: MemorySetting, fromJson — each with its parameters, what it returns, the errors it raises and an example.

MemorySetting.MemorySetting

Use it to build the memory setting by hand for a test; the session carries the real one.

const MemorySetting({this.enabled = false})
ParameterTypeDefaultMeaning
enabledboolfalseTrue when the agent carries its memory of identified users from one conversation into the next.

Returns — a new MemorySetting.

Errors — none.

final result = MemorySetting();

MemorySetting.fromJson

Use it to read the memory setting from the session's JSON.

static MemorySetting? fromJson(Object? raw)
ParameterTypeDefaultMeaning
rawObject?requiredThe value as decoded from JSON; anything that is not the expected object reads as null.

Returns — MemorySetting?. Example value: null.

Errors — none.

final memory = MemorySetting.fromJson({'enabled': true});

Identity

Use it to check on the device what the SDK treats as a customer's identity: which trait, which values identify nobody, and what is sent with a request.

abstract final class Identity

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
static const String defaultFieldStringThe trait that identifies a customer when the agent's setting names none: 'user_id'.
static const Set<String> placeholdersSet<String>Values that identify nobody, compared case-insensitively after trimming: empty, null, undefined, none, nil, nan, unknown, anonymous, anon, guest, 0, -, n/a, na, test, and the SDK's own masks ([phone], [email], [card] and the other ID masks). An app that hands over an already-masked value has identified no one.
static const int maxValueLengthintThe longest an identifier the servers key on can be (a user id or custom trait has at most 256 characters); a longer value is never sent.

Its functions: normaliseField, valueOf, pick, normaliseInstallId, normaliseTimezone, uuidV4, serverLowercase, serverTrim, normaliseValue, memoryKey, withoutIdentity, requestFields — each with its parameters, what it returns, the errors it raises and an example.

Identity.normaliseField

Use it to turn a trait name into the form the identity setting stores — or to learn that it is not a valid trait name.

static String? normaliseField(Object? field)

A trait name as the setting stores it: trimmed, lower case, and matching ^[a-z][a-z0-9_]{0,39}$ — else null.

ParameterTypeDefaultMeaning
fieldObject?requiredA trait name as the dashboard may hold it.

Returns — String?. Example value: 'customer_id'.

Errors — none.

final field = Identity.normaliseField(' Customer_ID ');

Identity.valueOf

Use it to read one trait's value the way the SDK sends it as an identity — or learn that it identifies nobody.

static String? valueOf(Object? v)

Returns one trait in the raw form the identity channel sends, or null for a value that identifies nobody: null, a boolean, a fraction, an object or a list, a placeholder, or anything longer than maxValueLength.

ParameterTypeDefaultMeaning
vObject?requiredOne trait's value, as the app passed it to identify.

Returns — String?. Example value: null (a placeholder identifies nobody).

Errors — none.

final value = Identity.valueOf('guest');

Identity.pick

Use it to find the identity among a customer's traits for the agent's identity trait.

static String? pick(Map<String, Object?>? traits, String field)

The identity in traits for the setting's field: the trait under exactly that name, else one whose name normalises to it (Customer_ID for customer_id).

ParameterTypeDefaultMeaning
traitsMap<String, Object?>?requiredThe customer's traits, as passed to identify.
fieldStringrequiredThe trait that identifies a customer — IdentitySetting.field.

Returns — String?. Example value: 'u_8812'.

Errors — none.

final value = Identity.pick({'user_id': 'u_8812', 'plan': 'gold'}, 'user_id');

Identity.normaliseInstallId

Use it to check that an install id is in the shape the SDK sends.

static String? normaliseInstallId(Object? id)

An install id this SDK will send: lower case and matching ^[0-9a-f-]{16,64}$ — else null.

ParameterTypeDefaultMeaning
idObject?requiredAn install id, as stored.

Returns — String?. Example value: '6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44'.

Errors — none.

final id = Identity.normaliseInstallId('6F1C2A9E-7D1B-4C47-9A6E-2F0D8B1C5E44');

Identity.normaliseTimezone

Use it to check that a time zone name is one the SDK sends (an IANA name such as Asia/Kolkata).

static String? normaliseTimezone(Object? zone)

Returns an IANA zone name that Graine's servers accept, such as Asia/Kolkata or UTC, or null.

ParameterTypeDefaultMeaning
zoneObject?requiredA time zone name, as the device reports it.

Returns — String?. Example value: 'Asia/Kolkata'.

Errors — none.

final zone = Identity.normaliseTimezone('Asia/Kolkata');

Identity.uuidV4

Use it to make a random version-4 UUID, the shape of an install id.

static String uuidV4([math.Random? random])

Makes a random lower-case UUIDv4, used as the install id where the platform cannot supply one (a test or a desktop build).

ParameterTypeDefaultMeaning
randommath.Random?optionalThe randomness to use; a secure random generator when left out. A test passes a seeded one.

Returns — String. Example value: '6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44'.

Errors — none.

final id = Identity.uuidV4();

Identity.serverLowercase

Use it to lower-case text exactly as Graine does when it compares identities.

static String serverLowercase(String s)

Lower-cases text exactly as Graine does when it compares identities — including the full mapping of U+0130 ("İ" → "i̇") and the final sigma ("ΟΔΟΣ" → "οδος"), which Dart's toLowerCase() does not apply.

ParameterTypeDefaultMeaning
sStringrequiredThe text to lower-case.

Returns — String. Example value: 'asha@example.com'.

Errors — none.

final lower = Identity.serverLowercase('Asha@Example.COM');

Identity.serverTrim

Use it to trim whitespace exactly as Graine does when it compares identities.

static String serverTrim(String s)
ParameterTypeDefaultMeaning
sStringrequiredThe text to trim.

Returns — String. Example value: 'u_8812'.

Errors — none.

final trimmed = Identity.serverTrim('  u_8812 ');

Identity.normaliseValue

Use it to see how Graine reads an identity value for a trait — trimmed, and lower case for an email — before it compares it.

static String? normaliseValue(String field, Object? value)
ParameterTypeDefaultMeaning
fieldStringrequiredThe identity trait the value belongs to (user_id, email, phone or a custom trait).
valueObject?requiredThe trait's value, as the app passed it.

Returns — String?. Example value: 'asha@example.com'.

Errors — none.

final value = Identity.normaliseValue('email', ' Asha@Example.com ');

Identity.memoryKey

Use it to compute the key the customer's memory choice is kept under on this device.

static String memoryKey(String field, String value)

The key one customer's memory choice is kept under on the device, the same on every Graine SDK: the first 16 bytes of the SHA-256 of graine:memory:v1, a newline, the trait, a newline and the normalised value, as hex. Nothing about the customer can be read back from it.

ParameterTypeDefaultMeaning
fieldStringrequiredThe identity trait.
valueStringrequiredThe customer's identity value for that trait.

Returns — String. Example value: 32 hex characters, e.g. '3f2a…'.

Errors — none.

final key = Identity.memoryKey('user_id', 'u_8812');

Identity.withoutIdentity

Use it to take the identity trait out of a set of traits — what the SDK does before traits become prompt variables.

static Map<String, Object?>? withoutIdentity(Map<String, Object?>? traits, String? field)

traits without the trait that identifies the customer — that one travels only with the identity, never as a prompt variable or in the screen context.

ParameterTypeDefaultMeaning
traitsMap<String, Object?>?requiredThe customer's traits, as passed to identify.
fieldString?requiredThe identity trait to take out; null takes nothing out.

Returns — Map<String, Object?>?. Example value: {'plan': 'gold'}.

Errors — none.

final rest = Identity.withoutIdentity({'user_id': 'u_8812', 'plan': 'gold'}, 'user_id');

Identity.requestFields

Use it to build the identity fields the SDK adds to a request to Graine.

static Map<String, Object?> requestFields({ String? value, required String installId, bool? memory, String? timezone, String? callId})
ParameterTypeDefaultMeaning
valueString?optionalThe customer's identity value; null for an anonymous customer.
installIdStringrequiredThis install's id (GraineInApp.installId).
memorybool?optionalThe customer's memory choice; null when they made none.
timezoneString?optionalThe device's IANA time zone; null when unknown.
callIdString?optionalThe call this request belongs to; null outside a call.

Returns — Map<String, Object?>.

Errors — none.

final fields = Identity.requestFields(value: 'u_8812', installId: await GraineInApp.instance.installId, memory: true, timezone: 'Asia/Kolkata');

Chat

The agent's typed conversation: the same agent, screen context and actions as the call, in writing. GraineAgentBar offers and opens it for you; GraineChat places it yourself; for a chat of your own design, draw from GraineInApp.instance.chat (a GraineChatSession) and lay its rows out with ChatRows and ChatPieces. One conversation at a time: opening the chat ends a call the bar holds.

GraineChat

Use it to place the chat window yourself — a screen, a tab or a sheet of your own — in the agent's design.

class GraineChat extends StatefulWidget

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
final GraineChatState? chatGraineChatState?The chat to draw — your own GraineChatState; the agent's own chat (GraineInApp.chat) when null.
final GraineComponents? componentsGraineComponents?The components to draw above the composer.
final GraineInApp? appGraineInApp?The agent on the other side of this chat; the view uses that agent's chat, components, session and design. When left out: the agent that chat belongs to, or failing that the default agent.
final ResolvedDesign? designResolvedDesign?The design to draw.
final String? schemeString?"light", "dark" or "auto".
final String? accentString?#RRGGBB over the design's accent.
final String? nameString?The header's title; the design's chat title, else the dashboard's heading, when null.
final VoidCallback? onCloseVoidCallback?The chevron in the header — usually a pop.
final bool showComponentsboolDraw the components the agent shows.
static const Key screenKeyKeyThe whole screen.

Its functions: GraineChat, open — each with its parameters, what it returns, the errors it raises and an example.

GraineChat.GraineChat

Use it to place the chat window where you want it; it opens the chat while it is on screen and closes it when it leaves.

const GraineChat({ super.key, this.chat, this.components, this.design, this.scheme, this.accent, this.name, this.onClose, this.showComponents = true, this.app})
ParameterTypeDefaultMeaning
keyKey?optionalThe widget's key, as for any Flutter widget.
chatGraineChatState?optionalThe chat to draw — your own GraineChatState; the agent's own chat (GraineInApp.chat) when null.
componentsGraineComponents?optionalThe components to draw above the composer.
designResolvedDesign?optionalThe design to draw.
schemeString?optional"light", "dark" or "auto".
accentString?optional#RRGGBB over the design's accent.
nameString?optionalThe header's title; the design's chat title, else the dashboard's heading, when null.
onCloseVoidCallback?optionalThe chevron in the header — usually a pop.
showComponentsbooltrueDraw the components the agent shows.
appGraineInApp?optionalThe agent on the other side of this chat; the view uses that agent's chat, components, session and design. When left out: the agent that chat belongs to, or failing that the default agent.

Returns — a new GraineChat.

Errors — none.

Navigator.of(context).push(PageRouteBuilder<void>(
  pageBuilder: (context, _, __) => GraineChat(onClose: () => Navigator.of(context).pop()),
));

GraineChat.open

Call it to open the chat now, at a size you choose for this once — on the agent's bar when one is on screen, else over your navigator.

static Future<void> open(BuildContext context, {GraineInApp? app, GraineChatSize? size})

Opens the chat at size for this open — over the app's size (GraineConfig.chatSize) and the dashboard's (Embed & Widgets → Appearance → Chat window → Window size). With the agent's bar on screen, the bar moves to the chat and draws it; with no bar, the chat shows over your navigator — a sheet, a tablet's panel or a screen of its own — and compact, which rides on the bar, opens as large.

ParameterTypeDefaultMeaning
contextBuildContextrequiredWhere to open it: the agent's bar when one is on screen, else this context's navigator.
appGraineInApp?optionalThe configured agent to use — the default agent when left out.
sizeGraineChatSize?optionalHow big the chat opens this time: compact, large or full. Left out: the app's chatSize, then the agent's Window size.

Returns — Future<void>, which completes when the chat closes.

Errors — none.

await GraineChat.open(context, size: GraineChatSize.full);

GraineChatSession

Use it to draw a chat of your own: the conversation, the connection and the customer's booked calls, as a ChangeNotifier you listen to.

class GraineChatSession extends ChangeNotifier implements GraineChatState, GraineChatConnection, GraineChatTyping

What to expect — GraineInApp.instance.chat (each agent has its own, created on first use). 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, and a dropped connection is made again with backoff (the GraineTiming.retryDelaysMs ladder) — reconnecting is true meanwhile — then, every try spent, stays offline (failed) until the customer sends something. An identified customer who comes back finds the conversation restored when it opens. It is a ChangeNotifier: listen to it and redraw. Opening the chat ends a call the bar holds. A conversation that starts with a ticket carries the prompt variables on its ticket — since 0.8.0 they are no longer added to that connection's address — and one without a ticket carries them in its address.

Everything it has — its own members, and those of the three interfaces it implements:

MemberTypeWhat it is for
void open()voidCall it when the customer enters your chat screen: connect, and stay connected.
void close()voidCall it when they leave: hang up (the conversation stays).
void send(String text)voidSend the customer's message: shown at once, sent now or the moment the chat connects (and the chat connects for it).
List<Caption> get messagesList<Caption>The conversation as captions, oldest first — for a window that draws captions; turns says more.
bool get connectedboolTrue while the chat is connected; the header then reads "Online".
bool get connectingboolTrue while the chat is connecting, either for the first time or again after losing its connection.
bool get reconnectingboolThe chat dropped — or a try to open it failed — while the customer is in it, and it is being made again: true from then until it connects, gives up (failed) or is closed, with connecting true too. Say "Reconnecting…", not "Offline", while it is. New in 0.9.0.
bool get agentTypingboolThe agent owes an answer and has not said a word of it yet: draw a typing bubble.
GraineInApp get appGraineInAppThe agent this chat is with.
List<GraineChatTurn> get turnsList<GraineChatTurn>The conversation so far, oldest first: one per message, with who, what, when, the customer's delivery, whether the agent volunteered it, and the component it drew.
bool get failedboolEvery reconnect was spent: the chat stays offline until the customer sends something, or open() is called again.
String? get errorString?The reason the most recent connection attempt failed, meant for logs; it becomes null once the chat connects.
bool get isOpenboolThe customer is in the chat (open() was called and close() was not).
List<GraineScheduledCall> get upcomingCallsList<GraineScheduledCall>The customer's booked calls still to ring, soonest first — what the Upcoming calls card shows (it draws the first three). Read when the chat connects, after a booking or a cancel through the SDK, back in the foreground, and after the agent speaks (at most every 30 s).

Its functions: newConversation, answerWidget, clear, refreshUpcoming, cancelUpcoming, cancelStateOf — each with its parameters, what it returns, the errors it raises and an example.

GraineChatSession.answerWidget

Call it when the customer answers a component the agent drew in the chat, if you draw components yourself.

void answerWidget(String widgetId, Map<String, Object?> data, String summary)

Answers a component the agent drew in the chat: the structured answer, then summary as the customer's turn — without it the conversation does not move.

ParameterTypeDefaultMeaning
widgetIdStringrequiredThe id of the component the customer answered (GraineWidget.id).
dataMap<String, Object?>requiredThe structured answer (WidgetAnswer.data).
summaryStringrequiredThe sentence that is the customer's turn (WidgetAnswer.summary); without it the conversation does not move.

Returns — nothing; it returns when done.

Errors — none.

final components = GraineInApp.instance.components;
final widget = components.widget, content = components.content;
if (widget != null && content != null) {
  final answer = content.pick(0);   // the customer tapped the first row
  GraineInApp.instance.chat.answerWidget(widget.id, answer.data, answer.summary);
}

GraineChatSession.clear

Call it to empty the chat on screen without ending the conversation Graine keeps.

void clear()

Empties the chat on screen — only the screen. The connection stays as it is, and nothing Graine keeps is touched: an identified customer's conversation comes back with the history the next connection brings, and the agent's memories stay. A message still waiting to be sent is cleared with the rest. newConversation() starts over; forgetMemory() forgets.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.chat.clear();

GraineChatState

Implement it to hand the bar a chat your app runs itself; GraineChatSession is the SDK's own.

abstract class GraineChatState implements Listenable

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
List<Caption> get messagesList<Caption>The conversation up to now, oldest message first; a line still arriving is marked live.
bool get connectedboolTrue while the chat is connected; the header then reads "Online".

Its functions: send — each with its parameters, what it returns, the errors it raises and an example.

GraineChatState.send

Call it to send the customer's typed message.

void send(String text)

The customer typed text and sent it.

ParameterTypeDefaultMeaning
textStringrequiredWhat the customer typed. Blank text is ignored.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.chat.send('Where is my order?');

GraineChatConnection

Use it to connect the chat while the customer is in it and hang it up when they leave.

abstract interface class GraineChatConnection

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
bool get connectingboolTrue while the chat is connecting, either for the first time or again after losing its connection.

Its functions: open, close — each with its parameters, what it returns, the errors it raises and an example.

GraineChatConnection.open

Call it when the customer enters your chat screen: the chat connects, and the agent greets a new customer.

void open()

Call it when the customer is in the chat; it connects and keeps the connection up.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

@override
void initState() {
  super.initState();
  GraineInApp.instance.chat.open();
}

GraineChatConnection.close

Call it when the customer leaves your chat screen: the chat hangs up; the conversation stays.

void close()

The customer left the chat: hang up.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

@override
void dispose() {
  GraineInApp.instance.chat.close();
  super.dispose();
}

GraineChatTyping

Read it to show a typing bubble while the agent composes its answer.

abstract interface class GraineChatTyping

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
bool get agentTypingboolTrue when the agent has an answer to give but has not started saying it.

GraineChatTurn

Read it to draw one message of the chat — who said it, what, when, and whether it was delivered.

class GraineChatTurn

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 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. The list empties on sign-out, when another customer signs in, after a successful forgetMemory() and on newConversation().

PropertyTypeMeaning
final String idStringThe turn's id within this chat — t1, t2, … — or, for a restored turn, the id Graine gave it.
final GraineChatRole roleGraineChatRoleWho said it: GraineChatRole.user (the customer) or GraineChatRole.agent. Draw the bubble on the right side from it.
final String textStringWhat was said.
final DateTime atDateTimeThe time the turn was said; for an agent turn, the time of its first words.
final GraineChatDelivery? deliveryGraineChatDelivery?A user's turn only: sending, sent or failed.
final bool proactiveboolTrue for an unprompted agent turn, such as its welcome or a comment on something it noticed.
final GraineComponent? componentGraineComponent?The component drawn by the agent alongside this turn, or null when there is none.
final bool liveboolAn agent turn still arriving.
final bool restoredboolTrue for a turn from an earlier conversation that came back when the chat opened (the user returned): it is shown as delivered, with the time it was originally said, rather than as something just said.
final GraineChatChannel channelGraineChatChannelWhere it was said: typed (chat) or on an earlier call (voice). A chat window draws a run of voice messages as one "Voice call" row, never their words.
Caption get captionCaptionThe turn in Caption form, which is what a window that draws captions displays.

Its functions: GraineChatTurn, copyWith — each with its parameters, what it returns, the errors it raises and an example.

GraineChatTurn.GraineChatTurn

Use it to build a message by hand for a test or a preview; the chat makes the real ones.

const GraineChatTurn({ required this.id, required this.role, required this.text, required this.at, this.delivery, this.proactive = false, this.component, this.live = false, this.restored = false, this.channel = GraineChatChannel.chat})
ParameterTypeDefaultMeaning
idStringrequiredThe turn's id within this chat — t1, t2, … — or, for a restored turn, the id Graine gave it.
roleGraineChatRolerequiredWho said it: GraineChatRole.user (the customer) or GraineChatRole.agent. Draw the bubble on the right side from it.
textStringrequiredWhat was said.
atDateTimerequiredThe time the turn was said; for an agent turn, the time of its first words.
deliveryGraineChatDelivery?optionalA user's turn only: sending, sent or failed.
proactiveboolfalseTrue for an unprompted agent turn, such as its welcome or a comment on something it noticed.
componentGraineComponent?optionalThe component drawn by the agent alongside this turn, or null when there is none.
liveboolfalseAn agent turn still arriving.
restoredboolfalseTrue for a turn from an earlier conversation that came back when the chat opened (the user returned): it is shown as delivered, with the time it was originally said, rather than as something just said.
channelGraineChatChannelGraineChatChannel.chatWhere it was said: typed (chat) or on an earlier call (voice). A chat window draws a run of voice messages as one "Voice call" row, never their words.

Returns — a new GraineChatTurn.

Errors — none.

final turn = GraineChatTurn(id: 't1', role: GraineChatRole.user, text: 'Where is my order?', at: DateTime.now(), delivery: GraineChatDelivery.sent);

GraineChatTurn.copyWith

Use it to make a changed copy of a message — its text, whether it is still arriving, its delivery or its component.

GraineChatTurn copyWith({String? text, bool? live, GraineChatDelivery? delivery, GraineComponent? component})
ParameterTypeDefaultMeaning
textString?optionalWhat was said.
livebool?optionalThe words are still arriving.
deliveryGraineChatDelivery?optionalWhere the customer's message is.
componentGraineComponent?optionalThe component the agent drew.

Returns — GraineChatTurn.

Errors — none.

final turns = GraineInApp.instance.chat.turns;
final settled = turns.isEmpty ? null : turns.last.copyWith(live: false);

GraineChatRole

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

enum GraineChatRole { user, agent }

What to expect — Every message has one. Draw the customer's on one side and the agent's on the other.

CaseMeaning
userThe customer typed it (or said it, on an earlier call).
agentThe agent said it.

GraineChatDelivery

Read it to show whether the customer's message is sending, sent or failed — and offer a retry when it failed.

enum GraineChatDelivery { sending, sent, failed }

What to expect — Only on the customer's messages; null on the agent's. A message typed while the chat is not connected is shown at once as sending and goes the moment it connects.

CaseMeaning
sendingTyped and waiting to go out.
sentOn its way to the agent. Restored messages are sent.
failedNot sent: every reconnect was spent. The next message, or open(), sends it — offer "tap to retry".

GraineChatChannel

Read it to tell a typed message from one said on a call, so an earlier call is drawn as one "Voice call" row.

enum GraineChatChannel { chat, voice }

What to expect — chat for every message typed now. Only a restored message — from an earlier conversation — can be voice; the SDK's chat window draws a run of them as one "Voice call · 6:05 PM" row, never their words.

CaseMeaning
chatTyped in the chat.
voiceSaid on an earlier call.

GraineChatSize

Use it to choose how big the chat window opens: compact above the bar, a large sheet or panel, or the full screen.

enum GraineChatSize

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
compactThe usual window above the bar, the same as in earlier versions.
largeOn a phone, a sheet covering most of the screen; on a tablet, a panel in a corner.
fullThe whole screen.
PropertyTypeMeaning
String get wireStringThe design's word for it.

Its functions: fromWire — each with its parameters, what it returns, the errors it raises and an example.

GraineChatSize.fromWire

Use it to read a window size from its word (compact, large, full).

static GraineChatSize? fromWire(Object? raw)

Maps the design's size word to a value; returns null for anything else, whether the word is missing or one only a newer server uses.

ParameterTypeDefaultMeaning
rawObject?requiredThe size's word: compact, large or full; anything else reads as null.

Returns — GraineChatSize?. Example value: GraineChatSize.compact.

Errors — none.

final size = GraineChatSize.fromWire('large');

ChatRows

Use it to lay out a chat of your own exactly as the SDK's chat window does: bubbles, one row per earlier call, and day lines.

abstract final class ChatRows

What to expect — The rows a chat window draws for a list of turns: a bubble per chat turn, one "Voice call · time" row per call, and day separators (Today, Yesterday, a date) in the device's time zone.

Its functions: build, dayLabel, callLabel, callSpoken, offersNewConversation, offersStarters — each with its parameters, what it returns, the errors it raises and an example.

ChatRows.build

Use it to turn the chat's messages into the rows to draw, in order.

static List<ChatRow> build(List<GraineChatTurn> turns, {required DateTime now, DateTime Function(DateTime)? local})

The rows for turns (oldest first), in order: a day line before the first row when it is not from today and wherever the day changes, one row per earlier call, and a bubble per typed message.

ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe chat's messages, oldest first (GraineChatSession.turns).
nowDateTimerequiredThe time now — pass DateTime.now(); it decides which day is today.
localDateTime Function(DateTime)?optionalReads a time as the device's wall clock; the device's own zone when left out. A test passes its own.

Returns — List<ChatRow> — its fields: ChatRow. Example value: [].

Errors — none.

final rows = ChatRows.build(GraineInApp.instance.chat.turns, now: DateTime.now());

ChatRows.dayLabel

Use it to get the words for a day line: "Today", "Yesterday" or the date.

static String dayLabel(ChatDayRow row, {required DateTime now, DateTime Function(DateTime)? local})

Gives "Today" or "Yesterday", and otherwise the date in the form ChatTime.booked uses for a day: "Sat 10 Oct", plus the year when it is not the current one ("Wed 31 Dec 2025").

ParameterTypeDefaultMeaning
rowChatDayRowrequiredThe day line to write.
nowDateTimerequiredThe time now — pass DateTime.now().
localDateTime Function(DateTime)?optionalReads a time as the device's wall clock; the device's own zone when left out.

Returns — String. Example value: 'Yesterday'.

Errors — none.

final now = DateTime.now();
for (final row in ChatRows.build(GraineInApp.instance.chat.turns, now: now).whereType<ChatDayRow>()) {
  debugPrint(ChatRows.dayLabel(row, now: now));   // Today, Yesterday, Mon 13 Oct
}

ChatRows.callLabel

Use it to get the words for an earlier call's row.

static String callLabel(String clock)

"Voice call · 6:05 PM" — with clock, the call's first message's time in the device's short form.

ParameterTypeDefaultMeaning
clockStringrequiredThe call's first message's time, as ChatTime.clock writes it ("6:05 PM").

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

Errors — none.

for (final row in ChatRows.build(GraineInApp.instance.chat.turns, now: DateTime.now()).whereType<ChatVoiceRow>()) {
  debugPrint(ChatRows.callLabel(ChatTime.clock(row.at, h24: false)));   // Voice call · 6:05 PM
}

ChatRows.callSpoken

Use it to get what a screen reader says for an earlier call's row.

static String callSpoken(String clock)

The text a screen reader reads out for the row, such as "Voice call at 6:05 PM".

ParameterTypeDefaultMeaning
clockStringrequiredThe call's first message's time, as ChatTime.clock writes it.

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

Errors — none.

final spoken = ChatRows.callSpoken('6:05 PM');

ChatRows.offersNewConversation

Use it to decide whether to offer "New conversation" in your chat's header.

static bool offersNewConversation(List<GraineChatTurn> turns)

The header offers "New conversation" only for a continuing conversation, that is, while restored turns are on screen.

ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe chat's messages, oldest first.

Returns — bool. Example value: true.

Errors — none.

final offer = ChatRows.offersNewConversation(GraineInApp.instance.chat.turns);

ChatRows.offersStarters

Use it to decide whether to show the brand's conversation starters.

static bool offersStarters(List<GraineChatTurn> turns)

The brand's starters are for a fresh chat only: they are not offered after the user has said anything, or on a continuing conversation.

ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe chat's messages, oldest first.

Returns — bool. Example value: true.

Errors — none.

final offer = ChatRows.offersStarters(GraineInApp.instance.chat.turns);

ChatRow

Read it as one row of the chat to draw: a day line, an earlier call, or a message.

sealed class ChatRow

What to expect — The rows a chat window draws for a list of turns: a bubble per chat turn, one "Voice call · time" row per call, and day separators (Today, Yesterday, a date) in the device's time zone.

PropertyTypeMeaning
String get keyStringIt stays the same as the conversation grows: a separator is keyed on its date, a call on its first turn, and a bubble on its turn.

Its functions: ChatRow — each with its parameters, what it returns, the errors it raises and an example.

ChatRow.ChatRow

The base constructor of the three row types; you never call it directly.

const ChatRow()

Parameters — none.

Returns — nothing you call directly: ChatRow is sealed: its three kinds come from ChatRows.build.

Errors — none.

final rows = ChatRows.build(GraineInApp.instance.chat.turns, now: DateTime.now());

ChatDayRow

Read it to draw a day line between messages.

final class ChatDayRow extends ChatRow

What to expect — The rows a chat window draws for a list of turns: a bubble per chat turn, one "Voice call · time" row per call, and day separators (Today, Yesterday, a date) in the device's time zone.

PropertyTypeMeaning
final ChatDay dayChatDayWhich words the line takes: today, yesterday or date.
final String dateStringThe day on the device, YYYY-MM-DD — also the row's key.
final DateTime atDateTimeWhen the day's first message was said — ChatRows.dayLabel writes a date from it.
String get contractKeyStringThe day in one word, the same on every platform: 'today', 'yesterday' or the date — for comparing rows in a test.

Its functions: ChatDayRow — each with its parameters, what it returns, the errors it raises and an example.

ChatDayRow.ChatDayRow

Use it to build a day line by hand for a test; ChatRows.build makes the real ones.

const ChatDayRow(this.day, this.date, this.at)
ParameterTypeDefaultMeaning
dayChatDayrequiredWhich words the line takes: today, yesterday or date.
dateStringrequiredThe day on the device, YYYY-MM-DD — also the row's key.
atDateTimerequiredWhen the day's first message was said — ChatRows.dayLabel writes a date from it.

Returns — a new ChatDayRow.

Errors — none.

final row = ChatDayRow(ChatDay.yesterday, '2026-10-13', DateTime.utc(2026, 10, 13, 12, 30));

ChatVoiceRow

Read it to draw an earlier call as one "Voice call · 6:05 PM" row — that a call happened and when, never its words.

final class ChatVoiceRow extends ChatRow

What to expect — The rows a chat window draws for a list of turns: a bubble per chat turn, one "Voice call · time" row per call, and day separators (Today, Yesterday, a date) in the device's time zone.

PropertyTypeMeaning
final String idStringThe row's key: voice- and the call's first message's id.
final DateTime atDateTimeWhen the call's first message was said — the row shows it ("Voice call · 6:05 PM").
final int firstintThe index, in the chat's messages, of the call's first message.
final int lastintThe index of the call's last message.

Its functions: ChatVoiceRow — each with its parameters, what it returns, the errors it raises and an example.

ChatVoiceRow.ChatVoiceRow

Use it to build an earlier call's row by hand for a test; ChatRows.build makes the real ones.

const ChatVoiceRow(this.id, this.at, this.first, this.last)
ParameterTypeDefaultMeaning
idStringrequiredThe row's key: voice- and the call's first message's id.
atDateTimerequiredWhen the call's first message was said — the row shows it ("Voice call · 6:05 PM").
firstintrequiredThe index, in the chat's messages, of the call's first message.
lastintrequiredThe index of the call's last message.

Returns — a new ChatVoiceRow.

Errors — none.

final row = ChatVoiceRow('voice-m7', DateTime.utc(2026, 10, 14, 12, 35), 7, 12);

ChatTurnRow

Read it to draw one message bubble.

final class ChatTurnRow extends ChatRow

What to expect — The rows a chat window draws for a list of turns: a bubble per chat turn, one "Voice call · time" row per call, and day separators (Today, Yesterday, a date) in the device's time zone.

PropertyTypeMeaning
final GraineChatTurn turnGraineChatTurnThe message to draw as a bubble.
final int indexintIts index in the chat's messages.

Its functions: ChatTurnRow — each with its parameters, what it returns, the errors it raises and an example.

ChatTurnRow.ChatTurnRow

Use it to build a bubble row by hand for a test; ChatRows.build makes the real ones.

const ChatTurnRow(this.turn, this.index)
ParameterTypeDefaultMeaning
turnGraineChatTurnrequiredThe message to draw as a bubble.
indexintrequiredIts index in the chat's messages.

Returns — a new ChatTurnRow.

Errors — none.

final turn = GraineChatTurn(id: 't1', role: GraineChatRole.user, text: 'Where is my order?', at: DateTime.now());
final row = ChatTurnRow(turn, 0);

ChatDay

Read it to know which words a day line takes: today, yesterday or a date.

enum ChatDay { today, yesterday, date }

What to expect — Decided on the device's clock: a conversation that runs past midnight gets a new day line.

CaseMeaning
todayThe line says "Today".
yesterdayThe line says "Yesterday".
dateThe line says the date — "Sat 10 Oct", with the year when it is not this one ("Wed 31 Dec 2025").

ChatPieces

Use it to decide which optional pieces your own chat window draws, as the agent's design sets them: earlier messages, the starters, New conversation, the booked-calls card and Book a call.

final class ChatPieces

What to expect — Work them out again whenever the messages, the booked calls or booking's availability change. The SDK's chat window draws exactly these.

PropertyTypeMeaning
final List<GraineChatTurn> turnsList<GraineChatTurn>The messages to draw, oldest first — without the restored ones when earlier messages are hidden.
final bool startersboolShow the brand's conversation starters: nothing restored on screen, and the customer has said nothing yet.
final bool newConversationboolOffer New conversation in the header: the design keeps it, and earlier messages are on screen.
final bool upcomingboolDraw the booked-calls card: the design keeps it, and at least one call is booked.
final String upcomingTitleStringThe title of the card — the brand's own words when it set some, otherwise "Upcoming call" / "Upcoming calls".
final bool bookboolOffer Book a call — in the card, or as a compact row in its place while nothing is booked: the design keeps the booked-calls card, and booking is available here. New in 0.8.0.

Its functions: of, ofChat, book — each with its parameters, what it returns, the errors it raises and an example.

ChatPieces.of

Use it to work out the pieces from your own settings.

static ChatPieces of(List<GraineChatTurn> turns, { bool showHistory = true, bool newConversation = true, bool showUpcomingCalls = true, String upcomingTitle = '', int upcomingCount = 0, bool bookingAvailable = false})

The pieces for turns and upcomingCount booked calls, as the design sets them. With earlier messages hidden, the window opens like a new chat: the restored messages are not drawn (the agent still has them), the starters may show, and New conversation — which belongs to earlier messages on screen — does not.

ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe chat's messages, oldest first.
showHistorybooltrueDraw earlier messages (the design's Earlier messages); false leaves restored messages off the screen.
newConversationbooltrueOffer New conversation in the header (the design's New conversation button).
showUpcomingCallsbooltrueDraw the booked-calls card (the design's Booked calls) — and so Book a call.
upcomingTitleString''The card's title in the brand's words; empty: "Upcoming call" or "Upcoming calls".
upcomingCountint0How many booked calls the customer has (chat.upcomingCalls.length).
bookingAvailableboolfalseWhether booking is available here (GraineInApp.bookingAvailable).

Returns — ChatPieces — its fields: ChatPieces.

Errors — none.

final chat = GraineInApp.instance.chat;
final pieces = ChatPieces.of(chat.turns, upcomingCount: chat.upcomingCalls.length, bookingAvailable: GraineInApp.instance.bookingAvailable);

ChatPieces.ofChat

Use it to work out the pieces from the agent's design (tokens?.chat), as the SDK's chat window does.

static ChatPieces ofChat(List<GraineChatTurn> turns, ThemeChat? chat, {int upcomingCount = 0, bool bookingAvailable = false})

The same, under the design's chat tokens — what the SDK's chat window does. Null tokens (an agent with no design) turn every piece on.

ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe chat's messages, oldest first.
chatThemeChat?requiredThe design's chat tokens (GraineInApp.instance.tokens?.chat); null, from an agent with no design, turns every piece on.
upcomingCountint0How many booked calls the customer has.
bookingAvailableboolfalseWhether booking is available here (GraineInApp.bookingAvailable).

Returns — ChatPieces — its fields: ChatPieces.

Errors — none.

final graine = GraineInApp.instance;
final pieces = ChatPieces.ofChat(graine.chat.turns, graine.tokens?.chat,
    upcomingCount: graine.chat.upcomingCalls.length, bookingAvailable: graine.bookingAvailable);

ChatTime

Use it to write a chat's times the way the SDK does, in the device's own time zone.

abstract final class ChatTime

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.

Its functions: clock, booked, day — each with its parameters, what it returns, the errors it raises and an example.

ChatTime.clock

Use it to write the short time under a message.

static String clock(DateTime time, {required bool h24})

Formats a time in the device's short style: "9:41 PM", or "21:41" where the device uses a 24-hour clock.

ParameterTypeDefaultMeaning
timeDateTimerequiredThe moment to write, in any zone; it is shown on the device's clock.
h24boolrequiredtrue on a device set to the 24-hour clock — MediaQuery.alwaysUse24HourFormatOf(context).

Returns — String. Example value: '9:41 PM' ('21:41' with h24: true).

Errors — none.

final time = ChatTime.clock(DateTime(2026, 10, 14, 21, 41), h24: false);

ChatTime.booked

Use it to write when a booked call rings: "Today, 5:30 PM", "Tomorrow, 9:00 AM" or the date.

static String booked(DateTime at, DateTime now, {required bool h24})

Writes when a booked call is due: "Today, 5:30 PM" or "Tomorrow, 9:00 AM", and otherwise the weekday with the date, as in "Wed 14 Oct, 10:00 AM".

ParameterTypeDefaultMeaning
atDateTimerequiredWhen the call rings, in any zone; it is shown on the device's clock.
nowDateTimerequiredThe time now — pass DateTime.now(); it decides "Today" and "Tomorrow".
h24boolrequiredtrue on a device set to the 24-hour clock — MediaQuery.alwaysUse24HourFormatOf(context).

Returns — String. Example value: 'Tomorrow, 9:00 AM'.

Errors — none.

for (final call in await GraineInApp.instance.scheduledCalls()) {
  final at = call.at;
  if (at != null) debugPrint(ChatTime.booked(at, DateTime.now(), h24: MediaQuery.alwaysUse24HourFormatOf(context)));
}

ChatTime.day

Use it to write a day in short words — the weekday and the date, as the booking sheet's chips and lines do.

static String day(DateTime at)

A day in short words on the device's clock: the weekday and the date. New in 0.8.0; the booking sheet's chips, its time zone line and its "Booked — …" line use it.

ParameterTypeDefaultMeaning
atDateTimerequiredA time on the day, in any zone.

Returns — String. Example value: 'Wed 14 Oct'.

Errors — none.

final words = ChatTime.day(DateTime(2026, 10, 14, 18));

The voice bar

The assistant bar: the agent's voice call, its captions and components, and the switch to the chat, in one bar the SDK draws in your agent's design. Place GraineEngine once and GraineAgentBar where the bar should sit — or draw your own controls from GraineInApp.instance.voice with the words, phases and meters below, so every surface says the same thing at the same moment.

GraineVoice

Use it to start and end the agent's voice call from your own controls, and to draw its state; GraineAgentBar already does both.

class GraineVoice extends ChangeNotifier implements GraineCallState, GraineCallFault, GraineConnectProgress, GraineCaptionsSwitch

What to expect — GraineInApp.instance.voice (each agent has its own). GraineEngine attaches it once, above the navigator, and GraineAgentBar starts and ends calls itself; use it directly only for controls of your own. It is a ChangeNotifier: listen to it and redraw. One conversation at a time: starting it closes an open chat and ends another agent's call, and it waits while a native call is live.

Everything it has — its own members, and those of the four interfaces it implements:

MemberTypeWhat it is for
Future<void> start()Future<void>Start the call (see GraineCallState.start).
void end()voidHang up (see GraineCallState.end).
void setMuted(bool muted)voidStop or resume sending the customer's voice.
void retry()voidReload the page that carries the call and start its retry ladder over — for "Try again".
bool get connectedboolThe call is connected.
bool get connectingboolA call is being placed.
bool get mutedboolThe customer is muted.
bool get readyboolThe page is ready: a start connects at once.
bool get reconnectingboolTrue while the call is being brought back after a network interruption.
bool? get onlinebool?Whether the device is online; null before the first report.
ConnectionQuality get qualityConnectionQualityThe connection in one word.
GraineConnectivity get connectivityGraineConnectivityWhether the page that carries the call is there.
List<Caption> get captionsList<Caption>What has been said so far, oldest first.
Caption? get captionCaption?The line being said now, or the last one.
bool get agentSpeakingboolThe agent is mid-sentence.
bool get micDeniedboolThe last start was refused the microphone.
Map<String, Object?> get appearanceMap<String, Object?>The dashboard's Appearance for this agent.
String? get errorString?What is permanently wrong with this agent's setup (a way of carrying the audio that is not configured), or null; the bar says "Unavailable" while it is set, and a new call clears it.
ConnectStage? get connectStageConnectStage?Where the call being placed is; null outside a connect.
bool get slowConnectboolA connect the customer is waiting on has run past timing.slowConnectMs: say "Still connecting…".
bool get callFailedboolThe last start could not be connected at all; a new start simply tries again.
bool get captionsOnboolThe captions switch: the bar draws the line being said only while it is on. On by default; what is said stays in captions either way (see GraineCaptionsSwitch). New in 0.9.0.
void setCaptionsOn(bool on)voidTurn the captions on or off, for your own CC button; the call goes on. New in 0.9.0.
void clearCaptions()voidForget what has been said (captions) without ending the call. New in 0.9.0.
GraineConfig get configGraineConfigThe configuration this call was set up with.
GraineInApp get appGraineInAppThe agent this call belongs to.
CallPhase get phaseCallPhaseWhere the call stands in one word: idle, connecting, reconnecting, thinking, speaking or listening.
Transport? get transportTransport?The way the audio really travels, once the page has said.
bool get attachedboolWhether a page is attached (GraineEngine is mounted).

Its functions: attach, detachPage, detach, viewOpened, viewClosed, answerWidget, reportEvent — each with its parameters, what it returns, the errors it raises and an example.

GraineVoice.attach

Called by GraineEngine to give the call the page that carries it; call it yourself only with an engine page of your own.

void attach(GraineEnginePage page)

Attaches the engine. GraineEngine calls it with its WebView page, and a test can pass a fake instead.

ParameterTypeDefaultMeaning
pageGraineEnginePagerequiredThe page that carries the call — GraineEngine passes its own.

Returns — nothing; it returns when done.

Errors — none.

final page = WebViewEnginePage(baseUrl: 'https://www.graine.ai');   // or a page of your own
GraineInApp.instance.voice.attach(page);

GraineVoice.detachPage

Called by GraineEngine when its page goes away; call it yourself only with an engine page of your own.

void detachPage(GraineEnginePage page)

Detaches page if it is the one attached — what GraineEngine does when it leaves the screen.

ParameterTypeDefaultMeaning
pageGraineEnginePagerequiredThe page that is going away; nothing happens when another page is attached by now.

Returns — nothing; it returns when done.

Errors — none.

final page = WebViewEnginePage(baseUrl: 'https://www.graine.ai');
final voice = GraineInApp.instance.voice;
voice.attach(page);
// … and when that page leaves the screen:
voice.detachPage(page);

GraineVoice.detach

Call it to let go of the engine page entirely — when you tear down your own engine.

void detach()

Remove the engine.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.voice.detach();

GraineVoice.viewOpened

Call it when your own call UI comes on screen, so the engine page loads and a tap starts the call at once.

void viewOpened()

Reports that a UI for this call has appeared on screen. GraineAgentBar reports this on its own; an app that draws its own bar can report it too, matched with a viewClosed call.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.voice.viewOpened();

GraineVoice.viewClosed

Call it when your own call UI leaves the screen.

void viewClosed()

A UI from viewOpened left the screen.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.voice.viewClosed();

GraineVoice.answerWidget

Call it when the customer answers a component the agent drew during the call, if you draw components yourself.

void answerWidget(String widgetId, Map<String, Object?> data, String summary)

The customer answered a component.

ParameterTypeDefaultMeaning
widgetIdStringrequiredThe id of the component the customer answered.
dataMap<String, Object?>requiredThe structured answer (WidgetAnswer.data).
summaryStringrequiredThe sentence that is the customer's turn (WidgetAnswer.summary); without it the conversation does not move.

Returns — nothing; it returns when done.

Errors — none.

final components = GraineInApp.instance.components;
final widget = components.widget, content = components.content;
if (widget != null && content != null) {
  final answer = content.pick(0);   // the customer tapped the first row
  GraineInApp.instance.voice.answerWidget(widget.id, answer.data, answer.summary);
}

GraineAgentBar

Use it to give the customer the agent's voice bar — call, captions, components and the chat — drawn in your agent's design, with no UI work.

class GraineAgentBar extends StatefulWidget

What to expect — Place it once, over your app — in a Stack inside GraineEngine. It follows the call, the captions and the components by itself, offers the chat as the dashboard allows, and draws in the agent's design. The customer can drag it up and down; save onPlacementChange's offset and hand it back as initialOffset — the SDK stores nothing. Hide it from onClose and bring it back with a GraineLauncher.

PropertyTypeMeaning
final GraineCallState? callGraineCallState?The call to draw.
final GraineComponents? componentsGraineComponents?The components to draw above the bar.
final GraineInApp? appGraineInApp?In an app with more than one agent, the agent the bar speaks to, for example GraineAgentBar(app: GraineInApp.named('support')); the bar uses its call, chat, components, session and design.
final String? accentString?The colour, as #RRGGBB, of the avatar, the microphone, the meter and the components.
final String? nameString?The name beside the status.
final String? avatarUrlString?An image of a face to show in the avatar, taking priority over the design's glyph. Without it, the avatar falls back to the design's glyph, then the dashboard's logo, then the first letter of the name.
final String? schemeString?"light", "dark" or "auto" (the device).
final double? radiusdouble?How rounded the corners of the caption and components are.
final ResolvedDesign? designResolvedDesign?The design to draw. When left out, the session's design (GraineInApp.design) is used; if there is none either, the bar keeps its pre-design colours, corners and depth.
final double bottomInsetdoubleThe bar's resting position when it has no offset, which is above your tab bar.
final double horizontalInsetdoubleSpace between the bar and the screen's left and right edges, in logical pixels (default 12).
final bool draggableboolLet the customer drag it up and down.
final int? initialOffsetint?A saved position from your app, in logical pixels measured up from the anchor.
final ValueChanged<int>? onPlacementChangeValueChanged<int>?The customer let go somewhere new.
final bool keyboardAwareboolRise above the soft keyboard.
final bool showComponentsboolMount a GraineWidgetView above the bar.
final double? topMargindouble?Space to keep clear at the top.
final bool showCaptionsboolDuring a call, display the line currently being spoken above the bar — while the call's captions switch is on too (GraineCaptionsSwitch.captionsOn, on by default).
final VoidCallback? onCloseVoidCallback?Called when the end button is pressed — after hanging up, or, when no call is in progress, to close the bar.
final GraineChatState? chatGraineChatState?The chat, for an agent the dashboard offers as a chat or as both (Embed & Widgets → What visitors get). Default: the agent's own (GraineInApp.chat): the bar offers the switch as the dashboard allows, draws the chat window above itself, opens the chat while the customer is in it (ending the call first) and closes it when they leave. If your app runs the chat on its own, pass that chat here.
final bool builtInChatboolUse the SDK's own chat when chat is null (default true). Off: without a chat, the bar is the call alone, whatever the dashboard offers.
final AgentSurface? surfaceAgentSurface?The surface to open first when both are offered; left out, the dashboard's starting choice applies.
final ValueChanged<AgentSurface>? onSurfaceChangeValueChanged<AgentSurface>?Fires whenever the customer goes from the call to the chat or back, whether they chose to or the dashboard's setting moved them.
static const Key barKeyKeyKey of the bar proper: the 60-point frame that holds the avatar and the buttons.
static const Key callButtonKeyKeyKey of the button that starts a call, retries a failed load or ends the call — shown as the microphone when no call is on, and as the end button during one.
static const Key muteButtonKeyKeyKey of the microphone button shown in a call, which mutes.
static const Key closeButtonKeyKeyThe end button off a call, where it closes.
static const Key switchButtonKeyKeyKey of the switch between call and chat, present only when both are offered.
static const Key statusKeyKeyThe status line.
static const Key meterKeyKeyKey of the four-bar level meter shown next to "Listening" and "Speaking".
static const Key avatarKeyKeyThe avatar's key, for a widget test to find it.
static const Key captionKeyKeyThe live caption above the bar.
static const Key chatKeyKeyThe chat window above the bar.

Its functions: GraineAgentBar — each with its parameters, what it returns, the errors it raises and an example.

GraineAgentBar.GraineAgentBar

Use it to place the bar, usually in a Stack over your app inside GraineEngine.

const GraineAgentBar({ super.key, this.call, this.components, this.accent, this.name, this.avatarUrl, this.scheme, this.radius, this.design, this.bottomInset = 96, this.horizontalInset = 12, this.draggable = true, this.initialOffset, this.onPlacementChange, this.keyboardAware = true, this.showComponents = true, this.topMargin, this.showCaptions = true, this.onClose, this.chat, this.builtInChat = true, this.surface, this.onSurfaceChange, this.app})
ParameterTypeDefaultMeaning
keyKey?optionalThe widget's key, as for any Flutter widget.
callGraineCallState?optionalThe call to draw.
componentsGraineComponents?optionalThe components to draw above the bar.
accentString?optionalThe colour, as #RRGGBB, of the avatar, the microphone, the meter and the components.
nameString?optionalThe name beside the status.
avatarUrlString?optionalAn image of a face to show in the avatar, taking priority over the design's glyph. Without it, the avatar falls back to the design's glyph, then the dashboard's logo, then the first letter of the name.
schemeString?optional"light", "dark" or "auto" (the device).
radiusdouble?optionalHow rounded the corners of the caption and components are.
designResolvedDesign?optionalThe design to draw. When left out, the session's design (GraineInApp.design) is used; if there is none either, the bar keeps its pre-design colours, corners and depth.
bottomInsetdouble96The bar's resting position when it has no offset, which is above your tab bar.
horizontalInsetdouble12Space between the bar and the screen's left and right edges, in logical pixels (default 12).
draggablebooltrueLet the customer drag it up and down.
initialOffsetint?optionalA saved position from your app, in logical pixels measured up from the anchor.
onPlacementChangeValueChanged<int>?optionalThe customer let go somewhere new.
keyboardAwarebooltrueRise above the soft keyboard.
showComponentsbooltrueMount a GraineWidgetView above the bar.
topMargindouble?optionalSpace to keep clear at the top.
showCaptionsbooltrueDuring a call, display the line currently being spoken above the bar — while the call's captions switch is on too (GraineCaptionsSwitch.captionsOn, on by default).
onCloseVoidCallback?optionalCalled when the end button is pressed — after hanging up, or, when no call is in progress, to close the bar.
chatGraineChatState?optionalThe chat, for an agent the dashboard offers as a chat or as both (Embed & Widgets → What visitors get). Default: the agent's own (GraineInApp.chat): the bar offers the switch as the dashboard allows, draws the chat window above itself, opens the chat while the customer is in it (ending the call first) and closes it when they leave. If your app runs the chat on its own, pass that chat here.
builtInChatbooltrueUse the SDK's own chat when chat is null (default true). Off: without a chat, the bar is the call alone, whatever the dashboard offers.
surfaceAgentSurface?optionalThe surface to open first when both are offered; left out, the dashboard's starting choice applies.
onSurfaceChangeValueChanged<AgentSurface>?optionalFires whenever the customer goes from the call to the chat or back, whether they chose to or the dashboard's setting moved them.
appGraineInApp?optionalIn an app with more than one agent, the agent the bar speaks to, for example GraineAgentBar(app: GraineInApp.named('support')); the bar uses its call, chat, components, session and design.

Returns — a new GraineAgentBar.

Errors — none.

GraineAgentBar(
  initialOffset: prefs.getInt('graine.bar'),              // where the customer left it
  onPlacementChange: (o) => prefs.setInt('graine.bar', o), // the SDK stores nothing itself
  onClose: () => setState(() => showBar = false),
)

AgentSurface

Use it to say which way the customer is with the agent: the voice call or the chat.

enum AgentSurface { voice, chat }

What to expect — The dashboard decides which exist (Embed & Widgets → What visitors get); your preference wins only when it allows it.

CaseMeaning
voiceThe voice call.
chatThe typed chat.

SurfacePolicy

Read it to know which of the call and the chat the dashboard allows, which opens first, and whether to offer a switch.

class SurfacePolicy

What to expect — Worked out from the dashboard's Appearance (what visitors get, and which opens first) when the session loads; a preference counts only when the dashboard allows it.

PropertyTypeMeaning
final List<AgentSurface> allowedList<AgentSurface>In the order to offer them.
final AgentSurface startAgentSurfaceWhich opens first: your preference when the dashboard allows it, else the dashboard's choice, else the first allowed.
final bool canSwitchboolWhether to show a switch at all; false when the agent offers only one of the two.

Its functions: SurfacePolicy — each with its parameters, what it returns, the errors it raises and an example.

SurfacePolicy.SurfacePolicy

Use it to build a policy by hand for a test; Surfaces.resolve gives the real one.

const SurfacePolicy({required this.allowed, required this.start, required this.canSwitch})
ParameterTypeDefaultMeaning
allowedList<AgentSurface>requiredIn the order to offer them.
startAgentSurfacerequiredWhich opens first: your preference when the dashboard allows it, else the dashboard's choice, else the first allowed.
canSwitchboolrequiredWhether to show a switch at all; false when the agent offers only one of the two.

Returns — a new SurfacePolicy.

Errors — none.

const policy = SurfacePolicy(allowed: [AgentSurface.voice, AgentSurface.chat], start: AgentSurface.voice, canSwitch: true);

Surfaces

Use it to work out, from the dashboard's settings, whether the customer gets the call, the chat or both — as the bar does.

abstract final class Surfaces

What to expect — Worked out from the dashboard's Appearance when the session loads; a preference counts only when the dashboard allows it. The SDK's bar applies it for you.

Its functions: resolve — each with its parameters, what it returns, the errors it raises and an example.

Surfaces.resolve

Use it to get the policy for this agent's Appearance, with your preference when the dashboard allows it.

static SurfacePolicy resolve(Map<String, Object?>? appearance, [AgentSurface? prefer])
ParameterTypeDefaultMeaning
appearanceMap<String, Object?>?requiredThe dashboard's Appearance for the agent (session.appearance).
preferAgentSurface?optionalYour preference, or the customer's last switch; it wins only when the dashboard allows it.

Returns — SurfacePolicy — its fields: SurfacePolicy.

Errors — none.

final policy = Surfaces.resolve(GraineInApp.instance.session?.appearance, AgentSurface.chat);

GraineLauncher

Use it for a floating button that stands for the agent, in its design: a tap starts the call, or does what onPressed says — bring the bar back, open a screen of yours.

class GraineLauncher extends StatelessWidget

What to expect — Place it yourself — in a Stack, or a Scaffold's floating action button slot; it sizes itself (56 × 56, a pill as wide as its label). It draws in the agent's design: the launcher glyph or the brand's logo on the accent, the design's shape and shadow. A tap starts the call, or does what onPressed says.

PropertyTypeMeaning
final GraineCallState? callGraineCallState?The call a tap starts.
final GraineInApp? appGraineInApp?Which agent a tap calls; the launcher uses that agent's call, session and design. When left out: the agent that call belongs to, or failing that the default agent.
final VoidCallback? onPressedVoidCallback?An action to run on tap in place of starting the call.
final String? labelString?The text on a pill, also read out by screen readers. Without it, the dashboard's heading is used, or "Talk to us" when there is no heading.
final ResolvedDesign? designResolvedDesign?The design to draw.
final String? schemeString?"light", "dark" or "auto" (the device).
static const Key launcherKeyKeyThe button, for a test to find.

Its functions: GraineLauncher — each with its parameters, what it returns, the errors it raises and an example.

GraineLauncher.GraineLauncher

Use it to place the launcher — in a Stack, or as a Scaffold's floating action button.

const GraineLauncher({ super.key, this.call, this.onPressed, this.label, this.design, this.scheme, this.app})
ParameterTypeDefaultMeaning
keyKey?optionalThe widget's key, as for any Flutter widget.
callGraineCallState?optionalThe call a tap starts.
onPressedVoidCallback?optionalAn action to run on tap in place of starting the call.
labelString?optionalThe text on a pill, also read out by screen readers. Without it, the dashboard's heading is used, or "Talk to us" when there is no heading.
designResolvedDesign?optionalThe design to draw.
schemeString?optional"light", "dark" or "auto" (the device).
appGraineInApp?optionalWhich agent a tap calls; the launcher uses that agent's call, session and design. When left out: the agent that call belongs to, or failing that the default agent.

Returns — a new GraineLauncher.

Errors — none.

if (!showBar) GraineLauncher(onPressed: () => setState(() => showBar = true)),   // in your Stack

GraineLoaderView

Use it to draw a wait — connecting, reconnecting, thinking, a component loading — the way the dashboard's loader setting says.

class GraineLoaderView extends StatefulWidget

What to expect — Place it where the customer waits. It animates at the dashboard's speed, in the accent unless a colour was chosen, and stands still when the device asks for reduced motion.

PropertyTypeMeaning
final LoaderLook lookLoaderLookFrom Loaders.resolve.
final Color colorColorThe loader's colour — usually the accent.
final Color trackColorColorThe ring's unlit part.
final double? sizedouble?Replaces the look's own size; the bar's status line, for one, needs a small loader.
final String? labelString?Text describing the current moment; leave it null to show only the mark.
final Color labelColorColorThe colour of the loader's words, when labelStyle does not set one.
final TextStyle? labelStyleTextStyle?The complete text style for the label; the voice bar, for instance, uses the design's typeface.
final bool stackedboolPlace the label below the mark instead of next to it.
final String? logoUrlString?The brand's logo, drawn breathing for the mark kind; without one, mark draws the pulse.

Its functions: GraineLoaderView — each with its parameters, what it returns, the errors it raises and an example.

GraineLoaderView.GraineLoaderView

Use it to place a loader in your own UI.

const GraineLoaderView({ super.key, required this.look, required this.color, this.trackColor = const Color(0x332C2E33), this.size, this.label, this.labelColor = const Color(0xFF5F6672), this.labelStyle, this.stacked = false, this.logoUrl})
ParameterTypeDefaultMeaning
keyKey?optionalThe widget's key, as for any Flutter widget.
lookLoaderLookrequiredFrom Loaders.resolve.
colorColorrequiredThe loader's colour — usually the accent.
trackColorColorconst Color(0x332C2E33)The ring's unlit part.
sizedouble?optionalReplaces the look's own size; the bar's status line, for one, needs a small loader.
labelString?optionalText describing the current moment; leave it null to show only the mark.
labelColorColorconst Color(0xFF5F6672)The colour of the loader's words, when labelStyle does not set one.
labelStyleTextStyle?optionalThe complete text style for the label; the voice bar, for instance, uses the design's typeface.
stackedboolfalsePlace the label below the mark instead of next to it.
logoUrlString?optionalThe brand's logo, drawn breathing for the mark kind; without one, mark draws the pulse.

Returns — a new GraineLoaderView.

Errors — none.

final view = GraineLoaderView(
  look: Loaders.resolve(GraineInApp.instance.session?.appearance['loader']),
  color: const Color(0xFF318CE7),
  size: 18,
  label: 'Thinking…',
);

Loaders

Use it to read the dashboard's loader setting into a look you can draw.

abstract final class Loaders

What to expect — From the dashboard's Appearance → Loading. Anything unreadable gives the default: dots, medium size, normal speed, the default words.

PropertyTypeMeaning
static const List<String> kindsList<String>dots, pulse, ring, bars, mark.
static const List<String> sizesList<String>small (14), medium (20), large (28) — logical pixels.
static const List<String> speedsList<String>calm (1600 ms a cycle), normal (1100), quick (700).
static const List<String> momentsList<String>connecting, reconnecting, thinking, component.
static const String defaultKindStringThe loader when the dashboard sets none: 'dots'.
static const int textMaxintThe most characters a moment's words keep: 40.
static const LoaderText defaultTextLoaderText"Connecting…", "Reconnecting…", "Thinking…", "Loading…".

Its functions: normalise, resolve, resolveStyle — each with its parameters, what it returns, the errors it raises and an example.

Loaders.normalise

Use it to read the dashboard's loader setting, keeping only the values the SDK knows.

static LoaderStyle normalise(Object? raw)
ParameterTypeDefaultMeaning
rawObject?requiredThe dashboard's loader setting (appearance['loader']), as decoded.

Returns — LoaderStyle — its fields: LoaderStyle.

Errors — none.

final style = Loaders.normalise({'kind': 'ring', 'color': 'accent', 'text': {'thinking': 'One moment…'}});

Loaders.resolve

Use it to turn the dashboard's loader setting into the look to draw — with every default filled in.

static LoaderLook resolve(Object? raw)

The look for the dashboard's loader setting (appearance['loader']); anything unreadable gives the default.

ParameterTypeDefaultMeaning
rawObject?requiredThe dashboard's loader setting (appearance['loader']); anything unreadable gives the default.

Returns — LoaderLook — its fields: LoaderLook.

Errors — none.

final look = Loaders.resolve(GraineInApp.instance.session?.appearance['loader']);

Loaders.resolveStyle

Use it to turn an already-read loader setting into the look to draw.

static LoaderLook resolveStyle(LoaderStyle s)
ParameterTypeDefaultMeaning
sLoaderStylerequiredA loader setting already read with Loaders.normalise.

Returns — LoaderLook — its fields: LoaderLook.

Errors — none.

final look = Loaders.resolveStyle(const LoaderStyle(kind: 'ring'));

LoaderLook

Read it to draw a loader: its kind, colour, size, speed and words.

class LoaderLook

What to expect — Every value filled in, ready to draw — from Loaders.resolve.

PropertyTypeMeaning
final String kindStringThe loader to draw: dots, pulse, ring, bars or mark.
final String? colorString?Null means "use the accent".
final int pxintThe mark's size, in logical pixels.
final int periodMsintOne animation cycle.
final LoaderText textLoaderTextEvery moment filled in.
final bool skeletonboolDraw placeholder shapes where a component will appear.

Its functions: LoaderLook — each with its parameters, what it returns, the errors it raises and an example.

LoaderLook.LoaderLook

Use it to build a loader look by hand for a preview; Loaders.resolve gives the dashboard's.

const LoaderLook({ required this.kind, this.color, required this.px, required this.periodMs, required this.text, required this.skeleton})
ParameterTypeDefaultMeaning
kindStringrequiredThe loader to draw: dots, pulse, ring, bars or mark.
colorString?optionalNull means "use the accent".
pxintrequiredThe mark's size, in logical pixels.
periodMsintrequiredOne animation cycle.
textLoaderTextrequiredEvery moment filled in.
skeletonboolrequiredDraw placeholder shapes where a component will appear.

Returns — a new LoaderLook.

Errors — none.

const look = LoaderLook(kind: 'dots', px: 20, periodMs: 1100, text: Loaders.defaultText, skeleton: false);

LoaderStyle

Read it as the dashboard's loader setting, before defaults are filled in.

class LoaderStyle

What to expect — Only the values the SDK knows survive; anything else is left null, which means the default.

PropertyTypeMeaning
final String? kindString?dots, pulse, ring, bars or mark (the logo breathing); null: dots.
final String? colorString?"accent" or #RRGGBB.
final String? sizeString?small, medium or large; null: medium.
final String? speedString?calm, normal or quick; null: normal.
final LoaderText? textLoaderText?The brand's words for each moment; null: the defaults.
final bool? skeletonbool?Draw grey placeholder shapes where a component will appear; null: no.

Its functions: LoaderStyle, toMap — each with its parameters, what it returns, the errors it raises and an example.

LoaderStyle.LoaderStyle

Use it to build a loader setting by hand.

const LoaderStyle({this.kind, this.color, this.size, this.speed, this.text, this.skeleton})
ParameterTypeDefaultMeaning
kindString?optionaldots, pulse, ring, bars or mark (the logo breathing); null: dots.
colorString?optional"accent" or #RRGGBB.
sizeString?optionalsmall, medium or large; null: medium.
speedString?optionalcalm, normal or quick; null: normal.
textLoaderText?optionalThe brand's words for each moment; null: the defaults.
skeletonbool?optionalDraw grey placeholder shapes where a component will appear; null: no.

Returns — a new LoaderStyle.

Errors — none.

const style = LoaderStyle(kind: 'ring', size: 'small', speed: 'quick');

LoaderStyle.toMap

Use it to turn a loader setting back into the dashboard's JSON.

Map<String, Object?> toMap()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final map = const LoaderStyle(kind: 'ring', color: 'accent').toMap();

LoaderText

Use it to hold the words a loader says at each moment the customer waits.

class LoaderText

What to expect — A moment's words are at most 40 characters; null means the default words.

PropertyTypeMeaning
final String? connectingString?While a call connects; null: "Connecting…".
final String? reconnectingString?While a call reconnects; null: "Reconnecting…".
final String? thinkingString?While the agent prepares its answer; null: "Thinking…".
final String? componentString?While a component loads; null: "Loading…".
bool get isEmptyboolTrue when no moment has words of its own.

Its functions: LoaderText, forMoment, toMap — each with its parameters, what it returns, the errors it raises and an example.

LoaderText.LoaderText

Use it to build the loader's words by hand.

const LoaderText({this.connecting, this.reconnecting, this.thinking, this.component})
ParameterTypeDefaultMeaning
connectingString?optionalWhile a call connects; null: "Connecting…".
reconnectingString?optionalWhile a call reconnects; null: "Reconnecting…".
thinkingString?optionalWhile the agent prepares its answer; null: "Thinking…".
componentString?optionalWhile a component loads; null: "Loading…".

Returns — a new LoaderText.

Errors — none.

final result = LoaderText();

LoaderText.forMoment

Use it to get the words for one moment: connecting, reconnecting, thinking or component.

String? forMoment(String moment)
ParameterTypeDefaultMeaning
momentStringrequiredWhich moment of the call screen to draw.

Returns — String?. Example value: 'Thinking…'.

Errors — none.

final words = Loaders.defaultText.forMoment('thinking');

LoaderText.toMap

Use it to turn the loader's words back into the dashboard's JSON.

Map<String, Object?> toMap()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final map = const LoaderText(thinking: 'One moment…').toMap();

GraineTranscriptView

Use it to show the conversation so far — the call's captions as chat bubbles — in the agent's design.

class GraineTranscriptView extends StatelessWidget

What to expect — Place it where the conversation should show; it redraws as the call's captions arrive, each line as a bubble, and scrolls inside maxHeight.

PropertyTypeMeaning
final GraineCallState? callGraineCallState?The call whose words to show.
final GraineInApp? appGraineInApp?The agent the call belongs to; the view uses its call, session and design. When left out: the agent that call belongs to, or failing that the default agent.
final String? titleString?The header's words. Without it, the dashboard's heading is used, or "Assistant" when there is no heading.
final bool showHeaderboolDraw the header with the title (default true). False for a transcript inside a screen that has its own title.
final ResolvedDesign? designResolvedDesign?The design to draw.
final String? schemeString?"light", "dark" or "auto" (the device).
final double maxHeightdoubleThe tallest the view grows; beyond that, the conversation scrolls within it.
static const Key headerKeyKeyThe header's key, for a widget test.

Its functions: GraineTranscriptView, bubbleKey — each with its parameters, what it returns, the errors it raises and an example.

GraineTranscriptView.GraineTranscriptView

Use it to place the transcript in your own UI.

const GraineTranscriptView({ super.key, this.call, this.title, this.showHeader = true, this.design, this.scheme, this.maxHeight = 320, this.app})
ParameterTypeDefaultMeaning
keyKey?optionalThe widget's key, as for any Flutter widget.
callGraineCallState?optionalThe call whose words to show.
titleString?optionalThe header's words. Without it, the dashboard's heading is used, or "Assistant" when there is no heading.
showHeaderbooltrueDraw the header with the title (default true). False for a transcript inside a screen that has its own title.
designResolvedDesign?optionalThe design to draw.
schemeString?optional"light", "dark" or "auto" (the device).
maxHeightdouble320The tallest the view grows; beyond that, the conversation scrolls within it.
appGraineInApp?optionalThe agent the call belongs to; the view uses its call, session and design. When left out: the agent that call belongs to, or failing that the default agent.

Returns — a new GraineTranscriptView.

Errors — none.

const GraineTranscriptView(maxHeight: 240, showHeader: false)

GraineTranscriptView.bubbleKey

Use it in a widget test to find one bubble of the transcript.

static Key bubbleKey(int index)

The indexth turn's bubble (oldest is 0).

ParameterTypeDefaultMeaning
indexintrequiredThe position.

Returns — Key.

Errors — none.

import 'package:flutter_test/flutter_test.dart';
 
testWidgets('the first line of the call is on screen', (tester) async {
  await tester.pumpWidget(const MaterialApp(home: HomeScreen()));   // a screen of yours that shows GraineTranscriptView
  expect(find.byKey(GraineTranscriptView.bubbleKey(0)), findsOneWidget);
});

Caption

Read it to draw one line of the call's captions: who said it, what, and whether it is still being said.

class Caption

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.

PropertyTypeMeaning
final Role roleRoleWho said it: Role.agent or Role.customer.
final String textStringThe words, as far as they have been said or recognised.
final bool liveboolTrue while the text is still being recognised or streamed, so show it as provisional.

Its functions: Caption, settled — each with its parameters, what it returns, the errors it raises and an example.

Caption.Caption

Use it to build a caption by hand for a test or a preview.

const Caption(this.role, this.text, {this.live = false})
ParameterTypeDefaultMeaning
roleRolerequiredWho said it: Role.agent or Role.customer.
textStringrequiredThe words, as far as they have been said or recognised.
liveboolfalseTrue while the text is still being recognised or streamed, so show it as provisional.

Returns — a new Caption.

Errors — none.

const caption = Caption(Role.customer, 'Where is my order?');

Caption.settled

Use it to mark a caption as finished.

Caption settled()

Parameters — none.

Returns — Caption.

Errors — none.

final settled = GraineInApp.instance.voice.caption?.settled();

Captions

Use it to keep a list of captions the way the bar does, so a sentence growing word by word stays one line.

abstract final class Captions

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.

PropertyTypeMeaning
static const int historyintHow many turns are kept.

Its functions: fold — each with its parameters, what it returns, the errors it raises and an example.

Captions.fold

Use it to add a new caption to the list.

static List<Caption> fold(List<Caption> prev, Caption next, {int max = history})
ParameterTypeDefaultMeaning
prevList<Caption>requiredThe captions so far, oldest first.
nextCaptionrequiredThe newest caption: a live one replaces the open line of the same speaker; an empty one ends it.
maxinthistoryHow many lines to keep (12 by default).

Returns — List<Caption> — its fields: Caption. Example value: [].

Errors — none.

final lines = Captions.fold(const [], const Caption(Role.agent, 'Your order ships', live: true));

GraineCaptionsSwitch

Use it to give the customer a CC button: hide or show the line being said on the bar, and forget what was said, without ending the call.

abstract interface class GraineCaptionsSwitch

What to expect — GraineInApp.instance.voice implements it; each agent's voice has its own switch. It sits beside GraineCallState, as GraineCallFault does, so a call state of your own keeps compiling — implement it too to give the bar a switch. On by default (a caption nobody asked to hide is an accessibility default), and it holds for the rest of the app's run, from one call to the next. While it is off, GraineAgentBar hides its caption, as showCaptions: false does; what is said is still kept in captions. A change notifies the voice's listeners. New in 0.9.0.

PropertyTypeMeaning
bool get captionsOnboolThe CC switch: the bar draws the line being said only while it is on. On by default. What is said is kept (GraineCallState.captions) either way.

Its functions: setCaptionsOn, clearCaptions — each with its parameters, what it returns, the errors it raises and an example.

GraineCaptionsSwitch.setCaptionsOn

Call it from your own CC button to hide or show the bar's caption.

void setCaptionsOn(bool on)

Turns the captions on or off: the bar shows or hides the line being said. The call goes on, what is said is kept either way, and captionsOn follows at once.

ParameterTypeDefaultMeaning
onboolrequiredtrue to show the caption; false to hide it.

Returns — nothing; it returns when done.

Errors — none.

final voice = GraineInApp.instance.voice;
voice.setCaptionsOn(!voice.captionsOn);

GraineCaptionsSwitch.clearCaptions

Call it to forget what has been said so far, without ending the call.

void clearCaptions()

Empties captions — and so caption — without ending the call; a new call forgets them too. The switch stays as it is.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.voice.clearCaptions();

Role

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

enum Role { agent, customer }

What to expect — On every caption, on a call in the voice bar and on a native call alike.

CaseMeaning
agentThe agent's words, as they are said.
customerThe customer's words, as they are recognised.

Transport

Use it to choose how a call's audio travels: the standard path, or high quality (WebRTC) with the device's echo canceller.

enum Transport

What to expect — The dashboard's Call audio setting decides, unless you force one with GraineConfig(transport:). A call on webrtc that cannot connect falls back to websocket by itself; onTransport says which one carries it.

CaseMeaning
websocket('ws')Raw PCM on the conversation socket.
webrtc('rtc')Audio in Opus, carried over WebRTC, with a jitter buffer and the device's echo cancellation.
PropertyTypeMeaning
final String wireStringThe word on the wire and in the URL.

Its functions: Transport, from — each with its parameters, what it returns, the errors it raises and an example.

Transport.Transport

The enum's constructor; use the values (Transport.websocket, Transport.webrtc).

const Transport(this.wire)
ParameterTypeDefaultMeaning
wireStringrequiredThe word on the wire and in the URL.

Returns — nothing you call: an enum's constructor runs once for each value. Use the values; each carries its word in wire.

Errors — none.

const value = Transport.webrtc;
print(value.wire);   // rtc

Transport.from

Use it to read a transport from its word (ws, rtc).

static Transport from(String? s)
ParameterTypeDefaultMeaning
sString?requiredThe transport's word: rtc is webrtc, anything else websocket.

Returns — Transport. Example value: Transport.websocket.

Errors — none.

final transport = Transport.from('rtc');   // Transport.webrtc; any other word is Transport.websocket

ConnectionQuality

Read it to tell the customer when their connection is the problem, not the agent.

enum ConnectionQuality

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
excellentRound trip under 150 ms, jitter under 30 ms and loss under 2%: nothing to say.
goodWorse than excellent but below every "poor" line: the call works.
poorRound trip of 400 ms or more, jitter of 80 ms or more, or loss of 8% or more — any one is enough. Say "Weak connection".
lostThe device went offline during the call.
unknownNo measurement yet — before the first one, and between calls.

Its functions: classify — each with its parameters, what it returns, the errors it raises and an example.

ConnectionQuality.classify

Use it to turn one network measurement into the same word every Graine surface uses.

static ConnectionQuality classify(num? rttMs, num? jitterMs, num? loss)

One sample → one word.

ParameterTypeDefaultMeaning
rttMsnum?requiredThe round-trip time, in ms.
jitterMsnum?requiredThe jitter, in ms.
lossnum?requiredThe packet loss, from 0 to 1.

Returns — ConnectionQuality. Example value: ConnectionQuality.poor.

Errors — none.

final quality = ConnectionQuality.classify(420, 20, 0.01);

CallPhase

Read it to know where the call stands in one word — to draw your own bar.

enum CallPhase

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
idleNo call; ready to start one.
connectingThe page loading, or a call being placed.
reconnectingEither the page failed and is reloading, or the network cut the call off.
thinkingThe customer has finished speaking a sentence, and the agent's answer has not begun.
speakingThe agent is talking.
listeningThe agent is waiting for the customer.

ConnectStage

Read it to say where a call being placed is, so the customer waits instead of giving up.

enum ConnectStage

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
registeringSetting up a credential and a line — what happens on a cold start.
diallingThe call is on its way to the agent.
retryingNo answer came to the first attempt, so a new connection is being opened; on mobile data the cause is usually a connection the phone let go of while idle.
switchingThe call is moving to the other transport.
PropertyTypeMeaning
String get wireStringThe word on the wire.

Its functions: from — each with its parameters, what it returns, the errors it raises and an example.

ConnectStage.from

Use it to read a connect stage from its word.

static ConnectStage? from(String? wire)

Maps a wire word to its stage. Returns null for a word this SDK does not recognise, and also for "failed", since that ends a connect instead of being one of its stages.

ParameterTypeDefaultMeaning
wireString?requiredThe word on the wire.

Returns — ConnectStage?. Example value: ConnectStage.registering.

Errors — none.

final stage = ConnectStage.from('dialling');   // ConnectStage.dialling; a word this SDK does not know is null

ConnectProgress

Use its numbers and words to say "Still connecting…" and "Couldn't connect" exactly as the SDK's bar does.

abstract final class ConnectProgress

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.

PropertyTypeMeaning
static const int slowConnectMsintThe moment the status changes from "Connecting…" to "Still connecting…". It is later than a healthy connect takes (0.2–0.9 s warm, 1.5–2.5 s cold, on a good network) and comfortably short of the ten seconds after which people, given no explanation, decide it is broken.
static const String stillConnectingStringShown for a slow connect, as long as the brand has kept the default connecting words.
static const String couldNotConnectStringShown when the call could not be carried by either transport; the text uses a curly apostrophe.

GraineConnectProgress

Read it to say where a call being placed is, when a connect is slow, and when one failed — for your own bar.

abstract interface class GraineConnectProgress

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.

PropertyTypeMeaning
ConnectStage? get connectStageConnectStage?Where the call being placed is — registering, dialling, retrying or switching; null outside a connect. Say it: a bar that says "Reconnecting…" across a retry is one the customer waits for; "Connecting…" for eight seconds is one they close.
bool get slowConnectboolBecomes true when the customer has been kept waiting on a connect — the bar showing while the page loads, or a call being placed — for longer than timing.slowConnectMs.
bool get callFailedboolThe last start could not be connected at all — neither way of carrying the audio worked. Unlike error (a setting someone must change), the network may be better in a moment, so a start simply tries again. A new start, a call that connects, or the bar closing all clear it.

GraineCallFault

Read it to know when the agent's call cannot work until someone changes a setting, so your bar says "Unavailable".

abstract interface class GraineCallFault

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
String? get errorString?A permanent problem with this agent's setup that the page reported, such as a transport that has not been configured; null when there is none.

BarStatus

Read it to draw the bar's status line: its words, a loader, or the meter.

class BarStatus

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

PropertyTypeMeaning
final String textStringThe status words, in sentence case, exactly as the SDK's bar shows them ("Listening", "Weak connection").
final String? waitingString?Set for a loader moment (connecting, reconnecting or thinking), which is drawn in place of text using the dashboard's loader and the words it has for that moment.
final bool meterboolShow the four-bar meter (VoiceMeter) ahead of the words.
final String? labelString?Text the loader shows instead of the brand's waiting words. Only "Still connecting…" sets it, and only while the brand's words are still the defaults (connectingLook).

Its functions: BarStatus — each with its parameters, what it returns, the errors it raises and an example.

BarStatus.BarStatus

Use it to build a status line by hand for a preview.

const BarStatus(this.text, {this.waiting, this.meter = false, this.label})
ParameterTypeDefaultMeaning
textStringrequiredThe status words, in sentence case, exactly as the SDK's bar shows them ("Listening", "Weak connection").
waitingString?optionalSet for a loader moment (connecting, reconnecting or thinking), which is drawn in place of text using the dashboard's loader and the words it has for that moment.
meterboolfalseShow the four-bar meter (VoiceMeter) ahead of the words.
labelString?optionalText the loader shows instead of the brand's waiting words. Only "Still connecting…" sets it, and only while the brand's words are still the defaults (connectingLook).

Returns — a new BarStatus.

Errors — none.

const listening = BarStatus('Listening', meter: true);

agentBarStatus

Use it only to keep printing the older lower-case status words; new UIs use voiceBarStatus.

String agentBarStatus({ required GraineConnectivity connectivity, required bool? online, required bool reconnecting, required bool connecting, required bool ready, required bool muted, required bool connected, required ConnectionQuality quality, required bool agentSpeaking, bool micDenied = false})

The bar's one word in lower case, as the bar said it before 0.3 — the network named before the agent takes the blame for it. Kept for apps that print these words; the bar now says voiceBarStatus.

ParameterTypeDefaultMeaning
connectivityGraineConnectivityrequiredWhether the page that carries the call is there (GraineCallState.connectivity).
onlinebool?requiredWhether the device is online, as last reported; null when not reported yet.
reconnectingboolrequiredThe call is being re-established.
connectingboolrequiredA call is being placed.
readyboolrequiredThe page is ready to start a call.
mutedboolrequiredThe customer is muted.
connectedboolrequiredThe call is connected.
qualityConnectionQualityrequiredThe connection's quality.
agentSpeakingboolrequiredThe agent is speaking.
micDeniedboolfalseThe last start was refused the microphone.

Returns — String. Example value: 'speaking'.

Errors — none.

final word = agentBarStatus(connectivity: GraineConnectivity.ready, online: true, reconnecting: false, connecting: false, ready: true, muted: false, connected: true, quality: ConnectionQuality.good, agentSpeaking: true);

callPhase (function)

Use it to work out the call's phase from its state, exactly as the SDK does.

CallPhase callPhase({ required bool reconnecting, required GraineConnectivity connectivity, required bool connecting, required bool ready, required bool connected, required Caption? caption, bool agentSpeaking = false})

The call's phase, derived from its state as every Graine SDK derives it: thinking is the gap between the customer's last settled line and the agent's first words.

ParameterTypeDefaultMeaning
reconnectingboolrequiredThe call is being re-established.
connectivityGraineConnectivityrequiredWhether the page that carries the call is there.
connectingboolrequiredA call is being placed.
readyboolrequiredThe page is ready to start a call.
connectedboolrequiredThe call is connected.
captionCaption?requiredThe latest caption (GraineCallState.caption); a settled line from the customer means the agent is thinking.
agentSpeakingboolfalseThe agent is speaking.

Returns — CallPhase — its fields: CallPhase. Example value: CallPhase.idle.

Errors — none.

final voice = GraineInApp.instance.voice;
final phase = callPhase(reconnecting: voice.reconnecting, connectivity: voice.connectivity, connecting: voice.connecting, ready: voice.ready, connected: voice.connected, caption: voice.caption, agentSpeaking: voice.agentSpeaking);

connectingLook

Use it to get what the bar says while a call is being placed.

BarStatus connectingLook({ ConnectStage? stage, bool slow = false, String? connectingText})

What a call being placed says while it connects: a retry, or a switch to the other way of carrying the audio, says "Reconnecting…"; a slow connect says "Still connecting…" — but only while the brand's words for connecting are the default "Connecting…", so a wording someone chose is never replaced.

ParameterTypeDefaultMeaning
stageConnectStage?optionalWhere the connect is (connectStage); null before the first word from the engine.
slowboolfalseThe connect has run past GraineTiming.slowConnectMs.
connectingTextString?optionalThe brand's words for connecting (the loader's), so a chosen wording is never replaced; "Connecting…" when left out.

Returns — BarStatus — its fields: BarStatus. Example value: BarStatus('Reconnecting…', waiting: 'reconnecting').

Errors — none.

final status = connectingLook(stage: ConnectStage.retrying);

voiceBarStatus

Use it to get the bar's status line for the call's state, exactly as the SDK's bar says it.

BarStatus voiceBarStatus({ bool error = false, bool inChat = false, bool chatOnline = false, required bool? online, required bool reconnecting, required GraineConnectivity connectivity, required bool micDenied, required bool ready, required bool connecting, required bool muted, required bool connected, bool thinking = false, required bool speaking, required ConnectionQuality quality, bool callFailed = false, ConnectStage? connectStage, bool slowConnect = false, String? connectingText})

The voice bar's status line

ParameterTypeDefaultMeaning
errorboolfalseThe agent's setup is wrong (GraineCallFault.error is set): "Unavailable".
inChatboolfalseThe bar shows the chat, not the call.
chatOnlineboolfalseThe chat is connected (with inChat).
onlinebool?requiredWhether the device is online; null when not reported yet.
reconnectingboolrequiredThe call is being re-established.
connectivityGraineConnectivityrequiredWhether the page that carries the call is there.
micDeniedboolrequiredThe last start was refused the microphone.
readyboolrequiredThe page is ready to start a call.
connectingboolrequiredA call is being placed.
mutedboolrequiredThe customer is muted.
connectedboolrequiredThe call is connected.
thinkingboolfalseThe customer finished speaking and the agent has not answered yet.
speakingboolrequiredThe agent is speaking.
qualityConnectionQualityrequiredThe connection's quality.
callFailedboolfalseThe last start could not be connected at all (GraineConnectProgress.callFailed).
connectStageConnectStage?optionalWhere a connect is (GraineConnectProgress.connectStage).
slowConnectboolfalseThe connect has run past GraineTiming.slowConnectMs.
connectingTextString?optionalThe brand's words for connecting (the loader's); "Connecting…" when left out.

Returns — BarStatus — its fields: BarStatus.

Errors — none.

final voice = GraineInApp.instance.voice;
final status = voiceBarStatus(online: voice.online, reconnecting: voice.reconnecting, connectivity: voice.connectivity, micDenied: voice.micDenied, ready: voice.ready, connecting: voice.connecting, muted: voice.muted, connected: voice.connected, speaking: voice.agentSpeaking, quality: voice.quality, callFailed: voice.callFailed, connectStage: voice.connectStage, slowConnect: voice.slowConnect);

VoiceMeter

Use it to draw the four-bar meter beside "Listening" and "Speaking" at the design's own pace.

abstract final class VoiceMeter

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

PropertyTypeMeaning
static const List<double> heightsList<double>Each bar's height, as a fraction of height.
static const double heightdoubleThe meter's full height, in logical pixels: 12.
static const double barWidthdoubleEach bar's width, in logical pixels: 2.5.
static const double gapdoubleThe space between two bars, in logical pixels: 2.
static const double lowdoubleThe bottom of the swing; each bar scales up from its base between this value and 1.
static const int staggerMsintBar i starts i × this late.

Its functions: legMs, still, scale — each with its parameters, what it returns, the errors it raises and an example.

VoiceMeter.legMs

Use it to get how long one rise or fall of a meter bar takes.

static double legMs(int durationMs)

How long one leg of the swing takes (from low to full, or from full back to low): max(420, durationMs × 3.2) ms.

ParameterTypeDefaultMeaning
durationMsintrequiredThe design's motion duration (tokens.motion.durationMs).

Returns — double. Example value: 640.0.

Errors — none.

final leg = VoiceMeter.legMs(200);

VoiceMeter.still

Use it to know whether the meter should stand still (no motion in the design, or the device asks for less).

static bool still(int durationMs)

True when the bars should stay still at full height: the design has no motion, or the OS has asked for less motion.

ParameterTypeDefaultMeaning
durationMsintrequiredThe design's motion duration; 0 when the device asks for less motion.

Returns — bool. Example value: true.

Errors — none.

final still = VoiceMeter.still(0);

VoiceMeter.scale

Use it to get one meter bar's height at a moment.

static double scale(int bar, double elapsedMs, int durationMs, [double Function(double t)? ease])

Bar bar's height, as a fraction of VoiceMeter.height, elapsedMs after the meter appeared. Before its stagger a bar waits at its low point; then it rises for one leg and falls for the next, forever. ease shapes each leg; with no motion every bar is 1.

ParameterTypeDefaultMeaning
barintrequiredWhich bar, 0–3.
elapsedMsdoublerequiredHow long since the meter appeared, in ms.
durationMsintrequiredThe design's motion duration.
easedouble Function(double t)?optionalThe easing for each rise and fall (the SDK passes ease-in-out); linear when left out.

Returns — double. Example value: 3.

Errors — none.

final h = VoiceMeter.scale(1, 500, 200);

Placement

Use it to keep a dragged bar on screen the way the SDK does.

abstract final class Placement

What to expect — Arithmetic in logical pixels, the same on every Graine SDK, so an offset you persist means the same everywhere. The bar can never leave the screen.

PropertyTypeMeaning
static const double dragSlopdoubleA drag shorter than this on both axes counts as a tap, and that tap still has to work.

Its functions: clampOffset, offsetDuringDrag, isTap — each with its parameters, what it returns, the errors it raises and an example.

Placement.clampOffset

Use it to keep a bar's offset inside the screen.

static int clampOffset(double offset, PlacementBounds b)
ParameterTypeDefaultMeaning
offsetdoublerequiredThe wanted offset above the bar's resting place, in logical pixels.
bPlacementBoundsrequiredWhere the bar may go.

Returns — int. Example value: 3.

Errors — none.

final bounds = PlacementBounds(windowHeight: MediaQuery.sizeOf(context).height, baseBottom: 96, barHeight: 64);
final offset = Placement.clampOffset(10000, bounds);   // as high as the bar may go

Placement.offsetDuringDrag

Use it to move a bar while the customer drags it.

static int offsetDuringDrag(double startOffset, double dy, PlacementBounds b)

Computes the offset mid-drag as the starting offset plus the distance the finger has moved UP; because dy increases downward, it is subtracted.

ParameterTypeDefaultMeaning
startOffsetdoublerequiredThe offset when the drag began.
dydoublerequiredHow far the finger moved down since then, in logical pixels (negative is up).
bPlacementBoundsrequiredWhere the bar may go.

Returns — int. Example value: 3.

Errors — none.

final bounds = PlacementBounds(windowHeight: MediaQuery.sizeOf(context).height, baseBottom: 96, barHeight: 64);
// In onVerticalDragUpdate: the offset when the drag began, and how far the finger has moved since.
final offset = Placement.offsetDuringDrag(0, -120, bounds);   // 120 px up

Placement.isTap

Use it to tell a tap from the start of a drag.

static bool isTap(double dx, double dy)
ParameterTypeDefaultMeaning
dxdoublerequiredHow far the finger moved across, in logical pixels.
dydoublerequiredHow far the finger moved down.

Returns — bool. Example value: true.

Errors — none.

final tap = Placement.isTap(2, 3);

PlacementBounds

Use it to describe where a dragged bar may go.

class PlacementBounds

What to expect — Measured from the screen and the bar as last laid out; build new bounds when either changes.

PropertyTypeMeaning
final double windowHeightdoubleThe screen's height.
final double baseBottomdoubleWhere the bar sits with no offset.
final double barHeightdoubleThe bar's rendered height, as last measured.
final double topMargindoubleHow much room to leave free at the top, for the status bar and the notch.

Its functions: PlacementBounds — each with its parameters, what it returns, the errors it raises and an example.

PlacementBounds.PlacementBounds

Use it to build the bounds for Placement.

const PlacementBounds({ required this.windowHeight, required this.baseBottom, required this.barHeight, this.topMargin = 48})
ParameterTypeDefaultMeaning
windowHeightdoublerequiredThe screen's height.
baseBottomdoublerequiredWhere the bar sits with no offset.
barHeightdoublerequiredThe bar's rendered height, as last measured.
topMargindouble48How much room to leave free at the top, for the status bar and the notch.

Returns — a new PlacementBounds.

Errors — none.

final bounds = PlacementBounds(windowHeight: MediaQuery.sizeOf(context).height, baseBottom: 96, barHeight: 64);

KeyboardLift

Use it to lift a bottom bar above the keyboard by exactly as much as it needs.

abstract final class KeyboardLift

What to expect — Public so a custom UI can reuse it; an integration does not need it.

Its functions: liftFor — each with its parameters, what it returns, the errors it raises and an example.

KeyboardLift.liftFor

Use it to get how far to lift the bar for the keyboard on screen now.

static int liftFor(double keyboardHeight, double windowShrink)
ParameterTypeDefaultMeaning
keyboardHeightdoublerequiredThe keyboard's height on screen — MediaQuery.viewInsetsOf(context).bottom.
windowShrinkdoublerequiredHow much the window already shrank for the keyboard; 0 in Flutter, where the inset already accounts for it.

Returns — int. Example value: 3.

Errors — none.

final lift = KeyboardLift.liftFor(MediaQuery.viewInsetsOf(context).bottom, 0);

Several agents in one app

One app can run several agents, each with its own publishable key — a support agent in Help, a sales agent on Plans — and choose per screen which one the customer talks to. Configure each with GraineInApp.init(config, name: 'support') (see GraineInApp.init) and hand it to the views with app:.

  • Each agent has its own session and design, chat, call, components, actions, prompt variables, and scheduling and memory requests.
  • Shared by every agent: the install id; who is signed in (identify on any agent identifies the customer for all of them, and identify(null) signs out everywhere); the memory choice; and the screen (setScreen, track, tap).
  • One conversation at a time: starting a call or a chat with one agent ends a live one with another.
  • Engines: each agent the customer can call needs its own GraineEngine near the root; an agent you only chat with needs none.
  • Native calls: GraineCalls.instance.startCall(agent: 'support') calls a named agent; a ring Graine sends for one rings as that agent.

An app with one agent changes nothing: GraineInApp.init(config) is the default agent, and every view draws it.

GraineInApp.named

Use it to reach an agent you configured under a name — in a screen that talks to that agent.

static GraineInApp? named(String name)

The agent configured under name (trimmed; empty means the default agent), or null.

ParameterTypeDefaultMeaning
nameStringrequiredThe name the agent was configured under.

Returns — GraineInApp? — the agent, or null when none was configured under that name.

Errors — none.

final result = GraineInApp.named('support');

GraineInApp.all

Use it to go through every agent the app configured — to register an action with each, say, or to check one exists.

static Map<String, GraineInApp> get all

Every configured agent, by the name it was configured under, in the order they were configured — the default agent under 'default'. Unmodifiable; empty before the first init.

Parameters — none.

Returns — Map<String, GraineInApp>. Example value: {'default': GraineInApp, 'support': GraineInApp}.

Errors — none.

for (final agent in GraineInApp.all.values) {
  agent.registerAction('open_screen', (args) {
    navigatorKey.currentState?.pushNamed(args['path'] as String);
    return const ActionResult.ok();
  });
}

GraineInApp.instance

Use it to reach the default agent anywhere in your app after init.

static GraineInApp get instance

The default agent: the one init configured without a name — or, in an app that named every agent, the first one configured. Every view draws it unless you hand it another with app:.

Parameters — none.

Returns — GraineInApp.

Errors — StateError ("GraineInApp.init(config) has not been called.") before init; ask maybeInstance when you are not sure.

GraineInApp.instance.setScreen(const GraineScreen('home', title: 'Home'));

GraineInApp.maybeInstance

Use it to ask whether the SDK has been configured yet, without the error instance throws before init.

static GraineInApp? get maybeInstance

The default agent, or null before init.

Parameters — none.

Returns — GraineInApp?. Example value: null before init.

Errors — none.

final graine = GraineInApp.maybeInstance;
if (graine != null) graine.track('opened_help');

GraineInApp.defaultName

Use it to name the default agent explicitly — for example in startCall(agent:) or when comparing a call's agent.

static const String defaultName = 'default'

The name of the default agent: the one init configures when given no name (or an empty one), which instance returns, and which a native call's agent names.

Parameters — none.

Returns — String. Example value: 'default'.

Errors — none.

final withDefault = GraineCalls.instance.value.agent == GraineInApp.defaultName;

GraineInApp.name

Read it to know which agent an instance is — the name it was configured under.

final String name

The name this agent was configured under — trimmed; defaultName for the default agent.

Parameters — none.

Returns — String. Example value: 'support'.

Errors — none.

final support = GraineInApp.named('support')!;
print(support.name);   // support

Screen context, actions and components

Tell the agent what the customer is looking at — the screen, its fields and how each is doing — so it can help with exactly that; let it act in your app through the actions you register; and draw the components it shows (product lists, choices, time slots, information cards) in your design. Everything that leaves the device is masked first; the friction rules let the agent speak up when a customer looks stuck.

GraineInApp.setVariables

Call it when something the agent's prompt uses changes at runtime — a plan, a cart total, a language.

void setVariables(Map<String, Object?>? values)

Prompt variables from the app's own data, for this agent. Merged into what is there: null under a key removes it; null for the whole map clears them all. Values become strings (an integer never gains ".0"; maps and lists are dropped) and are masked on the device. They reach the next conversation, and a ready engine page receives them at once. A conversation that starts with a ticket carries them on its ticket — since 0.8.0 the chat's are no longer added to that connection's address — and one without a ticket carries them in its address. A conversation carries up to 12 variables, about 1 KB: past that, the biggest are left off first, who_called and call_reason last.

ParameterTypeDefaultMeaning
valuesMap<String, Object?>?requiredThe variables by name. Merged over what is there: a null value removes its variable; null for the whole map clears them all.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.setVariables({'plan': 'Pro', 'cart_total': 42.5, 'language': 'en'});

GraineInApp.currentVariables

Call it to see exactly which prompt variables the agent gets now, after masking.

Map<String, String> currentVariables()

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 once the configuration has loaded).

Parameters — none.

Returns — Map<String, String>.

Errors — none.

final result = GraineInApp.instance.currentVariables();

GraineInApp.setScreen

Call it whenever the customer reaches a screen, so the agent knows what they are looking at and which fields need them.

void setScreen(GraineScreen? next)

What the customer is looking at; null forgets it. Call it on every meaningful change. Values are masked on the device before they leave, and an unchanged screen is not resent. The first change goes out at once; a burst after it is coalesced into one more update at the end of a 400 ms window, so the agent's opening line is never a screen behind. The screen is the app's: every agent hears it and tells its own conversation while one is live. A new screen id starts a new visit for friction detection.

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

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.setScreen(const GraineScreen('checkout_address',
    title: 'Delivery address',
    journey: GraineJourney('Checkout', step: 2, of: 3, next: 'Payment'),
    fields: [GraineField('postal_code', label: 'Postal code', value: '400001', status: FieldStatus.invalid, error: "We don't deliver there yet")]));

GraineInApp.updateField

Call it when one field on the current screen changes — typed, validated, cleared — without describing the whole screen again.

void updateField(GraineField field)

Replaces the current screen's field of the same name, or adds it after the others. Does nothing before a screen is set (the next setScreen carries the field itself) 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.instance.updateField(const GraineField('postal_code', label: 'Postal code', value: '400001', status: FieldStatus.filled));

GraineInApp.removeField

Call it when a field leaves the current screen.

void removeField(String name)

Removes a field from the current screen. Does nothing if no screen is set or the field was never reported.

ParameterTypeDefaultMeaning
nameStringrequiredThe field's name.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.removeField('coupon');

GraineInApp.clearScreen

Call it when the customer leaves a screen you described and the next one has nothing to say.

void clearScreen([String? screenId])

Forgets the screen, but only if screenId (when given) is still the current screen's id. Call it from a screen's cleanup: a navigator keeps the old screen through its exit transition, and an unconditional setScreen(null) there would wipe the screen that has just replaced it.

ParameterTypeDefaultMeaning
screenIdString?optionalThe screen's name.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.clearScreen();

GraineInApp.track

Call it to tell the agent something the customer did — added to cart, opened help — so it can mention it.

void track(String name, [Map<String, Object?> data = const <String, Object?>{}])

A product event the agent is told about without being interrupted. It rides the next context update; the last ten events are kept. Every agent records it. An empty name is ignored.

ParameterTypeDefaultMeaning
nameStringrequiredThe product event's name, for example "plan_viewed".
dataMap<String, Object?>const <String, Object?>{}Details of the event, by name; masked on the device.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.track('opened_help', {'from': 'order_page'});

GraineInApp.tap

Call it to tell the agent the customer tapped a control you name yourself.

void tap(String name)

The customer tapped a control you name: recorded as "tap:<name>" in the recent events (the name cut to one line of at most 40 characters), so the agent knows what they just did. Context, not news: it goes out at the end of the 400 ms window. Every agent records it. An empty name is ignored.

ParameterTypeDefaultMeaning
nameStringrequiredThe control's name, as the agent sees it: tap:<name>.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.tap('Apply coupon');

GraineInApp.captureTap

Called by GraineEngine for every tap it sees; call it yourself only if you capture taps another way.

void captureTap({required double x, required double y, String? label})

A tap anywhere in the app. GraineEngine reports every tap it sees; an app with its own gesture layer may call it too. On a control with an accessibility label it is recorded as "tap:<label>" (one line, at most 40 characters); elsewhere as "tap:screen" with x and y in logical pixels. Recorded only by agents with GraineConfig.captureTaps on that have a screen set, and at most two taps a second across the app. Context, not news: it rides the next update.

ParameterTypeDefaultMeaning
xdoublerequiredThe horizontal position, in points.
ydoublerequiredThe vertical position, in points.
labelString?optionalThe tapped control's accessibility label; null for a tap anywhere else.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.captureTap(x: 120, y: 640, label: 'Apply coupon');

GraineInApp.noteInteraction

Call it when the customer does something on the screen that setScreen does not show, so the stuck-customer timer restarts.

void noteInteraction()

Tells the SDK the customer did something other than editing a field, such as scrolling a list or opening a picker. Restarts every agent's idle clock. Field edits do not count.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.noteInteraction();

GraineInApp.registerAction

Call it to let the agent do something in your app — open a screen, apply a coupon — by name, with your handler.

void Function() registerAction(String name, GraineActionHandler handler)

Something the agent can do in the app, for this agent. Declare the same action on the agent in the dashboard: a handler with no declaration is never offered, and a declaration with no handler is refused. The handler may answer synchronously or with a Future and must answer within six seconds, or the agent is told it was not done; returning null means ok, and a throw is answered as error. Registering a name again replaces its handler. The returned function unregisters it, if it is still the one registered.

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

Returns — void Function() — unregisters it.

Errors — none.

final unregister = GraineInApp.instance.registerAction('set_language', (args) async {
  final language = args['language'];
  if (language is! String) return const ActionResult.refused('No language was given.');
  await settings.setLanguage(language);
  return ActionResult.ok('The app is now in $language.');
});

GraineInApp.checkActions

Call it once while you build, to compare the actions your app registers with the ones the dashboard declares.

Map<String, List<String>> checkActions()

Compares what this build registered with what the agent declares: usable (both), undeclared (registered, not declared on the agent), unhandled (declared, not registered). Declarations come from the agent's configuration, so read it after the session has loaded; log it once at startup.

Parameters — none.

Returns — Map<String, List<String>>.

Errors — none.

final result = GraineInApp.instance.checkActions();

GraineInApp.executeAction

Call it to run one of your registered actions as the agent would — from a component's button, or in a test.

Future<ActionResult> executeAction(String name, Map<String, Object?> args)

Runs one registered action exactly as the agent would, against the six-second deadline, and reports onActionRequested and onActionCompleted to listeners. Always completes: an unknown name answers refused; a throw or a timeout answers error.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, as registered.
argsMap<String, Object?>requiredIts arguments, as the agent would pass them.

Returns — Future<ActionResult> — its fields: ActionResult.

Errors — none.

final result = await GraineInApp.instance.executeAction('set_language', {'language': 'fr'});

GraineInApp.configureFriction

Call it to change, for this app, when the agent speaks up for a customer who looks stuck.

void configureFriction(FrictionConfig? override)

Replaces the app's layer of friction rules (the layer GraineConfig.friction set). Safe at any time. The rules in force become the defaults, then the dashboard's, then override.

ParameterTypeDefaultMeaning
overrideFrictionConfig?requiredA value to use instead.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.configureFriction(const FrictionConfig(idleAfterMs: 30000, maxPerSession: 2));

GraineInApp.clearPendingWidget

Call it when your own UI has dealt with the component the agent drew, so no view draws it again.

void clearPendingWidget()

Forgets the retained component.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.clearPendingWidget();

GraineVoice.reportEvent

Call it to tell the agent, during a call, about something it may want to speak up about.

void reportEvent(String name, {String? detail, Map<String, Object?> data = const <String, Object?>{}})

Reports something that has happened which the agent may choose to comment on.

ParameterTypeDefaultMeaning
nameStringrequiredWhat happened, in a word — the agent may speak up about it.
detailString?optionalA sentence about it, or nothing.
dataMap<String, Object?>const <String, Object?>{}Facts about it, as JSON values (masked before they leave the device).

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.voice.reportEvent('coupon_applied', detail: 'The customer applied SAVE10.');

GraineScreen

Use it to describe what the customer is looking at: the screen, where it sits in a flow, its fields and what the agent may do there.

class GraineScreen

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.

PropertyTypeMeaning
final String screenStringAn id for the screen that never changes; analytics rely on it, so do not vary it.
final String? titleString?The screen's title as the customer sees it ("Delivery address"); the agent names the screen by it. Null: the screen id is used.
final GraineJourney? journeyGraineJourney?Where the screen sits in a flow (step 2 of 4) and what comes next; null for a screen in no flow.
final List<GraineField> fieldsList<GraineField>The inputs on the screen and how each is doing — what lets the agent name the field that needs the customer. Empty for a screen with no inputs.
final String? errorString?An error banner for the whole screen, separate from any field's own error.
final Map<String, Object?> dataMap<String, Object?>Anything else the agent should know.
final List<String>? availableActionsList<String>?Which of your registered actions the agent may use on this screen. Null allows every registered action; [] allows none.

Its functions: GraineScreen, copyWith, withField, withoutField — each with its parameters, what it returns, the errors it raises and an example.

GraineScreen.GraineScreen

Use it to describe a screen for setScreen.

const GraineScreen(this.screen, { this.title, this.journey, this.fields = const <GraineField>[], this.error, this.data = const <String, Object?>{}, this.availableActions})
ParameterTypeDefaultMeaning
screenStringrequiredAn id for the screen that never changes; analytics rely on it, so do not vary it.
titleString?optionalThe screen's title as the customer sees it ("Delivery address"); the agent names the screen by it. Null: the screen id is used.
journeyGraineJourney?optionalWhere the screen sits in a flow (step 2 of 4) and what comes next; null for a screen in no flow.
fieldsList<GraineField>const <GraineField>[]The inputs on the screen and how each is doing — what lets the agent name the field that needs the customer. Empty for a screen with no inputs.
errorString?optionalAn error banner for the whole screen, separate from any field's own error.
dataMap<String, Object?>const <String, Object?>{}Anything else the agent should know.
availableActionsList<String>?optionalWhich of your registered actions the agent may use on this screen. Null allows every registered action; [] allows none.

Returns — a new GraineScreen.

Errors — none.

GraineInApp.instance.setScreen(GraineScreen('checkout_address',
    title: 'Delivery address',
    journey: const GraineJourney('Checkout', step: 2, of: 3, next: 'Payment'),
    fields: [GraineField('postal_code', label: 'Postal code', status: FieldStatus.invalid, error: "We don't deliver there yet")]));

GraineScreen.copyWith

Use it to make the same screen with new fields.

GraineScreen copyWith({List<GraineField>? fields})

This screen with fields in place of its own.

ParameterTypeDefaultMeaning
fieldsList<GraineField>?optionalThe inputs on the screen and how each is doing — what lets the agent name the field that needs the customer. Empty for a screen with no inputs.

Returns — GraineScreen.

Errors — none.

const screen = GraineScreen('sign_up', title: 'Create your account');
final next = screen.copyWith(fields: [const GraineField('email', label: 'Email', status: FieldStatus.filled)]);

GraineScreen.withField

Use it to make the same screen with one field replaced or added.

GraineScreen withField(GraineField field)

Returns a copy of this screen where field replaces the field with the same name, or is appended after the existing fields if no field has that name.

ParameterTypeDefaultMeaning
fieldGraineFieldrequiredThe field to put in place of the one with its name — or add after the others.

Returns — GraineScreen.

Errors — none.

const screen = GraineScreen('checkout_address', title: 'Delivery address');
final next = screen.withField(const GraineField('postal_code', label: 'Postal code', status: FieldStatus.filled));

GraineScreen.withoutField

Use it to make the same screen without one field.

GraineScreen withoutField(String name)

Returns this screen minus the field called name; if no field has that name, the same screen is returned.

ParameterTypeDefaultMeaning
nameStringrequiredThe name of the field to leave out.

Returns — GraineScreen.

Errors — none.

const screen = GraineScreen('checkout', fields: [GraineField('coupon', label: 'Coupon', optional: true)]);
final next = screen.withoutField('coupon');

GraineField

Use it to describe one input on a screen — its label, value and status — so the agent can name the field that needs the customer.

class GraineField

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.

PropertyTypeMeaning
final String nameStringThe field's stable id ("email"). updateField and removeField find a field by it.
final String? labelString?The field's label as the customer sees it ("Email address"); the agent names the field by it. Null: the name is used.
final String? valueString?The real value.
final FieldStatus? statusFieldStatus?How the field is doing — empty, filled, invalid, pending or locked. What makes the agent proactive: after a pause it names a field that is invalid or empty.
final String? errorString?The validation message that is really on screen, which the agent can offer to help fix.
final bool optionalboolThis field may be left empty: it stays in the frame but is not counted.

Its functions: GraineField — each with its parameters, what it returns, the errors it raises and an example.

GraineField.GraineField

Use it to describe one field for GraineScreen or updateField.

const GraineField(this.name, {this.label, this.value, this.status, this.error, this.optional = false})
ParameterTypeDefaultMeaning
nameStringrequiredThe field's stable id ("email"). updateField and removeField find a field by it.
labelString?optionalThe field's label as the customer sees it ("Email address"); the agent names the field by it. Null: the name is used.
valueString?optionalThe real value.
statusFieldStatus?optionalHow the field is doing — empty, filled, invalid, pending or locked. What makes the agent proactive: after a pause it names a field that is invalid or empty.
errorString?optionalThe validation message that is really on screen, which the agent can offer to help fix.
optionalboolfalseThis field may be left empty: it stays in the frame but is not counted.

Returns — a new GraineField.

Errors — none.

const field = GraineField('email', label: 'Email address', value: 'asha@', status: FieldStatus.invalid, error: 'Enter a valid email');

FieldStatus

Use it to say how a field is doing, so the agent can point at the one that is empty or wrong.

enum FieldStatus

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
empty('empty')Nothing entered yet. Counts as outstanding unless the field is optional.
filled('filled')Entered, and nothing is wrong with it.
invalid('invalid')Entered, but wrong — put the message you show in GraineField.error, so the agent can offer to fix it.
pending('pending')Being checked or loaded (an address lookup, a code being verified). Not outstanding.
locked('locked')Shown but not the customer's to change. Never outstanding.
PropertyTypeMeaning
final String wireStringThe status as it is sent — the same word as the value's name.

Its functions: FieldStatus, fromWire — each with its parameters, what it returns, the errors it raises and an example.

FieldStatus.FieldStatus

The enum's constructor; use the values (FieldStatus.empty, FieldStatus.invalid, …).

const FieldStatus(this.wire)
ParameterTypeDefaultMeaning
wireStringrequiredThe status as it is sent — the same word as the value's name.

Returns — nothing you call: an enum's constructor runs once for each value. Use the values; each carries its word in wire.

Errors — none.

const value = FieldStatus.invalid;
print(value.wire);   // invalid

FieldStatus.fromWire

Use it to read a field status from its word.

static FieldStatus? fromWire(String? s)
ParameterTypeDefaultMeaning
sString?requiredThe status's word; anything else reads as null.

Returns — FieldStatus?. Example value: FieldStatus.empty.

Errors — none.

final status = FieldStatus.fromWire('invalid');   // FieldStatus.invalid

GraineJourney

Use it to say where a screen sits in a flow (step 2 of 4) and what comes next, so the agent can answer "what happens after this?".

class GraineJourney

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.

PropertyTypeMeaning
final String nameStringThe flow's name as the customer would say it ("Checkout").
final int? stepint?Which step of the flow this screen is, counting from 1; null when you do not number the steps.
final int? ofint?How many steps the flow has; null when it varies or you do not say.
final String? nextString?What comes after this step; if asked, the agent repeats this instead of making something up.

Its functions: GraineJourney — each with its parameters, what it returns, the errors it raises and an example.

GraineJourney.GraineJourney

Use it to describe a screen's place in a flow.

const GraineJourney(this.name, {this.step, this.of, this.next})
ParameterTypeDefaultMeaning
nameStringrequiredThe flow's name as the customer would say it ("Checkout").
stepint?optionalWhich step of the flow this screen is, counting from 1; null when you do not number the steps.
ofint?optionalHow many steps the flow has; null when it varies or you do not say.
nextString?optionalWhat comes after this step; if asked, the agent repeats this instead of making something up.

Returns — a new GraineJourney.

Errors — none.

const GraineJourney('Checkout', step: 2, of: 3, next: 'Payment')

GraineActionHandler

Implement it to run one action the agent asks for, and answer what happened.

typedef GraineActionHandler = FutureOr<ActionResult?> Function( Map<String, Object?> args);

What to expect — What your app answers an agent's action with: ok (with an optional sentence and data), refused (the customer declined) or error.

ActionResult

Return it from an action handler to tell the agent what happened — it speaks about exactly this.

class ActionResult

What to expect — What your app answers an agent's action with: ok (with an optional sentence and data), refused (the customer declined) or error.

PropertyTypeMeaning
final ActionStatus statusActionStatusWhat happened: ok (the agent says it is done), refused (not done, with a reason) or error (it failed).
final String messageStringWhat the agent is told, in a sentence — the reason for a refusal, what failed, or a detail of what was done. Empty: the status alone.
final Map<String, Object?> dataMap<String, Object?>Anything the agent should know about the result (an order number, a new total), as JSON values. Empty by default.

Its functions: ActionResult, ok, refused, error — each with its parameters, what it returns, the errors it raises and an example.

ActionResult.ActionResult

Use it to answer with a status, a message and data of your choosing.

const ActionResult(this.status, {this.message = '', this.data = const <String, Object?>{}})
ParameterTypeDefaultMeaning
statusActionStatusrequiredWhat happened: ok (the agent says it is done), refused (not done, with a reason) or error (it failed).
messageString''What the agent is told, in a sentence — the reason for a refusal, what failed, or a detail of what was done. Empty: the status alone.
dataMap<String, Object?>const <String, Object?>{}Anything the agent should know about the result (an order number, a new total), as JSON values. Empty by default.

Returns — a new ActionResult.

Errors — none.

GraineInApp.instance.registerAction('track_order', (args) async {
  return const ActionResult(ActionStatus.ok, message: 'Order 1042 is on its way.', data: {'order': '1042'});
});

ActionResult.ok

Use it to answer that the action was done.

const ActionResult.ok([this.message = '', this.data = const <String, Object?>{}])
ParameterTypeDefaultMeaning
messageString''What the agent may add about it; empty by default.
dataMap<String, Object?>const <String, Object?>{}Anything the agent should know about the result; empty by default.

Returns — a new ActionResult with status ActionStatus.ok.

Errors — none.

GraineInApp.instance.registerAction('apply_coupon', (args) async {
  await cart.apply(args['code'] as String);
  return const ActionResult.ok('The coupon is applied.');
});

ActionResult.refused

Use it to answer that the action was not done, and why — the agent passes the reason on.

const ActionResult.refused(this.message)
ParameterTypeDefaultMeaning
messageStringrequiredWhy it was not done — the agent passes it on.

Returns — a new ActionResult with status ActionStatus.refused and no data.

Errors — none.

GraineInApp.instance.registerAction('show_orders', (args) async {
  if (!signedIn) return const ActionResult.refused('The customer is not signed in.');
  Navigator.of(context).pushNamed('/orders');
  return const ActionResult.ok('The orders are on screen.');
});

ActionResult.error

Use it to answer that the action failed.

const ActionResult.error(this.message)
ParameterTypeDefaultMeaning
messageStringrequiredWhat went wrong, in a sentence.

Returns — a new ActionResult with status ActionStatus.error and no data.

Errors — none.

GraineInApp.instance.registerAction('apply_coupon', (args) async {
  try {
    await cart.apply(args['code'] as String);
    return const ActionResult.ok('The coupon is applied.');
  } catch (_) {
    return const ActionResult.error('The cart could not be updated.');
  }
});

ActionStatus

Read it to know what an action's answer told the agent: done, refused or failed.

enum ActionStatus

What to expect — What your app answers an agent's action with: ok (with an optional sentence and data), refused (the customer declined) or error.

CaseMeaning
ok('ok')Done: the agent tells the customer it is done.
refused('refused')Not done, on purpose: the agent passes on the reason in ActionResult.message.
error('error')Failed: the agent says it could not do it. A handler that throws, or does not answer in time, answers this.
PropertyTypeMeaning
final String wireStringThe status as it is sent — the same word as the value's name.

Its functions: ActionStatus — each with its parameters, what it returns, the errors it raises and an example.

ActionStatus.ActionStatus

The enum's constructor; use the values (ActionStatus.ok, refused, error).

const ActionStatus(this.wire)
ParameterTypeDefaultMeaning
wireStringrequiredThe status as it is sent — the same word as the value's name.

Returns — nothing you call: an enum's constructor runs once for each value. Use the values; each carries its word in wire.

Errors — none.

const value = ActionStatus.refused;
print(value.wire);   // refused

Masking

Use it to see — and extend — what the SDK masks before anything leaves the device.

abstract final class Masking

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, expandReplacement — each with its parameters, what it returns, the errors it raises and an example.

Masking.mask

Use it to see how a piece of text leaves the device.

static String mask(String input)

Masks identifiers in a single string, applying the five built-in rules first and then the rules the app added (addRule).

ParameterTypeDefaultMeaning
inputStringrequiredThe text to mask.

Returns — String. Example value: 'Mail me at [EMAIL]'.

Errors — none.

final safe = Masking.mask('Mail me at asha@example.com');

Masking.addRule

Call it at launch to mask a value shape only your app knows, such as an account number.

static void addRule(RegExp pattern, [String token = '[REDACTED]'])

A rule for a value shape only your app knows — an account number, an order reference.

ParameterTypeDefaultMeaning
patternRegExprequiredWhat to mask: every match is replaced.
tokenString'[REDACTED]'What replaces it; [REDACTED] by default. It may use $1, $&, $$ and $<name> as a JavaScript replacement does.

Returns — nothing; it returns when done.

Errors — none.

Masking.addRule(RegExp(r'\bACME-\d{6}\b'), '[ORDER_ID]');

Masking.clearCustomRules

Call it to remove the rules you added (the built-in ones stay).

static void clearCustomRules()

Meant for tests alone; in production, register rules a single time when the app starts.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

Masking.clearCustomRules();

Masking.deep

Use it to mask every text inside a map or a list, as the SDK masks the screen context.

static Object? deep(Object? value)

Applies masking to all strings inside a JSON value, however deeply nested.

ParameterTypeDefaultMeaning
valueObject?requiredA JSON value: every text inside it is masked; keys, numbers and booleans are left as they are.

Returns — Object?. Example value: null.

Errors — none.

final masked = Masking.deep({'email': 'asha@example.com', 'items': ['mug'], 'count': 2});

Masking.expandReplacement

Use it to see how a rule's replacement is written for one match.

static String expandReplacement(String token, Match m)

Writes a rule's replacement for one match, reading its $ patterns ($1, $&, $$, $<name>) exactly as a JavaScript replacement string does — so a rule written for the website or React Native means the same here.

ParameterTypeDefaultMeaning
tokenStringrequiredA rule's replacement, with its $ patterns.
mMatchrequiredOne match of the rule.

Returns — String. Example value: 'ORD-[1234]'.

Errors — none.

final m = RegExp(r'ORD-(\d+)').firstMatch('Order ORD-1234')!;
final out = Masking.expandReplacement(r'ORD-[$1]', m);

FrictionConfig

Use it to set, for this app, when the agent speaks up for a customer who looks stuck — over the dashboard's rules.

class FrictionConfig

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.

PropertyTypeMeaning
final bool? enabledbool?Turns the stuck-customer rule on or off for this app; null inherits the dashboard's (on by default).
final int? idleAfterMsint?Staying on one screen for this long raises stalled_on_step.
final int? cooldownMsint?The shortest time allowed between two signals on one screen.
final int? cooldownSecondsint?The pacing Graine applies to interventions the agent makes without being asked.
final int? maxPerSessionint?At most this many unasked interventions from the agent in one conversation; null inherits (default 3).
final bool? openLauncherbool?Passed through to GraineListener.onFriction, for an app that opens its own launcher.
final String? messageString?What the agent is told instead of the built-in sentence, with the placeholders {title}, {screen}, {seconds}, {field} and {error}.
final bool? requireOutstandingFieldbool?Count the customer as stuck only while a field still needs them (invalid, or empty and not optional), with the clock running from when it started needing them. Default true; false counts any idle time on the screen.
final Map<String, FrictionScreenRule> screensMap<String, FrictionScreenRule>Per-screen rules, keyed by the screen id you pass to setScreen — to switch the rule off, or change it, on one screen.

Its functions: FrictionConfig, fromJson — each with its parameters, what it returns, the errors it raises and an example.

FrictionConfig.FrictionConfig

Use it to build the app's friction rules for GraineConfig(friction:) or configureFriction.

const FrictionConfig({ this.enabled, this.idleAfterMs, this.cooldownMs, this.cooldownSeconds, this.maxPerSession, this.openLauncher, this.message, this.requireOutstandingField, this.screens = const <String, FrictionScreenRule>{}})
ParameterTypeDefaultMeaning
enabledbool?optionalTurns the stuck-customer rule on or off for this app; null inherits the dashboard's (on by default).
idleAfterMsint?optionalStaying on one screen for this long raises stalled_on_step.
cooldownMsint?optionalThe shortest time allowed between two signals on one screen.
cooldownSecondsint?optionalThe pacing Graine applies to interventions the agent makes without being asked.
maxPerSessionint?optionalAt most this many unasked interventions from the agent in one conversation; null inherits (default 3).
openLauncherbool?optionalPassed through to GraineListener.onFriction, for an app that opens its own launcher.
messageString?optionalWhat the agent is told instead of the built-in sentence, with the placeholders {title}, {screen}, {seconds}, {field} and {error}.
requireOutstandingFieldbool?optionalCount the customer as stuck only while a field still needs them (invalid, or empty and not optional), with the clock running from when it started needing them. Default true; false counts any idle time on the screen.
screensMap<String, FrictionScreenRule>const <String, FrictionScreenRule>{}Per-screen rules, keyed by the screen id you pass to setScreen — to switch the rule off, or change it, on one screen.

Returns — a new FrictionConfig.

Errors — none.

const FrictionConfig(idleAfterMs: 30000, screens: {'terms': FrictionScreenRule(ignore: true)})

FrictionConfig.fromJson

Use it to read friction rules from the dashboard's JSON.

static FrictionConfig? fromJson(Map<String, Object?>? o)

Builds the dashboard's layer out of the friction object in the session.

ParameterTypeDefaultMeaning
oMap<String, Object?>?requiredThe object.

Returns — FrictionConfig?. Example value: null.

Errors — none.

final rules = FrictionConfig.fromJson({'idle': {'afterMs': 30000}, 'nudge': {'maxPerSession': 2}});

FrictionScreenRule

Use it to change the friction rules on one screen — or switch them off there.

class FrictionScreenRule

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.

PropertyTypeMeaning
final bool? ignorebool?Turns friction detection off entirely for this screen; a person reading terms, for example, is not stuck.
final int? idleAfterMsint?How long the customer may stay idle on this screen before they count as stuck, in ms; 0 or less switches the rule off here; null inherits.
final String? messageString?What the agent is told on this screen instead of the built-in sentence, with the same placeholders as FrictionConfig.message; null inherits.

Its functions: FrictionScreenRule — each with its parameters, what it returns, the errors it raises and an example.

FrictionScreenRule.FrictionScreenRule

Use it to build one screen's rule for FrictionConfig.screens.

const FrictionScreenRule({this.ignore, this.idleAfterMs, this.message})
ParameterTypeDefaultMeaning
ignorebool?optionalTurns friction detection off entirely for this screen; a person reading terms, for example, is not stuck.
idleAfterMsint?optionalHow long the customer may stay idle on this screen before they count as stuck, in ms; 0 or less switches the rule off here; null inherits.
messageString?optionalWhat the agent is told on this screen instead of the built-in sentence, with the same placeholders as FrictionConfig.message; null inherits.

Returns — a new FrictionScreenRule.

Errors — none.

const FrictionScreenRule(idleAfterMs: 60000, message: 'The customer has spent {seconds}s on {title}.')

FrictionSignal

Read it in onFriction to know that a customer looks stuck, where, and what the agent was told.

class FrictionSignal

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.

PropertyTypeMeaning
final String nameStringWhat kind of friction it is: stalled_on_step (the customer stayed idle on a step).
final String screenStringThe id of the screen the customer is stuck on, as you passed it to setScreen.
final String detailStringThe sentence the agent is told, in plain words (your message with its placeholders filled, or the built-in one).
final Map<String, Object?> dataMap<String, Object?>The facts behind it: seconds idle, the field that needs the customer and its error (empty when there is none).
final int cooldownSecondsintThe least time between two unasked interventions from the agent, in seconds.
final int maxPerSessionintThe most unasked interventions the agent makes in one conversation.
final bool openLauncherboolTrue when the rules ask your app to open its own launcher or bar for the customer.

Its functions: FrictionSignal — each with its parameters, what it returns, the errors it raises and an example.

FrictionSignal.FrictionSignal

Use it to build a signal by hand for a test.

const FrictionSignal({ required this.name, required this.screen, required this.detail, required this.data, required this.cooldownSeconds, required this.maxPerSession, required this.openLauncher})
ParameterTypeDefaultMeaning
nameStringrequiredWhat kind of friction it is: stalled_on_step (the customer stayed idle on a step).
screenStringrequiredThe id of the screen the customer is stuck on, as you passed it to setScreen.
detailStringrequiredThe sentence the agent is told, in plain words (your message with its placeholders filled, or the built-in one).
dataMap<String, Object?>requiredThe facts behind it: seconds idle, the field that needs the customer and its error (empty when there is none).
cooldownSecondsintrequiredThe least time between two unasked interventions from the agent, in seconds.
maxPerSessionintrequiredThe most unasked interventions the agent makes in one conversation.
openLauncherboolrequiredTrue when the rules ask your app to open its own launcher or bar for the customer.

Returns — a new FrictionSignal.

Errors — none.

const signal = FrictionSignal(name: 'stalled_on_step', screen: 'checkout_address', detail: 'The customer has not touched the "Delivery address" screen for 45s.', data: {'seconds': 45, 'field': 'Postal code', 'error': ''}, cooldownSeconds: 20, maxPerSession: 3, openLauncher: false);

FrictionRules

Read it to see the friction rules in force: the defaults, the dashboard's and the app's, applied in that order.

class FrictionRules

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.

PropertyTypeMeaning
final bool enabledboolWhether the stuck-customer rule runs at all (default true).
final int? idleAfterMsint?Null: the idle rule is off.
final int cooldownMsintThe least time between two signals on the same screen, in ms (default 60 000).
final int cooldownSecondsintThe least time between two unasked interventions from the agent, in seconds (default 20).
final int maxPerSessionintThe most unasked interventions in one conversation (default 3).
final bool openLauncherboolAsk your app to open its own launcher or bar on a signal (default false).
final String messageStringThe sentence the agent is told; empty: the built-in one.
final bool requireOutstandingFieldboolCount a customer as stuck only while a field still needs them (default true).
final Map<String, FrictionScreenRule> screensMap<String, FrictionScreenRule>Per-screen rules, keyed by screen id.

Its functions: FrictionRules, forScreen — each with its parameters, what it returns, the errors it raises and an example.

FrictionRules.FrictionRules

Use it to build rules by hand for a test.

const FrictionRules({ this.enabled = true, this.idleAfterMs = 45000, this.cooldownMs = 60000, this.cooldownSeconds = 20, this.maxPerSession = 3, this.openLauncher = false, this.message = '', this.requireOutstandingField = true, this.screens = const <String, FrictionScreenRule>{}})
ParameterTypeDefaultMeaning
enabledbooltrueWhether the stuck-customer rule runs at all (default true).
idleAfterMsint?45000Null: the idle rule is off.
cooldownMsint60000The least time between two signals on the same screen, in ms (default 60 000).
cooldownSecondsint20The least time between two unasked interventions from the agent, in seconds (default 20).
maxPerSessionint3The most unasked interventions in one conversation (default 3).
openLauncherboolfalseAsk your app to open its own launcher or bar on a signal (default false).
messageString''The sentence the agent is told; empty: the built-in one.
requireOutstandingFieldbooltrueCount a customer as stuck only while a field still needs them (default true).
screensMap<String, FrictionScreenRule>const <String, FrictionScreenRule>{}Per-screen rules, keyed by screen id.

Returns — a new FrictionRules.

Errors — none.

final result = FrictionRules();

FrictionRules.forScreen

Use it to see the rules in force on one screen.

FrictionForScreen forScreen(String? id)

The rules that apply to one screen: the top-level rules, with that screen's overrides applied.

ParameterTypeDefaultMeaning
idString?requiredThe screen id, as passed to setScreen; null for no screen.

Returns — FrictionForScreen — its fields: FrictionForScreen.

Errors — none.

final checkout = GraineInApp.instance.frictionRules.forScreen('checkout_address');

FrictionForScreen

Read it as the friction rules in force on one screen.

class FrictionForScreen

What to expect — Worked out by FrictionRules.forScreen for the screen in view; it changes when the screen or the rules do.

PropertyTypeMeaning
final bool ignoreboolTrue when the rule is off on this screen.
final int? idleAfterMsint?How long the customer may stay idle here before they count as stuck, in ms; null: the rule is off here.
final String messageStringThe sentence the agent is told here; empty: the built-in one.

Its functions: FrictionForScreen — each with its parameters, what it returns, the errors it raises and an example.

FrictionForScreen.FrictionForScreen

Use it to build one screen's rules by hand for a test.

const FrictionForScreen({required this.ignore, required this.idleAfterMs, required this.message})
ParameterTypeDefaultMeaning
ignoreboolrequiredTrue when the rule is off on this screen.
idleAfterMsint?requiredHow long the customer may stay idle here before they count as stuck, in ms; null: the rule is off here.
messageStringrequiredThe sentence the agent is told here; empty: the built-in one.

Returns — a new FrictionForScreen.

Errors — none.

const rule = FrictionForScreen(ignore: false, idleAfterMs: 30000, message: 'Need a hand with the address?');

Friction

Use it to apply layers of friction rules the way the SDK does.

abstract final class Friction

What to expect — Public so a custom UI can reuse it; an integration does not need it.

Its functions: resolve — each with its parameters, what it returns, the errors it raises and an example.

Friction.resolve

Use it to get the rules in force from the defaults and your layers.

static FrictionRules resolve(List<FrictionConfig?> layers)

Starts from the defaults and applies each layer in turn; a layer changes only the settings it names.

ParameterTypeDefaultMeaning
layersList<FrictionConfig?>requiredThe layers in order — usually the dashboard's, then the app's; a null layer is skipped.

Returns — FrictionRules — its fields: FrictionRules.

Errors — none.

final rules = Friction.resolve([null, const FrictionConfig(idleAfterMs: 30000)]);   // the defaults, then each layer over them

FrictionDetector

Use it to run the stuck-customer rule yourself — the SDK runs one for every agent.

class FrictionDetector

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.

PropertyTypeMeaning
FrictionRules get rulesFrictionRulesThe rules it applies now.

Its functions: FrictionDetector, configure, onScreen, onInteraction, onCall, idleDueIn, onIdle, outstanding, fill — each with its parameters, what it returns, the errors it raises and an example.

FrictionDetector.FrictionDetector

Use it to make a detector with the rules to apply.

FrictionDetector({FrictionRules rules = const FrictionRules(), bool gateOnCall = false})
ParameterTypeDefaultMeaning
rulesFrictionRulesconst FrictionRules()The rules it applies now.
gateOnCallboolfalseRun the idle clock only while a call is live (the SDK passes true).

Returns — a new FrictionDetector.

Errors — none.

final result = FrictionDetector();

FrictionDetector.configure

Use it to change the rules a detector applies.

void configure(FrictionRules next)
ParameterTypeDefaultMeaning
nextFrictionRulesrequiredThe rules to apply from now on.

Returns — nothing; it returns when done.

Errors — none.

final detector = FrictionDetector();
detector.configure(GraineInApp.instance.frictionRules);

FrictionDetector.onScreen

Call it when the screen changes.

void onScreen(GraineScreen? next, int now)

The app reported a screen.

ParameterTypeDefaultMeaning
nextGraineScreen?requiredThe screen now; null when none is set.
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — nothing; it returns when done.

Errors — none.

final detector = FrictionDetector(rules: GraineInApp.instance.frictionRules);
detector.onScreen(const GraineScreen('checkout_address'), DateTime.now().millisecondsSinceEpoch);

FrictionDetector.onInteraction

Call it when the customer does something, so the idle clock restarts.

void onInteraction(int now)

Records that the customer acted, which restarts the idle clock.

ParameterTypeDefaultMeaning
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — nothing; it returns when done.

Errors — none.

final detector = FrictionDetector(rules: GraineInApp.instance.frictionRules);
detector.onInteraction(DateTime.now().millisecondsSinceEpoch);

FrictionDetector.onCall

Call it when a call connects or ends.

void onCall(bool live, int now)

A call connected or ended.

ParameterTypeDefaultMeaning
liveboolrequiredtrue when a call connected, false when it ended.
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — nothing; it returns when done.

Errors — none.

final detector = FrictionDetector(rules: GraineInApp.instance.frictionRules, gateOnCall: true);
detector.onCall(true, DateTime.now().millisecondsSinceEpoch);

FrictionDetector.idleDueIn

Use it to know when to check again.

int? idleDueIn(int now)

The time left before the idle rule fires, or null when it should not fire.

ParameterTypeDefaultMeaning
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — int?. Example value: 3.

Errors — none.

final detector = FrictionDetector(rules: GraineInApp.instance.frictionRules);
final dueIn = detector.idleDueIn(DateTime.now().millisecondsSinceEpoch);

FrictionDetector.onIdle

Call it when the idle timer goes off, to get the signal to send, if any.

FrictionSignal? onIdle(int now)

The idle timer went off.

ParameterTypeDefaultMeaning
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — FrictionSignal? — its fields: FrictionSignal. Example value: null.

Errors — none.

final detector = FrictionDetector(rules: GraineInApp.instance.frictionRules);
final signal = detector.onIdle(DateTime.now().millisecondsSinceEpoch);

FrictionDetector.outstanding

Use it to list the fields the customer still has to deal with.

static List<GraineField> outstanding(List<GraineField> fields)

What the customer still has to deal with.

ParameterTypeDefaultMeaning
fieldsList<GraineField>requiredThe screen's fields.

Returns — List<GraineField> — its fields: GraineField. Example value: [].

Errors — none.

final missing = FrictionDetector.outstanding(const [
  GraineField('postal_code', label: 'Postal code', status: FieldStatus.empty),
  GraineField('coupon', label: 'Coupon', optional: true),
]);

FrictionDetector.fill

Use it to fill a message's placeholders the way the SDK does.

static String fill(String template, Map<String, Object?> vars)

Fills {name} placeholders; an unknown one becomes empty.

ParameterTypeDefaultMeaning
templateStringrequiredThe message with {title}, {screen}, {seconds}, {field} and {error} placeholders.
varsMap<String, Object?>requiredThe values by placeholder name; an unknown placeholder becomes empty.

Returns — String. Example value: 'Stuck on Delivery address for 45s'.

Errors — none.

final sentence = FrictionDetector.fill('Stuck on {title} for {seconds}s', {'title': 'Delivery address', 'seconds': 45});

GraineComponents

Use it to draw the components the agent shows — or answer them from your own UI.

class GraineComponents extends ChangeNotifier

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.

PropertyTypeMeaning
final void Function(String widgetId, Map<String, Object?> data, String summary)nothingCalled when the customer answers, to pass the answer to the agent; the SDK sends it to whichever of the call or the chat drew the component.
final Future<ActionResult> Function(String name, Map<String, Object?> args)Future<ActionResult>Runs an in-app action from a component; in the SDK this is GraineInApp.executeAction.
final void Function(String name, String detail, Map<String, Object?> data)nothingPasses the agent a quiet note; the SDK sends it to the live call and the chat.
GraineInApp? get appGraineInApp?The agent these components belong to, or null when they stand alone.
bool get fromChatboolTrue when the component on screen came from the typed chat rather than a call. Nothing is listening then, so its view does not mention listening.
GraineWidget? get widgetGraineWidget?The component on screen, or null.
WidgetContent? get contentWidgetContent?The content widget boils down to — rows, chips, days — plus the answer that each tap sends.
WidgetLook? get lookWidgetLook?How widget asked to look.
WidgetPending? get loadingWidgetPending?A component that has been announced but is still loading; otherwise null.
WidgetAnswered? get answeredWidgetAnswered?The last answer, kept on screen as one line.
bool get awaitingReplyboolTrue after the customer answers and before the agent begins its reply.
GraineWidget? get closedGraineWidget?The most recent component that was closed unanswered, so it can be offered with a "show it again".

Its functions: GraineComponents, present, presentPending, answer, dismiss, reopen, reset — each with its parameters, what it returns, the errors it raises and an example.

GraineComponents.GraineComponents

Use it to make a components state of your own, for a host that drives its own conversation; the SDK makes one per agent.

GraineComponents({ required String baseUrl, this.onAnswer, this.onAction, this.onReport, int Function()? clock})
ParameterTypeDefaultMeaning
baseUrlStringrequired"https://www.graine.ai" unless your workspace is self-hosted.
onAnswer—optionalCalled when the customer answers.
onAction—optionalCalled when the customer taps an action.
onReport—optionalCalled with a report.
clockint Function()?optionalThe clock, in milliseconds since the epoch; the device's when left out. A test passes its own.

Returns — a new GraineComponents.

Errors — none.

final result = GraineComponents(baseUrl: 'https://www.graine.ai');

GraineComponents.present

Call it to put a component on screen from a frame you received.

void present(Map<String, Object?> frame)

Draws a frame that came from somewhere else, such as a preview or a test, just as it would if the agent had sent it.

ParameterTypeDefaultMeaning
frameMap<String, Object?>requiredThe component as Graine sent it.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.components.present({'widget_id': 'w_1', 'kind': 'choices', 'data': {'options': ['Morning', 'Evening']}});

GraineComponents.presentPending

Call it when a component is on its way, so a loader stands where it will appear.

void presentPending(Map<String, Object?> hint)

Displays the loader for an incoming component, the same as when the agent announces one.

ParameterTypeDefaultMeaning
hintMap<String, Object?>requiredWhat is known of the coming component — its id and kind.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.components.presentPending({'widget_id': 'w_1', 'kind': 'product_list'});

GraineComponents.answer

Call it when the customer chose something in a component you draw yourself.

void answer(WidgetAnswer a)

The customer answered.

ParameterTypeDefaultMeaning
aWidgetAnswerrequiredThe answer: its data and its sentence (WidgetContent.pick and friends build one).

Returns — nothing; it returns when done.

Errors — none.

final components = GraineInApp.instance.components;
final content = components.content;
if (content != null) components.answer(content.pick(0));   // the customer tapped the first row

GraineComponents.dismiss

Call it when the customer closes a component without choosing.

void dismiss()

Closes the component with no choice made, or acts as "Clear" for the answered line.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.components.dismiss();

GraineComponents.reopen

Call it to bring back the component the customer closed.

void reopen()

Restores the most recently closed component in the same state it was in.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.components.reopen();

GraineComponents.reset

Call it to clear every component, answer and loader.

void reset()

Discards everything that has been drawn, as when a call ends or a new call starts.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

GraineInApp.instance.components.reset();

GraineWidgetView

Use it to draw the agent's components yourself — where you want them, in the agent's design — instead of on the bar.

class GraineWidgetView extends StatefulWidget

What to expect — Place it where the components should appear; while it is mounted it draws them alone (the bar stops drawing them). It shows a loader while one is on its way and keeps the customer's answer on screen as one line.

PropertyTypeMeaning
final GraineComponents? componentsGraineComponents?The state to draw.
final GraineInApp? appGraineInApp?The agent the components belong to; the view uses its components, session and design. When left out: the agent that components belong to, or failing that the default agent.
final String? accentString?#RRGGBB for buttons and badges.
final double? radiusdouble?Corner rounding. When left out, it falls back to the component's own rounding, then a pinned platform's, then the dashboard's.
final String? schemeString?"light", "dark" or "auto" (the device).
final GraineWidgetTheme? themeGraineWidgetTheme?Single colours of your own over the design's — surface, border, text, muted and the ink on the accent.
final Map<String, Object?>? appearanceMap<String, Object?>?The dashboard's Appearance.
final ResolvedDesign? designResolvedDesign?The design to draw. When left out, the session's design (GraineInApp.design) is used; if there is none either, components keep their pre-design look.
final double maxHeightdoubleThe tallest the component can grow; beyond that, its body scrolls within it.

Its functions: GraineWidgetView — each with its parameters, what it returns, the errors it raises and an example.

GraineWidgetView.GraineWidgetView

Use it to place the component area in your own layout.

const GraineWidgetView({ super.key, this.components, this.accent, this.radius, this.scheme, this.theme, this.appearance, this.design, this.maxHeight = 460, this.app})
ParameterTypeDefaultMeaning
keyKey?optionalThe widget's key, as for any Flutter widget.
componentsGraineComponents?optionalThe state to draw.
accentString?optional#RRGGBB for buttons and badges.
radiusdouble?optionalCorner rounding. When left out, it falls back to the component's own rounding, then a pinned platform's, then the dashboard's.
schemeString?optional"light", "dark" or "auto" (the device).
themeGraineWidgetTheme?optionalSingle colours of your own over the design's — surface, border, text, muted and the ink on the accent.
appearanceMap<String, Object?>?optionalThe dashboard's Appearance.
designResolvedDesign?optionalThe design to draw. When left out, the session's design (GraineInApp.design) is used; if there is none either, components keep their pre-design look.
maxHeightdouble460The tallest the component can grow; beyond that, its body scrolls within it.
appGraineInApp?optionalThe agent the components belong to; the view uses its components, session and design. When left out: the agent that components belong to, or failing that the default agent.

Returns — a new GraineWidgetView.

Errors — none.

Column(children: [
  Expanded(child: productList),
  const GraineWidgetView(maxHeight: 360),   // the agent's components, here instead of on the bar
])

GraineWidgetTheme

Use it to give the components single colours of your own over the agent's design.

class GraineWidgetTheme

What to expect — Pass it as GraineWidgetView(theme:). Each colour left null keeps the design's.

PropertyTypeMeaning
final Color? surfaceColor?Behind the component.
final Color? borderColor?Hairlines and chip fills.
final Color? textColor?Titles and prices.
final Color? mutedColor?Subtitles, facts, secondary labels.
final Color? onAccentColor?Readable ON the accent.

Its functions: GraineWidgetTheme — each with its parameters, what it returns, the errors it raises and an example.

GraineWidgetTheme.GraineWidgetTheme

Use it to build the colour overrides for GraineWidgetView(theme:).

const GraineWidgetTheme({this.surface, this.border, this.text, this.muted, this.onAccent})
ParameterTypeDefaultMeaning
surfaceColor?optionalBehind the component.
borderColor?optionalHairlines and chip fills.
textColor?optionalTitles and prices.
mutedColor?optionalSubtitles, facts, secondary labels.
onAccentColor?optionalReadable ON the accent.

Returns — a new GraineWidgetTheme.

Errors — none.

final result = GraineWidgetTheme();

GraineComponent

Read it as one component the agent drew — the same type as GraineWidget, under the name every Graine SDK uses.

typedef GraineComponent = GraineWidget;

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.

GraineWidget

Read it to draw one component the agent drew: its kind, data, style and what a tap does in your app.

class GraineWidget

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.

PropertyTypeMeaning
final String idStringThe component's id — every answer and dismissal carries it back.
final String kindStringNames the component's kind, such as "product_list", "plan_comparison" or "choices", or any other kind the agent declares.
final Map<String, Object?> dataMap<String, Object?>Its data, with root-relative picture paths already resolved against the agent's origin.
final String fallbackTextStringWhat the agent would have said instead.
final WidgetStyle styleWidgetStyleHow the dashboard asked for it to look.
final WidgetSelectAction onSelectWidgetSelectActionThe action the app takes when an item is tapped, in addition to informing the agent.

Its functions: GraineWidget, adopt — each with its parameters, what it returns, the errors it raises and an example.

GraineWidget.GraineWidget

Use it to build a component by hand for a preview or a test.

const GraineWidget({ required this.id, required this.kind, required this.data, required this.fallbackText, required this.style, required this.onSelect})
ParameterTypeDefaultMeaning
idStringrequiredThe component's id — every answer and dismissal carries it back.
kindStringrequiredNames the component's kind, such as "product_list", "plan_comparison" or "choices", or any other kind the agent declares.
dataMap<String, Object?>requiredIts data, with root-relative picture paths already resolved against the agent's origin.
fallbackTextStringrequiredWhat the agent would have said instead.
styleWidgetStylerequiredHow the dashboard asked for it to look.
onSelectWidgetSelectActionrequiredThe action the app takes when an item is tapped, in addition to informing the agent.

Returns — a new GraineWidget.

Errors — none.

const widget = GraineWidget(id: 'w_1', kind: 'choices', data: {'options': ['Morning', 'Evening']}, fallbackText: 'Morning or evening?', style: WidgetStyle(), onSelect: WidgetSelectAction());

GraineWidget.adopt

Use it to read a component from the frame Graine sends.

static GraineWidget? adopt(Map<String, Object?>? raw, String baseUrl)

Turns the page's widget object into a component. Returns null when the object has no widget_id, because a frame the SDK cannot answer should not be drawn.

ParameterTypeDefaultMeaning
rawMap<String, Object?>?requiredThe component as Graine sent it; null, or one with no id, gives null.
baseUrlStringrequired"https://www.graine.ai" unless your workspace is self-hosted.

Returns — GraineWidget?. Example value: null.

Errors — none.

final widget = GraineWidget.adopt({'widget_id': 'w_1', 'kind': 'choices', 'data': {'options': ['Morning', 'Evening']}}, 'https://www.graine.ai');

WidgetContent

Use it to draw a component of your own: its rows, chips or days, and the answer each tap sends.

class WidgetContent

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final GraineWidget widgetGraineWidgetThe component it was read from.
late final bool isQuickActionsboolA quick-actions component: draw chips.
late final String selectLabelStringThe text on the button, which is also the verb used for "choose several".
late final List<WidgetRow> rowsList<WidgetRow>The rows to draw, up to eight.
late final bool isProductsboolDraw rows — a product list, plans or a card.
late final bool isChipsboolDraw chips — choices or quick actions.
late final bool isCalendarboolDraw days — time slots.
late final bool drawableboolSomething this build can draw.
late final List<WidgetChip> chipsList<WidgetChip>The chips to draw.
late final List<WidgetDay> daysList<WidgetDay>The days and their times, up to four days.
late final String titleStringThe component's title; empty for none.
late final String promptStringThe question it asks; empty for none.
late final String noteStringA line under it; empty for none.

Its functions: WidgetContent, surface, many, pick, pickMany, pickChip, pickSlot, toggle, manyButtonLabel, screenFooter, closeLabel, answeredLine — each with its parameters, what it returns, the errors it raises and an example.

WidgetContent.WidgetContent

Use it to read a component into what you draw.

WidgetContent(this.widget)
ParameterTypeDefaultMeaning
widgetGraineWidgetrequiredThe component it was read from.

Returns — a new WidgetContent.

Errors — none.

final widget = GraineWidget.adopt({'widget_id': 'w_1', 'kind': 'choices', 'data': {'options': ['Morning', 'Evening']}}, 'https://www.graine.ai');
final content = widget == null ? null : WidgetContent(widget);

WidgetContent.surface

Use it to know where the component is drawn: a full screen, a sheet or a card.

String surface(WidgetLook look)

Which surface is really used: a kind that cannot be drawn never claims the screen or a sheet, and is given 'card'.

ParameterTypeDefaultMeaning
lookWidgetLookrequiredHow it is drawn.

Returns — String. Example value: 'sheet'.

Errors — none.

final components = GraineInApp.instance.components;
final content = components.content, look = components.look;
if (content != null && look != null) debugPrint(content.surface(look));

WidgetContent.many

Use it to know whether the customer may choose several.

bool many(WidgetLook look)

Multiple selection is used only when there are items that can be ticked.

ParameterTypeDefaultMeaning
lookWidgetLookrequiredHow it is drawn.

Returns — bool. Example value: true.

Errors — none.

final components = GraineInApp.instance.components;
final content = components.content, look = components.look;
final ticks = content != null && look != null && content.many(look);   // the customer ticks several

WidgetContent.pick

Use it to build the answer when the customer taps one row.

WidgetAnswer pick(int index)

One row tapped.

ParameterTypeDefaultMeaning
indexintrequiredThe position.

Returns — WidgetAnswer — its fields: WidgetAnswer.

Errors — none.

final content = GraineInApp.instance.components.content;
final answer = content?.pick(0);   // the customer tapped the first row

WidgetContent.pickMany

Use it to build one answer for several rows the customer ticked.

WidgetAnswer pickMany(List<String> ids)

Turns several picks into a single answer: all the items, their ids and titles flattened the way an in-app action's arguments usually expect, and one sentence naming them as a person would.

ParameterTypeDefaultMeaning
idsList<String>requiredThe ids of the rows the customer ticked, in order.

Returns — WidgetAnswer — its fields: WidgetAnswer.

Errors — none.

final content = GraineInApp.instance.components.content;
final answer = content?.pickMany(['mug-blue', 'mug-red']);   // the rows the customer ticked, by id

WidgetContent.pickChip

Use it to build the answer when the customer taps a chip.

WidgetAnswer pickChip(WidgetChip chip)

A chip tapped.

ParameterTypeDefaultMeaning
chipWidgetChiprequiredThe chip tapped.

Returns — WidgetAnswer — its fields: WidgetAnswer.

Errors — none.

final content = GraineInApp.instance.components.content;
if (content != null && content.chips.isNotEmpty) {
  final answer = content.pickChip(content.chips.first);
}

WidgetContent.pickSlot

Use it to build the answer when the customer taps a time.

WidgetAnswer pickSlot(WidgetDay day, String slot)

Handles a tap on a time, giving the day and the slot — the shape resultToText("calendar_slots") reads.

ParameterTypeDefaultMeaning
dayWidgetDayrequiredThe day the time belongs to.
slotStringrequiredThe time tapped, as shown.

Returns — WidgetAnswer — its fields: WidgetAnswer.

Errors — none.

final content = GraineInApp.instance.components.content;
if (content != null && content.days.isNotEmpty) {
  final answer = content.pickSlot(content.days.first, '6:00 PM');
}

WidgetContent.toggle

Use it to tick or untick a row while the customer chooses several.

static List<String> toggle(List<String> ticked, String id, int maxSelect)

Ticks or unticks one row in a multiple selection; ticking at the cap drops the earliest tick to make room.

ParameterTypeDefaultMeaning
tickedList<String>requiredThe ids ticked so far.
idStringrequiredThe row tapped.
maxSelectintrequiredThe most rows that may be ticked (WidgetLook.maxSelect).

Returns — List<String>. Example value: ['b', 'c', 'd'].

Errors — none.

final ticked = WidgetContent.toggle(const ['a', 'b', 'c'], 'd', 3);

WidgetContent.manyButtonLabel

Use it to get the words on the button under a multi-select list.

String manyButtonLabel(int chosen)

The one button under a multi-select list.

ParameterTypeDefaultMeaning
chosenintrequiredHow many rows are ticked.

Returns — String. Example value: 'Pick 1 more'.

Errors — none.

final words = GraineInApp.instance.components.content?.manyButtonLabel(1);   // "Pick 1 more", then the button's label

WidgetContent.screenFooter

Use it to get a full-screen component's footer, when it has one.

static ({String label, String line})? screenFooter(String prompt, {required bool typed})

A full-screen component's footer: on a call, the label STILL LISTENING and how to answer — the agent is still there, listening. A component that came through the chat belongs to no call, so it gets no footer.

ParameterTypeDefaultMeaning
promptStringrequiredThe component's question (WidgetContent.prompt); empty for the default line.
typedboolrequiredThe component arrived in the chat (no call is listening).

Returns — ({String label, String line})? — the footer's label and line, or null with typed. Example value: (label: 'STILL LISTENING', line: 'Tap one, or just say it.').

Errors — none.

final components = GraineInApp.instance.components;
final content = components.content;
final footer = content == null ? null : WidgetContent.screenFooter(content.prompt, typed: components.fromChat);

WidgetContent.closeLabel

Use it to get what a screen reader says for the component's close control.

static String closeLabel({required bool typed})

What a screen reader announces for the component's close control; in the chat it leaves out talking, since there is none to carry on with.

ParameterTypeDefaultMeaning
typedboolrequiredThe component arrived in the chat (no call to keep talking on).

Returns — String. Example value: 'Close and keep talking'.

Errors — none.

final words = WidgetContent.closeLabel(typed: false);

WidgetContent.answeredLine

Use it to get the line that stays on screen after the customer answered.

static ({String title, String detail}) answeredLine(Map<String, Object?> data)

The line that stays on screen after an answer, read from the answer's data: a title (its name, title, label or slot) and a detail (its price and day, joined with " · "). Both empty means there is no line to draw.

ParameterTypeDefaultMeaning
dataMap<String, Object?>requiredThe answer's data.

Returns — ({String title, String detail}). Example value: (title: 'Blue Mug', detail: '$12').

Errors — none.

final answered = GraineInApp.instance.components.answered;
final line = answered == null ? null : WidgetContent.answeredLine(answered.data);

WidgetAnswer

Read it as what a tap sends the agent: the data, and the sentence that is the customer's turn.

class WidgetAnswer

What to expect — What a tap on a component sends the agent: a sentence for the agent, and structured data for your webhooks.

PropertyTypeMeaning
final Map<String, Object?> dataMap<String, Object?>The structured answer the agent reads (the chosen item's id, title and price; a day and a slot).
final String summaryStringThe sentence that is the customer's turn ("I'll take a look at Blue Mug at $12."). Without it the conversation does not move.

Its functions: WidgetAnswer — each with its parameters, what it returns, the errors it raises and an example.

WidgetAnswer.WidgetAnswer

Use it to build an answer for GraineComponents.answer.

const WidgetAnswer(this.data, this.summary)
ParameterTypeDefaultMeaning
dataMap<String, Object?>requiredThe structured answer the agent reads (the chosen item's id, title and price; a day and a slot).
summaryStringrequiredThe sentence that is the customer's turn ("I'll take a look at Blue Mug at $12."). Without it the conversation does not move.

Returns — a new WidgetAnswer.

Errors — none.

const answer = WidgetAnswer({'id': 'mug-blue', 'title': 'Blue Mug', 'price': '\$12'}, "I'll take a look at Blue Mug at \$12.");

WidgetRow

Read it to draw one row of a component: a product, a plan or a card.

class WidgetRow

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final String idStringThe item's id (its title when it has none).
final String titleStringThe item's name.
final String subtitleStringA line under the name; empty for none.
final String priceStringThe price as shown, with its suffix ("$12/mo"); empty for none.
final List<String> factsList<String>Up to three short facts.
final String imageStringThe picture's address; empty for none.
final bool unavailableboolDraw it greyed out and not choosable.
final String selectLabelStringThe row's button words ("Choose", "Select").

Its functions: WidgetRow — each with its parameters, what it returns, the errors it raises and an example.

WidgetRow.WidgetRow

Use it to build a row by hand for a preview.

const WidgetRow({ required this.id, required this.title, required this.subtitle, required this.price, required this.facts, required this.image, required this.unavailable, required this.selectLabel})
ParameterTypeDefaultMeaning
idStringrequiredThe item's id (its title when it has none).
titleStringrequiredThe item's name.
subtitleStringrequiredA line under the name; empty for none.
priceStringrequiredThe price as shown, with its suffix ("$12/mo"); empty for none.
factsList<String>requiredUp to three short facts.
imageStringrequiredThe picture's address; empty for none.
unavailableboolrequiredDraw it greyed out and not choosable.
selectLabelStringrequiredThe row's button words ("Choose", "Select").

Returns — a new WidgetRow.

Errors — none.

const row = WidgetRow(id: 'mug-blue', title: 'Blue Mug', subtitle: 'Stoneware', price: '\$12', facts: ['350 ml'], image: 'https://www.graine.ai/samples/item.png', unavailable: false, selectLabel: 'Choose');

WidgetChip

Read it to draw one chip: a choice or a quick action.

class WidgetChip

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final String idStringThe choice's id (its label when it has none).
final String labelStringThe chip's words.

Its functions: WidgetChip — each with its parameters, what it returns, the errors it raises and an example.

WidgetChip.WidgetChip

Use it to build a chip by hand for a preview.

const WidgetChip(this.id, this.label)
ParameterTypeDefaultMeaning
idStringrequiredThe choice's id (its label when it has none).
labelStringrequiredThe chip's words.

Returns — a new WidgetChip.

Errors — none.

const chip = WidgetChip('morning', 'Morning');

WidgetDay

Read it to draw one day of a time-slot component.

class WidgetDay

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final String labelStringThe day as shown ("Tomorrow", "Thu 16 Oct").
final List<String> slotsList<String>Its times as shown, up to eight.

Its functions: WidgetDay — each with its parameters, what it returns, the errors it raises and an example.

WidgetDay.WidgetDay

Use it to build a day by hand for a preview.

const WidgetDay(this.label, this.slots)
ParameterTypeDefaultMeaning
labelStringrequiredThe day as shown ("Tomorrow", "Thu 16 Oct").
slotsList<String>requiredIts times as shown, up to eight.

Returns — a new WidgetDay.

Errors — none.

const day = WidgetDay('Thu 16 Oct', ['5:00 PM', '6:00 PM']);

WidgetPending

Read it to draw the loader that stands where a component is about to appear.

class WidgetPending

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final String idStringThe coming component's id, when known.
final String kindStringIts kind, when known — to draw a fitting placeholder.

Its functions: WidgetPending — each with its parameters, what it returns, the errors it raises and an example.

WidgetPending.WidgetPending

Use it to build a loading component by hand for a preview.

const WidgetPending(this.id, this.kind)
ParameterTypeDefaultMeaning
idStringrequiredThe coming component's id, when known.
kindStringrequiredIts kind, when known — to draw a fitting placeholder.

Returns — a new WidgetPending.

Errors — none.

const loading = WidgetPending('w_1', 'product_list');

WidgetAnswered

Read it to draw the line the customer's answer leaves on screen.

class WidgetAnswered

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final GraineWidget widgetGraineWidgetThe component that was answered.
final Map<String, Object?> dataMap<String, Object?>The answer's data.
final String summaryStringThe answer's sentence.

Its functions: WidgetAnswered — each with its parameters, what it returns, the errors it raises and an example.

WidgetAnswered.WidgetAnswered

Use it to build an answered line by hand for a preview.

const WidgetAnswered(this.widget, this.data, this.summary)
ParameterTypeDefaultMeaning
widgetGraineWidgetrequiredThe component that was answered.
dataMap<String, Object?>requiredThe answer's data.
summaryStringrequiredThe answer's sentence.

Returns — a new WidgetAnswered.

Errors — none.

final widget = GraineWidget.adopt({'widget_id': 'w_1', 'kind': 'product_list', 'data': {'items': []}}, 'https://www.graine.ai');
final line = widget == null ? null : WidgetAnswered(widget, {'id': 'mug-blue', 'title': 'Blue Mug'}, "I'll take a look at Blue Mug.");

WidgetSubmission

Read it as what to send for an answer, and the action of yours to run first.

class WidgetSubmission

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final String widgetIdStringThe component answered.
final Map<String, Object?> dataMap<String, Object?>The answer's data.
final String summaryStringThe answer's sentence.
final SelectCall? actionSelectCall?An action of yours to run first; null for none.

Its functions: WidgetSubmission — each with its parameters, what it returns, the errors it raises and an example.

WidgetSubmission.WidgetSubmission

Use it to build a submission by hand for a test.

const WidgetSubmission(this.widgetId, this.data, this.summary, this.action)
ParameterTypeDefaultMeaning
widgetIdStringrequiredThe component answered.
dataMap<String, Object?>requiredThe answer's data.
summaryStringrequiredThe answer's sentence.
actionSelectCall?requiredAn action of yours to run first; null for none.

Returns — a new WidgetSubmission.

Errors — none.

const submission = WidgetSubmission('w_1', {'id': 'mug-blue'}, "I'll take a look at Blue Mug.", null);

WidgetDismissal

Read it as what the agent is quietly told when the customer closes a component without choosing.

class WidgetDismissal

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final String toolNameStringThe component's id.
final String kindStringIts kind.
final String titleStringIts title.

Its functions: WidgetDismissal, eventData — each with its parameters, what it returns, the errors it raises and an example.

WidgetDismissal.WidgetDismissal

Use it to build a dismissal by hand for a test.

const WidgetDismissal(this.toolName, this.kind, this.title)
ParameterTypeDefaultMeaning
toolNameStringrequiredThe component's id.
kindStringrequiredIts kind.
titleStringrequiredIts title.

Returns — a new WidgetDismissal.

Errors — none.

const dismissal = WidgetDismissal('w_1', 'choices', 'Pick a time');

WidgetDismissal.eventData

Use it to get the event's data as it is sent.

Map<String, Object?> eventData()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

const dismissal = WidgetDismissal('w_1', 'choices', 'Pick a time');
final data = dismissal.eventData();

WidgetFlow

Use it to run a component's lifecycle yourself — loading, shown, answered, closed — with time passed in.

class WidgetFlow

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final String baseUrlStringThe agent's address, for pictures given as paths.
GraineWidget? get widgetGraineWidget?The component on screen, or null.
WidgetAnswered? get answeredWidgetAnswered?The last answer, kept on screen as one line; or null.
WidgetPending? get loadingWidgetPending?A component on its way, or null.
bool get awaitingReplyboolTrue after the customer answers and before the agent begins its reply.
GraineWidget? get closedGraineWidget?The most recent component that was closed unanswered, so it can be offered with a "show it again".

Its functions: WidgetFlow, seed, onPending, onWidget, onAgentStarted, submit, dismiss, reopen, reset, tick, nextDeadline — each with its parameters, what it returns, the errors it raises and an example.

WidgetFlow.WidgetFlow

Use it to make a lifecycle for components from one agent.

WidgetFlow(this.baseUrl)
ParameterTypeDefaultMeaning
baseUrlStringrequiredThe agent's address, for pictures given as paths.

Returns — a new WidgetFlow.

Errors — none.

final result = WidgetFlow('https://www.graine.ai');

WidgetFlow.seed

Call it on mount with a component drawn while nothing was listening.

void seed(Map<String, Object?>? raw)

Picks up, when mounted, a component that was drawn while nothing was listening.

ParameterTypeDefaultMeaning
rawMap<String, Object?>?requiredThe component waiting to be drawn (GraineInApp.pendingWidget).

Returns — nothing; it returns when done.

Errors — none.

final flow = WidgetFlow('https://www.graine.ai');
flow.seed(GraineInApp.instance.pendingWidget);   // a component still waiting, drawn by a view mounted later

WidgetFlow.onPending

Call it when a component is on its way.

void onPending(Map<String, Object?>? hint, int now)

A component is on its way.

ParameterTypeDefaultMeaning
hintMap<String, Object?>?requiredWhat is known of the coming component.
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — nothing; it returns when done.

Errors — none.

final flow = WidgetFlow('https://www.graine.ai');
flow.onPending({'widget_id': 'w_1', 'kind': 'choices'}, DateTime.now().millisecondsSinceEpoch);

WidgetFlow.onWidget

Call it when the agent drew a component.

bool onWidget(Map<String, Object?>? raw, int now)

The agent drew one.

ParameterTypeDefaultMeaning
rawMap<String, Object?>?requiredThe component as Graine sent it.
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — bool. Example value: true.

Errors — none.

final flow = WidgetFlow('https://www.graine.ai');
final shown = flow.onWidget({'widget_id': 'w_1', 'kind': 'choices', 'data': {'options': ['Morning', 'Evening']}}, DateTime.now().millisecondsSinceEpoch);

WidgetFlow.onAgentStarted

Call it when the agent starts answering.

void onAgentStarted()

Marks that the agent has started to speak — a caption or a component arrived — so the view no longer says it is waiting.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final flow = WidgetFlow('https://www.graine.ai');
flow.onAgentStarted();

WidgetFlow.submit

Call it when the customer answered.

WidgetSubmission? submit(Map<String, Object?> data, String summary, int now)

The customer answered.

ParameterTypeDefaultMeaning
dataMap<String, Object?>requiredThe answer's data.
summaryStringrequiredA short summary.
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — WidgetSubmission? — its fields: WidgetSubmission. Example value: null.

Errors — none.

final flow = WidgetFlow('https://www.graine.ai');
final now = DateTime.now().millisecondsSinceEpoch;
flow.onWidget({'widget_id': 'w_1', 'kind': 'choices', 'data': {'options': ['Morning', 'Evening']}}, now);
final answer = WidgetContent(flow.widget!).pick(0);
final submission = flow.submit(answer.data, answer.summary, now);

WidgetFlow.dismiss

Call it when the customer closed the component.

WidgetDismissal? dismiss()

The component was closed with no choice made, or "Clear" was pressed on the answered line.

Parameters — none.

Returns — WidgetDismissal? — its fields: WidgetDismissal. Example value: null.

Errors — none.

final flow = WidgetFlow('https://www.graine.ai');
final dismissal = flow.dismiss();   // null when nothing was on screen

WidgetFlow.reopen

Call it to bring back the component the customer closed.

bool reopen()

Restores the most recently closed component in the same state it was in.

Parameters — none.

Returns — bool. Example value: true.

Errors — none.

final flow = WidgetFlow('https://www.graine.ai');
final back = flow.reopen();

WidgetFlow.reset

Call it to forget everything.

void reset()

Use it when a call ends or another begins; nothing from the previous call should stay on screen.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final flow = WidgetFlow('https://www.graine.ai');
flow.reset();

WidgetFlow.tick

Call it at nextDeadline to move the lifecycle on.

bool tick(int now)

Apply every deadline that has passed.

ParameterTypeDefaultMeaning
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — bool. Example value: true.

Errors — none.

final flow = WidgetFlow('https://www.graine.ai');
final changed = flow.tick(DateTime.now().millisecondsSinceEpoch);

WidgetFlow.nextDeadline

Use it to know when to call tick next.

int? nextDeadline()

When tick next has something to do, or null.

Parameters — none.

Returns — int?. Example value: 3.

Errors — none.

final flow = WidgetFlow('https://www.graine.ai');
final due = flow.nextDeadline();   // null when nothing is waiting

WidgetStyle

Read it as how the dashboard asked a component to look.

class WidgetStyle

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final String? presetString?gallery, cards, list, spotlight or compact.
final String? imageSizeString?small, medium or large.
final String? densityString?comfortable or compact.
final String? buttonString?filled, outline or row.
final bool? factsbool?Show the facts under each item.
final int? columnsint?1 or 2 (gallery and cards).
final String? surfaceString?auto, screen, sheet or card.
final int? radiusint?Corner radius, 0–24.
final String? accentString?#RRGGBB over the design's accent.
final String? platformString?auto, ios, material or oneui.
final String? selectString?one or many.
final int? maxSelectint?The most items when choosing several, 2–8.

Its functions: WidgetStyle, toMap — each with its parameters, what it returns, the errors it raises and an example.

WidgetStyle.WidgetStyle

Use it to build a component's style by hand for a preview.

const WidgetStyle({ this.preset, this.imageSize, this.density, this.button, this.facts, this.columns, this.surface, this.radius, this.accent, this.platform, this.select, this.maxSelect})
ParameterTypeDefaultMeaning
presetString?optionalgallery, cards, list, spotlight or compact.
imageSizeString?optionalsmall, medium or large.
densityString?optionalcomfortable or compact.
buttonString?optionalfilled, outline or row.
factsbool?optionalShow the facts under each item.
columnsint?optional1 or 2 (gallery and cards).
surfaceString?optionalauto, screen, sheet or card.
radiusint?optionalCorner radius, 0–24.
accentString?optional#RRGGBB over the design's accent.
platformString?optionalauto, ios, material or oneui.
selectString?optionalone or many.
maxSelectint?optionalThe most items when choosing several, 2–8.

Returns — a new WidgetStyle.

Errors — none.

final result = WidgetStyle();

WidgetStyle.toMap

Use it to turn a style back into its JSON.

Map<String, Object?> toMap()

The keys that are set, as JSON.

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final map = Widgets.normaliseStyle({'preset': 'compact', 'columns': 2}).toMap();

WidgetLook

Read it to draw a component: its look with every default filled in.

class WidgetLook

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final String presetStringThe preset drawn.
final String imageSizeStringThe picture size drawn.
final String densityStringThe density drawn.
final String buttonStringThe button style drawn.
final bool factsboolWhether facts are drawn.
final int columnsint1 or 2 — two only for the gallery and cards presets.
final String surfaceStringThe surface to use, with "auto" already turned into the right surface for the kind.
final int? radiusint?The corner radius; null: the design's.
final String? accentString?#RRGGBB; null: the design's accent.
final String platformStringThe platform as picked; the renderer resolves "auto", because it knows which phone it is on.
final String selectStringone or many.
final int maxSelectintThe most items when choosing several.

Its functions: WidgetLook, toMap — each with its parameters, what it returns, the errors it raises and an example.

WidgetLook.WidgetLook

Use it to build a look by hand for a preview.

const WidgetLook({ required this.preset, required this.imageSize, required this.density, required this.button, required this.facts, required this.columns, required this.surface, this.radius, this.accent, required this.platform, required this.select, required this.maxSelect})
ParameterTypeDefaultMeaning
presetStringrequiredThe preset drawn.
imageSizeStringrequiredThe picture size drawn.
densityStringrequiredThe density drawn.
buttonStringrequiredThe button style drawn.
factsboolrequiredWhether facts are drawn.
columnsintrequired1 or 2 — two only for the gallery and cards presets.
surfaceStringrequiredThe surface to use, with "auto" already turned into the right surface for the kind.
radiusint?optionalThe corner radius; null: the design's.
accentString?optional#RRGGBB; null: the design's accent.
platformStringrequiredThe platform as picked; the renderer resolves "auto", because it knows which phone it is on.
selectStringrequiredone or many.
maxSelectintrequiredThe most items when choosing several.

Returns — a new WidgetLook.

Errors — none.

const look = WidgetLook(preset: 'cards', imageSize: 'medium', density: 'comfortable', button: 'filled', facts: true, columns: 1, surface: 'screen', platform: 'auto', select: 'one', maxSelect: 3);

WidgetLook.toMap

Use it to turn a look into JSON.

Map<String, Object?> toMap()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final map = Widgets.resolveLook(null, 'product_list').toMap();

PlatformGrammar

Read it to draw a component in the shapes people on that phone already know.

class PlatformGrammar

What to expect — Fixed per platform (Widgets.grammar): the shapes change, the colours stay the brand's.

PropertyTypeMeaning
final int radiusintThe card's corner radius, used only for an explicit platform.
final int chipRadiusintA chip's corner radius.
final int buttonRadiusintA button's corner radius.
final bool pillButtonsboolDraw buttons as pills.
final int separatorInsetintHow far row separators are inset.
final int grabberWidthintA sheet's handle width.
final int grabberHeightintA sheet's handle height.
final int titleWeightintCSS weight: 500, 600 or 700.
final int padBoostintExtra padding inside cards.
final bool chipOutlinedboolDraw chips outlined rather than filled.

Its functions: PlatformGrammar — each with its parameters, what it returns, the errors it raises and an example.

PlatformGrammar.PlatformGrammar

Use it to build a platform's shapes by hand.

const PlatformGrammar({ required this.radius, required this.chipRadius, required this.buttonRadius, required this.pillButtons, required this.separatorInset, required this.grabberWidth, required this.grabberHeight, required this.titleWeight, required this.padBoost, required this.chipOutlined})
ParameterTypeDefaultMeaning
radiusintrequiredThe card's corner radius, used only for an explicit platform.
chipRadiusintrequiredA chip's corner radius.
buttonRadiusintrequiredA button's corner radius.
pillButtonsboolrequiredDraw buttons as pills.
separatorInsetintrequiredHow far row separators are inset.
grabberWidthintrequiredA sheet's handle width.
grabberHeightintrequiredA sheet's handle height.
titleWeightintrequiredCSS weight: 500, 600 or 700.
padBoostintrequiredExtra padding inside cards.
chipOutlinedboolrequiredDraw chips outlined rather than filled.

Returns — a new PlatformGrammar.

Errors — none.

final ios = Widgets.grammar['ios']!;   // or build one: PlatformGrammar(radius: 12, chipRadius: 8, …)

WidgetSelectAction

Read it as what your app should do when the customer taps a component's item, beyond telling the agent.

class WidgetSelectAction

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.

PropertyTypeMeaning
final String? pathString?A screen of your app to open (for the open_screen action), with placeholders such as {id} filled from the tapped item.
final String? actionString?The action of yours to run, by its registered name.
final Map<String, String>? argsMap<String, String>?Its arguments, each a template filled from the tapped item.
final String? sayString?What to tell the agent instead of the default sentence.
bool get isEmptyboolTrue when the component names no action and no path: a tap only answers the agent.

Its functions: WidgetSelectAction, toMap — each with its parameters, what it returns, the errors it raises and an example.

WidgetSelectAction.WidgetSelectAction

Use it to build a tap action by hand for a test.

const WidgetSelectAction({this.path, this.action, this.args, this.say})
ParameterTypeDefaultMeaning
pathString?optionalA screen of your app to open (for the open_screen action), with placeholders such as {id} filled from the tapped item.
actionString?optionalThe action of yours to run, by its registered name.
argsMap<String, String>?optionalIts arguments, each a template filled from the tapped item.
sayString?optionalWhat to tell the agent instead of the default sentence.

Returns — a new WidgetSelectAction.

Errors — none.

final result = WidgetSelectAction();

WidgetSelectAction.toMap

Use it to turn a tap action into JSON.

Map<String, Object?> toMap()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final map = Widgets.normaliseSelectAction({'path': '/products/{id}'}).toMap();

SelectCall

Read it as the action of yours to run for one tap, and its arguments.

class SelectCall

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.

PropertyTypeMeaning
final String nameStringThe action to run.
final Map<String, String> argsMap<String, String>Its arguments, filled in.

Its functions: SelectCall — each with its parameters, what it returns, the errors it raises and an example.

SelectCall.SelectCall

Use it to build a tap's action by hand for a test.

const SelectCall(this.name, this.args)
ParameterTypeDefaultMeaning
nameStringrequiredThe action to run.
argsMap<String, String>requiredIts arguments, filled in.

Returns — a new SelectCall.

Errors — none.

const call = SelectCall('open_screen', {'path': '/products/mug-blue'});

Widgets

Use it to apply the component rules every Graine SDK shares.

abstract final class Widgets

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
static const List<String> presetsList<String>gallery, cards, list, spotlight, compact.
static const List<String> imageSizesList<String>small, medium, large.
static const List<String> densitiesList<String>comfortable, compact.
static const List<String> buttonsList<String>filled, outline, row.
static const List<String> surfaceChoicesList<String>auto, screen, sheet, card.
static const List<String> platformsList<String>auto, ios, material, oneui.
static const List<String> selectsList<String>one, many.
static const int maxSelectDefaultintThe most items when choosing several, unless the component says: 3.
static const String defaultPresetStringThe preset when none is set: 'cards'.
static const String navigateActionStringThe single handler an app registers to let the dashboard open any of its screens.
static const int selectArgMaxintThe most arguments a tap action keeps: 10.
static const int selectValueMaxintThe most characters a tap action's path, argument or sentence keeps: 200.
static const int minLoaderMsintThe minimum time a shown loader stays up, so that it looks like loading and not a glitch.
static const int pendingWidgetMsintThe longest a "component coming" loader may stay up while nothing arrives.
static const int awaitingReplyMsintThe longest the answered line keeps saying "thinking" while the agent has yet to start.
static const Map<String, PlatformGrammar> grammarMap<String, PlatformGrammar>The shapes for ios, material and oneui.

Its functions: surface, normaliseStyle, resolveLook, resolvePlatform, manySummary, normaliseSelectAction, fillSelectTemplate, fillSelectArgs, selectCall, resolveImages — each with its parameters, what it returns, the errors it raises and an example.

Widgets.surface

Use it to know where a kind of component is drawn by default.

static String surface(String? kind)

Picks the surface for a component — one of "screen", "sheet" or "card".

ParameterTypeDefaultMeaning
kindString?requiredThe component's kind (product_list, choices, …).

Returns — String. Example value: 'screen'.

Errors — none.

final where = Widgets.surface('product_list');

Widgets.normaliseStyle

Use it to read a component's style, keeping only the values the SDK knows.

static WidgetStyle normaliseStyle(Object? raw)

Only the keys and values the renderers read.

ParameterTypeDefaultMeaning
rawObject?requiredThe component's style as sent; anything unknown is dropped.

Returns — WidgetStyle — its fields: WidgetStyle.

Errors — none.

final style = Widgets.normaliseStyle({'preset': 'compact', 'columns': 2});

Widgets.resolveLook

Use it to get a component's look with every default filled in.

static WidgetLook resolveLook(WidgetStyle? style, String? kind)
ParameterTypeDefaultMeaning
styleWidgetStyle?requiredThe component's style; null for none.
kindString?requiredThe component's kind, which decides where it is drawn by default.

Returns — WidgetLook — its fields: WidgetLook.

Errors — none.

final widget = GraineInApp.instance.components.widget;
final look = widget == null ? null : Widgets.resolveLook(widget.style, widget.kind);

Widgets.resolvePlatform

Use it to know which phone's shapes to draw a component in.

static String resolvePlatform(String? platform, String? os, [String? manufacturer])

Resolves "auto" to the phone's native design language: Material on Android, One UI on a Samsung, and Apple's on iOS as well as on anything it does not recognise.

ParameterTypeDefaultMeaning
platformString?requiredThe style's platform: auto, ios, material or oneui.
osString?requiredThe phone's system: android or ios.
manufacturerString?optionalThe phone's maker, on Android; a Samsung phone gets One UI's shapes.

Returns — String. Example value: 'oneui'.

Errors — none.

final platform = Widgets.resolvePlatform('auto', 'android', 'samsung');

Widgets.manySummary

Use it to write the sentence for several choices.

static String manySummary(List<String> titles, [String label = 'Compare'])

Builds the sentence the agent hears after a multiple choice, which is also the user's bubble in a text chat: the button's label exactly as written, followed by the names ("Taken: Morning and Evening").

ParameterTypeDefaultMeaning
titlesList<String>requiredThe names of the chosen items, in order.
labelString'Compare'The button's words, as written ("Compare"); empty for the names alone.

Returns — String. Example value: 'Taken: Morning and Evening'.

Errors — none.

final sentence = Widgets.manySummary(['Morning', 'Evening'], 'Taken');

Widgets.normaliseSelectAction

Use it to read a component's tap action, keeping only what is valid.

static WidgetSelectAction normaliseSelectAction(Object? raw)
ParameterTypeDefaultMeaning
rawObject?requiredThe component's tap action as sent.

Returns — WidgetSelectAction — its fields: WidgetSelectAction.

Errors — none.

final onSelect = Widgets.normaliseSelectAction({'path': '/products/{id}'});

Widgets.fillSelectTemplate

Use it to fill a tap action's placeholders from the tapped item.

static String fillSelectTemplate(String template, Map<String, Object?> item)

Fills {id}, {title}… from the tapped item. A placeholder it does not know is replaced with nothing rather than left as written.

ParameterTypeDefaultMeaning
templateStringrequiredA path or argument with {id}, {title}… placeholders.
itemMap<String, Object?>requiredThe tapped item's data.

Returns — String. Example value: '/products/mug-blue'.

Errors — none.

final path = Widgets.fillSelectTemplate('/products/{id}', {'id': 'mug-blue', 'title': 'Blue Mug'});

Widgets.fillSelectArgs

Use it to fill every argument of a tap action from the tapped item.

static Map<String, String> fillSelectArgs(Map<String, String>? args, Map<String, Object?> item)
ParameterTypeDefaultMeaning
argsMap<String, String>?requiredThe tap action's arguments; null for none.
itemMap<String, Object?>requiredThe tapped item's data.

Returns — Map<String, String>. Example value: {'sku': 'mug-blue'}.

Errors — none.

final args = Widgets.fillSelectArgs({'sku': '{id}'}, {'id': 'mug-blue'});

Widgets.selectCall

Use it to get the action of yours to run for a tap.

static SelectCall? selectCall(WidgetSelectAction? onSelect, Map<String, Object?> item)

Works out which action one tap runs: an explicit action takes priority, otherwise a path is sent to navigateAction; in both cases the path is passed along as an argument.

ParameterTypeDefaultMeaning
onSelectWidgetSelectAction?requiredThe component's tap action; null for none.
itemMap<String, Object?>requiredThe tapped item's data.

Returns — SelectCall? — its fields: SelectCall. Example value: null.

Errors — none.

final onSelect = Widgets.normaliseSelectAction({'path': '/products/{id}'});
final call = Widgets.selectCall(onSelect, {'id': 'mug-blue', 'title': 'Blue Mug'});

Widgets.resolveImages

Use it to turn a component's pictures that start with / into full addresses on the agent's site.

static Map<String, Object?> resolveImages(Map<String, Object?> data, String baseUrl)

A root-relative picture (/samples/item.png) is resolved against the agent's origin — the one place it could have come from.

ParameterTypeDefaultMeaning
dataMap<String, Object?>requiredThe component's data.
baseUrlStringrequiredThe agent's address (GraineConfig.baseUrl); empty leaves the data as it is.

Returns — Map<String, Object?>. Example value: {'imageUrl': 'https://www.graine.ai/samples/item.png'}.

Errors — none.

final data = Widgets.resolveImages({'imageUrl': '/samples/item.png'}, 'https://www.graine.ai');

Calls the user places

WhatsApp-style calls with the agent, on the native call engine the package carries: CallKit on iOS, a call notification and call screen on Android, the call going on with the screen off and the platform's echo canceller on the audio. Configure GraineCalls.instance once at launch, attach your agent so the call knows the customer, the screen and your actions, and place a call from a button. The in-app call the voice bar places is GraineCallState, at the end of this section.

A native call carries its audio over the WebSocket — or over WebRTC, in a build that has it (WebRTC calls). GraineCallsOptions.callTransport chooses, for calls in both directions (the ones the customer places and the ones Graine rings), and each call's snapshot says which one carried it: transport, and transportFallbackReason for a call that wanted WebRTC and runs on the WebSocket.

What 0.8.0's native engine does for every call:

  • It starts at once on the agent's configuration the device already keeps (this launch's, or the last one), and refreshes that configuration behind the call instead of waiting for the network first.
  • iOS: one deadline before the conversation opens. Fetching the configuration, the ticket and the connection share one 20-second deadline; past it the call fails with "The call could not connect within 20 seconds." (Before, each step's own timeout could add up to most of a minute on "Connecting".) The connection itself must still open within 15 seconds of the ticket, which can come first: "The conversation did not open within 15 seconds."
  • iOS: a call is connected only 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; still nothing 3 seconds later, the call fails with "The call's audio could not start." instead of staying open and silent.
  • Android: a dead connection ends the call. A live connection answers every keep-alive; when nothing at all arrives for 50 seconds the call ends — the Android SDK words it "Nothing arrived for 50 seconds: the connection was lost." Before the conversation opens, Android's limit stays 15 seconds ("The call could not connect within 15 seconds.").
  • Android: a slow network no longer freezes a call. Sending never waits for the network: at most 300 ms of the customer's audio is kept while the line falls behind (the oldest goes first), and hanging up lets go of the microphone at once.

Where your app sees these: a call that never connected reaches outcomes as GraineCallOutcomeKind.failed with the sentence in reason, and GraineCalls.instance.value.error keeps it until the next call starts; a call that was connected and then lost reaches outcomes as ended with reason 'failed'.

GraineCalls

Use it to place native calls with the agent and to draw them: configure it at launch, attach your agent, call startCall from a button, and listen to what became of each call.

class GraineCalls extends ChangeNotifier implements ValueListenable<GraineCallSnapshot>

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
static final GraineCalls instanceGraineCallsThe one instance.
bool get isSupportedboolNative calls run on Android and iOS.
Stream<GraineCallOutcome> get outcomesStream<GraineCallOutcome>The result of each call, for you to pass on to your server. Outcomes from before anyone listened (a call that rang and ended while the app was not running) reach the first listener, oldest first — the last 20.
Stream<GraineIncomingCall> get incomingCallsStream<GraineIncomingCall>Fires when a call from an agent begins ringing, for your own ringing UI or for analytics.

Its functions: configure, attach, startCall, end, setMuted, setSpeaker, resetForTesting, registerPushToken, enableVoIPPushes, handlePush, answer, decline, callPermissions, requestCallPermissions, openCallSettings, callSetupProblems — each with its parameters, what it returns, the errors it raises and an example.

GraineCalls.configure

Call it once at launch, before a push can arrive, to set how calls ring and what they show.

Future<void> configure(GraineCallsOptions options)

Hands the options to the native call engine, which keeps them — so a push that wakes a closed app rings before any Dart has run. Every agent the app configured under a name goes with them (each with its own key), so startCall(agent:) can call it and Graine's rings for it ring as it; an agent configured later reaches the engine then. Who is signed in reaches the engine too. Call it once at launch, after GraineInApp.init. On a platform other than Android and iOS it does nothing.

ParameterTypeDefaultMeaning
optionsGraineCallsOptionsrequiredHow calls ring and what they show — see GraineCallsOptions.

Returns — Future<void>, which completes once the engine has the options and the current call's state.

Errors — ArgumentError for an empty publishableKey or displayName; a PlatformException with the code invalid_options, naming the option, for anything else the engine refuses.

await GraineCalls.instance.configure(const GraineCallsOptions(
  publishableKey: 'pk_live_…',
  displayName: 'Acme Assistant',
));

GraineCallsOptions

Use it to say how native calls ring and what they show: the agent's key, the name on calls, the ring and who draws the call screen.

class GraineCallsOptions

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.

PropertyTypeMeaning
final String publishableKeyStringThe default agent's publishable key, pk_live_… (Agent → Embed & Widgets → Install) — public by design. Agents the app configured under a name go with it, each with its own key.
final String displayNameStringThe name on calls the customer places, and on a ring whose push names nobody. A push's caller_name wins for its own call — so a call Graine rings shows the agent's call name from Calls & scheduling.
final String baseUrlStringWhere Graine is: 'https://www.graine.ai' unless your workspace is self-hosted. Use the same value as GraineConfig.baseUrl.
final String? ringtoneSoundString?The ring: on iOS a sound file of 30 seconds or less in the app bundle, on Android a res/raw resource name. Null (the default): the ring the brand chose in the call screen design — the phone's own unless it chose one. Set, it replaces the brand's ring.
final Duration ringTimeoutDurationHow long a ring from your own server lasts before it is missed (default 30 seconds). A ring Graine sends lasts the agent's ring time instead (CallsSetting.ringTimeoutSeconds, 45 seconds by default).
final String? appVersionString?The app build that placed the call, recorded on the call record.
final GraineCallScreen callScreenGraineCallScreenWho draws the call while the app is open: GraineCallScreen.sdk (the default) — the SDK's call screen in the brand's call screen design, a full-screen call activity on Android and a full-screen view over your app on iOS — or GraineCallScreen.app, your own screen drawn from GraineCalls.instance in the design's call colours (GraineInApp.instance.tokens?.call; with several agents, those of the agent the call is with). The system's call screen always appears on a locked iPhone.
final GraineCallTransport callTransportGraineCallTransportHow calls carry their audio, in both directions — the calls the customer places and the ones Graine rings: GraineCallTransport.auto (the default) takes WebRTC in a build that has it (WebRTC calls), else the WebSocket; websocket always the WebSocket; webrtc WebRTC wherever it can carry the call. A call WebRTC cannot set up continues on the WebSocket inside the same call, and the agent's In-app call audio setting can move every install to the WebSocket without a release. New in 0.9.0.
final bool debugboolTurns on the native SDK's debug log in Xcode's console or logcat, showing how each call connected and what its audio did, so you can work out why a call did not connect. Off by default; turn it on in debug builds. Read once at launch: a change takes effect at the next launch.
final String? androidSmallIconString?Android only: the name of a monochrome drawable used in the call notifications.
final String? iosIconTemplateString?iOS only: the name of a template image in the app bundle, shown on the system call screen.
final bool iosIncludesCallsInRecentsbooliOS only: show these calls in the Recents list of the Phone app.

Its functions: GraineCallsOptions, toMap — each with its parameters, what it returns, the errors it raises and an example.

GraineCallsOptions.GraineCallsOptions

Use it to build the options you pass to GraineCalls.instance.configure.

const GraineCallsOptions({ required this.publishableKey, required this.displayName, this.baseUrl = 'https://www.graine.ai', this.ringtoneSound, this.ringTimeout = const Duration(seconds: 30), this.appVersion, this.callScreen = GraineCallScreen.sdk, this.callTransport = GraineCallTransport.auto, this.debug = false, this.androidSmallIcon, this.iosIconTemplate, this.iosIncludesCallsInRecents = false})
ParameterTypeDefaultMeaning
publishableKeyStringrequiredThe default agent's publishable key, pk_live_… (Agent → Embed & Widgets → Install) — public by design. Agents the app configured under a name go with it, each with its own key.
displayNameStringrequiredThe name on calls the customer places, and on a ring whose push names nobody. A push's caller_name wins for its own call — so a call Graine rings shows the agent's call name from Calls & scheduling.
baseUrlString'https://www.graine.ai'Where Graine is: 'https://www.graine.ai' unless your workspace is self-hosted. Use the same value as GraineConfig.baseUrl.
ringtoneSoundString?optionalThe ring: on iOS a sound file of 30 seconds or less in the app bundle, on Android a res/raw resource name. Null (the default): the ring the brand chose in the call screen design — the phone's own unless it chose one. Set, it replaces the brand's ring.
ringTimeoutDurationconst Duration(seconds: 30)How long a ring from your own server lasts before it is missed (default 30 seconds). A ring Graine sends lasts the agent's ring time instead (CallsSetting.ringTimeoutSeconds, 45 seconds by default).
appVersionString?optionalThe app build that placed the call, recorded on the call record.
callScreenGraineCallScreenGraineCallScreen.sdkWho draws the call while the app is open: GraineCallScreen.sdk (the default) — the SDK's call screen in the brand's call screen design, a full-screen call activity on Android and a full-screen view over your app on iOS — or GraineCallScreen.app, your own screen drawn from GraineCalls.instance in the design's call colours (GraineInApp.instance.tokens?.call; with several agents, those of the agent the call is with). The system's call screen always appears on a locked iPhone.
callTransportGraineCallTransportGraineCallTransport.autoHow calls carry their audio, in both directions: auto takes WebRTC in a build that has it (WebRTC calls), else the WebSocket; websocket always the WebSocket; webrtc WebRTC wherever it can carry the call. A call WebRTC cannot set up continues on the WebSocket inside the same call.
debugboolfalseTurns on the native SDK's debug log in Xcode's console or logcat, showing how each call connected and what its audio did, so you can work out why a call did not connect. Off by default; turn it on in debug builds. Read once at launch: a change takes effect at the next launch.
androidSmallIconString?optionalAndroid only: the name of a monochrome drawable used in the call notifications.
iosIconTemplateString?optionaliOS only: the name of a template image in the app bundle, shown on the system call screen.
iosIncludesCallsInRecentsboolfalseiOS only: show these calls in the Recents list of the Phone app.

Returns — a new GraineCallsOptions.

Errors — none.

final result = GraineCallsOptions(publishableKey: 'pk_live_…', displayName: 'Acme Assistant');

GraineCallsOptions.toMap

Use it to see the options exactly as the native engine receives them — for a log or a test.

Map<String, Object?> toMap()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

const options = GraineCallsOptions(publishableKey: 'pk_live_…', displayName: 'Acme Assistant');
debugPrint(options.toMap().toString());

GraineCallScreen

Use it to choose who draws the call while your app is open: the SDK's call screen, or your own.

enum GraineCallScreen { sdk, app }

What to expect — Read once, from GraineCallsOptions.callScreen. A locked iPhone always shows the system's call screen, whichever you choose.

CaseMeaning
sdkThe SDK's call screen, drawn from the brand's call screen design — a full-screen call activity on Android, a full-screen view over your app on iOS. The default.
appYour own screen, drawn from GraineCalls.instance in the design's call colours (GraineInApp.instance.tokens?.call).

GraineCallTransport

Use it to choose how native calls carry their audio — WebRTC in a build that has it, or always the WebSocket — and to read which one carried a call.

enum GraineCallTransport

What to expect — Chosen once, in GraineCallsOptions.callTransport, for calls in both directions: the ones the customer places and the ones Graine rings. Whatever is chosen, a call WebRTC cannot set up continues on the WebSocket inside the same call, and the agent's In-app call audio setting set to Standard moves every install's calls to the WebSocket without an app release (WebRTC calls). Each call then reports what carried it in GraineCallSnapshot.transport — webrtc or websocket, never auto. Native calls only: the voice bar's in-app call is Transport. New in 0.9.0.

CaseMeaning
auto('auto')WebRTC when the app was built with it and the agent's In-app call audio allows it, else the WebSocket. The default.
websocket('websocket')Always the WebSocket.
webrtc('webrtc')WebRTC wherever it can carry the call; the WebSocket only when the build has no WebRTC, the agent's In-app call audio is Standard, or WebRTC cannot set up this call.
PropertyTypeMeaning
final String wireStringThe value as the native engine reads and reports it — the same word as the value's name.

Its functions: GraineCallTransport, fromWire — each with its parameters, what it returns, the errors it raises and an example.

GraineCallTransport.GraineCallTransport

The enum's constructor; use the values (GraineCallTransport.auto, …).

const GraineCallTransport(this.wire)
ParameterTypeDefaultMeaning
wireStringrequiredThe value as the native engine reads and reports it — the same word as the value's name.

Returns — nothing you call: an enum's constructor runs once for each value. Use the values; each carries its word in wire.

Errors — none.

const choice = GraineCallTransport.webrtc;
print(choice.wire);   // webrtc

GraineCallTransport.fromWire

Use it to read what carried a call from the native engine's word — for a test; the snapshot does this itself.

static GraineCallTransport? fromWire(Object? s)
ParameterTypeDefaultMeaning
sObject?requiredThe word the native engine reports for a call: 'webrtc' or 'websocket'.

Returns — GraineCallTransport? — webrtc or websocket; null for anything else, 'auto' included: it is a choice, never what carried a call. Example value: GraineCallTransport.webrtc.

Errors — none.

final carried = GraineCallTransport.fromWire('webrtc');   // GraineCallTransport.webrtc

GraineCalls.attach

Call it once after configure so native calls know who the customer is, the prompt variables, the screen and your actions — kept in step as they change.

void Function() attach([GraineInApp? graine])

Hands native calls what the app already tells the agent through graine — who the customer is, the prompt variables, the screen and the actions it registered — kept in step as they change, and answers the agent's actions on a call through the app's own handlers. With several agents, attach the default one: while a call is with another agent (startCall(agent:), or a ring for it), the engine holds that agent's variables and actions instead, and its actions run in that agent's handlers.

ParameterTypeDefaultMeaning
graineGraineInApp?optionalThe agent whose context native calls carry; the default agent when left out.

Returns — void Function() — call it to detach: the engine then holds no actions, and an action the agent asks for on a call is refused.

Errors — none. Before init, leaving out graine throws StateError (from GraineInApp.instance).

final detach = GraineCalls.instance.attach();

GraineCalls.startCall

Call it from a "Call" button to call the agent now.

Future<String?> startCall({String? reason, Map<String, Object?> variables = const <String, Object?>{}, String? agent})

The customer calls the agent, now, as a native call — the system's call UI on iOS (CallKit), the SDK's call screen (or yours) on Android. The agent is given who_called = "customer" and call_reason; variables are merged over the app's own and take precedence. The call starts at once on the agent's configuration the device already keeps and refreshes it behind the call. Before the conversation opens, iOS gives the whole connect one 20-second deadline and Android 15 seconds; a call that misses it ends failed — "The call could not connect within 20 seconds." on iOS (or, when the connection itself takes more than 15 seconds to open, "The conversation did not open within 15 seconds."), "The call could not connect within 15 seconds." on Android. On iOS a call whose audio never starts fails with "The call's audio could not start."; on Android a connected call on which nothing arrives for 50 seconds ends with reason failed. Follow it on GraineCalls.instance and outcomes.

ParameterTypeDefaultMeaning
reasonString?optionalWhy the customer is calling — the agent hears it as call_reason.
variablesMap<String, Object?>const <String, Object?>{}Prompt variables for this call, over the app's own; masked on the device before they leave it.
agentString?optionalWhich agent to call, by the name the app configured it under; the default agent when left out.

Returns — Future<String?> — the call's id, which every outcome carries; null when a call is already live (one at a time), on a platform other than Android and iOS, or in a build without the native engine. Example value: 'call_6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44'.

Errors — a PlatformException when the engine refuses to place it (for example before configure). The microphone is asked for by the SDK; refused, the call ends failed ("The microphone permission was denied.").

final callId = await GraineCalls.instance.startCall(reason: 'order_help', agent: 'support');

GraineCalls.end

Call it from your own call screen to hang up — or decline, while it rings.

Future<void> end()

Hangs up the live call — or declines it while it rings. On Android 0.8.0 the microphone is let go at once, and a hang-up the network cannot carry is forced within about a second. The outcome follows on outcomes (ended, reason local; or declined).

Parameters — none.

Returns — Future<void>, which completes once the engine has it.

Errors — none; with no call it does nothing.

await GraineCalls.instance.end();

GraineCalls.setMuted

Call it from your own call screen to stop, or resume, sending the customer's voice.

Future<void> setMuted(bool muted)

Stops, or resumes, sending the customer's voice without ending the call. The snapshot's muted follows.

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

Returns — Future<void>.

Errors — none; with no call it does nothing.

await GraineCalls.instance.setMuted(!GraineCalls.instance.value.muted);

GraineCalls.setSpeaker

Call it from your own call screen to switch between the loudspeaker and the earpiece or headset.

Future<void> setSpeaker(bool on)

Moves the call's audio to the loudspeaker, or back to the earpiece or a headset. Calls start on the earpiece, like a phone call. The snapshot's speakerOn follows.

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

Returns — Future<void>.

Errors — none.

await GraineCalls.instance.setSpeaker(true);

GraineCalls.resetForTesting

Call it between tests to forget everything the instance holds.

void resetForTesting()

For tests only (@visibleForTesting): forgets everything the instance holds — the listener, the call's state, outcomes and rings waiting for a listener, the token watchers, the attached agent and the options.

Parameters — none.

Returns — nothing.

Errors — none.

import 'package:flutter_test/flutter_test.dart';
 
tearDown(GraineCalls.instance.resetForTesting);

GraineCallSnapshot

Read it to draw your own call screen: where the call is, who it is with, the caption and the controls' state.

class GraineCallSnapshot

What to expect — GraineCalls.instance.value, replaced on every change (listen to GraineCalls.instance). status goes 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). While no call is live, each field holds its idle value. error keeps why the last call could not connect — "The call could not connect within 20 seconds.", "The conversation did not open within 15 seconds." or "The call's audio could not start." on iOS, "The call could not connect within 15 seconds." on Android — until the next call starts; a call that connected and was then lost leaves no error (its outcome says ended, failed).

PropertyTypeMeaning
static const GraineCallSnapshot idleGraineCallSnapshotThe state with no call: every field at its idle value.
final GraineNativeCallStatus statusGraineNativeCallStatusWhere the call is: idle, ringing, connecting, active or ended. Pick the call screen's state from it.
final String? callIdString?For a call your server rang, your server's id; for a call the customer placed, an id generated for it. Every outcome carries it.
final GraineCallDirection? directionGraineCallDirection?Who placed the call: incoming (a ring reached this phone) or outgoing (the customer called); null with no call.
final String? reasonString?The reason carried by the push, or the reason passed to startCall.
final String? callerNameString?The name the call shows.
final bool mutedboolTrue while the customer's voice is not being sent. Draw the mute button's state from it.
final bool speakerOnboolTrue when sound plays through the loudspeaker instead of the earpiece or a headset.
final bool agentSpeakingboolThe agent's voice is playing right now.
final GraineCallCaption? captionGraineCallCaption?The most recent line — the agent's words while being spoken, or the customer's while being heard.
final DateTime? connectedAtDateTime?The time the conversation began, for showing a call timer.
final String? errorString?Why the last call could not connect, in the engine's words; cleared when the next call starts.
final String? agentString?The agent the call is with, by the name the app configured it under ('default' for the default agent) — the one startCall named, or the one a ring was routed to by its push. Null with no call.
final GraineCallTransport? transportGraineCallTransport?What carries the call's audio: GraineCallTransport.webrtc or GraineCallTransport.websocket — for a call the customer placed and a call Graine rang alike. Read it once the call is active. Null with no call, before the way is decided, and from an engine that predates WebRTC. New in 0.9.0.
final String? transportFallbackReasonString?Why a call that wanted WebRTC runs on the WebSocket instead — one of the words below; null when it did not fall back. Read it once the call is active. New in 0.9.0.

Why a call is on the WebSocket — transportFallbackReason, set only for a call that wanted WebRTC and runs on the WebSocket. The first six come from a WebRTC setup that gave up before any audio flowed; the call went on, inside the same call:

ValueMeaning
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.
dashboardAndroid only: the agent's In-app call audio setting is Standard, so a call that would have taken WebRTC runs on the WebSocket. On iOS such a call simply runs on the WebSocket, with no reason.
unavailableAndroid only: the app asked for GraineCallTransport.webrtc in a build without WebRTC. On iOS such a call simply runs on the WebSocket, with no reason.

Its functions: GraineCallSnapshot, fromMap — each with its parameters, what it returns, the errors it raises and an example.

GraineCallSnapshot.GraineCallSnapshot

Use it to build a call's state by hand for a preview or a test.

const GraineCallSnapshot({ this.status = GraineNativeCallStatus.idle, this.callId, this.direction, this.reason, this.callerName, this.muted = false, this.speakerOn = false, this.agentSpeaking = false, this.caption, this.connectedAt, this.error, this.agent, this.transport, this.transportFallbackReason})
ParameterTypeDefaultMeaning
statusGraineNativeCallStatusGraineNativeCallStatus.idleWhere the call is: idle, ringing, connecting, active or ended. Pick the call screen's state from it.
callIdString?optionalFor a call your server rang, your server's id; for a call the customer placed, an id generated for it. Every outcome carries it.
directionGraineCallDirection?optionalWho placed the call: incoming (a ring reached this phone) or outgoing (the customer called); null with no call.
reasonString?optionalThe reason carried by the push, or the reason passed to startCall.
callerNameString?optionalThe name the call shows.
mutedboolfalseTrue while the customer's voice is not being sent. Draw the mute button's state from it.
speakerOnboolfalseTrue when sound plays through the loudspeaker instead of the earpiece or a headset.
agentSpeakingboolfalseThe agent's voice is playing right now.
captionGraineCallCaption?optionalThe most recent line — the agent's words while being spoken, or the customer's while being heard.
connectedAtDateTime?optionalThe time the conversation began, for showing a call timer.
errorString?optionalWhy the last call could not connect, in the engine's words; cleared when the next call starts.
agentString?optionalThe agent the call is with, by the name the app configured it under ('default' for the default agent) — the one startCall named, or the one a ring was routed to by its push. Null with no call.
transportGraineCallTransport?optionalWhat carries the call's audio: webrtc or websocket; null with no call.
transportFallbackReasonString?optionalWhy a call that wanted WebRTC runs on the WebSocket (no_answer, ice_timeout, …); null when it did not fall back.

Returns — a new GraineCallSnapshot.

Errors — none.

const preview = GraineCallSnapshot(status: GraineNativeCallStatus.active, callerName: 'Acme Assistant', agent: 'support');

GraineCallSnapshot.fromMap

Use it to read a call's state from the native engine's message — for a test; GraineCalls.instance does this itself.

factory GraineCallSnapshot.fromMap(Map<Object?, Object?>? m)

From the engine's snapshot.

ParameterTypeDefaultMeaning
mMap<Object?, Object?>?requiredThe call's state as the native engine sent it; null reads as no call.

Returns — GraineCallSnapshot — the call's state; anything missing reads as its idle value, and null as GraineCallSnapshot.idle.

Errors — none.

final snapshot = GraineCallSnapshot.fromMap({'state': 'active', 'callId': 'call_6f1c2a9e', 'direction': 'outgoing', 'muted': false, 'transport': 'webrtc'});

GraineNativeCallStatus

Read it to know where a native call is: idle, ringing, connecting, active or ended.

enum GraineNativeCallStatus

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

CaseMeaning
idle('idle')No call. The state between calls, and before the first one.
ringing('ringing')An agent's call is ringing on this phone and has not been answered.
connecting('connecting')The customer answered, or placed the call, and the conversation is being opened.
active('active')The call is connected and its audio runs: the customer and the agent can hear each other.
ended('ended')The call is over; idle follows once it has been reported.
PropertyTypeMeaning
final String wireStringThe status as the native engine words it — the same word as the value's name.
bool get isLiveboolRinging, connecting or connected.

Its functions: GraineNativeCallStatus, fromWire — each with its parameters, what it returns, the errors it raises and an example.

GraineNativeCallStatus.GraineNativeCallStatus

The enum's constructor; use the values (GraineNativeCallStatus.ringing, …).

const GraineNativeCallStatus(this.wire)
ParameterTypeDefaultMeaning
wireStringrequiredThe status as the native engine words it — the same word as the value's name.

Returns — nothing you call: an enum's constructor runs once for each value. Use the values; each carries its word in wire.

Errors — none.

const value = GraineNativeCallStatus.active;
print(value.wire);   // active

GraineNativeCallStatus.fromWire

Use it to read a call's status from its word.

static GraineNativeCallStatus fromWire(Object? s)
ParameterTypeDefaultMeaning
sObject?requiredThe status's word; anything else reads as idle.

Returns — GraineNativeCallStatus. Example value: GraineNativeCallStatus.idle.

Errors — none.

final status = GraineNativeCallStatus.fromWire('active');   // GraineNativeCallStatus.active

GraineCallDirection

Read it to know who placed the call: the agent (a ring reached the phone) or the customer.

enum GraineCallDirection { incoming, outgoing }

What to expect — Set while a call is live; null with no call.

CaseMeaning
incomingThe agent placed it: a ring reached this phone (a push from Graine or your server).
outgoingThe customer placed it (startCall).

GraineCallCaption

Read it to show the line being said on a native call.

class GraineCallCaption

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.

PropertyTypeMeaning
final Role roleRoleWho is saying it: Role.agent or Role.customer.
final String textStringThe words, as far as they have been said or heard.
final bool liveboolStill being said.

Its functions: GraineCallCaption — each with its parameters, what it returns, the errors it raises and an example.

GraineCallCaption.GraineCallCaption

Use it to build a caption by hand for a preview.

const GraineCallCaption(this.role, this.text, {this.live = false})
ParameterTypeDefaultMeaning
roleRolerequiredWho is saying it: Role.agent or Role.customer.
textStringrequiredThe words, as far as they have been said or heard.
liveboolfalseStill being said.

Returns — a new GraineCallCaption.

Errors — none.

const caption = GraineCallCaption(Role.agent, 'Your order ships today.', live: true);

GraineCallOutcome

Read it to learn what became of each call — to tell your own server, or to follow up.

class GraineCallOutcome

What to expect — Reported once per change on GraineCalls.instance.outcomes: ringing (a call Graine rang), then answered and ended (with reason local, remote or failed, and durationSeconds where the platform reports it) — or declined, missed (busy when another call was live) or failed with why. Outcomes from before anyone listened — a call that rang and ended while the app was not running — reach the first listener, oldest first (the last 20). hosted means Graine rang it and the SDK has already told Graine; tell your own server about the others, under callId. Do not rely on their order relative to the call's state changes.

PropertyTypeMeaning
final String callIdStringThe call's id — your server's call_id for a ring it sent, Graine's for a ring Graine sent, or the one startCall returned. Report the outcome under it.
final GraineCallOutcomeKind outcomeGraineCallOutcomeKindWhat became of the call: ringing, answered, declined, missed, ended or failed. Branch on it; wire is the same as one word.
final String? reasonString?With ended: local, remote or failed. With failed: why the call never connected, as the native engine words it — "The call could not connect within 20 seconds.", "The conversation did not open within 15 seconds." or "The call's audio could not start." (iOS), "The call could not connect within 15 seconds." (Android), "The microphone permission was denied.", …
final int? durationSecondsint?How long the conversation lasted, with ended, when the engine says.
final bool hostedboolTrue when Graine rang the call (hosted ringing); the SDK has already reported its result to Graine.
final String? agentString?The name under which the app configured the agent on this call ('default' for the default agent), or null if the engine cannot tell.
String get wireStringIts value as one word, for your server: one of ringing, answered, declined, missed, ended, failed.
GraineCallOutcomeKind get kindGraineCallOutcomeKindoutcome, by its 0.6 name. Deprecated: use outcome.
String? get endReasonString?reason of an ended call, by its 0.6 name. Deprecated: use reason.
String? get errorString?reason of a failed call, by its 0.6 name. Deprecated: use reason.

Its functions: GraineCallOutcome, fromMap — each with its parameters, what it returns, the errors it raises and an example.

GraineCallOutcome.GraineCallOutcome

Use it to build an outcome by hand for a test.

const GraineCallOutcome(this.callId, this.outcome, {this.reason, this.durationSeconds, this.hosted = false, this.agent})
ParameterTypeDefaultMeaning
callIdStringrequiredThe call's id — your server's call_id for a ring it sent, Graine's for a ring Graine sent, or the one startCall returned. Report the outcome under it.
outcomeGraineCallOutcomeKindrequiredWhat became of the call: ringing, answered, declined, missed, ended or failed. Branch on it; wire is the same as one word.
reasonString?optionalWith ended: local, remote or failed. With failed: why the call never connected, as the native engine words it — "The call could not connect within 20 seconds.", "The conversation did not open within 15 seconds." or "The call's audio could not start." (iOS), "The call could not connect within 15 seconds." (Android), "The microphone permission was denied.", …
durationSecondsint?optionalHow long the conversation lasted, with ended, when the engine says.
hostedboolfalseTrue when Graine rang the call (hosted ringing); the SDK has already reported its result to Graine.
agentString?optionalThe name under which the app configured the agent on this call ('default' for the default agent), or null if the engine cannot tell.

Returns — a new GraineCallOutcome.

Errors — none.

const outcome = GraineCallOutcome('call_6f1c2a9e', GraineCallOutcomeKind.ended, reason: 'remote', durationSeconds: 95);

GraineCallOutcome.fromMap

Use it to read an outcome from the native engine's message — for a test; outcomes does this itself.

factory GraineCallOutcome.fromMap(Map<Object?, Object?> m)
ParameterTypeDefaultMeaning
mMap<Object?, Object?>requiredThe outcome as the native engine sent it.

Returns — GraineCallOutcome. An outcome word this build does not know reads as failed; a failed call's reason is the engine's sentence for why it never connected.

Errors — none.

final outcome = GraineCallOutcome.fromMap({'callId': 'call_6f1c2a9e', 'outcome': 'failed', 'error': 'The call could not connect within 20 seconds.'});

GraineCallOutcomeKind

Read it to know what became of a call: ringing, answered, declined, missed, ended or failed.

enum GraineCallOutcomeKind { ringing, answered, declined, missed, ended, failed }

What to expect — A call Graine rang reports ringing first; then answered, and ended when it is over — or declined, missed or failed instead. A word this build does not know reads as failed.

CaseMeaning
ringingIt started ringing on this device.
answeredThe customer answered.
declinedThe customer declined.
missedNobody answered before ringTimeout, or another call was live (busy).
endedIt was connected and is over. reason: local (this app hung up), remote (the agent did) or failed (the connection was lost mid-call — on Android, also when nothing at all arrived for 50 seconds).
failedIt never connected. reason says why, in a sentence — for example "The call could not connect within 20 seconds.", "The conversation did not open within 15 seconds." or "The call's audio could not start." on iOS, "The call could not connect within 15 seconds." on Android, or "The microphone permission was denied."

GraineCallState

Use it to draw your own controls for the in-app call the voice bar places — GraineInApp.instance.voice is the one to use; a test hands the widgets a fake.

abstract class GraineCallState implements Listenable

What to expect — This is the voice bar's in-app call, not a native call (that is GraineCallSnapshot). GraineInApp.instance.voice implements it and GraineAgentBar draws from it; listen to it and redraw on every change. connecting turns true at start() — while the app is asked for the microphone, and while a start waits for the page — then connected once the conversation opens. One conversation at a time: starting it ends a chat or a call another agent holds, and it waits while a native call is live.

PropertyTypeMeaning
bool get connectedboolTrue while the call is connected — the customer and the agent can hear each other. Draw the in-call controls from it.
bool get connectingboolTrue from start() until the call connects or fails — a held start, the microphone question and the dial included. Draw "Connecting…" from it.
bool get mutedboolTrue while the customer's voice is not being sent (setMuted(true)). Draw the microphone button's state from it.
bool get readyboolThe page that carries the call is loaded and ready: a start connects at once. Until then a start is held and fired when it is ready.
bool get reconnectingboolTrue while the call is being brought back after a network interruption.
bool? get onlinebool?Whether the device is online, as last reported; null before the first report.
ConnectionQuality get qualityConnectionQualityThe call's connection right now, in one word; unknown before the first measurement and between calls. Show "Weak connection" on poor.
GraineConnectivity get connectivityGraineConnectivityWhether the page that carries the call is there: loading, ready, retrying or failed. Offer "Try again" (retry()) on failed.
List<Caption> get captionsList<Caption>What has been said so far, oldest first, a sentence growing word by word kept as one line.
Caption? get captionCaption?The line being said now, or the last one; null before any.
bool get agentSpeakingboolThe agent is mid-sentence.
bool get micDeniedboolThe last start was refused the microphone.
Map<String, Object?> get appearanceMap<String, Object?>The dashboard's Appearance for this agent; empty until the session loads.

Its functions: start, end, setMuted, retry — each with its parameters, what it returns, the errors it raises and an example.

GraineCallState.start

Call it from your own button to start the agent's in-app voice call.

Future<void> start()

Places the agent's in-app voice call — what the bar's microphone does. With GraineConfig.requestMicrophone set, the app is asked for the microphone first; a refusal places nothing and reports onMicDenied('NotAllowedError'). Before the page that carries the call is ready, the start is held and fired when it is. One conversation at a time: it ends a chat or a call another agent holds.

Parameters — none.

Returns — Future<void>, which completes once the call is placed or held — not when it connects; follow connecting and connected.

Errors — none thrown: with a native call live it reports onError('A call is already in progress.'); with no GraineEngine on screen it reports onError asking you to place one, and places nothing.

await GraineInApp.instance.voice.start();

GraineCallState.end

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

void end()

Hangs up the in-app call — or cancels a start still waiting for the page. If the page never confirms the end, the call is torn down on the device after GraineTiming.endWatchdogMs (4 seconds by default).

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.instance.voice.end();

GraineCallState.setMuted

Call it from your own button to stop, or resume, sending the customer's voice on the in-app call.

void setMuted(bool muted)

Stops, or resumes, sending the customer's voice on the in-app call without ending it. muted follows at once.

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

Returns — nothing.

Errors — none.

final voice = GraineInApp.instance.voice;
voice.setMuted(!voice.muted);

GraineCallState.retry

Call it from a "Try again" button when the page that carries the call could not load.

void retry()

Reloads the page that carries the call and starts its retry ladder over — for a "Try again" button when connectivity is failed. connectivity goes back to loading.

Parameters — none.

Returns — nothing.

Errors — none.

final voice = GraineInApp.instance.voice;
if (voice.connectivity == GraineConnectivity.failed) voice.retry();

Calls Graine rings

When your workspace has uploaded its push credentials (Workspace settings → In-app calling), Graine itself rings your users for the calls the agent or the user booked — and your own server can ring them with a push too. On Android, Firebase Cloud Messaging in the app is all it takes — the SDK registers for pushes itself — and an app whose own messaging service receives the pushes (firebase_messaging) forwards them to handlePush; on iOS, enableVoIPPushes is all it takes. The SDK tells Graine what became of every ring it sent; a ring nobody answers reaches outcomes as missed. The push carries nothing personal: type, call_id, caller_name, hosted and agent_id.

A call answered here carries its audio the way the calls the customer places do: over WebRTC in a build that has it (WebRTC calls) — prepared while the ring shows, so answering connects at once — as GraineCallsOptions.callTransport and the agent's In-app call audio allow, and over the WebSocket otherwise. Once it is active, its snapshot says which (GraineCallSnapshot.transport, transportFallbackReason).

GraineCalls.registerPushToken

Call it on Android only when your app's own messaging service receives the tokens (firebase_messaging), from onTokenRefresh. With Firebase Cloud Messaging in the app the SDK reads the token itself at launch, and without a messaging service of your own the SDK's receives every new one.

Future<void> registerPushToken(String token)

Android: registers this device with Graine for hosted ringing, with your Firebase messaging token — when the workspace has Graine ring its users — together with who is signed in, and again whenever they change. Most apps never call it: the SDK reads the token at launch. On iOS it does nothing: the VoIP token is registered natively (enableVoIPPushes).

ParameterTypeDefaultMeaning
tokenStringrequiredThe FCM token; trimmed, and an empty one is ignored.

Returns — Future<void>.

Errors — none: a failure is logged with debugPrint ("[Graine] calls: registerPushToken failed (…)"); a build without the native engine ignores it.

FirebaseMessaging.instance.onTokenRefresh.listen(GraineCalls.instance.registerPushToken);   // firebase_messaging only

GraineCalls.enableVoIPPushes

Call it once on iOS to receive the agent's calls as VoIP pushes — all an iOS app needs for Graine to ring it.

void Function() enableVoIPPushes(void Function(String? token) onToken)

iOS: receives the agent's calls as VoIP pushes through CallKit. The SDK registers the VoIP token with Graine itself when your workspace has Graine ring its users; onToken gets it too, for your own server's pushes (APNs, apns-push-type: voip) — now, if the device has one, and whenever it changes; null when it is invalidated. Every VoIP push rings, as iOS requires. On Android it does nothing — use handlePush.

ParameterTypeDefaultMeaning
onTokenvoid Function(String? token)requiredCalled with the VoIP token, and with null when it is invalidated.

Returns — void Function() — call it to stop hearing tokens.

Errors — none.

GraineCalls.instance.enableVoIPPushes((token) => myServer.saveVoipToken(token));

GraineCalls.handlePush

Call it on Android from your Firebase message handlers, foreground and background, when your app's own messaging service receives the pushes (firebase_messaging), so a Graine call rings even when the app was closed. Without one, the SDK's own messaging service hands every push to the engine natively.

Future<bool> handlePush(Map<String, Object?> data)

Android: hands a push's data to the call engine, from your Firebase message handlers in the foreground and the background. A Graine call rings with a call notification and the call screen, even when the app was not running; a ring that arrives during a live call is missed (busy). The push carries nothing personal — type, call_id, caller_name, hosted and agent_id. On iOS it does nothing: VoIP pushes arrive natively.

ParameterTypeDefaultMeaning
dataMap<String, Object?>requiredThe push's data, exactly as Firebase delivered it (RemoteMessage.data).

Returns — Future<bool> — true when the push was a call (ringing, busy or malformed) and the app should do nothing more with it; false for any other push, on iOS, or in a build without the native engine. Example value: true.

Errors — none.

FirebaseMessaging.onMessage.listen((m) => GraineCalls.instance.handlePush(m.data));

GraineCalls.answer

Call it to answer the ringing call from your own screen; the SDK's call screens and the system's answer by themselves.

Future<void> answer()

Answers the ringing call from a screen of your own (GraineCallScreen.app). The SDK's call screen and the system's answer by themselves. The microphone is checked as for a call placed: refused, the call ends failed after answered.

Parameters — none.

Returns — Future<void>.

Errors — none; with nothing ringing it does nothing.

if (GraineCalls.instance.value.status == GraineNativeCallStatus.ringing) await GraineCalls.instance.answer();

GraineCalls.decline

Call it to decline the ringing call from your own screen.

Future<void> decline()

Declines the ringing call from a screen of your own. For a call Graine rang, nothing rings this customer again for 6 hours; the outcome is declined.

Parameters — none.

Returns — Future<void>.

Errors — none; with nothing ringing it does nothing.

await GraineCalls.instance.decline();

GraineIncomingCall

Read it to know an agent's call has started ringing on this device — for your own ringing UI or analytics.

class GraineIncomingCall

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.

PropertyTypeMeaning
final String callIdStringThe ringing call's id, as the push named it — the same id its outcome will carry.
final String? reasonString?Why the agent is calling, as the push said it; null when it said nothing. Graine's own rings never carry the booking's reason.
final String? callerNameString?The name the ring shows — the push's caller_name, else GraineCallsOptions.displayName.
final bool hostedboolTrue when the ring came from Graine (hosted ringing) and not from a push your server sent.
final String? agentString?The name under which the app configured the ringing agent ('default' for the default agent). The native engine finds it by matching the push's agent_id against the configured agents' ids, falling back to the default agent when none match.

Its functions: GraineIncomingCall — each with its parameters, what it returns, the errors it raises and an example.

GraineIncomingCall.GraineIncomingCall

Use it to build a ring by hand for a test.

const GraineIncomingCall(this.callId, {this.reason, this.callerName, this.hosted = false, this.agent})
ParameterTypeDefaultMeaning
callIdStringrequiredThe ringing call's id, as the push named it — the same id its outcome will carry.
reasonString?optionalWhy the agent is calling, as the push said it; null when it said nothing. Graine's own rings never carry the booking's reason.
callerNameString?optionalThe name the ring shows — the push's caller_name, else GraineCallsOptions.displayName.
hostedboolfalseTrue when the ring came from Graine (hosted ringing) and not from a push your server sent.
agentString?optionalThe name under which the app configured the ringing agent ('default' for the default agent). The native engine finds it by matching the push's agent_id against the configured agents' ids, falling back to the default agent when none match.

Returns — a new GraineIncomingCall.

Errors — none.

const ring = GraineIncomingCall('call_6f1c2a9e', callerName: 'Acme Assistant', hosted: true);

CallsSetting

Read it to know whether Graine rings this workspace's apps, on which platforms, for how long, and what customers may book.

class CallsSetting

What to expect — Part of the session; null until it arrives, and from an agent that does not say — which means no hosted ringing and no booking.

PropertyTypeMeaning
final bool hostedboolTrue if push credentials were uploaded for the workspace, which lets Graine ring its users' devices.
final List<String> platformsList<String>Where it can ring: ios, android.
final int ringTimeoutSecondsintHow long a ring Graine sends lasts before it is missed, in seconds (default 45; read within 20–60).
final String? callNameString?The name the phone displays for the agent's calls; when null, the agent's display name is shown.
final SchedulingSetting? schedulingSchedulingSetting?What customers may book from the app; null when the agent's settings say nothing (no booking).

Its functions: CallsSetting, fromJson — each with its parameters, what it returns, the errors it raises and an example.

CallsSetting.CallsSetting

Use it to build the call settings by hand for a test; the session carries the real ones.

const CallsSetting({ this.hosted = false, this.platforms = const <String>[], this.ringTimeoutSeconds = 45, this.callName, this.scheduling})
ParameterTypeDefaultMeaning
hostedboolfalseTrue if push credentials were uploaded for the workspace, which lets Graine ring its users' devices.
platformsList<String>const <String>[]Where it can ring: ios, android.
ringTimeoutSecondsint45How long a ring Graine sends lasts before it is missed, in seconds (default 45; read within 20–60).
callNameString?optionalThe name the phone displays for the agent's calls; when null, the agent's display name is shown.
schedulingSchedulingSetting?optionalWhat customers may book from the app; null when the agent's settings say nothing (no booking).

Returns — a new CallsSetting.

Errors — none.

final result = CallsSetting();

CallsSetting.fromJson

Use it to read the call settings from the session's JSON.

static CallsSetting? fromJson(Object? raw)
ParameterTypeDefaultMeaning
rawObject?requiredThe value as decoded from JSON; anything that is not the expected object reads as null.

Returns — CallsSetting?. Example value: null.

Errors — none.

final calls = CallsSetting.fromJson({'hosted': true, 'platforms': ['ios', 'android'], 'ringTimeoutSeconds': 45, 'scheduling': {'usersCanBook': true}});

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 — on Android from a transparent screen of the SDK's own, so nothing forwards a permission result. callSetupProblems() says what your setup still lacks; the native SDK logs each problem once from configure on, and a debug build also prints each once in the console flutter run shows. 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.

Future<GraineCallPermissions> callPermissions()

The microphone, and on Android the ring's notifications (13+ asks) and the full-screen ring (14+, Settings only); on iOS those two are notNeeded — CallKit rings and draws its own screen. Readable before configure. On a platform other than Android and iOS, everything is notNeeded.

Returns — Future<GraineCallPermissions>. Read it again when the app returns to the foreground: the customer may have changed Settings.

Errors — none: a build without the plugin's native side reads everything as denied.

final p = await GraineCalls.instance.callPermissions();
if (!p.allGranted) showFinishSetupCard();

GraineCalls.requestCallPermissions

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

Future<GraineCallPermissions> requestCallPermissions()

Asks for the call permissions the system's dialog can still ask for — the microphone, and notifications on Android 13+ — and returns them as they stand after. Nothing to ask: the answer comes at once. The full-screen ring has no dialog: openCallSettings(GraineCallPermission.fullScreen) opens its page. The SDK never asks on its own at launch; a call that starts without the microphone still asks then.

Returns — Future<GraineCallPermissions>, after the customer answers.

Errors — none.

final p = await GraineCalls.instance.requestCallPermissions();
if (p.fullScreen == GraineCallPermissionStatus.blocked) {
  await GraineCalls.instance.openCallSettings(GraineCallPermission.fullScreen);
}

GraineCalls.openCallSettings

Call it to send the customer to the Settings page that changes a call permission.

Future<bool> openCallSettings(GraineCallPermission item)

Opens, on Android, the full-screen ring's own page (14+) or the app's notification settings; otherwise the app's page, where the microphone is.

ParameterTypeDefaultMeaning
itemGraineCallPermissionrequiredThe permission the customer should change.

Returns — Future<bool>: true when a page opened. Example value: true.

Errors — none: a build without the plugin's native side returns false and says so in the console.

await GraineCalls.instance.openCallSettings(GraineCallPermission.microphone);

GraineCalls.callSetupProblems

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

Future<List<GraineCallSetupProblem>> callSetupProblems()

What the app's setup still lacks for calls, read now — each a stable code and a sentence with the exact fix. Empty when nothing is missing, and before configure.

CodePlatformWhen
microphone_usage_missingiOSios/Runner/Info.plist has no NSMicrophoneUsageDescription: iOS closes the app when a call asks for the microphone. A plugin cannot add it.
background_audio_missingiOSUIBackgroundModes lacks audio: a call goes silent when the screen locks.
background_voip_missingiOSUIBackgroundModes lacks voip: iOS does not wake the app for a ring.
push_entitlement_missingiOSSigned without Push Notifications (read from development and ad hoc builds).
voip_pushes_not_enablediOSenableVoIPPushes not called 15 seconds after configure.
no_voip_tokeniOSNo VoIP token 15 seconds after enableVoIPPushes (never on the simulator).
manifest_entry_missingAndroidThe merged manifest lost a call entry (look for tools:node="remove").
no_push_tokenAndroidNo push token 15 seconds after configure: Firebase Cloud Messaging is missing or not set up in the Android app (the message says which), or an app whose own messaging service receives the tokens never called registerPushToken. Calls can't ring.
notifications_not_allowedAndroidA call can't ring while the app is closed or in the background.
full_screen_not_allowedAndroidA locked phone rings with a heads-up notification instead.
microphone_blockedBothOnly Settings can let calls hear the customer.
hosted_ringing_offBothNo push credentials uploaded for this platform: Graine can't ring this phone.

Returns — Future<List<GraineCallSetupProblem>>.

Errors — none.

for (final p in await GraineCalls.instance.callSetupProblems()) {
  debugPrint('${p.code}: ${p.message}');
}

GraineCallPermission

Use it to name a call permission.

enum GraineCallPermission

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

CaseMeaning
microphone('microphone')The customer's voice.
notifications('notifications')A call that rings while the app is closed or in the background — Android (13+ asks). iOS rings through CallKit, which needs none.
fullScreen('fullScreen')A ring that fills a locked screen — Android 14+, allowed in Settings. iOS: CallKit's own screen.
PropertyTypeMeaning
final String wireStringThe word every Graine SDK uses for it.

Its functions: GraineCallPermission — each with its parameters, what it returns, the errors it raises and an example.

GraineCallPermission.GraineCallPermission

The enum's constructor; use the values (GraineCallPermission.microphone, …).

const GraineCallPermission(this.wire)
ParameterTypeDefaultMeaning
wireStringrequiredThe word every Graine SDK uses for it.

Returns — nothing you call: an enum's constructor runs once for each value.

Errors — none.

print(GraineCallPermission.fullScreen.wire);   // fullScreen

GraineCallPermissionStatus

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

enum GraineCallPermissionStatus

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

CaseMeaning
granted('granted')Allowed.
denied('denied')Not allowed yet: requestCallPermissions() can ask for it.
blocked('blocked')Refused for good, or only allowed in Settings: only the Settings app can change it.
notNeeded('notNeeded')This phone needs no such permission.
PropertyTypeMeaning
final String wireStringThe word every Graine SDK uses for it.
bool get isSatisfiedboolGranted, or nothing to grant.

Its functions: GraineCallPermissionStatus, fromWire — each with its parameters, what it returns, the errors it raises and an example.

GraineCallPermissionStatus.GraineCallPermissionStatus

The enum's constructor; use the values (GraineCallPermissionStatus.granted, …).

const GraineCallPermissionStatus(this.wire)
ParameterTypeDefaultMeaning
wireStringrequiredThe word every Graine SDK uses for it.

Returns — nothing you call: an enum's constructor runs once for each value.

Errors — none.

print(GraineCallPermissionStatus.notNeeded.wire);   // notNeeded

GraineCallPermissionStatus.fromWire

Use it to read a status from the engine's word — for a test; GraineCallPermissions.fromMap does this itself.

static GraineCallPermissionStatus fromWire(Object? s)
ParameterTypeDefaultMeaning
sObject?requiredThe engine's word: 'granted', 'denied', 'blocked' or 'notNeeded'.

Returns — GraineCallPermissionStatus; anything it does not know is denied, never granted. Example value: GraineCallPermissionStatus.blocked.

Errors — none.

final s = GraineCallPermissionStatus.fromWire('blocked');   // GraineCallPermissionStatus.blocked

GraineCallPermissions

Read it to see every call permission at once.

class GraineCallPermissions

What to expect — A snapshot of the phone when read: call callPermissions() again for the latest. allGranted is the one check before offering calls; permissions[GraineCallPermission.microphone] reads one by name. Equal when every status is.

PropertyTypeMeaning
final GraineCallPermissionStatus microphoneGraineCallPermissionStatusThe microphone.
final GraineCallPermissionStatus notificationsGraineCallPermissionStatusThe ring's notifications — notNeeded on iOS.
final GraineCallPermissionStatus fullScreenGraineCallPermissionStatusThe full-screen ring — notNeeded on iOS.
bool get allGrantedboolEvery permission is granted or not needed: calls can ring and connect.

Its functions: GraineCallPermissions, fromMap — each with its parameters, what it returns, the errors it raises and an example.

GraineCallPermissions.GraineCallPermissions

Use it to build permissions for a test or a preview.

const GraineCallPermissions({required GraineCallPermissionStatus microphone, required GraineCallPermissionStatus notifications, required GraineCallPermissionStatus fullScreen})
ParameterTypeDefaultMeaning
microphoneGraineCallPermissionStatusrequiredThe microphone's status.
notificationsGraineCallPermissionStatusrequiredThe notifications' status.
fullScreenGraineCallPermissionStatusrequiredThe full-screen ring's status.

Returns — a new GraineCallPermissions.

Errors — none.

const p = GraineCallPermissions(
  microphone: GraineCallPermissionStatus.granted,
  notifications: GraineCallPermissionStatus.notNeeded,
  fullScreen: GraineCallPermissionStatus.notNeeded,
);

GraineCallPermissions.fromMap

Use it to read the permissions from the engine's map — for a test; callPermissions() does this itself.

factory GraineCallPermissions.fromMap(Map<Object?, Object?>? m)
ParameterTypeDefaultMeaning
mMap<Object?, Object?>?required{microphone, notifications, fullScreen}, each the engine's word.

Returns — a GraineCallPermissions; a missing or unknown word reads denied.

Errors — none.

final p = GraineCallPermissions.fromMap({'microphone': 'granted', 'notifications': 'blocked', 'fullScreen': 'denied'});

GraineCallSetupProblem

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

class GraineCallSetupProblem

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. Equal when both are.

PropertyTypeMeaning
final String codeStringStable: act on it, or count it.
final String messageStringWhat is missing and how to fix it, in plain words.

Its functions: GraineCallSetupProblem — each with its parameters, what it returns, the errors it raises and an example.

GraineCallSetupProblem.GraineCallSetupProblem

Use it to build a problem for a test.

const GraineCallSetupProblem(this.code, this.message)
ParameterTypeDefaultMeaning
codeStringrequiredThe stable code.
messageStringrequiredThe sentence with the fix.

Returns — a new GraineCallSetupProblem.

Errors — none.

const p = GraineCallSetupProblem('no_push_token', 'Hand Firebase\'s token to registerPushToken.');

Booking with the SDK's sheet

New in 0.8.0: the SDK draws the booking screen itself — "Call me at…" with no screen of your own. GraineBooking.show(context) opens it over your app in the agent's design, light or dark: a sheet up from the bottom on a phone, a centred panel on a tablet. The customer picks a day and a time, may say what the call is about, taps Book call, and Graine rings them then.

What the customer sees, top to bottom:

  1. Book a call, and under it who will ring them — "Asha will call you" — named by the agent's call name from Calls & scheduling, else the chat's title, else the heading, else "Assistant".
  2. Day — chips from today to the furthest day the agent allows: "Today", "Tomorrow", then the weekday and date ("Thu 16 Oct"). Days that have no time remaining are left out.
  3. Time — Flutter's own time picker (showTimePicker, which works without a MaterialApp too). The choice is rounded to 5 minutes and held between the earliest and the latest time the agent allows; the first time on the chosen day is offered to start with.
  4. The device's time zone and the time chosen: "Asia/Kolkata · Wed 14 Oct, 6:00 PM".
  5. What's it about? (optional) — one line, at most 120 characters; reason fills it in, and the customer may change it.
  6. Book call — a spinner while it books; then the sheet says "Booked — Asha will call you on Wed 14 Oct, 6:00 PM" (always the date, never "Today") with Done.

A refusal stays in the sheet as one line under the button, word for word:

WhenThe line
tooSoonPick a time at least 5 minutes from now (the agent's notice, counted: "1 minute", "5 minutes") — the sheet then moves on to the new earliest time
tooFarPick a time within 14 days (the agent's horizon)
outsideWindowThat time is outside the hours we can call — pick another
tooManyPendingYou already have 3 calls booked — cancel one first (the agent's limit)
schedulingOffBooking a call isn't available right now
identityRequiredSign in to book a call
rateLimited, unavailable, network and anything elseCouldn't book just now — try again

The future GraineBooking.show returns completes with the booked GraineScheduledCall once the sheet closes, or null when the customer closed it without booking. Show your own "Call me" button only when bookingAvailable is true; the SDK's chat window does the same: Book a call sits under the booked calls in its Upcoming calls card — or on its own in the card's place while none are booked — and never when the design hides the booked-calls card (ChatPieces.book). A booking from the sheet or from scheduleCall shows on the card at once. For a booking screen of your own, Booking holds the same rules and words.

GraineInApp.bookingAvailable

Read it to decide whether to show your own "Call me" button: true only when this customer can book a call from here.

bool get bookingAvailable

True only when all of these hold:

  • the agent lets customers book (Agent → Calls & scheduling → users may book; schedulingRules.usersCanBook);
  • Graine rings this phone: the workspace set up In-app calling (session.calls.hosted), and its platforms are empty or name this one — ios or android;
  • the customer is identified with the trait the agent's Identity setting names (identify — user_id unless the dashboard names another).

False until the agent's settings have loaded (loadSession()), and false on any platform other than iOS and Android. Each agent has its own. It changes when the settings arrive and when the customer signs in or out; bookingListenable redraws a widget then, and bookingBlocker says why it is false. GraineBooking.show opens the SDK's booking sheet.

Parameters — none.

Returns — bool. Example value: true.

Errors — none.

if (GraineInApp.instance.bookingAvailable) {
  final call = await GraineBooking.show(context, reason: 'Talk through my order');
}

GraineInApp.bookingBlocker

Read it to know why booking is not available — to say so, or to ask the customer to sign in.

GraineErrorCode? get bookingBlocker

Why bookingAvailable is false, as the booking sheet says it — or null when it is true. The agent's settings are checked first: signing in would not help while booking is off.

ValueWhenThe sheet's line
GraineErrorCode.schedulingOffThe agent takes no bookings, Graine does not ring this phone (no In-app calling, or not for this platform), the app runs on neither iOS nor Android, or the settings have not loadedBooking a call isn't available right now
GraineErrorCode.identityRequiredBooking is on, but nobody is identifiedSign in to book a call
nullBooking is availableNo line: Book call books

Parameters — none.

Returns — GraineErrorCode?. Example value: GraineErrorCode.identityRequired.

Errors — none.

final why = GraineInApp.instance.bookingBlocker;
if (why == GraineErrorCode.identityRequired) showSignIn(context);

GraineInApp.bookingListenable

Use it to redraw a widget — your "Call me" button — when booking becomes available or stops being so.

ValueListenable<bool> get bookingListenable

bookingAvailable, as a listenable: it starts false and changes when the agent's settings arrive and when the customer signs in or out.

Parameters — none.

Returns — ValueListenable<bool>. Example value: its value is true once booking is available.

Errors — none.

ValueListenableBuilder<bool>(
  valueListenable: GraineInApp.instance.bookingListenable,
  builder: (context, canBook, _) => canBook
      ? TextButton(onPressed: () => GraineBooking.show(context), child: const Text('Call me'))
      : const SizedBox.shrink(),
)

GraineInApp.deviceTimezone

Read it to show the customer which time zone a time is in, as the booking sheet does.

String? get deviceTimezone

The device's time zone as an IANA name ("Asia/Kolkata"), once the SDK has read it — it reads it as it starts, and again before every request, since people travel. Null before then, or when the platform cannot say. The booking sheet shows this zone next to the chosen time.

Parameters — none.

Returns — String?. Example value: 'Asia/Kolkata'.

Errors — none.

final zone = GraineInApp.instance.deviceTimezone ?? 'your time zone';

GraineBooking

Use it to open the SDK's booking sheet — "Call me at…" with no screen of your own.

abstract final class GraineBooking

What to expect — One static function, show. The sheet books through the agent's scheduleCall, so a booking appears on the chat's Upcoming calls card at once and in scheduledCalls(); Graine rings the customer at the time chosen.

Its functions: show — each with its parameters, what it returns, the errors it raises and an example.

GraineBooking.show

Call it from a "Call me" button to let the customer book a call: it opens the sheet and completes with the booked call.

static Future<GraineScheduledCall?> show(BuildContext context, {GraineInApp? app, String? reason})

Opens the SDK's booking sheet over context's navigator, in the agent's design: on a phone (shortest side under 600) a sheet slides up from the bottom, the app dimmed behind it; on a tablet a panel 420 wide fades in at the centre. The customer picks a day and a time, may say what the call is about, and taps Book call; the sheet then says when the agent will call, and Done closes it. A tap outside the sheet closes it too. A refusal stays in the sheet as one line under the button (the table above), and the customer can try again. The sheet loads the agent's settings itself when they have not arrived, and checks the time every 30 seconds while it is open: what was bookable a minute ago may not be. Opened while booking is not available, it says why and books nothing.

ParameterTypeDefaultMeaning
contextBuildContextrequiredWhere to open it: the sheet goes over this context's navigator.
appGraineInApp?optionalThe agent to book with; the default agent when left out.
reasonString?optionalFills "What's it about?" — trimmed and cut to 120 characters; the customer may change it. It is kept with the call for the agent and never put in the push that rings it.

Returns — Future<GraineScheduledCall?> — once the sheet closes: the booked call (status scheduled) — also when the customer booked and then tapped outside instead of Done — or null when they closed it without booking. Example value: GraineScheduledCall(6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44, 2026-10-14T12:30:00.000Z, scheduled).

Errors — none: every refusal (tooSoon, tooFar, outsideWindow, tooManyPending, schedulingOff, identityRequired, rateLimited, unavailable, network) is shown in the sheet, word for word as listed above. Called before GraineInApp.init, it logs "[Graine] GraineBooking.show before GraineInApp.init: there is no agent to book with." and completes with null without opening.

final call = await GraineBooking.show(context, reason: 'Talk through my order');
if (call != null) {
  ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Booked for ${ChatTime.booked(call.at!, DateTime.now(), h24: false)}')));
}

GraineBookingSheet

Use it to show the booking sheet your own way — on a route, in your own sheet, or in a widget test.

class GraineBookingSheet extends StatefulWidget

What to expect — The same sheet GraineBooking.show opens, as a widget you place yourself: "Book a call", who will call, the day chips, the time (Flutter's showTimePicker), the time zone line, "What's it about? (optional)", Book call — then "Booked — …" and Done. It sizes itself to its content and scrolls when space is short; it keeps clear of the keyboard and, as a sheet, of the home indicator. It does not close itself: onClose (Done, or the close button) is yours to handle.

PropertyTypeMeaning
final GraineInApp? appGraineInApp?The agent to book with; the default agent when null.
final String? reasonString?Fills "What's it about?" (at most 120 characters); the customer may change it.
final ValueChanged<GraineScheduledCall>? onBookedValueChanged<GraineScheduledCall>?Called with the booked call, as the sheet switches to "Booked — …".
final VoidCallback? onCloseVoidCallback?Called by Done and by the close button; null draws no close button (Done still shows, and does nothing).
final bool sheetboolTrue (the default): drawn as a sheet — rounded top corners and a grab handle; false: a panel with every corner rounded and an outline.
final DateTime Function()? nowDateTime Function()?The clock, for a test; the device's when null.
static const Key sheetKeyKeyThe whole sheet, for a widget test ('graine-booking-sheet').
static Key dayKey(String day)KeyOne day chip, by its YYYY-MM-DD date, for a widget test.
static const Key timeKeyKeyThe time field that opens the time picker.
static const Key zoneKeyKeyThe time zone line.
static const Key aboutKeyKeyThe "What's it about?" field.
static const Key bookKeyKeyThe Book call button.
static const Key lineKeyKeyThe refusal line under the button.
static const Key doneKeyKeyThe Done button, once booked.
static const Key closeKeyKeyThe close button (shown when onClose is set).

Its functions: GraineBookingSheet, dayKey — each with its parameters, what it returns, the errors it raises and an example.

GraineBookingSheet.GraineBookingSheet

Use it to place the booking sheet yourself, instead of GraineBooking.show.

const GraineBookingSheet({super.key, this.app, this.reason, this.onBooked, this.onClose, this.sheet = true, this.now})
ParameterTypeDefaultMeaning
keyKey?optionalThe widget's key, as for any Flutter widget.
appGraineInApp?optionalThe agent to book with; the default agent when null.
reasonString?optionalFills "What's it about?" (at most 120 characters); the customer may change it.
onBookedValueChanged<GraineScheduledCall>?optionalCalled with the booked call, as the sheet switches to "Booked — …".
onCloseVoidCallback?optionalCalled by Done and by the close button; null draws no close button (Done still shows, and does nothing).
sheetbooltrueTrue (the default): drawn as a sheet — rounded top corners and a grab handle; false: a panel with every corner rounded and an outline.
nowDateTime Function()?optionalThe clock, for a test.

Returns — a new GraineBookingSheet.

Errors — none; refusals are shown in the sheet.

Navigator.of(context).push(MaterialPageRoute<void>(
  builder: (route) => Scaffold(
    body: SafeArea(
      child: GraineBookingSheet(
        sheet: false,
        reason: 'Talk through my order',
        onBooked: (call) => analytics.log('booked', {'id': call.id}),
        onClose: () => Navigator.of(route).pop(),
      ),
    ),
  ),
));

GraineBookingSheet.dayKey

Use it in a widget test to find one day chip of the sheet.

static Key dayKey(String day)

The key of one day chip.

ParameterTypeDefaultMeaning
dayStringrequiredThe day on the device, YYYY-MM-DD (BookingDay.key).

Returns — Key — ValueKey<String>('graine-booking-day-' + day).

Errors — none.

import 'package:flutter_test/flutter_test.dart';
 
testWidgets('books a call for tomorrow', (tester) async {
  await tester.pumpWidget(const MaterialApp(home: Scaffold(body: GraineBookingSheet(sheet: false))));
  await tester.tap(find.byKey(GraineBookingSheet.dayKey('2026-10-16')));
});

ChatPieces.book

Read it to decide whether your own chat window shows "Book a call" — as the SDK's chat window does.

final bool book

True when the chat window offers Book a call: the design keeps the booked-calls card (ThemeChat.showUpcomingCalls) and booking is available here (GraineInApp.bookingAvailable). With calls booked it sits under them in the Upcoming calls card; with none, it is a compact row in the card's place. It never shows when the design hides the booked-calls card. The SDK's chat window opens GraineBooking.show from it.

Parameters — none.

Returns — bool. Example value: true.

Errors — none.

final chat = GraineInApp.instance.chat;
final children = <Widget>[];   // the card you are building
final pieces = ChatPieces.ofChat(chat.turns, GraineInApp.instance.tokens?.chat,
    upcomingCount: chat.upcomingCalls.length, bookingAvailable: GraineInApp.instance.bookingAvailable);
if (pieces.book) children.add(TextButton(onPressed: () => GraineBooking.show(context), child: const Text('Book a call')));

Booking

Use it to build a booking screen of your own with the same rules and words as the SDK's sheet.

abstract final class Booking

What to expect — Pure rules and words, the same on every Graine SDK: nothing here asks Graine anything. Book with GraineInApp.scheduleCall once the customer has chosen.

PropertyTypeMeaning
static const String titleStringThe sheet's title: 'Book a call'.
static const String aboutStringThe reason field's label: "What's it about? (optional)".
static const String bookStringThe button: 'Book call'.
static const String doneStringThe button once booked: 'Done'.
static const String webStringWhat a website's sheet says, since a browser cannot be rung: "We'll ring you in the app".
static const int reasonMaxintThe longest reason a booking carries: 120 characters.
static const int roundMinutesintThe grid every time is on: 5 minutes — the earliest rounded up to it, the latest down.

Its functions: subtitle, callName, blocker, available, bounds, clamp, dayLabel, days, onDay, zoneLine, doneLine, errorLine — each with its parameters, what it returns, the errors it raises and an example.

Booking.subtitle

Use it to write the line under the sheet's title.

static String subtitle(String callName)

The line under the title: who will call.

ParameterTypeDefaultMeaning
callNameStringrequiredWho calls (Booking.callName).

Returns — String. Example value: 'Asha will call you'.

Errors — none.

final line = Booking.subtitle('Asha');

Booking.callName

Use it to pick the name the sheet says will call.

static String callName(List<String?> names)

The first name that is not empty after trimming, else "Assistant". The sheet passes the agent's call name (Calls & scheduling), the chat's title and the heading, in that order.

ParameterTypeDefaultMeaning
namesList<String?>requiredThe candidates, best first; nulls and blanks are skipped.

Returns — String. Example value: 'Asha'.

Errors — none.

final graine = GraineInApp.instance;
final who = Booking.callName([graine.session?.calls?.callName, graine.tokens?.chat.title]);

Booking.blocker

Use it to work out why booking is not offered — the same rule as bookingAvailable.

static GraineErrorCode? blocker(CallsSetting? calls, {required String? platform, required bool identified})

Why booking is not offered, or null when it is — one rule for every SDK. Booking needs all of: the agent lets customers book; Graine can ring this phone (hosted ringing, and platforms empty or naming platform) — a web page cannot be rung, so with platform 'web' it books for the customer's app and needs only the rest; and the customer is identified. The agent's settings are checked first.

ParameterTypeDefaultMeaning
callsCallsSetting?requiredThe session's call settings (session?.calls); null allows nothing.
platformString?required'ios', 'android' or 'web'; anything else (or null) is schedulingOff.
identifiedboolrequiredThe customer is identified with the agent's identity trait.

Returns — GraineErrorCode? — schedulingOff, identityRequired, or null. Example value: GraineErrorCode.identityRequired.

Errors — none.

final why = Booking.blocker(GraineInApp.instance.session?.calls, platform: 'android', identified: false);

Booking.available

Use it to work out whether booking is offered from the agent's call settings, the platform and whether the customer is identified.

static bool available(CallsSetting? calls, {required String? platform, required bool identified})

True when Booking.blocker says nothing — the rule GraineInApp.bookingAvailable applies.

ParameterTypeDefaultMeaning
callsCallsSetting?requiredThe session's call settings.
platformString?required'ios', 'android' or 'web'.
identifiedboolrequiredThe customer is identified.

Returns — bool. Example value: false.

Errors — none.

final canBook = Booking.available(GraineInApp.instance.session?.calls, platform: 'ios', identified: true);

Booking.bounds

Use it to get the earliest and the latest time a booking may take, on the 5-minute grid.

static BookingBounds bounds(GraineSchedulingRules rules, DateTime now)

The interval a booking may take at now: now plus the agent's notice, rounded up to 5 minutes, to now plus its horizon in days, rounded down.

ParameterTypeDefaultMeaning
rulesGraineSchedulingRulesrequiredThe agent's rules (GraineInApp.schedulingRules).
nowDateTimerequiredThe time now — pass DateTime.now().

Returns — BookingBounds. Example value: at 6:02 PM with 5 minutes' notice, earliest is 6:10 PM.

Errors — none.

final bounds = Booking.bounds(GraineInApp.instance.schedulingRules, DateTime.now());

Booking.clamp

Use it to move any time inside the bounds, on the 5-minute grid.

static DateTime clamp(DateTime at, BookingBounds bounds)

at moved to the nearest 5-minute mark, then held inside the bounds.

ParameterTypeDefaultMeaning
atDateTimerequiredAny time.
boundsBookingBoundsrequiredFrom Booking.bounds.

Returns — DateTime. Example value: 6:12 PM becomes 6:10 PM; a time before earliest becomes earliest.

Errors — none.

final picked = DateTime.now().add(const Duration(days: 30));   // what the time picker gave
final at = Booking.clamp(picked, Booking.bounds(GraineInApp.instance.schedulingRules, DateTime.now()));

Booking.dayLabel

Use it to get a day chip's words: "Today", "Tomorrow" or the weekday and date.

static String dayLabel(DateTime at, DateTime now)

A day chip's words, on the device's clock: "Today", "Tomorrow", else the weekday and date as ChatTime.day writes it.

ParameterTypeDefaultMeaning
atDateTimerequiredA time on the day.
nowDateTimerequiredThe time now.

Returns — String. Example value: 'Thu 16 Oct'.

Errors — none.

final words = Booking.dayLabel(DateTime(2026, 10, 16, 10), DateTime(2026, 10, 14, 18));

Booking.days

Use it to get the days to offer, each with its first and last bookable time.

static List<BookingDay> days(BookingBounds bounds, DateTime now)

The days to offer, from the day of the earliest time to the day of the latest, each with its first and last bookable time on the 5-minute grid. A day with no time left in it is left out — late in the evening, "Today" may not be offered.

ParameterTypeDefaultMeaning
boundsBookingBoundsrequiredFrom Booking.bounds.
nowDateTimerequiredThe time now, for the chips' words.

Returns — List<BookingDay> — empty when the bounds hold no time at all. Example value: [Today, Tomorrow, Thu 16 Oct, …].

Errors — none.

final now = DateTime.now();
final days = Booking.days(Booking.bounds(GraineInApp.instance.schedulingRules, now), now);

Booking.onDay

Use it to turn the hour and minute a time picker answered into a bookable time on the chosen day.

static DateTime onDay(BookingDay day, int hour, int minute)

The time a picker answered (hour:minute on the device's clock) on day, moved to the nearest 5-minute mark and held between the day's first and last time.

ParameterTypeDefaultMeaning
dayBookingDayrequiredThe chosen day.
hourintrequired0–23.
minuteintrequired0–59.

Returns — DateTime. Example value: 18:02 on a day that offers until 22:00 becomes 18:00.

Errors — none.

final now = DateTime.now();
final days = Booking.days(Booking.bounds(GraineInApp.instance.schedulingRules, now), now);
if (days.isNotEmpty) {
  final day = days.first;   // the chip the customer tapped
  final picked = await showTimePicker(context: context, initialTime: TimeOfDay.fromDateTime(day.first));
  final at = picked == null ? null : Booking.onDay(day, picked.hour, picked.minute);
}

Booking.zoneLine

Use it to write the time zone line under the time.

static String zoneLine(DateTime at, String? zone, {required bool h24})

The line under the time: the zone, then the day and time chosen — or just the day and time when the zone is unknown.

ParameterTypeDefaultMeaning
atDateTimerequiredThe time chosen.
zoneString?requiredThe device's zone (GraineInApp.deviceTimezone); null or empty leaves it out.
h24boolrequiredtrue on a device set to the 24-hour clock — MediaQuery.alwaysUse24HourFormatOf(context).

Returns — String. Example value: 'Asia/Kolkata · Wed 14 Oct, 6:00 PM'.

Errors — none.

final at = DateTime.now().add(const Duration(days: 1));   // the time chosen
final line = Booking.zoneLine(at, GraineInApp.instance.deviceTimezone, h24: false);

Booking.doneLine

Use it to write the sentence the sheet shows once the call is booked.

static String doneLine(String callName, DateTime? at, {required bool h24})

The sentence once booked — always with the date ("on Wed 14 Oct, 6:00 PM"), never "on Today".

ParameterTypeDefaultMeaning
callNameStringrequiredWho calls.
atDateTime?requiredWhen the call rings; null leaves the time out.
h24boolrequiredtrue on a device set to the 24-hour clock.

Returns — String. Example value: 'Booked — Asha will call you on Wed 14 Oct, 6:00 PM'.

Errors — none.

final call = await GraineInApp.instance.scheduleCall(at: DateTime.now().add(const Duration(days: 1)));
final line = Booking.doneLine('Asha', call.at?.toLocal(), h24: false);

Booking.errorLine

Use it to turn a refusal into the one line the sheet shows under its button.

static String errorLine(GraineErrorCode? code, GraineSchedulingRules rules)

The one line the sheet shows under Book call for a refusal, in plain words, with the agent's own numbers:

CodeThe line
tooSoonPick a time at least 5 minutes from now (the notice, counted: "1 minute", "5 minutes")
tooFarPick a time within 14 days (the horizon)
outsideWindowThat time is outside the hours we can call — pick another
tooManyPendingYou already have 3 calls booked — cancel one first (the limit)
schedulingOffBooking a call isn't available right now
identityRequiredSign in to book a call
anything else, or null — rateLimited, unavailable, network, unknownCouldn't book just now — try again
ParameterTypeDefaultMeaning
codeGraineErrorCode?requiredThe refusal's code (GraineException.code).
rulesGraineSchedulingRulesrequiredThe agent's rules, for the numbers.

Returns — String. Example value: 'Pick a time at least 5 minutes from now'.

Errors — none.

String? line;
try {
  await GraineInApp.instance.scheduleCall(at: DateTime.now().add(const Duration(minutes: 1)));
} on GraineException catch (e) {
  line = Booking.errorLine(e.code, GraineInApp.instance.schedulingRules);   // "Pick a time at least 5 minutes from now"
}

BookingBounds

Read it as the earliest and the latest time a booking may take right now.

final class BookingBounds

What to expect — Both ends on the 5-minute grid. Computed for one moment: compute them again as time passes (the SDK's sheet does every 30 seconds).

PropertyTypeMeaning
final DateTime earliestDateTimeNow plus the agent's notice (minLeadMinutes), rounded up to 5 minutes.
final DateTime latestDateTimeNow plus the agent's horizon (maxDaysAhead days), rounded down to 5 minutes.

Its functions: BookingBounds — each with its parameters, what it returns, the errors it raises and an example.

BookingBounds.BookingBounds

Use it to build bounds by hand for a test; Booking.bounds gives the real ones.

const BookingBounds(this.earliest, this.latest)
ParameterTypeDefaultMeaning
earliestDateTimerequiredNow plus the agent's notice (minLeadMinutes), rounded up to 5 minutes.
latestDateTimerequiredNow plus the agent's horizon (maxDaysAhead days), rounded down to 5 minutes.

Returns — a new BookingBounds.

Errors — none.

final bounds = BookingBounds(DateTime(2026, 10, 14, 18, 10), DateTime(2026, 10, 28, 18));

BookingDay

Read it to draw one day chip: its words and the first and last time it offers.

final class BookingDay

What to expect — One chip of the sheet's day row, in order from the first bookable day. first and last are on the 5-minute grid and inside the bounds; on the first day first is the earliest bookable time, on the last day last is the latest.

PropertyTypeMeaning
final String keyStringThe day on the device, YYYY-MM-DD — the chip's key (GraineBookingSheet.dayKey).
final String labelStringThe chip's words: "Today", "Tomorrow", then the weekday and date ("Thu 16 Oct").
final DateTime firstDateTimeThe first time the day offers.
final DateTime lastDateTimeThe last time the day offers.

Its functions: BookingDay — each with its parameters, what it returns, the errors it raises and an example.

BookingDay.BookingDay

Use it to build a day by hand for a test; Booking.days gives the real ones.

const BookingDay({required this.key, required this.label, required this.first, required this.last})
ParameterTypeDefaultMeaning
keyStringrequiredThe day on the device, YYYY-MM-DD — the chip's key (GraineBookingSheet.dayKey).
labelStringrequiredThe chip's words: "Today", "Tomorrow", then the weekday and date ("Thu 16 Oct").
firstDateTimerequiredThe first time the day offers.
lastDateTimerequiredThe last time the day offers.

Returns — a new BookingDay.

Errors — none.

final day = BookingDay(key: '2026-10-16', label: 'Thu 16 Oct', first: DateTime(2026, 10, 16), last: DateTime(2026, 10, 16, 23, 55));

The scheduled-calls list

Book a call from your own screen, list the customer's booked calls and cancel one. Bookings are checked on the device first against the agent's rules (schedulingRules), then by Graine — the calling hours, and how many calls each customer may have waiting. The SDK's chat window shows the booked calls in its Upcoming calls card, with Cancel; GraineInApp.instance.chat exposes the same list for a chat of your own.

GraineInApp.schedulingRules

Read it to know what the agent lets customers book — whether at all, how soon, how far ahead and how many — before you offer times.

GraineSchedulingRules get schedulingRules

What the agent lets customers book, and how Graine rings — from the agent's settings, with no request. Before the settings load, and from an agent that says nothing, the defaults, which allow no booking. earliest(now) and latest(now) bound a booking; scheduleCall checks them before it asks Graine.

Parameters — none.

Returns — GraineSchedulingRules — its fields: GraineSchedulingRules. Example value: usersCanBook: true, minLeadMinutes: 5, maxDaysAhead: 14, maxPendingPerUser: 3.

Errors — none.

final rules = GraineInApp.instance.schedulingRules;
if (rules.usersCanBook) showTimes(from: rules.earliest(DateTime.now()), to: rules.latest(DateTime.now()));

GraineInApp.scheduleCall

Call it from your own booking screen to book a call from the agent at a time the customer chose.

Future<GraineScheduledCall> scheduleCall({required DateTime at, String? reason})

Books a call from the agent at at ("call me tomorrow at 6"). The agent rings this user's devices (with nobody identified, this install) through the app. reason is trimmed, at most 120 characters, kept with the call for the agent, and never put in the push that rings it. Checks made on the device before the booking request is sent: a reason over 120 characters (invalidRequest, checked first); then, once the agent's configuration has loaded, schedulingOff when users may not book, tooSoon before schedulingRules.earliest(now) and tooFar after schedulingRules.latest(now), where now is the device clock. Those three say the booking sheet's words as their message — "Booking a call isn't available right now", "Pick a time at least 5 minutes from now", "Pick a time within 14 days" (the agent's notice and horizon, counted: "1 minute" for one) — the same on every SDK and the website. The time is sent as ISO-8601 with the device's offset. An open chat's Upcoming calls card shows the booking at once.

ParameterTypeDefaultMeaning
atDateTimerequiredWhen the call should ring: a moment in time. It is sent with the device's offset and time zone; it must fall between schedulingRules.earliest(now) and latest(now).
reasonString?optionalWhat the call is about, at most 120 characters — the agent hears it as call_reason when it rings.

Returns — Future<GraineScheduledCall> — the booked call, status scheduled.

Errors — GraineException — on the device: invalidRequest, schedulingOff, tooSoon, tooFar; from Graine: outsideWindow, tooManyPending, identityRequired, deviceLimit, invalidRequest, rateLimited, unavailable, invalidKey, originRefused, network; unknown (codeName "bad_response") when the booking is not confirmed

final call = await GraineInApp.instance.scheduleCall(at: DateTime.now().add(const Duration(hours: 1)), reason: 'follow_up');

GraineInApp.scheduledCalls

Call it to list the customer's booked calls, upcoming first — for a screen of your own.

Future<List<GraineScheduledCall>> scheduledCalls()

This user's calls (with nobody identified, this install's), in Graine's order: upcoming first. Entries that are not calls are skipped.

Parameters — none.

Returns — Future<List<GraineScheduledCall>> — its fields: GraineScheduledCall. Example value: [GraineScheduledCall(id: '6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44', at: 2026-10-15 12:30:00.000Z, status: GraineCallStatus.scheduled, source: GraineCallSource.app, reason: 'follow_up')].

Errors — GraineException: rateLimited, unavailable, invalidKey, originRefused, network, notFound (a server with no booked calls to list), unknown

final result = await GraineInApp.instance.scheduledCalls();

GraineInApp.cancelScheduledCall

Call it when the customer cancels a booked call from your own screen.

Future<GraineCallStatus> cancelScheduledCall(String id)

Cancels one of this user's booked calls. The id is trimmed. Completes with the status Graine reports afterwards — GraineCallStatus.cancelled when it does not say. An open chat's Upcoming calls card drops the call.

ParameterTypeDefaultMeaning
idStringrequiredThe booking's ID (a UUID), from GraineScheduledCall.id.

Returns — Future<GraineCallStatus> — the call's status afterwards. Example value: GraineCallStatus.cancelled.

Errors — GraineException: invalidRequest (an empty id, on the device), notFound (not this user's or install's), rateLimited, unavailable, invalidKey, originRefused, network, unknown

final result = await GraineInApp.instance.cancelScheduledCall('6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44');

GraineChatSession.refreshUpcoming

Call it to read the customer's booked calls again now — after your own screen booked or cancelled one.

Future<void> refreshUpcoming()

Read the booked calls again now.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

await GraineInApp.instance.chat.refreshUpcoming();

GraineChatSession.cancelUpcoming

Call it from your own chat's Upcoming calls card when the customer taps Cancel.

Future<void> cancelUpcoming(String id)

Runs the card's Cancel. The row reads "Cancelling…" until Graine replies and then disappears; if it fails, the row shows the failure and tapping it retries.

ParameterTypeDefaultMeaning
idStringrequiredThe booking's ID, from the chat's upcoming calls.

Returns — nothing; it returns when done.

Errors — none.

await GraineInApp.instance.chat.cancelUpcoming('6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44');

GraineChatSession.cancelStateOf

Use it to draw a booked call's Cancel: not asked, cancelling, or failed.

GraineUpcomingCancel cancelStateOf(String id)

Where a booked call's Cancel is.

ParameterTypeDefaultMeaning
idStringrequiredThe booked call's id (GraineScheduledCall.id).

Returns — GraineUpcomingCancel — its fields: GraineUpcomingCancel. Example value: GraineUpcomingCancel.none.

Errors — none.

final result = GraineInApp.instance.chat.cancelStateOf('6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44');

GraineUpcomingCancel

Read it to draw a booked call's Cancel on your own Upcoming calls card.

enum GraineUpcomingCancel

What to expect — Give every switch over it a default case.

CaseMeaning
noneNot asked.
cancellingThe cancellation has been requested, and the row shows "Cancelling…" until Graine replies.
failedThe cancellation did not go through, and the row reads "Couldn’t cancel · tap to retry".

GraineScheduledCall

Read it to show one booked call: when it rings, what it is about, where it stands, and whether it can still be cancelled.

class GraineScheduledCall

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.

PropertyTypeMeaning
final String idStringThe id Graine gave the booking; a GraineScheduledCall is cancelled by this id.
final DateTime? atDateTime?The UTC time the call rings (use .toLocal() for the user's clock); it is null only if the server's time could not be read.
final GraineCallStatus statusGraineCallStatusWhere the booked call stands: scheduled until it rings, then ringing, answered, completed — or why it did not happen. A status newer than this SDK is unknown (see statusName).
final String statusNameStringThe server's own word for the status; when the status is GraineCallStatus.unknown, this holds the word this build does not recognise.
final GraineCallSource? sourceGraineCallSource?Who made the booking, or null if the server leaves it out.
final String? sourceNameString?The server's own word for the source (sdk, agent, …), or null.
final String? reasonString?What it is about, as booked.
bool get isUpcomingboolStill to come: scheduled or ringing.
bool get canCancelboolIt can still be called off: scheduled.
String get statusWireStringThe status as one word — statusName if the server sent one, otherwise the word for status.

Its functions: GraineScheduledCall, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

GraineScheduledCall.GraineScheduledCall

Use it to build a booked call by hand for a preview or a test.

const GraineScheduledCall({ required this.id, this.at, this.status = GraineCallStatus.scheduled, String? statusName, this.source, this.sourceName, this.reason})
ParameterTypeDefaultMeaning
idStringrequiredThe id Graine gave the booking; a GraineScheduledCall is cancelled by this id.
atDateTime?optionalThe UTC time the call rings (use .toLocal() for the user's clock); it is null only if the server's time could not be read.
statusGraineCallStatusGraineCallStatus.scheduledWhere the booked call stands: scheduled until it rings, then ringing, answered, completed — or why it did not happen. A status newer than this SDK is unknown (see statusName).
statusNameString?optionalThe server's own word for the status; when the status is GraineCallStatus.unknown, this holds the word this build does not recognise.
sourceGraineCallSource?optionalWho made the booking, or null if the server leaves it out.
sourceNameString?optionalThe server's own word for the source (sdk, agent, …), or null.
reasonString?optionalWhat it is about, as booked.

Returns — a new GraineScheduledCall.

Errors — none.

final call = GraineScheduledCall(id: '6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44', at: DateTime.utc(2026, 10, 14, 12, 30), source: GraineCallSource.app, reason: 'Talk through my order');

GraineScheduledCall.fromJson

Use it to read a booked call from JSON you stored.

static GraineScheduledCall? fromJson(Object? raw)

Returns null unless the input is a call that has an id.

ParameterTypeDefaultMeaning
rawObject?requiredThe booked call as Graine sent it; anything without an id reads as null.

Returns — GraineScheduledCall?. Example value: GraineScheduledCall(id: '6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44', at: 2026-10-15 12:30:00.000Z, status: GraineCallStatus.scheduled, source: GraineCallSource.app, reason: 'follow_up').

Errors — none.

final call = GraineScheduledCall.fromJson({'id': 'sc_1', 'at': '2026-10-16T12:30:00Z', 'status': 'scheduled', 'source': 'sdk', 'reason': 'Talk through my order'});

GraineScheduledCall.toJson

Use it to turn a booked call into JSON — to cache it or send it to your own server.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

for (final call in await GraineInApp.instance.scheduledCalls()) {
  debugPrint(call.toJson().toString());
}

GraineCallStatus

Read it to show where a booked call stands, from scheduled to completed — or why it never rang.

enum GraineCallStatus

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.

CaseMeaning
scheduled('scheduled')Booked, waiting for its time.
ringing('ringing')The device is ringing now.
answered('answered')The customer answered the ring; the conversation is under way.
completed('completed')The call took place and is over.
declined('declined')The customer declined the ring. Nothing rings them again for 6 hours.
missed('missed')Rang out with no answer.
failed('failed')The call could not be placed or connected.
cancelled('cancelled')Called off before it rang — by the customer, your app or the agent.
expired('expired')The booked time went by without the call being able to ring, either because it fell outside the calling hours or because it was held too long.
noDevice('no_device')No device of the user's could be rung.
unknown('unknown')A status that this build does not recognise; the call's statusName holds the actual word.
PropertyTypeMeaning
final String wireStringThe word on the wire.
bool get upcomingboolStill to come: it will ring, or is ringing.

Its functions: GraineCallStatus, fromWire — each with its parameters, what it returns, the errors it raises and an example.

GraineCallStatus.GraineCallStatus

The enum's constructor; use the values (GraineCallStatus.scheduled, …).

const GraineCallStatus(this.wire)
ParameterTypeDefaultMeaning
wireStringrequiredThe word on the wire.

Returns — nothing you call: an enum's constructor runs once for each value. Use the values; each carries its word in wire.

Errors — none.

const value = GraineCallStatus.noDevice;
print(value.wire);   // no_device

GraineCallStatus.fromWire

Use it to read a booked call's status from its word.

static GraineCallStatus fromWire(Object? s)
ParameterTypeDefaultMeaning
sObject?requiredThe status's word, as Graine sent it; anything this build does not know reads as unknown.

Returns — GraineCallStatus. Example value: GraineCallStatus.scheduled.

Errors — none.

final status = GraineCallStatus.fromWire('scheduled');   // GraineCallStatus.scheduled

GraineCallSource

Read it to show who booked a call: the agent, your app, your team in the dashboard, or your server.

enum GraineCallSource

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.

CaseMeaning
agent('agent')The agent, in a conversation.
app('sdk')The app, through this SDK.
dashboard('dashboard')Your team booked it in the dashboard.
api('api')Your own server booked it through Graine's API.
unknown('unknown')A source that this build does not recognise; the call's sourceName holds the actual word.
PropertyTypeMeaning
final String wireStringThe word on the wire.

Its functions: GraineCallSource, fromWire — each with its parameters, what it returns, the errors it raises and an example.

GraineCallSource.GraineCallSource

The enum's constructor; use the values (GraineCallSource.app, …).

const GraineCallSource(this.wire)
ParameterTypeDefaultMeaning
wireStringrequiredThe word on the wire.

Returns — nothing you call: an enum's constructor runs once for each value. Use the values; each carries its word in wire.

Errors — none.

const value = GraineCallSource.app;
print(value.wire);   // sdk

GraineCallSource.fromWire

Use it to read who booked a call from its word.

static GraineCallSource? fromWire(Object? s)

Null for no source at all.

ParameterTypeDefaultMeaning
sObject?requiredThe source's word (sdk is app); empty reads as null, anything unknown as unknown.

Returns — GraineCallSource?. Example value: GraineCallSource.agent.

Errors — none.

final source = GraineCallSource.fromWire('sdk');   // GraineCallSource.app

GraineSchedulingRules

Read it to offer only times the agent allows, and to check a time before you book it.

class GraineSchedulingRules

What to expect — A property: no request. Read from the agent's settings when the session loads; 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.

PropertyTypeMeaning
final bool usersCanBookboolscheduleCall is allowed at all.
final int minLeadMinutesintThe least notice a booking needs, in minutes (default 5; the dashboard allows 2–1440). earliest(now) is now plus this.
final int maxDaysAheadintHow far ahead a booking may be, in days (default 14; the dashboard allows 1–30). latest(now) is now plus this.
final int maxPendingPerUserintHow many booked calls one customer may have waiting at once (default 3; 1–10). One more is refused with tooManyPending.
final int ringTimeoutSecondsintHow long a ring lasts before it is missed.
final String? callNameString?The name the phone displays for the agent's calls; when null, the agent's name is shown.
final bool hostedRingingboolTrue when Graine rings the devices of the workspace's users.
final List<String> platformsList<String>Where it rings: ios, android.

Its functions: GraineSchedulingRules, of, earliest, latest — each with its parameters, what it returns, the errors it raises and an example.

GraineSchedulingRules.GraineSchedulingRules

Use it to build rules by hand for a test or a preview; schedulingRules gives the agent's.

const GraineSchedulingRules({ this.usersCanBook = false, this.minLeadMinutes = 5, this.maxDaysAhead = 14, this.maxPendingPerUser = 3, this.ringTimeoutSeconds = 45, this.callName, this.hostedRinging = false, this.platforms = const <String>[]})
ParameterTypeDefaultMeaning
usersCanBookboolfalsescheduleCall is allowed at all.
minLeadMinutesint5The least notice a booking needs, in minutes (default 5; the dashboard allows 2–1440). earliest(now) is now plus this.
maxDaysAheadint14How far ahead a booking may be, in days (default 14; the dashboard allows 1–30). latest(now) is now plus this.
maxPendingPerUserint3How many booked calls one customer may have waiting at once (default 3; 1–10). One more is refused with tooManyPending.
ringTimeoutSecondsint45How long a ring lasts before it is missed.
callNameString?optionalThe name the phone displays for the agent's calls; when null, the agent's name is shown.
hostedRingingboolfalseTrue when Graine rings the devices of the workspace's users.
platformsList<String>const <String>[]Where it rings: ios, android.

Returns — a new GraineSchedulingRules.

Errors — none.

final result = GraineSchedulingRules();

GraineSchedulingRules.of

Use it to read the rules from the session's call settings.

factory GraineSchedulingRules.of(CallsSetting? calls)

The rules in calls, the session's call settings; null (settings that say nothing) allows no booking.

ParameterTypeDefaultMeaning
callsCallsSetting?requiredThe session's call settings (session?.calls); null, from an agent that says nothing, allows no booking.

Returns — GraineSchedulingRules — the defaults (no booking) for anything the settings leave out.

Errors — none.

final rules = GraineSchedulingRules.of(GraineInApp.instance.session?.calls);

GraineSchedulingRules.earliest

Use it to get the soonest time a call can be booked for.

DateTime earliest(DateTime now)

The soonest a call can be booked for: now plus minLeadMinutes.

ParameterTypeDefaultMeaning
nowDateTimerequiredThe time now — pass DateTime.now().

Returns — DateTime.

Errors — none.

final soonest = GraineInApp.instance.schedulingRules.earliest(DateTime.now());

GraineSchedulingRules.latest

Use it to get the latest time a call can be booked for.

DateTime latest(DateTime now)

The latest a call can be booked for: now plus maxDaysAhead days.

ParameterTypeDefaultMeaning
nowDateTimerequiredThe time now — pass DateTime.now().

Returns — DateTime.

Errors — none.

final furthest = GraineInApp.instance.schedulingRules.latest(DateTime.now());

SchedulingSetting

Read it as what the dashboard lets customers book (Agent → Calls & scheduling).

class SchedulingSetting

What to expect — A property: no request. Read from the agent's settings when the session loads; 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.

PropertyTypeMeaning
final bool usersCanBookboolscheduleCall is allowed at all.
final int maxDaysAheadintHow far ahead a booking may be, in days (default 14; read within 1–30).
final int minLeadMinutesintThe least notice a booking needs, in minutes (default 5; read within 2–1440).
final int maxPendingPerUserintHow many booked calls one customer may have waiting (default 3; read within 1–10).

Its functions: SchedulingSetting, fromJson — each with its parameters, what it returns, the errors it raises and an example.

SchedulingSetting.SchedulingSetting

Use it to build the booking setting by hand for a test.

const SchedulingSetting({ this.usersCanBook = false, this.maxDaysAhead = 14, this.minLeadMinutes = 5, this.maxPendingPerUser = 3})
ParameterTypeDefaultMeaning
usersCanBookboolfalsescheduleCall is allowed at all.
maxDaysAheadint14How far ahead a booking may be, in days (default 14; read within 1–30).
minLeadMinutesint5The least notice a booking needs, in minutes (default 5; read within 2–1440).
maxPendingPerUserint3How many booked calls one customer may have waiting (default 3; read within 1–10).

Returns — a new SchedulingSetting.

Errors — none.

final result = SchedulingSetting();

SchedulingSetting.fromJson

Use it to read the booking setting from the session's JSON.

static SchedulingSetting? fromJson(Object? raw)
ParameterTypeDefaultMeaning
rawObject?requiredThe value as decoded from JSON; anything that is not the expected object reads as null.

Returns — SchedulingSetting?. Example value: null.

Errors — none.

final scheduling = SchedulingSetting.fromJson({'usersCanBook': true, 'minLeadMinutes': 5, 'maxDaysAhead': 14, 'maxPendingPerUser': 3});

Design

The design the brand chose in the Garage (Embed & Widgets → Appearance), resolved into light and dark tokens and carried by the session: colours, type, shapes, spacing, depth, motion, icons, the chat window and the call screen. The SDK's own views draw from it; read GraineInApp.instance.tokens to draw your own UI in the same design. A token that is missing or malformed draws as Graine Classic; a design that is not one at all leaves every surface on the dashboard's Appearance.

GraineInApp.loadDesignFonts

Call it to fetch a design's typefaces ahead of time — the SDK already does it when the session arrives.

Future<void> loadDesignFonts(ResolvedDesign? design)

Fetches and registers the faces design draws with. For the session's design the SDK takes care of this on its own; call it yourself for a design you pass straight to a widget. Any family named in GraineConfig.fonts belongs to the app, so it is never downloaded. Calling it more than once is safe, since no face is fetched twice.

ParameterTypeDefaultMeaning
designResolvedDesign?requiredThe design whose typefaces to fetch — usually GraineInApp.instance.design; null does nothing.

Returns — nothing; it returns when done.

Errors — none.

await GraineInApp.instance.loadDesignFonts(GraineInApp.instance.design);

ResolvedDesign

Read it as the brand's design with both its light and dark tokens — and pick the set to draw with.

class ResolvedDesign

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final Map<String, Object?> configMap<String, Object?>The brand's choices as the dashboard saved them. Only scheme is read; the rest is already applied to the tokens.
final ThemeTokens lightThemeTokensThe tokens to draw in light mode.
final ThemeTokens darkThemeTokensThe tokens to draw in dark mode.
String get schemeStringEither "light" or "dark" if the brand set a fixed scheme, or "auto" (follow the device) if not.

Its functions: ResolvedDesign, pick, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ResolvedDesign.ResolvedDesign

Use it to build a design by hand for a preview.

const ResolvedDesign({required this.config, required this.light, required this.dark})
ParameterTypeDefaultMeaning
configMap<String, Object?>requiredThe brand's choices as the dashboard saved them. Only scheme is read; the rest is already applied to the tokens.
lightThemeTokensrequiredThe tokens to draw in light mode.
darkThemeTokensrequiredThe tokens to draw in dark mode.

Returns — a new ResolvedDesign.

Errors — none.

final light = ThemeTokens.fromJson({'colors': {'accent': '#5B5BD6'}}, scheme: 'light');
final dark = ThemeTokens.fromJson({'colors': {'accent': '#8B8BFF'}}, scheme: 'dark');
if (light != null && dark != null) {
  final design = ResolvedDesign(config: const {'scheme': 'auto'}, light: light, dark: dark);
}

ResolvedDesign.pick

Use it to choose the token set to draw with: the brand's fixed scheme, else the device's light or dark.

ThemeTokens pick({bool? deviceDark, String? scheme})

Which set to draw.

ParameterTypeDefaultMeaning
deviceDarkbool?optionalWhether the device is in dark mode — MediaQuery.platformBrightnessOf(context) == Brightness.dark; null when unknown, read as light.
schemeString?optionalYour own choice — light, dark or auto — over the brand's; the brand's when left out.

Returns — ThemeTokens — its fields: ThemeTokens.

Errors — none.

final tokens = GraineInApp.instance.design?.pick(deviceDark: MediaQuery.platformBrightnessOf(context) == Brightness.dark);

ResolvedDesign.fromJson

Use it to read a design from the session's JSON.

static ResolvedDesign? fromJson(Object? raw)

Returns null for input that is not a design. An older server sends no design, and the SDK then keeps drawing from appearance as it used to.

ParameterTypeDefaultMeaning
rawObject?requiredThe session's design, as decoded; anything that is not a design reads as null.

Returns — ResolvedDesign?. Example value: null.

Errors — none.

final design = ResolvedDesign.fromJson({
  'config': {'scheme': 'auto'},
  'light': {'colors': {'accent': '#5B5BD6'}},
  'dark': {'colors': {'accent': '#8B8BFF'}},
});

ResolvedDesign.toJson

Use it to turn a design back into JSON.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.design?.toJson();

ThemeTokens

Use it to draw your own UI in the agent's design: everything a surface needs for one scheme, light or dark.

class ThemeTokens

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final int vintThe tokens' format version (1).
final String presetStringThe design the brand started from, by name.
final String schemeString"light" or "dark".
final ThemeColors colorsThemeColorsEvery colour a surface uses.
final ThemeFont fontThemeFontFamilies, sizes, weights and spacing.
final ThemeShape shapeThemeShapeCorner radii, outline width and the launcher's shape.
final ThemeSpacing spacingThemeSpacingThe base step, card padding and gaps.
final ThemeElevation elevationThemeElevationShadows, or a solid offset.
final ThemeMotion motionThemeMotionHow long transitions take and how they ease.
final ThemeIcons iconsThemeIconsThe icons' style and stroke, and the launcher's glyph.
final ThemeGlass? glassThemeGlass?Null: opaque surfaces.
final String bubblesStringOne of Design.bubbleStyles.
final String chromeStringA value from Design.chromes. With "window", the chat window, the caption card and each component card open as desktop windows (the measurements are in WindowChrome); "none", which is also what an older server that never sends it means, gives a plain surface.
final ThemeChat? servedChatThemeChat?The chat window's tokens, unchanged from the session; null when the server is older than these tokens.
final ThemeCall? servedCallThemeCall?The call screen's tokens, unchanged from the session; null when the server is older than these tokens.
bool get darkboolTrue for the dark set.
bool get windowboolTrue if the design's chrome is "window", so its surfaces open as desktop windows.
ThemeChat get chatThemeChatThe tokens for the chat window: the session's when present, otherwise ones that reproduce the window as it looked before these tokens existed.
ThemeCall get callThemeCallThe tokens for the call screen: the session's when present, otherwise the fixed dark screen.

Its functions: ThemeTokens, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeTokens.ThemeTokens

Use it to build a token set by hand — for a preview or a test; the session's design carries the real one.

const ThemeTokens({ this.v = 1, required this.preset, required this.scheme, required this.colors, required this.font, required this.shape, required this.spacing, required this.elevation, required this.motion, required this.icons, this.glass, required this.bubbles, this.chrome = 'none', this.servedChat, this.servedCall})
ParameterTypeDefaultMeaning
vint1The tokens' format version (1).
presetStringrequiredThe design the brand started from, by name.
schemeStringrequired"light" or "dark".
colorsThemeColorsrequiredEvery colour a surface uses.
fontThemeFontrequiredFamilies, sizes, weights and spacing.
shapeThemeShaperequiredCorner radii, outline width and the launcher's shape.
spacingThemeSpacingrequiredThe base step, card padding and gaps.
elevationThemeElevationrequiredShadows, or a solid offset.
motionThemeMotionrequiredHow long transitions take and how they ease.
iconsThemeIconsrequiredThe icons' style and stroke, and the launcher's glyph.
glassThemeGlass?optionalNull: opaque surfaces.
bubblesStringrequiredOne of Design.bubbleStyles.
chromeString'none'A value from Design.chromes. With "window", the chat window, the caption card and each component card open as desktop windows (the measurements are in WindowChrome); "none", which is also what an older server that never sends it means, gives a plain surface.
servedChatThemeChat?optionalThe chat window's tokens, unchanged from the session; null when the server is older than these tokens.
servedCallThemeCall?optionalThe call screen's tokens, unchanged from the session; null when the server is older than these tokens.

Returns — a new ThemeTokens.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the design's; build one by hand only for a preview

ThemeTokens.fromJson

Use it to read a token set from the design's JSON — the SDK does this when the session arrives.

static ThemeTokens? fromJson(Object? raw, {String scheme = 'light'})

One scheme's tokens. scheme names the slot the tokens came in, and applies when the tokens themselves do not specify one. Returns null if raw is not an object containing a colors object, since that is too little to count as a design.

ParameterTypeDefaultMeaning
rawObject?requiredOne scheme's tokens as decoded; null without a colors object.
schemeString'light'The slot the tokens arrived in, light or dark, used when the tokens do not say.

Returns — ThemeTokens?. Example value: null.

Errors — none.

final tokens = ThemeTokens.fromJson({'colors': {'accent': '#5B5BD6'}, 'font': {'body': 'DM Sans'}}, scheme: 'light');

ThemeTokens.toJson

Use it to turn a token set back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Writes the tokens back in the session's form, matching what fromJson read key by key.

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.toJson();

ThemeColors

Use it to colour your own UI like the agent's: every colour a surface uses.

class ThemeColors

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final String backgroundStringBehind everything in the chat window.
final String surfaceStringThe voice bar, the chat header, sheets.
final String surfaceRaisedStringCards, the composer field.
final String textStringThe main text colour.
final String textMutedStringSecondary text: captions, status lines.
final String borderStringHairlines and outlines.
final String accentStringApplied to primary buttons, as well as the launcher and the send button.
final String onAccentStringWhat sits on accent.
final String accentSoftStringA faint version of the accent, for chips, selected rows and the focus halo.
final String successStringGood news: "Online", a done state.
final String warningStringCaution: a weak connection, a booking about to lapse.
final String dangerStringThe end-call button.
final String userBubbleStringThe customer's chat bubbles.
final String userBubbleTextStringText on the customer's bubbles.
final String agentBubbleStringThe agent's chat bubbles.
final String agentBubbleTextStringText on the agent's bubbles.
final String scrimStringBehind a modal sheet, at 40% opacity.

Its functions: ThemeColors, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeColors.ThemeColors

Use it to build a colour set by hand — for a preview or a test; the session's design carries the real one.

const ThemeColors({ required this.background, required this.surface, required this.surfaceRaised, required this.text, required this.textMuted, required this.border, required this.accent, required this.onAccent, required this.accentSoft, required this.success, required this.warning, required this.danger, required this.userBubble, required this.userBubbleText, required this.agentBubble, required this.agentBubbleText, required this.scrim})
ParameterTypeDefaultMeaning
backgroundStringrequiredBehind everything in the chat window.
surfaceStringrequiredThe voice bar, the chat header, sheets.
surfaceRaisedStringrequiredCards, the composer field.
textStringrequiredThe main text colour.
textMutedStringrequiredSecondary text: captions, status lines.
borderStringrequiredHairlines and outlines.
accentStringrequiredApplied to primary buttons, as well as the launcher and the send button.
onAccentStringrequiredWhat sits on accent.
accentSoftStringrequiredA faint version of the accent, for chips, selected rows and the focus halo.
successStringrequiredGood news: "Online", a done state.
warningStringrequiredCaution: a weak connection, a booking about to lapse.
dangerStringrequiredThe end-call button.
userBubbleStringrequiredThe customer's chat bubbles.
userBubbleTextStringrequiredText on the customer's bubbles.
agentBubbleStringrequiredThe agent's chat bubbles.
agentBubbleTextStringrequiredText on the agent's bubbles.
scrimStringrequiredBehind a modal sheet, at 40% opacity.

Returns — a new ThemeColors.

Errors — none.

const colors = ThemeColors(background: '#F7F7F5', surface: '#FFFFFF', surfaceRaised: '#FFFFFF', text: '#101318', textMuted: '#5F6672', border: '#E5E4E0', accent: '#318CE7', onAccent: '#FFFFFF', accentSoft: '#E6F1FC', success: '#16A34A', warning: '#B45309', danger: '#DC2626', userBubble: '#318CE7', userBubbleText: '#FFFFFF', agentBubble: '#F1F1EF', agentBubbleText: '#101318', scrim: '#000000');

ThemeColors.fromJson

Use it to read a colour set from the design's JSON — the SDK does this when the session arrives.

static ThemeColors fromJson(Map<String, Object?> r, ThemeColors d)
ParameterTypeDefaultMeaning
rMap<String, Object?>requiredThe JSON object to read.
dThemeColorsrequiredThe colours to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeColors.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final colours = ThemeColors.fromJson({'accent': '#5B5BD6'}, tokens.colors);   // another accent
}

ThemeColors.toJson

Use it to turn a colour set back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.colors.toJson();

ThemeFont

Use it to set your own text like the agent's: families, sizes, weights and spacing.

class ThemeFont

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final String bodyStringEither "system", meaning the platform's UI font, or a family from the dashboard's list; the SDK downloads that family's faces on its own (see DesignFonts).
final String headingStringThe titles' family: "system" or a family from the dashboard's list.
final ThemeFontScale scaleThemeFontScaleThe sizes: caption, body, title, display.
final ThemeFontWeights weightsThemeFontWeightsThe weights: body, strong, title.
final ThemeFontPair letterSpacingThemeFontPairIn em: multiply by the size.
final ThemeFontPair lineHeightThemeFontPairLine height as a multiple of the size, for body and titles.

Its functions: ThemeFont, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeFont.ThemeFont

Use it to build a type set by hand — for a preview or a test; the session's design carries the real one.

const ThemeFont({ required this.body, required this.heading, required this.scale, required this.weights, required this.letterSpacing, required this.lineHeight})
ParameterTypeDefaultMeaning
bodyStringrequiredEither "system", meaning the platform's UI font, or a family from the dashboard's list; the SDK downloads that family's faces on its own (see DesignFonts).
headingStringrequiredThe titles' family: "system" or a family from the dashboard's list.
scaleThemeFontScalerequiredThe sizes: caption, body, title, display.
weightsThemeFontWeightsrequiredThe weights: body, strong, title.
letterSpacingThemeFontPairrequiredIn em: multiply by the size.
lineHeightThemeFontPairrequiredLine height as a multiple of the size, for body and titles.

Returns — a new ThemeFont.

Errors — none.

const font = ThemeFont(body: 'Inter', heading: 'Inter', scale: ThemeFontScale(caption: 12, body: 15, title: 17, display: 22), weights: ThemeFontWeights(body: 400, strong: 600, title: 600), letterSpacing: ThemeFontPair(body: 0, title: -0.01), lineHeight: ThemeFontPair(body: 1.45, title: 1.25));

ThemeFont.fromJson

Use it to read a type set from the design's JSON — the SDK does this when the session arrives.

static ThemeFont fromJson(Object? raw, ThemeFont d)
ParameterTypeDefaultMeaning
rawObject?requiredThe type as decoded.
dThemeFontrequiredThe type to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeFont.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final font = ThemeFont.fromJson({'heading': 'Newsreader'}, tokens.font);   // another heading face
}

ThemeFont.toJson

Use it to turn a type set back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.font.toJson();

ThemeFontScale

Use it to size your text like the agent's.

class ThemeFontScale

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final double captiondoubleThe smallest size — timestamps, captions — in logical pixels.
final double bodydoubleBody text, in logical pixels.
final double titledoubleTitles and headers, in logical pixels.
final double displaydoubleThe largest size — a call screen's name — in logical pixels.

Its functions: ThemeFontScale, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeFontScale.ThemeFontScale

Use it to build a size scale by hand — for a preview or a test; the session's design carries the real one.

const ThemeFontScale({required this.caption, required this.body, required this.title, required this.display})
ParameterTypeDefaultMeaning
captiondoublerequiredThe smallest size — timestamps, captions — in logical pixels.
bodydoublerequiredBody text, in logical pixels.
titledoublerequiredTitles and headers, in logical pixels.
displaydoublerequiredThe largest size — a call screen's name — in logical pixels.

Returns — a new ThemeFontScale.

Errors — none.

const scale = ThemeFontScale(caption: 12, body: 15, title: 17, display: 22);

ThemeFontScale.fromJson

Use it to read a size scale from the design's JSON — the SDK does this when the session arrives.

static ThemeFontScale fromJson(Object? raw, ThemeFontScale d)
ParameterTypeDefaultMeaning
rawObject?requiredThe sizes as decoded.
dThemeFontScalerequiredThe sizes to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeFontScale.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final scale = ThemeFontScale.fromJson({'body': 16}, tokens.font.scale);
}

ThemeFontScale.toJson

Use it to turn a size scale back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.font.scale.toJson();

ThemeFontWeights

Use it to weight your text like the agent's.

class ThemeFontWeights

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final int bodyintBody text's weight, 100–900.
final int strongintEmphasis's weight, 100–900.
final int titleintTitles' weight, 100–900.

Its functions: ThemeFontWeights, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeFontWeights.ThemeFontWeights

Use it to build a weight set by hand — for a preview or a test; the session's design carries the real one.

const ThemeFontWeights({required this.body, required this.strong, required this.title})
ParameterTypeDefaultMeaning
bodyintrequiredBody text's weight, 100–900.
strongintrequiredEmphasis's weight, 100–900.
titleintrequiredTitles' weight, 100–900.

Returns — a new ThemeFontWeights.

Errors — none.

const weights = ThemeFontWeights(body: 400, strong: 600, title: 600);

ThemeFontWeights.fromJson

Use it to read a weight set from the design's JSON — the SDK does this when the session arrives.

static ThemeFontWeights fromJson(Object? raw, ThemeFontWeights d)
ParameterTypeDefaultMeaning
rawObject?requiredThe weights as decoded.
dThemeFontWeightsrequiredThe weights to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeFontWeights.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final weights = ThemeFontWeights.fromJson({'title': 700}, tokens.font.weights);
}

ThemeFontWeights.toJson

Use it to turn a weight set back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.font.weights.toJson();

ThemeFontPair

Use it to space your text like the agent's — letter spacing and line height, for body and titles.

class ThemeFontPair

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final double bodydoubleThe value for body text.
final double titledoubleThe value for titles.

Its functions: ThemeFontPair, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeFontPair.ThemeFontPair

Use it to build a body-and-title pair by hand — for a preview or a test; the session's design carries the real one.

const ThemeFontPair({required this.body, required this.title})
ParameterTypeDefaultMeaning
bodydoublerequiredThe value for body text.
titledoublerequiredThe value for titles.

Returns — a new ThemeFontPair.

Errors — none.

const lineHeight = ThemeFontPair(body: 1.45, title: 1.25);

ThemeFontPair.fromJson

Use it to read a body-and-title pair from the design's JSON — the SDK does this when the session arrives.

static ThemeFontPair fromJson(Object? raw, ThemeFontPair d, {double? min})
ParameterTypeDefaultMeaning
rawObject?requiredThe pair as decoded.
dThemeFontPairrequiredThe values to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.
mindouble?optionalThe least value allowed; none when left out.

Returns — ThemeFontPair.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final lineHeight = ThemeFontPair.fromJson({'body': 1.5}, tokens.font.lineHeight, min: 0.5);
}

ThemeFontPair.toJson

Use it to turn a body-and-title pair back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.font.lineHeight.toJson();

ThemeShape

Use it to shape your UI like the agent's: corner radii, outlines and the launcher's shape.

class ThemeShape

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final ThemeRadius radiusThemeRadiusThe four corner radii.
final double borderWidthdoubleThe width of every outline.
final String launcherStringOne of Design.launcherShapes.

Its functions: ThemeShape, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeShape.ThemeShape

Use it to build a shape set by hand — for a preview or a test; the session's design carries the real one.

const ThemeShape({required this.radius, required this.borderWidth, required this.launcher})
ParameterTypeDefaultMeaning
radiusThemeRadiusrequiredThe four corner radii.
borderWidthdoublerequiredThe width of every outline.
launcherStringrequiredOne of Design.launcherShapes.

Returns — a new ThemeShape.

Errors — none.

const shape = ThemeShape(radius: ThemeRadius(sm: 8, md: 12, lg: 18, xl: 24), borderWidth: 1, launcher: 'circle');

ThemeShape.fromJson

Use it to read a shape set from the design's JSON — the SDK does this when the session arrives.

static ThemeShape fromJson(Object? raw, ThemeShape d)
ParameterTypeDefaultMeaning
rawObject?requiredThe shape as decoded.
dThemeShaperequiredThe shape to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeShape.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final shape = ThemeShape.fromJson({'launcher': 'pill'}, tokens.shape);
}

ThemeShape.toJson

Use it to turn a shape set back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.shape.toJson();

ThemeRadius

Use it to round your corners like the agent's.

class ThemeRadius

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final double smdoubleThe smallest corner radius — chips, small buttons — in logical pixels.
final double mddoubleThe middle radius — inputs, cards inside cards.
final double lgdoubleThe large radius — cards and sheets.
final double xldoubleThe largest radius — the bar, the chat window, the booking sheet.

Its functions: ThemeRadius, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeRadius.ThemeRadius

Use it to build a radius scale by hand — for a preview or a test; the session's design carries the real one.

const ThemeRadius({required this.sm, required this.md, required this.lg, required this.xl})
ParameterTypeDefaultMeaning
smdoublerequiredThe smallest corner radius — chips, small buttons — in logical pixels.
mddoublerequiredThe middle radius — inputs, cards inside cards.
lgdoublerequiredThe large radius — cards and sheets.
xldoublerequiredThe largest radius — the bar, the chat window, the booking sheet.

Returns — a new ThemeRadius.

Errors — none.

const radius = ThemeRadius(sm: 8, md: 12, lg: 18, xl: 24);

ThemeRadius.fromJson

Use it to read a radius scale from the design's JSON — the SDK does this when the session arrives.

static ThemeRadius fromJson(Object? raw, ThemeRadius d)
ParameterTypeDefaultMeaning
rawObject?requiredThe radii as decoded.
dThemeRadiusrequiredThe radii to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeRadius.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final radius = ThemeRadius.fromJson({'md': 12}, tokens.shape.radius);
}

ThemeRadius.toJson

Use it to turn a radius scale back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.shape.radius.toJson();

ThemeSpacing

Use it to space your UI like the agent's.

class ThemeSpacing

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final double unitdoubleThe base spacing step, in logical pixels — every gap is a multiple of it.
final double cardPaddingdoublePadding inside cards, in logical pixels.
final double gapdoubleBetween items.

Its functions: ThemeSpacing, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeSpacing.ThemeSpacing

Use it to build a spacing set by hand — for a preview or a test; the session's design carries the real one.

const ThemeSpacing({required this.unit, required this.cardPadding, required this.gap})
ParameterTypeDefaultMeaning
unitdoublerequiredThe base spacing step, in logical pixels — every gap is a multiple of it.
cardPaddingdoublerequiredPadding inside cards, in logical pixels.
gapdoublerequiredBetween items.

Returns — a new ThemeSpacing.

Errors — none.

const spacing = ThemeSpacing(unit: 4, cardPadding: 14, gap: 10);

ThemeSpacing.fromJson

Use it to read a spacing set from the design's JSON — the SDK does this when the session arrives.

static ThemeSpacing fromJson(Object? raw, ThemeSpacing d)
ParameterTypeDefaultMeaning
rawObject?requiredThe spacing as decoded.
dThemeSpacingrequiredThe spacing to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeSpacing.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final spacing = ThemeSpacing.fromJson({'gap': 12}, tokens.spacing);
}

ThemeSpacing.toJson

Use it to turn a spacing set back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.spacing.toJson();

ThemeElevation

Use it to give your UI the agent's depth: shadows, or a solid offset.

class ThemeElevation

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final ThemeShadow? cardThemeShadow?The shadow under cards; null for none.
final ThemeShadow? floatingThemeShadow?The shadow under floating surfaces — the bar, sheets; null for none.
final ThemeOffset? hardOffsetThemeOffset?A solid offset shadow drawn in the text colour; when set it replaces both shadows.

Its functions: ThemeElevation, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeElevation.ThemeElevation

Use it to build an elevation by hand — for a preview or a test; the session's design carries the real one.

const ThemeElevation({this.card, this.floating, this.hardOffset})
ParameterTypeDefaultMeaning
cardThemeShadow?optionalThe shadow under cards; null for none.
floatingThemeShadow?optionalThe shadow under floating surfaces — the bar, sheets; null for none.
hardOffsetThemeOffset?optionalA solid offset shadow drawn in the text colour; when set it replaces both shadows.

Returns — a new ThemeElevation.

Errors — none.

final result = ThemeElevation();

ThemeElevation.fromJson

Use it to read an elevation from the design's JSON — the SDK does this when the session arrives.

static ThemeElevation fromJson(Object? raw, ThemeElevation d)

A missing key takes the fallback's value; a key that is there, even if null, is taken from the design.

ParameterTypeDefaultMeaning
rawObject?requiredThe elevation as decoded; a key that is present — null included — is the design's word.
dThemeElevationrequiredThe elevation to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeElevation.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final elevation = ThemeElevation.fromJson({'card': null}, tokens.elevation);   // no shadow under cards
}

ThemeElevation.toJson

Use it to turn an elevation back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.elevation.toJson();

ThemeShadow

Use it to draw one of the design's shadows.

class ThemeShadow

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final double ydoubleHow far down the shadow falls, in logical pixels.
final double blurdoubleHow soft it is: the blur radius, in logical pixels (0 or more).
final double spreaddoubleHow far it grows beyond the surface, in logical pixels.
final double opacitydouble0–1, applied to color.
final String colorStringThe shadow's colour, #RRGGBB; opacity applies to it.

Its functions: ThemeShadow, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeShadow.ThemeShadow

Use it to build a shadow by hand — for a preview or a test; the session's design carries the real one.

const ThemeShadow({ required this.y, required this.blur, required this.spread, required this.opacity, required this.color})
ParameterTypeDefaultMeaning
ydoublerequiredHow far down the shadow falls, in logical pixels.
blurdoublerequiredHow soft it is: the blur radius, in logical pixels (0 or more).
spreaddoublerequiredHow far it grows beyond the surface, in logical pixels.
opacitydoublerequired0–1, applied to color.
colorStringrequiredThe shadow's colour, #RRGGBB; opacity applies to it.

Returns — a new ThemeShadow.

Errors — none.

const shadow = ThemeShadow(y: 8, blur: 24, spread: 0, opacity: 0.16, color: '#000000');

ThemeShadow.fromJson

Use it to read a shadow from the design's JSON — the SDK does this when the session arrives.

static ThemeShadow? fromJson(Object? raw)

Returns null when given null (meaning no shadow) and when given anything that is not a shadow.

ParameterTypeDefaultMeaning
rawObject?requiredOne shadow as decoded; null for none.

Returns — ThemeShadow?. Example value: null.

Errors — none.

final shadow = ThemeShadow.fromJson({'y': 8, 'blur': 24, 'spread': 0, 'opacity': 0.16, 'color': '#000000'});

ThemeShadow.toJson

Use it to turn a shadow back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.elevation.floating?.toJson();

ThemeOffset

Use it to draw the design's solid offset shadow.

class ThemeOffset

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final double xdoubleHow far right the solid shadow sits, in logical pixels.
final double ydoubleHow far down the solid shadow sits, in logical pixels.

Its functions: ThemeOffset, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeOffset.ThemeOffset

Use it to build an offset shadow by hand — for a preview or a test; the session's design carries the real one.

const ThemeOffset({required this.x, required this.y})
ParameterTypeDefaultMeaning
xdoublerequiredHow far right the solid shadow sits, in logical pixels.
ydoublerequiredHow far down the solid shadow sits, in logical pixels.

Returns — a new ThemeOffset.

Errors — none.

const offset = ThemeOffset(x: 4, y: 4);

ThemeOffset.fromJson

Use it to read an offset shadow from the design's JSON — the SDK does this when the session arrives.

static ThemeOffset? fromJson(Object? raw)
ParameterTypeDefaultMeaning
rawObject?requiredThe offset as decoded; null for none.

Returns — ThemeOffset?. Example value: null.

Errors — none.

final offset = ThemeOffset.fromJson({'x': 4, 'y': 4});

ThemeOffset.toJson

Use it to turn an offset shadow back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.elevation.hardOffset?.toJson();

ThemeMotion

Use it to animate your UI at the design's pace.

class ThemeMotion

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final int durationMsint0 means no transitions at all.
final String easingStringOne of Design.easings.
final double bouncedoubleHow far a spring goes past its target, from 0 (no overshoot) upward.
bool get noneboolTrue when there is no transition: the duration is zero, or the easing is none.

Its functions: ThemeMotion, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeMotion.ThemeMotion

Use it to build a motion setting by hand — for a preview or a test; the session's design carries the real one.

const ThemeMotion({required this.durationMs, required this.easing, required this.bounce})
ParameterTypeDefaultMeaning
durationMsintrequired0 means no transitions at all.
easingStringrequiredOne of Design.easings.
bouncedoublerequiredHow far a spring goes past its target, from 0 (no overshoot) upward.

Returns — a new ThemeMotion.

Errors — none.

const motion = ThemeMotion(durationMs: 200, easing: 'standard', bounce: 0);

ThemeMotion.fromJson

Use it to read a motion setting from the design's JSON — the SDK does this when the session arrives.

static ThemeMotion fromJson(Object? raw, ThemeMotion d)
ParameterTypeDefaultMeaning
rawObject?requiredThe motion as decoded.
dThemeMotionrequiredThe motion to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeMotion.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final motion = ThemeMotion.fromJson({'durationMs': 160, 'easing': 'emphasized'}, tokens.motion);
}

ThemeMotion.toJson

Use it to turn a motion setting back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.motion.toJson();

ThemeIcons

Use it to draw icons in the design's style and stroke, and to know the launcher's glyph.

class ThemeIcons

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final String styleStringOne of Design.iconStyles.
final double strokedoubleHow thick the lines are, in units of the glyph's 24-unit box.
final LauncherGlyph? launcherLauncherGlyph?Null: the launcher shows the brand's logo.

Its functions: ThemeIcons, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeIcons.ThemeIcons

Use it to build an icon setting by hand — for a preview or a test; the session's design carries the real one.

const ThemeIcons({required this.style, required this.stroke, this.launcher})
ParameterTypeDefaultMeaning
styleStringrequiredOne of Design.iconStyles.
strokedoublerequiredHow thick the lines are, in units of the glyph's 24-unit box.
launcherLauncherGlyph?optionalNull: the launcher shows the brand's logo.

Returns — a new ThemeIcons.

Errors — none.

const icons = ThemeIcons(style: 'outline', stroke: 2);

ThemeIcons.fromJson

Use it to read an icon setting from the design's JSON — the SDK does this when the session arrives.

static ThemeIcons fromJson(Object? raw, ThemeIcons d)
ParameterTypeDefaultMeaning
rawObject?requiredThe icons as decoded.
dThemeIconsrequiredThe icon setting to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeIcons.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final icons = ThemeIcons.fromJson({'style': 'filled'}, tokens.icons);
}

ThemeIcons.toJson

Use it to turn an icon setting back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.icons.toJson();

LauncherGlyph

Use it to draw the launcher's glyph — the mark that stands for the agent.

class LauncherGlyph

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final String idStringThe glyph's name in the dashboard's set.
final String pathStringIts SVG path data, in a 24 × 24 box.
final bool strokeboolTrue: drawn as lines at icons.stroke; false: filled.

Its functions: LauncherGlyph, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

LauncherGlyph.LauncherGlyph

Use it to build a glyph by hand — for a preview or a test; the session's design carries the real one.

const LauncherGlyph({required this.id, required this.path, required this.stroke})
ParameterTypeDefaultMeaning
idStringrequiredThe glyph's name in the dashboard's set.
pathStringrequiredIts SVG path data, in a 24 × 24 box.
strokeboolrequiredTrue: drawn as lines at icons.stroke; false: filled.

Returns — a new LauncherGlyph.

Errors — none.

const glyph = LauncherGlyph(id: 'waveform', path: 'M4 12h2M8 8v8M12 5v14M16 8v8M20 12h-2', stroke: true);

LauncherGlyph.fromJson

Use it to read a glyph from the design's JSON — the SDK does this when the session arrives.

static LauncherGlyph? fromJson(Object? raw)
ParameterTypeDefaultMeaning
rawObject?requiredThe glyph as decoded; null for none.

Returns — LauncherGlyph?. Example value: null.

Errors — none.

final glyph = LauncherGlyph.fromJson({'id': 'waveform', 'stroke': true});

LauncherGlyph.toJson

Use it to turn a glyph back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.icons.launcher?.toJson();

ThemeGlass

Use it to draw translucent, blurred surfaces when the design has them.

class ThemeGlass

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final double blurdoubleHow strongly the content behind is blurred as it shows through, given as a standard deviation, the way CSS's blur() expects.
final double surfaceOpacitydoublecolors.surface at this opacity, 0–1.

Its functions: ThemeGlass, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeGlass.ThemeGlass

Use it to build a glass setting by hand — for a preview or a test; the session's design carries the real one.

const ThemeGlass({required this.blur, required this.surfaceOpacity})
ParameterTypeDefaultMeaning
blurdoublerequiredHow strongly the content behind is blurred as it shows through, given as a standard deviation, the way CSS's blur() expects.
surfaceOpacitydoublerequiredcolors.surface at this opacity, 0–1.

Returns — a new ThemeGlass.

Errors — none.

const glass = ThemeGlass(blur: 20, surfaceOpacity: 0.72);

ThemeGlass.fromJson

Use it to read a glass setting from the design's JSON — the SDK does this when the session arrives.

static ThemeGlass? fromJson(Object? raw)
ParameterTypeDefaultMeaning
rawObject?requiredThe glass as decoded; null for opaque surfaces.

Returns — ThemeGlass?. Example value: null.

Errors — none.

final glass = ThemeGlass.fromJson({'blur': 20, 'surfaceOpacity': 0.72});

ThemeGlass.toJson

Use it to turn a glass setting back into the design's JSON — to store it or compare it.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.glass?.toJson();

ThemeChat

Use it to draw a chat window of your own like the agent's: its words, colours and which pieces it shows.

class ThemeChat

What to expect — Resolved from the agent's design in the Garage when the session loads; 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 const List<String> avatarsList<String>What may stand for the agent in the header: logo, glyph, none.
static const List<String> patternsList<String>The backgrounds behind the messages: none, dots.
static const int maxSuggestionsintThe most conversation starters a chat shows.
final String titleStringTitle shown in the header; an empty string ("") means the dashboard's heading.
final String subtitleStringThe line under the title while online; an empty string ("") means "Online".
final String avatarStringWhich mark the header shows for the agent: logo, glyph or none.
final String placeholderStringPlaceholder text in the composer; an empty string ("") means "Message…".
final bool showTimestampsboolShow the time under each message.
final List<String> suggestionsList<String>Starters offered in a new chat, up to maxSuggestions of them.
final String backgroundStringBehind the messages and the composer strip.
final String patternStringWith dots, a dot pattern in patternColor is drawn over background.
final String patternColorStringThe dots' colour, when pattern is dots.
final String headerBackgroundStringThe header's fill.
final String headerTextStringThe header's title.
final String headerSubtextStringThe header's subtitle ("Online").
final String composerBackgroundStringThe message field's fill.
final String composerTextStringWhat the customer types.
final String composerPlaceholderStringThe field's placeholder.
final String composerBorderStringThe field's outline.
final GraineChatSize sizeGraineChatSizeThe size the window opens at (Window size in the dashboard); compact unless the design chooses another.
final bool showUpcomingCallsboolWhether a card at the top of the chat lists the calls this user has booked (Booked calls in the dashboard).
final String upcomingTitleStringTitle of the booked-calls card; an empty string ("") means "Upcoming call" / "Upcoming calls".
final bool showHistoryboolWhether the earlier conversation that was kept is shown on screen (Earlier messages).
final bool newConversationboolWhether the header has its compose control, the New conversation button.

Its functions: ThemeChat, fallback, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeChat.ThemeChat

Use it to build a set of chat window tokens by hand — for a preview or a test; the session's design carries the real one.

const ThemeChat({ this.title = '', this.subtitle = '', this.avatar = 'logo', this.placeholder = '', this.showTimestamps = false, this.suggestions = const <String>[], required this.background, this.pattern = 'none', required this.patternColor, required this.headerBackground, required this.headerText, required this.headerSubtext, required this.composerBackground, required this.composerText, required this.composerPlaceholder, required this.composerBorder, this.size = GraineChatSize.compact, this.showUpcomingCalls = true, this.upcomingTitle = '', this.showHistory = true, this.newConversation = true})
ParameterTypeDefaultMeaning
titleString''Title shown in the header; an empty string ("") means the dashboard's heading.
subtitleString''The line under the title while online; an empty string ("") means "Online".
avatarString'logo'Which mark the header shows for the agent: logo, glyph or none.
placeholderString''Placeholder text in the composer; an empty string ("") means "Message…".
showTimestampsboolfalseShow the time under each message.
suggestionsList<String>const <String>[]Starters offered in a new chat, up to maxSuggestions of them.
backgroundStringrequiredBehind the messages and the composer strip.
patternString'none'With dots, a dot pattern in patternColor is drawn over background.
patternColorStringrequiredThe dots' colour, when pattern is dots.
headerBackgroundStringrequiredThe header's fill.
headerTextStringrequiredThe header's title.
headerSubtextStringrequiredThe header's subtitle ("Online").
composerBackgroundStringrequiredThe message field's fill.
composerTextStringrequiredWhat the customer types.
composerPlaceholderStringrequiredThe field's placeholder.
composerBorderStringrequiredThe field's outline.
sizeGraineChatSizeGraineChatSize.compactThe size the window opens at (Window size in the dashboard); compact unless the design chooses another.
showUpcomingCallsbooltrueWhether a card at the top of the chat lists the calls this user has booked (Booked calls in the dashboard).
upcomingTitleString''Title of the booked-calls card; an empty string ("") means "Upcoming call" / "Upcoming calls".
showHistorybooltrueWhether the earlier conversation that was kept is shown on screen (Earlier messages).
newConversationbooltrueWhether the header has its compose control, the New conversation button.

Returns — a new ThemeChat.

Errors — none.

const chat = ThemeChat(title: 'Help', background: '#F7F7F5', patternColor: '#E2E2DF', headerBackground: '#FFFFFF', headerText: '#101318', headerSubtext: '#5F6672', composerBackground: '#FFFFFF', composerText: '#101318', composerPlaceholder: '#5F6672', composerBorder: '#E5E4E0');

ThemeChat.fallback

Use it to get the chat window an agent with no chat tokens draws.

factory ThemeChat.fallback(ThemeColors c, {required bool dark, required bool hasGlyph})

The chat an older server gets: the window drawn the way it was before these tokens, with the page, the surface header, the raised composer and the launcher's mark (which is the design's glyph when there is one).

ParameterTypeDefaultMeaning
cThemeColorsrequiredThe scheme's colours.
darkboolrequiredThe dark scheme.
hasGlyphboolrequiredThe design has a launcher glyph (the header shows it instead of the logo).

Returns — ThemeChat — the chat window as it was drawn before chat tokens: the page background, the surface header, the raised composer, and the launcher's glyph (or the logo) in the header.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final chat = ThemeChat.fallback(tokens.colors, dark: tokens.dark, hasGlyph: tokens.icons.launcher != null);
}

ThemeChat.fromJson

Use it to read a set of chat window tokens from the design's JSON — the SDK does this when the session arrives.

static ThemeChat fromJson(Map<String, Object?> r, ThemeChat d)
ParameterTypeDefaultMeaning
rMap<String, Object?>requiredThe JSON object to read.
dThemeChatrequiredThe chat tokens to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeChat.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final chat = ThemeChat.fromJson({'title': 'Help', 'showTimestamps': true},
      ThemeChat.fallback(tokens.colors, dark: tokens.dark, hasGlyph: tokens.icons.launcher != null));
}

ThemeChat.toJson

Use it to turn the chat window's tokens back into the design's JSON — to store them or compare them.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.chat.toJson();

ThemeCall

Use it to draw a call screen of your own (GraineCallScreen.app) in the agent's call-screen design.

class ThemeCall

What to expect — Resolved from the agent's design in the Garage when the session loads; 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 const List<String> avatarsList<String>What may stand for the agent: logo, glyph, initials.
static const List<String> avatarShapesList<String>The avatar's shapes: circle, squircle, rounded-square.
static const List<String> endShapesList<String>The hang-up button's shapes: round, pill.
static const List<String> ringtonesList<String>The rings: system (the phone's own), graine, soft.
static const List<String> allControlsList<String>Every in-call button: mute, speaker, chat, end.
static const List<String> defaultControlsList<String>The buttons when the design says nothing: mute, speaker, chat, end.
final String backgroundStringThe fill, or a gradient's top.
final String? backgroundToString?The colour at the bottom of a gradient, or null for a single solid fill.
final String textStringThe name and the status line.
final String textMutedStringSecondary text: the timer, the caption.
final String avatarStringWhat stands for the agent: logo, glyph or initials.
final String avatarShapeStringThe avatar's shape: circle, squircle or rounded-square.
final String avatarFillStringBehind the avatar.
final String avatarTextStringThe initials, on avatarFill.
final String buttonFillStringAn in-call button's fill.
final String buttonIconStringAn in-call button's icon.
final String buttonActiveFillStringA button that is on (muted, speaker) — its fill.
final String buttonActiveIconStringA button that is on — its icon.
final String endFillStringThe hang-up button's fill.
final String endIconStringThe hang-up button's icon.
final String endShapeStringThe hang-up button's shape: round or pill.
final String acceptFillStringThe answer button's fill, while it rings.
final String acceptIconStringThe answer button's icon.
final String declineFillStringThe decline button's fill, while it rings.
final String declineIconStringThe decline button's icon.
final String callbackFillString"Call back", on a missed call.
final String callbackIconStringThe "Call back" button's icon, on a missed call.
final bool captionsboolShow the line being said.
final bool timerboolShow the call's running time.
final List<String> controlsList<String>The buttons shown during a call, in order; end is always included.
final String ringtoneStringThe ring: system, graine or soft (Ringtone.all has the notes).
final ThemeCallStatusText statusTextThemeCallStatusTextWhat the screen says at each moment, in the brand's words.

Its functions: ThemeCall, fallback, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeCall.ThemeCall

Use it to build a set of call screen tokens by hand — for a preview or a test; the session's design carries the real one.

const ThemeCall({ required this.background, this.backgroundTo, required this.text, required this.textMuted, this.avatar = 'logo', this.avatarShape = 'circle', required this.avatarFill, required this.avatarText, required this.buttonFill, required this.buttonIcon, required this.buttonActiveFill, required this.buttonActiveIcon, required this.endFill, required this.endIcon, this.endShape = 'round', required this.acceptFill, required this.acceptIcon, required this.declineFill, required this.declineIcon, required this.callbackFill, required this.callbackIcon, this.captions = true, this.timer = true, this.controls = defaultControls, this.ringtone = 'system', this.statusText = const ThemeCallStatusText()})
ParameterTypeDefaultMeaning
backgroundStringrequiredThe fill, or a gradient's top.
backgroundToString?optionalThe colour at the bottom of a gradient, or null for a single solid fill.
textStringrequiredThe name and the status line.
textMutedStringrequiredSecondary text: the timer, the caption.
avatarString'logo'What stands for the agent: logo, glyph or initials.
avatarShapeString'circle'The avatar's shape: circle, squircle or rounded-square.
avatarFillStringrequiredBehind the avatar.
avatarTextStringrequiredThe initials, on avatarFill.
buttonFillStringrequiredAn in-call button's fill.
buttonIconStringrequiredAn in-call button's icon.
buttonActiveFillStringrequiredA button that is on (muted, speaker) — its fill.
buttonActiveIconStringrequiredA button that is on — its icon.
endFillStringrequiredThe hang-up button's fill.
endIconStringrequiredThe hang-up button's icon.
endShapeString'round'The hang-up button's shape: round or pill.
acceptFillStringrequiredThe answer button's fill, while it rings.
acceptIconStringrequiredThe answer button's icon.
declineFillStringrequiredThe decline button's fill, while it rings.
declineIconStringrequiredThe decline button's icon.
callbackFillStringrequired"Call back", on a missed call.
callbackIconStringrequiredThe "Call back" button's icon, on a missed call.
captionsbooltrueShow the line being said.
timerbooltrueShow the call's running time.
controlsList<String>defaultControlsThe buttons shown during a call, in order; end is always included.
ringtoneString'system'The ring: system, graine or soft (Ringtone.all has the notes).
statusTextThemeCallStatusTextconst ThemeCallStatusText()What the screen says at each moment, in the brand's words.

Returns — a new ThemeCall.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final call = ThemeCall.fallback(tokens.colors);   // or ThemeCall(background: '#0F1115', text: '#FFFFFF', …) with every colour
}

ThemeCall.fallback

Use it to get the call screen an agent with no call tokens draws.

factory ThemeCall.fallback(ThemeColors c)

The call screen an older server gets: the fixed dark screen that the native SDKs drew before these tokens existed.

ParameterTypeDefaultMeaning
cThemeColorsrequiredThe scheme's colours.

Returns — ThemeCall — the fixed dark call screen, with the accent on the avatar and the Call back button.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final call = ThemeCall.fallback(tokens.colors);
}

ThemeCall.fromJson

Use it to read a set of call screen tokens from the design's JSON — the SDK does this when the session arrives.

static ThemeCall fromJson(Map<String, Object?> r, ThemeCall d)
ParameterTypeDefaultMeaning
rMap<String, Object?>requiredThe JSON object to read.
dThemeCallrequiredThe call tokens to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeCall.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final call = ThemeCall.fromJson({'ringtone': 'soft'}, ThemeCall.fallback(tokens.colors));
}

ThemeCall.toJson

Use it to turn the call screen's tokens back into the design's JSON — to store them or compare them.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.call.toJson();

ThemeCallStatusText

Use it to say what your call screen says at each moment, in the brand's words.

class ThemeCallStatusText

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final String ringingStringWhile an agent's call rings (default "Incoming call").
final String scheduledStringShown when Graine rings at a booked time, whether the user or the agent made the booking.
final String connectingStringWhile the call connects (default "Connecting…").
final String endedStringOnce the call is over (default "Call ended").
final String missedStringOn a ring nobody answered (default "Missed call").

Its functions: ThemeCallStatusText, fromJson, toJson — each with its parameters, what it returns, the errors it raises and an example.

ThemeCallStatusText.ThemeCallStatusText

Use it to build a set of call screen words by hand — for a preview or a test; the session's design carries the real one.

const ThemeCallStatusText({ this.ringing = 'Incoming call', this.scheduled = 'Scheduled call', this.connecting = 'Connecting…', this.ended = 'Call ended', this.missed = 'Missed call'})
ParameterTypeDefaultMeaning
ringingString'Incoming call'While an agent's call rings (default "Incoming call").
scheduledString'Scheduled call'Shown when Graine rings at a booked time, whether the user or the agent made the booking.
connectingString'Connecting…'While the call connects (default "Connecting…").
endedString'Call ended'Once the call is over (default "Call ended").
missedString'Missed call'On a ring nobody answered (default "Missed call").

Returns — a new ThemeCallStatusText.

Errors — none.

final result = ThemeCallStatusText();

ThemeCallStatusText.fromJson

Use it to read a set of call screen words from the design's JSON — the SDK does this when the session arrives.

static ThemeCallStatusText fromJson(Object? raw, ThemeCallStatusText d)
ParameterTypeDefaultMeaning
rawObject?requiredThe words as decoded.
dThemeCallStatusTextrequiredThe words to keep for anything the JSON leaves out or gets wrong — the scheme's defaults.

Returns — ThemeCallStatusText.

Errors — none.

final tokens = GraineInApp.instance.tokens;   // the brand's, once the session is in
if (tokens != null) {
  final words = ThemeCallStatusText.fromJson({'connecting': 'Calling Asha…'}, tokens.call.statusText);
}

ThemeCallStatusText.toJson

Use it to turn the call screen's words back into the design's JSON — to store them or compare them.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = GraineInApp.instance.tokens?.call.statusText.toJson();

Ringtone

Use it to play the agent's ring on a call screen of your own.

class Ringtone

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
final String labelStringThe ring's name, as the dashboard shows it.
final String waveStringsine or triangle.
final double gaindoubleHow loud, from 0 to 1 of full scale.
final List<RingNote> notesList<RingNote>The notes, played once per ring.
final int pauseMsintThe silence after the notes, in milliseconds, before they play again.
static const Map<String, Ringtone> allMap<String, Ringtone>The rings by their call.ringtone names.

Its functions: Ringtone, toJson — each with its parameters, what it returns, the errors it raises and an example.

Ringtone.Ringtone

Use it to describe a ring of your own.

const Ringtone({required this.label, required this.wave, required this.gain, required this.notes, required this.pauseMs})
ParameterTypeDefaultMeaning
labelStringrequiredThe ring's name, as the dashboard shows it.
waveStringrequiredsine or triangle.
gaindoublerequiredHow loud, from 0 to 1 of full scale.
notesList<RingNote>requiredThe notes, played once per ring.
pauseMsintrequiredThe silence after the notes, in milliseconds, before they play again.

Returns — a new Ringtone.

Errors — none.

const ring = Ringtone(label: 'Two notes', wave: 'sine', gain: 0.5, notes: [RingNote(659.25, 150), RingNote(987.77, 230)], pauseMs: 1500);

Ringtone.toJson

Use it to turn a ring into JSON.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = Ringtone.all['soft']?.toJson();

RingNote

Use it to play one note of a ring.

class RingNote

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
final double hzdoubleThe note's frequency, in hertz; 0 is a rest.
final int msintHow long it lasts, in milliseconds.

Its functions: RingNote, toJson — each with its parameters, what it returns, the errors it raises and an example.

RingNote.RingNote

Use it to describe one note of a ring of your own.

const RingNote(this.hz, this.ms)
ParameterTypeDefaultMeaning
hzdoublerequiredThe note's frequency, in hertz; 0 is a rest.
msintrequiredHow long it lasts, in milliseconds.

Returns — a new RingNote.

Errors — none.

const note = RingNote(659.25, 150);   // E5 for 150 ms

RingNote.toJson

Use it to turn a note into JSON.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = const RingNote(880, 120).toJson();   // {hz: 880.0, ms: 120}

Design (class)

Use it to apply the rules every Graine surface shares: the allowed values, colours, fonts and the launcher's geometry.

abstract final class Design

What to expect — Resolved from the agent's design in the Garage when the session loads; 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 const List<String> schemesList<String>auto (the device), light, dark.
static const List<String> launcherShapesList<String>circle, squircle, rounded-square, square, pill.
static const List<String> bubbleStylesList<String>filled, outline, minimal.
static const List<String> iconStylesList<String>outline, filled, duotone, sharp.
static const List<String> easingsList<String>standard, emphasized, spring, linear, none.
static const List<String> chromesList<String>The ways a content surface can be framed: as a plain surface or as a desktop window.
static const double launcherSizedoubleSize of the launcher, 56 × 56; a pill launcher is 56 tall and as wide as its label.
static const double launcherGlyphScaledoubleThe size of the glyph or logo inside the launcher, relative to the launcher's height.
static const double squircleScaledoubleThe corner size of a squircle, expressed as a fraction of the squircle's size.
static const int glyphAssetVersionintRaised whenever a glyph's path data changes, so an image cached for a year never pairs an outdated drawing with a new id.
static const String defaultOriginStringWhere glyph images come from when no base address is given: 'https://www.graine.ai'.

Its functions: normaliseHex, mixHex, fontFamily, launcherRadius, glyphUrl — each with its parameters, what it returns, the errors it raises and an example.

Design.normaliseHex

Use it to read any hex colour as #RRGGBB.

static String? normaliseHex(Object? value)

Returns "#RRGGBB" for any hex colour (#rgb or #rrggbb, with the "#" or without it), and null for anything else.

ParameterTypeDefaultMeaning
valueObject?requiredA colour: #rgb or #rrggbb, with or without the #.

Returns — String?. Example value: '#33AAEE'.

Errors — none.

final hex = Design.normaliseHex('3ae');

Design.mixHex

Use it to mix two colours, as the design system does.

static String mixHex(String a, String b, double t)

The colour a fraction t of the way from a to b, channel by channel, as #RRGGBB. A colour that does not read counts as black.

ParameterTypeDefaultMeaning
aStringrequiredThe colour to start from, #RRGGBB.
bStringrequiredThe colour to move towards.
tdoublerequiredHow far, from 0 (all a) to 1 (all b).

Returns — String. Example value: '#808080'.

Errors — none.

final mid = Design.mixHex('#000000', '#FFFFFF', 0.5);

Design.fontFamily

Use it to pick the family to draw with from the fonts your app has.

static String? fontFamily(String? family, Iterable<String> available)

The app's own copy of a family, in the app's spelling, or null. system always means the platform's font; any other family is the app's when it is listed in available (matched without regard to case) — and then that copy wins over the faces the SDK fetches.

ParameterTypeDefaultMeaning
familyString?requiredThe design's family (tokens.font.body or heading).
availableIterable<String>requiredThe families your app bundles (GraineConfig.fonts).

Returns — String?. Example value: 'Inter'.

Errors — none.

final family = Design.fontFamily('inter', const ['Inter']);

Design.launcherRadius

Use it to round the launcher like the design's shape.

static double launcherRadius(String shape, ThemeRadius radius, double height)

Works out the launcher's corner radius for a given height: fully round for circle and pill, 30% of the size for squircle, radius.lg for rounded-square and radius.sm for square, capped at half the height.

ParameterTypeDefaultMeaning
shapeStringrequiredThe launcher's shape (tokens.shape.launcher).
radiusThemeRadiusrequiredThe design's radii (tokens.shape.radius).
heightdoublerequiredThe launcher's height, in logical pixels.

Returns — double. Example value: 16.8.

Errors — none.

final tokens = GraineInApp.instance.tokens;
final radius = tokens == null ? null : Design.launcherRadius('squircle', tokens.shape.radius, 56);

Design.glyphUrl

Use it to get the launcher glyph as an image — for a notification or a native view.

static String glyphUrl(String? origin, String id, String color, num sizePx, [num stroke = 2])

The address of the launcher glyph as an image, built as every Graine SDK builds it: the id lower-cased, the colour as upper-case hex without its #, the size clamped to 16–512 physical pixels (the logical size × the screen's scale, so the image is sharp on every screen), the stroke clamped to 0.5–4 in quarter steps, and the asset version.

ParameterTypeDefaultMeaning
originString?requiredThe agent's address; 'https://www.graine.ai' when null.
idStringrequiredThe glyph's id (tokens.icons.launcher.id).
colorStringrequiredThe glyph's colour, #RRGGBB.
sizePxnumrequiredThe image's size in physical pixels — the logical size times the screen's scale (16–512).
strokenum2The stroke width for a line glyph (0.5–4, in quarter steps); 2 when left out.

Returns — String. Example value: 'https://www.graine.ai/api/embed/glyph?id=waveform&c=FFFFFF&s=96&w=2&v=2'.

Errors — none.

final url = Design.glyphUrl('https://www.graine.ai', 'waveform', '#FFFFFF', 96);

DesignFonts

Use it to find the font files for the agent's typefaces — the SDK fetches and registers them itself.

abstract final class DesignFonts

What to expect — Resolved from the agent's design in the Garage when the session loads; 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 const int assetVersionintRaised whenever a font file Graine serves changes, so no cache — on the device or in between — keeps an old file. Part of every font address.
static const Map<String, List<int>> weightsMap<String, List<int>>All the families the dashboard offers, each with the weights it actually comes in. A family missing here is drawn in the system font and never fetched.

Its functions: key, nearestWeight, canonicalFamily, faceName, url, faceFor, facesFor, facesForDesign — each with its parameters, what it returns, the errors it raises and an example.

DesignFonts.key

Use it to look a family and weight up in GraineInApp.fontFaces.

static String key(String family, int weight)

family|weight — what a loaded face is looked up by in GraineInApp.fontFaces.

ParameterTypeDefaultMeaning
familyStringrequiredThe family, as the design spells it.
weightintrequiredThe weight the family really has.

Returns — String. Example value: 'DM Sans|700'.

Errors — none.

final key = DesignFonts.key('DM Sans', 700);

DesignFonts.nearestWeight

Use it to pick the closest weight a family really has.

static int? nearestWeight(List<int> available, int want)

The weight in available nearest want; on a tie, the heavier — a 500 asked of a family with 400 and 600 is drawn at 600. Null when available is empty.

ParameterTypeDefaultMeaning
availableList<int>requiredThe weights the family has.
wantintrequiredThe weight the design asks for.

Returns — int?. Example value: 600 (a tie goes to the heavier).

Errors — none.

final w = DesignFonts.nearestWeight(const [400, 600], 500);

DesignFonts.canonicalFamily

Use it to match a family name to the one the SDK has files for.

static String? canonicalFamily(String? family)

Returns the family spelled the way this build spells it, matching first exactly and then ignoring case. Returns null for "system", for an empty value, and for a family this build has no files for.

ParameterTypeDefaultMeaning
familyString?requiredA family name, in any case.

Returns — String?. Example value: 'DM Sans'.

Errors — none.

final family = DesignFonts.canonicalFamily('dm sans');

DesignFonts.faceName

Use it to get the family name a face is registered under.

static String faceName(String family, int weight)

The name a face is registered under — Graine-, the family with only its letters and digits, -, and the weight — the same on every Graine SDK.

ParameterTypeDefaultMeaning
familyStringrequiredThe family.
weightintrequiredThe weight.

Returns — String. Example value: 'Graine-IBMPlexSans-600'.

Errors — none.

final name = DesignFonts.faceName('IBM Plex Sans', 600);

DesignFonts.url

Use it to get the address of one face's font file.

static String url(String? origin, String family, int weight)
ParameterTypeDefaultMeaning
originString?requiredThe agent's address; 'https://www.graine.ai' when null.
familyStringrequiredThe family, as this build spells it.
weightintrequiredThe weight the family really has.

Returns — String. Example value: 'https://www.graine.ai/api/embed/font?f=DM%20Sans&w=700&v=1'.

Errors — none.

final url = DesignFonts.url(null, 'DM Sans', 700);

DesignFonts.faceFor

Use it to get the face to draw a family at a weight.

static DesignFontFace? faceFor(String? family, int weight, {String? origin})

The face to draw family at weight with — at the nearest weight the family really has — or null when the family is the system font or one this build has no files for.

ParameterTypeDefaultMeaning
familyString?requiredThe design's family; system or one with no files gives null.
weightintrequiredThe weight the design asks for.
originString?optionalThe agent's address.

Returns — DesignFontFace? — its fields: DesignFontFace. Example value: null.

Errors — none.

final face = DesignFonts.faceFor('Inter', 600);

DesignFonts.facesFor

Use it to list the faces one token set draws with.

static List<DesignFontFace> facesFor(ThemeTokens t, {String? origin})

Lists the faces a single token set uses — the body family at weights.body and weights.strong, and the heading family at weights.title and weights.strong — with no face repeated.

ParameterTypeDefaultMeaning
tThemeTokensrequiredOne scheme's tokens.
originString?optionalThe agent's address.

Returns — List<DesignFontFace> — its fields: DesignFontFace. Example value: [].

Errors — none.

final tokens = GraineInApp.instance.tokens;
final faces = tokens == null ? const <DesignFontFace>[] : DesignFonts.facesFor(tokens);

DesignFonts.facesForDesign

Use it to list the faces a whole design draws with, light and dark.

static List<DesignFontFace> facesForDesign(ResolvedDesign design, {String? origin})

Lists all faces a design may use in either scheme. The SDK fetches these when a session brings the design, so switching to dark never has to wait.

ParameterTypeDefaultMeaning
designResolvedDesignrequiredThe design.
originString?optionalThe agent's address.

Returns — List<DesignFontFace> — its fields: DesignFontFace. Example value: [].

Errors — none.

final design = GraineInApp.instance.design;
final faces = design == null ? const <DesignFontFace>[] : DesignFonts.facesForDesign(design);

DesignFontFace

Read it as one typeface the SDK fetches and registers.

class DesignFontFace

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final String familyStringThe family name spelled as in the dashboard, for example "IBM Plex Sans".
final int weightintThe closest weight the family actually has to the one the design requested.
final String nameStringThe name the face is registered with as a family, such as Graine-IBMPlexSans-600.
final String urlStringWhere Graine serves the TTF.
String get keyStringfamily|weight — what a loaded face is looked up by.

Its functions: DesignFontFace, toJson — each with its parameters, what it returns, the errors it raises and an example.

DesignFontFace.DesignFontFace

Use it to describe a face by hand.

const DesignFontFace({required this.family, required this.weight, required this.name, required this.url})
ParameterTypeDefaultMeaning
familyStringrequiredThe family name spelled as in the dashboard, for example "IBM Plex Sans".
weightintrequiredThe closest weight the family actually has to the one the design requested.
nameStringrequiredThe name the face is registered with as a family, such as Graine-IBMPlexSans-600.
urlStringrequiredWhere Graine serves the TTF.

Returns — a new DesignFontFace.

Errors — none.

final face = DesignFonts.faceFor('Inter', 600);   // or DesignFontFace(family: 'Inter', weight: 600, name: 'Graine-Inter-600', url: …)

DesignFontFace.toJson

Use it to turn a face into JSON.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = DesignFonts.faceFor('DM Sans', 700)?.toJson();

SdkIcons

Use it to draw the SDK's own icons — microphone, hang up, send, chat, call, chevron, close, compose, speaker — in your own UI.

abstract final class SdkIcons

What to expect — Resolved from the agent's design in the Garage when the session loads; 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 const SdkIcon micSdkIconThe microphone: start a call, or unmute.
static const SdkIcon micOffSdkIconThe microphone struck through: muted.
static const SdkIcon endSdkIconThe handset turned down: hang up.
static const SdkIcon sendSdkIconThe paper plane: send a message.
static const SdkIcon chatSdkIconThe speech bubble: open the chat.
static const SdkIcon callSdkIconThe handset: call.
static const SdkIcon chevronSdkIconDrawn pointing UP; the chat header rotates it by 180°.
static const SdkIcon closeSdkIconThe cross: close.
static const SdkIcon compose =—Icon used for "New conversation" in the header of a chat window; it pictures a page and a pen.
static const SdkIcon speaker =—The loudspeaker icon of the call screen, for apps that draw their own call screen.
static const Map<String, SdkIcon> allMap<String, SdkIcon>Every icon by name: mic, micOff, end, send, chat, call, chevron, close, compose, speaker.

Its functions: treatment — each with its parameters, what it returns, the errors it raises and an example.

SdkIcons.treatment

Use it to know how an icon style strokes and fills.

static IconTreatment treatment(String style, double stroke)

How style draws an icon at stroke: outline — stroked, round caps and joins; sharp — stroked, square caps, mitred joins; filled — the body filled, stroked at 0.75 × the stroke (at least 1); duotone — the body filled at 28%, then stroked. A style this build does not know draws as outline.

ParameterTypeDefaultMeaning
styleStringrequiredThe design's icon style (tokens.icons.style); an unknown one draws as outline.
strokedoublerequiredThe design's stroke width (tokens.icons.stroke), in the icon's 24-unit box.

Returns — IconTreatment — its fields: IconTreatment.

Errors — none.

final icons = GraineInApp.instance.tokens?.icons;
final treatment = icons == null ? null : SdkIcons.treatment(icons.style, icons.stroke);

SdkIcon

Use it to draw one of the SDK's icons from its path data.

class SdkIcon

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
final String? bodyString?A closed path that may be filled (24 × 24 SVG path data); null for a line-only icon.
final String? linesString?Paths that are only ever stroked; null for a body-only icon.

Its functions: SdkIcon, toJson — each with its parameters, what it returns, the errors it raises and an example.

SdkIcon.SdkIcon

Use it to describe an icon by hand.

const SdkIcon({this.body, this.lines})
ParameterTypeDefaultMeaning
bodyString?optionalA closed path that may be filled (24 × 24 SVG path data); null for a line-only icon.
linesString?optionalPaths that are only ever stroked; null for a body-only icon.

Returns — a new SdkIcon.

Errors — none.

final result = SdkIcon();

SdkIcon.toJson

Use it to turn an icon into JSON.

Map<String, Object?> toJson()

Parameters — none.

Returns — Map<String, Object?>.

Errors — none.

final json = SdkIcons.send.toJson();

IconTreatment

Read it to stroke and fill an icon in the design's style.

class IconTreatment

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
final bool fillBodyboolTrue when the body has a fill; it is stroked in every case.
final double bodyFillOpacitydoubleHow opaque the body's fill is, measured as a share of the icon's colour.
final double bodyStrokedoubleHow thick the body's outline is, measured in the icon's own 24-unit box.
final double lineStrokedoubleThe stroke width of the icon's lines, in its 24-unit box.
final bool squareboolWhen true, lines have square caps and mitred joins for a sharp look; when false, caps and joins are round.

Its functions: IconTreatment — each with its parameters, what it returns, the errors it raises and an example.

IconTreatment.IconTreatment

Use it to describe a treatment by hand.

const IconTreatment({ required this.fillBody, required this.bodyFillOpacity, required this.bodyStroke, required this.lineStroke, required this.square})
ParameterTypeDefaultMeaning
fillBodyboolrequiredTrue when the body has a fill; it is stroked in every case.
bodyFillOpacitydoublerequiredHow opaque the body's fill is, measured as a share of the icon's colour.
bodyStrokedoublerequiredHow thick the body's outline is, measured in the icon's own 24-unit box.
lineStrokedoublerequiredThe stroke width of the icon's lines, in its 24-unit box.
squareboolrequiredWhen true, lines have square caps and mitred joins for a sharp look; when false, caps and joins are round.

Returns — a new IconTreatment.

Errors — none.

final t = SdkIcons.treatment('duotone', 2);   // or IconTreatment(fillBody: true, bodyFillOpacity: 0.28, bodyStroke: 2, lineStroke: 2, square: false)

Appearance

Use it to read the dashboard's Appearance settings — for an agent with no design, or a surface of your own.

abstract final class Appearance

What to expect — Resolved from the agent's design in the Garage when the session loads; 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 const String defaultAccentStringThe accent used when the dashboard sets none: '#318CE7'.
static const String defaultOriginStringWhere pictures and glyphs are fetched from when no base address is given: 'https://www.graine.ai'.

Its functions: resolve, isDark, palette, luminance, argb, tint, encodeURIComponent — each with its parameters, what it returns, the errors it raises and an example.

Appearance.resolve

Use it to read the dashboard's Appearance into what a phone draws.

static NativeAppearance resolve(Map<String, Object?>? appearance, { required String accent, required String heading, double? radius, double? maxRadius, String? origin})

The dashboard's values win, and the host app's values serve only as the fallback; the rule always runs in this one direction.

ParameterTypeDefaultMeaning
appearanceMap<String, Object?>?requiredThe dashboard's Appearance (session.appearance).
accentStringrequiredThe accent to use when the dashboard sets none.
headingStringrequiredThe heading to use when the dashboard sets none.
radiusdouble?optionalThe corner radius to use when the dashboard sets none; 18 when left out.
maxRadiusdouble?optionalThe most rounding your surface can draw; 48 when left out.
originString?optionalThe agent's address, for the logo's address; 'https://www.graine.ai' when left out.

Returns — NativeAppearance — its fields: NativeAppearance.

Errors — none.

final look = Appearance.resolve(GraineInApp.instance.session?.appearance, accent: '#318CE7', heading: 'Assistant', origin: 'https://www.graine.ai');

Appearance.isDark

Use it to know whether to draw dark, from the dashboard's theme and the device.

static bool isDark(String? theme, bool? deviceDark)

Handles light, dark or "auto"; "auto" follows the device rather than the dashboard's own default.

ParameterTypeDefaultMeaning
themeString?requiredThe dashboard's theme: light, dark or auto.
deviceDarkbool?requiredWhether the device is in dark mode — MediaQuery.platformBrightnessOf(context) == Brightness.dark; null when unknown, read as light.

Returns — bool. Example value: true.

Errors — none.

final dark = Appearance.isDark('auto', true);

Appearance.palette

Use it to get the colours a component is drawn in.

static WidgetPalette palette(Map<String, Object?>? appearance, {bool? deviceDark, String? accent})
ParameterTypeDefaultMeaning
appearanceMap<String, Object?>?requiredThe dashboard's Appearance, for its theme.
deviceDarkbool?optionalWhether the device is in dark mode — MediaQuery.platformBrightnessOf(context) == Brightness.dark; null when unknown, read as light.
accentString?optionalThe accent, to pick readable ink on it.

Returns — WidgetPalette — its fields: WidgetPalette.

Errors — none.

final colours = Appearance.palette(GraineInApp.instance.session?.appearance, deviceDark: false, accent: '#318CE7');

Appearance.luminance

Use it to know how light a colour is.

static double luminance(String hex)

Returns the WCAG relative luminance of a #rrggbb colour, or 0 for any other input.

ParameterTypeDefaultMeaning
hexStringrequiredA colour as #RRGGBB.

Returns — double. Example value: 0.25.

Errors — none.

final l = Appearance.luminance('#318CE7');

Appearance.argb

Use it to turn #RRGGBB into a colour value Flutter takes.

static int? argb(String? hex)

Converts #rgb, #rrggbb or #aarrggbb (the "#" is optional) into a 32-bit ARGB value.

ParameterTypeDefaultMeaning
hexString?requiredA colour as #RGB, #RRGGBB or #AARRGGBB, with or without the #.

Returns — int?. Example value: 0xFF318CE7.

Errors — none.

final argb = Appearance.argb('#318CE7');

Appearance.tint

Use it to make a colour see-through.

static int tint(String? hex, double alpha)

Returns a #rrggbb colour at reduced strength, as a 32-bit ARGB value, for badge and selection fills.

ParameterTypeDefaultMeaning
hexString?requiredA colour as #RRGGBB; slate for anything else.
alphadoublerequiredHow strong, from 0 to 1.

Returns — int. Example value: 0x1F318CE7.

Errors — none.

final fill = Appearance.tint('#318CE7', 0.12);

Appearance.encodeURIComponent

Use it to put text in an address the way the SDK does.

static String encodeURIComponent(String s)

Encodes text for an address exactly as Uri.encodeComponent does (a space as %20).

ParameterTypeDefaultMeaning
sStringrequiredThe text to put in an address.

Returns — String. Example value: 'Order%20ORD%201234'.

Errors — none.

final part = Appearance.encodeURIComponent('Order ORD 1234');

NativeAppearance

Read it as the dashboard's Appearance, translated for a phone.

class NativeAppearance

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final String accentStringAlways a valid "#rgb" or "#rrggbb".
final String headingStringThe heading of the panel, such as "Talk to our AI assistant"; this is NOT the assistant's name.
final String? logoUrlString?Absolute, or null when no logo is set.
final LogoPlate logoPlateLogoPlateThe background under the logo when it replaces the launcher glyph: the accent or a white plate.
final double radiusdoubleHow rounded the corners are, limited to what the host app can draw.

Its functions: NativeAppearance — each with its parameters, what it returns, the errors it raises and an example.

NativeAppearance.NativeAppearance

Use it to build an appearance by hand for a preview.

const NativeAppearance({ required this.accent, required this.heading, this.logoUrl, this.logoPlate = LogoPlate.accent, required this.radius})
ParameterTypeDefaultMeaning
accentStringrequiredAlways a valid "#rgb" or "#rrggbb".
headingStringrequiredThe heading of the panel, such as "Talk to our AI assistant"; this is NOT the assistant's name.
logoUrlString?optionalAbsolute, or null when no logo is set.
logoPlateLogoPlateLogoPlate.accentThe background under the logo when it replaces the launcher glyph: the accent or a white plate.
radiusdoublerequiredHow rounded the corners are, limited to what the host app can draw.

Returns — a new NativeAppearance.

Errors — none.

const look = NativeAppearance(accent: '#318CE7', heading: 'Talk to us', radius: 18);

WidgetPalette

Read it as the colours a component is drawn in.

class WidgetPalette

What to expect — How the SDK draws a component and reads the customer's answer to it. Public so a component view of your own can reuse it.

PropertyTypeMeaning
final String surfaceStringBehind the component, as #RRGGBB.
final String borderStringHairlines and chip fills, as #RRGGBB.
final String textStringTitles and prices, as #RRGGBB.
final String mutedStringSubtitles, facts and secondary labels, as #RRGGBB.
final String onAccentStringA colour that stays legible ON the accent; it is picked by luminance, so labels still show on a yellow accent.
final String modeStringEither "light" or "dark", with "auto" already settled using the device's setting.

Its functions: WidgetPalette — each with its parameters, what it returns, the errors it raises and an example.

WidgetPalette.WidgetPalette

Use it to build a palette by hand for a preview.

const WidgetPalette({ required this.surface, required this.border, required this.text, required this.muted, required this.onAccent, required this.mode})
ParameterTypeDefaultMeaning
surfaceStringrequiredBehind the component, as #RRGGBB.
borderStringrequiredHairlines and chip fills, as #RRGGBB.
textStringrequiredTitles and prices, as #RRGGBB.
mutedStringrequiredSubtitles, facts and secondary labels, as #RRGGBB.
onAccentStringrequiredA colour that stays legible ON the accent; it is picked by luminance, so labels still show on a yellow accent.
modeStringrequiredEither "light" or "dark", with "auto" already settled using the device's setting.

Returns — a new WidgetPalette.

Errors — none.

const palette = WidgetPalette(surface: '#FFFFFF', border: '#E5E4E0', text: '#101318', muted: '#5F6672', onAccent: '#FFFFFF', mode: 'light');

LogoPlate

Read it to know what the brand's logo sits on: the accent, or a white plate.

enum LogoPlate

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
accentThe logo sits on the shape's own fill; this suits a white or light logo meant for a coloured background.
whitePuts the logo on a white plate with a hairline, suited to a coloured or dark logo or an opaque image — and most logo files are opaque images.
PropertyTypeMeaning
String get wireStringThe word on the wire.

Its functions: from — each with its parameters, what it returns, the errors it raises and an example.

LogoPlate.from

Use it to read the plate from the dashboard's setting.

static LogoPlate from(Object? value)

The dashboard's logoPlate as every Graine SDK reads it: exactly "white" is white; anything else — absent, "WHITE", a number — is the accent.

ParameterTypeDefaultMeaning
valueObject?requiredThe dashboard's logoPlate: exactly "white" is white; anything else is the accent.

Returns — LogoPlate. Example value: LogoPlate.accent.

Errors — none.

final plate = LogoPlate.from('white');   // LogoPlate.white; anything else is LogoPlate.accent

LogoPlateLook

Read it to draw the plate under a logo: its fill and outline.

class LogoPlateLook

What to expect — Resolved from the agent's design in the Garage when the session loads; 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
final String fillStringA "#RRGGBB" colour, either the accent or BrandLogo.plateWhite.
final double? outlineWidthdouble?Width of the hairline around a white plate, or null when there is none.
final String? outlineColorString?The hairline's colour round a white plate; null for none.
bool get hasOutlineboolTrue when the plate has a hairline (the white plate).

Its functions: LogoPlateLook — each with its parameters, what it returns, the errors it raises and an example.

LogoPlateLook.LogoPlateLook

Use it to describe a plate by hand.

const LogoPlateLook({required this.fill, this.outlineWidth, this.outlineColor})
ParameterTypeDefaultMeaning
fillStringrequiredA "#RRGGBB" colour, either the accent or BrandLogo.plateWhite.
outlineWidthdouble?optionalWidth of the hairline around a white plate, or null when there is none.
outlineColorString?optionalThe hairline's colour round a white plate; null for none.

Returns — a new LogoPlateLook.

Errors — none.

const plate = LogoPlateLook(fill: '#FFFFFF', outlineWidth: 1, outlineColor: '#E5E4E0');

Use it to fit the brand's logo into the launcher, the bar's avatar or the chat header exactly as the SDK does.

abstract final class BrandLogo

What to expect — Resolved from the agent's design in the Garage when the session loads; 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 const String plateWhiteStringThe white plate.
static const String labelOnWhiteStringColour of a pill launcher's label on the white plate — an ink that reads well on white.

Its functions: box, outlineWidth, plateLook — each with its parameters, what it returns, the errors it raises and an example.

BrandLogo.box

Use it to get the square a logo is fitted into.

static int box(String shape, double size)

The square a logo is fitted into — whole, proportions kept, centred, never cropped — inside a shape size across (a pill: pass its height and circle). A circle or squircle holds 70% of its size, a rounded square or square 80%; rounded down, so a corner never pokes out.

ParameterTypeDefaultMeaning
shapeStringrequiredThe shape the logo sits in: circle, squircle, rounded-square, square (a pill: pass circle).
sizedoublerequiredThe shape's size across, in logical pixels (a pill: its height).

Returns — int. Example value: 39.

Errors — none.

final side = BrandLogo.box('circle', 56);

BrandLogo.outlineWidth

Use it to get the white plate's outline width.

static double outlineWidth(double borderWidth)

Width of the white plate's hairline, max(1, borderWidth). Even a design with no outlines needs it; otherwise a white plate disappears against a white bar.

ParameterTypeDefaultMeaning
borderWidthdoublerequiredThe design's outline width.

Returns — double. Example value: 1.0.

Errors — none.

final w = BrandLogo.outlineWidth(0);

BrandLogo.plateLook

Use it to get the plate's fill and outline.

static LogoPlateLook plateLook(LogoPlate plate, {required String accent, required String border, double borderWidth = 1})

The plate's fill and outline.

ParameterTypeDefaultMeaning
plateLogoPlaterequiredThe plate the logo needs.
accentStringrequiredThe accent, #RRGGBB.
borderStringrequiredThe design's border colour, for the white plate's hairline.
borderWidthdouble1The design's outline width; 1 when left out.

Returns — LogoPlateLook — its fields: LogoPlateLook.

Errors — none.

final plate = BrandLogo.plateLook(LogoPlate.white, accent: '#318CE7', border: '#E5E4E0');

WindowChrome

Use it to draw the design's desktop-window title bar (chrome: "window") on a surface of your own.

abstract final class WindowChrome

What to expect — Resolved from the agent's design in the Garage when the session loads; 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 const double lightsInsetdoubleHow far the three lights sit in from the surface's left edge, inside its outline: 12.
static const double lightsGapdoubleBetween one light and the next.
static const List<String> lightsList<String>Always the same three — close, minimise and zoom — in every design, because they are window controls rather than brand colours.
static const double onlineDotdoubleUsed for the success dot ahead of the chat's title while online, and for the gap between that dot and the heading.
static const double onlineDotGapdoubleThe space between the online dot and the title: 6.
static const double chevronIcondoubleThe collapse chevron at the right end of the chat window's bar: an 18-point glyph inside a 30-point target inset by 6, which puts the glyph itself 12 in — the same inset as the lights, on the opposite side.
static const double chevronBoxdoubleThe chevron's tap target: 30.
static const double chevronInsetdoubleHow far the chevron's target sits in from the right edge: 6.
static const double titleGapdoubleHow much room separates the title from the lights (or from the chevron).
static const String monospaceStringThe family name for monospaced text: monospace works on Android, which defines it; iOS does not, so there use Menlo.
static const List<String> monospaceFallbackList<String>Where to look for a monospaced face when the platform names none: Menlo, Roboto Mono, monospace.
static const String captionTitleStringThe caption card's title.

Its functions: barHeight, lightSize, lightsWidth, titleInset, title, innerRadius — each with its parameters, what it returns, the errors it raises and an example.

WindowChrome.barHeight

Use it to get the title bar's height on a surface.

static double barHeight(ChromeSurface surface)

Height of the title bar including its bottom outline — 32 for the chat window and components, 24 for the caption card.

ParameterTypeDefaultMeaning
surfaceChromeSurfacerequiredWhich surface: chat, caption or component.

Returns — double. Example value: 3.

Errors — none.

final height = WindowChrome.barHeight(ChromeSurface.chat);

WindowChrome.lightSize

Use it to get the size of each of the three lights.

static double lightSize(ChromeSurface surface)

The diameter of each light: 10, or 8 on the caption card.

ParameterTypeDefaultMeaning
surfaceChromeSurfacerequiredWhich surface.

Returns — double. Example value: 3.

Errors — none.

final size = WindowChrome.lightSize(ChromeSurface.caption);

WindowChrome.lightsWidth

Use it to get the width of the three lights.

static double lightsWidth(ChromeSurface surface)

The three lights, edge to edge.

ParameterTypeDefaultMeaning
surfaceChromeSurfacerequiredWhich surface.

Returns — double. Example value: 3.

Errors — none.

final width = WindowChrome.lightsWidth(ChromeSurface.component);

WindowChrome.titleInset

Use it to keep the title clear of the lights and the chevron.

static double titleInset(ChromeSurface surface, {bool chevron = false})

The distance the title stays from each end of the bar, so it is centred across the full bar and never runs under the lights or the chevron.

ParameterTypeDefaultMeaning
surfaceChromeSurfacerequiredWhich surface.
chevronboolfalseThe bar has the collapse chevron at its right end (the chat window).

Returns — double. Example value: 3.

Errors — none.

final inset = WindowChrome.titleInset(ChromeSurface.chat, chevron: true);

WindowChrome.title

Use it to get the title a surface's bar shows.

static String title(ChromeSurface surface, {String heading = '', String componentTitle = ''})

Gives the title in lower case: the heading on the chat window, "captions" on the caption card, and a component card's own title — empty when a component has none, leaving only the lights on its bar.

ParameterTypeDefaultMeaning
surfaceChromeSurfacerequiredWhich surface.
headingString''The chat window's heading.
componentTitleString''A component's own title.

Returns — String. Example value: 'help desk'.

Errors — none.

final title = WindowChrome.title(ChromeSurface.chat, heading: 'Help Desk');

WindowChrome.innerRadius

Use it to round the title bar's top corners inside the surface's outline.

static double innerRadius(double radius, double borderWidth)

The radius of a surface's top corner inside its outline, which is what the title bar's corners use.

ParameterTypeDefaultMeaning
radiusdoublerequiredThe surface's corner radius.
borderWidthdoublerequiredThe surface's outline width.

Returns — double. Example value: 23.0.

Errors — none.

final r = WindowChrome.innerRadius(24, 1);

ChromeSurface

Use it to say which surface wears the title bar: the chat window, the caption card or a component.

enum ChromeSurface { chat, caption, component }

What to expect — Only matters when the design's chrome is "window"; the caption card's bar is the small one.

CaseMeaning
chatThe chat window: a 32-high bar with the lights, the heading and the collapse chevron.
captionThe caption card: a 24-high bar titled "captions".
componentA component card: a 32-high bar with the component's own title.

Going live

Before the store build: the publishable key is the live one, app:// is in the agent's Allowed domains (Embed & Widgets), the microphone and notification permissions are asked for, and the native call engine is in the build (adding or updating graine_inapp needs a new store build). Turn the logs on while you test, and off for release.

Debug logging

Turn it on while you build and test, to see everything the SDK and the call engine do; turn it off for release.

Two switches, both off by default — turn them on in debug builds, for example with Flutter's kDebugMode:

SwitchWhat it logs
GraineConfig(debug: true)Every message between the SDK and the page that carries the voice call, both ways, the chat's frames, and the page's console, prefixed [Graine]. Outgoing variables and screen context are logged by key names and screen id only; incoming messages are cut to 600 characters. On Android the page becomes inspectable at chrome://inspect, on iOS in Safari's Web Inspector.
GraineCallsOptions(debug: true)The native call engine's log, in Xcode's console or logcat: how each call connected, what its audio did, and why one did not (on iOS, CallKit starting and stopping the call's audio too). Read once at launch: a change takes effect at the next launch.

"The agent did not speak" is answered in one read of these logs, and cannot be answered without them. They never show the customer anything.

GraineInApp.init(GraineConfig(baseUrl: 'https://www.graine.ai', publishableKey: 'pk_live_…', debug: kDebugMode));
await GraineCalls.instance.configure(GraineCallsOptions(publishableKey: 'pk_live_…', displayName: 'Acme Assistant', debug: kDebugMode));

GraineVersion

Read it to report which version of the package an app runs — in your logs, or when you ask for support.

abstract final class GraineVersion

What to expect — The SDK's version, as a constant: include it when you ask for support.

PropertyTypeMeaning
static const String nameStringThe package's version, as in its pubspec.yaml — '0.10.3' in this release.

Events and listeners

Everything an agent reports — its session arriving, the call's state, captions, components, the network, actions, friction, the agent speaking up in the chat, and errors — through one listener you register. Every callback is optional and runs on the main isolate.

GraineInApp.addListener

Call it to hear what the agent reports: register your listener once, usually at launch.

void addListener(GraineListener l)

Adds a listener for this agent's events. It is kept until removed; adding the same object twice has no further effect. A listener that throws is logged and does not stop the others.

ParameterTypeDefaultMeaning
lGraineListenerrequiredThe listener.

Returns — nothing; it returns when done.

Errors — none.

class ErrorLog extends GraineListener {
  @override
  void onError(String message) => debugPrint('[Graine] $message');
}
 
GraineInApp.instance.addListener(ErrorLog());

GraineInApp.removeListener

Call it when a listener should stop hearing from the agent — when its screen goes away.

void removeListener(GraineListener l)

Removes a listener.

ParameterTypeDefaultMeaning
lGraineListenerrequiredThe listener.

Returns — nothing; it returns when done.

Errors — none.

class ErrorLog extends GraineListener {
  @override
  void onError(String message) => debugPrint('[Graine] $message');
}
 
final log = ErrorLog();
GraineInApp.instance.addListener(log);
// … and when that screen goes away:
GraineInApp.instance.removeListener(log);

GraineListener

Implement it to hear what the agent reports; override only the callbacks you need.

Everything an agent reports. An abstract mixin class: use it with extends or with; every method has an empty default, so override only what you use. Register it with GraineInApp.addListener. Delivered on the main isolate.

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

Its functions: onSessionReady, onCallState, onCaption, onWidget, onWidgetPending, onEnded, onMicDenied, onQuality, onNetwork, onReconnecting, onTransport, onConnectivity, onActionRequested, onActionCompleted, onFriction, onAgentVolunteered, onError — each with its parameters, what it returns, the errors it raises and an example.

GraineListener.onSessionReady

Override it to act once the agent's settings have arrived — to show your own button only when they allow it.

void onSessionReady(SessionConfig session) {}

The agent's configuration has loaded.

ParameterTypeDefaultMeaning
sessionSessionConfigrequiredThe agent's settings.

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.

class AppEvents with GraineListener {
  @override
  void onSessionReady(SessionConfig session) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onCallState

Override it to follow the voice call connecting, connected and ending.

void onCallState(bool connected, bool connecting) {}

The voice call connected, started connecting, or stopped. Also fires when a connect attempt ends because nothing could carry the call.

ParameterTypeDefaultMeaning
connectedboolrequiredThe call is connected.
connectingboolrequiredA call is being placed.

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.

class AppEvents with GraineListener {
  @override
  void onCallState(bool connected, bool connecting) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onCaption

Override it to follow what is said on the call, as it is said.

void onCaption(Role role, String text, bool live) {}

A line of the voice call: repeated with live: true as it fills in, then once settled. The chat's lines are in GraineChatSession.turns.

ParameterTypeDefaultMeaning
roleRolerequiredWho said it: Role.agent or Role.customer.
textStringrequiredThe words so far.
liveboolrequiredStill being said or recognised; the same line arrives again, fuller, until it settles.

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.

class AppEvents with GraineListener {
  @override
  void onCaption(Role role, String text, bool live) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onWidget

Override it to hear when the agent draws a component — if you draw components yourself.

void onWidget(Map<String, Object?> widget) {}

The agent drew a component, in the call or the chat, as Graine sent it. GraineComponents already holds it for GraineWidgetView; an app drawing its own answers it with GraineVoice.answerWidget (call) or GraineChatSession.answerWidget (chat).

ParameterTypeDefaultMeaning
widgetMap<String, Object?>requiredThe component as Graine sent it; GraineWidget.adopt reads it.

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.

class AppEvents with GraineListener {
  @override
  void onWidget(Map<String, Object?> widget) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onWidgetPending

Override it to hear that a component is on its way.

void onWidgetPending(Map<String, Object?> hint) {}

A component is on its way and its content is loading; the hint carries its widget_id and kind when Graine gives them.

ParameterTypeDefaultMeaning
hintMap<String, Object?>requiredWhat is known of the coming component — its id and kind.

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.

class AppEvents with GraineListener {
  @override
  void onWidgetPending(Map<String, Object?> hint) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onEnded

Override it to hear that a call ended.

void onEnded() {}

The voice call is over, however it ended (including a hang-up the engine never confirmed). 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.

class AppEvents with GraineListener {
  @override
  void onEnded() {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onMicDenied

Override it to tell the customer the call needs the microphone.

void onMicDenied(String reason) {}

The microphone was refused: "NotAllowedError" when requestMicrophone answered false (or the engine asked for it after that), otherwise the engine's reason, such as "NotFoundError".

ParameterTypeDefaultMeaning
reasonStringrequiredNotAllowedError (refused) or NotFoundError (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.

class AppEvents with GraineListener {
  @override
  void onMicDenied(String reason) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onQuality

Override it to follow the call's connection quality.

void onQuality(ConnectionQuality quality) {}

One word for the connection; fires only when the word changes. lost when the network goes; unknown when a call ends.

ParameterTypeDefaultMeaning
qualityConnectionQualityrequiredThe connection now, in one word.

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.

class AppEvents with GraineListener {
  @override
  void onQuality(ConnectionQuality quality) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onNetwork

Override it to hear when the device goes offline or comes back.

void onNetwork(bool online) {}

The engine page's view of the network changed.

ParameterTypeDefaultMeaning
onlineboolrequiredtrue when the device came back online, false when it went offline.

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.

class AppEvents with GraineListener {
  @override
  void onNetwork(bool online) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onReconnecting

Override it to hear that the network interrupted the call and it is being re-established.

void onReconnecting(bool active) {}

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.

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.

class AppEvents with GraineListener {
  @override
  void onReconnecting(bool active) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onTransport

Override it to learn which way the call's audio really travels, and why it moved.

void onTransport(Transport transport, Transport requested, String? reason, String? message) {}

Which transport carries the call, which was asked for, and why they differ. With reason "not_configured", message is also reported to onError and held as GraineVoice.error.

ParameterTypeDefaultMeaning
transportTransportrequiredThe way the audio really travels.
requestedTransportrequiredThe way that was asked for.
reasonString?requiredWhy they differ, in one word (not_configured when high-quality audio is not enabled for the workspace); null when they do not.
messageString?requiredWhy, in a sentence for you; null when nothing went wrong.

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.

class AppEvents with GraineListener {
  @override
  void onTransport(Transport transport, Transport requested, String? reason, String? message) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onConnectivity

Override it to follow the page that carries the call loading, retrying and failing.

void onConnectivity(GraineConnectivity state, int attempt, String? reason) {}

The engine page is loading, ready, retrying or has failed. attempt is the number of reloads so far; reason is a short word such as "stalled", "boot_timeout", "load_cap" or "session".

ParameterTypeDefaultMeaning
stateGraineConnectivityrequiredWhere the page is: loading, ready, retrying or failed.
attemptintrequiredHow many reloads have been scheduled so far.
reasonString?requiredWhy it is retrying or failed, in one word (stalled, boot_timeout, load_cap, session, or the page's error); null 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.

class AppEvents with GraineListener {
  @override
  void onConnectivity(GraineConnectivity state, int attempt, String? reason) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onActionRequested

Override it to hear that the agent asked your app to do something.

void onActionRequested(String name) {}

The agent asked the app to run an action; fires before the handler runs.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, as registered.

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.

class AppEvents with GraineListener {
  @override
  void onActionRequested(String name) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onActionCompleted

Override it to hear what an action answered.

void onActionCompleted(String name, ActionResult result) {}

The answer the agent was given for that action.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, as registered.
resultActionResultrequiredWhat it answered — what the agent was told.

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.

class AppEvents with GraineListener {
  @override
  void onActionCompleted(String name, ActionResult result) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onFriction

Override it to hear that a customer looks stuck — to open the bar, say.

void onFriction(FrictionSignal signal) {}

The customer looks stuck on a step (stalled_on_step): on one screen, with a field outstanding, through a stretch of a live conversation. The conversation is told at the same moment; this is for the app — badge a control, count it.

ParameterTypeDefaultMeaning
signalFrictionSignalrequiredThe friction signal.

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.

class AppEvents with GraineListener {
  @override
  void onFriction(FrictionSignal signal) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onAgentVolunteered

Override it to hear that the agent spoke up in the chat unasked — to show a badge or a notification.

void onAgentVolunteered(String text) {}

In the chat, the agent spoke without being asked — its welcome, or something it noticed on screen; text is the turn's first words. Open the chat if it is closed.

ParameterTypeDefaultMeaning
textStringrequiredWhat the agent said.

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.

class AppEvents with GraineListener {
  @override
  void onAgentVolunteered(String text) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

GraineListener.onError

Override it to log what went wrong; the customer never sees these.

void onError(String message) {}

Something your team must fix, for your logs and never shown to the customer: the agent's configuration could not load, a call was started with no GraineEngine for the agent, a call was started while a native call holds the line, the engine page could not load after every retry, or a transport is not configured.

ParameterTypeDefaultMeaning
messageStringrequiredWhat went wrong, in a sentence for you — 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.

class AppEvents with GraineListener {
  @override
  void onError(String message) {
    // …
  }
}
 
GraineInApp.instance.addListener(AppEvents());

Errors

A request to Graine that is refused, or cannot be made, fails with one error type, GraineException, carrying a typed GraineErrorCode. Branch on the code; show the message, which is written for the customer and never carries the identifier, traits or a push token.

GraineException

Catch it around any call that asks Graine — memory, booking — and branch on its code.

What a request to Graine fails with: a typed code, a sentence written for whoever has to fix it, and, with a rate limit, how long to wait. Implements Exception; thrown by the memory and scheduling methods of GraineInApp.

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
final String messageStringrequiredThe sentence Graine (or the SDK) gave, written for whoever has to fix it.
final GraineErrorCode codeGraineErrorCodeGraineErrorCode.unknownWhy — branch on this. network when Graine could not be reached; unknown for a code this build does not know (see codeName).
final String? codeNameString?nullThe code as Graine said it, including a newer one this build does not know. Null when it said none.
final int? statusCodeint?nullThe refusal's HTTP status, or null when there was none.
final int? retryAfterSecondsint?nullWith rateLimited: how long Graine asked the app to wait, from the Retry-After header.
@override String toString()String—The message.

Its functions: GraineException — each with its parameters, what it returns, the errors it raises and an example.

GraineException.GraineException

Use it to build an error by hand for a test or a fake.

const GraineException(this.message, {this.code = GraineErrorCode.unknown, this.codeName, this.statusCode, this.retryAfterSeconds})

The constructor; the SDK builds these.

ParameterTypeDefaultMeaning
messageStringrequiredThe sentence Graine (or the SDK) gave, written for whoever has to fix it.
codeGraineErrorCodeGraineErrorCode.unknownWhy — branch on this. network when Graine could not be reached; unknown for a code this build does not know (see codeName).
codeNameString?optionalThe code as Graine said it, including a newer one this build does not know. Null when it said none.
statusCodeint?optionalThe refusal's HTTP status, or null when there was none.
retryAfterSecondsint?optionalWith rateLimited: how long Graine asked the app to wait, from the Retry-After header.

Returns — a new GraineException.

Errors — none.

// A fake refusal, for a widget test:
const refusal = GraineException('Book a call at least 5 minutes from now.', code: GraineErrorCode.tooSoon, codeName: 'too_soon');

GraineErrorCode

Branch on it to handle why Graine said no — and show the message.

Why Graine said no: the codes Graine sends, typed, plus network and unknown. When a refusal carries no code, the HTTP status decides: 400 invalidRequest, 401 invalidKey, 403 originRefused, 404 notFound, 429 rateLimited, 503 unavailable, anything else unknown.

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. Error codes and their fixes says when each happens and how to fix it.

FieldTypeDefaultMeaning
final String wireString—The word on the wire, as listed above.

Its functions: invalidKey, originRefused, rateLimited, invalidRequest, identityRequired, deviceLimit, schedulingOff, tooSoon, tooFar, outsideWindow, tooManyPending, notFound, unavailable, network, unknown, fromWire — each with its parameters, what it returns, the errors it raises and an example.

GraineErrorCode.invalidKey

Branch on it to handle the case where the publishable key was rejected.

invalidKey('invalid_key')

The publishable key was rejected. Check it against Embed & Widgets in the dashboard.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'invalid_key'.

Errors — none.

try {
  await GraineInApp.instance.memories();
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.invalidKey) debugPrint('Check the publishable key: ${e.message}');
}

GraineErrorCode.originRefused

Branch on it to handle the case where the agent does not accept connections from this app.

originRefused('origin_refused')

The agent does not accept connections from a mobile app: add app:// to its Allowed domains.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'origin_refused'.

Errors — none.

try {
  await GraineInApp.instance.memories();
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.originRefused) debugPrint('Add app:// to Allowed domains: ${e.message}');
}

GraineErrorCode.rateLimited

Branch on it to handle the case where there were too many requests.

rateLimited('rate_limited')

Too many requests. Wait retryAfterSeconds when given.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'rate_limited'.

Errors — none.

try {
  await GraineInApp.instance.scheduledCalls();
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.rateLimited) {
    await Future<void>.delayed(Duration(seconds: e.retryAfterSeconds ?? 30));   // then ask again
  }
}

GraineErrorCode.invalidRequest

Branch on it to handle the case where a request was missing something or malformed.

invalidRequest('invalid_request')

A field is missing or malformed. Also raised on the device for a booking reason over 120 characters and for an empty id to cancel.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'invalid_request'.

Errors — none.

try {
  await GraineInApp.instance.scheduleCall(at: at, reason: reason);
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.invalidRequest) showMessage(e.message);
}

GraineErrorCode.identityRequired

Branch on it to handle the case where the call needs an identified customer.

identityRequired('identity_required')

Nobody is identified. Raised on the device by memories and forgetMemory, and by Graine.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'identity_required'.

Errors — none.

try {
  await GraineInApp.instance.memories();
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.identityRequired) showSignIn(context);
}

GraineErrorCode.deviceLimit

Branch on it to handle the case where this device is not one of the customer's allowed devices.

deviceLimit('device_limit')

This install is not one of the user's devices.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'device_limit'.

Errors — none.

try {
  await GraineInApp.instance.memories();
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.deviceLimit) showMessage(e.message);
}

GraineErrorCode.schedulingOff

Branch on it to handle the case where the agent takes no bookings from the app.

schedulingOff('scheduling_off')

The agent does not take bookings from the app. Checked on the device.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'scheduling_off'.

Errors — none.

try {
  await GraineInApp.instance.scheduleCall(at: at);
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.schedulingOff) hideCallMeButton();
}

GraineErrorCode.tooSoon

Branch on it to handle the case where a booking was too soon.

tooSoon('too_soon')

Earlier than schedulingRules.earliest(now). Checked on the device.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'too_soon'.

Errors — none.

try {
  await GraineInApp.instance.scheduleCall(at: at);
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.tooSoon) showMessage(e.message);
}

GraineErrorCode.tooFar

Branch on it to handle the case where a booking was too far ahead.

tooFar('too_far')

Later than schedulingRules.latest(now). Checked on the device.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'too_far'.

Errors — none.

try {
  await GraineInApp.instance.scheduleCall(at: at);
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.tooFar) showMessage(e.message);
}

GraineErrorCode.outsideWindow

Branch on it to handle the case where a booking falls outside the agent's calling hours.

outsideWindow('outside_window')

Outside the agent's calling window.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'outside_window'.

Errors — none.

try {
  await GraineInApp.instance.scheduleCall(at: at);
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.outsideWindow) showMessage(e.message);   // it names a time that works
}

GraineErrorCode.tooManyPending

Branch on it to handle the case where the customer already has the most calls waiting.

tooManyPending('too_many_pending')

The user already has the most calls waiting that the agent allows.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'too_many_pending'.

Errors — none.

try {
  await GraineInApp.instance.scheduleCall(at: at);
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.tooManyPending) showBookedCalls();
}

GraineErrorCode.notFound

Branch on it to handle the case where the booking is not this customer's, or no longer exists.

notFound('not_found')

No such booking for this user or install.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'not_found'.

Errors — none.

try {
  await GraineInApp.instance.cancelScheduledCall(callId);
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.notFound) refreshList();
}

GraineErrorCode.unavailable

Branch on it to handle the case where Graine could not answer just now.

unavailable('unavailable')

Graine could not answer just now. Try again with backoff.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'unavailable'.

Errors — none.

try {
  await GraineInApp.instance.scheduledCalls();
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.unavailable) retryLater();
}

GraineErrorCode.network

Branch on it to handle the case where no answer came back.

network('network')

Graine could not be reached: offline, timed out, or the agent's configuration could not be loaded.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'network'.

Errors — none.

try {
  await GraineInApp.instance.scheduledCalls();
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.network) retryWhenOnline();
}

GraineErrorCode.unknown

Branch on it to handle the case where the code is newer than this SDK.

unknown('unknown')

A code this build does not know (its word is in GraineException.codeName), or an unexpected answer.

Parameters — none.

Returns — the value to compare GraineException.code with; its wire is 'unknown'.

Errors — none.

try {
  await GraineInApp.instance.scheduledCalls();
} on GraineException catch (e) {
  if (e.code == GraineErrorCode.unknown) debugPrint('Graine said ${e.codeName}: ${e.message}');
}

GraineErrorCode.fromWire

Use it to read an error code from its word.

static GraineErrorCode fromWire(String? s)

The value for a wire word (trimmed, any case); unknown for anything else.

ParameterTypeDefaultMeaning
sString?requiredThe code's word, as Graine sent it; anything this build does not know reads as unknown.

Returns — GraineErrorCode.

Errors — none.

final code = GraineErrorCode.fromWire('too_soon');   // GraineErrorCode.tooSoon

EmbedRefusal

Use it to turn a refused answer from Graine into the code and sentence the SDK reports.

class EmbedRefusal

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
final String messageStringThe sentence for whoever has to act on it — the customer, or you, for a key or a domain to fix.
final GraineErrorCode codeGraineErrorCodeThe code from the wire as a typed value. It is GraineErrorCode.unknown if the server sent a code this build does not recognise (see codeName), or when no code came and the status does not imply one.
final String? codeNameString?The server's own word for the code, or null if it sent no code.
final int? statusCodeint?The HTTP status of the refusal.
final int? retryAfterSecondsint?The wait before trying again, taken from the Retry-After header.

Its functions: EmbedRefusal, of — each with its parameters, what it returns, the errors it raises and an example.

EmbedRefusal.EmbedRefusal

Use it to build a refusal by hand for a test.

const EmbedRefusal(this.message, {required this.code, this.codeName, this.statusCode, this.retryAfterSeconds})
ParameterTypeDefaultMeaning
messageStringrequiredThe sentence for whoever has to act on it — the customer, or you, for a key or a domain to fix.
codeGraineErrorCoderequiredThe code from the wire as a typed value. It is GraineErrorCode.unknown if the server sent a code this build does not recognise (see codeName), or when no code came and the status does not imply one.
codeNameString?optionalThe server's own word for the code, or null if it sent no code.
statusCodeint?optionalThe HTTP status of the refusal.
retryAfterSecondsint?optionalThe wait before trying again, taken from the Retry-After header.

Returns — a new EmbedRefusal.

Errors — none.

const refusal = EmbedRefusal('Pick a time at least 5 minutes from now', code: GraineErrorCode.tooSoon, statusCode: 400);

EmbedRefusal.of

Use it to read a refusal from an answer's status, body and Retry-After.

static EmbedRefusal? of(int status, Map<String, Object?> body, {String? retryAfter})

Null for a 2xx.

ParameterTypeDefaultMeaning
statusintrequiredThe answer's HTTP status; a 2xx gives null.
bodyMap<String, Object?>requiredIts body, decoded from JSON.
retryAfterString?optionalIts Retry-After header, in seconds; null when absent.

Returns — EmbedRefusal?. Example value: null.

Errors — none.

final refusal = EmbedRefusal.of(429, {'error': 'Too many requests.', 'code': 'rate_limited'}, retryAfter: '30');

Error codes and their fixes

Use it to look up any code: what it means, which calls raise it, and what to do.

A method that asks Graine on the customer's behalf fails with one error type carrying a code. Branch on the code; show the message, which is written for the customer. Errors never carry the identifier, traits or a push token.

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
unknownunknownotherA code newer than the SDK, or an unexpected answer (carrying its word)Every requestLog the code and message; show the message

When the bar or the chat cannot start, the reason reaches GraineListener.onError as a sentence for you, never as a coded exception and never shown to the customer: a key Graine rejected ("Publishable key was rejected. Check it against Embed & Widgets in the dashboard."), an agent that does not accept the app ("This agent does not accept connections from a mobile app yet. Add "app://" to its Allowed domains in the dashboard, under Embed & Widgets."), Graine out of reach ("Could not reach the agent — the request timed out. Check the connection and baseUrl."), high-quality audio not enabled for the workspace (the call falls back to the standard path), "A call is already in progress." while a native call is live, or a start with no GraineEngine on screen. A refused microphone arrives as onMicDenied('NotAllowedError') (NotFoundError for no usable microphone). Native calls configured wrongly fail GraineCalls.configure with an ArgumentError, or a PlatformException with the code invalid_options naming the option.

Rate limits and quotas

Use it to know the limits a busy app or customer can reach, and where each applies.

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; credit and agent holds last at most 10 minutes
Booking reason · variables120 characters · 12 variables, about 1 KB — past that, the biggest are left off first, who_called and call_reason lastBookings 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 · 8 secondsAfter that, the agent is told the action did not finish; a component shows its authored content

Lower-level building blocks

Exports an integration rarely needs: the engine page and its messages, the chat connection and its frames, the context the agent reads, and the helpers the SDK's own views are built from. They are public so a fully custom surface, or a test, can reuse exactly what the SDK does — each with the same rules as every other Graine SDK.

GraineEnginePage

Implement it only to carry the voice call in a page of your own; GraineEngine builds the SDK's.

abstract class GraineEnginePage

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

Its functions: bind, load, reload, run, dispose — each with its parameters, what it returns, the errors it raises and an example.

GraineEnginePage.bind

Called once by the call, before anything else, with where to report what happens to the page.

void bind(GraineEngineSink sink)

GraineVoice.attach calls this once, ahead of everything else; from then on the page reports everything that happens to it through sink.

ParameterTypeDefaultMeaning
sinkGraineEngineSinkrequiredWhere to report what happens to the page.

Returns — nothing; it returns when done.

Errors — none.

/// A page for tests: it is ready at once, and keeps every script the call runs in it.
class FakeEnginePage implements GraineEnginePage {
  GraineEngineSink? sink;
  final scripts = <String>[];
 
  @override
  void bind(GraineEngineSink sink) => this.sink = sink;   // everything the page reports goes through it
  @override
  void load(String url) => sink?.pageFinished();
  @override
  void reload() => sink?.pageFinished();
  @override
  void run(String javaScript) => scripts.add(javaScript);
  @override
  void dispose() => sink = null;
}
 
GraineInApp.instance.voice.attach(FakeEnginePage());   // the call binds it, then loads it

GraineEnginePage.load

Called by the call to load the engine page.

void load(String url)

Load the engine page.

ParameterTypeDefaultMeaning
urlStringrequiredThe engine page's address (Protocol.engineUrl).

Returns — nothing; it returns when done.

Errors — none.

@override
void load(String url) => controller.loadRequest(Uri.parse(url));

GraineEnginePage.reload

Called by the call to reload the engine page.

void reload()

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

@override
void reload() => controller.reload();

GraineEnginePage.run

Called by the call to run a script in the page.

void run(String javaScript)

Evaluate JavaScript in the page.

ParameterTypeDefaultMeaning
javaScriptStringrequiredThe script to run (Protocol.deliver builds one).

Returns — nothing; it returns when done.

Errors — none.

@override
void run(String javaScript) => controller.runJavaScript(javaScript);

GraineEnginePage.dispose

Called by the call to tear the page down.

void dispose()

Tears the page down, after which nothing the page does reaches the call.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

@override
void dispose() {
  sink = null;   // nothing the page does reaches the call any more
  controller.loadRequest(Uri.parse('about:blank'));
}

GraineEngineSink

Use it, in a page of your own, to report everything that happens to the page to the call.

class GraineEngineSink

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

Its functions: pageStarted, progress, pageFinished, mainFrameError, message, microphoneRequested — each with its parameters, what it returns, the errors it raises and an example.

GraineEngineSink.pageStarted

Call it when the page starts loading.

void pageStarted()

A navigation began (a load, or a reload).

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

controller.setNavigationDelegate(NavigationDelegate(onPageStarted: (_) => sink?.pageStarted()));

GraineEngineSink.progress

Call it as the page downloads.

void progress(int percent)

Download progress, 0–100.

ParameterTypeDefaultMeaning
percentintrequiredHow much has downloaded, 0–100.

Returns — nothing; it returns when done.

Errors — none.

controller.setNavigationDelegate(NavigationDelegate(onProgress: (percent) => sink?.progress(percent)));

GraineEngineSink.pageFinished

Call it when the page has downloaded.

void pageFinished()

The document finished downloading.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

controller.setNavigationDelegate(NavigationDelegate(onPageFinished: (_) => sink?.pageFinished()));

GraineEngineSink.mainFrameError

Call it when the page could not load.

void mainFrameError(String reason)

Reports that the main frame did not load — because of a network error, an HTTP error or a renderer that crashed.

ParameterTypeDefaultMeaning
reasonStringrequiredWhy the page could not load, in a few words.

Returns — nothing; it returns when done.

Errors — none.

controller.setNavigationDelegate(NavigationDelegate(onWebResourceError: (error) {
  if (error.isForMainFrame ?? true) sink?.mainFrameError(error.description);
}));

GraineEngineSink.message

Call it with every message the page sends.

void message(String? raw)

Receives a message the page posted to window.ReactNativeWebView, one at a time.

ParameterTypeDefaultMeaning
rawString?requiredThe message, as the page posted it.

Returns — nothing; it returns when done.

Errors — none.

controller.addJavaScriptChannel(Protocol.bridgeName, onMessageReceived: (message) => sink?.message(message.message));

GraineEngineSink.microphoneRequested

Call it when the page asks for the microphone, to know whether to grant it.

bool microphoneRequested({required bool onlyMicrophone})

The page asks for capture devices.

ParameterTypeDefaultMeaning
onlyMicrophoneboolrequiredThe page asked for the microphone alone (not the camera) — the only request ever granted.

Returns — bool. Example value: true.

Errors — none.

// The page asked for the microphone: grant it only when the call says so.
final grant = sink?.microphoneRequested(onlyMicrophone: true) ?? false;

WebViewEnginePage

Use it as the engine page built on webview_flutter — what GraineEngine uses.

class WebViewEnginePage implements GraineEnginePage

What to expect — Built by GraineEngine — build one yourself only for an engine of your own. It never navigates away from Graine's host, grants the page the microphone only when the app holds it, and with debug logs the page's console.

PropertyTypeMeaning
late final WebViewController controllerWebViewControllerThe WebView's controller, for WebViewWidget.

Its functions: WebViewEnginePage — each with its parameters, what it returns, the errors it raises and an example.

WebViewEnginePage.WebViewEnginePage

Use it to make the engine page yourself, for an engine of your own.

WebViewEnginePage({required String baseUrl, bool debug = false})
ParameterTypeDefaultMeaning
baseUrlStringrequiredGraine's address (GraineConfig.baseUrl); the page never navigates away from its host.
debugboolfalseLog the page's console (prefixed [Graine] page console) and make the page inspectable in the platform's web inspector.

Returns — a new WebViewEnginePage.

Errors — none.

final result = WebViewEnginePage(baseUrl: 'https://www.graine.ai');

Protocol

Use it to speak to the engine page exactly as the SDK does: its address, its messages and the variables it carries.

abstract final class Protocol

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

PropertyTypeMeaning
static const String bridgeNameStringThe name the page looks for.
static const int actionDeadlineMsintThe time an action handler has to finish; after it, the agent hears the action was not done.
static const int ticketMaxKeysintCaps on the variables a ticket can carry, needed because the ticket is sent in a subprotocol header.
static const int ticketMaxKeyLengthintA variable's name is cut to 40 characters on a ticket.
static const int ticketMaxValueLengthintA variable's value is cut to 200 characters on a ticket.
static const int ticketMaxBytesintA ticket carries about 1 KB of variables; past that, the biggest are left off first, one at a time, ticketFirstKeys last.
static const List<String> ticketFirstKeysList<String>['who_called', 'call_reason'] — why a call is happening. A ticket carries them first, and leaves them off last when the variables pass its kilobyte. New in 0.9.0.

Its functions: engineUrl, deliver, out, toJson, parse, decodeJson, contextToken, ticketMetadata, trimTrailingSlashes — each with its parameters, what it returns, the errors it raises and an example.

Protocol.engineUrl

Use it to get the engine page's address for an agent.

static String engineUrl({ required String baseUrl, required String voiceAgentId, required Transport transport, required String cacheKey, required bool idle, required String publishableKey, String? accent})

The engine page's URL.

ParameterTypeDefaultMeaning
baseUrlStringrequired"https://www.graine.ai" unless your workspace is self-hosted.
voiceAgentIdStringrequiredThe agent's ID for calls.
transportTransportrequiredHow a call's audio travels: the standard path or high quality (WebRTC); left out, the dashboard decides.
cacheKeyStringrequiredThe cache key.
idleboolrequiredWhether it is idle.
publishableKeyStringrequiredThe agent's publishable key, pk_live_…, from Agent → Embed & Widgets → Install.
accentString?optionalThe accent colour, #RRGGBB.

Returns — String. Example value: 'https://www.graine.ai/embed/agent_123_web?mode=voice&branding=0&embed=1&transport=ws&b=1&idle=1&pk=pk_live_…'.

Errors — none.

final session = await GraineInApp.instance.loadSession();
if (session != null) {
  final url = Protocol.engineUrl(baseUrl: 'https://www.graine.ai', voiceAgentId: session.webcallAgentId, transport: Transport.websocket, cacheKey: session.pageVersion ?? '1', idle: true, publishableKey: 'pk_live_…');
}

Protocol.deliver

Use it to get the script that hands the page one message.

static String deliver(Map<String, Object?> message)

Builds the JavaScript that hands a single message to the page.

ParameterTypeDefaultMeaning
messageMap<String, Object?>requiredThe message (Protocol.out builds one).

Returns — String. Example value: 'window.postMessage({"type":"graine:set-muted","muted":true}, "*"); true;'.

Errors — none.

final script = Protocol.deliver(Protocol.out(ProtocolOut.setMuted, {'muted': true}));

Protocol.out

Use it to build one message for the page.

static Map<String, Object?> out(String type, [Map<String, Object?> fields = const <String, Object?>{}])

Assembles a message to send, shaped {type, ...fields}, after making each value safe for JSON.

ParameterTypeDefaultMeaning
typeStringrequiredThe message's type (ProtocolOut.startCall, …).
fieldsMap<String, Object?>const <String, Object?>{}Its other fields, made JSON-safe.

Returns — Map<String, Object?>. Example value: {'type': 'graine:set-muted', 'muted': true}.

Errors — none.

final message = Protocol.out(ProtocolOut.setMuted, {'muted': true});

Protocol.toJson

Use it to turn any value into what JSON can carry, as the SDK does.

static Object? toJson(Object? v)

Dart values → JSON values, recursively.

ParameterTypeDefaultMeaning
vObject?requiredAny value.

Returns — Object?. Example value: null.

Errors — none.

final json = Protocol.toJson({'plan': 'Pro', 'quality': ConnectionQuality.good, 'items': {'mug'}});   // {plan: Pro, quality: good, items: [mug]}

Protocol.parse

Use it to read one message from the page.

static PageMessage? parse(String? raw)

Parse one message from the page.

ParameterTypeDefaultMeaning
rawString?requiredOne message, as the page posted it.

Returns — PageMessage? — its fields: PageMessage. Example value: null.

Errors — none.

final message = Protocol.parse('{"type": "graine:muted", "muted": true}');   // PageMuted(true)

Protocol.decodeJson

Use it to read JSON the way the SDK does.

static Object? decodeJson(Object? raw)

Decodes a socket frame, given as text or UTF-8 bytes, into JSON.

ParameterTypeDefaultMeaning
rawObject?requiredText, or UTF-8 bytes.

Returns — Object?. Example value: null.

Errors — none.

final frame = Protocol.decodeJson('{"type": "text", "data": "Your order ships today."}');

Protocol.contextToken

Use it to build the variables' token a direct connection carries in its address.

static String? contextToken(Map<String, Object?>? variables, {String callType = 'webcall'})

The variables as a connection without a ticket carries them, in its address (?context_token=): base64 of the UTF-8 JSON of the values — nulls, maps and lists dropped, everything else as text — with call_type added at the top and again under recipient_data. Null when there is nothing to carry. A ticketed connection never carries it: its variables ride the ticket (ticketMetadata) — since 0.8.0 for the chat too.

ParameterTypeDefaultMeaning
variablesMap<String, Object?>?requiredThe prompt variables.
callTypeString'webcall'What the conversation is filed under: webcall for a call, text_chat for the chat.

Returns — String?. Example value: 'eyJwbGFuIjoiUHJvIiwiY2FsbF90eXBlIjoidGV4dF9jaGF0IiwicmVjaXBpZW50X2RhdGEiOnsicGxhbiI6IlBybyIsImNhbGxfdHlwZSI6InRleHRfY2hhdCJ9fQ=='.

Errors — none.

final token = Protocol.contextToken({'plan': 'Pro'}, callType: 'text_chat');

Protocol.ticketMetadata

Use it to get the variables that ride a ticket, cut to the ticket's limits.

static Map<String, String>? ticketMetadata(Map<String, Object?>? variables)

The variables a ticket carries: the first 12 usable values — who_called and call_reason (ticketFirstKeys) first, then the rest in order — names cut to 40 characters and values to 200, trimmed to fit a kilobyte (counted in UTF-8 bytes): the biggest are left off first, one at a time (the later of two the same size), and who_called and call_reason only when nothing else is left. Null when nothing is left to carry. Until 0.9.0 a set over the kilobyte lost every variable. A call's and, since 0.8.0, a chat's variables reach a ticketed connection this way only; they are no longer added to its address.

ParameterTypeDefaultMeaning
variablesMap<String, Object?>?requiredThe prompt variables.

Returns — Map<String, String>?. Example value: {'plan': 'Pro'} (a list is dropped).

Errors — none.

final metadata = Protocol.ticketMetadata({'plan': 'Pro', 'cart': ['mug']});

Protocol.trimTrailingSlashes

Use it to drop the slashes at the end of an address.

static String trimTrailingSlashes(String s)

baseUrl without its trailing slashes.

ParameterTypeDefaultMeaning
sStringrequiredAn address.

Returns — String. Example value: 'https://www.graine.ai'.

Errors — none.

final base = Protocol.trimTrailingSlashes('https://www.graine.ai//');

ProtocolOut

Use its names for the messages the SDK sends into the engine page.

abstract final class ProtocolOut

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

PropertyTypeMeaning
static const String startCallString'graine:start-call' — start the call.
static const String endCallString'graine:end-call' — hang up.
static const String setMutedString'graine:set-muted' — mute or unmute.
static const String warmString'graine:warm' — get ready, so a tap starts the call at once.
static const String variablesString'graine:variables' — the prompt variables.
static const String appContextString'graine:app-context' — the screen context.
static const String appActionResultString'graine:app-action-result' — the answer to an action.
static const String appEventString'graine:app-event' — something the agent may speak up about.
static const String widgetResultString'graine:widget-result' — the answer to a component.
static const String identityString'graine:identity' — who the customer is, for the tickets the page asks for: {value, installId, memory, timezone}, value null for an anonymous customer.

ProtocolIn

Use its names for the messages the engine page sends out.

abstract final class ProtocolIn

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

PropertyTypeMeaning
static const String readyString'graine:ready' — the page is ready to start a call.
static const String callString'graine:call' — the call connected, is connecting or ended.
static const String mutedString'graine:muted' — muted or unmuted.
static const String captionString'graine:caption' — a caption line.
static const String widgetString'graine:widget' — a component the agent drew.
static const String widgetPendingString'graine:widget-pending' — a component on its way.
static const String appActionString'graine:app-action' — an action the agent asks for.
static const String endedString'graine:ended' — the call ended.
static const String micDeniedString'graine:mic-denied' — the microphone was refused.
static const String transportString'graine:transport' — which way the audio travels.
static const String qualityString'graine:quality' — a connection measurement.
static const String networkString'graine:network' — online or offline.
static const String reconnectingString'graine:reconnecting' — the call is being re-established.
static const String progressString'graine:progress' — where a connect is: {stage} — registering, dialling, retrying, switching, or failed.

PageMessage

Read it as one message from the engine page, typed — Protocol.parse makes them.

sealed class PageMessage

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

Its functions: PageMessage — each with its parameters, what it returns, the errors it raises and an example.

PageMessage.PageMessage

The base constructor of the page's messages; you never call it directly.

const PageMessage()

Parameters — none.

Returns — nothing you call directly: PageMessage is sealed: messages come from Protocol.parse.

Errors — none.

final message = Protocol.parse('{"type": "graine:ready"}');   // PageReady, PageCall, PageCaption, … or null

PageReady

Read it as the page's word that it is ready to start a call.

final class PageReady extends PageMessage

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

Its functions: PageReady — each with its parameters, what it returns, the errors it raises and an example.

PageReady.PageReady

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageReady()

Parameters — none.

Returns — a new PageReady.

Errors — none.

final result = PageReady();

PageCall

Read it as the page's word that the call connected, started connecting, or ended.

final class PageCall extends PageMessage

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

PropertyTypeMeaning
final bool connectedboolThe call is connected.
final bool connectingboolThe call is connecting.

Its functions: PageCall — each with its parameters, what it returns, the errors it raises and an example.

PageCall.PageCall

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageCall({required this.connected, required this.connecting})
ParameterTypeDefaultMeaning
connectedboolrequiredThe call is connected.
connectingboolrequiredThe call is connecting.

Returns — a new PageCall.

Errors — none.

const message = PageCall(connected: true, connecting: false);

PageMuted

Read it as the page's word that the call was muted or unmuted.

final class PageMuted extends PageMessage

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

PropertyTypeMeaning
final bool mutedboolTrue: muted.

Its functions: PageMuted — each with its parameters, what it returns, the errors it raises and an example.

PageMuted.PageMuted

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageMuted(this.muted)
ParameterTypeDefaultMeaning
mutedboolrequiredTrue: muted.

Returns — a new PageMuted.

Errors — none.

final result = PageMuted(false);

PageCaption

Read it as one caption line from the page.

final class PageCaption extends PageMessage

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

PropertyTypeMeaning
final Role roleRoleWho said it.
final String textStringThe words so far.
final bool liveboolStill being said or recognised.

Its functions: PageCaption — each with its parameters, what it returns, the errors it raises and an example.

PageCaption.PageCaption

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageCaption({required this.role, required this.text, required this.live})
ParameterTypeDefaultMeaning
roleRolerequiredWho said it.
textStringrequiredThe words so far.
liveboolrequiredStill being said or recognised.

Returns — a new PageCaption.

Errors — none.

const caption = PageCaption(role: Role.customer, text: 'Where is my order?', live: true);

PageWidget

Read it as a component the agent drew, from the page.

final class PageWidget extends PageMessage

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

PropertyTypeMeaning
final Map<String, Object?> widgetMap<String, Object?>The component as sent; GraineWidget.adopt reads it.

Its functions: PageWidget — each with its parameters, what it returns, the errors it raises and an example.

PageWidget.PageWidget

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageWidget(this.widget)
ParameterTypeDefaultMeaning
widgetMap<String, Object?>requiredThe component as sent; GraineWidget.adopt reads it.

Returns — a new PageWidget.

Errors — none.

const message = PageWidget({'widget_id': 'w_1', 'kind': 'choices'});

PageWidgetPending

Read it as the page's word that a component is on its way.

final class PageWidgetPending extends PageMessage

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

PropertyTypeMeaning
final Map<String, Object?> widgetMap<String, Object?>What is known of the coming component.

Its functions: PageWidgetPending — each with its parameters, what it returns, the errors it raises and an example.

PageWidgetPending.PageWidgetPending

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageWidgetPending(this.widget)
ParameterTypeDefaultMeaning
widgetMap<String, Object?>requiredWhat is known of the coming component.

Returns — a new PageWidgetPending.

Errors — none.

const message = PageWidgetPending({'widget_id': 'w_1', 'kind': 'choices'});

PageAppAction

Read it as an action the agent asks your app to do, from the page.

final class PageAppAction extends PageMessage

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

PropertyTypeMeaning
final String actionIdStringThe request's id; the answer carries it back.
final String nameStringThe action's name, as you registered it.
final Map<String, Object?> argumentsMap<String, Object?>The action's arguments.

Its functions: PageAppAction — each with its parameters, what it returns, the errors it raises and an example.

PageAppAction.PageAppAction

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageAppAction({required this.actionId, required this.name, required this.arguments})
ParameterTypeDefaultMeaning
actionIdStringrequiredThe request's id; the answer carries it back.
nameStringrequiredThe action's name, as you registered it.
argumentsMap<String, Object?>requiredThe action's arguments.

Returns — a new PageAppAction.

Errors — none.

const message = PageAppAction(actionId: 'a_1', name: 'set_language', arguments: {'language': 'fr'});

PageEnded

Read it as the page's word that the call ended.

final class PageEnded extends PageMessage

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

Its functions: PageEnded — each with its parameters, what it returns, the errors it raises and an example.

PageEnded.PageEnded

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageEnded()

Parameters — none.

Returns — a new PageEnded.

Errors — none.

final result = PageEnded();

PageMicDenied

Read it as the page's word that the microphone was refused.

final class PageMicDenied extends PageMessage

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

PropertyTypeMeaning
final String reasonStringNotAllowedError (refused) or NotFoundError (no usable microphone).

Its functions: PageMicDenied — each with its parameters, what it returns, the errors it raises and an example.

PageMicDenied.PageMicDenied

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageMicDenied(this.reason)
ParameterTypeDefaultMeaning
reasonStringrequiredNotAllowedError (refused) or NotFoundError (no usable microphone).

Returns — a new PageMicDenied.

Errors — none.

const message = PageMicDenied('NotAllowedError');

PageTransport

Read it as the page's word on which way the audio travels, and why.

final class PageTransport extends PageMessage

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

PropertyTypeMeaning
final Transport transportTransportThe way the audio really travels.
final Transport requestedTransportThe way that was asked for.
final String? reasonString?Why they differ, in one word; null when they do not.
final String? messageString?Why, in a sentence; null when nothing went wrong.

Its functions: PageTransport — each with its parameters, what it returns, the errors it raises and an example.

PageTransport.PageTransport

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageTransport({required this.transport, required this.requested, this.reason, this.message})
ParameterTypeDefaultMeaning
transportTransportrequiredThe way the audio really travels.
requestedTransportrequiredThe way that was asked for.
reasonString?optionalWhy they differ, in one word; null when they do not.
messageString?optionalWhy, in a sentence; null when nothing went wrong.

Returns — a new PageTransport.

Errors — none.

const message = PageTransport(transport: Transport.websocket, requested: Transport.webrtc, reason: 'ice_timeout');

PageQuality

Read it as one connection measurement from the page.

final class PageQuality extends PageMessage

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

PropertyTypeMeaning
final double? rttMsdouble?Round-trip time, in ms; null when not measured.
final double? jitterMsdouble?Jitter, in ms; null when not measured.
final double? lossdouble?Packet loss as a fraction (0.02 is 2%); null when not measured.

Its functions: PageQuality — each with its parameters, what it returns, the errors it raises and an example.

PageQuality.PageQuality

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageQuality({this.rttMs, this.jitterMs, this.loss})
ParameterTypeDefaultMeaning
rttMsdouble?optionalRound-trip time, in ms; null when not measured.
jitterMsdouble?optionalJitter, in ms; null when not measured.
lossdouble?optionalPacket loss as a fraction (0.02 is 2%); null when not measured.

Returns — a new PageQuality.

Errors — none.

final result = PageQuality();

PageNetwork

Read it as the page's word that the device went offline or came back.

final class PageNetwork extends PageMessage

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

PropertyTypeMeaning
final bool onlineboolTrue: the device is online.

Its functions: PageNetwork — each with its parameters, what it returns, the errors it raises and an example.

PageNetwork.PageNetwork

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageNetwork(this.online)
ParameterTypeDefaultMeaning
onlineboolrequiredTrue: the device is online.

Returns — a new PageNetwork.

Errors — none.

final result = PageNetwork(true);

PageReconnecting

Read it as the page's word that the call is being re-established, or is back.

final class PageReconnecting extends PageMessage

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

PropertyTypeMeaning
final bool activeboolTrue while the call is being re-established.

Its functions: PageReconnecting — each with its parameters, what it returns, the errors it raises and an example.

PageReconnecting.PageReconnecting

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageReconnecting(this.active)
ParameterTypeDefaultMeaning
activeboolrequiredTrue while the call is being re-established.

Returns — a new PageReconnecting.

Errors — none.

final result = PageReconnecting(true);

PageProgress

Read it as the page's word on where a call being placed is.

final class PageProgress extends PageMessage

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

PropertyTypeMeaning
final String stageStringThe stage as sent: registering, dialling, retrying, switching or failed.
bool get failedboolTrue when no transport could carry the call, so connecting should stop.

Its functions: PageProgress — each with its parameters, what it returns, the errors it raises and an example.

PageProgress.PageProgress

Use it to build the message by hand for a test; Protocol.parse makes the real ones.

const PageProgress(this.stage)
ParameterTypeDefaultMeaning
stageStringrequiredThe stage as sent: registering, dialling, retrying, switching or failed.

Returns — a new PageProgress.

Errors — none.

const message = PageProgress('retrying');

LoadWatchdog

Use it to tell a slow engine page from a stuck one, as the SDK does.

class LoadWatchdog

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
final int stallMsintHow long nothing may arrive before the load is stalled, in ms.
final int bootMsintHow long a downloaded page may take to be ready, in ms.
final int hardCapMsintThe ceiling on one load, in ms.

Its functions: LoadWatchdog, start, progress, loaded, check, nextCheckInMs — each with its parameters, what it returns, the errors it raises and an example.

LoadWatchdog.LoadWatchdog

Use it to make a watchdog with its three limits.

LoadWatchdog({this.stallMs = 15000, this.bootMs = 15000, this.hardCapMs = 120000})
ParameterTypeDefaultMeaning
stallMsint15000How long nothing may arrive before the load is stalled, in ms.
bootMsint15000How long a downloaded page may take to be ready, in ms.
hardCapMsint120000The ceiling on one load, in ms.

Returns — a new LoadWatchdog.

Errors — none.

final result = LoadWatchdog();

LoadWatchdog.start

Call it when a load starts.

void start(int now)
ParameterTypeDefaultMeaning
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — nothing; it returns when done.

Errors — none.

final watchdog = LoadWatchdog();
watchdog.start(DateTime.now().millisecondsSinceEpoch);   // a load began

LoadWatchdog.progress

Call it as the page downloads.

void progress(int percent, int now)

Progress 0–100 from the WebView.

ParameterTypeDefaultMeaning
percentintrequiredHow much has downloaded, 0–100.
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — nothing; it returns when done.

Errors — none.

final watchdog = LoadWatchdog()..start(DateTime.now().millisecondsSinceEpoch);
watchdog.progress(60, DateTime.now().millisecondsSinceEpoch);

LoadWatchdog.loaded

Call it when the page has downloaded.

void loaded(int now)

Marks the page as downloaded; from now on it is starting up rather than fetching.

ParameterTypeDefaultMeaning
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — nothing; it returns when done.

Errors — none.

final watchdog = LoadWatchdog()..start(DateTime.now().millisecondsSinceEpoch);
watchdog.loaded(DateTime.now().millisecondsSinceEpoch);

LoadWatchdog.check

Call it to know whether to keep waiting.

LoadVerdict check(int now)
ParameterTypeDefaultMeaning
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — LoadVerdict — its fields: LoadVerdict. Example value: LoadVerdict.wait.

Errors — none.

final watchdog = LoadWatchdog()..start(DateTime.now().millisecondsSinceEpoch);
final verdict = watchdog.check(DateTime.now().millisecondsSinceEpoch);

LoadWatchdog.nextCheckInMs

Use it to know when to check again.

int nextCheckInMs(int now)

How soon to check again — whichever of the two limits comes first.

ParameterTypeDefaultMeaning
nowintrequiredThe time now, in milliseconds since the epoch — DateTime.now().millisecondsSinceEpoch; a test passes its own.

Returns — int. Example value: 3.

Errors — none.

final watchdog = LoadWatchdog()..start(DateTime.now().millisecondsSinceEpoch);
final wait = watchdog.nextCheckInMs(DateTime.now().millisecondsSinceEpoch);

LoadVerdict

Read it as what the load watchdog concluded.

enum LoadVerdict

What to expect — Give every switch over it a default case.

CaseMeaning
wait('wait')Still moving: keep waiting.
stalled('stalled')Nothing has arrived for stallMs: a dead connection — reload.
bootTimeout('boot_timeout')Everything arrived but the page never said it was ready within bootMs: broken, not slow — reload.
loadCap('load_cap')The load ran past hardCapMs however steadily it arrived — reload.
PropertyTypeMeaning
final String reasonStringThe verdict as one word, as onConnectivity reports it.

Its functions: LoadVerdict — each with its parameters, what it returns, the errors it raises and an example.

LoadVerdict.LoadVerdict

The enum's constructor; use the values (LoadVerdict.wait, …).

const LoadVerdict(this.reason)
ParameterTypeDefaultMeaning
reasonStringrequiredThe verdict as one word, as onConnectivity reports it.

Returns — nothing you call: an enum's constructor runs once for each value. Use the values; each carries its word in wire (here reason).

Errors — none.

const value = LoadVerdict.bootTimeout;
print(value.reason);   // boot_timeout

RetryLadder

Use it to space out reloads of a page that failed, and to know when to stop.

class RetryLadder

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
int get attemptintReloads scheduled so far.
bool get exhaustedboolTrue once every delay is spent: stop and say so.

Its functions: RetryLadder, next, reset — each with its parameters, what it returns, the errors it raises and an example.

RetryLadder.RetryLadder

Use it to make a ladder with its delays.

RetryLadder({ List<int> delaysMs = const <int>[1000, 2000, 4000, 8000, 15000, 15000], double Function()? random})
ParameterTypeDefaultMeaning
delaysMsList<int>const <int>[1000, 2000, 4000, 8000, 15000, 15000]The waits between reloads, in ms; each is jittered by ±25%, and their number is how many reloads are tried.
randomdouble Function()?optionalWhere the jitter comes from, 0–1; the platform's random when left out.

Returns — a new RetryLadder.

Errors — none.

final result = RetryLadder();

RetryLadder.next

Use it to get the wait before the next attempt.

int? next()

Returns how long to wait before the next try, or null once all attempts are used up.

Parameters — none.

Returns — int?. Example value: 3.

Errors — none.

final ladder = RetryLadder();
final delay = ladder.next();   // about 1000 ms the first time; null once the ladder is spent

RetryLadder.reset

Call it to start the ladder over.

void reset()

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final ladder = RetryLadder();
ladder.reset();   // a load that worked: the next failure starts from the first rung

GraineChatSocket

Implement it only to carry the chat on a connection of your own.

abstract class GraineChatSocket

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.

PropertyTypeMeaning
Stream<Object?> get framesStream<Object?>Each frame as it comes in; the stream completes when the socket closes.

Its functions: send, close — each with its parameters, what it returns, the errors it raises and an example.

GraineChatSocket.send

Called by the chat to send one frame.

void send(String data)

Put one text frame on the wire.

ParameterTypeDefaultMeaning
dataStringrequiredOne frame, as JSON text.

Returns — nothing; it returns when done.

Errors — none.

@override
void send(String data) => ws.add(data);

GraineChatSocket.close

Called by the chat to close the connection.

Future<void> close()

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

@override
Future<void> close() => ws.close();

GraineChatConnector

Implement it only to open the chat's connection yourself — for a test.

typedef GraineChatConnector = Future<GraineChatSocket> Function(Uri url, List<String>? protocols);

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.

ChatWire

Use its constants and frames to speak the chat's protocol exactly as the SDK does.

abstract final class ChatWire

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.

PropertyTypeMeaning
static const String subprotocolStringThe subprotocol a ticketed chat connection asks for: 'graine.embed.v1'. The ticket travels beside it, never in the address.
static const int pingMsintHow often the chat sends a keep-alive, in ms: 25 000.
static const int openTimeoutMsintIf the socket is not open within this time, the attempt is abandoned.
static const int stopGraceMsintThe time allowed for stop to be sent before the socket is closed.
static const String clientStringWho this client is, in the chat's first frame: 'graine-flutter-sdk', filed with the conversation.
static const String defaultWsHostStringWhere a direct chat connects when the session names no host: 'wss://ws.graine.ai'.
static const String callTypeStringThe call_type value under which a text conversation is recorded.
static const Map<String, Object?> pingMap<String, Object?>The keep-alive frame: {'type': 'ping'}.
static const Map<String, Object?> stopMap<String, Object?>The frame that hangs up properly, so the record shows the conversation ended instead of being abandoned.

Its functions: directUrl, withContextToken, init, text, widgetResult, actionResult, appContext, appEvent — each with its parameters, what it returns, the errors it raises and an example.

ChatWire.directUrl

Use it to get the address of a chat that connects directly, without a ticket.

static String directUrl(String? wsHost, String agentId)
ParameterTypeDefaultMeaning
wsHostString?requiredThe session's host (session.wsHost); the default host when null.
agentIdStringrequiredThe agent's id.

Returns — String. Example value: 'wss://ws.graine.ai/chat/v1/agent_123?text_chat=1'.

Errors — none.

final url = ChatWire.directUrl(null, 'agent_123');

ChatWire.withContextToken

Use it to add the variables' token to a direct chat's address.

static String withContextToken(String url, String? token)

url with the variables' context_token added, when there is one. Since 0.8.0 only a chat without a ticket uses it: a ticketed chat's variables ride its ticket.

ParameterTypeDefaultMeaning
urlStringrequiredA direct chat's address.
tokenString?requiredThe variables' token (Protocol.contextToken); null leaves the address as it is.

Returns — String. Example value: 'wss://ws.graine.ai/chat/v1/agent_123?text_chat=1&context_token=eyJwbGFuIjoiUHJvIiwiY2FsbF90eXBlIjoidGV4dF9jaGF0IiwicmVjaXBpZW50X2RhdGEiOnsicGxhbiI6IlBybyIsImNhbGxfdHlwZSI6InRleHRfY2hhdCJ9fQ%3D%3D'.

Errors — none.

final url = ChatWire.withContextToken(ChatWire.directUrl(null, 'agent_123'), Protocol.contextToken({'plan': 'Pro'}, callType: ChatWire.callType));

ChatWire.init

Use it to build the chat's first frame.

static Map<String, Object?> init(Map<String, String> variables)

Builds the opening frame — a start — which carries the variables for the record.

ParameterTypeDefaultMeaning
variablesMap<String, String>requiredThe prompt variables, already merged and masked.

Returns — a new ChatWire.

Errors — none.

final frame = ChatWire.init({'plan': 'Pro'});

ChatWire.text

Use it to build one message from the customer.

static Map<String, Object?> text(String data)

One customer turn.

ParameterTypeDefaultMeaning
dataStringrequiredThe customer's message.

Returns — Map<String, Object?>. Example value: {'type': 'text', 'data': 'Where is my order?'}.

Errors — none.

final frame = ChatWire.text('Where is my order?');

ChatWire.widgetResult

Use it to build the answer to a component.

static Map<String, Object?> widgetResult(String widgetId, Map<String, Object?> data)

The structured answer to a component.

ParameterTypeDefaultMeaning
widgetIdStringrequiredThe component answered.
dataMap<String, Object?>requiredThe structured answer.

Returns — Map<String, Object?>.

Errors — none.

final frame = ChatWire.widgetResult('w_1', {'id': 'mug-blue', 'title': 'Blue Mug'});

ChatWire.actionResult

Use it to build the answer to an action.

static Map<String, Object?> actionResult(String actionId, ActionResult r)

The answer to an app_action, always given.

ParameterTypeDefaultMeaning
actionIdStringrequiredThe request's id (ChatAction.actionId).
rActionResultrequiredWhat the action answered.

Returns — Map<String, Object?>.

Errors — none.

final frame = ChatWire.actionResult('act_1', const ActionResult.ok('Done'));   // the action's id, from ChatAction.actionId

ChatWire.appContext

Use it to build the screen context for the chat.

static Map<String, Object?> appContext(Map<String, Object?> frame)

A context frame — {context, availableActions?, seq}, as the SDK builds it — in the chat's own spelling (available_actions).

ParameterTypeDefaultMeaning
frameMap<String, Object?>requiredThe screen context (ContextFrame.build).

Returns — Map<String, Object?>.

Errors — none.

final now = DateTime.now().millisecondsSinceEpoch;
final screen = ContextFrame.build(screen: const GraineScreen('checkout_address'), screenSinceMs: now - 12000, nowMs: now);
final frame = screen == null ? null : ChatWire.appContext(screen);

ChatWire.appEvent

Use it to build an event the agent may speak up about.

static Map<String, Object?> appEvent(String name, {String? detail, Map<String, Object?> data = const <String, Object?>{}})

Builds an event the agent might want to comment on, or, when it is quiet, only needs to know about.

ParameterTypeDefaultMeaning
nameStringrequiredThe event's name.
detailString?optionalA sentence about it; null for none.
dataMap<String, Object?>const <String, Object?>{}Facts about it.

Returns — Map<String, Object?>.

Errors — none.

final frame = ChatWire.appEvent('coupon_applied', detail: 'The customer applied SAVE10.');

AgentChunk

Read it as one piece of the agent's words, and what it says about the agent's turn.

class AgentChunk

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

PropertyTypeMeaning
static const String streamStartStringThe marker that opens an agent's turn: '<beginning_of_stream>'. It is never shown.
static const String streamEndStringThe marker that ends an agent's turn: '<end_of_stream>'. A reply ends there — one bubble per reply.
final String textStringWhat the customer should see.
final bool startsTurnboolThe frame opened the agent's turn.
final bool endsTurnboolThe frame ended the agent's turn: the next words open a new bubble.

Its functions: AgentChunk, read — each with its parameters, what it returns, the errors it raises and an example.

AgentChunk.AgentChunk

Use it to build a piece by hand for a test.

const AgentChunk(this.text, {this.startsTurn = false, this.endsTurn = false})
ParameterTypeDefaultMeaning
textStringrequiredWhat the customer should see.
startsTurnboolfalseThe frame opened the agent's turn.
endsTurnboolfalseThe frame ended the agent's turn: the next words open a new bubble.

Returns — a new AgentChunk.

Errors — none.

const chunk = AgentChunk('Your order ships today.', endsTurn: true);

AgentChunk.read

Use it to read one text frame into the words to show and the turn markers.

static AgentChunk read(Object? raw)
ParameterTypeDefaultMeaning
rawObject?requiredOne text frame's data, as received.

Returns — AgentChunk.

Errors — none.

final chunk = AgentChunk.read('<beginning_of_stream>Your order ships today.');   // startsTurn: true

ChatFrame

Read it as one frame from the chat's connection, typed.

sealed class ChatFrame

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.

Its functions: ChatFrame, parse — each with its parameters, what it returns, the errors it raises and an example.

ChatFrame.ChatFrame

The base constructor of the chat's frames; you never call it directly.

const ChatFrame()

Parameters — none.

Returns — nothing you call directly: ChatFrame is sealed: frames come from ChatFrame.parse.

Errors — none.

final frame = ChatFrame.parse('{"type": "text", "data": "Your order ships today."}');   // ChatText, ChatWidget, ChatAction, … or null

ChatFrame.parse

Use it to read one frame from the chat's connection.

static ChatFrame? parse(Object? raw)

Parses a single socket frame, returning null for anything that is not one of the chat's own frames.

ParameterTypeDefaultMeaning
rawObject?requiredOne frame, as received (text or UTF-8 bytes).

Returns — ChatFrame?. Example value: null.

Errors — none.

final frame = ChatFrame.parse('{"type": "app_action", "action": {"action_id": "act_1", "name": "open_screen", "arguments": {"path": "/orders"}}}');

ChatText

Read it as the agent's words, or a turn marker.

final class ChatText extends ChatFrame

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.

PropertyTypeMeaning
final AgentChunk chunkAgentChunkThe words, with the turn markers already taken out.

Its functions: ChatText — each with its parameters, what it returns, the errors it raises and an example.

ChatText.ChatText

Use it to build a text frame by hand for a test.

const ChatText(this.chunk)
ParameterTypeDefaultMeaning
chunkAgentChunkrequiredThe words, with the turn markers already taken out.

Returns — a new ChatText.

Errors — none.

const frame = ChatText(AgentChunk('Your order ships today.', startsTurn: true));

ChatWidget

Read it as a component the agent drew in the chat.

final class ChatWidget extends ChatFrame

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.

PropertyTypeMeaning
final Map<String, Object?> widgetMap<String, Object?>The component as Graine sent it; GraineWidget.adopt reads it.

Its functions: ChatWidget — each with its parameters, what it returns, the errors it raises and an example.

ChatWidget.ChatWidget

Use it to build a component frame by hand for a test.

const ChatWidget(this.widget)
ParameterTypeDefaultMeaning
widgetMap<String, Object?>requiredThe component as Graine sent it; GraineWidget.adopt reads it.

Returns — a new ChatWidget.

Errors — none.

final frame = ChatWidget({'widget_id': 'w_1', 'kind': 'choices', 'data': {'options': ['Morning', 'Evening']}});

ChatWidgetPending

Read it as a component on its way.

final class ChatWidgetPending extends ChatFrame

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.

PropertyTypeMeaning
final Map<String, Object?> hintMap<String, Object?>What is known of the coming component — its id and kind.

Its functions: ChatWidgetPending — each with its parameters, what it returns, the errors it raises and an example.

ChatWidgetPending.ChatWidgetPending

Use it to build a component-on-its-way frame by hand for a test.

const ChatWidgetPending(this.hint)
ParameterTypeDefaultMeaning
hintMap<String, Object?>requiredWhat is known of the coming component — its id and kind.

Returns — a new ChatWidgetPending.

Errors — none.

const frame = ChatWidgetPending({'widget_id': 'w_1', 'kind': 'product_list'});

ChatAction

Read it as something the agent asks your app to do in the chat.

final class ChatAction extends ChatFrame

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.

PropertyTypeMeaning
final String actionIdStringThe request's id; the answer (ChatWire.actionResult) carries it back.
final String nameStringThe action's name, as you registered it.
final Map<String, Object?> argumentsMap<String, Object?>The action's arguments, as JSON values.

Its functions: ChatAction — each with its parameters, what it returns, the errors it raises and an example.

ChatAction.ChatAction

Use it to build an action frame by hand for a test.

const ChatAction({required this.actionId, required this.name, required this.arguments})
ParameterTypeDefaultMeaning
actionIdStringrequiredThe request's id; the answer (ChatWire.actionResult) carries it back.
nameStringrequiredThe action's name, as you registered it.
argumentsMap<String, Object?>requiredThe action's arguments, as JSON values.

Returns — a new ChatAction.

Errors — none.

const frame = ChatAction(actionId: 'a_1', name: 'set_language', arguments: {'language': 'fr'});

ChatStream

Read it as an explicit start or end of the agent's turn.

final class ChatStream extends ChatFrame

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.

PropertyTypeMeaning
final bool startboolTrue for the start of the agent's turn, false for its end.

Its functions: ChatStream — each with its parameters, what it returns, the errors it raises and an example.

ChatStream.ChatStream

Use it to build a turn marker by hand for a test.

const ChatStream({required this.start})
ParameterTypeDefaultMeaning
startboolrequiredTrue for the start of the agent's turn, false for its end.

Returns — a new ChatStream.

Errors — none.

final result = ChatStream(start: true);

ChatClear

Read it as the agent's turn being cut off.

final class ChatClear extends ChatFrame

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.

Its functions: ChatClear — each with its parameters, what it returns, the errors it raises and an example.

ChatClear.ChatClear

Use it to build a cut-off frame by hand for a test.

const ChatClear()

Parameters — none.

Returns — a new ChatClear.

Errors — none.

final result = ChatClear();

ChatHistory

Read it as the earlier conversation the chat restores when an identified customer comes back.

final class ChatHistory extends ChatFrame

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.

PropertyTypeMeaning
final List<GraineChatTurn> turnsList<GraineChatTurn>The restored messages, oldest first — possibly none. Each is restored, delivered and never live.

Its functions: ChatHistory — each with its parameters, what it returns, the errors it raises and an example.

ChatHistory.ChatHistory

Use it to build a restored conversation by hand for a test.

const ChatHistory(this.turns)
ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe restored messages, oldest first — possibly none. Each is restored, delivered and never live.

Returns — a new ChatHistory.

Errors — none.

final result = ChatHistory(const []);

ChatHistoryReader

Use it to read a restored conversation exactly as the SDK does.

abstract final class ChatHistoryReader

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.

Its functions: read, time — each with its parameters, what it returns, the errors it raises and an example.

ChatHistoryReader.read

Use it to read the turns of a restored conversation.

static List<GraineChatTurn>? read(Map<String, Object?> frame)

The turns a restored-conversation frame holds, oldest first, or null when its turns is not a list.

ParameterTypeDefaultMeaning
frameMap<String, Object?>requiredA restored-conversation frame.

Returns — List<GraineChatTurn>?. Example value: [].

Errors — none.

final turns = ChatHistoryReader.read({'type': 'history', 'turns': [{'id': 'm1', 'role': 'user', 'text': 'Hi', 'at': '2026-10-14T12:30:00Z', 'channel': 'chat'}], 'continued': true});

ChatHistoryReader.time

Use it to read one turn's time.

static DateTime? time(String raw)

Reads an ISO-8601 time that has a zone, with or without fractions of a second, and returns it in UTC; the server writes UTC with a Z.

ParameterTypeDefaultMeaning
rawStringrequiredA time with its zone, ISO-8601.

Returns — DateTime?. Example value: null.

Errors — none.

final at = ChatHistoryReader.time('2026-10-14T12:30:00Z');

ChatTranscript

Use it to turn the chat's frames into its turns exactly as the SDK does — one bubble per agent reply.

class ChatTranscript

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.

PropertyTypeMeaning
final int maxintThe number of turns kept; when full, the oldest are dropped first.
List<GraineChatTurn> get turnsList<GraineChatTurn>Every turn kept, oldest first (at most max). Unmodifiable.
List<Caption> get messagesList<Caption>Each turn as a Caption, for windows built to show captions.
bool get agentTypingboolTrue while the agent is preparing an answer and has not yet said any of it.
List<GraineChatTurn> get pendingList<GraineChatTurn>The user's turns that have not been sent yet (still sending, or failed), oldest first.

Its functions: agent, ChatTranscript, customer, sent, failPending, retryFailed, expectReply, turnStarted, component, turnEnded, closed, clear, opened, restore — each with its parameters, what it returns, the errors it raises and an example.

ChatTranscript.agent

Call it with each piece of the agent's words.

bool agent(String words, DateTime at)

Words of the agent's turn.

ParameterTypeDefaultMeaning
wordsStringrequiredThe words that arrived.
atDateTimerequiredWhen.

Returns — bool. Example value: true.

Errors — none.

final transcript = ChatTranscript();
final volunteered = transcript.agent('Your order ships today.', DateTime.now());

ChatTranscript.ChatTranscript

Use it to make a transcript that keeps at most max turns.

ChatTranscript({this.max = 200})
ParameterTypeDefaultMeaning
maxint200The number of turns kept; when full, the oldest are dropped first.

Returns — a new ChatTranscript.

Errors — none.

final result = ChatTranscript();

ChatTranscript.customer

Call it when the customer sends a message.

GraineChatTurn customer(String text, DateTime at, {bool pending = false})

The customer said text: whatever the agent was saying is over. With pending it is not on the wire yet — sent marks it when it goes.

ParameterTypeDefaultMeaning
textStringrequiredWhat the customer typed.
atDateTimerequiredWhen.
pendingboolfalseNot sent yet (the chat is not connected); sent marks it when it goes.

Returns — GraineChatTurn — its fields: GraineChatTurn.

Errors — none.

final transcript = ChatTranscript();
final turn = transcript.customer('Where is my order?', DateTime.now(), pending: true);   // not on the wire yet

ChatTranscript.sent

Call it when a waiting message went out.

void sent(String id)

Marks the queued turn id as sent, so the agent now owes it an answer.

ParameterTypeDefaultMeaning
idStringrequiredThe waiting message's id.

Returns — nothing; it returns when done.

Errors — none.

final transcript = ChatTranscript();
final turn = transcript.customer('Where is my order?', DateTime.now(), pending: true);
transcript.sent(turn.id);   // it went out

ChatTranscript.failPending

Call it when the chat gave up connecting.

void failPending()

Called when the chat stops trying to connect; every turn waiting to go is marked as not sent, for now.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final transcript = ChatTranscript();
transcript.customer('Where is my order?', DateTime.now(), pending: true);
transcript.failPending();   // every reconnect was spent

ChatTranscript.retryFailed

Call it when the chat tries again.

void retryFailed()

Starts another attempt, putting every unsent turn back on its way.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final transcript = ChatTranscript();
transcript.retryFailed();

ChatTranscript.expectReply

Call it when the customer answered a component.

void expectReply()

Records that the user answered a component, with the answer's sentence sent as their turn; the agent now owes a reply and is no longer in the middle of a turn.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final transcript = ChatTranscript();
transcript.expectReply();

ChatTranscript.turnStarted

Call it when the agent's turn begins.

void turnStarted()

Handles the agent's start marker: a turn has begun and its words are coming.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final transcript = ChatTranscript();
transcript.turnStarted();

ChatTranscript.component

Call it when the agent drew a component.

void component(GraineComponent c, DateTime at)

Adds a component the agent drew. It joins the agent's turn in progress, or the last turn if that one is the agent's; otherwise it becomes a separate turn.

ParameterTypeDefaultMeaning
cGraineComponentrequiredThe component the agent drew.
atDateTimerequiredWhen.

Returns — nothing; it returns when done.

Errors — none.

final transcript = ChatTranscript();
final component = GraineWidget.adopt({'widget_id': 'w_1', 'kind': 'choices', 'data': {'options': ['Morning', 'Evening']}}, 'https://www.graine.ai');
if (component != null) transcript.component(component, DateTime.now());

ChatTranscript.turnEnded

Call it when the agent's turn is over.

void turnEnded()

Marks the agent's turn as finished; this happens on its end marker, on an explicit end, or when it is interrupted.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final transcript = ChatTranscript();
transcript.turnEnded();

ChatTranscript.closed

Call it when the connection closed.

void closed()

Handles the socket closing: nothing more will arrive and no answer is owed.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final transcript = ChatTranscript();
transcript.closed();

ChatTranscript.clear

Call it to forget everything.

void clear()

Clears everything, for a different user or a deliberately started new conversation.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final transcript = ChatTranscript();
transcript.clear();

ChatTranscript.opened

Call it when the connection opened.

void opened()

Called when the connection opens, before any waiting turns are sent; whatever is settled at this point is what the next history will replace.

Parameters — none.

Returns — nothing; it returns when done.

Errors — none.

final transcript = ChatTranscript();
transcript.opened();

ChatTranscript.restore

Call it with a restored conversation.

bool restore(List<GraineChatTurn> restored)

Restores an earlier conversation: first its turns, then, in order, every turn the history does not include yet — those still sending or failed, and those said since the connection opened.

ParameterTypeDefaultMeaning
restoredList<GraineChatTurn>requiredThe restored messages, oldest first.

Returns — bool. Example value: true.

Errors — none.

final transcript = ChatTranscript();
final restored = transcript.restore([
  GraineChatTurn(id: 'm1', role: GraineChatRole.user, text: 'Hi', at: DateTime.utc(2026, 10, 14, 12, 30), restored: true),
]);

ContextFrame

Use it to build the screen context the agent reads, exactly as the SDK does.

abstract final class ContextFrame

What to expect — Built by the SDK every time the screen changes and masked before it leaves the device; public so a custom surface can send exactly what the agent reads.

Its functions: build, availableActions — each with its parameters, what it returns, the errors it raises and an example.

ContextFrame.build

Use it to build the context for a screen.

static Map<String, Object?>? build({ required GraineScreen? screen, required int screenSinceMs, required int nowMs, Map<String, String> identity = const <String, String>{}, String? appVersion, List<Map<String, Object?>> recentEvents = const <Map<String, Object?>>[], List<String>? registeredActions})

Returns the frame {context, availableActions?}, or null if there is nothing worth sending.

ParameterTypeDefaultMeaning
screenGraineScreen?requiredThe screen now (setScreen); null for none.
screenSinceMsintrequiredWhen the customer reached it, in ms since the epoch.
nowMsintrequiredThe time now, in ms since the epoch.
identityMap<String, String>const <String, String>{}The identity settings.
appVersionString?optionalYour app's version; it lands on the call record.
recentEventsList<Map<String, Object?>>const <Map<String, Object?>>[]What the customer did lately (track, taps), newest last.
registeredActionsList<String>?optionalThe actions this build registered; null sends the screen's list as stated.

Returns — Map<String, Object?>?. Example value: null.

Errors — none.

final now = DateTime.now().millisecondsSinceEpoch;
final frame = ContextFrame.build(screen: const GraineScreen('checkout_address'), screenSinceMs: now - 12000, nowMs: now, registeredActions: ['set_language']);

ContextFrame.availableActions

Use it to work out which actions a screen offers the agent.

static List<String>? availableActions(List<String>? stated, List<String>? registered)

The actions a frame offers — see build.

ParameterTypeDefaultMeaning
statedList<String>?requiredThe screen's own list (GraineScreen.availableActions); null for none.
registeredList<String>?requiredThe actions this build registered; null for no narrowing.

Returns — List<String>?. Example value: [].

Errors — none.

final actions = ContextFrame.availableActions(['open_screen', 'set_language'], ['set_language']);   // [set_language]

ActionRegistry

Use it to register and run actions exactly as the SDK does — always answering the agent.

class ActionRegistry

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
final Duration deadlineDurationThe time a handler has to finish; after it, the agent hears the action was not done.
List<String> get namesList<String>Registered names, in registration order.

Its functions: ActionRegistry, register, has, execute, describeError — each with its parameters, what it returns, the errors it raises and an example.

ActionRegistry.ActionRegistry

Use it to make a registry with its deadline.

ActionRegistry({this.deadline = const Duration(milliseconds: Protocol.actionDeadlineMs)})
ParameterTypeDefaultMeaning
deadlineDurationconst Duration(milliseconds: Protocol.actionDeadlineMs)How long a handler may take before the agent is told it was not done; 6 seconds by default.

Returns — a new ActionRegistry.

Errors — none.

final result = ActionRegistry();

ActionRegistry.register

Use it to register an action.

void Function() register(String name, GraineActionHandler handler)

Registers handler under name; the returned function unregisters it (only if it is still the one registered).

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, as the dashboard declares it.
handlerGraineActionHandlerrequiredWhat runs it.

Returns — void Function().

Errors — none.

final registry = ActionRegistry();
final unregister = registry.register('open_plan', (args) => const ActionResult.ok('The plan is on screen.'));

ActionRegistry.has

Use it to ask whether an action is registered.

bool has(String name)
ParameterTypeDefaultMeaning
nameStringrequiredThe action's name.

Returns — bool. Example value: true.

Errors — none.

final registry = ActionRegistry();
registry.register('open_plan', (args) => const ActionResult.ok('The plan is on screen.'));
final known = registry.has('open_plan');   // true

ActionRegistry.execute

Use it to run one action and get its answer.

Future<ActionResult> execute(String name, Map<String, Object?> args, { void Function(String name)? onRequested, void Function(String name, ActionResult result)? onCompleted})

Run one action.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name.
argsMap<String, Object?>requiredIts arguments.
onRequestedvoid Function(String name)?optionalCalled first, with the name.
onCompletedvoid Function(String name, ActionResult result)?optionalCalled once, with the answer the agent is given.

Returns — Future<ActionResult> — its fields: ActionResult.

Errors — none.

final registry = ActionRegistry();
registry.register('set_language', (args) => ActionResult.ok('The app is now in ${args['language']}.'));
final result = await registry.execute('set_language', {'language': 'fr'});

ActionRegistry.describeError

Use it to turn an exception into the sentence the agent is given.

static String describeError(Object e)

Turns an exception into the sentence passed to the agent.

ParameterTypeDefaultMeaning
eObjectrequiredWhat the handler threw.

Returns — String. Example value: 'The coupon has expired.'.

Errors — none.

final sentence = ActionRegistry.describeError(Exception('The coupon has expired.'));

deepEquals

Use it to compare two JSON-like values by what they hold.

bool deepEquals(Object? a, Object? b)

Compares JSON-like values (maps, lists and scalars) by structure.

ParameterTypeDefaultMeaning
aObject?requiredOne value.
bObject?requiredThe other.

Returns — bool. Example value: true.

Errors — none.

final same = deepEquals({'plan': 'Pro', 'items': ['mug']}, {'plan': 'Pro', 'items': ['mug']});   // true

deepHash

Use it to hash a JSON-like value consistently with deepEquals.

int deepHash(Object? v)

A hash consistent with deepEquals.

ParameterTypeDefaultMeaning
vObject?requiredA JSON-like value.

Returns — int. Example value: 3.

Errors — none.

final hash = deepHash({'plan': 'Pro', 'items': ['mug']});

VariableStore

Use it to merge prompt variables from every place they come from, exactly as the SDK does.

class VariableStore

What to expect — Public so a custom UI can reuse it; an integration does not need it.

Its functions: VariableStore, setDashboardDefaults, setRuntime, setIdentity, identity, merged, flatten, flattenOne — each with its parameters, what it returns, the errors it raises and an example.

VariableStore.VariableStore

Use it to make a store with your build's variables.

VariableStore([Map<String, Object?> buildTime = const <String, Object?>{}])
ParameterTypeDefaultMeaning
buildTimeMap<String, Object?>const <String, Object?>{}Your build's variables (GraineConfig.variables).

Returns — a new VariableStore.

Errors — none.

final result = VariableStore();

VariableStore.setDashboardDefaults

Call it with the dashboard's defaults.

void setDashboardDefaults(Map<String, Object?> values)
ParameterTypeDefaultMeaning
valuesMap<String, Object?>requiredThe dashboard's defaults (session.variables).

Returns — nothing; it returns when done.

Errors — none.

final store = VariableStore();
store.setDashboardDefaults(GraineInApp.instance.session?.variables ?? const <String, Object?>{});

VariableStore.setRuntime

Call it with the app's runtime variables.

void setRuntime(Map<String, Object?>? values)

Merged over what is there.

ParameterTypeDefaultMeaning
valuesMap<String, Object?>?requiredThe variables to merge in; a null value removes its variable, null clears them all.

Returns — nothing; it returns when done.

Errors — none.

final store = VariableStore();
store.setRuntime({'plan': 'Pro', 'coupon': null});   // sets plan, removes coupon

VariableStore.setIdentity

Call it with the customer's traits.

void setIdentity(Map<String, Object?>? traits)

Who is signed in.

ParameterTypeDefaultMeaning
traitsMap<String, Object?>?requiredThe customer's traits; null on sign-out.

Returns — nothing; it returns when done.

Errors — none.

final store = VariableStore();
store.setIdentity({'user_id': 'u_8812', 'plan': 'gold'});

VariableStore.identity

Use it to get the customer's traits as variables.

Map<String, String> identity()

Who is signed in, masked.

Parameters — none.

Returns — Map<String, String>.

Errors — none.

final store = VariableStore()..setIdentity({'user_id': 'u_8812', 'plan': 'gold'});
final traits = store.identity();

VariableStore.merged

Use it to get every variable, merged.

Map<String, String> merged()

Every variable, in precedence order.

Parameters — none.

Returns — Map<String, String>.

Errors — none.

final store = VariableStore({'app_version': '4.2.0'})..setRuntime({'plan': 'Pro'});
final variables = store.merged();   // {app_version: 4.2.0, plan: Pro}

VariableStore.flatten

Use it to turn values into the text variables carry.

static Map<String, String> flatten(Map<String, Object?> values)
ParameterTypeDefaultMeaning
valuesMap<String, Object?>requiredThe values to turn into text.

Returns — Map<String, String>. Example value: {'items': '3', 'total': '42'}.

Errors — none.

final text = VariableStore.flatten({'items': 3, 'total': 42.0, 'cart': ['mug']});

VariableStore.flattenOne

Use it to turn one value into the text a variable carries.

static String? flattenOne(Object? v)

Converts a single value into the string a prompt will substitute, or returns null to leave it out.

ParameterTypeDefaultMeaning
vObject?requiredOne value.

Returns — String?. Example value: '42'.

Errors — none.

final text = VariableStore.flattenOne(42.0);

ScheduledCalls

Use it to read and write booked calls exactly as the SDK does.

abstract final class ScheduledCalls

What to expect — Public so a custom UI can reuse it; an integration does not need it.

PropertyTypeMeaning
static const int maxReasonLengthintThe longest reason a booking carries.

Its functions: atWire, parseTime, oneFrom, listFrom, statusAfterCancel — each with its parameters, what it returns, the errors it raises and an example.

ScheduledCalls.atWire

Use it to write a time as a booking sends it.

static String atWire(DateTime at, {Duration? offset})

A time as a booking sends it: ISO-8601 with the device's own offset, to the second — 2026-10-14T18:00:00+05:30 (Z where the offset is zero), as every Graine SDK writes it.

ParameterTypeDefaultMeaning
atDateTimerequiredThe time to write.
offsetDuration?optionalThe offset to write it with; the device's when left out.

Returns — String. Example value: '2026-10-14T18:00:00+05:30' on a device in India.

Errors — none.

final at = ScheduledCalls.atWire(DateTime(2026, 10, 14, 18));

ScheduledCalls.parseTime

Use it to read a time Graine sent.

static DateTime? parseTime(Object? v)

Parses an ISO-8601 string (or an epoch in milliseconds) into UTC, reading a string that has no zone as UTC; returns null for anything else.

ParameterTypeDefaultMeaning
vObject?requiredAn ISO-8601 time with its zone, or milliseconds since the epoch.

Returns — DateTime?. Example value: null.

Errors — none.

final at = ScheduledCalls.parseTime('2026-10-16T12:30:00Z');

ScheduledCalls.oneFrom

Use it to read the booked call a booking answered with.

static GraineScheduledCall? oneFrom(Object? body)

The booking a booking request answered with: {call: {...}}, or the call itself.

ParameterTypeDefaultMeaning
bodyObject?requiredA booking's answer, decoded.

Returns — GraineScheduledCall? — its fields: GraineScheduledCall. Example value: GraineScheduledCall(id: '6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44', at: 2026-10-15 12:30:00.000Z, status: GraineCallStatus.scheduled, source: GraineCallSource.app, reason: 'follow_up').

Errors — none.

final call = ScheduledCalls.oneFrom({'call': {'id': 'sc_1', 'at': '2026-10-16T12:30:00Z', 'status': 'scheduled'}});

ScheduledCalls.listFrom

Use it to read the booked calls a list answered with.

static List<GraineScheduledCall> listFrom(Object? body)

The bookings a list request answered with — {calls: [...]} or a bare list — in Graine's order (upcoming first). Entries that are not calls are skipped.

ParameterTypeDefaultMeaning
bodyObject?requiredA list's answer, decoded.

Returns — List<GraineScheduledCall> — its fields: GraineScheduledCall. Example value: [GraineScheduledCall(id: '6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44', at: 2026-10-15 12:30:00.000Z, status: GraineCallStatus.scheduled, source: GraineCallSource.app, reason: 'follow_up')].

Errors — none.

final calls = ScheduledCalls.listFrom({'calls': [{'id': 'sc_1', 'at': '2026-10-16T12:30:00Z', 'status': 'scheduled'}]});

ScheduledCalls.statusAfterCancel

Use it to read a booked call's status after a cancel.

static GraineCallStatus statusAfterCancel(Object? body)

A cancel's answer, {ok, status?}: the call's status afterwards — GraineCallStatus.cancelled when Graine does not say.

ParameterTypeDefaultMeaning
bodyObject?requiredA cancel's answer, decoded.

Returns — GraineCallStatus — its fields: GraineCallStatus. Example value: GraineCallStatus.scheduled.

Errors — none.

final status = ScheduledCalls.statusAfterCancel({'ok': true, 'status': 'cancelled'});   // GraineCallStatus.cancelled

Sha256

Use it to hash bytes with SHA-256 on the device, with no dependency.

abstract final class Sha256

What to expect — Public so a custom UI can reuse it; an integration does not need it.

Its functions: hash, ofText, hex — each with its parameters, what it returns, the errors it raises and an example.

Sha256.hash

Use it to hash bytes.

static List<int> hash(List<int> message)

The 32-byte SHA-256 digest of message.

ParameterTypeDefaultMeaning
messageList<int>requiredThe bytes to hash.

Returns — List<int>. Example value: [].

Errors — none.

final digest = Sha256.hash('abc'.codeUnits);

Sha256.ofText

Use it to hash text.

static List<int> ofText(String text)

The SHA-256 digest of text's UTF-8 bytes.

ParameterTypeDefaultMeaning
textStringrequiredThe text, hashed as UTF-8.

Returns — List<int>. Example value: [].

Errors — none.

final result = Sha256.ofText('Where is my order?');

Sha256.hex

Use it to write bytes as hex.

static String hex(List<int> bytes)

bytes as lower-case hex.

ParameterTypeDefaultMeaning
bytesList<int>requiredThe bytes to write.

Returns — String. Example value: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'.

Errors — none.

final hex = Sha256.hex(Sha256.ofText('abc'));

What's new in 0.9.0

  • WebRTC calls. Turn them on in the app's build — ENV['GRAINE_WEBRTC'] = '1' in ios/Podfile, graineWebRtc=true in android/gradle.properties — and native calls, the ones the customer places and the ones Graine rings, carry their audio over WebRTC: steadier on a weak network, with the same ring, call screen and outcomes. GraineCallsOptions(callTransport:) chooses (GraineCallTransport); GraineCallSnapshot.transport and transportFallbackReason say which one carried a call (WebRTC calls).
  • chat.reconnecting: true while a dropped chat — or a first try to open it that failed — is being made again, until it is back, gives up (failed) or is closed. Say "Reconnecting…" then, not "Offline" (GraineChatSession).
  • The captions switch: voice.captionsOn (on by default), setCaptionsOn(on) and clearCaptions() (GraineCaptionsSwitch). GraineAgentBar shows its caption only while the switch is on; what was said stays in captions either way.
  • Variables over a conversation's kilobyte are trimmed to fit, not all dropped: who_called and call_reason ride first (Protocol.ticketFirstKeys) and the biggest of the rest are left off, one at a time — the same rule on every SDK.
  • 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), "Pick a time within 14 days".
  • The full list of changes is in the Changelog.

What's new in 0.8.0

  • Booking a call from your app, drawn by the SDK. GraineBooking.show(context, app:, reason:) opens the booking sheet in the agent's design — a sheet on a phone, a centred panel on a tablet — and completes with the booked GraineScheduledCall, or null when the customer closed it. GraineBookingSheet is the same sheet as a widget. See Booking with the SDK's sheet.
  • bookingAvailable on each agent, with bookingBlocker, bookingListenable and deviceTimezone: show your own "Call me" button only when Graine rings this phone, the agent lets customers book, and the customer is identified.
  • The chat window offers Book a call under the booked calls in its Upcoming calls card, or on its own in the card's place when none are booked — never when the design hides the booked-calls card (ChatPieces.book). Booking holds the rules and the words, and ChatTime.day writes a day the way the sheet does.
  • Every piece of the chat is set in the dashboard (Embed & Widgets → Appearance → Chat window): the booked-calls card and its title, earlier messages and the New conversation button (ThemeChat.showUpcomingCalls, upcomingTitle, showHistory, newConversation; ChatPieces for a chat of your own). The bar's sheet, panel and full screen offer New conversation as the compact window does.
  • Several agents in one app: GraineInApp.init(config, name:), GraineInApp.named, all, defaultName and each agent's name; the views take app:; native calls take startCall(agent:) and say which agent each call is with.
  • The native call engine's 0.8.0 fixes: a call starts at once on the configuration already kept on the device; on iOS one 20-second deadline before the conversation opens ("The call could not connect within 20 seconds.") and "The call's audio could not start." when the call's audio never arrives; on Android a dead connection ends the call after 50 seconds with nothing arriving, a slow network no longer freezes a call, and hanging up lets go of the microphone at once. See Calls the user places.
  • The chat's variables ride its ticket and are no longer added to the ticketed connection's address, which never read them; the direct connection still carries them.
  • A multi-select answers with its button's label, then the names — "Taken: Morning and Evening".
  • A full-screen GraineChat closing while a GraineAgentBar is on screen no longer trips Flutter's locked-tree assertion.

Every change, on every platform: Changelog.

Every symbol, A to Z

Every public symbol of the Flutter SDK — 1733 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 initialisationWebRTC callsInstall and the first answerGraineInApp.initGraineInAppGraineInApp.loadSessionGraineConfigGraineConfig.GraineConfigGraineTimingGraineTiming.GraineTimingGraineEngineGraineEngine.GraineEngineSessionConfigSessionConfig.SessionConfigSessionConfig.fromJsonSessionConfig.errorForGraineConnectivityIdentity, memory and conversations that continueGraineInApp.identifyGraineInApp.installIdGraineInApp.setMemoryGraineInApp.memoryEnabledGraineInApp.memoriesGraineInApp.forgetMemoryGraineChatSession.newConversationGraineMemoryGraineMemory.GraineMemoryGraineMemory.fromJsonGraineMemory.listFromGraineMemory.toJsonIdentitySettingIdentitySetting.IdentitySettingIdentitySetting.fromJsonMemorySettingMemorySetting.MemorySettingMemorySetting.fromJsonIdentityIdentity.normaliseFieldIdentity.valueOfIdentity.pickIdentity.normaliseInstallIdIdentity.normaliseTimezoneIdentity.uuidV4Identity.serverLowercaseIdentity.serverTrimIdentity.normaliseValueIdentity.memoryKeyIdentity.withoutIdentityIdentity.requestFieldsChatGraineChatGraineChat.GraineChatGraineChat.openGraineChatSessionGraineChatSession.answerWidgetGraineChatSession.clearGraineChatStateGraineChatState.sendGraineChatConnectionGraineChatConnection.openGraineChatConnection.closeGraineChatTypingGraineChatTurnGraineChatTurn.GraineChatTurnGraineChatTurn.copyWithGraineChatRoleGraineChatDeliveryGraineChatChannelGraineChatSizeGraineChatSize.fromWireChatRowsChatRows.buildChatRows.dayLabelChatRows.callLabelChatRows.callSpokenChatRows.offersNewConversationChatRows.offersStartersChatRowChatRow.ChatRowChatDayRowChatDayRow.ChatDayRowChatVoiceRowChatVoiceRow.ChatVoiceRowChatTurnRowChatTurnRow.ChatTurnRowChatDayChatPiecesChatPieces.ofChatPieces.ofChatChatTimeChatTime.clockChatTime.bookedChatTime.dayThe voice barGraineVoiceGraineVoice.attachGraineVoice.detachPageGraineVoice.detachGraineVoice.viewOpenedGraineVoice.viewClosedGraineVoice.answerWidgetGraineAgentBarGraineAgentBar.GraineAgentBarAgentSurfaceSurfacePolicySurfacePolicy.SurfacePolicySurfacesSurfaces.resolveGraineLauncherGraineLauncher.GraineLauncherGraineLoaderViewGraineLoaderView.GraineLoaderViewLoadersLoaders.normaliseLoaders.resolveLoaders.resolveStyleLoaderLookLoaderLook.LoaderLookLoaderStyleLoaderStyle.LoaderStyleLoaderStyle.toMapLoaderTextLoaderText.LoaderTextLoaderText.forMomentLoaderText.toMapGraineTranscriptViewGraineTranscriptView.GraineTranscriptViewGraineTranscriptView.bubbleKeyCaptionCaption.CaptionCaption.settledCaptionsCaptions.foldGraineCaptionsSwitchGraineCaptionsSwitch.setCaptionsOnGraineCaptionsSwitch.clearCaptionsRoleTransportTransport.TransportTransport.fromConnectionQualityConnectionQuality.classifyCallPhaseConnectStageConnectStage.fromConnectProgressGraineConnectProgressGraineCallFaultBarStatusBarStatus.BarStatusagentBarStatuscallPhase (function)connectingLookvoiceBarStatusVoiceMeterVoiceMeter.legMsVoiceMeter.stillVoiceMeter.scalePlacementPlacement.clampOffsetPlacement.offsetDuringDragPlacement.isTapPlacementBoundsPlacementBounds.PlacementBoundsKeyboardLiftKeyboardLift.liftForSeveral agents in one appGraineInApp.namedGraineInApp.allGraineInApp.instanceGraineInApp.maybeInstanceGraineInApp.defaultNameGraineInApp.nameScreen context, actions and componentsGraineInApp.setVariablesGraineInApp.currentVariablesGraineInApp.setScreenGraineInApp.updateFieldGraineInApp.removeFieldGraineInApp.clearScreenGraineInApp.trackGraineInApp.tapGraineInApp.captureTapGraineInApp.noteInteractionGraineInApp.registerActionGraineInApp.checkActionsGraineInApp.executeActionGraineInApp.configureFrictionGraineInApp.clearPendingWidgetGraineVoice.reportEventGraineScreenGraineScreen.GraineScreenGraineScreen.copyWithGraineScreen.withFieldGraineScreen.withoutFieldGraineFieldGraineField.GraineFieldFieldStatusFieldStatus.FieldStatusFieldStatus.fromWireGraineJourneyGraineJourney.GraineJourneyGraineActionHandlerActionResultActionResult.ActionResultActionResult.okActionResult.refusedActionResult.errorActionStatusActionStatus.ActionStatusMaskingMasking.maskMasking.addRuleMasking.clearCustomRulesMasking.deepMasking.expandReplacementFrictionConfigFrictionConfig.FrictionConfigFrictionConfig.fromJsonFrictionScreenRuleFrictionScreenRule.FrictionScreenRuleFrictionSignalFrictionSignal.FrictionSignalFrictionRulesFrictionRules.FrictionRulesFrictionRules.forScreenFrictionForScreenFrictionForScreen.FrictionForScreenFrictionFriction.resolveFrictionDetectorFrictionDetector.FrictionDetectorFrictionDetector.configureFrictionDetector.onScreenFrictionDetector.onInteractionFrictionDetector.onCallFrictionDetector.idleDueInFrictionDetector.onIdleFrictionDetector.outstandingFrictionDetector.fillGraineComponentsGraineComponents.GraineComponentsGraineComponents.presentGraineComponents.presentPendingGraineComponents.answerGraineComponents.dismissGraineComponents.reopenGraineComponents.resetGraineWidgetViewGraineWidgetView.GraineWidgetViewGraineWidgetThemeGraineWidgetTheme.GraineWidgetThemeGraineComponentGraineWidgetGraineWidget.GraineWidgetGraineWidget.adoptWidgetContentWidgetContent.WidgetContentWidgetContent.surfaceWidgetContent.manyWidgetContent.pickWidgetContent.pickManyWidgetContent.pickChipWidgetContent.pickSlotWidgetContent.toggleWidgetContent.manyButtonLabelWidgetContent.screenFooterWidgetContent.closeLabelWidgetContent.answeredLineWidgetAnswerWidgetAnswer.WidgetAnswerWidgetRowWidgetRow.WidgetRowWidgetChipWidgetChip.WidgetChipWidgetDayWidgetDay.WidgetDayWidgetPendingWidgetPending.WidgetPendingWidgetAnsweredWidgetAnswered.WidgetAnsweredWidgetSubmissionWidgetSubmission.WidgetSubmissionWidgetDismissalWidgetDismissal.WidgetDismissalWidgetDismissal.eventDataWidgetFlowWidgetFlow.WidgetFlowWidgetFlow.seedWidgetFlow.onPendingWidgetFlow.onWidgetWidgetFlow.onAgentStartedWidgetFlow.submitWidgetFlow.dismissWidgetFlow.reopenWidgetFlow.resetWidgetFlow.tickWidgetFlow.nextDeadlineWidgetStyleWidgetStyle.WidgetStyleWidgetStyle.toMapWidgetLookWidgetLook.WidgetLookWidgetLook.toMapPlatformGrammarPlatformGrammar.PlatformGrammarWidgetSelectActionWidgetSelectAction.WidgetSelectActionWidgetSelectAction.toMapSelectCallSelectCall.SelectCallWidgetsWidgets.surfaceWidgets.normaliseStyleWidgets.resolveLookWidgets.resolvePlatformWidgets.manySummaryWidgets.normaliseSelectActionWidgets.fillSelectTemplateWidgets.fillSelectArgsWidgets.selectCallWidgets.resolveImagesCalls the user placesGraineCallsGraineCalls.configureGraineCallsOptionsGraineCallsOptions.GraineCallsOptionsGraineCallsOptions.toMapGraineCallScreenGraineCallTransportGraineCallTransport.GraineCallTransportGraineCallTransport.fromWireGraineCalls.attachGraineCalls.startCallGraineCalls.endGraineCalls.setMutedGraineCalls.setSpeakerGraineCalls.resetForTestingGraineCallSnapshotGraineCallSnapshot.GraineCallSnapshotGraineCallSnapshot.fromMapGraineNativeCallStatusGraineNativeCallStatus.GraineNativeCallStatusGraineNativeCallStatus.fromWireGraineCallDirectionGraineCallCaptionGraineCallCaption.GraineCallCaptionGraineCallOutcomeGraineCallOutcome.GraineCallOutcomeGraineCallOutcome.fromMapGraineCallOutcomeKindGraineCallStateGraineCallState.startGraineCallState.endGraineCallState.setMutedGraineCallState.retryCalls Graine ringsGraineCalls.registerPushTokenGraineCalls.enableVoIPPushesGraineCalls.handlePushGraineCalls.answerGraineCalls.declineGraineIncomingCallGraineIncomingCall.GraineIncomingCallCallsSettingCallsSetting.CallsSettingCallsSetting.fromJsonPermissions and the setup checkGraineCalls.callPermissionsGraineCalls.requestCallPermissionsGraineCalls.openCallSettingsGraineCalls.callSetupProblemsGraineCallPermissionGraineCallPermission.GraineCallPermissionGraineCallPermissionStatusGraineCallPermissionStatus.GraineCallPermissionStatusGraineCallPermissionStatus.fromWireGraineCallPermissionsGraineCallPermissions.GraineCallPermissionsGraineCallPermissions.fromMapGraineCallSetupProblemGraineCallSetupProblem.GraineCallSetupProblemBooking with the SDK's sheetGraineInApp.bookingAvailableGraineInApp.bookingBlockerGraineInApp.bookingListenableGraineInApp.deviceTimezoneGraineBookingGraineBooking.showGraineBookingSheetGraineBookingSheet.GraineBookingSheetGraineBookingSheet.dayKeyChatPieces.bookBookingBooking.subtitleBooking.callNameBooking.blockerBooking.availableBooking.boundsBooking.clampBooking.dayLabelBooking.daysBooking.onDayBooking.zoneLineBooking.doneLineBooking.errorLineBookingBoundsBookingBounds.BookingBoundsBookingDayBookingDay.BookingDayThe scheduled-calls listGraineInApp.schedulingRulesGraineInApp.scheduleCallGraineInApp.scheduledCallsGraineInApp.cancelScheduledCallGraineChatSession.refreshUpcomingGraineChatSession.cancelUpcomingGraineChatSession.cancelStateOfGraineUpcomingCancelGraineScheduledCallGraineScheduledCall.GraineScheduledCallGraineScheduledCall.fromJsonGraineScheduledCall.toJsonGraineCallStatusGraineCallStatus.GraineCallStatusGraineCallStatus.fromWireGraineCallSourceGraineCallSource.GraineCallSourceGraineCallSource.fromWireGraineSchedulingRulesGraineSchedulingRules.GraineSchedulingRulesGraineSchedulingRules.ofGraineSchedulingRules.earliestGraineSchedulingRules.latestSchedulingSettingSchedulingSetting.SchedulingSettingSchedulingSetting.fromJsonDesignGraineInApp.loadDesignFontsResolvedDesignResolvedDesign.ResolvedDesignResolvedDesign.pickResolvedDesign.fromJsonResolvedDesign.toJsonThemeTokensThemeTokens.ThemeTokensThemeTokens.fromJsonThemeTokens.toJsonThemeColorsThemeColors.ThemeColorsThemeColors.fromJsonThemeColors.toJsonThemeFontThemeFont.ThemeFontThemeFont.fromJsonThemeFont.toJsonThemeFontScaleThemeFontScale.ThemeFontScaleThemeFontScale.fromJsonThemeFontScale.toJsonThemeFontWeightsThemeFontWeights.ThemeFontWeightsThemeFontWeights.fromJsonThemeFontWeights.toJsonThemeFontPairThemeFontPair.ThemeFontPairThemeFontPair.fromJsonThemeFontPair.toJsonThemeShapeThemeShape.ThemeShapeThemeShape.fromJsonThemeShape.toJsonThemeRadiusThemeRadius.ThemeRadiusThemeRadius.fromJsonThemeRadius.toJsonThemeSpacingThemeSpacing.ThemeSpacingThemeSpacing.fromJsonThemeSpacing.toJsonThemeElevationThemeElevation.ThemeElevationThemeElevation.fromJsonThemeElevation.toJsonThemeShadowThemeShadow.ThemeShadowThemeShadow.fromJsonThemeShadow.toJsonThemeOffsetThemeOffset.ThemeOffsetThemeOffset.fromJsonThemeOffset.toJsonThemeMotionThemeMotion.ThemeMotionThemeMotion.fromJsonThemeMotion.toJsonThemeIconsThemeIcons.ThemeIconsThemeIcons.fromJsonThemeIcons.toJsonLauncherGlyphLauncherGlyph.LauncherGlyphLauncherGlyph.fromJsonLauncherGlyph.toJsonThemeGlassThemeGlass.ThemeGlassThemeGlass.fromJsonThemeGlass.toJsonThemeChatThemeChat.ThemeChatThemeChat.fallbackThemeChat.fromJsonThemeChat.toJsonThemeCallThemeCall.ThemeCallThemeCall.fallbackThemeCall.fromJsonThemeCall.toJsonThemeCallStatusTextThemeCallStatusText.ThemeCallStatusTextThemeCallStatusText.fromJsonThemeCallStatusText.toJsonRingtoneRingtone.RingtoneRingtone.toJsonRingNoteRingNote.RingNoteRingNote.toJsonDesign (class)Design.normaliseHexDesign.mixHexDesign.fontFamilyDesign.launcherRadiusDesign.glyphUrlDesignFontsDesignFonts.keyDesignFonts.nearestWeightDesignFonts.canonicalFamilyDesignFonts.faceNameDesignFonts.urlDesignFonts.faceForDesignFonts.facesForDesignFonts.facesForDesignDesignFontFaceDesignFontFace.DesignFontFaceDesignFontFace.toJsonSdkIconsSdkIcons.treatmentSdkIconSdkIcon.SdkIconSdkIcon.toJsonIconTreatmentIconTreatment.IconTreatmentAppearanceAppearance.resolveAppearance.isDarkAppearance.paletteAppearance.luminanceAppearance.argbAppearance.tintAppearance.encodeURIComponentNativeAppearanceNativeAppearance.NativeAppearanceWidgetPaletteWidgetPalette.WidgetPaletteLogoPlateLogoPlate.fromLogoPlateLookLogoPlateLook.LogoPlateLookBrandLogoBrandLogo.boxBrandLogo.outlineWidthBrandLogo.plateLookWindowChromeWindowChrome.barHeightWindowChrome.lightSizeWindowChrome.lightsWidthWindowChrome.titleInsetWindowChrome.titleWindowChrome.innerRadiusChromeSurfaceGoing liveDebug loggingGraineVersionEvents and listenersGraineInApp.addListenerGraineInApp.removeListenerGraineListenerGraineListener.onSessionReadyGraineListener.onCallStateGraineListener.onCaptionGraineListener.onWidgetGraineListener.onWidgetPendingGraineListener.onEndedGraineListener.onMicDeniedGraineListener.onQualityGraineListener.onNetworkGraineListener.onReconnectingGraineListener.onTransportGraineListener.onConnectivityGraineListener.onActionRequestedGraineListener.onActionCompletedGraineListener.onFrictionGraineListener.onAgentVolunteeredGraineListener.onErrorErrorsGraineExceptionGraineException.GraineExceptionGraineErrorCodeGraineErrorCode.invalidKeyGraineErrorCode.originRefusedGraineErrorCode.rateLimitedGraineErrorCode.invalidRequestGraineErrorCode.identityRequiredGraineErrorCode.deviceLimitGraineErrorCode.schedulingOffGraineErrorCode.tooSoonGraineErrorCode.tooFarGraineErrorCode.outsideWindowGraineErrorCode.tooManyPendingGraineErrorCode.notFoundGraineErrorCode.unavailableGraineErrorCode.networkGraineErrorCode.unknownGraineErrorCode.fromWireEmbedRefusalEmbedRefusal.EmbedRefusalEmbedRefusal.ofError codes and their fixesRate limits and quotasLower-level building blocksGraineEnginePageGraineEnginePage.bindGraineEnginePage.loadGraineEnginePage.reloadGraineEnginePage.runGraineEnginePage.disposeGraineEngineSinkGraineEngineSink.pageStartedGraineEngineSink.progressGraineEngineSink.pageFinishedGraineEngineSink.mainFrameErrorGraineEngineSink.messageGraineEngineSink.microphoneRequestedWebViewEnginePageWebViewEnginePage.WebViewEnginePageProtocolProtocol.engineUrlProtocol.deliverProtocol.outProtocol.toJsonProtocol.parseProtocol.decodeJsonProtocol.contextTokenProtocol.ticketMetadataProtocol.trimTrailingSlashesProtocolOutProtocolInPageMessagePageMessage.PageMessagePageReadyPageReady.PageReadyPageCallPageCall.PageCallPageMutedPageMuted.PageMutedPageCaptionPageCaption.PageCaptionPageWidgetPageWidget.PageWidgetPageWidgetPendingPageWidgetPending.PageWidgetPendingPageAppActionPageAppAction.PageAppActionPageEndedPageEnded.PageEndedPageMicDeniedPageMicDenied.PageMicDeniedPageTransportPageTransport.PageTransportPageQualityPageQuality.PageQualityPageNetworkPageNetwork.PageNetworkPageReconnectingPageReconnecting.PageReconnectingPageProgressPageProgress.PageProgressLoadWatchdogLoadWatchdog.LoadWatchdogLoadWatchdog.startLoadWatchdog.progressLoadWatchdog.loadedLoadWatchdog.checkLoadWatchdog.nextCheckInMsLoadVerdictLoadVerdict.LoadVerdictRetryLadderRetryLadder.RetryLadderRetryLadder.nextRetryLadder.resetGraineChatSocketGraineChatSocket.sendGraineChatSocket.closeGraineChatConnectorChatWireChatWire.directUrlChatWire.withContextTokenChatWire.initChatWire.textChatWire.widgetResultChatWire.actionResultChatWire.appContextChatWire.appEventAgentChunkAgentChunk.AgentChunkAgentChunk.readChatFrameChatFrame.ChatFrameChatFrame.parseChatTextChatText.ChatTextChatWidgetChatWidget.ChatWidgetChatWidgetPendingChatWidgetPending.ChatWidgetPendingChatActionChatAction.ChatActionChatStreamChatStream.ChatStreamChatClearChatClear.ChatClearChatHistoryChatHistory.ChatHistoryChatHistoryReaderChatHistoryReader.readChatHistoryReader.timeChatTranscriptChatTranscript.agentChatTranscript.ChatTranscriptChatTranscript.customerChatTranscript.sentChatTranscript.failPendingChatTranscript.retryFailedChatTranscript.expectReplyChatTranscript.turnStartedChatTranscript.componentChatTranscript.turnEndedChatTranscript.closedChatTranscript.clearChatTranscript.openedChatTranscript.restoreContextFrameContextFrame.buildContextFrame.availableActionsActionRegistryActionRegistry.ActionRegistryActionRegistry.registerActionRegistry.hasActionRegistry.executeActionRegistry.describeErrordeepEqualsdeepHashVariableStoreVariableStore.VariableStoreVariableStore.setDashboardDefaultsVariableStore.setRuntimeVariableStore.setIdentityVariableStore.identityVariableStore.mergedVariableStore.flattenVariableStore.flattenOneScheduledCallsScheduledCalls.atWireScheduledCalls.parseTimeScheduledCalls.oneFromScheduledCalls.listFromScheduledCalls.statusAfterCancelSha256Sha256.hashSha256.ofTextSha256.hexWhat's new in 0.9.0What's new in 0.8.0Every symbol, A to Z