Răsfoiți Sursa

Improve paywall loading UX and fix footer layout.

Show the paywall immediately with bundled config while products and remote config refresh in the background, surface product load failures with retry, and pin footer links to the bottom without stretching trust badges.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 săptămâni în urmă
părinte
comite
3580905933
1 a modificat fișierele cu 206 adăugiri și 26 ștergeri
  1. 206 26
      smart_printer/PaywallView.swift

+ 206 - 26
smart_printer/PaywallView.swift

@@ -180,6 +180,7 @@ final class StoreManager {
     var hasActiveSubscription: Bool { premiumAccessKind == .subscription }
     var hasActiveSubscription: Bool { premiumAccessKind == .subscription }
     var hasLifetimeAccess: Bool { premiumAccessKind == .lifetime }
     var hasLifetimeAccess: Bool { premiumAccessKind == .lifetime }
     private(set) var isLoadingProducts = false
     private(set) var isLoadingProducts = false
+    private(set) var productLoadError: String?
     private(set) var isPurchasing = false
     private(set) var isPurchasing = false
     private(set) var isResolvingEntitlements = true
     private(set) var isResolvingEntitlements = true
     private(set) var introOfferEligibleByProductID: [String: Bool] = [:]
     private(set) var introOfferEligibleByProductID: [String: Bool] = [:]
@@ -234,6 +235,7 @@ final class StoreManager {
 
 
     func loadProducts() async {
     func loadProducts() async {
         isLoadingProducts = true
         isLoadingProducts = true
+        productLoadError = nil
         postStoreStateDidChange()
         postStoreStateDidChange()
         defer {
         defer {
             isLoadingProducts = false
             isLoadingProducts = false
@@ -241,13 +243,20 @@ final class StoreManager {
         }
         }
 
 
         do {
         do {
-            products = try await Product.products(for: StoreProductID.all)
-                .sorted { lhs, rhs in
-                    productSortOrder(for: lhs.id) < productSortOrder(for: rhs.id)
-                }
+            let loaded = try await Product.products(for: StoreProductID.all)
+            guard !loaded.isEmpty else {
+                productLoadError = "No subscription plans are available right now."
+                products = []
+                return
+            }
+            products = loaded.sorted { lhs, rhs in
+                productSortOrder(for: lhs.id) < productSortOrder(for: rhs.id)
+            }
+            productLoadError = nil
             await refreshIntroOfferEligibility()
             await refreshIntroOfferEligibility()
             NotificationCenter.default.post(name: .storeProductsDidUpdate, object: nil)
             NotificationCenter.default.post(name: .storeProductsDidUpdate, object: nil)
         } catch {
         } catch {
+            productLoadError = "Couldn't load subscription plans. Check your connection and try again."
             NSLog("Failed to load products: \(error.localizedDescription)")
             NSLog("Failed to load products: \(error.localizedDescription)")
         }
         }
     }
     }
@@ -908,6 +917,7 @@ private final class PaywallTrustItemView: NSView, AppearanceRefreshable {
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
         titleLabel.translatesAutoresizingMaskIntoConstraints = false
 
 
         subtitleLabel.lineBreakMode = .byTruncatingTail
         subtitleLabel.lineBreakMode = .byTruncatingTail
+        subtitleLabel.maximumNumberOfLines = 2
         subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
         subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
 
 
@@ -917,6 +927,8 @@ private final class PaywallTrustItemView: NSView, AppearanceRefreshable {
         addSubview(subtitleLabel)
         addSubview(subtitleLabel)
 
 
         NSLayoutConstraint.activate([
         NSLayoutConstraint.activate([
+            heightAnchor.constraint(equalToConstant: Self.preferredHeight),
+
             iconContainer.leadingAnchor.constraint(equalTo: leadingAnchor),
             iconContainer.leadingAnchor.constraint(equalTo: leadingAnchor),
             iconContainer.topAnchor.constraint(equalTo: topAnchor),
             iconContainer.topAnchor.constraint(equalTo: topAnchor),
             iconContainer.widthAnchor.constraint(equalToConstant: 20),
             iconContainer.widthAnchor.constraint(equalToConstant: 20),
@@ -939,9 +951,13 @@ private final class PaywallTrustItemView: NSView, AppearanceRefreshable {
 
 
         setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
         setContentHuggingPriority(.defaultLow, for: .horizontal)
         setContentHuggingPriority(.defaultLow, for: .horizontal)
+        setContentHuggingPriority(.required, for: .vertical)
+        setContentCompressionResistancePriority(.required, for: .vertical)
         refreshAppearance()
         refreshAppearance()
     }
     }
 
 
+    fileprivate static let preferredHeight: CGFloat = 34
+
     func update(iconName: String, title: String, subtitle: String) {
     func update(iconName: String, title: String, subtitle: String) {
         titleLabel.stringValue = title
         titleLabel.stringValue = title
         subtitleLabel.stringValue = subtitle
         subtitleLabel.stringValue = subtitle
@@ -962,6 +978,64 @@ private final class PaywallTrustItemView: NSView, AppearanceRefreshable {
     }
     }
 }
 }
 
 
+// MARK: - Products Error
+
+private final class PaywallProductsErrorView: NSView, AppearanceRefreshable {
+    var onRetry: (() -> Void)?
+
+    private let messageLabel = NSTextField(wrappingLabelWithString: "")
+    private let retryButton = PaywallFooterLink(title: "Retry")
+
+    init() {
+        super.init(frame: .zero)
+        translatesAutoresizingMaskIntoConstraints = false
+
+        messageLabel.font = AppTheme.regularFont(size: 12)
+        messageLabel.textColor = AppTheme.textSecondary
+        messageLabel.alignment = .center
+        messageLabel.maximumNumberOfLines = 3
+        messageLabel.lineBreakMode = .byWordWrapping
+        messageLabel.cell?.wraps = true
+        messageLabel.cell?.isScrollable = false
+        messageLabel.translatesAutoresizingMaskIntoConstraints = false
+
+        retryButton.target = self
+        retryButton.action = #selector(retryTapped)
+
+        addSubview(messageLabel)
+        addSubview(retryButton)
+
+        NSLayoutConstraint.activate([
+            messageLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
+            messageLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
+            messageLabel.topAnchor.constraint(equalTo: topAnchor),
+
+            retryButton.centerXAnchor.constraint(equalTo: centerXAnchor),
+            retryButton.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 8),
+            retryButton.bottomAnchor.constraint(equalTo: bottomAnchor),
+        ])
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) { nil }
+
+    func update(message: String, isRetrying: Bool) {
+        messageLabel.stringValue = message
+        retryButton.isHidden = false
+        retryButton.isEnabled = !isRetrying
+        retryButton.updateTitle(isRetrying ? "Retrying…" : "Retry")
+    }
+
+    func refreshAppearance() {
+        messageLabel.textColor = AppTheme.textSecondary
+        retryButton.refreshAppearance()
+    }
+
+    @objc private func retryTapped() {
+        onRetry?()
+    }
+}
+
 // MARK: - Close Button
 // MARK: - Close Button
 
 
 private final class PaywallCloseButton: NSButton, AppearanceRefreshable {
 private final class PaywallCloseButton: NSButton, AppearanceRefreshable {
@@ -1088,6 +1162,10 @@ final class PaywallView: NSView, AppearanceRefreshable {
     private var featureRows: [PaywallFeatureRow] = []
     private var featureRows: [PaywallFeatureRow] = []
     private var trustItemViews: [PaywallTrustItemView] = []
     private var trustItemViews: [PaywallTrustItemView] = []
     private var trustStack: NSStackView!
     private var trustStack: NSStackView!
+    private var productsErrorView: PaywallProductsErrorView!
+    private var plansStack: NSStackView!
+    private var plansStackHeightConstraint: NSLayoutConstraint!
+    private var productsErrorHeightConstraint: NSLayoutConstraint!
     private var storeObservers: [NSObjectProtocol] = []
     private var storeObservers: [NSObjectProtocol] = []
 
 
     init() {
     init() {
@@ -1201,14 +1279,41 @@ final class PaywallView: NSView, AppearanceRefreshable {
             )
             )
         )
         )
         refreshTrustItemsForSelectedPlan()
         refreshTrustItemsForSelectedPlan()
+        refreshProductsErrorState()
         refreshPurchaseState()
         refreshPurchaseState()
     }
     }
 
 
+    private func refreshProductsErrorState() {
+        let store = StoreManager.shared
+        let shouldShowError = store.productLoadError != nil
+            && store.products.isEmpty
+            && !store.isLoadingProducts
+
+        productsErrorView.isHidden = !shouldShowError
+        plansStack.isHidden = shouldShowError
+        plansStackHeightConstraint.constant = shouldShowError ? 0 : plansAreaHeight
+        productsErrorHeightConstraint.constant = shouldShowError ? productsErrorAreaHeight : 0
+
+        if shouldShowError, let error = store.productLoadError {
+            productsErrorView.update(message: error, isRetrying: false)
+        }
+    }
+
+    private var plansAreaHeight: CGFloat {
+        let cardHeight: CGFloat = 86
+        let spacing: CGFloat = 12
+        let planCount = CGFloat(PaywallPlan.allCases.count)
+        return planCount * cardHeight + max(0, planCount - 1) * spacing
+    }
+
+    private var productsErrorAreaHeight: CGFloat { 86 }
+
     private func refreshPurchaseState() {
     private func refreshPurchaseState() {
         let store = StoreManager.shared
         let store = StoreManager.shared
+        let hasProductLoadFailure = store.productLoadError != nil && store.products.isEmpty
         let isProductReady = store.product(for: selectedPlan) != nil
         let isProductReady = store.product(for: selectedPlan) != nil
         let isBusy = store.isPurchasing || store.isLoadingProducts
         let isBusy = store.isPurchasing || store.isLoadingProducts
-        let isDisabled = isBusy || !isProductReady
+        let isDisabled = isBusy || !isProductReady || hasProductLoadFailure
         ctaButton.isEnabled = !isDisabled
         ctaButton.isEnabled = !isDisabled
         ctaButton.alphaValue = isDisabled ? 0.65 : 1
         ctaButton.alphaValue = isDisabled ? 0.65 : 1
         refreshPrimaryFooterLink()
         refreshPrimaryFooterLink()
@@ -1380,6 +1485,17 @@ final class PaywallView: NSView, AppearanceRefreshable {
         plansStack.orientation = .vertical
         plansStack.orientation = .vertical
         plansStack.spacing = 12
         plansStack.spacing = 12
         plansStack.translatesAutoresizingMaskIntoConstraints = false
         plansStack.translatesAutoresizingMaskIntoConstraints = false
+        self.plansStack = plansStack
+
+        let plansAreaView = NSView()
+        plansAreaView.translatesAutoresizingMaskIntoConstraints = false
+
+        let productsErrorView = PaywallProductsErrorView()
+        productsErrorView.isHidden = true
+        productsErrorView.onRetry = { [weak self] in
+            self?.retryProductLoad()
+        }
+        self.productsErrorView = productsErrorView
 
 
         for plan in PaywallPlan.allCases {
         for plan in PaywallPlan.allCases {
             let card = PaywallPlanCard(plan: plan)
             let card = PaywallPlanCard(plan: plan)
@@ -1420,12 +1536,24 @@ final class PaywallView: NSView, AppearanceRefreshable {
         let trustRow = makeTrustRow()
         let trustRow = makeTrustRow()
         let footerLinks = makeFooterLinks()
         let footerLinks = makeFooterLinks()
 
 
+        let footerSpacer = NSView()
+        footerSpacer.translatesAutoresizingMaskIntoConstraints = false
+        footerSpacer.setContentHuggingPriority(.defaultLow, for: .vertical)
+        footerSpacer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
+
+        plansAreaView.addSubview(plansStack)
+        plansAreaView.addSubview(productsErrorView)
+
+        plansStackHeightConstraint = plansStack.heightAnchor.constraint(equalToConstant: plansAreaHeight)
+        productsErrorHeightConstraint = productsErrorView.heightAnchor.constraint(equalToConstant: 0)
+
         panel.addSubview(title)
         panel.addSubview(title)
         panel.addSubview(subtitle)
         panel.addSubview(subtitle)
-        panel.addSubview(plansStack)
+        panel.addSubview(plansAreaView)
         panel.addSubview(trustRow)
         panel.addSubview(trustRow)
         panel.addSubview(ctaButton)
         panel.addSubview(ctaButton)
         panel.addSubview(renewalDisclosureLabel)
         panel.addSubview(renewalDisclosureLabel)
+        panel.addSubview(footerSpacer)
         panel.addSubview(footerLinks)
         panel.addSubview(footerLinks)
 
 
         NSLayoutConstraint.activate([
         NSLayoutConstraint.activate([
@@ -1437,27 +1565,42 @@ final class PaywallView: NSView, AppearanceRefreshable {
             subtitle.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
             subtitle.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
             subtitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6),
             subtitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6),
 
 
-            plansStack.leadingAnchor.constraint(equalTo: title.leadingAnchor),
-            plansStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
-            plansStack.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 24),
+            plansAreaView.leadingAnchor.constraint(equalTo: title.leadingAnchor),
+            plansAreaView.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
+            plansAreaView.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 24),
+            plansAreaView.heightAnchor.constraint(equalToConstant: plansAreaHeight),
+
+            plansStack.leadingAnchor.constraint(equalTo: plansAreaView.leadingAnchor),
+            plansStack.trailingAnchor.constraint(equalTo: plansAreaView.trailingAnchor),
+            plansStack.topAnchor.constraint(equalTo: plansAreaView.topAnchor),
+            plansStackHeightConstraint,
+
+            productsErrorView.leadingAnchor.constraint(equalTo: plansAreaView.leadingAnchor),
+            productsErrorView.trailingAnchor.constraint(equalTo: plansAreaView.trailingAnchor),
+            productsErrorView.topAnchor.constraint(equalTo: plansAreaView.topAnchor),
+            productsErrorHeightConstraint,
 
 
-            trustRow.leadingAnchor.constraint(equalTo: plansStack.leadingAnchor),
-            trustRow.trailingAnchor.constraint(equalTo: plansStack.trailingAnchor),
-            trustRow.topAnchor.constraint(equalTo: plansStack.bottomAnchor, constant: 18),
+            trustRow.leadingAnchor.constraint(equalTo: plansAreaView.leadingAnchor),
+            trustRow.trailingAnchor.constraint(equalTo: plansAreaView.trailingAnchor),
+            trustRow.topAnchor.constraint(equalTo: plansAreaView.bottomAnchor, constant: 18),
 
 
-            ctaButton.leadingAnchor.constraint(equalTo: plansStack.leadingAnchor),
-            ctaButton.trailingAnchor.constraint(equalTo: plansStack.trailingAnchor),
+            ctaButton.leadingAnchor.constraint(equalTo: plansAreaView.leadingAnchor),
+            ctaButton.trailingAnchor.constraint(equalTo: plansAreaView.trailingAnchor),
             ctaButton.topAnchor.constraint(equalTo: trustRow.bottomAnchor, constant: 16),
             ctaButton.topAnchor.constraint(equalTo: trustRow.bottomAnchor, constant: 16),
             ctaButton.heightAnchor.constraint(equalToConstant: 48),
             ctaButton.heightAnchor.constraint(equalToConstant: 48),
 
 
-            renewalDisclosureLabel.leadingAnchor.constraint(equalTo: plansStack.leadingAnchor),
-            renewalDisclosureLabel.trailingAnchor.constraint(equalTo: plansStack.trailingAnchor),
+            renewalDisclosureLabel.leadingAnchor.constraint(equalTo: plansAreaView.leadingAnchor),
+            renewalDisclosureLabel.trailingAnchor.constraint(equalTo: plansAreaView.trailingAnchor),
             renewalDisclosureLabel.topAnchor.constraint(equalTo: ctaButton.bottomAnchor, constant: 10),
             renewalDisclosureLabel.topAnchor.constraint(equalTo: ctaButton.bottomAnchor, constant: 10),
 
 
+            footerSpacer.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
+            footerSpacer.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
+            footerSpacer.topAnchor.constraint(equalTo: renewalDisclosureLabel.bottomAnchor, constant: 16),
+            footerSpacer.bottomAnchor.constraint(equalTo: footerLinks.topAnchor),
+
             footerLinks.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
             footerLinks.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
             footerLinks.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
             footerLinks.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
-            footerLinks.topAnchor.constraint(equalTo: renewalDisclosureLabel.bottomAnchor, constant: 14),
-            footerLinks.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -20),
+            footerLinks.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -16),
         ])
         ])
 
 
         return panel
         return panel
@@ -1485,6 +1628,11 @@ final class PaywallView: NSView, AppearanceRefreshable {
         trustStack.layer?.masksToBounds = true
         trustStack.layer?.masksToBounds = true
         trustStack.edgeInsets = NSEdgeInsets(top: 12, left: 14, bottom: 12, right: 14)
         trustStack.edgeInsets = NSEdgeInsets(top: 12, left: 14, bottom: 12, right: 14)
         trustStack.translatesAutoresizingMaskIntoConstraints = false
         trustStack.translatesAutoresizingMaskIntoConstraints = false
+        trustStack.setContentHuggingPriority(.required, for: .vertical)
+        trustStack.setContentCompressionResistancePriority(.required, for: .vertical)
+        trustStack.heightAnchor.constraint(
+            equalToConstant: PaywallTrustItemView.preferredHeight + trustStack.edgeInsets.top + trustStack.edgeInsets.bottom
+        ).isActive = true
         self.trustStack = trustStack
         self.trustStack = trustStack
 
 
         return trustStack
         return trustStack
@@ -1493,8 +1641,15 @@ final class PaywallView: NSView, AppearanceRefreshable {
     private func makeFooterLinks() -> NSView {
     private func makeFooterLinks() -> NSView {
         let container = NSView()
         let container = NSView()
         container.translatesAutoresizingMaskIntoConstraints = false
         container.translatesAutoresizingMaskIntoConstraints = false
+        container.setContentHuggingPriority(.required, for: .vertical)
+        container.setContentCompressionResistancePriority(.required, for: .vertical)
         let config = paywallConfig
         let config = paywallConfig
 
 
+        let separator = NSView()
+        separator.translatesAutoresizingMaskIntoConstraints = false
+        separator.wantsLayer = true
+        separator.layer?.backgroundColor = AppTheme.paywallBorder.cgColor
+
         continueFreePlanLink.updateTitle(config.footer.continueFree)
         continueFreePlanLink.updateTitle(config.footer.continueFree)
         continueFreePlanLink.target = self
         continueFreePlanLink.target = self
         continueFreePlanLink.action = #selector(continueWithFreePlanTapped)
         continueFreePlanLink.action = #selector(continueWithFreePlanTapped)
@@ -1538,11 +1693,17 @@ final class PaywallView: NSView, AppearanceRefreshable {
         linksStack.distribution = .fillEqually
         linksStack.distribution = .fillEqually
         linksStack.translatesAutoresizingMaskIntoConstraints = false
         linksStack.translatesAutoresizingMaskIntoConstraints = false
 
 
+        container.addSubview(separator)
         container.addSubview(linksStack)
         container.addSubview(linksStack)
         NSLayoutConstraint.activate([
         NSLayoutConstraint.activate([
+            separator.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+            separator.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+            separator.topAnchor.constraint(equalTo: container.topAnchor),
+            separator.heightAnchor.constraint(equalToConstant: 1),
+
             linksStack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
             linksStack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
             linksStack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
             linksStack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
-            linksStack.topAnchor.constraint(equalTo: container.topAnchor),
+            linksStack.topAnchor.constraint(equalTo: separator.bottomAnchor, constant: 10),
             linksStack.bottomAnchor.constraint(equalTo: container.bottomAnchor),
             linksStack.bottomAnchor.constraint(equalTo: container.bottomAnchor),
         ])
         ])
 
 
@@ -1552,6 +1713,8 @@ final class PaywallView: NSView, AppearanceRefreshable {
     private func makePrimaryFooterLinkCell() -> NSView {
     private func makePrimaryFooterLinkCell() -> NSView {
         let cell = NSView()
         let cell = NSView()
         cell.translatesAutoresizingMaskIntoConstraints = false
         cell.translatesAutoresizingMaskIntoConstraints = false
+        cell.setContentHuggingPriority(.required, for: .vertical)
+        cell.setContentCompressionResistancePriority(.required, for: .vertical)
 
 
         for link in [continueFreePlanLink, manageSubscriptionLink] {
         for link in [continueFreePlanLink, manageSubscriptionLink] {
             cell.addSubview(link)
             cell.addSubview(link)
@@ -1569,6 +1732,8 @@ final class PaywallView: NSView, AppearanceRefreshable {
     private func makeFooterLinkCell(link: PaywallFooterLink) -> NSView {
     private func makeFooterLinkCell(link: PaywallFooterLink) -> NSView {
         let cell = NSView()
         let cell = NSView()
         cell.translatesAutoresizingMaskIntoConstraints = false
         cell.translatesAutoresizingMaskIntoConstraints = false
+        cell.setContentHuggingPriority(.required, for: .vertical)
+        cell.setContentCompressionResistancePriority(.required, for: .vertical)
 
 
         cell.addSubview(link)
         cell.addSubview(link)
         NSLayoutConstraint.activate([
         NSLayoutConstraint.activate([
@@ -1602,6 +1767,15 @@ final class PaywallView: NSView, AppearanceRefreshable {
         refreshPurchaseState()
         refreshPurchaseState()
     }
     }
 
 
+    private func retryProductLoad() {
+        Task { @MainActor in
+            refreshProductsErrorState()
+            refreshPurchaseState()
+            await StoreManager.shared.loadProducts()
+            refreshStoreState()
+        }
+    }
+
     @objc private func purchaseTapped() {
     @objc private func purchaseTapped() {
         Task { @MainActor in
         Task { @MainActor in
             refreshPurchaseState()
             refreshPurchaseState()
@@ -1770,17 +1944,23 @@ final class PaywallOverlayView: NSView, AppearanceRefreshable {
                 bottomAnchor.constraint(equalTo: parent.bottomAnchor),
                 bottomAnchor.constraint(equalTo: parent.bottomAnchor),
             ])
             ])
         }
         }
-        acceptsHitTesting = false
-        alphaValue = 0
+        alphaValue = 1
+        acceptsHitTesting = true
+        paywallView.refreshStoreState()
+
         Task { @MainActor in
         Task { @MainActor in
-            await StoreManager.shared.ensureEntitlementsResolved()
-            if StoreManager.shared.products.isEmpty {
+            if StoreManager.shared.isResolvingEntitlements {
+                await StoreManager.shared.ensureEntitlementsResolved()
+                paywallView.refreshStoreState()
+            }
+            if StoreManager.shared.products.isEmpty, !StoreManager.shared.isLoadingProducts {
                 await StoreManager.shared.loadProducts()
                 await StoreManager.shared.loadProducts()
+                paywallView.refreshStoreState()
             }
             }
+        }
+
+        Task { @MainActor in
             await PaywallConfigService.shared.refreshFromRemote()
             await PaywallConfigService.shared.refreshFromRemote()
-            paywallView.refreshStoreState()
-            alphaValue = 1
-            acceptsHitTesting = true
         }
         }
     }
     }