ebchat 5.1.0 copy "ebchat: ^5.1.0" to clipboard
ebchat: ^5.1.0 copied to clipboard

Embeddable ebchat chat SDK for Flutter apps. Drop-in chat screen and widgets on the first-party livechat protocol, with host-owned push ("notification outsourcing"), botflow forms and multi-company support.

example/lib/main.dart

import 'package:ebchat/ebchat.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'form_pickers.dart';
import 'prefs_session_store.dart';

/// Standalone host app for the ebchat mobile SDK — a manual test harness.
///
/// Two ways to point it at a gateway:
///
///  • Bake it in at build time:
///      flutter run --dart-define=EBCHAT_BASE_URL=https://api.enable.tech \
///        --dart-define=EBCHAT_API_KEY=eb_... --dart-define=EBCHAT_PARTNER=your-app
///
///  • Or just run it and paste the base URL + API key + partner id on the
///    connect screen. The values (and the guest session) are persisted, so you
///    enter them once.
///
/// Get a LIVECHAT-scoped API token + its partner id from the ebchat dashboard
/// (or locally with `make livechat-info`). Note: a real device can't resolve
/// the dev proxy's `.localhost` subdomains — use a LAN-reachable or deployed
/// gateway URL when testing off-machine.
const _defBaseUrl = String.fromEnvironment(
  'EBCHAT_BASE_URL',
  defaultValue: 'http://be-ebchat-gateway.localhost:3004',
);
const _defApiKey = String.fromEnvironment('EBCHAT_API_KEY');
const _defPartner = String.fromEnvironment('EBCHAT_PARTNER');
const _defLanguage = String.fromEnvironment('EBCHAT_LANGUAGE', defaultValue: 'en');
const _defFlowTrigger = String.fromEnvironment('EBCHAT_FLOW_TRIGGER');

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final prefs = await SharedPreferences.getInstance();
  runApp(EbchatExampleApp(prefs: prefs));
}

class EbchatExampleApp extends StatelessWidget {
  final SharedPreferences prefs;
  const EbchatExampleApp({super.key, required this.prefs});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ebchat example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF214496),
        useMaterial3: true,
      ),
      home: ConnectScreen(prefs: prefs),
    );
  }
}

/// Enter/confirm the connection, then open the drop-in [EbChatScreen].
class ConnectScreen extends StatefulWidget {
  final SharedPreferences prefs;
  const ConnectScreen({super.key, required this.prefs});

  @override
  State<ConnectScreen> createState() => _ConnectScreenState();
}

class _ConnectScreenState extends State<ConnectScreen> {
  late final TextEditingController _url;
  late final TextEditingController _key;
  late final TextEditingController _partner;
  late final TextEditingController _flow;
  late String _language;

  bool _connecting = false;
  String? _error;

  SharedPreferences get _prefs => widget.prefs;

  @override
  void initState() {
    super.initState();
    // Persisted value first, else the --dart-define default.
    _url = TextEditingController(text: _prefs.getString('cfg_base_url') ?? _defBaseUrl);
    _key = TextEditingController(text: _prefs.getString('cfg_api_key') ?? _defApiKey);
    _partner = TextEditingController(text: _prefs.getString('cfg_partner') ?? _defPartner);
    _flow = TextEditingController(text: _prefs.getString('cfg_flow') ?? _defFlowTrigger);
    _language = _prefs.getString('cfg_language') ?? _defLanguage;
  }

  @override
  void dispose() {
    _url.dispose();
    _key.dispose();
    _partner.dispose();
    _flow.dispose();
    super.dispose();
  }

  /// Namespace the persisted session by account, so switching the app between
  /// keys never resumes the wrong guest.
  String _scope(String partner, String apiKey) {
    final tail = apiKey.length > 6 ? apiKey.substring(apiKey.length - 6) : apiKey;
    return '${partner}_$tail'.replaceAll(RegExp(r'[^A-Za-z0-9_]'), '');
  }

  Future<void> _connect() async {
    final baseUrl = _url.text.trim();
    final apiKey = _key.text.trim();
    final partner = _partner.text.trim();
    final flow = _flow.text.trim();
    final uri = Uri.tryParse(baseUrl);

    if (uri == null || !uri.hasScheme || !uri.hasAuthority) {
      setState(() => _error = 'Enter a full base URL, e.g. https://api.enable.tech');
      return;
    }
    if (apiKey.isEmpty || partner.isEmpty) {
      setState(() => _error = 'API key and partner id are both required.');
      return;
    }

    // Remember the config for next launch.
    await _prefs.setString('cfg_base_url', baseUrl);
    await _prefs.setString('cfg_api_key', apiKey);
    await _prefs.setString('cfg_partner', partner);
    await _prefs.setString('cfg_flow', flow);
    await _prefs.setString('cfg_language', _language);

    setState(() {
      _connecting = true;
      _error = null;
    });

    final store = PrefsSessionStore(_prefs, scope: _scope(partner, apiKey));
    try {
      final chat = await EbChat.init(
        baseUrl: uri,
        apiKey: apiKey,
        partnerId: partner,
        user: const EbChatUser.guest(),
        language: _language,
        sessionStore: store,
      );
      if (!mounted) return;
      await Navigator.of(context).push(MaterialPageRoute(
        builder: (_) => ChatScreen(
          chat: chat,
          store: store,
          flowTrigger: flow.isEmpty ? null : flow,
        ),
      ));
    } catch (e) {
      if (mounted) setState(() => _error = '$e');
    } finally {
      if (mounted) setState(() => _connecting = false);
    }
  }

