flutter: 改造in_app_purchase以支持在purchase failed时获取Apple pay的实际StoreKit error code.

原创文章
声明:作者声明此文章为原创,未经作者同意,请勿转载,若转载,务必注明本站出处,本平台保留追究侵权法律责任的权利。
全栈老韩
全栈工程师,擅长iOS App开发、前端(vue、react、nuxt、小程序&Taro)开发、Flutter、React Native、后端(midwayjs、golang、express、koa)开发、docker容器、seo优化等。

背景

app最近一直有一些用户反馈支付失败的问题,根据早期的埋点数据,无法正确定位到用户购买失败的真正原因,所以需要进一步分析用户在支付失败情况下的真实原因。

当前的app是使用flutter开发的,并且使用了flutter的in_app_purchase这个库来实现的,但是在监听StoreKit的购买状态时候,error这个状态太模糊了,flutter在platform层hook支付结果后暴露出的error,和iOS StoreKit的error,形成了一对多的情况。当flutter监听到error状态时,可能指的是StoreKit众多失败原因中某一个。

in_app_purchase的flutter package地址:https://pub.dev/packages/in_app_purchase

目的

将iOS的StoreKit error错误code值,原原本本的反馈到flutter的error信息中。

我们先来看看SKError的error code枚举值:https://developer.apple.com/documentation/storekit/skerror

SKError-1.png
SKError enum 复制代码
Error codes

enum Code

Error codes for StoreKit errors.

static var unknown: SKError.Code
Error code indicating that an unknown or unexpected error occurred.

static var clientInvalid: SKError.Code
Error code indicating that the client is not allowed to perform the attempted action.

static var paymentCancelled: SKError.Code
Error code indicating that the user canceled a payment request.

static var paymentInvalid: SKError.Code
Error code indicating that one of the payment parameters was not recognized by the App Store.

static var paymentNotAllowed: SKError.Code
Error code indicating that the user is not allowed to authorize payments.

static var storeProductNotAvailable: SKError.Code
Error code indicating that the requested product is not available in the store.

static var cloudServicePermissionDenied: SKError.Code
Error code indicating that the user has not allowed access to Cloud service information.

static var cloudServiceNetworkConnectionFailed: SKError.Code
Error code indicating that the device could not connect to the network.

static var cloudServiceRevoked: SKError.Code
Error code indicating that the user has revoked permission to use this cloud service.

static var privacyAcknowledgementRequired: SKError.Code
Error code indicating that the user has not yet acknowledged Apple’s privacy policy for Apple Music.

static var unauthorizedRequestData: SKError.Code
Error code indicating that the app is attempting to use a property for which it does not have the required entitlement.

static var invalidOfferIdentifier: SKError.Code
Error code indicating that the offer identifier cannot be found or is not active.

static var invalidOfferPrice: SKError.Code
Error code indicating that the price you specified in App Store Connect is no longer valid.

static var invalidSignature: SKError.Code
Error code indicating that the signature in a payment discount is not valid.

static var missingOfferParams: SKError.Code
Error code indicating that parameters are missing in a payment discount.

static var ineligibleForOffer: SKError.Code
An error code that indicates the user is ineligible for the subscription offer.

static var overlayCancelled: SKError.Code
An error code that indicates the cancellation of an overlay.

static var overlayInvalidConfiguration: SKError.Code
An error code that indicates the overlay’s configuration is invalid.

static var overlayPresentedInBackgroundScene: SKError.Code

static var overlayTimeout: SKError.Code
An error code that indicates the timing out of an overlay.

static var unsupportedPlatform: SKError.Code
An error code that indicates the current platform doesn’t support overlays.

flutter的in_app_purchase的支付状态枚举:

purchase_status.dart 复制代码
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// Status for a [PurchaseDetails].
///
/// This is the type for [PurchaseDetails.status].
enum PurchaseStatus {
  /// The purchase process is pending.
  ///
  /// You can update UI to let your users know the purchase is pending.
  pending,

  /// The purchase is finished and successful.
  ///
  /// Update your UI to indicate the purchase is finished and deliver the product.
  purchased,

  /// Some error occurred in the purchase. The purchasing process if aborted.
  error,

  /// The purchase has been restored to the device.
  ///
  /// You should validate the purchase and if valid deliver the content. Once the
  /// content has been delivered or if the receipt is invalid you should finish
  /// the purchase by calling the `completePurchase` method. More information on
  /// verifying purchases can be found [here](https://pub.dev/packages/in_app_purchase#restoring-previous-purchases).
  restored,

  /// The purchase has been canceled.
  ///
  /// Update your UI to indicate the purchase is canceled.
  canceled,
}

很明显,这里flutter的error值明显对应了iOS的众多error,导致埋点中无法细分具体error状态。

解决办法

思路是要git clone in_app_purchase fork到本地,修改in_app_purchase中的错误返回。

梳理逻辑

1.flutter中purchase error的业务代码中获取error code

在in_app_purchase的purchase stream listen中监听购买状态(具体使用方法请参考:https://pub.dev/packages/in_app_purchase):

example.dart 复制代码
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
  purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.pending) {
      _showPendingUI();
    } else {
      if (purchaseDetails.status == PurchaseStatus.error) {
        _handleError(purchaseDetails.error!);
      } else if (purchaseDetails.status == PurchaseStatus.purchased ||
                 purchaseDetails.status == PurchaseStatus.restored) {
        bool valid = await _verifyPurchase(purchaseDetails);
        if (valid) {
          _deliverProduct(purchaseDetails);
        } else {
          _handleInvalidPurchase(purchaseDetails);
        }
      }
      if (purchaseDetails.pendingCompletePurchase) {
        await InAppPurchase.instance
            .completePurchase(purchaseDetails);
      }
    }
  });
}

