easylivechat_ui 0.1.75
easylivechat_ui: ^0.1.75 copied to clipboard
Prebuilt, themeable Flutter chat UI for EasyLiveChat live customer support: launcher bubble, chat screen, pre-chat form, attachments and CSAT, driven by your widget config.
easylivechat_ui #
Drop real-time customer support into a Flutter app. Add a floating bubble, or push a full chat screen — the pre-chat form, message thread, attachments, typing indicators, CSAT survey and offline form are all built, themed from your EasyLiveChat dashboard, and translated into 13 languages.
Built on the headless easylivechat
core and re-exports it, so this is the only dependency you need.
dependencies:
easylivechat_ui: ^0.1.74
or
flutter pub add easylivechat_ui
Quick start #
Two things: boot once at startup, then show a widget. This is a complete app —
paste it into main.dart, put your own workspace slug in, and it runs.
import 'package:flutter/material.dart';
import 'package:easylivechat_ui/easylivechat_ui.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLiveChat.instance.boot(
const EasyLiveChatConfig(
apiBase: 'https://api.livechattools.com',
tenantSlug: 'your-workspace',
),
// Durable storage for the visitor id + session token. Without this the
// visitor is a stranger on every launch and their history is lost.
storage: SecurePrefsStorage(),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('My app')),
// The launcher positions itself; give it a Stack to sit in.
body: const Stack(
children: [
Center(child: Text('Your app')),
EasyLiveChatLauncher(),
],
),
),
);
}
}
tenantSlug is your workspace slug — the one in your dashboard URL
(your-workspace.livechattools.com).
That is the whole integration. Everything below is optional.
…or your own entry point #
If you would rather open the chat from a menu item or a support button, skip the launcher and push the screen:
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EasyLiveChatScreen()),
);
EasyLiveChatScreen is a normal widget — put it in a tab, a dialog, wherever
you like. Both integrations are in example/main.dart.
Platform setup #
Attachments use the camera and photo library, and voice messages use the
microphone, so iOS needs three usage strings. Add them to ios/Runner/Info.plist:
<key>NSPhotoLibraryUsageDescription</key>
<string>Attach images to your support conversation.</string>
<key>NSCameraUsageDescription</key>
<string>Take a photo to send to support.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Record a voice message for support.</string>
Without the microphone string, iOS terminates the app the moment a visitor
starts recording. On macOS add the same string, and the
com.apple.security.device.audio-input entitlement if the app is sandboxed.
If you supply your own picker via onPickAttachments, the photo and camera
strings are not required.
Android needs nothing for the default setup. Voice messages bring the
RECORD_AUDIO permission into your merged manifest (it is only requested when a
visitor taps the microphone, and the microphone only appears when your workspace
turns voice messages on). If you will never use them and don't want the
permission declared, remove it in android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.RECORD_AUDIO"
tools:node="remove" />
Supported platforms: Android, iOS, macOS, Windows, Linux. Web is not supported — the web product is the embeddable script-tag widget, which you already have in your dashboard.
What the visitor sees #
The UI follows EasyLiveChat.instance.phase, and you do not have to manage
any of it:
| Phase | Screen |
|---|---|
idle |
Nothing open |
loading |
Fetching your workspace config |
prechat |
The pre-chat form you configured (skipped if you identify) |
chat |
The conversation |
feedback |
Post-chat survey / CSAT |
offline |
Your out-of-hours notice |
Out-of-hours behaviour is a dashboard setting, not a client one — take a message, show a notice, or accept chats anyway.
Cards your agents send are drawn as cards — image, title, text and link buttons — and the quick replies you set on the greeting, like the AI assistant's Yes / No when it offers to hand over, are one-tap buttons under the message, as in the web widget. Both follow your workspace's colours and the visitor's language and direction.
Reactions. A long press on one of the team's messages opens the quick row (👍 ❤️ 😂 😮 😢 🙏) and a + for every emoji, by category, the visitor's recent ones first, with a search over the emoji's English names, as on the web; the same emoji again takes it back. Chips under a bubble show the visitor's own reaction and the team's on theirs — never an agent's name — and change live. Screen readers get a "React" action on the message instead of the long press. Reacting needs a live connection and a workspace that takes messages, as on the web.
Link previews. A link the server could preview gets a card under the bubble — the site, the title, a line of description, the picture — that opens in the visitor's browser. A picture that will not load simply goes. Your dashboard's switch turns them off.
Light and dark. The chat follows your workspace's colour scheme: light,
dark, or the device's own appearance. Dark is derived from your one light look,
exactly as the web widget does it — your brand stays on the send button, the
visitor's bubbles and the launcher; the surfaces become a dark ground and card
with every text on them at AA or better. Every view follows: thread, composer,
forms, survey, help screen, cards, notices, sheets, the launcher. A themeOverride
still wins, in either scheme. The launcher takes your dashboard's size and, when
you wrote some, the words beside its icon.
Closed, busy, queued. While you are shut, a notice sits above the thread — your own notice when you wrote one, always; otherwise ours in the visitor's language, saying the assistant can help while it covers and when a person is back. The full closed screen says the same. A visitor waiting for a person sees their place and the expected wait above the conversation, which stays readable and writable underneath; it goes the moment somebody is there.
Emails to the visitor. With your dashboard's switches on, the chat offers
what the web widget offers. "Email me this chat" sits in the header's ⋯
(EasyLiveChatMenuButton) and under the survey once a chat has ended: an inline
card that confirms the address on file in one tap, or takes another. "Get the
reply by email" appears above the thread once the visitor has written while
nobody can answer — closed, queued, nobody accepting — and you hold no address
for them; asked once per conversation, never again once answered or dismissed.
Addresses are shown masked.
Recipes #
Identify a signed-in user #
Call before opening. The pre-chat form is skipped and the agent sees who they are talking to.
EasyLiveChat.instance.identify(
name: user.name,
email: user.email,
phone: user.phone,
fields: {'plan': 'pro'}, // shown to the agent
// Identity verification: your BACKEND computes
// userHash = hex(HMAC_SHA256(identitySecret, userId)).
userId: user.id,
userHash: user.chatHash,
);
With userId the SDK also keeps one visitor per person: a different user
signing in on the device, or the chat opening with no userId after one was
set, starts a fresh visitor, so nobody resumes the previous person's
conversation. Call it on every launch before the chat opens, and
EasyLiveChat.instance.clearIdentity() on sign-out. A workspace that
enforces verification refuses a missing or wrong hash: the chat does not
start, and EasyLiveChat.instance.onError carries an error with
isIdentityNotVerified and a reason saying what to fix. See the core
package's README.
Help articles #
Turn on "Show help articles" in the dashboard and the chat screen offers
Browse help articles above the pre-chat form and the thread until the
visitor has written. It opens EasyLiveChatHelpScreen: search over your top
articles, the article itself, "Was this helpful?", and "Still need help? Start
a chat", which comes back to the chat. Give help its own entry in your app too:
if (EasyLiveChat.instance.widgetConfig.value?.helpArticlesEnabled ?? false) {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => EasyLiveChatHelpScreen(
// Optional: route "Start a chat" yourself (default: opens the chat
// screen in place of the help screen).
onStartChat: () => openMyChat(context),
// Optional: handle links yourself (default: url_launcher).
onOpenLink: (uri) => launchUrl(uri),
),
));
}
Article bodies are drawn natively by ElcHelpMarkdown — headings, lists,
bold, italic, code, links and images — with no HTML and no WebView;
only http(s)/mailto:/tel: links and http(s) images are ever live. Use it
directly if you build your own help UI on the core's searchHelp() /
getHelpArticle(). EasyLiveChatScreen(showHelpArticles: false) hides the
entry.
Show an unread badge #
ValueListenableBuilder<int>(
valueListenable: EasyLiveChat.instance.unreadCount,
builder: (_, count, __) => Badge(
isLabelVisible: count > 0,
label: Text('$count'),
child: const Icon(Icons.support_agent),
),
);
Match your app's theme #
Colours come from your dashboard so non-developers can rebrand without a release. Override when you need to match the host app:
final scheme = Theme.of(context).colorScheme;
EasyLiveChatLauncher(
themeOverride: EasyLiveChatTheme(
primary: scheme.primary,
background: scheme.surface,
surface: scheme.surfaceContainerHighest,
text: scheme.onSurface,
),
)
Layout direction is deliberately not taken from the override — it follows the resolved locale, so a colours-only override cannot accidentally force an RTL workspace back to LTR.
An override is used as given in light and dark alike. To follow your own app's
dark mode, pass the scheme you are in — primaryInk (the brand as text: links,
outlined buttons) and danger are optional, and brightness is read off the
background when you leave it out:
EasyLiveChatTheme(
primary: scheme.primary,
background: scheme.surface,
surface: scheme.surfaceContainerHighest,
text: scheme.onSurface,
primaryInk: scheme.primary,
danger: scheme.error,
brightness: scheme.brightness,
)
End the conversation #
Backing out of the screen does not end the chat — reopening resumes it with its history, which is what visitors expect. Ending is explicit:
AppBar(actions: const [EasyLiveChatEndChatButton()])
It appears only while a conversation is live, confirms first, then shows the
post-chat survey in place. If your app bar takes an icon and a callback
instead, call EasyLiveChatEndChatButton.confirmAndEnd(context) — it no-ops
when there is nothing to end.
Email me this chat #
The ⋯ menu holds "Email me this chat" and draws nothing while it has nothing in it, so it can sit in your app bar permanently, beside the end-chat button:
AppBar(actions: const [EasyLiveChatMenuButton(), EasyLiveChatEndChatButton()])
EasyLiveChatScreen(showAppBar: true) carries both already. For an app bar that
takes a callback, EasyLiveChatMenuButton.emailTranscript() opens the card
(false, doing nothing, when it is not on offer). The card opens above the
thread; the chat that just ended gets the same offer under the survey without
any of this.
Open the chat from a "Reply in the chat" email #
The workspace's emails link back into the conversation. When that link opens your app — a universal link / app link on your site's domain — pass it on and show the chat:
if (await EasyLiveChat.instance.resumeFromLink(uri.toString())) {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EasyLiveChatScreen()),
);
}
Call identify() first, as you would before opening the chat: a signed-in user
is never moved into another visitor's conversation. See the core package's
README.
Notifications #
EasyLiveChat does not push to your users and needs no Firebase key from you.
Your server sends them, on the message.created webhook: for a reply from the
team or the assistant whose contact.userId is set — the userId you passed
to identify() — notify that user's devices. setPushToken() is a deprecated
no-op. The core package's README has the recipe.
Language #
Chrome (buttons, labels, errors) ships in en, ar, ckb (Sorani), kmr (Badini), de, es, fr, hi, it, pt, tr, ur, zh, and follows the device locale. Force one, or add your own:
const EasyLiveChatLauncher(locale: 'ar'); // force one
// Add a language, or reword an existing one, without waiting on a release.
ElcStrings.overrideByLocale({
'sv': {'send': 'Skicka'},
});
Your own copy — greeting, welcome text, survey questions — is authored per language in the dashboard and is never machine-translated by the SDK. RTL follows the locale automatically.
Your own attachment picker #
EasyLiveChatScreen(
onPickAttachments: () async => [
ElcPickedFile(filename: 'screenshot.png', bytes: bytes),
],
)
API #
Every widget takes themeOverride, directionOverride, onPickAttachments,
strings and locale.
| Widget | Purpose |
|---|---|
EasyLiveChatLauncher |
Floating bubble with unread badge. alignment: to move it, useBottomSheet: true to open as a sheet |
EasyLiveChatScreen |
The full chat screen, for your own navigation |
EasyLiveChatEndChatButton |
Explicit end-chat action for an app bar |
EasyLiveChatMenuButton |
The header's ⋯ — "Email me this chat" — for an app bar; drawn only when it has something in it |
EasyLiveChatHelpScreen |
Help articles: search, reader, "Was this helpful?", start a chat. onStartChat:, onOpenLink:, initialArticleId: |
ElcHelpMarkdown |
Renders a help article's markdown body natively (no HTML) |
ElcLinkPreviewCard |
A message's link preview card, for your own thread |
ElcReactionChips, ElcQuickReactions, showElcEmojiPicker |
Reaction chips, the quick row and the whole picker, for your own thread |
SecurePrefsStorage |
Durable storage default (secure storage + shared prefs) |
The core's API — identify, clearIdentity, open, messages,
unreadCount, phase, sendMessage, searchHelp, getHelpArticle — is
re-exported here and documented in
easylivechat.
Troubleshooting #
The visitor is a stranger on every launch. You booted without
storage: SecurePrefsStorage(), so the default in-memory store is used and the
visitor id does not survive a restart.
The launcher is invisible. It needs a Stack (or another widget that
allows overlap) as its parent. In a Column it has nowhere to position itself.
"boot() must be called before use". A widget was built before boot()
finished. Await it in main() before runApp, as above.
Nothing happens when opening. Check apiBase and tenantSlug against your
dashboard, and listen to EasyLiveChat.instance.onError — configuration
problems surface there rather than throwing.
Attachments do nothing on iOS. The two Info.plist usage strings are
missing; iOS silently denies the picker without them.
Want a different UI? #
Use easylivechat directly. It is the
same protocol and state machine with no widgets — bind the ValueListenables
to your own design system.
Server #
Talks to an EasyLiveChat workspace. Create one at livechattools.com.