Skip to content

Commit

Permalink
Paywall Components Initial Commit (#4224)
Browse files Browse the repository at this point in the history
Initial components merge. Notes:

- Linting enabled except for public documentation which will come as the
API matures and stabilizes.
- Took out unused files (tier-related ones), which will come back in a
future version. The comments were useful and will be referenced when
they come back so thank you for the comments on these.
- Left in tvOS and watchOS availability checks for now - may revisit
this later.
- Everything is codable.
- Features added:
- Layout components allow other components to be laid out along x/y/z
axes.
- Text component supports font weight. (Changing actual font will come
later.)
  - LinkButton component links out to external website of choice.
- RemoteImage allows the image to be arbitrarily styled by the calling
code by passing the image back in a closure.
- Image component supports corner rounding an overlaid gradient. (These
will likely be pulled out into common code somewhere in a future PR.)
- Support for OfferingResponse that includes a separate
`PaywallComponentsData` structure, which is separate from `PaywallData`
used for template-based paywalls.

---------

Co-authored-by: Josh Holtz <[email protected]>
  • Loading branch information
2 people authored and nyeu committed Oct 1, 2024
1 parent 82e0aa6 commit 11f272e
Show file tree
Hide file tree
Showing 25 changed files with 1,645 additions and 42 deletions.
60 changes: 60 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
} else {
paywallView
}
}
#else
let (paywall, displayedLocale, template, error) = offering.validatedPaywall(locale: self.locale)

let paywallView = LoadedOfferingPaywallView(
Expand All @@ -252,6 +287,7 @@ public struct PaywallView: View {
} else {
paywallView
}
#endif
}

// MARK: -
Expand Down
53 changes: 53 additions & 0 deletions RevenueCatUI/Templates/Components/ImageComponentView.swift
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
33 changes: 33 additions & 0 deletions RevenueCatUI/Templates/Components/LinkButtonComponentView.swift
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
32 changes: 32 additions & 0 deletions RevenueCatUI/Templates/Components/SpacerComponentView.swift
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
71 changes: 71 additions & 0 deletions RevenueCatUI/Templates/Components/StackComponentView.swift
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
}

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
}

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
85 changes: 85 additions & 0 deletions RevenueCatUI/Templates/Components/TemplateComponentsView.swift
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!
}

@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
50 changes: 50 additions & 0 deletions RevenueCatUI/Templates/Components/TextComponentView.swift
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
Loading

0 comments on commit 11f272e

Please sign in to comment.