kindly 3.0.15 copy "kindly: ^3.0.15" to clipboard
kindly: ^3.0.15 copied to clipboard

Kindly Chat SDK for Flutter - customer support chat widget for iOS and Android. Successor to the kindly_sdk package.

Kindly Chat SDK for Flutter #

pub package License: MIT

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: ^2.0.0

Then flutter pub get.

Migrating from kindly_sdk? This package is the renamed successor to kindly_sdk. The public API is identical — only the package name changed. To migrate: bump the dep from kindly_sdk to kindly: ^2.0.0, and replace import 'package:kindly_sdk/kindly_sdk.dart' with import 'package:kindly/kindly.dart'. Nothing else needs to change.

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.)

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 out

authenticate() 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': 'john@example.com',
  '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();

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 id

Handover 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 #

  • isChatDisplayedFuture<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.


Maintenance #

Internal note for future maintainers — not relevant if you're integrating the SDK into your app.

This package wraps two native artefacts that are built from sibling repos and bundled inline:

  • ios/Frameworks/KindlySDK.xcframework — built from ../ios-source/, signed with the team G2P4AFMWD6 Apple Distribution cert. (Renamed from upstream Kindly.framework during bundling — see AGENTS.md for the why.)
  • android/libs/kindlysdk.aar — built from ../android-source/ via ./gradlew :kindlysdk:assembleRelease -PsdkMode=true.

There are two release paths:

  1. Manual local (make publish from a developer laptop) — used when the bridge layer (lib/kindly.dart + the two KindlyPlugin files) needs hand-edits because EntryPoint.swift / EntryPoint.kt changed.
  2. Cross-repo CI (automatic on every iOS / Android source release) — ios-source/.github/workflows/deploy-sdk.yml and android-source/.github/workflows/deploy-sdk.yml clone this repo, rebundle the corresponding native binary, bump pubspec.yaml's patch, and dart pub publish — keeping kindly in sync with the latest native bug fixes without any manual step.

See docs/flutter-sdk-release-architecture.md for the full diagram of the auth model, secrets, and how to flip between architectures.

make setup        # one-time: install Ansible / coreutils / etc.
make bundle       # build-only: refresh the xcframework + AAR (no publish)
make publish-dry  # bundle + validate via `dart pub publish --dry-run`
make publish      # bundle + validate + confirmation prompt + upload + tag + bump

make publish is the canonical release command. It runs a single Ansible invocation that bundles, validates, prompts before the irreversible upload, then publishes + tags + bumps. make bundle is a stand-alone convenience for build-only work — make publish doesn't need it.

Version cycle (mirrors the iOS pipeline) #

pubspec.yaml's version: field is the canonical source of truth, equivalent to iOS's .bumpversion.cfg. The release flow is:

  1. Whatever version is in pubspec.yaml right now is what gets published. If pubspec.yaml says 1.1.0, make publish publishes 1.1.0.
  2. Right after a successful dart pub publish, the playbook tags the released commit v1.1.0 and pushes the tag.
  3. It then auto-increments the patch (1.1.01.1.1), commits the bump with message Bump version for next release, and pushes that commit on the current branch.
  4. So at any point in time, HEAD of your branch carries the next version waiting to be released, and the most recent release tag is one patch behind.

For a minor or major bump, edit pubspec.yaml and CHANGELOG.md by hand before running make publish — same model as bumping .bumpversion.cfg manually on iOS before make deploy-sdk. Patch bumps are automatic.

Cert prerequisite: the bundling step requires the Apple Distribution cert for team G2P4AFMWD6 to already be in your login keychain. If you regularly release the iOS SDK from ../ios-source/, you have it. Otherwise run once: cd ../ios-source && bundle exec fastlane match development --readonly.

The plugin's bridge layer (lib/kindly.dart + ios/Classes/KindlyPlugin.swift + android/src/main/kotlin/ai/kindly/flutter_sdk/KindlyPlugin.kt) is a thin facade over the public EntryPoint API of each native SDK. If a method isn't on EntryPoint, it isn't reachable from Flutter — that's by design.

When the iOS or Android SDK changes:

  • No public-API change (bug fix, internal refactor): just make publish (patch bumps automatically).
  • EntryPoint API change (new method, signature change): update the three bridge files first, optionally hand-bump minor/major in pubspec.yaml, then make publish.

Manual ansible-playbook invocations bundle by default too — you can pass -e skip_bundle=true to skip the bundle step (only useful when iterating on the publish playbook itself).

See scripts/README.md for what each playbook does.

0
likes
140
points
569
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

Kindly Chat SDK for Flutter - customer support chat widget for iOS and Android. Successor to the kindly_sdk package.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on kindly

Packages that implement kindly