flutter 實(shí)現(xiàn)ios內(nèi)購(gòu) iap

in_app_purchase: ^3.1.13

用谷歌推出的內(nèi)購(gòu)插件,這個(gè)插件就是把ios內(nèi)購(gòu)api全部翻譯一遍
所以原生能實(shí)現(xiàn)的功能 這個(gè)插件完全可以實(shí)現(xiàn)

要在初始化時(shí)候先注冊(cè)下平臺(tái),因?yàn)榘沧渴枪雀柚Ц?我們只用ios的內(nèi)購(gòu)
我的下面代碼有很多異常處理,沒完成訂單查詢等, 連續(xù)出錯(cuò)幾次 直接finish等,確保丟單率最低,并且寫入本地文件日志,有問題可以排查

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

  delayAction() async {
    if (LoginTool.isLogin()) {
      if (defaultTargetPlatform == TargetPlatform.iOS) {
        //內(nèi)購(gòu)監(jiān)聽
        await IOSPayment.instance.init();
        IOSPayment.instance.startSubscription();
        // 查詢沒完成訂單 進(jìn)入支付頁(yè)面 調(diào)用一下
        // TDIOSPayment.instance.checkUnfinishedPayment(() => null);
      }
    }
  }

開始支付時(shí)候調(diào)用
    IOSPayment.instance.isPaying = true;
    IOSPayment.instance.iosStartPay('xxxxxx', orderId,
        iapCallback: (isSuccess, errorMsg) {
      Future.delayed(const Duration(seconds: 1), () {
        TDIOSPayment.instance.isPaying = false;
      });
    });

