Concierge for Flutter

In-app support chat for Flutter apps on iOS and Android. concierge_sdk wraps the native Concierge iOS and Android SDKs, so the chat is the same native SwiftUI and Jetpack Compose screen, with the same features: AI replies, cached history, offline outbox, photos, cards, offers, client tools and push.

  • iOS 16.4+ and Android 7 (API 24)+. Web and desktop are not supported (use the web SDK on the web); there every call is a no op.
  • One static Dart API that mirrors the React Native package one to one, with streams where React Native has hooks.
  • configure makes no network calls. The device registers the first time the chat opens or a push token arrives.

Full docs: https://conciergesdk.com/docs/flutter/

Install

flutter pub add concierge_sdk

Raise the minimum platform versions if your app is below them:

  • ios/Podfile: platform :ios, '16.4'
  • android/app/build.gradle(.kts): minSdk = 24 and compileSdk = 37 (the native SDK compiles against Android SDK 37)

The iOS side installs through CocoaPods and ships the prebuilt Concierge.xcframework inside the package. The Android side pulls com.conciergesdk:concierge from Maven Central. Or let the CLI do all of it: npx @conciergesdk/cli init detects Flutter projects (pubspec.yaml).

Quick start

import 'package:concierge_sdk/concierge_sdk.dart';
import 'package:flutter/material.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  Concierge.configure(appKey: 'app_YOUR_APP_KEY');
  runApp(const MyApp());
}

// After sign in, and on every launch while signed in. `token` is an HS256 JWT signed on your server.
Concierge.identify(user.id, name: user.name, email: user.email, token: token);

// Your Help button
Concierge.open();

// Badge
StreamBuilder<int>(
  stream: Concierge.unreadCountStream,
  initialData: Concierge.getUnreadCount(),
  builder: (context, snapshot) => Badge(
    isLabelVisible: (snapshot.data ?? 0) > 0,
    label: Text('${snapshot.data ?? 0}'),
    child: const Icon(Icons.support_agent),
  ),
);

// On sign out (never at launch for signed-out users)
Concierge.logout();

API

All methods are static on Concierge. Calls before configure are ignored with a warning.

Method What it does
configure(appKey:, baseUrl:, debug:, reviewPromptAfterSupport:, locale:) Sets the SDK up. baseUrl must be https (http only for localhost and emulator hosts, or any host in debug). debug shows the native debug overlay in debug builds.
identify(userId, name:, email:, token:) / logout() Ties the device to your user (cross-device history needs token) / forgets the user.
setMetadata(map), breadcrumb(name), setContext(map), clearContext(), captureError(error, [stackTrace]) What the team sees next to the conversation. A null context value removes the key.
open(), dismiss(), isChatVisible() The native chat (a sheet on iOS, an activity on Android).
getUnreadCount(), unreadCountStream, refreshUnreadCount() Unread team and AI replies.
isEnabled(), enabledStream The dashboard kill switch. Hide your Support button while false.
registerPushToken(token), isConciergeNotification(data), handleNotification(data, openChat:) Push: the APNs token (hex) on iOS, the FCM token on Android.
onPaywallCard(handler), onOfferCard(handler) Cards. redemption.mechanism is a sealed ConciergeOfferMechanism.
events, addEventListener(listener) Sealed ConciergeEvent subclasses (ChatOpenedEvent, UnreadCountChangedEvent, OfferRedeemedEvent, ...).
registerTool(name, description, schema, handler, title:, requiresConsent:), unregisterTool(name) Actions the AI or your team can ask the app to run. The native SDK asks the user first; the handler has 30 seconds.
registerScreen(id, title, deepLink), unregisterScreen(id), onOpenScreen(handler) Knowledge autopilot screen links.
setAppearance(ConciergeAppearance(...)), getAppearance() Accent colors, title, welcome; showsCloseButton (iOS), colorScheme (Android).
maybeRequestReview(moment) The native review prompt governor. Resolves ReviewPromptResult(shown, reason), never throws.
presentCancelFlow(productId:), paywallHelp(PaywallContext(...)) Retention: the in-app cancel flow and the paywall concierge.
getAttestationStatus() AttestationStatus.none/dev/unverified/verified/failed.
getRefundConsentText(), setRefundDataConsent(granted, [text]) The refund data consent toggle.
setLocale(tag), getLocale() The chat language (BCP 47, 34 languages, right to left for Arabic, Hebrew and Persian); null follows the device.
reset(), suspend(), resume(), isSuspended() Back to a fresh install / invisible and silent until resumed.

