[in_app_purchase] Add support for SKPaymentQueueDelegate and showPriceConsentIfNeeded (#4085)
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 4b2d8ce..c4c4eb0 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/example/ios/Podfile b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile
index ae87502..5200b9f 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 @@
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 590b07f..61a5da6 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 = "<group>"; };
+ 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 = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; };
+ 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 = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
- 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 = "<group>"; };
6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ProductRequestHandlerTests.m; sourceTree = "<group>"; };
6896B34A21EEB4B800D37AEF /* Stubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Stubs.h; sourceTree = "<group>"; };
@@ -71,11 +71,13 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 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 = "<group>"; };
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 = "<group>"; };
A59001A821E69658004A3E5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
+ F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIAPPaymentQueueDeleteTests.m; sourceTree = "<group>"; };
F6E5D5F926131C4800C68BED /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = "<group>"; };
F78AF3132342BC89008449C7 /* PaymentQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PaymentQueueTests.m; sourceTree = "<group>"; };
/* 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 = "<group>";
@@ -187,6 +189,7 @@
6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */,
F78AF3132342BC89008449C7 /* PaymentQueueTests.m */,
688DE35021F2A5A100EA2684 /* TranslatorTests.m */,
+ F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */,
);
path = RunnerTests;
sourceTree = "<group>";
@@ -196,7 +199,7 @@
children = (
A5279297219369C600FF69E6 /* StoreKit.framework */,
1630769A874F9381BC761FE1 /* libPods-Runner.a */,
- 630DD71BB3F145A22B1DE15D /* libPods-RunnerTests.a */,
+ 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -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 */,
@@ -310,7 +313,21 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
- 321E2F5767F55B0A360AA77E /* [CP] Check Pods Manifest.lock */ = {
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 95C7A5986B77A8DF76F6DF3A /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -332,20 +349,6 @@
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;
};
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- );
- name = "Thin Binary";
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
- };
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -400,6 +403,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 */,
@@ -593,7 +597,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 +620,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/Runner/Configuration.storekit b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit
index 4958a84..b98fefb 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/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m
new file mode 100644
index 0000000..810e1fa
--- /dev/null
+++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m
@@ -0,0 +1,120 @@
+// 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 <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+#import "FIAObjectTranslator.h"
+#import "FIAPaymentQueueHandler.h"
+#import "Stubs.h"
+
+@import in_app_purchase_ios;
+
+API_AVAILABLE(ios(13.0))
+@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 241ea0d..045abcd 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,81 @@
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];
+
+ // 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
+}
+
@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 687118f..7b6842d 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,9 @@
- (instancetype)initWithFailureError:(NSError *)error;
@end
+API_AVAILABLE(ios(13.0), macos(10.15))
+@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 8af326a..a57831c 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 @@
}
@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 385d291..42c51b8 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 @@
@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
@@ -84,6 +86,15 @@
@"key" : @"value",
}
};
+ self.storefrontMap = @{
+ @"countryCode" : @"USA",
+ @"identifier" : @"unique_identifier",
+ };
+
+ self.storefrontAndPaymentTransactionMap = @{
+ @"storefront" : self.storefrontMap,
+ @"transaction" : self.transactionMap,
+ };
}
- (void)testSKProductSubscriptionPeriodStubToMap {
@@ -144,4 +155,23 @@
}
}
+- (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/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 0000000..dfebdf9
--- /dev/null
+++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart
@@ -0,0 +1,23 @@
+// 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
+/// [`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 5452f5a..1988474 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: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 _MyAppState extends State<_MyApp> {
final InAppPurchaseIosPlatform _iapIosPlatform =
InAppPurchasePlatform.instance as InAppPurchaseIosPlatform;
+ final InAppPurchaseIosPlatformAddition _iapIosPlatformAddition =
+ InAppPurchasePlatformAddition.instance
+ as InAppPurchaseIosPlatformAddition;
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<String> _notFoundIds = [];
List<ProductDetails> _products = [];
@@ -61,6 +65,10 @@
}, onError: (error) {
// handle error here.
});
+
+ // Register the example payment queue delegate
+ _iapIosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
+
initStoreInfo();
super.initState();
}
@@ -241,7 +249,11 @@
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/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h
index 2d0187e..95a5edc 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,26 +9,44 @@
@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));
+
@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 5d6e0a2..30b0b81 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 @@
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 0000000..a6c91fa
--- /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 <Flutter/Flutter.h>
+#import <Foundation/Foundation.h>
+#import <StoreKit/StoreKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+API_AVAILABLE(ios(13))
+@interface FIAPPaymentQueueDelegate : NSObject <SKPaymentQueueDelegate>
+- (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 0000000..1056086
--- /dev/null
+++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m
@@ -0,0 +1,78 @@
+// 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 30865b2..8019831 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,9 @@
@interface FIAPaymentQueueHandler : NSObject <SKPaymentTransactionObserver>
+@property(NS_NONATOMIC_IOSONLY, weak, nullable) id<SKPaymentQueueDelegate> 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 +46,15 @@
// @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 20ccbc5..2166795 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 @@
_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)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 8a998d9..c0db38e 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,6 +5,7 @@
#import "InAppPurchasePlugin.h"
#import <StoreKit/StoreKit.h>
#import "FIAObjectTranslator.h"
+#import "FIAPPaymentQueueDelegate.h"
#import "FIAPReceiptManager.h"
#import "FIAPRequestHandler.h"
#import "FIAPaymentQueueHandler.h"
@@ -19,13 +20,19 @@
// 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<FlutterTextureRegistry> *registry;
@property(strong, nonatomic, readonly) NSObject<FlutterBinaryMessenger> *messenger;
@property(strong, nonatomic, readonly) NSObject<FlutterPluginRegistrar> *registrar;
@property(strong, nonatomic, readonly) FIAPReceiptManager *receiptManager;
+@property(strong, nonatomic, readonly)
+ FIAPPaymentQueueDelegate *paymentQueueDelegate API_AVAILABLE(ios(13));
@end
@@ -73,7 +80,8 @@
updatedDownloads:^void(NSArray<SKDownload *> *_Nonnull downloads) {
[weakSelf updatedDownloads:downloads];
}];
- _callbackChannel =
+
+ _transactionObserverCallbackChannel =
[FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase"
binaryMessenger:[registrar messenger]];
return self;
@@ -100,9 +108,15 @@
} 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:result];
+ } else if ([@"-[SKPaymentQueue removeDelegate]" isEqualToString:call.method]) {
+ [self removePaymentQueueDelegate:result];
+ } else if ([@"-[SKPaymentQueue showPriceConsentIfNeeded]" isEqualToString:call.method]) {
+ [self showPriceConsentIfNeeded:result];
} else {
result(FlutterMethodNotImplemented);
}
@@ -301,14 +315,53 @@
}];
}
-#pragma mark - delegates:
+- (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"
+ binaryMessenger:_messenger];
+
+ _paymentQueueDelegate = [[FIAPPaymentQueueDelegate alloc]
+ initWithMethodChannel:_paymentQueueDelegateCallbackChannel];
+ _paymentQueueHandler.delegate = _paymentQueueDelegate;
+ }
+ result(nil);
+}
+
+- (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:
- (void)handleTransactionsUpdated:(NSArray<SKPaymentTransaction *> *)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<SKPaymentTransaction *> *)transactions {
@@ -316,17 +369,19 @@
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"
- arguments:[FIAObjectTranslator getMapFromNSError:error]];
+ [self.transactionObserverCallbackChannel
+ invokeMethod:@"restoreCompletedTransactionsFailed"
+ arguments:[FIAObjectTranslator getMapFromNSError:error]];
}
- (void)restoreCompletedTransactionsFinished {
- [self.callbackChannel invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished"
- arguments:nil];
+ [self.transactionObserverCallbackChannel
+ invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished"
+ arguments:nil];
}
- (void)updatedDownloads:(NSArray<SKDownload *> *)downloads {
@@ -338,11 +393,12 @@
// 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"
- 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 f8ab4d4..d045dab 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 @@
/// 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/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 0c7b2de..bcc4ddf 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 @@
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();
}
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 0000000..2759a29
--- /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,39 @@
+// 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
+/// [`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 fe5f14b..c39ad9e 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,12 +6,15 @@
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';
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 +43,7 @@
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 +74,39 @@
///
/// Call this method when the first listener is subscribed to the
/// [InAppPurchaseIosPlatform.purchaseStream].
- Future startObservingTransactionQueue() async =>
- await channel.invokeListMethod<void>(
- '-[SKPaymentQueue startObservingTransactionQueue]');
+ Future startObservingTransactionQueue() => channel
+ .invokeMethod<void>('-[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<void>(
- '-[SKPaymentQueue stopObservingTransactionQueue]');
+ Future stopObservingTransactionQueue() => channel
+ .invokeMethod<void>('-[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<void>('-[SKPaymentQueue removeDelegate]');
+ paymentQueueDelegateChannel.setMethodCallHandler(null);
+ } else {
+ await channel.invokeMethod<void>('-[SKPaymentQueue registerDelegate]');
+ paymentQueueDelegateChannel
+ .setMethodCallHandler(handlePaymentQueueDelegateCallbacks);
+ }
+
+ _paymentQueueDelegate = delegate;
+ }
/// Posts a payment to the queue.
///
@@ -170,8 +195,21 @@
'-[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<void> showPriceConsentIfNeeded() async {
+ await channel
+ .invokeMethod<void>('-[SKPaymentQueue showPriceConsentIfNeeded]');
+ }
+
// Triage a method channel call from the platform and triggers the correct observer method.
- Future<void> _handleObserverCallbacks(MethodCall call) async {
+ Future<dynamic> _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!;
@@ -235,6 +273,35 @@
Map.castFrom<dynamic, dynamic, String, dynamic>(map));
}).toList();
}
+
+ /// 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<dynamic> 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;
+ }
+ 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 0000000..934fdea
--- /dev/null
+++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart
@@ -0,0 +1,65 @@
+// 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';
+
+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,
+ });
+
+ /// 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<String, dynamic> map) {
+ return _$SKStorefrontWrapperFromJson(map);
+ }
+
+ /// 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();
+
+ /// Converts the instance to a key value map which can be used to serialize
+ /// to JSON format.
+ Map<String, dynamic> toMap() => _$SKStorefrontWrapperToJson(this);
+}
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 0000000..f75cfc5
--- /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<String, dynamic> _$SKStorefrontWrapperToJson(
+ SKStorefrontWrapper instance) =>
+ <String, dynamic>{
+ '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 b687d23..09eb1ac 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';
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 5b9e389..00929d9 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 @@
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_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart
index edb50ae..6a01fe4 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 @@
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 @@
// 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 @@
case '-[SKPaymentQueue stopObservingTransactionQueue]':
queueIsActive = false;
return Future<void>.sync(() {});
+ case '-[SKPaymentQueue registerDelegate]':
+ isPaymentQueueDelegateRegistered = true;
+ return Future<void>.sync(() {});
+ case '-[SKPaymentQueue removeDelegate]':
+ isPaymentQueueDelegateRegistered = false;
+ return Future<void>.sync(() {});
+ case '-[SKPaymentQueue showPriceConsentIfNeeded]':
+ showPriceConsentIfNeeded = true;
+ return Future<void>.sync(() {});
}
return Future.error('method not mocked');
}
}
+class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper {}
+
class TestPaymentTransactionObserver extends SKTransactionObserverWrapper {
void updatedTransactions(
{required List<SKPaymentTransactionWrapper> transactions}) {}
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 0000000..b61411d
--- /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,109 @@
+// 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';
+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<String, dynamic> arguments = <String, dynamic>{
+ 'storefront': <String, String>{
+ 'countryCode': 'USA',
+ 'identifier': 'unique_identifier',
+ },
+ 'transaction': <String, dynamic>{
+ 'payment': <String, dynamic>{
+ 'productIdentifier': 'product_identifier',
+ }
+ },
+ };
+
+ final result = await queue.handlePaymentQueueDelegateCallbacks(
+ MethodCall('shouldContinueTransaction', arguments),
+ );
+
+ expect(result, false);
+ expect(
+ testDelegate.log,
+ <Matcher>{
+ 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,
+ <Matcher>{
+ equals('shouldShowPriceConsent'),
+ },
+ );
+ });
+}
+
+class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper {
+ final List<String> log = <String>[];
+
+ @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<dynamic> onMethodCall(MethodCall call) {
+ switch (call.method) {
+ case '-[SKPaymentQueue registerDelegate]':
+ isPaymentQueueDelegateRegistered = true;
+ return Future<void>.sync(() {});
+ case '-[SKPaymentQueue removeDelegate]':
+ isPaymentQueueDelegateRegistered = false;
+ return Future<void>.sync(() {});
+ }
+ return Future.error('method not mocked');
+ }
+}