Skip to main content

Flutter SDK hatang_chat

Version

hatang_chat 0.6.1 · protocol v1 · Flutter 3.32+ / Dart 3.8+ · iOS 15+ · Android 6.0 (API 23)+ · web (including WebAssembly) · pub.dev · Changelog · API reference (dartdoc)

note

Version 0.5 has the chat screen and floating launcher, identity, events, push notifications, deep links, photo and file attachments, product, order and payment cards with in-app purchase hooks, the visitor's conversation inbox, and a chat screen that follows the site's widget settings. The API may still change before 1.0.

hatang_chat is a native chat screen for Flutter apps. It talks the realtime protocol directly to the Ha Tang gateway (no web view) and ships a chat screen, a floating launcher, an inbox of past conversations, push notifications through your own Firebase project, and deep links. Customers who write from your app land in the same inbox as visitors from your website, Zalo, Messenger, Instagram and TikTok Shop.

Chat screen with a product card and an order cardChat screen in dark modeThe visitor's conversation listIn-app chat button with an unread badge

Installation​

Try it without an account​

The example app that ships with the package talks to Ha Tang's public demo inbox: an AI assistant answers questions about Ha Tang, and messages are deleted after 24 hours.

dart pub unpack hatang_chat
cd hatang_chat-*/example
flutter run

With the demo, the example app receives real push notifications on Android and the iOS simulator with no setup: ask a question, send the app to the background, and the assistant's answer arrives as a notification. The Push tab shows the token, lets you paste one, and simulates tapping a notification.

Pass --dart-define=HATANG_SITE_ID=… --dart-define=HATANG_APP_KEY=… to point the example app at your own support site. Add --dart-define=FIREBASE_API_KEY=… FIREBASE_APP_ID=… FIREBASE_PROJECT_ID=… FIREBASE_SENDER_ID=… (from your app's Firebase config file) to try push with your own Firebase project.

Get an app key from the dashboard​

The SDK needs two values: the Site ID and an app key (hak_…). The key replaces the browser's Origin check: the app sends it on start, so the gateway knows which app the session comes from.

  1. In the dashboard, open Support → Channels → Add channel → Mobile app.
  2. Click Create app key: give it a name (for example "Acme iOS"), pick the Flutter platform, and optionally add bundle ids.
  3. The key is shown once. Pass it to HatangChat.configure(appKey:).

The key is public like the site id (it ships inside your build) but it can be revoked. To rotate it safely, create a new key, ship an update that uses it, then revoke the old one. Sessions that use a revoked key are cut off within a minute.

Add the package​

flutter pub add hatang_chat
pubspec.yaml
dependencies:
hatang_chat: ^0.6.1

Platform permissions​

The composer has an attach button: camera (phones), photo library and files (up to 10 MB). iPhone HEIC photos are converted to JPEG and large photos are scaled down to 2048 px before sending. The message shows upload progress and can be tapped to retry on failure. Received images open full screen with pinch and double-tap zoom.

The Files entry only appears when you pass a file picker to configure: pickFile: hatangFilePicker (import package:hatang_chat/file_picker.dart, which uses file_picker 13), or your own Future<UploadSource?> Function() if your app pins a different file_picker version. To send a file from your own UI, call controller.attach(UploadSource(bytes: …, name: 'quote.pdf', mimeType: 'application/pdf')).

Photo and camera access needs a usage description on iOS. Android needs no extra permission with the system pickers.

ios/Runner/Info.plist
<key>NSPhotoLibraryUsageDescription</key>
<string>Send photos to our support team</string>
<key>NSCameraUsageDescription</key>
<string>Take photos to send to our support team</string>

The package runs on iOS, Android, macOS, Windows, Linux and the web. The session token is kept in the keychain / keystore (on Linux it falls back to shared preferences).

Initialization​

Call HatangChat.configure once in main() before runApp. The SDK bootstraps the visitor, stores the token and opens the socket right away (autoConnect: true).

lib/main.dart
import 'package:flutter/material.dart';
import 'package:hatang_chat/hatang_chat.dart';

final navigatorKey = GlobalKey<NavigatorState>();

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await HatangChat.configure(
siteId: 'site_…',
appKey: 'hak_…',
// baseUrl: Uri.parse('https://chat.hatang.dev'), // default
locale: HatangLocale.en,
navigatorKey: navigatorKey, // lets the SDK open the chat from a push notification
);
runApp(MaterialApp(navigatorKey: navigatorKey, home: const HomePage()));
}
ParameterMeaning
siteId, appKeyRequired. From the website's Apps tab
baseUrlThe gateway. Default https://chat.hatang.dev; change it only for a local gateway (see Troubleshooting)
localeHatangLocale.vi / .en. When unset: the visitor's stored choice, then the site default, then the device
themeMode, theme, darkThemeSee Theming
stringsPer-locale string overrides, see Languages
navigatorKeyLets handleNotification and proactive messages open the screen when you pass no context
autoConnectfalse to call HatangChat.instance.connect() yourself later (for example after a consent screen)
openOnProactiveMessageOpen the chat when the team starts a conversation (default true)
commerceHow "Buy" and "Pay" on cards behave, see Sales cards and in-app purchase
pickFileEnables the Files attachment entry, see Platform permissions
logger, appInfoLogging, and overrides for the app name and version sent to the gateway

