| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- import Foundation
- import StoreKit
- struct PaywallConfig: Codable, Sendable {
- struct PlanCopy: Codable, Sendable {
- let title: String
- let badge: String?
- let subtitleTemplate: String
- let trialSubtitleTemplate: String?
- let ctaTemplate: String
- init(
- title: String,
- badge: String? = nil,
- subtitleTemplate: String,
- trialSubtitleTemplate: String? = nil,
- ctaTemplate: String
- ) {
- self.title = title
- self.badge = badge
- self.subtitleTemplate = subtitleTemplate
- self.trialSubtitleTemplate = trialSubtitleTemplate
- self.ctaTemplate = ctaTemplate
- }
- }
- struct TrustItem: Codable, Sendable {
- let icon: String
- let title: String
- let subtitle: String
- }
- struct Footer: Codable, Sendable {
- let continueFree: String
- let manageSubscription: String
- let restorePurchase: String
- let privacyPolicy: String
- let termsOfService: String
- let support: String
- }
- struct URLs: Codable, Sendable {
- let privacy: String
- let terms: String
- let support: String
- let manageSubscriptions: String
- }
- struct Plans: Codable, Sendable {
- let monthly: PlanCopy
- let yearly: PlanCopy
- let lifetime: PlanCopy
- }
- struct DurationUnit: Codable, Sendable {
- let one: String
- let other: String
- let lowerOne: String
- let lowerOther: String
- }
- struct Duration: Codable, Sendable {
- let units: DurationUnits
- let countUnitTemplate: String
- let unknownFallback: String
- }
- struct DurationUnits: Codable, Sendable {
- let day: DurationUnit
- let week: DurationUnit
- let month: DurationUnit
- let year: DurationUnit
- }
- struct Trial: Codable, Sendable {
- let badgeTemplate: String
- let ctaTemplate: String
- }
- struct Messages: Codable, Sendable {
- let productNotFound: String
- let failedVerification: String
- let noPlansAvailable: String
- let plansLoadFailed: String
- let purchaseFailedTitle: String
- let restoreNotFoundTitle: String
- let restoreNotFoundMessage: String
- let pendingPurchaseTitle: String
- let pendingPurchaseMessage: String
- let alertOK: String
- let retry: String
- let retrying: String
- let periodFallback: String
- }
- struct Sidebar: Codable, Sendable {
- let unlockTitle: String
- let proTitle: String
- let unlockDescription: String
- let proDescription: String
- let upgradeAction: String
- let manageSubscriptionAction: String
- let lifetimeAction: String
- }
- let leftPanelTitle: String
- let rightTitle: String
- let rightSubtitle: String
- let features: [String]
- let trustItems: [TrustItem]
- let lifetimeTrustItem: TrustItem
- let plans: Plans
- let footer: Footer
- let urls: URLs
- let trial: Trial
- let duration: Duration
- let messages: Messages
- let sidebar: Sidebar
- let loadingPrice: String
- let loadingCTA: String
- let subscriptionDisclosureTemplate: String
- let trialDisclosureTemplate: String
- let lifetimeDisclosure: String
- let loadingDisclosure: String
- }
- extension PaywallConfig {
- func mergingMarketing(from remote: PaywallConfig) -> PaywallConfig {
- PaywallConfig(
- leftPanelTitle: remote.leftPanelTitle,
- rightTitle: remote.rightTitle,
- rightSubtitle: remote.rightSubtitle,
- features: remote.features,
- trustItems: remote.trustItems,
- lifetimeTrustItem: remote.lifetimeTrustItem,
- plans: remote.plans,
- footer: footer,
- urls: urls,
- trial: trial,
- duration: duration,
- messages: messages,
- sidebar: remote.sidebar,
- loadingPrice: loadingPrice,
- loadingCTA: loadingCTA,
- subscriptionDisclosureTemplate: subscriptionDisclosureTemplate,
- trialDisclosureTemplate: trialDisclosureTemplate,
- lifetimeDisclosure: lifetimeDisclosure,
- loadingDisclosure: loadingDisclosure
- )
- }
- static func loadBundled() -> PaywallConfig {
- guard let url = Bundle.main.url(forResource: "paywall", withExtension: "json"),
- let data = try? Data(contentsOf: url),
- let config = try? JSONDecoder().decode(PaywallConfig.self, from: data) else {
- fatalError("Missing or invalid paywall.json in app bundle.")
- }
- return config
- }
- func formatDuration(count: Int, unit: Product.SubscriptionPeriod.Unit, lowercase: Bool) -> String {
- guard count > 0 else {
- return duration.unknownFallback.replacingOccurrences(of: "{count}", with: "0")
- }
- let durationUnit: DurationUnit
- switch unit {
- case .day: durationUnit = duration.units.day
- case .week: durationUnit = duration.units.week
- case .month: durationUnit = duration.units.month
- case .year: durationUnit = duration.units.year
- @unknown default:
- return duration.unknownFallback.replacingOccurrences(of: "{count}", with: "\(count)")
- }
- let unitWord: String
- if count == 1 {
- unitWord = lowercase ? durationUnit.lowerOne : durationUnit.one
- } else {
- unitWord = lowercase ? durationUnit.lowerOther : durationUnit.other
- }
- if count == 1, lowercase {
- return unitWord
- }
- return duration.countUnitTemplate
- .replacingOccurrences(of: "{count}", with: "\(count)")
- .replacingOccurrences(of: "{unit}", with: unitWord)
- }
- func trialDurationDescription(from offer: Product.SubscriptionOffer, lowercase: Bool = false) -> String {
- let count = offer.period.value * offer.periodCount
- return formatDuration(count: count, unit: offer.period.unit, lowercase: lowercase)
- }
- func subscriptionPeriodDescription(from period: Product.SubscriptionPeriod) -> String {
- formatDuration(count: period.value, unit: period.unit, lowercase: true)
- }
- func trialBadgeText(from offer: Product.SubscriptionOffer) -> String {
- let duration = trialDurationDescription(from: offer)
- return trial.badgeTemplate.replacingOccurrences(of: "{duration}", with: duration)
- }
- func trialCTATitle(from offer: Product.SubscriptionOffer) -> String {
- let duration = trialDurationDescription(from: offer)
- return trial.ctaTemplate.replacingOccurrences(of: "{duration}", with: duration)
- }
- }
- extension PaywallConfig: Equatable {
- static func == (lhs: PaywallConfig, rhs: PaywallConfig) -> Bool {
- guard let lhsData = try? JSONEncoder().encode(lhs),
- let rhsData = try? JSONEncoder().encode(rhs) else {
- return false
- }
- return lhsData == rhsData
- }
- }
- extension Notification.Name {
- static let paywallConfigDidUpdate = Notification.Name("paywallConfigDidUpdate")
- }
|