# 把第三方 webhook 推到使用者正在看的頁面

> 付款確認、包裹移動、建置完成：把 Stripe、物流或 GitHub 的 webhook 轉到使用者正在看的頁面，不必重新整理。

Source: https://hibira.io/zh-tw/use-cases/webhooks-to-browser

顧客在 Stripe Checkout 付款後回到你的「感謝」頁面，但付款是由 webhook 確認的，可能晚幾秒才到。頁面顯示「正在確認付款…」，顧客只好重新整理，甚至再付一次。物流、CI 服務、電子簽章工具和電商平台也是一樣：消息送到的是你的伺服器，而不是使用者正在看的頁面。用 Hibira，webhook 處理程式一收到更新，就把它轉給那個頁面。

## 使用者得到什麼

- 在正在看的頁面上直接出現「已收到付款」，不必重新整理。
- 物流、建置和簽署進度自己出現。
- 不會再有人以為第一次失敗而重複付款。

## 用到的功能

- **私人頻道**（`private-order-42`），只有訂單本人可以加入，由你的授權端點檢查。
- 在驗證服務商簽章並儲存變更的同一個 webhook 處理程式裡呼叫 **`trigger`**。
- **`pusher:subscription_succeeded`**，用來讀一次目前狀態，頁面打開前就到的 webhook 也不會漏掉。

```text
sequence: 服務商通知你的伺服器；你的伺服器經由 Hibira 通知頁面
瀏覽器 | 你的伺服器 | Stripe | *Hibira
1 > 3: 在 Checkout 付款
3 >> 1: 導回 /orders/42 | 「正在確認付款…」
3 > 2: webhook | checkout.session.completed
2 > 4: trigger("status") | private-order-42
4 > 1: status: paid | 顯示收據
```

## 步驟 1：儲存後轉發 webhook

驗證簽章、略過已處理過的事件（服務商會重試）、儲存新狀態，再發布：

```js
app.post('/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
  if (await db.webhookEvents.seen(event.id)) return res.sendStatus(200);
  if (event.type === 'checkout.session.completed') {
    const orderId = event.data.object.metadata.orderId;
    await db.orders.update(orderId, { status: 'paid' });
    await pusher.trigger(`private-order-${orderId}`, 'status', { status: 'paid' });
  }
  res.sendStatus(200);
});
```

## 步驟 2：只讓本人收聽

由你的頻道授權端點決定誰可以訂閱：

```js
app.post('/pusher/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
  const id = req.body.channel_name.match(/^private-order-(.+)$/)?.[1];
  if (!id || !(await db.orders.ownedBy(id, req.user.id))) return res.sendStatus(403);
  res.json(pusher.authorizeChannel(req.body.socket_id, req.body.channel_name));
});
```

## 步驟 3：頁面上收聽，並讀一次狀態

先訂閱，訂閱就緒時再讀一次目前狀態，這樣不管 webhook 比頁面早到還是晚到都沒關係：

```js
const channel = pusher.subscribe('private-order-42');
channel.bind('status', ({ status }) => show(status));
channel.bind('pusher:subscription_succeeded', async () => {
  const order = await fetch('/api/orders/42').then((r) => r.json());
  show(order.status);
});
```

## 其他 webhook 也是同一個做法

| 來源 | Webhook | 打開的頁面顯示 |
| --- | --- | --- |
| Stripe、PayPal、Adyen | 付款成功或失敗 | 收據，或重試按鈕 |
| 物流業者 | 追蹤更新 | 訂單頁上的新進度 |
| GitHub、GitLab、Vercel | 建置或部署完成 | 狀態標籤和預覽連結 |
| 電子簽章工具 | 文件已簽署 | 「雙方已簽署」 |
| Shopify、電商平台 | 訂單成立或已付款 | 賣家儀表板上多一列 |

## 小提醒

- 盡快回應服務商 200。慢的工作放在回應之後或背景工作裡做，請看[長時間工作的進度](https://hibira.io/zh-tw/use-cases/job-progress)。
- 只轉發頁面需要的內容（狀態和 ID），不要轉發服務商的原始內容：裡面可能有個人資料。
- 使用者已經關掉頁面的話，照[推播通知指南](https://hibira.io/zh-tw/guides/push-notifications)發送推播。
- 整個團隊共用的儀表板，可以一個團隊一個頻道，不必每張訂單一個。
