Просмотр исходного кода

Differentiate lifetime and subscription UX in the paywall.

Track premium access kind so lifetime buyers no longer see Manage Subscription, and update trust copy when the lifetime plan is selected.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 4 недель назад
Родитель
Сommit
2c0096a7a8
1 измененных файлов с 69 добавлено и 27 удалено
  1. 69 27
      smart_printer/PaywallView.swift

+ 69 - 27
smart_printer/PaywallView.swift

@@ -99,6 +99,12 @@ enum StoreProductID {
     static let all = AppStoreConfig.ProductID.all
     static let all = AppStoreConfig.ProductID.all
 }
 }
 
 
+enum PremiumAccessKind: String {
+    case none
+    case subscription
+    case lifetime
+}
+
 enum StoreError: LocalizedError {
 enum StoreError: LocalizedError {
     case productNotFound
     case productNotFound
     case failedVerification
     case failedVerification
@@ -118,10 +124,21 @@ final class StoreManager {
     static let shared = StoreManager()
     static let shared = StoreManager()
 
 
     private static let cachedPremiumKey = "StoreManager.cachedIsPremium"
     private static let cachedPremiumKey = "StoreManager.cachedIsPremium"
+    private static let cachedPremiumKindKey = "StoreManager.cachedPremiumAccessKind"
 
 
     private(set) var products: [Product] = []
     private(set) var products: [Product] = []
     private(set) var isPremium = UserDefaults.standard.bool(forKey: cachedPremiumKey)
     private(set) var isPremium = UserDefaults.standard.bool(forKey: cachedPremiumKey)
+    private(set) var premiumAccessKind: PremiumAccessKind = {
+        guard let raw = UserDefaults.standard.string(forKey: cachedPremiumKindKey),
+              let kind = PremiumAccessKind(rawValue: raw) else {
+            return .none
+        }
+        return kind
+    }()
+
     var isPro: Bool { isPremium }
     var isPro: Bool { isPremium }
+    var hasActiveSubscription: Bool { premiumAccessKind == .subscription }
+    var hasLifetimeAccess: Bool { premiumAccessKind == .lifetime }
     private(set) var isLoadingProducts = false
     private(set) var isLoadingProducts = false
     private(set) var isPurchasing = false
     private(set) var isPurchasing = false
     private(set) var isResolvingEntitlements = true
     private(set) var isResolvingEntitlements = true
@@ -284,18 +301,21 @@ final class StoreManager {
     }
     }
 
 
     func refreshEntitlementsOnly() async {
     func refreshEntitlementsOnly() async {
-        applyPremiumStatus(await hasActivePremiumAccess())
+        applyPremiumStatus(await resolvePremiumAccessKind())
     }
     }
 
 
     func refreshPremiumStatus() async {
     func refreshPremiumStatus() async {
         await refreshIntroOfferEligibility()
         await refreshIntroOfferEligibility()
-        applyPremiumStatus(await hasActivePremiumAccess())
+        applyPremiumStatus(await resolvePremiumAccessKind())
     }
     }
 
 
-    private func applyPremiumStatus(_ hasPremium: Bool) {
-        let didChange = hasPremium != isPremium
+    private func applyPremiumStatus(_ kind: PremiumAccessKind) {
+        let hasPremium = kind != .none
+        let didChange = hasPremium != isPremium || kind != premiumAccessKind
         isPremium = hasPremium
         isPremium = hasPremium
+        premiumAccessKind = hasPremium ? kind : .none
         UserDefaults.standard.set(hasPremium, forKey: Self.cachedPremiumKey)
         UserDefaults.standard.set(hasPremium, forKey: Self.cachedPremiumKey)
+        UserDefaults.standard.set(premiumAccessKind.rawValue, forKey: Self.cachedPremiumKindKey)
 
 
         if didChange {
         if didChange {
             NotificationCenter.default.post(name: .premiumStatusDidChange, object: nil)
             NotificationCenter.default.post(name: .premiumStatusDidChange, object: nil)
@@ -303,38 +323,35 @@ final class StoreManager {
         postStoreStateDidChange()
         postStoreStateDidChange()
     }
     }
 
 
-    private func hasActivePremiumAccess() async -> Bool {
-        if await hasEntitlementFromCurrentEntitlements() {
-            return true
+    private func resolvePremiumAccessKind() async -> PremiumAccessKind {
+        if await hasActiveEntitlement(for: StoreProductID.lifetime) {
+            return .lifetime
+        }
+        if await hasActiveEntitlement(for: StoreProductID.monthly) {
+            return .subscription
         }
         }
-        if await hasEntitlementFromLatestTransactions() {
-            return true
+        if await hasActiveEntitlement(for: StoreProductID.yearly) {
+            return .subscription
         }
         }
         if await hasEntitlementFromSubscriptionStatus() {
         if await hasEntitlementFromSubscriptionStatus() {
-            return true
+            return .subscription
         }
         }
-        return false
+        return .none
     }
     }
 
 
-    private func hasEntitlementFromCurrentEntitlements() async -> Bool {
+    private func hasActiveEntitlement(for productID: String) async -> Bool {
         for await result in Transaction.currentEntitlements {
         for await result in Transaction.currentEntitlements {
             guard let transaction = try? checkVerified(result) else { continue }
             guard let transaction = try? checkVerified(result) else { continue }
-            if isActivePremiumTransaction(transaction) {
+            if transaction.productID == productID, isActivePremiumTransaction(transaction) {
                 return true
                 return true
             }
             }
         }
         }
-        return false
-    }
 
 
-    private func hasEntitlementFromLatestTransactions() async -> Bool {
-        for productID in StoreProductID.all {
-            guard let result = await Transaction.latest(for: productID),
-                  let transaction = try? checkVerified(result) else { continue }
-            if isActivePremiumTransaction(transaction) {
-                return true
-            }
+        guard let result = await Transaction.latest(for: productID),
+              let transaction = try? checkVerified(result) else {
+            return false
         }
         }
-        return false
+        return isActivePremiumTransaction(transaction)
     }
     }
 
 
     private func hasEntitlementFromSubscriptionStatus() async -> Bool {
     private func hasEntitlementFromSubscriptionStatus() async -> Bool {
@@ -1136,6 +1153,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
             config: config,
             config: config,
             introOffer: store.eligibleIntroOffer(for: selectedPlan)
             introOffer: store.eligibleIntroOffer(for: selectedPlan)
         )
         )
+        refreshTrustItemsForSelectedPlan()
         refreshPurchaseState()
         refreshPurchaseState()
     }
     }
 
 
@@ -1150,14 +1168,37 @@ final class PaywallView: NSView, AppearanceRefreshable {
     }
     }
 
 
     private func refreshPrimaryFooterLink() {
     private func refreshPrimaryFooterLink() {
-        let isPro = StoreManager.shared.isPro
-        continueFreePlanLink.isHidden = isPro
-        manageSubscriptionLink.isHidden = !isPro
-        premiumCloseButton.isHidden = !isPro
+        let store = StoreManager.shared
+        let showContinue = !store.isPro
+        let showManage = store.hasActiveSubscription
+
+        continueFreePlanLink.isHidden = !showContinue
+        manageSubscriptionLink.isHidden = !showManage
+        premiumCloseButton.isHidden = !store.isPro
+        primaryFooterLinkCell?.isHidden = !showContinue && !showManage
         primaryFooterLinkCell?.needsLayout = true
         primaryFooterLinkCell?.needsLayout = true
         primaryFooterLinkCell?.layoutSubtreeIfNeeded()
         primaryFooterLinkCell?.layoutSubtreeIfNeeded()
     }
     }
 
 
+    private func refreshTrustItemsForSelectedPlan() {
+        guard trustItemViews.count > 1, paywallConfig.trustItems.count > 1 else { return }
+
+        let cancelItem = paywallConfig.trustItems[1]
+        if selectedPlan == .lifetime {
+            trustItemViews[1].update(
+                iconName: "checkmark.seal.fill",
+                title: "One-Time Purchase",
+                subtitle: "Pay once, no recurring charges."
+            )
+        } else {
+            trustItemViews[1].update(
+                iconName: cancelItem.icon,
+                title: cancelItem.title,
+                subtitle: cancelItem.subtitle
+            )
+        }
+    }
+
     func refreshStoreState() {
     func refreshStoreState() {
         refreshProductDisplay()
         refreshProductDisplay()
     }
     }
@@ -1464,6 +1505,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
             config: paywallConfig,
             config: paywallConfig,
             introOffer: StoreManager.shared.eligibleIntroOffer(for: plan)
             introOffer: StoreManager.shared.eligibleIntroOffer(for: plan)
         )
         )
+        refreshTrustItemsForSelectedPlan()
         refreshPurchaseState()
         refreshPurchaseState()
     }
     }