diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 535295a2f8af..84a65f4159f3 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.1 + +* [iOS] Introduce `SKPaymentQueueWrapper.presentCodeRedemptionSheet` + ## 0.5.0 * Migrate to Google Billing Library 3.0 diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h index 54898d170304..a27855230adb 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h @@ -29,6 +29,7 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); // Can throw exceptions if the transaction type is purchasing, should always used in a @try block. - (void)finishTransaction:(nonnull SKPaymentTransaction *)transaction; - (void)restoreTransactions:(nullable NSString *)applicationName; +- (void)presentCodeRedemptionSheet; - (NSArray *)getUnfinishedTransactions; // This method needs to be called before any other methods. diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index ecbd237c90ce..8d179aee7ba8 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -66,6 +66,14 @@ - (void)restoreTransactions:(nullable NSString *)applicationName { } } +- (void)presentCodeRedemptionSheet { + if (@available(iOS 14, *)) { + [self.queue presentCodeRedemptionSheet]; + } else { + NSLog(@"presentCodeRedemptionSheet is only available on iOS 14 or newer"); + } +} + #pragma mark - observing // Sent when the transaction array has changed (additions or state changes). Client should check diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 9b44ad766a98..e9b6bb9b8490 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -93,6 +93,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self finishTransaction:call result:result]; } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { [self restoreTransactions:call result:result]; + } else if ([@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + isEqualToString:call.method]) { + [self presentCodeRedemptionSheet:call result:result]; } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { [self retrieveReceiptData:call result:result]; } else if ([@"-[InAppPurchasePlugin refreshReceipt:result:]" isEqualToString:call.method]) { @@ -246,6 +249,11 @@ - (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)resu result(nil); } +- (void)presentCodeRedemptionSheet:(FlutterMethodCall *)call result:(FlutterResult)result { + [self.paymentQueueHandler presentCodeRedemptionSheet]; + result(nil); +} + - (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { FlutterError *error = nil; NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; diff --git a/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m b/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m index 4025e9270fa9..31e0f255034d 100644 --- a/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m @@ -250,6 +250,22 @@ - (void)testRefreshReceiptRequest { XCTAssertTrue(result); } +- (void)testPresentCodeRedemptionSheet { + XCTestExpectation* expectation = + [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + arguments:nil]; + __block BOOL callbackInvoked = NO; + [self.plugin handleMethodCall:call + result:^(id r) { + callbackInvoked = YES; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); +} + - (void)testGetPendingTransactions { XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; FlutterMethodCall* call = diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart index 79a4a61fb328..e437e7f99cfc 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart @@ -201,6 +201,11 @@ class AppStoreConnection implements InAppPurchaseConnection { ); return productDetailsResponse; } + + @override + Future presentCodeRedemptionSheet() { + return _skPaymentQueueWrapper.presentCodeRedemptionSheet(); + } } class _TransactionObserver implements SKTransactionObserverWrapper { diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart index c45512ed353f..83435d23d395 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart @@ -179,6 +179,12 @@ class GooglePlayConnection 'The method only works on iOS.'); } + @override + Future presentCodeRedemptionSheet() async { + throw UnsupportedError( + 'The method only works on iOS.'); + } + /// Resets the connection instance. /// /// The next call to [instance] will create a new instance. Should only be diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart index aac5eae93e55..751bab62803c 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart @@ -239,6 +239,12 @@ abstract class InAppPurchaseConnection { /// Throws an [UnsupportedError] on Android. Future refreshPurchaseVerificationData(); + /// (App Store only) present Code Redemption Sheet. + /// Available on devices running iOS 14 and iPadOS 14 and later. + /// + /// Throws an [UnsupportedError] on Android. + Future presentCodeRedemptionSheet(); + /// The [InAppPurchaseConnection] implemented for this platform. /// /// Throws an [UnsupportedError] when accessed on a platform other than diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index d56fbd00c6fe..f17166fbc969 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -137,6 +137,17 @@ class SKPaymentQueueWrapper { applicationUserName); } + /// Present Code Redemption Sheet + /// + /// Use this to allow Users to enter and redeem Codes + /// + /// This method triggers [`-[SKPayment + /// presentCodeRedemptionSheet]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3566726-presentcoderedemptionsheet?language=objc) + Future presentCodeRedemptionSheet() async { + await channel.invokeMethod( + '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]'); + } + // Triage a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) async { assert(_observer != null, diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml index 3d121fd51e5e..c7582f91d8c2 100644 --- a/packages/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/pubspec.yaml @@ -1,7 +1,7 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase -version: 0.5.0 +version: 0.5.1 dependencies: flutter: diff --git a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart index bfcab085e26a..a6f9b07a59b3 100644 --- a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart +++ b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart @@ -300,6 +300,13 @@ void main() { throwsUnsupportedError); }); }); + + group('present code redemption sheet', () { + test('null', () async { + expect( + await AppStoreConnection.instance.presentCodeRedemptionSheet(), null); + }); + }); } class FakeIOSPlatform { diff --git a/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart index 5a265b8de907..6630b4e1734a 100644 --- a/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart +++ b/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart @@ -238,6 +238,13 @@ void main() { }); }); + group('present code redemption sheet', () { + test('should throw on android', () { + expect(GooglePlayConnection.instance.presentCodeRedemptionSheet(), + throwsUnsupportedError); + }); + }); + group('make payment', () { final String launchMethodName = 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)'; diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index d41a1269d6c9..8df380bc223b 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -133,6 +133,15 @@ void main() { expect(fakeIOSPlatform.applicationNameHasTransactionRestored, 'aUserID'); }); }); + + group('Code Redemption Sheet', () { + test('presentCodeRedemptionSheet should not throw', () async { + expect(fakeIOSPlatform.presentCodeRedemption, false); + await SKPaymentQueueWrapper().presentCodeRedemptionSheet(); + expect(fakeIOSPlatform.presentCodeRedemption, true); + fakeIOSPlatform.presentCodeRedemption = false; + }); + }); } class FakeIOSPlatform { @@ -153,6 +162,9 @@ class FakeIOSPlatform { List> transactionsFinished = []; String applicationNameHasTransactionRestored = ''; + // present Code Redemption + bool presentCodeRedemption = false; + Future onMethodCall(MethodCall call) { switch (call.method) { // request makers @@ -193,6 +205,9 @@ class FakeIOSPlatform { case '-[InAppPurchasePlugin restoreTransactions:result:]': applicationNameHasTransactionRestored = call.arguments; return Future.sync(() {}); + case '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]': + presentCodeRedemption = true; + return Future.sync(() {}); } return Future.sync(() {}); }