HatangChat.instance.session exposes the current visitorId, conversationId, locale, isIdentified, email and name.

Identify visitors​

When a user signs in, call identify with their id in your system. hash is the HMAC-SHA256 of userId with the site's identity secret, computed on your backend and returned to the app. Never put the secret in the app. A signed identity is merged with earlier sessions of the same person (including on the web); an unsigned one is only stored as self-reported details. See Identity verification.

// Your backend returns {userId, hash} after sign-in
await HatangChat.instance.identify(HatangUser(
userId: me.id,
email: me.email,
name: me.name,
phone: me.phone,
hash: me.hatangHash,
data: {'plan': 'pro', 'orders': 12},
));

One line on your backend computes hash:

Node
const hash = require("node:crypto").createHmac("sha256", process.env.HATANG_IDENTITY_SECRET).update(userId).digest("hex");
PHP
$hash = hash_hmac('sha256', $userId, getenv('HATANG_IDENTITY_SECRET'));

When identities merge, the SDK receives a new token, reloads the conversation and emits HatangSessionChanged(reason: identityMerged). You do not need to do anything else. On sign-out, call HatangChat.instance.reset() so the next session on the same device is a new visitor.

Open and embed the chat​

Open full screen​

ElevatedButton(
onPressed: () => HatangChat.open(context),
child: const Text('Chat with us'),
)

open pushes a MaterialPageRoute(fullscreenDialog: true) (a Cupertino transition on iOS). Pass conversationId: to open an earlier conversation, and call HatangChat.close() to close it. Apps on go_router can declare their own route that renders HatangChatScreen:

GoRoute(path: '/support', builder: (_, __) => const HatangChatScreen(showClose: false)),

Conversation inbox​

HatangChat.openInbox(context) opens the visitor's list of conversations (who answered, last message, time, unread count, resolved). Pull down to refresh, or place HatangChatInbox() in one of your tabs. HatangChat.open(context, conversationId: id) opens that conversation; scroll up to load older messages. Writing in a resolved conversation reopens it; a closed one is read-only, with a "New message" button.

Floating launcher​

HatangChatLauncher is a chat bubble with an unread badge. It hides itself when the site is set to hide while offline. Its icon, image and label follow the Launcher button settings of the site, like the web widget; with a label it becomes a wide button such as "Support". Pass child to use your own icon instead. Put it in your home screen's Stack:

Stack(children: [
const HomeBody(),
HatangChatLauncher(alignment: Alignment.bottomRight, margin: const EdgeInsets.all(16)),
])

