# Push notifications for in-app customers

When an agent or a bot replies and the customer does not have the chat open, Ha Tang sends a push notification to the customer's phone through **your own Firebase project** (FCM HTTP v1). There is no shared Ha Tang Firebase project: notifications carry your app's name and icon, and they count against your project's quota. This page covers the setup shared by the native SDKs ([Flutter](https://hatang.dev/docs/guides/sdks/flutter.md#push-notifications), [React Native](https://hatang.dev/docs/guides/sdks/react-native.md#push-notifications), [iOS](https://hatang.dev/docs/guides/sdks/ios.md#push-notifications) and [Android](https://hatang.dev/docs/guides/sdks/android.md#push-notifications)). Website visitors get a "we replied" email instead.

## Before you start

- A mobile app that uses one of the native SDKs, with an app key (`hak_...`).
- A Firebase project with your Android app and/or iOS app added to it.
- For iOS: an Apple Developer account that owns your app's bundle ID.

## 1. Upload the Firebase service account {#service-account}

1. Firebase console → **Project settings → Service accounts → Generate new private key**. A JSON file downloads.
2. Ha Tang dashboard → **Support → Channels** → your website → **Apps** tab → **Push notifications (Firebase)** → choose the file → **Save Firebase credentials**.
3. The card then shows the project, the service account, when it was last used and the last error, if Google refused the key.

The file is encrypted at rest. Until you upload one, in-app customers only get an email when someone replies, and only if they left an email address. **Remove credentials** deletes it.

### iOS apps {#ios-apps}

Firebase delivers to iPhones through APNs, so your Firebase project also needs:

1. An **App ID** in Apple Developer (Certificates, Identifiers & Profiles → Identifiers) whose bundle ID matches your app **exactly**, with **Push Notifications** turned on. You only need the App ID; you do not have to create the app in App Store Connect.
2. An **APNs key** (Keys → +, tick Apple Push Notifications service). Choose the **Sandbox & Production** environment so it works for debug builds, TestFlight and the App Store. Download the `.p8` file and note the Key ID and your Team ID.
3. Firebase → Project settings → **Cloud Messaging → Apple app configuration** → upload the key with its Key ID and Team ID.

When the App ID does not match the bundle ID (a trailing dot is enough) or the key belongs to another team, FCM answers every send with the APNs error `TopicDisallowed`. Ha Tang shows it as the last error on the Firebase card. Fixing the App ID needs no new app build.

> **Note: Direct APNs**
>
> Ha Tang sends through Firebase only. The iOS SDK can register a raw APNs device token, and the gateway accepts devices with `provider: "apns"`, but nothing is delivered to them yet. Until direct APNs delivery ships, iOS apps should use Firebase Messaging and register the FCM token.

### Restrict your app's Firebase API key {#api-key}

The API key inside your app's Firebase config (`google-services.json`, `GoogleService-Info.plist`) is not a secret: it ships in every build. Restrict it in Google Cloud console → APIs & Services → Credentials → your key → **API restrictions** to the APIs your app calls to get a token, such as the Firebase Installations API and the FCM Registration API. Ha Tang does not use this key; it sends with the service account.

## 2. Register the device from the SDK {#register}

Your app gets the FCM token from Firebase and hands it to the SDK:

| SDK | Call |
|---|---|
| Flutter | `HatangChat.instance.setPushToken(token, provider: PushProvider.fcm)` |
| React Native | `Hatang.setPushToken(token, "fcm")` |
| iOS | `await HatangChat.setPushToken(fcmToken: token)` |
| Android | `HatangChat.setPushToken(token)` (a suspend function) |

The SDK calls `PUT /widget/v1/sites/{siteId}/devices` with the token, the provider, the platform and, when known, the install id, language, app version, OS version and device model. A token is one phone. When a visitor's identity is [merged](https://hatang.dev/docs/guides/support/web-widget/identity-verification.md), their devices follow the person. `clearPushToken()` unregisters the token (`DELETE devices`); call it on sign-out before `reset()`.

The website's **Apps** tab lists the **Registered devices** with the time of their last push.

