Apple和Google内购,in_app_purchase

2023-08-09  本文已影响0人  雨泽Sunshine

一、有关Apple内购

1. SKStorefront:包含App Store店面位置和唯一标识符的对象。

您可以通过 App Store Connect创建的应用内产品可在每个拥有App Store的地区销售。您可以使用店面信息来确定客户所在的地区,并提供适合该地区的应用内产品。您必须维护自己的产品标识符列表以及要在其中提供它们的店面。

2.SKPaymentQueueDelegate

<1>shouldContinueTransaction: 当设备的App Store店面在交易期间发生更改时(比如:设备的App Store原先登录的是中国地区的店面,在交易期间切换到了美国地区的店面),询问是否继续交易:

<2>shouldShowPriceConsent:当App Store Connect中的订阅价格已更改,且订阅者尚未采取任何行动时,询问是否立即显示价格变化同意书:

此方法仅适用于需要客户同意的自动续订订阅价格上涨。当你提高自动续订订阅的价格并需要客户同意时,Apple会通过电子邮件、推送通知和应用内价格同意书通知受影响的订阅者,并要求他们同意新价格。如果客户不同意或不采取任何行动,他们的订阅将在当前计费周期结束时到期。

3.如何沙盒测试

<1>进入沙盒测试员,点击添加按钮,需要注意的是沙盒账号必须是没有注册过App Store和沙盒账号的邮箱,邮箱可以随便填,记住邮箱密码就可以。

<2>打开手机的设置 --> 点击App Store --> 往下滑有个沙盒账户 --> 登录 --> 然后Apple ID安全,点击其他选项,选择不升级。

<3>如果你的App Store下面没有沙盒账户,说明你的手机之前没有做过沙盒测试,那就连接你的电脑运行一下,点击测试购买的商品,会弹出提示框让你登录沙盒账户。

4.充值成功后不走购买成功的回调

完成购买

当用户付款成功后会弹出上面的弹窗,只有当用户点击“好”才会触发成功``的回调。如果这时候客户不点击"好",将app杀死或者卸载,那么服务器是不知道用户在IAP服务器上已经完成付款了,是不会给客户发货的。

5.漏单处理

https://blog.csdn.net/pengyuan_D/article/details/121335745

https://www.devfutao.com/archives/224/

处理漏单的关键点:在确认服务端收到receipt并验证成功之前不要结束订单,即不要调用finishTransaction结束该笔订单。

处理漏单顺序:当客户端产生了多个漏单,向服务端验证时,服务端向苹果服务器获取的验证数据in_app会包含所有未处理的订单,最后产生的订单数据在最后,客户端处理漏单顺序和服务端验证顺序要保持一致。

6.票据

<1>如何使用cancellation_data字段?

该字段仅适用于自动续期订阅、非续期订阅和非消耗型产品。当用户申请退款(Refund)或撤销家庭共享(Family Sharing)时,票据校验返回的JSON数据中才会有该字段。因此可以利用该字段监测用户退款,并及时收回已经发放的产品或服务。

<2>如何选择票据校验地址?

测试阶段使用沙盒地址:

https://sandbox.itunes.apple.com/verifyReceipt

App Store发布之后使用正式地址:

https://buy.itunes.apple.com/verifyReceipt

最佳实践:无论是测试阶段还是正式发布阶段,总是先去正式环境校验,如果返回21007状态码,再去沙盒环境校验,这样无需在测试、审核、发布等各个阶段频繁切换地址。

<3>如何处理appStoreReceiptURL为空的情况?

[NSBundle mainBundle].appStoreReceiptURL只是一个URL,当用户付款成功后,系统会把receipt写入到这个位置。取receipt时需要判空,如果文件不存在,就需要从苹果服务器重新刷新下载了。

SKReceiptRefreshRequest刷新系统会弹窗让用户授权,而用户会取消授权,App必须要能正确处理取消授权的情况。若刷新成功,拿到票据走正常验证发货流程;若刷新失败时App应释放该请求,而不是尝试再次调用它。