Push notifications

With firebase_messaging:

final messaging = FirebaseMessaging.instance;
final token = Platform.isIOS ? await messaging.getAPNSToken() : await messaging.getToken();
if (token != null) Concierge.registerPushToken(token);

FirebaseMessaging.onMessage.listen((m) => Concierge.handleNotification(m.data, openChat: false));
FirebaseMessaging.onMessageOpenedApp.listen((m) => Concierge.handleNotification(m.data));

Offer cards

Concierge.onOfferCard((redemption) async {
  switch (redemption.mechanism) {
    case ProductMechanism(:final productId):
      await buy(productId, accountToken: redemption.accountToken);
    case AndroidOfferMechanism(:final productId, :final basePlanId, :final offerId):
      await buyPlayOffer(productId, basePlanId, offerId, redemption.accountToken);
    case PaywallMechanism(:final paywallId):
      showPaywall(paywallId);
    case PromotionalMechanism() || WinBackMechanism() || OfferCodeMechanism() || CompMechanism() || UnknownMechanism():
      break;
  }
});

Without onOfferCard the native SDK runs the default handling (StoreKit purchase on iOS, offer code links, paywall offers to onPaywallCard).

The chat closes before either handler runs, so push your paywall route directly. No tap is a dead end: without onPaywallCard a paywall card closes the chat, emits PaywallUnhandledEvent and, in debug builds, prints a hint; an offer nothing can run does the same with OfferUnhandledEvent. Review cards are fully native (the store review page, else the system review prompt on iOS or the app's Play Store page on Android).

How the wrapping works

Dart talks to the native SDKs through a typed Pigeon bridge (pigeons/messages.dart). The native SDKs own the chat UI, networking, consent sheets and purchases; the plugin only forwards calls and events. Unlike React Native, the Dart isolate keeps running while the native chat covers the Flutter view, so client tools work without a keep alive.

Example

cd example
flutter run --dart-define=CONCIERGE_APP_KEY=app_... [--dart-define=CONCIERGE_BASE_URL=http://10.0.2.2:3000]
flutter test integration_test --dart-define=CONCIERGE_APP_KEY=app_...   # on a simulator or emulator

Developing this package (Concierge monorepo only)

Inside the monorepo the plugin builds the native SDKs from source, so a change in sdk/ios or sdk/android shows up in the example without publishing:

  • iOS: ios/concierge_sdk.podspec copies sdk/ios/Sources/Concierge into ios/Vendor (gitignored) and compiles it into the pod. Outside the monorepo it vendors ios/concierge_sdk/Concierge.xcframework.
  • Android: android/build.gradle.kts compiles sdk/android/concierge/src/main into the plugin. Outside the monorepo it depends on com.conciergesdk:concierge:<version> from Maven Central.
  • CONCIERGE_NATIVE=binary|source (or -PconciergeNative= on Android) forces one mode.
  • tool/pigeon.sh regenerates the bridge after editing pigeons/messages.dart.
  • flutter test runs the Dart unit tests (the Pigeon channels are mocked).
  • Publishing: tool/sync_native.sh ios-binary (after sdk/ios/scripts/build-xcframework.sh), tool/sync_native.sh check-pack, then flutter pub publish. See docs/RELEASING.md.

The plugin supports CocoaPods. Apps with Swift Package Manager turned on still build it: Flutter falls back to CocoaPods for plugins without a Package.swift.

License

Proprietary. See LICENSE and https://conciergesdk.com/legal/sdk-license/.

Libraries

concierge_sdk
Concierge in-app support chat for Flutter. Wraps the native iOS and Android SDKs.