[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fL6DUlC5qsQnD9Z3vircnsE36yxV33-COFvAB2QzXo7g":3},{"code":4,"message":5,"data":6},200,"成功",{"id":7,"createdAt":8,"title":9,"content":10,"summary":11,"image":12,"uid":13,"user":14,"categoryId":21,"category":22,"subCategoryId":24,"subCategory":25,"comments":27,"status":17,"reason":28,"notice":28,"visitCount":29,"commentCount":30,"keywords":31},161,"2025-05-30T04:40:15.124Z","flutter: 改造in_app_purchase以支持在purchase failed时获取Apple pay的实际StoreKit error code.","## 背景\napp最近一直有一些用户反馈支付失败的问题，根据早期的埋点数据，无法正确定位到用户购买失败的真正原因，所以需要进一步分析用户在支付失败情况下的真实原因。\n\n当前的app是使用flutter开发的，并且使用了flutter的`in_app_purchase`这个库来实现的，但是在监听StoreKit的购买状态时候，error这个状态太模糊了，flutter在platform层hook支付结果后暴露出的error，和iOS StoreKit的error，形成了一对多的情况。当flutter监听到error状态时，可能指的是StoreKit众多失败原因中某一个。\n\n`in_app_purchase`的flutter package地址：[https://pub.dev/packages/in_app_purchase](https://pub.dev/packages/in_app_purchase)\n\n## 目的\n将iOS的`StoreKit` error错误code值，原原本本的反馈到flutter的error信息中。\n\n我们先来看看`SKError`的error code枚举值：[https://developer.apple.com/documentation/storekit/skerror](https://developer.apple.com/documentation/storekit/skerror)\n\n![SKError-1.png](https://image.xinwei.ltd/image1748576472033.png)\n\n```SKError enum\nError codes\n\nenum Code\n\nError codes for StoreKit errors.\n\nstatic var unknown: SKError.Code\nError code indicating that an unknown or unexpected error occurred.\n\nstatic var clientInvalid: SKError.Code\nError code indicating that the client is not allowed to perform the attempted action.\n\nstatic var paymentCancelled: SKError.Code\nError code indicating that the user canceled a payment request.\n\nstatic var paymentInvalid: SKError.Code\nError code indicating that one of the payment parameters was not recognized by the App Store.\n\nstatic var paymentNotAllowed: SKError.Code\nError code indicating that the user is not allowed to authorize payments.\n\nstatic var storeProductNotAvailable: SKError.Code\nError code indicating that the requested product is not available in the store.\n\nstatic var cloudServicePermissionDenied: SKError.Code\nError code indicating that the user has not allowed access to Cloud service information.\n\nstatic var cloudServiceNetworkConnectionFailed: SKError.Code\nError code indicating that the device could not connect to the network.\n\nstatic var cloudServiceRevoked: SKError.Code\nError code indicating that the user has revoked permission to use this cloud service.\n\nstatic var privacyAcknowledgementRequired: SKError.Code\nError code indicating that the user has not yet acknowledged Apple’s privacy policy for Apple Music.\n\nstatic var unauthorizedRequestData: SKError.Code\nError code indicating that the app is attempting to use a property for which it does not have the required entitlement.\n\nstatic var invalidOfferIdentifier: SKError.Code\nError code indicating that the offer identifier cannot be found or is not active.\n\nstatic var invalidOfferPrice: SKError.Code\nError code indicating that the price you specified in App Store Connect is no longer valid.\n\nstatic var invalidSignature: SKError.Code\nError code indicating that the signature in a payment discount is not valid.\n\nstatic var missingOfferParams: SKError.Code\nError code indicating that parameters are missing in a payment discount.\n\nstatic var ineligibleForOffer: SKError.Code\nAn error code that indicates the user is ineligible for the subscription offer.\n\nstatic var overlayCancelled: SKError.Code\nAn error code that indicates the cancellation of an overlay.\n\nstatic var overlayInvalidConfiguration: SKError.Code\nAn error code that indicates the overlay’s configuration is invalid.\n\nstatic var overlayPresentedInBackgroundScene: SKError.Code\n\nstatic var overlayTimeout: SKError.Code\nAn error code that indicates the timing out of an overlay.\n\nstatic var unsupportedPlatform: SKError.Code\nAn error code that indicates the current platform doesn’t support overlays.\n\n```\n\nflutter的`in_app_purchase`的支付状态枚举：\n```purchase_status.dart\n// Copyright 2013 The Flutter Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\n/// Status for a [PurchaseDetails].\n///\n/// This is the type for [PurchaseDetails.status].\nenum PurchaseStatus {\n  /// The purchase process is pending.\n  ///\n  /// You can update UI to let your users know the purchase is pending.\n  pending,\n\n  /// The purchase is finished and successful.\n  ///\n  /// Update your UI to indicate the purchase is finished and deliver the product.\n  purchased,\n\n  /// Some error occurred in the purchase. The purchasing process if aborted.\n  error,\n\n  /// The purchase has been restored to the device.\n  ///\n  /// You should validate the purchase and if valid deliver the content. Once the\n  /// content has been delivered or if the receipt is invalid you should finish\n  /// the purchase by calling the `completePurchase` method. More information on\n  /// verifying purchases can be found [here](https://pub.dev/packages/in_app_purchase#restoring-previous-purchases).\n  restored,\n\n  /// The purchase has been canceled.\n  ///\n  /// Update your UI to indicate the purchase is canceled.\n  canceled,\n}\n```\n\n很明显，这里flutter的error值明显对应了iOS的众多error，导致埋点中无法细分具体error状态。\n\n## 解决办法\n思路是要git clone `in_app_purchase` fork到本地，修改`in_app_purchase`中的错误返回。\n\n## 梳理逻辑\n### 1.flutter中purchase error的业务代码中获取error code\n\n在in_app_purchase的purchase stream listen中监听购买状态（具体使用方法请参考：[https://pub.dev/packages/in_app_purchase](https://pub.dev/packages/in_app_purchase)）:\n```example.dart\nvoid _listenToPurchaseUpdated(List\u003CPurchaseDetails> purchaseDetailsList) {\n  purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {\n    if (purchaseDetails.status == PurchaseStatus.pending) {\n      _showPendingUI();\n    } else {\n      if (purchaseDetails.status == PurchaseStatus.error) {\n        _handleError(purchaseDetails.error!);\n      } else if (purchaseDetails.status == PurchaseStatus.purchased ||\n                 purchaseDetails.status == PurchaseStatus.restored) {\n        bool valid = await _verifyPurchase(purchaseDetails);\n        if (valid) {\n          _deliverProduct(purchaseDetails);\n        } else {\n          _handleInvalidPurchase(purchaseDetails);\n        }\n      }\n      if (purchaseDetails.pendingCompletePurchase) {\n        await InAppPurchase.instance\n            .completePurchase(purchaseDetails);\n      }\n    }\n  });\n}\n```\n\n对于error状态中error code获取：\n```example.dart\n if (purchaseDetails.status == PurchaseStatus.error) {\n ...\n          var errorCode = -1;\n          if (purchaseDetails.error != null && purchaseDetails.error.code != null) {\n              errorCode = purchaseDetails.error.code;\n          }\n          PayTraceUtil.doPayTrace(payPageEventFrom!, PaySceneSourceType.ScenceDialogPayResult, errorCode);\n...\n}\n```\n在iOS平台下，上面监听purchase status示例代码中的`PurchaseDetails`的实例类型是`AppStorePurchaseDetails`\n![AppStorePurchaseDetails.png](https://image.xinwei.ltd/image1748577582712.png)\n\n所以从iOS platform中传递过来的purchase购买实例，最终会包装为`AppStorePurchaseDetails`，那么我们就查一下这个class的构造函数：\n```app_store_purhase_details.dart\n...\n/// Generate a [AppStorePurchaseDetails] object based on an iOS\n  /// [SKPaymentTransactionWrapper] object.\n  factory AppStorePurchaseDetails.fromSKTransaction(\n    SKPaymentTransactionWrapper transaction,\n    String base64EncodedReceipt,\n  ) {\n    final AppStorePurchaseDetails purchaseDetails = AppStorePurchaseDetails(\n      productID: transaction.payment.productIdentifier,\n      purchaseID: transaction.transactionIdentifier,\n      skPaymentTransaction: transaction,\n      status: const SKTransactionStatusConverter()\n          .toPurchaseStatus(transaction.transactionState, transaction.error),\n      transactionDate: transaction.transactionTimeStamp != null\n          ? (transaction.transactionTimeStamp! * 1000).toInt().toString()\n          : null,\n      verificationData: PurchaseVerificationData(\n          localVerificationData: base64EncodedReceipt,\n          serverVerificationData: base64EncodedReceipt,\n          source: kIAPSource),\n    );\n    if (purchaseDetails.status == PurchaseStatus.error ||\n        purchaseDetails.status == PurchaseStatus.canceled) {\n      purchaseDetails.error = IAPError(\n        source: kIAPSource,\n        code: kPurchaseErrorCode\",\n        message: transaction.error?.domain ?? '',\n        details: transaction.error?.userInfo,\n      );\n    }\n...\n```\n\n重点要看purchaseDetails.error的赋值，可以看到赋值的`IAPError`的`code`是`kPurchaseErrorCode`，查看这个常量值为：\n![kPurchaseErrorCode.png](https://image.xinwei.ltd/image1748577943055.png)\n\n如你所见，这里的code将永远是`purchase_error`。可以佐证，flutter的购买失败状态下，error code的值，针对任何iOS的失败枚举都只返回了这个常量值。\n\n### 2. 如何传递正确的error code\n还是在上面1中的构造函数中，我们可以看到有一个`transaction`，这个其实就是对应于`StoreKit`中的购买trasaction，它的类型是`SKPaymentTransactionWrapper`，我们来看看它的声明：\n![SKPaymentTransactionWrapper.png](https://image.xinwei.ltd/image1748578310932.png)\n\n这里面有error，看看error的声明：\n![flutter SKError](https://image.xinwei.ltd/image1748578348063.png)\n\n看到这个命名，大家也应该知道这个正好是对应于原生iOS的`SKError`。\n\n所以我们需要将`transaction`的error（SKError）中的error code传递给`IAPError`的code，那么就可以达成我们的目的。\n\n## 解决步骤\n### 1. fork flutter的in_app_purchase\n找到in_app_purchase的github地址：[https://github.com/flutter/packages/tree/main/packages/in_app_purchase](https://github.com/flutter/packages/tree/main/packages/in_app_purchase)\n\n\ngit仓库结构是：\n![in_app_purchase git.png](https://image.xinwei.ltd/image1748578544885.png)\n\n可以看到这里在flutter的整个package仓库中，无法单独fork，那我们直接就克隆整个package到本地。\n\n我们需要找一个in_app_purchase的某一个版本，这里我使用的是3.2.0版本\n![in_app_purchase 3.2.0](https://image.xinwei.ltd/image1748578801008.png)\n\n那么我们在某一个文件夹下，克隆到这个版本的所有flutter package\n```terminal\ngit clone -b in_app_purchase-v3.2.0 --single-branch git@github.com:flutter/packages.git\n```\n\n克隆后找到`in_app_purchase`:\n![in_app_purchase fork folder](https://image.xinwei.ltd/image1748578942058.png)\n\n### 2. 修改in_app_purchase_storekit库\n首先明确以下几个事情：\n1. 从上面的梳理思路中可以看到，我们需要修改的代码在`in_app_purchase_storekit`这个package中。\n2. `in_app_purchase`这个package是内部依赖`in_app_purchase_android`、`in_app_purchase_platform_interface`、`in_app_purchase_storekit`这3个平台库的。\n3. 修改`in_app_purchase_storekit`后依赖，那么必须要指定`in_app_purchase`去依赖我们本地修改后的。\n\n看看`in_app_purchase`的pubspec.yaml:\n![in_app_purchase pubspec.yarml](https://image.xinwei.ltd/image1748579384384.png)\n\n修改`in_app_purchase_storekit`，路径在`/Users/edy/Desktop/purchase/packages/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart`:\n![修改`in_app_purchase_storekit`](https://image.xinwei.ltd/image1748579456449.png)\n\n```app_store_purchase_details.dart\n...\nif (purchaseDetails.status == PurchaseStatus.error ||\n    purchaseDetails.status == PurchaseStatus.canceled) {\n      purchaseDetails.error = IAPError(\n        source: kIAPSource,\n        code: \"${transaction.error?.code ?? kPurchaseErrorCode}\", // ,\n        message: transaction.error?.domain ?? '',\n        details: transaction.error?.userInfo,\n      );\n}\n...\n```\n\n4. 将修改后的`in_app_purchase_storekit`引入到flutter工程中\n我们可以在flutter中新建一个文件夹，将修改的库拷贝到这里。\n![plugin folder](https://image.xinwei.ltd/image1748579673091.png)\n\n我新建一个plugin文件夹，将修改后的库放在这里.\n\n5. 让`in_app_purchase`使用修改后的`in_app_purchase_storekit`\n首先引入`in_app_purchase`是必要的\n![pubspec.yaml](https://image.xinwei.ltd/image1748579790181.png)\n```pubspec.yaml\n...\nin_app_purchase: ^3.2.0\n...\n```\n\n要让`in_app_purchase`引用我们plugin文件夹下的`in_app_purchase_storekit`，那么我们需要知道必须要使用override的方式，同样需要在pubspec.yaml文件中引入：\n```pubspec.yaml\n...\ndependency_overrides:\n  in_app_purchase_storekit:\n    path: plugin/in_app_purchase_storekit\n...\n```\n\n6. 运行flutter到iOS上，断点调试，成功。\n\n\n以上就是所有的流程了，大家如果有类似的需要，不妨试试。\n\n\n\n\n\n\n\n","这篇文章记录一下我在flutter开发中如何改造in_app_purchase这个flutter的支付package，以便能上报用户在苹果支付中实际遇到的问题。","https://image.xinwei.ltd/image1748576472033.png",499668042977349,{"phone":15,"userId":13,"nickName":16,"vipType":17,"avatar":18,"sign":19,"createdAt":20},"13121171998","全栈老韩",1,"https://image.xinwei.ltd/images/IMG_5430.JPG","全栈工程师，擅长iOS App开发、前端（vue、react、nuxt、小程序&Taro）开发、Flutter、React Native、后端（midwayjs、golang、express、koa）开发、docker容器、seo优化等。","2024-01-01T16:14:30.305Z",2,{"id":21,"name":23},"IT技术",11,{"id":24,"name":26,"parentId":21},"Flutter",[],"",54,0,"in_app_purchase,StoreKit error,flutter StoreKit error,flutter in_app_purchase,apple pay,flutter plugin,error code"]