# Thông báo đẩy cho khách trong ứng dụng

Khi nhân viên hoặc bot trả lời mà khách không mở chat, Hạ Tầng gửi thông báo đẩy tới điện thoại của khách qua **dự án Firebase của bạn** (FCM HTTP v1). Hạ Tầng không có dự án Firebase dùng chung: thông báo mang tên và biểu tượng ứng dụng của bạn, hạn mức tính vào dự án của bạn. Trang này mô tả phần cài đặt chung cho các SDK native ([Flutter](https://hatang.dev/vi/docs/guides/sdks/flutter.md#push-notifications), [React Native](https://hatang.dev/vi/docs/guides/sdks/react-native.md#push-notifications), [iOS](https://hatang.dev/vi/docs/guides/sdks/ios.md#push-notifications) và [Android](https://hatang.dev/vi/docs/guides/sdks/android.md#push-notifications)). Khách trên website nhận email "chúng tôi đã trả lời" thay cho thông báo đẩy.

## Trước khi bắt đầu

- Ứng dụng di động dùng một trong các SDK native, có khoá ứng dụng (`hak_...`).
- Một dự án Firebase đã thêm ứng dụng Android và/hoặc iOS của bạn.
- Với iOS: tài khoản Apple Developer sở hữu bundle ID của ứng dụng.

## 1. Tải tài khoản dịch vụ Firebase {#service-account}

1. Firebase console → **Project settings → Service accounts → Generate new private key**; một tệp JSON được tải về.
2. Dashboard Hạ Tầng → **Hỗ trợ → Kênh** → chọn website → tab **Ứng dụng** → **Thông báo đẩy (Firebase)** → chọn tệp → **Lưu thông tin Firebase**.
3. Thẻ này hiện dự án, tài khoản dịch vụ, lần dùng cuối và lỗi gần nhất (nếu Google từ chối khoá).

Tệp được mã hoá khi lưu. Khi chưa có tệp, khách trong ứng dụng chỉ nhận email khi có trả lời, và chỉ khi họ đã để lại email. **Gỡ thông tin** xoá tệp.

### Ứng dụng iOS {#ios-apps}

Firebase chuyển thông báo tới iPhone qua APNs, nên dự án Firebase cần thêm:

1. Một **App ID** trên Apple Developer (Certificates, Identifiers & Profiles → Identifiers) có bundle ID **trùng chính xác** với ứng dụng và bật **Push Notifications**. Chỉ cần App ID, không cần tạo ứng dụng trên App Store Connect.
2. Một **APNs key** (Keys → +, tick Apple Push Notifications service). Chọn môi trường **Sandbox & Production** để dùng được cho bản debug, TestFlight và App Store. Tải tệp `.p8`, ghi lại Key ID và Team ID.
3. Firebase → Project settings → **Cloud Messaging → Apple app configuration** → tải key lên cùng Key ID và Team ID.

Khi App ID không khớp bundle ID (chỉ thừa một dấu chấm ở cuối cũng đủ) hoặc key thuộc team khác, FCM trả lỗi APNs `TopicDisallowed` cho mọi lần gửi. Hạ Tầng hiện lỗi này ở mục lỗi gần nhất của thẻ Firebase. Sửa App ID không cần build lại ứng dụng.

> **Note: APNs trực tiếp**
>
> Hạ Tầng chỉ gửi qua Firebase. SDK iOS đăng ký được token thiết bị APNs thô, và gateway nhận thiết bị có `provider: "apns"`, nhưng hiện chưa gửi gì tới chúng. Cho đến khi có gửi thẳng qua APNs, ứng dụng iOS nên dùng Firebase Messaging và đăng ký token FCM.

### Giới hạn API key Firebase của ứng dụng {#api-key}

API key trong cấu hình Firebase của ứng dụng (`google-services.json`, `GoogleService-Info.plist`) không phải bí mật: nó nằm trong mọi bản build. Giới hạn nó trong Google Cloud console → APIs & Services → Credentials → key → **API restrictions**, chỉ cho các API ứng dụng dùng để lấy token, như Firebase Installations API và FCM Registration API. Hạ Tầng không dùng key này; Hạ Tầng gửi bằng tài khoản dịch vụ.

## 2. Đăng ký thiết bị từ SDK {#register}

Ứng dụng lấy token FCM từ Firebase rồi giao cho SDK:

| SDK | Lệnh |
|---|---|
| Flutter | `HatangChat.instance.setPushToken(token, provider: PushProvider.fcm)` |
| React Native | `Hatang.setPushToken(token, "fcm")` |
| iOS | `await HatangChat.setPushToken(fcmToken: token)` |
| Android | `HatangChat.setPushToken(token)` (hàm suspend) |

SDK gọi `PUT /widget/v1/sites/{siteId}/devices` với token, provider, nền tảng và, nếu có, install id, ngôn ngữ, phiên bản ứng dụng, phiên bản hệ điều hành và model máy. Một token là một điện thoại. Khi danh tính của khách được [gộp](https://hatang.dev/vi/docs/guides/support/web-widget/identity-verification.md), thiết bị đi theo người. `clearPushToken()` gỡ token (`DELETE devices`); gọi nó khi đăng xuất, trước `reset()`.

Tab **Ứng dụng** của website liệt kê **Thiết bị đã đăng ký** cùng lần đẩy cuối.

## 3. Khi nào Hạ Tầng gửi đẩy {#when}

Với mỗi tin **công khai** từ nhân viên hoặc bot (không phải tin của khách, không phải ghi chú nội bộ), Hạ Tầng kiểm tra theo thứ tự:

| Điều kiện | Nếu không đạt |
|---|---|
| Hội thoại không bị đánh dấu spam, khách vẫn tồn tại | Bỏ qua |
| Khách có ít nhất một thiết bị đã đăng ký | Không đẩy. Email "chúng tôi đã trả lời" theo quy tắc riêng (khách đang offline và đã để lại email) |
| Khách **không** đang kết nối với ứng dụng ở tiền cảnh | Bỏ qua: tin đến qua socket |
| Khách **không** đang xem đúng hội thoại này (`app.state` kèm `conversation_id`) | Bỏ qua |
| Site đã có thông tin Firebase | Bỏ qua, ghi log một lần mỗi giờ cho mỗi site |
| Dưới 20 lần đẩy trong một phút cho khách này | Bỏ qua |

Sau đó Hạ Tầng gửi tới từng thiết bị. Token mà FCM báo không còn đăng ký thì bị xoá, lỗi tạm thời của FCM thì thử lại, lỗi thông tin đăng nhập (Google từ chối khoá) hiện ở mục lỗi gần nhất của thẻ Firebase. Khách đã có điện thoại đăng ký thì **không** nhận thêm email cho mỗi trả lời; email bản ghi khi hội thoại được giải quyết vẫn được gửi.

SDK gửi `app.state` với `foreground: false` ngay khi ứng dụng vào nền, nên "đang kết nối" nghĩa là ứng dụng thật sự ở trên màn hình. Một client không gửi `app.state` sẽ trông như online tới 60 giây sau khi vào nền và bỏ lỡ thông báo trong khoảng đó.

## 4. Payload {#payload}

```json
{
  "notification": { "title": "Hà (Mộc Hương)", "body": "Dạ shop còn hàng ạ" },
  "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 } }
  }
}
```

| Trường | Ý nghĩa |
|---|---|
| `notification.title` | Tên người gửi; tên site khi tin không có tên người gửi |
| `notification.body` | 140 ký tự đầu của tin (thẻ được rút thành chữ). Tin chỉ có tệp: "🖼 Ảnh" hoặc "📎 Tệp đính kèm", theo ngôn ngữ của khách |
| `data.*` | Chỉ có chuỗi. `hatang: "1"` để `isHatangNotification` nhận ra; `kind` là `message`, hoặc `test` với thông báo thử từ dashboard |
| `collapse_key`, `tag`, `apns-collapse-id`, `thread-id` | Id hội thoại: nhiều trả lời liên tiếp thay thế nhau và được nhóm theo hội thoại |
| `aps.badge` (iOS) | Tổng số tin chưa đọc của khách trong các hội thoại đang mở và đang chờ |
| `android.notification.channel_id` | `hatang_messages`. Trên Android 8+, tạo kênh có id này trong ứng dụng để đặt tên và mức quan trọng; không có thì Android dùng kênh dự phòng của Firebase |

Khi khách bấm thông báo, truyền `data` cho hàm `handleNotification` của SDK để mở đúng hội thoại. Trang của từng SDK chỉ cách nối sự kiện bấm khi khởi động lạnh và khi ứng dụng đang ở nền.

## 5. Gửi thử {#test}

Nút **Gửi thử** ở tab **Ứng dụng** của website gửi một thông báo cố định ("Thông báo thử từ Hạ Tầng: mọi thứ đã sẵn sàng.") tới thiết bị kết nối gần nhất của site, với `data` là `{ site_id, kind: "test", hatang: "1" }` và không cần hội thoại. Dùng nó để kiểm tra tệp Firebase, kênh Android và quyền thông báo trước khi phát hành. Chưa có thiết bị nào đăng ký thì dashboard báo "Chưa có thiết bị nào đăng ký nhận thông báo."

## Bước tiếp theo

- [Giao thức realtime](https://hatang.dev/vi/docs/reference/realtime-protocol.md): khung `app.state` và endpoint `PUT` / `DELETE devices`.
- [Nhận diện khách](https://hatang.dev/vi/docs/guides/support/web-widget/identity-verification.md): thiết bị đi theo người khi gộp danh tính.
- Nối sự kiện bấm trong SDK của bạn: [Flutter](https://hatang.dev/vi/docs/guides/sdks/flutter.md#push-notifications), [React Native](https://hatang.dev/vi/docs/guides/sdks/react-native.md#push-notifications), [iOS](https://hatang.dev/vi/docs/guides/sdks/ios.md#push-notifications), [Android](https://hatang.dev/vi/docs/guides/sdks/android.md#push-notifications).

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