## 3. When Ha Tang sends a push {#when}

For every **public** message from an agent or a bot (not the customer's own messages, and not internal notes), Ha Tang checks, in this order:

| Check | If it fails |
|---|---|
| The conversation is not marked as spam and the visitor still exists | Skip |
| The visitor has at least one registered device | Skip the push. The "we replied" email follows its own rules (the visitor is offline and left an email address) |
| The visitor is **not** connected with the app in the foreground | Skip: the message arrives over the socket |
| The visitor is **not** looking at this very conversation (`app.state` with its `conversation_id`) | Skip |
| The site has Firebase credentials | Skip, and log it once an hour per site |
| Fewer than 20 pushes in the last minute for this visitor | Skip |

Ha Tang then sends to each device. A token FCM reports as unregistered is deleted, a temporary FCM error is retried, and a credentials error (Google refused the key) is shown as the last error on the Firebase card. A visitor with a registered phone does **not** also get an email for each reply; the transcript email when a conversation is resolved still goes out.

The SDKs send `app.state` with `foreground: false` as soon as the app goes to the background, so "connected" means the app is really on screen. A client that does not send `app.state` looks online for up to 60 seconds after it goes to the background and misses pushes in that window.

## 4. The payload {#payload}

```json
{
  "notification": { "title": "Lan (Moc Huong Tea)", "body": "Yes, it's in stock." },
  "data": {
    "hatang": "1",
    "site_id": "site_…",
    "conversation_id": "cnv_…",
    "message_id": "msg_…",
    "visitor_id": "vis_…",
    "kind": "message"
  },
  "android": { "priority": "high", "collapse_key": "cnv_…", "notification": { "tag": "cnv_…", "channel_id": "hatang_messages" } },
  "apns": {
    "headers": { "apns-priority": "10", "apns-collapse-id": "cnv_…" },
    "payload": { "aps": { "sound": "default", "thread-id": "cnv_…", "badge": 3, "mutable-content": 1 } }
  }
}
```

| Field | Meaning |
|---|---|
| `notification.title` | The sender's name; the site name when the message has none |
| `notification.body` | The first 140 characters of the message (cards are turned into text). For a message with only an attachment: "🖼 Photo" or "📎 Attachment", in the visitor's language |
| `data.*` | Strings only. `hatang: "1"` lets `isHatangNotification` recognize the push; `kind` is `message`, or `test` for the dashboard's test push |
| `collapse_key`, `tag`, `apns-collapse-id`, `thread-id` | The conversation id: several replies in a row replace each other and group by conversation |
| `aps.badge` (iOS) | The visitor's unread total across open and pending conversations |
| `android.notification.channel_id` | `hatang_messages`. On Android 8+, create a channel with this id in your app to give it a name and an importance; without it, Android uses Firebase's fallback channel |

When the customer taps the notification, pass `data` to the SDK's `handleNotification`, which opens the right conversation. Each SDK page shows how to wire the tap for a cold start and for an app in the background.

## 5. Send a test {#test}

**Send a test** on the website's **Apps** tab sends a fixed notification ("Test notification from Ha Tang: everything is set up.") to the site's most recently connected device, with `data` `{ site_id, kind: "test", hatang: "1" }` and no conversation. Use it to check the Firebase file, the Android channel and the notification permission before you release. With no registered device, the dashboard says "No device has registered for notifications yet."

## Next steps

- [Realtime protocol](https://hatang.dev/docs/reference/realtime-protocol.md): the `app.state` frame and the `PUT` / `DELETE devices` endpoints.
- [Identity verification](https://hatang.dev/docs/guides/support/web-widget/identity-verification.md): devices follow the person when identities merge.
- Wire the tap in your SDK: [Flutter](https://hatang.dev/docs/guides/sdks/flutter.md#push-notifications), [React Native](https://hatang.dev/docs/guides/sdks/react-native.md#push-notifications), [iOS](https://hatang.dev/docs/guides/sdks/ios.md#push-notifications), [Android](https://hatang.dev/docs/guides/sdks/android.md#push-notifications).

Source: https://hatang.dev/docs/guides/sdks/push-notifications
