termii_resolve_livechat 0.2.1 copy "termii_resolve_livechat: ^0.2.1" to clipboard
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.')),
    );
  }
}
3
likes
160
points
228
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Homepage
Repository (GitHub)

License

MIT (license)

Dependencies

flutter, webview_flutter, webview_flutter_android, webview_flutter_wkwebview

More

Packages that depend on termii_resolve_livechat