Pārlūkot izejas kodu

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 dienas atpakaļ
vecāks
revīzija
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() {