Ver Fonte

Fix paywall showing placeholder prices before StoreKit loads.

Preload and apply cached product prices before presenting the sheet, and defer entitlement refresh so pricing is not blocked.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 há 1 mês atrás
pai
commit
d543ea97f0

+ 32 - 5
App for Indeed/Controllers/PremiumPlansWindowController.swift

@@ -25,6 +25,14 @@ final class PremiumPlansWindowController: NSWindowController {
     required init?(coder: NSCoder) {
         nil
     }
+
+    /// Loads StoreKit prices into the paywall view before the sheet is shown (avoids "—" placeholders flashing in).
+    @MainActor
+    func prepareForPresentation() async {
+        _ = window
+        guard let viewController = window?.contentViewController as? PremiumPlansViewController else { return }
+        await viewController.prepareStorePricingForDisplay()
+    }
 }
 
 private final class PremiumPlansViewController: NSViewController {
@@ -499,6 +507,7 @@ private final class PremiumPlansViewController: NSViewController {
     private var subscriptionStatusObservation: NSObjectProtocol?
     private var appearanceObserver: NSObjectProtocol?
     private var languageObserver: NSObjectProtocol?
+    private var storeProductsLoadTask: Task<Void, Never>?
 
     /// Core Pro capabilities shown on every pricing card (replaces generic “All premium features”).
     private var proCapabilityFeatures: [String] {
@@ -607,15 +616,30 @@ private final class PremiumPlansViewController: NSViewController {
             queue: .main
         ) { [weak self] _ in
             Task { @MainActor in
-                await self?.subscriptionStore.loadProducts()
+                await self?.subscriptionStore.ensureProductsLoaded()
                 self?.applyStorePricing()
                 self?.updateSubscriptionPrimaryFooter()
                 self?.updatePremiumCloseButtonVisibility()
             }
         }
-        Task { @MainActor in
-            await loadStoreProducts()
+        applyStorePricing()
+        storeProductsLoadTask = Task { @MainActor [weak self] in
+            await self?.loadStoreProducts()
+            self?.storeProductsLoadTask = nil
+        }
+    }
+
+    /// Ensures localized prices are on screen; reuses an in-flight load started from `viewDidLoad`.
+    @MainActor
+    func prepareStorePricingForDisplay() async {
+        applyStorePricing()
+        if let storeProductsLoadTask {
+            await storeProductsLoadTask.value
+            applyStorePricing()
+            return
         }
+        await subscriptionStore.ensureProductsLoaded()
+        applyStorePricing()
     }
 
     override func viewDidLayout() {
@@ -1156,9 +1180,12 @@ private final class PremiumPlansViewController: NSViewController {
     }
 
     private func loadStoreProducts() async {
-        await subscriptionStore.refreshEntitlements(deep: true)
-        await subscriptionStore.loadProducts()
         applyStorePricing()
+        await subscriptionStore.ensureProductsLoaded()
+        applyStorePricing()
+        updateSubscriptionPrimaryFooter()
+        updatePremiumCloseButtonVisibility()
+        await subscriptionStore.refreshEntitlements(deep: true)
         updateSubscriptionPrimaryFooter()
         updatePremiumCloseButtonVisibility()
     }

+ 6 - 0
App for Indeed/Subscription/SubscriptionStore.swift

@@ -54,6 +54,12 @@ final class SubscriptionStore {
         }
     }
 
+    /// Fetches the App Store product catalog only when it is not already in memory (e.g. after launch preload).
+    func ensureProductsLoaded() async {
+        guard productsByID.isEmpty else { return }
+        await loadProducts()
+    }
+
     func product(forPlanKey planKey: String) -> Product? {
         guard let id = SubscriptionProductIDs.productID(planKey: planKey) else { return nil }
         return productsByID[id]

+ 31 - 20
App for Indeed/Views/DashboardView.swift

@@ -185,6 +185,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     private var chatThinkingRowHost: NSView?
     private let jobSearchService = OpenAIJobSearchService()
     private var premiumPlansWindowController: PremiumPlansWindowController?
+    private var isPreparingPremiumPlansSheet = false
     private var indeedJobBrowserViewController: IndeedJobBrowserViewController?
     private var isIndeedJobBrowserPresented = false
     private weak var sidebarUpgradeCard: NSView?
@@ -777,31 +778,41 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
 
     private func presentPremiumPlansSheet() {
         guard let hostWindow = window else { return }
+        if isPreparingPremiumPlansSheet { return }
 
-        if premiumPlansWindowController == nil {
-            premiumPlansWindowController = PremiumPlansWindowController()
-        }
-        guard let paywallWindow = premiumPlansWindowController?.window else { return }
+        isPreparingPremiumPlansSheet = true
+        Task { @MainActor [weak self] in
+            defer { self?.isPreparingPremiumPlansSheet = false }
+            guard let self else { return }
 
-        if hostWindow.attachedSheet === paywallWindow {
-            return
-        }
+            if self.premiumPlansWindowController == nil {
+                self.premiumPlansWindowController = PremiumPlansWindowController()
+            }
+            guard let controller = self.premiumPlansWindowController else { return }
+            await controller.prepareForPresentation()
 
-        paywallWindow.styleMask = [.borderless, .closable, .resizable]
-        paywallWindow.isOpaque = true
-        paywallWindow.backgroundColor = PremiumPlansWindowController.paywallSheetBackground
+            guard let paywallWindow = controller.window else { return }
 
-        let hostContentRect = hostWindow.contentRect(forFrameRect: hostWindow.frame)
-        let overscan = PremiumSheetLayout.overscanPerEdge
-        var expandedContentRect = hostContentRect.insetBy(dx: -overscan, dy: -overscan)
-        expandedContentRect.size.height += PremiumSheetLayout.overscanExtraTop
-        let paywallFrame = paywallWindow.frameRect(forContentRect: expandedContentRect)
-        paywallWindow.setFrame(paywallFrame, display: false)
-        let lockedSize = paywallWindow.frame.size
-        paywallWindow.minSize = lockedSize
-        paywallWindow.maxSize = lockedSize
+            if hostWindow.attachedSheet === paywallWindow {
+                return
+            }
+
+            paywallWindow.styleMask = [.borderless, .closable, .resizable]
+            paywallWindow.isOpaque = true
+            paywallWindow.backgroundColor = PremiumPlansWindowController.paywallSheetBackground
 
-        hostWindow.beginSheet(paywallWindow)
+            let hostContentRect = hostWindow.contentRect(forFrameRect: hostWindow.frame)
+            let overscan = PremiumSheetLayout.overscanPerEdge
+            var expandedContentRect = hostContentRect.insetBy(dx: -overscan, dy: -overscan)
+            expandedContentRect.size.height += PremiumSheetLayout.overscanExtraTop
+            let paywallFrame = paywallWindow.frameRect(forContentRect: expandedContentRect)
+            paywallWindow.setFrame(paywallFrame, display: false)
+            let lockedSize = paywallWindow.frame.size
+            paywallWindow.minSize = lockedSize
+            paywallWindow.maxSize = lockedSize
+
+            await hostWindow.beginSheet(paywallWindow)
+        }
     }
 
     private func configureFeatureShortcutCards() {