BTX Flutter SDK
btx is the BTX Flutter SDK for customer-app telemetry, feature flags,
messaging, and native integrations.
Install
dependencies:
btx: ^0.0.21
Quickstart
import 'package:btx/btx.dart';
await Btx.configure(
BtxConfiguration(
publishableClientKey: 'cfk_...',
),
);
await Btx.identify(
const BtxCustomer(
externalId: 'customer_123',
name: 'Taylor',
email: 'taylor@example.com',
),
);
Btx.log(
'checkout_started',
level: BtxLogLevel.info,
message: 'Customer started checkout.',
properties: <String, Object?>{'cartId': 'cart_123'},
);
Btx.messenger.present();
BTX derives app version, build number, platform, bundle ID/package name, OS name and version, client-device model/manufacturer, physical-device status, and BTX SDK name/version from the host app by default. Android also reports the API level and security-patch version when available.
The SDK does not collect persistent phone identifiers, user-assigned device names, phone serial numbers, storage readings, or memory readings. Runtime metadata collection is best effort and never blocks SDK configuration.
Mount BtxHost inside your MaterialApp so the SDK can present messenger UI
and foreground notification surfaces:
MaterialApp(
home: BtxHost(
child: MyAppHome(),
),
);
Facade Surface
Btx.configure(BtxConfiguration)configures the SDK once.Btx.sdkNameandBtx.sdkVersionexpose the SDK identity reported in app context for support diagnostics.Btx.identify(BtxCustomer?)binds or clears the active customer.Btx.ready()waits for pending configure/identity work to settle.Btx.log(...)enqueues host telemetry. The SDK flushes logs automatically after enqueue, on app lifecycle transitions, and when batches fill.Btx.refreshFeatureFlags()refreshes the active customer flag snapshot.Btx.isFeatureEnabled(...)reads the current flag snapshot synchronously.Btx.featureFlagEnabled(...)waits for pending SDK work and optionally refreshes before reading a flag.Btx.featureFlagsListenablelets host UI rebuild when refreshed flag values change.Btx.statusandBtx.statusListenableexpose whether telemetry is active, inactive, disabled, or disposed. Disabled states include a typed reason such asinvalidPublishableClientKeyorappContextMismatch.Btx.messenger.present(...),presentThread(...),presentCompose(...),createThread(...), anddismiss()own messenger presentation and thread creation.Btx.push.bindAndroidSource(...),setAndroidToken(...),handleAndroidNotificationOpen(...), andunregisterAndroidDevice()own Android BTX push lifecycle once the host supplies a Firebase-neutral source.Btx.flush()is available for rare lifecycle/debug waits; normal host apps should not call it for routine telemetry.Btx.dispose()supports teardown and test cleanup.
Messenger Behavior
The packaged messenger renders URLs in message bodies as tappable links and opens them with the platform's external URL handler. Customers can long-press a message bubble to copy the message text.
Host-owned feedback forms can create a customer message thread directly without opening the SDK-owned messenger UI:
final thread = await Btx.messenger.createThread(
subject: 'App feedback',
body: feedbackText,
launchContext: const BtxLaunchContext(
conversationPurpose: BtxConversationPurpose.feedback,
entryPoint: 'feedback',
sourceType: 'host_feedback_form',
),
);
BtxConversationPurpose.support is the default for normal messenger threads.
Feedback threads remain replyable, but BTX operator surfaces can label and
prioritize them separately.
Messenger Realtime Lifecycle
The public host API is unchanged. Internally, the Flutter SDK advertises the
transportVisibility: "explicit_v1" capability and requires the backend to
return server-owned reconnect delays for both messenger states:
active: any BTX messenger surface is presented, including the thread list, compose screen, or an open thread.hidden: the host app is foregrounded but no BTX messenger surface is presented.- background or disposed: no messenger SSE connection is retained.
Presenting the messenger or returning to the foreground performs one explicit reconciliation. A visibility change cancels the current SSE request and opens one replacement stream from the latest cursor using the matching server profile. In this first request-volume release, a clean stream rotation performs one single-flight reconciliation before the SDK waits the selected server delay and reconnects from that cursor. Hidden foreground sessions no longer run an independent five-second thread-sync poll, so Captify's current duty cycle reduces hidden full syncs from roughly 720 to 55 per app-hour. Push is the immediate attention path while the messenger is closed when the host platform is configured end to end: APNs on iOS, or Android FCM after the host binds the BTX push bridge, notification permission is granted, the Android package matches the configured client, and backend Firebase credentials are present. Without working push on the current platform, Captify's hidden SSE transport converges on the configured approximately 65-second stream-plus-reconnect cycle; that is the rollout freshness target, not an immediate-delivery guarantee.
Presented open threads temporarily retain their existing five-second catch-up watchdog. It protects visible conversation freshness until heartbeat-based stalled-stream detection ships separately; the thread list and compose screen do not start that watchdog.
Bootstrap fails explicitly if either reconnect profile is absent, malformed, or non-positive. This prevents a client-side hardcoded delay from silently overriding the backend policy.
Configuration
await Btx.configure(
BtxConfiguration(
apiBaseUrl: Uri.parse('http://localhost:3000'),
projectId: 'project_123',
publishableClientKey: 'cfk_...',
features: const <BtxFeature>{
BtxFeature.logs,
BtxFeature.messenger,
},
),
);
For apps with multiple build variants, provide the key set once and let BTX resolve the current platform/package key:
await Btx.configure(
BtxConfiguration.withPublishableClientKeys(
publishableClientKeys: const BtxPublishableClientKeys(
defaultKey: 'cfk_ios_prod',
iosBundleIds: <String, String>{
'com.example.app.beta': 'cfk_ios_beta',
},
androidPackageNames: <String, String>{
'com.example.app': 'cfk_android',
},
),
features: const <BtxFeature>{BtxFeature.logs},
),
);
Use appContext only for app-specific low-cardinality attributes or explicit
app-version/build-number overrides. Platform, OS, client-device, and BTX SDK
attributes are SDK-owned so one host cannot accidentally report stale runtime
metadata:
await Btx.configure(
BtxConfiguration(
publishableClientKey: 'cfk_...',
appContext: const BtxAppContext(
attributes: <String, String>{
'releaseRing': 'beta',
},
),
features: const <BtxFeature>{
BtxFeature.logs,
BtxFeature.messenger,
},
),
);
Use features: {BtxFeature.logs} for telemetry-only integrations such as the
current Captify use case.
Telemetry delivery preserves queued logs across runtime recreation. A rejected publishable key or app-context mismatch pauses delivery without retrying every two seconds; update the platform/package key configuration and recreate the runtime to resume delivery. Network, rate-limit, and server failures remain queued and retry with bounded exponential backoff.
Android
Android messenger UI is supported. Android push is Firebase-neutral in the core
package: host apps own Firebase initialization and dependencies, then bind plain
Dart token/open callbacks into BTX. Android push is scoped to the messenger
feature; BtxFeature.messenger and enablePushBridge=true are both required
for BTX registration and tap routing.
final messaging = FirebaseMessaging.instance;
await Btx.push.bindAndroidSource(
BtxAndroidPushSource(
firebaseProjectId: messaging.app.options.projectId,
getToken: messaging.getToken,
tokenRefreshes: messaging.onTokenRefresh,
getInitialNotification: () async {
final message = await messaging.getInitialMessage();
return message == null
? null
: BtxAndroidNotificationOpen(
data: Map<String, Object?>.from(message.data),
handleIfUnhandled: () => handleHostNotification(message),
);
},
notificationOpens: FirebaseMessaging.onMessageOpenedApp.map(
(message) => BtxAndroidNotificationOpen(
data: Map<String, Object?>.from(message.data),
handleIfUnhandled: () => handleHostNotification(message),
),
),
),
);
The btx package does not depend on FlutterFire, declare a Firebase receiver or
service, or delete/change host Firebase tokens. Android notification permission
is requested by BTX on first messenger presentation when an Android source or
token is available.
Advanced Surfaces
The package still exposes lower-level controller/runtime types for tests,
custom embedded views, and advanced host ownership. New app integrations should
prefer the Btx facade first. See doc/advanced.md for
worker-mode telemetry, direct controller ownership, and native push details.
Libraries
- btx
- Public BTX Flutter SDK.