From 0ebc6ddcaec2a3d7ea4e529f0ac7a46558e2ce2b Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Fri, 25 Aug 2023 13:58:42 -0700 Subject: [PATCH 1/2] `Paywalls`: template 5 --- RevenueCatUI/Data/Constants.swift | 1 + RevenueCatUI/Data/PaywallTemplate.swift | 1 + RevenueCatUI/Data/TestData.swift | 54 +++ RevenueCatUI/PaywallView.swift | 1 + RevenueCatUI/Templates/Template2View.swift | 3 +- RevenueCatUI/Templates/Template3View.swift | 3 +- RevenueCatUI/Templates/Template4View.swift | 1 - RevenueCatUI/Templates/Template5View.swift | 316 ++++++++++++++++++ RevenueCatUI/Templates/TemplateViewType.swift | 3 + RevenueCatUI/Views/IconView.swift | 10 + RevenueCatUI/Views/PackageButtonStyle.swift | 1 + .../Templates/Template5ViewTests.swift | 67 ++++ .../SimpleApp/SimpleApp/SamplePaywalls.swift | 50 +++ .../SimpleApp/Views/CustomPaywall.swift | 11 + .../SimpleApp/Views/SamplePaywallsList.swift | 2 + 15 files changed, 519 insertions(+), 5 deletions(-) create mode 100644 RevenueCatUI/Templates/Template5View.swift create mode 100644 Tests/RevenueCatUITests/Templates/Template5ViewTests.swift diff --git a/RevenueCatUI/Data/Constants.swift b/RevenueCatUI/Data/Constants.swift index 31df77eac5..79dfc0cacc 100644 --- a/RevenueCatUI/Data/Constants.swift +++ b/RevenueCatUI/Data/Constants.swift @@ -21,6 +21,7 @@ enum Constants { static let toggleAllPlansAnimation: Animation = .spring(response: 0.35, dampingFraction: 0.7) static let defaultCornerRadius: CGFloat = 20 + static let defaultPackageCornerRadius: CGFloat = 16 /// For UI elements that wouldn't make sense to keep scaling up forever static let maximumDynamicTypeSize: DynamicTypeSize = .accessibility3 diff --git a/RevenueCatUI/Data/PaywallTemplate.swift b/RevenueCatUI/Data/PaywallTemplate.swift index a0efb25c88..d9864e96b3 100644 --- a/RevenueCatUI/Data/PaywallTemplate.swift +++ b/RevenueCatUI/Data/PaywallTemplate.swift @@ -22,6 +22,7 @@ internal enum PaywallTemplate: String { // Temporarily disabled until it's supported in the dashboard case template4 = "4_disabled" + case template5 = "5_disabled" } diff --git a/RevenueCatUI/Data/TestData.swift b/RevenueCatUI/Data/TestData.swift index 83f58448dd..b28aa6c4f2 100644 --- a/RevenueCatUI/Data/TestData.swift +++ b/RevenueCatUI/Data/TestData.swift @@ -323,6 +323,60 @@ internal enum TestData { TestData.annualPackage] ) + static let offeringWithTemplate5Paywall = Offering( + identifier: Self.offeringIdentifier, + serverDescription: "Offering", + metadata: [:], + paywall: .init( + templateName: PaywallTemplate.template5.rawValue, + config: .init( + packages: [PackageType.annual.identifier, + PackageType.monthly.identifier], + defaultPackage: PackageType.annual.identifier, + images: .init( + header: "954459_1692992845.png" + ), + colors: .init( + light: .init( + background: "#FFFFFF", + text1: "#000000", + callToActionBackground: "#008575", + callToActionForeground: "#FFFFFF", + accent1: "#008575", + accent2: "#DFDFDF" + ), + dark: .init( + background: "#000000", + text1: "#FFFFFF", + callToActionBackground: "#41E194", + callToActionForeground: "#000000", + accent1: "#41E194", + accent2: "#DFDFDF" + ) + ), + termsOfServiceURL: URL(string: "https://revenuecat.com/tos")! + ), + localization: .init( + title: "Spice Up Your Kitchen - Go Pro for Exclusive Benefits!", + callToAction: "Continue", + callToActionWithIntroOffer: "Start your Free Trial", + offerDetails: "{{ total_price_and_per_month }}", + offerDetailsWithIntroOffer: "Free for {{ sub_offer_duration }}, then {{ total_price_and_per_month }}", + offerName: "{{ sub_period }}", + features: [ + .init(title: "Unique gourmet recipes", iconID: "tick"), + .init(title: "Advanced nutritional recipes", iconID: "tick"), + .init(title: "Personalized support from our Chef", iconID: "tick"), + .init(title: "Unlimited receipt collections", iconID: "tick") + ] + ), + assetBaseURL: Self.paywallAssetBaseURL + ), + availablePackages: [TestData.monthlyPackage, + TestData.sixMonthPackage, + TestData.annualPackage] + ) + static let offeringWithNoPaywall = Offering( identifier: Self.offeringIdentifier, serverDescription: "Offering", diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index 71348d0fc6..9a077e8e11 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -305,6 +305,7 @@ private extension PaywallTemplate { case .template2: return "Bold Packages" case .template3: return "Feature List" case .template4: return "Horizontal" + case .template5: return "Small Banner" } } diff --git a/RevenueCatUI/Templates/Template2View.swift b/RevenueCatUI/Templates/Template2View.swift index 550e228ed1..e7540cf679 100644 --- a/RevenueCatUI/Templates/Template2View.swift +++ b/RevenueCatUI/Templates/Template2View.swift @@ -118,7 +118,6 @@ struct Template2View: TemplateViewType { self.selectedPackage = package } label: { self.packageButton(package, selected: isSelected) - .contentShape(Rectangle()) } .buttonStyle(PackageButtonStyle(isSelected: isSelected)) } @@ -265,7 +264,7 @@ struct Template2View: TemplateViewType { private var iconSize: CGFloat = 140 private static let fadedColorOpacity: CGFloat = 0.3 - private static let cornerRadius: CGFloat = 15 + private static let cornerRadius: CGFloat = Constants.defaultPackageCornerRadius private static let packageButtonAlignment: Alignment = .leading } diff --git a/RevenueCatUI/Templates/Template3View.swift b/RevenueCatUI/Templates/Template3View.swift index d7b5be68e0..7769676cd3 100644 --- a/RevenueCatUI/Templates/Template3View.swift +++ b/RevenueCatUI/Templates/Template3View.swift @@ -129,8 +129,7 @@ private struct FeatureView: View { private var icon: some View { Circle() .overlay { - if let iconName = self.feature.iconID, - let icon = PaywallIcon(rawValue: iconName) { + if let icon = self.feature.icon { IconView(icon: icon, tint: self.colors.accent1Color) .padding(self.iconPadding) } diff --git a/RevenueCatUI/Templates/Template4View.swift b/RevenueCatUI/Templates/Template4View.swift index 93e1b75fb2..c0b095d889 100644 --- a/RevenueCatUI/Templates/Template4View.swift +++ b/RevenueCatUI/Templates/Template4View.swift @@ -136,7 +136,6 @@ struct Template4View: TemplateViewType { selected: isSelected, packageWidth: self.packageWidth, desiredHeight: self.packageContentHeight) - .contentShape(Rectangle()) } .buttonStyle(PackageButtonStyle(isSelected: isSelected)) } diff --git a/RevenueCatUI/Templates/Template5View.swift b/RevenueCatUI/Templates/Template5View.swift new file mode 100644 index 0000000000..fde3e08915 --- /dev/null +++ b/RevenueCatUI/Templates/Template5View.swift @@ -0,0 +1,316 @@ +// +// 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 +// +// Template5View.swift +// +// Created by Nacho Soto. + +import RevenueCat +import SwiftUI + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +@available(tvOS, unavailable) +struct Template5View: TemplateViewType { + + let configuration: TemplateViewConfiguration + + @State + private var selectedPackage: TemplateViewConfiguration.Package + + @State + private var displayingAllPlans: Bool + + @Environment(\.userInterfaceIdiom) + var userInterfaceIdiom + + @Environment(\.locale) + var locale + + @EnvironmentObject + private var introEligibilityViewModel: IntroEligibilityViewModel + @EnvironmentObject + private var purchaseHandler: PurchaseHandler + + init(_ configuration: TemplateViewConfiguration) { + self._selectedPackage = .init(initialValue: configuration.packages.default) + self.configuration = configuration + self._displayingAllPlans = .init(initialValue: configuration.mode.displayAllPlansByDefault) + } + + var body: some View { + self.content + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + @ViewBuilder + var content: some View { + VStack(spacing: self.defaultVerticalPaddingLength) { + if self.configuration.mode.shouldDisplayIcon { + if let header = self.configuration.headerImageURL { + RemoteImage(url: header, + aspectRatio: Self.headerAspectRatio, + maxWidth: .infinity) + + Spacer() + } + } + + self.scrollableContent + .scrollableIfNecessary(enabled: self.configuration.mode.shouldDisplayPackages) + + Spacer(minLength: 0) + + if self.configuration.mode.shouldDisplayInlineOfferDetails { + self.offerDetails(package: self.selectedPackage, selected: false) + } + + self.subscribeButton + .defaultHorizontalPadding() + + FooterView(configuration: self.configuration, + purchaseHandler: self.purchaseHandler, + displayingAllPlans: self.$displayingAllPlans) + } + .foregroundColor(self.configuration.colors.text1Color) + .edgesIgnoringSafeArea(.top) + .animation(Constants.fastAnimation, value: self.selectedPackage) + .frame(maxHeight: .infinity) + } + + private var scrollableContent: some View { + VStack(spacing: self.defaultVerticalPaddingLength) { + if self.configuration.mode.shouldDisplayText { + Text(.init(self.selectedLocalization.title)) + .font(self.font(for: .largeTitle).bold()) + .defaultHorizontalPadding() + + Spacer() + + self.features + .defaultHorizontalPadding() + + Spacer() + } + + if self.configuration.mode.shouldDisplayPackages { + self.packages + } else { + self.packages + .padding(.top, self.defaultHorizontalPaddingLength) + .hideFooterContent(self.configuration, + hide: !self.displayingAllPlans) + } + } + .frame(maxHeight: .infinity) + } + + @ViewBuilder + private var features: some View { + VStack(spacing: self.defaultVerticalPaddingLength) { + ForEach(self.selectedLocalization.features, id: \.title) { feature in + HStack { + Rectangle() + .foregroundStyle(.clear) + .aspectRatio(1, contentMode: .fit) + .overlay { + if let icon = feature.icon { + IconView(icon: icon, tint: self.configuration.colors.accent1Color) + } + } + .frame(width: self.iconSize, height: self.iconSize) + + Text(.init(feature.title)) + .font(self.font(for: .body)) + .lineLimit(nil) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + .accessibilityElement(children: .combine) + } + } + } + + @ViewBuilder + private var packages: some View { + VStack(spacing: 16) { + ForEach(self.configuration.packages.all, id: \.content.id) { package in + let isSelected = self.selectedPackage.content === package.content + + Button { + self.selectedPackage = package + } label: { + self.packageButton(package, selected: isSelected) + } + .buttonStyle(PackageButtonStyle(isSelected: isSelected)) + } + } + .defaultHorizontalPadding() + + Spacer() + } + + @ViewBuilder + private func packageButton(_ package: TemplateViewConfiguration.Package, selected: Bool) -> some View { + VStack(alignment: Self.packageButtonAlignment.horizontal, spacing: 5) { + self.packageButtonTitle(package, selected: selected) + + self.offerDetails(package: package, selected: selected) + } + .font(self.font(for: .body).weight(.medium)) + .defaultPadding() + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: Self.packageButtonAlignment) + .overlay { + self.roundedRectangle + .stroke( + selected + ? self.configuration.colors.accent1Color + : self.configuration.colors.accent2Color, + lineWidth: 2 + ) + } + .overlay(alignment: .topTrailing) { + self.packageDiscountLabel(package) + .padding(8) + } + } + + @ViewBuilder + private func packageDiscountLabel(_ package: TemplateViewConfiguration.Package) -> some View { + if let discount = package.discountRelativeToMostExpensivePerMonth { + Text(Localization.localized(discount: discount, locale: self.locale)) + .textCase(.uppercase) + .padding(.vertical, 4) + .padding(.horizontal, 8) + .background(self.roundedRectangle.foregroundColor(self.configuration.colors.accent1Color)) + .foregroundColor(self.configuration.colors.callToActionForegroundColor) + .font(self.font(for: .caption)) + .dynamicTypeSize(...Constants.maximumDynamicTypeSize) + } + } + + private var roundedRectangle: some Shape { + RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous) + } + + private func packageButtonTitle( + _ package: TemplateViewConfiguration.Package, + selected: Bool + ) -> some View { + HStack { + Image(systemName: "checkmark.circle.fill") + .hidden(if: !selected) + .overlay { + if selected { + EmptyView() + } else { + Circle() + .foregroundColor(self.selectedBackgroundColor) + } + } + .foregroundColor(self.configuration.colors.accent1Color) + + Text(package.localization.offerName ?? package.content.productName) + } + } + + private func offerDetails(package: TemplateViewConfiguration.Package, selected: Bool) -> some View { + IntroEligibilityStateView( + textWithNoIntroOffer: package.localization.offerDetails, + textWithIntroOffer: package.localization.offerDetailsWithIntroOffer, + introEligibility: self.introEligibility[package.content], + foregroundColor: self.configuration.colors.text1Color, + alignment: Self.packageButtonAlignment + ) + .fixedSize(horizontal: false, vertical: true) + .font(self.font(for: .body)) + } + + private var subscribeButton: some View { + PurchaseButton( + package: self.selectedPackage, + configuration: self.configuration, + introEligibility: self.introEligibility[self.selectedPackage.content], + purchaseHandler: self.purchaseHandler + ) + } + + // MARK: - + + private var introEligibility: [Package: IntroEligibilityStatus] { + return self.introEligibilityViewModel.allEligibility + } + + private var selectedBackgroundColor: Color { self.configuration.colors.accent2Color } + + @ScaledMetric(relativeTo: .body) + private var iconSize = 25 + + private static let cornerRadius: CGFloat = Constants.defaultPackageCornerRadius + private static let packageButtonAlignment: Alignment = .leading + + private static let headerAspectRatio: CGFloat = 2 + +} + +// MARK: - Extensions + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +@available(tvOS, unavailable) +private extension Template5View { + + var selectedLocalization: ProcessedLocalizedConfiguration { + return self.selectedPackage.localization + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +private extension PaywallViewMode { + + var shouldDisplayPackages: Bool { + switch self { + case .fullScreen: return true + case .footer, .condensedFooter: return false + } + } + + var shouldDisplayInlineOfferDetails: Bool { + switch self { + case .fullScreen: return false + case .footer, .condensedFooter: return true + } + } + +} + +// MARK: - + +#if DEBUG + +@available(iOS 16.0, macOS 13.0, tvOS 16.0, *) +@available(watchOS, unavailable) +@available(macOS, unavailable) +@available(tvOS, unavailable) +struct Template5View_Previews: PreviewProvider { + + static var previews: some View { + ForEach(PaywallViewMode.allCases, id: \.self) { mode in + PreviewableTemplate( + offering: TestData.offeringWithTemplate5Paywall, + mode: mode + ) { + Template5View($0) + } + } + } + +} + +#endif diff --git a/RevenueCatUI/Templates/TemplateViewType.swift b/RevenueCatUI/Templates/TemplateViewType.swift index 9f6fc2167b..c10f9db59b 100644 --- a/RevenueCatUI/Templates/TemplateViewType.swift +++ b/RevenueCatUI/Templates/TemplateViewType.swift @@ -64,6 +64,7 @@ private extension PaywallTemplate { case .template2: return .multiple case .template3: return .single case .template4: return .multiple + case .template5: return .multiple } } @@ -139,6 +140,8 @@ extension PaywallData { Template3View(configuration) case .template4: Template4View(configuration) + case .template5: + Template5View(configuration) } } diff --git a/RevenueCatUI/Views/IconView.swift b/RevenueCatUI/Views/IconView.swift index c7856241e3..3b3d59fe95 100644 --- a/RevenueCatUI/Views/IconView.swift +++ b/RevenueCatUI/Views/IconView.swift @@ -11,6 +11,7 @@ // // Created by Nacho Soto on 7/25/23. +import RevenueCat import SwiftUI /// A view that renders an icon by name, tinted with a color. @@ -26,6 +27,7 @@ struct IconView: View { .resizable() .scaledToFit() .foregroundStyle(self.tint) + .accessibilityHidden(true) } } @@ -82,6 +84,14 @@ enum PaywallIcon: String, CaseIterable { } +extension PaywallData.LocalizedConfiguration.Feature { + + var icon: PaywallIcon? { + return self.iconID.flatMap(PaywallIcon.init(rawValue:)) + } + +} + #if DEBUG @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) diff --git a/RevenueCatUI/Views/PackageButtonStyle.swift b/RevenueCatUI/Views/PackageButtonStyle.swift index aab03ff07f..e0c4352c37 100644 --- a/RevenueCatUI/Views/PackageButtonStyle.swift +++ b/RevenueCatUI/Views/PackageButtonStyle.swift @@ -28,6 +28,7 @@ struct PackageButtonStyle: ButtonStyle { func makeBody(configuration: ButtonStyleConfiguration) -> some View { configuration .label + .contentShape(Rectangle()) .hidden(if: self.purchaseHandler.actionInProgress) .overlay { if self.isSelected, self.purchaseHandler.actionInProgress { diff --git a/Tests/RevenueCatUITests/Templates/Template5ViewTests.swift b/Tests/RevenueCatUITests/Templates/Template5ViewTests.swift new file mode 100644 index 0000000000..3b06f5bb78 --- /dev/null +++ b/Tests/RevenueCatUITests/Templates/Template5ViewTests.swift @@ -0,0 +1,67 @@ +// +// 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 +// +// Template5ViewTests.swift + +import Nimble +import RevenueCat +@testable import RevenueCatUI +import SnapshotTesting + +#if !os(macOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +class Template5ViewTests: BaseSnapshotTest { + + func testSamplePaywall() { + Self.createPaywall(offering: Self.offering.withLocalImages) + .snapshot(size: Self.fullScreenSize) + } + + func testTabletPaywall() { + Self.createPaywall(offering: Self.offering.withLocalImages) + .environment(\.userInterfaceIdiom, .pad) + .snapshot(size: Self.iPadSize) + } + + func testCustomFont() { + Self.createPaywall(offering: Self.offering.withLocalImages, + fonts: Self.fonts) + .snapshot(size: Self.fullScreenSize) + } + + func testLargeDynamicType() { + Self.createPaywall(offering: Self.offering.withLocalImages) + .environment(\.dynamicTypeSize, .xxLarge) + .snapshot(size: Self.fullScreenSize) + } + + func testLargerDynamicType() { + Self.createPaywall(offering: Self.offering.withLocalImages) + .environment(\.dynamicTypeSize, .accessibility2) + .snapshot(size: Self.fullScreenSize) + } + + func testFooterPaywall() { + Self.createPaywall(offering: Self.offering.withLocalImages, + mode: .footer) + .snapshot(size: Self.footerSize) + } + + func testCondensedFooterPaywall() { + Self.createPaywall(offering: Self.offering.withLocalImages, + mode: .condensedFooter) + .snapshot(size: Self.footerSize) + } + + private static let offering = TestData.offeringWithTemplate5Paywall + +} + +#endif diff --git a/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift b/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift index 0f5e5cf86a..1861cc0758 100644 --- a/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift +++ b/Tests/TestingApps/SimpleApp/SimpleApp/SamplePaywalls.swift @@ -65,6 +65,8 @@ final class SamplePaywallLoader { return Self.template3() case .template4: return Self.template4() + case .template5: + return Self.template5() } } @@ -345,6 +347,54 @@ private extension SamplePaywallLoader { ) } + static func template5() -> PaywallData { + return .init( + templateName: PaywallTemplate.template5.rawValue, + config: .init( + packages: [PackageType.annual.identifier, + PackageType.monthly.identifier], + defaultPackage: PackageType.annual.identifier, + images: .init( + header: "954459_1692992845.png" + ), + colors: .init( + light: .init( + background: "#FFFFFF", + text1: "#000000", + callToActionBackground: "#008575", + callToActionForeground: "#FFFFFF", + accent1: "#008575", + accent2: "#DFDFDF" + ), + dark: .init( + background: "#000000", + text1: "#FFFFFF", + callToActionBackground: "#41E194", + callToActionForeground: "#000000", + accent1: "#41E194", + accent2: "#DFDFDF" + ) + ), + termsOfServiceURL: URL(string: "https://revenuecat.com/tos")! + ), + localization: .init( + title: "Spice Up Your Kitchen - Go Pro for Exclusive Benefits!", + callToAction: "Continue", + callToActionWithIntroOffer: "Start your Free Trial", + offerDetails: "{{ total_price_and_per_month }}", + offerDetailsWithIntroOffer: "Free for {{ sub_offer_duration }}, then {{ total_price_and_per_month }}", + offerName: "{{ sub_period }}", + features: [ + .init(title: "Unique gourmet recipes", iconID: "tick"), + .init(title: "Advanced nutritional recipes", iconID: "tick"), + .init(title: "Personalized support from our Chef", iconID: "tick"), + .init(title: "Unlimited receipt collections", iconID: "tick") + ] + ), + assetBaseURL: Self.paywallAssetBaseURL + ) + } + static func unrecognizedTemplate() -> PaywallData { return .init( templateName: "unrecognized_template_name", diff --git a/Tests/TestingApps/SimpleApp/SimpleApp/Views/CustomPaywall.swift b/Tests/TestingApps/SimpleApp/SimpleApp/Views/CustomPaywall.swift index 69aab88f64..a0b7855132 100644 --- a/Tests/TestingApps/SimpleApp/SimpleApp/Views/CustomPaywall.swift +++ b/Tests/TestingApps/SimpleApp/SimpleApp/Views/CustomPaywall.swift @@ -63,6 +63,17 @@ struct CustomPaywall_Previews: PreviewProvider { ) .previewDisplayName("Template4\(mode ? " condensed" : "")") } + + ForEach(Self.condensedOptions, id: \.self) { mode in + CustomPaywall( + offering: TestData.offeringWithTemplate5Paywall, + customerInfo: TestData.customerInfo, + condensed: mode, + introEligibility: .producing(eligibility: .eligible), + purchaseHandler: .mock() + ) + .previewDisplayName("Template5\(mode ? " condensed" : "")") + } } private static let condensedOptions: [Bool] = [ diff --git a/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift b/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift index 1dc4b15185..fa6688e188 100644 --- a/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift +++ b/Tests/TestingApps/SimpleApp/SimpleApp/Views/SamplePaywallsList.swift @@ -221,6 +221,8 @@ extension PaywallTemplate { return "#3: Feature list" case .template4: return "#4: Horizontal packages" + case .template5: + return "#5: Minimalist with Small Banner" } } From 1b24890b59f6fb5f323f5b177e0c2da7bd5fd12f Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 29 Aug 2023 13:52:39 -0700 Subject: [PATCH 2/2] More improvements --- RevenueCatUI/Data/Constants.swift | 10 ++++++++++ RevenueCatUI/Templates/Template2View.swift | 5 +++-- RevenueCatUI/Templates/Template5View.swift | 15 +++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/RevenueCatUI/Data/Constants.swift b/RevenueCatUI/Data/Constants.swift index 79dfc0cacc..9f6241d901 100644 --- a/RevenueCatUI/Data/Constants.swift +++ b/RevenueCatUI/Data/Constants.swift @@ -22,6 +22,7 @@ enum Constants { static let defaultCornerRadius: CGFloat = 20 static let defaultPackageCornerRadius: CGFloat = 16 + static let defaultPackageBorderWidth: CGFloat = 2 /// For UI elements that wouldn't make sense to keep scaling up forever static let maximumDynamicTypeSize: DynamicTypeSize = .accessibility3 @@ -44,6 +45,15 @@ enum Constants { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +extension Constants { + + static var checkmarkImage: some View { + Image(systemName: "checkmark.circle.fill") + } + +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) extension TemplateViewType { diff --git a/RevenueCatUI/Templates/Template2View.swift b/RevenueCatUI/Templates/Template2View.swift index e7540cf679..4700c76062 100644 --- a/RevenueCatUI/Templates/Template2View.swift +++ b/RevenueCatUI/Templates/Template2View.swift @@ -143,7 +143,8 @@ struct Template2View: TemplateViewType { EmptyView() } else { self.roundedRectangle - .stroke(self.configuration.colors.text1Color.opacity(Self.fadedColorOpacity), lineWidth: 2) + .stroke(self.configuration.colors.text1Color.opacity(Self.fadedColorOpacity), + lineWidth: Constants.defaultPackageBorderWidth) } } .background { @@ -172,7 +173,7 @@ struct Template2View: TemplateViewType { selected: Bool ) -> some View { HStack { - Image(systemName: "checkmark.circle.fill") + Constants.checkmarkImage .hidden(if: !selected) .overlay { if selected { diff --git a/RevenueCatUI/Templates/Template5View.swift b/RevenueCatUI/Templates/Template5View.swift index fde3e08915..683ea99ea8 100644 --- a/RevenueCatUI/Templates/Template5View.swift +++ b/RevenueCatUI/Templates/Template5View.swift @@ -54,8 +54,9 @@ struct Template5View: TemplateViewType { if self.configuration.mode.shouldDisplayIcon { if let header = self.configuration.headerImageURL { RemoteImage(url: header, - aspectRatio: Self.headerAspectRatio, + aspectRatio: self.headerAspectRatio, maxWidth: .infinity) + .clipped() Spacer() } @@ -88,6 +89,7 @@ struct Template5View: TemplateViewType { if self.configuration.mode.shouldDisplayText { Text(.init(self.selectedLocalization.title)) .font(self.font(for: .largeTitle).bold()) + .frame(maxWidth: .infinity) .defaultHorizontalPadding() Spacer() @@ -172,7 +174,7 @@ struct Template5View: TemplateViewType { selected ? self.configuration.colors.accent1Color : self.configuration.colors.accent2Color, - lineWidth: 2 + lineWidth: Constants.defaultPackageBorderWidth ) } .overlay(alignment: .topTrailing) { @@ -204,7 +206,7 @@ struct Template5View: TemplateViewType { selected: Bool ) -> some View { HStack { - Image(systemName: "checkmark.circle.fill") + Constants.checkmarkImage .hidden(if: !selected) .overlay { if selected { @@ -255,7 +257,12 @@ struct Template5View: TemplateViewType { private static let cornerRadius: CGFloat = Constants.defaultPackageCornerRadius private static let packageButtonAlignment: Alignment = .leading - private static let headerAspectRatio: CGFloat = 2 + private var headerAspectRatio: CGFloat { + switch self.userInterfaceIdiom { + case .pad: return 3 + default: return 2 + } + } }