【Flutter】Stripe支付集成流程

Flutter-Stripe支付框架集成过程

前言

在国内我们的应用支付大家都是微信和支付宝了,但是海外应用的支付场景还是以信用卡支付为主,这里我们的应用选择的是 Stripe 支付平台。

这里我们选择的是 官方的 flutter_stripe 插件,它是Flutter 与 Stripe 的深度结合,为跨平台支付集成提供了高效解决方案。相较于原生开发中冗杂的支付接口适配工作,Flutter 通过 flutter_stripe 库实现了客户端与 Stripe 服务的标准化对接,开发者可专注于业务逻辑而非底层协议。

其实我在三四年前就已经用过这个框架,我以为不会有什么问题,谁知道现在更新改版之后我都不认识了。初次接触这个框架的同学可能也会有疑问怎么这么多功能这么多场景,到底怎么用?

这里我就以 Stripe 的老版使用方法和新版使用方法过渡,为什么 Stripe 要这么改版。实在是 Stripe 怕大家不会用超碎了心。

接下来就一起看看吧。

一、自定义Card的方案

早期的时候 Stripe 的用法是,我们通过 Stripe 创建支付意图 (PaymentIntent),然后通过自定义的UI的方式手机用户的银行卡信息。

ini 复制代码
          //选择银行卡去支付调用Stripe的支付
            String expireTimeStr = mResponseData.month;
            String[] expireTime = expireTimeStr.split("/");
            Card card = Card.create(mResponseData.number, Integer.parseInt(expireTime[0]), Integer.parseInt(expireTime[1]), mResponseData.cvv);

            createStripePayment(card);

是的所有的信息都需要自己处理,只需要在 Card.create 的时候填入正确的参数即可。

然后我们需要在客户端创建支付方式,调用后台接口获取到支付意图:

ini 复制代码
   PaymentMethodCreateParams.Card paramsCard = card.toPaymentMethodParamsCard();
        PaymentMethodCreateParams paymentMethodCreateParams = PaymentMethodCreateParams.create(paramsCard);
        BaseApplication.mStripe.createPaymentMethod(paymentMethodCreateParams, new ApiResultCallback<PaymentMethod>() {
            @Override
            public void onSuccess(PaymentMethod paymentMethod) {
                String paymentId = paymentMethod.id;

                mViewModel.getPaymentIntent(mJobId, paymentId);
            }

            @Override
            public void onError(@NotNull Exception e) {
                e.printStackTrace();

                ToastUtils.get().makeText(mActivity, "Payment Error");
                LoadingDialogManager.get().dismissLoading();
            }
        });

最后在调用支付的API去支付

ini 复制代码
 BaseApplication.mStripe.confirmPayment(XXX);

整体流程:

  1. 先初始化 Stripe SDK
  2. 创建 Card 对象,创建支付方式
  3. 调用接口创建 PaymentIntent
  4. 调用 Stripe API confirmPayment 去发起支付

说实话,我之前写起来怎么没觉得这么麻烦,好在现在方便了不少。

二、集成Card方案

接下来 Stripe 可能是觉得这过程太麻烦了,Stripe 就给直接提供了集成式UI ,对内部判断卡类型,判断卡号 CVV 是否正确等逻辑进行了封装。

于是就有了现在的基于 Card 的集成,为了适配你的设计它还内置了两种 Card 的样式 (PS:你想的太天真,我们设计师怎会按照你的设计来)

于是就有了横向的 Card 样式 (Cardfield):

php 复制代码
    CardField(
        onCardChanged: (card) {
            Log.d("card: $card");
        },
        ),

和 Card 表单的样式 (Card form):

