Prechádzať zdrojové kódy

Move remaining paywall copy into paywall.json so marketing and error strings can be updated without code changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 týždňov pred
rodič
commit
4e7acf64a0

+ 5 - 30
Reddit App/Managers/SubscriptionManager.swift

@@ -13,29 +13,6 @@ enum PurchaseError: Equatable {
     case noActivePurchases
     case restoreSyncTimeout
     case generic
-
-    var message: String {
-        switch self {
-        case .productsLoadTimeout:
-            "Couldn't load subscription products right now. Check your StoreKit configuration / internet and try again."
-        case .subscriptionUnavailable:
-            "This subscription is currently unavailable. Please try again in a moment."
-        case .planUnavailable:
-            "This plan isn't available right now. Please try again later."
-        case .purchaseNotVerified:
-            "Apple couldn't verify this purchase."
-        case .purchasePending:
-            "This purchase is waiting for approval (for example, Ask to Buy)."
-        case .noActiveSubscriptions:
-            "No active subscriptions were found for this Apple ID."
-        case .noActivePurchases:
-            "No premium purchases were found for this Apple ID."
-        case .restoreSyncTimeout:
-            "Couldn't reach the App Store in time. Check your connection and tap Restore Purchases again."
-        case .generic:
-            "Something went wrong with your subscription. Please try again."
-        }
-    }
 }
 
 @MainActor
