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

Flutter iOS初始化时内购监听器返回历史购买记录问题

Flutter in_app_purchase iOS初始化时历史购买记录重复处理问题

我正在使用in_app_purchase包为Flutter应用集成内购功能,已完成Google Play和Apple App Store沙箱环境配置,购买功能在双平台均正常运行。

当前配置

购买流程在双平台均正常,可成功订阅并在Google Play及iOS沙箱中正确显示。我创建了Getx Controller来初始化内购监听器和商店信息,相关代码如下:

监听器初始化(onInit方法)

late InAppPurchasePlatform _inAppPurchasePlatform;

@override
Future<void> onInit() async {
  _inAppPurchasePlatform = InAppPurchasePlatform.instance;

  final Stream<List<PurchaseDetails>> purchaseUpdated =
    _inAppPurchasePlatform.purchaseStream;

  _subscription0 =
    purchaseUpdated.listen((List<PurchaseDetails> purchaseDetailsList) {
   _handlePurchaseUpdates(purchaseDetailsList);
  }, onDone: () {
    _subscription0.cancel();
    print("purchase listener onDone :");
  }, onError: (Object error) {
    print("purchase listener erro :" + error.toString());
  });

  initStoreInfo();

  super.onInit();
}

initStoreInfo实现

Future<void> initStoreInfo() async {
    print("initStoreInfo called:");

     try {
      final bool isAvailable = await _inAppPurchasePlatform.isAvailable();
      if (!isAvailable) {
      error.value = 'Store is unavailable. Please try again later.';
      return;
     }

     print("Store is available.");
     final List<IAPPlanModel> fetchedPlans = await fetchIAPPlanModel();
     plans.addAll(fetchedPlans);

    _kProductIds.addAll(fetchedPlans
      .map((plan) => plan.productId ?? '')
      .where((id) => id.isNotEmpty));

    final ProductDetailsResponse productDetailResponse =
       await _inAppPurchasePlatform
          .queryProductDetails(_kProductIds.toSet());

    if (productDetailResponse.error != null) {
     error.value = productDetailResponse.error!.message!;
     return;
   }

   if (productDetailResponse.productDetails.isEmpty) {
     error.value = 'No products available at the moment.';
     return;
   }

   _linkPlansWithProductDetails(productDetailResponse.productDetails);

   if (GetPlatform.isAndroid) {
    final InAppPurchaseAndroidPlatformAddition addition =
        InAppPurchasePlatformAddition.instance!
            as InAppPurchaseAndroidPlatformAddition;
    unawaited(
        addition.setBillingChoice(BillingChoiceMode.userChoiceBilling));
    } else if (Platform.isIOS) {
      //write code for ios
   }
  } catch (e) {
   print("Error initializing store: $e");
   error.value = "Error initializing store: $e";
   isLoading.value = false;
   return;
  }
}

_handlePurchaseUpdates函数

void _handlePurchaseUpdates(List<PurchaseDetails> purchaseDetailsList) async {
print("purchaseDetailsList : " + purchaseDetailsList.length.toString());

final list = _plansController.subscriptionsList
    .map((e) => e.transactionIds)
    .toList();
list.map((e) => print("listitems : " + e.toString()));

print("list : " + list.toString());
print("list : " + list.length.toString());

for (final PurchaseDetails details in purchaseDetailsList) {
  purchases[details.productID] = details;

  var purchaseID = details.purchaseID;
  print(" purchaseID:" + purchaseID.toString());

  print("PurchaseDetails.status :" + details.status.toString());

  if (details.pendingCompletePurchase) {
    try {
      await _inAppPurchasePlatform.completePurchase(details);
    } catch (e) {
      print("Error completing purchase: $e");
    }
  }

  if (details.status == PurchaseStatus.canceled) {
    purchasePending.value = false;
    isLoading.value = false;
    isButtonClicked.value = false;
    return;
  }


  if (details.status == PurchaseStatus.error) {
    error.value = details.error?.message ?? "Unknown error occurred.";
    purchasePending.value = false;
    isLoading.value = false;
    isButtonClicked.value = false;
    Get.snackbar(
      'Purchase Error',
      '',
      backgroundColor: Colors.red,
      snackPosition: SnackPosition.BOTTOM,
      duration: const Duration(seconds: 3),
    );
    if (details.error?.message == "BillingResponse.itemAlreadyOwned") {
      _showAlreadyOwnedDialog();
    }
    print("PurchaseStatus.error " + details.error!.message.toString());
    return;
  }
  isLoading.value = true;

  if (details.status == PurchaseStatus.pending) {
    purchasePending.value = true;
    return;
  } else if (details.status == PurchaseStatus.error) {
    error.value = details.error?.message ?? "Unknown error occurred.";
    purchasePending.value = false;
    await _sendPurchaseToBackend(details, false);
  } else if (details.status == PurchaseStatus.restored) {
    error.value = details.error?.message ?? "Subscription restored";
    isButtonClicked.value = false;
    return;
  } else if (details.status == PurchaseStatus.purchased) {
    activePlanId.value = details.productID;
    purchasePending.value = true;
    await _sendPurchaseToBackend(details, true);
    _processedPurchases.add(details.productID);
  }

  if (details.pendingCompletePurchase) {
    await _inAppPurchasePlatform.completePurchase(details);
    await fetchUserActivePlan();
  }
}
}

