You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS端In-App Purchase订阅流返回错误AppStorePurchaseDetails问题

问题描述
  • 在App Store Connect中创建了单个非消耗型订阅,iOS端重复执行以下操作5-7次后出现异常:
    1. 调用InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam)完成测试交易
    2. 删除并重装App
    3. 调用InAppPurchase.instance.restorePurchases()恢复订阅
  • 异常表现:订阅流返回大量错误的AppStorePurchaseDetails列表
  • 预期结果:因仅存在一个非消耗型订阅,购买或恢复时应仅返回一个交易详情对象(Android端功能正常)
问题分析

从提供的_onPurchaseUpdate代码来看,存在以下核心问题:

  1. iOS端未正确完成交易:在PurchaseStatus.purchased分支中,仅调用了finishTransaction()但未执行InAppPurchase.instance.completePurchase(purchaseDetails),导致App Store的交易未被标记为完成,后续恢复操作会反复返回历史交易记录。
  2. 恢复状态(restored)未处理:代码仅打印日志,既没有验证恢复的交易,也没有完成交易流程,未收尾的恢复交易会被框架持续返回。
  3. 跨平台逻辑混淆:Android端的consumePurchase和交易验证逻辑被放在了通用的purchased分支中,iOS代码块执行后会再次跑Android逻辑,导致交易处理混乱。
  4. 错误的列表操作:手动调用purchaseDetailsList.clear()属于越权操作,框架内部的交易列表应自行维护,强行清空会导致状态不一致。
修复方案

修改后的_onPurchaseUpdate函数如下:

Future<void> _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) async {
  for (final purchaseDetails in purchaseDetailsList) {
    switch (purchaseDetails.status) {
      case PurchaseStatus.canceled:
        await InAppPurchase.instance.completePurchase(purchaseDetails);
        paymentState.value = PaymentState.failed;
        errorMessage = "Purchase Cancelled";
        update();
        Future.delayed(const Duration(seconds: 1), () {
          paymentState.value = PaymentState.init;
        });
        break;

      case PurchaseStatus.purchased:
        try {
          if (Platform.isIOS) {
            final String? iosPurchaseToken = purchaseDetails.verificationData.localVerificationData;
            if (iosPurchaseToken?.isNotEmpty == true) {
              print("IOS >> $iosPurchaseToken");
              final params = CheckPurchaseTokenValidityParams(
                deviceType: 'IOS',
                token: iosPurchaseToken!,
                price: _purchasePrice,
              );
              final result = await checkPurchaseTokenValidityUseCase(params);
              result.fold(
                (failure) {
                  errorMessage = failure.toString();
                  paymentState.value = PaymentState.failed;
                },
                (isValid) {
                  _subscriptionDetailsData = isValid;
                  SharedPrefUtility.setisPremium();
                  paymentState.value = PaymentState.success;
                  _navigateToWelcomeIfNeeded();
                },
              );
            }
          } else if (Platform.isAndroid) {
            final purchaseData = json.decode(purchaseDetails.verificationData.localVerificationData);
            final subscriptionID = purchaseData?["productId"];
            final packageName = purchaseData?["packageName"];
            final purchaseToken = purchaseData?["purchaseToken"];

            if (purchaseData?["acknowledged"] == false) {
              final androidAddition = InAppPurchase.instance.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
              await androidAddition.consumePurchase(purchaseDetails);
            }

            final params = CheckPurchaseTokenValidityParams(
              deviceType: "ANDROID",
              token: purchaseToken,
              price: _purchasePrice,
              packageName: packageName,
              subscriptionID: subscriptionID,
              isUpgraded: _isSubscribed,
            );
            final result = await checkPurchaseTokenValidityUseCase(params);
            result.fold(
              (failure) {
                errorMessage = failure.toString();
                paymentState.value = PaymentState.failed;
              },
              (isValid) {
                _subscriptionDetailsData = isValid;
                SharedPrefUtility.setisPremium();
                paymentState.value = PaymentState.success;
                _navigateToWelcomeIfNeeded();
              },
            );
          }
          // 统一完成交易,避免重复返回
          await InAppPurchase.instance.completePurchase(purchaseDetails);
          update();
        } catch (e) {
          errorMessage = e.toString();
          paymentState.value = PaymentState.failed;
          update();
        }
        break;

      case PurchaseStatus.error:
        errorMessage = purchaseDetails.error.toString();
        paymentState.value = PaymentState.failed;
        update();
        // 错误交易也需完成,防止重复推送
        await InAppPurchase.instance.completePurchase(purchaseDetails);
        break;

      case PurchaseStatus.restored:
        print("Restore Purchased Called");
        // 处理恢复的交易
        if (Platform.isIOS) {
          final iosPurchaseToken = purchaseDetails.verificationData.localVerificationData;
          if (iosPurchaseToken?.isNotEmpty == true) {
            final params = CheckPurchaseTokenValidityParams(
              deviceType: 'IOS',
              token: iosPurchaseToken!,
              price: _purchasePrice,
            );
            final result = await checkPurchaseTokenValidityUseCase(params);
            result.fold(
              (failure) => errorMessage = failure.toString(),
              (isValid) {
                _subscriptionDetailsData = isValid;
                SharedPrefUtility.setisPremium();
                paymentState.value = PaymentState.success;
                _navigateToWelcomeIfNeeded();
              },
            );
          }
        }
        await InAppPurchase.instance.completePurchase(purchaseDetails);
        update();
        break;
    }
  }
}

// 提取重复导航逻辑
void _navigateToWelcomeIfNeeded() {
  if (!isNavigated && !SharedPrefUtility.getIsVisitedToWelcomeScreen()) {
    locator.get<NavigationService>().pushNamed(
      AppRoutes.welcomePlusUser,
      arguments: const <String, dynamic>{"isFirst": false},
    );
    SharedPrefUtility.setIsVisitedToWelcomeScreen();
    isNavigated = true;
    update();
  }
}
关键修复点说明
  1. 统一交易完成流程:所有交易状态(取消、购买、错误、恢复)都调用completePurchase,确保App Store将交易标记为已处理,不会重复返回。
  2. 严格区分跨平台逻辑:用Platform.isIOS/Platform.isAndroid隔离两端代码,避免Android逻辑在iOS端执行。
  3. 完善恢复交易处理:在restored分支中添加验证和完成步骤,确保恢复的交易被正确收尾。
  4. 移除错误的列表清空操作:删除purchaseDetailsList.clear(),交由框架自行维护交易列表。
  5. 提取重复逻辑:将导航逻辑抽为单独函数,提升代码可读性和可维护性。

内容的提问来源于stack exchange,提问作者Mrudang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 15:41:11