Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] [in_app_purchase] Can't restore iOS last purchases #89950

Closed
erperejildo opened this issue Sep 12, 2021 · 32 comments
Closed

[iOS] [in_app_purchase] Can't restore iOS last purchases #89950

erperejildo opened this issue Sep 12, 2021 · 32 comments
Labels
found in release: 2.5 Found to occur in 2.5 has reproducible steps The issue has been confirmed reproducible and is ready to work on p: in_app_purchase Plugin for in-app purchase P2 Important issues not at the top of the work list package flutter/packages repository. See also p: labels. platform-ios iOS applications specifically r: solved Issue is closed as solved

Comments

@erperejildo
Copy link

erperejildo commented Sep 12, 2021

Steps to Reproduce

  1. Install in_app_purchase: https://pub.dev/packages/in_app_purchase#restoring-previous-purchases
  2. Configure to work on iOS
  3. Try to get past purchases

Expected results:

I should my past purchases

Actual results:

I don't get any purchases on iOS (Android works fine)

More info:

I had already this plugin working perfectly on both platforms but I updated it to latest version facing all the breaking changes (so all products are setup ok and already have user on both platforms that bought my app). The documentation is not helping much because I read different steps/code:

if (defaultTargetPlatform == TargetPlatform.android) {
    InAppPurchaseAndroidPlatformAddition.enablePendingPurchases();
  }
  runApp(MyApp());

That's what I read here: https://pub.dev/packages/in_app_purchase but if I go to https://codelabs.developers.google.com/codelabs/flutter-in-app-purchases#5 (also linked from previous page) we don't have the check for Android:

InAppPurchaseAndroidPlatformAddition.enablePendingPurchases();
  runApp(MyApp());

This is my code already working for Android:

final Stream<List<PurchaseDetails>> purchaseUpdated =
          inAppPurchase.purchaseStream;

      _subscription = purchaseUpdated.listen((purchaseDetailsList) {
        if (purchaseDetailsList.isEmpty) {
          Provider.of<AdState>(context, listen: false).toggleAds(context, true);
        } else {
          Provider.of<AdState>(context, listen: false)
              .toggleAds(context, false);
          this.purchases.addAll(purchaseDetailsList);
          listenToPurchaseUpdated(purchaseDetailsList);
        }
      }, onDone: () {
        _subscription.cancel();
      }, onError: (error) {
        // handle error here.
      });

      inAppPurchase.restorePurchases();

I can add _inAppPurchase.completePurchase(purchase); or:

Map<String, PurchaseDetails> purchases =
            Map.fromEntries(this.purchases.map((PurchaseDetails purchase) {
          if (purchase.pendingCompletePurchase) {
            inAppPurchase.completePurchase(purchase);
          }
          return MapEntry<String, PurchaseDetails>(
              purchase.productID, purchase);
        }));

but that doesn't solve anything because this.purchases still empty since we are not going inside the first listen and the list of purchases still empty.