@@ -140,20 +117,18 @@ final class SubscriptionManager: ObservableObject {
     }
 
     func mainPrice(for plan: PaywallPlan) -> String {
-        product(for: plan)?.displayPrice ?? plan.fallbackMainPrice
+        let config = PaywallConfigService.shared.config
+        return product(for: plan)?.displayPrice ?? plan.planCopy(from: config).fallbackPrice
     }
 
     func priceSuffix(for plan: PaywallPlan) -> String? {
-        switch plan {
-        case .monthly: "/mo"
-        case .yearly: "/yr"
-        case .lifetime: nil
-        }
+        plan.planCopy(from: PaywallConfigService.shared.config).priceSuffix
     }
 
     func billingDescription(for plan: PaywallPlan) -> String {
+        let config = PaywallConfigService.shared.config
         guard let product = product(for: plan) else {
-            return plan.fallbackBillingDescription
+            return plan.fallbackBillingDescription(config: config)
         }
 
         if let subscription = product.subscription,

+ 47 - 2
Reddit App/Models/PaywallConfig.swift

@@ -8,19 +8,31 @@ struct PaywallConfig: Codable, Sendable {
         let subtitleTemplate: String
         let trialSubtitleTemplate: String?
         let ctaTemplate: String
+        let fallbackPrice: String
+        let priceSuffix: String?
+        let fallbackBillingDescription: String
+        let features: [String]
 
         init(
             title: String,
             badge: String? = nil,
             subtitleTemplate: String,
             trialSubtitleTemplate: String? = nil,
-            ctaTemplate: String
+            ctaTemplate: String,
+            fallbackPrice: String,
+            priceSuffix: String? = nil,
+            fallbackBillingDescription: String,
+            features: [String]
         ) {
             self.title = title
             self.badge = badge
             self.subtitleTemplate = subtitleTemplate
             self.trialSubtitleTemplate = trialSubtitleTemplate
             self.ctaTemplate = ctaTemplate
+            self.fallbackPrice = fallbackPrice
+            self.priceSuffix = priceSuffix
+            self.fallbackBillingDescription = fallbackBillingDescription
+            self.features = features
         }
     }
 
@@ -91,6 +103,12 @@ struct PaywallConfig: Codable, Sendable {
         let retry: String
         let retrying: String
         let periodFallback: String
+        let productsLoadTimeout: String
+        let subscriptionUnavailable: String
+        let planUnavailable: String
+        let noActiveSubscriptions: String
+        let restoreSyncTimeout: String
+        let purchaseGeneric: String
     }
 
     struct Sidebar: Codable, Sendable {
@@ -122,6 +140,8 @@ struct PaywallConfig: Codable, Sendable {
     let trialDisclosureTemplate: String
     let lifetimeDisclosure: String
     let loadingDisclosure: String
+    let securePaymentNote: String
+    let closeButtonHelp: String
 }
 
 extension PaywallConfig {
@@ -145,10 +165,35 @@ extension PaywallConfig {
             subscriptionDisclosureTemplate: subscriptionDisclosureTemplate,
             trialDisclosureTemplate: trialDisclosureTemplate,
             lifetimeDisclosure: lifetimeDisclosure,
-            loadingDisclosure: loadingDisclosure
+            loadingDisclosure: loadingDisclosure,
+            securePaymentNote: securePaymentNote,
+            closeButtonHelp: closeButtonHelp
         )
     }
 
+    func message(for error: PurchaseError) -> String {
+        switch error {
+        case .productsLoadTimeout:
+            messages.productsLoadTimeout
+        case .subscriptionUnavailable:
+            messages.subscriptionUnavailable
+        case .planUnavailable:
+            messages.planUnavailable
+        case .purchaseNotVerified:
+            messages.failedVerification
+        case .purchasePending:
+            messages.pendingPurchaseMessage
+        case .noActiveSubscriptions:
+            messages.noActiveSubscriptions
+        case .noActivePurchases:
+            messages.restoreNotFoundMessage
+        case .restoreSyncTimeout:
+            messages.restoreSyncTimeout
+        case .generic:
+            messages.purchaseGeneric
+        }
+    }
+
     static func loadBundled() -> PaywallConfig {
         guard let url = Bundle.main.url(forResource: "paywall", withExtension: "json"),
               let data = try? Data(contentsOf: url),

+ 6 - 93
Reddit App/Models/PaywallModels.swift

@@ -8,22 +8,6 @@ enum PaywallPlan: String, CaseIterable, Identifiable {
 
     var id: String { rawValue }
 
-    var title: String {
-        switch self {
-        case .monthly: "Monthly"
-        case .yearly: "Yearly"
-        case .lifetime: "Lifetime"
-        }
-    }
-
-    var subtitle: String {
-        switch self {
-        case .monthly: "Billed every month"
-        case .yearly: "Save 42%. Billed once per year."
-        case .lifetime: "One-time payment. All future updates."
-        }
-    }
-
     var productID: String {
         switch self {
         case .monthly: SubscriptionProductID.monthly
@@ -32,55 +16,6 @@ enum PaywallPlan: String, CaseIterable, Identifiable {
         }
     }
 
-    var fallbackMainPrice: String {
-        switch self {
-        case .monthly: "$7.99"
-        case .yearly: "$49.99"
-        case .lifetime: "$79.99"
-        }
-    }
-
-    var price: String { fallbackMainPrice }
-
-    var priceSuffix: String? {
-        switch self {
-        case .monthly: "/mo"
-        case .yearly: "/yr"
-        case .lifetime: nil
-        }
-    }
-
-    var ctaTitle: String {
-        switch self {
-        case .monthly: "Get Premium"
-        case .yearly: "Get Premium"
-        case .lifetime: "Unlock Lifetime"
-        }
-    }
-
-    var fallbackBillingDescription: String {
-        switch self {
-        case .monthly:
-            return "Billed at \(fallbackMainPrice) every month"
-        case .yearly:
-            return "Billed at \(fallbackMainPrice) every year"
-        case .lifetime:
-            return "One-time payment of \(fallbackMainPrice)"
-        }
-    }
-
-    var isBestValue: Bool {
-        self == .lifetime
-    }
-
-    var features: [String] {
-        [
-            "Unlimited AI Generations",
-            "Post Generator & Title Optimizer",
-            "Comment Writer with smart replies",
-        ]
-    }
-
     func planCopy(from config: PaywallConfig) -> PaywallConfig.PlanCopy {
         switch self {
         case .monthly: config.plans.monthly
@@ -90,7 +25,7 @@ enum PaywallPlan: String, CaseIterable, Identifiable {
     }
 
     func localizedPrice(from product: Product?, config: PaywallConfig) -> String {
-        product?.displayPrice ?? fallbackMainPrice
+        product?.displayPrice ?? planCopy(from: config).fallbackPrice
     }
 
     func localizedSubtitle(
@@ -156,32 +91,10 @@ enum PaywallPlan: String, CaseIterable, Identifiable {
                 .replacingOccurrences(of: "{period}", with: period)
         }
     }
-}
-
-struct PaywallFeature: Identifiable {
-    let id = UUID()
-    let title: String
-    let systemImage: String
-}
-
-enum PaywallContent {
-    static let title = "Unlock Full Pro Power"
-    static let subtitle = "Generate unlimited posts, optimize titles, and craft smarter replies with advanced AI."
 
-    static let includedSectionTitle = "Everything included in Premium"
-
-    static let features: [PaywallFeature] = [
-        PaywallFeature(title: "Unlimited AI Generations", systemImage: "sparkles"),
-        PaywallFeature(title: "Post Generator", systemImage: "square.and.pencil"),
-        PaywallFeature(title: "Title Optimizer", systemImage: "textformat.size"),
-        PaywallFeature(title: "Comment Writer", systemImage: "bubble.left.and.bubble.right.fill"),
-        PaywallFeature(title: "Custom AI Prompts", systemImage: "wand.and.stars"),
-        PaywallFeature(title: "Post Analytics", systemImage: "chart.bar.fill"),
-    ]
-
-    static let continueWithFreePlan = "Continue with free plan"
-    static let restorePurchases = "Restore Purchases"
-    static let privacyPolicy = "Privacy Policy"
-    static let support = "Support"
-    static let termsOfService = "Terms of Services"
+    func fallbackBillingDescription(config: PaywallConfig) -> String {
+        let copy = planCopy(from: config)
+        return copy.fallbackBillingDescription
+            .replacingOccurrences(of: "{price}", with: copy.fallbackPrice)
+    }
 }

+ 6 - 6
Reddit App/Views/PaywallView.swift

@@ -92,7 +92,7 @@ struct PaywallView: View {
         ) {
             Button(config.messages.alertOK, role: .cancel) {}
         } message: {
-            Text(subscriptions.purchaseError?.message ?? "")
+            Text(subscriptions.purchaseError.map { config.message(for: $0) } ?? "")
         }
     }
 
@@ -115,7 +115,7 @@ struct PaywallView: View {
                         )
                 }
                 .buttonStyle(AppPlainButtonStyle())
-                .help("Close")
+                .help(config.closeButtonHelp)
             }
             Spacer()
         }
@@ -250,7 +250,7 @@ struct PaywallView: View {
             HStack(spacing: 6) {
                 Image(systemName: "lock.fill")
                     .font(.system(size: metrics.footerNoteFontSize))
-                Text("Secure payment. Cancel anytime.")
+                Text(config.securePaymentNote)
                     .font(.system(size: metrics.footerNoteFontSize))
             }
             .foregroundStyle(AppTheme.textTertiary)
@@ -427,8 +427,8 @@ private struct PaywallPricingCard: View {
                         .font(.system(size: metrics.titleFontSize * 0.85, weight: .bold))
                         .foregroundStyle(AppTheme.textPrimary)
 
-                    if plan.priceSuffix != nil {
-                        Text(plan.priceSuffix ?? "")
+                    if planCopy.priceSuffix != nil {
+                        Text(planCopy.priceSuffix ?? "")
                             .font(.system(size: metrics.featureFontSize))
                             .foregroundStyle(AppTheme.textSecondary)
                     }
@@ -437,7 +437,7 @@ private struct PaywallPricingCard: View {
                 .padding(.bottom, metrics.cardPadding * 0.9)
 
                 VStack(alignment: .leading, spacing: metrics.featureRowSpacing * 0.7) {
-                    ForEach(plan.features, id: \.self) { feature in
+                    ForEach(planCopy.features, id: \.self) { feature in
                         HStack(alignment: .top, spacing: 8) {
                             Image(systemName: "checkmark")
                                 .font(.system(size: metrics.featureFontSize * 0.75, weight: .bold))

+ 37 - 5
Reddit App/paywall.json

@@ -41,19 +41,43 @@
     "monthly": {
       "title": "Monthly",
       "subtitleTemplate": "{price} / month, cancel anytime",
-      "ctaTemplate": "Get Premium for {price} / Month"
+      "ctaTemplate": "Get Premium for {price} / Month",
+      "fallbackPrice": "$7.99",
+      "priceSuffix": "/mo",
+      "fallbackBillingDescription": "Billed at {price} every month",
+      "features": [
+        "Unlimited AI Generations",
+        "Post Generator & Title Optimizer",
+        "Comment Writer with smart replies"
+      ]
     },
     "yearly": {
       "title": "Yearly",
       "subtitleTemplate": "{price} / year, cancel anytime",
       "trialSubtitleTemplate": "Get {trial} free, then {price} / year",
-      "ctaTemplate": "Get Premium for {price} / Year"
+      "ctaTemplate": "Get Premium for {price} / Year",
+      "fallbackPrice": "$49.99",
+      "priceSuffix": "/yr",
+      "fallbackBillingDescription": "Billed at {price} every year",
+      "features": [
+        "Unlimited AI Generations",
+        "Post Generator & Title Optimizer",
+        "Comment Writer with smart replies"
+      ]
     },
     "lifetime": {
       "title": "Lifetime",
       "badge": "BEST VALUE",
       "subtitleTemplate": "{price} once, lifetime access",
-      "ctaTemplate": "Unlock Lifetime for {price}"
+      "ctaTemplate": "Unlock Lifetime for {price}",
+      "fallbackPrice": "$79.99",
+      "priceSuffix": null,
+      "fallbackBillingDescription": "One-time payment of {price}",
+      "features": [
+        "Unlimited AI Generations",
+        "Post Generator & Title Optimizer",
+        "Comment Writer with smart replies"
+      ]
     }
   },
   "footer": {
@@ -97,7 +121,13 @@
     "alertOK": "OK",
     "retry": "Retry",
     "retrying": "Retrying…",
-    "periodFallback": "period"
+    "periodFallback": "period",
+    "productsLoadTimeout": "Couldn't load subscription products right now. Check your connection and try again.",
+    "subscriptionUnavailable": "This subscription is currently unavailable. Please try again in a moment.",
+    "planUnavailable": "This plan isn't available right now. Please try again later.",
+    "noActiveSubscriptions": "No active subscriptions were found for this Apple ID.",
+    "restoreSyncTimeout": "Couldn't reach the App Store in time. Check your connection and tap Restore Purchases again.",
+    "purchaseGeneric": "Something went wrong with your subscription. Please try again."
   },
   "sidebar": {
     "unlockTitle": "Upgrade to Premium",
@@ -113,5 +143,7 @@
   "subscriptionDisclosureTemplate": "Payment will be charged to your Apple ID. Subscription automatically renews at {price} per {period} unless canceled at least 24 hours before the end of the current period.",
   "trialDisclosureTemplate": "After your {trial} free trial, payment will be charged to your Apple ID. Subscription automatically renews at {price} per {period} unless canceled at least 24 hours before the end of the current period.",
   "lifetimeDisclosure": "One-time purchase. No subscription or recurring charges.",
-  "loadingDisclosure": "Payment will be charged to your Apple ID. Subscription automatically renews unless canceled at least 24 hours before the end of the current period."
+  "loadingDisclosure": "Payment will be charged to your Apple ID. Subscription automatically renews unless canceled at least 24 hours before the end of the current period.",
+  "securePaymentNote": "Secure payment. Cancel anytime.",
+  "closeButtonHelp": "Close"
 }