termii_resolve_livechat 0.2.1
termii_resolve_livechat: ^0.2.1 copied to clipboard
Termii Resolve Live Chat SDK for Flutter. Add Resolve in-app customer support to any Flutter app in a few lines, with user identification and unread-count events.
example/lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:termii_resolve_livechat/termii_resolve_livechat.dart';
// Replace with your widget ID from the Termii dashboard, or run with
// --dart-define=RESOLVE_WIDGET_ID=...
const String widgetId = String.fromEnvironment('RESOLVE_WIDGET_ID');
const ResolveUser demoUser = ResolveUser(
email: 'jane@example.com',
name: 'peace Doe',
);
/// Lets the notification tap open the chat from outside the widget tree.
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final FlutterLocalNotificationsPlugin notifications =
FlutterLocalNotificationsPlugin();
const AndroidNotificationChannel _channel = AndroidNotificationChannel(
'termii_chat',
'Support replies',
description: 'Notifies you when support replies while the chat is closed',
importance: Importance.high,
);
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await _initNotifications();
// Warm the chat so the first open is instant, and — because a preloaded
// session is kept hidden — so replies arriving before the user ever opens it
// accrue as unread and reach [onUnreadCountChanged].
//
// This callback is the one that fires while the chat is CLOSED. The
// equivalent argument to ResolveLiveChat.open is wired to the chat screen, so
// it stops firing the moment that screen is dismissed.
ResolveLiveChat.preload(
widgetId: widgetId,
user: demoUser,
onUnreadCountChanged: _onUnreadCountChanged,
);
runApp(const DemoApp());
}
/// Registers the plugin and the Android channel. Deliberately does NOT ask for
/// permission: that shows a system modal, and awaiting it before `runApp`
/// blocks startup behind a dialog on a blank screen.
Future<void> _initNotifications() async {
await notifications.initialize(
const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
// requestAlertPermission etc. default to true and would prompt on
// initialize; ask explicitly later instead.
iOS: DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
),
),
onDidReceiveNotificationResponse: (_) => _openChatFromNotification(),
);
await notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.createNotificationChannel(_channel);
}
/// Asks for notification permission. Called when the user first opens the chat
/// — the point where "tell me when support replies" actually makes sense, and
/// where a prompt is not blocking anything.
Future<void> requestNotificationPermission() async {
await notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.requestNotificationsPermission();
await notifications
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true);
}
/// Last count we notified for, so re-emitted or decreasing counts stay quiet.
int _lastUnread = 0;
/// Whether a move from [previous] to [next] unread means something new arrived.
///
/// The page re-emits the count on reconnect and drops it to zero when the user
/// reads the thread; only a rise is a new message.
bool shouldAlertForUnread(int previous, int next) => next > previous;
void _onUnreadCountChanged(int count) {
unreadCount.value = count;
if (shouldAlertForUnread(_lastUnread, count)) showUnreadNotification(count);
_lastUnread = count;
}
/// Posts the banner + sound. The chat page plays its own chime, but only while
/// its WebView is on screen — once the chat screen is gone the WebView is
/// detached and iOS will not play its audio, so the app has to speak up.
Future<void> showUnreadNotification(int count) async {
await notifications.show(
0,
'New message',
count == 1 ? 'You have a new reply' : 'You have $count unread replies',
NotificationDetails(
android: AndroidNotificationDetails(
_channel.id,
_channel.name,
channelDescription: _channel.description,
importance: Importance.high,
priority: Priority.high,
),
// presentAlert is what makes iOS show this while the app is foregrounded;
// without it the banner is suppressed and only the sound would play.
iOS: const DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
);
}
/// Mirrors the unread count into the badge without rebuilding the whole app.
final ValueNotifier<int> unreadCount = ValueNotifier<int>(0);
void _openChatFromNotification() {
final context = navigatorKey.currentContext;
if (context != null) openChat(context);
}
Future<void> openChat(BuildContext context) async {
if (widgetId.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'No widget ID. Run with --dart-define=RESOLVE_WIDGET_ID=...',
),
),
);
return;
}
final messenger = ScaffoldMessenger.of(context);
// Opening the chat is the user acknowledging the replies.
await notifications.cancelAll();
await requestNotificationPermission();
if (!context.mounted) return;
await ResolveLiveChat.open(
context,
widgetId: widgetId,
user: demoUser,
onError: (event) => messenger.showSnackBar(
SnackBar(content: Text('Chat error: ${event.errorMessage}')),
),
);
}
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Termii Live Chat Demo',
navigatorKey: navigatorKey,
theme: ThemeData(colorSchemeSeed: const Color(0xFF1F3D2B)),
// Parks warm chat sessions invisibly while no chat screen shows them,
// so iOS keeps their JavaScript (and unread events) running. Without
// it, a detached WebView's socket events can stall until reopened.
builder: (context, child) =>
ResolveSessionHost(child: child ?? const SizedBox.shrink()),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Termii Live Chat Demo'),
actions: [
// Debug builds only: fires the same notification an incoming reply
// would, so the banner and sound can be checked without waiting on
// an agent.
if (kDebugMode)
IconButton(
tooltip: 'Test notification',
icon: const Icon(Icons.notifications_active_outlined),
onPressed: () async {
await requestNotificationPermission();
await showUnreadNotification(1);
},
),
IconButton(
tooltip: 'Sign out (clears the chat session)',
icon: const Icon(Icons.logout),
onPressed: () {
ResolveLiveChat.logout();
unreadCount.value = 0;
_lastUnread = 0;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chat session cleared')),
);
},
),
],
),
floatingActionButton: ValueListenableBuilder<int>(
valueListenable: unreadCount,
builder: (context, count, _) => Badge(
isLabelVisible: count > 0,
label: Text('$count'),
child: FloatingActionButton.extended(
onPressed: () => openChat(context),
icon: const Icon(Icons.chat_bubble_outline),
label: const Text('Support'),
),
),
),
body: const Center(child: Text('Tap "Support" to open the chat.')),
);
}
}