소스 검색

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 일 전
부모
커밋
d543ea97f0

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

@@ -25,6 +25,14 @@ final class PremiumPlansWindowController: NSWindowController {
25
     required init?(coder: NSCoder) {
25
     required init?(coder: NSCoder) {
26
         nil
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
 private final class PremiumPlansViewController: NSViewController {
38
 private final class PremiumPlansViewController: NSViewController {
@@ -499,6 +507,7 @@ private final class PremiumPlansViewController: NSViewController {
499
     private var subscriptionStatusObservation: NSObjectProtocol?
507
     private var subscriptionStatusObservation: NSObjectProtocol?
500
     private var appearanceObserver: NSObjectProtocol?
508
     private var appearanceObserver: NSObjectProtocol?
501
     private var languageObserver: NSObjectProtocol?
509
     private var languageObserver: NSObjectProtocol?
510
+    private var storeProductsLoadTask: Task<Void, Never>?
502
 
511
 
503
     /// Core Pro capabilities shown on every pricing card (replaces generic “All premium features”).
512
     /// Core Pro capabilities shown on every pricing card (replaces generic “All premium features”).
504
     private var proCapabilityFeatures: [String] {
513
     private var proCapabilityFeatures: [String] {
@@ -607,15 +616,30 @@ private final class PremiumPlansViewController: NSViewController {
607
             queue: .main
616
             queue: .main
608
         ) { [weak self] _ in
617
         ) { [weak self] _ in
609
             Task { @MainActor in
618
             Task { @MainActor in
610
-                await self?.subscriptionStore.loadProducts()
619
+                await self?.subscriptionStore.ensureProductsLoaded()
611
                 self?.applyStorePricing()
620
                 self?.applyStorePricing()
612
                 self?.updateSubscriptionPrimaryFooter()
621
                 self?.updateSubscriptionPrimaryFooter()
613
                 self?.updatePremiumCloseButtonVisibility()
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
     override func viewDidLayout() {
645
     override func viewDidLayout() {
@@ -1156,9 +1180,12 @@ private final class PremiumPlansViewController: NSViewController {
1156
     }
1180
     }
1157
 
1181
 
1158
     private func loadStoreProducts() async {
1182
     private func loadStoreProducts() async {
1159
-        await subscriptionStore.refreshEntitlements(deep: true)
1160
-        await subscriptionStore.loadProducts()
1161
         applyStorePricing()
1183
         applyStorePricing()
1184
+        await subscriptionStore.ensureProductsLoaded()
1185
+        applyStorePricing()
1186
+        updateSubscriptionPrimaryFooter()
1187
+        updatePremiumCloseButtonVisibility()
1188
+        await subscriptionStore.refreshEntitlements(deep: true)
1162
         updateSubscriptionPrimaryFooter()
1189
         updateSubscriptionPrimaryFooter()
1163
         updatePremiumCloseButtonVisibility()
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
     func product(forPlanKey planKey: String) -> Product? {
63
     func product(forPlanKey planKey: String) -> Product? {
58
         guard let id = SubscriptionProductIDs.productID(planKey: planKey) else { return nil }
64
         guard let id = SubscriptionProductIDs.productID(planKey: planKey) else { return nil }
59
         return productsByID[id]
65
         return productsByID[id]

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

@@ -185,6 +185,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
185
     private var chatThinkingRowHost: NSView?
185
     private var chatThinkingRowHost: NSView?
186
     private let jobSearchService = OpenAIJobSearchService()
186
     private let jobSearchService = OpenAIJobSearchService()
187
     private var premiumPlansWindowController: PremiumPlansWindowController?
187
     private var premiumPlansWindowController: PremiumPlansWindowController?
188
+    private var isPreparingPremiumPlansSheet = false
188
     private var indeedJobBrowserViewController: IndeedJobBrowserViewController?
189
     private var indeedJobBrowserViewController: IndeedJobBrowserViewController?
189
     private var isIndeedJobBrowserPresented = false
190
     private var isIndeedJobBrowserPresented = false
190
     private weak var sidebarUpgradeCard: NSView?
191
     private weak var sidebarUpgradeCard: NSView?
@@ -777,31 +778,41 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
777
 
778
 
778
     private func presentPremiumPlansSheet() {
779
     private func presentPremiumPlansSheet() {
779
         guard let hostWindow = window else { return }
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
     private func configureFeatureShortcutCards() {
818
     private func configureFeatureShortcutCards() {