kindly 2.0.7 copy "kindly: ^2.0.7" to clipboard
kindly: ^2.0.7 copied to clipboard

Kindly Chat SDK for Flutter - customer support chat widget for iOS and Android. Successor to the kindly_sdk package.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:kindly/kindly.dart';

void main() {
  runApp(const KindlyExampleApp());
}

class KindlyExampleApp extends StatelessWidget {
  const KindlyExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Kindly SDK Example',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const ExampleHomePage(),
    );
  }
}

/// Plugin smoke-test app — exercises every method on `KindlySDK` so we can
/// validate the bridge end-to-end on a real device. Mirrors (in spirit) the
/// iOS source's Example app: bot key dropdown, language picker, dialogue
/// trigger, theming, send-message, delegate hookup, etc.
///
/// This is the developer-side test app for the plugin itself. The
/// `flutter-sample/` repo is the host-app-side integration view.
class ExampleHomePage extends StatefulWidget {
  const ExampleHomePage({super.key});

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage> {
  // ────────────────────────────────────────────────────────────────────────
  // Test bot keys (mirrors iOS Example/Core/Constants.swift bot key list)
  // ────────────────────────────────────────────────────────────────────────
  static const Map<String, String> _botKeys = <String, String>{
    'Default (no handover)': 'aef0b638-2256-4f20-b2ac-7beaf89dedc6',
    'Handover': 'a74ed93d-2690-4d0c-92bf-7ee84578e43d',
  };

  static const List<String> _languages = <String>['en', 'no', 'sv', 'lt'];

  String _selectedBotLabel = 'Default (no handover)';
  String _selectedLanguage = 'en';

  bool _isInitialized = false;
  bool _isLoading = false;
  String _status = 'Not initialized';
  String _eventLog = '';

  final TextEditingController _dialogueIdController = TextEditingController();
  final TextEditingController _sendMessageController = TextEditingController();

  String get _selectedBotKey => _botKeys[_selectedBotLabel]!;

  void _appendLog(String line) {
    setState(() {
      final stamp = DateTime.now().toIso8601String().substring(11, 19);
      _eventLog = '[$stamp] $line\n$_eventLog';
      // Cap at 50 lines to keep the UI readable.
      if (_eventLog.split('\n').length > 50) {
        _eventLog = _eventLog.split('\n').take(50).join('\n');
      }
    });
  }

  void _showSnack(String text, {Color? color}) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(text),
        backgroundColor: color,
        duration: const Duration(seconds: 2),
      ),
    );
  }

  // ────────────────────────────────────────────────────────────────────────
  // SDK lifecycle
  // ────────────────────────────────────────────────────────────────────────

  Future<void> _initSDK() async {
    setState(() {
      _isLoading = true;
      _status = 'Initializing...';
    });

    try {
      await KindlySDK.start(
        botKey: _selectedBotKey,
        languageCode: _selectedLanguage,
        authTokenCallback: () async {
          _appendLog('AuthTokenCallback fired (returning null = anonymous)');
          return null;
        },
      );

      // Wire the delegate so we can see button-press / link / notification
      // events as they happen.
      await KindlySDK.setDelegate(
        onButtonPressed: (button, chatLog) {
          _appendLog(
            'Button pressed: ${button['label']} (id=${button['id']})',
          );
        },
        shouldHandleLink: (url) {
          _appendLog('shouldHandleLink: $url → letting SDK handle');
          return true;
        },
        shouldHandleNotification: (notification) {
          _appendLog(
            'shouldHandleNotification: ${notification['title']} → letting SDK handle',
          );
          return true;
        },
      );

      setState(() {
        _isInitialized = true;
        _status = 'SDK initialized ($_selectedBotLabel, $_selectedLanguage)';
      });
      _appendLog('start() OK');
    } catch (e) {
      setState(() => _status = 'Init failed: $e');
      _appendLog('start() ERROR: $e');
    } finally {
      setState(() => _isLoading = false);
    }
  }

  Future<void> _displayChat() async {
    if (!_isInitialized) return _showSnack('Init SDK first');
    final dialogueId = _dialogueIdController.text.trim();
    try {
      await KindlySDK.displayChat(
        triggerDialogueId: dialogueId.isEmpty ? null : dialogueId,
      );
      _appendLog('displayChat() OK${dialogueId.isEmpty ? "" : " trig=$dialogueId"}');
    } catch (e) {
      _showSnack('Display failed: $e');
      _appendLog('displayChat() ERROR: $e');
    }
  }

  Future<void> _launchChat() async {
    if (!_isInitialized) return _showSnack('Init SDK first');
    try {
      await KindlySDK.launchChat();
      _appendLog('launchChat() OK');
    } catch (e) {
      _showSnack('Launch failed: $e');
    }
  }

  Future<void> _closeChat() async {
    await KindlySDK.closeChat();
    _appendLog('closeChat() OK');
  }

  Future<void> _endChat() async {
    await KindlySDK.endChat();
    _appendLog('endChat() OK');
  }

  Future<void> _kill() async {
    await KindlySDK.kill();
    setState(() {
      _isInitialized = false;
      _status = 'SDK killed';
    });
    _appendLog('kill() OK');
  }

  Future<void> _setLanguage() async {
    if (!_isInitialized) return _showSnack('Init SDK first');
    await KindlySDK.setLanguage(_selectedLanguage);
    _appendLog('setLanguage($_selectedLanguage) OK');
  }

  Future<void> _triggerDialogue() async {
    if (!_isInitialized) return _showSnack('Init SDK first');
    final id = _dialogueIdController.text.trim();
    if (id.isEmpty) return _showSnack('Enter a dialogue id first');
    await KindlySDK.triggerDialogue(id);
    _appendLog('triggerDialogue($id) OK');
  }

  Future<void> _sendMessage() async {
    if (!_isInitialized) return _showSnack('Init SDK first');
    final text = _sendMessageController.text.trim();
    if (text.isEmpty) return _showSnack('Enter message text first');
    try {
      final result = await KindlySDK.sendMessage(text);
      _appendLog('sendMessage("$text") OK → id=${result?['id']}');
      _sendMessageController.clear();
    } catch (e) {
      _showSnack('Send failed: $e');
      _appendLog('sendMessage() ERROR: $e');
    }
  }

  Future<void> _setContext() async {
    if (!_isInitialized) return _showSnack('Init SDK first');
    await KindlySDK.setNewContext(<String, String>{
      'userId': '12345',
      'userName': 'John Doe',
      'email': 'john@example.com',
      'plan': 'premium',
    });
    _appendLog('setNewContext({...}) OK');
  }

  Future<void> _setCustomTheme() async {
    if (!_isInitialized) return _showSnack('Init SDK first');
    await KindlySDK.setCustomTheme(
      background: Colors.white,
      botMessageBackground: Colors.blue.shade50,
      botMessageText: Colors.black87,
      userMessageBackground: Colors.blue,
      userMessageText: Colors.white,
      buttonBackground: Colors.blue,
      buttonText: Colors.white,
      navBarBackground: Colors.blue.shade700,
      navBarText: Colors.white,
      inputBackground: Colors.grey.shade100,
      inputText: Colors.black87,
    );
    _appendLog('setCustomTheme() OK');
  }

  Future<void> _clearTheme() async {
    await KindlySDK.clearCustomTheme();
    _appendLog('clearCustomTheme() OK');
  }

  Future<void> _handleUrl() async {
    final id = _dialogueIdController.text.trim();
    if (id.isEmpty) return _showSnack('Enter a dialogue id first');
    final handled = await KindlySDK.handleUrl('kindly://chat/dialogue/$id');
    _appendLog('handleUrl(...) → handled=$handled');
  }

  Future<void> _toggleVerbose() async {
    await KindlySDK.setVerboseLogging(true);
    _appendLog('setVerboseLogging(true) OK');
  }

  Future<void> _checkChatDisplayed() async {
    final shown = await KindlySDK.isChatDisplayed;
    _appendLog('isChatDisplayed = $shown');
    _showSnack('isChatDisplayed = $shown');
  }

  // ────────────────────────────────────────────────────────────────────────
  // UI
  // ────────────────────────────────────────────────────────────────────────

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Kindly SDK Example'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              _statusCard(),
              const SizedBox(height: 8),
              Expanded(
                child: ListView(
                  children: <Widget>[
                    _configSection(),
                    _lifecycleSection(),
                    _chatSection(),
                    _themeSection(),
                    _miscSection(),
                    _eventLogSection(),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _statusCard() => Card(
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Row(
            children: <Widget>[
              Icon(
                _isInitialized ? Icons.check_circle : Icons.power_settings_new,
                color: _isInitialized ? Colors.green : Colors.grey,
              ),
              const SizedBox(width: 8),
              Expanded(child: Text(_status)),
              if (_isLoading)
                const SizedBox(
                  width: 18,
                  height: 18,
                  child: CircularProgressIndicator(strokeWidth: 2),
                ),
            ],
          ),
        ),
      );

  Widget _section(String title, List<Widget> children) => Padding(
        padding: const EdgeInsets.symmetric(vertical: 4),
        child: ExpansionTile(
          title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600)),
          initiallyExpanded: title == 'Lifecycle' || title == 'Configuration',
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: children,
              ),
            ),
          ],
        ),
      );

  Widget _configSection() => _section('Configuration', <Widget>[
        DropdownButtonFormField<String>(
          decoration: const InputDecoration(labelText: 'Bot key'),
          value: _selectedBotLabel,
          items: _botKeys.keys
              .map((k) => DropdownMenuItem<String>(value: k, child: Text(k)))
              .toList(),
          onChanged: _isInitialized
              ? null
              : (v) => setState(() => _selectedBotLabel = v ?? _selectedBotLabel),
        ),
        const SizedBox(height: 8),
        DropdownButtonFormField<String>(
          decoration: const InputDecoration(labelText: 'Language'),
          value: _selectedLanguage,
          items: _languages
              .map((l) => DropdownMenuItem<String>(value: l, child: Text(l)))
              .toList(),
          onChanged: (v) => setState(() => _selectedLanguage = v ?? 'en'),
        ),
        const SizedBox(height: 8),
        TextField(
          controller: _dialogueIdController,
          decoration: const InputDecoration(
            labelText: 'Dialogue ID (for trigger / handleUrl / displayChat)',
          ),
        ),
      ]);

  Widget _lifecycleSection() => _section('Lifecycle', <Widget>[
        ElevatedButton(
          onPressed: _isInitialized ? null : _initSDK,
          child: const Text('start()'),
        ),
        ElevatedButton(
          onPressed: _isInitialized ? _displayChat : null,
          child: const Text('displayChat()'),
        ),
        ElevatedButton(
          onPressed: _isInitialized ? _launchChat : null,
          child: const Text('launchChat()'),
        ),
        ElevatedButton(
          onPressed: _isInitialized ? _closeChat : null,
          child: const Text('closeChat()'),
        ),
        ElevatedButton(
          style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
          onPressed: _isInitialized ? _endChat : null,
          child: const Text('endChat()'),
        ),
        ElevatedButton(
          style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
          onPressed: _isInitialized ? _kill : null,
          child: const Text('kill()'),
        ),
        ElevatedButton(
          onPressed: _isInitialized ? _setLanguage : null,
          child: Text('setLanguage($_selectedLanguage)'),
        ),
      ]);

  Widget _chatSection() => _section('Chat / Messaging', <Widget>[
        TextField(
          controller: _sendMessageController,
          decoration: const InputDecoration(labelText: 'Message text'),
        ),
        const SizedBox(height: 8),
        ElevatedButton(
          onPressed: _isInitialized ? _sendMessage : null,
          child: const Text('sendMessage()'),
        ),
        ElevatedButton(
          onPressed: _isInitialized ? _triggerDialogue : null,
          child: const Text('triggerDialogue()'),
        ),
        ElevatedButton(
          onPressed: _setContext,
          child: const Text('setNewContext({...})'),
        ),
        ElevatedButton(
          onPressed: _handleUrl,
          child: const Text('handleUrl(kindly://chat/dialogue/...)'),
        ),
      ]);

  Widget _themeSection() => _section('Theme', <Widget>[
        ElevatedButton(
          onPressed: _isInitialized ? _setCustomTheme : null,
          child: const Text('setCustomTheme(blue)'),
        ),
        ElevatedButton(
          onPressed: _isInitialized ? _clearTheme : null,
          child: const Text('clearCustomTheme()'),
        ),
      ]);

  Widget _miscSection() => _section('State / Settings', <Widget>[
        ElevatedButton(
          onPressed: _checkChatDisplayed,
          child: const Text('isChatDisplayed?'),
        ),
        ElevatedButton(
          onPressed: _toggleVerbose,
          child: const Text('setVerboseLogging(true)'),
        ),
      ]);

  Widget _eventLogSection() => _section('Event log', <Widget>[
        ConstrainedBox(
          constraints: const BoxConstraints(maxHeight: 240),
          child: SingleChildScrollView(
            child: Container(
              width: double.infinity,
              padding: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                color: Colors.black87,
                borderRadius: BorderRadius.circular(6),
              ),
              child: Text(
                _eventLog.isEmpty ? 'No events yet.' : _eventLog,
                style: const TextStyle(
                  fontFamily: 'Menlo',
                  fontSize: 11,
                  color: Colors.greenAccent,
                ),
              ),
            ),
          ),
        ),
        TextButton(
          onPressed: () => setState(() => _eventLog = ''),
          child: const Text('Clear log'),
        ),
      ]);
}
0
likes
0
points
1.45k
downloads

Documentation

Documentation

Publisher

unverified uploader

Weekly Downloads

Kindly Chat SDK for Flutter - customer support chat widget for iOS and Android. Successor to the kindly_sdk package.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on kindly

Packages that implement kindly