Files
purchases-ios/Sources/Networking/Responses/RevenueCatUI/PaywallComponentsData.swift
T
Facundo Menzella c22bdf12b6 Add exit offers support for paywalls (#5944)
* Starting with exit offers

* Done final tweaks

* add missing file to proejct

* Update missing test

* Cleanup - don't show if purchase

* nits

* minor tweaks after testing

* use purchase result

* fixed test

* camelCase

* remove v1 mention

* rename PresentPaywallBindingModifier to PresentingPaywallBindingModifier

* remove ExitOfferItem

* tweaks for fetchOFfering

* final tweaks

* update comment

* add purchase logic, and task with id

* check shouldDisplay on restore, not on completed

* Updated APITests

* move eixtOffering above body, and check for cancellation

* close on completed

* removed unused code

* added comments

* rewrote handle dismiss

* add guard to avoid double dismiss
2025-12-23 17:41:03 +01:00

219 lines
7.1 KiB
Swift

//
// 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
//
// PaywallComponentsData.swift
//
// Created by Josh Holtz on 11/11/24.
// swiftlint:disable identifier_name missing_docs
import Foundation
public struct PaywallComponentsData: Codable, Equatable, Sendable {
public struct ComponentsConfig: Codable, Equatable, Sendable {
public var base: PaywallComponentsConfig
public init(base: PaywallComponentsConfig) {
self.base = base
}
}
public struct PaywallComponentsConfig: Codable, Equatable, Sendable {
public var stack: PaywallComponent.StackComponent
public let stickyFooter: PaywallComponent.StickyFooterComponent?
public var background: PaywallComponent.Background
public init(
stack: PaywallComponent.StackComponent,
stickyFooter: PaywallComponent.StickyFooterComponent?,
background: PaywallComponent.Background
) {
self.stack = stack
self.stickyFooter = stickyFooter
self.background = background
}
}
public enum LocalizationData: Codable, Equatable, Sendable {
case string(String), image(PaywallComponent.ThemeImageUrls)
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let stringValue = try? container.decode(String.self) {
self = .string(stringValue)
} else if let imageValue = try? container.decode(PaywallComponent.ThemeImageUrls.self) {
self = .image(imageValue)
} else {
throw DecodingError.typeMismatch(
LocalizationData.self,
DecodingError.Context(codingPath: decoder.codingPath,
debugDescription: "Wrong type for LocalizationData")
)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let stringValue):
try container.encode(stringValue)
case .image(let imageValue):
try container.encode(imageValue)
}
}
}
public var templateName: String
/// The base remote URL where assets for this paywall are stored.
public var assetBaseURL: URL
/// The revision identifier for this paywall.
public var revision: Int {
get { return self._revision }
set { self._revision = newValue }
}
public var componentsConfig: ComponentsConfig
public var componentsLocalizations: [PaywallComponent.LocaleID: PaywallComponent.LocalizationDictionary]
public var defaultLocale: String
/// Exit offers configuration for this paywall.
public var exitOffers: ExitOffers?
@DefaultDecodable.Zero
internal private(set) var _revision: Int = 0
public var errorInfo: [String: EquatableError]?
private enum CodingKeys: String, CodingKey {
case templateName
case componentsConfig
case componentsLocalizations
case defaultLocale
case assetBaseURL = "assetBaseUrl"
case _revision = "revision"
case exitOffers
}
public init(templateName: String,
assetBaseURL: URL,
componentsConfig: ComponentsConfig,
componentsLocalizations: [PaywallComponent.LocaleID: PaywallComponent.LocalizationDictionary],
revision: Int,
defaultLocaleIdentifier: String,
exitOffers: ExitOffers? = nil) {
self.templateName = templateName
self.assetBaseURL = assetBaseURL
self.componentsConfig = componentsConfig
self.componentsLocalizations = componentsLocalizations
self._revision = revision
self.defaultLocale = defaultLocaleIdentifier
self.exitOffers = exitOffers
}
}
extension PaywallComponentsData {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var errors: [String: EquatableError] = [:]
do {
templateName = try container.decode(String.self, forKey: .templateName)
} catch {
errors["templateName"] = .init(error)
templateName = ""
}
do {
assetBaseURL = try container.decode(URL.self, forKey: .assetBaseURL)
} catch {
errors["assetBaseURL"] = .init(error)
// swiftlint:disable:next force_unwrapping
assetBaseURL = URL(string: "https://example.com")!
}
do {
componentsConfig = try container.decode(ComponentsConfig.self, forKey: .componentsConfig)
} catch {
errors["componentsConfig"] = .init(error)
componentsConfig = ComponentsConfig(base: PaywallComponentsConfig(
stack: .init(components: []),
stickyFooter: nil,
background: .color(.init(light: .hex("#ffffff")))
))
}
do {
componentsLocalizations = try container.decode(
[PaywallComponent.LocaleID: PaywallComponent.LocalizationDictionary].self,
forKey: .componentsLocalizations
)
} catch {
errors["componentsLocalizations"] = .init(error)
componentsLocalizations = [:]
}
do {
defaultLocale = try container.decode(String.self, forKey: .defaultLocale)
} catch {
errors["defaultLocale"] = .init(error)
defaultLocale = "en"
}
do {
_revision = try container.decode(Int.self, forKey: ._revision)
} catch {
errors["_revision"] = .init(error)
_revision = 0
}
exitOffers = try container.decodeIfPresent(ExitOffers.self, forKey: .exitOffers)
if !errors.isEmpty {
errorInfo = errors
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(templateName, forKey: .templateName)
try container.encode(assetBaseURL, forKey: .assetBaseURL)
try container.encode(componentsConfig, forKey: .componentsConfig)
try container.encode(componentsLocalizations, forKey: .componentsLocalizations)
try container.encode(defaultLocale, forKey: .defaultLocale)
try container.encode(_revision, forKey: ._revision)
try container.encodeIfPresent(exitOffers, forKey: .exitOffers)
}
}
extension PaywallComponentsData {
public struct EquatableError: Equatable, Sendable {
let description: String
init(_ error: Error) {
self.description = String(describing: error)
}
public static func == (lhs: EquatableError, rhs: EquatableError) -> Bool {
return lhs.description == rhs.description
}
}
}