Embed in your own screen​

HatangConversationView is the conversation body (message list, composer, attachments) without an app bar, to place under your tab bar or in a "Support" tab. It needs a HatangChatController:

class SupportTab extends StatefulWidget {
const SupportTab({super.key});

State<SupportTab> createState() => _SupportTabState();
}

class _SupportTabState extends State<SupportTab> {
final controller = HatangChatController();


void dispose() { controller.dispose(); super.dispose(); }


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Support')),
body: HatangConversationView(controller: controller, bottomPadding: 56),
);
}
}

Only mount the view while it is visible (do not keep it in an IndexedStack that holds hidden tabs), or call controller.setVisible(false) when the tab is hidden: a visible chat marks messages as read. HatangChat.instance.setBottomPadding(56) lifts the launcher and the chat screen above your tab bar or bottom sheet; open screens update at once.

The controller exposes state, send(text), retry(clientId), pickAndSendImage(), pickAndSendFile(), loadEarlier(), rate(score, comment: …) and setVisible(bool) (call it when the tab shows or hides so unread counts and read receipts stay right). HatangChatInbox lists earlier conversations (resolved ones read-only) and takes onOpenConversation.

HatangChat.instance.showMessage(text) inserts a local bot bubble (a contextual greeting, for example) that is never sent to the server.

Sales cards and in-app purchase​

Agents send product cards, order cards, payment requests, quick replies and star ratings from the inbox; the SDK renders them under the message. By default Buy writes "I'd like to buy …" in the composer, and a payment card shows a VietQR code, the transfer note and a Pay button that opens the checkout page. That is meant for physical goods and real-world services: the App Store (Guideline 3.1.3(e)) and Google Play do not allow them through in-app purchase, and in-app purchase cannot charge an arbitrary order total anyway.

