Flutter SDK
Kindly Chat SDK for Flutter â customer support chat widget for iOS and Android applications. Thin Flutter facade over the public Kindly iOS and Android SDKs.
The published package ships the native iOS xcframework and Android AAR inline, so consumer apps don't need to wire up Swift Package Manager or JitPack themselves.
Features
- Customisable themes and colours
- Authentication via JWT (callback-based)
- Multi-language support
- Real-time chat
- Push notifications (APNS + FCM)
- Trigger specific dialogues / deep-link URLs
- Programmatic message send
- Context attached to next message or button click
- Native iOS and Android implementations bundled â no extra setup
Requirements
- Flutter >= 3.0 (Dart >= 2.17)
- iOS >= 17.2 (matches the bundled xcframework â older targets fail at launch with missing dyld symbols)
- Android minSdk >= 21, plugin builds with compileSdk 35 (matches android-source)
Installation
Add kindly to your pubspec.yaml:
dependencies:
kindly: ^3.0.26Then flutter pub get.
iOS Setup
No additional setup required. The package vendors a signed KindlySDK.xcframework and CocoaPods links it automatically when Flutter runs pod install. (The framework is named KindlySDK rather than Kindly so it doesn't case-collide with the kindly pod on macOS's case-insensitive filesystem.) The podspec pulls in Sentry as its one external dependency, matching what the bundled binary imports.
Android Setup
No additional setup required. The package ships a kindlysdk.aar that Gradle picks up directly via implementation files("libs/kindlysdk.aar").
Note on manifest merger. The bundled AAR declares android:label="@string/app_name" on its <application> element. If your app's AndroidManifest.xml also sets android:label, Android's manifest merger fails with "Attribute application@label is also present at [kindlysdk.aar]". To resolve, add tools:replace="android:label" to your <application> tag (and declare xmlns:tools="http://schemas.android.com/tools" on the root <manifest> if not already):
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:label="My App"
âĻ
tools:replace="android:label">
âĻ
</application>
</manifest>Usage
Basic Setup
import 'package:kindly/kindly.dart';
await KindlySDK.start(
botKey: 'YOUR_BOT_KEY',
market: 'general', // required â your market slug (Settings â General â Details & markets)
);
await KindlySDK.displayChat();Authentication
await KindlySDK.start(
botKey: 'YOUR_BOT_KEY',
market: 'general',
authTokenCallback: (chatId) async {
// chatId is the backend-generated identifier for this chat session
// (from /connect). Include it when calling your auth backend so the
// JWT is scoped to this chat. Matches the Web / iOS / Android contract.
final token = await yourAuthService.getToken(chatId: chatId);
return token;
},
);Use authCredentialsCallback instead when your identity provider issues more than a single JWT: a companion access token, an issuer, or an opaque token whose expiry only you know.
await KindlySDK.start(
botKey: 'YOUR_BOT_KEY',
market: 'general',
authCredentialsCallback: (chatId) async {
final tokens = await yourAuthService.getTokens(chatId: chatId);
return KindlyAuthCredentials(
idToken: tokens.idToken, // optional
accessToken: tokens.accessToken, // optional
issuer: 'https://idp.example.com/', // optional
expiresAt: tokens.expiresAtUnixSeconds, // optional, Unix seconds
);
},
);Every field is optional, but set at least one of idToken / accessToken. Either token alone is a valid credential: a provider that issues only an access token is normal and supported, and that access token becomes the bearer. Credentials carrying neither are refused and the chat stays anonymous. KindlyAuthCredentials.bearerToken tells you which token will be sent as Authorization: Bearer, idToken first.
An opaque token that nobody can decode goes in idToken with issuer set, so the backend knows which provider to introspect it against. The access token is only sent alongside an id token, as X-Access-Token, and only when the two differ. A duplicate would fail the auth outright.
A single-JWT integration never needs this callback. When both callbacks are given, authCredentialsCallback wins.
The SDK refreshes credentials on its own, 30 seconds before they expire, using whichever is earliest of the backend's reported expiry, your expiresAt, and the exp claim of the bearer. If none is known the credentials are treated as non-expiring, which is a trap for opaque tokens, so set expiresAt there.
When the identity changes rather than the token, tell the SDK:
await KindlySDK.authenticate(); // user signed in, or switched account
await KindlySDK.deauthenticate(); // user signed outauthenticate() re-binds fresh credentials to the chat that is already open, keeping the conversation. deauthenticate() unbinds the identity on the backend and clears the stored credentials; the chat carries on anonymously with its history intact, and calling it again when the chat is already anonymous does nothing.
Custom Theme
await KindlySDK.setCustomTheme(
background: Colors.white,
botMessageBackground: Colors.grey[200],
botMessageText: Colors.black,
userMessageBackground: Colors.blue,
userMessageText: Colors.white,
buttonBackground: Colors.blue,
buttonText: Colors.white,
navBarBackground: Colors.blue,
navBarText: Colors.white,
inputBackground: Colors.grey[100],
inputText: Colors.black,
);
// Revert to default
await KindlySDK.clearCustomTheme();Context Data
await KindlySDK.setNewContext({
'userId': '12345',
'userName': 'John Doe',
'email': '[email protected]',
'plan': 'premium',
});
await KindlySDK.clearNewContext();Trigger Specific Dialogues
// Launch chat with a specific dialogue
await KindlySDK.launchChat(triggerDialogueId: 'welcome_dialogue');
// Or trigger a dialogue inside an active chat
await KindlySDK.triggerDialogue('help_dialogue');
// Clear any pending trigger (Android only)
await KindlySDK.clearTriggerDialogue();Deep links
The SDK recognises kindly://chat/dialogue/{id} URLs.
final handled = await KindlySDK.handleUrl('kindly://chat/dialogue/welcome');Programmatic Message Send
Send a message on behalf of the user. The SDK renders the bubble locally and reconciles the server-assigned id when the POST returns â do not also display the message yourself.
final reconciled = await KindlySDK.sendMessage(
'Hello there',
newContext: {'priority': 'high'},
);
print(reconciled?['id']); // server idHandover to a Human Agent
Initiate a handover and receive a callback when the handover begins.
await KindlySDK.callHandover(() {
print('Handover initiated');
});Push Notifications
iOS
await KindlySDK.setAPNSDeviceToken(deviceToken);
await KindlySDK.handleNotification(notificationData);
final isKindly = await KindlySDK.isKindlyNotification(notificationData);Android
await KindlySDK.saveNotificationToken(fcmToken);
await KindlySDK.handleNotification(remoteMessage.data);
final isKindly = await KindlySDK.isKindlyNotification(remoteMessage.data);Other Features
// Check chat UI state (iOS only)
final isDisplayed = await KindlySDK.isChatDisplayed;
// Update language for the active session
await KindlySDK.setLanguage('no');
// Save a JWT to the platform keychain (iOS only)
await KindlySDK.saveAuthToken('jwt-here');
// Toggle SDK logging / Sentry crash reporting
await KindlySDK.setVerboseLogging(true);
await KindlySDK.setCrashReporting(true);
// Close UI without ending the session
await KindlySDK.closeChat();
// End the active session (clears messages + token, dismisses UI)
await KindlySDK.endChat();
// Tear the SDK down completely â must call start() again before reuse
await KindlySDK.kill();API Reference
Lifecycle
- start({botKey, market, languageCode?, authTokenCallback?, authCredentialsCallback?}) â initialise the SDK. First call before anything else. market is required (Markets feature); the SDK loads that market's settings with no fallback. Use authCredentialsCallback for identity providers that issue more than a single JWT.
- displayChat({languageCode?, triggerDialogueId?}) â show the chat UI.
- launchChat({triggerDialogueId?}) â connect (if needed) and show the chat UI. Recommended on Android; routed to displayChat on iOS.
- closeChat() â dismiss the UI without ending the session.
- endChat() â end the active session, clear messages + token, dismiss UI.
- kill() â fully tear down the SDK; start() must be called again before reuse.
- setLanguage(languageCode) â change language for the current session.
Authentication
- authenticate(): re-authenticate the live chat with fresh credentials, for a sign-in mid-session. Expiry is handled by the SDK on its own
- deauthenticate(): sign out of the chat without ending it. History is kept and the chat continues anonymously
Messaging
- sendMessage(text, {newContext?}) â send a message programmatically; resolves with {id, text, created, sender, ...}.
- setNewContext(context) â stage Map<String, String> context for the next message or click.
- clearNewContext() â clear staged context.
- triggerDialogue(dialogueId) â trigger a dialogue by id (must be connected).
- clearTriggerDialogue() â clear pending trigger (Android only).
- handleUrl(url) â handle kindly://chat/dialogue/{id} deep links; returns bool.
- callHandover(callback) â request handover to a human agent (Android only).
Theme
- setCustomTheme({...colors}) â override individual theme slots (background, botMessageBackground, botMessageText, userMessageBackground, userMessageText, buttonBackground, buttonText, buttonOutline, buttonSelectedBackground, navBarBackground, navBarText, inputBackground, inputText, inputCursor, chatLogElements, defaultShadow, maintenanceHeaderBackground).
- clearCustomTheme() â revert to the default / API theme.
Push
- setAPNSDeviceToken(token) â register the APNS token (iOS only).
- saveNotificationToken(token) â register the FCM token (Android only).
- handleNotification(data) â forward a push to the SDK.
- isKindlyNotification(data) â true if the push originated from Kindly.
State & settings
- isChatDisplayed â Future<bool>; iOS only (Android always returns false â track via host UI state).
- saveAuthToken(token) â persist a JWT to the platform keychain (iOS).
- setVerboseLogging(bool) â toggle verbose logging.
- setCrashReporting(bool) â toggle Sentry crash reporting.
License
This project is licensed under the MIT License â see the LICENSE file for details.