dani@DanielRodriguezs-MacBook-Pro my_rents % flutter analyze
The plugins `flutter_statusbarcolor_ns, libphonenumber` use a deprecated version of the Android embedding.
To avoid unexpected runtime failures, or future build failures, try to see if these plugins support the Android V2 embedding. Otherwise, consider removing them since a future release of Flutter will remove
these deprecated APIs.
If you are plugin author, take a look at the docs for migrating the plugin to the V2 embedding: https://flutter.dev/go/android-plugin-migration.
Analyzing my_rents...                                                   

   info • The value of the local variable 'daysFromTodayToEndDate' isn't used • lib/helpers/helpers.dart:429:9 • unused_local_variable
   info • The value of the local variable 'daysFromTodayToStartDate' isn't used • lib/helpers/helpers.dart:430:9 • unused_local_variable
   info • The value of the local variable 'locale' isn't used • lib/helpers/helpers.dart:532:12 • unused_local_variable
   info • Unused import: 'dart:io' • lib/main.dart:4:8 • unused_import
   info • 'buttonColor' is deprecated and shouldn't be used. No longer used by the framework, please remove any reference to it. This feature was deprecated after v2.3.0-0.2.pre. • lib/main.dart:122:11 •
          deprecated_member_use
   info • The operand can't be null, so the condition is always true • lib/models/invoice.dart:22:29 • unnecessary_null_comparison
   info • The value of the field '_numInterstitialLoadAttempts' isn't used • lib/providers/ad_state.dart:36:7 • unused_field
   info • The value of the field '_loadingBannerAd' isn't used • lib/providers/ad_state.dart:170:8 • unused_field
   info • 'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre. • lib/screens/Invoices/invoice_details.dart:33:36 •
          deprecated_member_use
   info • 'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre. • lib/screens/Invoices/my_invoice.dart:457:43 •
          deprecated_member_use
   info • The operand can't be null, so the condition is always true • lib/screens/Invoices/taxes_withholdings.dart:133:46 • unnecessary_null_comparison
   info • The operand can't be null, so the condition is always true • lib/screens/Invoices/taxes_withholdings.dart:185:44 • unnecessary_null_comparison
   info • The operand can't be null, so the condition is always true • lib/screens/Invoices/taxes_withholdings.dart:208:22 • unnecessary_null_comparison
   info • This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final: TaxesWithholdingsGroupsScreen._mainColor •
          lib/screens/Invoices/taxes_withholdings_group.dart:14:7 • must_be_immutable
   info • The value of the field '_events' isn't used • lib/screens/calendar.dart:17:37 • unused_field
   info • The declaration '_fileDownloadProgress' isn't referenced • lib/screens/expense.dart:646:10 • unused_element
   info • 'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre. • lib/screens/lease.dart:356:39 • deprecated_member_use
   info • The operand can't be null, so the condition is always true • lib/screens/new_event.dart:227:32 • unnecessary_null_comparison
   info • The value of the field '_invoiceFile' isn't used • lib/screens/new_expense.dart:72:9 • unused_field
   info • The value of the field '_invoiceUrl' isn't used • lib/screens/new_expense.dart:73:11 • unused_field
   info • 'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre. • lib/screens/new_expense.dart:736:41 •
          deprecated_member_use
   info • The declaration '_total' isn't referenced • lib/screens/new_expense.dart:755:10 • unused_element
   info • The declaration '_buildCurrencyDialogItem' isn't referenced • lib/screens/options.dart:247:10 • unused_element
   info • The declaration '_generateBorderRadius' isn't referenced • lib/screens/portfolio_info.dart:778:3 • unused_element
   info • The declaration '_statOptions' isn't referenced • lib/screens/portfolio_info.dart:787:3 • unused_element
   info • The value of the field '_selectedEvents' isn't used • lib/screens/rent.dart:65:22 • unused_field
   info • The value of the field '_isLoading' isn't used • lib/screens/rent.dart:73:13 • unused_field
   info • The declaration '_pickImageOptions' isn't referenced • lib/screens/rent.dart:691:3 • unused_element
   info • 'getImage' is deprecated and shouldn't be used. Switch to using pickImage instead • lib/screens/rent.dart:761:42 • deprecated_member_use
   info • The declaration '_showPaymentConfirmation' isn't referenced • lib/screens/rent.dart:1110:10 • unused_element
   info • The declaration '_calendarActions' isn't referenced • lib/screens/rent.dart:1246:10 • unused_element
   info • 'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre. • lib/screens/rent.dart:1269:39 • deprecated_member_use
   info • The value of the local variable 'createPaymentDayEventCopy' isn't used • lib/screens/rent.dart:1290:21 • unused_local_variable
   info • The declaration '_fileDownloadProgress' isn't referenced • lib/screens/rent.dart:2125:10 • unused_element
   info • 'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre. • lib/screens/rent.dart:2335:45 • deprecated_member_use
   info • The declaration '_stopLease' isn't referenced • lib/screens/rent.dart:2592:10 • unused_element
   info • The declaration '_deleteDocument' isn't referenced • lib/screens/rent.dart:2753:3 • unused_element
   info • The operand can't be null, so the condition is always true • lib/screens/rent.dart:2856:11 • unnecessary_null_comparison
   info • 'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre. • lib/screens/rent.dart:2863:16 • deprecated_member_use
   info • Equality operator `==` invocation with references of unrelated types • lib/screens/rent.dart:2934:25 • unrelated_type_equality_checks
   info • The declaration '_buildActionForTask' isn't referenced • lib/screens/rent.dart:2957:10 • unused_element
   info • The value of the field '_events' isn't used • lib/screens/table_events.dart:57:37 • unused_field
   info • The value of the local variable 'snackBar' isn't used • lib/screens/table_events.dart:380:31 • unused_local_variable
   info • Cancel instances of dart.async.StreamSubscription • lib/screens/versions.dart:25:23 • cancel_subscriptions
   info • 'FlatButton' is deprecated and shouldn't be used. Use TextButton instead. See the migration guide in flutter.dev/go/material-button-migration-guide). This feature was deprecated after
          v1.26.0-18.0.pre. • lib/screens/versions.dart:341:12 • deprecated_member_use
   info • The value of the local variable 'user' isn't used • lib/services/auth.dart:39:18 • unused_local_variable
   info • The value of the local variable 'user' isn't used • lib/services/auth.dart:64:18 • unused_local_variable
   info • The operand can't be null, so the condition is always true • lib/services/create_invoice_pdf.dart:160:18 • unnecessary_null_comparison
   info • The operand can't be null, so the condition is always true • lib/services/create_invoice_pdf.dart:161:18 • unnecessary_null_comparison
   info • The operand can't be null, so the condition is always true • lib/services/create_invoice_pdf.dart:162:18 • unnecessary_null_comparison
   info • The declaration '_formatDate' isn't referenced • lib/services/create_invoice_pdf.dart:659:8 • unused_element
   info • Avoid empty statements • lib/services/firebase_profile.dart:337:9 • empty_statements
   info • 'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre. • lib/services/phone_contact.dart:49:28 •
          deprecated_member_use
   info • The declaration '_showError' isn't referenced • lib/services/phone_contact.dart:53:3 • unused_element
   info • 'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre. • lib/services/phone_contact.dart:57:26 •
          deprecated_member_use
   info • 'getImage' is deprecated and shouldn't be used. Switch to using pickImage instead • lib/services/select_documents.dart:101:42 • deprecated_member_use
   info • The value of the local variable '_timer' isn't used • lib/widgets/important_message.dart:9:11 • unused_local_variable
   info • 'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre. • lib/widgets/important_message.dart:21:25 •
          deprecated_member_use
   info • This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final: MySwitch.param, MySwitch.callback • lib/widgets/my_switch.dart:4:7
          • must_be_immutable
   info • The operand can't be null, so the condition is always true • lib/widgets/phone_number_selector.dart:42:49 • unnecessary_null_comparison
   info • The value of the local variable 'deletedPortfolio' isn't used • lib/widgets/portfolio_options.dart:40:23 • unused_local_variable
   info • This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final: Rents.portfolio • lib/widgets/rents.dart:21:7 • must_be_immutable
   info • Name types using UpperCamelCase • lib/widgets/taxes_groups_dropdown.dart:134:9 • camel_case_types

