diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md index 18072ee2a3ee..785e6472eaa1 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.17+1 + +* Converts main plugin tests to swift. + ## 0.3.17 * Removes OCMock from tests. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.swift index 4ebd88281842..dad613571388 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.swift +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.swift @@ -22,15 +22,10 @@ public class InAppPurchasePlugin: NSObject, FlutterPlugin, InAppPurchaseAPI { // TODO(louisehsu): Change it back to a set when removing obj-c dependancies from this file via type erasure private var requestHandlers = NSHashTable() private var handlerFactory: ((SKRequest) -> FLTRequestHandlerProtocol) - // TODO(louisehsu): Once tests are migrated to swift, we can use @testable import, and make theses vars private again and remove all instances of @objc - @objc + private var transactionObserverCallbackChannel: FLTMethodChannelProtocol? public var registrar: FlutterPluginRegistrar? // This property is optional, as it requires self to exist to be initialized. - @objc public var paymentQueueHandler: FLTPaymentQueueHandlerProtocol? - // This property is optional, as it needs to be set during plugin registration, and can't be directly initialized. - @objc - public var transactionObserverCallbackChannel: FLTMethodChannelProtocol? public static func register(with registrar: FlutterPluginRegistrar) { #if os(iOS) @@ -48,16 +43,17 @@ public class InAppPurchasePlugin: NSObject, FlutterPlugin, InAppPurchaseAPI { SetUpInAppPurchaseAPI(messenger, instance) } - @objc // This init is used for tests public init( receiptManager: FIAPReceiptManager, handlerFactory: @escaping (SKRequest) -> FLTRequestHandlerProtocol = { DefaultRequestHandler(requestHandler: FIAPRequestHandler(request: $0)) - } + }, + transactionCallbackChannel: FLTMethodChannelProtocol? = nil ) { self.receiptManager = receiptManager self.handlerFactory = handlerFactory + self.transactionObserverCallbackChannel = transactionCallbackChannel super.init() } @@ -93,11 +89,7 @@ public class InAppPurchasePlugin: NSObject, FlutterPlugin, InAppPurchaseAPI { #if os(macOS) let messenger = registrar.messenger #endif - transactionObserverCallbackChannel = DefaultMethodChannel( - channel: FlutterMethodChannel( - name: "plugins.flutter.io/in_app_purchase", - binaryMessenger: messenger) - ) + setupTransactionObserverChannelIfNeeded(withMessenger: messenger) } // MARK: - Pigeon Functions @@ -359,7 +351,6 @@ public class InAppPurchasePlugin: NSObject, FlutterPlugin, InAppPurchaseAPI { #endif } - @objc public func handleTransactionsUpdated(_ transactions: [SKPaymentTransaction]) { let translatedTransactions = transactions.map { FIAObjectTranslator.getMapFrom($0) @@ -368,7 +359,6 @@ public class InAppPurchasePlugin: NSObject, FlutterPlugin, InAppPurchaseAPI { "updatedTransactions", arguments: translatedTransactions) } - @objc public func handleTransactionsRemoved(_ transactions: [SKPaymentTransaction]) { let translatedTransactions = transactions.map { FIAObjectTranslator.getMapFrom($0) @@ -377,19 +367,16 @@ public class InAppPurchasePlugin: NSObject, FlutterPlugin, InAppPurchaseAPI { "removedTransactions", arguments: translatedTransactions) } - @objc public func handleTransactionRestoreFailed(_ error: NSError) { transactionObserverCallbackChannel?.invokeMethod( "restoreCompletedTransactionsFailed", arguments: FIAObjectTranslator.getMapFrom(error)) } - @objc public func restoreCompletedTransactionsFinished() { transactionObserverCallbackChannel?.invokeMethod( "paymentQueueRestoreCompletedTransactionsFinished", arguments: nil) } - @objc public func shouldAddStorePayment(payment: SKPayment, product: SKProduct) -> Bool { productsCache[product.productIdentifier] = product transactionObserverCallbackChannel?.invokeMethod( @@ -432,4 +419,18 @@ public class InAppPurchasePlugin: NSObject, FlutterPlugin, InAppPurchaseAPI { } return paymentQueueHandler } + + private func setupTransactionObserverChannelIfNeeded( + withMessenger messenger: FlutterBinaryMessenger + ) { + // If the channel is already set (e.g., injected in tests), don't overwrite it. + guard self.transactionObserverCallbackChannel == nil else { return } + + self.transactionObserverCallbackChannel = DefaultMethodChannel( + channel: FlutterMethodChannel( + name: "plugins.flutter.io/in_app_purchase", + binaryMessenger: messenger + ) + ) + } } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj index 95588f6aa3f1..f24f5e17add2 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj @@ -18,10 +18,10 @@ C4667AA10A6BC70CE9A5007C /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AB9CD9DD098BDAB3D5053EE5 /* libPods-RunnerTests.a */; }; E680BD031412EB2D02C9190B /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 21CE6E615CF661FC0E18FB0A /* libPods-Runner.a */; }; F22BF91C2BC9B40B00713878 /* SwiftStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22BF91B2BC9B40B00713878 /* SwiftStubs.swift */; }; - F276940B2C47268700277144 /* ProductRequestHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F276940A2C47268700277144 /* ProductRequestHandlerTests.swift */; }; + F24C45E22C409D42000C6C72 /* InAppPurchasePluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24C45E12C409D41000C6C72 /* InAppPurchasePluginTests.swift */; }; F295AD3A2C1256DD0067C78A /* Stubs.m in Sources */ = {isa = PBXBuildFile; fileRef = F295AD392C1256DD0067C78A /* Stubs.m */; }; F295AD412C1256F50067C78A /* FIAPPaymentQueueDeleteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F295AD3B2C1256F50067C78A /* FIAPPaymentQueueDeleteTests.m */; }; - F295AD422C1256F50067C78A /* InAppPurchasePluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F295AD3C2C1256F50067C78A /* InAppPurchasePluginTests.m */; }; + F276940B2C47268700277144 /* ProductRequestHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F276940A2C47268700277144 /* ProductRequestHandlerTests.swift */; }; F295AD442C1256F50067C78A /* FIATransactionCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F295AD3E2C1256F50067C78A /* FIATransactionCacheTests.m */; }; F295AD452C1256F50067C78A /* PaymentQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F295AD3F2C1256F50067C78A /* PaymentQueueTests.m */; }; F295AD462C1256F50067C78A /* TranslatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F295AD402C1256F50067C78A /* TranslatorTests.m */; }; @@ -75,11 +75,11 @@ CC9E5595B2B9B9B90632DA75 /* 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 = ""; }; F22BF91A2BC9B40B00713878 /* RunnerTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RunnerTests-Bridging-Header.h"; sourceTree = ""; }; F22BF91B2BC9B40B00713878 /* SwiftStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftStubs.swift; sourceTree = ""; }; - F276940A2C47268700277144 /* ProductRequestHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProductRequestHandlerTests.swift; path = ../../shared/RunnerTests/ProductRequestHandlerTests.swift; sourceTree = ""; }; + F24C45E12C409D41000C6C72 /* InAppPurchasePluginTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InAppPurchasePluginTests.swift; path = ../../shared/RunnerTests/InAppPurchasePluginTests.swift; sourceTree = ""; }; F295AD362C1251300067C78A /* Stubs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Stubs.h; path = ../../shared/RunnerTests/Stubs.h; sourceTree = ""; }; F295AD392C1256DD0067C78A /* Stubs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Stubs.m; path = ../../shared/RunnerTests/Stubs.m; sourceTree = ""; }; F295AD3B2C1256F50067C78A /* FIAPPaymentQueueDeleteTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIAPPaymentQueueDeleteTests.m; path = ../../shared/RunnerTests/FIAPPaymentQueueDeleteTests.m; sourceTree = ""; }; - F295AD3C2C1256F50067C78A /* InAppPurchasePluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = InAppPurchasePluginTests.m; path = ../../shared/RunnerTests/InAppPurchasePluginTests.m; sourceTree = ""; }; + F276940A2C47268700277144 /* ProductRequestHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProductRequestHandlerTests.swift; path = ../../shared/RunnerTests/ProductRequestHandlerTests.swift; sourceTree = ""; }; F295AD3E2C1256F50067C78A /* FIATransactionCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIATransactionCacheTests.m; path = ../../shared/RunnerTests/FIATransactionCacheTests.m; sourceTree = ""; }; F295AD3F2C1256F50067C78A /* PaymentQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PaymentQueueTests.m; path = ../../shared/RunnerTests/PaymentQueueTests.m; sourceTree = ""; }; F295AD402C1256F50067C78A /* TranslatorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TranslatorTests.m; path = ../../shared/RunnerTests/TranslatorTests.m; sourceTree = ""; }; @@ -186,10 +186,10 @@ A59001A521E69658004A3E5E /* RunnerTests */ = { isa = PBXGroup; children = ( + F24C45E12C409D41000C6C72 /* InAppPurchasePluginTests.swift */, F276940A2C47268700277144 /* ProductRequestHandlerTests.swift */, F295AD3B2C1256F50067C78A /* FIAPPaymentQueueDeleteTests.m */, F295AD3E2C1256F50067C78A /* FIATransactionCacheTests.m */, - F295AD3C2C1256F50067C78A /* InAppPurchasePluginTests.m */, F295AD3F2C1256F50067C78A /* PaymentQueueTests.m */, F295AD402C1256F50067C78A /* TranslatorTests.m */, F295AD392C1256DD0067C78A /* Stubs.m */, @@ -433,13 +433,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F24C45E22C409D42000C6C72 /* InAppPurchasePluginTests.swift in Sources */, F22BF91C2BC9B40B00713878 /* SwiftStubs.swift in Sources */, F295AD412C1256F50067C78A /* FIAPPaymentQueueDeleteTests.m in Sources */, F295AD452C1256F50067C78A /* PaymentQueueTests.m in Sources */, F295AD442C1256F50067C78A /* FIATransactionCacheTests.m in Sources */, F276940B2C47268700277144 /* ProductRequestHandlerTests.swift in Sources */, F295AD462C1256F50067C78A /* TranslatorTests.m in Sources */, - F295AD422C1256F50067C78A /* InAppPurchasePluginTests.m in Sources */, F295AD3A2C1256DD0067C78A /* Stubs.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m deleted file mode 120000 index 495146dde20b..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ /dev/null @@ -1 +0,0 @@ -../../shared/RunnerTests/InAppPurchasePluginTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.swift b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.swift new file mode 120000 index 000000000000..adb0ea0dc27c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.swift @@ -0,0 +1 @@ +../../shared/RunnerTests/InAppPurchasePluginTests.swift \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj index ff32a54e51de..9050d5673654 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj @@ -27,11 +27,11 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + F24C45E42C409D87000C6C72 /* InAppPurchasePluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24C45E32C409D87000C6C72 /* InAppPurchasePluginTests.swift */; }; F27694092C4724B200277144 /* ProductRequestHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F27694082C4724B200277144 /* ProductRequestHandlerTests.swift */; }; F2C3A7412BD9D33D000D35F2 /* Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C3A7402BD9D33D000D35F2 /* Stubs.swift */; }; F79BDC102905FBE300E3999D /* FIAPPaymentQueueDeleteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC0F2905FBE300E3999D /* FIAPPaymentQueueDeleteTests.m */; }; F79BDC122905FBF700E3999D /* FIATransactionCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC112905FBF700E3999D /* FIATransactionCacheTests.m */; }; - F79BDC142905FBFE00E3999D /* InAppPurchasePluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC132905FBFE00E3999D /* InAppPurchasePluginTests.m */; }; F79BDC182905FC1800E3999D /* PaymentQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC172905FC1800E3999D /* PaymentQueueTests.m */; }; F79BDC1C2905FC3200E3999D /* Stubs.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC1B2905FC3200E3999D /* Stubs.m */; }; F79BDC1E2905FC3900E3999D /* TranslatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC1D2905FC3900E3999D /* TranslatorTests.m */; }; @@ -89,6 +89,7 @@ 46EFB01DD1BBB34F886C33A0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + F24C45E32C409D87000C6C72 /* InAppPurchasePluginTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InAppPurchasePluginTests.swift; path = ../../shared/RunnerTests/InAppPurchasePluginTests.swift; sourceTree = ""; }; AEB2EC182EA43F26A351EE3E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B537BC9F2D936311267ABC65 /* 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 = ""; }; F27694082C4724B200277144 /* ProductRequestHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProductRequestHandlerTests.swift; path = ../../shared/RunnerTests/ProductRequestHandlerTests.swift; sourceTree = ""; }; @@ -97,7 +98,6 @@ F700DD0228E652A10004836B /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F79BDC0F2905FBE300E3999D /* FIAPPaymentQueueDeleteTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIAPPaymentQueueDeleteTests.m; path = ../../shared/RunnerTests/FIAPPaymentQueueDeleteTests.m; sourceTree = ""; }; F79BDC112905FBF700E3999D /* FIATransactionCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIATransactionCacheTests.m; path = ../../shared/RunnerTests/FIATransactionCacheTests.m; sourceTree = ""; }; - F79BDC132905FBFE00E3999D /* InAppPurchasePluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = InAppPurchasePluginTests.m; path = ../../shared/RunnerTests/InAppPurchasePluginTests.m; sourceTree = ""; }; F79BDC152905FC0500E3999D /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../../shared/RunnerTests/Info.plist; sourceTree = ""; }; F79BDC172905FC1800E3999D /* PaymentQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PaymentQueueTests.m; path = ../../shared/RunnerTests/PaymentQueueTests.m; sourceTree = ""; }; F79BDC1B2905FC3200E3999D /* Stubs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Stubs.m; path = ../../shared/RunnerTests/Stubs.m; sourceTree = ""; }; @@ -217,9 +217,9 @@ F700DD0328E652A10004836B /* RunnerTests */ = { isa = PBXGroup; children = ( + F24C45E32C409D87000C6C72 /* InAppPurchasePluginTests.swift */, F27694082C4724B200277144 /* ProductRequestHandlerTests.swift */, F79BDC0F2905FBE300E3999D /* FIAPPaymentQueueDeleteTests.m */, - F79BDC132905FBFE00E3999D /* InAppPurchasePluginTests.m */, F79BDC172905FC1800E3999D /* PaymentQueueTests.m */, F79BDC1F2906023C00E3999D /* Stubs.h */, F79BDC152905FC0500E3999D /* Info.plist */, @@ -463,12 +463,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F24C45E42C409D87000C6C72 /* InAppPurchasePluginTests.swift in Sources */, F79BDC1E2905FC3900E3999D /* TranslatorTests.m in Sources */, F79BDC182905FC1800E3999D /* PaymentQueueTests.m in Sources */, F79BDC1C2905FC3200E3999D /* Stubs.m in Sources */, F79BDC102905FBE300E3999D /* FIAPPaymentQueueDeleteTests.m in Sources */, F27694092C4724B200277144 /* ProductRequestHandlerTests.swift in Sources */, - F79BDC142905FBFE00E3999D /* InAppPurchasePluginTests.m in Sources */, F79BDC122905FBF700E3999D /* FIATransactionCacheTests.m in Sources */, F2C3A7412BD9D33D000D35F2 /* Stubs.swift in Sources */, ); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/InAppPurchasePluginTests.m deleted file mode 120000 index 495146dde20b..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/InAppPurchasePluginTests.m +++ /dev/null @@ -1 +0,0 @@ -../../shared/RunnerTests/InAppPurchasePluginTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/InAppPurchasePluginTests.swift b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/InAppPurchasePluginTests.swift new file mode 120000 index 000000000000..adb0ea0dc27c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/InAppPurchasePluginTests.swift @@ -0,0 +1 @@ +../../shared/RunnerTests/InAppPurchasePluginTests.swift \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m deleted file mode 100644 index 1820ff8f65a4..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m +++ /dev/null @@ -1,990 +0,0 @@ -// 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 "FIAPaymentQueueHandler.h" -#import "RunnerTests-Swift.h" -#import "Stubs.h" - -@import in_app_purchase_storekit; - -@interface InAppPurchasePluginTest : XCTestCase - -@property(nonatomic, strong) FIAPReceiptManagerStub *receiptManagerStub; -@property(nonatomic, strong) InAppPurchasePlugin *plugin; - -@end - -@implementation InAppPurchasePluginTest - -- (void)setUp { - self.receiptManagerStub = [FIAPReceiptManagerStub new]; - self.plugin = [[InAppPurchasePluginStub alloc] - initWithReceiptManager:self.receiptManagerStub - handlerFactory:^DefaultRequestHandler *(SKRequest *request) { - return [[DefaultRequestHandler alloc] - initWithRequestHandler:[[FIAPRequestHandler alloc] initWithRequest:request]]; - }]; -} - -- (void)tearDown { -} - -- (void)testCanMakePayments { - FlutterError *error; - NSNumber *result = [self.plugin canMakePaymentsWithError:&error]; - XCTAssertTrue([result boolValue]); - XCTAssertNil(error); -} - -- (void)testPaymentQueueStorefront { - if (@available(iOS 13, macOS 10.15, *)) { - NSDictionary *storefrontMap = @{ - @"countryCode" : @"USA", - @"identifier" : @"unique_identifier", - }; - PaymentQueueStub *queueStub = [[PaymentQueueStub alloc] init]; - TransactionCacheStub *cache = [[TransactionCacheStub alloc] init]; - - queueStub.storefront = [[SKStorefrontStub alloc] initWithMap:storefrontMap]; - - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queueStub - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:cache]; - - FlutterError *error; - SKStorefrontMessage *result = [self.plugin storefrontWithError:&error]; - - XCTAssertEqualObjects(result.countryCode, storefrontMap[@"countryCode"]); - XCTAssertEqualObjects(result.identifier, storefrontMap[@"identifier"]); - XCTAssertNil(error); - } else { - NSLog(@"Skip testPaymentQueueStorefront for iOS lower than 13.0 or macOS lower than 10.15."); - } -} - -- (void)testPaymentQueueStorefrontReturnsNil { - if (@available(iOS 13, macOS 10.15, *)) { - PaymentQueueStub *queueStub = [[PaymentQueueStub alloc] init]; - TransactionCacheStub *cache = [[TransactionCacheStub alloc] init]; - - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queueStub - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:cache]; - - FlutterError *error; - SKStorefrontMessage *resultMap = [self.plugin storefrontWithError:&error]; - - XCTAssertNil(resultMap); - XCTAssertNil(error); - } else { - NSLog(@"Skip testPaymentQueueStorefront for iOS lower than 13.0 or macOS lower than 10.15."); - } -} - -- (void)testGetProductResponse { - NSArray *argument = @[ @"123" ]; - XCTestExpectation *expectation = - [self expectationWithDescription:@"completion handler successfully called"]; - [self.plugin - startProductRequestProductIdentifiers:argument - completion:^(SKProductsResponseMessage *_Nullable response, - FlutterError *_Nullable startProductRequestError) { - XCTAssert( - [response isKindOfClass:[SKProductsResponseMessage class]]); - XCTAssertEqual(response.products.count, 1); - XCTAssertEqual(response.invalidProductIdentifiers.count, 0); - XCTAssertEqual(response.products[0].productIdentifier, @"123"); - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -- (void)testFinishTransactionSucceeds { - NSDictionary *args = @{ - @"transactionIdentifier" : @"567", - @"productIdentifier" : @"unique_identifier", - }; - - NSDictionary *transactionMap = @{ - @"transactionIdentifier" : @"567", - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : [NSNull null], - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - }; - - SKPaymentTransactionStub *paymentTransactionStub = - [[SKPaymentTransactionStub alloc] initWithMap:transactionMap]; - NSArray *array = @[ paymentTransactionStub ]; - - PaymentQueueStub *queue = [[PaymentQueueStub alloc] init]; - queue.transactions = array; - - TransactionCacheStub *cache = [[TransactionCacheStub alloc] init]; - - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:cache]; - - FlutterError *error; - [self.plugin finishTransactionFinishMap:args error:&error]; - - XCTAssertNil(error); -} - -- (void)testFinishTransactionSucceedsWithNilTransaction { - NSDictionary *args = @{ - @"transactionIdentifier" : [NSNull null], - @"productIdentifier" : @"unique_identifier", - }; - - NSDictionary *paymentMap = @{ - @"productIdentifier" : @"123", - @"requestData" : @"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", - @"quantity" : @(2), - @"applicationUsername" : @"app user name", - @"simulatesAskToBuyInSandbox" : @(NO) - }; - - NSDictionary *transactionMap = @{ - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : paymentMap, - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - }; - - SKPaymentTransactionStub *paymentTransactionStub = - [[SKPaymentTransactionStub alloc] initWithMap:transactionMap]; - - PaymentQueueStub *queueStub = [[PaymentQueueStub alloc] init]; - queueStub.transactions = @[ paymentTransactionStub ]; - - TransactionCacheStub *cache = [[TransactionCacheStub alloc] init]; - - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queueStub - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:cache]; - ; - - FlutterError *error; - [self.plugin finishTransactionFinishMap:args error:&error]; - - XCTAssertNil(error); -} - -- (void)testGetProductResponseWithRequestError { - NSArray *argument = @[ @"123" ]; - XCTestExpectation *expectation = - [self expectationWithDescription:@"completion handler successfully called"]; - - RequestHandlerStub *handlerStub = [[RequestHandlerStub alloc] init]; - InAppPurchasePlugin *plugin = [[InAppPurchasePlugin alloc] - initWithReceiptManager:_receiptManagerStub - handlerFactory:^RequestHandlerStub *(SKRequest *request) { - return handlerStub; - }]; - - NSError *error = [NSError errorWithDomain:@"errorDomain" - code:0 - userInfo:@{NSLocalizedDescriptionKey : @"description"}]; - - handlerStub.startProductRequestWithCompletionHandlerStub = - ^(ProductRequestCompletion _Nonnull completion) { - completion(nil, error); - }; - - [plugin - startProductRequestProductIdentifiers:argument - completion:^(SKProductsResponseMessage *_Nullable response, - FlutterError *_Nullable startProductRequestError) { - [expectation fulfill]; - XCTAssertNotNil(error); - XCTAssertNotNil(startProductRequestError); - XCTAssertEqualObjects( - startProductRequestError.code, - @"storekit_getproductrequest_platform_error"); - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -- (void)testGetProductResponseWithNoResponse { - NSArray *argument = @[ @"123" ]; - XCTestExpectation *expectation = - [self expectationWithDescription:@"completion handler successfully called"]; - - RequestHandlerStub *handlerStub = [[RequestHandlerStub alloc] init]; - InAppPurchasePlugin *plugin = [[InAppPurchasePlugin alloc] - initWithReceiptManager:_receiptManagerStub - handlerFactory:^RequestHandlerStub *(SKRequest *request) { - return handlerStub; - }]; - - handlerStub.startProductRequestWithCompletionHandlerStub = - ^(ProductRequestCompletion _Nonnull completion) { - completion(nil, nil); - }; - - [plugin - startProductRequestProductIdentifiers:argument - completion:^(SKProductsResponseMessage *_Nullable response, - FlutterError *_Nullable startProductRequestError) { - [expectation fulfill]; - XCTAssertNotNil(startProductRequestError); - XCTAssertEqualObjects(startProductRequestError.code, - @"storekit_platform_no_response"); - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -- (void)testAddPaymentShouldReturnFlutterErrorWhenPaymentFails { - NSDictionary *argument = @{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }; - - PaymentQueueHandlerStub *handlerStub = [[PaymentQueueHandlerStub alloc] init]; - self.plugin.paymentQueueHandler = handlerStub; - - FlutterError *error; - - __block NSInteger addPaymentInvokeCount = 0; - handlerStub.addPaymentStub = ^(SKPayment *payment) { - addPaymentInvokeCount += 1; - return NO; - }; - - [self.plugin addPaymentPaymentMap:argument error:&error]; - - XCTAssertEqual(addPaymentInvokeCount, 1); - XCTAssertEqualObjects(@"storekit_duplicate_product_object", error.code); - XCTAssertEqualObjects(@"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.", - error.message); - XCTAssertEqualObjects(argument, error.details); -} - -- (void)testAddPaymentShouldReturnFlutterErrorWhenInvalidProduct { - NSDictionary *argument = @{ - // stubbed function will return nil for an empty productIdentifier - @"productIdentifier" : @"", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }; - - FlutterError *error; - - [self.plugin addPaymentPaymentMap:argument error:&error]; - - XCTAssertEqualObjects(@"storekit_invalid_payment_object", error.code); - XCTAssertEqualObjects( - @"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.", - error.message); - XCTAssertEqualObjects(argument, error.details); -} - -- (void)testAddPaymentSuccessWithoutPaymentDiscount { - NSDictionary *argument = @{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }; - - PaymentQueueHandlerStub *handlerStub = [[PaymentQueueHandlerStub alloc] init]; - self.plugin.paymentQueueHandler = handlerStub; - - __block NSInteger addPaymentInvokeCount = 0; - handlerStub.addPaymentStub = ^(SKPayment *payment) { - XCTAssert(payment != nil); - XCTAssertEqual(payment.productIdentifier, @"123"); - XCTAssert(payment.quantity == 1); - addPaymentInvokeCount++; - return YES; - }; - - FlutterError *error; - - [self.plugin addPaymentPaymentMap:argument error:&error]; - - XCTAssertNil(error); - XCTAssertEqual(addPaymentInvokeCount, 1); -} - -- (void)testAddPaymentSuccessWithPaymentDiscount { - NSDictionary *argument = @{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - @"paymentDiscount" : @{ - @"identifier" : @"test_identifier", - @"keyIdentifier" : @"test_key_identifier", - @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", - @"signature" : @"test_signature", - @"timestamp" : @(1635847102), - } - }; - - PaymentQueueHandlerStub *handlerStub = [[PaymentQueueHandlerStub alloc] init]; - self.plugin.paymentQueueHandler = handlerStub; - - __block NSInteger addPaymentInvokeCount = 0; - handlerStub.addPaymentStub = ^(SKPayment *payment) { - if (@available(iOS 12.2, *)) { - SKPaymentDiscount *discount = payment.paymentDiscount; - XCTAssertEqual(discount.identifier, @"test_identifier"); - XCTAssertEqual(discount.keyIdentifier, @"test_key_identifier"); - XCTAssertEqualObjects( - discount.nonce, - [[NSUUID alloc] initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]); - XCTAssertEqual(discount.signature, @"test_signature"); - addPaymentInvokeCount++; - return YES; - } - addPaymentInvokeCount++; - return YES; - }; - - FlutterError *error; - - [self.plugin addPaymentPaymentMap:argument error:&error]; - XCTAssertEqual(addPaymentInvokeCount, 1); - XCTAssertNil(error); -} - -- (void)testAddPaymentFailureWithInvalidPaymentDiscount { - // Support for payment discount is only available on iOS 12.2 and higher. - if (@available(iOS 12.2, *)) { - NSDictionary *invalidDiscount = @{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - @"paymentDiscount" : @{ - /// This payment discount is missing the field `identifier`, and is thus malformed - @"keyIdentifier" : @"test_key_identifier", - @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", - @"signature" : @"test_signature", - @"timestamp" : @(1635847102), - } - }; - - PaymentQueueHandlerStub *handlerStub = [[PaymentQueueHandlerStub alloc] init]; - - __block NSInteger addPaymentCount = 0; - handlerStub.addPaymentStub = ^BOOL(SKPayment *_Nonnull payment) { - addPaymentCount++; - return YES; - }; - - self.plugin.paymentQueueHandler = handlerStub; - FlutterError *error; - - [self.plugin addPaymentPaymentMap:invalidDiscount error:&error]; - - XCTAssertEqualObjects(@"storekit_invalid_payment_discount_object", error.code); - XCTAssertEqualObjects(@"You have requested a payment and specified a " - @"payment discount with invalid properties. When specifying a payment " - @"discount the 'identifier' field is mandatory.", - error.message); - XCTAssertEqualObjects(invalidDiscount, error.details); - XCTAssertEqual(0, addPaymentCount); - } -} - -- (void)testAddPaymentWithNullSandboxArgument { - NSDictionary *argument = @{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : [NSNull null], - }; - - PaymentQueueHandlerStub *handlerStub = [[PaymentQueueHandlerStub alloc] init]; - self.plugin.paymentQueueHandler = handlerStub; - FlutterError *error; - - __block NSInteger addPaymentInvokeCount = 0; - handlerStub.addPaymentStub = ^(SKPayment *payment) { - XCTAssertEqual(payment.simulatesAskToBuyInSandbox, false); - addPaymentInvokeCount++; - return YES; - }; - - [self.plugin addPaymentPaymentMap:argument error:&error]; - XCTAssertEqual(addPaymentInvokeCount, 1); -} - -- (void)testRestoreTransactions { - XCTestExpectation *expectation = - [self expectationWithDescription:@"result successfully restore transactions"]; - - TransactionCacheStub *cacheStub = [[TransactionCacheStub alloc] init]; - PaymentQueueStub *queueStub = [[PaymentQueueStub alloc] init]; - - __block BOOL callbackInvoked = NO; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queueStub - transactionsUpdated:^(NSArray *_Nonnull transactions) { - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:^() { - callbackInvoked = YES; - [expectation fulfill]; - } - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:cacheStub]; - [queueStub addTransactionObserver:self.plugin.paymentQueueHandler]; - - FlutterError *error; - [self.plugin restoreTransactionsApplicationUserName:nil error:&error]; - - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue(callbackInvoked); -} - -- (void)testRetrieveReceiptDataSuccess { - FlutterError *error; - NSString *result = [self.plugin retrieveReceiptDataWithError:&error]; - XCTAssertNotNil(result); - XCTAssert([result isKindOfClass:[NSString class]]); -} - -- (void)testRetrieveReceiptDataNil { - self.receiptManagerStub.returnNilURL = YES; - - FlutterError *error; - NSString *result = [self.plugin retrieveReceiptDataWithError:&error]; - XCTAssertNil(result); -} - -- (void)testRetrieveReceiptDataError { - self.receiptManagerStub.returnError = YES; - - FlutterError *error; - NSString *result = [self.plugin retrieveReceiptDataWithError:&error]; - - XCTAssertNil(result); - XCTAssertNotNil(error); - XCTAssert([error.code isKindOfClass:[NSString class]]); - NSDictionary *details = error.details; - XCTAssertNotNil(details[@"error"]); - NSNumber *errorCode = (NSNumber *)details[@"error"][@"code"]; - XCTAssertEqual(errorCode, [NSNumber numberWithInteger:99]); -} - -- (void)testRefreshReceiptRequest { - XCTestExpectation *expectation = - [self expectationWithDescription:@"completion handler successfully called"]; - - RequestHandlerStub *handlerStub = [[RequestHandlerStub alloc] init]; - InAppPurchasePlugin *plugin = [[InAppPurchasePlugin alloc] - initWithReceiptManager:_receiptManagerStub - handlerFactory:^RequestHandlerStub *(SKRequest *request) { - return handlerStub; - }]; - - NSError *recieptError = [NSError errorWithDomain:@"errorDomain" - code:0 - userInfo:@{NSLocalizedDescriptionKey : @"description"}]; - - handlerStub.startProductRequestWithCompletionHandlerStub = - ^(ProductRequestCompletion _Nonnull completion) { - completion(nil, recieptError); - }; - - [plugin refreshReceiptReceiptProperties:nil - completion:^(FlutterError *_Nullable error) { - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -- (void)testRefreshReceiptRequestWithParams { - NSDictionary *properties = @{ - @"isExpired" : @NO, - @"isRevoked" : @NO, - @"isVolumePurchase" : @NO, - }; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"completion handler successfully called"]; - - RequestHandlerStub *handlerStub = [[RequestHandlerStub alloc] init]; - InAppPurchasePlugin *plugin = [[InAppPurchasePlugin alloc] - initWithReceiptManager:_receiptManagerStub - handlerFactory:^RequestHandlerStub *(SKRequest *request) { - return handlerStub; - }]; - - NSError *recieptError = [NSError errorWithDomain:@"errorDomain" - code:0 - userInfo:@{NSLocalizedDescriptionKey : @"description"}]; - - handlerStub.startProductRequestWithCompletionHandlerStub = - ^(ProductRequestCompletion _Nonnull completion) { - completion(nil, recieptError); - }; - - [plugin refreshReceiptReceiptProperties:properties - completion:^(FlutterError *_Nullable error) { - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -- (void)testRefreshReceiptRequestWithError { - NSDictionary *properties = @{ - @"isExpired" : @NO, - @"isRevoked" : @NO, - @"isVolumePurchase" : @NO, - }; - XCTestExpectation *expectation = - [self expectationWithDescription:@"completion handler successfully called"]; - - RequestHandlerStub *handlerStub = [[RequestHandlerStub alloc] init]; - InAppPurchasePlugin *plugin = [[InAppPurchasePlugin alloc] - initWithReceiptManager:_receiptManagerStub - handlerFactory:^RequestHandlerStub *(SKRequest *request) { - return handlerStub; - }]; - - NSError *recieptError = [NSError errorWithDomain:@"errorDomain" - code:0 - userInfo:@{NSLocalizedDescriptionKey : @"description"}]; - - handlerStub.startProductRequestWithCompletionHandlerStub = - ^(ProductRequestCompletion _Nonnull completion) { - completion(nil, recieptError); - }; - - [plugin refreshReceiptReceiptProperties:properties - completion:^(FlutterError *_Nullable error) { - XCTAssertNotNil(error); - XCTAssertEqualObjects( - error.code, @"storekit_refreshreceiptrequest_platform_error"); - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -/// presentCodeRedemptionSheetWithError:error is only available on iOS -#if TARGET_OS_IOS -- (void)testPresentCodeRedemptionSheet { - PaymentQueueHandlerStub *handlerStub = [[PaymentQueueHandlerStub alloc] init]; - self.plugin.paymentQueueHandler = handlerStub; - - __block NSInteger presentCodeRedemptionSheetCount = 0; - handlerStub.presentCodeRedemptionSheetStub = ^{ - presentCodeRedemptionSheetCount++; - }; - - FlutterError *error; - [self.plugin presentCodeRedemptionSheetWithError:&error]; - - XCTAssertEqual(1, presentCodeRedemptionSheetCount); -} -#endif - -- (void)testGetPendingTransactions { - PaymentQueueStub *queueStub = [[PaymentQueueStub alloc] init]; - TransactionCacheStub *cacheStub = [[TransactionCacheStub alloc] init]; - 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], - }; - queueStub.transactions = @[ [[SKPaymentTransactionStub alloc] initWithMap:transactionMap] ]; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queueStub - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:cacheStub]; - FlutterError *error; - SKPaymentTransactionStub *original = - [[SKPaymentTransactionStub alloc] initWithMap:transactionMap]; - - SKPaymentTransactionMessage *originalPigeon = - [FIAObjectTranslator convertTransactionToPigeon:original]; - SKPaymentTransactionMessage *result = [self.plugin transactionsWithError:&error][0]; - - XCTAssertEqualObjects([self paymentTransactionToList:result], - [self paymentTransactionToList:originalPigeon]); -} - -- (void)testStartObservingPaymentQueue { - PaymentQueueHandlerStub *handlerStub = [[PaymentQueueHandlerStub alloc] init]; - self.plugin.paymentQueueHandler = handlerStub; - - __block NSInteger startObservingCount = 0; - handlerStub.startObservingPaymentQueueStub = ^{ - startObservingCount++; - }; - - FlutterError *error; - [self.plugin startObservingPaymentQueueWithError:&error]; - - XCTAssertEqual(1, startObservingCount); -} - -- (void)testStopObservingPaymentQueue { - PaymentQueueHandlerStub *handlerStub = [[PaymentQueueHandlerStub alloc] init]; - self.plugin.paymentQueueHandler = handlerStub; - - __block NSInteger stopObservingCount = 0; - handlerStub.stopObservingPaymentQueueStub = ^{ - stopObservingCount++; - }; - - FlutterError *error; - [self.plugin stopObservingPaymentQueueWithError:&error]; - - XCTAssertEqual(1, stopObservingCount); -} - -#if TARGET_OS_IOS -- (void)testRegisterPaymentQueueDelegate { - TransactionCacheStub *cacheStub = [[TransactionCacheStub alloc] init]; - PaymentQueueStub *queueStub = [[PaymentQueueStub alloc] init]; - if (@available(iOS 13, *)) { - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queueStub - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:cacheStub]; - - self.plugin.registrar = [[FlutterPluginRegistrarStub alloc] init]; - - // Verify the delegate is nil before we register one. - XCTAssertNil(self.plugin.paymentQueueHandler.delegate); - - FlutterError *error; - [self.plugin registerPaymentQueueDelegateWithError:&error]; - - // Verify the delegate is not nil after we registered one. - XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); - } -} - -- (void)testRemovePaymentQueueDelegate { - if (@available(iOS 13, *)) { - TransactionCacheStub *cacheStub = [[TransactionCacheStub alloc] init]; - PaymentQueueStub *queueStub = [[PaymentQueueStub alloc] init]; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queueStub - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:cacheStub]; - - self.plugin.registrar = [[FlutterPluginRegistrarStub alloc] init]; - - // Verify the delegate is nil before we register one. - XCTAssertNil(self.plugin.paymentQueueHandler.delegate); - - FlutterError *error; - [self.plugin registerPaymentQueueDelegateWithError:&error]; - - // Verify the delegate is not nil before removing it. - XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); - - [self.plugin removePaymentQueueDelegateWithError:&error]; - - // Verify the delegate is nill after removing it. - XCTAssertNil(self.plugin.paymentQueueHandler.delegate); - } -} -#endif - -- (void)testHandleTransactionsUpdated { - NSDictionary *transactionMap = @{ - @"transactionIdentifier" : @"567", - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : [NSNull null], - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - }; - - InAppPurchasePlugin *plugin = [[InAppPurchasePluginStub alloc] - initWithReceiptManager:self.receiptManagerStub - handlerFactory:^DefaultRequestHandler *(SKRequest *request) { - return [[DefaultRequestHandler alloc] - initWithRequestHandler:[[FIAPRequestHandler alloc] initWithRequest:request]]; - }]; - MethodChannelStub *channelStub = [[MethodChannelStub alloc] init]; - __block NSInteger invokeMethodCount = 0; - - channelStub.invokeMethodChannelStub = ^(NSString *_Nonnull method, id _Nonnull arguments) { - XCTAssertEqualObjects(@"updatedTransactions", method); - XCTAssertNotNil(arguments); - invokeMethodCount++; - }; - - // (TODO: louisehsu) Change this to inject the channel, like requestHandler - plugin.transactionObserverCallbackChannel = channelStub; - - SKPaymentTransactionStub *paymentTransactionStub = - [[SKPaymentTransactionStub alloc] initWithMap:transactionMap]; - NSArray *array = [NSArray arrayWithObjects:paymentTransactionStub, nil]; - NSMutableArray *maps = [NSMutableArray new]; - [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:paymentTransactionStub]]; - - [plugin handleTransactionsUpdated:array]; - XCTAssertEqual(invokeMethodCount, 1); -} - -- (void)testHandleTransactionsRemoved { - NSDictionary *transactionMap = @{ - @"transactionIdentifier" : @"567", - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : [NSNull null], - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - }; - - InAppPurchasePlugin *plugin = [[InAppPurchasePluginStub alloc] - initWithReceiptManager:self.receiptManagerStub - handlerFactory:^DefaultRequestHandler *(SKRequest *request) { - return [[DefaultRequestHandler alloc] - initWithRequestHandler:[[FIAPRequestHandler alloc] initWithRequest:request]]; - }]; - SKPaymentTransactionStub *paymentTransactionStub = - [[SKPaymentTransactionStub alloc] initWithMap:transactionMap]; - NSArray *array = [NSArray arrayWithObjects:paymentTransactionStub, nil]; - NSMutableArray *maps = [NSMutableArray new]; - [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:paymentTransactionStub]]; - - MethodChannelStub *channelStub = [[MethodChannelStub alloc] init]; - __block NSInteger invokeMethodCount = 0; - - channelStub.invokeMethodChannelStub = ^(NSString *_Nonnull method, id _Nonnull arguments) { - XCTAssertEqualObjects(@"removedTransactions", method); - XCTAssertEqualObjects(maps, arguments); - invokeMethodCount++; - }; - - // (TODO: louisehsu) Change this to inject the channel, like requestHandler - plugin.transactionObserverCallbackChannel = channelStub; - - [plugin handleTransactionsRemoved:array]; - XCTAssertEqual(invokeMethodCount, 1); -} - -- (void)testHandleTransactionRestoreFailed { - InAppPurchasePlugin *plugin = [[InAppPurchasePluginStub alloc] - initWithReceiptManager:self.receiptManagerStub - handlerFactory:^DefaultRequestHandler *(SKRequest *request) { - return [[DefaultRequestHandler alloc] - initWithRequestHandler:[[FIAPRequestHandler alloc] initWithRequest:request]]; - }]; - MethodChannelStub *channelStub = [[MethodChannelStub alloc] init]; - __block NSInteger invokeMethodCount = 0; - NSError *error = [NSError errorWithDomain:@"error" code:0 userInfo:nil]; - - channelStub.invokeMethodChannelStub = ^(NSString *_Nonnull method, id _Nonnull arguments) { - XCTAssertEqualObjects(@"restoreCompletedTransactionsFailed", method); - XCTAssertEqualObjects([FIAObjectTranslator getMapFromNSError:error], arguments); - invokeMethodCount++; - }; - - // (TODO: louisehsu) Change this to inject the channel, like requestHandler - plugin.transactionObserverCallbackChannel = channelStub; - - [plugin handleTransactionRestoreFailed:error]; - XCTAssertEqual(invokeMethodCount, 1); -} - -- (void)testRestoreCompletedTransactionsFinished { - InAppPurchasePlugin *plugin = [[InAppPurchasePluginStub alloc] - initWithReceiptManager:self.receiptManagerStub - handlerFactory:^DefaultRequestHandler *(SKRequest *request) { - return [[DefaultRequestHandler alloc] - initWithRequestHandler:[[FIAPRequestHandler alloc] initWithRequest:request]]; - }]; - MethodChannelStub *channelStub = [[MethodChannelStub alloc] init]; - __block NSInteger invokeMethodCount = 0; - channelStub.invokeMethodChannelStub = ^(NSString *_Nonnull method, id _Nonnull arguments) { - XCTAssertEqualObjects(@"paymentQueueRestoreCompletedTransactionsFinished", method); - XCTAssertNil(arguments); - invokeMethodCount++; - }; - - // (TODO: louisehsu) Change this to inject the channel, like requestHandler - plugin.transactionObserverCallbackChannel = channelStub; - - [plugin restoreCompletedTransactionsFinished]; - XCTAssertEqual(invokeMethodCount, 1); -} - -- (void)testShouldAddStorePayment { - NSDictionary *paymentMap = @{ - @"productIdentifier" : @"123", - @"requestData" : @"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", - @"quantity" : @(2), - @"applicationUsername" : @"app user name", - @"simulatesAskToBuyInSandbox" : @(NO) - }; - - NSDictionary *productMap = @{ - @"price" : @"1", - @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], - @"productIdentifier" : @"123", - @"localizedTitle" : @"title", - @"localizedDescription" : @"des", - }; - - SKMutablePayment *payment = [FIAObjectTranslator getSKMutablePaymentFromMap:paymentMap]; - SKProductStub *productStub = [[SKProductStub alloc] initWithMap:productMap]; - - InAppPurchasePlugin *plugin = [[InAppPurchasePluginStub alloc] - initWithReceiptManager:self.receiptManagerStub - handlerFactory:^DefaultRequestHandler *(SKRequest *request) { - return [[DefaultRequestHandler alloc] - initWithRequestHandler:[[FIAPRequestHandler alloc] initWithRequest:request]]; - }]; - - NSDictionary *args = @{ - @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], - @"product" : [FIAObjectTranslator getMapFromSKProduct:productStub] - }; - - MethodChannelStub *channelStub = [[MethodChannelStub alloc] init]; - - __block NSInteger invokeMethodCount = 0; - channelStub.invokeMethodChannelStub = ^(NSString *_Nonnull method, id _Nonnull arguments) { - XCTAssertEqualObjects(@"shouldAddStorePayment", method); - XCTAssertEqualObjects(args, arguments); - invokeMethodCount++; - }; - - // (TODO: louisehsu) Change this to inject the channel, like requestHandler - plugin.transactionObserverCallbackChannel = channelStub; - - BOOL result = [plugin shouldAddStorePaymentWithPayment:payment product:productStub]; - XCTAssertEqual(result, NO); - XCTAssertEqual(invokeMethodCount, 1); -} - -#if TARGET_OS_IOS -- (void)testShowPriceConsentIfNeeded { - TransactionCacheStub *cacheStub = [[TransactionCacheStub alloc] init]; - PaymentQueueStub *queueStub = [[PaymentQueueStub alloc] init]; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queueStub - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:cacheStub]; - - FlutterError *error; - __block NSInteger showPriceConsentIfNeededCount = 0; - - queueStub.showPriceConsentIfNeededStub = ^(void) { - showPriceConsentIfNeededCount++; - }; - - [self.plugin showPriceConsentIfNeededWithError:&error]; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpartial-availability" - if (@available(iOS 13.4, *)) { - XCTAssertEqual(showPriceConsentIfNeededCount, 1); - } else { - XCTAssertEqual(showPriceConsentIfNeededCount, 0); - } -#pragma clang diagnostic pop -} -#endif - -// The following methods are deserializer copied from Pigeon's output. - -- (NSArray *)paymentTransactionToList:(SKPaymentTransactionMessage *)paymentTransaction { - return @[ - (paymentTransaction.payment ? [self paymentToList:paymentTransaction.payment] : [NSNull null]), - @(paymentTransaction.transactionState), - (paymentTransaction.originalTransaction - ? [self paymentTransactionToList:paymentTransaction.originalTransaction] - : [NSNull null]), - paymentTransaction.transactionTimeStamp ?: [NSNull null], - paymentTransaction.transactionIdentifier ?: [NSNull null], - (paymentTransaction.error ? [self errorToList:paymentTransaction.error] : [NSNull null]), - ]; -} - -- (NSArray *)paymentToList:(SKPaymentMessage *)payment { - return @[ - payment.productIdentifier ?: [NSNull null], - payment.applicationUsername ?: [NSNull null], - payment.requestData ?: [NSNull null], - @(payment.quantity), - @(payment.simulatesAskToBuyInSandbox), - (payment.paymentDiscount ? [self paymentDiscountToList:payment.paymentDiscount] - : [NSNull null]), - ]; -} - -- (NSArray *)paymentDiscountToList:(SKPaymentDiscountMessage *)discount { - return @[ - discount.identifier ?: [NSNull null], - discount.keyIdentifier ?: [NSNull null], - discount.nonce ?: [NSNull null], - discount.signature ?: [NSNull null], - @(discount.timestamp), - ]; -} - -- (NSArray *)errorToList:(SKErrorMessage *)error { - return @[ - @(error.code), - error.domain ?: [NSNull null], - error.userInfo ?: [NSNull null], - ]; -} -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.swift b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.swift new file mode 100644 index 000000000000..d8cce0138787 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.swift @@ -0,0 +1,856 @@ +// 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 Foundation +import StoreKitTest +import XCTest + +@testable import in_app_purchase_storekit + +final class InAppPurchasePluginTests: XCTestCase { + var receiptManagerStub: FIAPReceiptManagerStub! + var plugin: InAppPurchasePlugin! + + override func setUp() { + super.setUp() + receiptManagerStub = FIAPReceiptManagerStub() + plugin = InAppPurchasePluginStub(receiptManager: receiptManagerStub) { request in + DefaultRequestHandler(requestHandler: FIAPRequestHandler(request: request)) + } + } + + override func tearDown() { + receiptManagerStub = nil + plugin = nil + super.tearDown() + } + + func testCanMakePayments() throws { + var error: FlutterError? + let result = plugin.canMakePaymentsWithError(&error) + let unwrappedResult = try XCTUnwrap(result) + XCTAssertTrue(unwrappedResult.boolValue) + XCTAssertNil(error) + } + + func testPaymentQueueStorefrontReturnsNil() throws { + if #available(iOS 13, macOS 10.15, *) { + let storefrontMap = [ + "countryCode": "USA", + "identifier": "unique_identifier", + ] + let queueStub = PaymentQueueStub() + let cache = TransactionCacheStub() + + queueStub.storefront = SKStorefrontStub(map: storefrontMap) + + plugin.paymentQueueHandler = FIAPaymentQueueHandler( + queue: queueStub, + transactionsUpdated: nil, + transactionRemoved: nil, + restoreTransactionFailed: nil, + restoreCompletedTransactionsFinished: nil, + shouldAddStorePayment: nil, + updatedDownloads: nil, + transactionCache: cache) + + var error: FlutterError? + let result = plugin.storefrontWithError(&error) + + let unwrappedResult = try XCTUnwrap(result) + XCTAssertEqual(unwrappedResult.countryCode, storefrontMap["countryCode"]) + XCTAssertEqual(unwrappedResult.identifier, storefrontMap["identifier"]) + XCTAssertNil(error) + } else { + print("Skip testPaymentQueueStorefront for iOS lower than 13.0 or macOS lower than 10.15.") + } + } + func testGetProductResponse() { + let argument = ["123"] + let expectation = self.expectation(description: "completion handler successfully called") + + plugin.startProductRequestProductIdentifiers(argument) { response, startProductRequestError in + guard let response = response else { + XCTFail("Response should not be nil") + return + } + + guard let unwrappedProducts = response.products else { + XCTFail("Products should not be nil") + return + } + + XCTAssertEqual(unwrappedProducts.count, 1) + XCTAssertEqual(response.invalidProductIdentifiers, nil) + XCTAssertEqual(unwrappedProducts[0].productIdentifier, "123") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testFinishTransactionSucceedsWithNilTransaction() { + let args: [String: Any] = [ + "transactionIdentifier": NSNull(), + "productIdentifier": "unique_identifier", + ] + + let paymentMap: [String: Any] = [ + "productIdentifier": "123", + "requestData": "abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", + "quantity": 2, + "applicationUsername": "app user name", + "simulatesAskToBuyInSandbox": false, + ] + + let transactionMap: [String: Any] = [ + "transactionState": SKPaymentTransactionState.purchasing.rawValue, + "payment": paymentMap, + "error": FIAObjectTranslator.getMapFrom( + NSError(domain: "test_stub", code: 123, userInfo: [:])), + "transactionTimeStamp": NSDate().timeIntervalSince1970, + ] + + let paymentTransactionStub = SKPaymentTransactionStub(map: transactionMap) + + let queueStub = PaymentQueueStub() + queueStub.transactions = [paymentTransactionStub] + + let cache = TransactionCacheStub() + + plugin.paymentQueueHandler = FIAPaymentQueueHandler( + queue: queueStub, + transactionsUpdated: nil, + transactionRemoved: nil, + restoreTransactionFailed: nil, + restoreCompletedTransactionsFinished: nil, + shouldAddStorePayment: nil, + updatedDownloads: nil, + transactionCache: cache) + + var error: FlutterError? + plugin.finishTransactionFinishMap(args, error: &error) + + XCTAssertNil(error) + } + + func testGetProductResponseWithRequestError() { + let argument = ["123"] + let expectation = self.expectation(description: "completion handler successfully called") + + let handlerStub = RequestHandlerStub() + let plugin = InAppPurchasePlugin(receiptManager: receiptManagerStub) { request in + return handlerStub + } + + let error = NSError( + domain: "errorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "description"]) + + handlerStub.startProductRequestWithCompletionHandlerStub = { completion in + completion(nil, error) + } + + plugin.startProductRequestProductIdentifiers(argument) { response, startProductRequestError in + expectation.fulfill() + XCTAssertNotNil(error) + XCTAssertNotNil(startProductRequestError) + XCTAssertEqual(startProductRequestError?.code, "storekit_getproductrequest_platform_error") + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testGetProductResponseWithNoResponse() { + let argument = ["123"] + let expectation = self.expectation(description: "completion handler successfully called") + + let handlerStub = RequestHandlerStub() + let plugin = InAppPurchasePlugin(receiptManager: receiptManagerStub) { request in + return handlerStub + } + + let error = NSError( + domain: "errorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "description"]) + + handlerStub.startProductRequestWithCompletionHandlerStub = { completion in + completion(nil, nil) + } + + plugin.startProductRequestProductIdentifiers(argument) { response, startProductRequestError in + expectation.fulfill() + XCTAssertNotNil(error) + XCTAssertNotNil(startProductRequestError) + XCTAssertEqual(startProductRequestError?.code, "storekit_platform_no_response") + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testAddPaymentShouldReturnFlutterErrorWhenPaymentFails() { + let argument: [String: Any] = [ + "productIdentifier": "123", + "quantity": 1, + "simulatesAskToBuyInSandbox": true, + ] + + let handlerStub = PaymentQueueHandlerStub() + plugin.paymentQueueHandler = handlerStub + + var error: FlutterError? + + var addPaymentInvokeCount = 0 + handlerStub.addPaymentStub = { payment in + addPaymentInvokeCount += 1 + return false + } + + plugin.addPaymentPaymentMap(argument, error: &error) + + XCTAssertEqual(addPaymentInvokeCount, 1) + + guard let error = error else { + XCTFail("Error should not be nil") + return + } + + XCTAssertEqual(error.code, "storekit_duplicate_product_object") + XCTAssertEqual( + error.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." + ) + XCTAssertEqual(error.details as! NSDictionary, argument as NSDictionary) + } + + func testAddPaymentShouldReturnFlutterErrorWhenInvalidProduct() { + let argument: [String: Any] = [ + // stubbed function will return nil for an empty productIdentifier + "productIdentifier": "", + "quantity": 1, + "simulatesAskToBuyInSandbox": true, + ] + + var error: FlutterError? + plugin.addPaymentPaymentMap(argument, error: &error) + + guard let error = error else { + XCTFail("Error should not be nil") + return + } + + XCTAssertEqual(error.code, "storekit_invalid_payment_object") + XCTAssertEqual( + error.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." + ) + XCTAssertEqual(error.details as! NSDictionary, argument as NSDictionary) + } + + func testAddPaymentSuccessWithoutPaymentDiscount() { + let argument: [String: Any] = [ + "productIdentifier": "123", + "quantity": 1, + "simulatesAskToBuyInSandbox": true, + ] + + let handlerStub = PaymentQueueHandlerStub() + plugin.paymentQueueHandler = handlerStub + + var error: FlutterError? + + var addPaymentInvokeCount = 0 + handlerStub.addPaymentStub = { payment in + XCTAssertNotNil(payment) + XCTAssertEqual(payment.productIdentifier, "123") + XCTAssertEqual(payment.quantity, 1) + addPaymentInvokeCount += 1 + return false + } + + plugin.addPaymentPaymentMap(argument, error: &error) + + XCTAssertEqual(addPaymentInvokeCount, 1) + + guard let error = error else { + XCTFail("Error should not be nil") + return + } + + XCTAssertEqual(error.code, "storekit_duplicate_product_object") + XCTAssertEqual( + error.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." + ) + XCTAssertEqual(error.details as! NSDictionary, argument as NSDictionary) + } + + func testAddPaymentSuccessWithPaymentDiscount() { + let argument: [String: Any] = [ + "productIdentifier": "123", + "quantity": 1, + "simulatesAskToBuyInSandbox": true, + "paymentDiscount": [ + "identifier": "test_identifier", + "keyIdentifier": "test_key_identifier", + "nonce": "4a11a9cc-3bc3-11ec-8d3d-0242ac130003", + "signature": "test_signature", + "timestamp": 1_635_847_102, + ], + ] + + let handlerStub = PaymentQueueHandlerStub() + plugin.paymentQueueHandler = handlerStub + + var addPaymentInvokeCount = 0 + handlerStub.addPaymentStub = { payment in + if #available(iOS 12.2, *) { + guard let discount = payment.paymentDiscount else { + XCTFail("Discount should not be nil") + return false + } + XCTAssertEqual(discount.identifier, "test_identifier") + XCTAssertEqual(discount.keyIdentifier, "test_key_identifier") + XCTAssertEqual(discount.nonce, UUID(uuidString: "4a11a9cc-3bc3-11ec-8d3d-0242ac130003")) + XCTAssertEqual(discount.signature, "test_signature") + XCTAssertEqual(discount.timestamp, 1_635_847_102) + addPaymentInvokeCount += 1 + return true + } else { + addPaymentInvokeCount += 1 + return true + } + } + + var error: FlutterError? + + plugin.addPaymentPaymentMap(argument, error: &error) + XCTAssertEqual(addPaymentInvokeCount, 1) + XCTAssertNil(error) + } + + func testAddPaymentFailureWithInvalidPaymentDiscount() { + if #available(iOS 12.2, *) { + let invalidDiscount: [String: Any] = [ + "productIdentifier": "123", + "quantity": 1, + "simulatesAskToBuyInSandbox": true, + "paymentDiscount": [ + // This payment discount is missing the field `identifier`, and is thus malformed + "keyIdentifier": "test_key_identifier", + "nonce": "4a11a9cc-3bc3-11ec-8d3d-0242ac130003", + "signature": "test_signature", + "timestamp": 1_635_847_102, + ], + ] + + let handlerStub = PaymentQueueHandlerStub() + + var addPaymentCount = 0 + handlerStub.addPaymentStub = { payment in + addPaymentCount += 1 + return true + } + + plugin.paymentQueueHandler = handlerStub + var error: FlutterError? + + plugin.addPaymentPaymentMap(invalidDiscount, error: &error) + + guard let error = error else { + XCTFail("Error should not be nil") + return + } + + XCTAssertEqual(error.code, "storekit_invalid_payment_discount_object") + XCTAssertEqual( + error.message, + "You have requested a payment and specified a payment discount with invalid properties. When specifying a payment discount the 'identifier' field is mandatory." + ) + XCTAssertEqual(error.details as! NSDictionary, invalidDiscount as NSDictionary) + XCTAssertEqual(addPaymentCount, 0) + } + } + + func testAddPaymentWithNullSandboxArgument() { + let argument: [String: Any] = [ + "productIdentifier": "123", + "quantity": 1, + "simulatesAskToBuyInSandbox": NSNull(), + ] + + let handlerStub = PaymentQueueHandlerStub() + plugin.paymentQueueHandler = handlerStub + var error: FlutterError? + + var addPaymentInvokeCount = 0 + handlerStub.addPaymentStub = { payment in + XCTAssertEqual(payment.simulatesAskToBuyInSandbox, false) + addPaymentInvokeCount += 1 + return true + } + + plugin.addPaymentPaymentMap(argument, error: &error) + XCTAssertEqual(addPaymentInvokeCount, 1) + } + + func testRestoreTransactions() { + let expectation = self.expectation(description: "result successfully restore transactions") + + let cacheStub = TransactionCacheStub() + let queueStub = PaymentQueueStub() + + var callbackInvoked = false + plugin.paymentQueueHandler = FIAPaymentQueueHandler( + queue: queueStub, + transactionsUpdated: { transactions in }, + transactionRemoved: nil, + restoreTransactionFailed: nil, + restoreCompletedTransactionsFinished: { + callbackInvoked = true + expectation.fulfill() + }, + shouldAddStorePayment: nil, + updatedDownloads: nil, + transactionCache: cacheStub) + queueStub.add(plugin.paymentQueueHandler!) + + var error: FlutterError? + plugin.restoreTransactionsApplicationUserName(nil, error: &error) + + waitForExpectations(timeout: 5, handler: nil) + XCTAssertTrue(callbackInvoked) + } + + func testRetrieveReceiptDataSuccess() { + var error: FlutterError? + let result = plugin.retrieveReceiptDataWithError(&error) + XCTAssertNotNil(result) + } + + func testRetrieveReceiptDataNil() { + receiptManagerStub.returnNilURL = true + + var error: FlutterError? + let result = plugin.retrieveReceiptDataWithError(&error) + XCTAssertNil(result) + } + + func testRetrieveReceiptDataError() { + receiptManagerStub.returnError = true + var error: FlutterError? + let result = plugin.retrieveReceiptDataWithError(&error) + + XCTAssertNil(result) + XCTAssertNotNil(error) + + guard let error = error else { + XCTFail("Error should not be nil") + return + } + + guard let details = error.details as? [String: Any], + let errorDetails = details["error"] as? [String: Any], + let errorCode = errorDetails["code"] as? NSNumber + else { + XCTFail("Error details are not in the correct format") + return + } + + XCTAssertEqual(errorCode, 99) + } + + func testRefreshReceiptRequest() { + let expectation = self.expectation(description: "completion handler successfully called") + + let handlerStub = RequestHandlerStub() + let plugin = InAppPurchasePlugin(receiptManager: receiptManagerStub) { request in + return handlerStub + } + + let receiptError = NSError( + domain: "errorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "description"]) + + handlerStub.startProductRequestWithCompletionHandlerStub = { completion in + completion(nil, receiptError) + } + + plugin.refreshReceiptReceiptProperties(nil) { error in + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testRefreshReceiptRequestWithParams() { + let properties = [ + "isExpired": false, + "isRevoked": false, + "isVolumePurchase": false, + ] + let expectation = self.expectation(description: "completion handler successfully called") + + let handlerStub = RequestHandlerStub() + let plugin = InAppPurchasePlugin(receiptManager: receiptManagerStub) { request in + return handlerStub + } + + let receiptError = NSError( + domain: "errorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "description"]) + + handlerStub.startProductRequestWithCompletionHandlerStub = { completion in + completion(nil, receiptError) + } + + plugin.refreshReceiptReceiptProperties(properties) { error in + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + func testRefreshReceiptRequestWithError() { + let properties: [String: Any] = [ + "isExpired": false, + "isRevoked": false, + "isVolumePurchase": false, + ] + + let expectation = self.expectation(description: "completion handler successfully called") + + let handlerStub = RequestHandlerStub() + let plugin = InAppPurchasePlugin(receiptManager: receiptManagerStub) { request in + return handlerStub + } + + let receiptError = NSError( + domain: "errorDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "description"]) + + handlerStub.startProductRequestWithCompletionHandlerStub = { completion in + completion(nil, receiptError) + } + + plugin.refreshReceiptReceiptProperties(properties) { error in + XCTAssertNotNil(error) + XCTAssertEqual(error?.code, "storekit_refreshreceiptrequest_platform_error") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + #if os(iOS) + func testPresentCodeRedemptionSheet() { + let handlerStub = PaymentQueueHandlerStub() + plugin.paymentQueueHandler = handlerStub + + var presentCodeRedemptionSheetCount = 0 + handlerStub.presentCodeRedemptionSheetStub = { + presentCodeRedemptionSheetCount += 1 + } + + var error: FlutterError? + plugin.presentCodeRedemptionSheetWithError(&error) + + XCTAssertEqual(presentCodeRedemptionSheetCount, 1) + } + #endif + + func testGetPendingTransactions() { + let queueStub = PaymentQueueStub() + let cacheStub = TransactionCacheStub() + let transactionMap: [String: Any] = [ + "transactionIdentifier": "567", + "transactionState": SKPaymentTransactionState.purchasing.rawValue, + "payment": NSNull(), + "error": FIAObjectTranslator.getMapFrom( + NSError(domain: "test_stub", code: 123, userInfo: [:])), + "transactionTimeStamp": NSDate().timeIntervalSince1970, + "originalTransaction": NSNull(), + ] + queueStub.transactions = [SKPaymentTransactionStub(map: transactionMap)] + plugin.paymentQueueHandler = FIAPaymentQueueHandler( + queue: queueStub, + transactionsUpdated: nil, + transactionRemoved: nil, + restoreTransactionFailed: nil, + restoreCompletedTransactionsFinished: nil, + shouldAddStorePayment: nil, + updatedDownloads: nil, + transactionCache: cacheStub) + + var error: FlutterError? + let original = SKPaymentTransactionStub(map: transactionMap) + let originalPigeon = FIAObjectTranslator.convertTransaction(toPigeon: original) + + guard let result = plugin.transactionsWithError(&error)?.first else { + XCTFail("Expected a transaction but got nil") + return + } + XCTAssertEqual(result.payment, originalPigeon?.payment) + XCTAssertEqual(result.transactionIdentifier, originalPigeon?.transactionIdentifier) + XCTAssertEqual(result.transactionState, originalPigeon?.transactionState) + XCTAssertEqual(result.transactionTimeStamp, originalPigeon?.transactionTimeStamp) + XCTAssertEqual(result.originalTransaction, originalPigeon?.originalTransaction) + XCTAssertEqual(result.error?.domain, originalPigeon?.error?.domain) + XCTAssertEqual(result.error?.code, originalPigeon?.error?.code) + } + + func testStartObservingPaymentQueue() { + let handlerStub = PaymentQueueHandlerStub() + plugin.paymentQueueHandler = handlerStub + + var startObservingCount = 0 + handlerStub.startObservingPaymentQueueStub = { + startObservingCount += 1 + } + + var error: FlutterError? + plugin.startObservingPaymentQueueWithError(&error) + + XCTAssertEqual(startObservingCount, 1) + } + + func testStopObservingPaymentQueue() { + let handlerStub = PaymentQueueHandlerStub() + plugin.paymentQueueHandler = handlerStub + + var stopObservingCount = 0 + handlerStub.stopObservingPaymentQueueStub = { + stopObservingCount += 1 + } + + var error: FlutterError? + plugin.stopObservingPaymentQueueWithError(&error) + + XCTAssertEqual(stopObservingCount, 1) + } + + #if os(iOS) + func testRegisterPaymentQueueDelegate() { + let cacheStub = TransactionCacheStub() + let queueStub = PaymentQueueStub() + + if #available(iOS 13, *) { + plugin.paymentQueueHandler = FIAPaymentQueueHandler( + queue: queueStub, + transactionsUpdated: nil, + transactionRemoved: nil, + restoreTransactionFailed: nil, + restoreCompletedTransactionsFinished: nil, + shouldAddStorePayment: nil, + updatedDownloads: nil, + transactionCache: cacheStub) + + plugin.registrar = FlutterPluginRegistrarStub() + + // Verify the delegate is nil before we register one. + XCTAssertNil(plugin.paymentQueueHandler?.delegate) + + var error: FlutterError? + plugin.registerPaymentQueueDelegateWithError(&error) + + // Verify the delegate is not nil after we registered one. + XCTAssertNotNil(plugin.paymentQueueHandler?.delegate) + } + } + + func testRemovePaymentQueueDelegate() { + if #available(iOS 13, *) { + let cacheStub = TransactionCacheStub() + let queueStub = PaymentQueueStub() + + plugin.paymentQueueHandler = FIAPaymentQueueHandler( + queue: queueStub, + transactionsUpdated: nil, + transactionRemoved: nil, + restoreTransactionFailed: nil, + restoreCompletedTransactionsFinished: nil, + shouldAddStorePayment: nil, + updatedDownloads: nil, + transactionCache: cacheStub) + + plugin.registrar = FlutterPluginRegistrarStub() + + // Verify the delegate is nil before we register one. + XCTAssertNil(plugin.paymentQueueHandler?.delegate) + + var error: FlutterError? + plugin.registerPaymentQueueDelegateWithError(&error) + + // Verify the delegate is not nil before removing it. + XCTAssertNotNil(plugin.paymentQueueHandler?.delegate) + + plugin.removePaymentQueueDelegateWithError(&error) + + // Verify the delegate is nil after removing it. + XCTAssertNil(plugin.paymentQueueHandler?.delegate) + } + } + #endif + + func testHandleTransactionsUpdated() { + let transactionMap: [String: Any] = [ + "transactionIdentifier": "567", + "transactionState": SKPaymentTransactionState.purchasing.rawValue, + "payment": NSNull(), + "error": FIAObjectTranslator.getMapFrom( + NSError(domain: "test_stub", code: 123, userInfo: [:])), + "transactionTimeStamp": NSDate().timeIntervalSince1970, + ] + + let channelStub = MethodChannelStub() + var invokeMethodCount = 0 + + channelStub.invokeMethodChannelStub = { method, arguments in + XCTAssertEqual(method, "updatedTransactions") + XCTAssertNotNil(arguments) + invokeMethodCount += 1 + } + + let plugin = InAppPurchasePlugin( + receiptManager: receiptManagerStub, transactionCallbackChannel: channelStub) + + let paymentTransactionStub = SKPaymentTransactionStub(map: transactionMap) + let array = [paymentTransactionStub] + + plugin.handleTransactionsUpdated(array) + XCTAssertEqual(invokeMethodCount, 1) + } + + func testHandleTransactionsRemoved() { + let transactionMap: [String: Any] = [ + "transactionIdentifier": "567", + "transactionState": SKPaymentTransactionState.purchasing.rawValue, + "payment": NSNull(), + "error": FIAObjectTranslator.getMapFrom( + NSError(domain: "test_stub", code: 123, userInfo: [:])), + "transactionTimeStamp": NSDate().timeIntervalSince1970, + ] + + let paymentTransactionStub = SKPaymentTransactionStub(map: transactionMap) + let array = [paymentTransactionStub] + let maps = [FIAObjectTranslator.getMapFrom(paymentTransactionStub)] + + let channelStub = MethodChannelStub() + var invokeMethodCount = 0 + + channelStub.invokeMethodChannelStub = { method, arguments in + XCTAssertEqual(method, "removedTransactions") + XCTAssertEqual(arguments as! NSObject, maps as NSObject) + invokeMethodCount += 1 + } + + let plugin = InAppPurchasePlugin( + receiptManager: receiptManagerStub, transactionCallbackChannel: channelStub) + + plugin.handleTransactionsRemoved(array) + XCTAssertEqual(invokeMethodCount, 1) + } + + func testHandleTransactionRestoreFailed() { + let channelStub = MethodChannelStub() + var invokeMethodCount = 0 + let error = NSError(domain: "error", code: 0, userInfo: nil) + + channelStub.invokeMethodChannelStub = { method, arguments in + XCTAssertEqual(method, "restoreCompletedTransactionsFailed") + XCTAssertEqual(arguments as! NSObject, FIAObjectTranslator.getMapFrom(error) as NSObject) + invokeMethodCount += 1 + } + + let plugin = InAppPurchasePlugin( + receiptManager: receiptManagerStub, transactionCallbackChannel: channelStub) + + plugin.handleTransactionRestoreFailed(error) + XCTAssertEqual(invokeMethodCount, 1) + } + + func testRestoreCompletedTransactionsFinished() { + let channelStub = MethodChannelStub() + var invokeMethodCount = 0 + + channelStub.invokeMethodChannelStub = { method, arguments in + XCTAssertEqual(method, "paymentQueueRestoreCompletedTransactionsFinished") + XCTAssertNil(arguments) + invokeMethodCount += 1 + } + + let plugin = InAppPurchasePlugin( + receiptManager: receiptManagerStub, transactionCallbackChannel: channelStub) + + plugin.restoreCompletedTransactionsFinished() + XCTAssertEqual(invokeMethodCount, 1) + } + + func testShouldAddStorePayment() { + let paymentMap: [String: Any] = [ + "productIdentifier": "123", + "requestData": "abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", + "quantity": 2, + "applicationUsername": "app user name", + "simulatesAskToBuyInSandbox": false, + ] + + let productMap: [String: Any] = [ + "price": "1", + "priceLocale": FIAObjectTranslator.getMapFrom(Locale.current), + "productIdentifier": "123", + "localizedTitle": "title", + "localizedDescription": "des", + ] + + let payment = FIAObjectTranslator.getSKMutablePayment(fromMap: paymentMap) + let productStub = SKProductStub(map: productMap) + + let args: [String: Any] = [ + "payment": FIAObjectTranslator.getMapFrom(payment), + "product": FIAObjectTranslator.getMapFrom(productStub), + ] + + let channelStub = MethodChannelStub() + + var invokeMethodCount = 0 + channelStub.invokeMethodChannelStub = { method, arguments in + XCTAssertEqual(method, "shouldAddStorePayment") + XCTAssertEqual(arguments as! NSObject, args as NSObject) + invokeMethodCount += 1 + } + + let plugin = InAppPurchasePlugin( + receiptManager: receiptManagerStub, transactionCallbackChannel: channelStub) + + let result = plugin.shouldAddStorePayment(payment: payment, product: productStub) + XCTAssertEqual(result, false) + XCTAssertEqual(invokeMethodCount, 1) + } + #if os(iOS) + func testShowPriceConsentIfNeeded() { + let cacheStub = TransactionCacheStub() + let queueStub = PaymentQueueStub() + plugin.paymentQueueHandler = FIAPaymentQueueHandler( + queue: queueStub, + transactionsUpdated: nil, + transactionRemoved: nil, + restoreTransactionFailed: nil, + restoreCompletedTransactionsFinished: nil, + shouldAddStorePayment: nil, + updatedDownloads: nil, + transactionCache: cacheStub) + + var error: FlutterError? + var showPriceConsentIfNeededCount = 0 + + queueStub.showPriceConsentIfNeededStub = { + showPriceConsentIfNeededCount += 1 + } + + plugin.showPriceConsentIfNeededWithError(&error) + + if #available(iOS 13.4, *) { + XCTAssertEqual(showPriceConsentIfNeededCount, 1) + } else { + XCTAssertEqual(showPriceConsentIfNeededCount, 0) + } + } + #endif +} diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.h index 8e7769c5b5a1..422711b13f12 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.h @@ -117,8 +117,8 @@ API_AVAILABLE(ios(13.0), macos(10.15)) @interface MethodChannelStub : NSObject // Stubs -@property(nonatomic, copy, nullable) void (^invokeMethodChannelStub)(NSString *method, id arguments) - ; +@property(nonatomic, copy, nullable) void (^invokeMethodChannelStub) + (NSString *method, id _Nullable arguments); @property(nonatomic, copy, nullable) void (^invokeMethodChannelWithResultsStub) (NSString *method, id arguments, FlutterResult _Nullable); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml index bcb280781d08..2bc2c6d73590 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_storekit description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_storekit issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.17 +version: 0.3.17+1 environment: sdk: ^3.2.3