scss 复制代码
  final controller = CardFormEditController();

  @override
  void initState() {
    controller.addListener(update);
    super.initState();
  }

  void update() => setState(() {});
  @override
  void dispose() {
    controller.removeListener(update);
    controller.dispose();
    super.dispose();


  CardFormField(
        controller: controller,
        )

当然除了以上两种内置的 Card 的控件,我们还是一样可以使用自定义UI,自行校验,自行封装Card对象,你要是不嫌麻烦也是一样的效果。

总的来说基于这种方法的流程就是

  1. 先初始化 Stripe SDK
  2. 调用接口创建 PaymentIntent
  3. 调用 Stripe API confirmPayment 去发起支付,传入返回的clientSecret与Card对象即可调起支付

步骤相比最初已经有些简化了。

三、Payment sheet方案

其实用 SDK 提供的 Card 控件已经很方便了,封装了各种校验逻辑很方便,但是实在是丑。于是 Stipe 就搞了一个场景化的封装叫 Payment sheet 。

它的UI效果更好,是基于 BottomSheet 的一个弹窗,内部统一了UI,并且对 Card 控件进行了集成,我们无需关心 Card 的控件以及对应的监听,Card的校验与对象封装。

并且在 Payment sheet 中默认支持银行卡,除此之外还能配置苹果支付,谷歌支付,支付宝和微信支付也能配置。

我们仅仅只需要配置 Payment sheet 的参数,直接展示 Payment sheet 即可完成全部流程。

php 复制代码
  await Stripe.instance.initPaymentSheet(
      paymentSheetParameters: SetupPaymentSheetParameters(
        customFlow: false,
        paymentIntentClientSecret: clientSecret,
        merchantDisplayName: 'Your App Demo',
        style: ThemeMode.light,
        allowsDelayedPaymentMethods: false,
      ),
    );   

然后展示这个弹窗即可:

csharp 复制代码
 await Stripe.instance.presentPaymentSheet();

总的来说基于这种方法的流程就是

  1. 先初始化 Stripe SDK
  2. 调用接口创建 PaymentIntent
  3. 调用 Stripe API confirmPayment 去发起支付,传入返回的clientSecret与initPaymentSheet对象即可调起PaymentSheet

效果:

三、Payment sheet方案封装

完整的代码给出,基于 Payment sheet 的封装方案,搞一个服务类,我用的 riverpod 就写了个对应的 provider,设置为单例。

如果你没有用 riverpod 你可以用 getx 的 GetService 也行,如果你也没用 getx 那么你直接写单例也是可以的。

stripe_service.dart

dart 复制代码
import 'package:domain/repository/payment_repository.dart';
import 'package:flutter/material.dart';
import 'package:plugin_basic/constants/app_constant.dart';
import 'package:plugin_platform/engine/notify/notify_engine.dart';
import 'package:plugin_platform/engine/toast/toast_engine.dart';
import 'package:plugin_platform/platform_export.dart';
import 'package:shared/utils/log_utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared/utils/util.dart';
import 'package:shared/utils/event_bus.dart';

part 'stripe_service.g.dart';

@Riverpod(keepAlive: true)
StripeService stripeService(Ref ref) {
  return StripeService(ref.read(paymentRepositoryProvider));
}

class StripeService {
  final PaymentRepository _paymentRepository;

  StripeService(this._paymentRepository); // 依赖注入 Dio 实例

  bool _isInitialized = false;

  // 4242 4242 4242 4242 -- Visa (无需 3D)
  // 4000 0025 0000 3155 -- 需要 3D Secure 验证

  //初始化 Stripe
  Future<void> _ensureInitialized() async {
    if (_isInitialized) return;

    Stripe.publishableKey = 'pk_test_51RMmxaRpg7SPAcNndAqMUMEOkRFsY5mL0JqJmdunL3vspxrYsGjGARQddaVu3ZQVEy1e1WTF8yalt0cYZCXXXXXXXXXX';
    await Stripe.instance.applySettings();

    Log.d("Stripe 初始化完成");
    _isInitialized = true;
  }

  ///执行支付
  Future<bool> executePayment({
    required String orderId,
  }) async {
    try {
      //尝试初始化 Stripe SDK
      await _ensureInitialized();

      //从服务端获取 clientSecret
      final clientSecret = await _fetchPaymentIntent(orderId);

      await _initializePaymentSheet(clientSecret);
      await Stripe.instance.presentPaymentSheet();

      Log.d("presentPaymentSheet 关闭,用户操作完成 !");

      return await _verifyPaymentResult(clientSecret);

    } on StripeException catch (e) {
      // Stripe SDK 出错
      _handleStripeError(e);
      return false;
    } catch (e) {
      // 其他的错误捕获
      _handleGenericError(e);
      return false;
    }
  }

  ///调用网络请求
  Future<String> _fetchPaymentIntent(String orderId) async {
    final result = await _paymentRepository.obtainPaymentIntent(orderId: orderId);
    if (result.isSuccess) {
      if (Utils.isNotEmpty(result.data?.clientSecret)) {
        return result.data!.clientSecret!;
      } else {
        throw Exception("Empty client secret");
      }
    } else {
      ToastEngine.show(result.errorMsg ?? "UnKnow Error");
      throw Exception("Failed to get payment intent");
    }
  }

  Future<void> _initializePaymentSheet(String clientSecret) async {
    await Stripe.instance.initPaymentSheet(
      paymentSheetParameters: SetupPaymentSheetParameters(
        customFlow: false,
        paymentIntentClientSecret: clientSecret,
        merchantDisplayName: 'Your App Demo',
        style: ThemeMode.light,
        allowsDelayedPaymentMethods: false,
      ),
    );
  }

  Future<bool> _verifyPaymentResult(String clientSecret) async {
    final paymentIntent = await Stripe.instance.retrievePaymentIntent(clientSecret);

    switch (paymentIntent.status) {
      case PaymentIntentsStatus.Succeeded:
        NotifyEngine.showSuccess('支付成功');
        //发送 EventBus 事件
        bus.emit(AppConstant.eventStripePaymentSuccess, true);
        return true;
      case PaymentIntentsStatus.RequiresPaymentMethod:
        NotifyEngine.showFailure('支付方式无效');
        return false;
      case PaymentIntentsStatus.Processing:
        return await _handleProcessingPayment(clientSecret);
      default:
        return false;
    }
  }

  Future<bool> _handleProcessingPayment(String clientSecret) async {
    // 处理需要等待的支付状态
    await Future.delayed(const Duration(seconds: 2));
    final updatedIntent = await Stripe.instance.retrievePaymentIntent(clientSecret);

    return updatedIntent.status == PaymentIntentsStatus.Succeeded;
  }

  void _handleStripeError(StripeException e) {
    final errorMessage = e.error.localizedMessage ?? '支付失败';
    Log.e('Stripe Error: ${e.error.code} - $errorMessage');
    NotifyEngine.showError(errorMessage);
  }

  void _handleGenericError(dynamic e) {
    Log.e('System Error: $e');
    NotifyEngine.showError('支付异常:$e');
  }
}

使用的是:

scss 复制代码
ref.read(stripeServiceProvider).executePayment(orderId: orderId);
或者
globalContainer.read(stripeServiceProvider).executePayment(orderId: orderId);

传入订单id,生成对应的 paymentIntent,然后初始化 PaymentSheet,展示出来。

注意要点:

iOS没什么坑点,需设置最小支持版本为iOS13 即可

ruby 复制代码
platform :ios, '13.0'

而Android就需要按照它的文档推荐:

其他的条件我们基本都满足了,剩下的还需要我们处理:

样式需要在res/values/style.xml中修改

ini 复制代码
<style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">

在 gradle.properties 中添加对应的配置

混淆规则添加进去

这样就可以运行和打包测试了。

后记

本文我们介绍了 Stripe 的 SDK 变化历程,以及可用的几种方案。

我个人建议的话,如果有自定义UI的需求,可以直接用最原始的 Card 对象封装的方案,自己画输入框。如果没有自定义 UI 的需求,可以直接用 PaymentSheet 的方案更加的简单和方便。

由于我目前自用的是 PaymentSheet 方案所以只给出了对应的源码,其他的方案是我早期用过的现在不能用了就没有源码了,不过思路在这里并不复杂。

OK,那么今天的分享就到这里啦,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

这一期就此完结了。

相关推荐
北极象22 分钟前
在Flutter中定义全局对象(如$http)而不需要import
网络协议·flutter·http
明似水2 小时前
Flutter 包依赖升级指南:让项目保持最新状态
前端·flutter
唯有选择7 小时前
flutter_localizations:轻松实现Flutter国际化
flutter
Chuck_Chan8 小时前
Launcher3体系化之路
android·app
初遇你时动了情1 天前
dart常用语法详解/数组list/map数据/class类详解
数据结构·flutter·list
OldBirds1 天前
Flutter element 复用:隐藏的风险
flutter
爱意随风起风止意难平1 天前
002 flutter基础 初始文件讲解(1)
学习·flutter
OldBirds1 天前
理解 Flutter Element 复用
flutter
xq95271 天前
flutter 带你玩转flutter读取本地json并展示UI
flutter
hepherd1 天前
Flutter - 原生交互 - 相机Camera - 01
flutter·ios·dart