From e66fb9d505a7ae2f22dd7f28c5e2fa732c714687 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 08:35:10 +0200 Subject: [PATCH 01/13] Native iOS implementation show price consent --- .../in_app_purchase_ios/example/ios/Podfile | 4 +- .../ios/Runner.xcodeproj/project.pbxproj | 63 ++++---- .../RunnerTests/FIAPPaymentQueueDeleteTests.m | 113 +++++++++++++++ .../RunnerTests/InAppPurchasePluginTests.m | 25 ++++ .../example/ios/RunnerTests/Stubs.h | 4 + .../example/ios/RunnerTests/Stubs.m | 14 ++ .../example/ios/RunnerTests/TranslatorTests.m | 44 ++++-- .../ios/Classes/FIAObjectTranslator.h | 5 + .../ios/Classes/FIAObjectTranslator.m | 27 ++++ .../ios/Classes/FIAPPaymentQueueDelegate.h | 16 +++ .../ios/Classes/FIAPPaymentQueueDelegate.m | 71 +++++++++ .../ios/Classes/FIAPaymentQueueHandler.h | 11 ++ .../ios/Classes/FIAPaymentQueueHandler.m | 9 ++ .../ios/Classes/InAppPurchasePlugin.m | 135 ++++++++++-------- 14 files changed, 444 insertions(+), 97 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h create mode 100644 packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile index ae8750242a6e..5200b9fa5045 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile @@ -29,12 +29,12 @@ flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - + target 'RunnerTests' do inherit! :search_paths # Matches in_app_purchase test_spec dependency. - pod 'OCMock','3.5' + pod 'OCMock', '~> 3.6' end end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj index 590b07f0d385..bff90da1b610 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 688DE35121F2A5A100EA2684 /* TranslatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 688DE35021F2A5A100EA2684 /* TranslatorTests.m */; }; 6896B34621E9363700D37AEF /* ProductRequestHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */; }; 6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34B21EEB4B800D37AEF /* Stubs.m */; }; + 7E34217B7715B1918134647A /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -20,7 +21,7 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5279297219369C600FF69E6 /* StoreKit.framework */; }; A59001A721E69658004A3E5E /* InAppPurchasePluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */; }; - AB7252348F077C046D6617D3 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 630DD71BB3F145A22B1DE15D /* libPods-RunnerTests.a */; }; + F67646F82681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */; }; F78AF3142342BC89008449C7 /* PaymentQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3132342BC89008449C7 /* PaymentQueueTests.m */; }; /* End PBXBuildFile section */ @@ -48,14 +49,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 027D04BC80EACAAB3B5232B8 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 10B860DFD91A1DF639D7BE1D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 1630769A874F9381BC761FE1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 194D4829A79EF6C7426B39F7 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 2550EB3A5A3E749A54ADCA2D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 630DD71BB3F145A22B1DE15D /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 688DE35021F2A5A100EA2684 /* TranslatorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TranslatorTests.m; sourceTree = ""; }; 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ProductRequestHandlerTests.m; sourceTree = ""; }; 6896B34A21EEB4B800D37AEF /* Stubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Stubs.h; sourceTree = ""; }; @@ -71,11 +71,13 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9D681E092EB0D20D652F69FC /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; A5279297219369C600FF69E6 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; A59001A421E69658004A3E5E /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InAppPurchasePluginTests.m; sourceTree = ""; }; A59001A821E69658004A3E5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E4F9651425A612301059769C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIAPPaymentQueueDeleteTests.m; sourceTree = ""; }; F6E5D5F926131C4800C68BED /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = ""; }; F78AF3132342BC89008449C7 /* PaymentQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PaymentQueueTests.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -94,7 +96,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - AB7252348F077C046D6617D3 /* libPods-RunnerTests.a in Frameworks */, + 7E34217B7715B1918134647A /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -106,8 +108,8 @@ children = ( E4F9651425A612301059769C /* Pods-Runner.debug.xcconfig */, 2550EB3A5A3E749A54ADCA2D /* Pods-Runner.release.xcconfig */, - 194D4829A79EF6C7426B39F7 /* Pods-RunnerTests.debug.xcconfig */, - 027D04BC80EACAAB3B5232B8 /* Pods-RunnerTests.release.xcconfig */, + 9D681E092EB0D20D652F69FC /* Pods-RunnerTests.debug.xcconfig */, + 10B860DFD91A1DF639D7BE1D /* Pods-RunnerTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -187,6 +189,7 @@ 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */, F78AF3132342BC89008449C7 /* PaymentQueueTests.m */, 688DE35021F2A5A100EA2684 /* TranslatorTests.m */, + F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */, ); path = RunnerTests; sourceTree = ""; @@ -196,7 +199,7 @@ children = ( A5279297219369C600FF69E6 /* StoreKit.framework */, 1630769A874F9381BC761FE1 /* libPods-Runner.a */, - 630DD71BB3F145A22B1DE15D /* libPods-RunnerTests.a */, + 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -229,7 +232,7 @@ isa = PBXNativeTarget; buildConfigurationList = A59001AD21E69658004A3E5E /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 321E2F5767F55B0A360AA77E /* [CP] Check Pods Manifest.lock */, + 95C7A5986B77A8DF76F6DF3A /* [CP] Check Pods Manifest.lock */, A59001A021E69658004A3E5E /* Sources */, A59001A121E69658004A3E5E /* Frameworks */, A59001A221E69658004A3E5E /* Resources */, @@ -256,6 +259,7 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 7624MWN53C; SystemCapabilities = { com.apple.InAppPurchase = { enabled = 1; @@ -310,41 +314,41 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 321E2F5767F55B0A360AA77E /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Thin Binary"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 95C7A5986B77A8DF76F6DF3A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -400,6 +404,7 @@ buildActionMask = 2147483647; files = ( F78AF3142342BC89008449C7 /* PaymentQueueTests.m in Sources */, + F67646F82681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m in Sources */, 6896B34621E9363700D37AEF /* ProductRequestHandlerTests.m in Sources */, 688DE35121F2A5A100EA2684 /* TranslatorTests.m in Sources */, A59001A721E69658004A3E5E /* InAppPurchasePluginTests.m in Sources */, @@ -549,7 +554,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -561,7 +566,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.inAppPurchaseExample; + PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.inAppPurchaseExample; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; @@ -573,7 +578,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -585,7 +590,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.inAppPurchaseExample; + PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.inAppPurchaseExample; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; @@ -593,7 +598,7 @@ }; A59001AB21E69658004A3E5E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 194D4829A79EF6C7426B39F7 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 9D681E092EB0D20D652F69FC /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -616,7 +621,7 @@ }; A59001AC21E69658004A3E5E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 027D04BC80EACAAB3B5232B8 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 10B860DFD91A1DF639D7BE1D /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m new file mode 100644 index 000000000000..a869881256e5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m @@ -0,0 +1,113 @@ +// 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 +#import +#import "FIAPaymentQueueHandler.h" +#import "FIAObjectTranslator.h" +#import "Stubs.h" + +@import in_app_purchase_ios; + +@interface FIAPPaymentQueueDelegateTests : XCTestCase + +@property (strong, nonatomic) FlutterMethodChannel *channel; +@property (strong, nonatomic) SKPaymentTransaction *transaction; +@property (strong, nonatomic) SKStorefront *storefront; + +@end + +@implementation FIAPPaymentQueueDelegateTests + +- (void)setUp { + self.channel = OCMClassMock(FlutterMethodChannel.class); + + NSDictionary* transactionMap = @{ + @"transactionIdentifier" : [NSNull null], + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : [NSNull null], + }; + self.transaction = [[SKPaymentTransactionStub alloc] initWithMap:transactionMap]; + + NSDictionary* storefrontMap = @{ + @"countryCode" : @"USA", + @"identifier" : @"unique_identifier", + }; + self.storefront = [[SKStorefrontStub alloc] initWithMap:storefrontMap]; +} + +- (void)tearDown { + self.channel = nil; +} + +- (void)testShouldContinueTransaction { + if(@available(iOS 13.0, *)) { + FIAPPaymentQueueDelegate *delegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub( + [self.channel invokeMethod:@"shouldContinueTransaction" + arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront + andSKPaymentTransaction:self.transaction] + result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); + + BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) shouldContinueTransaction:self.transaction + inStorefront:self.storefront]; + + XCTAssertFalse(shouldContinue); + } +} + +- (void)testShouldContinueTransaction_should_default_to_yes { + if(@available(iOS 13.0, *)) { + FIAPPaymentQueueDelegate *delegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub( + [self.channel invokeMethod:@"shouldContinueTransaction" + arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront + andSKPaymentTransaction:self.transaction] + result:[OCMArg any]]); + + BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) shouldContinueTransaction:self.transaction + inStorefront:self.storefront]; + + XCTAssertTrue(shouldContinue); + } +} + +- (void)testShouldShowPriceConsentIfNeeded { + if(@available(iOS 13.4, *)) { + FIAPPaymentQueueDelegate *delegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub( + [self.channel invokeMethod:@"shouldShowPriceConsent" + arguments:nil + result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); + + BOOL shouldShow = [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; + + XCTAssertFalse(shouldShow); + } +} + +- (void)testShouldShowPriceConsentIfNeeded_should_default_to_yes { + if(@available(iOS 13.4, *)) { + FIAPPaymentQueueDelegate *delegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub( + [self.channel invokeMethod:@"shouldShowPriceConsent" + arguments:nil + result:[OCMArg any]]); + + BOOL shouldShow = [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; + + XCTAssertTrue(shouldShow); + } +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m index 241ea0d5cb0d..adc3f8f8434a 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -343,4 +343,29 @@ - (void)testStartAndStopObservingPaymentQueue { XCTAssertNil(queue.observer); } +- (void)testShowPriceConsentIfNeeded { + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" + arguments:nil]; + + + FIAPaymentQueueHandler* mockQueueHandler = OCMClassMock(FIAPaymentQueueHandler.class); + + self.plugin.paymentQueueHandler = mockQueueHandler; + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + if (@available(iOS 13.4, *)) { + OCMVerify(times(1), [mockQueueHandler showPriceConsentIfNeeded]); + } else { + + OCMVerify(never(), [mockQueueHandler showPriceConsentIfNeeded]); + } +#pragma clang diagnostic pop +} + @end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h index 687118febb29..7f72325ee9a8 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h @@ -60,4 +60,8 @@ API_AVAILABLE(ios(11.2), macos(10.13.2)) - (instancetype)initWithFailureError:(NSError *)error; @end +@interface SKStorefrontStub : SKStorefront +- (instancetype)initWithMap:(NSDictionary *)map; +@end + NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m index 8af326a48722..a57831c61da5 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m @@ -290,3 +290,17 @@ - (void)start { } @end + +@implementation SKStorefrontStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + // Set stub values + [self setValue:map[@"countryCode"] forKey:@"countryCode"]; + [self setValue:map[@"identifier"] forKey:@"identifier"]; + } + return self; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m index 385d29140e49..9949723c9150 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m @@ -17,6 +17,8 @@ @interface TranslatorTest : XCTestCase @property(strong, nonatomic) NSDictionary *transactionMap; @property(strong, nonatomic) NSDictionary *errorMap; @property(strong, nonatomic) NSDictionary *localeMap; +@property(strong, nonatomic) NSDictionary *storefrontMap; +@property(strong, nonatomic) NSDictionary *storefrontAndPaymentTransactionMap; @end @@ -31,7 +33,7 @@ - (void)setUp { @"subscriptionPeriod" : self.periodMap, @"paymentMode" : @1 }; - + self.productMap = [[NSMutableDictionary alloc] initWithDictionary:@{ @"price" : @"1", @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], @@ -43,13 +45,13 @@ - (void)setUp { self.productMap[@"subscriptionPeriod"] = self.periodMap; self.productMap[@"introductoryPrice"] = self.discountMap; } - + if (@available(iOS 12.0, *)) { self.productMap[@"subscriptionGroupIdentifier"] = @"com.group"; } - + self.productResponseMap = - @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]}; + @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]}; self.paymentMap = @{ @"productIdentifier" : @"123", @"requestData" : @"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", @@ -81,15 +83,24 @@ - (void)setUp { @"code" : @(123), @"domain" : @"test_domain", @"userInfo" : @{ - @"key" : @"value", + @"key" : @"value", } }; + self.storefrontMap = @{ + @"countryCode" : @"USA", + @"identifier" : @"unique_identifier", + }; + + self.storefrontAndPaymentTransactionMap = @{ + @"storefront" : self.storefrontMap, + @"transaction" : self.transactionMap, + }; } - (void)testSKProductSubscriptionPeriodStubToMap { if (@available(iOS 11.2, *)) { SKProductSubscriptionPeriodStub *period = - [[SKProductSubscriptionPeriodStub alloc] initWithMap:self.periodMap]; + [[SKProductSubscriptionPeriodStub alloc] initWithMap:self.periodMap]; NSDictionary *map = [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:period]; XCTAssertEqualObjects(map, self.periodMap); } @@ -111,7 +122,7 @@ - (void)testProductToMap { - (void)testProductResponseToMap { SKProductsResponseStub *response = - [[SKProductsResponseStub alloc] initWithMap:self.productResponseMap]; + [[SKProductsResponseStub alloc] initWithMap:self.productResponseMap]; NSDictionary *map = [FIAObjectTranslator getMapFromSKProductsResponse:response]; XCTAssertEqualObjects(map, self.productResponseMap); } @@ -125,7 +136,7 @@ - (void)testPaymentToMap { - (void)testPaymentTransactionToMap { // payment is not KVC, cannot test payment field. SKPaymentTransactionStub *paymentTransaction = - [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; NSDictionary *map = [FIAObjectTranslator getMapFromSKPaymentTransaction:paymentTransaction]; XCTAssertEqualObjects(map, self.transactionMap); } @@ -144,4 +155,21 @@ - (void)testLocaleToMap { } } +- (void)testSKStorefrontToMap { + if (@available(iOS 13.0, *)) { + SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront]; + XCTAssertEqualObjects(map, self.storefrontMap); + } +} + +- (void)testSKStorefrontAndSKPaymentTransactionToMap { + if (@available(iOS 13.0, *)) { + SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; + SKPaymentTransaction *transaction = [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront andSKPaymentTransaction:transaction]; + XCTAssertEqualObjects(map, self.storefrontAndPaymentTransactionMap); + } +} + @end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h index 2d0187e88aed..4cc0980cb149 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h @@ -29,6 +29,11 @@ NS_ASSUME_NONNULL_BEGIN + (NSDictionary *)getMapFromNSError:(NSError *)error; ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); + ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront + andSKPaymentTransaction:(SKPaymentTransaction *)transaction API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); + @end ; diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m index 5d6e0a244a96..7dd056bd4f72 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m @@ -169,4 +169,31 @@ + (NSDictionary *)getMapFromNSError:(NSError *)error { return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo}; } ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront { + if (!storefront) { + return nil; + } + + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"countryCode" : storefront.countryCode, + @"identifier" : storefront.identifier + }]; + + return map; +} + ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront + andSKPaymentTransaction:(SKPaymentTransaction *)transaction { + if(!storefront || !transaction) { + return nil; + } + + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"storefront" : [FIAObjectTranslator getMapFromSKStorefront:storefront], + @"transaction" : [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction] + }]; + + return map; +} + @end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h new file mode 100644 index 000000000000..387593fbc257 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h @@ -0,0 +1,16 @@ +// 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 +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +API_AVAILABLE(ios(13), macos(10.15)) +@interface FIAPPaymentQueueDelegate : NSObject +- (id) initWithMethodChannel:(FlutterMethodChannel *) methodChannel; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m new file mode 100644 index 000000000000..218f19d4ff2c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m @@ -0,0 +1,71 @@ +// 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 "FIAPPaymentQueueDelegate.h" +#import "FIAObjectTranslator.h" + +@interface FIAPPaymentQueueDelegate () + +@property (strong, nonatomic, readonly) FlutterMethodChannel *callbackChannel; + +@end + +@implementation FIAPPaymentQueueDelegate + +- (id) initWithMethodChannel:(FlutterMethodChannel *)methodChannel { + self = [super init]; + if (self) { + _callbackChannel = methodChannel; + } + + return self; +} + +- (BOOL)paymentQueue:(SKPaymentQueue *)paymentQueue shouldContinueTransaction:(SKPaymentTransaction *)transaction inStorefront:(SKStorefront *)newStorefront { + // Default return value for this method is true (see https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) + __block BOOL shouldContinue = YES; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.callbackChannel invokeMethod:@"shouldContinueTransaction" + arguments:[FIAObjectTranslator getMapFromSKStorefront:newStorefront andSKPaymentTransaction:transaction] + result:^(id _Nullable result) { + // When result is a valid instance of NSNumber use it to determine if + // the transaction should continue. Otherwise use the default value. + if (result && [result isKindOfClass: [NSNumber class]]) { + shouldContinue = [(NSNumber *)result boolValue]; + } + + dispatch_semaphore_signal(semaphore); + }]; + + // The client should respond within 1 second otherwise continue + // with default value. + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); + + return shouldContinue; +} + +- (BOOL)paymentQueueShouldShowPriceConsent:(SKPaymentQueue *)paymentQueue { + // Default return value for this method is true (see https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) + __block BOOL shouldShowPriceConsent = YES; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.callbackChannel invokeMethod:@"shouldShowPriceConsent" + arguments:nil + result:^(id _Nullable result) { + // When result is a valid instance of NSNumber use it to determine if + // the transaction should continue. Otherwise use the default value. + if (result && [result isKindOfClass: [NSNumber class]]) { + shouldShowPriceConsent = [(NSNumber *)result boolValue]; + } + + dispatch_semaphore_signal(semaphore); + }]; + + // The client should respond within 1 second otherwise continue + // with default value. + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); + + return shouldShowPriceConsent; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h index 30865b2c3598..2bb0995823f9 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h @@ -18,6 +18,8 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); @interface FIAPaymentQueueHandler : NSObject +@property (NS_NONATOMIC_IOSONLY, weak, nullable) id delegate API_AVAILABLE(ios(13.0), macos(10.15), watchos(6.2)); + - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved @@ -43,6 +45,15 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); // @return whether "addPayment" was successful. - (BOOL)addPayment:(SKPayment *)payment; +// Displays the price consent sheet. +// +// The price consent sheet is only displayed when the following +// it true: +// - You have increased the price of the subscription in App Store Connect. +// - The subscriber has not yet responded to a price consent query. +// Otherwise the method has no effect. +- (void)showPriceConsentIfNeeded API_AVAILABLE(ios(13.4)); + @end NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m index 20ccbc5adb48..54e6d170a22f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m @@ -3,6 +3,7 @@ // found in the LICENSE file. #import "FIAPaymentQueueHandler.h" +#import "FIAPPaymentQueueDelegate.h" @interface FIAPaymentQueueHandler () @@ -36,6 +37,10 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; _shouldAddStorePayment = shouldAddStorePayment; _updatedDownloads = updatedDownloads; + + if (@available(iOS 13.0, macOS 10.15, *)) { + queue.delegate = self.delegate; + } } return self; } @@ -78,6 +83,10 @@ - (void)presentCodeRedemptionSheet { } } +- (void)showPriceConsentIfNeeded { + [self.queue showPriceConsentIfNeeded]; +} + #pragma mark - observing // Sent when the transaction array has changed (additions or state changes). Client should check diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m index 8a998d9f4300..f8e830be9802 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m @@ -8,6 +8,7 @@ #import "FIAPReceiptManager.h" #import "FIAPRequestHandler.h" #import "FIAPaymentQueueHandler.h" +#import "FIAPPaymentQueueDelegate.h" @interface InAppPurchasePlugin () @@ -19,13 +20,18 @@ @interface InAppPurchasePlugin () // for purchase. @property(strong, nonatomic, readonly) NSMutableDictionary *productsCache; -// Call back channel to dart used for when a listener function is triggered. -@property(strong, nonatomic, readonly) FlutterMethodChannel *callbackChannel; +// Callback channel to dart used for when a function from the transaction observer is triggered. +@property(strong, nonatomic, readonly) FlutterMethodChannel *transactionObserverCallbackChannel; + +// Callback channel to dart used for when a function from the payment queue delegate is triggered. +@property(strong, nonatomic, readonly) FlutterMethodChannel *paymentQueueDelegateCallbackChannel; + @property(strong, nonatomic, readonly) NSObject *registry; @property(strong, nonatomic, readonly) NSObject *messenger; @property(strong, nonatomic, readonly) NSObject *registrar; @property(strong, nonatomic, readonly) FIAPReceiptManager *receiptManager; +@property(strong, nonatomic, readonly) FIAPPaymentQueueDelegate *paymentQueueDelegate API_AVAILABLE(ios(13)); @end @@ -33,8 +39,8 @@ @implementation InAppPurchasePlugin + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" - binaryMessenger:[registrar messenger]]; + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" + binaryMessenger:[registrar messenger]]; InAppPurchasePlugin *instance = [[InAppPurchasePlugin alloc] initWithRegistrar:registrar]; [registrar addMethodCallDelegate:instance channel:channel]; } @@ -52,30 +58,39 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar _registrar = registrar; _registry = [registrar textures]; _messenger = [registrar messenger]; - + __weak typeof(self) weakSelf = self; _paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] - transactionsUpdated:^(NSArray *_Nonnull transactions) { - [weakSelf handleTransactionsUpdated:transactions]; - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - [weakSelf handleTransactionsRemoved:transactions]; - } - restoreTransactionFailed:^(NSError *_Nonnull error) { - [weakSelf handleTransactionRestoreFailed:error]; - } - restoreCompletedTransactionsFinished:^{ - [weakSelf restoreCompletedTransactionsFinished]; - } - shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { - return [weakSelf shouldAddStorePayment:payment product:product]; - } - updatedDownloads:^void(NSArray *_Nonnull downloads) { - [weakSelf updatedDownloads:downloads]; - }]; - _callbackChannel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" - binaryMessenger:[registrar messenger]]; + transactionsUpdated:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsUpdated:transactions]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsRemoved:transactions]; + } + restoreTransactionFailed:^(NSError *_Nonnull error) { + [weakSelf handleTransactionRestoreFailed:error]; + } + restoreCompletedTransactionsFinished:^{ + [weakSelf restoreCompletedTransactionsFinished]; + } + shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { + return [weakSelf shouldAddStorePayment:payment product:product]; + } + updatedDownloads:^void(NSArray *_Nonnull downloads) { + [weakSelf updatedDownloads:downloads]; + }]; + + if (@available(iOS 13.0, *)) { + _paymentQueueDelegateCallbackChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate" + binaryMessenger:_messenger]; + + _paymentQueueDelegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel: _paymentQueueDelegateCallbackChannel]; + _paymentQueueHandler.delegate = _paymentQueueDelegate; + } + + _transactionObserverCallbackChannel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" + binaryMessenger:[registrar messenger]]; return self; } @@ -93,7 +108,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { [self restoreTransactions:call result:result]; } else if ([@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" - isEqualToString:call.method]) { + isEqualToString:call.method]) { [self presentCodeRedemptionSheet:call result:result]; } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { [self retrieveReceiptData:call result:result]; @@ -103,6 +118,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [_paymentQueueHandler startObservingPaymentQueue]; } else if ([@"-[SKPaymentQueue stopObservingTransactionQueue]" isEqualToString:call.method]) { [_paymentQueueHandler stopObservingPaymentQueue]; + } else if ([@"-[SKPaymentQueue showPriceConsentIfNeeded]" isEqualToString:call.method]) { + if (@available(iOS 13.4, *)) { + [_paymentQueueHandler showPriceConsentIfNeeded]; + } } else { result(FlutterMethodNotImplemented); } @@ -114,7 +133,7 @@ - (void)canMakePayments:(FlutterResult)result { - (void)getPendingTransactions:(FlutterResult)result { NSArray *transactions = - [self.paymentQueueHandler getUnfinishedTransactions]; + [self.paymentQueueHandler getUnfinishedTransactions]; NSMutableArray *transactionMaps = [[NSMutableArray alloc] init]; for (SKPaymentTransaction *transaction in transactions) { [transactionMaps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; @@ -131,7 +150,7 @@ - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(Flutter } NSArray *productIdentifiers = (NSArray *)call.arguments; SKProductsRequest *request = - [self getProductRequestWithIdentifiers:[NSSet setWithArray:productIdentifiers]]; + [self getProductRequestWithIdentifiers:[NSSet setWithArray:productIdentifiers]]; FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; [self.requestHandlers addObject:handler]; __weak typeof(self) weakSelf = self; @@ -146,7 +165,7 @@ - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(Flutter if (!response) { result([FlutterError errorWithCode:@"storekit_platform_no_response" message:@"Failed to get SKProductResponse in startRequest " - @"call. Error occured on iOS platform" + @"call. Error occured on iOS platform" details:call.arguments]); return; } @@ -172,12 +191,12 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { SKProduct *product = [self getProduct:productID]; if (!product) { result([FlutterError - errorWithCode:@"storekit_invalid_payment_object" - message: - @"You have requested a payment for an invalid product. Either the " - @"`productIdentifier` of the payment is not valid or the product has not been " - @"fetched before adding the payment to the payment queue." - details:call.arguments]); + errorWithCode:@"storekit_invalid_payment_object" + message: + @"You have requested a payment for an invalid product. Either the " + @"`productIdentifier` of the payment is not valid or the product has not been " + @"fetched before adding the payment to the payment queue." + details:call.arguments]); return; } SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; @@ -187,18 +206,18 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { if (@available(iOS 8.3, *)) { NSNumber *simulatesAskToBuyInSandbox = [paymentMap objectForKey:@"simulatesAskToBuyInSandbox"]; payment.simulatesAskToBuyInSandbox = (id)simulatesAskToBuyInSandbox == (id)[NSNull null] - ? NO - : [simulatesAskToBuyInSandbox boolValue]; + ? NO + : [simulatesAskToBuyInSandbox boolValue]; } - + if (![self.paymentQueueHandler addPayment:payment]) { result([FlutterError - errorWithCode:@"storekit_duplicate_product_object" - message:@"There is a pending transaction for the same product identifier. Please " - @"either wait for it to be finished or finish it manually using " - @"`completePurchase` to avoid edge cases." - - details:call.arguments]); + errorWithCode:@"storekit_duplicate_product_object" + message:@"There is a pending transaction for the same product identifier. Please " + @"either wait for it to be finished or finish it manually using " + @"`completePurchase` to avoid edge cases." + + details:call.arguments]); return; } result(nil); @@ -214,10 +233,10 @@ - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result NSDictionary *paymentMap = (NSDictionary *)call.arguments; NSString *transactionIdentifier = [paymentMap objectForKey:@"transactionIdentifier"]; NSString *productIdentifier = [paymentMap objectForKey:@"productIdentifier"]; - + NSArray *pendingTransactions = - [self.paymentQueueHandler getUnfinishedTransactions]; - + [self.paymentQueueHandler getUnfinishedTransactions]; + for (SKPaymentTransaction *transaction in pendingTransactions) { // If the user cancels the purchase dialog we won't have a transactionIdentifier. // So if it is null AND a transaction in the pendingTransactions list has @@ -236,16 +255,16 @@ - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result } } } - + result(nil); } - (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)result { if (call.arguments && ![call.arguments isKindOfClass:[NSString class]]) { result([FlutterError - errorWithCode:@"storekit_invalid_argument" - message:@"Argument is not nil and the type of finishTransaction is not a string." - details:call.arguments]); + errorWithCode:@"storekit_invalid_argument" + message:@"Argument is not nil and the type of finishTransaction is not a string." + details:call.arguments]); return; } [self.paymentQueueHandler restoreTransactions:call.arguments]; @@ -301,14 +320,14 @@ - (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { }]; } -#pragma mark - delegates: +#pragma mark - transaction observer: - (void)handleTransactionsUpdated:(NSArray *)transactions { NSMutableArray *maps = [NSMutableArray new]; for (SKPaymentTransaction *transaction in transactions) { [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; } - [self.callbackChannel invokeMethod:@"updatedTransactions" arguments:maps]; + [self.transactionObserverCallbackChannel invokeMethod:@"updatedTransactions" arguments:maps]; } - (void)handleTransactionsRemoved:(NSArray *)transactions { @@ -316,16 +335,16 @@ - (void)handleTransactionsRemoved:(NSArray *)transaction for (SKPaymentTransaction *transaction in transactions) { [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; } - [self.callbackChannel invokeMethod:@"removedTransactions" arguments:maps]; + [self.transactionObserverCallbackChannel invokeMethod:@"removedTransactions" arguments:maps]; } - (void)handleTransactionRestoreFailed:(NSError *)error { - [self.callbackChannel invokeMethod:@"restoreCompletedTransactionsFailed" + [self.transactionObserverCallbackChannel invokeMethod:@"restoreCompletedTransactionsFailed" arguments:[FIAObjectTranslator getMapFromNSError:error]]; } - (void)restoreCompletedTransactionsFinished { - [self.callbackChannel invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" + [self.transactionObserverCallbackChannel invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" arguments:nil]; } @@ -338,7 +357,7 @@ - (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product // have a interception method that deciding if the payment should be processed (implemented by the // programmer). [self.productsCache setObject:product forKey:product.productIdentifier]; - [self.callbackChannel invokeMethod:@"shouldAddStorePayment" + [self.transactionObserverCallbackChannel invokeMethod:@"shouldAddStorePayment" arguments:@{ @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], @"product" : [FIAObjectTranslator getMapFromSKProduct:product] From fe7f2f93a17e681d27d0444cceda0fe5d200a4cb Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 10:53:31 +0200 Subject: [PATCH 02/13] Add support to register and remove payment queue delegate --- .../RunnerTests/InAppPurchasePluginTests.m | 367 ++++++++++-------- .../ios/Classes/FIAPPaymentQueueDelegate.h | 2 +- .../ios/Classes/InAppPurchasePlugin.m | 30 +- 3 files changed, 233 insertions(+), 166 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m index adc3f8f8434a..deb9c30446d8 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -19,7 +19,7 @@ @implementation InAppPurchasePluginTest - (void)setUp { self.plugin = - [[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]]; + [[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]]; } - (void)tearDown { @@ -27,14 +27,14 @@ - (void)tearDown { - (void)testInvalidMethodCall { XCTestExpectation* expectation = - [self expectationWithDescription:@"expect result to be not implemented"]; + [self expectationWithDescription:@"expect result to be not implemented"]; FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; __block id result; [self.plugin handleMethodCall:call result:^(id r) { - [expectation fulfill]; - result = r; - }]; + [expectation fulfill]; + result = r; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(result, FlutterMethodNotImplemented); } @@ -42,30 +42,30 @@ - (void)testInvalidMethodCall { - (void)testCanMakePayments { XCTestExpectation* expectation = [self expectationWithDescription:@"expect result to be YES"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" - arguments:NULL]; + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" + arguments:NULL]; __block id result; [self.plugin handleMethodCall:call result:^(id r) { - [expectation fulfill]; - result = r; - }]; + [expectation fulfill]; + result = r; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(result, @YES); } - (void)testGetProductResponse { XCTestExpectation* expectation = - [self expectationWithDescription:@"expect response contains 1 item"]; + [self expectationWithDescription:@"expect response contains 1 item"]; FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" - arguments:@[ @"123" ]]; + methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" + arguments:@[ @"123" ]]; __block id result; [self.plugin handleMethodCall:call result:^(id r) { - [expectation fulfill]; - result = r; - }]; + [expectation fulfill]; + result = r; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssert([result isKindOfClass:[NSDictionary class]]); NSArray* resultArray = [result objectForKey:@"products"]; @@ -75,146 +75,146 @@ - (void)testGetProductResponse { - (void)testAddPaymentFailure { XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return failed state"]; + [self expectationWithDescription:@"result should return failed state"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }]; + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStateFailed; __block SKPaymentTransaction* transactionForUpdateBlock; self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - SKPaymentTransaction* transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStateFailed) { - transactionForUpdateBlock = transaction; - [expectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStateFailed) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - + [self.plugin handleMethodCall:call result:^(id r){ - }]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); } - (void)testAddPaymentSuccessWithMockQueue { XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return success state"]; + [self expectationWithDescription:@"result should return success state"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }]; + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStatePurchased; __block SKPaymentTransaction* transactionForUpdateBlock; self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - SKPaymentTransaction* transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStatePurchased) { - transactionForUpdateBlock = transaction; - [expectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; [self.plugin handleMethodCall:call result:^(id r){ - }]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); } - (void)testAddPaymentWithNullSandboxArgument { XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return success state"]; + [self expectationWithDescription:@"result should return success state"]; XCTestExpectation* simulatesAskToBuyInSandboxExpectation = - [self expectationWithDescription:@"payment isn't simulatesAskToBuyInSandbox"]; + [self expectationWithDescription:@"payment isn't simulatesAskToBuyInSandbox"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : [NSNull null], - }]; + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : [NSNull null], + }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStatePurchased; __block SKPaymentTransaction* transactionForUpdateBlock; self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - SKPaymentTransaction* transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStatePurchased) { - transactionForUpdateBlock = transaction; - [expectation fulfill]; - } - if (@available(iOS 8.3, *)) { - if (!transaction.payment.simulatesAskToBuyInSandbox) { - [simulatesAskToBuyInSandboxExpectation fulfill]; - } - } else { - [simulatesAskToBuyInSandboxExpectation fulfill]; - } + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + if (@available(iOS 8.3, *)) { + if (!transaction.payment.simulatesAskToBuyInSandbox) { + [simulatesAskToBuyInSandboxExpectation fulfill]; } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; + } else { + [simulatesAskToBuyInSandboxExpectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; [self.plugin handleMethodCall:call result:^(id r){ - }]; + }]; [self waitForExpectations:@[ expectation, simulatesAskToBuyInSandboxExpectation ] timeout:5]; XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); } - (void)testRestoreTransactions { XCTestExpectation* expectation = - [self expectationWithDescription:@"result successfully restore transactions"]; + [self expectationWithDescription:@"result successfully restore transactions"]; FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" - arguments:nil]; + methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" + arguments:nil]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStatePurchased; __block BOOL callbackInvoked = NO; self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:^() { - callbackInvoked = YES; - [expectation fulfill]; - } - shouldAddStorePayment:nil - updatedDownloads:nil]; + transactionsUpdated:^(NSArray* _Nonnull transactions) { + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:^() { + callbackInvoked = YES; + [expectation fulfill]; + } + shouldAddStorePayment:nil + updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; [self.plugin handleMethodCall:call result:^(id r){ - }]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertTrue(callbackInvoked); } @@ -222,14 +222,14 @@ - (void)testRestoreTransactions { - (void)testRetrieveReceiptData { XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" - arguments:nil]; + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; __block NSDictionary* result; [self.plugin handleMethodCall:call result:^(id r) { - result = r; - [expectation fulfill]; - }]; + result = r; + [expectation fulfill]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; NSLog(@"%@", result); XCTAssertNotNil(result); @@ -238,30 +238,30 @@ - (void)testRetrieveReceiptData { - (void)testRefreshReceiptRequest { XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" - arguments:nil]; + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" + arguments:nil]; __block BOOL result = NO; [self.plugin handleMethodCall:call result:^(id r) { - result = YES; - [expectation fulfill]; - }]; + result = YES; + [expectation fulfill]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertTrue(result); } - (void)testPresentCodeRedemptionSheet { XCTestExpectation* expectation = - [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; + [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" - arguments:nil]; + methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + arguments:nil]; __block BOOL callbackInvoked = NO; [self.plugin handleMethodCall:call result:^(id r) { - callbackInvoked = YES; - [expectation fulfill]; - }]; + callbackInvoked = YES; + [expectation fulfill]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertTrue(callbackInvoked); } @@ -269,7 +269,7 @@ - (void)testPresentCodeRedemptionSheet { - (void)testGetPendingTransactions { XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; SKPaymentQueue* mockQueue = OCMClassMock(SKPaymentQueue.class); NSDictionary* transactionMap = @{ @"transactionIdentifier" : [NSNull null], @@ -282,8 +282,8 @@ - (void)testGetPendingTransactions { @"originalTransaction" : [NSNull null], }; OCMStub(mockQueue.transactions).andReturn(@[ [[SKPaymentTransactionStub alloc] - initWithMap:transactionMap] ]); - + initWithMap:transactionMap] ]); + __block NSArray* resultArray; self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue transactionsUpdated:nil @@ -294,75 +294,128 @@ - (void)testGetPendingTransactions { updatedDownloads:nil]; [self.plugin handleMethodCall:call result:^(id r) { - resultArray = r; - [expectation fulfill]; - }]; + resultArray = r; + [expectation fulfill]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqualObjects(resultArray, @[ transactionMap ]); } - (void)testStartAndStopObservingPaymentQueue { FlutterMethodCall* startCall = [FlutterMethodCall - methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]" - arguments:nil]; + methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]" + arguments:nil]; FlutterMethodCall* stopCall = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]" - arguments:nil]; - + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]" + arguments:nil]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; - + self.plugin.paymentQueueHandler = - [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, - SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; - + [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, + SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + // Check that there is no observer to start with. XCTAssertNil(queue.observer); - + // Start observing [self.plugin handleMethodCall:startCall result:^(id r){ - }]; - + }]; + // Observer should be set XCTAssertNotNil(queue.observer); - + // Stop observing [self.plugin handleMethodCall:stopCall result:^(id r){ - }]; - + }]; + // No observer should be set XCTAssertNil(queue.observer); } -- (void)testShowPriceConsentIfNeeded { - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" - arguments:nil]; +- (void)testRegisterPaymentQueueDelegate { + if (@available(iOS 13, *)) { + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[SKPaymentQueue registerDelegate]" + arguments:nil]; + + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil]; + + // Verify the delegate is nil before we register one. + XCTAssertNil(self.plugin.paymentQueueHandler.delegate); + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + + // Verify the delegate is not nil after we registered one. + XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); + } +} +- (void)testRemovePaymentQueueDelegate { + if (@available(iOS 13, *)) { + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[SKPaymentQueue removeDelegate]" + arguments:nil]; + + + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil]; + self.plugin.paymentQueueHandler.delegate = OCMProtocolMock(@protocol(SKPaymentQueueDelegate)); + + // Verify the delegate is not nil before removing it. + XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + + // Verify the delegate is nill after removing it. + XCTAssertNil(self.plugin.paymentQueueHandler.delegate); + } +} +- (void)testShowPriceConsentIfNeeded { + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" + arguments:nil]; + + FIAPaymentQueueHandler* mockQueueHandler = OCMClassMock(FIAPaymentQueueHandler.class); - self.plugin.paymentQueueHandler = mockQueueHandler; [self.plugin handleMethodCall:call result:^(id r){ - }]; - + }]; + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" if (@available(iOS 13.4, *)) { OCMVerify(times(1), [mockQueueHandler showPriceConsentIfNeeded]); } else { - + OCMVerify(never(), [mockQueueHandler showPriceConsentIfNeeded]); } #pragma clang diagnostic pop diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h index 387593fbc257..911e923da87f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h @@ -8,7 +8,7 @@ NS_ASSUME_NONNULL_BEGIN -API_AVAILABLE(ios(13), macos(10.15)) +API_AVAILABLE(ios(13)) @interface FIAPPaymentQueueDelegate : NSObject - (id) initWithMethodChannel:(FlutterMethodChannel *) methodChannel; @end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m index f8e830be9802..69d0342af45f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m @@ -80,14 +80,6 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar [weakSelf updatedDownloads:downloads]; }]; - if (@available(iOS 13.0, *)) { - _paymentQueueDelegateCallbackChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate" - binaryMessenger:_messenger]; - - _paymentQueueDelegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel: _paymentQueueDelegateCallbackChannel]; - _paymentQueueHandler.delegate = _paymentQueueDelegate; - } - _transactionObserverCallbackChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" binaryMessenger:[registrar messenger]]; @@ -118,6 +110,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [_paymentQueueHandler startObservingPaymentQueue]; } else if ([@"-[SKPaymentQueue stopObservingTransactionQueue]" isEqualToString:call.method]) { [_paymentQueueHandler stopObservingPaymentQueue]; + } else if ([@"-[SKPaymentQueue registerDelegate]" isEqualToString:call.method]) { + [self registerPaymentQueueDelegate]; + } else if ([@"-[SKPaymentQueue removeDelegate]" isEqualToString:call.method]) { + [self removePaymentQueueDelegate]; } else if ([@"-[SKPaymentQueue showPriceConsentIfNeeded]" isEqualToString:call.method]) { if (@available(iOS 13.4, *)) { [_paymentQueueHandler showPriceConsentIfNeeded]; @@ -320,6 +316,24 @@ - (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { }]; } +- (void)registerPaymentQueueDelegate { + if (@available(iOS 13.0, *)) { + _paymentQueueDelegateCallbackChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate" + binaryMessenger:_messenger]; + + _paymentQueueDelegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel: _paymentQueueDelegateCallbackChannel]; + _paymentQueueHandler.delegate = _paymentQueueDelegate; + } +} + +- (void)removePaymentQueueDelegate { + if (@available(iOS 13.0, *)) { + _paymentQueueHandler.delegate = nil; + } + _paymentQueueDelegate = nil; + _paymentQueueDelegateCallbackChannel = nil; +} + #pragma mark - transaction observer: - (void)handleTransactionsUpdated:(NSArray *)transactions { From 6a9e64ed73cb12525824b9410674cf02cf7ece08 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 13:14:28 +0200 Subject: [PATCH 03/13] Added Dart implementation of PaymentQueueDelegate --- .../RunnerTests/FIAPPaymentQueueDeleteTests.m | 102 ++--- .../RunnerTests/InAppPurchasePluginTests.m | 377 +++++++++--------- .../example/ios/RunnerTests/TranslatorTests.m | 34 +- .../ios/Classes/FIAObjectTranslator.h | 6 +- .../ios/Classes/FIAObjectTranslator.m | 10 +- .../ios/Classes/FIAPPaymentQueueDelegate.h | 2 +- .../ios/Classes/FIAPPaymentQueueDelegate.m | 65 +-- .../ios/Classes/FIAPaymentQueueHandler.h | 3 +- .../ios/Classes/FIAPaymentQueueHandler.m | 2 +- .../ios/Classes/InAppPurchasePlugin.m | 136 ++++--- .../in_app_purchase_ios/lib/src/channel.dart | 5 + .../sk_payment_queue_delegate_wrapper.dart | 35 ++ .../sk_payment_queue_wrapper.dart | 53 ++- .../sk_storefront_wrapper.dart | 47 +++ .../sk_storefront_wrapper.g.dart | 21 + .../lib/store_kit_wrappers.dart | 2 + 16 files changed, 537 insertions(+), 363 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart create mode 100644 packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m index a869881256e5..58fe01da444e 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m @@ -4,17 +4,17 @@ #import #import -#import "FIAPaymentQueueHandler.h" #import "FIAObjectTranslator.h" +#import "FIAPaymentQueueHandler.h" #import "Stubs.h" @import in_app_purchase_ios; @interface FIAPPaymentQueueDelegateTests : XCTestCase -@property (strong, nonatomic) FlutterMethodChannel *channel; -@property (strong, nonatomic) SKPaymentTransaction *transaction; -@property (strong, nonatomic) SKStorefront *storefront; +@property(strong, nonatomic) FlutterMethodChannel *channel; +@property(strong, nonatomic) SKPaymentTransaction *transaction; +@property(strong, nonatomic) SKStorefront *storefront; @end @@ -22,8 +22,8 @@ @implementation FIAPPaymentQueueDelegateTests - (void)setUp { self.channel = OCMClassMock(FlutterMethodChannel.class); - - NSDictionary* transactionMap = @{ + + NSDictionary *transactionMap = @{ @"transactionIdentifier" : [NSNull null], @"transactionState" : @(SKPaymentTransactionStatePurchasing), @"payment" : [NSNull null], @@ -34,8 +34,8 @@ - (void)setUp { @"originalTransaction" : [NSNull null], }; self.transaction = [[SKPaymentTransactionStub alloc] initWithMap:transactionMap]; - - NSDictionary* storefrontMap = @{ + + NSDictionary *storefrontMap = @{ @"countryCode" : @"USA", @"identifier" : @"unique_identifier", }; @@ -47,65 +47,71 @@ - (void)tearDown { } - (void)testShouldContinueTransaction { - if(@available(iOS 13.0, *)) { - FIAPPaymentQueueDelegate *delegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; - - OCMStub( - [self.channel invokeMethod:@"shouldContinueTransaction" - arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront - andSKPaymentTransaction:self.transaction] - result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); - - BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) shouldContinueTransaction:self.transaction - inStorefront:self.storefront]; - + if (@available(iOS 13.0, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel + invokeMethod:@"shouldContinueTransaction" + arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront + andSKPaymentTransaction:self.transaction] + result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); + + BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) + shouldContinueTransaction:self.transaction + inStorefront:self.storefront]; + XCTAssertFalse(shouldContinue); } } - (void)testShouldContinueTransaction_should_default_to_yes { - if(@available(iOS 13.0, *)) { - FIAPPaymentQueueDelegate *delegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; - - OCMStub( - [self.channel invokeMethod:@"shouldContinueTransaction" + if (@available(iOS 13.0, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel invokeMethod:@"shouldContinueTransaction" arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront - andSKPaymentTransaction:self.transaction] + andSKPaymentTransaction:self.transaction] result:[OCMArg any]]); - - BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) shouldContinueTransaction:self.transaction - inStorefront:self.storefront]; - + + BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) + shouldContinueTransaction:self.transaction + inStorefront:self.storefront]; + XCTAssertTrue(shouldContinue); } } - (void)testShouldShowPriceConsentIfNeeded { - if(@available(iOS 13.4, *)) { - FIAPPaymentQueueDelegate *delegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; - - OCMStub( - [self.channel invokeMethod:@"shouldShowPriceConsent" - arguments:nil - result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); - - BOOL shouldShow = [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; - + if (@available(iOS 13.4, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel + invokeMethod:@"shouldShowPriceConsent" + arguments:nil + result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); + + BOOL shouldShow = + [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; + XCTAssertFalse(shouldShow); } } - (void)testShouldShowPriceConsentIfNeeded_should_default_to_yes { - if(@available(iOS 13.4, *)) { - FIAPPaymentQueueDelegate *delegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; - - OCMStub( - [self.channel invokeMethod:@"shouldShowPriceConsent" + if (@available(iOS 13.4, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel invokeMethod:@"shouldShowPriceConsent" arguments:nil result:[OCMArg any]]); - - BOOL shouldShow = [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; - + + BOOL shouldShow = + [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; + XCTAssertTrue(shouldShow); } } diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m index deb9c30446d8..045abcdea922 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -19,7 +19,7 @@ @implementation InAppPurchasePluginTest - (void)setUp { self.plugin = - [[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]]; + [[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]]; } - (void)tearDown { @@ -27,14 +27,14 @@ - (void)tearDown { - (void)testInvalidMethodCall { XCTestExpectation* expectation = - [self expectationWithDescription:@"expect result to be not implemented"]; + [self expectationWithDescription:@"expect result to be not implemented"]; FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; __block id result; [self.plugin handleMethodCall:call result:^(id r) { - [expectation fulfill]; - result = r; - }]; + [expectation fulfill]; + result = r; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(result, FlutterMethodNotImplemented); } @@ -42,30 +42,30 @@ - (void)testInvalidMethodCall { - (void)testCanMakePayments { XCTestExpectation* expectation = [self expectationWithDescription:@"expect result to be YES"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" - arguments:NULL]; + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" + arguments:NULL]; __block id result; [self.plugin handleMethodCall:call result:^(id r) { - [expectation fulfill]; - result = r; - }]; + [expectation fulfill]; + result = r; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(result, @YES); } - (void)testGetProductResponse { XCTestExpectation* expectation = - [self expectationWithDescription:@"expect response contains 1 item"]; + [self expectationWithDescription:@"expect response contains 1 item"]; FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" - arguments:@[ @"123" ]]; + methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" + arguments:@[ @"123" ]]; __block id result; [self.plugin handleMethodCall:call result:^(id r) { - [expectation fulfill]; - result = r; - }]; + [expectation fulfill]; + result = r; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssert([result isKindOfClass:[NSDictionary class]]); NSArray* resultArray = [result objectForKey:@"products"]; @@ -75,146 +75,146 @@ - (void)testGetProductResponse { - (void)testAddPaymentFailure { XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return failed state"]; + [self expectationWithDescription:@"result should return failed state"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }]; + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStateFailed; __block SKPaymentTransaction* transactionForUpdateBlock; self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - SKPaymentTransaction* transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStateFailed) { - transactionForUpdateBlock = transaction; - [expectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStateFailed) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - + [self.plugin handleMethodCall:call result:^(id r){ - }]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); } - (void)testAddPaymentSuccessWithMockQueue { XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return success state"]; + [self expectationWithDescription:@"result should return success state"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }]; + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStatePurchased; __block SKPaymentTransaction* transactionForUpdateBlock; self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - SKPaymentTransaction* transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStatePurchased) { - transactionForUpdateBlock = transaction; - [expectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; [self.plugin handleMethodCall:call result:^(id r){ - }]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); } - (void)testAddPaymentWithNullSandboxArgument { XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return success state"]; + [self expectationWithDescription:@"result should return success state"]; XCTestExpectation* simulatesAskToBuyInSandboxExpectation = - [self expectationWithDescription:@"payment isn't simulatesAskToBuyInSandbox"]; + [self expectationWithDescription:@"payment isn't simulatesAskToBuyInSandbox"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : [NSNull null], - }]; + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : [NSNull null], + }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStatePurchased; __block SKPaymentTransaction* transactionForUpdateBlock; self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - SKPaymentTransaction* transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStatePurchased) { - transactionForUpdateBlock = transaction; - [expectation fulfill]; - } - if (@available(iOS 8.3, *)) { - if (!transaction.payment.simulatesAskToBuyInSandbox) { - [simulatesAskToBuyInSandboxExpectation fulfill]; + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + if (@available(iOS 8.3, *)) { + if (!transaction.payment.simulatesAskToBuyInSandbox) { + [simulatesAskToBuyInSandboxExpectation fulfill]; + } + } else { + [simulatesAskToBuyInSandboxExpectation fulfill]; + } } - } else { - [simulatesAskToBuyInSandboxExpectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; [self.plugin handleMethodCall:call result:^(id r){ - }]; + }]; [self waitForExpectations:@[ expectation, simulatesAskToBuyInSandboxExpectation ] timeout:5]; XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); } - (void)testRestoreTransactions { XCTestExpectation* expectation = - [self expectationWithDescription:@"result successfully restore transactions"]; + [self expectationWithDescription:@"result successfully restore transactions"]; FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" - arguments:nil]; + methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" + arguments:nil]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStatePurchased; __block BOOL callbackInvoked = NO; self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:^() { - callbackInvoked = YES; - [expectation fulfill]; - } - shouldAddStorePayment:nil - updatedDownloads:nil]; + transactionsUpdated:^(NSArray* _Nonnull transactions) { + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:^() { + callbackInvoked = YES; + [expectation fulfill]; + } + shouldAddStorePayment:nil + updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; [self.plugin handleMethodCall:call result:^(id r){ - }]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertTrue(callbackInvoked); } @@ -222,14 +222,14 @@ - (void)testRestoreTransactions { - (void)testRetrieveReceiptData { XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" - arguments:nil]; + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; __block NSDictionary* result; [self.plugin handleMethodCall:call result:^(id r) { - result = r; - [expectation fulfill]; - }]; + result = r; + [expectation fulfill]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; NSLog(@"%@", result); XCTAssertNotNil(result); @@ -238,30 +238,30 @@ - (void)testRetrieveReceiptData { - (void)testRefreshReceiptRequest { XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" - arguments:nil]; + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" + arguments:nil]; __block BOOL result = NO; [self.plugin handleMethodCall:call result:^(id r) { - result = YES; - [expectation fulfill]; - }]; + result = YES; + [expectation fulfill]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertTrue(result); } - (void)testPresentCodeRedemptionSheet { XCTestExpectation* expectation = - [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; + [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" - arguments:nil]; + methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + arguments:nil]; __block BOOL callbackInvoked = NO; [self.plugin handleMethodCall:call result:^(id r) { - callbackInvoked = YES; - [expectation fulfill]; - }]; + callbackInvoked = YES; + [expectation fulfill]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertTrue(callbackInvoked); } @@ -269,7 +269,7 @@ - (void)testPresentCodeRedemptionSheet { - (void)testGetPendingTransactions { XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; SKPaymentQueue* mockQueue = OCMClassMock(SKPaymentQueue.class); NSDictionary* transactionMap = @{ @"transactionIdentifier" : [NSNull null], @@ -282,8 +282,8 @@ - (void)testGetPendingTransactions { @"originalTransaction" : [NSNull null], }; OCMStub(mockQueue.transactions).andReturn(@[ [[SKPaymentTransactionStub alloc] - initWithMap:transactionMap] ]); - + initWithMap:transactionMap] ]); + __block NSArray* resultArray; self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue transactionsUpdated:nil @@ -294,76 +294,77 @@ - (void)testGetPendingTransactions { updatedDownloads:nil]; [self.plugin handleMethodCall:call result:^(id r) { - resultArray = r; - [expectation fulfill]; - }]; + resultArray = r; + [expectation fulfill]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqualObjects(resultArray, @[ transactionMap ]); } - (void)testStartAndStopObservingPaymentQueue { FlutterMethodCall* startCall = [FlutterMethodCall - methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]" - arguments:nil]; + methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]" + arguments:nil]; FlutterMethodCall* stopCall = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]" - arguments:nil]; - + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]" + arguments:nil]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; - + self.plugin.paymentQueueHandler = - [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, - SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; - + [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, + SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + // Check that there is no observer to start with. XCTAssertNil(queue.observer); - + // Start observing [self.plugin handleMethodCall:startCall result:^(id r){ - }]; - + }]; + // Observer should be set XCTAssertNotNil(queue.observer); - + // Stop observing [self.plugin handleMethodCall:stopCall result:^(id r){ - }]; - + }]; + // No observer should be set XCTAssertNil(queue.observer); } - (void)testRegisterPaymentQueueDelegate { if (@available(iOS 13, *)) { - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[SKPaymentQueue registerDelegate]" - arguments:nil]; - - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil]; - + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue registerDelegate]" + arguments:nil]; + + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil]; + // Verify the delegate is nil before we register one. XCTAssertNil(self.plugin.paymentQueueHandler.delegate); - + [self.plugin handleMethodCall:call result:^(id r){ - }]; - + }]; + // Verify the delegate is not nil after we registered one. XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); } @@ -371,51 +372,49 @@ - (void)testRegisterPaymentQueueDelegate { - (void)testRemovePaymentQueueDelegate { if (@available(iOS 13, *)) { - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[SKPaymentQueue removeDelegate]" - arguments:nil]; - - - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue removeDelegate]" + arguments:nil]; + + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil]; self.plugin.paymentQueueHandler.delegate = OCMProtocolMock(@protocol(SKPaymentQueueDelegate)); - + // Verify the delegate is not nil before removing it. XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); - + [self.plugin handleMethodCall:call result:^(id r){ - }]; - + }]; + // Verify the delegate is nill after removing it. XCTAssertNil(self.plugin.paymentQueueHandler.delegate); } } - (void)testShowPriceConsentIfNeeded { - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" - arguments:nil]; - - + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" + arguments:nil]; + FIAPaymentQueueHandler* mockQueueHandler = OCMClassMock(FIAPaymentQueueHandler.class); self.plugin.paymentQueueHandler = mockQueueHandler; - + [self.plugin handleMethodCall:call result:^(id r){ - }]; - + }]; + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" if (@available(iOS 13.4, *)) { OCMVerify(times(1), [mockQueueHandler showPriceConsentIfNeeded]); } else { - OCMVerify(never(), [mockQueueHandler showPriceConsentIfNeeded]); } #pragma clang diagnostic pop diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m index 9949723c9150..42c51b846857 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m @@ -33,7 +33,7 @@ - (void)setUp { @"subscriptionPeriod" : self.periodMap, @"paymentMode" : @1 }; - + self.productMap = [[NSMutableDictionary alloc] initWithDictionary:@{ @"price" : @"1", @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], @@ -45,13 +45,13 @@ - (void)setUp { self.productMap[@"subscriptionPeriod"] = self.periodMap; self.productMap[@"introductoryPrice"] = self.discountMap; } - + if (@available(iOS 12.0, *)) { self.productMap[@"subscriptionGroupIdentifier"] = @"com.group"; } - + self.productResponseMap = - @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]}; + @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]}; self.paymentMap = @{ @"productIdentifier" : @"123", @"requestData" : @"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", @@ -83,14 +83,14 @@ - (void)setUp { @"code" : @(123), @"domain" : @"test_domain", @"userInfo" : @{ - @"key" : @"value", + @"key" : @"value", } }; self.storefrontMap = @{ @"countryCode" : @"USA", @"identifier" : @"unique_identifier", }; - + self.storefrontAndPaymentTransactionMap = @{ @"storefront" : self.storefrontMap, @"transaction" : self.transactionMap, @@ -100,7 +100,7 @@ - (void)setUp { - (void)testSKProductSubscriptionPeriodStubToMap { if (@available(iOS 11.2, *)) { SKProductSubscriptionPeriodStub *period = - [[SKProductSubscriptionPeriodStub alloc] initWithMap:self.periodMap]; + [[SKProductSubscriptionPeriodStub alloc] initWithMap:self.periodMap]; NSDictionary *map = [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:period]; XCTAssertEqualObjects(map, self.periodMap); } @@ -122,7 +122,7 @@ - (void)testProductToMap { - (void)testProductResponseToMap { SKProductsResponseStub *response = - [[SKProductsResponseStub alloc] initWithMap:self.productResponseMap]; + [[SKProductsResponseStub alloc] initWithMap:self.productResponseMap]; NSDictionary *map = [FIAObjectTranslator getMapFromSKProductsResponse:response]; XCTAssertEqualObjects(map, self.productResponseMap); } @@ -136,7 +136,7 @@ - (void)testPaymentToMap { - (void)testPaymentTransactionToMap { // payment is not KVC, cannot test payment field. SKPaymentTransactionStub *paymentTransaction = - [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; NSDictionary *map = [FIAObjectTranslator getMapFromSKPaymentTransaction:paymentTransaction]; XCTAssertEqualObjects(map, self.transactionMap); } @@ -162,14 +162,16 @@ - (void)testSKStorefrontToMap { XCTAssertEqualObjects(map, self.storefrontMap); } } - + - (void)testSKStorefrontAndSKPaymentTransactionToMap { - if (@available(iOS 13.0, *)) { - SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; - SKPaymentTransaction *transaction = [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront andSKPaymentTransaction:transaction]; - XCTAssertEqualObjects(map, self.storefrontAndPaymentTransactionMap); - } + if (@available(iOS 13.0, *)) { + SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; + SKPaymentTransaction *transaction = + [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront + andSKPaymentTransaction:transaction]; + XCTAssertEqualObjects(map, self.storefrontAndPaymentTransactionMap); + } } @end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h index 4cc0980cb149..09b2ef3b4f63 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h @@ -29,10 +29,12 @@ NS_ASSUME_NONNULL_BEGIN + (NSDictionary *)getMapFromNSError:(NSError *)error; -+ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront + API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); + (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront - andSKPaymentTransaction:(SKPaymentTransaction *)transaction API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); + andSKPaymentTransaction:(SKPaymentTransaction *)transaction + API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); @end ; diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m index 7dd056bd4f72..30b0b812da15 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m @@ -173,26 +173,26 @@ + (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront { if (!storefront) { return nil; } - + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ @"countryCode" : storefront.countryCode, @"identifier" : storefront.identifier }]; - + return map; } + (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront andSKPaymentTransaction:(SKPaymentTransaction *)transaction { - if(!storefront || !transaction) { + if (!storefront || !transaction) { return nil; } - + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ @"storefront" : [FIAObjectTranslator getMapFromSKStorefront:storefront], @"transaction" : [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction] }]; - + return map; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h index 911e923da87f..a6c91fa9e6b6 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h @@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN API_AVAILABLE(ios(13)) @interface FIAPPaymentQueueDelegate : NSObject -- (id) initWithMethodChannel:(FlutterMethodChannel *) methodChannel; +- (id)initWithMethodChannel:(FlutterMethodChannel *)methodChannel; @end NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m index 218f19d4ff2c..1056086030a5 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m @@ -7,64 +7,71 @@ @interface FIAPPaymentQueueDelegate () -@property (strong, nonatomic, readonly) FlutterMethodChannel *callbackChannel; +@property(strong, nonatomic, readonly) FlutterMethodChannel *callbackChannel; @end @implementation FIAPPaymentQueueDelegate -- (id) initWithMethodChannel:(FlutterMethodChannel *)methodChannel { +- (id)initWithMethodChannel:(FlutterMethodChannel *)methodChannel { self = [super init]; if (self) { _callbackChannel = methodChannel; } - + return self; } -- (BOOL)paymentQueue:(SKPaymentQueue *)paymentQueue shouldContinueTransaction:(SKPaymentTransaction *)transaction inStorefront:(SKStorefront *)newStorefront { - // Default return value for this method is true (see https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) +- (BOOL)paymentQueue:(SKPaymentQueue *)paymentQueue + shouldContinueTransaction:(SKPaymentTransaction *)transaction + inStorefront:(SKStorefront *)newStorefront { + // Default return value for this method is true (see + // https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) __block BOOL shouldContinue = YES; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [self.callbackChannel invokeMethod:@"shouldContinueTransaction" - arguments:[FIAObjectTranslator getMapFromSKStorefront:newStorefront andSKPaymentTransaction:transaction] - result:^(id _Nullable result) { - // When result is a valid instance of NSNumber use it to determine if - // the transaction should continue. Otherwise use the default value. - if (result && [result isKindOfClass: [NSNumber class]]) { - shouldContinue = [(NSNumber *)result boolValue]; - } - - dispatch_semaphore_signal(semaphore); - }]; - + arguments:[FIAObjectTranslator getMapFromSKStorefront:newStorefront + andSKPaymentTransaction:transaction] + result:^(id _Nullable result) { + // When result is a valid instance of NSNumber use it to determine + // if the transaction should continue. Otherwise use the default + // value. + if (result && [result isKindOfClass:[NSNumber class]]) { + shouldContinue = [(NSNumber *)result boolValue]; + } + + dispatch_semaphore_signal(semaphore); + }]; + // The client should respond within 1 second otherwise continue // with default value. dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); - + return shouldContinue; } - (BOOL)paymentQueueShouldShowPriceConsent:(SKPaymentQueue *)paymentQueue { - // Default return value for this method is true (see https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) + // Default return value for this method is true (see + // https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) __block BOOL shouldShowPriceConsent = YES; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [self.callbackChannel invokeMethod:@"shouldShowPriceConsent" arguments:nil - result:^(id _Nullable result) { - // When result is a valid instance of NSNumber use it to determine if - // the transaction should continue. Otherwise use the default value. - if (result && [result isKindOfClass: [NSNumber class]]) { - shouldShowPriceConsent = [(NSNumber *)result boolValue]; - } - - dispatch_semaphore_signal(semaphore); - }]; - + result:^(id _Nullable result) { + // When result is a valid instance of NSNumber use it to determine + // if the transaction should continue. Otherwise use the default + // value. + if (result && [result isKindOfClass:[NSNumber class]]) { + shouldShowPriceConsent = [(NSNumber *)result boolValue]; + } + + dispatch_semaphore_signal(semaphore); + }]; + // The client should respond within 1 second otherwise continue // with default value. dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); - + return shouldShowPriceConsent; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h index 2bb0995823f9..8019831d6355 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h @@ -18,7 +18,8 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); @interface FIAPaymentQueueHandler : NSObject -@property (NS_NONATOMIC_IOSONLY, weak, nullable) id delegate API_AVAILABLE(ios(13.0), macos(10.15), watchos(6.2)); +@property(NS_NONATOMIC_IOSONLY, weak, nullable) id delegate API_AVAILABLE( + ios(13.0), macos(10.15), watchos(6.2)); - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m index 54e6d170a22f..21667954cf8d 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m @@ -37,7 +37,7 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; _shouldAddStorePayment = shouldAddStorePayment; _updatedDownloads = updatedDownloads; - + if (@available(iOS 13.0, macOS 10.15, *)) { queue.delegate = self.delegate; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m index 69d0342af45f..bdc4b1fc15e4 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m @@ -5,10 +5,10 @@ #import "InAppPurchasePlugin.h" #import #import "FIAObjectTranslator.h" +#import "FIAPPaymentQueueDelegate.h" #import "FIAPReceiptManager.h" #import "FIAPRequestHandler.h" #import "FIAPaymentQueueHandler.h" -#import "FIAPPaymentQueueDelegate.h" @interface InAppPurchasePlugin () @@ -31,7 +31,8 @@ @interface InAppPurchasePlugin () @property(strong, nonatomic, readonly) NSObject *registrar; @property(strong, nonatomic, readonly) FIAPReceiptManager *receiptManager; -@property(strong, nonatomic, readonly) FIAPPaymentQueueDelegate *paymentQueueDelegate API_AVAILABLE(ios(13)); +@property(strong, nonatomic, readonly) + FIAPPaymentQueueDelegate *paymentQueueDelegate API_AVAILABLE(ios(13)); @end @@ -39,8 +40,8 @@ @implementation InAppPurchasePlugin + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" - binaryMessenger:[registrar messenger]]; + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" + binaryMessenger:[registrar messenger]]; InAppPurchasePlugin *instance = [[InAppPurchasePlugin alloc] initWithRegistrar:registrar]; [registrar addMethodCallDelegate:instance channel:channel]; } @@ -58,31 +59,31 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar _registrar = registrar; _registry = [registrar textures]; _messenger = [registrar messenger]; - + __weak typeof(self) weakSelf = self; _paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] - transactionsUpdated:^(NSArray *_Nonnull transactions) { - [weakSelf handleTransactionsUpdated:transactions]; - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - [weakSelf handleTransactionsRemoved:transactions]; - } - restoreTransactionFailed:^(NSError *_Nonnull error) { - [weakSelf handleTransactionRestoreFailed:error]; - } - restoreCompletedTransactionsFinished:^{ - [weakSelf restoreCompletedTransactionsFinished]; - } - shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { - return [weakSelf shouldAddStorePayment:payment product:product]; - } - updatedDownloads:^void(NSArray *_Nonnull downloads) { - [weakSelf updatedDownloads:downloads]; - }]; - + transactionsUpdated:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsUpdated:transactions]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsRemoved:transactions]; + } + restoreTransactionFailed:^(NSError *_Nonnull error) { + [weakSelf handleTransactionRestoreFailed:error]; + } + restoreCompletedTransactionsFinished:^{ + [weakSelf restoreCompletedTransactionsFinished]; + } + shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { + return [weakSelf shouldAddStorePayment:payment product:product]; + } + updatedDownloads:^void(NSArray *_Nonnull downloads) { + [weakSelf updatedDownloads:downloads]; + }]; + _transactionObserverCallbackChannel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" - binaryMessenger:[registrar messenger]]; + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" + binaryMessenger:[registrar messenger]]; return self; } @@ -100,7 +101,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { [self restoreTransactions:call result:result]; } else if ([@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" - isEqualToString:call.method]) { + isEqualToString:call.method]) { [self presentCodeRedemptionSheet:call result:result]; } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { [self retrieveReceiptData:call result:result]; @@ -129,7 +130,7 @@ - (void)canMakePayments:(FlutterResult)result { - (void)getPendingTransactions:(FlutterResult)result { NSArray *transactions = - [self.paymentQueueHandler getUnfinishedTransactions]; + [self.paymentQueueHandler getUnfinishedTransactions]; NSMutableArray *transactionMaps = [[NSMutableArray alloc] init]; for (SKPaymentTransaction *transaction in transactions) { [transactionMaps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; @@ -146,7 +147,7 @@ - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(Flutter } NSArray *productIdentifiers = (NSArray *)call.arguments; SKProductsRequest *request = - [self getProductRequestWithIdentifiers:[NSSet setWithArray:productIdentifiers]]; + [self getProductRequestWithIdentifiers:[NSSet setWithArray:productIdentifiers]]; FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; [self.requestHandlers addObject:handler]; __weak typeof(self) weakSelf = self; @@ -161,7 +162,7 @@ - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(Flutter if (!response) { result([FlutterError errorWithCode:@"storekit_platform_no_response" message:@"Failed to get SKProductResponse in startRequest " - @"call. Error occured on iOS platform" + @"call. Error occured on iOS platform" details:call.arguments]); return; } @@ -187,12 +188,12 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { SKProduct *product = [self getProduct:productID]; if (!product) { result([FlutterError - errorWithCode:@"storekit_invalid_payment_object" - message: - @"You have requested a payment for an invalid product. Either the " - @"`productIdentifier` of the payment is not valid or the product has not been " - @"fetched before adding the payment to the payment queue." - details:call.arguments]); + errorWithCode:@"storekit_invalid_payment_object" + message: + @"You have requested a payment for an invalid product. Either the " + @"`productIdentifier` of the payment is not valid or the product has not been " + @"fetched before adding the payment to the payment queue." + details:call.arguments]); return; } SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; @@ -202,18 +203,18 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { if (@available(iOS 8.3, *)) { NSNumber *simulatesAskToBuyInSandbox = [paymentMap objectForKey:@"simulatesAskToBuyInSandbox"]; payment.simulatesAskToBuyInSandbox = (id)simulatesAskToBuyInSandbox == (id)[NSNull null] - ? NO - : [simulatesAskToBuyInSandbox boolValue]; + ? NO + : [simulatesAskToBuyInSandbox boolValue]; } - + if (![self.paymentQueueHandler addPayment:payment]) { result([FlutterError - errorWithCode:@"storekit_duplicate_product_object" - message:@"There is a pending transaction for the same product identifier. Please " - @"either wait for it to be finished or finish it manually using " - @"`completePurchase` to avoid edge cases." - - details:call.arguments]); + errorWithCode:@"storekit_duplicate_product_object" + message:@"There is a pending transaction for the same product identifier. Please " + @"either wait for it to be finished or finish it manually using " + @"`completePurchase` to avoid edge cases." + + details:call.arguments]); return; } result(nil); @@ -229,10 +230,10 @@ - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result NSDictionary *paymentMap = (NSDictionary *)call.arguments; NSString *transactionIdentifier = [paymentMap objectForKey:@"transactionIdentifier"]; NSString *productIdentifier = [paymentMap objectForKey:@"productIdentifier"]; - + NSArray *pendingTransactions = - [self.paymentQueueHandler getUnfinishedTransactions]; - + [self.paymentQueueHandler getUnfinishedTransactions]; + for (SKPaymentTransaction *transaction in pendingTransactions) { // If the user cancels the purchase dialog we won't have a transactionIdentifier. // So if it is null AND a transaction in the pendingTransactions list has @@ -251,16 +252,16 @@ - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result } } } - + result(nil); } - (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)result { if (call.arguments && ![call.arguments isKindOfClass:[NSString class]]) { result([FlutterError - errorWithCode:@"storekit_invalid_argument" - message:@"Argument is not nil and the type of finishTransaction is not a string." - details:call.arguments]); + errorWithCode:@"storekit_invalid_argument" + message:@"Argument is not nil and the type of finishTransaction is not a string." + details:call.arguments]); return; } [self.paymentQueueHandler restoreTransactions:call.arguments]; @@ -318,10 +319,12 @@ - (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { - (void)registerPaymentQueueDelegate { if (@available(iOS 13.0, *)) { - _paymentQueueDelegateCallbackChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate" - binaryMessenger:_messenger]; - - _paymentQueueDelegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel: _paymentQueueDelegateCallbackChannel]; + _paymentQueueDelegateCallbackChannel = [FlutterMethodChannel + methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate" + binaryMessenger:_messenger]; + + _paymentQueueDelegate = [[FIAPPaymentQueueDelegate alloc] + initWithMethodChannel:_paymentQueueDelegateCallbackChannel]; _paymentQueueHandler.delegate = _paymentQueueDelegate; } } @@ -353,13 +356,15 @@ - (void)handleTransactionsRemoved:(NSArray *)transaction } - (void)handleTransactionRestoreFailed:(NSError *)error { - [self.transactionObserverCallbackChannel invokeMethod:@"restoreCompletedTransactionsFailed" - arguments:[FIAObjectTranslator getMapFromNSError:error]]; + [self.transactionObserverCallbackChannel + invokeMethod:@"restoreCompletedTransactionsFailed" + arguments:[FIAObjectTranslator getMapFromNSError:error]]; } - (void)restoreCompletedTransactionsFinished { - [self.transactionObserverCallbackChannel invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" - arguments:nil]; + [self.transactionObserverCallbackChannel + invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" + arguments:nil]; } - (void)updatedDownloads:(NSArray *)downloads { @@ -371,11 +376,12 @@ - (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product // have a interception method that deciding if the payment should be processed (implemented by the // programmer). [self.productsCache setObject:product forKey:product.productIdentifier]; - [self.transactionObserverCallbackChannel invokeMethod:@"shouldAddStorePayment" - arguments:@{ - @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], - @"product" : [FIAObjectTranslator getMapFromSKProduct:product] - }]; + [self.transactionObserverCallbackChannel + invokeMethod:@"shouldAddStorePayment" + arguments:@{ + @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], + @"product" : [FIAObjectTranslator getMapFromSKProduct:product] + }]; return NO; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart index f8ab4d48be7e..d045dab448e8 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart @@ -7,3 +7,8 @@ import 'package:flutter/services.dart'; /// Method channel for the plugin's platform<-->Dart calls. const MethodChannel channel = MethodChannel('plugins.flutter.io/in_app_purchase'); + +/// Method channel used to deliver the payment queue delegate system calls to +/// Dart. +const MethodChannel paymentQueueDelegateChannel = + MethodChannel('plugins.flutter.io/in_app_purchase_payment_queue_delegate'); diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart new file mode 100644 index 000000000000..cfd0a1d2c7f8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart @@ -0,0 +1,35 @@ +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; + +/// A wrapper around +/// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc). +/// +/// The payment queue delegate can be implementated to provide information +/// needed to complete transactions. +/// +/// The [SKPaymentQueueDelegateWrapper] is only available on iOS 13 and higher. +/// Using the delegate on older iOS version will be ignored. +abstract class SKPaymentQueueDelegateWrapper { + /// Called by the system to check whether the transaction should continue if + /// the device's App Store storefront has changed during a transaction. + /// + /// - Return `true` if the transaction should continue within the updated + /// storefront (default behaviour). + /// - Return `false` if the transaction should be cancelled. In this case the + /// transaction will fail with the error [SKErrorStoreProductNotAvailable](https://developer.apple.com/documentation/storekit/skerrorcode/skerrorstoreproductnotavailable?language=objc). + /// + /// See the documentation in StoreKit's [`[-SKPaymentQueueDelegate shouldContinueTransaction]`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3242935-paymentqueue?language=objc). + bool shouldContinueTransaction( + SKPaymentTransactionWrapper transaction, + SKStorefrontWrapper storefront, + ) => + true; + + /// Called by the system to check whether to immediately show the price + /// consent form. + /// + /// The default return value is `true`. This will inform the system to display + /// the price consent sheet when the subscription price has been changed in + /// App Store Connect and the subscriber has not yet taken action. See the + /// documentation in StoreKit's [`[-SKPaymentQueueDelegate shouldShowPriceConsent:]`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc). + bool shouldShowPriceConsent() => true; +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index fe5f14ba44a5..7e6ba3c9e469 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -12,6 +12,7 @@ import 'package:meta/meta.dart'; import '../channel.dart'; import '../in_app_purchase_ios_platform.dart'; +import 'sk_payment_queue_delegate_wrapper.dart'; import 'sk_payment_transaction_wrappers.dart'; import 'sk_product_wrapper.dart'; @@ -40,6 +41,7 @@ class SKPaymentQueueWrapper { static final SKPaymentQueueWrapper _singleton = SKPaymentQueueWrapper._(); + SKPaymentQueueDelegateWrapper? _paymentQueueDelegate; SKTransactionObserverWrapper? _observer; /// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc) @@ -70,18 +72,39 @@ class SKPaymentQueueWrapper { /// /// Call this method when the first listener is subscribed to the /// [InAppPurchaseIosPlatform.purchaseStream]. - Future startObservingTransactionQueue() async => - await channel.invokeListMethod( - '-[SKPaymentQueue startObservingTransactionQueue]'); + Future startObservingTransactionQueue() => channel + .invokeMethod('-[SKPaymentQueue startObservingTransactionQueue]'); /// Instructs the iOS implementation to remove the transaction observer and /// stop listening to it. /// /// Call this when there are no longer any listeners subscribed to the /// [InAppPurchaseIosPlatform.purchaseStream]. - Future stopObservingTransactionQueue() async => - await channel.invokeListMethod( - '-[SKPaymentQueue stopObservingTransactionQueue]'); + Future stopObservingTransactionQueue() => channel + .invokeMethod('-[SKPaymentQueue stopObservingTransactionQueue]'); + + /// Sets an implementation of the [SKPaymentQueueDelegateWrapper]. + /// + /// The [SKPaymentQueueDelegateWrapper] can be used to inform iOS how to + /// finish transactions when the storefront changes or if the price consent + /// sheet should be displayed when the price of a subscription has changed. If + /// no delegate is registered iOS will fallback to it's default configuration. + /// See the documentation on StoreKite's [`-[SKPaymentQueue delegate:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc). + /// + /// When set to `null` the payment queue delegate will be removed and the + /// default behaviour will apply (see [documentation](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc)). + Future setDelegate(SKPaymentQueueDelegateWrapper? delegate) async { + if (delegate == null) { + await channel.invokeMethod('-[SKPaymentQueue removeDelegate]'); + paymentQueueDelegateChannel.setMethodCallHandler(null); + } else { + await channel.invokeMethod('-[SKPaymentQueue registerDelegate]'); + paymentQueueDelegateChannel + .setMethodCallHandler(_handlePaymentQueueDelegateCallbacks); + } + + _paymentQueueDelegate = delegate; + } /// Posts a payment to the queue. /// @@ -235,6 +258,24 @@ class SKPaymentQueueWrapper { Map.castFrom(map)); }).toList(); } + + // Triage a method channel call from the platform and triggers the correct observer method. + Future _handlePaymentQueueDelegateCallbacks(MethodCall call) async { + assert(_paymentQueueDelegate != null, + '[in_app_purchase]: (Fatal)The payment queue delegate has not been set but we received a payment queue notification. Please ensure the payment queue has been set using `setDelegate`.'); + + final SKPaymentQueueDelegateWrapper delegate = _paymentQueueDelegate!; + switch (call.method) { + case 'shouldContinueTransaction': + case 'shouldShowPriceConsent': + default: + break; + } + throw PlatformException( + code: 'no_such_callback', + message: + 'Did not recognize the payment queue delegate callback ${call.method}.'); + } } /// Dart wrapper around StoreKit's diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart new file mode 100644 index 000000000000..afd1d22b97e9 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart @@ -0,0 +1,47 @@ +import 'dart:ui' show hashValues; + +import 'package:json_annotation/json_annotation.dart'; + +part 'sk_storefront_wrapper.g.dart'; + +/// Contains the location and unique identifier of an Apple App Store storefront. +/// +/// Dart wrapper around StoreKit's +/// [SKStorefront](https://developer.apple.com/documentation/storekit/skstorefront?language=objc). +@JsonSerializable() +class SKStorefrontWrapper { + /// Creates a new [SKStorefrontWrapper] with the provided information. + SKStorefrontWrapper({ + required this.countryCode, + required this.identifier, + }); + + /// The three-letter code representing the country or region associated with + /// the App Store storefront. + final String countryCode; + + /// A value defined by Apple that uniquely identifies an App Store storefront. + final String identifier; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKStorefrontWrapper typedOther = other as SKStorefrontWrapper; + return typedOther.countryCode == countryCode && + typedOther.identifier == identifier; + } + + @override + int get hashCode => hashValues( + this.countryCode, + this.identifier, + ); + + @override + String toString() => _$SKStorefrontWrapperToJson(this).toString(); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart new file mode 100644 index 000000000000..f75cfc5711e8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_storefront_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SKStorefrontWrapper _$SKStorefrontWrapperFromJson(Map json) { + return SKStorefrontWrapper( + countryCode: json['countryCode'] as String, + identifier: json['identifier'] as String, + ); +} + +Map _$SKStorefrontWrapperToJson( + SKStorefrontWrapper instance) => + { + 'countryCode': instance.countryCode, + 'identifier': instance.identifier, + }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart index b687d238083c..09eb1acb8420 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart @@ -2,8 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart'; export 'src/store_kit_wrappers/sk_payment_queue_wrapper.dart'; export 'src/store_kit_wrappers/sk_payment_transaction_wrappers.dart'; export 'src/store_kit_wrappers/sk_product_wrapper.dart'; export 'src/store_kit_wrappers/sk_receipt_manager.dart'; export 'src/store_kit_wrappers/sk_request_maker.dart'; +export 'src/store_kit_wrappers/sk_storefront_wrapper.dart'; From cac4e73107b8659e31265a17fb6d21569f04a705 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 13:59:47 +0200 Subject: [PATCH 04/13] Added showPriceConsent method to SKPaymentQueueWrapper --- .../sk_payment_queue_wrapper.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 7e6ba3c9e469..5efb76b272e5 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -193,6 +193,19 @@ class SKPaymentQueueWrapper { '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]'); } + /// Shows the price consent sheet if the user has not yet responded to a + /// subscription price change. + /// + /// Use this function when you have registered a [SKPaymentQueueDelegateWrapper] + /// (using the [setDelegate] method) and returned `false` when the + /// `SKPaymentQueueDelegateWrapper.shouldShowPriceConsent()` method was called. + /// + /// See documentation of StoreKit's [`-[SKPaymentQueue showPriceConsentIfNeeded]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3521327-showpriceconsentifneeded?language=objc). + Future showPriceConsentIfNeeded() async { + await channel + .invokeMethod('-[SKPaymentQueue showPriceConsentIfNeeded]'); + } + // Triage a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) async { assert(_observer != null, From 535d67b6cc97ebab4901ddb2bfc9ab9df183d859 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 16:19:49 +0200 Subject: [PATCH 05/13] Forward calls to payment queue delegate --- .../sk_payment_queue_delegate_wrapper.dart | 6 ++-- .../sk_payment_queue_wrapper.dart | 21 ++++++++++--- .../sk_storefront_wrapper.dart | 10 ++++++ .../sk_methodchannel_apis_test.dart | 31 +++++++++++++++++++ 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart index cfd0a1d2c7f8..2586564fc06f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart @@ -18,11 +18,11 @@ abstract class SKPaymentQueueDelegateWrapper { /// transaction will fail with the error [SKErrorStoreProductNotAvailable](https://developer.apple.com/documentation/storekit/skerrorcode/skerrorstoreproductnotavailable?language=objc). /// /// See the documentation in StoreKit's [`[-SKPaymentQueueDelegate shouldContinueTransaction]`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3242935-paymentqueue?language=objc). - bool shouldContinueTransaction( + Future shouldContinueTransaction( SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront, ) => - true; + Future.value(true); /// Called by the system to check whether to immediately show the price /// consent form. @@ -31,5 +31,5 @@ abstract class SKPaymentQueueDelegateWrapper { /// the price consent sheet when the subscription price has been changed in /// App Store Connect and the subscriber has not yet taken action. See the /// documentation in StoreKit's [`[-SKPaymentQueueDelegate shouldShowPriceConsent:]`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc). - bool shouldShowPriceConsent() => true; + Future shouldShowPriceConsent() => Future.value(true); } diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 5efb76b272e5..c39ad9efddd7 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -6,7 +6,9 @@ import 'dart:async'; import 'dart:ui' show hashValues; import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; @@ -100,7 +102,7 @@ class SKPaymentQueueWrapper { } else { await channel.invokeMethod('-[SKPaymentQueue registerDelegate]'); paymentQueueDelegateChannel - .setMethodCallHandler(_handlePaymentQueueDelegateCallbacks); + .setMethodCallHandler(handlePaymentQueueDelegateCallbacks); } _paymentQueueDelegate = delegate; @@ -207,7 +209,7 @@ class SKPaymentQueueWrapper { } // Triage a method channel call from the platform and triggers the correct observer method. - Future _handleObserverCallbacks(MethodCall call) async { + Future _handleObserverCallbacks(MethodCall call) async { assert(_observer != null, '[in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. Make sure the observer is added right at the App Launch.'); final SKTransactionObserverWrapper observer = _observer!; @@ -272,15 +274,26 @@ class SKPaymentQueueWrapper { }).toList(); } - // Triage a method channel call from the platform and triggers the correct observer method. - Future _handlePaymentQueueDelegateCallbacks(MethodCall call) async { + /// Triage a method channel call from the platform and triggers the correct + /// payment queue delegate method. + /// + /// This method is public for testing purposes only and should not be used + /// outside this class. + @visibleForTesting + Future handlePaymentQueueDelegateCallbacks(MethodCall call) async { assert(_paymentQueueDelegate != null, '[in_app_purchase]: (Fatal)The payment queue delegate has not been set but we received a payment queue notification. Please ensure the payment queue has been set using `setDelegate`.'); final SKPaymentQueueDelegateWrapper delegate = _paymentQueueDelegate!; switch (call.method) { case 'shouldContinueTransaction': + final SKPaymentTransactionWrapper transaction = + SKPaymentTransactionWrapper.fromJson(call.arguments['transaction']); + final SKStorefrontWrapper storefront = + SKStorefrontWrapper.fromJson(call.arguments['storefront']); + return delegate.shouldContinueTransaction(transaction, storefront); case 'shouldShowPriceConsent': + return delegate.shouldShowPriceConsent(); default: break; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart index afd1d22b97e9..3e2b3957ea6f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart @@ -16,6 +16,16 @@ class SKStorefrontWrapper { required this.identifier, }); + /// Constructs an instance of the [SKStorefrontWrapper] from a key value map + /// of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. The `map` parameter must not be + /// null. + factory SKStorefrontWrapper.fromJson(Map map) { + return _$SKStorefrontWrapperFromJson(map); + } + /// The three-letter code representing the country or region associated with /// the App Store storefront. final String countryCode; diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index edb50aeb62a0..6a01fe4caecb 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -145,6 +145,20 @@ void main() { await SKPaymentQueueWrapper().stopObservingTransactionQueue(); expect(fakeIOSPlatform.queueIsActive, false); }); + + test('setDelegate should call methodChannel', () async { + expect(fakeIOSPlatform.isPaymentQueueDelegateRegistered, false); + await SKPaymentQueueWrapper().setDelegate(TestPaymentQueueDelegate()); + expect(fakeIOSPlatform.isPaymentQueueDelegateRegistered, true); + await SKPaymentQueueWrapper().setDelegate(null); + expect(fakeIOSPlatform.isPaymentQueueDelegateRegistered, false); + }); + + test('showPriceConsentIfNeeded should call methodChannel', () async { + expect(fakeIOSPlatform.showPriceConsentIfNeeded, false); + await SKPaymentQueueWrapper().showPriceConsentIfNeeded(); + expect(fakeIOSPlatform.showPriceConsentIfNeeded, true); + }); }); group('Code Redemption Sheet', () { @@ -178,6 +192,12 @@ class FakeIOSPlatform { // present Code Redemption bool presentCodeRedemption = false; + // show price consent sheet + bool showPriceConsentIfNeeded = false; + + // indicate if the payment queue delegate is registered + bool isPaymentQueueDelegateRegistered = false; + // Listen to purchase updates bool? queueIsActive; @@ -230,11 +250,22 @@ class FakeIOSPlatform { case '-[SKPaymentQueue stopObservingTransactionQueue]': queueIsActive = false; return Future.sync(() {}); + case '-[SKPaymentQueue registerDelegate]': + isPaymentQueueDelegateRegistered = true; + return Future.sync(() {}); + case '-[SKPaymentQueue removeDelegate]': + isPaymentQueueDelegateRegistered = false; + return Future.sync(() {}); + case '-[SKPaymentQueue showPriceConsentIfNeeded]': + showPriceConsentIfNeeded = true; + return Future.sync(() {}); } return Future.error('method not mocked'); } } +class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper {} + class TestPaymentTransactionObserver extends SKTransactionObserverWrapper { void updatedTransactions( {required List transactions}) {} From d8fe8efd2cf217c6cf4c8d1cc4d3bb6600924025 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 17:03:22 +0200 Subject: [PATCH 06/13] Added missing unit tests --- .../sk_payment_queue_delegate_wrapper.dart | 6 +- .../sk_storefront_wrapper.dart | 4 + .../sk_payment_queue_delegate_api_test.dart | 105 ++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart index 2586564fc06f..cfd0a1d2c7f8 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart @@ -18,11 +18,11 @@ abstract class SKPaymentQueueDelegateWrapper { /// transaction will fail with the error [SKErrorStoreProductNotAvailable](https://developer.apple.com/documentation/storekit/skerrorcode/skerrorstoreproductnotavailable?language=objc). /// /// See the documentation in StoreKit's [`[-SKPaymentQueueDelegate shouldContinueTransaction]`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3242935-paymentqueue?language=objc). - Future shouldContinueTransaction( + bool shouldContinueTransaction( SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront, ) => - Future.value(true); + true; /// Called by the system to check whether to immediately show the price /// consent form. @@ -31,5 +31,5 @@ abstract class SKPaymentQueueDelegateWrapper { /// the price consent sheet when the subscription price has been changed in /// App Store Connect and the subscriber has not yet taken action. See the /// documentation in StoreKit's [`[-SKPaymentQueueDelegate shouldShowPriceConsent:]`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc). - Future shouldShowPriceConsent() => Future.value(true); + bool shouldShowPriceConsent() => true; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart index 3e2b3957ea6f..d2297925199f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart @@ -54,4 +54,8 @@ class SKStorefrontWrapper { @override String toString() => _$SKStorefrontWrapperToJson(this).toString(); + + /// Converts the instance to a key value map which can be used to serialize + /// to JSON format. + Map toMap() => _$SKStorefrontWrapperToJson(this); } diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart new file mode 100644 index 000000000000..3244c9403915 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_ios/src/channel.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); + + setUpAll(() { + SystemChannels.platform + .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); + }); + + test( + 'handlePaymentQueueDelegateCallbacks should call SKPaymentQueueDelegateWrapper.shouldContinueTransaction', + () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestPaymentQueueDelegate testDelegate = TestPaymentQueueDelegate(); + await queue.setDelegate(testDelegate); + + final Map arguments = { + 'storefront': { + 'countryCode': 'USA', + 'identifier': 'unique_identifier', + }, + 'transaction': { + 'payment': { + 'productIdentifier': 'product_identifier', + } + }, + }; + + final result = await queue.handlePaymentQueueDelegateCallbacks( + MethodCall('shouldContinueTransaction', arguments), + ); + + expect(result, false); + expect( + testDelegate.log, + { + equals('shouldContinueTransaction'), + }, + ); + }); + + test( + 'handlePaymentQueueDelegateCallbacks should call SKPaymentQueueDelegateWrapper.shouldShowPriceConsent', + () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestPaymentQueueDelegate testDelegate = TestPaymentQueueDelegate(); + await queue.setDelegate(testDelegate); + + final result = await queue.handlePaymentQueueDelegateCallbacks( + MethodCall('shouldShowPriceConsent'), + ); + + expect(result, false); + expect( + testDelegate.log, + { + equals('shouldShowPriceConsent'), + }, + ); + }); +} + +class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper { + final List log = []; + + @override + bool shouldContinueTransaction( + SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) { + log.add('shouldContinueTransaction'); + return false; + } + + @override + bool shouldShowPriceConsent() { + log.add('shouldShowPriceConsent'); + return false; + } +} + +class FakeIOSPlatform { + FakeIOSPlatform() { + channel.setMockMethodCallHandler(onMethodCall); + } + + // indicate if the payment queue delegate is registered + bool isPaymentQueueDelegateRegistered = false; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case '-[SKPaymentQueue registerDelegate]': + isPaymentQueueDelegateRegistered = true; + return Future.sync(() {}); + case '-[SKPaymentQueue removeDelegate]': + isPaymentQueueDelegateRegistered = false; + return Future.sync(() {}); + } + return Future.error('method not mocked'); + } +} From 0fdde1228cee347ab74f7a509ff12d350d4ef927 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 17:13:25 +0200 Subject: [PATCH 07/13] update version number --- packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md | 8 ++++++-- .../sk_payment_queue_delegate_wrapper.dart | 4 ++++ .../lib/src/store_kit_wrappers/sk_storefront_wrapper.dart | 4 ++++ packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml | 2 +- .../sk_payment_queue_delegate_api_test.dart | 4 ++++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 4b2d8ce1dc24..c4c4eb05cecd 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,7 +1,11 @@ +## 0.1.1 + +* Added support to register a `SKPaymentQueueDelegateWrapper` and handle changes to active subscriptions accordingly (see also Store Kit's [SKPaymentQueueDelegate](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc)). + ## 0.1.0+2 -* Changed the iOS payment queue handler in such a way that it only adds a listener to the SKPaymentQueue when there - is a listener to the Dart purchaseStream. +* Changed the iOS payment queue handler in such a way that it only adds a listener to the `SKPaymentQueue` when there + is a listener to the Dart `purchaseStream`. ## 0.1.0+1 diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart index cfd0a1d2c7f8..2759a296389b 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart @@ -1,3 +1,7 @@ +// 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 'package:in_app_purchase_ios/store_kit_wrappers.dart'; /// A wrapper around diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart index d2297925199f..934fdea355e3 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart @@ -1,3 +1,7 @@ +// 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:ui' show hashValues; import 'package:json_annotation/json_annotation.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 5b9e3892d40d..00929d9c024b 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.0+2 +version: 0.1.1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart index 3244c9403915..b61411dfffa4 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart @@ -1,3 +1,7 @@ +// 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 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_ios/src/channel.dart'; From b046748a89e7423cfb0bdff95874e71ad4f88ba0 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 17:40:09 +0200 Subject: [PATCH 08/13] Make sure result is called --- .../ios/Classes/InAppPurchasePlugin.m | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m index bdc4b1fc15e4..c0db38e5cfe0 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m @@ -108,17 +108,15 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([@"-[InAppPurchasePlugin refreshReceipt:result:]" isEqualToString:call.method]) { [self refreshReceipt:call result:result]; } else if ([@"-[SKPaymentQueue startObservingTransactionQueue]" isEqualToString:call.method]) { - [_paymentQueueHandler startObservingPaymentQueue]; + [self startObservingPaymentQueue:result]; } else if ([@"-[SKPaymentQueue stopObservingTransactionQueue]" isEqualToString:call.method]) { - [_paymentQueueHandler stopObservingPaymentQueue]; + [self stopObservingPaymentQueue:result]; } else if ([@"-[SKPaymentQueue registerDelegate]" isEqualToString:call.method]) { - [self registerPaymentQueueDelegate]; + [self registerPaymentQueueDelegate:result]; } else if ([@"-[SKPaymentQueue removeDelegate]" isEqualToString:call.method]) { - [self removePaymentQueueDelegate]; + [self removePaymentQueueDelegate:result]; } else if ([@"-[SKPaymentQueue showPriceConsentIfNeeded]" isEqualToString:call.method]) { - if (@available(iOS 13.4, *)) { - [_paymentQueueHandler showPriceConsentIfNeeded]; - } + [self showPriceConsentIfNeeded:result]; } else { result(FlutterMethodNotImplemented); } @@ -317,7 +315,17 @@ - (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { }]; } -- (void)registerPaymentQueueDelegate { +- (void)startObservingPaymentQueue:(FlutterResult)result { + [_paymentQueueHandler startObservingPaymentQueue]; + result(nil); +} + +- (void)stopObservingPaymentQueue:(FlutterResult)result { + [_paymentQueueHandler stopObservingPaymentQueue]; + result(nil); +} + +- (void)registerPaymentQueueDelegate:(FlutterResult)result { if (@available(iOS 13.0, *)) { _paymentQueueDelegateCallbackChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate" @@ -327,14 +335,23 @@ - (void)registerPaymentQueueDelegate { initWithMethodChannel:_paymentQueueDelegateCallbackChannel]; _paymentQueueHandler.delegate = _paymentQueueDelegate; } + result(nil); } -- (void)removePaymentQueueDelegate { +- (void)removePaymentQueueDelegate:(FlutterResult)result { if (@available(iOS 13.0, *)) { _paymentQueueHandler.delegate = nil; } _paymentQueueDelegate = nil; _paymentQueueDelegateCallbackChannel = nil; + result(nil); +} + +- (void)showPriceConsentIfNeeded:(FlutterResult)result { + if (@available(iOS 13.4, *)) { + [_paymentQueueHandler showPriceConsentIfNeeded]; + } + result(nil); } #pragma mark - transaction observer: From 066ef0875585d749023e1e7089f780bd363ce102 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 17:47:24 +0200 Subject: [PATCH 09/13] Add API_AVAILABLE attribute to SKStorefrontStub --- .../in_app_purchase_ios/example/ios/RunnerTests/Stubs.h | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h index 7f72325ee9a8..7b6842da4c77 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h @@ -60,6 +60,7 @@ API_AVAILABLE(ios(11.2), macos(10.13.2)) - (instancetype)initWithFailureError:(NSError *)error; @end +API_AVAILABLE(ios(13.0), macos(10.15)) @interface SKStorefrontStub : SKStorefront - (instancetype)initWithMap:(NSDictionary *)map; @end From 933ac2b81c7089182dd4f1a054b1b7221d9a14e4 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 17:54:51 +0200 Subject: [PATCH 10/13] Add missing documentation --- .../ios/Classes/FIAObjectTranslator.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h index 09b2ef3b4f63..95a5edc245dc 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h @@ -9,29 +9,40 @@ NS_ASSUME_NONNULL_BEGIN @interface FIAObjectTranslator : NSObject +// Converts an instance of SKProduct into a dictionary. + (NSDictionary *)getMapFromSKProduct:(SKProduct *)product; +// Converts an instance of SKProductSubscriptionPeriod into a dictionary. + (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period API_AVAILABLE(ios(11.2)); +// Converts an instance of SKProductDiscount into a dictionary. + (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount API_AVAILABLE(ios(11.2)); +// Converts an instance of SKProductsResponse into a dictionary. + (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse; +// Converts an instance of SKPayment into a dictionary. + (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment; +// Converts an instance of NSLocale into a dictionary. + (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale; +// Creates an instance of the SKMutablePayment class based on the supplied dictionary. + (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map; +// Converts an instance of SKPaymentTransaction into a dictionary. + (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction; +// Converts an instance of NSError into a dictionary. + (NSDictionary *)getMapFromNSError:(NSError *)error; +// Converts an instance of SKStorefront into a dictionary. + (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); +// Converts the supplied instances of SKStorefront and SKPaymentTransaction into a dictionary. + (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront andSKPaymentTransaction:(SKPaymentTransaction *)transaction API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); From 4f558d04f6105573ae1285d9be02f4d94c02c3e1 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 23 Jun 2021 19:21:24 +0200 Subject: [PATCH 11/13] Add API_AVAILABLE attribute to FIAPPaymentDelegateTests --- .../example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m index 58fe01da444e..810e1fafe11a 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m @@ -10,6 +10,7 @@ @import in_app_purchase_ios; +API_AVAILABLE(ios(13.0)) @interface FIAPPaymentQueueDelegateTests : XCTestCase @property(strong, nonatomic) FlutterMethodChannel *channel; From 24a33b061e99b5829af262d647fef9200b58237e Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 25 Jun 2021 09:43:17 +0200 Subject: [PATCH 12/13] Added showPriceConsent feature to example app --- .../ios/Runner.xcodeproj/project.pbxproj | 9 ++++--- .../example/ios/Runner/Configuration.storekit | 10 +++++--- .../lib/example_payment_queue_delegate.dart | 19 +++++++++++++++ .../in_app_purchase_ios/example/lib/main.dart | 14 ++++++++++- ...in_app_purchase_ios_platform_addition.dart | 24 +++++++++++++++++++ 5 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj index bff90da1b610..61a5da696986 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -259,7 +259,6 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 7624MWN53C; SystemCapabilities = { com.apple.InAppPurchase = { enabled = 1; @@ -554,7 +553,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 7624MWN53C; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -566,7 +565,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.inAppPurchaseExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.inAppPurchaseExample; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; @@ -578,7 +577,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 7624MWN53C; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -590,7 +589,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.inAppPurchaseExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.inAppPurchaseExample; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit index 4958a846e67d..b98fefb68a95 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit @@ -1,4 +1,8 @@ { + "identifier" : "6073E9A3", + "nonRenewingSubscriptions" : [ + + ], "products" : [ { "displayPrice" : "0.99", @@ -46,7 +50,7 @@ "adHocOffers" : [ ], - "displayPrice" : "3.99", + "displayPrice" : "4.99", "familyShareable" : false, "groupNumber" : 1, "internalID" : "922EB597", @@ -59,7 +63,7 @@ } ], "productID" : "subscription_silver", - "recurringSubscriptionPeriod" : "P1M", + "recurringSubscriptionPeriod" : "P1W", "referenceName" : "subscription_silver", "subscriptionGroupID" : "D0FEE8D8", "type" : "RecurringSubscription" @@ -91,6 +95,6 @@ ], "version" : { "major" : 1, - "minor" : 0 + "minor" : 1 } } diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart b/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart new file mode 100644 index 000000000000..73bdadc37bee --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart @@ -0,0 +1,19 @@ +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; + +/// 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; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart index 5452f5a0ee83..19884745bce8 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_ios_example/example_payment_queue_delegate.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'consumable_store.dart'; @@ -40,6 +41,9 @@ class _MyApp extends StatefulWidget { class _MyAppState extends State<_MyApp> { final InAppPurchaseIosPlatform _iapIosPlatform = InAppPurchasePlatform.instance as InAppPurchaseIosPlatform; + final InAppPurchaseIosPlatformAddition _iapIosPlatformAddition = + InAppPurchasePlatformAddition.instance + as InAppPurchaseIosPlatformAddition; late StreamSubscription> _subscription; List _notFoundIds = []; List _products = []; @@ -61,6 +65,10 @@ class _MyAppState extends State<_MyApp> { }, onError: (error) { // handle error here. }); + + // Register the example payment queue delegate + _iapIosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + initStoreInfo(); super.initState(); } @@ -241,7 +249,11 @@ class _MyAppState extends State<_MyApp> { productDetails.description, ), trailing: previousPurchase != null - ? Icon(Icons.check) + ? IconButton( + onPressed: () { + _iapIosPlatformAddition.showPriceConsentIfNeeded(); + }, + icon: Icon(Icons.upgrade)) : TextButton( child: Text(productDetails.price), style: TextButton.styleFrom( diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart index 0c7b2de860b6..bcc4ddf48200 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart @@ -30,4 +30,28 @@ class InAppPurchaseIosPlatformAddition extends InAppPurchasePlatformAddition { serverVerificationData: receipt, source: kIAPSource); } + + /// Sets an implementation of the [SKPaymentQueueDelegateWrapper]. + /// + /// The [SKPaymentQueueDelegateWrapper] can be used to inform iOS how to + /// finish transactions when the storefront changes or if the price consent + /// sheet should be displayed when the price of a subscription has changed. If + /// no delegate is registered iOS will fallback to it's default configuration. + /// See the documentation on StoreKite's [`-[SKPaymentQueue delegate:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc). + /// + /// When set to `null` the payment queue delegate will be removed and the + /// default behaviour will apply (see [documentation](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc)). + Future setDelegate(SKPaymentQueueDelegateWrapper? delegate) => + SKPaymentQueueWrapper().setDelegate(delegate); + + /// Shows the price consent sheet if the user has not yet responded to a + /// subscription price change. + /// + /// Use this function when you have registered a [SKPaymentQueueDelegateWrapper] + /// (using the [setDelegate] method) and returned `false` when the + /// `SKPaymentQueueDelegateWrapper.shouldShowPriceConsent()` method was called. + /// + /// See documentation of StoreKit's [`-[SKPaymentQueue showPriceConsentIfNeeded]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3521327-showpriceconsentifneeded?language=objc). + Future showPriceConsentIfNeeded() => + SKPaymentQueueWrapper().showPriceConsentIfNeeded(); } From 3b3699022f784bc1b2806e1a92454a13d245b434 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 25 Jun 2021 09:45:50 +0200 Subject: [PATCH 13/13] Added missing license info --- .../example/lib/example_payment_queue_delegate.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart b/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart index 73bdadc37bee..dfebdf9cdf98 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart @@ -1,3 +1,7 @@ +// 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 'package:in_app_purchase_ios/store_kit_wrappers.dart'; /// Example implementation of the