Skip to content

Commit

Permalink
[Paywalls V2] Added fallback components (#4621)
Browse files Browse the repository at this point in the history
* Added fallback components

* Fixed a test

* Added swiftui preview for fallback

* Way more better testing
  • Loading branch information
joshdholtz authored Dec 30, 2024
1 parent 97a31da commit 8984913
Show file tree
Hide file tree
Showing 6 changed files with 454 additions and 22 deletions.
8 changes: 8 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

/* Begin PBXBuildFile section */
0313FD41268A506400168386 /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313FD40268A506400168386 /* DateProvider.swift */; };
03A98CEF2D1EE048009BCA61 /* FallbackComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A98CEE2D1EE040009BCA61 /* FallbackComponentTests.swift */; };
03A98CF12D222F5F009BCA61 /* FallbackComponentPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A98CF02D222F53009BCA61 /* FallbackComponentPreview.swift */; };
1E2F910B2CC8FE5600BDB016 /* ContactSupportUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F910A2CC8FE5600BDB016 /* ContactSupportUtilities.swift */; };
1E2F911B2CC918ED00BDB016 /* ContactSupportUtilitiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F911A2CC918ED00BDB016 /* ContactSupportUtilitiesTests.swift */; };
1E2F91722CCFA98C00BDB016 /* WebRedemptionStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F91712CCFA98C00BDB016 /* WebRedemptionStrings.swift */; };
Expand Down Expand Up @@ -1237,6 +1239,8 @@

