btx 0.0.21
btx: ^0.0.21 copied to clipboard
BTX Flutter SDK for customer app telemetry, feature flags, messaging, and native integrations.
example/lib/main.dart
import 'dart:async';
import 'package:btx/btx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const BtxCustomerSdkExampleApp());
}
class BtxCustomerSdkExampleApp extends StatelessWidget {
const BtxCustomerSdkExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BTX Customer SDK Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0F172A)),
scaffoldBackgroundColor: const Color(0xFFF8FAFC),
useMaterial3: true,
),
home: const _ExampleHomeScreen(),
);
}
}
class _ExampleHomeScreen extends StatefulWidget {
const _ExampleHomeScreen();
@override
State<_ExampleHomeScreen> createState() => _ExampleHomeScreenState();
}
class _ExampleHomeScreenState extends State<_ExampleHomeScreen> {
static final _configuration = _ExampleConfiguration.fromEnvironment();
BtxCustomerSdk? _telemetrySdk;
BtxClientController? _messengerController;
BtxPushCoordinator? _pushCoordinator;
Object? _initializationError;
bool _isInitializing = true;
BtxHostIdentity get _telemetryIdentity {
return BtxHostIdentity(
externalId: _configuration.customerExternalId,
name: _configuration.customerName,
email: _configuration.customerEmail,
);
}
BtxClientConfiguration get _messengerConfiguration {
return BtxClientConfiguration.withApiBaseUrl(
apiBaseUrl: Uri.parse(_configuration.apiBaseUrl),
projectId: _configuration.projectIdOrNull,
publishableClientKey: _configuration.publishableClientKey,
customer: BtxCustomer(
externalId: _configuration.customerExternalId,
name: _configuration.customerName,
email: _configuration.customerEmail,
),
appContext: BtxAppContext(
appVersion: _configuration.appVersion,
buildNumber: _configuration.buildNumber,
attributes: <String, String>{
if (_configuration.iosBundleId != null)
'bundleId': _configuration.iosBundleId!,
if (_configuration.androidPackageName != null)
'packageName': _configuration.androidPackageName!,
},
),
theme: const BtxClientTheme(
backgroundColor: Color(0xFF05070B),
primaryTextColor: Color(0xFFF8FAFC),
secondaryTextColor: Color(0xFFCBD5E1),
emptyStateAccentColor: Color(0xFFE2E8F0),
primaryCtaColor: Color(0xFFF8FAFC),
),
);
}
@override
void initState() {
super.initState();
if (_configuration.isReady) {
unawaited(_initializeSurfaces());
} else {
_isInitializing = false;
}
}
@override
void dispose() {
final pushCoordinator = _pushCoordinator;
if (pushCoordinator != null) {
pushCoordinator.removeListener(_handlePushStateChanged);
pushCoordinator.dispose();
}
final telemetrySdk = _telemetrySdk;
if (telemetrySdk != null) {
unawaited(telemetrySdk.dispose());
}
_messengerController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final sdk = _telemetrySdk;
final controller = _messengerController;
if (!_configuration.isReady) {
return _MissingConfigurationScreen(configuration: _configuration);
}
if (_isInitializing) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (_initializationError != null) {
return _InitializationErrorScreen(error: _initializationError!);
}
if (sdk == null || controller == null) {
return const Scaffold(
body: Center(child: Text('SDK initialization did not complete.')),
);
}
final pushCoordinator = _pushCoordinator;
return BtxMessengerMount(
controller: controller,
presentation: BtxMessengerPresentation.fullScreenSheet,
sheetConfiguration: const BtxMessengerSheetConfiguration(
title: 'BTX Support',
emptyPromptTitle: 'Send us a message',
emptyPromptMessage: 'This example uses the packaged messenger UI.',
createThreadLabel: 'Open support',
),
child: Scaffold(
appBar: AppBar(
title: const Text('BTX Customer SDK Example'),
backgroundColor: Colors.transparent,
),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.all(20),
children: <Widget>[
_ActionCard(
title: 'Messenger UI',
message:
'The messenger controller stays separate from the telemetry SDK facade.',
actionLabel: 'Open support',
onPressed: () => controller.present(),
),
const SizedBox(height: 16),
_ActionCard(
title: 'Record product log',
message:
'Send a canonical structured log through the SDK facade.',
actionLabel: 'Record log',
onPressed: () => _recordProductLog(sdk),
),
const SizedBox(height: 16),
_ActionCard(
title: 'Record warning log',
message:
'Use the same log API for product events, checkpoints, and operational warnings.',
actionLabel: 'Record warning',
onPressed: () => _recordWarningLog(sdk),
),
const SizedBox(height: 16),
_ActionCard(
title: 'Record error log',
message:
'Queue an operational error with structured JSON context.',
actionLabel: 'Record error',
onPressed: () => _recordErrorLog(sdk),
),
const SizedBox(height: 16),
_ActionCard(
title: 'Telemetry identity',
message:
'The SDK now exposes imperative identity updates and structured runtime status.',
actionLabel: sdk.status.isActive
? 'Sign out telemetry'
: 'Restore telemetry',
onPressed: () => _toggleTelemetryIdentity(sdk),
),
const SizedBox(height: 16),
_InfoCard(
title: 'Config',
rows: <(String, String)>[
('API base URL', _configuration.apiBaseUrl),
(
'Publishable key',
_configuration.publishableClientKey.isEmpty
? '(empty)'
: 'set',
),
('Project ID', _configuration.projectIdOrNull ?? '(unset)'),
('Customer', _configuration.customerExternalId),
('Platform label', _platformLabel),
],
),
const SizedBox(height: 16),
ValueListenableBuilder<BtxCustomerSdkStatus>(
valueListenable: sdk.statusListenable,
builder: (
BuildContext context,
BtxCustomerSdkStatus status,
Widget? child,
) {
return _InfoCard(
title: 'Telemetry status',
rows: <(String, String)>[
('Kind', status.kind.name),
(
'Identity',
status.identity?.externalId ?? 'signed out',
),
(
'Disable reason',
status.disableReason?.code.name ?? 'none',
),
],
);
},
),
const SizedBox(height: 16),
_InfoCard(
title: 'Push status',
rows: <(String, String)>[
(
'Supported',
pushCoordinator == null
? 'initializing'
: pushCoordinator.isSupported
? 'yes'
: 'no',
),
(
'Status',
pushCoordinator?.statusLabel ?? 'Initializing push state.',
),
(
'Native bridge',
pushCoordinator?.nativePushStatusLabel ??
'Initializing push state.',
),
],
),
],
),
),
),
);
}
Future<void> _initializeSurfaces() async {
try {
final telemetrySdk = await BtxCustomerSdk.initialize(
initialization: BtxCustomerSdkInitialization.explicit(
configuration: _messengerConfiguration,
telemetry: const BtxTelemetryConfiguration(bindAppLifecycle: true),
),
);
final messengerController = BtxClientController(
configuration: _messengerConfiguration,
);
final pushCoordinator =
BtxPushCoordinator(controller: messengerController);
pushCoordinator.addListener(_handlePushStateChanged);
await pushCoordinator.ready;
if (!mounted) {
pushCoordinator.removeListener(_handlePushStateChanged);
pushCoordinator.dispose();
messengerController.dispose();
await telemetrySdk.dispose();
return;
}
setState(() {
_telemetrySdk = telemetrySdk;
_messengerController = messengerController;
_pushCoordinator = pushCoordinator;
_isInitializing = false;
});
} catch (error) {
if (!mounted) {
return;
}
setState(() {
_initializationError = error;
_isInitializing = false;
});
}
}
void _handlePushStateChanged() {
if (!mounted) {
return;
}
setState(() {});
}
Future<void> _recordProductLog(BtxCustomerSdk sdk) async {
await sdk.log(
'example.home_opened',
message: 'Example home opened.',
properties: const <String, Object?>{'screen': 'example_home'},
);
_showSnackBar('Queued product log.');
}
Future<void> _recordWarningLog(BtxCustomerSdk sdk) async {
await sdk.log(
'example.button_pressed',
level: BtxLogLevel.warning,
message: 'Example warning log recorded.',
occurredAt: DateTime.now().toUtc(),
properties: const <String, Object?>{
'source': 'example_app',
'button': 'record_warning',
},
);
_showSnackBar('Queued warning log.');
}
Future<void> _recordErrorLog(BtxCustomerSdk sdk) async {
await sdk.log(
'example.error',
level: BtxLogLevel.error,
message: 'Example error log recorded.',
occurredAt: DateTime.now().toUtc(),
properties: const <String, Object?>{
'source': 'example_app',
'code': 'EXAMPLE_ERROR',
},
);
_showSnackBar('Queued error log.');
}
Future<void> _toggleTelemetryIdentity(BtxCustomerSdk sdk) async {
if (sdk.status.isActive) {
await sdk.setIdentity(null);
_showSnackBar('Telemetry identity cleared.');
return;
}
await sdk.setIdentity(_telemetryIdentity);
_showSnackBar('Telemetry identity restored.');
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
}
class _ActionCard extends StatelessWidget {
const _ActionCard({
required this.title,
required this.message,
required this.actionLabel,
required this.onPressed,
});
final String title;
final String message;
final String actionLabel;
final Future<void> Function() onPressed;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(message),
const SizedBox(height: 16),
FilledButton(
onPressed: () => unawaited(onPressed()),
child: Text(actionLabel),
),
],
),
),
);
}
}
class _InfoCard extends StatelessWidget {
const _InfoCard({
required this.title,
required this.rows,
});
final String title;
final List<(String, String)> rows;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
for (final row in rows) ...<Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(
width: 120,
child: Text(
row.$1,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.black54,
),
),
),
Expanded(child: Text(row.$2)),
],
),
const SizedBox(height: 8),
],
],
),
),
);
}
}
class _MissingConfigurationScreen extends StatelessWidget {
const _MissingConfigurationScreen({required this.configuration});
final _ExampleConfiguration configuration;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BTX Customer SDK Example')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Missing configuration',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 12),
const Text(
'Set the required --dart-define values before running the example app.',
),
const SizedBox(height: 24),
_InfoCard(
title: 'Current values',
rows: <(String, String)>[
('API base URL', configuration.apiBaseUrl),
(
'Publishable key',
configuration.publishableClientKey.isEmpty
? '(empty)'
: 'set',
),
('Project ID', configuration.projectIdOrNull ?? '(unset)'),
('Customer', configuration.customerExternalId),
],
),
],
),
),
);
}
}
class _InitializationErrorScreen extends StatelessWidget {
const _InitializationErrorScreen({required this.error});
final Object error;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BTX Customer SDK Example')),
body: Padding(
padding: const EdgeInsets.all(24),
child: SelectableText(
'Failed to initialize example app:\n\n$error',
),
),
);
}
}
String get _platformLabel {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return 'android';
case TargetPlatform.iOS:
return 'ios';
case TargetPlatform.fuchsia:
return 'fuchsia';
case TargetPlatform.linux:
return 'linux';
case TargetPlatform.macOS:
return 'macOS';
case TargetPlatform.windows:
return 'windows';
}
}
class _ExampleConfiguration {
const _ExampleConfiguration({
required this.apiBaseUrl,
required this.projectId,
required this.publishableClientKey,
required this.customerExternalId,
required this.customerName,
required this.customerEmail,
required this.appVersion,
required this.buildNumber,
required this.iosBundleId,
required this.androidPackageName,
});
factory _ExampleConfiguration.fromEnvironment() {
return const _ExampleConfiguration(
apiBaseUrl: String.fromEnvironment(
'BTX_API_BASE_URL',
defaultValue: 'http://localhost:3000',
),
projectId: String.fromEnvironment('BTX_CUSTOMER_MESSENGER_PROJECT_ID'),
publishableClientKey: String.fromEnvironment(
'BTX_CUSTOMER_MESSENGER_PUBLISHABLE_CLIENT_KEY',
),
customerExternalId: String.fromEnvironment(
'BTX_CUSTOMER_EXTERNAL_ID',
defaultValue: 'customer_123',
),
customerName: String.fromEnvironment(
'BTX_CUSTOMER_NAME',
defaultValue: 'Taylor Example',
),
customerEmail: String.fromEnvironment(
'BTX_CUSTOMER_EMAIL',
defaultValue: 'taylor@example.com',
),
appVersion: String.fromEnvironment(
'BTX_APP_VERSION',
defaultValue: '1.0.0',
),
buildNumber: String.fromEnvironment(
'BTX_BUILD_NUMBER',
defaultValue: '1',
),
iosBundleId: String.fromEnvironment(
'BTX_IOS_BUNDLE_ID',
defaultValue: 'com.example.btxcustomer',
),
androidPackageName: String.fromEnvironment(
'BTX_ANDROID_PACKAGE_NAME',
defaultValue: 'com.example.btxcustomer',
),
);
}
final String apiBaseUrl;
final String projectId;
final String publishableClientKey;
final String customerExternalId;
final String customerName;
final String customerEmail;
final String appVersion;
final String buildNumber;
final String? iosBundleId;
final String? androidPackageName;
String? get projectIdOrNull {
final trimmed = projectId.trim();
return trimmed.isEmpty ? null : trimmed;
}
bool get isReady =>
apiBaseUrl.trim().isNotEmpty &&
publishableClientKey.trim().isNotEmpty &&
customerExternalId.trim().isNotEmpty;
}