Forráskód Böngészése

Fix paywall currency consistency and savings badges

Remove hardcoded currency formatting so paywall prices follow StoreKit/localized display, and compute a dynamic yearly savings badge from monthly vs yearly pricing.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 hónapja
szülő
commit
befca59572
1 módosított fájl, 107 hozzáadás és 23 törlés
  1. 107 23
      meetings_app/ViewController.swift

+ 107 - 23
meetings_app/ViewController.swift

@@ -398,6 +398,8 @@ final class ViewController: NSViewController {
     private var paywallPurchaseTask: Task<Void, Never>?
     private var paywallPriceLabels: [PremiumPlan: NSTextField] = [:]
     private var paywallSubtitleLabels: [PremiumPlan: NSTextField] = [:]
+    private var paywallBadgeLabels: [PremiumPlan: NSTextField] = [:]
+    private var paywallStrikePriceLabels: [PremiumPlan: NSTextField] = [:]
     private var paywallContinueEnabled = true
     private var paywallUpgradeFlowEnabled = false
     private let launchPaywallDelay: TimeInterval = 3
@@ -1329,6 +1331,8 @@ private extension ViewController {
         paywallFooterActionByView.removeAll()
         paywallPriceLabels.removeAll()
         paywallSubtitleLabels.removeAll()
+        paywallBadgeLabels.removeAll()
+        paywallStrikePriceLabels.removeAll()
         paywallContinueLabel = nil
         paywallContinueButton = nil
         paywallContinueEnabled = true
@@ -2388,39 +2392,100 @@ private extension ViewController {
         }
         let productID = PremiumStoreProduct.productID(for: plan)
         if let product = storeKitCoordinator.productsByID[productID] {
-            let pkrPrice = pkrDisplayPrice(product.displayPrice)
+            let localizedPrice = localizedDisplayPrice(product.displayPrice)
             if product.type == .nonConsumable {
-                return "\(pkrPrice) one-time purchase"
+                return "\(localizedPrice) one-time purchase"
             }
             if let subscription = product.subscription {
-                let billingText = "\(pkrPrice)/\(subscriptionUnitText(subscription.subscriptionPeriod.unit))"
+                let billingText = "\(localizedPrice)/\(subscriptionUnitText(subscription.subscriptionPeriod.unit))"
                 if let introOffer = subscription.introductoryOffer,
                    introOffer.paymentMode == .freeTrial {
                     return "Free for \(subscriptionPeriodText(introOffer.period)), then \(billingText)"
                 }
                 return billingText
             }
-            return pkrPrice
+            return localizedPrice
         }
+        let fallbackPrice = fallbackPriceText(for: plan)
         switch plan {
         case .weekly:
-            return "PKR 1,100.00/week"
+            return "\(fallbackPrice)/week"
         case .monthly:
-            return "PKR 2,500.00/month"
+            return "\(fallbackPrice)/month"
         case .yearly:
-            return "PKR 9,900.00/year (3 days free trial)"
+            return "\(fallbackPrice)/year (3 days free trial)"
         case .lifetime:
-            return "PKR 14,900.00 one-time purchase"
+            return "\(fallbackPrice) one-time purchase"
         }
     }
 
-    private func pkrDisplayPrice(_ value: String) -> String {
-        if value.hasPrefix("PKR ") { return value }
-        if value.hasPrefix("Rs ") {
-            return "PKR " + value.dropFirst(3)
+    private func localizedDisplayPrice(_ value: String) -> String {
+        value.trimmingCharacters(in: .whitespacesAndNewlines)
+    }
+
+    private func fallbackPriceText(for plan: PremiumPlan) -> String {
+        let fallbackAmount: Decimal
+        switch plan {
+        case .weekly:
+            fallbackAmount = 1100
+        case .monthly:
+            fallbackAmount = 2500
+        case .yearly:
+            fallbackAmount = 9900
+        case .lifetime:
+            fallbackAmount = 14900
+        }
+        return localeCurrencyText(for: fallbackAmount)
+    }
+
+    private func fallbackLifetimeStrikePriceText() -> String {
+        localeCurrencyText(for: 29_800)
+    }
+
+    private func fallbackAmount(for plan: PremiumPlan) -> Decimal {
+        switch plan {
+        case .weekly:
+            return 1100
+        case .monthly:
+            return 2500
+        case .yearly:
+            return 9900
+        case .lifetime:
+            return 14900
         }
-        if value.contains("PKR") { return value }
-        return "PKR \(value)"
+    }
+
+    private func yearlySavingsBadgeText() -> String {
+        let yearlyProduct = storeKitCoordinator.productsByID[PremiumStoreProduct.yearly]
+        let monthlyProduct = storeKitCoordinator.productsByID[PremiumStoreProduct.monthly]
+
+        let yearlyAmount = yearlyProduct?.price ?? fallbackAmount(for: .yearly)
+        let monthlyAmount = monthlyProduct?.price ?? fallbackAmount(for: .monthly)
+        let yearlyCostIfMonthly = monthlyAmount * Decimal(12)
+        guard yearlyCostIfMonthly > 0 else { return "Save 0%" }
+
+        let savingsRatio = (yearlyCostIfMonthly - yearlyAmount) / yearlyCostIfMonthly
+        guard savingsRatio > 0 else { return "Save 0%" }
+        let savingsPercentDecimal = savingsRatio * Decimal(100)
+        let roundedPercent = Int(NSDecimalNumber(decimal: savingsPercentDecimal).doubleValue.rounded())
+        let savingsPercent = max(1, roundedPercent)
+        return "Save \(savingsPercent)%"
+    }
+
+    private func localeCurrencyText(for amount: Decimal) -> String {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .currency
+        formatter.locale = Locale.current
+
+        if formatter.currencyCode == nil {
+            formatter.currencyCode = "USD"
+        }
+        return formatter.string(from: amount as NSDecimalNumber) ?? "\(amount)"
+    }
+
+    private func strikePriceText(for product: Product) -> String {
+        let strikeAmount = product.price * Decimal(2)
+        return strikeAmount.formatted(product.priceFormatStyle)
     }
 
     private func subscriptionUnitText(_ unit: Product.SubscriptionPeriod.Unit) -> String {
@@ -2465,7 +2530,9 @@ private extension ViewController {
         for (plan, label) in paywallPriceLabels {
             let productID = PremiumStoreProduct.productID(for: plan)
             if let product = storeKitCoordinator.productsByID[productID] {
-                label.stringValue = pkrDisplayPrice(product.displayPrice)
+                label.stringValue = localizedDisplayPrice(product.displayPrice)
+            } else {
+                label.stringValue = fallbackPriceText(for: plan)
             }
         }
         for (plan, label) in paywallSubtitleLabels {
@@ -2476,13 +2543,24 @@ private extension ViewController {
                 label.stringValue = "Billed via App Store"
                 continue
             }
-            let recurringText = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
+            let recurringText = "\(localizedDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
             if let trialText = freeTrialPackageText(for: product) {
                 label.stringValue = "\(recurringText)  •  \(trialText)"
             } else {
                 label.stringValue = recurringText
             }
         }
+        for (plan, label) in paywallStrikePriceLabels {
+            guard plan == .lifetime else { continue }
+            let productID = PremiumStoreProduct.productID(for: plan)
+            if let product = storeKitCoordinator.productsByID[productID] {
+                label.stringValue = strikePriceText(for: product)
+            } else {
+                label.stringValue = fallbackLifetimeStrikePriceText()
+            }
+        }
+        paywallBadgeLabels[.yearly]?.stringValue = yearlySavingsBadgeText()
+        paywallBadgeLabels[.lifetime]?.stringValue = "Best Value"
         refreshSidebarPremiumButton()
         refreshInstantMeetPremiumState()
         updatePaywallPlanSelection()
@@ -5735,6 +5813,10 @@ private extension ViewController {
     func makePaywallContent() -> NSView {
         paywallPlanViews.removeAll()
         premiumPlanByView.removeAll()
+        paywallPriceLabels.removeAll()
+        paywallSubtitleLabels.removeAll()
+        paywallBadgeLabels.removeAll()
+        paywallStrikePriceLabels.removeAll()
 
         let panel = NSView()
         panel.translatesAutoresizingMaskIntoConstraints = false
@@ -5834,7 +5916,7 @@ private extension ViewController {
 
         let weeklyCard = paywallPlanCard(
             title: "Weekly",
-            price: "PKR 1,100.00",
+            price: fallbackPriceText(for: .weekly),
             badge: "Basic",
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
             subtitle: nil,
@@ -5843,7 +5925,7 @@ private extension ViewController {
         )
         let monthlyCard = paywallPlanCard(
             title: "Monthly",
-            price: "PKR 2,500.00",
+            price: fallbackPriceText(for: .monthly),
             badge: "Popular",
             badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
             subtitle: nil,
@@ -5852,8 +5934,8 @@ private extension ViewController {
         )
         let yearlyCard = paywallPlanCard(
             title: "Yearly",
-            price: "PKR 9,900.00",
-            badge: "Best Value",
+            price: fallbackPriceText(for: .yearly),
+            badge: yearlySavingsBadgeText(),
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
             subtitle: "3 days free trial",
             plan: .yearly,
@@ -5861,12 +5943,12 @@ private extension ViewController {
         )
         let lifetimeCard = paywallPlanCard(
             title: "Lifetime",
-            price: "PKR 14,900.00",
-            badge: "Save 50%",
+            price: fallbackPriceText(for: .lifetime),
+            badge: "Best Value",
             badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
             subtitle: nil,
             plan: .lifetime,
-            strikePrice: "PKR 29,800.00"
+            strikePrice: nil
         )
         plansRow.addArrangedSubview(weeklyCard)
         plansRow.addArrangedSubview(monthlyCard)
@@ -5997,6 +6079,7 @@ private extension ViewController {
         styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
 
         let badgeLabel = textLabel(badge, font: NSFont.systemFont(ofSize: 10, weight: .bold), color: .white)
+        paywallBadgeLabels[plan] = badgeLabel
         let badgeWrap = roundedContainer(cornerRadius: 10, color: badgeColor)
         badgeWrap.translatesAutoresizingMaskIntoConstraints = false
         badgeWrap.wantsLayer = true
@@ -6047,6 +6130,7 @@ private extension ViewController {
         if let strikePrice {
             let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
             card.addSubview(strike)
+            paywallStrikePriceLabels[plan] = strike
             NSLayoutConstraint.activate([
                 strike.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
                 strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4),