/* Begin PBXFileReference section */
0313FD40268A506400168386 /* DateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProvider.swift; sourceTree = "<group>"; };
03A98CEE2D1EE040009BCA61 /* FallbackComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackComponentTests.swift; sourceTree = "<group>"; };
03A98CF02D222F53009BCA61 /* FallbackComponentPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackComponentPreview.swift; sourceTree = "<group>"; };
1E2F910A2CC8FE5600BDB016 /* ContactSupportUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactSupportUtilities.swift; sourceTree = "<group>"; };
1E2F911A2CC918ED00BDB016 /* ContactSupportUtilitiesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactSupportUtilitiesTests.swift; sourceTree = "<group>"; };
1E2F91712CCFA98C00BDB016 /* WebRedemptionStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRedemptionStrings.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2470,6 +2474,7 @@
2C2AEB0D2CA64DA900A50F38 /* TemplateComponentsViewPreviews */ = {
isa = PBXGroup;
children = (
03A98CF02D222F53009BCA61 /* FallbackComponentPreview.swift */,
2C2AEB0E2CA64E0E00A50F38 /* Template1Preview.swift */,
2C8EC6DA2CCC23B700D6CCF8 /* Template5Preview.swift */,
);
Expand Down Expand Up @@ -2540,6 +2545,7 @@
2C8EC6AD2CCBD33800D6CCF8 /* Components */ = {
isa = PBXGroup;
children = (
03A98CEE2D1EE040009BCA61 /* FallbackComponentTests.swift */,
2C08B2E82CD40DBF0024857B /* ButtonComponentTests.swift */,
2C8EC6DE2CCD27A100D6CCF8 /* PartialComponentTests.swift */,
);
Expand Down Expand Up @@ -6300,6 +6306,7 @@
2C8EC6DF2CCD27A500D6CCF8 /* PartialComponentTests.swift in Sources */,
351B51A326D450BC00BD2BD7 /* DictionaryExtensionsTests.swift in Sources */,
4FB2B5512AA7DBA40087EDB5 /* MockFileHandler.swift in Sources */,
03A98CEF2D1EE048009BCA61 /* FallbackComponentTests.swift in Sources */,
573A10D92800ADCD00F976E5 /* StoreKitErrorTests.swift in Sources */,
5766AABF283E80B500FA6091 /* BasePurchasesTests.swift in Sources */,
351B515826D44B3E00BD2BD7 /* MockOfferingsFactory.swift in Sources */,
Expand Down Expand Up @@ -6553,6 +6560,7 @@
2C7457482CEA66AB004ACE52 /* ComponentsView.swift in Sources */,
353756722C382C2800A1B8D6 /* URLUtilities.swift in Sources */,
353FDC0F2CA446FA0055F328 /* StoreProductDiscount+Extensions.swift in Sources */,
03A98CF12D222F5F009BCA61 /* FallbackComponentPreview.swift in Sources */,
887A60862C1D037000E1A461 /* FooterHidingModifier.swift in Sources */,
FDAD6AC72D132DD900FB047E /* CustomerCenterStoreKitUtilitiesType.swift in Sources */,
887A60C02C1D037000E1A461 /* AsyncButton.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//
// 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
//
// FallbackComponentPreview.swift
//
// Created by Josh Holtz on 12/29/24.

import RevenueCat
import SwiftUI

// swiftlint:disable force_try

#if PAYWALL_COMPONENTS

#if DEBUG

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct FallbackComponentPreview_Previews: PreviewProvider {

static let jsonStringDefaultStack = """
{
"type": "stack",
"dimension": {
"type": "vertical",
"alignment": "center",
"distribution": "center"
},
"size": {
"width": { "type": "fixed", "value": 200 },
"height": { "type": "fixed", "value": 100 }
},
"padding": {
"top": 0,
"bottom": 0,
"leading": 0,
"trailing": 0
},
"margin": {
"top": 0,
"bottom": 0,
"leading": 0,
"trailing": 0
},
"background_color": {
"light": {
"type": "hex",
"value": "#ffcc00"
}
},
"components": [
{
"type": "text",
"text_lid": "text1",
"font_weight": "regular",
"color": {
"light": {
"type": "hex",
"value": "#000000"
}
},
"font_size": "body_m",
"horizontal_alignment": "center",
"padding": {
"top": 0,
"bottom": 0,
"leading": 0,
"trailing": 0
},
"margin": {
"top": 0,
"bottom": 0,
"leading": 0,
"trailing": 0
},
"size": {
"width": { "type": "fit" },
"height": { "type": "fit" }
}
}
]
}
"""

static let jsonStringComponentWithFallback = """
{
"type": "super_new_type",
"unknown_property": {
"type": "more_unknown"
},
"fallback": \(jsonStringDefaultStack)
}
"""

static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601

return decoder
}()

static let componentWithFallback = {
let jsonData = jsonStringComponentWithFallback.data(using: .utf8)!
return try! decoder.decode(PaywallComponent.self, from: jsonData)
}()

static func toViewModels(
component: PaywallComponent,
packageValidator: PackageValidator,
offering: Offering,
localizationProvider: LocalizationProvider
) -> PaywallComponentViewModel {
let factory = ViewModelFactory()
return try! factory.toViewModel(
component: component,
packageValidator: packageValidator,
offering: offering,
localizationProvider: localizationProvider
)
}

static let localizationProvider = LocalizationProvider(
locale: .init(identifier: "en_US"),
localizedStrings: [
"text1": .string("Fallback is showing")
]
)

static let offering = Offering(identifier: "default",
serverDescription: "",
availablePackages: [])

static var previews: some View {

// Component With Fallback
ComponentsView(
componentViewModels: [
toViewModels(
component: componentWithFallback,
packageValidator: PackageValidator(),
offering: offering,
localizationProvider: localizationProvider
)
],
onDismiss: {}
)
.previewRequiredEnvironmentProperties()
.previewLayout(.sizeThatFits)
.previewDisplayName("Component With Fallback")
}
}

#endif

#endif
65 changes: 55 additions & 10 deletions Sources/Paywalls/Components/Common/PaywallComponentBase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ extension PaywallComponent: Codable {
enum CodingKeys: String, CodingKey {

case type
case fallback

}

Expand Down Expand Up @@ -90,27 +91,71 @@ extension PaywallComponent: Codable {

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(ComponentType.self, forKey: .type)

// Decode the raw string for the `type` field
let typeString = try container.decode(String.self, forKey: .type)

// Attempt to convert raw string into our `ComponentType` enum
if let type = ComponentType(rawValue: typeString) {
self = try Self.decodeType(from: decoder, type: type)
} else {
if !container.contains(.fallback) {
let context = DecodingError.Context(
codingPath: container.codingPath,
debugDescription:
"""
Failed to decode unknown type "\(typeString)" without a fallback.
"""
)
throw DecodingError.dataCorrupted(context)
}

do {
// If `typeString` is unknown, try to decode the fallback
self = try container.decode(PaywallComponent.self, forKey: .fallback)
} catch DecodingError.valueNotFound {
let context = DecodingError.Context(
codingPath: container.codingPath,
debugDescription:
"""
Failed to decode unknown type "\(typeString)" without a fallback.
"""
)
throw DecodingError.dataCorrupted(context)
} catch {
let context = DecodingError.Context(
codingPath: container.codingPath,
debugDescription:
"""
Failed to decode fallback for unknown type "\(typeString)".
""",
underlyingError: error
)
throw DecodingError.dataCorrupted(context)
}
}
}

private static func decodeType(from decoder: Decoder, type: ComponentType) throws -> PaywallComponent {
switch type {
case .text:
self = .text(try TextComponent(from: decoder))
return .text(try TextComponent(from: decoder))
case .image:
self = .image(try ImageComponent(from: decoder))
return .image(try ImageComponent(from: decoder))
case .spacer:
self = .spacer(try SpacerComponent(from: decoder))
return .spacer(try SpacerComponent(from: decoder))
case .stack:
self = .stack(try StackComponent(from: decoder))
return .stack(try StackComponent(from: decoder))
case .linkButton:
self = .linkButton(try LinkButtonComponent(from: decoder))
return .linkButton(try LinkButtonComponent(from: decoder))
case .button:
self = .button(try ButtonComponent(from: decoder))
return .button(try ButtonComponent(from: decoder))
case .package:
self = .package(try PackageComponent(from: decoder))
return .package(try PackageComponent(from: decoder))
case .purchaseButton:
self = .purchaseButton(try PurchaseButtonComponent(from: decoder))
return .purchaseButton(try PurchaseButtonComponent(from: decoder))
case .stickyFooter:
self = .stickyFooter(try StickyFooterComponent(from: decoder))
return .stickyFooter(try StickyFooterComponent(from: decoder))
}
}

Expand Down
43 changes: 36 additions & 7 deletions Tests/UnitTests/Paywalls/Components/ButtonComponentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ class ButtonComponentCodableTests: TestCase {
"type": "stack",
"dimension": {
"type": "vertical",
"alignment": "center"
"alignment": "center",
"distribution": "start"
},
"size": {
"width": { "type": "fill" },
"height": { "type": "fill" }
},
"padding": {
"top": 0,
Expand Down Expand Up @@ -44,7 +49,11 @@ class ButtonComponentCodableTests: TestCase {

let buttonComponent = PaywallComponent.ButtonComponent(
action: .restorePurchases,
stack: .init(components: [])
stack: .init(
components: [],
dimension: .vertical(.center, .start),
size: .init(width: .fill, height: .fill)
)
)

XCTAssertEqual(decodedButton, buttonComponent)
Expand All @@ -65,7 +74,11 @@ class ButtonComponentCodableTests: TestCase {

let buttonComponent = PaywallComponent.ButtonComponent(
action: .navigateBack,
stack: .init(components: [])
stack: .init(
components: [],
dimension: .vertical(.center, .start),
size: .init(width: .fill, height: .fill)
)
)

XCTAssertEqual(decodedButton, buttonComponent)
Expand All @@ -87,7 +100,11 @@ class ButtonComponentCodableTests: TestCase {

let buttonComponent = PaywallComponent.ButtonComponent(
action: .navigateTo(destination: .customerCenter),
stack: .init(components: [])
stack: .init(
components: [],
dimension: .vertical(.center, .start),
size: .init(width: .fill, height: .fill)
)
)

XCTAssertEqual(decodedButton, buttonComponent)
Expand Down Expand Up @@ -116,7 +133,11 @@ class ButtonComponentCodableTests: TestCase {
destination: .terms(urlLid: "re45",
method: .inAppBrowser)
),
stack: .init(components: [])
stack: .init(
components: [],
dimension: .vertical(.center, .start),
size: .init(width: .fill, height: .fill)
)
)

XCTAssertEqual(decodedButton, buttonComponent)
Expand Down Expand Up @@ -145,7 +166,11 @@ class ButtonComponentCodableTests: TestCase {
destination: .privacyPolicy(urlLid: "re45",
method: .externalBrowser)
),
stack: .init(components: [])
stack: .init(
components: [],
dimension: .vertical(.center, .start),
size: .init(width: .fill, height: .fill)
)
)

XCTAssertEqual(decodedButton, buttonComponent)
Expand Down Expand Up @@ -174,7 +199,11 @@ class ButtonComponentCodableTests: TestCase {
destination: .url(urlLid: "re45",
method: .deepLink)
),
stack: .init(components: [])
stack: .init(
components: [],
dimension: .vertical(.center, .start),
size: .init(width: .fill, height: .fill)
)
)

XCTAssertEqual(decodedButton, buttonComponent)
Expand Down
Loading

0 comments on commit 8984913

Please sign in to comment.