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
相关产品推荐
相关产品推荐