  Future<void> _resetSavedSession() async {
    await PrefsSessionStore(_prefs, scope: _scope(_partner.text.trim(), _key.text.trim()))
        .clear();
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
            content: Text('Saved guest session cleared — next connect starts fresh.')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ebchat SDK — test harness')),
      body: AbsorbPointer(
        absorbing: _connecting,
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            Text(
              'Point this at a gateway with a LIVECHAT-scoped API token + its '
              'partner id, then open the embedded chat.',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 16),
            _field(_url, 'Base URL',
                hint: 'https://api.enable.tech', keyboard: TextInputType.url),
            const SizedBox(height: 12),
            _field(_key, 'API key (eb_…)', hint: 'eb_live_…'),
            const SizedBox(height: 12),
            _field(_partner, 'Partner id', hint: 'your-app'),
            const SizedBox(height: 12),
            _field(_flow, 'Flow trigger (optional)',
                hint: 'deep-link a specific botflow'),
            const SizedBox(height: 16),
            Align(
              alignment: AlignmentDirectional.centerStart,
              child: Text('Language', style: Theme.of(context).textTheme.labelLarge),
            ),
            const SizedBox(height: 8),
            SegmentedButton<String>(
              segments: const [
                ButtonSegment(value: 'en', label: Text('English')),
                ButtonSegment(value: 'ar', label: Text('العربية')),
              ],
              selected: {_language},
              onSelectionChanged: (s) => setState(() => _language = s.first),
            ),
            if (_error != null) ...[
              const SizedBox(height: 16),
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.errorContainer,
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Text(
                  _error!,
                  style:
                      TextStyle(color: Theme.of(context).colorScheme.onErrorContainer),
                ),
              ),
            ],
            const SizedBox(height: 24),
            FilledButton.icon(
              onPressed: _connecting ? null : _connect,
              icon: _connecting
                  ? const SizedBox(
                      width: 18,
                      height: 18,
                      child: CircularProgressIndicator(strokeWidth: 2))
                  : const Icon(Icons.chat_bubble_outline),
              label: Text(_connecting ? 'Connecting…' : 'Open chat'),
            ),
            const SizedBox(height: 8),
            TextButton(
              onPressed: _connecting ? null : _resetSavedSession,
              child: const Text('Reset saved session'),
            ),
          ],
        ),
      ),
    );
  }

  Widget _field(TextEditingController c, String label,
      {String? hint, TextInputType? keyboard}) {
    return TextField(
      controller: c,
      keyboardType: keyboard,
      autocorrect: false,
      enableSuggestions: false,
      decoration: InputDecoration(
        labelText: label,
        hintText: hint,
        border: const OutlineInputBorder(),
      ),
    );
  }
}

/// Hosts the drop-in [EbChatScreen] under the app's own AppBar.
class ChatScreen extends StatelessWidget {
  final EbChat chat;
  final SessionStore store;
  final String? flowTrigger;

  const ChatScreen({
    super.key,
    required this.chat,
    required this.store,
    this.flowTrigger,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Support'),
        actions: [
          IconButton(
            tooltip: 'Reset guest session',
            icon: const Icon(Icons.refresh),
            onPressed: () async {
              await store.clear();
              if (context.mounted) Navigator.of(context).pop();
            },
          ),
        ],
      ),
      // Botflow form questions are answered by the SDK; it only needs the two
      // platform hooks (see form_pickers.dart). Voice notes stay omitted, so
      // the composer shows text + send — wire onVoiceStart/Stop/Cancel to add
      // the mic.
      body: EbChatScreen(
        chat: chat,
        flowTrigger: flowTrigger,
        onPickFile: (request) => pickFormFile(context, request),
        onResolveLocation: resolveFormLocation,
      ),
    );
  }
}
2
likes
140
points
191
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Embeddable ebchat chat SDK for Flutter apps. Drop-in chat screen and widgets on the first-party livechat protocol, with host-owned push ("notification outsourcing"), botflow forms and multi-company support.

Homepage
Repository (GitHub)
View/report issues

Topics

#chat #messaging #support #livechat

License

MIT (license)

Dependencies

ebchat_core, flutter, http, intl, web

More

Packages that depend on ebchat