React Native Widget
The Qafka component is a drop-in chat widget with full UI, theming, streaming, and voice support.
Basic Usage
import { Qafka } from '@qafka/react-native'
export default function ChatScreen() {
return <Qafka />
}The widget calls useSafeAreaInsets internally, so make sure a <SafeAreaProvider> exists somewhere above it in the tree (Expo Router, React Navigation, and most RN starter templates already include one).
Set up your project with the Qafka CLI (qafka init) so the widget can find the development key — see Quick Start.
Responses stream in real-time by default — tokens render in the message bubble as they arrive, with a typing indicator while the AI is thinking.
Display Modes
| Mode | Description |
|---|---|
fullscreen | Takes up the entire screen (default) |
inline | Embeds within a parent container |
floating | Floating chat bubble overlay |
<Qafka mode="fullscreen" />Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
projectId | string | runtime-config default | Pick a specific project when the CLI registered more than one |
apiUrl | string | Production URL | Backend API endpoint |
subProjectId | string | — | Sub-project identifier for multi-tenant setups |
mode | 'fullscreen' | 'inline' | 'floating' | 'fullscreen' | Display mode |
theme | 'light' | 'dark' | Theme | 'light' | Theme name or custom theme object |
themeOverride | ThemeOverride | — | Partial theme overrides |
placeholder | string | 'Type a message...' | Input placeholder text |
title | string | — | Header title |
showHeader | boolean | true | Show header bar |
maxMessageLength | number | 500 | Maximum message character count |
greetingMessage | string | — | Custom greeting (overrides project-configured greeting) |
showTimestamps | boolean | true | Show message timestamps |
showMetadata | boolean | false | Show message metadata (handled by, confidence, tokens) |
voiceEnabled | boolean | true | Allow voice chat (server must also allow it) |
isAuthenticated | boolean | — | Hides screens marked auth-only / public-only based on this flag |
context | Record<string, any> | — | Runtime context sent with messages and used for tool matching |
contextDescription | string | — | Human description of context keys for the AI |
components | ComponentRegistry | — | Custom components for tool response rendering. See Customizing Components. |
voiceComponents | VoiceComponents | — | Custom voice indicator/background/transcript components |
toolRenderMode | 'upsert' | 'replace' | 'upsert' | How voice tool results accumulate on screen |
BackComponent | ComponentType | — | Custom back button (replaces default rendering when onBack is set) |
CloseComponent | ComponentType | — | Custom close button (replaces default rendering when onClose is set) |
NavigationButtonComponent | ComponentType<NavigationButtonProps> | — | Custom navigation suggestion button |
navigationLabelFormat | (screenName: string) => string | — | Format the label shown on navigation buttons (e.g. "Go to Cart") |
Event Callbacks
| Callback | Type | Description |
|---|---|---|
onReady | () => void | SDK initialized successfully |
onMessageSent | (message: string) => void | User sent a message |
onResponseReceived | (response) => void | AI response received |
onError | (error: Error) => void | Error occurred |
onNavigationSuggest | (suggestion) => void | AI suggests navigation (always fires) |
onNavigationAction | (suggestion) => void | User pressed a navigation button. If omitted, the SDK navigates automatically via Expo Router. |
onExternalSuggestion | (suggestion) => void | User pressed an external action (WhatsApp, phone, map, app store…). Defaults to opening the URL via Linking. See External Navigation. |
onToolSuggested | (tools, addResponse) => void | Promise<void> | Backend matched a tool to the user’s message. Provide a handler to resolve it; otherwise, the widget renders defaults. |
onActionResult | (results) => void | Backend action execution finished |
onStepCompleted | (result) => void | A step finished inside a multi-step tool flow |
onFileUploadRequest | (request) => void | A tool requires a file upload — open your picker and call submit() |
onExtractionResult | (result) => void | AI finished extracting structured data from an uploaded file |
onClose | () => void | Close button pressed (button only renders when this is set) |
onBack | () => void | Back button pressed (button only renders when this is set) |
onCardDeepLink | (path: string, action) => void | Card button fired a deep_link action — partner navigates to the route. Required for cards that use deep links; otherwise the action silently skips. |
onCardSuggestMessage | (text: string) => void | Card fired suggest_message. Default behavior submits the text as if the user typed it; override only when you want custom routing. |
onCardExternalNavigation | (action) => void | Card fired external_navigation (WhatsApp / phone / map / etc.). Falls back to the SDK’s external navigation handler when omitted — you usually rely on the default. |
onCardShare | ({ text, url }) => void | Card fired share. Default opens the OS share sheet. |
onCardCopy | (value: string) => void | Card fired copy. Default copies to the clipboard. |
onCardToolTrigger | (toolName, params, meta) => void | Card fired tool_trigger — calls another tool from a button. The partner forwards this to their tool execution path. |
onCardCTAClick | (event) => void | Telemetry hook called once per CTA click. Event includes cardTemplateId, cardSlug, actionType, item (full bound record), and itemIndex (only present in list mode). Use it to feed analytics. |
Imperative Handle
Pass a ref to control the widget programmatically:
import { useRef } from 'react'
import { Qafka, type QafkaHandle } from '@qafka/react-native'
const qafkaRef = useRef<QafkaHandle>(null)
<Qafka ref={qafkaRef} />
// Send a message as if the user typed it
qafkaRef.current?.sendMessage('Show my orders')
// Voice session control
await qafkaRef.current?.connectVoice()
await qafkaRef.current?.disconnectVoice()
qafkaRef.current?.pauseMic()
qafkaRef.current?.resumeMic()
// Voice loading pill (for async work outside onToolSuggested)
qafkaRef.current?.setLoading(true, 'Fetching data…')
qafkaRef.current?.setLoading(false)
// Reset voice tool cards
qafkaRef.current?.clearRenderedTools()Customizing Components
The widget renders sensible defaults for every interactive surface, but you can swap any of them out for your own component to match your app’s visual language.
Header Buttons (Back / Close)
The back and close buttons appear in the header only when you wire up their callbacks. To replace the default rendering, pass BackComponent / CloseComponent:
<Qafka
onBack={() => navigation.goBack()}
onClose={() => navigation.navigate('Home')}
BackComponent={() => <MyBackButton />}
CloseComponent={() => <MyCloseButton />}
/>Navigation Buttons
When the AI suggests a navigation target, the widget renders a button. Replace the rendering with NavigationButtonComponent, or just rewrite the label with navigationLabelFormat.
// Just change the label text
<Qafka navigationLabelFormat={(screen) => `Go to ${screen}`} />
// Replace the entire button
<Qafka
NavigationButtonComponent={({ screenName, onPress, theme, label }) => (
<TouchableOpacity onPress={onPress} style={myButtonStyle(theme)}>
<Text>{label}</Text>
</TouchableOpacity>
)}
/>The component receives { screenName, suggestion, onPress, theme, style, label, icon }.
Voice Page Components
The voice page has three slots — animation indicator, background container, and transcript text. Provide any subset; missing ones fall back to defaults.
import LottieView from 'lottie-react-native'
import voiceLottie from './voice-blob.json'
<Qafka
voiceComponents={{
VoiceIndicator: ({ state, amplitude, theme }) => (
<LottieView
source={voiceLottie}
autoPlay
loop
speed={state === 'speaking' ? 1.5 : 1}
/>
),
VoiceBackground: ({ state, children, theme }) => (
<LinearGradient colors={['#1A1A2E', '#16213E']} style={{ flex: 1 }}>
{children}
</LinearGradient>
),
VoiceTranscript: ({ transcript, userTranscript, state, theme }) => (
<Text style={{ color: theme.colors.text }}>
{state === 'listening' ? userTranscript : transcript}
</Text>
),
}}
/>Each slot receives the live voice state ('idle' | 'connecting' | 'listening' | 'thinking' | 'speaking'), the current audio amplitude (0–1), and the active theme.
Tool Response Components
When a tool result comes back, the widget renders it with a default Card / List / Detail / Table component based on the tool’s response.type. Override these by registering your own components and referencing them by name from the dashboard’s tool config.
import { CampaignCard, ProductRow } from './chat-renderers'
<Qafka
components={{
CampaignCard,
ProductRow,
}}
/>Each registered component receives { data, tool, theme, onAction }. In the dashboard, set the tool’s response.itemComponent to the matching key ("CampaignCard", "ProductRow") to use your renderer instead of the default.
Card CTAs
When a tool is configured to use the Card renderer in the dashboard (see Tools — UI Rendering), buttons inside the card fire one of seven action types. Five of them have built-in SDK behavior; two cross the SDK boundary and need a host callback.
SDK-internal actions (no setup needed)
| Action type | Default behavior |
|---|---|
external_navigation | Goes through the SDK’s external nav handler (same path as External Navigation guide). |
suggest_message | Submits a new message as if the user typed it. |
copy | Copies the value to the clipboard. |
dismiss | Hides the card locally. |
share | Opens the OS share sheet. |
Override any of these via onCardSuggestMessage, onCardCopy, onCardShare, onCardExternalNavigation if you want custom behavior (e.g. show a toast on copy).
Host-bound actions
deep_link — internal route navigation
Card author writes the path in the dashboard (with optional {{template}} placeholders that resolve at click time):
{
"component": "QButton",
"label": "Detayları Gör",
"variant": "primary",
"action": {
"action": { "type": "deep_link", "path": "/store/{{id}}" }
}
}Partner registers the handler:
import { useRouter } from 'expo-router'
const router = useRouter()
<Qafka
// ...other props
onCardDeepLink={(path) => router.push(path)}
/>When the partner doesn’t provide onCardDeepLink, the action is silently skipped and a dev-mode warning is logged.
tool_trigger — fire another tool from a card
For chains where one card spawns another (e.g. “Confirm booking” button calls a bookAppointment tool):
<Qafka
onCardToolTrigger={(toolName, params, meta) => {
sdk.invokeTool(toolName, params)
}}
/>meta.ctaDisplayMode is 'user_message' (default) or 'silent' — controls whether the trigger appears as a user bubble in chat history.
CTA telemetry
onCardCTAClick fires once per button press. Event shape:
{
event: 'cta_click',
cardTemplateId: string,
cardSlug: string,
actionType: 'external_navigation' | 'deep_link' | ...,
toolName?: string,
messageId?: string,
itemIndex?: number, // only set in list mode
item: unknown, // the bound record (item in list mode, full result in detail mode)
confirmed: boolean,
}item carries the actual record the partner is acting on, so you don’t need to keep the source array around to look it up by index. Forward it to your analytics service:
<Qafka
onCardCTAClick={(event) => {
analytics.track('card_cta_click', {
action: event.actionType,
template: event.cardTemplateId,
itemId: (event.item as any)?.id,
})
}}
/>