PAYATHON 2026

如何在 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 可以被伪造,有些支付渠道也可能在交易最终入账前完成页面跳转。

更稳妥的流程如下:

  1. WebView 拦截 callback URL。
  2. 从 URL 中获取 orderId、transactionId 等业务标识。
  3. Flutter 调用业务服务器查询订单状态。
  4. 业务服务器通过支付平台接口或异步通知确认结果。
  5. 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 时,支付仍有可能成功,因此返回上一页后仍需向服务器刷新订单状态。

备注:内容仅供参考。