Screen context and actions
setScreen, fields, clearing, track, taps, reportEvent, noteInteraction, prompt variables, registering actions and checking them, components, pointing at an element, masking and friction rules — every platform's signature, parameters, returns, timing and threading.
Why and when to use these is on Screen context,
Actions and Components. The
structures — GraineScreen, GraineField, GraineActionResult, the component —
are on Types.
setScreen
What the customer can see. Call it whenever the screen or a field changes.
With navigationRef on the provider, the route name is reported for every screen
by itself.
Returns nothing. What happens: values are masked on the device; an unchanged screen is not sent again; the first change goes at once and later ones at most every 400 ms; the newest screen is sent again when the connection rejoins and when the app returns to the foreground. Each frame carries a sequence number, so a late one never replaces a newer one.
Fields and clearing
| Member | React Native | iOS | Android | Flutter | Kotlin Multiplatform |
|---|---|---|---|---|---|
| Update one field | useGraineField(name, field), client.updateField(field) | updateField(_:) | updateField(field) | updateField(f) | updateField(field) |
| Remove one | client.removeField(name) | removeField(_:) | removeField(name) | removeField(name) | removeField(name) |
| Withdraw a screen | client.clearScreen(screenId) | clearScreen(_:) | clearScreen(screenId = null) | clearScreen([screenId]) | clearScreen(screenId = null) |
A field merges into the current screen by name. Clearing with a screen id withdraws that screen only if it is still the current one, so a screen leaving during a transition cannot wipe the one that arrived.
track, taps and events
| Member | What it does | |
|---|---|---|
track | A product event the agent knows about without speaking — the last 10 are kept | coalesce: a burst of the same name counts once |
tap | A tap on a named control | At most two a second reach the agent |
reportEvent | Something worth speaking up about unprompted: a dead end the customer would want addressed | detail says what to offer, in plain words. In React Native, cooldown_seconds (default 20) and max_per_session (default 3) pace the agent; quiet: true records it without making the agent speak |
noteInteraction | The customer did something that is not a field edit — the idle clock restarts |
Automatic taps: with captureTaps (default on in iOS, Android, Flutter and
Kotlin Multiplatform; on in React Native with fab or the captureTaps prop),
taps on labelled controls are tracked by themselves. In React Native,
<GraineTouchCapture enabled={true}> does the same for the tree it wraps: a
touch that moves under 12 px and lifts within 400 ms is a tap.
Reading what will be sent (React Native)
| Member | Returns |
|---|---|
client.getScreen() | ScreenContext | null — the screen as the next frame will carry it |
client.getVariables() | Record<string, string> — every variable the agent will receive, merged in precedence order (the dashboard's defaults, the provider's variables, setVariables, then identify): the other SDKs' currentVariables(). A copy |
client.getRecentEvents() | Array<{ name, at, data? }> — the recent tracked events, oldest first (the last 10) |
client.getAvailableActions() | string[] — declared actions this build implements |
client.setAppVersion(version) | void — which build of your app the conversation happened in; it lands on the call record |
Prompt variables
| React Native | iOS | Android | Flutter | Kotlin Multiplatform | |
|---|---|---|---|---|---|
| Set | useGraineVariables() → (vars) => void, client.setVariables(vars) | setVariables(_:) | setVariables(values) | setVariables(values) | setVariables(values) |
| Read the merged set | client.getVariables() | currentVariables() -> [String: String] | currentVariables(): Map<String, String> | currentVariables() | currentVariables(): Map<String, String> (each agent's too) |
Merged in, later wins: the agent's defaults, the variables given at start-up, then
these, then the customer's traits. null under a key removes it. Values become
strings; everything is masked. The set reaches the next conversation; a
conversation carries up to 12 variables, about 1 KB — past that, the biggest are
left off first, who_called and call_reason last.
Actions
One handler per action declared on the agent.
Registered on mount, removed on unmount.
Returns a function that unregisters (React Native's hook unregisters on
unmount; the website has removeAction).
The handler gets the agent's arguments and answers a
GraineActionResult — ok,
refused or error, with a message for the agent. It runs on the main thread
(iOS, Android). It has 6 seconds; after that the agent is told it did not
finish. A throw is answered as error, an action this build has no handler for
as refused, and every request gets exactly one answer.
To run one of your actions exactly as the agent would — same deadline, same
answers — for a test or a screen of your own: client.executeAction(name, args)
(React Native, Promise<ActionResult>) or executeAction(name, args) (Flutter,
Future<ActionResult>). An unknown name answers refused; a throw or a timeout
answers error.
checkActions compares what the build registered with what the agent declares:
Components
| React Native | iOS | Android | Flutter | |
|---|---|---|---|---|
| The component on screen | useGraineWidget() → widget, loading ({ id, kind } while one is on its way), answered, awaitingReply, closed | GraineWidgetView; onWidget | GraineWidgetView; onWidget | GraineWidgetView, GraineComponents; onWidget |
| Answer it | submit(data, summary) | the view; answerComponent(widgetId:data:summary:) | the view; answerWidget(widgetId, data, summary) | chat.answerWidget(id, data, summary), the view |
| Close without answering | dismiss() | the view | the view | the view |
| Put it back | reopen() | reopenComponent() -> Bool | the view | the view |
An answer is a turn: summary reaches the agent as the customer's sentence and
data goes to your webhook — send both. A component closed without an answer is
reported to the agent quietly.
| React Native | Android | Flutter | |
|---|---|---|---|
| The component still waiting to be answered | client.pendingWidget | pendingWidget: JSONObject? | pendingWidget |
| Forget it | client.clearPendingWidget() | clearPendingWidget() | clearPendingWidget() |
| Answer from your own UI | client.submitWidget(widgetId, data, summary) | answerWidget(widgetId, data, summary) on the instance | chat.answerWidget(id, data, summary) |
| Draw one yourself — a preview, a test | client.presentWidget(frame), client.presentPendingWidget(hint) | GraineWidgetView.present(frame), presentPending(hint) | GraineComponents.present(frame), presentPending(hint) |
| What the view shows | useGraineWidget() | GraineWidgetView.answered, closedWidget, isLoading | GraineComponents |
The waiting component is kept because the view that draws it may not be on screen when it arrives; it is cleared when answered or dismissed, and when a new call starts.
React Native's timings, exported as constants: PENDING_WIDGET_MS (12,000 — how
long a "component coming" loader may stand with nothing arriving),
AWAITING_REPLY_MS (12,000 — how long the answered line waits for the agent's
reply) and MIN_LOADER_MS (450 — a loader, once shown, stays at least this long).
Pointing at an element
React Native: useGraineHighlight(name: string): { highlighted: boolean }. The
name is offered to the agent while the component is mounted; highlighted is true
for up to 6 seconds when the agent points at it. Declare highlight_element on the
agent; the SDK answers it.
Without the hook: client.registerHighlightable(name) offers a name and returns a
function that withdraws it (names are cleared with their screen);
client.getHighlightables() lists what can be pointed at now; and
client.highlight(name, ttlMs = 6000) points, emitting highlight — null stops.
Masking your own formats
| Platform | |
|---|---|
| React Native | addMaskRule(pattern: RegExp, token: string) from @graineai/inapp-react-native/mask |
| iOS | GraineMasking.addRule(_ pattern: NSRegularExpression, token: String = "[REDACTED]") |
| Android | Masking.addRule(pattern: Regex, token: String = "[REDACTED]") |
| Flutter | Masking.addRule(RegExp pattern, [String token = '[REDACTED]']) |
| Website | GraineAgent.addMaskRule(pattern, token) |
Once, at start-up. Rules apply to screens, variables and traits before they leave the device, after the built-in ones (PAN, Aadhaar, card and account numbers, phone numbers, email addresses).
React Native also exports the masking itself from the same module:
maskString(input: string): string — the built-in rules, then yours — and
maskDeep(value), which masks every string inside an object or array the same
way, turns a Date into its ISO string (as JSON sends it), and leaves numbers and
other values untouched.
Friction rules
Your app's layer over the dashboard's rules: friction on the React Native
provider (and client.configureFriction(override)), and GraineConfig.friction
on iOS, Android and Flutter. The rules and their defaults are on
Friction detection.