63 issues found. (ran in 24.5s)

flutter doctor -v
[✓] Flutter (Channel stable, 2.5.0, on macOS 11.5.2 20G95 darwin-x64, locale en-GB)
    • Flutter version 2.5.0 at /Users/dani/development/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 4cc385b4b8 (5 days ago), 2021-09-07 23:01:49 -0700
    • Engine revision f0826da7ef
    • Dart version 2.14.0

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    • Android SDK at /Users/dani/Library/Android/sdk
    • Platform android-30, build-tools 30.0.3
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.5.1, Build version 12E507
    • CocoaPods version 1.10.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2020.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)

[✓] VS Code (version 1.60.0)
    • VS Code at /Users/dani/Desktop/Visual Studio Code.app/Contents
    • Flutter extension version 3.26.0

[✓] Connected device (2 available)
    • iPhone 11 Pro Max (mobile) • 71D058B9-0AC2-4EFB-8F49-F9ADB8B78F13 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-14-5 (simulator)
    • Chrome (web)               • chrome                               • web-javascript • Google Chrome 93.0.4577.63

• No issues found!
@erperejildo erperejildo changed the title Can't restore iOS last purchases [iOS] [in_app_purchase] Can't restore iOS last purchases Sep 12, 2021
@darshankawar darshankawar added the in triage Presently being triaged by the triage team label Sep 13, 2021
@darshankawar
Copy link
Member

@erperejildo
I think this issue may be causing the issue you are facing.
Can you check and confirm ?

@darshankawar darshankawar added the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Sep 13, 2021
@erperejildo
Copy link
Author

@erperejildo
I think this issue may be causing the issue you are facing.
Can you check and confirm ?

in my case I don't receive any kind of response. I'm not talking about after a new purchase, just to restore them

@github-actions github-actions bot removed the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Sep 13, 2021
@darshankawar
Copy link
Member

@erperejildo
Thanks for the response. I tried a sample IAP app with test products. Tried to Restore Purchases with or without making a purchase, but don't see any response upon tapping on it.

Screenshot 2021-09-14 at 1 50 54 PM

Is this a correct interpretation of the issue you are facing ?

@darshankawar darshankawar added the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Sep 14, 2021
@erperejildo
Copy link
Author

@erperejildo
Thanks for the response. I tried a sample IAP app with test products. Tried to Restore Purchases with or without making a purchase, but don't see any response upon tapping on it.

Screenshot 2021-09-14 at 1 50 54 PM

Is this a correct interpretation of the issue you are facing ?

exactly

@github-actions github-actions bot removed the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Sep 14, 2021
@darshankawar
Copy link
Member

Thanks for confirming.

@darshankawar darshankawar added p: first party p: in_app_purchase Plugin for in-app purchase platform-ios iOS applications specifically found in release: 2.5 Found to occur in 2.5 has reproducible steps The issue has been confirmed reproducible and is ready to work on and removed in triage Presently being triaged by the triage team labels Sep 14, 2021
@erperejildo
Copy link
Author

any estimation for this fix @darshankawar?

@stuartmorgan
Copy link
Contributor

/cc @mvanbeusekom

@stuartmorgan stuartmorgan added the P2 Important issues not at the top of the work list label Sep 16, 2021
@mvanbeusekom
Copy link

Hi @erperejildo,

Regarding the documentation, this is indeed not very consistent and I will submit a fix shortly (thank you for pointing that out). The correct way to handle it is to include the platform check:

if (defaultTargetPlatform == TargetPlatform.android) {
    InAppPurchaseAndroidPlatformAddition.enablePendingPurchases();
}
runApp(MyApp());

Regarding the issue with restoring the In App Purchases, according to Apple's documentation restoring transactions has no effect when:

  • All transactions are unfinished.
  • The user did not purchase anything that is restorable.
  • You tried to restore items that are not restorable, such as a non-renewing subscription or a consumable product.
  • Your app's build version does not meet the guidelines for the CFBundleVersion key.

Could you make sure your configuration complies to these points? In the meantime I will also look into the issue and see if everything is configured correctly on our end.

@erperejildo
Copy link
Author

erperejildo commented Sep 17, 2021

Hi @erperejildo,

Regarding the documentation, this is indeed not very consistent and I will submit a fix shortly (thank you for pointing that out). The correct way to handle it is to include the platform check:

if (defaultTargetPlatform == TargetPlatform.android) {
    InAppPurchaseAndroidPlatformAddition.enablePendingPurchases();
}
runApp(MyApp());

Regarding the issue with restoring the In App Purchases, according to Apple's documentation restoring transactions has no effect when:

  • All transactions are unfinished.
  • The user did not purchase anything that is restorable.
  • You tried to restore items that are not restorable, such as a non-renewing subscription or a consumable product.
  • Your app's build version does not meet the guidelines for the CFBundleVersion key.

Could you make sure your configuration complies to these points? In the meantime I will also look into the issue and see if everything is configured correctly on our end.

Thanks for replying.

