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

Flutter in_app_purchase iOS端偶发SKError(代码0)问题求助

Flutter iOS内购偶发SKError(code:0)问题

问题描述

iOS端启动应用恢复过往购买记录时,偶发代码为0的SKError(未知错误),导致无法获取可用商品列表与购买记录。该问题非必现,Android端功能完全正常。

环境信息

  • Xcode:14.2
  • Flutter版本:3.7.6
  • Dart版本:2.19.3
  • 使用包及版本:in_app_purchase: ^3.1.5

问题设备

  • iPhone Xs MAX(iOS 16.3.1)
  • iPhone 6s(iOS 15.7.3)
  • iPad Air 2(iOS 15.7.3)

相关代码

PaymentsCubit实现

import 'dart:async';
import 'dart:io';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

part 'payments_state.dart';

class PaymentsCubit extends Cubit<PaymentsState> {
  PaymentsCubit()
      : super(PaymentsState(
          isPremium: null,
          avaliable: false,
          products: [],
          purchases: [],
        ));
  late StreamSubscription subscription;
  late InAppPurchase iap;

  void start() async {
    iap = InAppPurchase.instance;

    state.avaliable = await iap.isAvailable();
    if (state.avaliable) {
      //listener must be declared before get products and purchases calls
      subscription = iap.purchaseStream.listen(
        (purchaseDetailsList) {
          listenToPurchaseUpdated(purchaseDetailsList);
        },
        onDone: () => subscription.cancel(),
        onError: (error) => print(error),
      );
      await getProducts();
      await getPastPurchases();
      //it's listening to every changes in purchases e.g. buy new product, restore old products.
      //everytime when change will occur, then function is triggered
      //remember to always cancel listeners to avoid memmory leaks
    }
    emit(state);
  }

  Future<void> getProducts() async {
    Set<String> ids = Platform.isAndroid
        ? {
            'com.example.example.example',
            'com.example.example.exampleNew'
          }
        : {
            'com.example.example.exampleNew',
          };
    ProductDetailsResponse response = await iap.queryProductDetails(ids);
    state.products = response.productDetails;
  }

  Future<void> getPastPurchases() async {
    //this will trigger listener if user made purchases in the past
    await iap.restorePurchases();
  }

  void buyProduct() async {
    state.avaliable = await iap.isAvailable();
    if (state.avaliable) {
      final PurchaseParam purchaseParam = PurchaseParam(
        productDetails: state.products.firstWhere(
          (element) =>
              element.id ==
              (Platform.isAndroid
                  ? 'com.example.example.example'
                  : 'com.example.example.exampleNew'),
        ),
      );
      await InAppPurchase.instance
          .buyNonConsumable(purchaseParam: purchaseParam);
    }
  }

  void listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
    purchaseDetailsList.forEach((element) {
      if (element.status == PurchaseStatus.pending) {
        //handle situation when user pressed button to start purchase, (on iOS it takes more time, android have almost immediately response) maybe show some loading spinners
      } else if (element.status == PurchaseStatus.error) {
      } else if (element.status == PurchaseStatus.canceled) {
      } else if (element.status == PurchaseStatus.purchased ||
          element.status == PurchaseStatus.restored) {
        state.purchases.add(element);
        emit(state.copyWith(
          isPremium: true,
        ));
        iap.completePurchase(element);
      }
    });
    if (state.isPremium == null) {
      emit(state.copyWith(isPremium: false));
    }
  }
}

MyApp中调用start方法

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider(create: (context) => PaymentsCubit()..start()),
      ],
      child: MaterialApp(
          title: 'Flutter Demo',
          debugShowCheckedModeBanner: false,
          theme: ThemeData(
            fontFamily: 'Rubik',
            primarySwatch: Colors.blue,
          ),
          home: const FakeSplashscreen()),
    );
  }
}

解决方案

1. 修复状态修改的错误操作

直接修改state属性违反了Cubit状态不可变原则,会导致状态更新异常,进而触发内购未知错误。需通过copyWith创建新状态并emit:

void start() async {
  iap = InAppPurchase.instance;
  final available = await iap.isAvailable();
  if (available) {
    subscription = iap.purchaseStream.listen(
      (purchaseDetailsList) {
        listenToPurchaseUpdated(purchaseDetailsList);
      },
      onDone: () => subscription.cancel(),
      onError: (error) => print(error),
    );
    final products = await getProducts();
    await getPastPurchases();
    emit(state.copyWith(
      avaliable: available,
      products: products,
    ));
  } else {
    emit(state.copyWith(avaliable: available));
  }
}

Future<List<ProductDetails>> getProducts() async {
  Set<String> ids = Platform.isAndroid
      ? {
          'com.example.example.example',
          'com.example.example.exampleNew'
        }
      : {
          'com.example.example.exampleNew',
        };
  ProductDetailsResponse response = await iap.queryProductDetails(ids);
  return response.productDetails;
}

2. 捕获restorePurchases异常并添加重试逻辑

iOS的restorePurchases偶发未知错误,添加异常捕获和重试,避免阻塞流程:

Future<void> getPastPurchases() async {
  try {
    await iap.restorePurchases();
  } catch (e) {
    // 最多重试2次
    for (int i = 0; i < 2; i++) {
      try {
        await Future.delayed(const Duration(milliseconds: 500));
        await iap.restorePurchases();
        break;
      } catch (retryError) {
        if (i == 1) {
          print("恢复购买两次失败: $retryError");
          // 可在此处提示用户恢复失败
        }
      }
    }
  }
}

3. 调整操作顺序避免竞态条件

先获取商品列表并更新状态,延迟后再执行恢复购买,避免iOS内购服务同时处理多个请求:

void start() async {
  iap = InAppPurchase.instance;
  final available = await iap.isAvailable();
  if (available) {
    subscription = iap.purchaseStream.listen(
      (purchaseDetailsList) {
        listenToPurchaseUpdated(purchaseDetailsList);
      },
      onDone: () => subscription.cancel(),
      onError: (error) => print(error),
    );
    final products = await getProducts();
    emit(state.copyWith(
      avaliable: available,
      products: products,
    ));
    await Future.delayed(const Duration(milliseconds: 300));
    await getPastPurchases();
  } else {
    emit(state.copyWith(avaliable: available));
  }
}

4. 完善错误处理逻辑

在listenToPurchaseUpdated中处理错误状态,打印详细信息便于排查:

else if (element.status == PurchaseStatus.error) {
  print("购买错误: ${element.error?.message},code: ${element.error?.code}");
  // code为0时可提示用户重试
}

5. 正确管理订阅

在Cubit的close方法中取消订阅,避免内存泄漏:

@override
Future<void> close() {
  subscription.cancel();
  return super.close();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 15:45:15