diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 3ccbb28c31..63653ec5d1 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -1000,6 +1000,9 @@ F5FCD3EA27DA0D0B003BDC04 /* PriceFormatterProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5FCD3E927DA0D0B003BDC04 /* PriceFormatterProvider.swift */; }; F5FCD3FC27DA2034003BDC04 /* PriceFormatterProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5FCD3FB27DA2034003BDC04 /* PriceFormatterProviderTests.swift */; }; FD18ED4E2837F89200C5AA4F /* StoreKitWorkaroundsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD18ED4D2837F89200C5AA4F /* StoreKitWorkaroundsTests.swift */; }; + FD20472A2CC19DCD00166727 /* OfferCodeRedemptionSheetPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2047292CC19DCD00166727 /* OfferCodeRedemptionSheetPresenter.swift */; }; + FD2047462CC2D62C00166727 /* MockOfferCodeSheetPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2047452CC2D62C00166727 /* MockOfferCodeSheetPresenter.swift */; }; + FD2047472CC2D62C00166727 /* MockOfferCodeSheetPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2047452CC2D62C00166727 /* MockOfferCodeSheetPresenter.swift */; }; FD2E6C9F2C480FF000CB4BD7 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = FD2E6C9E2C480FF000CB4BD7 /* OHHTTPStubs */; }; FD2E6CA12C48100900CB4BD7 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD2E6CA02C48100900CB4BD7 /* OHHTTPStubsSwift */; }; FD43D2FC2C41864000077235 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43D2FA2C4185B700077235 /* TimeInterval+Extensions.swift */; }; @@ -2202,6 +2205,8 @@ F5FCD3E927DA0D0B003BDC04 /* PriceFormatterProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceFormatterProvider.swift; sourceTree = ""; }; F5FCD3FB27DA2034003BDC04 /* PriceFormatterProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceFormatterProviderTests.swift; sourceTree = ""; }; FD18ED4D2837F89200C5AA4F /* StoreKitWorkaroundsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitWorkaroundsTests.swift; sourceTree = ""; }; + FD2047292CC19DCD00166727 /* OfferCodeRedemptionSheetPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferCodeRedemptionSheetPresenter.swift; sourceTree = ""; }; + FD2047452CC2D62C00166727 /* MockOfferCodeSheetPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOfferCodeSheetPresenter.swift; sourceTree = ""; }; FD43D2FA2C4185B700077235 /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = ""; }; FD43D2FD2C41867600077235 /* TimeInterval+ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+ExtensionsTests.swift"; sourceTree = ""; }; FDAADFCA2BE2A5BF00BD1659 /* MockAllTransactionsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAllTransactionsProvider.swift; sourceTree = ""; }; @@ -2472,6 +2477,7 @@ 2D1015D9275959840086173F /* StoreTransaction.swift */, 2D1015DD275A57FC0086173F /* SubscriptionPeriod.swift */, 4F174F462B07EA7E00FE538E /* StorefrontProvider.swift */, + FD2047292CC19DCD00166727 /* OfferCodeRedemptionSheetPresenter.swift */, ); path = StoreKitAbstractions; sourceTree = ""; @@ -3199,6 +3205,7 @@ 35316DA82BD14BFD00E4A970 /* MockDiagnosticsSynchronizer.swift */, FDAADFCA2BE2A5BF00BD1659 /* MockAllTransactionsProvider.swift */, FDAADFD22BE2B99900BD1659 /* MockStoreKit2ObserverModePurchaseDetector.swift */, + FD2047452CC2D62C00166727 /* MockOfferCodeSheetPresenter.swift */, ); path = Mocks; sourceTree = ""; @@ -5448,6 +5455,7 @@ A563F589271E1DAD00246E0C /* MockBeginRefundRequestHelper.swift in Sources */, 57F3C10C29B7EAE90004FD7E /* MockUserDefaults.swift in Sources */, 35316DAA2BD14BFD00E4A970 /* MockDiagnosticsSynchronizer.swift in Sources */, + FD2047472CC2D62C00166727 /* MockOfferCodeSheetPresenter.swift in Sources */, 2D90F8B426FD208B009B9142 /* MockStoreKit1Wrapper.swift in Sources */, FD9F982D2BE28A7F0091A5BF /* MockNotificationCenter.swift in Sources */, 2D90F8BB26FD20BD009B9142 /* MockIdentityManager.swift in Sources */, @@ -5552,6 +5560,7 @@ 4FBBC5682A61E42F0077281F /* NonEmptyStringDecodable.swift in Sources */, 2DDF41A324F6F331005BC22D /* PurchasesReceiptParser.swift in Sources */, 2CB8CF9327BF538F00C34DE3 /* PlatformInfo.swift in Sources */, + FD20472A2CC19DCD00166727 /* OfferCodeRedemptionSheetPresenter.swift in Sources */, 35D832F4262E606500E60AC5 /* HTTPResponse.swift in Sources */, 352B7D7927BD919B002A47DD /* DangerousSettings.swift in Sources */, A56F9AB126990E9200AFC48F /* CustomerInfo.swift in Sources */, @@ -5921,6 +5930,7 @@ 351B51C226D450E800BD2BD7 /* ProductRequestDataTests.swift in Sources */, 4F54DF422A1D8D0700FD72BF /* MockTransactionPoster.swift in Sources */, 4FFFE6C62AA9465000B2955C /* MockPaywallEventsManager.swift in Sources */, + FD2047462CC2D62C00166727 /* MockOfferCodeSheetPresenter.swift in Sources */, 351B51BE26D450E800BD2BD7 /* CustomerInfoTests.swift in Sources */, 35272E1B26D0029300F22C3B /* DeviceCacheSubscriberAttributesTests.swift in Sources */, 5796A39627D6BDAB00653165 /* BackendPostOfferForSigningTests.swift in Sources */, diff --git a/Sources/Logging/Strings/StoreKitStrings.swift b/Sources/Logging/Strings/StoreKitStrings.swift index 7e72d7eaa1..ad23c463cb 100644 --- a/Sources/Logging/Strings/StoreKitStrings.swift +++ b/Sources/Logging/Strings/StoreKitStrings.swift @@ -85,6 +85,14 @@ enum StoreKitStrings { case error_displaying_store_message(Error) + case error_displaying_offer_code_redemption_sheet(Error) + + case not_displaying_offer_code_redemption_sheet_because_ios_app_on_macos + + case error_displaying_offer_code_redemption_sheet_no_window_scene + + case error_displaying_offer_code_redemption_sheet_unavailable_in_app_extension + } extension StoreKitStrings: LogMessage { @@ -200,6 +208,20 @@ extension StoreKitStrings: LogMessage { case let .error_displaying_store_message(error): return "Error displaying StoreKit message: '\(error)'" + + case let .error_displaying_offer_code_redemption_sheet(error): + return "Error displaying Offer Code redemption sheet: '\(error)'" + + case .error_displaying_offer_code_redemption_sheet_no_window_scene: + return "Could not display the Offer Code redemption sheet: could not " + + "determine the UIWindowScene to present the sheet over. Please try " + + "passing in the UIWindowScene that you'd like to present the sheet over." + + case .error_displaying_offer_code_redemption_sheet_unavailable_in_app_extension: + return "Could not display the Offer Code redemption sheet: only available on iOS 16+" + + case .not_displaying_offer_code_redemption_sheet_because_ios_app_on_macos: + return "Could not display the Offer Code redemption sheet: not supported in iOS apps running on macOS." } } diff --git a/Sources/Misc/Deprecations.swift b/Sources/Misc/Deprecations.swift index ee6fe0077f..1888ecb6bb 100644 --- a/Sources/Misc/Deprecations.swift +++ b/Sources/Misc/Deprecations.swift @@ -261,6 +261,29 @@ public extension Purchases { } +extension Purchases { + #if os(iOS) || VISION_OS + @available( + iOS, + introduced: 14.0, + deprecated, + renamed: "presentCodeRedemptionSheet(_:)", + message: "Use async/throwing version instead" + ) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + @available(macOS, unavailable) + @available(macCatalyst, unavailable) + @objc public func presentCodeRedemptionSheet() { + Task { + #if !targetEnvironment(macCatalyst) + try await self.presentOfferCodeRedemptionSheet(uiWindowScene: nil) + #endif + } + } + #endif +} + public extension StoreProduct { @available(iOS, introduced: 13.0, deprecated, renamed: "eligiblePromotionalOffers()") diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 1662142b5a..ea1c34e635 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -265,6 +265,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void fileprivate let systemInfo: SystemInfo private let storeMessagesHelper: StoreMessagesHelperType? private var customerInfoObservationDisposable: (() -> Void)? + private let offerCodeRedemptionSheetPresenter: OfferCodeRedemptionSheetPresenterType private let syncAttributesAndOfferingsIfNeededRateLimiter = RateLimiter(maxCalls: 5, period: 60) @@ -307,6 +308,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void let purchasedProductsFetcher = OfflineCustomerInfoCreator.createPurchasedProductsFetcherIfAvailable() let transactionFetcher = StoreKit2TransactionFetcher() + let offerCodeRedemptionSheetPresenter = OfferCodeRedemptionSheetPresenter() let diagnosticsFileHandler: DiagnosticsFileHandlerType? = { guard diagnosticsEnabled, #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) else { return nil } @@ -570,7 +572,8 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void purchasesOrchestrator: purchasesOrchestrator, purchasedProductsFetcher: purchasedProductsFetcher, trialOrIntroPriceEligibilityChecker: trialOrIntroPriceChecker, - storeMessagesHelper: storeMessagesHelper + storeMessagesHelper: storeMessagesHelper, + offerCodeRedemptionSheetPresenter: offerCodeRedemptionSheetPresenter ) } @@ -599,7 +602,8 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void purchasesOrchestrator: PurchasesOrchestrator, purchasedProductsFetcher: PurchasedProductsFetcherType?, trialOrIntroPriceEligibilityChecker: CachingTrialOrIntroPriceEligibilityChecker, - storeMessagesHelper: StoreMessagesHelperType? + storeMessagesHelper: StoreMessagesHelperType?, + offerCodeRedemptionSheetPresenter: OfferCodeRedemptionSheetPresenterType ) { if systemInfo.dangerousSettings.customEntitlementComputation { @@ -647,6 +651,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void self.purchasedProductsFetcher = purchasedProductsFetcher self.trialOrIntroPriceEligibilityChecker = trialOrIntroPriceEligibilityChecker self.storeMessagesHelper = storeMessagesHelper + self.offerCodeRedemptionSheetPresenter = offerCodeRedemptionSheetPresenter super.init() @@ -1062,15 +1067,44 @@ public extension Purchases { } #endif -#if os(iOS) || VISION_OS + #if (os(iOS) || VISION_OS) && !targetEnvironment(macCatalyst) @available(iOS 14.0, *) @available(watchOS, unavailable) @available(tvOS, unavailable) @available(macOS, unavailable) + @available(macCatalyst 16.0, *) + @objc func presentCodeRedemptionSheet( + uiWindowScene: UIWindowScene? = nil + ) async throws { + try await self.presentOfferCodeRedemptionSheet(uiWindowScene: uiWindowScene) + } + + @available(iOS 14.0, *) @available(macCatalyst, unavailable) - @objc func presentCodeRedemptionSheet() { - self.paymentQueueWrapper.paymentQueueWrapperType.presentCodeRedemptionSheet() + @available(watchOS, unavailable) + @available(tvOS, unavailable) + @available(macOS, unavailable) + internal func presentOfferCodeRedemptionSheet(uiWindowScene: UIWindowScene?) async throws { + var windowScene = uiWindowScene + if windowScene == nil { + windowScene = try await systemInfo.currentWindowScene + } + + guard let windowScene else { + Logger.error(Strings.storeKit.error_displaying_offer_code_redemption_sheet_no_window_scene) + return + } + + do { + try await self.offerCodeRedemptionSheetPresenter.presentCodeRedemptionSheet( + windowScene: windowScene, + storeKitVersion: self.systemInfo.storeKitVersion + ) + } catch { + Logger.error(Strings.storeKit.error_displaying_offer_code_redemption_sheet(error)) + throw error + } } #endif diff --git a/Sources/Purchasing/Purchases/PurchasesType.swift b/Sources/Purchasing/Purchases/PurchasesType.swift index 60fc450381..706515c014 100644 --- a/Sources/Purchasing/Purchases/PurchasesType.swift +++ b/Sources/Purchasing/Purchases/PurchasesType.swift @@ -679,6 +679,28 @@ public protocol PurchasesType: AnyObject { #if os(iOS) || VISION_OS + /** + * Displays a sheet that enables users to redeem subscription offer codes that you generated in App Store Connect. + * + * - Important: Even though the docs in `SKPaymentQueue.presentCodeRedemptionSheet` + * say that it's available on Catalyst 14.0, there is a note: + * "`This function doesn’t affect Mac apps built with Mac Catalyst.`" + * when, in fact, it crashes when called both from Catalyst and also when running as "Designed for iPad". + * This is why RevenueCat's SDK makes it unavailable in mac catalyst. + */ + @available( + iOS, + introduced: 14.0, + deprecated, + renamed: "presentCodeRedemptionSheet(_:)", + message: "Use async/throwing version instead" + ) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + @available(macOS, unavailable) + @available(macCatalyst, unavailable) + func presentCodeRedemptionSheet() + /** * Presents a refund request sheet in the current window scene for * the latest transaction associated with the `productID` @@ -736,22 +758,6 @@ public protocol PurchasesType: AnyObject { #endif - /** - * Displays a sheet that enables users to redeem subscription offer codes that you generated in App Store Connect. - * - * - Important: Even though the docs in `SKPaymentQueue.presentCodeRedemptionSheet` - * say that it's available on Catalyst 14.0, there is a note: - * "`This function doesn’t affect Mac apps built with Mac Catalyst.`" - * when, in fact, it crashes when called both from Catalyst and also when running as "Designed for iPad". - * This is why RevenueCat's SDK makes it unavailable in mac catalyst. - */ - @available(iOS 14.0, *) - @available(watchOS, unavailable) - @available(tvOS, unavailable) - @available(macOS, unavailable) - @available(macCatalyst, unavailable) - func presentCodeRedemptionSheet() - #if os(iOS) || targetEnvironment(macCatalyst) || VISION_OS /** * Displays price consent sheet if needed. You only need to call this manually if you implement @@ -771,6 +777,24 @@ public protocol PurchasesType: AnyObject { */ @available(iOS 13.4, macCatalyst 13.4, *) @objc func showPriceConsentIfNeeded() + + /** + * Displays a sheet that enables users to redeem subscription offer codes that you generated in App Store Connect. + * + * - Important: Even though the docs in `AppStore.presentOfferCodeRedeemSheet(in:)` + * say that it's available on Catalyst 16.0+, there is a note: + * "`In Mac apps built with Mac Catalyst, this method throws a StoreKitError.unknown error.`". + * The function also throws a StoreKitError.unknown error when running as "Designed for iPad" on macOS. + * + * For these reasons, RevenueCat's SDK makes this function unavailable on Mac Catalyst, and it no-ops + * for iOS apps running as a "Designed for iPad" app on macOS. + */ + @available(iOS 14.0, *) + @available(macCatalyst, unavailable) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + @available(macOS, unavailable) + func presentCodeRedemptionSheet(uiWindowScene: UIWindowScene?) async throws #endif #if os(iOS) || os(macOS) || VISION_OS diff --git a/Sources/Purchasing/StoreKit1/PaymentQueueWrapper.swift b/Sources/Purchasing/StoreKit1/PaymentQueueWrapper.swift index 67bf7a168c..378eb6d6d6 100644 --- a/Sources/Purchasing/StoreKit1/PaymentQueueWrapper.swift +++ b/Sources/Purchasing/StoreKit1/PaymentQueueWrapper.swift @@ -38,13 +38,6 @@ protocol PaymentQueueWrapperType: AnyObject { func showPriceConsentIfNeeded() #endif - @available(iOS 14.0, *) - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - @available(macCatalyst, unavailable) - func presentCodeRedemptionSheet() - } /// The choice between SK1's `StoreKit1Wrapper` or `PaymentQueueWrapper` when SK2 is enabled. @@ -92,13 +85,6 @@ class PaymentQueueWrapper: NSObject, PaymentQueueWrapperType { } #endif - #if (os(iOS) && !targetEnvironment(macCatalyst)) || VISION_OS - @available(iOS 14.0, *) - func presentCodeRedemptionSheet() { - self.paymentQueue.presentCodeRedemptionSheetIfAvailable() - } - #endif - } extension PaymentQueueWrapper: SKPaymentQueueDelegate { diff --git a/Sources/Purchasing/StoreKitAbstractions/OfferCodeRedemptionSheetPresenter.swift b/Sources/Purchasing/StoreKitAbstractions/OfferCodeRedemptionSheetPresenter.swift new file mode 100644 index 0000000000..e6ea71c31d --- /dev/null +++ b/Sources/Purchasing/StoreKitAbstractions/OfferCodeRedemptionSheetPresenter.swift @@ -0,0 +1,117 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// OfferCodeRedemptionSheetPresenter.swift +// +// Created by Will Taylor on 10/17/24. + +import Foundation +import StoreKit + +@objc protocol OfferCodeRedemptionSheetPresenterType: Sendable { + + #if (os(iOS) || VISION_OS) && !targetEnvironment(macCatalyst) + @available(iOS 14.0, *) + @available(tvOS, unavailable) + @available(macOS, unavailable) + @available(macCatalyst, unavailable) + func presentCodeRedemptionSheet( + windowScene: UIWindowScene, + storeKitVersion: StoreKitVersion + ) async throws + #endif +} + +final internal class OfferCodeRedemptionSheetPresenter: OfferCodeRedemptionSheetPresenterType, Sendable { + + private let paymentQueue: SKPaymentQueue + private let isiOSAppOnMac: Bool + private let osMajorVersion: Int + + init( + paymentQueue: SKPaymentQueue = .default(), + isiOSAppOnMac: Bool = { + if #available( + iOS 14.0, + iOSApplicationExtension 14.0, + macOS 11.0, + watchOS 7.0, + tvOS 14.0, + * + ) { + return ProcessInfo().isiOSAppOnMac + } else { + return false + } + }(), + osMajorVersion: Int = { + return ProcessInfo().operatingSystemVersion.majorVersion + }() + ) { + self.paymentQueue = paymentQueue + self.isiOSAppOnMac = isiOSAppOnMac + self.osMajorVersion = osMajorVersion + } + + #if (os(iOS) || VISION_OS) && !targetEnvironment(macCatalyst) + @available(iOS 14.0, *) + @available(tvOS, unavailable) + @available(macOS, unavailable) + @available(macCatalyst, unavailable) + func presentCodeRedemptionSheet( + windowScene: UIWindowScene, + storeKitVersion: StoreKitVersion + ) async throws { + + // Presenting the Offer Code Redemption sheet throws/crashes when running an iOS app + // as "Designed for iPad" on macOS, so we don't want to call it when the app is running on macOS + // as a "Designed for iPad" app. + if isiOSAppOnMac { + Logger.warn(Strings.storeKit.not_displaying_offer_code_redemption_sheet_because_ios_app_on_macos) + return + } + + if storeKitVersion.isStoreKit2EnabledAndAvailable { + if osMajorVersion < 16 { + // .presentOfferCodeRedeemSheet(in: windowScene) isn't available in iOS <16, so fall back + // to the SK1 implementation + self.sk1PresentCodeRedemptionSheet() + return + } + + if #available(iOS 16.0, iOSApplicationExtension 16.0, *) { + try await AppStore.presentOfferCodeRedeemSheet(in: windowScene) + } else { + // This case should be covered by the above OS check, but we'll include here + // since it's a possible code case + #if !targetEnvironment(macCatalyst) + self.sk1PresentCodeRedemptionSheet() + #else + Logger.warn(Strings.storeKit.error_displaying_offer_code_redemption_sheet_unavailable_in_app_extension) + #endif + } + } else { + self.sk1PresentCodeRedemptionSheet() + } + + } + #endif + + #if os(iOS) || VISION_OS + @available(iOS 14.0, iOSApplicationExtension 14.0, *) + @available(watchOS, unavailable) + @available(tvOS, unavailable) + @available(macOS, unavailable) + @available(macCatalyst, unavailable) + @available(macCatalystApplicationExtension, unavailable) + func sk1PresentCodeRedemptionSheet() { + self.paymentQueue.presentCodeRedemptionSheet() + } + #endif +} diff --git a/Tests/APITesters/AllAPITests/SwiftAPITester/PurchasesAPI.swift b/Tests/APITesters/AllAPITests/SwiftAPITester/PurchasesAPI.swift index 7d33ed1e18..37e9fc0d53 100644 --- a/Tests/APITesters/AllAPITests/SwiftAPITester/PurchasesAPI.swift +++ b/Tests/APITesters/AllAPITests/SwiftAPITester/PurchasesAPI.swift @@ -160,7 +160,9 @@ private func checkPurchasesPurchasingAPI(purchases: Purchases) { #if os(iOS) || VISION_OS if #available(iOS 14.0, *) { - purchases.presentCodeRedemptionSheet() + Task { + try await purchases.presentCodeRedemptionSheet(uiWindowScene: nil) + } } #endif @@ -344,6 +346,10 @@ private func checkDeprecatedMethods(_ purchases: Purchases) { purchases.logIn("") { (_: CustomerInfo?, _: Bool, _: Error?) in } + if #available(iOS 14.0, *) { + purchases.presentCodeRedemptionSheet() + } + Purchases.configure(withAPIKey: "", appUserID: "") let _: Bool = purchases.finishTransactions diff --git a/Tests/UnitTests/Mocks/MockOfferCodeSheetPresenter.swift b/Tests/UnitTests/Mocks/MockOfferCodeSheetPresenter.swift new file mode 100644 index 0000000000..c559efc297 --- /dev/null +++ b/Tests/UnitTests/Mocks/MockOfferCodeSheetPresenter.swift @@ -0,0 +1,28 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// MockOfferCodeSheetPresenter.swift +// +// Created by Will Taylor on 10/18/24. + +import Foundation + +@testable import RevenueCat +import StoreKit + +final class MockOfferCodeRedemptionSheetPresenter: OfferCodeRedemptionSheetPresenterType { + + #if (os(iOS) || VISION_OS) && !targetEnvironment(macCatalyst) + func presentCodeRedemptionSheet( + windowScene: UIWindowScene, + storeKitVersion: StoreKitVersion + ) async throws {} + #endif + +} diff --git a/Tests/UnitTests/Mocks/MockPaymentQueue.swift b/Tests/UnitTests/Mocks/MockPaymentQueue.swift index 62f9107cb4..c5d3410144 100644 --- a/Tests/UnitTests/Mocks/MockPaymentQueue.swift +++ b/Tests/UnitTests/Mocks/MockPaymentQueue.swift @@ -44,6 +44,13 @@ final class MockPaymentQueue: SKPaymentQueue { } #endif + #if os(iOS) || VISION_OS + var presentCodeRedemptionSheetCalled = false + override func presentCodeRedemptionSheet() { + presentCodeRedemptionSheetCalled = true + } + #endif + } extension MockPaymentQueue: @unchecked Sendable {} diff --git a/Tests/UnitTests/Mocks/MockPurchases.swift b/Tests/UnitTests/Mocks/MockPurchases.swift index 5400473db4..53ab80e9de 100644 --- a/Tests/UnitTests/Mocks/MockPurchases.swift +++ b/Tests/UnitTests/Mocks/MockPurchases.swift @@ -309,12 +309,16 @@ extension MockPurchases: PurchasesType { self.unimplemented() } - #if os(iOS) || VISION_OS + #if (os(iOS) || VISION_OS) && !targetEnvironment(macCatalyst) func presentCodeRedemptionSheet() { self.unimplemented() } + func presentCodeRedemptionSheet(uiWindowScene: UIWindowScene?) async throws { + self.unimplemented() + } + #endif func showPriceConsentIfNeeded() { diff --git a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift index 798eb67735..18e0a89bc9 100644 --- a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift @@ -72,6 +72,7 @@ class BasePurchasesTests: TestCase { self.mockProductEntitlementMappingFetcher = MockProductEntitlementMappingFetcher() self.mockPurchasedProductsFetcher = MockPurchasedProductsFetcher() self.mockTransactionFetcher = MockStoreKit2TransactionFetcher() + self.mockOfferCodeSheetPresenter = MockOfferCodeRedemptionSheetPresenter() let apiKey = "mockAPIKey" let httpClient = MockHTTPClient(apiKey: apiKey, @@ -187,6 +188,7 @@ class BasePurchasesTests: TestCase { var mockBeginRefundRequestHelper: MockBeginRefundRequestHelper! var mockStoreMessagesHelper: MockStoreMessagesHelper! var diagnosticsTracker: DiagnosticsTrackerType? + var mockOfferCodeSheetPresenter: MockOfferCodeRedemptionSheetPresenter! // swiftlint:disable:next weak_delegate var purchasesDelegate: MockPurchasesDelegate! @@ -298,7 +300,8 @@ class BasePurchasesTests: TestCase { purchasesOrchestrator: self.purchasesOrchestrator, purchasedProductsFetcher: self.mockPurchasedProductsFetcher, trialOrIntroPriceEligibilityChecker: self.cachingTrialOrIntroPriceEligibilityChecker, - storeMessagesHelper: self.mockStoreMessagesHelper) + storeMessagesHelper: self.mockStoreMessagesHelper, + offerCodeRedemptionSheetPresenter: mockOfferCodeSheetPresenter) self.purchasesOrchestrator.delegate = self.purchases @@ -546,6 +549,7 @@ private extension BasePurchasesTests { self.paywallCache = nil self.paywallEventsManager = nil self.purchases = nil + self.mockOfferCodeSheetPresenter = nil } } diff --git a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift index fb3140d271..11752ac56f 100644 --- a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift @@ -42,6 +42,7 @@ class PurchasesSubscriberAttributesTests: TestCase { var mockOperationDispatcher: MockOperationDispatcher! var mockIntroEligibilityCalculator: MockIntroEligibilityCalculator! var transactionPoster: TransactionPoster! + var mockOfferCodeSheetPresenter: MockOfferCodeRedemptionSheetPresenter! // swiftlint:disable:next weak_delegate var purchasesDelegate = MockPurchasesDelegate() @@ -153,6 +154,7 @@ class PurchasesSubscriberAttributesTests: TestCase { currentUserProvider: mockIdentityManager) self.mockTransactionsManager = MockTransactionsManager(receiptParser: mockReceiptParser) self.mockStoreMessagesHelper = .init() + self.mockOfferCodeSheetPresenter = .init() } override func tearDown() { @@ -221,7 +223,8 @@ class PurchasesSubscriberAttributesTests: TestCase { trialOrIntroPriceEligibilityChecker: .create( with: trialOrIntroductoryPriceEligibilityChecker ), - storeMessagesHelper: self.mockStoreMessagesHelper) + storeMessagesHelper: self.mockStoreMessagesHelper, + offerCodeRedemptionSheetPresenter: self.mockOfferCodeSheetPresenter) purchasesOrchestrator.delegate = purchases purchases!.delegate = purchasesDelegate Purchases.setDefaultInstance(purchases!)