The App Store and Google Play require digital goods and features used inside the app to be sold through in-app purchase (App Store Review Guideline 3.1.1, Google Play's payments policy). For those products, hand the purchase to your store integration and hide external checkout:

await HatangChat.configure(
siteId: 'site_…',
appKey: 'hak_…',
commerce: HatangCommerce(
showExternalCheckout: !Platform.isIOS, // iOS: no QR, no transfer note, no web checkout
onBuyProduct: (context, product) async {
// The App Store / Google Play id set for each product in the dashboard
final storeId = Platform.isIOS ? product.appStoreProductId : product.googlePlayProductId;
if (storeId == null) return false; // not an in-app product: keep the default behavior
await myIapService.buy(storeId); // for example package:in_app_purchase
return true;
},
),
);

Set the store product ids in the dashboard: Products → open a product → Prices on the App Store and Google Play (App Store product ID and Google Play product ID). Product cards carry both ids. Return true from a hook when you handled it. With showExternalCheckout off and no onPay, a payment card only shows the amount and a line saying the team will tell the customer how to complete the payment. The SDK depends on no store library; HatangConversationView(commerce: …) overrides the global policy for one embedded screen.

The whole flow, including store setup, reporting renewals and restores, importing existing subscribers and testing, is in In-app purchases and subscriptions.

Subscriptions. Inside your app the card shows the store price you set for that platform (App Store on iOS, Google Play on Android); subscription cards also carry billingPeriod, trialDays, and for Google Play basePlanId and offerId. Pass HatangChat.instance.purchaseToken(userId) as applicationUserName in in_app_purchase's PurchaseParam (it becomes StoreKit's appAccountToken and Play's obfuscated account id), and after a successful purchase call HatangChat.instance.reportPurchase(platform: Platform.isIOS ? 'ios' : 'android', storeProductId: …, transactionId: …) with the store's proof: signedTransaction: purchase.verificationData.serverVerificationData on iOS (StoreKit 2 enabled in in_app_purchase_storekit), or purchaseToken: purchase.verificationData.serverVerificationData, productType: 'subs' ('inapp' for one-time products) on Android. Hatang checks it with Apple or Google before recording it on the customer, and acknowledges Google Play purchases. Then connect the app under Channels → your app → Subscriptions from the App Store and Google Play.

Events and data​

final sub = HatangChat.instance.events.listen((e) {
switch (e) {
case HatangReady(:final visitorId): debugPrint('ready $visitorId');
case HatangMessageReceived(:final message): showInAppBanner(message.content);
case HatangConversion(:final kind, :final data): analytics.log(kind.name, data);
case HatangProtocolOutdated(): promptUpdate();
case _: break;
}
});
EventWhen
HatangReadyBootstrap and authentication finished
HatangOpened, HatangClosedThe chat screen appears or goes away
HatangMessageReceived, HatangMessageSentA message from the team or bot; a visitor message was stored (including from another device)
HatangConversationStatusopen, pending, resolved, closed
HatangConversionchatStarted, lead, purchase (with value, currency, transaction_id)
HatangSessionChangedidentityMerged, reset, restored, tokenRejected
HatangProtocolOutdatedThe gateway no longer serves this version; ask the user to update the app

Other streams: unreadCount (and unreadCountListenable for badges), availability (team online, within business hours, next opening time) and connectionState (disconnected, connecting, connected, outdated).

ValueListenableBuilder<int>(
valueListenable: HatangChat.instance.unreadCountListenable,
builder: (_, n, __) => Badge(label: Text('$n'), isLabelVisible: n > 0, child: const Icon(Icons.chat_bubble_outline)),
)

Send data to the inbox:

await HatangChat.instance.setData({'cart_total': 450000, 'last_order': 'SO-1001'}); // shown to agents, up to 8 KB
await HatangChat.instance.pushEvent('viewed_product', {'sku': 'TS-100'}); // the visitor's timeline
HatangChat.instance.setScreen('ProductPage', params: {'sku': 'TS-100'}); // agents see where the visitor is

The SDK sends app.state when the app goes to the background and comes back, so a backgrounded visitor counts as offline and replies go out as push notifications.

Push notifications​

Ha Tang sends push through your own Firebase project (FCM HTTP v1): upload the service account file on the website's Apps tab (see Push notifications). The SDK does not depend on Firebase; your app uses firebase_messaging and hands the token to the SDK.

flutter pub add firebase_core firebase_messaging
lib/push.dart
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:hatang_chat/hatang_chat.dart';

Future<void> wirePush() async {
final messaging = FirebaseMessaging.instance;
await messaging.requestPermission();

// Register the device; the SDK keeps the token and registers later if there is no session yet
final token = await messaging.getToken();
if (token != null) await HatangChat.instance.setPushToken(token, provider: PushProvider.fcm);
messaging.onTokenRefresh.listen((t) => HatangChat.instance.setPushToken(t, provider: PushProvider.fcm));

// App in the background, the user taps the notification
FirebaseMessaging.onMessageOpenedApp.listen((m) {
if (HatangChat.isHatangNotification(m.data)) HatangChat.instance.handleNotification(m.data);
});

// Cold start from a notification: the SDK holds the request until it is ready, then opens the conversation
final initial = await messaging.getInitialMessage();
if (initial != null && HatangChat.isHatangNotification(initial.data)) {
await HatangChat.instance.handleNotification(initial.data);
}
}

Call wirePush() after HatangChat.configure and Firebase.initializeApp. isHatangNotification checks data["hatang"] == "1" (a nested data is accepted too), so your own notifications and Ha Tang's can coexist. handleNotification opens the conversation in data["conversation_id"]; pass context: or let the SDK use navigatorKey. It returns as soon as the chat is open (or the request is held during a cold start), without waiting for the visitor to close the chat, so await in start-up code does not hang the app.

While the app is in the foreground and connected, the gateway does not send push (messages arrive over the socket), so FirebaseMessaging.onMessage rarely sees Ha Tang data. Call clearPushToken() on sign-out; reset() registers the token again for the new visitor.

Android 8+: Ha Tang posts replies to the hatang_messages notification channel. Create a channel with this id (for example with flutter_local_notifications) to set its name and importance in the device settings; otherwise Android shows the notification in Firebase's fallback channel.

iOS: besides the iOS app in Firebase, you need an App ID that matches the bundle ID exactly and an APNs key uploaded to Firebase (see Push notifications → iOS apps). Then in Xcode:

  1. Runner → Signing & Capabilities → choose the Team that owns the App ID.
  2. + Capability → Push Notifications (adds aps-environment to the entitlements).
  3. + Capability → Background Modes → check Remote notifications.

The example app (example/ios) has all three configured for reference.

The "we replied" email sent to visitors contains a link https://<your site>/c/<token>. If your app handles universal links / app links for that domain (or a custom scheme with ?hatang_restore=<token>), restore the session from the token and open the chat:

// app_links, go_router or any source of Uri
Future<void> onUri(Uri uri, BuildContext context) async {
final token = HatangChat.linkTokenFromUri(uri);
if (token == null) return; // not a Hatang link
await HatangChat.instance.restoreSession(token);
if (context.mounted) await HatangChat.open(context);
}

linkTokenFromUri accepts both /c/<token> and ?hatang_restore=<token>. restoreSession stores the new session token, bootstraps again and emits HatangSessionChanged(reason: restored). Restore tokens are single-use and expire, so do not store them.

Theming​

By default the SDK takes the brand color and light/dark mode from the site settings in the dashboard (themeMode: HatangChatThemeMode.site). Override them with theme / darkTheme:

await HatangChat.configure(
siteId: 'site_…',
appKey: 'hak_…',
themeMode: HatangChatThemeMode.system, // site | light | dark | system
theme: HatangChatTheme.light(accent: const Color(0xFF0284C7), fontFamily: 'Inter'),
darkTheme: HatangChatTheme.dark(accent: const Color(0xFF38BDF8)),
);

HatangChatTheme is a ThemeExtension: put it in your app's ThemeData.extensions so HatangChatScreen and HatangConversationView follow your theme when you do not pass one to configure; copyWith and lerp are available. The screens wrap themselves in Theme, Material and Localizations.override, so they run inside a CupertinoApp without extra delegates. Text and icons keep at least 4.5:1 contrast with the color you pick (the SDK darkens or lightens it when needed), respect the system font scale up to 200%, and reduce motion when the device asks for it.

Follow the widget settings in the dashboard​

Some parts of HatangChatScreen come straight from the site's widget settings (Support → Channels → your website). Change them in the dashboard and the app picks them up the next time the chat opens, with no code change:

  • Header: the subtitle under the site name (in the current language) and the profile: photos of online agents, or the business logo (the first letter of the site name when there is no logo).
  • Greeting: on or off, and whether it shows on phones (the SDK counts every platform as a phone, including tablets, desktop and web). When the sender is set to an online agent, that agent's name shows under the greeting while the team is online. The teaser over the launcher exists only in the web widget.
  • Contact buttons: Zalo, Messenger, phone and email appear as a horizontally scrolling row of buttons under the header and open in the matching app on the device (through url_launcher). Override the "Call us" and "Email" labels with the callUs and emailUs strings.

With an older gateway that does not send these settings, the screen stays as before: the greeting always shows, the header shows agents, and there are no contact buttons. HatangConversationView (no app bar) only follows the greeting settings.

Languages​

The SDK ships Vietnamese and English. The language is chosen in this order: locale in configure, the visitor's stored choice, the site's default language, then the device language. Switch at runtime:

await HatangChat.instance.setLocale(HatangLocale.en);

Override individual strings per language (strings you do not override keep their defaults):

await HatangChat.configure(
siteId: 'site_…',
appKey: 'hak_…',
strings: {
HatangLocale.en: HatangChatStrings.en.copyWith(placeholder: 'How can we help?', team: 'Acme Support'),
HatangLocale.vi: HatangChatStrings.vi.copyWith(placeholder: 'Bạn cần hỗ trợ gì?'),
},
);

Useful keys include placeholder, team, callUs, emailUs and poweredBy (the "Chat by hatang.dev" line under the composer). In your own widgets, HatangChatLocalizations.of(context).strings gives the strings in use; add HatangChatLocalizations.delegate to your MaterialApp's localizationsDelegates if you want to look them up outside the chat screen.

Troubleshooting​

SymptomCause and fix
HatangException(invalid_app_key 401) on startappKey is wrong or belongs to another site. Check the Apps tab of the right website; keys start with hak_.
HatangException(app_key_revoked 403)The key was revoked in the dashboard. Ship an update with a new key, and show an "update the app" message to users of the old build (HatangException.isAppKeyError).
connectionState is outdated, HatangProtocolOutdated eventThe gateway closed the socket with code 4004: the SDK is too old for the protocol. The SDK stops reconnecting; upgrade hatang_chat and ship a release.
Android emulator reaches the local API but the socket does not connectThe gateway builds the WebSocket address from its own CHAT_URL (localhost), so 10.0.2.2 is not enough. Run adb reverse tcp:3002 tcp:3002 and keep baseUrl: Uri.parse('http://localhost:3002'). For a real phone on Wi-Fi, set the gateway's CHAT_URL to your dev machine's LAN IP and use that same address as baseUrl.
iOS cannot call http:// during developmentApp Transport Security blocks plain HTTP. Add an NSAppTransportSecurity / NSAllowsLocalNetworking exception to Info.plist for debug builds only; production always uses https://chat.hatang.dev.
Flutter web: CORS error or 401 although the key is rightThe browser sends an Origin and the x-hatang-app-key header; the page's origin must be allowed for the site (the website's domain, or Extra allowed origins in its settings) for the preflight to pass. On the web, consider the web widget instead of the Flutter SDK.
No push notificationsCheck the decision order on Push notifications: no push while the app is in the foreground or the visitor is viewing the conversation; the website's Apps tab must have a Firebase file; click Send a test to reach the most recent device.
iOS: last error is APNs TopicDisallowedThe App ID in Apple Developer does not match the bundle ID exactly (a trailing dot counts), or the APNs key belongs to another team. Create an App ID with the exact bundle ID and Push Notifications on; no rebuild is needed.
iOS: firebase_messaging/apns-token-not-setThe Push Notifications capability is missing, or (on the simulator) its push service is stuck: restart the simulator. The simulator receives real push from iOS 16 on Apple silicon Macs.
Android: notifications land in "Miscellaneous"The app did not create the hatang_messages channel; see Push notifications.
dart pub publish refuses with "Potential leak of Google API Key"The app-side Firebase config is not a secret; list the file that contains it under false_secrets in pubspec.yaml.
rate_limit_exceeded5 messages per 10 seconds and 60 per minute per visitor; 30 bootstraps per minute per install. HatangException.isTransient says it is worth retrying later.

