Переглянути джерело

Sync paywall plans with StoreKit and remote config.

Use localized StoreKit prices, dynamically add or remove plan cards from available products, and refresh paywall copy from remote config in all builds.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 2 тижнів тому
батько
коміт
b2e789ccd5

+ 0 - 2
smart_printer/PaywallConfigService.swift

@@ -18,9 +18,7 @@ final class PaywallConfigService {
     func start() {
     func start() {
         guard !hasStarted else { return }
         guard !hasStarted else { return }
         hasStarted = true
         hasStarted = true
-        #if DEBUG
         Task { await refreshFromRemote() }
         Task { await refreshFromRemote() }
-        #endif
     }
     }
 
 
     func refreshFromRemote() async {
     func refreshFromRemote() async {

+ 87 - 0
smart_printer/PaywallModels.swift

@@ -1,6 +1,93 @@
 import Foundation
 import Foundation
 import StoreKit
 import StoreKit
 
 
+enum PaywallPlan: String, CaseIterable {
+    case monthly
+    case yearly
+    case lifetime
+
+    static let displayOrder: [PaywallPlan] = [.monthly, .yearly, .lifetime]
+
+    var productID: String {
+        switch self {
+        case .monthly: StoreProductID.monthly
+        case .yearly: StoreProductID.yearly
+        case .lifetime: StoreProductID.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 config.loadingPrice }
+        let copy = planCopy(from: config)
+        let price = product.displayPrice
+
+        if let trial, let trialTemplate = copy.trialSubtitleTemplate {
+            return trialTemplate
+                .replacingOccurrences(of: "{trial}", with: trial.durationLowercased)
+                .replacingOccurrences(of: "{price}", with: price)
+        }
+
+        return copy.subtitleTemplate.replacingOccurrences(of: "{price}", with: price)
+    }
+
+    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)
+        }
+    }
+}
+
 struct PaywallTrialDisplay: Equatable, Sendable {
 struct PaywallTrialDisplay: Equatable, Sendable {
     let badgeText: String
     let badgeText: String
     let durationLowercased: String
     let durationLowercased: String

+ 85 - 147
smart_printer/PaywallView.swift

@@ -1,93 +1,6 @@
 import Cocoa
 import Cocoa
 import StoreKit
 import StoreKit
 
 
-// MARK: - Plan Model
-
-enum PaywallPlan: String, CaseIterable {
-    case monthly
-    case yearly
-    case lifetime
-
-    var productID: String {
-        switch self {
-        case .monthly: StoreProductID.monthly
-        case .yearly: StoreProductID.yearly
-        case .lifetime: StoreProductID.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 ?? planCopy(from: config).fallbackPrice
-    }
-
-    func localizedSubtitle(
-        from product: Product?,
-        config: PaywallConfig,
-        trial: PaywallTrialDisplay? = nil
-    ) -> String {
-        guard let product else { return config.loadingPrice }
-        let copy = planCopy(from: config)
-        let price = product.displayPrice
-
-        if let trial, let trialTemplate = copy.trialSubtitleTemplate {
-            return trialTemplate
-                .replacingOccurrences(of: "{trial}", with: trial.durationLowercased)
-                .replacingOccurrences(of: "{price}", with: price)
-        }
-
-        return copy.subtitleTemplate.replacingOccurrences(of: "{price}", with: price)
-    }
-
-    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)
-        }
-    }
-}
-
 // MARK: - StoreKit
 // MARK: - StoreKit
 
 
 enum StoreProductID {
 enum StoreProductID {
@@ -160,14 +73,17 @@ final class StoreManager {
     }
     }
 
 
     var products: [Product] {
     var products: [Product] {
-        StoreProductID.all.compactMap { productsByID[$0] }
+        availablePlans.compactMap { productsByID[$0.productID] }
+    }
+
+    var availablePlans: [PaywallPlan] {
+        PaywallPlan.displayOrder.filter { productsByID[$0.productID] != nil }
     }
     }
 
 
     var isResolvingEntitlements: Bool { !hasResolvedPremiumStatus }
     var isResolvingEntitlements: Bool { !hasResolvedPremiumStatus }
     var hasResolvedPremiumStatus = false
     var hasResolvedPremiumStatus = false
-    var hasAllProductsLoaded: Bool {
-        StoreProductID.all.allSatisfy { productsByID[$0] != nil }
-    }
+    var hasAvailablePlans: Bool { !availablePlans.isEmpty }
+    var hasAllProductsLoaded: Bool { hasAvailablePlans }
 
 
     private(set) var isLoadingProducts = false
     private(set) var isLoadingProducts = false
     private(set) var productLoadError: String?
     private(set) var productLoadError: String?
@@ -432,10 +348,10 @@ final class StoreManager {
     }
     }
 
 
     private func performLoadProducts(presentLoadingUI: Bool?) async {
     private func performLoadProducts(presentLoadingUI: Bool?) async {
-        let missingAny = StoreProductID.all.contains { productsByID[$0] == nil }
-        let shouldShowLoading = presentLoadingUI ?? missingAny
+        let shouldReload = presentLoadingUI == true || productsByID.isEmpty
+        let shouldShowLoading = presentLoadingUI ?? productsByID.isEmpty
 
 
-        if !missingAny, presentLoadingUI != true {
+        if !shouldReload, presentLoadingUI != true {
             purchaseError = nil
             purchaseError = nil
             await refreshPremiumAccess()
             await refreshPremiumAccess()
             await refreshTrialOffers()
             await refreshTrialOffers()
@@ -1077,7 +993,6 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
     private let titleLabel = NSTextField(labelWithString: "")
     private let titleLabel = NSTextField(labelWithString: "")
     private let subtitleLabel = NSTextField(labelWithString: "")
     private let subtitleLabel = NSTextField(labelWithString: "")
     private let priceLabel = NSTextField(labelWithString: "")
     private let priceLabel = NSTextField(labelWithString: "")
-    private var badgeView: PaywallBadgeView?
     private var trialBadgeView: PaywallBadgeView?
     private var trialBadgeView: PaywallBadgeView?
     private var hoverTracker: HoverTracker?
     private var hoverTracker: HoverTracker?
     private var isHovered = false
     private var isHovered = false
@@ -1128,21 +1043,6 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
             priceLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
             priceLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
         ])
         ])
 
 
