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.
configuremakes 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 = 24andcompileSdk = 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 {
try {
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;
}
} catch (e) {
// buy() should return quietly when the customer cancels; anything thrown is a real failure.
await showPurchaseError(e);
}
});
// The purchase sheet opened over the chat: close it, then say what went wrong
// (billing unavailable, no network), or the tap looks like it did nothing.
Future<void> showPurchaseError(Object error) async {
await Concierge.dismiss();
messengerKey.currentState?.showSnackBar(SnackBar(content: Text('Purchase failed: $error')));
}
Without onOfferCard the native SDK runs the default handling (StoreKit purchase on iOS, offer
code links, paywall offers to onPaywallCard).
The chat closes before onPaywallCard runs and before onOfferCard gets an offer that resolved to a
paywall, so push your paywall route directly. Store offers run over the open chat (the purchase sheet
shows on top of it); on a failed purchase call Concierge.dismiss() before you show the error. 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.podspeccopiessdk/ios/Sources/Conciergeintoios/Vendor(gitignored) and compiles it into the pod. Outside the monorepo it vendorsios/concierge_sdk/Concierge.xcframework. - Android:
android/build.gradle.ktscompilessdk/android/concierge/src/maininto the plugin. Outside the monorepo it depends oncom.conciergesdk:concierge:<version>from Maven Central. CONCIERGE_NATIVE=binary|source(or-PconciergeNative=on Android) forces one mode.tool/pigeon.shregenerates the bridge after editingpigeons/messages.dart.flutter testruns the Dart unit tests (the Pigeon channels are mocked).- Publishing:
tool/sync_native.sh ios-binary(aftersdk/ios/scripts/build-xcframework.sh),tool/sync_native.sh check-pack, thenflutter 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.