To give you more context my app has 2 products: one is payed version and the other is a monthly subscription. My user has bought the first one, for which I used a generated iOS promo code.

  • All transactions are unfinished.
    It was finished because I activated it from iOS

  • The user did not purchase anything that is restorable.

  • You tried to restore items that are not restorable, such as a non-renewing subscription or a consumable product.
    It's not a consumable, just a payed version

  • Your app's build version does not meet the guidelines for the CFBundleVersion key.
    That was working with previous implementation of this plugin and I don't see changes on that

@erperejildo
Copy link
Author

Hi @mvanbeusekom, any update on this?

@mvanbeusekom
Copy link

Hi @stuartmorgan, I am working on this but having some issues with my Apple Sandbox account making it a bit difficult to make purchases. So bear with me, I will make sure to provide another update at the end of the week.

@mvanbeusekom
Copy link

@erperejildo, unfortunately I didn't get much further as of today. I will pick this back up on Monday, sorry for the delay but we are working on it.

@erperejildo
Copy link
Author

@erperejildo, unfortunately I didn't get much further as of today. I will pick this back up on Monday, sorry for the delay but we are working on it.

It's ok. Keep me updated and good weekend

@erperejildo
Copy link
Author

erperejildo commented Sep 29, 2021

Any update @mvanbeusekom ?

Is there any kind of temporary fix/hack I could do in the meantime? At least to not block the rest of the fixes and features I need to deploy to iOS

@mvanbeusekom
Copy link

@erperejildo, I am currently testing with the example App (with some help from my colleague @BeMacized). We discovered that the example App contains a mistake and doesn't test on the PurchaseStatus.restored status when restoring purchases which is why transactions aren't processed in the in_app_purchase Example App after clicking the Restore purchases button.

