kindly 3.0.4
kindly: ^3.0.4 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,
market: 'general',
authTokenCallback: (chatId) async {
_appendLog('AuthTokenCallback for chatId=$chatId (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'),
),
]);
}