diff --git a/RevenueCatUI/Helpers/Constants.swift b/RevenueCatUI/Data/Constants.swift similarity index 100% rename from RevenueCatUI/Helpers/Constants.swift rename to RevenueCatUI/Data/Constants.swift diff --git a/RevenueCatUI/Data/PaywallViewMode.swift b/RevenueCatUI/Data/PaywallViewMode.swift new file mode 100644 index 0000000000..da9275ca2e --- /dev/null +++ b/RevenueCatUI/Data/PaywallViewMode.swift @@ -0,0 +1,16 @@ +// +// PaywallViewMode.swift +// +// +// Created by Nacho Soto on 7/17/23. +// + +import RevenueCat + +/// The mode for how a paywall is rendered. +public enum PaywallViewMode { + + /// Paywall is displayed full-screen, with as much information as available. + case fullScreen + +} diff --git a/RevenueCatUI/Helpers/ProcessedLocalizedConfiguration.swift b/RevenueCatUI/Data/ProcessedLocalizedConfiguration.swift similarity index 100% rename from RevenueCatUI/Helpers/ProcessedLocalizedConfiguration.swift rename to RevenueCatUI/Data/ProcessedLocalizedConfiguration.swift diff --git a/RevenueCatUI/Data/TemplateError.swift b/RevenueCatUI/Data/TemplateError.swift new file mode 100644 index 0000000000..f7a64fe839 --- /dev/null +++ b/RevenueCatUI/Data/TemplateError.swift @@ -0,0 +1,46 @@ +// +// TemplateError.swift +// +// +// Created by Nacho Soto on 7/17/23. +// + +import Foundation +import RevenueCat + +/// Error produced when processing `PaywallData`. +enum TemplateError: Error { + + /// No packages available to create a paywall. + case noPackages + + /// Paywall configuration contained no package types. + case emptyPackageList + + /// No packages from the `PackageType` list could be found. + case couldNotFindAnyPackages(expectedTypes: [PackageType]) + +} + +extension TemplateError: CustomNSError { + + var errorUserInfo: [String: Any] { + return [ + NSLocalizedDescriptionKey: self.description + ] + } + + private var description: String { + switch self { + case .noPackages: + return "Attempted to display paywall with no packages." + + case .emptyPackageList: + return "Paywall configuration container no packages." + + case let .couldNotFindAnyPackages(expectedTypes): + return "Couldn't find any requested packages: \(expectedTypes)" + } + } + +} diff --git a/RevenueCatUI/Data/TemplateViewConfiguration.swift b/RevenueCatUI/Data/TemplateViewConfiguration.swift new file mode 100644 index 0000000000..e109f56502 --- /dev/null +++ b/RevenueCatUI/Data/TemplateViewConfiguration.swift @@ -0,0 +1,153 @@ +// +// TemplateViewConfiguration.swift +// +// +// Created by Nacho Soto on 7/17/23. +// + +import Foundation +import RevenueCat + +/// The processed data necessary to render a `TemplateViewType`. +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +struct TemplateViewConfiguration { + + let mode: PaywallViewMode + let packages: PackageConfiguration + let configuration: PaywallData.Configuration + let colors: PaywallData.Configuration.Colors + let headerImageURL: URL + +} + +// MARK: - Packages + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +extension TemplateViewConfiguration { + + /// A `Package` with its processed localized strings. + struct Package { + + let content: RevenueCat.Package + let localization: ProcessedLocalizedConfiguration + + } + + /// Whether a template displays 1 or multiple packages. + enum PackageSetting { + + case single + case multiple + + } + + /// Describes the possible displayed packages in a paywall. + /// See `create(with:filter:setting:)` for how to create these. + enum PackageConfiguration { + + case single(Package) + case multiple([Package]) + + } + +} + +// MARK: - Properties + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +extension TemplateViewConfiguration.PackageConfiguration { + + /// Returns a single package, useful for templates that expect a single package. + var single: TemplateViewConfiguration.Package { + switch self { + case let .single(package): + return package + case let .multiple(packages): + guard let package = packages.first else { + // `create()` makes this impossible. + fatalError("Unexpectedly found no packages in `PackageConfiguration.multiple`") + } + + return package + } + } + + /// Returns all packages, useful for templates that expect multiple packages + var all: [TemplateViewConfiguration.Package] { + switch self { + case let .single(package): + return [package] + case let .multiple(packages): + return packages + } + } + +} + +// MARK: - Creation + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +extension TemplateViewConfiguration.PackageConfiguration { + + /// Creates a `PackageConfiguration` based on `setting`. + /// - Throws: `TemplateError` + static func create( + with packages: [RevenueCat.Package], + filter: [PackageType], + localization: PaywallData.LocalizedConfiguration, + setting: TemplateViewConfiguration.PackageSetting + ) throws -> Self { + guard !packages.isEmpty else { throw TemplateError.noPackages } + guard !filter.isEmpty else { throw TemplateError.emptyPackageList } + + let filtered = TemplateViewConfiguration + .filter(packages: packages, with: filter) + .map { package in + TemplateViewConfiguration.Package( + content: package, + localization: localization.processVariables(with: package)) + } + + guard let firstPackage = filtered.first else { + throw TemplateError.couldNotFindAnyPackages(expectedTypes: filter) + } + + switch setting { + case .single: + return .single(firstPackage) + case .multiple: + return .multiple(filtered) + } + } + +} + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +extension TemplateViewConfiguration { + + /// Filters `packages`, extracting only the values corresponding to `list`. + static func filter(packages: [RevenueCat.Package], with list: [PackageType]) -> [RevenueCat.Package] { + // Only subscriptions are supported at the moment + let subscriptions = packages.filter { $0.storeProduct.productCategory == .subscription } + let map = Dictionary(grouping: subscriptions) { $0.packageType } + + return list.compactMap { type in + if let packages = map[type] { + switch packages.count { + case 0: + // This isn't actually possible because of `Dictionary(grouping:by:) + return nil + case 1: + return packages.first + default: + Logger.warning("Found multiple \(type) packages. Will use the first one.") + return packages.first + } + } else { + Logger.warning("Couldn't find '\(type)'") + return nil + } + } + } + +} diff --git a/RevenueCatUI/TestData.swift b/RevenueCatUI/Data/TestData.swift similarity index 100% rename from RevenueCatUI/TestData.swift rename to RevenueCatUI/Data/TestData.swift diff --git a/RevenueCatUI/Helpers/Variables.swift b/RevenueCatUI/Data/Variables.swift similarity index 100% rename from RevenueCatUI/Helpers/Variables.swift rename to RevenueCatUI/Data/Variables.swift diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index 27e4f8be61..f3c2432db4 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -5,6 +5,7 @@ import SwiftUI @available(iOS 16.0, macOS 13.0, tvOS 16.0, *) public struct PaywallView: View { + private let mode: PaywallViewMode private let offering: Offering private let paywall: PaywallData private let introEligibility: TrialOrIntroEligibilityChecker? @@ -12,8 +13,9 @@ public struct PaywallView: View { /// Create a view for the given offering and paywal. /// - Warning: `Purchases` must have been configured prior to displaying it. - public init(offering: Offering, paywall: PaywallData) { + public init(mode: PaywallViewMode, offering: Offering, paywall: PaywallData) { self.init( + mode: mode, offering: offering, paywall: paywall, introEligibility: Purchases.isConfigured ? .init() : nil, @@ -22,11 +24,13 @@ public struct PaywallView: View { } init( + mode: PaywallViewMode = .fullScreen, offering: Offering, paywall: PaywallData, introEligibility: TrialOrIntroEligibilityChecker?, purchaseHandler: PurchaseHandler? ) { + self.mode = mode self.offering = offering self.paywall = paywall self.introEligibility = introEligibility @@ -37,7 +41,7 @@ public struct PaywallView: View { public var body: some View { if let checker = self.introEligibility, let purchaseHandler = self.purchaseHandler { self.paywall - .createView(for: self.offering) + .createView(for: self.offering, mode: self.mode) .environmentObject(checker) .environmentObject(purchaseHandler) } else { @@ -58,6 +62,7 @@ struct PaywallView_Previews: PreviewProvider { if let paywall = offering.paywall { PaywallView( + mode: .fullScreen, offering: offering, paywall: paywall, introEligibility: TrialOrIntroEligibilityChecker diff --git a/RevenueCatUI/Templates/Example1Template.swift b/RevenueCatUI/Templates/Example1Template.swift index dc2f23cfaf..b8bfaae1d8 100644 --- a/RevenueCatUI/Templates/Example1Template.swift +++ b/RevenueCatUI/Templates/Example1Template.swift @@ -4,7 +4,7 @@ import SwiftUI @available(iOS 16.0, macOS 13.0, tvOS 16.0, *) struct Example1Template: TemplateViewType { - private var data: Result + private let configuration: TemplateViewConfiguration @EnvironmentObject private var introEligibilityChecker: TrialOrIntroEligibilityChecker @@ -12,53 +12,20 @@ struct Example1Template: TemplateViewType { @State private var introEligibility: IntroEligibilityStatus? - init( - packages: [Package], - localization: PaywallData.LocalizedConfiguration, - paywall: PaywallData, - colors: PaywallData.Configuration.Colors - ) { - // Fix-me: move this logic out to be used by all templates - if packages.isEmpty { - self.data = .failure(.noPackages) - } else { - let allPackages = paywall.config.packages - let packages = PaywallData.filter(packages: packages, with: allPackages) - - if let package = packages.first { - self.data = .success(.init( - package: package, - localization: localization.processVariables(with: package), - configuration: paywall.config, - headerImageURL: paywall.headerImageURL, - colors: colors - )) - } else { - self.data = .failure(.couldNotFindAnyPackages(expectedTypes: allPackages)) - } - } + init(_ configuration: TemplateViewConfiguration) { + self.configuration = configuration } - // Fix-me: this can be extracted to be used by all templates var body: some View { - switch self.data { - case let .success(data): - Example1TemplateContent(data: data, introEligibility: self.introEligibility) - .task(id: self.package) { - if let package = self.package { - self.introEligibility = await self.introEligibilityChecker.eligibility(for: package) - } - } - - case let .failure(error): - // Fix-me: consider changing this behavior once we understand - // how unlikely we can make this to happen thanks to server-side validations. - DebugErrorView(error, releaseBehavior: .emptyView) + Example1TemplateContent(configuration: self.configuration, + introEligibility: self.introEligibility) + .task(id: self.package) { + self.introEligibility = await self.introEligibilityChecker.eligibility(for: self.package) } } - private var package: Package? { - return (try? self.data.get())?.package + private var package: Package { + return self.configuration.packages.single.content } } @@ -66,39 +33,34 @@ struct Example1Template: TemplateViewType { @available(iOS 16.0, macOS 13.0, tvOS 16.0, *) private struct Example1TemplateContent: View { - private var data: Data + private var configuration: TemplateViewConfiguration private var introEligibility: IntroEligibilityStatus? + private var localization: ProcessedLocalizedConfiguration @EnvironmentObject private var purchaseHandler: PurchaseHandler @Environment(\.dismiss) private var dismiss - init(data: Data, introEligibility: IntroEligibilityStatus?) { - self.data = data + init(configuration: TemplateViewConfiguration, introEligibility: IntroEligibilityStatus?) { + self.configuration = configuration self.introEligibility = introEligibility + self.localization = configuration.packages.single.localization } var body: some View { - ZStack { - self.content - } - } - - @ViewBuilder - private var content: some View { VStack { ScrollView(.vertical) { VStack { AsyncImage( - url: self.data.headerImageURL, + url: self.configuration.headerImageURL, transaction: .init(animation: Constants.defaultAnimation) ) { phase in if let image = phase.image { image .fitToAspect(Self.imageAspectRatio, contentMode: .fill) } else if let error = phase.error { - DebugErrorView("Error loading image from '\(self.data.headerImageURL)': \(error)", + DebugErrorView("Error loading image from '\(self.configuration.headerImageURL)': \(error)", releaseBehavior: .emptyView) } else { Rectangle() @@ -117,17 +79,17 @@ private struct Example1TemplateContent: View { Spacer() Group { - Text(verbatim: self.data.localization.title) + Text(verbatim: self.localization.title) .font(.largeTitle) .fontWeight(.heavy) .padding(.bottom) - Text(verbatim: self.data.localization.subtitle) + Text(verbatim: self.localization.subtitle) .font(.subheadline) } .padding(.horizontal) } - .foregroundColor(self.data.colors.foregroundColor) + .foregroundColor(self.configuration.colors.foregroundColor) .multilineTextAlignment(.center) } .scrollContentBackground(.hidden) @@ -141,17 +103,17 @@ private struct Example1TemplateContent: View { self.button .padding(.horizontal) } - .background(self.data.colors.backgroundColor) + .background(self.configuration.colors.backgroundColor) } private var offerDetails: some View { - let detailsWithIntroOffer = self.data.localization.offerDetailsWithIntroOffer + let detailsWithIntroOffer = self.localization.offerDetailsWithIntroOffer func text() -> String { if let detailsWithIntroOffer = detailsWithIntroOffer, self.isEligibleForIntro { return detailsWithIntroOffer } else { - return self.data.localization.offerDetails + return self.localization.offerDetails } } @@ -163,13 +125,13 @@ private struct Example1TemplateContent: View { } private var ctaText: some View { - let ctaWithIntroOffer = self.data.localization.callToActionWithIntroOffer + let ctaWithIntroOffer = self.localization.callToActionWithIntroOffer func text() -> String { if let ctaWithIntroOffer = ctaWithIntroOffer, self.isEligibleForIntro { return ctaWithIntroOffer } else { - return self.data.localization.callToAction + return self.localization.callToAction } } @@ -189,20 +151,22 @@ private struct Example1TemplateContent: View { @ViewBuilder private var button: some View { + let package = self.configuration.packages.single.content + AsyncButton { - let cancelled = try await self.purchaseHandler.purchase(package: self.data.package).userCancelled + let cancelled = try await self.purchaseHandler.purchase(package: package).userCancelled if !cancelled { await self.dismiss() } } label: { self.ctaText - .foregroundColor(self.data.colors.callToActionForegroundColor) + .foregroundColor(self.configuration.colors.callToActionForegroundColor) .frame(maxWidth: .infinity) } .font(.title2) .fontWeight(.semibold) - .tint(self.data.colors.callToActionBackgroundColor.gradient) + .tint(self.configuration.colors.callToActionBackgroundColor.gradient) .buttonStyle(.borderedProminent) .buttonBorderShape(.capsule) .controlSize(.large) @@ -214,46 +178,6 @@ private struct Example1TemplateContent: View { // MARK: - -@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) -private extension Example1TemplateContent { - - struct Data { - let package: Package - let localization: ProcessedLocalizedConfiguration - let configuration: PaywallData.Configuration - let headerImageURL: URL - let colors: PaywallData.Configuration.Colors - } - - enum Error: Swift.Error { - - case noPackages - case couldNotFindAnyPackages(expectedTypes: [PackageType]) - - } - -} - -@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) -extension Example1TemplateContent.Error: CustomNSError { - - var errorUserInfo: [String: Any] { - return [ - NSLocalizedDescriptionKey: self.description - ] - } - - private var description: String { - switch self { - case .noPackages: - return "Attempted to display paywall with no packages." - case let .couldNotFindAnyPackages(expectedTypes): - return "Couldn't find any requested packages: \(expectedTypes)" - } - } - -} - @available(iOS 16.0, macOS 13.0, tvOS 16.0, *) private extension View { diff --git a/RevenueCatUI/Templates/TemplateViewType.swift b/RevenueCatUI/Templates/TemplateViewType.swift index d1513fafbb..2764ed7d2e 100644 --- a/RevenueCatUI/Templates/TemplateViewType.swift +++ b/RevenueCatUI/Templates/TemplateViewType.swift @@ -1,15 +1,22 @@ import RevenueCat import SwiftUI +/// A `SwiftUI` view that can display a paywall with `TemplateViewConfiguration`. @available(iOS 16.0, macOS 13.0, tvOS 16.0, *) protocol TemplateViewType: SwiftUI.View { - init( - packages: [Package], - localization: PaywallData.LocalizedConfiguration, - paywall: PaywallData, - colors: PaywallData.Configuration.Colors - ) + init(_ configuration: TemplateViewConfiguration) + +} + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +private extension PaywallTemplate { + + var packageSetting: TemplateViewConfiguration.PackageSetting { + switch self { + case .example1: return .single + } + } } @@ -17,41 +24,40 @@ protocol TemplateViewType: SwiftUI.View { extension PaywallData { @ViewBuilder - func createView(for offering: Offering) -> some View { - let colors = self.config.colors.multiScheme + func createView(for offering: Offering, mode: PaywallViewMode) -> some View { + switch self.configuration(for: offering, mode: mode) { + case let .success(configuration): + Self.createView(template: self.template, configuration: configuration) - switch self.template { - case .example1: - Example1Template( - packages: offering.availablePackages, - localization: self.localizedConfiguration, - paywall: self, - colors: colors + case let .failure(error): + DebugErrorView(error, releaseBehavior: .emptyView) + } + } + + private func configuration( + for offering: Offering, + mode: PaywallViewMode + ) -> Result { + return Result { + TemplateViewConfiguration( + mode: mode, + packages: try .create(with: offering.availablePackages, + filter: self.config.packages, + localization: self.localizedConfiguration, + setting: self.template.packageSetting), + configuration: self.config, + colors: self.config.colors.multiScheme, + headerImageURL: self.headerImageURL ) } } - static func filter(packages: [Package], with list: [PackageType]) -> [Package] { - // Only subscriptions are supported at the moment - let subscriptions = packages.filter { $0.storeProduct.productCategory == .subscription } - let map = Dictionary(grouping: subscriptions) { $0.packageType } - - return list.compactMap { type in - if let packages = map[type] { - switch packages.count { - case 0: - // This isn't actually possible because of `Dictionary(grouping:by:) - return nil - case 1: - return packages.first - default: - Logger.warning("Found multiple \(type) packages. Will use the first one.") - return packages.first - } - } else { - Logger.warning("Couldn't find '\(type)'") - return nil - } + @ViewBuilder + private static func createView(template: PaywallTemplate, + configuration: TemplateViewConfiguration) -> some View { + switch template { + case .example1: + Example1Template(configuration) } } diff --git a/Tests/RevenueCatUITests/Data/TemplateViewConfigurationTests.swift b/Tests/RevenueCatUITests/Data/TemplateViewConfigurationTests.swift new file mode 100644 index 0000000000..85eaead7f5 --- /dev/null +++ b/Tests/RevenueCatUITests/Data/TemplateViewConfigurationTests.swift @@ -0,0 +1,189 @@ +// +// TemplateViewConfigurationTests.swift +// +// +// Created by Nacho Soto on 7/13/23. +// + +import Nimble +import RevenueCat +@testable import RevenueCatUI +import XCTest + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) +class BaseTemplateViewConfigurationTests: TestCase {} + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) +class TemplateViewConfigurationCreationTests: BaseTemplateViewConfigurationTests { + + private typealias Config = TemplateViewConfiguration.PackageConfiguration + + func testCreateWithNoPackages() { + expect { + try Config.create( + with: [], + filter: [.monthly], + localization: TestData.paywallWithIntroOffer.localizedConfiguration, + setting: .single + ) + }.to(throwError(TemplateError.noPackages)) + } + + func testCreateWithNoFilter() { + expect { + try Config.create( + with: [Self.monthly], + filter: [], + localization: TestData.paywallWithIntroOffer.localizedConfiguration, + setting: .single + ) + }.to(throwError(TemplateError.emptyPackageList)) + } + + func testCreateSinglePackage() throws { + let result = try Config.create( + with: [Self.monthly], + filter: [.monthly], + localization: Self.localization, + setting: .single + ) + + switch result { + case let .single(package): + expect(package.content) === Self.monthly + Self.verifyLocalizationWasProcessed(package.localization, for: Self.monthly) + case .multiple: + fail("Invalid result: \(result)") + } + } + + func testCreateMultiplePackage() throws { + let result = try Config.create( + with: [Self.monthly, Self.annual, Self.weekly], + filter: [.annual, .monthly], + localization: Self.localization, + setting: .multiple + ) + + switch result { + case .single: + fail("Invalid result: \(result)") + case let .multiple(packages): + expect(packages).to(haveCount(2)) + + let annual = packages[0] + expect(annual.content) === Self.annual + Self.verifyLocalizationWasProcessed(annual.localization, for: Self.annual) + + let monthly = packages[1] + expect(monthly.content) === Self.monthly + Self.verifyLocalizationWasProcessed(monthly.localization, for: Self.monthly) + } + } + + private static func verifyLocalizationWasProcessed( + _ localization: ProcessedLocalizedConfiguration, + for package: Package + ) { + expect(localization.title).to( + contain(package.productName), + description: "Localization wasn't processed" + ) + } + +} + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) +class TemplateViewConfigurationFilteringTests: BaseTemplateViewConfigurationTests { + + func testFilterNoPackages() { + expect(TemplateViewConfiguration.filter(packages: [], with: [.monthly])) == [] + } + + func testFilterPackagesWithEmptyList() { + expect(TemplateViewConfiguration.filter(packages: [Self.monthly], with: [])) == [] + } + + func testFilterOutSinglePackge() { + expect(TemplateViewConfiguration.filter(packages: [Self.monthly], with: [.annual])) == [] + } + + func testFilterOutNonSubscriptions() { + expect(TemplateViewConfiguration.filter(packages: [Self.consumable], with: [.custom])) == [] + } + + func testFilterByPackageType() { + expect(TemplateViewConfiguration.filter(packages: [Self.monthly, Self.annual], with: [.monthly])) == [ + Self.monthly + ] + } + + func testFilterWithDuplicatedPackageTypes() { + expect(TemplateViewConfiguration.filter(packages: [Self.monthly, Self.annual], with: [.monthly, .monthly])) == [ + Self.monthly, + Self.monthly + ] + } + + func testFilterReturningMultiplePackages() { + expect(TemplateViewConfiguration.filter(packages: [Self.weekly, Self.monthly, Self.annual], + with: [.weekly, .monthly])) == [ + Self.weekly, + Self.monthly + ] + } + +} + +// MARK: - Private + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) +private extension BaseTemplateViewConfigurationTests { + + static let weekly = Package( + identifier: "weekly", + packageType: .weekly, + storeProduct: TestData.productWithIntroOffer.toStoreProduct(), + offeringIdentifier: offeringIdentifier + ) + static let monthly = Package( + identifier: "monthly", + packageType: .monthly, + storeProduct: TestData.productWithIntroOffer.toStoreProduct(), + offeringIdentifier: offeringIdentifier + ) + static let annual = Package( + identifier: "annual", + packageType: .annual, + storeProduct: TestData.productWithNoIntroOffer.toStoreProduct(), + offeringIdentifier: offeringIdentifier + ) + + static let consumable = Package( + identifier: "consumable", + packageType: .custom, + storeProduct: consumableProduct.toStoreProduct(), + offeringIdentifier: offeringIdentifier + ) + + static let localization: PaywallData.LocalizedConfiguration = .init( + title: "Title: {{ product_name }}", + subtitle: "Get access to all our educational content trusted by thousands of parents.", + callToAction: "Purchase for {{ price }}", + callToActionWithIntroOffer: nil, + offerDetails: "{{ price_per_month }} per month", + offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month" + ) + + private static let consumableProduct = TestStoreProduct( + localizedTitle: "Coins", + price: 199.99, + localizedPriceString: "$199.99", + productIdentifier: "com.revenuecat.coins", + productType: .consumable, + localizedDescription: "Coins" + ) + + private static let offeringIdentifier = "offering" + +} diff --git a/Tests/RevenueCatUITests/VariablesTests.swift b/Tests/RevenueCatUITests/Data/VariablesTests.swift similarity index 100% rename from Tests/RevenueCatUITests/VariablesTests.swift rename to Tests/RevenueCatUITests/Data/VariablesTests.swift diff --git a/Tests/RevenueCatUITests/PackageFilteringTests.swift b/Tests/RevenueCatUITests/PackageFilteringTests.swift deleted file mode 100644 index 1faac502d4..0000000000 --- a/Tests/RevenueCatUITests/PackageFilteringTests.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// PackageFilteringTests.swift -// -// -// Created by Nacho Soto on 7/13/23. -// - -import Nimble -import RevenueCat -@testable import RevenueCatUI -import XCTest - -@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) -class PackageFilteringTests: TestCase { - - func testFilterNoPackages() { - expect(PaywallData.filter(packages: [], with: [.monthly])) == [] - } - - func testFilterPackagesWithEmptyList() { - expect(PaywallData.filter(packages: [Self.monthly], with: [])) == [] - } - - func testFilterOutSinglePackge() { - expect(PaywallData.filter(packages: [Self.monthly], with: [.annual])) == [] - } - - func testFilterOutNonSubscriptions() { - expect(PaywallData.filter(packages: [Self.consumable], with: [.custom])) == [] - } - - func testFilterByPackageType() { - expect(PaywallData.filter(packages: [Self.monthly, Self.annual], with: [.monthly])) == [Self.monthly] - } - - func testFilterWithDuplicatedPackageTypes() { - expect(PaywallData.filter(packages: [Self.monthly, Self.annual], with: [.monthly, .monthly])) == [ - Self.monthly, - Self.monthly - ] - } - - func testFilterReturningMultiplePackages() { - expect(PaywallData.filter(packages: [Self.weekly, Self.monthly, Self.annual], with: [.weekly, .monthly])) == [ - Self.weekly, - Self.monthly - ] - } - -} - -@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) -private extension PackageFilteringTests { - - static let weekly = Package( - identifier: "weekly", - packageType: .weekly, - storeProduct: TestData.productWithIntroOffer.toStoreProduct(), - offeringIdentifier: offeringIdentifier - ) - static let monthly = Package( - identifier: "monthly", - packageType: .monthly, - storeProduct: TestData.productWithIntroOffer.toStoreProduct(), - offeringIdentifier: offeringIdentifier - ) - static let annual = Package( - identifier: "annual", - packageType: .annual, - storeProduct: TestData.productWithNoIntroOffer.toStoreProduct(), - offeringIdentifier: offeringIdentifier - ) - - static let consumable = Package( - identifier: "consumable", - packageType: .custom, - storeProduct: consumableProduct.toStoreProduct(), - offeringIdentifier: offeringIdentifier - ) - - private static let consumableProduct = TestStoreProduct( - localizedTitle: "Coins", - price: 199.99, - localizedPriceString: "$199.99", - productIdentifier: "com.revenuecat.coins", - productType: .consumable, - localizedDescription: "Coins" - ) - - private static let offeringIdentifier = "offering" - -}