From 069c5760751f47dcfabbf0c55704ab0a1e7121ff Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Thu, 24 Oct 2024 21:08:28 -0500 Subject: [PATCH 1/8] Improved JSON format for ButtonComponent codables --- .../Button/ButtonComponentView.swift | 2 +- .../Button/ButtonComponentViewModel.swift | 6 +- .../Components/PaywallButtonComponent.swift | 137 +++++++++++++++--- 3 files changed, 119 insertions(+), 26 deletions(-) diff --git a/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift b/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift index 5158ca83e8..f227cdaf16 100644 --- a/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift +++ b/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift @@ -76,7 +76,7 @@ struct ButtonComponentView: View { switch destination { case .customerCenter: showCustomerCenter = true - case .URL(let url, let method), + case .url(let url, let method), .privacyPolicy(let url, let method), .terms(let url, let method): navigateToUrl(url: url, method: method) diff --git a/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift b/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift index b81b6c35fa..8ba6136140 100644 --- a/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift @@ -34,7 +34,7 @@ public class ButtonComponentViewModel { /// This way the view layer doesn't need to handle this error scenario. internal enum Destination { case customerCenter - case URL(url: URL, method: PaywallComponent.ButtonComponent.URLMethod) + case url(url: URL, method: PaywallComponent.ButtonComponent.URLMethod) case privacyPolicy(url: URL, method: PaywallComponent.ButtonComponent.URLMethod) case terms(url: URL, method: PaywallComponent.ButtonComponent.URLMethod) } @@ -66,9 +66,9 @@ public class ButtonComponentViewModel { switch destination { case .customerCenter: self.action = .navigateTo(destination: .customerCenter) - case .URL(let urlLid, let method): + case .url(let urlLid, let method): self.action = .navigateTo( - destination: .URL(url: try localizedStrings.urlFromLid(urlLid), method: method) + destination: .url(url: try localizedStrings.urlFromLid(urlLid), method: method) ) case .privacyPolicy(let urlLid, let method): self.action = .navigateTo( diff --git a/Sources/Paywalls/Components/PaywallButtonComponent.swift b/Sources/Paywalls/Components/PaywallButtonComponent.swift index 08b2919961..096d4c8e50 100644 --- a/Sources/Paywalls/Components/PaywallButtonComponent.swift +++ b/Sources/Paywalls/Components/PaywallButtonComponent.swift @@ -21,28 +21,6 @@ public extension PaywallComponent { struct ButtonComponent: PaywallComponentBase { - // swiftlint:disable:next nesting - public enum Action: Codable, Sendable, Hashable, Equatable { - case restorePurchases - case navigateTo(destination: Destination) - case navigateBack - } - - // swiftlint:disable:next nesting - public enum Destination: Codable, Sendable, Hashable, Equatable { - case customerCenter - case URL(urlLid: String, method: URLMethod) - case privacyPolicy(urlLid: String, method: URLMethod) - case terms(urlLid: String, method: URLMethod) - } - - // swiftlint:disable:next nesting - public enum URLMethod: Codable, Sendable, Hashable, Equatable { - case inAppBrowser - case externalBrowser - case deepLink - } - let type: ComponentType public let action: Action public let stack: PaywallComponent.StackComponent @@ -60,4 +38,119 @@ public extension PaywallComponent { } +public extension PaywallComponent.ButtonComponent { + + enum Action: Codable, Sendable, Hashable, Equatable { + case restorePurchases + case navigateBack + case navigateTo(destination: Destination) + + // swiftlint:disable:next nesting + private enum CodingKeys: String, CodingKey { + case type + case destination + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .restorePurchases: + try container.encode("restore_purchases", forKey: .type) + case .navigateBack: + try container.encode("navigate_back", forKey: .type) + case .navigateTo(let destination): + try container.encode("navigate_to", forKey: .type) + try container.encode(destination, forKey: .destination) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + + switch type { + case "restore_purchases": + self = .restorePurchases + case "navigate_back": + self = .navigateBack + case "navigate_to": + let destination = try container.decode(Destination.self, forKey: .destination) + self = .navigateTo(destination: destination) + default: + throw DecodingError.dataCorruptedError( + forKey: .type, + in: container, + debugDescription: "Invalid action type" + ) + } + } + } + + enum Destination: Codable, Sendable, Hashable, Equatable { + case customerCenter + case terms(urlLid: String, method: URLMethod) + case privacyPolicy(urlLid: String, method: URLMethod) + case url(urlLid: String, method: URLMethod) + + // swiftlint:disable:next nesting + private enum CodingKeys: String, CodingKey { + case destination + case url + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .customerCenter: + try container.encode("customer_center", forKey: .destination) + case .terms(let urlLid, let method): + try container.encode("terms", forKey: .destination) + try container.encode(URLPayload(urlLid: urlLid, method: method), forKey: .url) + case .privacyPolicy(let urlLid, let method): + try container.encode("privacy_policy", forKey: .destination) + try container.encode(URLPayload(urlLid: urlLid, method: method), forKey: .url) + case .url(let urlLid, let method): + try container.encode("url", forKey: .destination) + try container.encode(URLPayload(urlLid: urlLid, method: method), forKey: .url) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let destination = try container.decode(String.self, forKey: .destination) + + switch destination { + case "customer_center": + self = .customerCenter + case "terms": + let urlPayload = try container.decode(URLPayload.self, forKey: .url) + self = .terms(urlLid: urlPayload.urlLid, method: urlPayload.method) + case "url": + let urlPayload = try container.decode(URLPayload.self, forKey: .url) + self = .url(urlLid: urlPayload.urlLid, method: urlPayload.method) + default: + throw DecodingError.dataCorruptedError( + forKey: .destination, + in: container, + debugDescription: "Invalid destination type" + ) + } + } + } + + enum URLMethod: String, Codable, Sendable, Hashable, Equatable { + case inAppBrowser + case externalBrowser + case deepLink + } + + private struct URLPayload: Codable, Hashable, Sendable { + let urlLid: String + let method: URLMethod + } + +} + #endif From 56ea4cb54bcdc6ec1b6f13c4cc65df72c20fb992 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Fri, 25 Oct 2024 08:08:10 -0500 Subject: [PATCH 2/8] Started to remove PackageGroup --- RevenueCat.xcodeproj/project.pbxproj | 4 - RevenueCatUI/Data/Strings.swift | 6 +- .../Button/ButtonComponentView.swift | 1 + .../Button/ButtonComponentViewModel.swift | 2 + .../Package/PackageComponentView.swift | 5 +- .../Package/PackageComponentViewModel.swift | 17 +- .../PackageGroupComponentView.swift | 223 +++++++++--------- .../PackageGroupComponentViewModel.swift | 180 +++++++------- .../PaywallComponentViewModel.swift | 30 ++- .../Stack/StackComponentViewModel.swift | 5 +- .../Components/TemplateComponentsView.swift | 50 +++- .../Common/PaywallComponentBase.swift | 7 - .../Components/PaywallPackageComponent.swift | 3 + .../PaywallPackageGroupComponent.swift | 39 --- .../Components/PaywallStackComponent.swift | 22 +- 15 files changed, 296 insertions(+), 298 deletions(-) delete mode 100644 Sources/Paywalls/Components/PaywallPackageGroupComponent.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index e9e0870165..60f1a585f2 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -20,7 +20,6 @@ 2C0B98CD2797070B00C5874F /* PromotionalOffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0B98CC2797070B00C5874F /* PromotionalOffer.swift */; }; 2C2AEB0F2CA64E0E00A50F38 /* Template1Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AEB0E2CA64E0E00A50F38 /* Template1Preview.swift */; }; 2C2AEB3B2CA7209F00A50F38 /* PaywallPackageComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AEB3A2CA7209F00A50F38 /* PaywallPackageComponent.swift */; }; - 2C2AEB3D2CA720B700A50F38 /* PaywallPackageGroupComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AEB3C2CA720B700A50F38 /* PaywallPackageGroupComponent.swift */; }; 2C2AEB3F2CA7235300A50F38 /* PaywallPurchaseButtonComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AEB3E2CA7235300A50F38 /* PaywallPurchaseButtonComponent.swift */; }; 2C4C36132C6FBA8B00AE959B /* CompatibilityTopBarTrailing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4C36122C6FBA8B00AE959B /* CompatibilityTopBarTrailing.swift */; }; 2C6CC1162B8D2B6900432E4D /* PurchasesSyncAttributesAndOfferingsIfNeededTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6CC1152B8D2B6800432E4D /* PurchasesSyncAttributesAndOfferingsIfNeededTests.swift */; }; @@ -1170,7 +1169,6 @@ 2C0B98CC2797070B00C5874F /* PromotionalOffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalOffer.swift; sourceTree = ""; }; 2C2AEB0E2CA64E0E00A50F38 /* Template1Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Template1Preview.swift; sourceTree = ""; }; 2C2AEB3A2CA7209F00A50F38 /* PaywallPackageComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallPackageComponent.swift; sourceTree = ""; }; - 2C2AEB3C2CA720B700A50F38 /* PaywallPackageGroupComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallPackageGroupComponent.swift; sourceTree = ""; }; 2C2AEB3E2CA7235300A50F38 /* PaywallPurchaseButtonComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallPurchaseButtonComponent.swift; sourceTree = ""; }; 2C4C36122C6FBA8B00AE959B /* CompatibilityTopBarTrailing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompatibilityTopBarTrailing.swift; sourceTree = ""; }; 2C5F71F52C3D6C2600B0FE4B /* header.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = header.heic; sourceTree = ""; }; @@ -4382,7 +4380,6 @@ 8893C9B42C7D1F090060B030 /* PaywallLinkButtonComponent.swift */, 88AD01072C740CF400AA1F2B /* PaywallTextComponent.swift */, 2C2AEB3A2CA7209F00A50F38 /* PaywallPackageComponent.swift */, - 2C2AEB3C2CA720B700A50F38 /* PaywallPackageGroupComponent.swift */, 2C2AEB3E2CA7235300A50F38 /* PaywallPurchaseButtonComponent.swift */, ); path = Components; @@ -5543,7 +5540,6 @@ B35F9E0926B4BEED00095C3F /* String+Extensions.swift in Sources */, 574A2EE7282C3F0800150D40 /* AnyDecodable.swift in Sources */, 2C2AEB3F2CA7235300A50F38 /* PaywallPurchaseButtonComponent.swift in Sources */, - 2C2AEB3D2CA720B700A50F38 /* PaywallPackageGroupComponent.swift in Sources */, 4FC883812AA7A2BD00A3DE03 /* ProcessInfo+Extensions.swift in Sources */, 57488B7F29CB70E50000EE7E /* ProductEntitlementMapping.swift in Sources */, B34605CF279A6E380031CA74 /* GetOfferingsOperation.swift in Sources */, diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index e26ae11968..728de5743c 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -56,7 +56,7 @@ enum Strings { case paywall_contains_no_localization_data case paywall_could_not_find_localization(String) case paywall_could_not_find_package(String) - case paywall_could_not_find_default_package(String) + case paywall_could_not_find_default_package case paywall_could_not_find_any_packages case paywall_invalid_url(String) case no_in_app_browser_tvos @@ -175,8 +175,8 @@ extension Strings: CustomStringConvertible { "This could be caused by a package that doesn't have a product on this platform or the product might not " + " be available for this region." - case .paywall_could_not_find_default_package(let identifier): - return "Could not find default package \(identifier) for paywall. Using first package instead. " + + case .paywall_could_not_find_default_package: + return "Could not find default package for paywall. Using first package instead. " + "This package will not show in the paywall. This could be caused by a package that doesn't have a " + "product on this platform or the product might not be available for this region." diff --git a/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift b/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift index f227cdaf16..100ceb0544 100644 --- a/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift +++ b/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift @@ -111,6 +111,7 @@ struct ButtonComponentView_Previews: PreviewProvider { ButtonComponentView( // swiftlint:disable:next force_try viewModel: try! .init( + packageCollector: PackageCollector(), component: .init( action: .navigateBack, stack: .init( diff --git a/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift b/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift index 8ba6136140..6082ec6e6d 100644 --- a/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift @@ -45,6 +45,7 @@ public class ButtonComponentViewModel { let stackViewModel: StackComponentViewModel init( + packageCollector: PackageCollector, component: PaywallComponent.ButtonComponent, localizedStrings: PaywallComponent.LocalizationDictionary, offering: Offering @@ -52,6 +53,7 @@ public class ButtonComponentViewModel { self.component = component self.localizedStrings = localizedStrings self.stackViewModel = try StackComponentViewModel( + packageCollector: packageCollector, component: component.stack, localizedStrings: localizedStrings, offering: offering diff --git a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift index 48b4160ba5..2e0dd91e51 100644 --- a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift +++ b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift @@ -78,18 +78,19 @@ struct PackageComponentView_Previews: PreviewProvider { PackageComponentView( // swiftlint:disable:next force_try viewModel: try! .init( + packageCollector: PackageCollector(), localizedStrings: [ "name": .string("Weekly"), "detail": .string("Get for $39.99/wk") ], component: .init( packageID: "weekly", + isDefaultSelected: false, stack: stack ), offering: .init(identifier: "default", serverDescription: "", - availablePackages: [package]), - package: package + availablePackages: [package]) ), onDismiss: {} ) .previewLayout(.sizeThatFits) diff --git a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentViewModel.swift b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentViewModel.swift index 4136ab1601..b949ab28f1 100644 --- a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentViewModel.swift @@ -23,19 +23,26 @@ class PackageComponentViewModel { private let component: PaywallComponent.PackageComponent private let offering: Offering - let package: Package + let isDefaultSelected: Bool + let package: Package? let stackViewModel: StackComponentViewModel - init(localizedStrings: PaywallComponent.LocalizationDictionary, + init(packageCollector: PackageCollector, + localizedStrings: PaywallComponent.LocalizationDictionary, component: PaywallComponent.PackageComponent, - offering: Offering, - package: Package) throws { + offering: Offering) throws { self.localizedStrings = localizedStrings self.component = component self.offering = offering - self.package = package + + self.isDefaultSelected = component.isDefaultSelected + self.package = offering.package(identifier: component.packageID) + if package == nil { + Logger.warning(Strings.paywall_could_not_find_package(component.packageID)) + } self.stackViewModel = try StackComponentViewModel( + packageCollector: packageCollector, component: component.stack, localizedStrings: localizedStrings, offering: offering diff --git a/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentView.swift b/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentView.swift index ccd3b4fd08..d6959732fe 100644 --- a/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentView.swift +++ b/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentView.swift @@ -17,116 +17,117 @@ import SwiftUI #if PAYWALL_COMPONENTS -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PackageGroupComponentView: View { - - @EnvironmentObject - private var paywallState: PaywallState - - let viewModel: PackageGroupComponentViewModel - let onDismiss: () -> Void - - var body: some View { - // WIP: Do something with default package id and selection - StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: self.onDismiss) - .onAppear { - self.paywallState.select(package: self.viewModel.defaultPackage) - } - } - -} - -#if DEBUG - -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -struct PackagesComponentView_Previews: PreviewProvider { - - static let paywallState = PaywallState() - - static let packages: [PaywallComponent.PackageComponent] = [ - makePackage(packageID: "weekly", - nameTextLid: "weekly_name", - detailTextLid: "weekly_detail"), - makePackage(packageID: "non_existant_package", - nameTextLid: "non_existant_name", - detailTextLid: "non_existant_detail"), - makePackage(packageID: "monthly", - nameTextLid: "monthly_name", - detailTextLid: "monthly_detail") - ] - - static func makePackage(packageID: String, - nameTextLid: String, - detailTextLid: String) -> PaywallComponent.PackageComponent { - let stack: PaywallComponent.StackComponent = .init( - components: [ - .text(.init( - text: nameTextLid, - fontWeight: .bold, - color: .init(light: "#000000"), - padding: .zero, - margin: .zero - )), - .text(.init( - text: detailTextLid, - color: .init(light: "#000000"), - padding: .zero, - margin: .zero - )) - ], - dimension: .vertical(.leading), - spacing: 0, - backgroundColor: nil, - padding: PaywallComponent.Padding(top: 10, - bottom: 10, - leading: 20, - trailing: 20) - ) - - return PaywallComponent.PackageComponent( - packageID: packageID, - stack: stack - ) - } - - static var previews: some View { - // Packages - PackageGroupComponentView( - // swiftlint:disable:next force_try - viewModel: try! PackageGroupComponentViewModel( - localizedStrings: [ - "weekly_name": .string("Weekly"), - "weekly_detail": .string("Get for $39.99/week"), - "monthly_name": .string("Monthly"), - "monthly_detail": .string("Get for $139.99/month"), - "non_existant_name": .string("THIS SHOULDN'T SHOW"), - "non_existant_detail": .string("THIS SHOULDN'T SHOW") - - ], - component: PaywallComponent.PackageGroupComponent( - defaultSelectedPackageID: "weekly", - stack: .init(components: packages) - ), - offering: Offering(identifier: "default", - serverDescription: "", - availablePackages: [ - Package(identifier: "weekly", - packageType: .weekly, - storeProduct: .init(sk1Product: .init()), - offeringIdentifier: "default"), - Package(identifier: "monthly", - packageType: .monthly, - storeProduct: .init(sk1Product: .init()), - offeringIdentifier: "default") - ]) - ), onDismiss: {} - ) - .environmentObject(paywallState) - .previewLayout(.sizeThatFits) - .previewDisplayName("Packages") - } -} - -#endif +//@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +//struct PackageGroupComponentView: View { +// +// @EnvironmentObject +// private var paywallState: PaywallState +// +// let viewModel: PackageGroupComponentViewModel +// let onDismiss: () -> Void +// +// var body: some View { +// // WIP: Do something with default package id and selection +// StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: self.onDismiss) +// .onAppear { +// self.paywallState.select(package: self.viewModel.defaultPackage) +// } +// } +// +//} +// +//#if DEBUG +// +//@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +//struct PackagesComponentView_Previews: PreviewProvider { +// +// static let paywallState = PaywallState() +// +// static let packages: [PaywallComponent.PackageComponent] = [ +// makePackage(packageID: "weekly", +// nameTextLid: "weekly_name", +// detailTextLid: "weekly_detail"), +// makePackage(packageID: "non_existant_package", +// nameTextLid: "non_existant_name", +// detailTextLid: "non_existant_detail"), +// makePackage(packageID: "monthly", +// nameTextLid: "monthly_name", +// detailTextLid: "monthly_detail") +// ] +// +// static func makePackage(packageID: String, +// nameTextLid: String, +// detailTextLid: String) -> PaywallComponent.PackageComponent { +// let stack: PaywallComponent.StackComponent = .init( +// components: [ +// .text(.init( +// text: nameTextLid, +// fontWeight: .bold, +// color: .init(light: "#000000"), +// padding: .zero, +// margin: .zero +// )), +// .text(.init( +// text: detailTextLid, +// color: .init(light: "#000000"), +// padding: .zero, +// margin: .zero +// )) +// ], +// dimension: .vertical(.leading), +// spacing: 0, +// backgroundColor: nil, +// padding: PaywallComponent.Padding(top: 10, +// bottom: 10, +// leading: 20, +// trailing: 20) +// ) +// +// return PaywallComponent.PackageComponent( +// packageID: packageID, +// isDefaultSelected: false, +// stack: stack +// ) +// } +// +// static var previews: some View { +// // Packages +// PackageGroupComponentView( +// // swiftlint:disable:next force_try +// viewModel: try! PackageGroupComponentViewModel( +// localizedStrings: [ +// "weekly_name": .string("Weekly"), +// "weekly_detail": .string("Get for $39.99/week"), +// "monthly_name": .string("Monthly"), +// "monthly_detail": .string("Get for $139.99/month"), +// "non_existant_name": .string("THIS SHOULDN'T SHOW"), +// "non_existant_detail": .string("THIS SHOULDN'T SHOW") +// +// ], +// component: PaywallComponent.PackageGroupComponent( +// defaultSelectedPackageID: "weekly", +// stack: .init(components: packages) +// ), +// offering: Offering(identifier: "default", +// serverDescription: "", +// availablePackages: [ +// Package(identifier: "weekly", +// packageType: .weekly, +// storeProduct: .init(sk1Product: .init()), +// offeringIdentifier: "default"), +// Package(identifier: "monthly", +// packageType: .monthly, +// storeProduct: .init(sk1Product: .init()), +// offeringIdentifier: "default") +// ]) +// ), onDismiss: {} +// ) +// .environmentObject(paywallState) +// .previewLayout(.sizeThatFits) +// .previewDisplayName("Packages") +// } +//} +// +//#endif #endif diff --git a/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentViewModel.swift b/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentViewModel.swift index b7fce3183e..d1153c1c21 100644 --- a/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentViewModel.swift @@ -16,95 +16,95 @@ import RevenueCat #if PAYWALL_COMPONENTS -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -class PackageGroupComponentViewModel { - - private let localizedStrings: PaywallComponent.LocalizationDictionary - private let component: PaywallComponent.PackageGroupComponent - private let offering: Offering - - let defaultPackage: Package - let stackViewModel: StackComponentViewModel - - init(localizedStrings: PaywallComponent.LocalizationDictionary, - component: PaywallComponent.PackageGroupComponent, - offering: Offering) throws { - self.localizedStrings = localizedStrings - self.component = component - self.offering = offering - - let info = try Self.getPackages(component: component, offering: offering) - self.defaultPackage = info.defaultPackage - - let componentViewModels = try info.availablePackageComponents.map { info in - try PackageComponentViewModel( - localizedStrings: localizedStrings, - component: info.component, - offering: offering, - package: info.package - ).stackViewModel - }.map(PaywallComponentViewModel.stack) - - self.stackViewModel = StackComponentViewModel( - component: .init( - components: [], // Empty on purpose because we are feeding this view models already - dimension: component.stack.dimension, - width: component.stack.width, - spacing: component.stack.spacing, - backgroundColor: component.stack.backgroundColor, - padding: component.stack.padding, - margin: component.stack.margin, - cornerRadiuses: component.stack.cornerRadiuses, - border: component.stack.border - ), - viewModels: componentViewModels - ) - } - - static func getPackages( - component: PaywallComponent.PackageGroupComponent, - offering: Offering - ) throws -> (defaultPackage: Package, - availablePackageComponents: [(component: PaywallComponent.PackageComponent, package: Package)]) { - - // Stack of packages - let packages = component.stack.components - - // Get list of available package components and their packages - let availablePackageInfos = packages.compactMap { packageComponent in - let pkg = offering.availablePackages.first(where: { $0.packageIdentifier == packageComponent.packageID }) - if let pkg { - return (component: packageComponent, package: pkg) - } else { - return nil - } - } - - // We need packages - guard let firstPackage = availablePackageInfos.first else { - Logger.error(Strings.paywall_could_not_find_any_packages) - throw PackageGroupValidationError.noAvailablePackages("No available packages found") - } - - // Attempt to get default package - let defaultPackage = availablePackageInfos.first { packageInfo in - return packageInfo.package.id == component.defaultSelectedPackageID - } - - if let defaultPackage { - return (defaultPackage.package, availablePackageInfos) - } else { - Logger.warning(Strings.paywall_could_not_find_default_package(component.defaultSelectedPackageID)) - return (firstPackage.package, availablePackageInfos) - } - } - - enum PackageGroupValidationError: Error { - - case noAvailablePackages(String) - - } - -} +//@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +//class PackageGroupComponentViewModel { +// +// private let localizedStrings: PaywallComponent.LocalizationDictionary +// private let component: PaywallComponent.PackageGroupComponent +// private let offering: Offering +// +// let defaultPackage: Package +// let stackViewModel: StackComponentViewModel +// +// init(localizedStrings: PaywallComponent.LocalizationDictionary, +// component: PaywallComponent.PackageGroupComponent, +// offering: Offering) throws { +// self.localizedStrings = localizedStrings +// self.component = component +// self.offering = offering +// +// let info = try Self.getPackages(component: component, offering: offering) +// self.defaultPackage = info.defaultPackage +// +// let componentViewModels = try info.availablePackageComponents.map { info in +// try PackageComponentViewModel( +// localizedStrings: localizedStrings, +// component: info.component, +// offering: offering, +// package: info.package +// ).stackViewModel +// }.map(PaywallComponentViewModel.stack) +// +// self.stackViewModel = StackComponentViewModel( +// component: .init( +// components: [], // Empty on purpose because we are feeding this view models already +// dimension: component.stack.dimension, +// width: component.stack.width, +// spacing: component.stack.spacing, +// backgroundColor: component.stack.backgroundColor, +// padding: component.stack.padding, +// margin: component.stack.margin, +// cornerRadiuses: component.stack.cornerRadiuses, +// border: component.stack.border +// ), +// viewModels: componentViewModels +// ) +// } +// +// static func getPackages( +// component: PaywallComponent.PackageGroupComponent, +// offering: Offering +// ) throws -> (defaultPackage: Package, +// availablePackageComponents: [(component: PaywallComponent.PackageComponent, package: Package)]) { +// +// // Stack of packages +// let packages = component.stack.components +// +// // Get list of available package components and their packages +// let availablePackageInfos = packages.compactMap { packageComponent in +// let pkg = offering.availablePackages.first(where: { $0.packageIdentifier == packageComponent.packageID }) +// if let pkg { +// return (component: packageComponent, package: pkg) +// } else { +// return nil +// } +// } +// +// // We need packages +// guard let firstPackage = availablePackageInfos.first else { +// Logger.error(Strings.paywall_could_not_find_any_packages) +// throw PackageGroupValidationError.noAvailablePackages("No available packages found") +// } +// +// // Attempt to get default package +// let defaultPackage = availablePackageInfos.first { packageInfo in +// return packageInfo.package.id == component.defaultSelectedPackageID +// } +// +// if let defaultPackage { +// return (defaultPackage.package, availablePackageInfos) +// } else { +// Logger.warning(Strings.paywall_could_not_find_default_package(component.defaultSelectedPackageID)) +// return (firstPackage.package, availablePackageInfos) +// } +// } +// +// enum PackageGroupValidationError: Error { +// +// case noAvailablePackages(String) +// +// } +// +//} #endif diff --git a/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift b/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift index 088c9e5760..6ae01948ca 100644 --- a/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift @@ -19,16 +19,21 @@ enum PaywallComponentViewModel { case stack(StackComponentViewModel) case linkButton(LinkButtonComponentViewModel) case button(ButtonComponentViewModel) - case packageGroup(PackageGroupComponentViewModel) - // Purposely leaving out a `case package` since `PackageGroupComponentViewModel` creates this model + case package(PackageComponentViewModel) case purchaseButton(PurchaseButtonComponentViewModel) } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +class PackageCollector { + var packageViewModels: [PackageComponentViewModel] = [] +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension PaywallComponent { func toViewModel( + packageCollector: PackageCollector, offering: Offering, localizedStrings: LocalizationDictionary ) throws -> PaywallComponentViewModel { @@ -47,7 +52,8 @@ extension PaywallComponent { ) case .stack(let component): return .stack( - try StackComponentViewModel(component: component, + try StackComponentViewModel(packageCollector: packageCollector, + component: component, localizedStrings: localizedStrings, offering: offering) ) @@ -59,20 +65,20 @@ extension PaywallComponent { case .button(let component): return .button( try ButtonComponentViewModel( + packageCollector: packageCollector, component: component, localizedStrings: localizedStrings, offering: offering ) ) - case .packageGroup(let component): - return .packageGroup( - try PackageGroupComponentViewModel(localizedStrings: localizedStrings, - component: component, - offering: offering) - ) - case .package: - // PackageGroupViewModel makes the PackageViewModel since it needs a Package - throw PaywallComponentViewModelError.invalidAttemptToCreatePackage + case .package(let component): + let viewModel = try PackageComponentViewModel(packageCollector: packageCollector, + localizedStrings: localizedStrings, + component: component, + offering: offering) + packageCollector.packageViewModels.append(viewModel) + + return .package(viewModel) case .purchaseButton(let component): return .purchaseButton( try PurchaseButtonComponentViewModel(localizedStrings: localizedStrings, diff --git a/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift b/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift index 570d43228e..cb19e59b45 100644 --- a/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift @@ -24,6 +24,7 @@ class StackComponentViewModel { let viewModels: [PaywallComponentViewModel] convenience init( + packageCollector: PackageCollector, component: PaywallComponent.StackComponent, localizedStrings: PaywallComponent.LocalizationDictionary, offering: Offering @@ -31,7 +32,9 @@ class StackComponentViewModel { self.init( component: component, viewModels: try component.components.map { - try $0.toViewModel(offering: offering, localizedStrings: localizedStrings) + try $0.toViewModel(packageCollector: packageCollector, + offering: offering, + localizedStrings: localizedStrings) } ) } diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsView.swift b/RevenueCatUI/Templates/Components/TemplateComponentsView.swift index cd4905ce0c..02d8e8bdd2 100644 --- a/RevenueCatUI/Templates/Components/TemplateComponentsView.swift +++ b/RevenueCatUI/Templates/Components/TemplateComponentsView.swift @@ -21,6 +21,12 @@ class PaywallState: ObservableObject { } +enum PackageGroupValidationError: Error { + + case noAvailablePackages(String) + +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct TemplateComponentsView: View { @@ -31,6 +37,8 @@ struct TemplateComponentsView: View { @StateObject private var paywallState = PaywallState() + private var packageCollector = PackageCollector() + public init(paywallComponentsData: PaywallComponentsData, offering: Offering, onDismiss: @escaping () -> Void) { self.paywallComponentsData = paywallComponentsData self.onDismiss = onDismiss @@ -42,9 +50,41 @@ struct TemplateComponentsView: View { let localization = Self.chooseLocalization(for: paywallComponentsData) do { - // STEP 2: Make the view models & validate all components have required localization and packages - self.componentViewModel = try PaywallComponent.stack(componentsConfig.stack) - .toViewModel(offering: offering, localizedStrings: localization.localizedStrings) + // STEP 2: Make the view models & validate all components have required localization + let componentViewModel = try PaywallComponent.stack(componentsConfig.stack) + .toViewModel(packageCollector: packageCollector, + offering: offering, + localizedStrings: localization.localizedStrings) + + // STETP 2.25: Collect all the PackageViewModels for validation + let packageInfos: [(Package, Bool)] = self.packageCollector.packageViewModels.compactMap { packageViewModel in + guard let package = packageViewModel.package else { + return nil + } + return (package: package, isDefaultSelected: packageViewModel.isDefaultSelected) + } + + // Validate at least 1 package exists + if packageInfos.isEmpty { + Logger.error(Strings.paywall_could_not_find_any_packages) + throw PackageGroupValidationError.noAvailablePackages("No available packages found") + } + + self.componentViewModel = componentViewModel + + let defaultSelectedPackage = packageInfos.first(where: { pkg in + return pkg.1 + }) + + // Set selected package + if let defaultSelectedPackage { + self.paywallState.select(package: defaultSelectedPackage.0) + } else { + Logger.warning(Strings.paywall_could_not_find_default_package) + if let firstPackage = packageInfos.first { + self.paywallState.select(package: firstPackage.0) + } + } } catch { // STEP 2.5: Use fallback paywall if viewmodel construction fails Logger.error(Strings.paywall_view_model_construction_failed(error)) @@ -127,8 +167,8 @@ struct ComponentsView: View { LinkButtonComponentView(viewModel: viewModel) case .button(let viewModel): ButtonComponentView(viewModel: viewModel, onDismiss: onDismiss) - case .packageGroup(let viewModel): - PackageGroupComponentView(viewModel: viewModel, onDismiss: onDismiss) + case .package(let viewModel): + PackageComponentView(viewModel: viewModel, onDismiss: onDismiss) case .purchaseButton(let viewModel): PurchaseButtonComponentView(viewModel: viewModel) } diff --git a/Sources/Paywalls/Components/Common/PaywallComponentBase.swift b/Sources/Paywalls/Components/Common/PaywallComponentBase.swift index de4aa7f710..140e5c036c 100644 --- a/Sources/Paywalls/Components/Common/PaywallComponentBase.swift +++ b/Sources/Paywalls/Components/Common/PaywallComponentBase.swift @@ -19,7 +19,6 @@ public enum PaywallComponent: PaywallComponentBase { case stack(StackComponent) case linkButton(LinkButtonComponent) case button(ButtonComponent) - case packageGroup(PackageGroupComponent) case package(PackageComponent) case purchaseButton(PurchaseButtonComponent) @@ -31,7 +30,6 @@ public enum PaywallComponent: PaywallComponentBase { case stack case linkButton = "link_button" case button - case packageGroup case package case purchaseButton @@ -76,9 +74,6 @@ extension PaywallComponent: Codable { case .button(let component): try container.encode(ComponentType.button, forKey: .type) try component.encode(to: encoder) - case .packageGroup(let component): - try container.encode(ComponentType.packageGroup, forKey: .type) - try component.encode(to: encoder) case .package(let component): try container.encode(ComponentType.package, forKey: .type) try component.encode(to: encoder) @@ -105,8 +100,6 @@ extension PaywallComponent: Codable { self = .linkButton(try LinkButtonComponent(from: decoder)) case .button: self = .button(try ButtonComponent(from: decoder)) - case .packageGroup: - self = .packageGroup(try PackageGroupComponent(from: decoder)) case .package: self = .package(try PackageComponent(from: decoder)) case .purchaseButton: diff --git a/Sources/Paywalls/Components/PaywallPackageComponent.swift b/Sources/Paywalls/Components/PaywallPackageComponent.swift index d0d9c70989..8fb8306399 100644 --- a/Sources/Paywalls/Components/PaywallPackageComponent.swift +++ b/Sources/Paywalls/Components/PaywallPackageComponent.swift @@ -23,13 +23,16 @@ public extension PaywallComponent { let type: ComponentType public let packageID: String + public let isDefaultSelected: Bool public let stack: PaywallComponent.StackComponent public init(packageID: String, + isDefaultSelected: Bool, stack: PaywallComponent.StackComponent ) { self.type = .package self.packageID = packageID + self.isDefaultSelected = isDefaultSelected self.stack = stack } } diff --git a/Sources/Paywalls/Components/PaywallPackageGroupComponent.swift b/Sources/Paywalls/Components/PaywallPackageGroupComponent.swift deleted file mode 100644 index 7f6c398266..0000000000 --- a/Sources/Paywalls/Components/PaywallPackageGroupComponent.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// 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 -// -// PaywallPackageGroupComponent.swift -// -// Created by Josh Holtz on 9/27/24. - -import Foundation - -// swiftlint:disable missing_docs - -#if PAYWALL_COMPONENTS - -public extension PaywallComponent { - - struct PackageGroupComponent: PaywallComponentBase { - - let type: ComponentType - public let defaultSelectedPackageID: String - public let stack: PaywallComponent.PackagesStackComponent - - public init(defaultSelectedPackageID: String, - stack: PaywallComponent.PackagesStackComponent - ) { - self.type = .packageGroup - self.defaultSelectedPackageID = defaultSelectedPackageID - self.stack = stack - } - } - -} - -#endif diff --git a/Sources/Paywalls/Components/PaywallStackComponent.swift b/Sources/Paywalls/Components/PaywallStackComponent.swift index 8b9a883708..f7bfc9714a 100644 --- a/Sources/Paywalls/Components/PaywallStackComponent.swift +++ b/Sources/Paywalls/Components/PaywallStackComponent.swift @@ -18,16 +18,10 @@ import Foundation public extension PaywallComponent { - // The most common type of stack that can contain any type of PaywallComponent - typealias StackComponent = GenericStackComponent - - // A specialized stack that can only hold PaywallComponent.PackageComponent - typealias PackagesStackComponent = GenericStackComponent - - struct GenericStackComponent: PaywallComponentBase { + struct StackComponent: PaywallComponentBase { let type: ComponentType - public let components: [T] + public let components: [PaywallComponent] public let width: WidthSize? public let spacing: CGFloat? public let backgroundColor: ColorInfo? @@ -37,7 +31,7 @@ public extension PaywallComponent { public let cornerRadiuses: CornerRadiuses? public let border: Border? - public init(components: [T], + public init(components: [PaywallComponent], dimension: Dimension = .vertical(.center), width: WidthSize? = nil, spacing: CGFloat? = 0, @@ -63,14 +57,4 @@ public extension PaywallComponent { } -public extension PaywallComponent { - - // Components that can be contained in a stack - protocol StackableComponent: PaywallComponentBase {} - -} - -extension PaywallComponent: PaywallComponent.StackableComponent {} -extension PaywallComponent.PackageComponent: PaywallComponent.StackableComponent {} - #endif From 9afc110e46c54436056404fc70ecc8cc050bf26e Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Fri, 25 Oct 2024 15:58:18 -0500 Subject: [PATCH 3/8] Removed PackageGroup and added Template 5 SwiftUI preview --- RevenueCat.xcodeproj/project.pbxproj | 20 +- .../Package/PackageComponentView.swift | 10 +- .../PackageGroupComponentView.swift | 133 --------- .../PackageGroupComponentViewModel.swift | 110 -------- .../Template1Preview.swift | 47 +++- .../Template5Preview.swift | 266 ++++++++++++++++++ .../Components/PaywallTextComponent.swift | 4 +- 7 files changed, 324 insertions(+), 266 deletions(-) delete mode 100644 RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentView.swift delete mode 100644 RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentViewModel.swift create mode 100644 RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index b928466685..987da95217 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -26,12 +26,11 @@ 2C7F0AD32B8EEB4600381179 /* RateLimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F0AD22B8EEB4600381179 /* RateLimiter.swift */; }; 2C7F0AD62B8EEF7B00381179 /* RateLimiterRests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F0AD42B8EEF0B00381179 /* RateLimiterRests.swift */; }; 2C8EC6AF2CCBD34100D6CCF8 /* ButtonComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8EC6AE2CCBD33E00D6CCF8 /* ButtonComponent.swift */; }; + 2C8EC6DB2CCC23B700D6CCF8 /* Template5Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8EC6DA2CCC23B700D6CCF8 /* Template5Preview.swift */; }; 2CAB87F72CAAB13200247013 /* CornerBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAB87F62CAAB13200247013 /* CornerBorder.swift */; }; 2CB8CF9327BF538F00C34DE3 /* PlatformInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB8CF9227BF538F00C34DE3 /* PlatformInfo.swift */; }; 2CC791552CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC791522CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift */; }; 2CC791562CC0452100FBE120 /* PackageComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC7914B2CC0452100FBE120 /* PackageComponentView.swift */; }; - 2CC791572CC0452100FBE120 /* PackageGroupComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC7914F2CC0452100FBE120 /* PackageGroupComponentViewModel.swift */; }; - 2CC791582CC0452100FBE120 /* PackageGroupComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC7914E2CC0452100FBE120 /* PackageGroupComponentView.swift */; }; 2CC791592CC0452100FBE120 /* PurchaseButtonComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC791512CC0452100FBE120 /* PurchaseButtonComponentView.swift */; }; 2CC7915A2CC0452100FBE120 /* PackageComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC7914C2CC0452100FBE120 /* PackageComponentViewModel.swift */; }; 2CC791622CC0493600FBE120 /* PaywallComponentPropertyTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC7915F2CC0493600FBE120 /* PaywallComponentPropertyTypes.swift */; }; @@ -1179,12 +1178,11 @@ 2C7F0AD22B8EEB4600381179 /* RateLimiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimiter.swift; sourceTree = ""; }; 2C7F0AD42B8EEF0B00381179 /* RateLimiterRests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimiterRests.swift; sourceTree = ""; }; 2C8EC6AE2CCBD33E00D6CCF8 /* ButtonComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonComponent.swift; sourceTree = ""; }; + 2C8EC6DA2CCC23B700D6CCF8 /* Template5Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Template5Preview.swift; sourceTree = ""; }; 2CAB87F62CAAB13200247013 /* CornerBorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerBorder.swift; sourceTree = ""; }; 2CB8CF9227BF538F00C34DE3 /* PlatformInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformInfo.swift; sourceTree = ""; }; 2CC7914B2CC0452100FBE120 /* PackageComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageComponentView.swift; sourceTree = ""; }; 2CC7914C2CC0452100FBE120 /* PackageComponentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageComponentViewModel.swift; sourceTree = ""; }; - 2CC7914E2CC0452100FBE120 /* PackageGroupComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageGroupComponentView.swift; sourceTree = ""; }; - 2CC7914F2CC0452100FBE120 /* PackageGroupComponentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageGroupComponentViewModel.swift; sourceTree = ""; }; 2CC791512CC0452100FBE120 /* PurchaseButtonComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseButtonComponentView.swift; sourceTree = ""; }; 2CC791522CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseButtonComponentViewModel.swift; sourceTree = ""; }; 2CC7915B2CC0493600FBE120 /* Border.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Border.swift; sourceTree = ""; }; @@ -2291,6 +2289,7 @@ isa = PBXGroup; children = ( 2C2AEB0E2CA64E0E00A50F38 /* Template1Preview.swift */, + 2C8EC6DA2CCC23B700D6CCF8 /* Template5Preview.swift */, ); path = TemplateComponentsViewPreviews; sourceTree = ""; @@ -2319,15 +2318,6 @@ path = Package; sourceTree = ""; }; - 2CC791502CC0452100FBE120 /* PackageGroup */ = { - isa = PBXGroup; - children = ( - 2CC7914E2CC0452100FBE120 /* PackageGroupComponentView.swift */, - 2CC7914F2CC0452100FBE120 /* PackageGroupComponentViewModel.swift */, - ); - path = PackageGroup; - sourceTree = ""; - }; 2CC791532CC0452100FBE120 /* PurchaseButton */ = { isa = PBXGroup; children = ( @@ -2341,7 +2331,6 @@ isa = PBXGroup; children = ( 2CC7914D2CC0452100FBE120 /* Package */, - 2CC791502CC0452100FBE120 /* PackageGroup */, 2CC791532CC0452100FBE120 /* PurchaseButton */, ); path = Packages; @@ -6177,6 +6166,7 @@ 887A60832C1D037000E1A461 /* VersionDetector.swift in Sources */, 88B1BAFC2C813A3C001B7EE5 /* ImageComponentView.swift in Sources */, 887A60872C1D037000E1A461 /* ViewExtensions.swift in Sources */, + 2C8EC6DB2CCC23B700D6CCF8 /* Template5Preview.swift in Sources */, 88B1BB022C813A3C001B7EE5 /* StackComponentView.swift in Sources */, 353756712C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift in Sources */, 35C200AF2C39252D00B9778B /* FeedbackSurveyData.swift in Sources */, @@ -6193,8 +6183,6 @@ 887A60BF2C1D037000E1A461 /* PaywallViewController.swift in Sources */, 2CC791552CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift in Sources */, 2CC791562CC0452100FBE120 /* PackageComponentView.swift in Sources */, - 2CC791572CC0452100FBE120 /* PackageGroupComponentViewModel.swift in Sources */, - 2CC791582CC0452100FBE120 /* PackageGroupComponentView.swift in Sources */, 2CC791592CC0452100FBE120 /* PurchaseButtonComponentView.swift in Sources */, 2CC7915A2CC0452100FBE120 /* PackageComponentViewModel.swift in Sources */, 887A60772C1D037000E1A461 /* TemplateViewConfiguration+Images.swift in Sources */, diff --git a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift index 2e0dd91e51..fbc75974a9 100644 --- a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift +++ b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift @@ -27,9 +27,13 @@ struct PackageComponentView: View { let onDismiss: () -> Void var body: some View { - // WIP: Do something with package id and selection - StackComponentView(viewModel: self.viewModel.stackViewModel, - onDismiss: self.onDismiss) + if let _ = self.viewModel.package { + // WIP: Do something with package id and selection + StackComponentView(viewModel: self.viewModel.stackViewModel, + onDismiss: self.onDismiss) + } else { + EmptyView() + } } } diff --git a/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentView.swift b/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentView.swift deleted file mode 100644 index d6959732fe..0000000000 --- a/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentView.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// 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 -// -// PackageGroupComponentView.swift -// -// Created by Josh Holtz on 9/27/24. - -import Foundation -import RevenueCat -import SwiftUI - -#if PAYWALL_COMPONENTS - -//@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -//struct PackageGroupComponentView: View { -// -// @EnvironmentObject -// private var paywallState: PaywallState -// -// let viewModel: PackageGroupComponentViewModel -// let onDismiss: () -> Void -// -// var body: some View { -// // WIP: Do something with default package id and selection -// StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: self.onDismiss) -// .onAppear { -// self.paywallState.select(package: self.viewModel.defaultPackage) -// } -// } -// -//} -// -//#if DEBUG -// -//@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -//struct PackagesComponentView_Previews: PreviewProvider { -// -// static let paywallState = PaywallState() -// -// static let packages: [PaywallComponent.PackageComponent] = [ -// makePackage(packageID: "weekly", -// nameTextLid: "weekly_name", -// detailTextLid: "weekly_detail"), -// makePackage(packageID: "non_existant_package", -// nameTextLid: "non_existant_name", -// detailTextLid: "non_existant_detail"), -// makePackage(packageID: "monthly", -// nameTextLid: "monthly_name", -// detailTextLid: "monthly_detail") -// ] -// -// static func makePackage(packageID: String, -// nameTextLid: String, -// detailTextLid: String) -> PaywallComponent.PackageComponent { -// let stack: PaywallComponent.StackComponent = .init( -// components: [ -// .text(.init( -// text: nameTextLid, -// fontWeight: .bold, -// color: .init(light: "#000000"), -// padding: .zero, -// margin: .zero -// )), -// .text(.init( -// text: detailTextLid, -// color: .init(light: "#000000"), -// padding: .zero, -// margin: .zero -// )) -// ], -// dimension: .vertical(.leading), -// spacing: 0, -// backgroundColor: nil, -// padding: PaywallComponent.Padding(top: 10, -// bottom: 10, -// leading: 20, -// trailing: 20) -// ) -// -// return PaywallComponent.PackageComponent( -// packageID: packageID, -// isDefaultSelected: false, -// stack: stack -// ) -// } -// -// static var previews: some View { -// // Packages -// PackageGroupComponentView( -// // swiftlint:disable:next force_try -// viewModel: try! PackageGroupComponentViewModel( -// localizedStrings: [ -// "weekly_name": .string("Weekly"), -// "weekly_detail": .string("Get for $39.99/week"), -// "monthly_name": .string("Monthly"), -// "monthly_detail": .string("Get for $139.99/month"), -// "non_existant_name": .string("THIS SHOULDN'T SHOW"), -// "non_existant_detail": .string("THIS SHOULDN'T SHOW") -// -// ], -// component: PaywallComponent.PackageGroupComponent( -// defaultSelectedPackageID: "weekly", -// stack: .init(components: packages) -// ), -// offering: Offering(identifier: "default", -// serverDescription: "", -// availablePackages: [ -// Package(identifier: "weekly", -// packageType: .weekly, -// storeProduct: .init(sk1Product: .init()), -// offeringIdentifier: "default"), -// Package(identifier: "monthly", -// packageType: .monthly, -// storeProduct: .init(sk1Product: .init()), -// offeringIdentifier: "default") -// ]) -// ), onDismiss: {} -// ) -// .environmentObject(paywallState) -// .previewLayout(.sizeThatFits) -// .previewDisplayName("Packages") -// } -//} -// -//#endif - -#endif diff --git a/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentViewModel.swift b/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentViewModel.swift deleted file mode 100644 index d1153c1c21..0000000000 --- a/RevenueCatUI/Templates/Components/Packages/PackageGroup/PackageGroupComponentViewModel.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// 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 -// -// PackageGroupComponentViewModel.swift -// -// Created by Josh Holtz on 9/27/24. - -import Foundation -import RevenueCat - -#if PAYWALL_COMPONENTS - -//@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -//class PackageGroupComponentViewModel { -// -// private let localizedStrings: PaywallComponent.LocalizationDictionary -// private let component: PaywallComponent.PackageGroupComponent -// private let offering: Offering -// -// let defaultPackage: Package -// let stackViewModel: StackComponentViewModel -// -// init(localizedStrings: PaywallComponent.LocalizationDictionary, -// component: PaywallComponent.PackageGroupComponent, -// offering: Offering) throws { -// self.localizedStrings = localizedStrings -// self.component = component -// self.offering = offering -// -// let info = try Self.getPackages(component: component, offering: offering) -// self.defaultPackage = info.defaultPackage -// -// let componentViewModels = try info.availablePackageComponents.map { info in -// try PackageComponentViewModel( -// localizedStrings: localizedStrings, -// component: info.component, -// offering: offering, -// package: info.package -// ).stackViewModel -// }.map(PaywallComponentViewModel.stack) -// -// self.stackViewModel = StackComponentViewModel( -// component: .init( -// components: [], // Empty on purpose because we are feeding this view models already -// dimension: component.stack.dimension, -// width: component.stack.width, -// spacing: component.stack.spacing, -// backgroundColor: component.stack.backgroundColor, -// padding: component.stack.padding, -// margin: component.stack.margin, -// cornerRadiuses: component.stack.cornerRadiuses, -// border: component.stack.border -// ), -// viewModels: componentViewModels -// ) -// } -// -// static func getPackages( -// component: PaywallComponent.PackageGroupComponent, -// offering: Offering -// ) throws -> (defaultPackage: Package, -// availablePackageComponents: [(component: PaywallComponent.PackageComponent, package: Package)]) { -// -// // Stack of packages -// let packages = component.stack.components -// -// // Get list of available package components and their packages -// let availablePackageInfos = packages.compactMap { packageComponent in -// let pkg = offering.availablePackages.first(where: { $0.packageIdentifier == packageComponent.packageID }) -// if let pkg { -// return (component: packageComponent, package: pkg) -// } else { -// return nil -// } -// } -// -// // We need packages -// guard let firstPackage = availablePackageInfos.first else { -// Logger.error(Strings.paywall_could_not_find_any_packages) -// throw PackageGroupValidationError.noAvailablePackages("No available packages found") -// } -// -// // Attempt to get default package -// let defaultPackage = availablePackageInfos.first { packageInfo in -// return packageInfo.package.id == component.defaultSelectedPackageID -// } -// -// if let defaultPackage { -// return (defaultPackage.package, availablePackageInfos) -// } else { -// Logger.warning(Strings.paywall_could_not_find_default_package(component.defaultSelectedPackageID)) -// return (firstPackage.package, availablePackageInfos) -// } -// } -// -// enum PackageGroupValidationError: Error { -// -// case noAvailablePackages(String) -// -// } -// -//} - -#endif diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift index f76f1b8a66..f009df2f44 100644 --- a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift +++ b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift @@ -59,6 +59,39 @@ private enum Template1Preview { horizontalAlignment: .center ) + static var packageStack: PaywallComponent.StackComponent { + return .init( + components: [ + .text(.init( + text: "package_name", + fontWeight: .bold, + color: .init(light: "#000000"), + padding: .zero, + margin: .zero + )), + .text(.init( + text: "package_detail", + color: .init(light: "#000000"), + padding: .zero, + margin: .zero + )) + ], + dimension: .vertical(.center), + spacing: 0, + backgroundColor: nil, + padding: .init(top: 0, + bottom: 0, + leading: 0, + trailing: 0) + ) + } + + static let package = PaywallComponent.PackageComponent( + packageID: "weekly", + isDefaultSelected: false, + stack: packageStack + ) + static let purchaseButton = PaywallComponent.PurchaseButtonComponent( cta: "cta", ctaIntroOffer: "cta_intro", @@ -76,6 +109,7 @@ private enum Template1Preview { components: [ .text(title), .text(body), + .package(package), .purchaseButton(purchaseButton) ], width: .init(type: .fill, value: nil), @@ -110,6 +144,8 @@ private enum Template1Preview { componentsLocalizations: ["en_US": [ "title": .string("Ignite your cat's curiosity"), "body": .string("Get access to all of our educational content trusted by thousands of pet parents."), + "package_name": .string("Monthly"), + "package_detail": .string("Some price into"), "cta": .string("Get Started"), "cta_intro": .string("Claim Free Trial") ]], @@ -121,15 +157,22 @@ private enum Template1Preview { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct Template1Preview_Previews: PreviewProvider { + static var package: Package { + return .init(identifier: "weekly", + packageType: .weekly, + storeProduct: .init(sk1Product: .init()), + offeringIdentifier: "default") + } + // Need to wrap in VStack otherwise preview rerenders and images won't show static var previews: some View { // Template 1 TemplateComponentsView( paywallComponentsData: Template1Preview.data, - offering: .init(identifier: "", + offering: .init(identifier: "default", serverDescription: "", - availablePackages: []), + availablePackages: [package]), onDismiss: { } ) .previewLayout(.fixed(width: 400, height: 800)) diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift new file mode 100644 index 0000000000..bad133f2fc --- /dev/null +++ b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift @@ -0,0 +1,266 @@ +// +// 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 +// +// TestPaywallPreviews.swift +// +// Created by Josh Holtz on 9/26/24. + +import Foundation +import RevenueCat +import SwiftUI + +#if PAYWALL_COMPONENTS + +#if DEBUG + +private enum Template5Preview { + + static let paywallState = PaywallState() + + static let catUrl = URL(string: "https://assets.pawwalls.com/954459_1701163461.jpg")! + + static let catImage = PaywallComponent.ImageComponent( + source: .init( + light: .init( + original: catUrl, + heic: catUrl, + heicLowRes: catUrl + ) + ), + fitMode: .fill, + maxHeight: 200, + gradientColors: ["#ffffff00", "#ffffff00", "#ffffffff"] + ) + + static let title = PaywallComponent.TextComponent( + text: "title", + fontFamily: nil, + fontWeight: .heavy, + color: .init(light: "#000000"), + backgroundColor: nil, + padding: .zero, + margin: .zero, + textStyle: .largeTitle, + horizontalAlignment: .leading + ) + + static let body = PaywallComponent.TextComponent( + text: "body", + fontFamily: nil, + fontWeight: .regular, + color: .init(light: "#000000"), + backgroundColor: nil, + padding: .zero, + margin: .zero, + textStyle: .body, + horizontalAlignment: .leading + ) + + static let packages: [PaywallComponent.PackageComponent] = [ + makePackage(packageID: "weekly", + nameTextLid: "weekly_name", + detailTextLid: "weekly_detail"), + makePackage(packageID: "non_existant_package", + nameTextLid: "non_existant_name", + detailTextLid: "non_existant_detail"), + makePackage(packageID: "monthly", + nameTextLid: "monthly_name", + detailTextLid: "monthly_detail", + isDefaultSelected: true) + ] + + static func makePackage( + packageID: String, + nameTextLid: String, + detailTextLid: String, + isDefaultSelected: Bool = false + ) -> PaywallComponent.PackageComponent { + let stack: PaywallComponent.StackComponent = .init( + components: [ + .text(.init( + text: nameTextLid, + fontWeight: .bold, + color: .init(light: "#000000"), + padding: .zero, + margin: .zero + )), + .text(.init( + text: detailTextLid, + color: .init(light: "#000000"), + padding: .zero, + margin: .zero + )) + ], + dimension: .vertical(.leading), + spacing: 0, + backgroundColor: nil, + padding: PaywallComponent.Padding(top: 10, + bottom: 10, + leading: 20, + trailing: 20), + cornerRadiuses: .init(topLeading: 16, + topTrailing: 16, + bottomLeading: 16, + bottomTrailing: 20), + border: .init(color: .init(light: "#cccccc"), width: 1) + ) + + return PaywallComponent.PackageComponent( + packageID: packageID, + isDefaultSelected: isDefaultSelected, + stack: stack + ) + } + + static let packagesStack = PaywallComponent.StackComponent( + components: [ + .package(makePackage(packageID: "weekly", + nameTextLid: "weekly_name", + detailTextLid: "weekly_detail")), + .package(makePackage(packageID: "non_existant_package", + nameTextLid: "non_existant_name", + detailTextLid: "non_existant_detail")), + .package(makePackage(packageID: "monthly", + nameTextLid: "monthly_name", + detailTextLid: "monthly_detail", + isDefaultSelected: true)), + .text(.init( + text: "package_terms", + color: .init(light: "#999999"), + textStyle: .caption + )) + ], + dimension: .vertical(.center), + spacing: 10, + backgroundColor: nil, + margin: .init(top: 20, + bottom: 0, + leading: 0, + trailing: 0) + ) + + static let purchaseButton = PaywallComponent.PurchaseButtonComponent( + cta: "cta", + ctaIntroOffer: "cta_intro", + fontWeight: .bold, + color: .init(light: "#ffffff"), + backgroundColor: .init(light: "#e89d89"), + padding: .init(top: 15, + bottom: 15, + leading: 50, + trailing: 50), + shape: .pill + ) + + static let purchaseButtonStack = PaywallComponent.StackComponent( + components: [ + .purchaseButton(purchaseButton) + ], + dimension: .horizontal(.center), + width: .init(type: .fill, value: nil), + spacing: 0, + backgroundColor: nil + ) + + static let contentStack = PaywallComponent.StackComponent( + components: [ + .text(title), + .text(body), + .stack(packagesStack), + .stack(purchaseButtonStack), + ], + dimension: .vertical(.leading), + width: .init(type: .fill, value: nil), + spacing: 30, + backgroundColor: nil, + margin: .init(top: 0, + bottom: 0, + leading: 20, + trailing: 20) + ) + + static let stack = PaywallComponent.StackComponent( + components: [ + .image(catImage), + .stack(contentStack) + ], + width: .init(type: .fill, value: nil), + spacing: 20, + backgroundColor: nil + ) + + static let data: PaywallComponentsData = .init( + templateName: "components", + assetBaseURL: URL(string: "https://assets.pawwalls.com")!, + componentsConfigs: .init( + base: .init(stack: .init( + components: [ + .stack(stack) + ] + )) + ), + componentsLocalizations: ["en_US": [ + "title": .string("Ignite your cat's curiosity"), + "body": .string("Get access to all of our educational content trusted by thousands of pet parents."), + "cta": .string("Get Started"), + "cta_intro": .string("Claim Free Trial"), + + // Packages + "weekly_name": .string("Weekly"), + "weekly_detail": .string("Get for $39.99/week"), + "monthly_name": .string("Monthly"), + "monthly_detail": .string("Get for $139.99/month"), + "non_existant_name": .string("THIS SHOULDN'T SHOW"), + "non_existant_detail": .string("THIS SHOULDN'T SHOW"), + + "package_terms": .string("Recurring billing. Cancel anytime.") + ]], + revision: 1, + defaultLocaleIdentifier: "en_US" + ) +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct Template5Preview_Previews: PreviewProvider { + + static var package: Package { + return .init(identifier: "weekly", + packageType: .weekly, + storeProduct: .init(sk1Product: .init()), + offeringIdentifier: "default") + } + + // Need to wrap in VStack otherwise preview rerenders and images won't show + static var previews: some View { + + // Template 5 + TemplateComponentsView( + paywallComponentsData: Template5Preview.data, + offering: Offering(identifier: "default", + serverDescription: "", + availablePackages: [ + Package(identifier: "weekly", + packageType: .weekly, + storeProduct: .init(sk1Product: .init()), + offeringIdentifier: "default"), + Package(identifier: "monthly", + packageType: .monthly, + storeProduct: .init(sk1Product: .init()), + offeringIdentifier: "default") + ]), + onDismiss: { } + ) + .previewLayout(.fixed(width: 400, height: 800)) + .previewDisplayName("Template 5") + } +} + +#endif + +#endif diff --git a/Sources/Paywalls/Components/PaywallTextComponent.swift b/Sources/Paywalls/Components/PaywallTextComponent.swift index 6c283c1668..723b46c9cd 100644 --- a/Sources/Paywalls/Components/PaywallTextComponent.swift +++ b/Sources/Paywalls/Components/PaywallTextComponent.swift @@ -31,8 +31,8 @@ public extension PaywallComponent { fontWeight: FontWeight = .regular, color: ColorInfo, backgroundColor: ColorInfo? = nil, - padding: Padding = .default, - margin: Padding = .default, + padding: Padding = .zero, + margin: Padding = .zero, textStyle: TextStyle = .body, horizontalAlignment: HorizontalAlignment = .center ) { From aa4899a8fb15daf6d65d0b333b8975b11e58d5c9 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Fri, 25 Oct 2024 20:59:39 -0500 Subject: [PATCH 4/8] Remove PackageGroup --- RevenueCat.xcodeproj/project.pbxproj | 4 ++ .../Button/ButtonComponentView.swift | 2 +- .../Button/ButtonComponentViewModel.swift | 4 +- .../Components/PackageValidator.swift | 55 +++++++++++++++++++ .../Package/PackageComponentView.swift | 4 +- .../Package/PackageComponentViewModel.swift | 6 +- .../PaywallComponentViewModel.swift | 15 ++--- .../Stack/StackComponentViewModel.swift | 4 +- .../Components/TemplateComponentsView.swift | 29 ++-------- .../Template5Preview.swift | 22 ++++---- .../Config/SamplePaywalls.swift | 32 +++++------ 11 files changed, 106 insertions(+), 71 deletions(-) create mode 100644 RevenueCatUI/Templates/Components/PackageValidator.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 987da95217..479a36d770 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 2C7F0AD62B8EEF7B00381179 /* RateLimiterRests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F0AD42B8EEF0B00381179 /* RateLimiterRests.swift */; }; 2C8EC6AF2CCBD34100D6CCF8 /* ButtonComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8EC6AE2CCBD33E00D6CCF8 /* ButtonComponent.swift */; }; 2C8EC6DB2CCC23B700D6CCF8 /* Template5Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8EC6DA2CCC23B700D6CCF8 /* Template5Preview.swift */; }; + 2C8EC6DD2CCC7C5B00D6CCF8 /* PackageValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8EC6DC2CCC7C5500D6CCF8 /* PackageValidator.swift */; }; 2CAB87F72CAAB13200247013 /* CornerBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAB87F62CAAB13200247013 /* CornerBorder.swift */; }; 2CB8CF9327BF538F00C34DE3 /* PlatformInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB8CF9227BF538F00C34DE3 /* PlatformInfo.swift */; }; 2CC791552CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC791522CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift */; }; @@ -1179,6 +1180,7 @@ 2C7F0AD42B8EEF0B00381179 /* RateLimiterRests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimiterRests.swift; sourceTree = ""; }; 2C8EC6AE2CCBD33E00D6CCF8 /* ButtonComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonComponent.swift; sourceTree = ""; }; 2C8EC6DA2CCC23B700D6CCF8 /* Template5Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Template5Preview.swift; sourceTree = ""; }; + 2C8EC6DC2CCC7C5500D6CCF8 /* PackageValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageValidator.swift; sourceTree = ""; }; 2CAB87F62CAAB13200247013 /* CornerBorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerBorder.swift; sourceTree = ""; }; 2CB8CF9227BF538F00C34DE3 /* PlatformInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformInfo.swift; sourceTree = ""; }; 2CC7914B2CC0452100FBE120 /* PackageComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageComponentView.swift; sourceTree = ""; }; @@ -4388,6 +4390,7 @@ 88AD01352C74196600AA1F2B /* Components */ = { isa = PBXGroup; children = ( + 2C8EC6DC2CCC7C5500D6CCF8 /* PackageValidator.swift */, 88B1BAE72C813A3C001B7EE5 /* PaywallComponentTypeTransformers.swift */, 88B1BAD92C813A3C001B7EE5 /* PaywallComponentViewModel.swift */, 88B1BAE32C813A3C001B7EE5 /* TemplateComponentsView.swift */, @@ -6102,6 +6105,7 @@ 887A606A2C1D037000E1A461 /* TrialOrIntroEligibilityChecker.swift in Sources */, 88B1BAEE2C813A3C001B7EE5 /* TextComponentView.swift in Sources */, 3546355F2C391F4D001D7E85 /* PromotionalOfferView.swift in Sources */, + 2C8EC6DD2CCC7C5B00D6CCF8 /* PackageValidator.swift in Sources */, 353756722C382C2800A1B8D6 /* URLUtilities.swift in Sources */, 353FDC0F2CA446FA0055F328 /* StoreProductDiscount+Extensions.swift in Sources */, 887A60862C1D037000E1A461 /* FooterHidingModifier.swift in Sources */, diff --git a/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift b/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift index 100ceb0544..c07c8ff9b5 100644 --- a/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift +++ b/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift @@ -111,7 +111,7 @@ struct ButtonComponentView_Previews: PreviewProvider { ButtonComponentView( // swiftlint:disable:next force_try viewModel: try! .init( - packageCollector: PackageCollector(), + packageValidator: PackageValidator(), component: .init( action: .navigateBack, stack: .init( diff --git a/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift b/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift index 6082ec6e6d..89447d2fa8 100644 --- a/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Button/ButtonComponentViewModel.swift @@ -45,7 +45,7 @@ public class ButtonComponentViewModel { let stackViewModel: StackComponentViewModel init( - packageCollector: PackageCollector, + packageValidator: PackageValidator, component: PaywallComponent.ButtonComponent, localizedStrings: PaywallComponent.LocalizationDictionary, offering: Offering @@ -53,7 +53,7 @@ public class ButtonComponentViewModel { self.component = component self.localizedStrings = localizedStrings self.stackViewModel = try StackComponentViewModel( - packageCollector: packageCollector, + packageValidator: packageValidator, component: component.stack, localizedStrings: localizedStrings, offering: offering diff --git a/RevenueCatUI/Templates/Components/PackageValidator.swift b/RevenueCatUI/Templates/Components/PackageValidator.swift new file mode 100644 index 0000000000..0f9c657072 --- /dev/null +++ b/RevenueCatUI/Templates/Components/PackageValidator.swift @@ -0,0 +1,55 @@ +// +// 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 +// +// PackageValidator.swift +// +// Created by Josh Holtz on 10/25/24. + +import Foundation +import RevenueCat + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +class PackageValidator { + + private var packageViewModels: [PackageComponentViewModel] = [] + + typealias PackageInfo = (package: Package, isDefaultSelected: Bool) + + var packageInfos: [PackageInfo] { + self.packageViewModels.compactMap { packageViewModel in + guard let package = packageViewModel.package else { + return nil + } + return (package: package, isDefaultSelected: packageViewModel.isDefaultSelected) + } + } + + func add(_ packageViewModel: PackageComponentViewModel) { + self.packageViewModels.append(packageViewModel) + } + + var isValid: Bool { + !packageInfos.isEmpty + } + + var defaultSelectedPackage: Package? { + let defaultSelectedPackage = packageInfos.first(where: { pkg in + return pkg.isDefaultSelected + }) + + // Set selected package + if let defaultSelectedPackage { + return defaultSelectedPackage.package + } + + Logger.warning(Strings.paywall_could_not_find_default_package) + return packageInfos.first?.package + } + +} diff --git a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift index fbc75974a9..6253e62fc4 100644 --- a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift +++ b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift @@ -27,7 +27,7 @@ struct PackageComponentView: View { let onDismiss: () -> Void var body: some View { - if let _ = self.viewModel.package { + if self.viewModel.package != nil { // WIP: Do something with package id and selection StackComponentView(viewModel: self.viewModel.stackViewModel, onDismiss: self.onDismiss) @@ -82,7 +82,7 @@ struct PackageComponentView_Previews: PreviewProvider { PackageComponentView( // swiftlint:disable:next force_try viewModel: try! .init( - packageCollector: PackageCollector(), + packageValidator: PackageValidator(), localizedStrings: [ "name": .string("Weekly"), "detail": .string("Get for $39.99/wk") diff --git a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentViewModel.swift b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentViewModel.swift index b949ab28f1..45b4c0be2a 100644 --- a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentViewModel.swift @@ -27,7 +27,7 @@ class PackageComponentViewModel { let package: Package? let stackViewModel: StackComponentViewModel - init(packageCollector: PackageCollector, + init(packageValidator: PackageValidator, localizedStrings: PaywallComponent.LocalizationDictionary, component: PaywallComponent.PackageComponent, offering: Offering) throws { @@ -39,10 +39,10 @@ class PackageComponentViewModel { self.package = offering.package(identifier: component.packageID) if package == nil { Logger.warning(Strings.paywall_could_not_find_package(component.packageID)) - } + } self.stackViewModel = try StackComponentViewModel( - packageCollector: packageCollector, + packageValidator: packageValidator, component: component.stack, localizedStrings: localizedStrings, offering: offering diff --git a/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift b/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift index 6ae01948ca..92a125f675 100644 --- a/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift @@ -24,16 +24,11 @@ enum PaywallComponentViewModel { } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -class PackageCollector { - var packageViewModels: [PackageComponentViewModel] = [] -} - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension PaywallComponent { func toViewModel( - packageCollector: PackageCollector, + packageValidator: PackageValidator, offering: Offering, localizedStrings: LocalizationDictionary ) throws -> PaywallComponentViewModel { @@ -52,7 +47,7 @@ extension PaywallComponent { ) case .stack(let component): return .stack( - try StackComponentViewModel(packageCollector: packageCollector, + try StackComponentViewModel(packageValidator: packageValidator, component: component, localizedStrings: localizedStrings, offering: offering) @@ -65,18 +60,18 @@ extension PaywallComponent { case .button(let component): return .button( try ButtonComponentViewModel( - packageCollector: packageCollector, + packageValidator: packageValidator, component: component, localizedStrings: localizedStrings, offering: offering ) ) case .package(let component): - let viewModel = try PackageComponentViewModel(packageCollector: packageCollector, + let viewModel = try PackageComponentViewModel(packageValidator: packageValidator, localizedStrings: localizedStrings, component: component, offering: offering) - packageCollector.packageViewModels.append(viewModel) + packageValidator.add(viewModel) return .package(viewModel) case .purchaseButton(let component): diff --git a/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift b/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift index cb19e59b45..3ad79682b2 100644 --- a/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift @@ -24,7 +24,7 @@ class StackComponentViewModel { let viewModels: [PaywallComponentViewModel] convenience init( - packageCollector: PackageCollector, + packageValidator: PackageValidator, component: PaywallComponent.StackComponent, localizedStrings: PaywallComponent.LocalizationDictionary, offering: Offering @@ -32,7 +32,7 @@ class StackComponentViewModel { self.init( component: component, viewModels: try component.components.map { - try $0.toViewModel(packageCollector: packageCollector, + try $0.toViewModel(packageValidator: packageValidator, offering: offering, localizedStrings: localizedStrings) } diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsView.swift b/RevenueCatUI/Templates/Components/TemplateComponentsView.swift index 02d8e8bdd2..544903b6fa 100644 --- a/RevenueCatUI/Templates/Components/TemplateComponentsView.swift +++ b/RevenueCatUI/Templates/Components/TemplateComponentsView.swift @@ -37,7 +37,7 @@ struct TemplateComponentsView: View { @StateObject private var paywallState = PaywallState() - private var packageCollector = PackageCollector() + private var packageValidator = PackageValidator() public init(paywallComponentsData: PaywallComponentsData, offering: Offering, onDismiss: @escaping () -> Void) { self.paywallComponentsData = paywallComponentsData @@ -52,38 +52,19 @@ struct TemplateComponentsView: View { do { // STEP 2: Make the view models & validate all components have required localization let componentViewModel = try PaywallComponent.stack(componentsConfig.stack) - .toViewModel(packageCollector: packageCollector, + .toViewModel(packageValidator: packageValidator, offering: offering, localizedStrings: localization.localizedStrings) - // STETP 2.25: Collect all the PackageViewModels for validation - let packageInfos: [(Package, Bool)] = self.packageCollector.packageViewModels.compactMap { packageViewModel in - guard let package = packageViewModel.package else { - return nil - } - return (package: package, isDefaultSelected: packageViewModel.isDefaultSelected) - } - - // Validate at least 1 package exists - if packageInfos.isEmpty { + guard packageValidator.isValid else { Logger.error(Strings.paywall_could_not_find_any_packages) throw PackageGroupValidationError.noAvailablePackages("No available packages found") } self.componentViewModel = componentViewModel - let defaultSelectedPackage = packageInfos.first(where: { pkg in - return pkg.1 - }) - - // Set selected package - if let defaultSelectedPackage { - self.paywallState.select(package: defaultSelectedPackage.0) - } else { - Logger.warning(Strings.paywall_could_not_find_default_package) - if let firstPackage = packageInfos.first { - self.paywallState.select(package: firstPackage.0) - } + if let defaultPackage = packageValidator.defaultSelectedPackage { + self.paywallState.select(package: defaultPackage) } } catch { // STEP 2.5: Use fallback paywall if viewmodel construction fails diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift index bad133f2fc..fde37e6cf5 100644 --- a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift +++ b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift @@ -121,15 +121,15 @@ private enum Template5Preview { static let packagesStack = PaywallComponent.StackComponent( components: [ .package(makePackage(packageID: "weekly", - nameTextLid: "weekly_name", - detailTextLid: "weekly_detail")), + nameTextLid: "weekly_name", + detailTextLid: "weekly_detail")), .package(makePackage(packageID: "non_existant_package", - nameTextLid: "non_existant_name", - detailTextLid: "non_existant_detail")), + nameTextLid: "non_existant_name", + detailTextLid: "non_existant_detail")), .package(makePackage(packageID: "monthly", - nameTextLid: "monthly_name", - detailTextLid: "monthly_detail", - isDefaultSelected: true)), + nameTextLid: "monthly_name", + detailTextLid: "monthly_detail", + isDefaultSelected: true)), .text(.init( text: "package_terms", color: .init(light: "#999999"), @@ -140,9 +140,9 @@ private enum Template5Preview { spacing: 10, backgroundColor: nil, margin: .init(top: 20, - bottom: 0, - leading: 0, - trailing: 0) + bottom: 0, + leading: 0, + trailing: 0) ) static let purchaseButton = PaywallComponent.PurchaseButtonComponent( @@ -173,7 +173,7 @@ private enum Template5Preview { .text(title), .text(body), .stack(packagesStack), - .stack(purchaseButtonStack), + .stack(purchaseButtonStack) ], dimension: .vertical(.leading), width: .init(type: .fill, value: nil), diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/Config/SamplePaywalls.swift b/Tests/TestingApps/PaywallsTester/PaywallsTester/Config/SamplePaywalls.swift index cebfd9e65d..12d4edc246 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester/Config/SamplePaywalls.swift +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/Config/SamplePaywalls.swift @@ -710,7 +710,8 @@ private extension SamplePaywallLoader { static func makePackage(packageID: String, nameTextLid: String, - detailTextLid: String) -> PaywallComponent.PackageComponent { + detailTextLid: String, + isDefaultSelected: Bool = false) -> PaywallComponent.PackageComponent { let stack: PaywallComponent.StackComponent = .init( components: [ .text(.init( @@ -743,6 +744,7 @@ private extension SamplePaywallLoader { return .init( packageID: packageID, + isDefaultSelected: isDefaultSelected, stack: stack ) } @@ -750,20 +752,18 @@ private extension SamplePaywallLoader { static var simpleSixPackages: PaywallComponent = { return .stack(.init( components: [ - .packageGroup(.init( - defaultSelectedPackageID: Package.string(from: PackageType.monthly)!, - stack: .init( - components: [ - makePackage(packageID: Package.string(from: PackageType.monthly)!, - nameTextLid: "monthly_package_name", - detailTextLid: "monthly_package_details"), - makePackage(packageID: Package.string(from: PackageType.annual)!, - nameTextLid: "annual_package_name", - detailTextLid: "annual_package_details") - ], - spacing: 20, - margin: .init(top: 20, bottom: 20, leading: 20, trailing: 20) - ) + .stack(.init( + components: [ + .package(makePackage(packageID: Package.string(from: PackageType.monthly)!, + nameTextLid: "monthly_package_name", + detailTextLid: "monthly_package_details")), + .package(makePackage(packageID: Package.string(from: PackageType.annual)!, + nameTextLid: "annual_package_name", + detailTextLid: "annual_package_details", + isDefaultSelected: true)) + ], + spacing: 20, + margin: .init(top: 20, bottom: 20, leading: 20, trailing: 20) )), .purchaseButton(.init( cta: "cta", @@ -783,7 +783,7 @@ private extension SamplePaywallLoader { )) ], width: .init(type: .fill, value: nil), - backgroundColor: .init(light: "#cccccc"), + backgroundColor: nil, margin: .init(top: 0, bottom: 0, leading: 20, trailing: 20) )) }() From 75564e7017cc66dd2a19c901dc4ad1151ead490e Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Sat, 26 Oct 2024 08:18:22 -0500 Subject: [PATCH 5/8] Fixed some thread, lint, and selection stuff --- .../Components/PackageValidator.swift | 4 ++++ .../Package/PackageComponentView.swift | 13 +++++++++---- .../Components/TemplateComponentsView.swift | 18 +++++++++++------- .../Template5Preview.swift | 2 +- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/RevenueCatUI/Templates/Components/PackageValidator.swift b/RevenueCatUI/Templates/Components/PackageValidator.swift index 0f9c657072..0a2b4c6b89 100644 --- a/RevenueCatUI/Templates/Components/PackageValidator.swift +++ b/RevenueCatUI/Templates/Components/PackageValidator.swift @@ -14,6 +14,8 @@ import Foundation import RevenueCat +#if PAYWALL_COMPONENTS + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) class PackageValidator { @@ -53,3 +55,5 @@ class PackageValidator { } } + +#endif diff --git a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift index 6253e62fc4..b1e4cf2932 100644 --- a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift +++ b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift @@ -27,10 +27,15 @@ struct PackageComponentView: View { let onDismiss: () -> Void var body: some View { - if self.viewModel.package != nil { - // WIP: Do something with package id and selection - StackComponentView(viewModel: self.viewModel.stackViewModel, - onDismiss: self.onDismiss) + if let package = self.viewModel.package { + Button { + self.paywallState.select(package: package) + } label: { + StackComponentView( + viewModel: self.viewModel.stackViewModel, + onDismiss: self.onDismiss + ) + } } else { EmptyView() } diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsView.swift b/RevenueCatUI/Templates/Components/TemplateComponentsView.swift index 544903b6fa..94e872c4f6 100644 --- a/RevenueCatUI/Templates/Components/TemplateComponentsView.swift +++ b/RevenueCatUI/Templates/Components/TemplateComponentsView.swift @@ -15,6 +15,10 @@ class PaywallState: ObservableObject { @Published var selectedPackage: Package? + init(selectedPackage: Package?) { + self.selectedPackage = selectedPackage + } + func select(package: Package) { self.selectedPackage = package } @@ -35,9 +39,7 @@ struct TemplateComponentsView: View { private let onDismiss: () -> Void @StateObject - private var paywallState = PaywallState() - - private var packageValidator = PackageValidator() + private var paywallState: PaywallState public init(paywallComponentsData: PaywallComponentsData, offering: Offering, onDismiss: @escaping () -> Void) { self.paywallComponentsData = paywallComponentsData @@ -51,6 +53,7 @@ struct TemplateComponentsView: View { do { // STEP 2: Make the view models & validate all components have required localization + let packageValidator = PackageValidator() let componentViewModel = try PaywallComponent.stack(componentsConfig.stack) .toViewModel(packageValidator: packageValidator, offering: offering, @@ -62,15 +65,16 @@ struct TemplateComponentsView: View { } self.componentViewModel = componentViewModel - - if let defaultPackage = packageValidator.defaultSelectedPackage { - self.paywallState.select(package: defaultPackage) - } + self._paywallState = .init(wrappedValue: PaywallState( + selectedPackage: packageValidator.defaultSelectedPackage + )) } catch { // STEP 2.5: Use fallback paywall if viewmodel construction fails Logger.error(Strings.paywall_view_model_construction_failed(error)) + // WIP: Need to select default package in fallback view model self.componentViewModel = Self.fallbackPaywallViewModels() + self._paywallState = .init(wrappedValue: PaywallState(selectedPackage: nil)) } } diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift index fde37e6cf5..c2f5515bd4 100644 --- a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift +++ b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift @@ -21,7 +21,7 @@ import SwiftUI private enum Template5Preview { - static let paywallState = PaywallState() + static let paywallState = PaywallState(selectedPackage: nil) static let catUrl = URL(string: "https://assets.pawwalls.com/954459_1701163461.jpg")! From 4cfce616f418efcca6367caf4c9f67c6f308c840 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Sat, 26 Oct 2024 20:14:05 -0500 Subject: [PATCH 6/8] PurchaseButton is now a stack like button --- .../Button/ButtonComponentView.swift | 10 ++- .../PurchaseButtonComponentView.swift | 85 ++++++++++--------- .../PurchaseButtonComponentViewModel.swift | 70 +++------------ .../PaywallComponentViewModel.swift | 6 +- .../Template1Preview.swift | 23 ++--- .../Template5Preview.swift | 29 ++++--- .../PaywallPurchaseButtonComponent.swift | 61 +------------ .../Config/SamplePaywalls.swift | 33 ++++--- 8 files changed, 125 insertions(+), 192 deletions(-) diff --git a/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift b/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift index c07c8ff9b5..8b671c98c9 100644 --- a/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift +++ b/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift @@ -35,10 +35,12 @@ struct ButtonComponentView: View { } var body: some View { - AsyncButton( - action: { try await performAction() }, - label: { StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: self.onDismiss) } - ) + AsyncButton { + try await performAction() + } label: { + // Not passing an onDismiss - nothing in this stack should be able to dismiss + StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: {}) + } #if canImport(SafariServices) && canImport(UIKit) .sheet(isPresented: .isNotNil($inAppBrowserURL)) { SafariView(url: inAppBrowserURL!) diff --git a/RevenueCatUI/Templates/Components/Packages/PurchaseButton/PurchaseButtonComponentView.swift b/RevenueCatUI/Templates/Components/Packages/PurchaseButton/PurchaseButtonComponentView.swift index f1a51113e0..6df2201f39 100644 --- a/RevenueCatUI/Templates/Components/Packages/PurchaseButton/PurchaseButtonComponentView.swift +++ b/RevenueCatUI/Templates/Components/Packages/PurchaseButton/PurchaseButtonComponentView.swift @@ -46,19 +46,8 @@ struct PurchaseButtonComponentView: View { _ = try await self.purchaseHandler.purchase(package: selectedPackage) } label: { - // WIP: Need to add logic for intro offer - Text(viewModel.cta) - .font(viewModel.textStyle) - .fontWeight(viewModel.fontWeight) - .fixedSize(horizontal: false, vertical: true) - .multilineTextAlignment(viewModel.horizontalAlignment) - .foregroundStyle(viewModel.color) - .padding(viewModel.padding) - .background(viewModel.backgroundColor) - .shape(viewModel.clipShape) - .cornerBorder(border: nil, - radiuses: viewModel.cornerRadiuses) - .padding(viewModel.margin) + // Not passing an onDismiss - nothing in this stack should be able to dismiss + StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: {}) } } @@ -96,22 +85,29 @@ struct PurchaseButtonComponentView_Previews: PreviewProvider { PurchaseButtonComponentView( // swiftlint:disable:next force_try viewModel: try! .init( + packageValidator: PackageValidator(), localizedStrings: [ "id_1": .string("Hello, world"), "id_2": .string("Hello, world intro offer") ], component: .init( - cta: "id_1", - ctaIntroOffer: "id_2", - fontWeight: .bold, - color: .init(light: "#ffffff"), - backgroundColor: .init(light: "#ff0000"), - padding: .init(top: 10, - bottom: 10, - leading: 30, - trailing: 30), - shape: .pill - ) + stack: .init(components: [ + // WIP: Intro offer state with "id_2", + .text(.init( + text: "id_1", + fontWeight: .bold, + color: .init(light: "#ffffff"), + backgroundColor: .init(light: "#ff0000"), + padding: .init(top: 10, + bottom: 10, + leading: 30, + trailing: 30) + )) + ]) + ), + offering: Offering(identifier: "", + serverDescription: "", + availablePackages: []) ) ) .previewLayout(.sizeThatFits) @@ -121,26 +117,37 @@ struct PurchaseButtonComponentView_Previews: PreviewProvider { PurchaseButtonComponentView( // swiftlint:disable:next force_try viewModel: try! .init( + packageValidator: PackageValidator(), localizedStrings: [ "id_1": .string("Hello, world"), "id_2": .string("Hello, world intro offer") ], component: .init( - cta: "id_1", - ctaIntroOffer: "id_2", - fontWeight: .bold, - color: .init(light: "#ffffff"), - backgroundColor: .init(light: "#ff0000"), - padding: .init(top: 10, - bottom: 10, - leading: 30, - trailing: 30), - shape: .rectangle, - cornerRadiuses: .init(topLeading: 8, - topTrailing: 8, - bottomLeading: 8, - bottomTrailing: 8) - ) + stack: .init( + components: [ + // WIP: Intro offer state with "id_2", + .text(.init( + text: "id_1", + fontWeight: .bold, + color: .init(light: "#ffffff") + )) + ], + backgroundColor: .init(light: "#ff0000"), + padding: .init(top: 8, + bottom: 8, + leading: 8, + trailing: 8), + cornerRadiuses: PaywallComponent.CornerRadiuses( + topLeading: 8, + topTrailing: 8, + bottomLeading: 8, + bottomTrailing: 8 + ) + ) + ), + offering: Offering(identifier: "", + serverDescription: "", + availablePackages: []) ) ) .previewLayout(.sizeThatFits) diff --git a/RevenueCatUI/Templates/Components/Packages/PurchaseButton/PurchaseButtonComponentViewModel.swift b/RevenueCatUI/Templates/Components/Packages/PurchaseButton/PurchaseButtonComponentViewModel.swift index 8356a2803c..68ac764e45 100644 --- a/RevenueCatUI/Templates/Components/Packages/PurchaseButton/PurchaseButtonComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Packages/PurchaseButton/PurchaseButtonComponentViewModel.swift @@ -23,65 +23,23 @@ class PurchaseButtonComponentViewModel { private let localizedStrings: PaywallComponent.LocalizationDictionary private let component: PaywallComponent.PurchaseButtonComponent - let cta: String - let ctaIntroOffer: String? - - init(localizedStrings: PaywallComponent.LocalizationDictionary, - component: PaywallComponent.PurchaseButtonComponent) throws { + let stackViewModel: StackComponentViewModel + + init( + packageValidator: PackageValidator, + localizedStrings: PaywallComponent.LocalizationDictionary, + component: PaywallComponent.PurchaseButtonComponent, + offering: Offering + ) throws { self.localizedStrings = localizedStrings self.component = component - self.cta = try localizedStrings.string(key: component.cta) - self.ctaIntroOffer = try component.ctaIntroOffer.flatMap { - try localizedStrings.string(key: $0) - } - } - - var fontFamily: String? { - component.fontFamily - } - - var fontWeight: Font.Weight { - component.fontWeight.fontWeight - } - - var color: Color { - component.color.toDyanmicColor() - } - - var textStyle: Font { - component.textStyle.font - } - - var horizontalAlignment: TextAlignment { - component.horizontalAlignment.textAlignment - } - - var backgroundColor: Color { - component.backgroundColor?.toDyanmicColor() ?? Color.clear - } - - var padding: EdgeInsets { - component.padding.edgeInsets - } - - var margin: EdgeInsets { - component.margin.edgeInsets - } - - var clipShape: PaywallComponent.Shape { - component.shape - } - - var cornerRadiuses: CornerBorderModifier.RaidusInfo? { - component.cornerRadiuses.flatMap { cornerRadiuses in - CornerBorderModifier.RaidusInfo( - topLeft: cornerRadiuses.topLeading, - topRight: cornerRadiuses.topTrailing, - bottomLeft: cornerRadiuses.bottomLeading, - bottomRight: cornerRadiuses.bottomLeading - ) - } + self.stackViewModel = try StackComponentViewModel( + packageValidator: packageValidator, + component: component.stack, + localizedStrings: localizedStrings, + offering: offering + ) } } diff --git a/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift b/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift index 92a125f675..6a8e33cfee 100644 --- a/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/PaywallComponentViewModel.swift @@ -76,8 +76,10 @@ extension PaywallComponent { return .package(viewModel) case .purchaseButton(let component): return .purchaseButton( - try PurchaseButtonComponentViewModel(localizedStrings: localizedStrings, - component: component) + try PurchaseButtonComponentViewModel(packageValidator: packageValidator, + localizedStrings: localizedStrings, + component: component, + offering: offering) ) } } diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift index f009df2f44..b17a8a4ff3 100644 --- a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift +++ b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift @@ -93,16 +93,19 @@ private enum Template1Preview { ) static let purchaseButton = PaywallComponent.PurchaseButtonComponent( - cta: "cta", - ctaIntroOffer: "cta_intro", - fontWeight: .bold, - color: .init(light: "#ffffff"), - backgroundColor: .init(light: "#e89d89"), - padding: .init(top: 10, - bottom: 10, - leading: 30, - trailing: 30), - shape: .pill + stack: .init(components: [ + // WIP: Intro offer state with "cta_intro", + .text(.init( + text: "cta", + fontWeight: .bold, + color: .init(light: "#ffffff"), + backgroundColor: .init(light: "#e89d89"), + padding: .init(top: 10, + bottom: 10, + leading: 30, + trailing: 30) + )) + ]) ) static let contentStack = PaywallComponent.StackComponent( diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift index c2f5515bd4..e85713e03e 100644 --- a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift +++ b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift @@ -146,16 +146,25 @@ private enum Template5Preview { ) static let purchaseButton = PaywallComponent.PurchaseButtonComponent( - cta: "cta", - ctaIntroOffer: "cta_intro", - fontWeight: .bold, - color: .init(light: "#ffffff"), - backgroundColor: .init(light: "#e89d89"), - padding: .init(top: 15, - bottom: 15, - leading: 50, - trailing: 50), - shape: .pill + stack: .init( + components: [ + // WIP: Intro offer state with "cta_intro", + .text(.init( + text: "cta", + fontWeight: .bold, + color: .init(light: "#ffffff") + )) + ], + backgroundColor: .init(light: "#e89d89"), + padding: .init(top: 15, + bottom: 15, + leading: 30, + trailing: 30), + cornerRadiuses: .init(topLeading: 16, + topTrailing: 16, + bottomLeading: 16, + bottomTrailing: 16) + ) ) static let purchaseButtonStack = PaywallComponent.StackComponent( diff --git a/Sources/Paywalls/Components/PaywallPurchaseButtonComponent.swift b/Sources/Paywalls/Components/PaywallPurchaseButtonComponent.swift index 0bed25ff30..3cb231da16 100644 --- a/Sources/Paywalls/Components/PaywallPurchaseButtonComponent.swift +++ b/Sources/Paywalls/Components/PaywallPurchaseButtonComponent.swift @@ -15,70 +15,17 @@ public extension PaywallComponent { struct PurchaseButtonComponent: PaywallComponentBase { let type: ComponentType - public let cta: LocalizationKey - public let ctaIntroOffer: LocalizationKey? - public let fontFamily: String? - public let fontWeight: FontWeight - public let color: ColorInfo - public let textStyle: TextStyle - public let horizontalAlignment: HorizontalAlignment - public let backgroundColor: ColorInfo? - public let padding: Padding - public let margin: Padding - public let shape: Shape - public let cornerRadiuses: CornerRadiuses? + public let stack: PaywallComponent.StackComponent public init( - cta: LocalizationKey, - ctaIntroOffer: LocalizationKey? = nil, - fontFamily: String? = nil, - fontWeight: FontWeight = .regular, - color: ColorInfo, - backgroundColor: ColorInfo? = nil, - padding: Padding = .default, - margin: Padding = .default, - textStyle: TextStyle = .body, - horizontalAlignment: HorizontalAlignment = .center, - shape: Shape = .pill, - cornerRadiuses: CornerRadiuses? = nil + stack: PaywallComponent.StackComponent ) { - self.type = .purchaseButton - self.cta = cta - self.ctaIntroOffer = ctaIntroOffer - self.fontFamily = fontFamily - self.fontWeight = fontWeight - self.color = color - self.backgroundColor = backgroundColor - self.padding = padding - self.margin = margin - self.textStyle = textStyle - self.horizontalAlignment = horizontalAlignment - self.shape = shape - self.cornerRadiuses = cornerRadiuses + self.type = .button + self.stack = stack } } } -extension PaywallComponent.PurchaseButtonComponent { - - enum CodingKeys: String, CodingKey { - case type - case cta = "cta_lid" - case ctaIntroOffer = "cta_intro_offer_lid" - case fontFamily - case fontWeight - case color - case textStyle - case horizontalAlignment - case backgroundColor - case padding - case margin - case shape - case cornerRadiuses - } - -} - #endif diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester/Config/SamplePaywalls.swift b/Tests/TestingApps/PaywallsTester/PaywallsTester/Config/SamplePaywalls.swift index 12d4edc246..c620d2a65b 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester/Config/SamplePaywalls.swift +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester/Config/SamplePaywalls.swift @@ -766,20 +766,25 @@ private extension SamplePaywallLoader { margin: .init(top: 20, bottom: 20, leading: 20, trailing: 20) )), .purchaseButton(.init( - cta: "cta", - ctaIntroOffer: "cta_intro", - fontWeight: .bold, - color: .init(light: "#ffffff"), - backgroundColor: .init(light: "#ff0000"), - padding: .init(top: 10, - bottom: 10, - leading: 30, - trailing: 30), - shape: .rectangle, - cornerRadiuses: .init(topLeading: 10, - topTrailing: 10, - bottomLeading: 10, - bottomTrailing: 10) + stack: .init( + components: [ + // WIP: Intro offer state with "cta_intro", + .text(.init( + text: "cta", + fontWeight: .bold, + color: .init(light: "#ffffff") + )) + ], + backgroundColor: .init(light: "#ff0000"), + padding: .init(top: 15, + bottom: 15, + leading: 30, + trailing: 30), + cornerRadiuses: .init(topLeading: 16, + topTrailing: 16, + bottomLeading: 16, + bottomTrailing: 16) + ) )) ], width: .init(type: .fill, value: nil), From fb9efdec7751d94cf41c6d47fd7f5f8bc47125f8 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Thu, 31 Oct 2024 09:29:37 -0500 Subject: [PATCH 7/8] Passing onDismiss --- .../Templates/Components/Button/ButtonComponentView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift b/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift index 8b671c98c9..1e17ff413a 100644 --- a/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift +++ b/RevenueCatUI/Templates/Components/Button/ButtonComponentView.swift @@ -38,8 +38,7 @@ struct ButtonComponentView: View { AsyncButton { try await performAction() } label: { - // Not passing an onDismiss - nothing in this stack should be able to dismiss - StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: {}) + StackComponentView(viewModel: viewModel.stackViewModel, onDismiss: self.onDismiss) } #if canImport(SafariServices) && canImport(UIKit) .sheet(isPresented: .isNotNil($inAppBrowserURL)) { From b5aa94839747b7b4896576713c4e194cb7818e14 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Thu, 31 Oct 2024 12:25:00 -0500 Subject: [PATCH 8/8] Lint --- .../Templates/Components/Text/TextComponentViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/Templates/Components/Text/TextComponentViewModel.swift b/RevenueCatUI/Templates/Components/Text/TextComponentViewModel.swift index 8b8d5d4b09..7af88acc8c 100644 --- a/RevenueCatUI/Templates/Components/Text/TextComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Text/TextComponentViewModel.swift @@ -18,7 +18,7 @@ import SwiftUI @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) class TextComponentViewModel { - + private let localizedStrings: PaywallComponent.LocalizationDictionary private let component: PaywallComponent.TextComponent