Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Paywalls V2] Allow for app specific component overrides #4652

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
03A98D322D2441B8009BCA61 /* PaywallDataDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A98D312D2441B2009BCA61 /* PaywallDataDecodingTests.swift */; };
03A98D362D244329009BCA61 /* UIConfigDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A98D352D244321009BCA61 /* UIConfigDecodingTests.swift */; };
03A98D382D2AC63B009BCA61 /* UIConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A98D372D2AC637009BCA61 /* UIConfigProvider.swift */; };
03C72FAD2D33F73C00297FEC /* PresentedPartialTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C72FAC2D33F73900297FEC /* PresentedPartialTests.swift */; };
03F446212D2F73240046129A /* StackComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F446202D2F73210046129A /* StackComponentTests.swift */; };
03F446242D2FE0C50046129A /* ShapePropertyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F446232D2FE0C10046129A /* ShapePropertyTests.swift */; };
03F446262D2FE1510046129A /* MaskShapePropertyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F446252D2FE1510046129A /* MaskShapePropertyTests.swift */; };
Expand Down Expand Up @@ -1253,6 +1254,7 @@
03A98D312D2441B2009BCA61 /* PaywallDataDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallDataDecodingTests.swift; sourceTree = "<group>"; };
03A98D352D244321009BCA61 /* UIConfigDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConfigDecodingTests.swift; sourceTree = "<group>"; };
03A98D372D2AC637009BCA61 /* UIConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConfigProvider.swift; sourceTree = "<group>"; };
03C72FAC2D33F73900297FEC /* PresentedPartialTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentedPartialTests.swift; sourceTree = "<group>"; };
03F446202D2F73210046129A /* StackComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackComponentTests.swift; sourceTree = "<group>"; };
03F446232D2FE0C10046129A /* ShapePropertyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapePropertyTests.swift; sourceTree = "<group>"; };
03F446252D2FE1510046129A /* MaskShapePropertyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskShapePropertyTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2467,6 +2469,7 @@
030890822D2B77DD0069677B /* PaywallsV2 */ = {
isa = PBXGroup;
children = (
03C72FAC2D33F73900297FEC /* PresentedPartialTests.swift */,
030890832D2B77E20069677B /* VariableHandlerV2Tests.swift */,
);
path = PaywallsV2;
Expand Down Expand Up @@ -6742,6 +6745,7 @@
887A633C2C1D177800E1A461 /* Template1ViewTests.swift in Sources */,
35A99C842CCB95A70074AB41 /* PurchaseInformationTests.swift in Sources */,
777FB4882C661C0600CD4749 /* SemanticVersionTests.swift in Sources */,
03C72FAD2D33F73C00297FEC /* PresentedPartialTests.swift in Sources */,
88AD4C482C24E8EA00943C3E /* ExternalPurchaseAndRestoreTests.swift in Sources */,
887A633D2C1D177800E1A461 /* Template2ViewTests.swift in Sources */,
887A633E2C1D177800E1A461 /* Template3ViewTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,23 +313,58 @@ struct TextComponentView_Previews: PreviewProvider {
.previewLayout(.sizeThatFits)
.previewDisplayName("Customizations")

// State - App Specific
TextComponentView(
// swiftlint:disable:next force_try
viewModel: try! .init(
localizationProvider: .init(
locale: Locale.current,
localizedStrings: [
"id_1": .string("Hello, world"),
"id_2": .string("Hello, world on iOS app")
]
),
uiConfigProvider: .init(uiConfig: PreviewUIConfig.make()),
component: .init(
text: "id_1",
color: .init(light: .hex("#000000")),
overrides: .init(
app: .init(
text: "id_2"
)
)
)
)
)
.previewRequiredEnvironmentProperties()
.previewLayout(.sizeThatFits)
.previewDisplayName("State - App Specific")

// State - Selected
TextComponentView(
// swiftlint:disable:next force_try
viewModel: try! .init(
localizationProvider: .init(
locale: Locale.current,
localizedStrings: [
"id_1": .string("Hello, world")
"id_1": .string("Hello, world"),
"id_2": .string("THIS SHOULDN'T SHOW")
]
),
uiConfigProvider: .init(uiConfig: PreviewUIConfig.make()),
component: .init(
text: "id_1",
color: .init(light: .hex("#000000")),
overrides: .init(
// None of this should be displayed
app: .init(
text: "id_2",
color: .init(light: .hex("#ffcc00"))
),
// Selected should override app
states: .init(
selected: .init(
text: "id_1",
fontWeight: .black,
color: .init(light: .hex("#ff0000")),
backgroundColor: .init(light: .hex("#0000ff")),
Expand Down
18 changes: 14 additions & 4 deletions RevenueCatUI/Templates/V2/ViewModelHelpers/PresentedPartials.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import RevenueCat
#if PAYWALL_COMPONENTS

/// Protocol defining how partial components can be combined
protocol PresentedPartial {
protocol PresentedPartial: Equatable {

/// Combines two partial components, allowing for override behavior
/// - Parameters:
Expand All @@ -32,6 +32,8 @@ protocol PresentedPartial {
/// Structure holding override configurations for different presentation states
struct PresentedOverrides<T: PresentedPartial> {

/// Override for this app
public let app: T?
/// Override for intro offer state
public let introOffer: T?
/// Override for different selection states
Expand Down Expand Up @@ -75,12 +77,18 @@ extension PresentedPartial {
isEligibleForIntroOffer: Bool,
with presentedOverrides: PresentedOverrides<Self>?
) -> Self? {
var conditionPartial = buildConditionPartial(for: condition, with: presentedOverrides)
// Start with partial for app specific overrides
var conditionPartial = Self.combine(nil, with: presentedOverrides?.app)

// Apply screen conditions (sizes)
conditionPartial = buildConditionPartial(conditionPartial, for: condition, with: presentedOverrides)

// Apply intro offer
if isEligibleForIntroOffer {
conditionPartial = Self.combine(conditionPartial, with: presentedOverrides?.introOffer)
}

// Apply state
switch state {
case .default:
break
Expand All @@ -97,9 +105,10 @@ extension PresentedPartial {
/// - presentedOverrides: Override configurations to apply
/// - Returns: Configured partial component for the given condition
private static func buildConditionPartial(
_ base: Self,
for conditionType: ScreenCondition,
with presentedOverrides: PresentedOverrides<Self>?
) -> Self? {
) -> Self {
let conditions = presentedOverrides?.conditions
let applicableConditions = conditionType.applicableConditions
.compactMap { type -> Self? in
Expand All @@ -110,7 +119,7 @@ extension PresentedPartial {
}
}

return applicableConditions.reduce(nil) { partial, next in
return applicableConditions.reduce(base) { partial, next in
Self.combine(partial, with: next)
}
}
Expand Down Expand Up @@ -149,6 +158,7 @@ extension PaywallComponent.ComponentOverrides {
/// - Returns: Presented overrides with converted components
func toPresentedOverrides<P: PresentedPartial>(convert: (T) throws -> P) throws -> PresentedOverrides<P> {
PresentedOverrides(
app: try mapPartial(self.app, using: convert),
introOffer: try mapPartial(self.introOffer, using: convert),
states: try self.states.flatMap { states in
PresentedStates(
Expand Down
3 changes: 3 additions & 0 deletions Sources/Paywalls/Components/Common/ComponentOverrides.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ public extension PaywallComponent {
struct ComponentOverrides<T: PartialComponent>: PaywallComponentBase {

public init(
app: T? = nil,
introOffer: T? = nil,
states: PaywallComponent.ComponentStates<T>? = nil,
conditions: PaywallComponent.ComponentConditions<T>? = nil
) {
self.app = app
self.introOffer = introOffer
self.states = states
self.conditions = conditions
}

public let app: T?
public let introOffer: T?
public let states: ComponentStates<T>?
public let conditions: ComponentConditions<T>?
Expand Down
181 changes: 181 additions & 0 deletions Tests/RevenueCatUITests/PaywallsV2/PresentedPartialTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
//
// 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
//
// Untitled.swift
//
// Created by Josh Holtz on 1/12/25.

import Nimble
import RevenueCat
@testable import RevenueCatUI
import XCTest

#if PAYWALL_COMPONENTS

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
class PresentedPartialTest: TestCase {

func testNothing() {
let presentedOverrides: PresentedOverrides<LocalizedTextPartial>? = nil

let localizedPartial = LocalizedTextPartial.buildPartial(
state: .default,
condition: .compact,
isEligibleForIntroOffer: false,
with: presentedOverrides
)

let expectedResult = LocalizedTextPartial(
text: nil,
partial: .init(
visible: nil
)
)

expect(localizedPartial).to(equal(expectedResult))
}

func testApp() {
let presentedOverrides: PresentedOverrides<LocalizedTextPartial> = .init(
app: .init(
text: "override_id_app",
partial: .init(
visible: nil,
fontSize: .bodyL
)
),
introOffer: nil,
states: nil,
conditions: nil
)

let localizedPartial = LocalizedTextPartial.buildPartial(
state: .default,
condition: .compact,
isEligibleForIntroOffer: false,
with: presentedOverrides
)

let expectedResult = LocalizedTextPartial(
text: "override_id_app",
partial: .init(
visible: nil,
fontSize: .bodyL
)
)

expect(localizedPartial).to(equal(expectedResult))
}

func testConditionCompactOverridesApp() {
let presentedOverrides: PresentedOverrides<LocalizedTextPartial> = .init(
app: .init(
text: "override_id_app",
partial: .init(
visible: nil,
fontWeight: .light,
fontSize: .bodyL
)
),
introOffer: nil,
states: nil,
conditions: .init(
compact: .init(
text: "override_id_compact",
partial: .init(
visible: nil,
fontSize: .bodyM
)
),
// This won't get used because `buildPartial` since its using `compact
medium: .init(
text: "override_id_medium",
partial: .init(
visible: nil,
horizontalAlignment: .trailing
)
),
expanded: nil
)
)

let localizedPartial = LocalizedTextPartial.buildPartial(
state: .default,
condition: .compact,
isEligibleForIntroOffer: false,
with: presentedOverrides
)

let expectedResult = LocalizedTextPartial(
text: "override_id_compact", // From compact
partial: .init(
visible: nil,
fontWeight: .light, // From app
fontSize: .bodyM // From compact
)
)

expect(localizedPartial).to(equal(expectedResult))
}

func testConditionMediumOverridesConditionCompactOverridesApp() {
let presentedOverrides: PresentedOverrides<LocalizedTextPartial> = .init(
app: .init(
text: "override_id_app",
partial: .init(
visible: nil,
fontWeight: .light,
fontSize: .bodyL
)
),
introOffer: nil,
states: nil,
conditions: .init(
compact: .init(
text: "override_id_compact",
partial: .init(
visible: nil,
fontSize: .bodyM,
horizontalAlignment: .leading
)
),
medium: .init(
text: "override_id_medium",
partial: .init(
visible: nil,
horizontalAlignment: .trailing
)
),
expanded: nil
)
)

let localizedPartial = LocalizedTextPartial.buildPartial(
state: .default,
condition: .medium,
isEligibleForIntroOffer: false,
with: presentedOverrides
)

let expectedResult = LocalizedTextPartial(
text: "override_id_medium", // From medium
partial: .init(
visible: nil,
fontWeight: .light, // From app
fontSize: .bodyM, // From compact
horizontalAlignment: .trailing // From medium
)
)

expect(localizedPartial).to(equal(expectedResult))
}

}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import RevenueCat
@testable import RevenueCatUI
import XCTest

#if PAYWALL_COMPONENTS

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
class VariableHandlerV2Test: TestCase {

Expand Down Expand Up @@ -515,3 +517,5 @@ class VariableHandlerV2Test: TestCase {
}

}

#endif