After adding this status (see PR flutter/plugins#4392) the example App works correctly and restores purchases after hitting the button.

Here is the test scenario we used ((note that to run the example App yourself you also need to configure App Store Connect correctly, documentation can be found here) :

  1. Run the in_app_purchase_ios/example App on a real device;
  2. Purchase the "upgrade" or "subscription_silver" product using a sandbox account;
  3. Remove the in_app_purchase_ios/example App from the device and wait a few minutes;
  4. Re-innstall and run the in_app_purchase_ios/example App on the device;
  5. Hit the Restore purchases button and check if products are returned in the _listenToPurchaseUpdated() method (during our debug sessions we put a breakpoint on line 396 of the in_app_purchase_ios/example/lib/main.dart file and inspect the contents of the purchaseDetailsList variable).

During our debug sessions all previously purchased products are correctly returned (in our case 52 items). So that seems to work according to expectations.

@erperejildo
Copy link
Author

erperejildo commented Sep 29, 2021

Cool, thanks. Let me have a look and I’ll get back to you ASAP.

@erperejildo
Copy link
Author

erperejildo commented Sep 30, 2021

@mvanbeusekom I tried the example and works fine. I put the restorePurchases after the listener so I can restore it right away.
I followed same approach on my app:

final Stream<List<PurchaseDetails>> purchaseUpdated =
          inAppPurchase.purchaseStream;

      _subscription = purchaseUpdated.listen((purchaseDetailsList) {
        if (purchaseDetailsList.isEmpty) {
          Provider.of<AdState>(context, listen: false).toggleAds(context, true);
        } else {
          Provider.of<AdState>(context, listen: false)
              .toggleAds(context, false);
          this.purchases.addAll(purchaseDetailsList);
          listenToPurchaseUpdated(purchaseDetailsList);
        }
      }, onDone: () {
        _subscription.cancel();
      }, onError: (error) {
        // handle error here.
      });

      inAppPurchase.restorePurchases();

so that's exactly what we have for the example, where I bought something that got restored after building again. The different on my app is that the listener is not being triggered. Here I didn't buy anything else because it was bought already.

Does this change only work for products that we bought after the upgrade maybe?

Also I get an empty list of products:
Screenshot 2021-09-30 at 12 50 56

I think the problem to that could be related to the check above if I'm not mistaken: productDetailResponse.error != null. It goes in with this info:
Screenshot 2021-09-30 at 12 50 22
I do have an error (not sure why, maybe is this what does work on simulator maybe?) and we try to do _products = productDetailResponse.productDetails;

@mvanbeusekom
Copy link

mvanbeusekom commented Sep 30, 2021

First a small clarification that is not directly related to the problem but I think is good to know: the reason the example App shows a button to execute the "Restore" functionality is because this is the recommended practise from Apple (see the "important" section of the article Restoring Purchased Products).

The error code storekit_getproductrequest_platform_error to my knowledge indicates your are trying to access the StoreKit API from an invalid device (in most cases this error occurs when using a simulator). Accessing the StoreKit API from the Apple Simulator is only supported from iOS 14+ and only when configured correctly (see here for more information). To be sure everything is working correctly it is however recommended to debug on a real Apple devices (iPad or iPhone).

Can you confirm you are running your debug tests on the simulator?

The indication that you do have an error is most certainly why the productDetailResponse.productDetails doesn't contain any records.

@erperejildo
Copy link
Author

Interesting article from Apple, but that makes the flow a bit different. For example, in my app, if you are a paid user you won’t see any ads, but that has to be checked as soon as the app is loading at beginning, without you clicking any button. Anyway, this is more an iOS thing that I’ll have to check. Thanks

It should be related to the emulator then. I thought it was fine because previously, I used to check everything on the simulator but yes, I’ll double check on real device again

@erperejildo
Copy link
Author

erperejildo commented Oct 3, 2021

@mvanbeusekom I tried on real device and I got similar result.

_subscription = purchaseUpdated.listen((purchaseDetailsList) {
        if (purchaseDetailsList.isNotEmpty) {
          listenToPurchaseUpdated(purchaseDetailsList);
        }
      }

That never goes in.

I created a button to restore the purchases just to double check what you mentioned wasn't affecting anything:

restorePurchasesIOS() {
    inAppPurchase.restorePurchases();
}

and again, this doesn't trigger the listener.

I'll try to create another repo following the example but replacing my info to see if the problem is coming from not buying new products or because I'm doing something wrong

@erperejildo
Copy link
Author

erperejildo commented Oct 3, 2021

Ok, I created as mentioned a new repo after cloning the example. The only change I made (apart from login with my team credentials, adding a version and use the real plugin package 1.0.9 to make it work) was adding my IDs:

const List<String> _kProductIds = <String>[
  'basic',
  'PRO',
];

These IDs can be checked also here:
Screenshot 2021-10-03 at 16 07 05

Something else I checked was going to my profile subscriptions on my device and check that I had a version bought there.

So after clicking Restore purchases button I don't get anything:
Screenshot 2021-10-03 at 16 05 47

The IDs were not found.

Also, I still not understanding why we try to do _products = productDetailResponse.productDetails; when productDetails is Empty.

The UI says that I need special configuration to run when everything is fine:
Screenshot 2021-10-03 at 16 14 49

What I do get (and never saw this in my app) is this message every time when I try to restore the purchases:

there is no information available for in-app purchases 21102

Someone mentioned it should be ok on production (https://stackoverflow.com/a/63611198/4858133) but I can't push a new version to test it like that.

@mvanbeusekom
Copy link

@erperejildo thank you for the additional information. Can I ask you to test with a separate or new sandbox tester account (not sure if you are able to create one, as you need to use an email address that has never been associated with an Apple ID before)?

I will do the same on my side and setup a new App (based on the in_app_purchase example), define the same products you did and do some additional testing. This will probably be tomorrow but will let you know the results.

@mvanbeusekom
Copy link

Hi @erperejildo,

Looking more detailed at your screenshot, I have the feeling you are looking in the wrong place. The code block shown (included below as reference) is part of the initialisation code of the example App and indeed doesn't include any products on start up.

    if (productDetailResponse.productDetails.isEmpty) {
      setState(() {
        _queryProductError = null;
        _isAvailable = isAvailable;
        _products = productDetailResponse.productDetails;
        _purchases = [];
        _notFoundIds = productDetailResponse.notFoundIDs;
        _consumables = [];
        _purchasePending = false;
        _loading = false;
      });
      return;
    }

The actual implementation of the purchaseStream listen method can be found on line 425 of the official example App. This is where the _listenToPurchaseUpdated() method is defined.

If have setup a new application in Apple App Store Connect and configured it with the two products you showed in your screenshot:

  1. Non consumable: Plus (reference name: basic)
  2. Auto-renewable Subscription: Pro (reference name: PRO)

Next I created a new Flutter App and copied the example code into the main.dart file. I removed all references to the ConsumableStore (as we don't have consumable products) and updated the product list. This resulted in the code listed below (see main.dart and pubspec.yaml sections). Next I ran the code with a breakpoint on line 344 (this is the first line in the _listenToPurchaseUpdated() method) and took the following steps:

  1. Purchased the "Plus" product;
  2. The breakpoint is hit and the product has a PurchaseStatus.pending status;
  3. Continue the program and finalise the purchase (login with sandbox account);
  4. After a small delay (0.5 to 1 second) the breakpoint is hit again and the product now has the PurchaseStatus.purchased status;
  5. Next I click the "Restore purchases" button;
  6. Almost immediately the breakpoint is hit again and the product now has the PurchaseStatus.restored status (see screenshot below).

Screenshot 2021-10-05 at 11 08 03

After this I closed and removed the App and did a reboot of my device (this last one just to make sure all is starting fresh, not sure it has any real impact). When the device restarted I ran the example App again and immediately hit the "Restore purchases" button. Again the breakpoint on line 344 was hit and the product was part of the list with the PurchaseStatus.restored status. In other words, in my tests restoring products seem to work fine. Please make sure though that you are looking at the correct code as the screenshot suggests you are not.

main.dart
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_ios/in_app_purchase_ios.dart';
import 'package:in_app_purchase_ios/store_kit_wrappers.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  if (defaultTargetPlatform == TargetPlatform.android) {
    // For play billing library 2.0 on Android, it is mandatory to call
    // [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases)
    // as part of initializing the app.
    InAppPurchaseAndroidPlatformAddition.enablePendingPurchases();
  }

  runApp(_MyApp());
}

const String _kNonConsumableId = 'basic';
const String _kSubscriptionId = 'PRO';
const List<String> _kProductIds = <String>[
  _kNonConsumableId,
  _kSubscriptionId,
];

class _MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<_MyApp> {
  final InAppPurchase _inAppPurchase = InAppPurchase.instance;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<String> _notFoundIds = [];
  List<ProductDetails> _products = [];
  List<PurchaseDetails> _purchases = [];
  List<String> _consumables = [];
  bool _isAvailable = false;
  bool _purchasePending = false;
  bool _loading = true;
  String? _queryProductError;

  @override
  void initState() {
    final Stream<List<PurchaseDetails>> purchaseUpdated =
        _inAppPurchase.purchaseStream;
    _subscription = purchaseUpdated.listen((purchaseDetailsList) {
      _listenToPurchaseUpdated(purchaseDetailsList);
    }, onDone: () {
      _subscription.cancel();
    }, onError: (error) {
      // handle error here.
    });
    initStoreInfo();
    super.initState();
  }

  Future<void> initStoreInfo() async {
    final bool isAvailable = await _inAppPurchase.isAvailable();
    if (!isAvailable) {
      setState(() {
        _isAvailable = isAvailable;
        _products = [];
        _purchases = [];
        _notFoundIds = [];
        _consumables = [];
        _purchasePending = false;
        _loading = false;
      });
      return;
    }

    if (Platform.isIOS) {
      var iosPlatformAddition = _inAppPurchase
          .getPlatformAddition<InAppPurchaseIosPlatformAddition>();
      await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
    }

    ProductDetailsResponse productDetailResponse =
        await _inAppPurchase.queryProductDetails(_kProductIds.toSet());
    if (productDetailResponse.error != null) {
      setState(() {
        _queryProductError = productDetailResponse.error!.message;
        _isAvailable = isAvailable;
        _products = productDetailResponse.productDetails;
        _purchases = [];
        _notFoundIds = productDetailResponse.notFoundIDs;
        _consumables = [];
        _purchasePending = false;
        _loading = false;
      });
      return;
    }

    if (productDetailResponse.productDetails.isEmpty) {
      setState(() {
        _queryProductError = null;
        _isAvailable = isAvailable;
        _products = productDetailResponse.productDetails;
        _purchases = [];
        _notFoundIds = productDetailResponse.notFoundIDs;
        _consumables = [];
        _purchasePending = false;
        _loading = false;
      });
      return;
    }

    setState(() {
      _isAvailable = isAvailable;
      _products = productDetailResponse.productDetails;
      _notFoundIds = productDetailResponse.notFoundIDs;
      _consumables = [];
      _purchasePending = false;
      _loading = false;
    });
  }

  @override
  void dispose() {
    if (Platform.isIOS) {
      var iosPlatformAddition = _inAppPurchase
          .getPlatformAddition<InAppPurchaseIosPlatformAddition>();
      iosPlatformAddition.setDelegate(null);
    }
    _subscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> stack = [];
    if (_queryProductError == null) {
      stack.add(
        ListView(
          children: [
            _buildConnectionCheckTile(),
            _buildProductList(),
            _buildRestoreButton(),
          ],
        ),
      );
    } else {
      stack.add(Center(
        child: Text(_queryProductError!),
      ));
    }
    if (_purchasePending) {
      stack.add(
        Stack(
          children: [
            Opacity(
              opacity: 0.3,
              child: const ModalBarrier(dismissible: false, color: Colors.grey),
            ),
            Center(
              child: CircularProgressIndicator(),
            ),
          ],
        ),
      );
    }

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('IAP Example'),
        ),
        body: Stack(
          children: stack,
        ),
      ),
    );
  }

  Card _buildConnectionCheckTile() {
    if (_loading) {
      return Card(child: ListTile(title: const Text('Trying to connect...')));
    }
    final Widget storeHeader = ListTile(
      leading: Icon(_isAvailable ? Icons.check : Icons.block,
          color: _isAvailable ? Colors.green : ThemeData.light().errorColor),
      title: Text(
          'The store is ' + (_isAvailable ? 'available' : 'unavailable') + '.'),
    );
    final List<Widget> children = <Widget>[storeHeader];

    if (!_isAvailable) {
      children.addAll([
        Divider(),
        ListTile(
          title: Text('Not connected',
              style: TextStyle(color: ThemeData.light().errorColor)),
          subtitle: const Text(
              'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'),
        ),
      ]);
    }
    return Card(child: Column(children: children));
  }

  Card _buildProductList() {
    if (_loading) {
      return Card(
          child: (ListTile(
              leading: CircularProgressIndicator(),
              title: Text('Fetching products...'))));
    }
    if (!_isAvailable) {
      return Card();
    }
    final ListTile productHeader = ListTile(title: Text('Products for Sale'));
    List<ListTile> productList = <ListTile>[];
    if (_notFoundIds.isNotEmpty) {
      productList.add(ListTile(
          title: Text('[${_notFoundIds.join(", ")}] not found',
              style: TextStyle(color: ThemeData.light().errorColor)),
          subtitle: Text(
              'This app needs special configuration to run. Please see example/README.md for instructions.')));
    }

    // This loading previous purchases code is just a demo. Please do not use this as it is.
    // In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it.
    // We recommend that you use your own server to verify the purchase data.
    Map<String, PurchaseDetails> purchases =
        Map.fromEntries(_purchases.map((PurchaseDetails purchase) {
      if (purchase.pendingCompletePurchase) {
        _inAppPurchase.completePurchase(purchase);
      }
      return MapEntry<String, PurchaseDetails>(purchase.productID, purchase);
    }));
    productList.addAll(_products.map(
      (ProductDetails productDetails) {
        PurchaseDetails? previousPurchase = purchases[productDetails.id];
        return ListTile(
            title: Text(
              productDetails.title,
            ),
            subtitle: Text(
              productDetails.description,
            ),
            trailing: previousPurchase != null
                ? IconButton(
                    onPressed: () => confirmPriceChange(context),
                    icon: Icon(Icons.upgrade))
                : TextButton(
                    child: Text(productDetails.price),
                    style: TextButton.styleFrom(
                      backgroundColor: Colors.green[800],
                      primary: Colors.white,
                    ),
                    onPressed: () {
                      late PurchaseParam purchaseParam;

                      if (Platform.isAndroid) {
                        purchaseParam = GooglePlayPurchaseParam(
                          productDetails: productDetails,
                          applicationUserName: null,
                          changeSubscriptionParam: null,
                        );
                      } else {
                        purchaseParam = PurchaseParam(
                          productDetails: productDetails,
                          applicationUserName: null,
                        );
                      }

                      _inAppPurchase.buyNonConsumable(
                          purchaseParam: purchaseParam);
                    },
                  ));
      },
    ));

    return Card(
        child:
            Column(children: <Widget>[productHeader, Divider()] + productList));
  }

  Widget _buildRestoreButton() {
    if (_loading) {
      return Container();
    }

    return Padding(
      padding: const EdgeInsets.all(4.0),
      child: Row(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          TextButton(
            child: Text('Restore purchases'),
            style: TextButton.styleFrom(
              backgroundColor: Theme.of(context).primaryColor,
              primary: Colors.white,
            ),
            onPressed: () => _inAppPurchase.restorePurchases(),
          ),
        ],
      ),
    );
  }

  void showPendingUI() {
    setState(() {
      _purchasePending = true;
    });
  }

  void deliverProduct(PurchaseDetails purchaseDetails) async {
    // IMPORTANT!! Always verify purchase details before delivering the product.
    setState(() {
      _purchases.add(purchaseDetails);
      _purchasePending = false;
    });
  }

  void handleError(IAPError error) {
    setState(() {
      _purchasePending = false;
    });
  }

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) {
    // IMPORTANT!! Always verify a purchase before delivering the product.
    // For the purpose of an example, we directly return true.
    return Future<bool>.value(true);
  }

  void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
    // handle invalid purchase here if  _verifyPurchase` failed.
  }

  void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
    purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
      if (purchaseDetails.status == PurchaseStatus.pending) {
        showPendingUI();
      } else {
        if (purchaseDetails.status == PurchaseStatus.error) {
          handleError(purchaseDetails.error!);
        } else if (purchaseDetails.status == PurchaseStatus.purchased ||
            purchaseDetails.status == PurchaseStatus.restored) {
          bool valid = await _verifyPurchase(purchaseDetails);
          if (valid) {
            deliverProduct(purchaseDetails);
          } else {
            _handleInvalidPurchase(purchaseDetails);
            return;
          }
        }
        if (purchaseDetails.pendingCompletePurchase) {
          await _inAppPurchase.completePurchase(purchaseDetails);
        }
      }
    });
  }

  Future<void> confirmPriceChange(BuildContext context) async {
    if (Platform.isAndroid) {
      final InAppPurchaseAndroidPlatformAddition androidAddition =
          _inAppPurchase
              .getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
      var priceChangeConfirmationResult =
          await androidAddition.launchPriceChangeConfirmationFlow(
        sku: 'purchaseId',
      );
      if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text('Price change accepted'),
        ));
      } else {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(
            priceChangeConfirmationResult.debugMessage ??
                "Price change failed with code ${priceChangeConfirmationResult.responseCode}",
          ),
        ));
      }
    }
    if (Platform.isIOS) {
      var iapIosPlatformAddition = _inAppPurchase
          .getPlatformAddition<InAppPurchaseIosPlatformAddition>();
      await iapIosPlatformAddition.showPriceConsentIfNeeded();
    }
  }
}

