Closed Muhammed-Irfan closed 1 week 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 ?
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.
Steps to reproduce
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! ```