# 把第三方 webhook 推到用户正在看的页面

> 付款确认、包裹移动、构建完成：把 Stripe、物流或 GitHub 的 webhook 转到用户正在看的页面，无需刷新。

Source: https://hibira.io/zh-cn/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-cn/use-cases/job-progress)。
- 只转发页面需要的内容（状态和 ID），不要转发服务商的原始内容：里面可能有个人数据。
- 用户已经关掉页面的话，按[推送通知指南](https://hibira.io/zh-cn/guides/push-notifications)发送推送。
- 整个团队共用的仪表盘，可以一个团队一个频道，不必每张订单一个。
