From 5f3e0bc6abc6c9af44cc500f9e85350429bc7462 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Sun, 17 Aug 2025 07:16:05 -0500 Subject: [PATCH 1/7] Slot and lottie mainly work --- RevenueCat.xcodeproj/project.pbxproj | 40 ++++ .../V2/Components/ComponentsView.swift | 4 + .../Components/Slot/SlotComponentView.swift | 191 ++++++++++++++++++ .../Slot/SlotComponentViewModel.swift | 116 +++++++++++ .../SlotLottie/SlotLottieComponentView.swift | 110 ++++++++++ .../SlotLottieComponentViewModel.swift | 117 +++++++++++ .../Templates/V2/PaywallsV2View.swift | 4 + .../PaywallComponentViewModel.swift | 3 + .../ViewModelHelpers/ViewModelFactory.swift | 18 ++ .../Common/PaywallComponentBase.swift | 16 ++ .../Components/PaywallSlotComponent.swift | 112 ++++++++++ .../PaywallSlotLottieComponent.swift | 156 ++++++++++++++ .../Components/PaywallV2CacheWarming.swift | 4 + 13 files changed, 891 insertions(+) create mode 100644 RevenueCatUI/Templates/V2/Components/Slot/SlotComponentView.swift create mode 100644 RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift create mode 100644 RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentView.swift create mode 100644 RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentViewModel.swift create mode 100644 Sources/Paywalls/Components/PaywallSlotComponent.swift create mode 100644 Sources/Paywalls/Components/PaywallSlotLottieComponent.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index ba44c4b724..d18ead198d 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -47,6 +47,12 @@ 03C72FC32D349BAE00297FEC /* IconComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C72FBF2D349BAE00297FEC /* IconComponentView.swift */; }; 03C7305B2D35985900297FEC /* TextComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C7305A2D35985500297FEC /* TextComponentTests.swift */; }; 03C730F32D35F14600297FEC /* PaywallV2CacheWarming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C730F22D35F13F00297FEC /* PaywallV2CacheWarming.swift */; }; + 03E1325D2E5007F70028D9FD /* PaywallSlotComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E1325C2E5007F70028D9FD /* PaywallSlotComponent.swift */; }; + 03E132602E500A570028D9FD /* SlotComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E1325F2E500A540028D9FD /* SlotComponentView.swift */; }; + 03E132622E500A600028D9FD /* SlotComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E132612E500A5B0028D9FD /* SlotComponentViewModel.swift */; }; + 03E132D92E510B7B0028D9FD /* PaywallSlotLottieComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E132D82E510B7B0028D9FD /* PaywallSlotLottieComponent.swift */; }; + 03E132E02E510C220028D9FD /* SlotLottieComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E132DE2E510C220028D9FD /* SlotLottieComponentView.swift */; }; + 03E132E12E510C220028D9FD /* SlotLottieComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E132DF2E510C220028D9FD /* SlotLottieComponentViewModel.swift */; }; 03E37BEA2D30B32200CD9678 /* PaywallTabsComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E37BE92D30B32200CD9678 /* PaywallTabsComponent.swift */; }; 03F0B6512DD5700D00E82199 /* LocalizationDictionaryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F0B6502DD5700D00E82199 /* LocalizationDictionaryExtensions.swift */; }; 03F0B6712DD6364100E82199 /* NavigatetoURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F0B6702DD6363B00E82199 /* NavigatetoURL.swift */; }; @@ -1496,6 +1502,12 @@ 03C7305A2D35985500297FEC /* TextComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextComponentTests.swift; sourceTree = ""; }; 03C730F22D35F13F00297FEC /* PaywallV2CacheWarming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallV2CacheWarming.swift; sourceTree = ""; }; 03CCFC172DE94FF70052B764 /* Paywall-Screenshots.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Paywall-Screenshots.xctestplan"; sourceTree = ""; }; + 03E1325C2E5007F70028D9FD /* PaywallSlotComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallSlotComponent.swift; sourceTree = ""; }; + 03E1325F2E500A540028D9FD /* SlotComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlotComponentView.swift; sourceTree = ""; }; + 03E132612E500A5B0028D9FD /* SlotComponentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlotComponentViewModel.swift; sourceTree = ""; }; + 03E132D82E510B7B0028D9FD /* PaywallSlotLottieComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallSlotLottieComponent.swift; sourceTree = ""; }; + 03E132DE2E510C220028D9FD /* SlotLottieComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlotLottieComponentView.swift; sourceTree = ""; }; + 03E132DF2E510C220028D9FD /* SlotLottieComponentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlotLottieComponentViewModel.swift; sourceTree = ""; }; 03E37BE92D30B32200CD9678 /* PaywallTabsComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallTabsComponent.swift; sourceTree = ""; }; 03F0B6502DD5700D00E82199 /* LocalizationDictionaryExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationDictionaryExtensions.swift; sourceTree = ""; }; 03F0B6702DD6363B00E82199 /* NavigatetoURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatetoURL.swift; sourceTree = ""; }; @@ -2913,6 +2925,24 @@ path = __PreviewResources__; sourceTree = ""; }; + 03E1325E2E500A420028D9FD /* Slot */ = { + isa = PBXGroup; + children = ( + 03E1325F2E500A540028D9FD /* SlotComponentView.swift */, + 03E132612E500A5B0028D9FD /* SlotComponentViewModel.swift */, + ); + path = Slot; + sourceTree = ""; + }; + 03E132DD2E510C1D0028D9FD /* SlotLottie */ = { + isa = PBXGroup; + children = ( + 03E132DE2E510C220028D9FD /* SlotLottieComponentView.swift */, + 03E132DF2E510C220028D9FD /* SlotLottieComponentViewModel.swift */, + ); + path = SlotLottie; + sourceTree = ""; + }; 03F446222D2FE0B90046129A /* Properties */ = { isa = PBXGroup; children = ( @@ -2977,6 +3007,7 @@ 2C7457432CEA6470004ACE52 /* Components */ = { isa = PBXGroup; children = ( + 03E132DD2E510C1D0028D9FD /* SlotLottie */, 2C7457472CEA66AB004ACE52 /* ComponentsView.swift */, 7707A94A2CAD936A006E0313 /* Button */, 03C06FC82D479C6300600693 /* Carousel */, @@ -2987,6 +3018,7 @@ 0354AA452D4029C300F9E330 /* Tabs */, 4D3BA5B12D47AB4400668AFC /* Timeline */, 88B1BADC2C813A3C001B7EE5 /* Text */, + 03E1325E2E500A420028D9FD /* Slot */, 778360772CCA85D1000785B8 /* StickyFooter */, 778360742CCA84FA000785B8 /* Root */, ); @@ -5457,6 +5489,8 @@ 03C730F22D35F13F00297FEC /* PaywallV2CacheWarming.swift */, 2CC791612CC0493600FBE120 /* Common */, 7707A94B2CAD93AC006E0313 /* PaywallButtonComponent.swift */, + 03E1325C2E5007F70028D9FD /* PaywallSlotComponent.swift */, + 03E132D82E510B7B0028D9FD /* PaywallSlotLottieComponent.swift */, 88AD01032C740CF400AA1F2B /* PaywallImageComponent.swift */, 03C72FBD2D34949600297FEC /* PaywallIconComponent.swift */, 88E679462C7503C1007E69D5 /* PaywallStackComponent.swift */, @@ -6557,6 +6591,7 @@ FD3A85FC2DDE7532005F3C79 /* VirtualCurrencies.swift in Sources */, 4F6BEDE22A26B69500CD9322 /* DebugContentViews.swift in Sources */, B3B5FBBC269D121B00104A0C /* Offerings.swift in Sources */, + 03E1325D2E5007F70028D9FD /* PaywallSlotComponent.swift in Sources */, 9A65E03B25918B0900DE00B0 /* CustomerInfoStrings.swift in Sources */, 5721360F28B4602C006C46BE /* Purchases+nonasync.swift in Sources */, 57DE806D28074976008D6C6F /* Storefront.swift in Sources */, @@ -6758,6 +6793,7 @@ F5BE424226965F9F00254A30 /* ProductRequestData+Initialization.swift in Sources */, 2DDF41AD24F6F37C005BC22D /* ASN1ObjectIdentifier.swift in Sources */, 75B6DFFB2E30EC86009F0A99 /* GetWebOfferingProductsOperation.swift in Sources */, + 03E132D92E510B7B0028D9FD /* PaywallSlotLottieComponent.swift in Sources */, 35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */, 57536A28278522B400E2AE7F /* SK2StoreTransaction.swift in Sources */, 2D9C7BB326D838FC006838BE /* UIApplication+RCExtensions.swift in Sources */, @@ -7390,6 +7426,7 @@ 778360792CCA85E4000785B8 /* StickyFooterComponentViewModel.swift in Sources */, 2C7457482CEA66AB004ACE52 /* ComponentsView.swift in Sources */, 03C06FCA2D479C7400600693 /* CarouselComponentView.swift in Sources */, + 03E132602E500A570028D9FD /* SlotComponentView.swift in Sources */, 353FDC0F2CA446FA0055F328 /* StoreProductDiscount+Extensions.swift in Sources */, 03A98CF12D222F5F009BCA61 /* FallbackComponentPreview.swift in Sources */, 887A60862C1D037000E1A461 /* FooterHidingModifier.swift in Sources */, @@ -7430,6 +7467,7 @@ FD9BCEBC2DC946C400408A5D /* BottomSheetView.swift in Sources */, 03F0B6512DD5700D00E82199 /* LocalizationDictionaryExtensions.swift in Sources */, 7783607B2CCA88E4000785B8 /* StickyFooterComponentView.swift in Sources */, + 03E132622E500A600028D9FD /* SlotComponentViewModel.swift in Sources */, FD15AF5B2DF9A8F30062467E /* VirtualCurrencyBalanceListRow.swift in Sources */, FD15AF5C2DF9A8F30062467E /* VirtualCurrencyBalancesScreen.swift in Sources */, FD15AF5D2DF9A8F30062467E /* VirtualCurrenciesScrollViewWithOSBackgroundSection.swift in Sources */, @@ -7558,6 +7596,8 @@ 887A60BF2C1D037000E1A461 /* PaywallViewController.swift in Sources */, 2CC791552CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift in Sources */, 2CC791562CC0452100FBE120 /* PackageComponentView.swift in Sources */, + 03E132E02E510C220028D9FD /* SlotLottieComponentView.swift in Sources */, + 03E132E12E510C220028D9FD /* SlotLottieComponentViewModel.swift in Sources */, 2CC791592CC0452100FBE120 /* PurchaseButtonComponentView.swift in Sources */, 2CC7915A2CC0452100FBE120 /* PackageComponentViewModel.swift in Sources */, 887A60772C1D037000E1A461 /* TemplateViewConfiguration+Images.swift in Sources */, diff --git a/RevenueCatUI/Templates/V2/Components/ComponentsView.swift b/RevenueCatUI/Templates/V2/Components/ComponentsView.swift index a89cf2faab..6136f526e1 100644 --- a/RevenueCatUI/Templates/V2/Components/ComponentsView.swift +++ b/RevenueCatUI/Templates/V2/Components/ComponentsView.swift @@ -76,6 +76,10 @@ struct ComponentsView: View { TabControlToggleComponentView(viewModel: viewModel, onDismiss: onDismiss) case .carousel(let viewModel): CarouselComponentView(viewModel: viewModel, onDismiss: onDismiss) + case .slot(let viewModel): + SlotComponentView(viewModel: viewModel) + case .slotLottie(let viewModel): + SlotLottieComponentView(viewModel: viewModel) } } // Applies a top padding to mimmic safe area insets diff --git a/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentView.swift b/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentView.swift new file mode 100644 index 0000000000..6ff2cc7777 --- /dev/null +++ b/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentView.swift @@ -0,0 +1,191 @@ +// +// 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 +// +// SlotComponentView.swift +// +// Created by Josh Holtz on 8/15/25. + +import Foundation +import RevenueCat +import SwiftUI + +private extension PaywallComponent { + + var type: PaywallComponent.ComponentType { + switch self { + case .text: + return .text + case .image: + return .image + case .icon: + return .icon + case .stack: + return .stack + case .button: + return .button + case .package: + return .package + case .purchaseButton: + return .purchaseButton + case .stickyFooter: + return .stickyFooter + case .timeline: + return .timeline + case .tabs: + return .tabs + case .tabControl: + return .tabControl + case .tabControlButton: + return .tabControlButton + case .tabControlToggle: + return .tabControlToggle + case .carousel: + return .carousel + case .slot: + return .slot + case .slotLottie: + return .slotLottie + } + } + +} + +// Returns AnyView so we can store any SwiftUI view +public typealias ViewProvider = (String) -> AnyView +public typealias ViewOtherProvider = (PaywallComponent) -> AnyView + +public final class ViewRegistry: ObservableObject { + + public static let shared = ViewRegistry() + + private var viewCallback: ViewProvider? + + private var otherCallback: [PaywallComponent.ComponentType: ViewOtherProvider] = [:] + + internal init() {} + + // Register a view-producing callback + public func register(type: PaywallComponent.ComponentType, @ViewBuilder _ callback: @escaping (PaywallComponent) -> Content) { + + self.otherCallback[type] = { component in + AnyView(callback(component)) + } + } + + // Register a view-producing callback + public func register(@ViewBuilder _ callback: @escaping (String) -> Content) { + self.viewCallback = { id in + AnyView(callback(id)) + } + } + + // Build the view; safe fallback wrapped as AnyView + func makeView(identifier: String) -> AnyView { + viewCallback?(identifier) ?? AnyView(EmptyView()) + } + + func makeView(component: PaywallComponent) -> AnyView { + self.otherCallback[component.type]?(component) ?? AnyView(EmptyView()) + } + +} + +#if !os(macOS) && !os(tvOS) // For Paywalls V2 + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct SlotComponentView: View { + + @EnvironmentObject + private var packageContext: PackageContext + + @EnvironmentObject + private var introOfferEligibilityContext: IntroOfferEligibilityContext + + @EnvironmentObject + private var paywallPromoOfferCache: PaywallPromoOfferCache + + @Environment(\.componentViewState) + private var componentViewState + + @Environment(\.screenCondition) + private var screenCondition + + @EnvironmentObject + private var viewRegistry: ViewRegistry + + let viewModel: SlotComponentViewModel + + var body: some View { + self.viewModel.styles( + state: self.componentViewState, + condition: self.screenCondition, + isEligibleForIntroOffer: self.introOfferEligibilityContext.isEligible( + package: self.packageContext.package + ), + isEligibleForPromoOffer: self.paywallPromoOfferCache.isMostLikelyEligible( + for: self.packageContext.package + ) + ) { style in + self.viewRegistry.makeView(identifier: viewModel.identifier) + // Style the carousel + .size(style.size) + .padding(style.padding) + .padding(style.margin) + } + } + +} + +#if DEBUG + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct SlotComponentView_Previews: PreviewProvider { + + static let viewRegistry: ViewRegistry = { + let vr = ViewRegistry() + + vr.register { identifier in + Text("Preview Slot \(identifier)") + } + + vr.register(type: .slotLottie) { component in + Text("Lottie goes here") + } + + return vr + }() + + // Need to wrap in VStack otherwise preview rerenders and images won't show + static var previews: some View { + + // Default + VStack { + SlotComponentView( + // swiftlint:disable:next force_try + viewModel: try! .init( + localizationProvider: .init( + locale: Locale.current, + localizedStrings: [:] + ), + component: .init(identifier: "test_slot_1") + ) + ) + } + .environmentObject(viewRegistry) + .previewRequiredPaywallsV2Properties() + .previewLayout(.fixed(width: 100, height: 100)) + .previewDisplayName("Default") + + } +} + +#endif + + +#endif diff --git a/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift b/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift new file mode 100644 index 0000000000..016674f916 --- /dev/null +++ b/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift @@ -0,0 +1,116 @@ +// +// 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 +// +// SlotComponentViewModel.swift +// +// Created by Josh Holtz on 8/15/25. + +import RevenueCat +import SwiftUI + +#if !os(macOS) && !os(tvOS) // For Paywalls V2 + +typealias PresentedSlotPartial = PaywallComponent.PartialSlotComponent + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +class SlotComponentViewModel { + + private let localizationProvider: LocalizationProvider + private let component: PaywallComponent.SlotComponent + + private let presentedOverrides: PresentedOverrides? + + init( + localizationProvider: LocalizationProvider, + component: PaywallComponent.SlotComponent + ) throws { + self.localizationProvider = localizationProvider + self.component = component + + self.presentedOverrides = try self.component.overrides?.toPresentedOverrides { $0 } + } + + var identifier: String { + return component.identifier + } + + @ViewBuilder + func styles( + state: ComponentViewState, + condition: ScreenCondition, + isEligibleForIntroOffer: Bool, + isEligibleForPromoOffer: Bool, + @ViewBuilder apply: @escaping (SlotComponentStyle) -> some View + ) -> some View { + let partial = PresentedSlotPartial.buildPartial( + state: state, + condition: condition, + isEligibleForIntroOffer: isEligibleForIntroOffer, + isEligibleForPromoOffer: isEligibleForPromoOffer, + with: self.presentedOverrides + ) + + let style = SlotComponentStyle( + visible: partial?.visible ?? self.component.visible ?? true, + size: partial?.size ?? self.component.size, + padding: partial?.padding ?? self.component.padding, + margin: partial?.margin ?? self.component.margin + ) + + apply(style) + } + +} + + +extension PresentedSlotPartial: PresentedPartial { + + static func combine( + _ base: PaywallComponent.PartialSlotComponent?, + with other: PaywallComponent.PartialSlotComponent? + ) -> Self { + + let visible = other?.visible ?? base?.visible + let size = other?.size ?? base?.size + let padding = other?.padding ?? base?.padding + let margin = other?.margin ?? base?.margin + + return .init( + visible: visible, + size: size, + padding: padding, + margin: margin + ) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct SlotComponentStyle { + + let visible: Bool + let size: PaywallComponent.Size + let padding: EdgeInsets + let margin: EdgeInsets + + init( + visible: Bool, + size: PaywallComponent.Size?, + padding: PaywallComponent.Padding?, + margin: PaywallComponent.Padding?, + ) { + self.visible = visible + self.size = size ?? .init(width: .fit, height: .fit) + self.padding = (padding ?? .zero).edgeInsets + self.margin = (margin ?? .zero).edgeInsets + } + +} + +#endif diff --git a/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentView.swift b/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentView.swift new file mode 100644 index 0000000000..f29e3a773f --- /dev/null +++ b/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentView.swift @@ -0,0 +1,110 @@ +// +// 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 +// +// SlotComponentView.swift +// +// Created by Josh Holtz on 8/15/25. + +import Foundation +import SwiftUI + +#if !os(macOS) && !os(tvOS) // For Paywalls V2 + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct SlotLottieComponentView: View { + + @EnvironmentObject + private var packageContext: PackageContext + + @EnvironmentObject + private var introOfferEligibilityContext: IntroOfferEligibilityContext + + @EnvironmentObject + private var paywallPromoOfferCache: PaywallPromoOfferCache + + @Environment(\.componentViewState) + private var componentViewState + + @Environment(\.screenCondition) + private var screenCondition + + @EnvironmentObject + private var viewRegistry: ViewRegistry + + let viewModel: SlotLottieComponentViewModel + + var body: some View { + self.viewModel.styles( + state: self.componentViewState, + condition: self.screenCondition, + isEligibleForIntroOffer: self.introOfferEligibilityContext.isEligible( + package: self.packageContext.package + ), + isEligibleForPromoOffer: self.paywallPromoOfferCache.isMostLikelyEligible( + for: self.packageContext.package + ) + ) { style in + self.viewRegistry.makeView( + component: .slotLottie(viewModel.component) + ) + // Style the carousel + .size(style.size) + .padding(style.padding) + .padding(style.margin) + } + } + +} + +#if DEBUG + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct SlotLottieComponentView_Previews: PreviewProvider { + + static let viewRegistry: ViewRegistry = { + let vr = ViewRegistry() + + vr.register(type: .slotLottie) { component in + Text("Lottie goes here") + } + + return vr + }() + + // Need to wrap in VStack otherwise preview rerenders and images won't show + static var previews: some View { + + // Default + VStack { + SlotLottieComponentView( + // swiftlint:disable:next force_try + viewModel: try! .init( + localizationProvider: .init( + locale: Locale.current, + localizedStrings: [:] + ), + component: .init( + identifier: "", + value: .url(URL(string: "https://something.com")!) + ) + ) + ) + } + .environmentObject(viewRegistry) + .previewRequiredPaywallsV2Properties() + .previewLayout(.fixed(width: 100, height: 100)) + .previewDisplayName("Default") + + } +} + +#endif + + +#endif diff --git a/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentViewModel.swift b/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentViewModel.swift new file mode 100644 index 0000000000..7864cbceff --- /dev/null +++ b/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentViewModel.swift @@ -0,0 +1,117 @@ +// +// 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 +// +// SlotComponentViewModel.swift +// +// Created by Josh Holtz on 8/15/25. + +import RevenueCat +import SwiftUI + +#if !os(macOS) && !os(tvOS) // For Paywalls V2 + +typealias PresentedSlotLottiePartial = PaywallComponent.PartialSlotLottieComponent + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +class SlotLottieComponentViewModel { + + private let localizationProvider: LocalizationProvider + let component: PaywallComponent.SlotLottieComponent + + private let presentedOverrides: PresentedOverrides? + + init( + localizationProvider: LocalizationProvider, + component: PaywallComponent.SlotLottieComponent + ) throws { + self.localizationProvider = localizationProvider + self.component = component + + self.presentedOverrides = try self.component.overrides?.toPresentedOverrides { $0 } + } + + @ViewBuilder + func styles( + state: ComponentViewState, + condition: ScreenCondition, + isEligibleForIntroOffer: Bool, + isEligibleForPromoOffer: Bool, + @ViewBuilder apply: @escaping (SlotLottieComponentStyle) -> some View + ) -> some View { + let partial = PresentedSlotLottiePartial.buildPartial( + state: state, + condition: condition, + isEligibleForIntroOffer: isEligibleForIntroOffer, + isEligibleForPromoOffer: isEligibleForPromoOffer, + with: self.presentedOverrides + ) + + let style = SlotLottieComponentStyle( + visible: partial?.visible ?? self.component.visible ?? true, + value: partial?.value ?? self.component.value, + size: partial?.size ?? self.component.size, + padding: partial?.padding ?? self.component.padding, + margin: partial?.margin ?? self.component.margin + ) + + apply(style) + } + +} + +extension PresentedSlotLottiePartial: PresentedPartial { + + static func combine( + _ base: PaywallComponent.PartialSlotLottieComponent?, + with other: PaywallComponent.PartialSlotLottieComponent? + ) -> Self { + + let visible = other?.visible ?? base?.visible + let value = other?.value ?? base?.value + let size = other?.size ?? base?.size + let padding = other?.padding ?? base?.padding + let margin = other?.margin ?? base?.margin + + return .init( + visible: visible, + value: value, + size: size, + padding: padding, + margin: margin + ) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct SlotLottieComponentStyle { + + let visible: Bool + let value: PaywallComponent.SlotLottieComponent.Value + let size: PaywallComponent.Size + let padding: EdgeInsets + let margin: EdgeInsets + + init( + visible: Bool, + value: PaywallComponent.SlotLottieComponent.Value, + size: PaywallComponent.Size?, + padding: PaywallComponent.Padding?, + margin: PaywallComponent.Padding?, + ) { + self.visible = visible + self.value = value + self.size = size ?? .init(width: .fit, height: .fit) + self.padding = (padding ?? .zero).edgeInsets + self.margin = (margin ?? .zero).edgeInsets + } + +} + +#endif diff --git a/RevenueCatUI/Templates/V2/PaywallsV2View.swift b/RevenueCatUI/Templates/V2/PaywallsV2View.swift index 89293558a1..d172db023f 100644 --- a/RevenueCatUI/Templates/V2/PaywallsV2View.swift +++ b/RevenueCatUI/Templates/V2/PaywallsV2View.swift @@ -107,6 +107,9 @@ struct PaywallsV2View: View { @StateObject private var paywallPromoOfferCache: PaywallPromoOfferCache + @StateObject + private var viewRegistery = ViewRegistry.shared + public init( paywallComponents: Offering.PaywallComponents, offering: Offering, @@ -176,6 +179,7 @@ struct PaywallsV2View: View { .environmentObject(self.purchaseHandler) .environmentObject(self.introOfferEligibilityContext) .environmentObject(self.paywallPromoOfferCache) + .environmentObject(self.viewRegistery) .disabled(self.purchaseHandler.actionInProgress) .onAppear { self.purchaseHandler.trackPaywallImpression( diff --git a/RevenueCatUI/Templates/V2/ViewModelHelpers/PaywallComponentViewModel.swift b/RevenueCatUI/Templates/V2/ViewModelHelpers/PaywallComponentViewModel.swift index 77eec1b5c9..7150a93790 100644 --- a/RevenueCatUI/Templates/V2/ViewModelHelpers/PaywallComponentViewModel.swift +++ b/RevenueCatUI/Templates/V2/ViewModelHelpers/PaywallComponentViewModel.swift @@ -38,6 +38,9 @@ enum PaywallComponentViewModel { case carousel(CarouselComponentViewModel) + case slot(SlotComponentViewModel) + case slotLottie(SlotLottieComponentViewModel) + } #endif diff --git a/RevenueCatUI/Templates/V2/ViewModelHelpers/ViewModelFactory.swift b/RevenueCatUI/Templates/V2/ViewModelHelpers/ViewModelFactory.swift index 5c8cdc1d3b..b1d548b3f0 100644 --- a/RevenueCatUI/Templates/V2/ViewModelHelpers/ViewModelFactory.swift +++ b/RevenueCatUI/Templates/V2/ViewModelHelpers/ViewModelFactory.swift @@ -404,6 +404,20 @@ struct ViewModelFactory { pageStackViewModels: pageStackViewModels ) ) + case .slot(let slot): + return .slot( + try SlotComponentViewModel( + localizationProvider: localizationProvider, + component: slot + ) + ) + case .slotLottie(let slotLottie): + return .slotLottie( + try SlotLottieComponentViewModel( + localizationProvider: localizationProvider, + component: slotLottie + ) + ) } } @@ -518,6 +532,10 @@ struct ViewModelFactory { return nil } return self.findFullWidthImageViewIfItsTheFirst(first) + case .slot: + return nil + case .slotLottie: + return nil } } diff --git a/Sources/Paywalls/Components/Common/PaywallComponentBase.swift b/Sources/Paywalls/Components/Common/PaywallComponentBase.swift index cb4a07af04..cd4122a730 100644 --- a/Sources/Paywalls/Components/Common/PaywallComponentBase.swift +++ b/Sources/Paywalls/Components/Common/PaywallComponentBase.swift @@ -28,6 +28,9 @@ public enum PaywallComponent: Codable, Sendable, Hashable, Equatable { case carousel(CarouselComponent) + case slot(SlotComponent) + case slotLottie(SlotLottieComponent) + public enum ComponentType: String, Codable, Sendable { case text @@ -47,6 +50,9 @@ public enum PaywallComponent: Codable, Sendable, Hashable, Equatable { case carousel + case slot + case slotLottie = "lottie" + } } @@ -114,6 +120,12 @@ extension PaywallComponent { case .carousel(let component): try container.encode(ComponentType.carousel, forKey: .type) try component.encode(to: encoder) + case .slot(let component): + try container.encode(ComponentType.slot, forKey: .type) + try component.encode(to: encoder) + case .slotLottie(let component): + try container.encode(ComponentType.slotLottie, forKey: .type) + try component.encode(to: encoder) } } @@ -195,6 +207,10 @@ extension PaywallComponent { return .tabControlToggle(try TabControlToggleComponent(from: decoder)) case .carousel: return .carousel(try CarouselComponent(from: decoder)) + case .slot: + return .slot(try SlotComponent(from: decoder)) + case .slotLottie: + return .slotLottie(try SlotLottieComponent(from: decoder)) } } diff --git a/Sources/Paywalls/Components/PaywallSlotComponent.swift b/Sources/Paywalls/Components/PaywallSlotComponent.swift new file mode 100644 index 0000000000..abb981b4ad --- /dev/null +++ b/Sources/Paywalls/Components/PaywallSlotComponent.swift @@ -0,0 +1,112 @@ +// +// 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 +// +// PaywallButtonComponent.swift +// +// Created by Jay Shortway on 02/10/2024. +// +// swiftlint:disable missing_docs nesting + +import Foundation + +public extension PaywallComponent { + + final class SlotComponent: PaywallComponentBase { + + let type: ComponentType + + public let visible: Bool? + public let identifier: String + public let size: Size? + public let padding: Padding? + public let margin: Padding? + + public let overrides: ComponentOverrides? + + public init( + identifier: String, + visible: Bool? = nil, + size: PaywallComponent.Size? = nil, + padding: PaywallComponent.Padding? = .zero, + margin: PaywallComponent.Padding? = .zero, + overrides: ComponentOverrides? = nil + ) { + self.type = .slot + self.visible = visible + self.identifier = identifier + self.size = size + self.padding = padding + self.margin = margin + self.overrides = overrides + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(type) + hasher.combine(visible) + hasher.combine(identifier) + hasher.combine(size) + hasher.combine(padding) + hasher.combine(margin) + hasher.combine(overrides) + } + + public static func == (lhs: SlotComponent, rhs: SlotComponent) -> Bool { + return lhs.type == rhs.type && + lhs.visible == rhs.visible && + lhs.identifier == rhs.identifier && + lhs.size == rhs.size && + lhs.padding == rhs.padding && + lhs.margin == rhs.margin && + lhs.overrides == rhs.overrides + } + + } + + final class PartialSlotComponent: PaywallPartialComponent { + + public let visible: Bool? + public let size: Size? + public let padding: Padding? + public let margin: Padding? + + public init( + visible: Bool? = true, + size: Size? = nil, + padding: Padding? = nil, + margin: Padding? = nil, + ) { + self.visible = visible + self.size = size + self.padding = padding + self.margin = margin + } + + private enum CodingKeys: String, CodingKey { + case visible + case size + case padding + case margin + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(visible) + hasher.combine(size) + hasher.combine(padding) + hasher.combine(margin) + } + + public static func == (lhs: PartialSlotComponent, rhs: PartialSlotComponent) -> Bool { + return lhs.visible == rhs.visible && + lhs.size == rhs.size && + lhs.padding == rhs.padding && + lhs.margin == rhs.margin + } + } + +} diff --git a/Sources/Paywalls/Components/PaywallSlotLottieComponent.swift b/Sources/Paywalls/Components/PaywallSlotLottieComponent.swift new file mode 100644 index 0000000000..1dd15d6b5d --- /dev/null +++ b/Sources/Paywalls/Components/PaywallSlotLottieComponent.swift @@ -0,0 +1,156 @@ +// +// 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 +// +// PaywallButtonComponent.swift +// +// Created by Jay Shortway on 02/10/2024. +// +// swiftlint:disable missing_docs nesting + +import Foundation + +public extension PaywallComponent { + + final class SlotLottieComponent: PaywallComponentBase { + + let type: ComponentType + + public let visible: Bool? + public let value: Value + public let size: Size? + public let padding: Padding? + public let margin: Padding? + + public let overrides: ComponentOverrides? + + public init( + visible: Bool? = nil, + identifier: String, + value: Value, + size: PaywallComponent.Size? = nil, + padding: PaywallComponent.Padding? = .zero, + margin: PaywallComponent.Padding? = .zero, + overrides: ComponentOverrides? = nil + ) { + self.type = .slotLottie + self.visible = visible + self.value = value + self.size = size + self.padding = padding + self.margin = margin + self.overrides = overrides + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(type) + hasher.combine(visible) + hasher.combine(value) + hasher.combine(size) + hasher.combine(padding) + hasher.combine(margin) + hasher.combine(overrides) + } + + public static func == (lhs: SlotLottieComponent, rhs: SlotLottieComponent) -> Bool { + return lhs.type == rhs.type && + lhs.visible == rhs.visible && + lhs.value == rhs.value && + lhs.size == rhs.size && + lhs.padding == rhs.padding && + lhs.margin == rhs.margin && + lhs.overrides == rhs.overrides + } + + public enum Value: Codable, Sendable, Hashable, Equatable { + case url(URL) + + case unknown + + private enum CodingKeys: String, CodingKey { + case type + case url + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .url: + try container.encode("restore_purchases", forKey: .type) + case .unknown: + try container.encode("unknown", forKey: .type) + } + } + + 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 "url": + let url = try container.decode(URL.self, forKey: .url) + self = .url(url) + case "unknown": + self = .unknown + default: + self = .unknown + } + } + } + + } + + final class PartialSlotLottieComponent: PaywallPartialComponent { + + public let visible: Bool? + public let value: SlotLottieComponent.Value? + public let size: Size? + public let padding: Padding? + public let margin: Padding? + + public init( + visible: Bool? = true, + value: SlotLottieComponent.Value? = nil, + size: Size? = nil, + padding: Padding? = nil, + margin: Padding? = nil, + ) { + self.visible = visible + self.value = value + self.size = size + self.padding = padding + self.margin = margin + } + + private enum CodingKeys: String, CodingKey { + case visible + case value + case size + case padding + case margin + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(visible) + hasher.combine(value) + hasher.combine(size) + hasher.combine(padding) + hasher.combine(margin) + } + + public static func == (lhs: PartialSlotLottieComponent, rhs: PartialSlotLottieComponent) -> Bool { + return lhs.visible == rhs.visible && + lhs.value == rhs.value && + lhs.size == rhs.size && + lhs.padding == rhs.padding && + lhs.margin == rhs.margin + } + } + +} diff --git a/Sources/Paywalls/Components/PaywallV2CacheWarming.swift b/Sources/Paywalls/Components/PaywallV2CacheWarming.swift index 9b492cdb72..47901ac904 100644 --- a/Sources/Paywalls/Components/PaywallV2CacheWarming.swift +++ b/Sources/Paywalls/Components/PaywallV2CacheWarming.swift @@ -87,6 +87,10 @@ extension PaywallComponentsData.PaywallComponentsConfig { urls += carousel.pages.flatMap({ stack in self.collectAllImageURLs(in: stack) }) + case .slot: + break + case .slotLottie: + break } } From 3a93b7e82a292f8d9c483afc52ec3471d898838b Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Mon, 18 Aug 2025 05:41:04 -0500 Subject: [PATCH 2/7] Start of Lottie target/library thing --- Package.swift | 15 ++++++- Package@swift-5.7.swift | 14 ++++++- Package@swift-5.8.swift | 14 ++++++- RevenueCat.xcodeproj/project.pbxproj | 8 ++++ RevenueCatUILottie.podspec | 44 +++++++++++++++++++++ RevenueCatUILottie/RevenueCatUILottie.swift | 10 +++++ 6 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 RevenueCatUILottie.podspec create mode 100644 RevenueCatUILottie/RevenueCatUILottie.swift diff --git a/Package.swift b/Package.swift index 6070802bd0..04b72ff8d5 100644 --- a/Package.swift +++ b/Package.swift @@ -42,7 +42,8 @@ var dependencies: [Package.Dependency] = [ .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", revision: "26ed3a2b4a2df47917ca9b790a57f91285b923fb" - ) + ), + .package(url: "https://github.com/airbnb/lottie-spm", from: "4.0.0") ] if shouldIncludeDocCPlugin { // Versions 1.4.0 and 1.4.1 are failing to compile, so we are pinning it to 1.3.0 for now @@ -75,7 +76,9 @@ let package = Package( .library(name: "ReceiptParser", targets: ["ReceiptParser"]), .library(name: "RevenueCatUI", - targets: ["RevenueCatUI"]) + targets: ["RevenueCatUI"]), + .library(name: "RevenueCatUILottie", + targets: ["RevenueCatUILottie"]) ], dependencies: dependencies, targets: [ @@ -115,6 +118,14 @@ let package = Package( .process("Resources/icons.xcassets") ], swiftSettings: ciCompilerFlags + additionalCompilerFlags), + // RevenueCatUILottie + .target(name: "RevenueCatUILottie", + dependencies: [ + "RevenueCatUI", + .product(name: "Lottie", package: "lottie-spm") + ], + path: "RevenueCatUILottie", + swiftSettings: ciCompilerFlags + additionalCompilerFlags), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index 19f4471cca..402af26577 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -15,7 +15,8 @@ var dependencies: [Package.Dependency] = [ .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", revision: "26ed3a2b4a2df47917ca9b790a57f91285b923fb" - ) + ), + .package(url: "https://github.com/airbnb/lottie-spm", from: "4.0.0") ] if shouldIncludeDocCPlugin { // Versions 1.4.0 and 1.4.1 are failing to compile, so we are pinning it to 1.3.0 for now @@ -43,7 +44,9 @@ let package = Package( .library(name: "ReceiptParser", targets: ["ReceiptParser"]), .library(name: "RevenueCatUI", - targets: ["RevenueCatUI"]) + targets: ["RevenueCatUI"]), + .library(name: "RevenueCatUILottie", + targets: ["RevenueCatUILottie"]) ], dependencies: dependencies, targets: [ @@ -77,6 +80,13 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ]), + // RevenueCatUILottie + .target(name: "RevenueCatUILottie", + dependencies: [ + "RevenueCatUI", + .product(name: "Lottie", package: "lottie-spm") + ], + path: "RevenueCatUILottie"), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift index 5b6e3c151e..35bef688bc 100644 --- a/Package@swift-5.8.swift +++ b/Package@swift-5.8.swift @@ -15,7 +15,8 @@ var dependencies: [Package.Dependency] = [ .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", revision: "26ed3a2b4a2df47917ca9b790a57f91285b923fb" - ) + ), + .package(url: "https://github.com/airbnb/lottie-spm", from: "4.0.0") ] if shouldIncludeDocCPlugin { // Versions 1.4.0 and 1.4.1 are failing to compile, so we are pinning it to 1.3.0 for now @@ -43,7 +44,9 @@ let package = Package( .library(name: "ReceiptParser", targets: ["ReceiptParser"]), .library(name: "RevenueCatUI", - targets: ["RevenueCatUI"]) + targets: ["RevenueCatUI"]), + .library(name: "RevenueCatUILottie", + targets: ["RevenueCatUILottie"]) ], dependencies: dependencies, targets: [ @@ -77,6 +80,13 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ]), + // RevenueCatUILottie + .target(name: "RevenueCatUILottie", + dependencies: [ + "RevenueCatUI", + .product(name: "Lottie", package: "lottie-spm") + ], + path: "RevenueCatUILottie"), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index d18ead198d..2591827b54 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -2943,6 +2943,13 @@ path = SlotLottie; sourceTree = ""; }; + 03E133542E5331990028D9FD /* RevenueCatUILottie */ = { + isa = PBXGroup; + children = ( + ); + path = RevenueCatUILottie; + sourceTree = ""; + }; 03F446222D2FE0B90046129A /* Properties */ = { isa = PBXGroup; children = ( @@ -3985,6 +3992,7 @@ 7707A93B2CAD8AA2006E0313 /* Local.xcconfig */, 77607FD72CAD87D60066C23C /* Global.xcconfig */, 887A60652C1D037000E1A461 /* RevenueCatUI */, + 03E133542E5331990028D9FD /* RevenueCatUILottie */, 2DC5621724EC63420031F69B /* Sources */, 2DDE870027E5238500D8B390 /* Tests */, 887A5FBB2C1D036200E1A461 /* RevenueCatUIDev */, diff --git a/RevenueCatUILottie.podspec b/RevenueCatUILottie.podspec new file mode 100644 index 0000000000..23d74b715b --- /dev/null +++ b/RevenueCatUILottie.podspec @@ -0,0 +1,44 @@ +Pod::Spec.new do |s| + s.name = "RevenueCatUILottie" + s.version = "5.36.0-SNAPSHOT" + s.summary = "UI library for RevenueCat paywalls." + + s.description = <<-DESC + Save yourself the hassle of implementing a subscriptions backend. Use RevenueCat instead https://www.revenuecat.com/ + DESC + + s.homepage = "https://www.revenuecat.com/" + s.license = { :type => 'MIT' } + s.author = { "RevenueCat, Inc." => "support@revenuecat.com" } + s.source = { :git => "https://github.com/revenuecat/purchases-ios.git", :tag => s.version.to_s } + s.documentation_url = "https://docs.revenuecat.com/" + + s.framework = 'SwiftUI' + s.swift_version = '5.7' + + # RevenueCatUI APIs are not available in all these platforms / versions, however retaining this support at the Pod level + # allows us to depend on it in the same platforms as RevenueCat. + # Opening support allows us to depend on it in the same platforms as RevenueCat. + s.ios.deployment_target = '13.0' + s.watchos.deployment_target = '6.2' + s.tvos.deployment_target = '13.0' + s.osx.deployment_target = '10.15' + s.visionos.deployment_target = '1.0' + + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + + s.source_files = 'RevenueCatUI/**/*.swift' + + s.dependency 'RevenueCat', s.version.to_s + + s.resource_bundles = { + 'RevenueCat_RevenueCatUI' => [ + # This is done automatically by SPM but must be added manually here: + 'RevenueCatUI/Resources/*.lproj/*.strings', + # Note: these have to match the values in Package.swift + 'RevenueCatUI/Resources/background.jpg', + 'RevenueCatUI/Resources/icons.xcassets', + ] + } + +end diff --git a/RevenueCatUILottie/RevenueCatUILottie.swift b/RevenueCatUILottie/RevenueCatUILottie.swift new file mode 100644 index 0000000000..99b1db0793 --- /dev/null +++ b/RevenueCatUILottie/RevenueCatUILottie.swift @@ -0,0 +1,10 @@ +// +// RevenueCatUILottie.swift +// RevenueCat +// +// Created by Josh Holtz on 8/18/25. +// + +public struct RevenueCatUILottie { + +} From 59c8f39dc90356397f4d2c1e6faacb80937c9aa4 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Mon, 18 Aug 2025 09:10:18 -0500 Subject: [PATCH 3/7] This is working --- Package.swift | 15 +- Package@swift-5.7.swift | 14 +- Package@swift-5.8.swift | 14 +- RevenueCat.xcodeproj/project.pbxproj | 4 + .../Components/Slot/SlotComponentView.swift | 93 +----------- .../Slot/SlotComponentViewModel.swift | 1 - .../SlotLottie/SlotLottieComponentView.swift | 9 +- RevenueCatUI/Templates/V2/ViewRegistry.swift | 143 ++++++++++++++++++ RevenueCatUILottie/RevenueCatUILottie.swift | 10 -- .../Common/PaywallComponentBase.swift | 2 +- 10 files changed, 161 insertions(+), 144 deletions(-) create mode 100644 RevenueCatUI/Templates/V2/ViewRegistry.swift delete mode 100644 RevenueCatUILottie/RevenueCatUILottie.swift diff --git a/Package.swift b/Package.swift index 04b72ff8d5..6070802bd0 100644 --- a/Package.swift +++ b/Package.swift @@ -42,8 +42,7 @@ var dependencies: [Package.Dependency] = [ .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", revision: "26ed3a2b4a2df47917ca9b790a57f91285b923fb" - ), - .package(url: "https://github.com/airbnb/lottie-spm", from: "4.0.0") + ) ] if shouldIncludeDocCPlugin { // Versions 1.4.0 and 1.4.1 are failing to compile, so we are pinning it to 1.3.0 for now @@ -76,9 +75,7 @@ let package = Package( .library(name: "ReceiptParser", targets: ["ReceiptParser"]), .library(name: "RevenueCatUI", - targets: ["RevenueCatUI"]), - .library(name: "RevenueCatUILottie", - targets: ["RevenueCatUILottie"]) + targets: ["RevenueCatUI"]) ], dependencies: dependencies, targets: [ @@ -118,14 +115,6 @@ let package = Package( .process("Resources/icons.xcassets") ], swiftSettings: ciCompilerFlags + additionalCompilerFlags), - // RevenueCatUILottie - .target(name: "RevenueCatUILottie", - dependencies: [ - "RevenueCatUI", - .product(name: "Lottie", package: "lottie-spm") - ], - path: "RevenueCatUILottie", - swiftSettings: ciCompilerFlags + additionalCompilerFlags), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index 402af26577..19f4471cca 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -15,8 +15,7 @@ var dependencies: [Package.Dependency] = [ .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", revision: "26ed3a2b4a2df47917ca9b790a57f91285b923fb" - ), - .package(url: "https://github.com/airbnb/lottie-spm", from: "4.0.0") + ) ] if shouldIncludeDocCPlugin { // Versions 1.4.0 and 1.4.1 are failing to compile, so we are pinning it to 1.3.0 for now @@ -44,9 +43,7 @@ let package = Package( .library(name: "ReceiptParser", targets: ["ReceiptParser"]), .library(name: "RevenueCatUI", - targets: ["RevenueCatUI"]), - .library(name: "RevenueCatUILottie", - targets: ["RevenueCatUILottie"]) + targets: ["RevenueCatUI"]) ], dependencies: dependencies, targets: [ @@ -80,13 +77,6 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ]), - // RevenueCatUILottie - .target(name: "RevenueCatUILottie", - dependencies: [ - "RevenueCatUI", - .product(name: "Lottie", package: "lottie-spm") - ], - path: "RevenueCatUILottie"), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift index 35bef688bc..5b6e3c151e 100644 --- a/Package@swift-5.8.swift +++ b/Package@swift-5.8.swift @@ -15,8 +15,7 @@ var dependencies: [Package.Dependency] = [ .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", revision: "26ed3a2b4a2df47917ca9b790a57f91285b923fb" - ), - .package(url: "https://github.com/airbnb/lottie-spm", from: "4.0.0") + ) ] if shouldIncludeDocCPlugin { // Versions 1.4.0 and 1.4.1 are failing to compile, so we are pinning it to 1.3.0 for now @@ -44,9 +43,7 @@ let package = Package( .library(name: "ReceiptParser", targets: ["ReceiptParser"]), .library(name: "RevenueCatUI", - targets: ["RevenueCatUI"]), - .library(name: "RevenueCatUILottie", - targets: ["RevenueCatUILottie"]) + targets: ["RevenueCatUI"]) ], dependencies: dependencies, targets: [ @@ -80,13 +77,6 @@ let package = Package( .copy("Resources/background.jpg"), .process("Resources/icons.xcassets") ]), - // RevenueCatUILottie - .target(name: "RevenueCatUILottie", - dependencies: [ - "RevenueCatUI", - .product(name: "Lottie", package: "lottie-spm") - ], - path: "RevenueCatUILottie"), .testTarget(name: "RevenueCatUITests", dependencies: [ "RevenueCatUI", diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 2591827b54..379714a7aa 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 03E132D92E510B7B0028D9FD /* PaywallSlotLottieComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E132D82E510B7B0028D9FD /* PaywallSlotLottieComponent.swift */; }; 03E132E02E510C220028D9FD /* SlotLottieComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E132DE2E510C220028D9FD /* SlotLottieComponentView.swift */; }; 03E132E12E510C220028D9FD /* SlotLottieComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E132DF2E510C220028D9FD /* SlotLottieComponentViewModel.swift */; }; + 03E133DA2E5343390028D9FD /* ViewRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E133D92E5343390028D9FD /* ViewRegistry.swift */; }; 03E37BEA2D30B32200CD9678 /* PaywallTabsComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E37BE92D30B32200CD9678 /* PaywallTabsComponent.swift */; }; 03F0B6512DD5700D00E82199 /* LocalizationDictionaryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F0B6502DD5700D00E82199 /* LocalizationDictionaryExtensions.swift */; }; 03F0B6712DD6364100E82199 /* NavigatetoURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F0B6702DD6363B00E82199 /* NavigatetoURL.swift */; }; @@ -1508,6 +1509,7 @@ 03E132D82E510B7B0028D9FD /* PaywallSlotLottieComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallSlotLottieComponent.swift; sourceTree = ""; }; 03E132DE2E510C220028D9FD /* SlotLottieComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlotLottieComponentView.swift; sourceTree = ""; }; 03E132DF2E510C220028D9FD /* SlotLottieComponentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlotLottieComponentViewModel.swift; sourceTree = ""; }; + 03E133D92E5343390028D9FD /* ViewRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewRegistry.swift; sourceTree = ""; }; 03E37BE92D30B32200CD9678 /* PaywallTabsComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallTabsComponent.swift; sourceTree = ""; }; 03F0B6502DD5700D00E82199 /* LocalizationDictionaryExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationDictionaryExtensions.swift; sourceTree = ""; }; 03F0B6702DD6363B00E82199 /* NavigatetoURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatetoURL.swift; sourceTree = ""; }; @@ -5516,6 +5518,7 @@ 88AD01352C74196600AA1F2B /* V2 */ = { isa = PBXGroup; children = ( + 03E133D92E5343390028D9FD /* ViewRegistry.swift */, 88B1BAE32C813A3C001B7EE5 /* PaywallsV2View.swift */, 2C7457492CEA6B06004ACE52 /* EnvironmentObjects */, 030F91882D55C0FF0085103F /* Localizations */, @@ -7585,6 +7588,7 @@ 5798C96F2DF1985700F44400 /* PurchaseDetailView.swift in Sources */, 5798C9702DF1985700F44400 /* SubscriptionDetailViewModel.swift in Sources */, 5798C9712DF1985700F44400 /* PurchaseInformation+History.swift in Sources */, + 03E133DA2E5343390028D9FD /* ViewRegistry.swift in Sources */, 5798C9722DF1985700F44400 /* DiscountsHandler.swift in Sources */, 75D7048B2DFC33A900F02803 /* Purchases+RevenueCatUI.swift in Sources */, 5798C9732DF1985700F44400 /* PurchaseHistoryViewModel.swift in Sources */, diff --git a/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentView.swift b/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentView.swift index 6ff2cc7777..2731ac9e5f 100644 --- a/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentView.swift +++ b/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentView.swift @@ -15,87 +15,6 @@ import Foundation import RevenueCat import SwiftUI -private extension PaywallComponent { - - var type: PaywallComponent.ComponentType { - switch self { - case .text: - return .text - case .image: - return .image - case .icon: - return .icon - case .stack: - return .stack - case .button: - return .button - case .package: - return .package - case .purchaseButton: - return .purchaseButton - case .stickyFooter: - return .stickyFooter - case .timeline: - return .timeline - case .tabs: - return .tabs - case .tabControl: - return .tabControl - case .tabControlButton: - return .tabControlButton - case .tabControlToggle: - return .tabControlToggle - case .carousel: - return .carousel - case .slot: - return .slot - case .slotLottie: - return .slotLottie - } - } - -} - -// Returns AnyView so we can store any SwiftUI view -public typealias ViewProvider = (String) -> AnyView -public typealias ViewOtherProvider = (PaywallComponent) -> AnyView - -public final class ViewRegistry: ObservableObject { - - public static let shared = ViewRegistry() - - private var viewCallback: ViewProvider? - - private var otherCallback: [PaywallComponent.ComponentType: ViewOtherProvider] = [:] - - internal init() {} - - // Register a view-producing callback - public func register(type: PaywallComponent.ComponentType, @ViewBuilder _ callback: @escaping (PaywallComponent) -> Content) { - - self.otherCallback[type] = { component in - AnyView(callback(component)) - } - } - - // Register a view-producing callback - public func register(@ViewBuilder _ callback: @escaping (String) -> Content) { - self.viewCallback = { id in - AnyView(callback(id)) - } - } - - // Build the view; safe fallback wrapped as AnyView - func makeView(identifier: String) -> AnyView { - viewCallback?(identifier) ?? AnyView(EmptyView()) - } - - func makeView(component: PaywallComponent) -> AnyView { - self.otherCallback[component.type]?(component) ?? AnyView(EmptyView()) - } - -} - #if !os(macOS) && !os(tvOS) // For Paywalls V2 @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @@ -148,17 +67,14 @@ struct SlotComponentView: View { struct SlotComponentView_Previews: PreviewProvider { static let viewRegistry: ViewRegistry = { - let vr = ViewRegistry() - - vr.register { identifier in + let viewRegistry = ViewRegistry() + viewRegistry.register { identifier in Text("Preview Slot \(identifier)") } - - vr.register(type: .slotLottie) { component in + viewRegistry.register(type: .slotLottie) { _ in Text("Lottie goes here") } - - return vr + return viewRegistry }() // Need to wrap in VStack otherwise preview rerenders and images won't show @@ -187,5 +103,4 @@ struct SlotComponentView_Previews: PreviewProvider { #endif - #endif diff --git a/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift b/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift index 016674f916..2832c1fa57 100644 --- a/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift +++ b/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift @@ -68,7 +68,6 @@ class SlotComponentViewModel { } - extension PresentedSlotPartial: PresentedPartial { static func combine( diff --git a/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentView.swift b/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentView.swift index f29e3a773f..c631a549fe 100644 --- a/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentView.swift +++ b/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentView.swift @@ -68,13 +68,11 @@ struct SlotLottieComponentView: View { struct SlotLottieComponentView_Previews: PreviewProvider { static let viewRegistry: ViewRegistry = { - let vr = ViewRegistry() - - vr.register(type: .slotLottie) { component in + let viewRegister = ViewRegistry() + viewRegister.register(type: .slotLottie) { _ in Text("Lottie goes here") } - - return vr + return viewRegister }() // Need to wrap in VStack otherwise preview rerenders and images won't show @@ -106,5 +104,4 @@ struct SlotLottieComponentView_Previews: PreviewProvider { #endif - #endif diff --git a/RevenueCatUI/Templates/V2/ViewRegistry.swift b/RevenueCatUI/Templates/V2/ViewRegistry.swift new file mode 100644 index 0000000000..011803159a --- /dev/null +++ b/RevenueCatUI/Templates/V2/ViewRegistry.swift @@ -0,0 +1,143 @@ +// +// 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 +// +// ViewRegistry.swift +// +// Created by Josh Holtz on 8/18/25. + +import Combine +import RevenueCat +import SwiftUI + +/// A plugin that can register custom UI components with PurchasesUI. +public protocol PurchasesUIPlugin: Equatable { + /// Unique identifier for this plugin. + var id: String { get } + + /// Register the plugin's custom UI components. + func register() +} + +/// Main entry point for registering custom UI components and plugins with RevenueCatUI. +public enum PurchasesUI { + + /// Register a custom view for a specific paywall component type (Internal SPI). + @_spi(Internal) public static func register( + type: PaywallComponent.ComponentType, + @ViewBuilder _ callback: @escaping (PaywallComponent) -> Content + ) { + ViewRegistry.shared.register(type: type, callback) + } + + /// Register multiple plugins at once. + /// ```swift + /// PurchasesUI.register([PurchasesUI.Lottie]) + /// ``` + public static func register(_ plugins: [any PurchasesUIPlugin]) { + plugins.forEach { $0.register() } + } + + /// Register a single plugin. + /// ```swift + /// PurchasesUI.register(PurchasesUI.Lottie) + /// ``` + public static func register(_ plugin: any PurchasesUIPlugin) { + plugin.register() + } + + /// Register a custom view builder for slot components (Internal SPI). + @_spi(Internal) public static func register(@ViewBuilder _ callback: @escaping (String) -> Content) { + ViewRegistry.shared.register(callback) + } + +} + +// Returns AnyView so we can store any SwiftUI view +private typealias ViewProvider = (String) -> AnyView +private typealias ViewOtherProvider = (PaywallComponent) -> AnyView + +final class ViewRegistry: ObservableObject { + + static let shared = ViewRegistry() + + private var viewCallback: ViewProvider? + + private var otherCallback: [PaywallComponent.ComponentType: ViewOtherProvider] = [:] + + internal init() {} + + // Register a view-producing callback + func register( + type: PaywallComponent.ComponentType, + @ViewBuilder _ callback: @escaping (PaywallComponent) -> Content + ) { + + self.otherCallback[type] = { component in + AnyView(callback(component)) + } + } + + // Register a view-producing callback + func register(@ViewBuilder _ callback: @escaping (String) -> Content) { + self.viewCallback = { id in + AnyView(callback(id)) + } + } + + // Build the view; safe fallback wrapped as AnyView + func makeView(identifier: String) -> AnyView { + viewCallback?(identifier) ?? AnyView(EmptyView()) + } + + func makeView(component: PaywallComponent) -> AnyView { + self.otherCallback[component.type]?(component) ?? AnyView(EmptyView()) + } + +} + +private extension PaywallComponent { + + var type: PaywallComponent.ComponentType { + switch self { + case .text: + return .text + case .image: + return .image + case .icon: + return .icon + case .stack: + return .stack + case .button: + return .button + case .package: + return .package + case .purchaseButton: + return .purchaseButton + case .stickyFooter: + return .stickyFooter + case .timeline: + return .timeline + case .tabs: + return .tabs + case .tabControl: + return .tabControl + case .tabControlButton: + return .tabControlButton + case .tabControlToggle: + return .tabControlToggle + case .carousel: + return .carousel + case .slot: + return .slot + case .slotLottie: + return .slotLottie + } + } + +} diff --git a/RevenueCatUILottie/RevenueCatUILottie.swift b/RevenueCatUILottie/RevenueCatUILottie.swift deleted file mode 100644 index 99b1db0793..0000000000 --- a/RevenueCatUILottie/RevenueCatUILottie.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// RevenueCatUILottie.swift -// RevenueCat -// -// Created by Josh Holtz on 8/18/25. -// - -public struct RevenueCatUILottie { - -} diff --git a/Sources/Paywalls/Components/Common/PaywallComponentBase.swift b/Sources/Paywalls/Components/Common/PaywallComponentBase.swift index cd4122a730..289ad28165 100644 --- a/Sources/Paywalls/Components/Common/PaywallComponentBase.swift +++ b/Sources/Paywalls/Components/Common/PaywallComponentBase.swift @@ -73,7 +73,7 @@ extension PaywallComponent { } - // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) From 104cbe6e7030e1efc8dd6a4e19809245491b2425 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Mon, 18 Aug 2025 09:15:05 -0500 Subject: [PATCH 4/7] Remove id cause not needed --- RevenueCatUI/Templates/V2/ViewRegistry.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/RevenueCatUI/Templates/V2/ViewRegistry.swift b/RevenueCatUI/Templates/V2/ViewRegistry.swift index 011803159a..3d0ffaf69c 100644 --- a/RevenueCatUI/Templates/V2/ViewRegistry.swift +++ b/RevenueCatUI/Templates/V2/ViewRegistry.swift @@ -17,9 +17,6 @@ import SwiftUI /// A plugin that can register custom UI components with PurchasesUI. public protocol PurchasesUIPlugin: Equatable { - /// Unique identifier for this plugin. - var id: String { get } - /// Register the plugin's custom UI components. func register() } From 049b36e5b3571db18c4ed831612e033ce4de2af9 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Mon, 18 Aug 2025 09:30:12 -0500 Subject: [PATCH 5/7] Remove extra podspec --- RevenueCatUILottie.podspec | 44 -------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 RevenueCatUILottie.podspec diff --git a/RevenueCatUILottie.podspec b/RevenueCatUILottie.podspec deleted file mode 100644 index 23d74b715b..0000000000 --- a/RevenueCatUILottie.podspec +++ /dev/null @@ -1,44 +0,0 @@ -Pod::Spec.new do |s| - s.name = "RevenueCatUILottie" - s.version = "5.36.0-SNAPSHOT" - s.summary = "UI library for RevenueCat paywalls." - - s.description = <<-DESC - Save yourself the hassle of implementing a subscriptions backend. Use RevenueCat instead https://www.revenuecat.com/ - DESC - - s.homepage = "https://www.revenuecat.com/" - s.license = { :type => 'MIT' } - s.author = { "RevenueCat, Inc." => "support@revenuecat.com" } - s.source = { :git => "https://github.com/revenuecat/purchases-ios.git", :tag => s.version.to_s } - s.documentation_url = "https://docs.revenuecat.com/" - - s.framework = 'SwiftUI' - s.swift_version = '5.7' - - # RevenueCatUI APIs are not available in all these platforms / versions, however retaining this support at the Pod level - # allows us to depend on it in the same platforms as RevenueCat. - # Opening support allows us to depend on it in the same platforms as RevenueCat. - s.ios.deployment_target = '13.0' - s.watchos.deployment_target = '6.2' - s.tvos.deployment_target = '13.0' - s.osx.deployment_target = '10.15' - s.visionos.deployment_target = '1.0' - - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - - s.source_files = 'RevenueCatUI/**/*.swift' - - s.dependency 'RevenueCat', s.version.to_s - - s.resource_bundles = { - 'RevenueCat_RevenueCatUI' => [ - # This is done automatically by SPM but must be added manually here: - 'RevenueCatUI/Resources/*.lproj/*.strings', - # Note: these have to match the values in Package.swift - 'RevenueCatUI/Resources/background.jpg', - 'RevenueCatUI/Resources/icons.xcassets', - ] - } - -end From ab0108e6bbb2edd320576a17953d3d3af8efb163 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Mon, 18 Aug 2025 09:32:20 -0500 Subject: [PATCH 6/7] Trailing comma --- Sources/Paywalls/Components/PaywallSlotComponent.swift | 2 +- Sources/Paywalls/Components/PaywallSlotLottieComponent.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Paywalls/Components/PaywallSlotComponent.swift b/Sources/Paywalls/Components/PaywallSlotComponent.swift index abb981b4ad..40b22545b4 100644 --- a/Sources/Paywalls/Components/PaywallSlotComponent.swift +++ b/Sources/Paywalls/Components/PaywallSlotComponent.swift @@ -79,7 +79,7 @@ public extension PaywallComponent { visible: Bool? = true, size: Size? = nil, padding: Padding? = nil, - margin: Padding? = nil, + margin: Padding? = nil ) { self.visible = visible self.size = size diff --git a/Sources/Paywalls/Components/PaywallSlotLottieComponent.swift b/Sources/Paywalls/Components/PaywallSlotLottieComponent.swift index 1dd15d6b5d..03d31bc217 100644 --- a/Sources/Paywalls/Components/PaywallSlotLottieComponent.swift +++ b/Sources/Paywalls/Components/PaywallSlotLottieComponent.swift @@ -119,7 +119,7 @@ public extension PaywallComponent { value: SlotLottieComponent.Value? = nil, size: Size? = nil, padding: Padding? = nil, - margin: Padding? = nil, + margin: Padding? = nil ) { self.visible = visible self.value = value From d62f6b961c456a38aaafbe5fbf9623d696f4dec8 Mon Sep 17 00:00:00 2001 From: Josh Holtz Date: Mon, 18 Aug 2025 09:34:59 -0500 Subject: [PATCH 7/7] more trailing comma --- .../Templates/V2/Components/Slot/SlotComponentViewModel.swift | 2 +- .../V2/Components/SlotLottie/SlotLottieComponentViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift b/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift index 2832c1fa57..5fb35e2d6f 100644 --- a/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift +++ b/RevenueCatUI/Templates/V2/Components/Slot/SlotComponentViewModel.swift @@ -102,7 +102,7 @@ struct SlotComponentStyle { visible: Bool, size: PaywallComponent.Size?, padding: PaywallComponent.Padding?, - margin: PaywallComponent.Padding?, + margin: PaywallComponent.Padding? ) { self.visible = visible self.size = size ?? .init(width: .fit, height: .fit) diff --git a/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentViewModel.swift b/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentViewModel.swift index 7864cbceff..5bd4caa27a 100644 --- a/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentViewModel.swift +++ b/RevenueCatUI/Templates/V2/Components/SlotLottie/SlotLottieComponentViewModel.swift @@ -103,7 +103,7 @@ struct SlotLottieComponentStyle { value: PaywallComponent.SlotLottieComponent.Value, size: PaywallComponent.Size?, padding: PaywallComponent.Padding?, - margin: PaywallComponent.Padding?, + margin: PaywallComponent.Padding? ) { self.visible = visible self.value = value