Kindly Web View SDK, Events Reference
Every event the SDK emits, what it carries and when it arrives, on all four platforms. Companion to SKILL.md. The guide page is Events
The SDK has two streams: events, everything that happens in the chat, and state, the SDK's own state. Events are not replayed: a listener sees the events emitted after it started listening, and it can start before start. A new state listener gets the current state first, then every change
Subscribing
iOS
import Combine
import KindlyWebView
var subscriptions = Set<AnyCancellable>()
KindlyWebViewSDK.events
.sink { event in print(event.rawType) }
.store(in: &subscriptions)
KindlyWebViewSDK.state
.sink { state in print(state.connectionState) }
.store(in: &subscriptions)
let now = KindlyWebViewSDK.currentStateevents is an AnyPublisher<KindlyEvent, Never> and state an AnyPublisher<KindlyEventState, Never>. Both emit on the main thread
Android
import ai.kindly.webview.KindlyWebViewSDK
lifecycleScope.launch {
KindlyWebViewSDK.events.collect { event -> Log.d("Chat", event.rawType) }
}
lifecycleScope.launch {
KindlyWebViewSDK.state.collect { state -> Log.d("Chat", state.connectionState) }
}
val now = KindlyWebViewSDK.currentStateevents is a Flow<KindlyEvent> and state a Flow<KindlyEventState>. The collecting block runs in the collector's coroutine context
Flutter
final events = KindlyWebViewSDK.events.listen((KindlyWebViewEvent event) {
debugPrint(event.rawType);
});
final state = KindlyWebViewSDK.state.listen((KindlyWebViewState state) {
debugPrint(state.rawConnectionState);
});
final KindlyWebViewState now = await KindlyWebViewSDK.currentState();
// later
await events.cancel();
await state.cancel();Both are broadcast streams, so any number of listeners can share them. A state equal to the previous one is not repeated
React Native
const events = KindlyWebViewSDK.addEventListener((event) => console.log(event.rawType));
const state = KindlyWebViewSDK.addStateListener((s) => console.log(s.connectionState));
const now = await KindlyWebViewSDK.getState();
// later
events.remove();
state.remove();A listener that throws does not stop the others
Event types
rawType | iOS KindlyEventType | Android KindlyEventType | Flutter KindlyWebViewEventType | React Native type | Emitted when |
|---|---|---|---|---|---|
kindly:load | load | LOAD | load | load | The widget loaded the bot's settings |
kindly:message | message | MESSAGE | message | message | A message was added to the conversation while the chat was open |
kindly:buttonclick | buttonClick | BUTTON_CLICK | buttonClick | buttonClick | The user clicked a message button |
kindly:ui | ui | UI | ui | ui | A UI action happened |
kindly:globalicon | globalIcon | GLOBAL_ICON | globalIcon | globalIcon | A global icon was clicked |
kindly:state | state | STATE | state | state | The SDK state changed |
as received | | | unknown | unknown | A type this version of the package does not know |
The chat hides the widget's global icons, so globalIcon events practically never arrive. load comes each time a chat page loads: on the first display, and again after the page was rebuilt
When message events arrive
The chat emits a message event only while it is open. There is none:
- while the chat is hidden, also for a message sent with sendMessage or a dialogue triggered then
- for the messages already there when the chat first renders: restored history, the greeting when it lands before that render, and the reply to a dialogue triggered as the chat opens, which on a new chat page often lands before that first render
The next message event's chatLog, the last 50 messages, carries all of them. To count or read messages the user has not seen in an event, read the chatLog of the next one
The event object
| iOS KindlyEvent | Android KindlyEvent | Flutter KindlyWebViewEvent | React Native KindlyWebViewEvent |
|---|---|---|---|---|
Type | type: KindlyEventType | type: KindlyEventType | type: KindlyWebViewEventType | type, a string union |
Raw type | rawType | rawType | rawType | rawType |
Detail | detail: KindlyEventDetail | detail: KindlyEventDetail | detail, a Map<String, Object?> | detail, typed by type |
Detail as JSON | detailJSON | detailJSON | detailJson | rawDetail, the parsed object |
Time | timestamp, epoch milliseconds, and date | timestamp, epoch milliseconds | timestamp, a DateTime | timestamp, epoch milliseconds |
iOS KindlyEventDetail is an enum and Android's a sealed class, with one case per type:
Type | iOS | Android |
|---|---|---|
load | .load(settings:) | KindlyEventDetail.Load(settings) |
message | .message(newMessage:chatLog:) | KindlyEventDetail.Message(newMessage, chatLog) |
buttonClick | .buttonClick(button:buttonType:chatLog:lastMessage:) | KindlyEventDetail.ButtonClick(button, buttonType, chatLog, lastMessage) |
ui | .ui(action:) | KindlyEventDetail.UI(action) |
globalIcon | .globalIcon(action:) | KindlyEventDetail.GlobalIcon(action) |
state | .state(state:) | KindlyEventDetail.State(state) |
Flutter's detail map and React Native's rawDetail hold the JSON shape listed under Payloads below. React Native's KindlyWebViewEvent is a union, so checking type narrows detail. Its rawDetail has no __proto__, constructor or prototype keys at any depth
KindlyEventType, KindlyUIActionType and the other Swift enums can gain cases in a later version, so give a switch over one of them a default or @unknown default case
Payloads
The JSON keys are what detailJSON holds, Flutter's detail map and React Native's rawDetail. React Native's typed detail uses the same names unless noted
load
iOS and Android (KindlyEventSettings) | JSON | Content |
|---|---|---|
name | name | The bot's settings name |
id | id | The settings id, a string |
languages | languages | The active languages, at most 50, each code, name and isDefault |
style | style | The colours, below |
style (KindlyEventStyle) has background, buttonBackground, buttonText, headerBackground, headerText, inputBackground, inputText, userMessageBackground, userMessageTextColor, botMessageBackground and botMessageTextColor, each a hex colour string or null
message
newMessage and chatLog, the last 50 messages. React Native's newMessage can be null. A message (KindlyEventMessage on iOS and Android, KindlyWebViewMessage in React Native):
iOS and Android | JSON | Content |
|---|---|---|
id | id | The message id. A message the user just typed carries a tempId-... id until the server confirms it |
chatId | chatId | The chat id, or null |
text | text | The text, at most 4,000 characters, or null |
sender | sender | USER, BOT, AGENT, SYSTEM or UNKNOWN (KindlyEventMessageSender) |
created | created | When it was created: a Date on iOS and Android, ISO 8601 text in JSON, or null |
isFromUser | fromBot | The typed models say whether the user sent it, the JSON whether the bot did |
exchangeID | exchangeId | The exchange id, or null |
exchangeType | exchangeType | dialogue, fallback, greeting, system_dialogue or user_says (KindlyEventExchangeType), or null |
videoSource | videoSource | A video URL, without its query, or null |
buttons | buttons | At most 20 buttons, below |
images | images | At most 20, each url (without its query), altText and id |
attachments | attachments | At most 10, each id, name, format, status, sender and storagePath. The download URL is never included |
A button (KindlyEventButton, KindlyWebViewButton):
Field | Content |
|---|---|
id | The button id, or null |
type | The button type, for example quick_reply |
label | The label, at most 500 characters |
value | The value, or null. A URL value has lost its query |
index | Its position, or null |
hasBeenSelected | Whether the user picked it |
context | Flat context, at most 20 primitive values, or null |
buttonClick
button, buttonType, lastMessage and chatLog. A bot button of type privacy_export also starts a download of the user's data, see Uploads and Downloads
ui
iOS and Android (KindlyEventUIAction) | JSON | React Native detail | Content |
|---|---|---|---|
type | type | action, and rawType as sent | The action, below |
source | source | source | user, or programmatic when a call to the SDK caused it |
chatId | chatId | chatId | The chat id. When there was none yet it is empty in the typed models and React Native's detail, and null in the JSON |
value | value | value | Extra data, the fields below are read from it |
languageCode, languageName, previousLanguageCode, previousLanguageName | inside value | the same names | change-language |
feedbackType, feedbackValue | inside value | the same names | show-feedback, submit-feedback and dismiss-feedback |
nextPage | inside value | nextPage | open-chatbubble: chat or welcome-page |
The actions, with the widget value that JSON, React Native and rawUiAction use:
Widget value | iOS and Flutter | Android | Happens when |
|---|---|---|---|
open-chatbubble | openChatBubble | OPEN_CHAT_BUBBLE | The chat opened |
close-chatbubble | closeChatBubble | CLOSE_CHAT_BUBBLE | The chat was minimised or closed |
end-chat | endChat | END_CHAT | The conversation ended |
restart-chat | restartChat | RESTART_CHAT | The conversation restarted |
download-chat | downloadChat | DOWNLOAD_CHAT | The user downloaded the conversation |
delete-chat | deleteChat | DELETE_CHAT | The user deleted the conversation |
change-language | changeLanguage | CHANGE_LANGUAGE | The user changed the chat language |
show-feedback | showFeedback | SHOW_FEEDBACK | The chat showed a feedback form |
submit-feedback | submitFeedback | SUBMIT_FEEDBACK | The user submitted feedback |
dismiss-feedback | dismissFeedback | DISMISS_FEEDBACK | The user dismissed the feedback form |
welcome-page-start-chat | welcomePageStartChat | WELCOME_PAGE_START_CHAT | The user started the chat from the welcome page |
Where to read it: iOS action.type (KindlyUIActionType), Android action.type (KindlyUIActionType), Flutter event.uiAction (KindlyWebViewUiAction) with the source in event.uiSource, React Native event.uiAction with the source in event.detail.source. Flutter and React Native report an action they do not know as unknown, with the widget value in Flutter's rawUiAction and React Native's detail.rawType. iOS and Android drop it
endChat() and kill() finish with an end-chat and then a close-chatbubble event that the SDK emits itself, with the source programmatic. The SDK also emits close-chatbubble when the chat closes without the widget reporting it. An event the SDK emits can have an empty chatId
globalIcon
iOS and Android (KindlyEventGlobalIcon): type, source and value, the icon. JSON and React Native: type and icon, with id, title, altText, imageUrl, type and action, or null
state
Field | Meaning |
|---|---|
isStarted | start was called and kill has not run since |
isChatDisplayed | The chat screen is visible |
isSocketConnected, isConnected | Both mean the chat page is loaded, also while the chat is hidden |
connectionState | Where the chat page is |
connectionState | Meaning |
|---|---|
none | No chat page yet, before the first display or after kill |
loading | The chat page is loading |
connected | The chat page is loaded. It stays loaded while the chat is hidden |
failed | The chat page failed to load, or it loaded and the chat did not open, even after one more try |
unsupported | This device's web view cannot run the chat |
disconnected | The chat page was released: after endChat, or while the chat was hidden, when memory ran low or the system ended the web view's process. The next display loads a new one |
On iOS and Android connectionState is a string. Flutter's KindlyWebViewConnectionState and React Native's string add unknown for a value this version does not know, with the raw value in Flutter's rawConnectionState and React Native's raw.connectionState. The same changes also arrive as state events
Limits and data handling
- The SDK caps each detail itself: at most 4,000 characters of message text, the last 50 messages of the chat log, 20 buttons, 20 images and 10 attachments per message, labels and other short strings at 500 characters
- URLs in events have their query removed, and attachment download links are never included
- Every value comes from the chat page, so treat it as untrusted input: never run it as code, build a query from it or open a URL from it without checking it
- Message events carry what the user wrote, so treat them as user data and keep them out of logs and analytics
Worked examples
React to a bot reply
KindlyWebViewSDK.events
.sink { event in
guard case let .message(newMessage, _) = event.detail, newMessage.sender == .bot else { return }
// a bot reply arrived while the chat is open
}
.store(in: &subscriptions)import ai.kindly.webview.models.KindlyEventDetail
import ai.kindly.webview.models.KindlyEventMessageSender
lifecycleScope.launch {
KindlyWebViewSDK.events.collect { event ->
val detail = event.detail
if (detail is KindlyEventDetail.Message && detail.newMessage.sender == KindlyEventMessageSender.BOT) {
// a bot reply arrived while the chat is open
}
}
}KindlyWebViewSDK.events.listen((KindlyWebViewEvent event) {
final Object? newMessage = event.detail['newMessage'];
if (event.type == KindlyWebViewEventType.message && newMessage is Map && newMessage['sender'] == 'BOT') {
// a bot reply arrived while the chat is open
}
});KindlyWebViewSDK.addEventListener((event) => {
if (event.type === 'message' && event.detail.newMessage?.sender === 'BOT') {
// a bot reply arrived while the chat is open
}
});Follow the chat language
KindlyWebViewSDK.events
.sink { event in
if case let .ui(action) = event.detail, action.type == .changeLanguage, let code = action.languageCode {
print("chat language", code)
}
}
.store(in: &subscriptions)import ai.kindly.webview.models.KindlyUIActionType
lifecycleScope.launch {
KindlyWebViewSDK.events.collect { event ->
val detail = event.detail
if (detail is KindlyEventDetail.UI && detail.action.type == KindlyUIActionType.CHANGE_LANGUAGE) {
Log.d("Chat", "chat language ${detail.action.languageCode}")
}
}
}KindlyWebViewSDK.events.listen((KindlyWebViewEvent event) {
final Object? value = event.detail['value'];
if (event.uiAction == KindlyWebViewUiAction.changeLanguage && value is Map) {
debugPrint('chat language ${value['languageCode']}');
}
});KindlyWebViewSDK.addEventListener((event) => {
if (event.type === 'ui' && event.uiAction === 'change-language') {
console.log('chat language', event.detail.languageCode);
}
});Know when the user leaves the chat
close-chatbubble arrives when the user minimises the chat, swipes the sheet down on iOS or presses back on Android, and when the app calls closeChat, endChat or kill. To catch every way the chat screen goes away, follow isChatDisplayed in the state instead, which turns false whenever the screen is gone
Threading notes
- iOS: events and state emit on the main thread, so UI work in sink is safe
- Android: the collecting block runs where the collector runs, so collect in lifecycleScope for UI work, and the SDK's calls can be made from any thread
- Flutter: listeners run in the Dart isolate that subscribed, normally the main one
- React Native: listeners run on the JavaScript thread