flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
161.86k stars 26.57k forks source link

Purshase Stream is not fired after a successful non consumable purchase in iOS. Instead the bottom sheet to purchase keeps popping up again. #145966

Closed Muhammed-Irfan closed 1 week ago

Muhammed-Irfan commented 1 month ago

Steps to reproduce

  1. Request a purchase by buying a non-consumable product.
  2. Enter necessary credentials if needed (e.g., email, password).
  3. Click on Sign In.

This is working fine in Android.

Expected results

The purchaseStream should fire after a successful purchase on iOS.

Actual results

The purchaseStream is not fired after a successful purchase on iOS and the bottom sheet to purchase keeps popping up again.

Code sample

Code sample ```dart import 'dart:async'; import 'dart:io'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:get/get.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_storekit/store_kit_wrappers.dart'; import 'package:wizad/app/common/constants/app_constants.dart'; import 'package:wizad/app/common/constants/endpoints.dart'; import 'package:wizad/app/common/services/auth_service.dart'; import 'package:wizad/app/common/utils/utils.dart'; import 'package:wizad/app/data/models/purchasable_product.dart'; import 'package:wizad/app/modules/edit_profile/controllers/profile_controller.dart'; import 'package:wizad/app/routes/app_pages.dart'; import '../../data/api/api_client.dart'; import 'analytics_service.dart'; import 'network_service.dart'; enum StoreState { loading, available, notAvailable } class InAppPurchaseService extends GetxService { static InAppPurchaseService purchaseService = Get.find(); static InAppPurchase inAppPurchase = InAppPurchase.instance; final ApiClient api = NetworkService.networkService.api; late StreamSubscription> _subscription; StoreState storeState = StoreState.loading; List products = []; @override void onInit() { super.onInit(); final purchaseUpdated = inAppPurchase.purchaseStream; _subscription = purchaseUpdated.listen(_onPurchaseUpdate, onDone: _updateStreamOnDone, onError: _updateStreamOnError); loadPurchases(); } Future loadPurchases() async { final available = await inAppPurchase.isAvailable(); if (!available) { storeState = StoreState.notAvailable; } else { const ids = { InAppSubscriptions.basic, InAppSubscriptions.premium, }; final response = await inAppPurchase.queryProductDetails(ids); for (var element in response.notFoundIDs) { FirebaseCrashlytics.instance.recordError('Subscription \"$element\" not found', null); } products = response.productDetails.map((e) => PurchasableProduct(e)).toList(); storeState = StoreState.available; } } Future buy(PurchasableProduct product) async { try { final _productDetail = product.productDetails; GooglePlayPurchaseDetails? _oldPurchaseDetail; if (Platform.isAndroid) { final ProfileController profileController = Get.find(); if ((profileController.customer.value.user?.subscription_plan ?? '').isNotEmpty) { _oldPurchaseDetail = await _retrievePreviousPurchase(); } } else if (Platform.isIOS) { final transactions = await SKPaymentQueueWrapper().transactions(); for (var transaction in transactions) { if (transaction.transactionState == SKPaymentTransactionStateWrapper.purchasing) { throw Exception("Previous transaction is still ongoing."); } else await SKPaymentQueueWrapper().finishTransaction(transaction); } } final _purchaseParam = _oldPurchaseDetail == null ? PurchaseParam(productDetails: _productDetail, applicationUserName: AuthService.auth.user.value.user_id) : GooglePlayPurchaseParam( productDetails: _productDetail, applicationUserName: AuthService.auth.user.value.user_id, changeSubscriptionParam: ChangeSubscriptionParam(oldPurchaseDetails: _oldPurchaseDetail)); await inAppPurchase.buyNonConsumable(purchaseParam: _purchaseParam); } catch (e, s) { Utils.closeDialog(); Utils.showSnackbar('Failed to purchase subscription. Please try again.'); FirebaseCrashlytics.instance.recordError(e, s); } } Future _onPurchaseUpdate(List purchaseDetailsList) async { for (var purchaseDetails in purchaseDetailsList) { await _handlePurchase(purchaseDetails); } } Future _handlePurchase(PurchaseDetails purchaseDetails) async { switch (purchaseDetails.status) { case PurchaseStatus.pending: Utils.loadingDialog(); break; case PurchaseStatus.purchased: // Send to server var validPurchase = await _verifyPurchase(purchaseDetails); Utils.closeDialog(); if (validPurchase) { // Apply changes locally final ProfileController profileController = Get.find(); profileController.customer.value.user?.subscription_plan = purchaseDetails.productID; profileController.customer.refresh(); Get.until((route) => Get.currentRoute == Routes.HOME); //Route to home after successful purchase AnalyticsService.analytics.logEvent('Buy_Subscription', parameters: {'product': purchaseDetails.productID}); Utils.showSnackbar('Successfully purchased the subscription.'); } else { Utils.showSnackbar('Something went wrong.'); } break; case PurchaseStatus.error: Utils.closeDialog(); Utils.showSnackbar('Failed to purchase subscription. Please try again.'); break; case PurchaseStatus.restored: Utils.closeDialog(); break; case PurchaseStatus.canceled: Utils.closeDialog(); Utils.showSnackbar('Purchase was cancelled. Please try again.'); break; } //It's important to call completePurchase after handling the purchase so the store knows the purchase is handled correctly. if (purchaseDetails.pendingCompletePurchase) { await inAppPurchase.completePurchase(purchaseDetails); } } Future _verifyPurchase(PurchaseDetails purchaseDetails) async { try { var body = { "source": Platform.isAndroid ? "play_store" : "app_store", "serverVerificationData": purchaseDetails.verificationData.serverVerificationData, // token "productId": purchaseDetails.productID, "sourceData": purchaseDetails.verificationData.source, "localVerificationData": purchaseDetails.verificationData.localVerificationData, "transactionDate": purchaseDetails.transactionDate, if (Platform.isAndroid) ...{ "signature": (purchaseDetails as GooglePlayPurchaseDetails).billingClientPurchase.signature, "products": {"items": purchaseDetails.billingClientPurchase.products} }, }; await api.post(Endpoints.verifyPurchase, data: body); return true; } catch (e) { return false; } } Future _retrievePreviousPurchase() async { try { var res = await api.get(Endpoints.subscriptionHistory); if (res == null || res.isEmpty) return null; else { var product = res["data"]["data"][0]; var _wrapper = PurchaseWrapper.fromJson({ "orderId": product["order_id"], "packageName": 'co.wizad.wizad', "purchaseTime": int.tryParse(product["transaction_date"]), "purchaseToken": product["server_verification_data"], "signature": product["signature"], "products": product["products"]["items"], "isAutoRenewing": true, "originalJson": product["local_verification_data"], "isAcknowledged": true, "purchaseState": 0, "obfuscatedAccountId": AuthService.auth.user.value.user_id, }); return GooglePlayPurchaseDetails( purchaseID: product["order_id"], productID: product["product_id"], verificationData: PurchaseVerificationData( localVerificationData: product["local_verification_data"], serverVerificationData: product["server_verification_data"], source: product["source"]), transactionDate: product["transaction_date"], status: PurchaseStatus.purchased, billingClientPurchase: _wrapper); } } catch (e, s) { FirebaseCrashlytics.instance.recordError('Failed to parse GooglePlayPurchaseDetails', s); } return null; } void _updateStreamOnDone() { _subscription.cancel(); } void _updateStreamOnError(Object error, StackTrace stack) { FirebaseCrashlytics.instance.recordError('In App Purchase Stream Error', stack); } @override void onClose() { _subscription.cancel(); super.onClose(); } } ```