/// Example implementation of the
/// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc).
///
/// The payment queue delegate can be implementated to provide information
/// needed to complete transactions.
class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
  @override
  bool shouldContinueTransaction(
      SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) {
    return true;
  }

  @override
  bool shouldShowPriceConsent() {
    return false;
  }
}
pubspec.yaml
name: fl_issue_89950
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: ">=2.14.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  in_app_purchase: ^1.0.9
  
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^1.0.0

flutter:
  uses-material-design: true

@erperejildo
Copy link
Author

erperejildo commented Oct 5, 2021

@erperejildo thank you for the additional information. Can I ask you to test with a separate or new sandbox tester account (not sure if you are able to create one, as you need to use an email address that has never been associated with an Apple ID before)?

I will do the same on my side and setup a new App (based on the in_app_purchase example), define the same products you did and do some additional testing. This will probably be tomorrow but will let you know the results.

forgot to mention but I also tried that. Created a new test user, replaced the one I had on my sandbox account and logged in with that one in the app. Same result.

I copied those 2 files but after this and the account changes still getting the same error:

there is no information available for in-app purchases 21102

Also what I tried:

  • Different device
  • Same account but login from app, not from settings (when you press the Restore button the login appears)
  • Create a new account, restart the device, and log again

I'm connected by cable and building the app from Android Studio/Xcode.

