Kaynağa Gözat

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 4 gün önce
ebeveyn
işleme
d543ea97f0

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

@@ -25,6 +25,14 @@ final class PremiumPlansWindowController: NSWindowController {
25 25
     required init?(coder: NSCoder) {
26 26
         nil
27 27
     }
28
+
29
+    /// Loads StoreKit prices into the paywall view before the sheet is shown (avoids "—" placeholders flashing in).
30
+    @MainActor
31
+    func prepareForPresentation() async {
32
+        _ = window
33
+        guard let viewController = window?.contentViewController as? PremiumPlansViewController else { return }
34
+        await viewController.prepareStorePricingForDisplay()
35
+    }
28 36
 }
29 37
 
30 38
 private final class PremiumPlansViewController: NSViewController {
@@ -499,6 +507,7 @@ private final class PremiumPlansViewController: NSViewController {
499 507
     private var subscriptionStatusObservation: NSObjectProtocol?
500 508
     private var appearanceObserver: NSObjectProtocol?
501 509
     private var languageObserver: NSObjectProtocol?
510
+    private var storeProductsLoadTask: Task<Void, Never>?
502 511
 
503 512
     /// Core Pro capabilities shown on every pricing card (replaces generic “All premium features”).
504 513
     private var proCapabilityFeatures: [String] {
@@ -607,15 +616,30 @@ private final class PremiumPlansViewController: NSViewController {
607 616
             queue: .main
608 617
         ) { [weak self] _ in
609 618
             Task { @MainActor in
610
-                await self?.subscriptionStore.loadProducts()
619
+                await self?.subscriptionStore.ensureProductsLoaded()
611 620
                 self?.applyStorePricing()
612 621
                 self?.updateSubscriptionPrimaryFooter()
613 622
                 self?.updatePremiumCloseButtonVisibility()
614 623
             }
615 624
         }
616
-        Task { @MainActor in
617
-            await loadStoreProducts()
625
+        applyStorePricing()
626
+        storeProductsLoadTask = Task { @MainActor [weak self] in
627
+            await self?.loadStoreProducts()
628
+            self?.storeProductsLoadTask = nil
629
+        }
630
+    }
631
+
632
+    /// Ensures localized prices are on screen; reuses an in-flight load started from `viewDidLoad`.
633
+    @MainActor
634
+    func prepareStorePricingForDisplay() async {
635
+        applyStorePricing()
636
+        if let storeProductsLoadTask {
637
+            await storeProductsLoadTask.value
638
+            applyStorePricing()
639
+            return
618 640
         }
641
+        await subscriptionStore.ensureProductsLoaded()
642
+        applyStorePricing()
619 643
     }
620 644
 
621 645
     override func viewDidLayout() {
@@ -1156,9 +1180,12 @@ private final class PremiumPlansViewController: NSViewController {
1156 1180
     }
1157 1181
 
1158 1182
     private func loadStoreProducts() async {
1159
-        await subscriptionStore.refreshEntitlements(deep: true)
1160
-        await subscriptionStore.loadProducts()
1161 1183
         applyStorePricing()
1184
+        await subscriptionStore.ensureProductsLoaded()
1185
+        applyStorePricing()
1186
+        updateSubscriptionPrimaryFooter()
1187
+        updatePremiumCloseButtonVisibility()
1188
+        await subscriptionStore.refreshEntitlements(deep: true)
1162 1189
         updateSubscriptionPrimaryFooter()
1163 1190
         updatePremiumCloseButtonVisibility()
1164 1191
     }

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

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

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

@@ -185,6 +185,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
185 185
     private var chatThinkingRowHost: NSView?
186 186
     private let jobSearchService = OpenAIJobSearchService()
187 187
     private var premiumPlansWindowController: PremiumPlansWindowController?
188
+    private var isPreparingPremiumPlansSheet = false
188 189
     private var indeedJobBrowserViewController: IndeedJobBrowserViewController?
189 190
     private var isIndeedJobBrowserPresented = false
190 191
     private weak var sidebarUpgradeCard: NSView?
@@ -777,31 +778,41 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
777 778
 
778 779
     private func presentPremiumPlansSheet() {
779 780
         guard let hostWindow = window else { return }
781
+        if isPreparingPremiumPlansSheet { return }
780 782
 
781
-        if premiumPlansWindowController == nil {
782
-            premiumPlansWindowController = PremiumPlansWindowController()
783
-        }
784
-        guard let paywallWindow = premiumPlansWindowController?.window else { return }
783
+        isPreparingPremiumPlansSheet = true
784
+        Task { @MainActor [weak self] in
785
+            defer { self?.isPreparingPremiumPlansSheet = false }
786
+            guard let self else { return }
785 787
 
786
-        if hostWindow.attachedSheet === paywallWindow {
787
-            return
788
-        }
788
+            if self.premiumPlansWindowController == nil {
789
+                self.premiumPlansWindowController = PremiumPlansWindowController()
790
+            }
791
+            guard let controller = self.premiumPlansWindowController else { return }
792
+            await controller.prepareForPresentation()
789 793
 
790
-        paywallWindow.styleMask = [.borderless, .closable, .resizable]
791
-        paywallWindow.isOpaque = true
792
-        paywallWindow.backgroundColor = PremiumPlansWindowController.paywallSheetBackground
794
+            guard let paywallWindow = controller.window else { return }
793 795
 
794
-        let hostContentRect = hostWindow.contentRect(forFrameRect: hostWindow.frame)
795
-        let overscan = PremiumSheetLayout.overscanPerEdge
796
-        var expandedContentRect = hostContentRect.insetBy(dx: -overscan, dy: -overscan)
797
-        expandedContentRect.size.height += PremiumSheetLayout.overscanExtraTop
798
-        let paywallFrame = paywallWindow.frameRect(forContentRect: expandedContentRect)
799
-        paywallWindow.setFrame(paywallFrame, display: false)
800
-        let lockedSize = paywallWindow.frame.size
801
-        paywallWindow.minSize = lockedSize
802
-        paywallWindow.maxSize = lockedSize
796
+            if hostWindow.attachedSheet === paywallWindow {
797
+                return
798
+            }
799
+
800
+            paywallWindow.styleMask = [.borderless, .closable, .resizable]
801
+            paywallWindow.isOpaque = true
802
+            paywallWindow.backgroundColor = PremiumPlansWindowController.paywallSheetBackground
803 803
 
804
-        hostWindow.beginSheet(paywallWindow)
804
+            let hostContentRect = hostWindow.contentRect(forFrameRect: hostWindow.frame)
805
+            let overscan = PremiumSheetLayout.overscanPerEdge
806
+            var expandedContentRect = hostContentRect.insetBy(dx: -overscan, dy: -overscan)
807
+            expandedContentRect.size.height += PremiumSheetLayout.overscanExtraTop
808
+            let paywallFrame = paywallWindow.frameRect(forContentRect: expandedContentRect)
809
+            paywallWindow.setFrame(paywallFrame, display: false)
810
+            let lockedSize = paywallWindow.frame.size
811
+            paywallWindow.minSize = lockedSize
812
+            paywallWindow.maxSize = lockedSize
813
+
814
+            await hostWindow.beginSheet(paywallWindow)
815
+        }
805 816
     }
806 817
 
807 818
     private func configureFeatureShortcutCards() {