Flutter in_app_purchase couse uknown SKError on iOS
Asked Answered
M

2

10

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()),
    );
  }
}
Merell answered 9/3, 2023 at 8:25 Comment(4)
have you found any solution? I have a similar issue.Britneybritni
@YatinjSutariya my temporary (and dirty) solution is to call the restore function 3 times with some time distance. Example: await InAppPurchase.instance.restorePurchases().onError( (error, stackTrace) => Future.delayed( Duration(seconds: 4), () async => but like I said before, this is not a valid solution to the problemMerell
Any luck figuring out what's happening? I have the same issue.. :(Remonstrate
@MarekHalmo Unfortunately notMerell
G
0

The code looks fine.

When SKError Code 0 occurs you might want to check localizedDescription property of the error object.

When this error occurs during testing, it can often be resolved by logging out of iTunes and/or the App Store and creating a new test user account in App Store Connect. This approach only works when running the app on a device, not the simulator.

When this error occurs in production, it may indicate a problem with the user’s iTunes account.

Goren answered 22/3, 2023 at 14:25 Comment(1)
Hello, thanks for your answer. I think this is not a solution because problem occurs on many different appStore test users accounts and devices. My temporary solution is to call restore app purchases 3 times in some time interval. Then this is more likely to work. But it is dirty solution and I want to make it right.Merell
R
0

I had the same issue and solved it by successfully publishing the app to AppStore. The app and IAP's have to bee approved in a review.

Please make sure that you also add your in app purchases to the review and provide screenshot where to find them + description on what benefit it gives to the user. A new version of app will be necessary with every addition of IAPs.

After the IAPs are accepted - this error will be gone.

Remonstrate answered 5/10, 2023 at 14:19 Comment(1)
Thank you for your response. I am currently unable to test it because I am no longer working on the project where this issue arises. However, as soon as I launch a new app, I will test it out and mark this comment as a solution.Merell

© 2022 - 2024 — McMap. All rights reserved.