Pass logger: HatangLogger.console to configure to see socket frames and HTTP calls in the console while debugging.

Changelog​

0.6.1: the pub.dev listing: a clearer description, the product page as homepage, in-app-chat and zalo topics, and an About Ha Tang section in the README. No code changes.

0.6.0: in-app purchases. HatangChat.purchaseToken(userId) (and top-level purchaseToken(siteId, userId)) gives the app account token for StoreKit / Google Play; HatangChat.reportPurchase(...) after a purchase, with signedTransaction (iOS) or purchaseToken + productType (Android): Ha Tang verifies it with the store and shows the plan or purchase on the customer. ProductCardItem carries basePlanId, offerId, billingPeriod, trialDays.

0.5.0: the chat screen follows more of the site's widget settings (older gateways that omit them keep the previous behavior). Header: the subtitle under the site name and the business logo (or the site name's first letter) instead of the agents when the profile is "business"; agent avatars show their photos. Greeting: hidden when turned off or off on phones; with the "online agent" sender, the first online agent's name shows under it. Contact buttons: a scrolling row of Zalo, Messenger, phone and email buttons under the header, opened with url_launcher. Adds SiteConfig.header, SiteConfig.greetingOptions, SiteConfig.contactButtons, greetingRowFor and the callUs / emailUs strings.