问题描述

在iOS端,应用启动并初始化Getx Controller时,内购监听器purchaseStream会立即返回包含历史购买记录的purchaseDetailsList,状态显示为PurchaseStatus.purchased,此时并未发起新的支付请求,导致无法区分历史购买记录与新订单。

打印的历史交易信息如下:

flutter: old originalT : {transactionState: 1, payment: {productIdentifier: spindle_plus_30_days, applicationUsername: null, requestData: null, quantity: 1, simulatesAskToBuyInSandbox: false
                }, originalTransaction: null, transactionTimeStamp: 1737634871.0, transactionIdentifier: 2000000838444102, error: null
            }

已尝试通过按钮触发支付弹窗,该功能正常,但初始化时的问题仍存在。

疑问解答

1. 为何iOS初始化时purchaseStream会返回历史购买记录?

这是iOS StoreKit框架的默认行为:当应用启动时,StoreKit会自动将用户账号下未完成、未标记为已处理的交易推送给应用,确保所有购买交易都能被正确处理,避免因应用崩溃、意外退出等情况导致购买记录丢失。即使交易已经完成,只要应用没有调用completePurchase标记为已处理,下次启动时仍会被推送。

2. 如何区分历史购买记录与新订单?

  • 跟踪交易ID:将已成功处理的transactionIdentifier存储在本地(比如用shared_preferences),每次收到交易时先检查该ID是否已存在于本地列表中,存在则为历史记录。
  • 检查交易时间戳:新订单的transactionTimeStamp是应用本次启动后的时间,历史记录的时间戳则更早,可以通过对比应用启动时间来区分。
  • 判断触发来源:新订单是用户主动点击购买按钮发起的,可以在发起购买时记录当前的交易ID或标记,收到回调时匹配该标记。

3. 推荐处理方案避免重复处理历史记录?

  1. 维护本地已处理交易ID列表

    • 使用shared_preferences存储已处理的transactionIdentifier,每次处理交易前先查询是否存在:
      final prefs = await SharedPreferences.getInstance();
      final processedIds = prefs.getStringList('processed_transactions') ?? [];
      if (processedIds.contains(details.transactionIdentifier)) {
        // 已处理过,跳过
        return;
      }
      
    • 处理完成后将交易ID添加到列表并保存:
      processedIds.add(details.transactionIdentifier!);
      await prefs.setStringList('processed_transactions', processedIds);
      
  2. 优化_handlePurchaseUpdates逻辑

    • 先判断交易是否已处理,再执行后续操作;
    • 对于历史购买记录,仅同步用户订阅状态到本地,不重复调用后端接口;
    • 确保所有交易都调用completePurchase,标记为已处理,避免下次启动时再次推送。
  3. 初始化时主动恢复购买(可选)

    • 在initStoreInfo中调用_inAppPurchasePlatform.restorePurchases(),确保所有已购买的订阅都被正确同步,同时结合本地已处理列表避免重复操作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 21:09:50