RevenueCat / purchases-flutter

Flutter plugin for in-app purchases and subscriptions. Supports iOS, macOS and Android.
https://www.revenuecat.com/
MIT License
608 stars 169 forks source link

Purchases.getOfferings throws PurchaseNotAllowedError #650

Closed UlyssesAlves closed 1 year ago

UlyssesAlves commented 1 year ago

I am being notified of the following error in my app by Firebase Crashlytics:

PlatformException(3, The device or user is not allowed to make the purchase., {code: 3, message: The device or user is not allowed to make the purchase., readableErrorCode: PurchaseNotAllowedError, readable_error_code: PurchaseNotAllowedError, underlyingErrorMessage: Billing is not available in this device. DebugMessage: Google Play In-app Billing API version is less than 3. ErrorCode: 3.}, null). Error thrown ... at StandardMethodCodec.decodeEnvelope(message_codecs.dart:653) at MethodChannel._invokeMethod(platform_channel.dart:315) at Purchases.getOfferings(purchases_flutter.dart:194) at ...

This error prevents the affected users from making purchases, which of course is very bad to my business.

From the crashlytics bug report, I could extract the following additional relevant information:

RCGitBot commented 1 year ago

👀 SDKONCALL-246 We've just linked this issue to our internal tracker and notified the team. Thank you for reporting, we're checking this out!

michaelAtRC commented 1 year ago

Hello @UlyssesAlves Can you please fill out all of the information in our bug report form?

‼️ Required data ‼️

Do not remove any of the steps from the template below. If a step is not applicable to your issue, please leave that step empty.

There are a lot of things that can contribute to things not working. Having a very basic understanding of your environment will help us understand your issue faster!

Environment

Describe the bug

A clear and concise description of what the bug is. The more detail you can provide the faster our team will be able to triage and resolve the issue.

Additional context

Add any other context about the problem here.

UlyssesAlves commented 1 year ago

Output of flutter doctor

C:\Users\ulyss>flutter doctor Doctor summary (to see all details, run flutter doctor -v): [√] Flutter (Channel stable, 3.7.7, on Microsoft Windows [versÆo 10.0.19044.2728], locale pt-BR) [X] Windows Version (Unable to confirm if installed Windows version is 10 or greater) Checking Android licenses is taking an unexpectedly long time...[√] Android toolchain - develop for Android devices (Android SDK version 31.0.0) [√] Chrome - develop for the web [!] Visual Studio - develop for Windows (Visual Studio Community 2022 17.5.3) X Visual Studio is missing necessary components. Please re-run the Visual Studio installer for the "Desktop development with C++" workload, and include these components: MSVC v142 - VS 2019 C++ x64/x86 build tools

  • If there are multiple build tool versions available, install the latest C++ CMake tools for Windows Windows 10 SDK [√] Android Studio (version 2020.3) [√] VS Code (version 1.70.0) [√] Connected device (3 available) [√] HTTP Host Availability

! Doctor found issues in 2 categories.

Version of purchases-flutter

4.11.1

Testing device version

How often the issue occurs

This error doesn't occur in my own test devices. It only occurs on my customers' devices. Not in all of them, but in many.

Firebase Crashlytics reports the following statistics for this error so far:

Debug logs that reproduce the isue

Fatal Exception: io.flutter.plugins.firebase.crashlytics.FlutterError: PlatformException(3, The device or user is not allowed to make the purchase., {code: 3, message: The device or user is not allowed to make the purchase., readableErrorCode: PurchaseNotAllowedError, readable_error_code: PurchaseNotAllowedError, underlyingErrorMessage: Billing is not available in this device. DebugMessage: Google Play In-app Billing API version is less than 3. ErrorCode: 3.}, null). Error thrown Erro genérico ao tentar obter oferta escolhida pelo usuário.. at StandardMethodCodec.decodeEnvelope(message_codecs.dart:653) at MethodChannel._invokeMethod(platform_channel.dart:315) at Purchases.getOfferings(purchases_flutter.dart:231) at _TelaControleContaState.obtenhaOfertaEscolhidaPeloUsuario(tela_controle_conta.dart:248) at _TelaControleContaState.apresenteOfertasAssinaturaParaUsuario(tela_controle_conta.dart:189) at _TelaControleContaState.construaOpcoesAssinaturaInativa.(tela_controle_conta.dart:544)

