# Flutter SDK `hatang_chat`

> **Info: 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](https://pub.dev/packages/hatang_chat) · [Changelog](https://pub.dev/packages/hatang_chat/changelog) · [API reference (dartdoc)](https://hatang.dev/docs/reference/flutter/index.html)

> **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](https://hatang.dev/docs/reference/realtime-protocol.md) 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.

<p>
  <img src="/img/sdk/flutter/conversation.png" width="200" alt="Chat screen with a product card and an order card" />
  <img src="/img/sdk/flutter/conversation_dark.png" width="200" alt="Chat screen in dark mode" />
  <img src="/img/sdk/flutter/inbox.png" width="200" alt="The visitor's conversation list" />
  <img src="/img/sdk/flutter/launcher.png" width="200" alt="In-app chat button with an unread badge" />
</p>

## Installation {#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.

```bash
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

```bash
flutter pub add hatang_chat
```

```yaml title="pubspec.yaml"
dependencies:
  hatang_chat: ^0.6.1
```

### Platform permissions {#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.

```xml title="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 {#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`).

```dart title="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()));
}
```

| Parameter | Meaning |
|---|---|
| `siteId`, `appKey` | Required. From the website's **Apps** tab |
| `baseUrl` | The gateway. Default `https://chat.hatang.dev`; change it only for a local gateway (see [Troubleshooting](#troubleshooting)) |
| `locale` | `HatangLocale.vi` / `.en`. When unset: the visitor's stored choice, then the site default, then the device |
| `themeMode`, `theme`, `darkTheme` | See [Theming](#theming) |
| `strings` | Per-locale string overrides, see [Languages](#languages) |
| `navigatorKey` | Lets `handleNotification` and proactive messages open the screen when you pass no `context` |
| `autoConnect` | `false` to call `HatangChat.instance.connect()` yourself later (for example after a consent screen) |
| `openOnProactiveMessage` | Open the chat when the team starts a conversation (default `true`) |
| `commerce` | How "Buy" and "Pay" on cards behave, see [Sales cards and in-app purchase](#commerce) |
| `pickFile` | Enables the **Files** attachment entry, see [Platform permissions](#platform-permissions) |
| `logger`, `appInfo` | Logging, 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 {#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](https://hatang.dev/docs/guides/support/web-widget/identity-verification.md).

```dart
// 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`:

```js title="Node"
const hash = require("node:crypto").createHmac("sha256", process.env.HATANG_IDENTITY_SECRET).update(userId).digest("hex");
```

```php title="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-and-embed}

### Open full screen

```dart
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`:

```dart
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`:

```dart
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`:

```dart
class SupportTab extends StatefulWidget {
  const SupportTab({super.key});
  @override
  State<SupportTab> createState() => _SupportTabState();
}

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

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

  @override
  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 {#commerce}

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:

```dart
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](https://hatang.dev/docs/guides/support/in-app-purchases.md).

**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 {#events-and-data}

```dart
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;
  }
});
```

| Event | When |
|---|---|
| `HatangReady` | Bootstrap and authentication finished |
| `HatangOpened`, `HatangClosed` | The chat screen appears or goes away |
| `HatangMessageReceived`, `HatangMessageSent` | A message from the team or bot; a visitor message was stored (including from another device) |
| `HatangConversationStatus` | `open`, `pending`, `resolved`, `closed` |
| `HatangConversion` | `chatStarted`, `lead`, `purchase` (with `value`, `currency`, `transaction_id`) |
| `HatangSessionChanged` | `identityMerged`, `reset`, `restored`, `tokenRejected` |
| `HatangProtocolOutdated` | The 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`).

```dart
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:

```dart
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 {#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](https://hatang.dev/docs/guides/sdks/push-notifications.md)). The SDK does not depend on Firebase; your app uses `firebase_messaging` and hands the token to the SDK.

```bash
flutter pub add firebase_core firebase_messaging
```

```dart title="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](https://hatang.dev/docs/guides/sdks/push-notifications.md#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.

## Deep links {#deep-links}

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:

```dart
// 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 {#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`:

```dart
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 {#widget-settings}

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 {#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:

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

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

```dart
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 {#troubleshooting}

| Symptom | Cause and fix |
|---|---|
| `HatangException(invalid_app_key 401)` on start | `appKey` 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` event | The 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 connect | The 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 development | App 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 right | The 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](https://hatang.dev/docs/guides/support/web-widget/javascript-api.md) instead of the Flutter SDK. |
| No push notifications | Check the decision order on [Push notifications](https://hatang.dev/docs/guides/sdks/push-notifications.md): 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 `TopicDisallowed` | The 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-set` | The 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](#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_exceeded` | 5 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 {#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](https://pub.dev/packages/hatang_chat/changelog). See also: [Compatibility](https://hatang.dev/docs/guides/sdks/compatibility.md) for the protocol version and minimum gateway, [Push notifications](https://hatang.dev/docs/guides/sdks/push-notifications.md), [Identity verification](https://hatang.dev/docs/guides/support/web-widget/identity-verification.md) and the [React Native SDK](https://hatang.dev/docs/guides/sdks/react-native.md).

Source: https://hatang.dev/docs/guides/sdks/flutter