出现这种情况的一种典型场景:从TestFlightXcode安装App。当从App Store安装或从iCloud恢复时,appStoreReceipt将始终存在,但是在一些未知情况票据确实不存在。

<4>校验票据时,返回的结果中的in_app是一个空数组,而不是预期的产品?

in_app数组表示App Store尚未记录用户的任何交易,可能票据未更新导致的。

消耗性产品在购买成功后会添加进票据,在finishTransaction成后后从交易队列移除,在下次更新票据时正式从票据中移除。而自动续期订阅、非续期订阅和非消耗型产品自购买成功后就会永久保留在票据中,如果应用只提供消耗型产品,那么在票据当前没有产品并且本次购买还未来得及更新时就拿去校验,就会出现空数组的情况。

出现这种情况,可以使用SKReceiptRefreshRequest来显示刷新票据。

7.App Store Server API(重要)

<1>新特征

2021年In App Purchase迎来了重大变化,苹果推出了StoreKit2App Store Server APIApp Store Server Notifications V2三大特征。

苹果将原先旧的内购变为Original API for in-app purchase,引入了全新的内购In-App Purchase(即StoreKit2)StoreKit2是基于Swift的API,从iOS15开始提供。

<2>appAccountToken:

其中最大的变化之一是新增了一个appAccountToken字段,用于开发者将交易与自己服务器上的用户关联起来的UUID。当开发者发起应用内购买时传入appAccountToken,该值会永久的在交易信息里面保存。

而且苹果打通了applicationUsernameappAccountToken,当用Original StoreKit创建订单时,applicationUsername字段赋值使用 UUID格式内容时,则可以在服务端通知或者解析receipt票据时,可以获取这个UUID值,也就是订单可以关联确认:

注意:当使用verifyReceipt验证票据时,如果是消耗型产品,返回的responseBody里面没有latest_receipt_info字段。

注意:苹果不保证非UUID格式的applicationUsername属性会在交易里面持续存在。

<3>使用新的验证方式

由于使用票据receipt有许多不合理的地方,我们可以放弃旧的读取本地receipt传给服务端验证的方式,直接采用App Store Server API中的Get Transaction Info API,将transaction_id传递给苹果服务器进行验证票据。

在创建订单时,自己的服务器会返回一个UUID与订单绑定,当发起交易时将该UUID通过applicationUsername传给App Store。校验时,自己的服务器通过Get Transaction Info API 获取到App Store回传的UUID,此时就可以与自己服务器的订单相关联从而进行充值。

二、有关Google内购

1.设定商品的定价

Google Play Console --> 设定 --> 定价范本

设定价格

2.建立产品

Google Play Console --> 产品 --> 应用程式内产品 --> 建立产品

建立产品

建立好产品后,一定需要启用,否则App会读取不到数据。

3.封闭测试

<1>Google Play Console --> 测试 --> 封闭测试 --> 建立测试群组

建立测试群组

<2> 管理测试群组 --> 版本 --> 建立封闭测试版本 --> 上传测试版本(aab文件)

上传测试版本(aab文件)

安装在手机上的包一定要和上传的包的版本号、build号、签名一致。

<3>管理测试群组 --> 测试人数 --> 建立電子郵件名單

添加测试人员

待审核通过后,复制链接发送给测试人员,让他接受邀请,否则测试人员会拉取不到订单数据。

4.授权测试

Google Play Console --> 设定 --> 授权测试 --> 授权回应RESPOND_NORMALLY

授权测试

5.Purchase交易订单

6.PENDING待处理状态的购买交易(一次性商品)

PENDING状态并不是等待支付的状态,而是客户端完成支付操作后,Google在成功扣款前,这笔购买交易将会处于待处理状态。可在测试时选择慢速测试卡,几分钟后批准,此时的状态会是PENDING

对于一次性商品,只有处于PENDING状态的购买交易才可取消。如果处于PURCHASED状态的一次性商品发生退款,需要通过Voided Purchases API获知。