isPaying設(shè)置 主要在支付期間 不讓里面進(jìn)入后臺(tái)調(diào)用檢查沒完成的訂單


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 {
    //登錄狀態(tài)監(jiān)聽
    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();

  // 應(yīng)用內(nèi)支付實(shí)例
  final InAppPurchasePlatform iosPurchase = InAppPurchasePlatform.instance;

  // iOS訂閱監(jiān)聽
  StreamSubscription<List<PurchaseDetails>>? _subscription;

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

  ///監(jiān)聽?wèi)?yīng)用生命周期變化
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    jdLog(':didChangeAppLifecycleState:$state');
    if (state == AppLifecycleState.resumed) {
      //從后臺(tái)切換前臺(tái),界面可見
      if (!isPaying) {
        checkUnfinishedPayment(() => null);
      }
    }
  }

  // 開始訂閱 app一啟動(dòng)或者登錄就訂閱 看是否有支付完成的訂單
  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 : 商品在蘋果后臺(tái)設(shè)置的id    orderId: 我們服務(wù)端生產(chǎn)的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 參數(shù)不對(duì) 或沒登錄 -------->');
      iapCallback?.call(false, '參數(shù)不對(duì) 或沒登錄');
      return;
    }
    if (!await isAvailable()) {
      bool hasNet = await isNetWorkConnected();
      if (hasNet) {
        alertCanNotBuyDialog();
      } else {
        TDToast.showToast("沒有網(wǎng)絡(luò)");
      }
      iapCallback?.call(false, '無法支付');
      return;
    }

    checkUnfinishedPayment(() async {
      // 獲取商品列表
      ProductDetailsResponse appStoreProducts =
          await iosPurchase.queryProductDetails({productId});
      if (appStoreProducts.productDetails.isNotEmpty) {
        // 發(fā)起支付 消耗商品的支付
        FileWriteLog.log('iap 發(fā)起支付productId --------> $productId');
        iosPurchase
            .buyConsumable(
          purchaseParam: PurchaseParam(
            productDetails: appStoreProducts.productDetails.first,
            applicationUserName: LoginTool.instance.userId,
          ),
        )
            .then((value) {
          if (value) {
            currentIapCallBack = iapCallback;
            // 只要能發(fā)起,就寫入
            // status 0 開始支付 1 蘋果回調(diào)失敗 2 蘋果支付成功  3服務(wù)器校驗(yàn)失敗
            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('當(dāng)前商品您有未完成的交易,請(qǐng)等待iOS系統(tǒng)核驗(yàn)后再次發(fā)起購(gòu)買。');
          iapCallback?.call(false, '當(dāng)前商品您有未完成的交易,請(qǐng)等待iOS系統(tǒng)核驗(yàn)后再次發(fā)起購(gòu)買。');
          jdLog(err);
        });
      } else {
        iapCallback?.call(false, '沒有這個(gè)商品');
        TDToast.showToast("查詢商品信息失敗");
      }
    });
  }

  //監(jiān)聽狀態(tài)回調(diào)
  Future<void> _handleReportedPurchaseState(
      AppStorePurchaseDetails purchaseDetail) async {
    FileWriteLog.log(
        'iap 監(jiān)聽狀態(tài)回調(diào) purchaseDetail.status  --------> ${purchaseDetail.status}');
    if (purchaseDetail.status == PurchaseStatus.pending) {
      // 有訂單開始支付
      FileWriteLog.log(
          'iap 監(jiān)聽狀態(tài)回調(diào) 有訂單開始支付productID--------> ${purchaseDetail.productID}');

      JDLoadingTool.showLoading();
    } else {
      if (purchaseDetail.status == PurchaseStatus.error) {
        //錯(cuò)誤
        FileWriteLog.log(
            'iap 監(jiān)聽狀態(tài)回調(diào) 錯(cuò)誤1  productID--------> ${purchaseDetail.productID}');
        finishTransaction(purchaseDetail);
        currentIapCallBack?.call(false, '蘋果支付失敗');
      } else if (purchaseDetail.status == PurchaseStatus.canceled) {
        /// 取消訂單
        FileWriteLog.log(
            'iap 監(jiān)聽狀態(tài)回調(diào) 取消訂單 productID--------> ${purchaseDetail.productID}');
        currentIapCallBack?.call(false, '取消訂單');
        finishTransaction(purchaseDetail);
      } else if (purchaseDetail.status == PurchaseStatus.purchased ||
          purchaseDetail.status == PurchaseStatus.restored) {
        FileWriteLog.log(
            'iap 監(jiān)聽狀態(tài)回調(diào) 支付完成  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()}');

          //已經(jīng)購(gòu)買   purchaseID是蘋果服務(wù)器的訂單id transactionIdentifier
          //      if (purchaseDetail.applicationUsername.isEmptyNullAble) {
          //        FileWriteLog.log("applicationUsername null ");
          //        /*
          // 如果某個(gè)transaction支付成功但是并沒有調(diào)用finishTransaction去完成這個(gè)交易的時(shí)候,
          // 下次啟動(dòng)App重新監(jiān)聽支付隊(duì)列的時(shí)候會(huì)重新調(diào)用paymentQueue:updatedTransactions:重新獲取到未完成的交易,
          // 這個(gè)時(shí)候獲取applicationUsername會(huì)出現(xiàn)nil的情況
          //
          // 目前可能場(chǎng)景是用戶只嘗試充值了一筆,收到蘋果兩次支付回調(diào)
          // 比如:
          // 1、不在常用區(qū)域網(wǎng)絡(luò)或長(zhǎng)時(shí)間未使用IAP, 則需要進(jìn)行短信校驗(yàn). 此時(shí)會(huì)有兩次支付回調(diào)
          // 2、如果在支付進(jìn)行時(shí), 我們將App進(jìn)程銷毀, 支付完成再啟動(dòng)App, 也會(huì)有連續(xù)兩次回調(diào)
          // 備注:第一次回調(diào)applicationUsername有值,第二次回調(diào)applicationUsername為空
          //   */
          //      }
          String orderId = resultMap.getStringNotNull(IapCache.kOrderId) ?? '';
          if (orderId.isNotEmpty) {
            FileWriteLog.log('iap 調(diào)用后臺(tái)接口,發(fā)放商品orderId-----> $orderId');

            /// 調(diào)用后臺(tái)接口,發(fā)放商品
            bool success =
                await requestVerifyToServer(purchaseDetail, orderId: orderId);
            if (success) {
              FileWriteLog.log('iap 服務(wù)器驗(yàn)證通過-----> ');
              deliverProduct(purchaseDetail);
              currentIapCallBack?.call(true, '');
              finishTransaction(purchaseDetail);
            } else {
              // 重試幾次強(qiáng)制刪除  重新請(qǐng)求 或者刪除
              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: '服務(wù)器校驗(yàn)失敗',
                      content: '請(qǐng)聯(lián)系客服',
                      rightButtonFunction: (context) {
                        //上傳日志
                      },
                    ).show(PreInit.currentContext);
                  } else {
                    int currentCount = retryCount++;
                    resultMap[IapCache.kRetryCount] = currentCount;
                    iapCache.saveOrUpdateWithMap(resultMap);
                    HintDialog(
                      title: '服務(wù)器校驗(yàn)失敗',
                      content: '請(qǐng)確保手機(jī)網(wǎng)絡(luò)良好 殺死app后重啟app',
                      rightButtonFunction: (context) {},
                    ).show(PreInit.currentContext);
                  }
                }
              });
            }
          } else {
            // 沒查到訂單id
            //本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)
            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: '服務(wù)器校驗(yàn)失敗',
                    content: '請(qǐng)聯(lián)系客服',
                    rightButtonFunction: (context) {},
                  ).show(PreInit.currentContext);
                } else {
                  int currentCount = retryCount++;
                  resultMap[IapCache.kRetryCount] = currentCount;
                  iapCache.saveOrUpdateWithMap(resultMap);
                  HintDialog(
                    title: '服務(wù)器校驗(yàn)失敗',
                    content: '請(qǐng)確保手機(jī)網(wǎng)絡(luò)良好 殺死app后重啟app',
                    rightButtonFunction: (context) {},
                  ).show(PreInit.currentContext);
                }
                FileWriteLog.log(
                    'iap 本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)失敗  productID--------> ${purchaseDetail.productID}');
              }
            });
          }
        } else {
          //本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)
          FileWriteLog.log(
              'iap 本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)  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, '服務(wù)端校驗(yàn)失敗');
              HintDialog(
                title: '服務(wù)器校驗(yàn)失敗',
                content: '請(qǐng)聯(lián)系客服',
                rightButtonFunction: (context) {},
              ).show(PreInit.currentContext);
              FileWriteLog.log(
                  'iap 本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)失敗  productID--------> ${purchaseDetail.productID}');
            }
          });
        }
      }
    }
  }

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

  //處理校驗(yàn)失敗邏輯 重試還是關(guān)閉
  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('校驗(yàn)支付失敗');
      complete(false);
    }
  }

  // 本地沒數(shù)據(jù) 發(fā)送服務(wù)端校驗(yàn) 可以加個(gè)參數(shù)
  noLocalDataSendServer(
      PurchaseDetails purchaseDetail, Function(bool success)? complete) async {
    bool success = await requestVerifyToServer(purchaseDetail);
    complete?.call(success);
  }

  //向服務(wù)器請(qǐng)求校驗(yàn)
  Future<bool> requestVerifyToServer(PurchaseDetails purchaseDetail,
      {String? orderId}) async {
/*
     orderId,
     purchaseID //蘋果的訂單id
                  purchaseDetail.productID,
                  purchaseDetail.verificationData.serverVerificationData,
                  purchaseDetail.transactionDate ?? ""
 */
    // 成功后刪除 已經(jīng)發(fā)送過商品 也是finish
    // finalTransaction(purchaseDetail);
    return Future<bool>.value(true);
  }

  //查詢支付完成 沒向服務(wù)端校驗(yàn)訂單
  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會(huì)出現(xiàn)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 重新校驗(yàn)成功 finishTransaction444  -------->');
                    finishTransaction(detail);
                    complete();
                  } else {
                    FileWriteLog.log('iap 重新校驗(yàn)失敗11 -------->');
                    retryOrFinishPurchase(detail, resultMap[IapCache.kOrderId],
                        (success) {
                      if (success) {
                        FileWriteLog.log(
                            'iap 重新校驗(yàn)成6666 inishTransaction-------->');
                        deliverProduct(detail);
                        finishTransaction(detail);
                      } else {
                        int? retryCount = resultMap[IapCache.kRetryCount];
                        retryCount ??= 0;
                        if (retryCount >= 5) {
                          finishTransaction(detail);
                          HintDialog(
                            title: '服務(wù)器校驗(yàn)失敗',
                            content: '請(qǐng)聯(lián)系客服',
                            rightButtonFunction: (context) {},
                          ).show(PreInit.currentContext);
                        } else {
                          int currentCount = retryCount++;
                          resultMap[IapCache.kRetryCount] = currentCount;
                          iapCache.saveOrUpdateWithMap(resultMap);
                          HintDialog(
                            title: '服務(wù)器校驗(yàn)失敗',
                            content: '請(qǐng)確保手機(jī)網(wǎng)絡(luò)良好 殺死app后重啟app',
                            rightButtonFunction: (context) {},
                          ).show(PreInit.currentContext);
                        }
                      }

                      complete();
                    });
                  }
                });
              } else {
                //本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)
                noLocalDataSendServer(detail, (success) {
                  if (success) {
                    deliverProduct(detail);
                    FileWriteLog.log('iap finishTransaction666  -------->');
                    finishTransaction(detail);
                  } else {
                    FileWriteLog.log('iap 本地?cái)?shù)據(jù)沒找到 服務(wù)端校驗(yàn)失敗  -------->');
                  }
                  complete();
                });
              }
            });
          }
        }
      } else {
        complete();
      }
    });
  }

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

  //  退出登錄 關(guān)閉監(jiān)聽
  stopIapListen() async {
    _subscription?.cancel();
    _subscription = null;
    if (hasBindAppLife) {
      WidgetsBinding.instance.removeObserver(this);
      hasBindAppLife = false;
    }
  }

  void alertCanNotBuyDialog() {
    showDialog(
        barrierDismissible: false, //表示點(diǎn)擊灰色背景的時(shí)候是否消失彈出框
        context: PreInit.currentContext,
        builder: (context) {
          return AlertDialog(
            title: const Text("訪問受限"),
            content: const Text(
                "你的手機(jī)關(guān)閉了“應(yīng)用內(nèi)購(gòu)買”,請(qǐng)?jiān)凇霸O(shè)置-屏幕使用時(shí)間-內(nèi)容和因素訪問限制”里重新打開該選項(xiàng)后嘗試。"),
            actions: [
              TextButton(
                  onPressed: () {
                    Navigator.of(context).pop("ok"); //點(diǎn)擊按鈕讓AlertDialog消失
                  },
                  child: const Text("確定")),
            ],
          );
        });
  }

  /// 判斷網(wǎng)絡(luò)是否連接
  Future<bool> isNetWorkConnected() async {
    var connectResult = await (Connectivity().checkConnectivity());
    return connectResult != ConnectivityResult.none;
  }
}

demo地址在https://gitee.com/kuaipai/my_app.git
里,你可以下載下來參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容