flutter 实现ios内购 iap

in_app_purchase: ^3.1.13

用谷歌推出的内购插件,这个插件就是把ios内购api全部翻译一遍
所以原生能实现的功能 这个插件完全可以实现

要在初始化时候先注册下平台,因为安卓是谷歌支付,我们只用ios的内购
我的下面代码有很多异常处理,没完成订单查询等, 连续出错几次 直接finish等,确保丢单率最低,并且写入本地文件日志,有问题可以排查

 if (defaultTargetPlatform == TargetPlatform.iOS) {
      InAppPurchaseStoreKitPlatform.registerPlatform();
    }
    Future.delayed(const Duration(seconds: 1), () {
      delayAction();
    });

  delayAction() async {
    if (LoginTool.isLogin()) {
      if (defaultTargetPlatform == TargetPlatform.iOS) {
        //内购监听
        await IOSPayment.instance.init();
        IOSPayment.instance.startSubscription();
        // 查询没完成订单 进入支付页面 调用一下
        // TDIOSPayment.instance.checkUnfinishedPayment(() => null);
      }
    }
  }

开始支付时候调用
    IOSPayment.instance.isPaying = true;
    IOSPayment.instance.iosStartPay('xxxxxx', orderId,
        iapCallback: (isSuccess, errorMsg) {
      Future.delayed(const Duration(seconds: 1), () {
        TDIOSPayment.instance.isPaying = false;
      });
    });

isPaying设置 主要在支付期间 不让里面进入后台调用检查没完成的订单


typedef IapCallback = void Function(bool isSuccess, String errorMsg);

class IOSPayment with WidgetsBindingObserver {
  listener() {
    if (LoginTool.isLogin()) {
      IOSPayment.instance.startSubscription();
    } else {
      IOSPayment.instance.stopIapListen();
    }
  }

  /// 单例模式
  IOSPayment._();

  init() async {
    //登录状态监听
    kLoginChangeNotifier.addListener(listener);
  }

  static IOSPayment? _instance;
  static IOSPayment get instance => _getOrCreateInstance();
  static IOSPayment _getOrCreateInstance() {
    if (_instance != null) {
      return _instance!;
    } else {
      _instance = IOSPayment._();
      return _instance!;
    }
  }

  bool hasBindAppLife = false;

  IapCache iapCache = IapCache();

  // 应用内支付实例
  final InAppPurchasePlatform iosPurchase = InAppPurchasePlatform.instance;

  // iOS订阅监听
  StreamSubscription<List<PurchaseDetails>>? _subscription;

  /// 判断是否可以使用支付
  Future<bool> isAvailable() async => await iosPurchase.isAvailable();

