如何在 Flutter Web 中集成第三方支付并获取回调?
结论
Flutter Web 不能直接监听第三方支付页面内部的事件。要获取支付进度和结果,需要根据支付网关提供的能力选择相应方式:
- 使用 JavaScript SDK 时,通过 Dart/JavaScript interop 接收 SDK 回调。
- 使用 iframe 或弹窗时,通过
window.postMessage()接收消息,但支付网关必须明确支持这种方式。 - 使用页面跳转支付时,通过
return_url返回 Flutter Web,再从 URL 中读取订单信息。 - 最终支付结果必须由服务端接收 webhook,并通过验签或调用支付网关接口查询确认。
浏览器回调适合更新页面状态,不能作为发货、充值或开通服务的唯一依据。
为什么 Flutter Web 不能直接获得支付页面事件
第三方支付页面通常位于其他域名。受浏览器同源策略限制,Flutter Web 无法读取该页面的 DOM、JavaScript 状态和网络请求。
即使 Payment UI 已经正常显示,Flutter 的点击事件和组件状态也无法判断用户是否完成支付。业务系统只能通过支付网关提供的 SDK callback、页面跳转、postMessage 或服务端 webhook 获知结果。
完整流程通常是:
- Flutter Web 请求业务服务端创建支付订单。
- 服务端使用支付平台密钥创建支付会话。
- Flutter Web 打开支付 SDK、iframe、弹窗或支付页面。
- 浏览器回调用于显示“支付处理中”等状态。
- 支付平台向业务服务端发送 webhook。
- 服务端验签并更新订单。
- Flutter Web 查询业务服务端,获取最终订单状态。
通过 JavaScript SDK 接收回调
如果支付网关提供 Web JavaScript SDK,可以先在 web/index.html 中封装 SDK,再通过 dart:js_interop 把结果传给 Flutter。
以下代码中的 PaymentGateway、open 和回调名称都是示例,必须替换成实际支付网关提供的 API。
<script src="https://example-payment.com/sdk.js"></script>
<script>
window.openPayment = function (paymentOptions, callback) {
const options = JSON.parse(paymentOptions);
PaymentGateway.open({
orderId: options.orderId,
onOpen: function () {
callback(JSON.stringify({
type: "opened"
}));
},
onSuccess: function (result) {
callback(JSON.stringify({
type: "success",
paymentId: result.paymentId,
orderId: result.orderId
}));
},
onFailure: function (error) {
callback(JSON.stringify({
type: "failure",
message: error.message
}));
},
onClose: function () {
callback(JSON.stringify({
type: "closed"
}));
}
});
};
</script>
在 Flutter Web 中声明这个 JavaScript 方法:
import 'dart:convert';
import 'dart:js_interop';
@JS('openPayment')
external void openPayment(JSString options, JSFunction callback);
void startPayment(String orderId) {
final options = jsonEncode({
'orderId': orderId,
});
openPayment(
options.toJS,
((JSString payload) {
final result =
jsonDecode(payload.toDart) as Map<String, dynamic>;
switch (result['type']) {
case 'opened':
print('支付窗口已打开');
break;
case 'success':
print('浏览器端收到支付成功通知');
verifyOrderWithBackend(orderId);
break;
case 'failure':
print('支付失败:${result['message']}');
break;
case 'closed':
print('用户关闭了支付窗口');
break;
}
}).toJS,
);
}
Future<void> verifyOrderWithBackend(String orderId) async {
// 向业务服务端查询订单状态。
// 不要仅根据 JavaScript 的 success 回调直接发货。
}
这里的 success 只说明 SDK 在浏览器端报告支付成功。收到回调后,Flutter Web 仍需向自己的服务端查询订单状态。
通过 iframe 或弹窗接收消息
如果支付页面运行在 iframe 或新窗口中,只有支付网关明确支持 window.postMessage(),父页面才能收到消息。
父页面可以监听 message:
<script>
window.addEventListener("message", function (event) {
if (event.origin !== "https://secure.example-payment.com") {
return;
}
const message = event.data;
if (message && message.type === "payment_result") {
window.dispatchEvent(
new CustomEvent("payment-result", {
detail: JSON.stringify(message)
})
);
}
});
</script>
必须严格验证 event.origin,不能像下面这样无条件接收消息:
// 不安全:任何页面都可以伪造支付消息
window.addEventListener("message", function (event) {
handlePaymentResult(event.data);
});
如果支付网关没有实现 postMessage(),业务页面就无法自行读取跨域 iframe 中的支付结果。此时需要改用该网关的 JavaScript SDK、跳转回调或服务端 webhook。
通过 return_url 处理页面跳转
部分支付网关会在用户完成或取消支付后,把浏览器跳转到创建支付时指定的 return_url,例如:
https://shop.example.com/payment/result?orderId=ORDER_123
Flutter Web 可以读取当前 URL:
void handlePaymentReturn() {
final uri = Uri.base;
final orderId = uri.queryParameters['orderId'];
if (orderId == null || orderId.isEmpty) {
return;
}
verifyOrderWithBackend(orderId);
}
不要直接信任这类参数:
?status=success
用户可以修改 URL 参数。返回页面应该只使用 orderId 等业务标识向服务端查询,不能直接把订单改为已支付。
如果项目使用 hash 路由,还要确认查询参数的具体位置。例如:
https://shop.example.com/#/payment/result?orderId=ORDER_123
此时参数可能位于 Uri.base.fragment 中,需要按照项目实际使用的路由配置解析。
服务端 webhook 才是最终依据
可靠的支付集成必须接入服务端通知。典型的处理逻辑如下:
app.post(
"/payment/webhook",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.headers["x-payment-signature"];
const rawBody = req.body;
const valid = verifyGatewaySignature(rawBody, signature);
if (!valid) {
return res.status(400).send("invalid signature");
}
const event = JSON.parse(rawBody.toString("utf8"));
if (event.type === "payment.succeeded") {
await markOrderPaidIdempotently(
event.data.orderId,
event.data.paymentId
);
}
res.status(200).send("ok");
}
);
x-payment-signature、事件结构和验签算法只是占位示例,实际实现必须以所用支付网关的官方文档为准。
处理 webhook 时还要满足以下要求:
- 使用原始请求体验签,避免 JSON 解析改变请求内容。
- 校验订单号、金额、币种和商户身份。
- 做好幂等处理,防止重复通知造成重复发货。
- 保存支付平台的交易号和事件号。
- 只有通过验证的成功事件才能改变订单状态。
- webhook 地址必须是支付平台可以访问的 HTTPS 服务端地址,不能指向 Flutter 页面。
推荐实现
如果所用支付网关没有特殊限制,可以采用以下方案:
Flutter Web
│
├── 请求服务端创建订单
│
├── 打开支付 SDK 或支付页面
│
└── 根据 orderId 查询订单状态
▲
│
业务服务端 ──┼── 创建支付会话
├── 接收并验证 webhook
└── 保存最终支付状态
浏览器端 callback 用于改善交互体验,比如显示支付窗口已经打开、用户已经取消或系统正在确认。服务端 webhook 则负责确认真实的支付结果。
注意事项
- 不要把商户密钥、签名密钥或私钥放入 Flutter Web。用户可能查看 Web 构建产物中的所有内容。
- 不要把“支付弹窗关闭”直接判定为支付失败。用户可能已经完成支付,只是在回调到达前关闭了页面。
- 不要根据 SDK 的
success回调直接发货。 - 不要尝试读取跨域 iframe 的 DOM,浏览器同源策略会阻止这种操作。
- CORS 只能控制跨域 HTTP 请求,不能让 Flutter Web 读取第三方 iframe 的内部状态。
- 支付成功页可以短时间轮询服务端的订单状态,但需要设置轮询间隔、超时和最大重试次数。
- 如果支付网关同时提供前端 callback 和 webhook,应同时接入,因为两者用途不同。
- 回调名称、签名方式和事件字段由所选支付网关决定,不能直接在不同平台之间套用。
备注:内容仅供参考。