-        if plan == .lifetime, let badgeText = plan.planCopy(from: config).badge {
-            let badge = PaywallBadgeView(
-                text: badgeText,
-                iconName: "star.fill",
-                background: AppTheme.paywallGold,
-                foreground: AppTheme.paywallGoldText
-            )
-            badgeView = badge
-            addSubview(badge)
-            NSLayoutConstraint.activate([
-                badge.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
-                badge.topAnchor.constraint(equalTo: topAnchor, constant: 8),
-            ])
-        }
-
         applyCardShadow()
         applyCardShadow()
         updateAppearance()
         updateAppearance()
 
 
@@ -1162,22 +1062,9 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
         titleLabel.stringValue = plan.planCopy(from: config).title
         titleLabel.stringValue = plan.planCopy(from: config).title
         subtitleLabel.stringValue = plan.localizedSubtitle(from: product, config: config, trial: trial)
         subtitleLabel.stringValue = plan.localizedSubtitle(from: product, config: config, trial: trial)
         priceLabel.stringValue = plan.localizedPrice(from: product, config: config)
         priceLabel.stringValue = plan.localizedPrice(from: product, config: config)
-        updateLifetimeBadge(config: config)
         updateTrialBadge(trial: trial)
         updateTrialBadge(trial: trial)
     }
     }
 
 