This breakpoint is never triggered because previous listener is never called:
Screenshot 2021-10-05 at 20 34 26

I can't reproduce those steps you mentioned because on the first one you are buying the product and my products list still empty.

My understanding is that is more related to my account, but I'm not sure how/why. I posted a message on iOS forum and opened a issue here as well:
https://stackoverflow.com/questions/69455996/there-is-no-information-available-for-in-app-purchases-try-again-later-21102

@mvanbeusekom
Copy link

mvanbeusekom commented Oct 6, 2021

Now I understand what you mean when you mentioned "products list still empty". Sorry it took some time for me to click.

For the product list to get populated the most important thing to do is to make sure you update your "Bundle identifier" in Xcode. This should match with the bundle identifier of the App you registered in the App Store Connect portal. In this comment you show a screenshot of Xcode in which it shows you are using a Bundle ID of "dev.flutter.plugins.inAppPurchaseExampleMyRents". You must have also registered an App with the same Bundle identifier in the App Store Connect portal. If this is not the case make sure you other change the Bundle ID in Xcode so it matches a valid App in registered in App Store Connect or create a new App in App Store Connect.

Yesterday I started with a completely new application and took the following steps (I tried to write this down as detailed as possible so you might be able to retrace if you have an inconsistency):

Register the App identifier
First step was to register a new App identifier in Apple's Developer Member center:

  1. Login with your Apple developer account at https://developer.apple.com/membercenter;
  2. When you are member of multiple organisations make sure to select the correct one (upper right corner);
  3. Navigate to "Certificates, Identifiers & Profiles";
  4. Choose the "Identifiers" option in the menu on the left;
  5. Click the "+" button to add a new identifier;
  6. Select the "App IDs" option and click "Continue";
  7. Select "App" for type and click "Continue";
  8. Add a "Description" and "explicit Bundle ID" (in my case I used "Baseflow IAP Test App" and "com.baselfow.example.iaptest");
  9. Click the "Continue" button (no need to change any "Capabilities" or "Services").