Steps to reproduce, with a description of expected vs. actual behavior

  1. Download and install my app at https://play.google.com/store/apps/details?id=alves.ulysses.biblereadingapp.
  2. Access left-hand main menu.
  3. Check if your device is connected to the internet.
  4. Select first option called "Ofertas de Assinatura" (the one with a shopping cart icon).
  5. At this moment my app makes a call to Purchases.getOfferings() to load the offerings prices, but then this method call throws the exception with error code PurchaseNotAllowedError, which interrupts the offerings screen from being loaded and shown to the user. My app has a try/catch block which handles the exception but is unable to show my subscriptions offers to the affected users.

The excepted behaviour would be purchases-flutter being successfully able to load my subscription offers, so that my app would show the offerings to the user, and then the user could purchase one of the available subscriptions.

Describe the bug

Many of my app users are being affected by this problem which prevents them from being presented to my app available subscription offers. Because of this, these users can't buy anything I sell in my app, even though they are showing interest in buying something, since they have taken the action to open the offerings screen. The screen unfortunately won't open because purchases_flutter throws an error when Purchases.getOfferings() is called. The error says:

Because these are very technical issues, I can't handle them to the user to try to solve the problem by himself. But I have written an article with some suggestions on how the user could hopefully make the purchase work, like updating his device or Android operating system to the latest available version, for example. But this is not a bug fix. I would rather have a real solution, but I couldn't find any yet.

Additional Context

My app manages users with Firebase.

The error reports suggest this error started happening when I implemented automatic restoring of the user purchases by calling Purchases.syncPurchases() right after calling Purchases.logIn(user.uid).

Future<void> logInUserToRevenueCat(User user) async {
    await Purchases.logIn(user.uid);

    await Purchases.syncPurchases();
  }

The logInUserToRevenueCat() method is conditionally called when the app detects that the RevenueCat user id is different from the Firebase user id. By calling this method in this scenario, I try to make sure the user is correctly logged in on RevenueCat with its Firebase user id.

Future<void> refreshUserAuthenticationStateOnRevenueCat() async {
    final User currentUser = FirebaseAuth.instance.currentUser;

    if (currentUser == null) {
      print('User is not authenticated on Firebase.');

      if (!await Purchases.isAnonymous) {
        print('Logging user out of RevenueCat.');

        try {
          await Purchases.logOut();
        } on PlatformException catch (error, stackTrace) {
          if (!await InternetConnectionChecker().hasConnection) {
            print(
                'Ignores logout erros due to internet connection issues.');
          } else if (error.code == "22") {
            print(
                'Ignoring logout errors due to user being anonymous on RevenueCat.');
          } else {
            reportExceptionToFirebaseCrashlytics(
                error, stackTrace);
          }
        } catch (error, stackTrace) {
          reportExceptionToFirebaseCrashlytics(
              error, stackTrace);
        }
      } else {
        print(
            'User is already anonimous on RevenueCat, so it does not need to be logged out.');
      }
    } else {
      print('User is authenticated on Firebase.');

      if (await Purchases.appUserID != currentUser.uid) {
        print('Logging user in RevenueCat.');
          // CALLS THE METHOD I WROTE ABOVE
        await logInUserToRevenueCat(currentUser);
      } else {
        print(
            'User is already logged in RevenueCat with same UID of Firebase user.');
      }
    }
  }

This user syncronization logic is called by a listener of Firebase auth changes, like this:

FirebaseAuth.instance.authStateChanges().listen((usuario) async {
    await refreshUserAuthenticationStateOnRevenueCat();
  });

I am trying to remove this call to Purchases.syncPurchases(), but then the app does not recognizes the current subscription status from other devices. So I think I may need to keep calling Purchases.syncPurchases(), but in a different way, if this is really the root cause of the problem.

UlyssesAlves commented 1 year ago

@michaelAtRC thanks for giving me the directions to report the problem. I've provided the information you asked for in the previous comment.

UlyssesAlves commented 1 year ago

I have just released a new version of my app, removing the call to Purchases.syncPurchases(), but the error PurchaseNotAllowed keeps being thrown regardless.

michaelAtRC commented 1 year ago

Hey @UlyssesAlves ,

I am going to close out this github issue because we are working on this together in the internal support ticket!