PaywallModels.swift 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import Foundation
  2. import StoreKit
  3. struct PaywallTrialDisplay: Equatable, Sendable {
  4. let badgeText: String
  5. let durationLowercased: String
  6. let ctaText: String
  7. static func from(offer: Product.SubscriptionOffer, config: PaywallConfig) -> PaywallTrialDisplay {
  8. let duration = config.trialDurationDescription(from: offer)
  9. let durationLowercased = config.trialDurationDescription(from: offer, lowercase: true)
  10. return PaywallTrialDisplay(
  11. badgeText: config.trialBadgeText(from: offer),
  12. durationLowercased: durationLowercased,
  13. ctaText: config.trialCTATitle(from: offer)
  14. )
  15. }
  16. static func from(fallback: PaywallConfig.TrialFallbackDuration, config: PaywallConfig) -> PaywallTrialDisplay? {
  17. guard let duration = config.trialDurationDescription(from: fallback),
  18. let durationLowercased = config.trialDurationDescription(from: fallback, lowercase: true) else {
  19. return nil
  20. }
  21. return PaywallTrialDisplay(
  22. badgeText: config.trialBadgeText(duration: duration),
  23. durationLowercased: durationLowercased,
  24. ctaText: config.trialCTATitle(duration: duration)
  25. )
  26. }
  27. }
  28. enum PaywallPlan: String, CaseIterable, Identifiable {
  29. case monthly
  30. case yearly
  31. case lifetime
  32. var id: String { rawValue }
  33. var productID: String {
  34. switch self {
  35. case .monthly: SubscriptionProductID.monthly
  36. case .yearly: SubscriptionProductID.yearly
  37. case .lifetime: SubscriptionProductID.lifetime
  38. }
  39. }
  40. func planCopy(from config: PaywallConfig) -> PaywallConfig.PlanCopy {
  41. switch self {
  42. case .monthly: config.plans.monthly
  43. case .yearly: config.plans.yearly
  44. case .lifetime: config.plans.lifetime
  45. }
  46. }
  47. func localizedPrice(from product: Product?, config: PaywallConfig) -> String {
  48. product?.displayPrice ?? config.loadingPrice
  49. }
  50. func localizedSubtitle(
  51. from product: Product?,
  52. config: PaywallConfig,
  53. trial: PaywallTrialDisplay? = nil
  54. ) -> String {
  55. guard let product else { return "" }
  56. let copy = planCopy(from: config)
  57. if let trial, let template = copy.trialSubtitleTemplate {
  58. return template
  59. .replacingOccurrences(of: "{price}", with: product.displayPrice)
  60. .replacingOccurrences(of: "{trial}", with: trial.durationLowercased)
  61. }
  62. return copy.subtitleTemplate.replacingOccurrences(of: "{price}", with: product.displayPrice)
  63. }
  64. func localizedCTATitle(
  65. from product: Product?,
  66. config: PaywallConfig,
  67. trial: PaywallTrialDisplay? = nil
  68. ) -> String {
  69. guard let product else { return config.loadingCTA }
  70. if let trial {
  71. return trial.ctaText
  72. }
  73. let copy = planCopy(from: config)
  74. return copy.ctaTemplate.replacingOccurrences(of: "{price}", with: product.displayPrice)
  75. }
  76. func localizedRenewalDisclosure(
  77. from product: Product?,
  78. config: PaywallConfig,
  79. trial: PaywallTrialDisplay? = nil
  80. ) -> String {
  81. switch self {
  82. case .lifetime:
  83. return config.lifetimeDisclosure
  84. case .monthly, .yearly:
  85. guard let product else { return config.loadingDisclosure }
  86. let price = product.displayPrice
  87. let period = product.subscription.map {
  88. config.subscriptionPeriodDescription(from: $0.subscriptionPeriod)
  89. } ?? config.messages.periodFallback
  90. if let trial {
  91. return config.trialDisclosureTemplate
  92. .replacingOccurrences(of: "{trial}", with: trial.durationLowercased)
  93. .replacingOccurrences(of: "{price}", with: price)
  94. .replacingOccurrences(of: "{period}", with: period)
  95. }
  96. return config.subscriptionDisclosureTemplate
  97. .replacingOccurrences(of: "{price}", with: price)
  98. .replacingOccurrences(of: "{period}", with: period)
  99. }
  100. }
  101. func fallbackBillingDescription(config: PaywallConfig) -> String {
  102. let copy = planCopy(from: config)
  103. return copy.fallbackBillingDescription
  104. .replacingOccurrences(of: "{price}", with: copy.fallbackPrice)
  105. }
  106. }