0.4.0: a payment card switches to "Paid" as soon as the timeline has an order_paid message or event for its order or payment; the VietQR code is drawn natively from the EMVCo payload (qr_payload, or from the public checkout for older cards) because the gateway serves qr_image_url as SVG, with a retry button on failure; product, order and payment cards no longer repeat their plain-text summary in a bubble above the card, only the intro line (metadata.intro); paid or expired cards hide the transfer note. Adds PaidMarks, PaymentCard.statusPaid / isPaid(marks), MessageRow.paid, PaymentCard.qrPayload, PaymentQr, HatangChatController.resolvePaymentQr, ProductCard.intro and cardBubbleText; depends on qr (pure Dart) to draw the code.

0.3.2: a "Chat by hatang.dev" line under the composer, like the web widget (plain text, not a link; change it with the poweredBy string).

0.3.1: HatangChatLauncher takes its icon, image and label from the site's Launcher button settings (a label makes it a wide button such as "Support"); child still overrides it; SiteConfig.launcher.

0.3.0 (breaking): the Files attachment entry only appears when you pass pickFile to configure (pickFile: hatangFilePicker from package:hatang_chat/file_picker.dart, or your own function), so apps that pin another file_picker version can install the SDK; camera and photo library are unchanged. Wider version ranges for flutter_secure_storage (10–11), image_picker (from 1.1.2) and flutter_image_compress (from 2.3.0). reset() before the chat ever connected (autoConnect: false) now forgets the right visitor.

