|
@@ -1,93 +1,6 @@
|
|
|
import Cocoa
|
|
import Cocoa
|
|
|
import StoreKit
|
|
import StoreKit
|
|
|
|
|
|
|
|
-// MARK: - Plan Model
|
|
|
|
|
-
|
|
|
|
|
-enum PaywallPlan: String, CaseIterable {
|
|
|
|
|
- case monthly
|
|
|
|
|
- case yearly
|
|
|
|
|
- case lifetime
|
|
|
|
|
-
|
|
|
|
|
- var productID: String {
|
|
|
|
|
- switch self {
|
|
|
|
|
- case .monthly: StoreProductID.monthly
|
|
|
|
|
- case .yearly: StoreProductID.yearly
|
|
|
|
|
- case .lifetime: StoreProductID.lifetime
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- func planCopy(from config: PaywallConfig) -> PaywallConfig.PlanCopy {
|
|
|
|
|
- switch self {
|
|
|
|
|
- case .monthly: config.plans.monthly
|
|
|
|
|
- case .yearly: config.plans.yearly
|
|
|
|
|
- case .lifetime: config.plans.lifetime
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- func localizedPrice(from product: Product?, config: PaywallConfig) -> String {
|
|
|
|
|
- product?.displayPrice ?? planCopy(from: config).fallbackPrice
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- func localizedSubtitle(
|
|
|
|
|
- from product: Product?,
|
|
|
|
|
- config: PaywallConfig,
|
|
|
|
|
- trial: PaywallTrialDisplay? = nil
|
|
|
|
|
- ) -> String {
|
|
|
|
|
- guard let product else { return config.loadingPrice }
|
|
|
|
|
- let copy = planCopy(from: config)
|
|
|
|
|
- let price = product.displayPrice
|
|
|
|
|
-
|
|
|
|
|
- if let trial, let trialTemplate = copy.trialSubtitleTemplate {
|
|
|
|
|
- return trialTemplate
|
|
|
|
|
- .replacingOccurrences(of: "{trial}", with: trial.durationLowercased)
|
|
|
|
|
- .replacingOccurrences(of: "{price}", with: price)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return copy.subtitleTemplate.replacingOccurrences(of: "{price}", with: price)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- func localizedCTATitle(
|
|
|
|
|
- from product: Product?,
|
|
|
|
|
- config: PaywallConfig,
|
|
|
|
|
- trial: PaywallTrialDisplay? = nil
|
|
|
|
|
- ) -> String {
|
|
|
|
|
- guard let product else { return config.loadingCTA }
|
|
|
|
|
- if let trial {
|
|
|
|
|
- return trial.ctaText
|
|
|
|
|
- }
|
|
|
|
|
- let copy = planCopy(from: config)
|
|
|
|
|
- return copy.ctaTemplate.replacingOccurrences(of: "{price}", with: product.displayPrice)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- func localizedRenewalDisclosure(
|
|
|
|
|
- from product: Product?,
|
|
|
|
|
- config: PaywallConfig,
|
|
|
|
|
- trial: PaywallTrialDisplay? = nil
|
|
|
|
|
- ) -> String {
|
|
|
|
|
- switch self {
|
|
|
|
|
- case .lifetime:
|
|
|
|
|
- return config.lifetimeDisclosure
|
|
|
|
|
- case .monthly, .yearly:
|
|
|
|
|
- guard let product else { return config.loadingDisclosure }
|
|
|
|
|
- let price = product.displayPrice
|
|
|
|
|
- let period = product.subscription.map {
|
|
|
|
|
- config.subscriptionPeriodDescription(from: $0.subscriptionPeriod)
|
|
|
|
|
- } ?? config.messages.periodFallback
|
|
|
|
|
-
|
|
|
|
|
- if let trial {
|
|
|
|
|
- return config.trialDisclosureTemplate
|
|
|
|
|
- .replacingOccurrences(of: "{trial}", with: trial.durationLowercased)
|
|
|
|
|
- .replacingOccurrences(of: "{price}", with: price)
|
|
|
|
|
- .replacingOccurrences(of: "{period}", with: period)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return config.subscriptionDisclosureTemplate
|
|
|
|
|
- .replacingOccurrences(of: "{price}", with: price)
|
|
|
|
|
- .replacingOccurrences(of: "{period}", with: period)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// MARK: - StoreKit
|
|
// MARK: - StoreKit
|
|
|
|
|
|
|
|
enum StoreProductID {
|
|
enum StoreProductID {
|
|
@@ -160,14 +73,17 @@ final class StoreManager {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var products: [Product] {
|
|
var products: [Product] {
|
|
|
- StoreProductID.all.compactMap { productsByID[$0] }
|
|
|
|
|
|
|
+ availablePlans.compactMap { productsByID[$0.productID] }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var availablePlans: [PaywallPlan] {
|
|
|
|
|
+ PaywallPlan.displayOrder.filter { productsByID[$0.productID] != nil }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var isResolvingEntitlements: Bool { !hasResolvedPremiumStatus }
|
|
var isResolvingEntitlements: Bool { !hasResolvedPremiumStatus }
|
|
|
var hasResolvedPremiumStatus = false
|
|
var hasResolvedPremiumStatus = false
|
|
|
- var hasAllProductsLoaded: Bool {
|
|
|
|
|
- StoreProductID.all.allSatisfy { productsByID[$0] != nil }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ var hasAvailablePlans: Bool { !availablePlans.isEmpty }
|
|
|
|
|
+ var hasAllProductsLoaded: Bool { hasAvailablePlans }
|
|
|
|
|
|
|
|
private(set) var isLoadingProducts = false
|
|
private(set) var isLoadingProducts = false
|
|
|
private(set) var productLoadError: String?
|
|
private(set) var productLoadError: String?
|
|
@@ -432,10 +348,10 @@ final class StoreManager {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func performLoadProducts(presentLoadingUI: Bool?) async {
|
|
private func performLoadProducts(presentLoadingUI: Bool?) async {
|
|
|
- let missingAny = StoreProductID.all.contains { productsByID[$0] == nil }
|
|
|
|
|
- let shouldShowLoading = presentLoadingUI ?? missingAny
|
|
|
|
|
|
|
+ let shouldReload = presentLoadingUI == true || productsByID.isEmpty
|
|
|
|
|
+ let shouldShowLoading = presentLoadingUI ?? productsByID.isEmpty
|
|
|
|
|
|
|
|
- if !missingAny, presentLoadingUI != true {
|
|
|
|
|
|
|
+ if !shouldReload, presentLoadingUI != true {
|
|
|
purchaseError = nil
|
|
purchaseError = nil
|
|
|
await refreshPremiumAccess()
|
|
await refreshPremiumAccess()
|
|
|
await refreshTrialOffers()
|
|
await refreshTrialOffers()
|
|
@@ -1077,7 +993,6 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
|
|
|
private let titleLabel = NSTextField(labelWithString: "")
|
|
private let titleLabel = NSTextField(labelWithString: "")
|
|
|
private let subtitleLabel = NSTextField(labelWithString: "")
|
|
private let subtitleLabel = NSTextField(labelWithString: "")
|
|
|
private let priceLabel = NSTextField(labelWithString: "")
|
|
private let priceLabel = NSTextField(labelWithString: "")
|
|
|
- private var badgeView: PaywallBadgeView?
|
|
|
|
|
private var trialBadgeView: PaywallBadgeView?
|
|
private var trialBadgeView: PaywallBadgeView?
|
|
|
private var hoverTracker: HoverTracker?
|
|
private var hoverTracker: HoverTracker?
|
|
|
private var isHovered = false
|
|
private var isHovered = false
|
|
@@ -1128,21 +1043,6 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
|
|
|
priceLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
priceLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
- if plan == .lifetime, let badgeText = plan.planCopy(from: config).badge {
|
|
|
|
|
- let badge = PaywallBadgeView(
|
|
|
|
|
- text: badgeText,
|
|
|
|
|
- iconName: "star.fill",
|
|
|
|
|
- background: AppTheme.paywallGold,
|
|
|
|
|
- foreground: AppTheme.paywallGoldText
|
|
|
|
|
- )
|
|
|
|
|
- badgeView = badge
|
|
|
|
|
- addSubview(badge)
|
|
|
|
|
- NSLayoutConstraint.activate([
|
|
|
|
|
- badge.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
|
|
|
|
|
- badge.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
|
|
|
|
- ])
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
applyCardShadow()
|
|
applyCardShadow()
|
|
|
updateAppearance()
|
|
updateAppearance()
|
|
|
|
|
|
|
@@ -1162,22 +1062,9 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
|
|
|
titleLabel.stringValue = plan.planCopy(from: config).title
|
|
titleLabel.stringValue = plan.planCopy(from: config).title
|
|
|
subtitleLabel.stringValue = plan.localizedSubtitle(from: product, config: config, trial: trial)
|
|
subtitleLabel.stringValue = plan.localizedSubtitle(from: product, config: config, trial: trial)
|
|
|
priceLabel.stringValue = plan.localizedPrice(from: product, config: config)
|
|
priceLabel.stringValue = plan.localizedPrice(from: product, config: config)
|
|
|
- updateLifetimeBadge(config: config)
|
|
|
|
|
updateTrialBadge(trial: trial)
|
|
updateTrialBadge(trial: trial)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private func updateLifetimeBadge(config: PaywallConfig) {
|
|
|
|
|
- guard plan == .lifetime else { return }
|
|
|
|
|
- if let badgeText = plan.planCopy(from: config).badge {
|
|
|
|
|
- if let badgeView {
|
|
|
|
|
- badgeView.updateText(badgeText)
|
|
|
|
|
- badgeView.isHidden = false
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- badgeView?.isHidden = true
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private func updateTrialBadge(trial: PaywallTrialDisplay?) {
|
|
private func updateTrialBadge(trial: PaywallTrialDisplay?) {
|
|
|
guard plan == PaywallConfigService.shared.config.trialEligiblePlan else { return }
|
|
guard plan == PaywallConfigService.shared.config.trialEligiblePlan else { return }
|
|
|
|
|
|
|
@@ -1594,6 +1481,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
private var productsErrorView: PaywallProductsErrorView!
|
|
private var productsErrorView: PaywallProductsErrorView!
|
|
|
private var plansStack: NSStackView!
|
|
private var plansStack: NSStackView!
|
|
|
private var plansStackHeightConstraint: NSLayoutConstraint!
|
|
private var plansStackHeightConstraint: NSLayoutConstraint!
|
|
|
|
|
+ private var plansAreaViewHeightConstraint: NSLayoutConstraint!
|
|
|
private var productsErrorHeightConstraint: NSLayoutConstraint!
|
|
private var productsErrorHeightConstraint: NSLayoutConstraint!
|
|
|
private var storeObservers: [NSObjectProtocol] = []
|
|
private var storeObservers: [NSObjectProtocol] = []
|
|
|
|
|
|
|
@@ -1699,6 +1587,9 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
private func refreshProductDisplay() {
|
|
private func refreshProductDisplay() {
|
|
|
let store = StoreManager.shared
|
|
let store = StoreManager.shared
|
|
|
let config = paywallConfig
|
|
let config = paywallConfig
|
|
|
|
|
+ let availablePlans = store.availablePlans
|
|
|
|
|
+ syncPlanCards(with: availablePlans)
|
|
|
|
|
+
|
|
|
for (plan, card) in planCards {
|
|
for (plan, card) in planCards {
|
|
|
let product = store.product(for: plan)
|
|
let product = store.product(for: plan)
|
|
|
card.updateDisplay(
|
|
card.updateDisplay(
|
|
@@ -1707,6 +1598,10 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
trial: store.trialDisplay(for: plan)
|
|
trial: store.trialDisplay(for: plan)
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ ensureValidSelectedPlan(from: availablePlans)
|
|
|
|
|
+ refreshPlansLayout(planCount: availablePlans.count)
|
|
|
|
|
+
|
|
|
ctaButton.title = selectedPlan.localizedCTATitle(
|
|
ctaButton.title = selectedPlan.localizedCTATitle(
|
|
|
from: store.product(for: selectedPlan),
|
|
from: store.product(for: selectedPlan),
|
|
|
config: config,
|
|
config: config,
|
|
@@ -1726,6 +1621,63 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
selectTrialEligiblePlanIfAvailable()
|
|
selectTrialEligiblePlanIfAvailable()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func syncPlanCards(with plans: [PaywallPlan]) {
|
|
|
|
|
+ guard let plansStack else { return }
|
|
|
|
|
+
|
|
|
|
|
+ let removedPlans = planCards.keys.filter { !plans.contains($0) }
|
|
|
|
|
+ for plan in removedPlans {
|
|
|
|
|
+ guard let card = planCards.removeValue(forKey: plan) else { continue }
|
|
|
|
|
+ plansStack.removeArrangedSubview(card)
|
|
|
|
|
+ card.removeFromSuperview()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for plan in plans where planCards[plan] == nil {
|
|
|
|
|
+ let card = PaywallPlanCard(plan: plan)
|
|
|
|
|
+ card.isChosen = plan == selectedPlan
|
|
|
|
|
+ card.onSelect = { [weak self] in self?.selectPlan(plan) }
|
|
|
|
|
+ planCards[plan] = card
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for view in plansStack.arrangedSubviews {
|
|
|
|
|
+ plansStack.removeArrangedSubview(view)
|
|
|
|
|
+ }
|
|
|
|
|
+ for plan in plans {
|
|
|
|
|
+ if let card = planCards[plan] {
|
|
|
|
|
+ plansStack.addArrangedSubview(card)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func ensureValidSelectedPlan(from availablePlans: [PaywallPlan]) {
|
|
|
|
|
+ guard !availablePlans.isEmpty else { return }
|
|
|
|
|
+ guard !availablePlans.contains(selectedPlan) else { return }
|
|
|
|
|
+
|
|
|
|
|
+ let preferred = paywallConfig.trialEligiblePlan
|
|
|
|
|
+ .flatMap { trialPlan in
|
|
|
|
|
+ availablePlans.contains(trialPlan) && StoreManager.shared.trialDisplay(for: trialPlan) != nil
|
|
|
|
|
+ ? trialPlan
|
|
|
|
|
+ : nil
|
|
|
|
|
+ }
|
|
|
|
|
+ ?? availablePlans.first(where: { $0 == .yearly })
|
|
|
|
|
+ ?? availablePlans[0]
|
|
|
|
|
+
|
|
|
|
|
+ selectPlan(preferred)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func refreshPlansLayout(planCount: Int) {
|
|
|
|
|
+ let height = plansAreaHeight(for: planCount)
|
|
|
|
|
+ plansStackHeightConstraint?.constant = height
|
|
|
|
|
+ plansAreaViewHeightConstraint?.constant = height
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func plansAreaHeight(for planCount: Int) -> CGFloat {
|
|
|
|
|
+ let cardHeight: CGFloat = 86
|
|
|
|
|
+ let spacing: CGFloat = 12
|
|
|
|
|
+ let count = CGFloat(max(planCount, 0))
|
|
|
|
|
+ guard count > 0 else { return 0 }
|
|
|
|
|
+ return count * cardHeight + max(0, count - 1) * spacing
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func refreshSecurePaymentNote() {
|
|
private func refreshSecurePaymentNote() {
|
|
|
let config = paywallConfig
|
|
let config = paywallConfig
|
|
|
securePaymentLabel.stringValue = selectedPlan == .lifetime
|
|
securePaymentLabel.stringValue = selectedPlan == .lifetime
|
|
@@ -1735,12 +1687,13 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
|
|
|
|
|
private func refreshProductsErrorState() {
|
|
private func refreshProductsErrorState() {
|
|
|
let store = StoreManager.shared
|
|
let store = StoreManager.shared
|
|
|
- let hasLoadFailure = store.productLoadError != nil && !store.hasAllProductsLoaded
|
|
|
|
|
|
|
+ let hasLoadFailure = store.productLoadError != nil && !store.hasAvailablePlans
|
|
|
let isRetrying = hasLoadFailure && store.isLoadingProducts
|
|
let isRetrying = hasLoadFailure && store.isLoadingProducts
|
|
|
|
|
|
|
|
productsErrorView.isHidden = !hasLoadFailure
|
|
productsErrorView.isHidden = !hasLoadFailure
|
|
|
- plansStack.isHidden = hasLoadFailure
|
|
|
|
|
- plansStackHeightConstraint.constant = hasLoadFailure ? 0 : plansAreaHeight
|
|
|
|
|
|
|
+ plansStack.isHidden = hasLoadFailure || store.availablePlans.isEmpty
|
|
|
|
|
+ let visiblePlanCount = hasLoadFailure ? 0 : store.availablePlans.count
|
|
|
|
|
+ refreshPlansLayout(planCount: visiblePlanCount)
|
|
|
productsErrorHeightConstraint.constant = hasLoadFailure ? productsErrorAreaHeight : 0
|
|
productsErrorHeightConstraint.constant = hasLoadFailure ? productsErrorAreaHeight : 0
|
|
|
|
|
|
|
|
if hasLoadFailure, let error = store.productLoadError {
|
|
if hasLoadFailure, let error = store.productLoadError {
|
|
@@ -1748,18 +1701,11 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 var productsErrorAreaHeight: CGFloat { 86 }
|
|
|
|
|
|
|
|
private func refreshPurchaseState() {
|
|
private func refreshPurchaseState() {
|
|
|
let store = StoreManager.shared
|
|
let store = StoreManager.shared
|
|
|
- let hasProductLoadFailure = store.productLoadError != nil && !store.hasAllProductsLoaded
|
|
|
|
|
|
|
+ let hasProductLoadFailure = store.productLoadError != nil && !store.hasAvailablePlans
|
|
|
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 || hasProductLoadFailure
|
|
let isDisabled = isBusy || !isProductReady || hasProductLoadFailure
|
|
@@ -1956,14 +1902,6 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
}
|
|
}
|
|
|
self.productsErrorView = productsErrorView
|
|
self.productsErrorView = productsErrorView
|
|
|
|
|
|
|
|
- for plan in PaywallPlan.allCases {
|
|
|
|
|
- let card = PaywallPlanCard(plan: plan)
|
|
|
|
|
- card.isChosen = plan == selectedPlan
|
|
|
|
|
- card.onSelect = { [weak self] in self?.selectPlan(plan) }
|
|
|
|
|
- planCards[plan] = card
|
|
|
|
|
- plansStack.addArrangedSubview(card)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
ctaButton.title = selectedPlan.localizedCTATitle(
|
|
ctaButton.title = selectedPlan.localizedCTATitle(
|
|
|
from: StoreManager.shared.product(for: selectedPlan),
|
|
from: StoreManager.shared.product(for: selectedPlan),
|
|
|
config: config,
|
|
config: config,
|
|
@@ -2009,8 +1947,9 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
plansAreaView.addSubview(plansStack)
|
|
plansAreaView.addSubview(plansStack)
|
|
|
plansAreaView.addSubview(productsErrorView)
|
|
plansAreaView.addSubview(productsErrorView)
|
|
|
|
|
|
|
|
- plansStackHeightConstraint = plansStack.heightAnchor.constraint(equalToConstant: plansAreaHeight)
|
|
|
|
|
|
|
+ plansStackHeightConstraint = plansStack.heightAnchor.constraint(equalToConstant: 0)
|
|
|
productsErrorHeightConstraint = productsErrorView.heightAnchor.constraint(equalToConstant: 0)
|
|
productsErrorHeightConstraint = productsErrorView.heightAnchor.constraint(equalToConstant: 0)
|
|
|
|
|
+ plansAreaViewHeightConstraint = plansAreaView.heightAnchor.constraint(equalToConstant: 0)
|
|
|
|
|
|
|
|
panel.addSubview(title)
|
|
panel.addSubview(title)
|
|
|
panel.addSubview(subtitle)
|
|
panel.addSubview(subtitle)
|
|
@@ -2034,7 +1973,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
plansAreaView.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
plansAreaView.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
|
plansAreaView.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
|
|
plansAreaView.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
|
|
|
plansAreaView.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 24),
|
|
plansAreaView.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 24),
|
|
|
- plansAreaView.heightAnchor.constraint(equalToConstant: plansAreaHeight),
|
|
|
|
|
|
|
+ plansAreaViewHeightConstraint,
|
|
|
|
|
|
|
|
plansStack.leadingAnchor.constraint(equalTo: plansAreaView.leadingAnchor),
|
|
plansStack.leadingAnchor.constraint(equalTo: plansAreaView.leadingAnchor),
|
|
|
plansStack.trailingAnchor.constraint(equalTo: plansAreaView.trailingAnchor),
|
|
plansStack.trailingAnchor.constraint(equalTo: plansAreaView.trailingAnchor),
|
|
@@ -2346,7 +2285,7 @@ final class PaywallOverlayView: NSView, AppearanceRefreshable {
|
|
|
|
|
|
|
|
private func refreshLoadingOverlay() {
|
|
private func refreshLoadingOverlay() {
|
|
|
let store = StoreManager.shared
|
|
let store = StoreManager.shared
|
|
|
- let shouldShow = store.isLoadingProducts && !store.hasAllProductsLoaded && store.productLoadError == nil
|
|
|
|
|
|
|
+ let shouldShow = store.isLoadingProducts && !store.hasAvailablePlans && store.productLoadError == nil
|
|
|
loadingOverlay.isHidden = !shouldShow
|
|
loadingOverlay.isHidden = !shouldShow
|
|
|
if shouldShow {
|
|
if shouldShow {
|
|
|
loadingIndicator.startAnimation(nil)
|
|
loadingIndicator.startAnimation(nil)
|
|
@@ -2470,9 +2409,8 @@ final class PaywallOverlayView: NSView, AppearanceRefreshable {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
Task { @MainActor in
|
|
Task { @MainActor in
|
|
|
- #if DEBUG
|
|
|
|
|
await PaywallConfigService.shared.refreshFromRemote()
|
|
await PaywallConfigService.shared.refreshFromRemote()
|
|
|
- #endif
|
|
|
|
|
|
|
+ paywallView.refreshStoreState()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|