|
|
@@ -285,6 +285,12 @@ final class StoreManager {
|
|
|
await initialEntitlementTask?.value
|
|
|
}
|
|
|
|
|
|
+ func ensurePaywallStoreDataLoaded(presentLoadingUI: Bool = true) async {
|
|
|
+ await ensureEntitlementsResolved()
|
|
|
+ await loadProducts(presentLoadingUI: presentLoadingUI && !hasAllProductsLoaded)
|
|
|
+ await refreshTrialOffers()
|
|
|
+ }
|
|
|
+
|
|
|
func product(for plan: PaywallPlan) -> Product? {
|
|
|
productsByID[plan.productID]
|
|
|
}
|
|
|
@@ -390,6 +396,7 @@ final class StoreManager {
|
|
|
}
|
|
|
|
|
|
await reconcilePremiumAccess(trustedPlan: nil)
|
|
|
+ await syncPurchaseHistory(allowReset: true)
|
|
|
if !isPremium {
|
|
|
purchaseError = .noActivePurchases
|
|
|
}
|
|
|
@@ -454,6 +461,7 @@ final class StoreManager {
|
|
|
guard !loaded.isEmpty else {
|
|
|
productLoadError = PaywallConfigService.shared.config.messages.noPlansAvailable
|
|
|
productsByID = [:]
|
|
|
+ await refreshTrialOffers()
|
|
|
return
|
|
|
}
|
|
|
var map: [String: Product] = [:]
|
|
|
@@ -468,9 +476,11 @@ final class StoreManager {
|
|
|
} catch is ProductLoadError {
|
|
|
productLoadError = PaywallConfigService.shared.config.messages.plansLoadFailed
|
|
|
purchaseError = .productsLoadTimeout
|
|
|
+ await refreshTrialOffers()
|
|
|
} catch {
|
|
|
productLoadError = PaywallConfigService.shared.config.messages.plansLoadFailed
|
|
|
purchaseError = .generic
|
|
|
+ await refreshTrialOffers()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -520,15 +530,12 @@ final class StoreManager {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- var finalPlan = mergeTrustedPlan(resolvedPlan, trustedPlan)
|
|
|
- if finalPlan == nil {
|
|
|
- finalPlan = restoredCachedPremiumPlan()
|
|
|
- }
|
|
|
+ let finalPlan = mergeTrustedPlan(resolvedPlan, trustedPlan)
|
|
|
|
|
|
applyPremiumStatus(plan: finalPlan)
|
|
|
hasResolvedPremiumStatus = true
|
|
|
NotificationCenter.default.post(name: .entitlementsDidResolve, object: nil)
|
|
|
- await syncPurchaseHistory()
|
|
|
+ await syncPurchaseHistory(allowReset: false)
|
|
|
await refreshTrialOffers()
|
|
|
}
|
|
|
|
|
|
@@ -607,7 +614,7 @@ final class StoreManager {
|
|
|
UserDefaults.standard.set(true, forKey: Self.cachedHasEverPurchasedKey)
|
|
|
}
|
|
|
|
|
|
- private func syncPurchaseHistory() async {
|
|
|
+ private func syncPurchaseHistory(allowReset: Bool) async {
|
|
|
var hasRecordedPurchase = false
|
|
|
|
|
|
for await result in Transaction.all {
|
|
|
@@ -622,7 +629,7 @@ final class StoreManager {
|
|
|
hasEverPurchasedPremium = true
|
|
|
UserDefaults.standard.set(true, forKey: Self.cachedHasEverPurchasedKey)
|
|
|
}
|
|
|
- } else if hasEverPurchasedPremium {
|
|
|
+ } else if allowReset, hasEverPurchasedPremium {
|
|
|
resetToNeverPurchasedState()
|
|
|
}
|
|
|
}
|
|
|
@@ -634,7 +641,7 @@ final class StoreManager {
|
|
|
}
|
|
|
|
|
|
private func refreshTrialOffers() async {
|
|
|
- guard isEligibleForIntroTrial else {
|
|
|
+ guard !isPremium else {
|
|
|
trialDisplayByPlan = [:]
|
|
|
postStoreStateDidChange()
|
|
|
return
|
|
|
@@ -656,13 +663,28 @@ final class StoreManager {
|
|
|
|
|
|
private func resolveTrialDisplay(for plan: PaywallPlan) async -> PaywallTrialDisplay? {
|
|
|
guard plan == PaywallConfigService.shared.config.trialEligiblePlan else { return nil }
|
|
|
+ guard !isPremium else { return nil }
|
|
|
+
|
|
|
+ let config = PaywallConfigService.shared.config
|
|
|
+
|
|
|
guard let product = product(for: plan),
|
|
|
let subscription = product.subscription else {
|
|
|
- return nil
|
|
|
+ return fallbackTrialDisplay(config: config)
|
|
|
}
|
|
|
+
|
|
|
guard await subscription.isEligibleForIntroOffer else { return nil }
|
|
|
- guard let offer = subscription.introductoryOffer else { return nil }
|
|
|
- return PaywallTrialDisplay.from(offer: offer, config: PaywallConfigService.shared.config)
|
|
|
+
|
|
|
+ if let offer = subscription.introductoryOffer {
|
|
|
+ return PaywallTrialDisplay.from(offer: offer, config: config)
|
|
|
+ }
|
|
|
+
|
|
|
+ return fallbackTrialDisplay(config: config)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func fallbackTrialDisplay(config: PaywallConfig) -> PaywallTrialDisplay? {
|
|
|
+ guard !isPremium else { return nil }
|
|
|
+ guard let fallback = config.trial.fallbackDuration else { return nil }
|
|
|
+ return PaywallTrialDisplay.from(fallback: fallback, config: config)
|
|
|
}
|
|
|
|
|
|
private func applyPremiumStatus(plan: PaywallPlan?) {
|
|
|
@@ -816,9 +838,6 @@ enum PremiumAccess {
|
|
|
@MainActor
|
|
|
static func canAccess(_ feature: AppFeature) -> Bool {
|
|
|
let store = StoreManager.shared
|
|
|
- if store.isResolvingEntitlements {
|
|
|
- return FreeTierManager.canAccess(feature, isPremium: false)
|
|
|
- }
|
|
|
return FreeTierManager.canAccess(feature, isPremium: store.isPremium)
|
|
|
}
|
|
|
|
|
|
@@ -841,12 +860,15 @@ enum PremiumAccess {
|
|
|
static func requireForAction(
|
|
|
feature: AppFeature,
|
|
|
from window: NSWindow?,
|
|
|
- returnToHomeOnDismiss: Bool = false
|
|
|
+ returnToHomeOnDismiss: Bool = false,
|
|
|
+ recordFreeUse: Bool = true
|
|
|
) -> Bool {
|
|
|
guard require(feature: feature, from: window, returnToHomeOnDismiss: returnToHomeOnDismiss) else {
|
|
|
return false
|
|
|
}
|
|
|
- recordFreeUseIfNeeded()
|
|
|
+ if recordFreeUse {
|
|
|
+ recordFreeUseIfNeeded()
|
|
|
+ }
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
@@ -1172,12 +1194,13 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
|
|
|
foreground: AppTheme.paywallPinkText
|
|
|
)
|
|
|
trialBadgeView = badge
|
|
|
- addSubview(badge)
|
|
|
+ addSubview(badge, positioned: .above, relativeTo: nil)
|
|
|
NSLayoutConstraint.activate([
|
|
|
badge.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
|
|
|
badge.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
|
|
])
|
|
|
}
|
|
|
+ trialBadgeView?.layer?.zPosition = 10
|
|
|
} else {
|
|
|
trialBadgeView?.isHidden = true
|
|
|
}
|
|
|
@@ -2441,14 +2464,8 @@ final class PaywallOverlayView: NSView, AppearanceRefreshable {
|
|
|
paywallView.refreshStoreState()
|
|
|
|
|
|
Task { @MainActor in
|
|
|
- if StoreManager.shared.isResolvingEntitlements {
|
|
|
- await StoreManager.shared.ensureEntitlementsResolved()
|
|
|
- paywallView.refreshStoreState()
|
|
|
- }
|
|
|
- if !StoreManager.shared.hasAllProductsLoaded, !StoreManager.shared.isLoadingProducts {
|
|
|
- await StoreManager.shared.loadProducts(presentLoadingUI: true)
|
|
|
- paywallView.refreshStoreState()
|
|
|
- }
|
|
|
+ await StoreManager.shared.ensurePaywallStoreDataLoaded()
|
|
|
+ paywallView.refreshStoreState()
|
|
|
refreshLoadingOverlay()
|
|
|
}
|
|
|
|