在移动应用开发中,对接支付网关 (PG) 通常是不可避免的一环。理想情况下,PG 提供商会给出封装完美的 Flutter SDK,我们只需几行代码即可调起支付界面。然而,现实往往是残酷的:许多本地化或特定行业的 PG 仅提供原生 Android/iOS SDK,或者干脆只给一个 Web 支付链接(Payment URL)。
本文将复盘我们在生产环境中遇到的真实案例:在没有专用 SDK 的情况下,如何利用 WebView 完美集成 PG 支付功能,并解决最棘手的 URL 拦截与应用间跳转(Deep Link)问题。
核心难点分析:为何 WebView 支付容易失败?
直接使用 webview_flutter 加载支付链接看似简单,但在实际对接复杂的 PG 时,你会立刻撞上以下墙壁:
- 非 HTTP 协议拦截: 现代支付流程通常涉及第三方应用跳转(如支付宝、微信支付、或是银行 App)。PG 会重定向到
alipays://或intent://等非 HTTP 协议的 URL。默认的 WebView 无法处理这些请求,会导致页面报错net::ERR_UNKNOWN_URL_SCHEME。 - Cookie 与 Session 同步: 某些 PG 依赖 Cookie 来维持会话状态,WebView 需要正确配置以确保持久化。
- 支付回调闭环: 支付完成后,PG 通常会重定向回商户定义的
Success URL。我们需要精准拦截这个 URL,关闭 WebView 并通知 Flutter 层更新 UI。
intent:// 协议,用户将无法唤起已安装的银行 App 进行 3D Secure 验证,直接导致支付转化率归零。
解决方案:构建智能拦截器
我们的核心策略是利用 NavigationDelegate 来接管所有的路由请求。我们需要引入 url_launcher 插件来处理非 HTTP 协议的外部跳转。
以下是经过生产环境验证的实现代码,重点在于处理 onNavigationRequest:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
class PaymentWebView extends StatefulWidget {
final String paymentUrl;
final String successUrl;
final String failUrl;
const PaymentWebView({
Key? key,
required this.paymentUrl,
required this.successUrl,
required this.failUrl,
}) : super(key: key);
@override
State<PaymentWebView> createState() => _PaymentWebViewState();
}
class _PaymentWebViewState extends State<PaymentWebView> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) async {
// 1. 拦截成功或失败的回调 URL
if (request.url.startsWith(widget.successUrl)) {
Navigator.pop(context, 'success');
return NavigationDecision.prevent;
}
if (request.url.startsWith(widget.failUrl)) {
Navigator.pop(context, 'fail');
return NavigationDecision.prevent;
}
// 2. 处理非 HTTP/HTTPS 协议 (关键逻辑)
final Uri uri = Uri.parse(request.url);
if (uri.scheme != 'http' && uri.scheme != 'https') {
// 尝试唤起外部应用 (如支付宝、银行App)
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
return NavigationDecision.prevent; // 阻止 WebView 继续加载
} else {
debugPrint("无法处理该协议: ${uri.scheme}");
// 可选:提示用户未安装对应 App
}
}
// 3. 允许正常的网页跳转
return NavigationDecision.navigate;
},
onPageStarted: (String url) {
debugPrint('Page started loading: $url');
},
onWebResourceError: (WebResourceError error) {
debugPrint('Web resource error: ${error.description}');
},
),
)
..loadRequest(Uri.parse(widget.paymentUrl));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('安全支付')),
body: WebViewWidget(controller: _controller),
);
}
}
LaunchMode.externalApplication 是关键。它告诉系统脱离当前 App 的 WebView 容器,去操作系统层面寻找能够响应此协议的应用。这对于处理 Android 的 intent:// 机制至关重要。
进阶:处理 Android Intent URL 解析
有些旧版 PG 返回的 URL 格式是复杂的 intent://...#Intent;scheme=...;package=...;end。直接传给 url_launcher 可能会失败。在这种情况下,你需要手动解析出真实的 scheme 或 fallback URL。
| 场景 | 典型 Scheme | 处理策略 |
|---|---|---|
| 支付宝 | alipays:// |
直接 launchUrl,需配置 Info.plist/Manifest |
| 微信支付 | weixin:// |
通常需要 Referer 头,WebView 中较难处理,建议 SDK |
| 韩国 ISP/银行 | ispmobile://, kb-acp:// |
必须拦截并使用 externalApplication 模式 |
| Android Intent | intent:// |
需解析 scheme 字段,若无法解析则跳转 fallback URL |
结论
虽然没有原生 SDK 会增加开发的复杂度,但通过精细控制 Flutter 的 WebView 行为,我们依然可以构建出流畅的支付体验。核心在于正确区分“页面加载”与“应用跳转”。
务必记住,支付流程涉及资金安全,在上线前请务必在真机上测试所有边界情况,特别是用户未安装目标银行 App 时的 Fallback 逻辑,以及支付中途切后台再切回来的状态恢复。
Post a Comment