btx 0.0.24 copy "btx: ^0.0.24" to clipboard
btx: ^0.0.24 copied to clipboard

BTX Flutter SDK for customer app telemetry, feature flags, messaging, knowledge base, 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
110
points
442
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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

Repository (GitHub)
View/report issues

License

(pending) (license)

Dependencies

chewie, device_info_plus, flutter, flutter_linkify, html, http, image, image_picker, package_info_plus, path, path_provider, shadcn_ui, shared_preferences, url_launcher, uuid, video_player, webview_flutter

More

Packages that depend on btx

Packages that implement btx