Skip to content

Commit

Permalink
Paywalls: added "features" to LocalizedConfiguration (#2899)
Browse files Browse the repository at this point in the history
To be used by upcoming templates.
  • Loading branch information
NachoSoto committed Sep 1, 2023
1 parent 2fb883f commit 465c9e2
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 14 deletions.
14 changes: 12 additions & 2 deletions RevenueCatUI/Data/ProcessedLocalizedConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import RevenueCat
@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
struct ProcessedLocalizedConfiguration: PaywallLocalizedConfiguration {

typealias Feature = PaywallData.LocalizedConfiguration.Feature

var title: String
var subtitle: String
var callToAction: String
var callToActionWithIntroOffer: String?
var offerDetails: String
var offerDetailsWithIntroOffer: String?
var offerName: String?
var features: [Feature]

init(
_ configuration: PaywallData.LocalizedConfiguration,
Expand All @@ -27,7 +30,12 @@ struct ProcessedLocalizedConfiguration: PaywallLocalizedConfiguration {
offerDetails: configuration.offerDetails.processed(with: dataProvider, locale: locale),
offerDetailsWithIntroOffer: configuration.offerDetailsWithIntroOffer?.processed(with: dataProvider,
locale: locale),
offerName: configuration.offerName?.processed(with: dataProvider, locale: locale)
offerName: configuration.offerName?.processed(with: dataProvider, locale: locale),
features: configuration.features.map {
.init(title: $0.title.processed(with: dataProvider, locale: locale),
content: $0.content?.processed(with: dataProvider, locale: locale),
iconID: $0.iconID)
}
)
}

Expand All @@ -38,7 +46,8 @@ struct ProcessedLocalizedConfiguration: PaywallLocalizedConfiguration {
callToActionWithIntroOffer: String?,
offerDetails: String,
offerDetailsWithIntroOffer: String?,
offerName: String?
offerName: String?,
features: [Feature]
) {
self.title = title
self.subtitle = subtitle
Expand All @@ -47,6 +56,7 @@ struct ProcessedLocalizedConfiguration: PaywallLocalizedConfiguration {
self.offerDetails = offerDetails
self.offerDetailsWithIntroOffer = offerDetailsWithIntroOffer
self.offerName = offerName
self.features = features
}

}
Expand Down
6 changes: 4 additions & 2 deletions RevenueCatUI/Data/TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,17 @@ internal enum TestData {
callToAction: "Purchase for {{ price }}",
callToActionWithIntroOffer: "Purchase for {{ price_per_month }} per month",
offerDetails: "{{ price_per_month }} per month",
offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month"
offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month",
features: []
)
static let localization2: PaywallData.LocalizedConfiguration = .init(
title: "Call to action for better conversion.",
subtitle: "Lorem ipsum is simply dummy text of the printing and typesetting industry.",
callToAction: "Subscribe for {{ price_per_month }}/mo",
offerDetails: "{{ total_price_and_per_month }}",
offerDetailsWithIntroOffer: "{{ total_price_and_per_month }} after {{ intro_duration }} trial",
offerName: "{{ period }}"
offerName: "{{ period }}",
features: []
)
static let paywallHeaderImageName = "9a17e0a7_1689854430..jpeg"
static let paywallBackgroundImageName = "9a17e0a7_1689854342..jpg"
Expand Down
3 changes: 2 additions & 1 deletion RevenueCatUI/Helpers/PaywallData+Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ private extension PaywallData {
subtitle: "Unlock access",
callToAction: "Purchase",
offerDetails: "{{ price_per_month }} per month",
offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month"
offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month",
features: []
)

static let backgroundImage = "background.jpg"
Expand Down
49 changes: 48 additions & 1 deletion Sources/Paywalls/PaywallData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ extension PaywallData {
var _callToActionWithIntroOffer: String?
@NonEmptyStringDecodable
var _offerDetailsWithIntroOffer: String?
@DefaultDecodable.EmptyArray
var _features: [Feature]

public var callToActionWithIntroOffer: String? {
get { return self._callToActionWithIntroOffer }
Expand All @@ -83,6 +85,10 @@ extension PaywallData {
get { return self._offerDetailsWithIntroOffer }
set { self._offerDetailsWithIntroOffer = newValue }
}
public var features: [Feature] {
get { return self._features }
set { self._features = newValue }
}

public init(
title: String,
Expand All @@ -91,7 +97,8 @@ extension PaywallData {
callToActionWithIntroOffer: String? = nil,
offerDetails: String,
offerDetailsWithIntroOffer: String? = nil,
offerName: String? = nil
offerName: String? = nil,
features: [Feature] = []
) {
self.title = title
self.subtitle = subtitle
Expand All @@ -100,6 +107,7 @@ extension PaywallData {
self.offerDetails = offerDetails
self._offerDetailsWithIntroOffer = offerDetailsWithIntroOffer
self.offerName = offerName
self.features = features
}

// swiftlint:enable missing_docs
Expand Down Expand Up @@ -140,6 +148,32 @@ extension PaywallData {

}

extension PaywallData.LocalizedConfiguration {

/// An item to be showcased in a paywall.
public struct Feature {

/// The title of the feature.
public var title: String
/// An optional description of the feature.
public var content: String?
/// An optional icon for the feature.
/// This must be an icon identifier known by `RevenueCatUI`.
public var iconID: String?

// swiftlint:disable:next missing_docs
public init(title: String, content: String? = nil, iconID: String? = nil) {
self.title = title
self.content = content
self.iconID = iconID
}

}

}

// MARK: - Configuration

extension PaywallData {

/// Generic configuration for any paywall.
Expand Down Expand Up @@ -340,6 +374,16 @@ extension PaywallData {

// MARK: - Codable

extension PaywallData.LocalizedConfiguration.Feature: Codable {

private enum CodingKeys: String, CodingKey {
case title
case content
case iconID = "iconId"
}

}

extension PaywallData.LocalizedConfiguration: Codable {

private enum CodingKeys: String, CodingKey {
Expand All @@ -350,6 +394,7 @@ extension PaywallData.LocalizedConfiguration: Codable {
case offerDetails
case _offerDetailsWithIntroOffer = "offerDetailsWithIntroOffer"
case offerName
case _features = "features"
}

}
Expand Down Expand Up @@ -388,6 +433,7 @@ extension PaywallData: Codable {

// MARK: - Equatable

extension PaywallData.LocalizedConfiguration.Feature: Equatable {}
extension PaywallData.LocalizedConfiguration: Equatable {}
extension PaywallData.Configuration.ColorInformation: Equatable {}
extension PaywallData.Configuration.Colors: Equatable {}
Expand All @@ -397,6 +443,7 @@ extension PaywallData: Equatable {}

// MARK: - Sendable

extension PaywallData.LocalizedConfiguration.Feature: Sendable {}
extension PaywallData.LocalizedConfiguration: Sendable {}
extension PaywallData.Configuration.ColorInformation: Sendable {}
extension PaywallData.Configuration.Colors: Sendable {}
Expand Down
15 changes: 14 additions & 1 deletion Tests/APITesters/SwiftAPITester/SwiftAPITester/PaywallAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func checkPaywallLocalizedConfig(_ config: PaywallData.LocalizedConfiguration) {
let offerDetails: String = config.offerDetails
let offerDetailsWithIntroOffer: String? = config.offerDetailsWithIntroOffer
let offerName: String? = config.offerName
let features: [PaywallData.LocalizedConfiguration.Feature] = config.features

let _: PaywallData.LocalizedConfiguration = .init(
title: title,
Expand All @@ -66,9 +67,21 @@ func checkPaywallLocalizedConfig(_ config: PaywallData.LocalizedConfiguration) {
callToActionWithIntroOffer: callToActionWithIntroOffer,
offerDetails: offerDetails,
offerDetailsWithIntroOffer: offerDetailsWithIntroOffer,
offerName: offerName
offerName: offerName,
features: features
)
}

func checkLocalizedConfigFeature(_ feature: PaywallData.LocalizedConfiguration.Feature) {
let title: String = feature.title
let content: String? = feature.content
let iconID: String? = feature.iconID

let _: PaywallData.LocalizedConfiguration.Feature = .init(title: title,
content: content,
iconID: iconID)
}

func checkPaywallImages(_ images: PaywallData.Configuration.Images) {
let header: String? = images.header
let background: String? = images.background
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ private extension BaseTemplateViewConfigurationTests {
callToAction: "Purchase for {{ price }}",
callToActionWithIntroOffer: nil,
offerDetails: "{{ price_per_month }} per month",
offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month"
offerDetailsWithIntroOffer: "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month",
features: []
)

private static let consumableProduct = TestStoreProduct(
Expand Down
18 changes: 17 additions & 1 deletion Tests/RevenueCatUITests/Data/VariablesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,15 @@ class VariablesTests: TestCase {
offerDetails: "Purchase for {{ price }}",
offerDetailsWithIntroOffer: "Start your {{ intro_duration }} free trial\n" +
"Then {{ price_per_month }} every month",
offerName: "{{ period }}"
offerName: "{{ period }}",
features: [
.init(title: "Purchase {{ product_name }}",
content: "Trial lasts {{ intro_duration }}",
iconID: nil),
.init(title: "Only {{ price }}",
content: "{{ period }} subscription",
iconID: nil)
]
)
let processed = configuration.processVariables(with: TestData.packageWithIntroOffer)

Expand All @@ -109,6 +117,14 @@ class VariablesTests: TestCase {
expect(processed.offerDetails) == "Purchase for $3.99"
expect(processed.offerDetailsWithIntroOffer) == "Start your 1 week free trial\nThen $3.99 every month"
expect(processed.offerName) == "Monthly"
expect(processed.features) == [
.init(title: "Purchase PRO monthly",
content: "Trial lasts 1 week",
iconID: nil),
.init(title: "Only $3.99",
content: "Monthly subscription",
iconID: nil)
]
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ import SwiftUI
subtitle: "Accede a todo nuestro contenido educativo, confiado por miles de padres.",
callToAction: "Comprar",
offerDetails: "{{ total_price_and_per_month }}",
offerDetailsWithIntroOffer: "Comienza tu prueba de {{ intro_duration }}, después {{ price_per_month }} cada mes"
offerDetailsWithIntroOffer: "Comienza tu prueba de {{ intro_duration }}, " +
"después {{ price_per_month }} cada mes",
features: []
)

}
9 changes: 8 additions & 1 deletion Tests/UnitTests/Networking/Responses/Fixtures/Offerings.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,14 @@
"call_to_action": "Purchase now",
"call_to_action_with_intro_offer": "Purchase now",
"offer_details": "{{ price_per_month }} per month",
"offer_details_with_intro_offer": "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month"
"offer_details_with_intro_offer": "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month",
"features": [
{
"title": "Feature 1",
"content": "Content",
"icon_id": "lock"
}
]
},
"es_ES": {
"title": "Tienda",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,34 @@
"call_to_action_with_intro_offer": "Purchase now",
"offer_details": "{{ price_per_month }} per month",
"offer_details_with_intro_offer": "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month",
"offer_name": "{{ period }}"
"offer_name": "{{ period }}",
"features": [
{
"title": "Feature 1",
"content": "Content 1",
"icon_id": "lock"
},
{
"title": "Feature 2",
"content": "Content 2",
"icon_id": "bell"
}
]
},
"es_ES": {
"title": "Tienda",
"subtitle": "Descripción",
"call_to_action": "Comprar",
"offer_details": "{{ price_per_month }} cada mes",
"offer_details_with_intro_offer": " ",
"offer_name": "{{ period }}"
"offer_name": "{{ period }}",
"features": [
{
"title": "Lista 1",
"content": "Contenido",
"icon_id": "lock"
}
]
}
},
"default_locale": "en_US",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ class OfferingsDecodingTests: BaseHTTPResponseTest {
expect(enConfig.offerDetailsWithIntroOffer)
== "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month"
expect(enConfig.offerName).to(beNil())
expect(enConfig.features) == [
.init(title: "Feature 1", content: "Content", iconID: "lock")
]

let esConfig = try XCTUnwrap(paywall.config(for: Locale(identifier: "es_ES")))
expect(esConfig.title) == "Tienda"
Expand All @@ -136,6 +139,8 @@ class OfferingsDecodingTests: BaseHTTPResponseTest {
expect(esConfig.offerDetails) == "{{ price_per_month }} cada mes"
expect(esConfig.offerDetailsWithIntroOffer)
== "Comienza tu prueba de {{ intro_duration }}, y después {{ price_per_month }} cada mes"
expect(esConfig.offerName).to(beNil())
expect(esConfig.features).to(beEmpty())

// This test relies on this
expect(Locale.current.identifier) == "en_US"
Expand Down
9 changes: 8 additions & 1 deletion Tests/UnitTests/Paywalls/PaywallDataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ class PaywallDataTests: BaseHTTPResponseTest {
expect(enConfig.offerDetailsWithIntroOffer)
== "Start your {{ intro_duration }} trial, then {{ price_per_month }} per month"
expect(enConfig.offerName) == "{{ period }}"
expect(enConfig.features) == [
.init(title: "Feature 1", content: "Content 1", iconID: "lock"),
.init(title: "Feature 2", content: "Content 2", iconID: "bell")
]

let esConfig = try XCTUnwrap(paywall.config(for: Locale(identifier: "es_ES")))
expect(esConfig.title) == "Tienda"
Expand All @@ -75,7 +79,10 @@ class PaywallDataTests: BaseHTTPResponseTest {
expect(esConfig.callToActionWithIntroOffer).to(beNil())
expect(esConfig.offerDetails) == "{{ price_per_month }} cada mes"
expect(esConfig.offerDetailsWithIntroOffer).to(beNil())
expect(enConfig.offerName) == "{{ period }}"
expect(esConfig.offerName) == "{{ period }}"
expect(esConfig.features) == [
.init(title: "Lista 1", content: "Contenido", iconID: "lock")
]

expect(paywall.localizedConfiguration) == paywall.config(for: Locale.current)

Expand Down

0 comments on commit 465c9e2

Please sign in to comment.