-
Notifications
You must be signed in to change notification settings - Fork 338
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
Paywall Components Initial Commit #4224
Changes from 60 commits
9f4419a
7d46bb2
1dd97de
b70eef9
f3bd65a
31af70c
ddc22d5
2a7b7a4
bf3ce3d
5fd18f9
84ac29a
a73c42d
381b96b
9c754ae
c2f461b
232f392
39d87fe
f048085
4f3784e
380a8e6
6ede294
38c11e3
714f483
3574199
a35aa7c
4fb295b
5622854
edd55f5
77b3468
0888b94
495b5fd
6730f0d
bdb3d80
c8a4460
52f2902
a05fb2b
1bad03d
86a9f36
3a60e57
eab0c8d
72ad780
ad1dd81
833ae17
de4fe87
80e0e81
56a7175
8c291b5
61727a8
7d23d2f
742fc32
1062ab7
bb3bf8c
dc4fdb4
1553917
95f4056
ee0ffbc
1614d80
7222042
b5ff52c
48e50ac
5fa016d
dba53ef
1a5961b
76f1ddb
116dc9a
8d0bbc0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -219,13 +219,48 @@ public struct PaywallView: View { | |
} | ||
|
||
@ViewBuilder | ||
// swiftlint:disable:next function_body_length | ||
private func paywallView( | ||
for offering: Offering, | ||
activelySubscribedProductIdentifiers: Set<String>, | ||
fonts: PaywallFontProvider, | ||
checker: TrialOrIntroEligibilityChecker, | ||
purchaseHandler: PurchaseHandler | ||
) -> some View { | ||
|
||
#if PAYWALL_COMPONENTS | ||
if let componentData = offering.paywallComponentsData { | ||
TemplateComponentsView(paywallComponentsData: componentData) | ||
} else { | ||
|
||
let (paywall, displayedLocale, template, error) = offering.validatedPaywall(locale: self.locale) | ||
|
||
let paywallView = LoadedOfferingPaywallView( | ||
offering: offering, | ||
activelySubscribedProductIdentifiers: activelySubscribedProductIdentifiers, | ||
paywall: paywall, | ||
template: template, | ||
mode: self.mode, | ||
fonts: fonts, | ||
displayCloseButton: self.displayCloseButton, | ||
introEligibility: checker, | ||
purchaseHandler: purchaseHandler, | ||
locale: displayedLocale | ||
) | ||
|
||
if let error { | ||
DebugErrorView( | ||
"\(error.description)\n" + | ||
"You can fix this by editing the paywall in the RevenueCat dashboard.\n" + | ||
"The displayed paywall contains default configuration.\n" + | ||
"This error will be hidden in production.", | ||
replacement: paywallView | ||
Comment on lines
+252
to
+257
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Not for this PR] |
||
) | ||
} else { | ||
paywallView | ||
} | ||
} | ||
#else | ||
let (paywall, displayedLocale, template, error) = offering.validatedPaywall(locale: self.locale) | ||
|
||
let paywallView = LoadedOfferingPaywallView( | ||
|
@@ -252,6 +287,7 @@ public struct PaywallView: View { | |
} else { | ||
paywallView | ||
} | ||
#endif | ||
} | ||
|
||
// MARK: - | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// | ||
// 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 | ||
// | ||
// ImageComponentView.swift | ||
// | ||
// Created by Josh Holtz on 6/11/24. | ||
|
||
import Foundation | ||
import RevenueCat | ||
import SwiftUI | ||
|
||
#if PAYWALL_COMPONENTS | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
struct ImageComponentView: View { | ||
|
||
let locale: Locale | ||
let component: PaywallComponent.ImageComponent | ||
|
||
var cornerRadius: CGFloat { | ||
component.cornerRadius | ||
} | ||
|
||
var gradientColors: [Color] { | ||
component.gradientColors.compactMap { try? $0.toColor() } | ||
} | ||
|
||
var body: some View { | ||
RemoteImage(url: component.url) { image in | ||
image | ||
.resizable() | ||
.aspectRatio(contentMode: .fit) | ||
.overlay( | ||
LinearGradient( | ||
gradient: Gradient(colors: gradientColors), | ||
startPoint: .top, | ||
endPoint: .bottom | ||
) | ||
) | ||
.cornerRadius(cornerRadius) | ||
} | ||
.clipped() | ||
} | ||
|
||
} | ||
|
||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// | ||
// LinkButtonComponentView.swift | ||
// | ||
// | ||
// Created by James Borthwick on 2024-08-21. | ||
// | ||
|
||
import Foundation | ||
import RevenueCat | ||
import SwiftUI | ||
|
||
#if PAYWALL_COMPONENTS | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
struct LinkButtonComponentView: View { | ||
|
||
let locale: Locale | ||
let component: PaywallComponent.LinkButtonComponent | ||
|
||
var url: URL { | ||
component.url | ||
} | ||
|
||
var body: some View { | ||
Link(destination: url) { | ||
TextComponentView(locale: locale, component: component.textComponent) | ||
.cornerRadius(25) | ||
} | ||
} | ||
|
||
} | ||
|
||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// | ||
// 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 | ||
// | ||
// SpacerComponentView.swift | ||
// | ||
// Created by James Borthwick on 2024-08-19. | ||
|
||
import Foundation | ||
import RevenueCat | ||
import SwiftUI | ||
|
||
#if PAYWALL_COMPONENTS | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
struct SpacerComponentView: View { | ||
|
||
let locale: Locale | ||
let component: PaywallComponent.SpacerComponent | ||
|
||
var body: some View { | ||
Spacer() | ||
} | ||
|
||
} | ||
|
||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// | ||
// 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 | ||
// | ||
// StackComponentView.swift | ||
// | ||
// Created by James Borthwick on 2024-08-20. | ||
|
||
import RevenueCat | ||
import SwiftUI | ||
|
||
#if PAYWALL_COMPONENTS | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
struct StackComponentView: View { | ||
|
||
let component: PaywallComponent.StackComponent | ||
|
||
var dimension: PaywallComponent.StackComponent.Dimension { | ||
component.dimension | ||
} | ||
var components: [PaywallComponent] { | ||
component.components | ||
jamesrb1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
var spacing: CGFloat? { | ||
component.spacing | ||
} | ||
|
||
var backgroundColor: Color { | ||
if let lightColor = component.backgroundColor?.light { | ||
return (try? PaywallColor(stringRepresentation: lightColor).underlyingColor) ?? Color.clear | ||
} | ||
return Color.clear | ||
jamesrb1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
let locale: Locale | ||
|
||
init(component: PaywallComponent.StackComponent, locale: Locale) { | ||
self.component = component | ||
self.locale = locale | ||
} | ||
|
||
var body: some View { | ||
switch dimension { | ||
case .vertical(let horizontalAlignment): | ||
VStack(alignment: horizontalAlignment.stackAlignment, spacing: spacing) { | ||
ComponentsView(locale: locale, components: components) | ||
} | ||
.background(backgroundColor) | ||
case .horizontal(let verticalAlignment): | ||
HStack(alignment: verticalAlignment.stackAlignment, spacing: spacing) { | ||
ComponentsView(locale: locale, components: components) | ||
} | ||
.background(backgroundColor) | ||
case .zlayer(let alignment): | ||
ZStack(alignment: alignment.stackAlignment) { | ||
ComponentsView(locale: locale, components: components) | ||
} | ||
.background(backgroundColor) | ||
} | ||
} | ||
|
||
} | ||
|
||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by Josh Holtz on 6/11/24. | ||
// | ||
|
||
import RevenueCat | ||
import SwiftUI | ||
|
||
#if PAYWALL_COMPONENTS | ||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
struct TemplateComponentsView: View { | ||
|
||
@Environment(\.locale) | ||
var locale | ||
|
||
let paywallComponentsData: PaywallComponentsData | ||
|
||
init(paywallComponentsData: PaywallComponentsData) { | ||
self.paywallComponentsData = paywallComponentsData | ||
} | ||
|
||
var body: some View { | ||
VStack(spacing: 0) { | ||
ComponentsView( | ||
locale: self.locale, | ||
components: paywallComponentsData.componentsConfig.components | ||
) | ||
} | ||
.edgesIgnoringSafeArea(.top) | ||
} | ||
|
||
} | ||
|
||
func getLocalization(_ locale: Locale, _ displayString: DisplayString) -> String { | ||
if let found = displayString.value[locale.identifier] { | ||
return found | ||
} | ||
|
||
return displayString.value.values.first! | ||
} | ||
jamesrb1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
// @PublicForExternalTesting | ||
struct ComponentsView: View { | ||
|
||
let locale: Locale | ||
let components: [PaywallComponent] | ||
|
||
// @PublicForExternalTesting | ||
init(locale: Locale, components: [PaywallComponent]) { | ||
self.locale = locale | ||
self.components = components | ||
} | ||
|
||
// @PublicForExternalTesting | ||
var body: some View { | ||
self.layoutComponents(self.components) | ||
} | ||
|
||
@ViewBuilder | ||
func layoutComponents(_ layoutComponentsArray: [PaywallComponent]) -> some View { | ||
ForEach(Array(layoutComponentsArray.enumerated()), id: \.offset) { _, item in | ||
switch item { | ||
case .text(let component): | ||
TextComponentView(locale: locale, component: component) | ||
case .image(let component): | ||
ImageComponentView(locale: locale, component: component) | ||
case .spacer(let component): | ||
SpacerComponentView( | ||
locale: locale, | ||
component: component | ||
) | ||
case .stack(let component): | ||
StackComponentView(component: component, locale: locale) | ||
case .linkButton(let component): | ||
LinkButtonComponentView(locale: locale, component: component) | ||
} | ||
} | ||
} | ||
|
||
} | ||
|
||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// | ||
// 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 | ||
// | ||
// TextComponentView.swift | ||
// | ||
// Created by Josh Holtz on 6/11/24. | ||
|
||
import Foundation | ||
import RevenueCat | ||
import SwiftUI | ||
|
||
#if PAYWALL_COMPONENTS | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
struct TextComponentView: View { | ||
|
||
let locale: Locale | ||
let component: PaywallComponent.TextComponent | ||
|
||
var backgroundColor: Color? { | ||
if let thing = component.backgroundColor?.light { | ||
return (try? PaywallColor(stringRepresentation: thing).underlyingColor) ?? Color.clear | ||
} | ||
return nil | ||
} | ||
|
||
var body: some View { | ||
Text(getLocalization(locale, component.text)) | ||
.font(component.textStyle.font) | ||
.fontWeight(component.fontWeight.fontWeight) | ||
.multilineTextAlignment(component.horizontalAlignment.textAlignment) | ||
.foregroundStyle( | ||
(try? PaywallColor(stringRepresentation: component.color.light).underlyingColor) ?? Color.clear | ||
) | ||
.padding(.top, component.padding.top) | ||
.padding(.bottom, component.padding.bottom) | ||
.padding(.leading, component.padding.leading) | ||
.padding(.trailing, component.padding.trailing) | ||
.background(self.backgroundColor) | ||
} | ||
|
||
} | ||
|
||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewer note: this will be reworked in future PR with proper loading and validation ☝️