Skip to content

Commit

Permalink
Paywalls: extracted configuration processing into a new `TemplateVi…
Browse files Browse the repository at this point in the history
…ewConfiguration` (#2830)

### Changes:
- Created `TemplateViewConfiguration` to encapsulate all processed
configuration for a paywall
- Created `TemplateViewConfiguration.PackageSetting` and
`TemplateViewConfiguration.PackageConfiguration` to represent paywalls
that support 1 or N packages
- Taken all common logic out of `Example1Template`
- Added new `PaywallViewMode` for upcoming "ramps" views
  • Loading branch information
NachoSoto committed Aug 28, 2023
1 parent b4cb9fd commit 3f00438
Show file tree
Hide file tree
Showing 13 changed files with 482 additions and 235 deletions.
File renamed without changes.
16 changes: 16 additions & 0 deletions RevenueCatUI/Data/PaywallViewMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// PaywallViewMode.swift
//
//
// Created by Nacho Soto on 7/17/23.
//

import RevenueCat

/// The mode for how a paywall is rendered.
public enum PaywallViewMode {

/// Paywall is displayed full-screen, with as much information as available.
case fullScreen

}
46 changes: 46 additions & 0 deletions RevenueCatUI/Data/TemplateError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// TemplateError.swift
//
//
// Created by Nacho Soto on 7/17/23.
//

import Foundation
import RevenueCat

/// Error produced when processing `PaywallData`.
enum TemplateError: Error {

/// No packages available to create a paywall.
case noPackages

/// Paywall configuration contained no package types.
case emptyPackageList

/// No packages from the `PackageType` list could be found.
case couldNotFindAnyPackages(expectedTypes: [PackageType])

}

extension TemplateError: CustomNSError {

var errorUserInfo: [String: Any] {
return [
NSLocalizedDescriptionKey: self.description
]
}

private var description: String {
switch self {
case .noPackages:
return "Attempted to display paywall with no packages."

case .emptyPackageList:
return "Paywall configuration contains no packages."

case let .couldNotFindAnyPackages(expectedTypes):
return "Couldn't find any requested packages: \(expectedTypes)"
}
}

}
153 changes: 153 additions & 0 deletions RevenueCatUI/Data/TemplateViewConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// TemplateViewConfiguration.swift
//
//
// Created by Nacho Soto on 7/17/23.
//

import Foundation
import RevenueCat

/// The processed data necessary to render a `TemplateViewType`.
@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
struct TemplateViewConfiguration {

let mode: PaywallViewMode
let packages: PackageConfiguration
let configuration: PaywallData.Configuration
let colors: PaywallData.Configuration.Colors
let headerImageURL: URL

}

// MARK: - Packages

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
extension TemplateViewConfiguration {

/// A `Package` with its processed localized strings.
struct Package {

let content: RevenueCat.Package
let localization: ProcessedLocalizedConfiguration

}

/// Whether a template displays 1 or multiple packages.
enum PackageSetting {

case single
case multiple

}

/// Describes the possible displayed packages in a paywall.
/// See `create(with:filter:setting:)` for how to create these.
enum PackageConfiguration {

case single(Package)
case multiple([Package])

}

}

// MARK: - Properties

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
extension TemplateViewConfiguration.PackageConfiguration {

/// Returns a single package, useful for templates that expect a single package.
var single: TemplateViewConfiguration.Package {
switch self {
case let .single(package):
return package
case let .multiple(packages):
guard let package = packages.first else {
// `create()` makes this impossible.
fatalError("Unexpectedly found no packages in `PackageConfiguration.multiple`")
}

return package
}
}

/// Returns all packages, useful for templates that expect multiple packages
var all: [TemplateViewConfiguration.Package] {
switch self {
case let .single(package):
return [package]
case let .multiple(packages):
return packages
}
}

}

// MARK: - Creation

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
extension TemplateViewConfiguration.PackageConfiguration {

/// Creates a `PackageConfiguration` based on `setting`.
/// - Throws: `TemplateError`
static func create(
with packages: [RevenueCat.Package],
filter: [PackageType],
localization: PaywallData.LocalizedConfiguration,
setting: TemplateViewConfiguration.PackageSetting
) throws -> Self {
guard !packages.isEmpty else { throw TemplateError.noPackages }
guard !filter.isEmpty else { throw TemplateError.emptyPackageList }

let filtered = TemplateViewConfiguration
.filter(packages: packages, with: filter)
.map { package in
TemplateViewConfiguration.Package(
content: package,
localization: localization.processVariables(with: package))
}

guard let firstPackage = filtered.first else {
throw TemplateError.couldNotFindAnyPackages(expectedTypes: filter)
}

switch setting {
case .single:
return .single(firstPackage)
case .multiple:
return .multiple(filtered)
}
}

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
extension TemplateViewConfiguration {

/// Filters `packages`, extracting only the values corresponding to `list`.
static func filter(packages: [RevenueCat.Package], with list: [PackageType]) -> [RevenueCat.Package] {
// Only subscriptions are supported at the moment
let subscriptions = packages.filter { $0.storeProduct.productCategory == .subscription }
let map = Dictionary(grouping: subscriptions) { $0.packageType }

return list.compactMap { type in
if let packages = map[type] {
switch packages.count {
case 0:
// This isn't actually possible because of `Dictionary(grouping:by:)
return nil
case 1:
return packages.first
default:
Logger.warning("Found multiple \(type) packages. Will use the first one.")
return packages.first
}
} else {
Logger.warning("Couldn't find '\(type)'")
return nil
}
}
}

}
File renamed without changes.
File renamed without changes.
9 changes: 7 additions & 2 deletions RevenueCatUI/PaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import SwiftUI
@available(macCatalyst, unavailable, message: "RevenueCatUI does not support Catalyst yet")
public struct PaywallView: View {

private let mode: PaywallViewMode
private let offering: Offering
private let paywall: PaywallData
private let introEligibility: TrialOrIntroEligibilityChecker?
private let purchaseHandler: PurchaseHandler?

/// Create a view for the given offering and paywal.
/// - Warning: `Purchases` must have been configured prior to displaying it.
public init(offering: Offering, paywall: PaywallData) {
public init(mode: PaywallViewMode, offering: Offering, paywall: PaywallData) {
self.init(
mode: mode,
offering: offering,
paywall: paywall,
introEligibility: Purchases.isConfigured ? .init() : nil,
Expand All @@ -25,11 +27,13 @@ public struct PaywallView: View {
}

init(
mode: PaywallViewMode = .fullScreen,
offering: Offering,
paywall: PaywallData,
introEligibility: TrialOrIntroEligibilityChecker?,
purchaseHandler: PurchaseHandler?
) {
self.mode = mode
self.offering = offering
self.paywall = paywall
self.introEligibility = introEligibility
Expand All @@ -40,7 +44,7 @@ public struct PaywallView: View {
public var body: some View {
if let checker = self.introEligibility, let purchaseHandler = self.purchaseHandler {
self.paywall
.createView(for: self.offering)
.createView(for: self.offering, mode: self.mode)
.environmentObject(checker)
.environmentObject(purchaseHandler)
} else {
Expand All @@ -64,6 +68,7 @@ struct PaywallView_Previews: PreviewProvider {

if let paywall = offering.paywall {
PaywallView(
mode: .fullScreen,
offering: offering,
paywall: paywall,
introEligibility: TrialOrIntroEligibilityChecker
Expand Down
Loading

0 comments on commit 3f00438

Please sign in to comment.