0.2.4: removes a stray "Load earlier messages" row in conversations an agent opened with a greeting (the SDK uses the gateway's timeline_has_more); the example app receives push from the demo on Android and iOS with no parameters.

0.2.3: handleNotification(data, context: …) returns at once instead of waiting for the visitor to close the chat (it used to hang start-up code that awaited it); the example app gets a Push tab and optional Firebase Messaging; docs for the iOS steps, the Android hatang_messages channel and adb reverse for the Android emulator.

0.2.2: screenshots on pub.dev and in the README; the example app runs against the public demo inbox when given no parameters.

0.2.1: WebAssembly support (flutter build web --wasm) and a fix for Flutter web failing to connect; text on the brand color picks black or white by WCAG contrast, and the brand color used as text is adjusted to 4.5:1; card buttons use the app's font; banners, the composer and order cards stay accessible at 200% font size.

0.2.0: product, order and payment (VietQR) cards, quick replies and ratings; HatangCommerce for in-app purchase with App Store / Google Play product ids; photo and file attachments (camera, photo library, files; upload progress; HEIC to JPEG; large photos scaled to 2048 px; full-screen image viewer); HatangChatInbox, HatangChat.openInbox and opening a conversation by id; setBottomPadding applies to open screens at once; the User-Agent includes the OS version.

0.1.0: configure with a site id and app key; HatangChatScreen and HatangChat.open; the session API (identify, setData, pushEvent, setLocale, setScreen, reset, restoreSession, linkTokenFromUri); the events, unreadCount, availability and connectionState streams; app.state on background and foreground; push hooks; offline form and email prompt; Vietnamese and English; light and dark themes from the site color.

The full history is in the changelog on pub.dev. See also: Compatibility for the protocol version and minimum gateway, Push notifications, Identity verification and the React Native SDK.

View this page as Markdown