注意:当选择慢速测试卡,几分钟后拒绝购买,客户端并没有收到取消的通知,即没有在PurchasesUpdatedListener里面监测到交易状态的取消更新。

7.在后端处理购买交易(一次性商品)

APIpurchases.products:get返回的结果:

{
  "kind": string,
  /// 购买产品的时间戳
  "purchaseTimeMillis": string,
  /// 订单状态,0:已购买 1:已取消 2:待处理
  "purchaseState": integer,
  /// 消耗状态,0:未消耗 1:已消耗
  "consumptionState": integer,
  /// 开发人员自定义的字段
  "developerPayload": string,
  /// 订单ID
  "orderId": string,
  /// 购买类型,0:测试  1:促销  2:激励广告
  "purchaseType": integer,
  /// 确认状态,0:未确认  1:已确认
  "acknowledgementState": integer,
  /// 交易令牌,可能不存在
  "purchaseToken": string,
  /// 产品ID,可能不存在
  "productId": string,
  /// 产品数量
  "quantity": integer,
  /// 发起交易时自定义传值,用于与用户账户唯一关联的ID(适用于应用内商品的购买交易)
  "obfuscatedExternalAccountId": string,
  /// 发起交易时自定义传值,用于与用户个人资料唯一关联的ID(适用于服务器端的订阅)
  "obfuscatedExternalProfileId": string,
  /// 结算区域
  "regionCode": string
}

消耗品返回的结果:
{
    "purchaseTimeMillis": "1691131391062",
    "purchaseState": 1,
    "consumptionState": 0,
    "developerPayload": "",
    "orderId": "GPA.3356-0813-8427-26633",
    "purchaseType": 0,
    "acknowledgementState": 0,
    "kind": "androidpublisher#productPurchase",
    "obfuscatedExternalAccountId": "自己服務器上的訂單號",
    "regionCode": "TW"
}

<1> 如果验证购买交易,必须先检查交易状态是否为PURCHASED

<2>purchaseToken具有全局唯一性,所以应该使用purchaseToken来作为是否已经发放产品的唯一值。

<3>验证当前购买交易的purchaseToken是否有效,若有效才发放产品。

<4>发放产品后需要进行消耗或确认购买交易。

对于消耗型产品首先需确保购买交易未被消耗,查看APIPurchases.products:get调用结果中的consumptionState,若未被消耗必须调用APIPurchases.products:consume进行消耗。

对于非消耗型产品首先需确保购买交易未被确认,查看APIPurchases.products:get调用结果中的acknowledgementState,若未被确认必须调用APIPurchases.products:acknowledge进行确认。

对于订阅产品首先需确保购买交易是否被确认,查看APIPurchases.subscriptions调用结果中的acknowledgementState,若未被确认必须调用APIPurchases.subscriptions.acknowledge进行确认。所有初始化订阅购买交易都需要确认,订阅续订不需要确认。

注意:请务在购买交易状态为PENDING时发放产品。

注意:请务使用orderId检查是否存在重复的购买交易或将其作为数据库中的主键,因为不能保证所有购买交易都会生成orderId。特别是,使用促销代码完成的购买交易不会生产orderId

三.上代码(仅供参考)

// ignore_for_file: avoid_print
class CommonInAppPurchaseViewModel extends YZBaseViewModel {
  final BuildContext context;
  final InAppPurchase _inAppPurchase = InAppPurchase.instance;
  late StreamSubscription<List<PurchaseDetails>> _purchaseSubscription;

  CommonInAppPurchaseViewModel(this.context) {
    /// InApp Purchase
    _purchaseSubscription = _inAppPurchase.purchaseStream.listen((purchaseDetailsList) {
      for (final purchaseDetails in purchaseDetailsList) {
        _listenToPurchaseUpdated(purchaseDetails);
      }
    }, onDone: () {
      _purchaseSubscription.cancel();
    });

    /// 处理未完成的订单
    _handlePastPurchases();
  }

