如何在 Flutter 中使用 webview_flutter 处理支付回调 URL?
支付老李
结论
可以使用 NavigationDelegate 的 onNavigationRequest 监听 WebView 页面跳转,识别支付平台重定向到的 callback URL。
拦截到回调地址后,返回 NavigationDecision.prevent,阻止 WebView 继续加载。随后可根据 URL 参数更新页面、关闭支付页,或向业务服务器查询最终支付结果。
为什么使用 onNavigationRequest
支付完成后,支付页面通常会通过 HTTP 重定向、表单提交或 JavaScript 跳转到商户配置的回调地址。WebView 导航时,onNavigationRequest 会提供目标 URL,很适合在客户端拦截这类跳转。
onPageFinished 也能监听页面加载完成,但不宜作为首选:
- 回调地址可能使用自定义 Scheme,例如
myapp://payment/callback,不一定能正常加载完成。 - 等页面加载完成后再处理,用户可能已经看到错误页或空白页。
- 某些支付流程会发生多次重定向,等待最终页面加载会带来更多不确定性。
基本实现
下面的示例处理了支付成功、失败和取消状态:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
enum PaymentResult {
success,
failed,
cancelled,
unknown,
}
class PaymentPage extends StatefulWidget {
const PaymentPage({
super.key,
required this.paymentUrl,
});
final String paymentUrl;
@override
State<PaymentPage> createState() => _PaymentPageState();
}
class _PaymentPageState extends State<PaymentPage> {
late final WebViewController _controller;
bool _callbackHandled = false;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
final uri = Uri.tryParse(request.url);
if (uri != null && _isPaymentCallback(uri)) {
_handlePaymentCallback(uri);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onWebResourceError: (WebResourceError error) {
// 这里只处理 WebView 加载错误,不应直接视为支付失败。
debugPrint('WebView error: ${error.description}');
},
),
)
..loadRequest(Uri.parse(widget.paymentUrl));
}
bool _isPaymentCallback(Uri uri) {
// HTTPS 回调示例:
// https://example.com/payment/callback?status=success
return uri.scheme == 'https' &&
uri.host == 'example.com' &&
uri.path == '/payment/callback';
// 如果使用自定义 Scheme,可以改成:
// return uri.scheme == 'myapp' &&
// uri.host == 'payment' &&
// uri.path == '/callback';
}
void _handlePaymentCallback(Uri uri) {
if (_callbackHandled) {
return;
}
_callbackHandled = true;
final status = uri.queryParameters['status'];
final result = switch (status) {
'success' => PaymentResult.success,
'failed' => PaymentResult.failed,
'cancelled' => PaymentResult.cancelled,
_ => PaymentResult.unknown,
};
if (!mounted) {
return;
}
Navigator.of(context).pop(result);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('支付')),
body: WebViewWidget(controller: _controller),
);
}
}
调用支付页面并接收结果:
final result = await Navigator.of(context).push<PaymentResult>(
MaterialPageRoute(
builder: (_) => const PaymentPage(
paymentUrl: 'https://payment.example.com/checkout',
),
),
);
switch (result) {
case PaymentResult.success:
// 刷新订单状态或跳转到支付成功页。
break;
case PaymentResult.failed:
// 提示支付失败。
break;
case PaymentResult.cancelled:
// 处理用户取消支付。
break;
case PaymentResult.unknown:
case null:
// 处理未知状态或用户直接关闭页面。
break;
}
更可靠的处理方式
不要只根据 callback URL 中的 status=success 判断订单已经支付。客户端 URL 可以被伪造,有些支付渠道也可能在交易最终入账前完成页面跳转。
更稳妥的流程如下:
- WebView 拦截 callback URL。
- 从 URL 中获取
orderId、transactionId等业务标识。 - Flutter 调用业务服务器查询订单状态。
- 业务服务器通过支付平台接口或异步通知确认结果。
- Flutter 根据服务器返回的状态展示成功、失败或处理中。
例如:
Future<void> _handlePaymentCallback(Uri uri) async {
if (_callbackHandled) {
return;
}
_callbackHandled = true;
final orderId = uri.queryParameters['orderId'];
if (orderId == null || orderId.isEmpty) {
_callbackHandled = false;
return;
}
try {
final result = await queryPaymentResultFromServer(orderId);
if (!mounted) {
return;
}
Navigator.of(context).pop(result);
} catch (error) {
_callbackHandled = false;
if (!mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('暂时无法确认支付结果,请稍后重试')),
);
}
}
queryPaymentResultFromServer 需要根据项目使用的网络库和后端接口自行实现。
兼容旧版写法
较早版本的 webview_flutter 使用 WebView 组件和 navigationDelegate 参数,但处理思路相同:
WebView(
initialUrl: paymentUrl,
javascriptMode: JavascriptMode.unrestricted,
navigationDelegate: (NavigationRequest request) {
final uri = Uri.tryParse(request.url);
if (uri != null &&
uri.host == 'example.com' &&
uri.path == '/payment/callback') {
handlePaymentCallback(uri);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
)
应根据项目当前安装的 webview_flutter API 选择对应写法,不要混用新旧接口。
注意事项
- 使用
Uri解析 URL,不要仅通过contains('success')判断结果,以免误匹配。 - 同时校验
scheme、host和path,必要时还要校验订单号或签名。 - 支付页面可能连续触发多次重定向,需要增加防重复处理逻辑。
onWebResourceError表示网页资源或导航加载失败,不能直接判定支付失败。- 如果支付平台通过新窗口打开页面,需要确认当前 WebView 能否捕获其跳转。
- 自定义 Scheme、App Link 或 Universal Link 还涉及 Android 和 iOS 原生配置。如果回调始终留在 WebView 内,通常通过导航代理拦截即可。
- 用户关闭 WebView 时,支付仍有可能成功,因此返回上一页后仍需向服务器刷新订单状态。
备注:内容仅供参考。
