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

Flutter同一账号跨设备内购异常:重复购买崩溃及循环问题

Flutter跨设备同一账号内购重复购买崩溃&无限循环问题排查与修复

核心问题根源

  1. 非消耗型/订阅项目的跨设备已购状态未被正确拦截:同一沙盒账号购买后,第二台设备未在购买前校验已购状态,导致用户点击确定时仍发起重复购买请求。
  2. 购买流的Stream管理混乱:重复订阅、未及时取消订阅,或者未正确处理购买状态回调,引发无限循环或内存泄漏,最终导致崩溃。
  3. 缺乏购买请求锁机制:多次点击确定按钮触发并发购买请求,导致支付SDK内部状态冲突。

分步修复方案

1. 购买前强制校验已购状态

在用户触发购买操作的第一时间,先查询账号的已购项目列表,若目标商品已购买,直接终止购买流程并提示用户。

示例代码(基于flutter_inapppurchase库):

Future<void> onPurchaseButtonTap(String productId) async {
  // 查询历史购买记录
  final purchaseQueryResult = await InAppPurchase.instance.queryPastPurchases();
  if (purchaseQueryResult.error != null) {
    debugPrint('查询已购项目失败: ${purchaseQueryResult.error!.message}');
    return;
  }

  // 校验目标商品是否已购买
  final hasPurchased = purchaseQueryResult.pastPurchases.any(
    (purchase) => purchase.productID == productId && purchase.status == PurchaseStatus.purchased
  );

  if (hasPurchased) {
    // 提示用户已购买,终止流程
    showDialog(
      context: context,
      builder: (_) => AlertDialog(
        title: const Text('提示'),
        content: const Text('该项目已购买,无需重复购买'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('确定')
          )
        ]
      )
    );
    return;
  }

  // 未购买则发起购买请求
  await _startPurchaseFlow(productId);
}

Future<void> _startPurchaseFlow(String productId) async {
  final products = await InAppPurchase.instance.queryProductDetails({productId});
  if (products.error != null || products.productDetails.isEmpty) {
    debugPrint('获取商品详情失败');
    return;
  }

  final purchaseParam = PurchaseParam(productDetails: products.productDetails.first);
  await InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam);
}

2. 规范Stream订阅管理

避免重复订阅购买状态流,确保页面销毁时取消订阅,防止内存泄漏和回调冲突。

示例代码:

StreamSubscription<List<PurchaseDetails>>? _purchaseStreamSub;

@override
void initState() {
  super.initState();
  // 仅在页面初始化时订阅一次购买流
  _purchaseStreamSub = InAppPurchase.instance.purchaseStream.listen(
    _handlePurchaseUpdates,
    onError: (error) => debugPrint('购买流异常: $error')
  );
}

@override
void dispose() {
  // 页面销毁时必须取消订阅
  _purchaseStreamSub?.cancel();
  super.dispose();
}

void _handlePurchaseUpdates(List<PurchaseDetails> purchases) async {
  for (final purchase in purchases) {
    switch (purchase.status) {
      case PurchaseStatus.pending:
        // 显示加载状态
        break;
      case PurchaseStatus.purchased:
        // 处理购买成功逻辑(同步本地/服务器状态)
        await _processSuccessfulPurchase(purchase);
        break;
      case PurchaseStatus.error:
        // 提示用户购买失败
        showDialog(
          context: context,
          builder: (_) => AlertDialog(
            title: const Text('购买失败'),
            content: Text(purchase.error?.message ?? '未知错误'),
            actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('确定'))]
          )
        );
        break;
      case PurchaseStatus.restored:
        // 处理恢复购买逻辑
        await _processRestoredPurchase(purchase);
        break;
    }

    // 必须完成购买处理,避免队列阻塞
    if (purchase.pendingCompletePurchase) {
      await InAppPurchase.instance.completePurchase(purchase);
    }
  }
}

3. 添加购买请求锁

用布尔变量标记当前是否有正在进行的购买请求,拦截重复点击导致的并发请求。

示例代码:

bool _isPurchaseInProgress = false;

Future<void> onPurchaseButtonTap(String productId) async {
  if (_isPurchaseInProgress) {
    // 已有购买请求在处理,直接返回
    return;
  }

  _isPurchaseInProgress = true;
  try {
    // 执行前面的校验和购买流程
    await _checkPurchasedAndStartFlow(productId);
  } finally {
    // 无论成功失败,释放锁
    _isPurchaseInProgress = false;
  }
}

4. 沙盒环境特殊适配

沙盒环境下支付回调可能存在延迟或重复通知,需额外处理:

  • 购买成功后优先向服务器验证凭证,再更新本地状态,确保跨设备状态一致。
  • 恢复购买后立即同步本地状态,避免用户因本地状态错误再次触发购买。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 11:37:27