React Native SDK @hatang/chat-react-native
@hatang/chat-react-native 0.4.2 · protocol v1 · React Native 0.76+ (New Architecture or not) · Expo SDK 52+ · React 18+ · npm · Compatibility
Version 0.3 has the chat screen, the visitor's conversation inbox, hooks for your own UI, signed identity, photo and file attachments, product, order and payment cards with in-app purchase hooks, push notifications through your own Firebase project, conversion events, deep links that restore a session, and a chat screen that follows the site's widget settings. The API may still change before 1.0.
Support chat for React Native and Expo apps. The SDK talks the realtime protocol directly to the Ha Tang gateway (no web view), like the Flutter, iOS and Android SDKs. The UI is built entirely from React Native core components and the package has no native code of its own; Expo modules (secure storage, device info, image and file pickers) are optional and loaded when installed. Customers who write from your app land in the same inbox as visitors from your website, Zalo, Messenger, Instagram and TikTok Shop.
Installation
Requirements
| Version | |
|---|---|
| React Native | 0.76+ (New Architecture or the old architecture) |
| Expo | SDK 52+ (managed or bare) |
| React | 18+ |
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.
- In the dashboard, open Support → Channels → Add channel → Mobile app.
- Click Create app key: give it a name (for example "Acme RN"), pick the platform, and optionally add bundle ids.
- The key is shown once. Pass it to
Hatang.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
# Expo: the package and the optional modules, at the versions that match your Expo SDK
npx expo install @hatang/chat-react-native expo-secure-store expo-application expo-device expo-image-picker expo-document-picker react-native-safe-area-context
# Bare React Native
npm install @hatang/chat-react-native
# or
pnpm add @hatang/chat-react-native
Optional Expo modules
Every expo-* package is an optional peer dependency. The SDK loads them at runtime when they are installed; if one is missing, the matching feature is turned off and the app still runs:
| Package | Used for | When it is missing |
|---|---|---|
expo-secure-store | The visitor token in the Keychain / Keystore, and a few small session settings | Kept in memory: the visitor is forgotten every time the app restarts, and the SDK logs a warning. Pass storage instead (see Storage). |
expo-application | The app version (1.4.0+12) | No app version is sent |
expo-device | Device model and OS version | Uses Platform.Version, no model |
expo-image-picker | "Take photo" and "Photo library" in the composer | Those two entries are hidden |
expo-document-picker | "Choose file" in the composer | That entry is hidden |
react-native-safe-area-context | Keeps the chat screen clear of the notch, status bar and home indicator | iOS falls back to React Native's SafeAreaView (with a deprecation warning); Android leaves no margin. Or pass insets. |
The attach button only shows when at least one picker is installed. Bare React Native apps can use these modules too once expo (Expo Modules) is installed.
Platform permissions
Photo attachments need a usage description on iOS. With Expo, set it through the expo-image-picker config plugin:
{
"expo": {
"plugins": [
["expo-image-picker", {
"photosPermission": "Send photos to our support team",
"cameraPermission": "Take photos to send to our support team"
}]
]
}
}
Bare React Native apps add NSPhotoLibraryUsageDescription and NSCameraUsageDescription to ios/<App>/Info.plist. Camera permission is requested on first use. Files can be up to 10 MB: images (the picker converts HEIC to JPEG), PDF, office documents, zip, mp4 and m4a.
On Android, keep softwareKeyboardLayoutMode: "resize" (Expo's default, equivalent to android:windowSoftInputMode="adjustResize"): the chat screen relies on the window shrinking when the keyboard opens. On iOS, KeyboardAvoidingView handles it.
Expo Go runs the SDK as long as you only use expo-secure-store, expo-application, expo-device and the pickers. Push notifications through Firebase need a development build.
Initialization
Call Hatang.configure once at start-up, outside any component (for example at the top of App.tsx or index.js). The SDK bootstraps the visitor, stores the token and opens the socket right away (autoConnect: true).
import { Hatang } from "@hatang/chat-react-native";
Hatang.configure({
siteId: "site_…",
appKey: "hak_…",
// baseUrl: "https://chat.hatang.dev", // default
// locale: "en",
// autoConnect: false, // call Hatang.connect() yourself later
});
| Option | 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) |
locale | "vi" / "en". When unset: the stored choice, then the site, then the device |
storage, secureStorage | Your own storage, see Storage |
autoConnect | false to call Hatang.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 |
logger | { debug?, info?, warn?, error? } for logs while debugging |
app | Overrides for the app details sent to the gateway (version, os, osVersion, deviceModel…) |
Calling configure again replaces the previous engine (listeners added with Hatang.on are kept). The facade also has Hatang.connect(), Hatang.disconnect(), Hatang.dispose(), Hatang.isConfigured, Hatang.session (visitorId, conversationId, locale, isIdentified, email, name), Hatang.unreadCount, Hatang.availability and Hatang.connection. Hatang.engine returns the full ChatEngine for advanced cases.
The SDK follows AppState: when the app goes to the background the gateway is told at once (app.state) so new replies go out as push notifications, and the socket closes about 10 seconds later. Back in the foreground, the SDK reconnects and catches up on missed messages.
Storage
By default the SDK uses expo-secure-store for both the visitor token and the small session settings. Keys are readable after the first device unlock, so a push that wakes the app in the background can still read the token. Without expo-secure-store and without storage, the SDK keeps everything in memory and the visitor is treated as new on every launch.
storage takes an AsyncStorage-like object (getItem, setItem, removeItem) or the SDK's { get, set, remove } shape (every function returns a Promise):
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Keychain from "react-native-keychain";
import { Hatang, type HatangStorage } from "@hatang/chat-react-native";
// A token adapter built on react-native-keychain
const keychainStore: HatangStorage = {
get: async (key) => {
const c = await Keychain.getGenericPassword({ service: key });
return c ? c.password : null;
},
set: async (key, value) => {
await Keychain.setGenericPassword("hatang", value, { service: key });
},
remove: async (key) => {
await Keychain.resetGenericPassword({ service: key });
},
};
Hatang.configure({ siteId: "site_…", appKey: "hak_…", storage: AsyncStorage, secureStorage: keychainStore });
Order of choice: the visitor token goes to secureStorage, else expo-secure-store, else storage. So when expo-secure-store is installed, the token always lives there even if you pass storage. Avoid putting the token in AsyncStorage if you have another option: AsyncStorage is not encrypted.
Identify visitors
When a user signs in, call identify with their id in your system. hash is the HMAC-SHA256 (hex) 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 Hatang.identify({
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:
const hash = require("node:crypto").createHmac("sha256", process.env.HATANG_IDENTITY_SECRET).update(userId).digest("hex");
$hash = hash_hmac('sha256', $userId, getenv('HATANG_IDENTITY_SECRET'));
When the identity already belongs to another visitor (the user chatted on another device, for example), the gateway merges the sessions: the SDK receives a new token, reloads the conversation and emits sessionChanged with reason: "identity_merged". You do not need to do anything else.
Sign-out
await Hatang.reset();
reset() revokes the old token, clears local state, starts a new visitor and registers the push token again for that visitor, so the next user on the same device does not see the previous user's conversations. To stop push for this device entirely, call Hatang.clearPushToken() before reset().
Open and embed the chat
HatangProvider and the default modal
Wrap your app in HatangProvider. When you do not pass onOpenRequest, the provider opens HatangChatScreen in a full-screen modal when Hatang.open() is called, when the user taps a push notification, or when the team starts a conversation.
import { Button } from "react-native";
import { Hatang, HatangProvider, useHatangUnread } from "@hatang/chat-react-native";
export default function App() {
return (
<HatangProvider config={{ siteId: "site_…", appKey: "hak_…" }}>
<Home />
</HatangProvider>
);
}
function SupportButton() {
const unread = useHatangUnread();
return <Button title={unread ? `Support (${unread})` : "Support"} onPress={() => Hatang.open()} />;
}
config is optional if Hatang.configure() already ran; the provider only reconfigures when siteId, appKey or baseUrl change. The provider also takes theme and strings for the default modal. Hatang.open(conversationId) opens a specific earlier conversation, and Hatang.close() closes the chat.
The chat screen in your navigation
To use your own route (React Navigation, Expo Router), pass onOpenRequest to the provider and render HatangChatScreen yourself:
<HatangProvider onOpenRequest={({ conversationId }) => navigation.navigate("Support", { conversationId })}>
{/* … */}
</HatangProvider>
// The "Support" route
<HatangChatScreen
conversationId={route.params?.conversationId}
onClose={() => navigation.goBack()}
keyboardVerticalOffset={headerHeight}
/>
onOpenRequest receives { conversationId, source }, where source is "api", "notification" or "proactive". On a cold start from a notification, the open request is held until the first provider listens.
HatangChatScreen prop | Meaning |
|---|---|
conversationId | Open this conversation (from the inbox or a notification); defaults to the active one |
onClose | Adds a close button and handles the Android back button |
header | false hides the built-in header, or pass a node to replace it |
keyboardVerticalOffset | Your navigation header's height, for KeyboardAvoidingView |
insets | Safe-area insets; read from react-native-safe-area-context by default. The top inset is ignored with header={false}; pass {} when your navigator already handles insets |
theme, strings | See Theming and Languages |
commerce | Purchase policy for this screen, overriding Hatang.configure({ commerce }); see Sales cards and in-app purchase |
style | Style of the outer container |
While the screen is visible, incoming messages count as read and the gateway sends no push for them.
Inbox
HatangInbox lists the visitor's open and resolved conversations (who answered, last message, time, unread count). Pull down to refresh.
<HatangInbox
onOpen={(conversationId) => navigation.navigate("Support", { conversationId })}
onNewConversation={() => navigation.navigate("Support")}
/>
onNewConversation shows a "New message" button. HatangInbox also takes theme, strings and style.
Your own UI with hooks
Every hook reads the engine from the nearest HatangProvider (or from Hatang after configure):
| Hook | Returns |
|---|---|
useHatangUnread() | Unread count for badges (0 before configure) |
useHatangAvailability() | { online, within_hours, next_open_at, agents_online } or null |
useHatangState() | An immutable state snapshot (items, pending, typingAgent, availability, unreadCount, connection, needsEmail, locale…) |
useHatangChat() | state plus actions: send, retry, discard, attach, loadEarlier, rate, submitEmail, skipEmail, sendOfflineMessage, setThreadVisible, openConversation… |
useHatangEvent(type, fn) | Calls fn for one event type (or for every event with useHatangEvent(fn)) |
useHatangEngine() | The ChatEngine, or null before configure |
import { useHatangAvailability, useHatangChat, useHatangEvent } from "@hatang/chat-react-native";
function SupportTab() {
const { state, send, loadEarlier, setThreadVisible } = useHatangChat();
const availability = useHatangAvailability();
useHatangEvent("messageReceived", (e) => console.log(e.message.content));
// render state.items, call send(text)…
}
A custom UI should call setThreadVisible(true) while the conversation is on screen and false when it is hidden, so unread counts, read receipts and push decisions stay right. HatangChatScreen does this for you.
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 sends "I'd like to buy: …", and a payment card shows a VietQR code, the transfer note and a Pay button that opens the checkout page.
- Physical goods and real-world services (delivered orders, table bookings, appointments, in-person classes…): keep VietQR and the web checkout, the default. The App Store (Guideline 3.1.3(e)) and Google Play do not allow these through in-app purchase.
- Digital goods and features used inside the app (a Pro plan, coins, unlocking lessons…): the App Store (Guideline 3.1.1) and Google Play's payments policy require in-app purchase. Hand the purchase to your store integration through
onBuyProductwith the store product id, and turn offshowExternalCheckout:
import { Platform } from "react-native";
import { Hatang, storeProductId } from "@hatang/chat-react-native";
Hatang.configure({
siteId: "site_…",
appKey: "hak_…",
commerce: {
showExternalCheckout: Platform.OS !== "ios", // iOS: no QR, no transfer note, no web checkout
onBuyProduct: async (product) => {
// The App Store / Google Play id set for each product in the dashboard
const storeId = storeProductId(product, Platform.OS);
if (!storeId) return false; // not an in-app product: keep the default behavior
await myIap.buy(storeId); // for example react-native-iap or expo-iap
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 in product.store_product_ids (app_store, google_play), and storeProductId(product, Platform.OS) picks the right one for the platform.
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 billing_period, trial_days, and for Google Play base_plan_id and offer_id. Pass Hatang.purchaseToken(userId) as the app account token when buying (request.apple.appAccountToken / request.google.obfuscatedAccountId in react-native-iap 15+ / expo-iap 4+), then call Hatang.reportPurchase({ storeProductId, transactionId, … }) after a successful purchase with the store's proof: signedTransaction (the StoreKit 2 JWS, purchase.purchaseToken on iOS in those libraries) on iOS, or purchaseToken and 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. Connect the app under Channels → your app → Subscriptions from the App Store and Google Play so plans, renewals and refunds show on the customer's profile.
commerce field | Meaning |
|---|---|
onBuyProduct(product) | The Buy button. Return true (or a Promise of true) when you handled it; false keeps the default |
onPay(payment) | The Pay button (payment is card.payment). Return true when handled; false opens the checkout page if showExternalCheckout allows it |
showExternalCheckout | Default true. false hides the VietQR code, the transfer note and the web checkout |
A hook that throws counts as handled, so a failed in-app purchase never falls through to the web checkout. 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; <HatangChatScreen commerce={…} /> overrides the global policy for one screen.
Events and data
const off = Hatang.on("conversion", (e) => analytics.track(e.kind, e.data)); // chat_started, lead, purchase
Hatang.on("messageReceived", (e) => showInAppBanner(e.message.content));
Hatang.on("protocolOutdated", () => promptUpdate());
Hatang.on("*", (e) => console.log(e.type));
off(); // unsubscribe
| Event | When |
|---|---|
ready | Bootstrap and authentication finished (visitorId, conversationId) |
opened, closed | The chat screen appears or goes away |
messageReceived, messageSent | A message from the team or bot; a visitor message was stored (including from another device) |
conversationStatus | open, pending, resolved, closed |
conversion | chat_started, lead, purchase (with value, currency, transaction_id) |
sessionChanged | reason: identity_merged, reset, restored, token_rejected |
protocolOutdated | The gateway no longer serves this version; ask the user to update the app |
Listeners added with Hatang.on survive a new configure call. Inside components, use useHatangEvent so the listener is removed on unmount.
Hatang.connection is one of "idle", "connecting", "connected", "reconnecting", "offline", "outdated" or "revoked".
Send data to the inbox:
await Hatang.setData({ cart_total: 450000, last_order: "SO-1001" }); // shown to agents
Hatang.pushEvent("viewed_product", { sku: "TS-100" }); // the visitor's timeline
Hatang.setScreen("ProductPage", { sku: "TS-100" }); // agents see where the visitor is
With React Navigation, call Hatang.setScreen(route.name) in the NavigationContainer's onStateChange.
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 @react-native-firebase/messaging and hands the token to the SDK. iOS uses Firebase Messaging too, because Ha Tang does not deliver through APNs directly yet.
npm install @react-native-firebase/app @react-native-firebase/messaging
# Expo: npx expo install @react-native-firebase/app @react-native-firebase/messaging
# then add "@react-native-firebase/app" to the plugins in app.json and create a development build
import { PermissionsAndroid, Platform } from "react-native";
import messaging from "@react-native-firebase/messaging";
import { Hatang } from "@hatang/chat-react-native";
export async function wirePush() {
if (Platform.OS === "ios") await messaging().requestPermission();
if (Platform.OS === "android" && Platform.Version >= 33) {
await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS);
}
// Register the device; safe before the SDK is ready, and sent again after the identity changes
const token = await messaging().getToken();
await Hatang.setPushToken(token, "fcm");
messaging().onTokenRefresh((t) => Hatang.setPushToken(t, "fcm"));
// App in the background, the user taps the notification
messaging().onNotificationOpenedApp((m) => {
if (Hatang.isHatangNotification(m.data)) Hatang.handleNotification(m.data);
});
// Cold start from a notification
const initial = await messaging().getInitialNotification();
if (initial && Hatang.isHatangNotification(initial.data)) Hatang.handleNotification(initial.data);
}
Call wirePush() after Hatang.configure. isHatangNotification checks data.hatang == "1", so your own notifications and Ha Tang's can coexist. handleNotification(data) loads the conversation in data.conversation_id (waiting until the session is ready, or until configure on a cold start), then asks the UI to open it: the provider's modal, or your onOpenRequest. It returns false for payloads that are not Ha Tang's.
While the app is in the foreground and connected, the gateway does not send push (messages arrive over the socket), and it does not send push while the visitor is viewing the conversation. To show something for a foreground message, check Hatang.isHatangNotification(remoteMessage.data) in messaging().onMessage, or use the messageReceived event. Call Hatang.clearPushToken() on sign-out; reset() registers the token again for the new visitor.
Using expo-notifications instead of Firebase: get the native device token with getDevicePushTokenAsync() (not the Expo push token) and pass "fcm" on Android. On iOS that function returns an APNs token, so keep using Firebase Messaging there.
Android 8+: Ha Tang posts replies to the hatang_messages notification channel. Create a channel with this id (for example with @notifee/react-native or expo-notifications) to set its name and importance; 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 enable the Push Notifications and Background Modes → Remote notifications capabilities in Xcode (with Expo, set ios.entitlements / UIBackgroundModes in app.json; the @react-native-firebase config plugin does the rest).
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:
import { Linking } from "react-native";
import { Hatang } from "@hatang/chat-react-native";
async function onUrl(url: string | null) {
const token = url ? Hatang.linkTokenFromUrl(url) : null;
if (!token) return; // not a Hatang link
await Hatang.restoreSession(token);
Hatang.open();
}
Linking.getInitialURL().then(onUrl);
Linking.addEventListener("url", ({ url }) => onUrl(url));
linkTokenFromUrl accepts both /c/<token> and ?hatang_restore=<token>. restoreSession stores the new session token, bootstraps again and emits sessionChanged with reason: "restored". Restore tokens are single-use and expire, so do not store them.
Theming
By default the brand color and light/dark mode come from the site settings in the dashboard (auto follows the device's useColorScheme()). Override them with the theme prop of HatangChatScreen, HatangInbox or HatangProvider:
<HatangChatScreen
theme={{
mode: "system", // "site" (default) | "system" | "light" | "dark"
colors: { primary: "#E11D48", background: "#FFFFFF", surface: "#F8FAFC", text: "#0F172A", muted: "#475569" },
darkColors: { background: "#0B1220" },
radius: 12,
fontFamily: "Inter-Regular",
}}
/>
Colors you can override: primary, onPrimary, background, surface, text, muted, bubbleVisitor, bubbleVisitorText, bubbleAgent, bubbleAgentText, danger, border. darkColors applies in dark mode (it falls back to the default dark palette, not to colors, except for primary). Text on colored backgrounds (send button, the visitor's bubbles, links) is adjusted to keep at least 4.5:1 contrast, even with a light brand color, and the UI follows the system font size.
Every button has an accessibilityLabel and accessibilityRole and a touch target of at least 44×44 pt; new messages from the team are announced to screen readers; the typing indicator does not animate.
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 title (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. 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 row of buttons under the header and open in the matching app on the device. Override the "Call us" and "Email" labels with the
callUsandemailUsstrings;header={false}hides this row too.
With an older gateway that does not send these settings, the screen stays as before: the greeting always shows, the header shows agent photos, and there are no contact buttons.
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 Hatang.setLocale("en");
Override strings with the strings prop (it applies to the current language; strings you do not override keep their defaults). To override per language, pick by the state's locale:
const { locale } = useHatangState();
<HatangChatScreen
strings={locale === "en" ? { placeholder: "How can we help?", team: "Acme Support" } : { placeholder: "Bạn cần hỗ trợ gì?" }}
/>
HATANG_STRINGS.vi / HATANG_STRINGS.en hold every default string (type HatangChatStrings) if you need them in your own UI.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
HatangError(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_. |
HatangError(app_key_revoked 403), Hatang.connection is "revoked" | The key was revoked in the dashboard. The SDK stops retrying and the chat screen asks the user to update the app. Ship an update with a new key; error.isAppKeyError identifies this error in a custom UI. |
Hatang.connection is "outdated", protocolOutdated event | The gateway closed the socket with code 4004: the SDK is too old for the protocol. The SDK stops reconnecting; upgrade @hatang/chat-react-native and ship a release. |
Warning [hatang] No persistent storage | expo-secure-store is not installed and no storage was passed: the visitor is forgotten on every launch. Install expo-secure-store or pass storage (see Storage). |
Metro: Unable to resolve module expo-… | A custom Metro config turned off optional dependencies. Expo's default config and @react-native/metro-config allow them; if you configure Metro yourself, set transformer.allowOptionalDependencies: true in metro.config.js. |
| No attach button, or no "Choose file" entry | expo-image-picker / expo-document-picker is not installed, or it is installed but the native app was not rebuilt (a new native module needs a new build, not just a JS reload). |
| The keyboard covers the composer on Android | softwareKeyboardLayoutMode is not "resize" (or windowSoftInputMode is not adjustResize). Under a navigation header, pass keyboardVerticalOffset. |
| 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: "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 NSAllowsLocalNetworking exception for debug builds only; production always uses https://chat.hatang.dev. |
| No push notifications | Check 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. Firebase does not run in Expo Go: use a development build. |
| Tapping a notification does not open the chat | handleNotification only requests an open. You need a HatangProvider without onOpenRequest (the default modal) or an onOpenRequest that navigates to a route with HatangChatScreen. |
rate_limit_exceeded | 5 messages per 10 seconds and 60 per minute per visitor; 30 bootstraps per minute per install. error.isTransient says it is worth retrying later. |
Pass logger: console to configure to see socket frames and HTTP calls in the console while debugging.
Changelog
0.4.1: the npm page describes the SDK and Ha Tang and links this guide; the README ends with an About Ha Tang section. No code changes.
0.4.0: in-app purchases. Hatang.purchaseToken(userId) (and purchaseToken(siteId, userId)) gives the app account token for StoreKit / Google Play; Hatang.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. Product cards carry base_plan_id, offer_id, billing_period, trial_days.
0.3.1: package metadata fix (repository.url in the git+https:// form npm expects).
0.3.0: HatangChatScreen follows the site's widget settings (older gateways keep the previous behavior): the header subtitle, the business logo (or the site name's initial) instead of agent photos when the profile is "business", the greeting on/off and on-phones switches with the first online agent's name under it for the "online agent" sender, and Zalo, Messenger, phone and email contact buttons under the header, opened with Linking (hidden with header={false}). Adds the callUs and emailUs strings and re-exports the ContactButton type.
0.2.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 with plain Views 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). Adds the commerce policy shared with the iOS, Android and Flutter SDKs: HatangCommerce (onBuyProduct, onPay, showExternalCheckout) through Hatang.configure({ commerce }) or <HatangChatScreen commerce={…} />, and the helpers storeProductId, paymentCardDisplay, runCommerceHook, paidMarks, isPaymentPaid and cardBubbleText. No new peer or native dependency: the QR encoder is bundled.
0.1.0: first release. Hatang.configure with a site id and app key; HatangProvider with the default modal or onOpenRequest; HatangChatScreen and HatangInbox; the useHatangChat, useHatangState, useHatangUnread, useHatangAvailability and useHatangEvent hooks; the session API (identify, setData, pushEvent, setScreen, setLocale, reset, restoreSession, linkTokenFromUrl); push (setPushToken, clearPushToken, isHatangNotification, handleNotification); app.state from AppState; photo and file attachments through optional Expo modules; storage through expo-secure-store or your own adapter; safe-area insets; offline form and email prompt; Vietnamese and English; light and dark themes from the site color.
See also: Compatibility for the protocol version and minimum gateway, Push notifications, Identity verification and the Flutter SDK.