I'm struggling with in app payments on iOS. When I try to launch app, I'm restoring past purchases, sometimes I'm getting SKError with code 0 (unknown error). I am not able to get available products, and purchase list. Have no idea what cause error, I am struggling with this for few days.
The important thing is that sometimes it works properly and sometimes it just doesn't. Android version is working perfectly.
Xcode: 14.2 Flutter version: 3.7.6 Dart version: 2.19.3 Package and version that I'm using: in_app_purchase: ^3.1.5
devices where problem occurs: iPhone Xs MAX, iOS 16.3.1 iPhone 6s, iOS 15.7.3 iPad Air 2, iOS 15.7.3
Here is my code inside cubit:
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));
}
}
}
Start method is calling after bloc declaration in MyApp like this:
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()),
);
}
}