btx 0.0.21 copy "btx: ^0.0.21" to clipboard
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;
}
0
likes
150
points
552
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

BTX Flutter SDK for customer app telemetry, feature flags, messaging, and native integrations.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

device_info_plus, flutter, flutter_linkify, http, image, image_picker, package_info_plus, path, path_provider, shared_preferences, url_launcher, uuid

More

Packages that depend on btx

Packages that implement btx