From 1f608eeb3fe5d299e597626070c3d63ce939f56a Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Tue, 17 Dec 2024 16:29:23 -0600 Subject: [PATCH 01/23] use renewalInfo to get renewal prices --- .../Data/PurchaseInformation.swift | 105 +++++++++++++++++- .../ViewModels/CustomerCenterViewModel.swift | 2 +- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index fc8144216c..7031304a8b 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -15,6 +15,7 @@ import Foundation import RevenueCat +import StoreKit // swiftlint:disable nesting struct PurchaseInformation { @@ -47,6 +48,7 @@ struct PurchaseInformation { init(entitlement: EntitlementInfo? = nil, subscribedProduct: StoreProduct? = nil, transaction: Transaction, + renewalPrice: PriceDetails? = nil, dateFormatter: DateFormatter = DateFormatter()) { dateFormatter.dateStyle = .medium @@ -60,7 +62,11 @@ struct PurchaseInformation { self.expirationOrRenewal = entitlement.expirationOrRenewal(dateFormatter: dateFormatter) self.productIdentifier = entitlement.productIdentifier self.store = entitlement.store - self.price = entitlement.priceBestEffort(product: subscribedProduct) + if let renewalPrice { + self.price = renewalPrice + } else { + self.price = entitlement.priceBestEffort(product: subscribedProduct) + } } else { switch transaction.type { case .subscription(let isActive, let willRenew, let expiresDate): @@ -81,8 +87,15 @@ struct PurchaseInformation { self.productIdentifier = transaction.productIdentifier self.store = transaction.store - self.price = transaction.store == .promotional ? .free - : (subscribedProduct.map { .paid($0.localizedPriceString) } ?? .unknown) + if transaction.store == .promotional { + self.price = .free + } else { + if let renewalPrice { + self.price = renewalPrice + } else { + self.price = subscribedProduct.map { .paid($0.localizedPriceString) } ?? .unknown + } + } } } @@ -123,6 +136,92 @@ struct PurchaseInformation { } // swiftlint:enable nesting +extension PurchaseInformation { + + /// Provides detailed information about a user's purchase, including entitlement and renewal price. + /// + /// This function fetches the renewal price details for the given product asynchronously from + /// StoreKit 2 and constructs a `PurchaseInformation` object with the provided + /// transaction, entitlement, and subscribed product details. + /// + /// - Parameters: + /// - entitlement: Optional entitlement information associated with the purchase. + /// - subscribedProduct: The product the user has subscribed to, represented as a `StoreProduct`. + /// - transaction: The transaction information for the purchase. + /// - Returns: A `PurchaseInformation` object containing the purchase details, including the renewal price. + /// + /// - Availability: iOS 15.0+ + @available(iOS 15.0, *) + static func purchaseInformationUsingRenewalInfo( + entitlement: EntitlementInfo? = nil, + subscribedProduct: StoreProduct, + transaction: Transaction + ) async -> PurchaseInformation { + let renewalPriceDetails = await Self.extractPriceDetailsFromRenwalInfo(forProduct: subscribedProduct) + return PurchaseInformation( + entitlement: entitlement, + subscribedProduct: subscribedProduct, + transaction: transaction, + renewalPrice: renewalPriceDetails + ) + } + + @available(iOS 15.0, *) + private static func extractPriceDetailsFromRenwalInfo( + forProduct product: StoreProduct + ) async -> PriceDetails? { + guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else { + // If StoreKit.Product.subscription is nil, then the product isn't a subscription + // If statuses is empty, the subscriber was never subscribed to a product in the subscription group. + return nil + } + + guard let purchaseSubscriptionStatus = statuses.first(where: { + do { + return try $0.transaction.payloadValue.ownershipType == .purchased + } catch { + return false + } + }) else { + return nil + } + + switch purchaseSubscriptionStatus.renewalInfo { + case .unverified: + return nil + case .verified(let renewalInfo): + guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil } + guard let currencyCode = product.currencyCode else { return nil } + + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = currencyCode + + guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil } + + return .paid(formattedPrice) + } + } + + @available(iOS 15.0, *) + private static func currencyCode( + fromRenewalInfo renewalInfo: Product.SubscriptionInfo.RenewalInfo, + locale: Locale = Locale.current + ) -> String? { + let currencyCode: String + if #available(iOS 16.0, *) { + guard let currency = renewalInfo.currency else { return nil } + if currency.isISOCurrency { + return currency.identifier + } else { + return nil + } + } else { + return renewalInfo.currencyCode + } + } +} + fileprivate extension EntitlementInfo { func priceBestEffort(product: StoreProduct?) -> PurchaseInformation.PriceDetails { diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 10e8684fab..57dfd5eba5 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -209,7 +209,7 @@ private extension CustomerCenterViewModel { entitlement: EntitlementInfo?) async throws -> PurchaseInformation { if transaction.store == .appStore { if let product = await purchasesProvider.products([transaction.productIdentifier]).first { - return PurchaseInformation( + return await PurchaseInformation.purchaseInformationUsingRenewalInfo( entitlement: entitlement, subscribedProduct: product, transaction: transaction From 4d0342f0d760da56016c001fec0a22be04d83132 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Tue, 17 Dec 2024 16:45:09 -0600 Subject: [PATCH 02/23] build on non-iOS platforms --- .../Data/PurchaseInformation.swift | 75 ++++++++++++------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 7031304a8b..156353750b 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -152,6 +152,9 @@ extension PurchaseInformation { /// /// - Availability: iOS 15.0+ @available(iOS 15.0, *) + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) static func purchaseInformationUsingRenewalInfo( entitlement: EntitlementInfo? = nil, subscribedProduct: StoreProduct, @@ -167,49 +170,61 @@ extension PurchaseInformation { } @available(iOS 15.0, *) + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) private static func extractPriceDetailsFromRenwalInfo( forProduct product: StoreProduct ) async -> PriceDetails? { - guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else { - // If StoreKit.Product.subscription is nil, then the product isn't a subscription - // If statuses is empty, the subscriber was never subscribed to a product in the subscription group. - return nil - } + if #available(macOS 12.0, tvOS 15.0, *) { + guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else { + // If StoreKit.Product.subscription is nil, then the product isn't a subscription + // If statuses is empty, the subscriber was never subscribed to a product in the subscription group. + return nil + } - guard let purchaseSubscriptionStatus = statuses.first(where: { - do { - return try $0.transaction.payloadValue.ownershipType == .purchased - } catch { - return false + guard let purchaseSubscriptionStatus = statuses.first(where: { + do { + return try $0.transaction.payloadValue.ownershipType == .purchased + } catch { + return false + } + }) else { + return nil } - }) else { - return nil - } - switch purchaseSubscriptionStatus.renewalInfo { - case .unverified: - return nil - case .verified(let renewalInfo): - guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil } - guard let currencyCode = product.currencyCode else { return nil } + switch purchaseSubscriptionStatus.renewalInfo { + case .unverified: + return nil + case .verified(let renewalInfo): + guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil } + guard let currencyCode = product.currencyCode else { return nil } - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.currencyCode = currencyCode + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = currencyCode - guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil } + guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil } - return .paid(formattedPrice) + return .paid(formattedPrice) + } + } else { + return nil } } @available(iOS 15.0, *) + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) private static func currencyCode( fromRenewalInfo renewalInfo: Product.SubscriptionInfo.RenewalInfo, locale: Locale = Locale.current ) -> String? { let currencyCode: String - if #available(iOS 16.0, *) { + // macOS 13.0 check is required for the compiler despite the function being marked + // as unavailable on macOS + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) { guard let currency = renewalInfo.currency else { return nil } if currency.isISOCurrency { return currency.identifier @@ -217,7 +232,15 @@ extension PurchaseInformation { return nil } } else { - return renewalInfo.currencyCode + if #available(macOS 12.0, tvOS 15.0, *) { + #if os(visionOS) + return nil + #else + return renewalInfo.currencyCode + #endif + } else { + return nil + } } } } From 1fc7513ebf29357761a4fb267f8cb9040bb230b7 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Tue, 17 Dec 2024 17:03:29 -0600 Subject: [PATCH 03/23] compiler checks for iOS <=17 --- .../Data/PurchaseInformation.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 156353750b..70212ffa33 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -197,7 +197,16 @@ extension PurchaseInformation { case .unverified: return nil case .verified(let renewalInfo): + + // renewalInfo.renewalPrice was introduced in iOS 18.0 and backdeployed through iOS 15.0 + // However, Xcode versions <15.0 don't have the symbols, so we need to check the compiler version + // to make sure that this is being built with an Xcode version >=15.0. + #if compiler(>=6.0) guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil } + #else + return nil + #endif + guard let currencyCode = product.currencyCode else { return nil } let formatter = NumberFormatter() @@ -225,15 +234,23 @@ extension PurchaseInformation { // macOS 13.0 check is required for the compiler despite the function being marked // as unavailable on macOS if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) { + + // renewalInfo.currency was introduced in iOS 18.0 and backdeployed through iOS 16.0 + // However, Xcode versions <15.0 don't have the symbols, so we need to check the compiler version + // to make sure that this is being built with an Xcode version >=15.0. + #if compiler(>=6.0) guard let currency = renewalInfo.currency else { return nil } if currency.isISOCurrency { return currency.identifier } else { return nil } + #else + return nil + #endif } else { if #available(macOS 12.0, tvOS 15.0, *) { - #if os(visionOS) + #if os(visionOS) || compiler(<6.0) return nil #else return renewalInfo.currencyCode From b8aaec45a66a1294eba5c247e8787af8a807b78f Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Tue, 17 Dec 2024 17:08:05 -0600 Subject: [PATCH 04/23] this should do the trick --- RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 70212ffa33..7a87085675 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -203,9 +203,6 @@ extension PurchaseInformation { // to make sure that this is being built with an Xcode version >=15.0. #if compiler(>=6.0) guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil } - #else - return nil - #endif guard let currencyCode = product.currencyCode else { return nil } @@ -216,6 +213,9 @@ extension PurchaseInformation { guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil } return .paid(formattedPrice) + #else + return nil + #endif } } else { return nil From c6e7d9f469dc61cce71be8c7bfd9f97d73510fc6 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Tue, 17 Dec 2024 17:08:35 -0600 Subject: [PATCH 05/23] remove unused currencyCode --- RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 7a87085675..c4490a21b1 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -230,7 +230,6 @@ extension PurchaseInformation { fromRenewalInfo renewalInfo: Product.SubscriptionInfo.RenewalInfo, locale: Locale = Locale.current ) -> String? { - let currencyCode: String // macOS 13.0 check is required for the compiler despite the function being marked // as unavailable on macOS if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) { From 78d03c5af221ced81d2bc6c261c5a8aa48f2bf49 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Tue, 17 Dec 2024 17:29:35 -0600 Subject: [PATCH 06/23] add watchOS OS checks --- .../CustomerCenter/Data/PurchaseInformation.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index c4490a21b1..a7306f10c1 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -173,10 +173,11 @@ extension PurchaseInformation { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) + @available(watchOSApplicationExtension, unavailable) private static func extractPriceDetailsFromRenwalInfo( forProduct product: StoreProduct ) async -> PriceDetails? { - if #available(macOS 12.0, tvOS 15.0, *) { + if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, watchOSApplicationExtension 8.0, *) { guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else { // If StoreKit.Product.subscription is nil, then the product isn't a subscription // If statuses is empty, the subscriber was never subscribed to a product in the subscription group. @@ -226,13 +227,14 @@ extension PurchaseInformation { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) + @available(watchOSApplicationExtension, unavailable) private static func currencyCode( fromRenewalInfo renewalInfo: Product.SubscriptionInfo.RenewalInfo, locale: Locale = Locale.current ) -> String? { // macOS 13.0 check is required for the compiler despite the function being marked // as unavailable on macOS - if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) { + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { // renewalInfo.currency was introduced in iOS 18.0 and backdeployed through iOS 16.0 // However, Xcode versions <15.0 don't have the symbols, so we need to check the compiler version @@ -248,7 +250,7 @@ extension PurchaseInformation { return nil #endif } else { - if #available(macOS 12.0, tvOS 15.0, *) { + if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, *) { #if os(visionOS) || compiler(<6.0) return nil #else From 3aeb44f333ae349b9b7eaa9cea5c1f81688c6c9f Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Tue, 17 Dec 2024 17:30:10 -0600 Subject: [PATCH 07/23] add extra watchOSApplicationExtension available check --- RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index a7306f10c1..0636eacb61 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -155,6 +155,7 @@ extension PurchaseInformation { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) + @available(watchOSApplicationExtension, unavailable) static func purchaseInformationUsingRenewalInfo( entitlement: EntitlementInfo? = nil, subscribedProduct: StoreProduct, From b15755d6a4c02aaf711d3aaffedae2826df1d125 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Tue, 17 Dec 2024 17:31:01 -0600 Subject: [PATCH 08/23] a few more checks --- RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 0636eacb61..f399016e34 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -235,7 +235,7 @@ extension PurchaseInformation { ) -> String? { // macOS 13.0 check is required for the compiler despite the function being marked // as unavailable on macOS - if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, watchOSApplicationExtension 9.0, *) { // renewalInfo.currency was introduced in iOS 18.0 and backdeployed through iOS 16.0 // However, Xcode versions <15.0 don't have the symbols, so we need to check the compiler version @@ -251,7 +251,7 @@ extension PurchaseInformation { return nil #endif } else { - if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, watchOSApplicationExtension 8.0, *) { #if os(visionOS) || compiler(<6.0) return nil #else From d26883dba6aa24bb98206b6921c72d1f748b58ae Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 10:30:48 -0600 Subject: [PATCH 09/23] refactor to modularize code --- RevenueCat.xcodeproj/project.pbxproj | 16 ++++++ .../Data/PurchaseInformation.swift | 51 +++++------------ .../CustomerCenterStoreKitUtilities.swift | 56 +++++++++++++++++++ .../CustomerCenterStoreKitUtilitiesType.swift | 27 +++++++++ 4 files changed, 114 insertions(+), 36 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift create mode 100644 RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index c88060a944..929c6cab07 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -1061,6 +1061,8 @@ FDAC7B572CD3FD8500DFC0D9 /* MockWinBackOfferEligibilityCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAC7B562CD3FD8500DFC0D9 /* MockWinBackOfferEligibilityCalculator.swift */; }; FDAC7B582CD3FD8500DFC0D9 /* MockWinBackOfferEligibilityCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAC7B562CD3FD8500DFC0D9 /* MockWinBackOfferEligibilityCalculator.swift */; }; FDAC7B5B2CD4085800DFC0D9 /* PurchasesWinBackOfferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAC7B5A2CD4085800DFC0D9 /* PurchasesWinBackOfferTests.swift */; }; + FDAD6AC72D132DD900FB047E /* CustomerCenterStoreKitUtilitiesType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAD6AC62D132DD900FB047E /* CustomerCenterStoreKitUtilitiesType.swift */; }; + FDAD6AC92D132E6500FB047E /* CustomerCenterStoreKitUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAD6AC82D132E6500FB047E /* CustomerCenterStoreKitUtilities.swift */; }; FDC892D12CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC892D02CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift */; }; FDC892D22CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC892D02CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift */; }; FDC892FE2CD157F1000AEB9F /* WinBackOffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC892FD2CD157F1000AEB9F /* WinBackOffer.swift */; }; @@ -2308,6 +2310,8 @@ FDAC7B542CD3D7A200DFC0D9 /* WinBackOfferEligibilityCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WinBackOfferEligibilityCalculator.swift; sourceTree = ""; }; FDAC7B562CD3FD8500DFC0D9 /* MockWinBackOfferEligibilityCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWinBackOfferEligibilityCalculator.swift; sourceTree = ""; }; FDAC7B5A2CD4085800DFC0D9 /* PurchasesWinBackOfferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesWinBackOfferTests.swift; sourceTree = ""; }; + FDAD6AC62D132DD900FB047E /* CustomerCenterStoreKitUtilitiesType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterStoreKitUtilitiesType.swift; sourceTree = ""; }; + FDAD6AC82D132E6500FB047E /* CustomerCenterStoreKitUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterStoreKitUtilities.swift; sourceTree = ""; }; FDC892D02CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreKit2PurchaseIntentListener.swift; sourceTree = ""; }; FDC892FD2CD157F1000AEB9F /* WinBackOffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WinBackOffer.swift; sourceTree = ""; }; FECF627761D375C8431EB866 /* StoreProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreProduct.swift; sourceTree = ""; }; @@ -3573,6 +3577,7 @@ 353756642C382C2800A1B8D6 /* CustomerCenter */ = { isa = PBXGroup; children = ( + FDAD6AC52D132DC600FB047E /* Utilities */, 35653BD32C46803A009E8ADB /* Abstractions */, 353756562C382C2800A1B8D6 /* Data */, 3537565A2C382C2800A1B8D6 /* ViewModels */, @@ -4994,6 +4999,15 @@ path = "Win-Back Offers"; sourceTree = ""; }; + FDAD6AC52D132DC600FB047E /* Utilities */ = { + isa = PBXGroup; + children = ( + FDAD6AC62D132DD900FB047E /* CustomerCenterStoreKitUtilitiesType.swift */, + FDAD6AC82D132E6500FB047E /* CustomerCenterStoreKitUtilities.swift */, + ); + path = Utilities; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -6512,6 +6526,7 @@ 887A60BE2C1D037000E1A461 /* PaywallFooterViewController.swift in Sources */, 887A608A2C1D037000E1A461 /* PurchaseHandler.swift in Sources */, 2D2AFE8D2C6A834D00D1B0B4 /* TestData.swift in Sources */, + FDAD6AC92D132E6500FB047E /* CustomerCenterStoreKitUtilities.swift in Sources */, 887A60C92C1D037000E1A461 /* PurchaseButton.swift in Sources */, 88B1BAF02C813A3C001B7EE5 /* TextComponentViewModel.swift in Sources */, 2D2AFE912C6A9EF500D1B0B4 /* Binding+Extensions.swift in Sources */, @@ -6528,6 +6543,7 @@ 353756722C382C2800A1B8D6 /* URLUtilities.swift in Sources */, 353FDC0F2CA446FA0055F328 /* StoreProductDiscount+Extensions.swift in Sources */, 887A60862C1D037000E1A461 /* FooterHidingModifier.swift in Sources */, + FDAD6AC72D132DD900FB047E /* CustomerCenterStoreKitUtilitiesType.swift in Sources */, 887A60C02C1D037000E1A461 /* AsyncButton.swift in Sources */, 887A60892C1D037000E1A461 /* PaywallPurchasesType.swift in Sources */, 2C8EC71B2CCDD43900D6CCF8 /* ComponentViewState.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index f399016e34..485d8a304c 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -138,7 +138,7 @@ struct PurchaseInformation { extension PurchaseInformation { - /// Provides detailed information about a user's purchase, including entitlement and renewal price. + /// Provides detailed information about a user's purchase, including renewal price. /// /// This function fetches the renewal price details for the given product asynchronously from /// StoreKit 2 and constructs a `PurchaseInformation` object with the provided @@ -176,49 +176,28 @@ extension PurchaseInformation { @available(watchOS, unavailable) @available(watchOSApplicationExtension, unavailable) private static func extractPriceDetailsFromRenwalInfo( - forProduct product: StoreProduct + forProduct product: StoreProduct, + customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType = CustomerCenterStoreKitUtilities() ) async -> PriceDetails? { if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, watchOSApplicationExtension 8.0, *) { - guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else { - // If StoreKit.Product.subscription is nil, then the product isn't a subscription - // If statuses is empty, the subscriber was never subscribed to a product in the subscription group. + guard let renewalInfo = await customerCenterStoreKitUtilities.renewalInfo(for: product) else { return nil } - guard let purchaseSubscriptionStatus = statuses.first(where: { - do { - return try $0.transaction.payloadValue.ownershipType == .purchased - } catch { - return false - } - }) else { - return nil - } - - switch purchaseSubscriptionStatus.renewalInfo { - case .unverified: - return nil - case .verified(let renewalInfo): - - // renewalInfo.renewalPrice was introduced in iOS 18.0 and backdeployed through iOS 15.0 - // However, Xcode versions <15.0 don't have the symbols, so we need to check the compiler version - // to make sure that this is being built with an Xcode version >=15.0. - #if compiler(>=6.0) - guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil } - - guard let currencyCode = product.currencyCode else { return nil } + #if compiler(>=6.0) + guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil } + guard let currencyCode = product.currencyCode else { return nil } - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.currencyCode = currencyCode + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = currencyCode - guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil } + guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil } - return .paid(formattedPrice) - #else - return nil - #endif - } + return .paid(formattedPrice) + #else + return nil + #endif } else { return nil } diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift new file mode 100644 index 0000000000..20cbbcd31e --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift @@ -0,0 +1,56 @@ +// +// 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 +// +// CustomerCenterStoreKitUtilities.swift +// +// Created by Will Taylor on 12/18/24. + +import Foundation +import StoreKit + +import RevenueCat + +class CustomerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType { + + @available(iOS 15.0, *) + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + @available(watchOSApplicationExtension, unavailable) + func renewalInfo( + for product: RevenueCat.StoreProduct + ) async -> Product.SubscriptionInfo.RenewalInfo? { + if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, watchOSApplicationExtension 8.0, *) { + guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else { + // If StoreKit.Product.subscription is nil, then the product isn't a subscription + // If statuses is empty, the subscriber was never subscribed to a product in the subscription group. + return nil + } + + guard let purchaseSubscriptionStatus = statuses.first(where: { + do { + return try $0.transaction.payloadValue.ownershipType == .purchased + } catch { + return false + } + }) else { + return nil + } + + switch purchaseSubscriptionStatus.renewalInfo { + case .unverified: + return nil + case .verified(let renewalInfo): + return renewalInfo + } + } else { + return nil + } + } +} diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift new file mode 100644 index 0000000000..44185d794d --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift @@ -0,0 +1,27 @@ +// +// 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 +// +// CustomerCenterStoreKitUtilitiesType.swift +// +// Created by Will Taylor on 12/18/24. + +import Foundation +import StoreKit + +import RevenueCat + +protocol CustomerCenterStoreKitUtilitiesType { + + @available(iOS 15.0, *) + @available(macOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + @available(watchOSApplicationExtension, unavailable) + func renewalInfo(for product: StoreProduct) async -> StoreKit.Product.SubscriptionInfo.RenewalInfo? +} From 31501b9438ca94e4437dd7d0610e0b1ef93dc379 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 10:35:56 -0600 Subject: [PATCH 10/23] try this --- .../Utilities/CustomerCenterStoreKitUtilitiesType.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift index 44185d794d..12d5eba852 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift @@ -16,12 +16,8 @@ import StoreKit import RevenueCat +@available(iOS 15.0, *) protocol CustomerCenterStoreKitUtilitiesType { - @available(iOS 15.0, *) - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - @available(watchOSApplicationExtension, unavailable) func renewalInfo(for product: StoreProduct) async -> StoreKit.Product.SubscriptionInfo.RenewalInfo? } From 9c452ebc4aff810d583e3571441f835f98aaa737 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 10:40:38 -0600 Subject: [PATCH 11/23] add all platforms --- .../Utilities/CustomerCenterStoreKitUtilitiesType.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift index 12d5eba852..af01f5cbef 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift @@ -16,7 +16,7 @@ import StoreKit import RevenueCat -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) protocol CustomerCenterStoreKitUtilitiesType { func renewalInfo(for product: StoreProduct) async -> StoreKit.Product.SubscriptionInfo.RenewalInfo? From 0725da85c5edcec6f9078f4ac6991a6b9c326414 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 10:47:17 -0600 Subject: [PATCH 12/23] move requirements to concrete type too --- .../Utilities/CustomerCenterStoreKitUtilities.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift index 20cbbcd31e..7156aab277 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift @@ -16,13 +16,9 @@ import StoreKit import RevenueCat +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) class CustomerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType { - @available(iOS 15.0, *) - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - @available(watchOSApplicationExtension, unavailable) func renewalInfo( for product: RevenueCat.StoreProduct ) async -> Product.SubscriptionInfo.RenewalInfo? { From a3a26a78e5b482397134aff6d9af54cd31a95d9c Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 10:52:15 -0600 Subject: [PATCH 13/23] push OS requirements up the call stack --- .../CustomerCenter/Data/PurchaseInformation.swift | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 485d8a304c..8c862f0cc6 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -150,12 +150,7 @@ extension PurchaseInformation { /// - transaction: The transaction information for the purchase. /// - Returns: A `PurchaseInformation` object containing the purchase details, including the renewal price. /// - /// - Availability: iOS 15.0+ - @available(iOS 15.0, *) - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - @available(watchOSApplicationExtension, unavailable) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) static func purchaseInformationUsingRenewalInfo( entitlement: EntitlementInfo? = nil, subscribedProduct: StoreProduct, @@ -170,11 +165,7 @@ extension PurchaseInformation { ) } - @available(iOS 15.0, *) - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - @available(watchOSApplicationExtension, unavailable) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) private static func extractPriceDetailsFromRenwalInfo( forProduct product: StoreProduct, customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType = CustomerCenterStoreKitUtilities() From 524bd07ad8f9ed1185d851f40eb9f92253cb6810 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 11:06:38 -0600 Subject: [PATCH 14/23] remove unnecessary checks --- RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift | 2 +- .../Utilities/CustomerCenterStoreKitUtilities.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 8c862f0cc6..17d3d0cb32 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -170,7 +170,7 @@ extension PurchaseInformation { forProduct product: StoreProduct, customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType = CustomerCenterStoreKitUtilities() ) async -> PriceDetails? { - if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, watchOSApplicationExtension 8.0, *) { + if #available(watchOSApplicationExtension 8.0, *) { guard let renewalInfo = await customerCenterStoreKitUtilities.renewalInfo(for: product) else { return nil } diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift index 7156aab277..58532e2b42 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift @@ -22,7 +22,7 @@ class CustomerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType { func renewalInfo( for product: RevenueCat.StoreProduct ) async -> Product.SubscriptionInfo.RenewalInfo? { - if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, watchOSApplicationExtension 8.0, *) { + if #available(watchOSApplicationExtension 8.0, *) { guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else { // If StoreKit.Product.subscription is nil, then the product isn't a subscription // If statuses is empty, the subscriber was never subscribed to a product in the subscription group. From 52479a1ddc67ee3278d7b150114461c602b59a2b Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 11:21:40 -0600 Subject: [PATCH 15/23] remove watchOSApplicationExtension checks --- .../Data/PurchaseInformation.swift | 32 +++++++--------- .../CustomerCenterStoreKitUtilities.swift | 38 +++++++++---------- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 17d3d0cb32..ac759e08e7 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -170,28 +170,24 @@ extension PurchaseInformation { forProduct product: StoreProduct, customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType = CustomerCenterStoreKitUtilities() ) async -> PriceDetails? { - if #available(watchOSApplicationExtension 8.0, *) { - guard let renewalInfo = await customerCenterStoreKitUtilities.renewalInfo(for: product) else { - return nil - } + guard let renewalInfo = await customerCenterStoreKitUtilities.renewalInfo(for: product) else { + return nil + } - #if compiler(>=6.0) - guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil } - guard let currencyCode = product.currencyCode else { return nil } +#if compiler(>=6.0) + guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil } + guard let currencyCode = product.currencyCode else { return nil } - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.currencyCode = currencyCode + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = currencyCode - guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil } + guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil } - return .paid(formattedPrice) - #else - return nil - #endif - } else { - return nil - } + return .paid(formattedPrice) +#else + return nil +#endif } @available(iOS 15.0, *) diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift index 58532e2b42..8d253e44f7 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift @@ -22,31 +22,27 @@ class CustomerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType { func renewalInfo( for product: RevenueCat.StoreProduct ) async -> Product.SubscriptionInfo.RenewalInfo? { - if #available(watchOSApplicationExtension 8.0, *) { - guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else { - // If StoreKit.Product.subscription is nil, then the product isn't a subscription - // If statuses is empty, the subscriber was never subscribed to a product in the subscription group. - return nil - } + guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else { + // If StoreKit.Product.subscription is nil, then the product isn't a subscription + // If statuses is empty, the subscriber was never subscribed to a product in the subscription group. + return nil + } - guard let purchaseSubscriptionStatus = statuses.first(where: { - do { - return try $0.transaction.payloadValue.ownershipType == .purchased - } catch { - return false - } - }) else { - return nil + guard let purchaseSubscriptionStatus = statuses.first(where: { + do { + return try $0.transaction.payloadValue.ownershipType == .purchased + } catch { + return false } + }) else { + return nil + } - switch purchaseSubscriptionStatus.renewalInfo { - case .unverified: - return nil - case .verified(let renewalInfo): - return renewalInfo - } - } else { + switch purchaseSubscriptionStatus.renewalInfo { + case .unverified: return nil + case .verified(let renewalInfo): + return renewalInfo } } } From 4154ddfefffed12c3e31335f49503863f0e04682 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 17:54:03 -0600 Subject: [PATCH 16/23] add tests --- RevenueCat.xcodeproj/project.pbxproj | 14 ++++- .../Data/PurchaseInformation.swift | 54 ++++--------------- .../CustomerCenterStoreKitUtilities.swift | 10 +++- .../CustomerCenterStoreKitUtilitiesType.swift | 2 +- .../ViewModels/CustomerCenterViewModel.swift | 8 ++- .../CustomerCenterViewModelTests.swift | 38 +++++++++++++ .../MockCustomerCenterPurchases.swift | 0 .../MockCustomerCenterStoreKitUtilities.swift | 29 ++++++++++ 8 files changed, 106 insertions(+), 49 deletions(-) rename Tests/RevenueCatUITests/CustomerCenter/{ => Mocks}/MockCustomerCenterPurchases.swift (100%) create mode 100644 Tests/RevenueCatUITests/CustomerCenter/Mocks/MockCustomerCenterStoreKitUtilities.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 929c6cab07..f7dc0ac191 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -1049,6 +1049,7 @@ FD33CD4D2D034CBD000D13A4 /* CustomerCenterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD33CD4C2D034CBD000D13A4 /* CustomerCenterViewController.swift */; }; FD43D2FC2C41864000077235 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43D2FA2C4185B700077235 /* TimeInterval+Extensions.swift */; }; FD43D2FE2C41867600077235 /* TimeInterval+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43D2FD2C41867600077235 /* TimeInterval+ExtensionsTests.swift */; }; + FD6186542D1393FA007843DA /* MockCustomerCenterStoreKitUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6186532D1393FA007843DA /* MockCustomerCenterStoreKitUtilities.swift */; }; FD9F982D2BE28A7F0091A5BF /* MockNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B515526D44B2300BD2BD7 /* MockNotificationCenter.swift */; }; FDAADFCB2BE2A5BF00BD1659 /* MockAllTransactionsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAADFCA2BE2A5BF00BD1659 /* MockAllTransactionsProvider.swift */; }; FDAADFCC2BE2A5BF00BD1659 /* MockAllTransactionsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAADFCA2BE2A5BF00BD1659 /* MockAllTransactionsProvider.swift */; }; @@ -2302,6 +2303,7 @@ FD33CD5F2D03500C000D13A4 /* CustomerCenterUIKitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerCenterUIKitView.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 = ""; }; + FD6186532D1393FA007843DA /* MockCustomerCenterStoreKitUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCustomerCenterStoreKitUtilities.swift; sourceTree = ""; }; FDAADFCA2BE2A5BF00BD1659 /* MockAllTransactionsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAllTransactionsProvider.swift; sourceTree = ""; }; FDAADFCE2BE2B84500BD1659 /* StoreKit2ObserverModePurchaseDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2ObserverModePurchaseDetector.swift; sourceTree = ""; }; FDAADFD22BE2B99900BD1659 /* MockStoreKit2ObserverModePurchaseDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreKit2ObserverModePurchaseDetector.swift; sourceTree = ""; }; @@ -3684,7 +3686,7 @@ 3544DA6B2C2C848E00704E9D /* CustomerCenter */ = { isa = PBXGroup; children = ( - 356523A92CF885CE00B6E3EA /* MockCustomerCenterPurchases.swift */, + FD6186522D1393E2007843DA /* Mocks */, 35A99C822CCB95950074AB41 /* SubscriptionInformationFixtures.swift */, 3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */, 3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */, @@ -4982,6 +4984,15 @@ path = "UIKit Compatibility"; sourceTree = ""; }; + FD6186522D1393E2007843DA /* Mocks */ = { + isa = PBXGroup; + children = ( + 356523A92CF885CE00B6E3EA /* MockCustomerCenterPurchases.swift */, + FD6186532D1393FA007843DA /* MockCustomerCenterStoreKitUtilities.swift */, + ); + path = Mocks; + sourceTree = ""; + }; FDAADFCD2BE2B82D00BD1659 /* Observer Mode */ = { isa = PBXGroup; children = ( @@ -6691,6 +6702,7 @@ 887A63412C1D177800E1A461 /* BaseSnapshotTest.swift in Sources */, 887A63422C1D177800E1A461 /* ImageLoaderTests.swift in Sources */, 887A63432C1D177800E1A461 /* LocalizationTests.swift in Sources */, + FD6186542D1393FA007843DA /* MockCustomerCenterStoreKitUtilities.swift in Sources */, 887A63442C1D177800E1A461 /* PaywallFooterTests.swift in Sources */, 887A63452C1D177800E1A461 /* PaywallViewEventsTests.swift in Sources */, 887A63472C1D177800E1A461 /* PurchaseCompletedHandlerTests.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index ac759e08e7..08ecfab8e5 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -154,9 +154,13 @@ extension PurchaseInformation { static func purchaseInformationUsingRenewalInfo( entitlement: EntitlementInfo? = nil, subscribedProduct: StoreProduct, - transaction: Transaction + transaction: Transaction, + customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType ) async -> PurchaseInformation { - let renewalPriceDetails = await Self.extractPriceDetailsFromRenwalInfo(forProduct: subscribedProduct) + let renewalPriceDetails = await Self.extractPriceDetailsFromRenwalInfo( + forProduct: subscribedProduct, + customerCenterStoreKitUtilities: customerCenterStoreKitUtilities + ) return PurchaseInformation( entitlement: entitlement, subscribedProduct: subscribedProduct, @@ -168,14 +172,15 @@ extension PurchaseInformation { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) private static func extractPriceDetailsFromRenwalInfo( forProduct product: StoreProduct, - customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType = CustomerCenterStoreKitUtilities() + customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType ) async -> PriceDetails? { - guard let renewalInfo = await customerCenterStoreKitUtilities.renewalInfo(for: product) else { + guard let renewalPrice = await customerCenterStoreKitUtilities.renewalPriceFromRenewalInfo( + for: product + ) as? NSNumber else { return nil } #if compiler(>=6.0) - guard let renewalPrice = renewalInfo.renewalPrice as? NSNumber else { return nil } guard let currencyCode = product.currencyCode else { return nil } let formatter = NumberFormatter() @@ -189,45 +194,6 @@ extension PurchaseInformation { return nil #endif } - - @available(iOS 15.0, *) - @available(macOS, unavailable) - @available(tvOS, unavailable) - @available(watchOS, unavailable) - @available(watchOSApplicationExtension, unavailable) - private static func currencyCode( - fromRenewalInfo renewalInfo: Product.SubscriptionInfo.RenewalInfo, - locale: Locale = Locale.current - ) -> String? { - // macOS 13.0 check is required for the compiler despite the function being marked - // as unavailable on macOS - if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, watchOSApplicationExtension 9.0, *) { - - // renewalInfo.currency was introduced in iOS 18.0 and backdeployed through iOS 16.0 - // However, Xcode versions <15.0 don't have the symbols, so we need to check the compiler version - // to make sure that this is being built with an Xcode version >=15.0. - #if compiler(>=6.0) - guard let currency = renewalInfo.currency else { return nil } - if currency.isISOCurrency { - return currency.identifier - } else { - return nil - } - #else - return nil - #endif - } else { - if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, watchOSApplicationExtension 8.0, *) { - #if os(visionOS) || compiler(<6.0) - return nil - #else - return renewalInfo.currencyCode - #endif - } else { - return nil - } - } - } } fileprivate extension EntitlementInfo { diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift index 8d253e44f7..e2348d7070 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift @@ -19,7 +19,15 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) class CustomerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType { - func renewalInfo( + func renewalPriceFromRenewalInfo(for product: StoreProduct) async -> Decimal? { + guard let renewalInfo = await renewalInfo(for: product) else { + return nil + } + + return renewalInfo.renewalPrice + } + + private func renewalInfo( for product: RevenueCat.StoreProduct ) async -> Product.SubscriptionInfo.RenewalInfo? { guard let statuses = try? await product.sk2Product?.subscription?.status, !statuses.isEmpty else { diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift index af01f5cbef..c02a91ca4f 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift @@ -19,5 +19,5 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) protocol CustomerCenterStoreKitUtilitiesType { - func renewalInfo(for product: StoreProduct) async -> StoreKit.Product.SubscriptionInfo.RenewalInfo? + func renewalPriceFromRenewalInfo(for product: StoreProduct) async -> Decimal? } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 57dfd5eba5..069cc09e73 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -34,6 +34,7 @@ import RevenueCat @Published private(set) var appIsLatestVersion: Bool = defaultAppIsLatestVersion private(set) var purchasesProvider: CustomerCenterPurchasesType + private(set) var customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType @Published private(set) var onUpdateAppClick: (() -> Void)? @@ -79,12 +80,14 @@ import RevenueCat currentVersionFetcher: @escaping CurrentVersionFetcher = { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String }, - purchasesProvider: CustomerCenterPurchasesType = CustomerCenterPurchases() + purchasesProvider: CustomerCenterPurchasesType = CustomerCenterPurchases(), + customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType = CustomerCenterStoreKitUtilities() ) { self.state = .notLoaded self.currentVersionFetcher = currentVersionFetcher self.customerCenterActionHandler = customerCenterActionHandler self.purchasesProvider = purchasesProvider + self.customerCenterStoreKitUtilities = customerCenterStoreKitUtilities } #if DEBUG @@ -212,7 +215,8 @@ private extension CustomerCenterViewModel { return await PurchaseInformation.purchaseInformationUsingRenewalInfo( entitlement: entitlement, subscribedProduct: product, - transaction: transaction + transaction: transaction, + customerCenterStoreKitUtilities: customerCenterStoreKitUtilities ) } else { Logger.warning( diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 02e2c679ed..5a36a41641 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -837,6 +837,44 @@ class CustomerCenterViewModelTests: TestCase { expect(viewModel.shouldShowAppUpdateWarnings).to(beFalse()) } + func testPurchaseInformationIsStillLoadedIfRenewalInfoCantBeFetched() async { + let mockPurchases = MockCustomerCenterPurchases() + let mockStoreKitUtilities = MockCustomerCenterStoreKitUtilities() + + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + currentVersionFetcher: { return "3.0.0" }, + purchasesProvider: mockPurchases, + customerCenterStoreKitUtilities: mockStoreKitUtilities as CustomerCenterStoreKitUtilitiesType + ) + + expect(mockStoreKitUtilities.returnRenewalPriceFromRenewalInfo).to(beNil()) + + await viewModel.loadScreen() + + expect(viewModel.purchaseInformation).toNot(beNil()) + expect(mockStoreKitUtilities.renewalPriceFromRenewalInfoCallCount).to(equal(1)) + } + + func testPurchaseInformationUsesInfoFromRenewalInfoWhenAvailable() async { + let mockPurchases = MockCustomerCenterPurchases() + let mockStoreKitUtilities = MockCustomerCenterStoreKitUtilities() + mockStoreKitUtilities.returnRenewalPriceFromRenewalInfo = 5 + + let viewModel = CustomerCenterViewModel( + customerCenterActionHandler: nil, + currentVersionFetcher: { return "3.0.0" }, + purchasesProvider: mockPurchases, + customerCenterStoreKitUtilities: mockStoreKitUtilities as CustomerCenterStoreKitUtilitiesType + ) + + expect(mockStoreKitUtilities.returnRenewalPriceFromRenewalInfo).to(equal(5)) + + await viewModel.loadScreen() + + expect(viewModel.purchaseInformation?.price).to(equal(.paid("$5.00"))) + expect(mockStoreKitUtilities.renewalPriceFromRenewalInfoCallCount).to(equal(1)) + } } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) diff --git a/Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift b/Tests/RevenueCatUITests/CustomerCenter/Mocks/MockCustomerCenterPurchases.swift similarity index 100% rename from Tests/RevenueCatUITests/CustomerCenter/MockCustomerCenterPurchases.swift rename to Tests/RevenueCatUITests/CustomerCenter/Mocks/MockCustomerCenterPurchases.swift diff --git a/Tests/RevenueCatUITests/CustomerCenter/Mocks/MockCustomerCenterStoreKitUtilities.swift b/Tests/RevenueCatUITests/CustomerCenter/Mocks/MockCustomerCenterStoreKitUtilities.swift new file mode 100644 index 0000000000..7f0f36518c --- /dev/null +++ b/Tests/RevenueCatUITests/CustomerCenter/Mocks/MockCustomerCenterStoreKitUtilities.swift @@ -0,0 +1,29 @@ +// +// 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 +// +// MockCustomerCenterStoreKitUtilities.swift +// +// Created by Will Taylor on 12/18/24. + +import Foundation +import StoreKit + +import RevenueCat +@testable import RevenueCatUI + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) +class MockCustomerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType { + + var returnRenewalPriceFromRenewalInfo: Decimal? + var renewalPriceFromRenewalInfoCallCount = 0 + func renewalPriceFromRenewalInfo(for product: StoreProduct) async -> Decimal? { + renewalPriceFromRenewalInfoCallCount += 1 + return returnRenewalPriceFromRenewalInfo + } +} From a2dcd7ab729d5c64cf20616f1747ce9f225bf307 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 18:06:55 -0600 Subject: [PATCH 17/23] compiler check fix --- RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift | 4 ---- .../Utilities/CustomerCenterStoreKitUtilities.swift | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 08ecfab8e5..fb592b0682 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -180,7 +180,6 @@ extension PurchaseInformation { return nil } -#if compiler(>=6.0) guard let currencyCode = product.currencyCode else { return nil } let formatter = NumberFormatter() @@ -190,9 +189,6 @@ extension PurchaseInformation { guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil } return .paid(formattedPrice) -#else - return nil -#endif } } diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift index e2348d7070..f99d971846 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift @@ -20,11 +20,16 @@ import RevenueCat class CustomerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType { func renewalPriceFromRenewalInfo(for product: StoreProduct) async -> Decimal? { + + #if compiler(>=6.0) guard let renewalInfo = await renewalInfo(for: product) else { return nil } return renewalInfo.renewalPrice + #else + return nil + #endif } private func renewalInfo( From 5635d08ea803175176c4b39a286e40f8b9b35cc8 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 18:18:38 -0600 Subject: [PATCH 18/23] get currencyCode from renewalInfo --- .../Data/PurchaseInformation.swift | 10 ++--- .../CustomerCenterStoreKitUtilities.swift | 42 ++++++++++++++++--- .../CustomerCenterStoreKitUtilitiesType.swift | 2 +- .../CustomerCenterViewModelTests.swift | 4 +- .../MockCustomerCenterStoreKitUtilities.swift | 4 +- 5 files changed, 46 insertions(+), 16 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index fb592b0682..0803fcd64e 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -174,19 +174,17 @@ extension PurchaseInformation { forProduct product: StoreProduct, customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType ) async -> PriceDetails? { - guard let renewalPrice = await customerCenterStoreKitUtilities.renewalPriceFromRenewalInfo( + guard let renewalPriceDetails = await customerCenterStoreKitUtilities.renewalPriceFromRenewalInfo( for: product - ) as? NSNumber else { + ) else { return nil } - guard let currencyCode = product.currencyCode else { return nil } - let formatter = NumberFormatter() formatter.numberStyle = .currency - formatter.currencyCode = currencyCode + formatter.currencyCode = renewalPriceDetails.currencyCode - guard let formattedPrice = formatter.string(from: renewalPrice) else { return nil } + guard let formattedPrice = formatter.string(from: renewalPriceDetails.price as NSNumber) else { return nil } return .paid(formattedPrice) } diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift index f99d971846..3231407f54 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift @@ -19,19 +19,51 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) class CustomerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType { - func renewalPriceFromRenewalInfo(for product: StoreProduct) async -> Decimal? { + func renewalPriceFromRenewalInfo(for product: StoreProduct) async -> (price: Decimal, currencyCode: String)? { #if compiler(>=6.0) - guard let renewalInfo = await renewalInfo(for: product) else { - return nil - } + guard let renewalInfo = await renewalInfo(for: product) else { return nil } + guard let renewalPrice = renewalInfo.renewalPrice else { return nil } + guard let currencyCode = currencyCode(fromRenewalInfo: renewalInfo) else { return nil } - return renewalInfo.renewalPrice + return (renewalPrice, currencyCode) #else return nil #endif } + private func currencyCode( + fromRenewalInfo renewalInfo: Product.SubscriptionInfo.RenewalInfo, + locale: Locale = Locale.current + ) -> String? { + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, watchOSApplicationExtension 9.0, *) { + + // renewalInfo.currency was introduced in iOS 18.0 and backdeployed through iOS 16.0 + // However, Xcode versions <15.0 don't have the symbols, so we need to check the compiler version + // to make sure that this is being built with an Xcode version >=15.0. + #if compiler(>=6.0) + guard let currency = renewalInfo.currency else { return nil } + if currency.isISOCurrency { + return currency.identifier + } else { + return nil + } + #else + return nil + #endif + } else { + if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, watchOSApplicationExtension 8.0, *) { + #if os(visionOS) || compiler(<6.0) + return nil + #else + return renewalInfo.currencyCode + #endif + } else { + return nil + } + } + } + private func renewalInfo( for product: RevenueCat.StoreProduct ) async -> Product.SubscriptionInfo.RenewalInfo? { diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift index c02a91ca4f..e623de26b2 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilitiesType.swift @@ -19,5 +19,5 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) protocol CustomerCenterStoreKitUtilitiesType { - func renewalPriceFromRenewalInfo(for product: StoreProduct) async -> Decimal? + func renewalPriceFromRenewalInfo(for product: StoreProduct) async -> (price: Decimal, currencyCode: String)? } diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 5a36a41641..ad5ef7ed16 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -859,7 +859,7 @@ class CustomerCenterViewModelTests: TestCase { func testPurchaseInformationUsesInfoFromRenewalInfoWhenAvailable() async { let mockPurchases = MockCustomerCenterPurchases() let mockStoreKitUtilities = MockCustomerCenterStoreKitUtilities() - mockStoreKitUtilities.returnRenewalPriceFromRenewalInfo = 5 + mockStoreKitUtilities.returnRenewalPriceFromRenewalInfo = (5, "USD") let viewModel = CustomerCenterViewModel( customerCenterActionHandler: nil, @@ -868,7 +868,7 @@ class CustomerCenterViewModelTests: TestCase { customerCenterStoreKitUtilities: mockStoreKitUtilities as CustomerCenterStoreKitUtilitiesType ) - expect(mockStoreKitUtilities.returnRenewalPriceFromRenewalInfo).to(equal(5)) + expect(mockStoreKitUtilities.returnRenewalPriceFromRenewalInfo).to(equal((5, "USD"))) await viewModel.loadScreen() diff --git a/Tests/RevenueCatUITests/CustomerCenter/Mocks/MockCustomerCenterStoreKitUtilities.swift b/Tests/RevenueCatUITests/CustomerCenter/Mocks/MockCustomerCenterStoreKitUtilities.swift index 7f0f36518c..08b3336176 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/Mocks/MockCustomerCenterStoreKitUtilities.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/Mocks/MockCustomerCenterStoreKitUtilities.swift @@ -20,9 +20,9 @@ import RevenueCat @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) class MockCustomerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType { - var returnRenewalPriceFromRenewalInfo: Decimal? + var returnRenewalPriceFromRenewalInfo: (price: Decimal, currencyCode: String)? var renewalPriceFromRenewalInfoCallCount = 0 - func renewalPriceFromRenewalInfo(for product: StoreProduct) async -> Decimal? { + func renewalPriceFromRenewalInfo(for product: StoreProduct) async -> (price: Decimal, currencyCode: String)? { renewalPriceFromRenewalInfoCallCount += 1 return returnRenewalPriceFromRenewalInfo } From 38264d57a84ae8a6361568ac0ea848dbe007fc91 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Wed, 18 Dec 2024 18:34:09 -0600 Subject: [PATCH 19/23] remove extra check --- .../CustomerCenterStoreKitUtilities.swift | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift index 3231407f54..74cee7dde9 100644 --- a/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift +++ b/RevenueCatUI/CustomerCenter/Utilities/CustomerCenterStoreKitUtilities.swift @@ -52,15 +52,11 @@ class CustomerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType { return nil #endif } else { - if #available(macOS 12.0, tvOS 15.0, watchOS 8.0, watchOSApplicationExtension 8.0, *) { - #if os(visionOS) || compiler(<6.0) - return nil - #else - return renewalInfo.currencyCode - #endif - } else { - return nil - } + #if os(visionOS) || compiler(<6.0) + return nil + #else + return renewalInfo.currencyCode + #endif } } From 61c699d5b1d2845847685515615570847de50db9 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Thu, 19 Dec 2024 09:09:35 -0600 Subject: [PATCH 20/23] update currentPrice string --- Sources/CustomerCenter/CustomerCenterConfigData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index bb1afc3b9d..a2cc6a69ad 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -122,7 +122,7 @@ public struct CustomerCenterConfigData { case .billingCycle: return "Billing cycle" case .currentPrice: - return "Current price" + return "Price" case .expired: return "Expired" case .expires: From 73ee1924060bbb086e25f0da8c135cbb2e5108aa Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Thu, 19 Dec 2024 13:59:54 -0600 Subject: [PATCH 21/23] Update RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift Co-authored-by: Cesar de la Vega --- RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 0803fcd64e..4f4041921f 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -138,7 +138,7 @@ struct PurchaseInformation { extension PurchaseInformation { - /// Provides detailed information about a user's purchase, including renewal price. + /// Provides detailed information about a user's purchase, including renewal price. /// /// This function fetches the renewal price details for the given product asynchronously from /// StoreKit 2 and constructs a `PurchaseInformation` object with the provided From 716f6a45036271acff6ebc2f8f202fbce3ae593e Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Thu, 19 Dec 2024 14:00:09 -0600 Subject: [PATCH 22/23] Update RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift Co-authored-by: Cesar de la Vega --- RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 4f4041921f..01a78adade 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -170,7 +170,7 @@ extension PurchaseInformation { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) - private static func extractPriceDetailsFromRenwalInfo( + private static func extractPriceDetailsFromRenewalInfo( forProduct product: StoreProduct, customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType ) async -> PriceDetails? { From 2445eb101a0d0e00b0bbcc1607963cfb49d882f8 Mon Sep 17 00:00:00 2001 From: Will Taylor Date: Thu, 19 Dec 2024 14:00:51 -0600 Subject: [PATCH 23/23] typo fix --- RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift index 01a78adade..482c6e20c5 100644 --- a/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift @@ -157,7 +157,7 @@ extension PurchaseInformation { transaction: Transaction, customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType ) async -> PurchaseInformation { - let renewalPriceDetails = await Self.extractPriceDetailsFromRenwalInfo( + let renewalPriceDetails = await Self.extractPriceDetailsFromRenewalInfo( forProduct: subscribedProduct, customerCenterStoreKitUtilities: customerCenterStoreKitUtilities )