diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md index a6897eaf1412..445fdb6aa491 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.1 + +* Adds ability to purchase more than one of a product. + ## 0.3.0+10 * Ignores deprecation warnings for upcoming styleFrom button API changes. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart index 629355d12d4c..f81b36699834 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart @@ -71,7 +71,8 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { Future buyNonConsumable({required PurchaseParam purchaseParam}) async { await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper( productIdentifier: purchaseParam.productDetails.id, - quantity: 1, + quantity: + purchaseParam is AppStorePurchaseParam ? purchaseParam.quantity : 1, applicationUsername: purchaseParam.applicationUserName, simulatesAskToBuyInSandbox: purchaseParam is AppStorePurchaseParam && purchaseParam.simulatesAskToBuyInSandbox, diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart index b2d8eea9d791..168ef5cea5f4 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart @@ -12,6 +12,7 @@ class AppStorePurchaseParam extends PurchaseParam { AppStorePurchaseParam({ required ProductDetails productDetails, String? applicationUserName, + this.quantity = 1, this.simulatesAskToBuyInSandbox = false, }) : super( productDetails: productDetails, @@ -28,4 +29,7 @@ class AppStorePurchaseParam extends PurchaseParam { /// /// See also [SKPaymentWrapper.simulatesAskToBuyInSandbox]. final bool simulatesAskToBuyInSandbox; + + /// Quantity of the product user requested to buy. + final int quantity; } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml index ada883ce8f57..dd65b259a283 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_storekit description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_storekit issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.0+10 +version: 0.3.1 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart index 7c543750c25e..e987a5ceac8c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart @@ -56,20 +56,24 @@ class FakeStoreKitPlatform { queueIsActive = false; } - SKPaymentTransactionWrapper createPendingTransaction(String id) { + SKPaymentTransactionWrapper createPendingTransaction(String id, + {int quantity = 1}) { return SKPaymentTransactionWrapper( - transactionIdentifier: '', - payment: SKPaymentWrapper(productIdentifier: id), - transactionState: SKPaymentTransactionStateWrapper.purchasing, - transactionTimeStamp: 123123.121, - error: null, - originalTransaction: null); + transactionIdentifier: '', + payment: SKPaymentWrapper(productIdentifier: id, quantity: quantity), + transactionState: SKPaymentTransactionStateWrapper.purchasing, + transactionTimeStamp: 123123.121, + error: null, + originalTransaction: null, + ); } SKPaymentTransactionWrapper createPurchasedTransaction( - String productId, String transactionId) { + String productId, String transactionId, + {int quantity = 1}) { return SKPaymentTransactionWrapper( - payment: SKPaymentWrapper(productIdentifier: productId), + payment: + SKPaymentWrapper(productIdentifier: productId, quantity: quantity), transactionState: SKPaymentTransactionStateWrapper.purchased, transactionTimeStamp: 123123.121, transactionIdentifier: transactionId, @@ -77,10 +81,12 @@ class FakeStoreKitPlatform { originalTransaction: null); } - SKPaymentTransactionWrapper createFailedTransaction(String productId) { + SKPaymentTransactionWrapper createFailedTransaction(String productId, + {int quantity = 1}) { return SKPaymentTransactionWrapper( transactionIdentifier: '', - payment: SKPaymentWrapper(productIdentifier: productId), + payment: + SKPaymentWrapper(productIdentifier: productId, quantity: quantity), transactionState: SKPaymentTransactionStateWrapper.failed, transactionTimeStamp: 123123.121, error: const SKError( @@ -91,10 +97,12 @@ class FakeStoreKitPlatform { } SKPaymentTransactionWrapper createCanceledTransaction( - String productId, int errorCode) { + String productId, int errorCode, + {int quantity = 1}) { return SKPaymentTransactionWrapper( transactionIdentifier: '', - payment: SKPaymentWrapper(productIdentifier: productId), + payment: + SKPaymentWrapper(productIdentifier: productId, quantity: quantity), transactionState: SKPaymentTransactionStateWrapper.failed, transactionTimeStamp: 123123.121, error: SKError( @@ -105,9 +113,11 @@ class FakeStoreKitPlatform { } SKPaymentTransactionWrapper createRestoredTransaction( - String productId, String transactionId) { + String productId, String transactionId, + {int quantity = 1}) { return SKPaymentTransactionWrapper( - payment: SKPaymentWrapper(productIdentifier: productId), + payment: + SKPaymentWrapper(productIdentifier: productId, quantity: quantity), transactionState: SKPaymentTransactionStateWrapper.restored, transactionTimeStamp: 123123.121, transactionIdentifier: transactionId, @@ -166,25 +176,29 @@ class FakeStoreKitPlatform { return Future.sync(() {}); case '-[InAppPurchasePlugin addPayment:result:]': final String id = call.arguments['productIdentifier'] as String; + final int quantity = call.arguments['quantity'] as int; final SKPaymentTransactionWrapper transaction = - createPendingTransaction(id); + createPendingTransaction(id, quantity: quantity); + transactions.add(transaction); InAppPurchaseStoreKitPlatform.observer.updatedTransactions( transactions: [transaction]); sleep(const Duration(milliseconds: 30)); if (testTransactionFail) { final SKPaymentTransactionWrapper transactionFailed = - createFailedTransaction(id); + createFailedTransaction(id, quantity: quantity); InAppPurchaseStoreKitPlatform.observer.updatedTransactions( transactions: [transactionFailed]); } else if (testTransactionCancel > 0) { final SKPaymentTransactionWrapper transactionCanceled = - createCanceledTransaction(id, testTransactionCancel); + createCanceledTransaction(id, testTransactionCancel, + quantity: quantity); InAppPurchaseStoreKitPlatform.observer.updatedTransactions( transactions: [transactionCanceled]); } else { final SKPaymentTransactionWrapper transactionFinished = createPurchasedTransaction( - id, transaction.transactionIdentifier ?? ''); + id, transaction.transactionIdentifier ?? '', + quantity: quantity); InAppPurchaseStoreKitPlatform.observer.updatedTransactions( transactions: [transactionFinished]); } @@ -192,7 +206,8 @@ class FakeStoreKitPlatform { case '-[InAppPurchasePlugin finishTransaction:result:]': finishedTransactions.add(createPurchasedTransaction( call.arguments['productIdentifier'] as String, - call.arguments['transactionIdentifier'] as String)); + call.arguments['transactionIdentifier'] as String, + quantity: transactions.first.payment.quantity)); break; case '-[SKPaymentQueue startObservingTransactionQueue]': queueIsActive = true; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart index e92d8487ed93..852599ac3670 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart @@ -427,6 +427,68 @@ void main() { final PurchaseStatus purchaseStatus = await completer.future; expect(purchaseStatus, PurchaseStatus.canceled); }); + + test( + 'buying non consumable, should be able to purchase multiple quantity of one product', + () async { + final List details = []; + final Completer> completer = + Completer>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + details.addAll(purchaseDetailsList); + for (final PurchaseDetails purchaseDetails in purchaseDetailsList) { + if (purchaseDetails.pendingCompletePurchase) { + iapStoreKitPlatform.completePurchase(purchaseDetails); + completer.complete(details); + subscription.cancel(); + } + } + }); + final AppStoreProductDetails productDetails = + AppStoreProductDetails.fromSKProduct(dummyProductWrapper); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: productDetails, + quantity: 5, + applicationUserName: 'appName'); + await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam); + await completer.future; + expect( + fakeStoreKitPlatform.finishedTransactions.first.payment.quantity, 5); + }); + + test( + 'buying consumable, should be able to purchase multiple quantity of one product', + () async { + final List details = []; + final Completer> completer = + Completer>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + details.addAll(purchaseDetailsList); + for (final PurchaseDetails purchaseDetails in purchaseDetailsList) { + if (purchaseDetails.pendingCompletePurchase) { + iapStoreKitPlatform.completePurchase(purchaseDetails); + completer.complete(details); + subscription.cancel(); + } + } + }); + final AppStoreProductDetails productDetails = + AppStoreProductDetails.fromSKProduct(dummyProductWrapper); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: productDetails, + quantity: 5, + applicationUserName: 'appName'); + await iapStoreKitPlatform.buyConsumable(purchaseParam: purchaseParam); + await completer.future; + expect( + fakeStoreKitPlatform.finishedTransactions.first.payment.quantity, 5); + }); }); group('complete purchase', () {