  /// 发起支付请求
  /// @productId: 产品ID
  /// @existedOrderNo:自己服务器产生的订单号,若有值则不重新创建订单,可用于补单时
  /// @pointId: 选择的点数套餐,创建订单时必传
  static Future<void> startPay({required String productId, String? existedOrderNo, int? pointId}) async {
    YZToastUtil.showLoading(message: '正在創建訂單');

    /// 检测内购是否可用
    final available = await InAppPurchase.instance.isAvailable();
    if (!available) {
      YZToastUtil.showMessage('不支持內購功能');
      return;
    }

    /// 查询产品id是否在服务器上注册了
    final response = await InAppPurchase.instance.queryProductDetails({productId});
    if (response.error != null) {
      YZToastUtil.showMessage(response.error!.message);
      return;
    }

    /// 未查询到产品
    if (response.productDetails.isEmpty) {
      YZToastUtil.showMessage('暫無產品');
      return;
    }

    /// 創建訂單
    var orderNo = existedOrderNo;
    String? orderUuidNo;
    if (orderNo == null) {
      final orderModel = await PurchaseApi.createOrder(pointId: pointId);
      orderNo = orderModel.number;
      orderUuidNo = orderModel.uuidNumber;
    }

    final purchaseParam = PurchaseParam(
      productDetails: response.productDetails.first,
      /// 注意:iOS的applicationUserName必须为uuid格式,否则App Store服务器不会保存
      /// android: 自己服務器上生成的訂單號
      /// iOS: 自己服务器上生成的与订单绑定的uuid字符串
      applicationUserName: Platform.isIOS ? orderUuidNo : orderNo,
    );

    /// 发起支付请求
    try {
      await InAppPurchase.instance.buyConsumable(
        purchaseParam: purchaseParam,
        /// 安卓不自动消耗,会在后台服务器进行消耗
        autoConsume: !Platform.isAndroid,
      );
      YZToastUtil.dismiss();
    } on PlatformException catch(e) {
      /// iOS重复订单: storekit_duplicate_product_object,可根据自己的业务做处理
      YZToastUtil.showMessage(e.message ?? '');

      // if (Platform.isIOS && e.code == 'storekit_duplicate_product_object') {
      //   /// 查询未处理的订单
      //   final transactions = await SKPaymentQueueWrapper().transactions();
      //   for (final transaction in transactions) {
      //     await SKPaymentQueueWrapper().finishTransaction(transaction);
      //   }
      // }
    }
  }

  /// 处理未完成订单
  Future<void> _handlePastPurchases() async {
    /// 如果是Android系统
    if (Platform.isAndroid) {
      /// 查询未处理的订单
      final androidPlatformAddition = _inAppPurchase.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
      final response = await androidPlatformAddition.queryPastPurchases();
      if (response.error == null) {
        final purchaseDetailsList = response.pastPurchases;
        for (final purchaseDetails in purchaseDetailsList) {
          if (purchaseDetails.billingClientPurchase.purchaseState == PurchaseStateWrapper.pending) {
            /// 订单未付款,此时需要提醒用户完成购买,否则无法进行下一笔订单
            _handleGooglePendingPurchase(purchaseDetails);
          } else if (purchaseDetails.billingClientPurchase.purchaseState == PurchaseStateWrapper.purchased) {
            /// 订单已付款,向服务器验证订单票据
            await _verifyPurchase(purchaseDetails);
          }
        }
      }
    }
  }