The App ID should now be created. Remember the value used as "Bundle ID" as you will need to add it in your App.

Add a new App to App Store Connect
Next I added a new App to the App Store Connect portal.

  1. Login with your Apple developer account at https://appstoreconnect.apple.com;
  2. When you are member of multiple organisations make sure to select the correct one (upper right corner);
  3. Click the "My Apps" button;
  4. Click the "+" button and select the "New App" option;
  5. Select "iOS" as platform;
  6. Supply a name for the App (I used "Baseflow IAP Test App");
  7. Choose a primary language (I used "English (US)");
  8. Select the Bundle ID (this should be the one we just created);
  9. Supply a value for the "SKU" field (can be any string, I used the "Bundle ID" but is not important);
  10. Select "Full Access" for the "User Access" field;
  11. Hit the "Create" button.

The new App is now created and we can now define the products to the test App.

Add products to the App
Assuming you are still logged in into Apple's App Store Connect portal and are editing the details of the newly created App, do the following:

  1. Navigate to "Manage" under the "In-App Purchases" option in the left menu;
  2. Add the products as shown in the screenshot below (take special care on the values used as "Product ID" as these need to match the strings used in my example snippet from the previous comment);

Screenshot 2021-10-06 at 16 39 52

Once the products are entered we are ready to configure the Flutter App.

Setting up the Flutter App

  1. Create a new Flutter App (I used the command line: flutter create fl_issue_89950;
  2. Replace the contents of the main.dart with the contents of the main.dart section in my previous comment;
  3. Replace the contents of the pubspec.yaml with the contents of the pubspec.yaml in my previous comment;
  4. Open the project in Xcode (make sure you open the fl_issue_89950/ios folder);
  5. In Xcode select "Runnner" in the "Project Navigator";
  6. On the "General" tab update the "Bundle identifier" field with your newly create bundle identifier (see screenshot below)

Screenshot 2021-10-06 at 16 46 38

7. On the "Signing & Capabilities" tab make sure you update the "Team" field and select the correct team (for me this is "Baseflow" as shown in the screenshot below).

Screenshot 2021-10-06 at 16 48 44

Now you should be able to run the App and the products should show up in your App and you should also be able to buy them with your sandbox account.

Let me know if this helps.

@erperejildo
Copy link
Author

Sorry for the confusion then, I thought that was like Android. I tried with my app and now it's fine.
I don't know why so many people are getting this and no one can provide that solution. Thanks for that and again for the steps.

With this and 1.0.9 everything is working. Thank you!

@mvanbeusekom
Copy link

No problem at all. Very glad I could help and you were able to resolve the issue.

Thank you for letting us know.

@jmagman
Copy link
Member

jmagman commented Oct 6, 2021

@mvanbeusekom Now that you did all the work and wrote it down, are there clarifications to be made in the README to avoid confusion? https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/in_app_purchase/README.md

@mvanbeusekom
Copy link

@mvanbeusekom Now that you did all the work and wrote it down, are there clarifications to be made in the README to avoid confusion? https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/in_app_purchase/README.md

I have been looking into this a bit more detailed and the codelab (which is referred to in the README.md) explains the configuration of Apple's App Store (via App Store Connect) in quite some detail, the one thing that is missing there is the part where you need to register your Bundle ID via Apple's Developer portal (or member center). So this would be good to add and make sure we stress on the relation between the Bundle ID that you register and the one you need to configure in Xcode.

Do you think this would suffice or would you feel we need to also include more details into the README.md file?

@jmagman
Copy link
Member

jmagman commented Oct 8, 2021

Do you think this would suffice or would you feel we need to also include more details into the README.md file?

Sounds like plenty of detail to me.

@adriende
Copy link

@github-actions
Copy link

github-actions bot commented Nov 4, 2021

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 4, 2021
@flutter-triage-bot flutter-triage-bot bot added the package flutter/packages repository. See also p: labels. label Jul 5, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
found in release: 2.5 Found to occur in 2.5 has reproducible steps The issue has been confirmed reproducible and is ready to work on p: in_app_purchase Plugin for in-app purchase P2 Important issues not at the top of the work list package flutter/packages repository. See also p: labels. platform-ios iOS applications specifically r: solved Issue is closed as solved
Projects
None yet
Development

No branches or pull requests

6 participants