-    private func updateLifetimeBadge(config: PaywallConfig) {
-        guard plan == .lifetime else { return }
-        if let badgeText = plan.planCopy(from: config).badge {
-            if let badgeView {
-                badgeView.updateText(badgeText)
-                badgeView.isHidden = false
-            }
-        } else {
-            badgeView?.isHidden = true
-        }
-    }
-
     private func updateTrialBadge(trial: PaywallTrialDisplay?) {
     private func updateTrialBadge(trial: PaywallTrialDisplay?) {
         guard plan == PaywallConfigService.shared.config.trialEligiblePlan else { return }
         guard plan == PaywallConfigService.shared.config.trialEligiblePlan else { return }
 
 
@@ -1594,6 +1481,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
     private var productsErrorView: PaywallProductsErrorView!
     private var productsErrorView: PaywallProductsErrorView!
     private var plansStack: NSStackView!
     private var plansStack: NSStackView!
     private var plansStackHeightConstraint: NSLayoutConstraint!
     private var plansStackHeightConstraint: NSLayoutConstraint!
+    private var plansAreaViewHeightConstraint: NSLayoutConstraint!
     private var productsErrorHeightConstraint: NSLayoutConstraint!
     private var productsErrorHeightConstraint: NSLayoutConstraint!
     private var storeObservers: [NSObjectProtocol] = []
     private var storeObservers: [NSObjectProtocol] = []
 
 
@@ -1699,6 +1587,9 @@ final class PaywallView: NSView, AppearanceRefreshable {
     private func refreshProductDisplay() {
     private func refreshProductDisplay() {
         let store = StoreManager.shared
         let store = StoreManager.shared
         let config = paywallConfig
         let config = paywallConfig
+        let availablePlans = store.availablePlans
+        syncPlanCards(with: availablePlans)
+
         for (plan, card) in planCards {
         for (plan, card) in planCards {
             let product = store.product(for: plan)
             let product = store.product(for: plan)
             card.updateDisplay(
             card.updateDisplay(
@@ -1707,6 +1598,10 @@ final class PaywallView: NSView, AppearanceRefreshable {
                 trial: store.trialDisplay(for: plan)
                 trial: store.trialDisplay(for: plan)
             )
             )
         }
         }
+
+        ensureValidSelectedPlan(from: availablePlans)
+        refreshPlansLayout(planCount: availablePlans.count)
+
         ctaButton.title = selectedPlan.localizedCTATitle(
         ctaButton.title = selectedPlan.localizedCTATitle(
             from: store.product(for: selectedPlan),
             from: store.product(for: selectedPlan),
             config: config,
             config: config,
@@ -1726,6 +1621,63 @@ final class PaywallView: NSView, AppearanceRefreshable {
         selectTrialEligiblePlanIfAvailable()
         selectTrialEligiblePlanIfAvailable()
     }
     }
 
 
+    private func syncPlanCards(with plans: [PaywallPlan]) {
+        guard let plansStack else { return }
+
+        let removedPlans = planCards.keys.filter { !plans.contains($0) }
+        for plan in removedPlans {
+            guard let card = planCards.removeValue(forKey: plan) else { continue }
+            plansStack.removeArrangedSubview(card)
+            card.removeFromSuperview()
+        }
+
+        for plan in plans where planCards[plan] == nil {
+            let card = PaywallPlanCard(plan: plan)
+            card.isChosen = plan == selectedPlan
+            card.onSelect = { [weak self] in self?.selectPlan(plan) }
+            planCards[plan] = card
+        }
+
+        for view in plansStack.arrangedSubviews {
+            plansStack.removeArrangedSubview(view)
+        }
+        for plan in plans {
+            if let card = planCards[plan] {
+                plansStack.addArrangedSubview(card)
+            }
+        }
+    }
+
+    private func ensureValidSelectedPlan(from availablePlans: [PaywallPlan]) {
+        guard !availablePlans.isEmpty else { return }
+        guard !availablePlans.contains(selectedPlan) else { return }
+
+        let preferred = paywallConfig.trialEligiblePlan
+            .flatMap { trialPlan in
+                availablePlans.contains(trialPlan) && StoreManager.shared.trialDisplay(for: trialPlan) != nil
+                    ? trialPlan
+                    : nil
+            }
+            ?? availablePlans.first(where: { $0 == .yearly })
+            ?? availablePlans[0]
+
+        selectPlan(preferred)
+    }
+
+    private func refreshPlansLayout(planCount: Int) {
+        let height = plansAreaHeight(for: planCount)
+        plansStackHeightConstraint?.constant = height
+        plansAreaViewHeightConstraint?.constant = height
+    }
+
+    private func plansAreaHeight(for planCount: Int) -> CGFloat {
+        let cardHeight: CGFloat = 86
+        let spacing: CGFloat = 12
+        let count = CGFloat(max(planCount, 0))
+        guard count > 0 else { return 0 }
+        return count * cardHeight + max(0, count - 1) * spacing
+    }
+
     private func refreshSecurePaymentNote() {
     private func refreshSecurePaymentNote() {
         let config = paywallConfig
         let config = paywallConfig
         securePaymentLabel.stringValue = selectedPlan == .lifetime
         securePaymentLabel.stringValue = selectedPlan == .lifetime
@@ -1735,12 +1687,13 @@ final class PaywallView: NSView, AppearanceRefreshable {
 
 
     private func refreshProductsErrorState() {
     private func refreshProductsErrorState() {
         let store = StoreManager.shared
         let store = StoreManager.shared
-        let hasLoadFailure = store.productLoadError != nil && !store.hasAllProductsLoaded
+        let hasLoadFailure = store.productLoadError != nil && !store.hasAvailablePlans
         let isRetrying = hasLoadFailure && store.isLoadingProducts
         let isRetrying = hasLoadFailure && store.isLoadingProducts
 
 
         productsErrorView.isHidden = !hasLoadFailure
         productsErrorView.isHidden = !hasLoadFailure
-        plansStack.isHidden = hasLoadFailure
-        plansStackHeightConstraint.constant = hasLoadFailure ? 0 : plansAreaHeight
+        plansStack.isHidden = hasLoadFailure || store.availablePlans.isEmpty
+        let visiblePlanCount = hasLoadFailure ? 0 : store.availablePlans.count
+        refreshPlansLayout(planCount: visiblePlanCount)
         productsErrorHeightConstraint.constant = hasLoadFailure ? productsErrorAreaHeight : 0
         productsErrorHeightConstraint.constant = hasLoadFailure ? productsErrorAreaHeight : 0
 
 
         if hasLoadFailure, let error = store.productLoadError {
         if hasLoadFailure, let error = store.productLoadError {
@@ -1748,18 +1701,11 @@ final class PaywallView: NSView, AppearanceRefreshable {
         }
         }
     }
     }
 
 
-    private var plansAreaHeight: CGFloat {
-        let cardHeight: CGFloat = 86
-        let spacing: CGFloat = 12
-        let planCount = CGFloat(PaywallPlan.allCases.count)
-        return planCount * cardHeight + max(0, planCount - 1) * spacing
-    }
-
     private var productsErrorAreaHeight: CGFloat { 86 }
     private var productsErrorAreaHeight: CGFloat { 86 }
 
 
     private func refreshPurchaseState() {
     private func refreshPurchaseState() {
         let store = StoreManager.shared
         let store = StoreManager.shared
-        let hasProductLoadFailure = store.productLoadError != nil && !store.hasAllProductsLoaded
+        let hasProductLoadFailure = store.productLoadError != nil && !store.hasAvailablePlans
         let isProductReady = store.product(for: selectedPlan) != nil
         let isProductReady = store.product(for: selectedPlan) != nil
         let isBusy = store.isPurchasing || store.isLoadingProducts
         let isBusy = store.isPurchasing || store.isLoadingProducts
         let isDisabled = isBusy || !isProductReady || hasProductLoadFailure
         let isDisabled = isBusy || !isProductReady || hasProductLoadFailure
@@ -1956,14 +1902,6 @@ final class PaywallView: NSView, AppearanceRefreshable {
         }
         }
         self.productsErrorView = productsErrorView
         self.productsErrorView = productsErrorView
 
 
-        for plan in PaywallPlan.allCases {
-            let card = PaywallPlanCard(plan: plan)
-            card.isChosen = plan == selectedPlan
-            card.onSelect = { [weak self] in self?.selectPlan(plan) }
-            planCards[plan] = card
-            plansStack.addArrangedSubview(card)
-        }
-
         ctaButton.title = selectedPlan.localizedCTATitle(
         ctaButton.title = selectedPlan.localizedCTATitle(
             from: StoreManager.shared.product(for: selectedPlan),
             from: StoreManager.shared.product(for: selectedPlan),
             config: config,
             config: config,
@@ -2009,8 +1947,9 @@ final class PaywallView: NSView, AppearanceRefreshable {
         plansAreaView.addSubview(plansStack)
         plansAreaView.addSubview(plansStack)
         plansAreaView.addSubview(productsErrorView)
         plansAreaView.addSubview(productsErrorView)
 
 
-        plansStackHeightConstraint = plansStack.heightAnchor.constraint(equalToConstant: plansAreaHeight)
+        plansStackHeightConstraint = plansStack.heightAnchor.constraint(equalToConstant: 0)
         productsErrorHeightConstraint = productsErrorView.heightAnchor.constraint(equalToConstant: 0)
         productsErrorHeightConstraint = productsErrorView.heightAnchor.constraint(equalToConstant: 0)
+        plansAreaViewHeightConstraint = plansAreaView.heightAnchor.constraint(equalToConstant: 0)
 
 
         panel.addSubview(title)
         panel.addSubview(title)
         panel.addSubview(subtitle)
         panel.addSubview(subtitle)
@@ -2034,7 +1973,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
             plansAreaView.leadingAnchor.constraint(equalTo: title.leadingAnchor),
             plansAreaView.leadingAnchor.constraint(equalTo: title.leadingAnchor),
             plansAreaView.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
             plansAreaView.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
             plansAreaView.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 24),
             plansAreaView.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 24),
-            plansAreaView.heightAnchor.constraint(equalToConstant: plansAreaHeight),
+            plansAreaViewHeightConstraint,
 
 
             plansStack.leadingAnchor.constraint(equalTo: plansAreaView.leadingAnchor),
             plansStack.leadingAnchor.constraint(equalTo: plansAreaView.leadingAnchor),
             plansStack.trailingAnchor.constraint(equalTo: plansAreaView.trailingAnchor),
             plansStack.trailingAnchor.constraint(equalTo: plansAreaView.trailingAnchor),
@@ -2346,7 +2285,7 @@ final class PaywallOverlayView: NSView, AppearanceRefreshable {
 
 
     private func refreshLoadingOverlay() {
     private func refreshLoadingOverlay() {
         let store = StoreManager.shared
         let store = StoreManager.shared
-        let shouldShow = store.isLoadingProducts && !store.hasAllProductsLoaded && store.productLoadError == nil
+        let shouldShow = store.isLoadingProducts && !store.hasAvailablePlans && store.productLoadError == nil
         loadingOverlay.isHidden = !shouldShow
         loadingOverlay.isHidden = !shouldShow
         if shouldShow {
         if shouldShow {
             loadingIndicator.startAnimation(nil)
             loadingIndicator.startAnimation(nil)
@@ -2470,9 +2409,8 @@ final class PaywallOverlayView: NSView, AppearanceRefreshable {
         }
         }
 
 
         Task { @MainActor in
         Task { @MainActor in
-            #if DEBUG
             await PaywallConfigService.shared.refreshFromRemote()
             await PaywallConfigService.shared.refreshFromRemote()
-            #endif
+            paywallView.refreshStoreState()
         }
         }
     }
     }
 
 

+ 5 - 6
smart_printer/paywall.json

@@ -42,7 +42,7 @@
       "title": "Monthly",
       "title": "Monthly",
       "subtitleTemplate": "{price} / month, cancel anytime",
       "subtitleTemplate": "{price} / month, cancel anytime",
       "ctaTemplate": "Subscribe for {price} / Month",
       "ctaTemplate": "Subscribe for {price} / Month",
-      "fallbackPrice": "$4.99",
+      "fallbackPrice": "",
       "priceSuffix": "/mo",
       "priceSuffix": "/mo",
       "fallbackBillingDescription": "Billed at {price} every month",
       "fallbackBillingDescription": "Billed at {price} every month",
       "features": [
       "features": [
@@ -56,7 +56,7 @@
       "subtitleTemplate": "{price} / year, cancel anytime",
       "subtitleTemplate": "{price} / year, cancel anytime",
       "trialSubtitleTemplate": "{trial} free, then {price} / year",
       "trialSubtitleTemplate": "{trial} free, then {price} / year",
       "ctaTemplate": "Subscribe for {price} / Year",
       "ctaTemplate": "Subscribe for {price} / Year",
-      "fallbackPrice": "$29.99",
+      "fallbackPrice": "",
       "priceSuffix": "/yr",
       "priceSuffix": "/yr",
       "fallbackBillingDescription": "Billed at {price} every year",
       "fallbackBillingDescription": "Billed at {price} every year",
       "features": [
       "features": [
@@ -67,10 +67,9 @@
     },
     },
     "lifetime": {
     "lifetime": {
       "title": "Lifetime",
       "title": "Lifetime",
-      "badge": "Best Value",
       "subtitleTemplate": "{price} once, lifetime access",
       "subtitleTemplate": "{price} once, lifetime access",
       "ctaTemplate": "Buy Lifetime Access for {price}",
       "ctaTemplate": "Buy Lifetime Access for {price}",
-      "fallbackPrice": "$99.99",
+      "fallbackPrice": "",
       "priceSuffix": null,
       "priceSuffix": null,
       "fallbackBillingDescription": "One-time payment of {price}",
       "fallbackBillingDescription": "One-time payment of {price}",
       "features": [
       "features": [
@@ -99,8 +98,8 @@
     "badgeTemplate": "{duration} Free Trial",
     "badgeTemplate": "{duration} Free Trial",
     "ctaTemplate": "Start {duration} Free Trial",
     "ctaTemplate": "Start {duration} Free Trial",
     "fallbackDuration": {
     "fallbackDuration": {
-      "count": 1,
-      "unit": "week"
+      "count": 3,
+      "unit": "day"
     }
     }
   },
   },
   "duration": {
   "duration": {