diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index c88060a944..87d267234c 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -351,6 +351,7 @@ 4DBF1F372B4D572400D52354 /* LocalReceiptFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DBF1F352B4D572400D52354 /* LocalReceiptFetcher.swift */; }; 4DC546272AD44BBE005CDB35 /* EncodedAppleReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC546262AD44BBE005CDB35 /* EncodedAppleReceipt.swift */; }; 4DE3D5742CDB646900838110 /* MockPaywallEventsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFFE6C52AA9465000B2955C /* MockPaywallEventsManager.swift */; }; + 4DEB9BC52D08CA1700D33E36 /* BadgeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DEB9BC42D08CA1500D33E36 /* BadgeModifier.swift */; }; 4F0201C42A13C85500091612 /* Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0201C32A13C85500091612 /* Assertions.swift */; }; 4F05876F2A5DE03F00E9A834 /* PaywallDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */; }; 4F062D322A85A11600A8A613 /* PaywallData+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F062D312A85A11600A8A613 /* PaywallData+Localization.swift */; }; @@ -1663,6 +1664,7 @@ 4DBC30952B1DFA97001D33C7 /* StoreKitVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitVersion.swift; sourceTree = ""; }; 4DBF1F352B4D572400D52354 /* LocalReceiptFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalReceiptFetcher.swift; sourceTree = ""; }; 4DC546262AD44BBE005CDB35 /* EncodedAppleReceipt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncodedAppleReceipt.swift; sourceTree = ""; }; + 4DEB9BC42D08CA1500D33E36 /* BadgeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeModifier.swift; sourceTree = ""; }; 4F0201C32A13C85500091612 /* Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assertions.swift; sourceTree = ""; }; 4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallDataTests.swift; sourceTree = ""; }; 4F062D312A85A11600A8A613 /* PaywallData+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaywallData+Localization.swift"; sourceTree = ""; }; @@ -2502,6 +2504,7 @@ 2C7457872CEDF7AC004ACE52 /* BackgroundStyle.swift */, 77089F9D2CD39EC100848CD5 /* ShadowModifier.swift */, 4D6F4BCF2CF69DE300353AF6 /* ForegroundColorScheme.swift */, + 4DEB9BC42D08CA1500D33E36 /* BadgeModifier.swift */, 2C91068D2CE2481800189565 /* SizeModifier.swift */, 2CAB87F62CAAB13200247013 /* Shape.swift */, ); @@ -6523,6 +6526,7 @@ 3546355F2C391F4D001D7E85 /* PromotionalOfferView.swift in Sources */, 2C7457882CEDF7C0004ACE52 /* BackgroundStyle.swift in Sources */, 2C8EC6DD2CCC7C5B00D6CCF8 /* PackageValidator.swift in Sources */, + 4DEB9BC52D08CA1700D33E36 /* BadgeModifier.swift in Sources */, 778360792CCA85E4000785B8 /* StickyFooterComponentViewModel.swift in Sources */, 2C7457482CEA66AB004ACE52 /* ComponentsView.swift in Sources */, 353756722C382C2800A1B8D6 /* URLUtilities.swift in Sources */, diff --git a/RevenueCatUI/Templates/V2/Components/Stack/StackComponentView.swift b/RevenueCatUI/Templates/V2/Components/Stack/StackComponentView.swift index 130dc0c2cf..0f5bad8f34 100644 --- a/RevenueCatUI/Templates/V2/Components/Stack/StackComponentView.swift +++ b/RevenueCatUI/Templates/V2/Components/Stack/StackComponentView.swift @@ -97,6 +97,7 @@ struct StackComponentView: View { // Without compositingGroup(), the shadow is applied to the stack's children as well. view.compositingGroup().shadow(shadow: shadow) } + .badge(style.badge, textComponentViewModel: viewModel.badgeTextViewModel) .padding(style.margin) } @@ -455,7 +456,8 @@ fileprivate extension StackComponentViewModel { try self.init( component: component, - viewModels: viewModels + viewModels: viewModels, + localizationProvider: localizationProvider ) } diff --git a/RevenueCatUI/Templates/V2/Components/Stack/StackComponentViewModel.swift b/RevenueCatUI/Templates/V2/Components/Stack/StackComponentViewModel.swift index 22ad28f05f..311ef22663 100644 --- a/RevenueCatUI/Templates/V2/Components/Stack/StackComponentViewModel.swift +++ b/RevenueCatUI/Templates/V2/Components/Stack/StackComponentViewModel.swift @@ -24,15 +24,35 @@ class StackComponentViewModel { private let component: PaywallComponent.StackComponent private let presentedOverrides: PresentedOverrides? + let badgeTextViewModel: TextComponentViewModel? let viewModels: [PaywallComponentViewModel] init( component: PaywallComponent.StackComponent, - viewModels: [PaywallComponentViewModel] + viewModels: [PaywallComponentViewModel], + localizationProvider: LocalizationProvider ) throws { self.component = component self.viewModels = viewModels + if let badge = component.badge { + badgeTextViewModel = try TextComponentViewModel( + localizationProvider: localizationProvider, + component: PaywallComponent.TextComponent( + text: badge.textLid, + fontName: badge.fontName, + fontWeight: badge.fontWeight, + color: badge.color, + padding: badge.padding, + margin: .zero, + fontSize: badge.fontSize, + horizontalAlignment: badge.horizontalAlignment + ) + ) + } else { + badgeTextViewModel = nil + } + self.presentedOverrides = try self.component.overrides?.toPresentedOverrides { $0 } } @@ -60,7 +80,8 @@ class StackComponentViewModel { margin: partial?.margin ?? self.component.margin, shape: partial?.shape ?? self.component.shape, border: partial?.border ?? self.component.border, - shadow: partial?.shadow ?? self.component.shadow + shadow: partial?.shadow ?? self.component.shadow, + badge: partial?.badge ?? self.component.badge ) apply(style) @@ -105,6 +126,7 @@ struct StackComponentStyle { let shape: ShapeModifier.Shape? let border: ShapeModifier.BorderInfo? let shadow: ShadowModifier.ShadowInfo? + let badge: BadgeModifier.BadgeInfo? init( visible: Bool, @@ -116,7 +138,8 @@ struct StackComponentStyle { margin: PaywallComponent.Padding, shape: PaywallComponent.Shape?, border: PaywallComponent.Border?, - shadow: PaywallComponent.Shadow? + shadow: PaywallComponent.Shadow?, + badge: PaywallComponent.Badge? ) { self.visible = visible self.dimension = dimension @@ -128,6 +151,7 @@ struct StackComponentStyle { self.shape = shape?.shape self.border = border?.border self.shadow = shadow?.shadow + self.badge = badge?.badge(stackShape: self.shape) } var vstackStrategy: StackStrategy { @@ -163,7 +187,7 @@ struct StackComponentStyle { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private extension PaywallComponent.Shape { - var shape: ShapeModifier.Shape? { + var shape: ShapeModifier.Shape { switch self { case .rectangle(let cornerRadiuses): let corners = cornerRadiuses.flatMap { cornerRadiuses in @@ -208,4 +232,27 @@ private extension PaywallComponent.Shadow { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension PaywallComponent.Badge { + + func badge(stackShape: ShapeModifier.Shape?) -> BadgeModifier.BadgeInfo? { + BadgeModifier.BadgeInfo( + style: self.style, + alignment: self.alignment, + shape: self.shape.shape, + padding: self.padding, + margin: self.margin, + textLid: self.textLid, + fontName: self.fontName, + fontWeight: self.fontWeight, + fontSize: self.fontSize, + horizontalAlignment: self.horizontalAlignment, + color: self.color, + backgroundColor: self.backgroundColor, + stackShape: stackShape + ) + } + +} + #endif diff --git a/RevenueCatUI/Templates/V2/ViewHelpers/BadgeModifier.swift b/RevenueCatUI/Templates/V2/ViewHelpers/BadgeModifier.swift new file mode 100644 index 0000000000..518a1f6688 --- /dev/null +++ b/RevenueCatUI/Templates/V2/ViewHelpers/BadgeModifier.swift @@ -0,0 +1,442 @@ +// +// 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 +// +// BadgeModifier.swift +// +// Created by Mark Villacampa 09/12/2024. + +// swiftlint:disable file_length + +import RevenueCat +import SwiftUI + +#if PAYWALL_COMPONENTS + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct BadgeModifier: ViewModifier { + + let badge: BadgeInfo? + let textComponentViewModel: TextComponentViewModel? + + struct BadgeInfo { + let style: PaywallComponent.BadgeStyle + let alignment: PaywallComponent.TwoDimensionAlignment + let shape: ShapeModifier.Shape + let padding: PaywallComponent.Padding + let margin: PaywallComponent.Padding + let textLid: String + let fontName: String? + let fontWeight: PaywallComponent.FontWeight + let fontSize: PaywallComponent.FontSize + let horizontalAlignment: PaywallComponent.HorizontalAlignment + let color: PaywallComponent.ColorScheme + let backgroundColor: PaywallComponent.ColorScheme + let stackShape: ShapeModifier.Shape? + } + + func body(content: Content) -> some View { + if let badge = badge, let textComponentViewModel = textComponentViewModel { + content.apply(badge: badge, textComponentViewModel: textComponentViewModel) + } else { + content + } + } +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +fileprivate extension View { + + @ViewBuilder + func apply(badge: BadgeModifier.BadgeInfo, textComponentViewModel: TextComponentViewModel) -> some View { + switch badge.style { + case .edgeToEdge: + self.appleBadgeEdgeToEdge(badge: badge, textComponentViewModel: textComponentViewModel) + case .overlaid: + self.overlay( + VStack(alignment: .leading) { + VStack { + TextComponentView(viewModel: textComponentViewModel) + .backgroundStyle(badge.backgroundColor.backgroundStyle) + .shape(border: nil, shape: effectiveShape(badge: badge)) + } + .fixedSize() + .padding(effectiveMargin(badge: badge).edgeInsets) + .alignmentGuide( + effetiveVerticalAlinmentForOverlaidBadge(alignment: badge.alignment.stackAlignment), + computeValue: { dim in dim[VerticalAlignment.center] }) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: badge.alignment.stackAlignment) + ) + case .nested: + self.overlay( + VStack(alignment: .leading) { + VStack { + TextComponentView(viewModel: textComponentViewModel) + .backgroundStyle(badge.backgroundColor.backgroundStyle) + .shape(border: nil, shape: effectiveShape(badge: badge)) + } + .fixedSize() + .padding(effectiveMargin(badge: badge).edgeInsets) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: badge.alignment.stackAlignment) + ) + } + } + + // Helper to apply the edge-to-edge badge style + @ViewBuilder + // swiftlint:disable:next function_body_length + private func appleBadgeEdgeToEdge( + badge: BadgeModifier.BadgeInfo, + textComponentViewModel: TextComponentViewModel) -> some View { + switch badge.alignment { + case .bottom: + self.background( + VStack(alignment: .leading) { + VStack { + TextComponentView(viewModel: textComponentViewModel) + .backgroundStyle(badge.backgroundColor.backgroundStyle) + .shape(border: nil, shape: effectiveShape(badge: badge)) + } + .alignmentGuide(.bottom) { dim in dim[VerticalAlignment.top] } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: badge.alignment.stackAlignment) + ) + .background( + VStack(alignment: .leading, spacing: 0) { + Rectangle() + .fill(Color.clear) + Rectangle() + .fill(Color.clear) + .backgroundStyle(badge.backgroundColor.backgroundStyle) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + ) + case .top: + self.background( + VStack(alignment: .leading) { + VStack { + TextComponentView(viewModel: textComponentViewModel) + .backgroundStyle(badge.backgroundColor.backgroundStyle) + .shape(border: nil, shape: effectiveShape(badge: badge)) + } + .alignmentGuide(.top) { dim in dim[VerticalAlignment.bottom] } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: badge.alignment.stackAlignment) + ) + .background( + VStack(alignment: .leading, spacing: 0) { + Rectangle() + .fill(Color.clear) + .backgroundStyle(badge.backgroundColor.backgroundStyle) + Rectangle() + .fill(Color.clear) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + ) + case .bottomLeading, .bottomTrailing, .topLeading, .topTrailing: + self.overlay( + VStack(alignment: .leading) { + VStack { + TextComponentView(viewModel: textComponentViewModel) + .backgroundStyle(badge.backgroundColor.backgroundStyle) + .shape(border: nil, shape: effectiveShape(badge: badge)) + } + .fixedSize() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: badge.alignment.stackAlignment) + ) + default: + self + } + } + + // Helper to calculate the position of an overlaid badge at the top or bottom of the stack + private func effetiveVerticalAlinmentForOverlaidBadge(alignment: Alignment) -> VerticalAlignment { + return switch alignment { + case .top, .topLeading, .topTrailing: + VerticalAlignment.top + case .bottom, .bottomLeading, .bottomTrailing: + VerticalAlignment.bottom + default: + VerticalAlignment.top + } + } + + // Helper to calculate the effective margins of a badge depending on its type: + // - Edge-to-ege: No margin allowed. + // - Overlaid: Only leading/trailing margins allowed if in the leading/trailing positions respectively. + // - Nested: Margin only allowed in the sides adjacent to the stack borders. + // swiftlint:disable:next cyclomatic_complexity + private func effectiveMargin(badge: BadgeModifier.BadgeInfo) -> PaywallComponent.Padding { + switch badge.style { + case .edgeToEdge: + return .zero + case .overlaid: + switch badge.alignment { + case .top, .bottom, .center: + return .zero + case .leading, .topLeading, .bottomLeading: + return .init(top: 0, bottom: 0, leading: badge.margin.leading, trailing: 0) + case .trailing, .topTrailing, .bottomTrailing: + return .init(top: 0, bottom: 0, leading: 0, trailing: badge.margin.trailing) + } + case .nested: + switch badge.alignment { + case .center, .leading, .trailing: + return .zero + case .top: + return .init(top: badge.margin.top, bottom: 0, leading: 0, trailing: 0) + case .bottom: + return .init(top: 0, bottom: badge.margin.bottom, leading: 0, trailing: 0) + case .topLeading: + return .init(top: badge.margin.top, bottom: 0, leading: badge.margin.leading, trailing: 0) + case .topTrailing: + return .init(top: badge.margin.top, bottom: 0, leading: 0, trailing: badge.margin.trailing) + case .bottomLeading: + return .init(top: 0, bottom: badge.margin.bottom, leading: badge.margin.leading, trailing: 0) + case .bottomTrailing: + return .init(top: 0, bottom: badge.margin.bottom, leading: 0, trailing: badge.margin.trailing) + } + } + } + + // Helper to calculate the shape of the edge-to-edge badge in trailing/leading positions. + // swiftlint:disable:next cyclomatic_complexity + private func effectiveShape(badge: BadgeModifier.BadgeInfo) -> ShapeModifier.Shape? { + switch badge.style { + case .edgeToEdge: + switch badge.shape { + case .pill, .concave, .convex: + // Edge-to-edge badge cannot have pill shape + return nil + case .rectangle(let corners): + switch badge.alignment { + case .center, .leading, .trailing: + return nil + case .top: + return .rectangle(.init( + topLeft: corners?.topLeft, + topRight: corners?.topRight, + bottomLeft: 0, + bottomRight: 0)) + case .bottom: + return .rectangle(.init( + topLeft: 0, + topRight: 0, + bottomLeft: corners?.bottomLeft, + bottomRight: corners?.bottomRight)) + case .topLeading: + return .rectangle(.init( + topLeft: radiusInfo(shape: badge.stackShape)?.topLeft, + topRight: 0, + bottomLeft: 0, + bottomRight: corners?.bottomRight)) + case .topTrailing: + return .rectangle(.init( + topLeft: 0.0, + topRight: radiusInfo(shape: badge.stackShape)?.topRight, + bottomLeft: corners?.bottomLeft, + bottomRight: 0)) + case .bottomLeading: + return .rectangle(.init( + topLeft: 0.0, + topRight: corners?.topRight, + bottomLeft: radiusInfo(shape: badge.stackShape)?.bottomLeft, + bottomRight: 0)) + case .bottomTrailing: + return .rectangle(.init( + topLeft: corners?.topLeft, + topRight: 0, + bottomLeft: 0, + bottomRight: radiusInfo(shape: badge.stackShape)?.bottomRight)) + } + } + case .nested, .overlaid: + return badge.shape + } + } + + // Helper to extract the RadiusInfo from a rectanle shape + private func radiusInfo(shape: ShapeModifier.Shape?) -> ShapeModifier.RadiusInfo? { + switch shape { + case .rectangle(let radius): + return radius + default: + return nil + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension View { + func badge(_ badge: BadgeModifier.BadgeInfo?, textComponentViewModel: TextComponentViewModel?) -> some View { + self.modifier(BadgeModifier(badge: badge, textComponentViewModel: textComponentViewModel)) + } +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@ViewBuilder +// swiftlint:disable:next function_body_length +private func badge(style: PaywallComponent.BadgeStyle, alignment: PaywallComponent.TwoDimensionAlignment) -> some View { + VStack(spacing: 16) { + Text("Standard") + .font(.title) + .fontWeight(.bold) + .foregroundColor(.black) + + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("Feature 1") + .foregroundColor(.black) + } + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("Feature 2") + .foregroundColor(.black) + } + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("Feature 3") + .foregroundColor(.black) + } + } + + Text("$9.99/month") + .font(.title) + .fontWeight(.bold) + .foregroundColor(.black) + + Text("Includes 7 Day Free Trial") + .font(.caption) + .foregroundColor(.gray) + + Text("Continue") + .fontWeight(.bold) + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(12) + + } + .padding() + .padding(.vertical, 34) + .backgroundStyle(.color(.init(light: .hex("#ffffff")))) + .shape( + border: .init(color: .blue, width: 10), + shape: .rectangle(ShapeModifier.RadiusInfo(topLeft: 12.0, topRight: 12, bottomLeft: 12, bottomRight: 12)) + ) + .compositingGroup() + .shadow(color: Color.black.opacity(0.5), radius: 4, x: 0, y: 4) + .badge( + BadgeModifier.BadgeInfo( + style: style, + alignment: alignment, + shape: .rectangle(.init(topLeft: 8.0, topRight: 8, bottomLeft: 8, bottomRight: 8)), + padding: .init(top: 4, bottom: 4, leading: 16, trailing: 16), + margin: .init(top: 10, bottom: 10, leading: 10, trailing: 10), + textLid: "id_1", + fontName: nil, + fontWeight: .bold, + fontSize: .bodyS, + horizontalAlignment: .center, + color: .init(light: .hex("#000000")), + backgroundColor: .init(light: .hex("#FA8072")), + stackShape: .rectangle(.init(topLeft: 12.0, topRight: 12, bottomLeft: 12, bottomRight: 12)) + ), + // swiftlint:disable:next force_try + textComponentViewModel: try! TextComponentViewModel( + localizationProvider: .init( + locale: Locale.current, + localizedStrings: [ + "id_1": .string("Special Discount\nSave 50%") + ] + ), + component: PaywallComponent.TextComponent( + text: "id_1", + fontName: nil, + fontWeight: .bold, + color: .init(light: .hex("#000000")), + padding: .init(top: 4, bottom: 4, leading: 16, trailing: 16), + margin: .zero, + fontSize: .bodyS, + horizontalAlignment: .center + ) + ) + + ) +} + +// As of Xcode 16, there is a limit of 15 views per PreviewProvider. +// To work around this, we can create multiple PreviewProviders with different sets of previews. + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct BadgeEdgeToEdge_Previews: PreviewProvider { + + static var previews: some View { + let alignments: [PaywallComponent.TwoDimensionAlignment] = [ + .topLeading, .top, .topTrailing, .bottomLeading, .bottom, .bottomTrailing + ] + ForEach(alignments, id: \.self) { alignment in + badge(style: .edgeToEdge, alignment: alignment) + .previewDisplayName("edgeToEdge - \(alignment)") + } + .previewLayout(.sizeThatFits) + .padding(30) + .padding(.vertical, 50) + .previewRequiredEnvironmentProperties() + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct BadgeOverlaid_Previews: PreviewProvider { + + static var previews: some View { + let alignments: [PaywallComponent.TwoDimensionAlignment] = [ + .topLeading, .top, .topTrailing, .bottomLeading, .bottom, .bottomTrailing + ] + ForEach(alignments, id: \.self) { alignment in + badge(style: .overlaid, alignment: alignment) + .previewDisplayName("overlaid - \(alignment)") + } + .previewLayout(.sizeThatFits) + .padding(30) + .padding(.vertical, 50) + .previewRequiredEnvironmentProperties() + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct BadgeNested_Previews: PreviewProvider { + + static var previews: some View { + let alignments: [PaywallComponent.TwoDimensionAlignment] = [ + .topLeading, .top, .topTrailing, .bottomLeading, .bottom, .bottomTrailing + ] + ForEach(alignments, id: \.self) { alignment in + badge(style: .nested, alignment: alignment) + .previewDisplayName("nested - \(alignment)") + } + .previewLayout(.sizeThatFits) + .padding(30) + .padding(.vertical, 50) + .previewRequiredEnvironmentProperties() + } + +} + +#endif diff --git a/RevenueCatUI/Templates/V2/ViewHelpers/Shape.swift b/RevenueCatUI/Templates/V2/ViewHelpers/Shape.swift index 96813e264f..e7c92b931e 100644 --- a/RevenueCatUI/Templates/V2/ViewHelpers/Shape.swift +++ b/RevenueCatUI/Templates/V2/ViewHelpers/Shape.swift @@ -48,13 +48,6 @@ struct ShapeModifier: ViewModifier { let bottomLeft: CGFloat? let bottomRight: CGFloat? - init(topLeft: Double? = nil, topRight: Double? = nil, bottomLeft: Double? = nil, bottomRight: Double? = nil) { - self.topLeft = topLeft.flatMap { CGFloat($0) } - self.topRight = topRight.flatMap { CGFloat($0) } - self.bottomLeft = bottomLeft.flatMap { CGFloat($0) } - self.bottomRight = bottomRight.flatMap { CGFloat($0) } - } - } var border: BorderInfo? diff --git a/RevenueCatUI/Templates/V2/ViewModelHelpers/ViewModelFactory.swift b/RevenueCatUI/Templates/V2/ViewModelHelpers/ViewModelFactory.swift index f147c93921..2fc928a0b7 100644 --- a/RevenueCatUI/Templates/V2/ViewModelHelpers/ViewModelFactory.swift +++ b/RevenueCatUI/Templates/V2/ViewModelHelpers/ViewModelFactory.swift @@ -81,7 +81,8 @@ struct ViewModelFactory { return .stack( try StackComponentViewModel(component: component, - viewModels: viewModels) + viewModels: viewModels, + localizationProvider: localizationProvider) ) case .linkButton(let component): return .linkButton( @@ -163,7 +164,8 @@ struct ViewModelFactory { return try StackComponentViewModel( component: component, - viewModels: viewModels + viewModels: viewModels, + localizationProvider: localizationProvider ) } diff --git a/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift b/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift index 785131c50f..550ee8b466 100644 --- a/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift +++ b/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift @@ -385,7 +385,7 @@ public extension PaywallComponent { } - enum TwoDimensionAlignment: String, Decodable, Sendable, Hashable, Equatable { + enum TwoDimensionAlignment: String, Codable, Sendable, Hashable, Equatable { case center case leading @@ -454,6 +454,31 @@ public extension PaywallComponent { } + enum BadgeStyle: String, Codable, Sendable, Hashable, Equatable { + + case edgeToEdge = "edge_to_edge" + case overlaid = "overlaid" + case nested = "nested" + + } + + struct Badge: Codable, Sendable, Hashable, Equatable { + + public let style: BadgeStyle + public let alignment: TwoDimensionAlignment + public let shape: Shape + public let padding: Padding + public let margin: Padding + public let textLid: String + public let fontName: String? + public let fontWeight: FontWeight + public let fontSize: FontSize + public let horizontalAlignment: HorizontalAlignment + public let color: ColorScheme + public let backgroundColor: ColorScheme + + } + } #endif diff --git a/Sources/Paywalls/Components/PaywallStackComponent.swift b/Sources/Paywalls/Components/PaywallStackComponent.swift index 3ae3d877f5..226434b119 100644 --- a/Sources/Paywalls/Components/PaywallStackComponent.swift +++ b/Sources/Paywalls/Components/PaywallStackComponent.swift @@ -31,6 +31,7 @@ public extension PaywallComponent { public let shape: Shape? public let border: Border? public let shadow: Shadow? + public let badge: Badge? public let overrides: ComponentOverrides? @@ -45,6 +46,7 @@ public extension PaywallComponent { shape: Shape? = nil, border: Border? = nil, shadow: Shadow? = nil, + badge: Badge? = nil, overrides: ComponentOverrides? = nil ) { self.components = components @@ -58,6 +60,7 @@ public extension PaywallComponent { self.shape = shape self.border = border self.shadow = shadow + self.badge = badge self.overrides = overrides } @@ -75,6 +78,7 @@ public extension PaywallComponent { public let shape: Shape? public let border: Border? public let shadow: Shadow? + public let badge: Badge? public init( visible: Bool? = true, @@ -86,7 +90,8 @@ public extension PaywallComponent { margin: Padding? = nil, shape: Shape? = nil, border: Border? = nil, - shadow: Shadow? = nil + shadow: Shadow? = nil, + badge: Badge? = nil ) { self.visible = visible self.size = size @@ -98,6 +103,7 @@ public extension PaywallComponent { self.shape = shape self.border = border self.shadow = shadow + self.badge = badge } }