  /// 监听内购更新
  Future<void> _listenToPurchaseUpdated(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.pending) {
      /// 等待购买中
      print('等待购买中');
      /// iOS系统此时正处于拉起支付状态,需要比较长的时间,此处给一个提示
      /// Android系统此时正处于支付操作完成,等待Google批准状态
      if (Platform.isIOS) {
        YZToastUtil.showLoading(message: '正在拉起支付');
      }
    } else {
      if (Platform.isIOS) {
        YZToastUtil.dismiss();
      }
      if (purchaseDetails.status == PurchaseStatus.error) {
        /// 购买出错,提示错误信息
        print('购买出错:${purchaseDetails.error?.message}');
        final error = purchaseDetails.error;
        if (error != null) {
          YZToastUtil.showMessage(error.message);
        }
        _completeApplePurchase(purchaseDetails);
      }  else if (purchaseDetails.status == PurchaseStatus.canceled) {
        /// 购买取消
        print('购买取消');
        _completeApplePurchase(purchaseDetails);
      } else if (purchaseDetails.status == PurchaseStatus.purchased || purchaseDetails.status == PurchaseStatus.restored) {
        /// 购买成功或恢复购买,向服务器验证订单票据
        print('购买成功或恢复购买:${purchaseDetails.status}');
        await _verifyPurchase(purchaseDetails);
      }
    }
  }

  /// 发放产品
  void _deliverProduct() {
    context.read<CommonMemberViewModel>().updateLocalMemberInfo();
  }

  /// 将Apple订单标记为已完成
  void _completeApplePurchase(PurchaseDetails purchaseDetails) {
    /// 只需要iOS,Android会在后台进行消耗或确认操作
    if (purchaseDetails is AppStorePurchaseDetails) {
      if (purchaseDetails.pendingCompletePurchase) {
        _inAppPurchase.completePurchase(purchaseDetails);
      }
    }
  }

  /// 将Google订单标记为已消耗
  void _completeGooglePurchase(PurchaseDetails purchaseDetails) {
    if (purchaseDetails is GooglePlayPurchaseDetails) {
      /// 在Android系统,[_inAppPurchase.completePurchase(purchaseDetails)]方法调用的是确认API[acknowledge],
      /// 所以对于消耗品,如果您需要在客户端进行消耗,您需要调用消耗API[consume]
      _inAppPurchase.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>().consumePurchase(purchaseDetails);
    }
  }

  /// 处理Google的PENDING待处理购买交易
  void _handleGooglePendingPurchase(GooglePlayPurchaseDetails purchaseDetails) {
    final productId = purchaseDetails.productID;
    final orderNo = purchaseDetails.billingClientPurchase.obfuscatedAccountId;
    MyMessageDialog(
      width: 305.wRate,
      icon: 'dialog/publish',
      title: '未完成訂單',
      message: '檢測到您有未完成訂單,是否繼續?',
      onConfirm: () {
        startPay(productId: productId, existedOrderNo: orderNo);
      },
    ).show(context);
  }

  /// 向服务器验证订单票据
  Future<void> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    YZToastUtil.showLoading(message: '正在驗證訂單');

    if (purchaseDetails is AppStorePurchaseDetails) {
      /// 向服务器验证票据
      final params = <String, dynamic>{
        'transaction_id': purchaseDetails.skPaymentTransaction.transactionIdentifier,
      };

      await PurchaseApi.verifyApplePay(params: params).then((value) {
        /// 验证成功,发放产品
        _deliverProduct();
        _completeApplePurchase(purchaseDetails);
        YZToastUtil.showMessage('儲值成功');
      }).catchError((e) {
        YZToastUtil.dismiss();
        /// 4000006:无效的交易id 4040010:没有找到交易id 4290000:请求超出速率限制 5000000:服务器错误 5000001:服务器错误 42:没有找到订单
        if (e is YZNetworkError) {
          if (e.code == 4000006 || e.code == 4040010 || e.code == 42) { /// 结束订单
            YZToastUtil.showMessage('無效訂單,請聯絡客服');
            _completeApplePurchase(purchaseDetails);
          } else { /// 重新验单
            _retryVerifyPurchase(purchaseDetails);
          }
        }
      });
    } else if (purchaseDetails is GooglePlayPurchaseDetails) {
      /// 向服务器验证票据
      final params = <String, dynamic>{
        'product_id': purchaseDetails.productID,
        'purchase_token': purchaseDetails.billingClientPurchase.purchaseToken,
        'order_no': purchaseDetails.billingClientPurchase.obfuscatedAccountId,
      };

      await PurchaseApi.verifyGooglePay(params: params).then((value) {
        /// 验证成功,发放产品
        _deliverProduct();
        YZToastUtil.showMessage('儲值成功');
      }).catchError((e) {
        YZToastUtil.dismiss();
        if (e is YZNetworkError) {
          /// 40:验证失败 41:PENDING状态未支付 42:没有找到订单 43:订单不匹配
          if (e.code == 41) { /// 提醒用户继续完成交易
            _handleGooglePendingPurchase(purchaseDetails);
          } else if (e.code == 42 || e.code == 43) {  /// 结束订单
            YZToastUtil.showMessage('無效訂單,請聯絡客服');
            _completeGooglePurchase(purchaseDetails);
          } else {  /// 重新验单
            _retryVerifyPurchase(purchaseDetails);
          }
        }
      });
    }
  }


  /// 验证订单票据失败不要将订单标记为已完成,会通过以下两种方法进行补单:
  /// 1.以斐波那契数列为间隔时间尝试10次重新验证订单票据
  /// 2.在App下次启动时,监听未完成订单
  Future<void> _retryVerifyPurchase(PurchaseDetails purchaseDetails) async {
    Future Function()? retryFn;
    void Function()? retryComplete;
    bool Function(YZNetworkError error)? retryIf;
    if (purchaseDetails is GooglePlayPurchaseDetails) {  /// 安卓订单
      retryFn = () {
        final params = <String, dynamic>{
          'product_id': purchaseDetails.productID,
          'purchase_token': purchaseDetails.billingClientPurchase.purchaseToken,
          'order_no': purchaseDetails.billingClientPurchase.obfuscatedAccountId,
        };
        return PurchaseApi.verifyGooglePay(params: params);
      };

      retryComplete = () {
        /// 重试成功,发放产品
        _deliverProduct();
        _completeApplePurchase(purchaseDetails);
        YZToastUtil.showMessage('儲值成功');
      };

      /// 40:验证失败 41:PENDING状态未支付 42:没有找到订单 43:订单不匹配
      retryIf = (e) => e.code != 41 && e.code != 42 && e.code != 43;
    }

    if (purchaseDetails is AppStorePurchaseDetails) { /// 苹果订单
      retryFn = () {
        final params = <String, dynamic>{
          'transaction_id': purchaseDetails.skPaymentTransaction.transactionIdentifier,
        };
        return PurchaseApi.verifyApplePay(params: params);
      };

      retryComplete = () {
        /// 重试成功,发放产品
        _deliverProduct();
        YZToastUtil.showMessage('儲值成功');
      };

      /// 4000006:无效的交易id 4040010:没有找到交易id 4290000:请求超出速率限制 5000000:服务器错误 5000001:服务器错误 42:没有找到订单
      retryIf = (e) => e.code != 4000006 && e.code != 4040010 && e.code != 42;
    }

    if (retryFn == null) return;

    var attempt = 0;
    while (true) {
      attempt++;
      await Future.delayed(_retryVerifyInterval(attempt));

      try {
        await retryFn();
        retryComplete?.call();
        return;
      } on YZNetworkError catch (e) {
        if (attempt >= 10 || (retryIf != null && !retryIf(e))) {
          rethrow;
        }
      }
    }
  }

  /// 重新验证订单票据间隔时间, 以斐波那契数列为间隔时间尝试10次重新验证订单票据
  Duration _retryVerifyInterval(int attempt) {
    if (attempt < 2) {
      return Duration(seconds: attempt);
    }
    const mod = 1000000007;
    var p = 0;
    var q = 0;
    var r = 1;
    for (var i = 2; i <= attempt; i++) {
      p = q;
      q = r;
      r = (p + q) % mod;
    }
    return Duration(seconds: r);
  }

  @override
  void dispose() {
    _purchaseSubscription.cancel();
    super.dispose();
  }
}
上一篇下一篇

猜你喜欢

热点阅读