  ///监听应用生命周期变化
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    jdLog(':didChangeAppLifecycleState:$state');
    if (state == AppLifecycleState.resumed) {
      //从后台切换前台,界面可见
      if (!isPaying) {
        checkUnfinishedPayment(() => null);
      }
    }
  }

  // 开始订阅 app一启动或者登录就订阅 看是否有支付完成的订单
  void startSubscription() async {
    if (!LoginTool.isLogin()) {
      return;
    }

    if (_subscription != null) {
      return;
    }
    if (!hasBindAppLife) {
      WidgetsBinding.instance.addObserver(this);
      hasBindAppLife = true;
    }
    jdLog('iap 开始订阅 -------->');
    // 支付消息订阅
    Stream purchaseStream = iosPurchase.purchaseStream;
    _subscription = purchaseStream.listen((purchaseDetailsList) {
      purchaseDetailsList.forEach(_handleReportedPurchaseState);
    }, onDone: () {
      jdLog('iap 开始订阅onDone -------->');
      // _subscription?.cancel();
      jdLog("onDone");
    }, onError: (error) {
      jdLog("error");
    }) as StreamSubscription<List<PurchaseDetails>>?;
  }

  bool isPaying = false;
  IapCallback? currentIapCallBack;
  // 开始支付  productId : 商品在苹果后台设置的id    orderId: 我们服务端生产的id
  void iosStartPay(String productId, String orderId,
      {IapCallback? iapCallback}) async {
    if (!LoginTool.isLogin() ||
        productId.isEmpty ||
        orderId.isEmpty ||
        LoginTool.instance.userId.isEmpty) {
      FileWriteLog.log('iap iosStartPay 参数不对 或没登录 -------->');
      iapCallback?.call(false, '参数不对 或没登录');
      return;
    }
    if (!await isAvailable()) {
      bool hasNet = await isNetWorkConnected();
      if (hasNet) {
        alertCanNotBuyDialog();
      } else {
        TDToast.showToast("没有网络");
      }
      iapCallback?.call(false, '无法支付');
      return;
    }

    checkUnfinishedPayment(() async {
      // 获取商品列表
      ProductDetailsResponse appStoreProducts =
          await iosPurchase.queryProductDetails({productId});
      if (appStoreProducts.productDetails.isNotEmpty) {
        // 发起支付 消耗商品的支付
        FileWriteLog.log('iap 发起支付productId --------> $productId');
        iosPurchase
            .buyConsumable(
          purchaseParam: PurchaseParam(
            productDetails: appStoreProducts.productDetails.first,
            applicationUserName: LoginTool.instance.userId,
          ),
        )
            .then((value) {
          if (value) {
            currentIapCallBack = iapCallback;
            // 只要能发起,就写入
            // status 0 开始支付 1 苹果回调失败 2 苹果支付成功  3服务器校验失败
            Map dataMap = {
              IapCache.kOrderId: orderId,
              IapCache.kProductId: productId,
              IapCache.kStatus: 0,
              IapCache.kUserId: LoginTool.instance.userId,
            };
            FileWriteLog.log('iap 开始支付保存的dataMap-----> ${dataMap.toString()}');
            iapCache.saveOrUpdateWithMap(dataMap);
          }
        }).catchError((err) {
          FileWriteLog.log('当前商品您有未完成的交易,请等待iOS系统核验后再次发起购买。');
          iapCallback?.call(false, '当前商品您有未完成的交易,请等待iOS系统核验后再次发起购买。');
          jdLog(err);
        });
      } else {
        iapCallback?.call(false, '没有这个商品');
        TDToast.showToast("查询商品信息失败");
      }
    });
  }

  //监听状态回调
  Future<void> _handleReportedPurchaseState(
      AppStorePurchaseDetails purchaseDetail) async {
    FileWriteLog.log(
        'iap 监听状态回调 purchaseDetail.status  --------> ${purchaseDetail.status}');
    if (purchaseDetail.status == PurchaseStatus.pending) {
      // 有订单开始支付
      FileWriteLog.log(
          'iap 监听状态回调 有订单开始支付productID--------> ${purchaseDetail.productID}');

      JDLoadingTool.showLoading();
    } else {
      if (purchaseDetail.status == PurchaseStatus.error) {
        //错误
        FileWriteLog.log(
            'iap 监听状态回调 错误1  productID--------> ${purchaseDetail.productID}');
        finishTransaction(purchaseDetail);
        currentIapCallBack?.call(false, '苹果支付失败');
      } else if (purchaseDetail.status == PurchaseStatus.canceled) {
        /// 取消订单
        FileWriteLog.log(
            'iap 监听状态回调 取消订单 productID--------> ${purchaseDetail.productID}');
        currentIapCallBack?.call(false, '取消订单');
        finishTransaction(purchaseDetail);
      } else if (purchaseDetail.status == PurchaseStatus.purchased ||
          purchaseDetail.status == PurchaseStatus.restored) {
        FileWriteLog.log(
            'iap 监听状态回调 支付完成  productID--------> ${purchaseDetail.productID}');
        Map? resultMap =
            await iapCache.findWithProductID(purchaseDetail.productID);
        if (resultMap != null) {
          FileWriteLog.log(
              'iap 缓存resultMap != null  productID--------> ${purchaseDetail.productID}');

          resultMap[IapCache.kServerVerificationData] =
              purchaseDetail.verificationData.serverVerificationData;
          resultMap[IapCache.kTransactionDate] = purchaseDetail.transactionDate;
          resultMap[IapCache.kPurchaseID] = purchaseDetail.purchaseID ?? "";
          resultMap[IapCache.kStatus] = 2;
          iapCache.saveOrUpdateWithMap(resultMap);

          FileWriteLog.log(
              'iap 支付成功保存的resultMap-----> ${resultMap.toString()}');

          //已经购买   purchaseID是苹果服务器的订单id transactionIdentifier
          //      if (purchaseDetail.applicationUsername.isEmptyNullAble) {
          //        FileWriteLog.log("applicationUsername null ");
          //        /*
          // 如果某个transaction支付成功但是并没有调用finishTransaction去完成这个交易的时候,
          // 下次启动App重新监听支付队列的时候会重新调用paymentQueue:updatedTransactions:重新获取到未完成的交易,
          // 这个时候获取applicationUsername会出现nil的情况
          //
          // 目前可能场景是用户只尝试充值了一笔,收到苹果两次支付回调
          // 比如:
          // 1、不在常用区域网络或长时间未使用IAP, 则需要进行短信校验. 此时会有两次支付回调
          // 2、如果在支付进行时, 我们将App进程销毁, 支付完成再启动App, 也会有连续两次回调
          // 备注:第一次回调applicationUsername有值,第二次回调applicationUsername为空
          //   */
          //      }
          String orderId = resultMap.getStringNotNull(IapCache.kOrderId) ?? '';
          if (orderId.isNotEmpty) {
            FileWriteLog.log('iap 调用后台接口,发放商品orderId-----> $orderId');

            /// 调用后台接口,发放商品
            bool success =
                await requestVerifyToServer(purchaseDetail, orderId: orderId);
            if (success) {
              FileWriteLog.log('iap 服务器验证通过-----> ');
              deliverProduct(purchaseDetail);
              currentIapCallBack?.call(true, '');
              finishTransaction(purchaseDetail);
            } else {
              // 重试几次强制删除  重新请求 或者删除
              retryOrFinishPurchase(purchaseDetail, orderId, (success) {
                if (success) {
                  currentIapCallBack?.call(true, '');
                  finishTransaction(purchaseDetail);
                  deliverProduct(purchaseDetail);
                } else {
                  int? retryCount = resultMap[IapCache.kRetryCount];
                  retryCount ??= 0;
                  if (retryCount >= 5) {
                    finishTransaction(purchaseDetail);
                    HintDialog(
                      title: '服务器校验失败',
                      content: '请联系客服',
                      rightButtonFunction: (context) {
                        //上传日志
                      },
                    ).show(PreInit.currentContext);
                  } else {
                    int currentCount = retryCount++;
                    resultMap[IapCache.kRetryCount] = currentCount;
                    iapCache.saveOrUpdateWithMap(resultMap);
                    HintDialog(
                      title: '服务器校验失败',
                      content: '请确保手机网络良好 杀死app后重启app',
                      rightButtonFunction: (context) {},
                    ).show(PreInit.currentContext);
                  }
                }
              });
            }
          } else {
            // 没查到订单id
            //本地数据没找到 服务端校验
            noLocalDataSendServer(purchaseDetail, (success) {
              if (success) {
                currentIapCallBack?.call(true, '');
                deliverProduct(purchaseDetail);
                FileWriteLog.log(
                    'iap finishTransaction222  productID--------> ${purchaseDetail.productID}');
                finishTransaction(purchaseDetail);
              } else {
                currentIapCallBack?.call(false, '');
                int? retryCount = resultMap[IapCache.kRetryCount];
                retryCount ??= 0;
                if (retryCount >= 5) {
                  finishTransaction(purchaseDetail);
                  HintDialog(
                    title: '服务器校验失败',
                    content: '请联系客服',
                    rightButtonFunction: (context) {},
                  ).show(PreInit.currentContext);
                } else {
                  int currentCount = retryCount++;
                  resultMap[IapCache.kRetryCount] = currentCount;
                  iapCache.saveOrUpdateWithMap(resultMap);
                  HintDialog(
                    title: '服务器校验失败',
                    content: '请确保手机网络良好 杀死app后重启app',
                    rightButtonFunction: (context) {},
                  ).show(PreInit.currentContext);
                }
                FileWriteLog.log(
                    'iap 本地数据没找到 服务端校验失败  productID--------> ${purchaseDetail.productID}');
              }
            });
          }
        } else {
          //本地数据没找到 服务端校验
          FileWriteLog.log(
              'iap 本地数据没找到 服务端校验  productID--------> ${purchaseDetail.productID}');
          noLocalDataSendServer(purchaseDetail, (success) {
            if (success) {
              FileWriteLog.log(
                  'iap finishTransaction333  productID--------> ${purchaseDetail.productID}');
              currentIapCallBack?.call(true, '');
              finishTransaction(purchaseDetail);
              deliverProduct(purchaseDetail);
            } else {
              currentIapCallBack?.call(false, '服务端校验失败');
              HintDialog(
                title: '服务器校验失败',
                content: '请联系客服',
                rightButtonFunction: (context) {},
              ).show(PreInit.currentContext);
              FileWriteLog.log(
                  'iap 本地数据没找到 服务端校验失败  productID--------> ${purchaseDetail.productID}');
            }
          });
        }
      }
    }
  }

  // 支付成功 发送商品
  deliverProduct(PurchaseDetails purchaseDetails) {
    TDToast.showToast('支付成功');
  }

  //处理校验失败逻辑 重试还是关闭
  retryOrFinishPurchase(PurchaseDetails purchaseDetails, String? orderId,
      Function(bool success) complete) async {
    bool hasNet = await isNetWorkConnected();
    if (hasNet) {
      if (orderId.isNotEmptyNullAble) {
        bool result = await requestVerifyToServer(purchaseDetails,
            orderId: orderId ?? "");
        complete(result);
      } else {
        noLocalDataSendServer(purchaseDetails, (success) {
          complete(success);
        });
      }
    } else {
      TDToast.showToast('校验支付失败');
      complete(false);
    }
  }

  // 本地没数据 发送服务端校验 可以加个参数
  noLocalDataSendServer(
      PurchaseDetails purchaseDetail, Function(bool success)? complete) async {
    bool success = await requestVerifyToServer(purchaseDetail);
    complete?.call(success);
  }

  //向服务器请求校验
  Future<bool> requestVerifyToServer(PurchaseDetails purchaseDetail,
      {String? orderId}) async {
/*
     orderId,
     purchaseID //苹果的订单id
                  purchaseDetail.productID,
                  purchaseDetail.verificationData.serverVerificationData,
                  purchaseDetail.transactionDate ?? ""
 */
    // 成功后删除 已经发送过商品 也是finish
    // finalTransaction(purchaseDetail);
    return Future<bool>.value(true);
  }

  //查询支付完成 没向服务端校验订单
  void checkUnfinishedPayment(Function() complete) async {
    if (LoginTool.isNotLogin()) {
      complete();
      return;
    }
    SKPaymentQueueWrapper()
        .transactions()
        .then((List<SKPaymentTransactionWrapper> values) async {
      if (values.isNotEmpty) {
        for (var element in values) {
          if (element.transactionState ==
              SKPaymentTransactionStateWrapper.purchased) {
            String productId = element.payment.productIdentifier;
            String receiptData = await SKReceiptManager.retrieveReceiptData();
            AppStorePurchaseDetails detail =
                AppStorePurchaseDetails.fromSKTransaction(element, receiptData);
            // detail.applicationUsername = element.payment.applicationUsername;

            FileWriteLog.log('iap 有没完成的交易productID---->${detail.productID}');
            if (element.payment.applicationUsername?.isEmpty ?? false) {
              FileWriteLog.log('iap---> applicationUsername会出现nil');
              // SKPaymentQueueWrapper().finishTransaction(element);
              // deleteWithProductID(productId);
              // complete();
            }
            iapCache
                .findWithProductID(element.payment.productIdentifier)
                .then((Map? resultMap) {
              if (resultMap != null && resultMap.keys.isNotEmpty) {
                requestVerifyToServer(detail,
                        orderId: resultMap[IapCache.kOrderId])
                    .then((value) {
                  if (value) {
                    FileWriteLog.log(
                        'iap 重新校验成功 finishTransaction444  -------->');
                    finishTransaction(detail);
                    complete();
                  } else {
                    FileWriteLog.log('iap 重新校验失败11 -------->');
                    retryOrFinishPurchase(detail, resultMap[IapCache.kOrderId],
                        (success) {
                      if (success) {
                        FileWriteLog.log(
                            'iap 重新校验成6666 inishTransaction-------->');
                        deliverProduct(detail);
                        finishTransaction(detail);
                      } else {
                        int? retryCount = resultMap[IapCache.kRetryCount];
                        retryCount ??= 0;
                        if (retryCount >= 5) {
                          finishTransaction(detail);
                          HintDialog(
                            title: '服务器校验失败',
                            content: '请联系客服',
                            rightButtonFunction: (context) {},
                          ).show(PreInit.currentContext);
                        } else {
                          int currentCount = retryCount++;
                          resultMap[IapCache.kRetryCount] = currentCount;
                          iapCache.saveOrUpdateWithMap(resultMap);
                          HintDialog(
                            title: '服务器校验失败',
                            content: '请确保手机网络良好 杀死app后重启app',
                            rightButtonFunction: (context) {},
                          ).show(PreInit.currentContext);
                        }
                      }

                      complete();
                    });
                  }
                });
              } else {
                //本地数据没找到 服务端校验
                noLocalDataSendServer(detail, (success) {
                  if (success) {
                    deliverProduct(detail);
                    FileWriteLog.log('iap finishTransaction666  -------->');
                    finishTransaction(detail);
                  } else {
                    FileWriteLog.log('iap 本地数据没找到 服务端校验失败  -------->');
                  }
                  complete();
                });
              }
            });
          }
        }
      } else {
        complete();
      }
    });
  }

  // 关闭交易
  void finishTransaction(PurchaseDetails purchaseDetails) async {
    await iosPurchase.completePurchase(purchaseDetails);
    iapCache.deleteWithProductID(purchaseDetails.productID);
    JDLoadingTool.dismissLoading();
    FileWriteLog.log('iap 关闭交易 finishTransaction  -------->');
  }

  //  退出登录 关闭监听
  stopIapListen() async {
    _subscription?.cancel();
    _subscription = null;
    if (hasBindAppLife) {
      WidgetsBinding.instance.removeObserver(this);
      hasBindAppLife = false;
    }
  }

  void alertCanNotBuyDialog() {
    showDialog(
        barrierDismissible: false, //表示点击灰色背景的时候是否消失弹出框
        context: PreInit.currentContext,
        builder: (context) {
          return AlertDialog(
            title: const Text("访问受限"),
            content: const Text(
                "你的手机关闭了“应用内购买”,请在“设置-屏幕使用时间-内容和因素访问限制”里重新打开该选项后尝试。"),
            actions: [
              TextButton(
                  onPressed: () {
                    Navigator.of(context).pop("ok"); //点击按钮让AlertDialog消失
                  },
                  child: const Text("确定")),
            ],
          );
        });
  }

  /// 判断网络是否连接
  Future<bool> isNetWorkConnected() async {
    var connectResult = await (Connectivity().checkConnectivity());
    return connectResult != ConnectivityResult.none;
  }
}

demo地址在https://gitee.com/kuaipai/my_app.git
里,你可以下载下来参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容