Преглед изворни кода

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 пре 1 месец
родитељ
комит
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() {