Screenshots or Video

Screenshots / Video demonstration [Upload media here]

Logs

Logs ```console ```

Flutter Doctor output

Doctor output ```console [✓] Flutter (Channel stable, 3.16.9, on macOS 14.4.1 23E224 darwin-arm64, locale en-IN) • Flutter version 3.16.9 on channel stable at /Users/irfan/Development/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 41456452f2 (9 weeks ago), 2024-01-25 10:06:23 -0800 • Engine revision f40e976bed • Dart version 3.2.6 • DevTools version 2.28.5 [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0) • Android SDK at /Users/irfan/Library/Android/sdk • Platform android-34, build-tools 33.0.0 • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 15.2) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 15C500b • CocoaPods version 1.13.0 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2023.2) • 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 17.0.9+0-17.0.9b1087.7-11185874) [✓] VS Code (version 1.85.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension can be installed from: 🔨 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter [✓] Connected device (4 available) • ScanPay’s iPhone (mobile) • 00008030-0012696621B9402E • ios • iOS 17.3.1 21D61 • iPhone 15 Pro Max (mobile) • 66F15FAE-65B1-4D96-8876-D3C4A7A1C0D8 • ios • com.apple.CoreSimulator.SimRuntime.iOS-17-2 (simulator) • macOS (desktop) • macos • darwin-arm64 • macOS 14.4.1 23E224 darwin-arm64 • Chrome (web) • chrome • web-javascript • Google Chrome 123.0.6312.8 [✓] Network resources • All expected network resources are available. • No issues found! ```
darshankawar commented 4 weeks ago

@Muhammed-Irfan Can you provide us only minimal but complete runnable reproducible code sample without any third party package implementation, that still shows the reported behavior ?

github-actions[bot] commented 1 week ago

Without additional information, we are unfortunately not sure how to resolve this issue. We are therefore reluctantly going to close this bug for now. If you find this problem please file a new issue with the same description, what happens, logs and the output of 'flutter doctor -v'. All system setups can be slightly different so it's always better to open new issues and reference the related ones. Thanks for your contribution.