import Foundation import StoreKit struct PaywallTrialDisplay: Equatable, Sendable { let badgeText: String let durationLowercased: String let ctaText: String static func from(offer: Product.SubscriptionOffer, config: PaywallConfig) -> PaywallTrialDisplay { let duration = config.trialDurationDescription(from: offer) let durationLowercased = config.trialDurationDescription(from: offer, lowercase: true) return PaywallTrialDisplay( badgeText: config.trialBadgeText(from: offer), durationLowercased: durationLowercased, ctaText: config.trialCTATitle(from: offer) ) } static func from(fallback: PaywallConfig.TrialFallbackDuration, config: PaywallConfig) -> PaywallTrialDisplay? { guard let duration = config.trialDurationDescription(from: fallback), let durationLowercased = config.trialDurationDescription(from: fallback, lowercase: true) else { return nil } return PaywallTrialDisplay( badgeText: config.trialBadgeText(duration: duration), durationLowercased: durationLowercased, ctaText: config.trialCTATitle(duration: duration) ) } } enum PaywallPlan: String, CaseIterable, Identifiable { case monthly case yearly case lifetime var id: String { rawValue } var productID: String { switch self { case .monthly: SubscriptionProductID.monthly case .yearly: SubscriptionProductID.yearly case .lifetime: SubscriptionProductID.lifetime } } func planCopy(from config: PaywallConfig) -> PaywallConfig.PlanCopy { switch self { case .monthly: config.plans.monthly case .yearly: config.plans.yearly case .lifetime: config.plans.lifetime } } func localizedPrice(from product: Product?, config: PaywallConfig) -> String { product?.displayPrice ?? config.loadingPrice } func localizedSubtitle( from product: Product?, config: PaywallConfig, trial: PaywallTrialDisplay? = nil ) -> String { guard let product else { return "" } let copy = planCopy(from: config) if let trial, let template = copy.trialSubtitleTemplate { return template .replacingOccurrences(of: "{price}", with: product.displayPrice) .replacingOccurrences(of: "{trial}", with: trial.durationLowercased) } return copy.subtitleTemplate.replacingOccurrences(of: "{price}", with: product.displayPrice) } func localizedCTATitle( from product: Product?, config: PaywallConfig, trial: PaywallTrialDisplay? = nil ) -> String { guard let product else { return config.loadingCTA } if let trial { return trial.ctaText } let copy = planCopy(from: config) return copy.ctaTemplate.replacingOccurrences(of: "{price}", with: product.displayPrice) } func localizedRenewalDisclosure( from product: Product?, config: PaywallConfig, trial: PaywallTrialDisplay? = nil ) -> String { switch self { case .lifetime: return config.lifetimeDisclosure case .monthly, .yearly: guard let product else { return config.loadingDisclosure } let price = product.displayPrice let period = product.subscription.map { config.subscriptionPeriodDescription(from: $0.subscriptionPeriod) } ?? config.messages.periodFallback if let trial { return config.trialDisclosureTemplate .replacingOccurrences(of: "{trial}", with: trial.durationLowercased) .replacingOccurrences(of: "{price}", with: price) .replacingOccurrences(of: "{period}", with: period) } return config.subscriptionDisclosureTemplate .replacingOccurrences(of: "{price}", with: price) .replacingOccurrences(of: "{period}", with: period) } } func fallbackBillingDescription(config: PaywallConfig) -> String { let copy = planCopy(from: config) return copy.fallbackBillingDescription .replacingOccurrences(of: "{price}", with: copy.fallbackPrice) } }