对于error状态中error code获取:

example.dart 复制代码
 if (purchaseDetails.status == PurchaseStatus.error) {
 ...
          var errorCode = -1;
          if (purchaseDetails.error != null && purchaseDetails.error.code != null) {
              errorCode = purchaseDetails.error.code;
          }
          PayTraceUtil.doPayTrace(payPageEventFrom!, PaySceneSourceType.ScenceDialogPayResult, errorCode);
...
}

在iOS平台下,上面监听purchase status示例代码中的PurchaseDetails的实例类型是AppStorePurchaseDetails
AppStorePurchaseDetails.png

所以从iOS platform中传递过来的purchase购买实例,最终会包装为AppStorePurchaseDetails,那么我们就查一下这个class的构造函数:

app_store_purhase_details.dart 复制代码
...
/// Generate a [AppStorePurchaseDetails] object based on an iOS
  /// [SKPaymentTransactionWrapper] object.
  factory AppStorePurchaseDetails.fromSKTransaction(
    SKPaymentTransactionWrapper transaction,
    String base64EncodedReceipt,
  ) {
    final AppStorePurchaseDetails purchaseDetails = AppStorePurchaseDetails(
      productID: transaction.payment.productIdentifier,
      purchaseID: transaction.transactionIdentifier,
      skPaymentTransaction: transaction,
      status: const SKTransactionStatusConverter()
          .toPurchaseStatus(transaction.transactionState, transaction.error),
      transactionDate: transaction.transactionTimeStamp != null
          ? (transaction.transactionTimeStamp! * 1000).toInt().toString()
          : null,
      verificationData: PurchaseVerificationData(
          localVerificationData: base64EncodedReceipt,
          serverVerificationData: base64EncodedReceipt,
          source: kIAPSource),
    );
    if (purchaseDetails.status == PurchaseStatus.error ||
        purchaseDetails.status == PurchaseStatus.canceled) {
      purchaseDetails.error = IAPError(
        source: kIAPSource,
        code: kPurchaseErrorCode",
        message: transaction.error?.domain ?? '',
        details: transaction.error?.userInfo,
      );
    }
...

重点要看purchaseDetails.error的赋值,可以看到赋值的IAPErrorcodekPurchaseErrorCode,查看这个常量值为:
kPurchaseErrorCode.png

如你所见,这里的code将永远是purchase_error。可以佐证,flutter的购买失败状态下,error code的值,针对任何iOS的失败枚举都只返回了这个常量值。

2. 如何传递正确的error code

还是在上面1中的构造函数中,我们可以看到有一个transaction,这个其实就是对应于StoreKit中的购买trasaction,它的类型是SKPaymentTransactionWrapper,我们来看看它的声明:
SKPaymentTransactionWrapper.png

这里面有error,看看error的声明:
flutter SKError

看到这个命名,大家也应该知道这个正好是对应于原生iOS的SKError

所以我们需要将transaction的error(SKError)中的error code传递给IAPError的code,那么就可以达成我们的目的。

解决步骤

1. fork flutter的in_app_purchase

找到in_app_purchase的github地址:https://github.com/flutter/packages/tree/main/packages/in_app_purchase

git仓库结构是:
in_app_purchase git.png

可以看到这里在flutter的整个package仓库中,无法单独fork,那我们直接就克隆整个package到本地。

我们需要找一个in_app_purchase的某一个版本,这里我使用的是3.2.0版本
in_app_purchase 3.2.0

那么我们在某一个文件夹下,克隆到这个版本的所有flutter package

terminal 复制代码
git clone -b in_app_purchase-v3.2.0 --single-branch git@github.com:flutter/packages.git

克隆后找到in_app_purchase:
in_app_purchase fork folder

2. 修改in_app_purchase_storekit库

首先明确以下几个事情:

  1. 从上面的梳理思路中可以看到,我们需要修改的代码在in_app_purchase_storekit这个package中。
  2. in_app_purchase这个package是内部依赖in_app_purchase_androidin_app_purchase_platform_interfacein_app_purchase_storekit这3个平台库的。
  3. 修改in_app_purchase_storekit后依赖,那么必须要指定in_app_purchase去依赖我们本地修改后的。

看看in_app_purchase的pubspec.yaml:
in_app_purchase pubspec.yarml

修改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:
修改

app_store_purchase_details.dart 复制代码
...
if (purchaseDetails.status == PurchaseStatus.error ||
    purchaseDetails.status == PurchaseStatus.canceled) {
      purchaseDetails.error = IAPError(
        source: kIAPSource,
        code: "${transaction.error?.code ?? kPurchaseErrorCode}", // ,
        message: transaction.error?.domain ?? '',
        details: transaction.error?.userInfo,
      );
}
...
  1. 将修改后的in_app_purchase_storekit引入到flutter工程中
    我们可以在flutter中新建一个文件夹,将修改的库拷贝到这里。
    plugin folder

我新建一个plugin文件夹,将修改后的库放在这里.

  1. in_app_purchase使用修改后的in_app_purchase_storekit
    首先引入in_app_purchase是必要的
    pubspec.yaml
pubspec.yaml 复制代码
...
in_app_purchase: ^3.2.0
...

要让in_app_purchase引用我们plugin文件夹下的in_app_purchase_storekit,那么我们需要知道必须要使用override的方式,同样需要在pubspec.yaml文件中引入:

pubspec.yaml 复制代码
...
dependency_overrides:
  in_app_purchase_storekit:
    path: plugin/in_app_purchase_storekit
...
  1. 运行flutter到iOS上,断点调试,成功。

以上就是所有的流程了,大家如果有类似的需要,不妨试试。

暂无评论,快来发表第一条评论吧