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

Fix premium UI state after purchase and align upgrade copy.

Keep verified purchases reflected in the sidebar until StoreKit entitlements sync, revoke access on refunds, and standardize PRO upgrade wording across the app.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 2 недель назад
Родитель
Сommit
0871e3e78f

+ 40 - 1
gramora/Managers/SubscriptionManager.swift

@@ -74,6 +74,9 @@ final class SubscriptionManager: ObservableObject {
     private var inFlightProductLoad: Task<Void, Never>?
     private var inFlightProductLoad: Task<Void, Never>?
     private var cancellables = Set<AnyCancellable>()
     private var cancellables = Set<AnyCancellable>()
     private let defaults: UserDefaults
     private let defaults: UserDefaults
+    /// Keeps premium UI in sync after a verified purchase until StoreKit entitlements catch up.
+    private var pendingVerifiedPurchase: (plan: PaywallPlan, purchasedAt: Date)?
+    private static let pendingPurchaseGraceInterval: TimeInterval = 24 * 60 * 60
 
 
     init(defaults: UserDefaults = .standard) {
     init(defaults: UserDefaults = .standard) {
         self.defaults = defaults
         self.defaults = defaults
@@ -394,11 +397,19 @@ final class SubscriptionManager: ObservableObject {
         var manageableSubscription = false
         var manageableSubscription = false
         var resolvedPlan: PaywallPlan?
         var resolvedPlan: PaywallPlan?
         var resolvedRecurringPlan: PaywallPlan?
         var resolvedRecurringPlan: PaywallPlan?
+        var revokedPendingPlan = false
 
 
         for await result in Transaction.currentEntitlements {
         for await result in Transaction.currentEntitlements {
             guard case .verified(let transaction) = result else { continue }
             guard case .verified(let transaction) = result else { continue }
             guard SubscriptionProductID.all.contains(transaction.productID) else { continue }
             guard SubscriptionProductID.all.contains(transaction.productID) else { continue }
-            guard transaction.revocationDate == nil else { continue }
+
+            if transaction.revocationDate != nil {
+                if pendingVerifiedPurchase?.plan == SubscriptionProductID.plan(for: transaction.productID) {
+                    revokedPendingPlan = true
+                    pendingVerifiedPurchase = nil
+                }
+                continue
+            }
 
 
             guard let plan = SubscriptionProductID.plan(for: transaction.productID) else { continue }
             guard let plan = SubscriptionProductID.plan(for: transaction.productID) else { continue }
             guard await isRecurringSubscriptionEntitled(transaction: transaction) else {
             guard await isRecurringSubscriptionEntitled(transaction: transaction) else {
@@ -407,6 +418,7 @@ final class SubscriptionManager: ObservableObject {
 
 
             manageableSubscription = true
             manageableSubscription = true
             hasPremium = true
             hasPremium = true
+            pendingVerifiedPurchase = nil
             if resolvedRecurringPlan == nil {
             if resolvedRecurringPlan == nil {
                 resolvedRecurringPlan = plan
                 resolvedRecurringPlan = plan
             }
             }
@@ -415,6 +427,16 @@ final class SubscriptionManager: ObservableObject {
             }
             }
         }
         }
 
 
+        if !hasPremium,
+           !revokedPendingPlan,
+           let pending = pendingVerifiedPurchase,
+           Date().timeIntervalSince(pending.purchasedAt) < Self.pendingPurchaseGraceInterval {
+            hasPremium = true
+            manageableSubscription = true
+            resolvedPlan = pending.plan
+            resolvedRecurringPlan = pending.plan
+        }
+
         hasPremiumAccess = hasPremium
         hasPremiumAccess = hasPremium
         hasManageableSubscription = manageableSubscription
         hasManageableSubscription = manageableSubscription
         activePlan = resolvedPlan
         activePlan = resolvedPlan
@@ -427,6 +449,7 @@ final class SubscriptionManager: ObservableObject {
               transaction.revocationDate == nil,
               transaction.revocationDate == nil,
               let plan = SubscriptionProductID.plan(for: transaction.productID) else { return }
               let plan = SubscriptionProductID.plan(for: transaction.productID) else { return }
 
 
+        pendingVerifiedPurchase = (plan: plan, purchasedAt: Date())
         hasPremiumAccess = true
         hasPremiumAccess = true
         hasResolvedPremiumStatus = true
         hasResolvedPremiumStatus = true
         activePlan = plan
         activePlan = plan
@@ -458,9 +481,20 @@ final class SubscriptionManager: ObservableObject {
             await refreshPremiumAccess()
             await refreshPremiumAccess()
             if hasPremiumAccess { return }
             if hasPremiumAccess { return }
         }
         }
+
+        if let pending = pendingVerifiedPurchase,
+           Date().timeIntervalSince(pending.purchasedAt) < Self.pendingPurchaseGraceInterval {
+            hasPremiumAccess = true
+            hasResolvedPremiumStatus = true
+            hasManageableSubscription = true
+            activePlan = pending.plan
+            activeRecurringPlan = pending.plan
+        }
     }
     }
 
 
     private func isRecurringSubscriptionEntitled(transaction: Transaction) async -> Bool {
     private func isRecurringSubscriptionEntitled(transaction: Transaction) async -> Bool {
+        guard transaction.revocationDate == nil else { return false }
+
         let productID = transaction.productID
         let productID = transaction.productID
         let product: Product?
         let product: Product?
         if let cached = productsByID[productID] {
         if let cached = productsByID[productID] {
@@ -514,6 +548,11 @@ final class SubscriptionManager: ObservableObject {
         for await update in Transaction.updates {
         for await update in Transaction.updates {
             switch update {
             switch update {
             case .verified(let transaction):
             case .verified(let transaction):
+                if transaction.revocationDate != nil,
+                   let plan = SubscriptionProductID.plan(for: transaction.productID),
+                   pendingVerifiedPurchase?.plan == plan {
+                    pendingVerifiedPurchase = nil
+                }
                 await transaction.finish()
                 await transaction.finish()
                 await refreshPremiumAccess()
                 await refreshPremiumAccess()
             case .unverified(_, let error):
             case .unverified(_, let error):

+ 3 - 3
gramora/Views/Components/PremiumCardView.swift

@@ -11,11 +11,11 @@ struct PremiumCardView: View {
 
 
     private var title: String {
     private var title: String {
         if isCheckingSubscription { return "Checking subscription…" }
         if isCheckingSubscription { return "Checking subscription…" }
-        return isPremium ? AppBrand.premiumName : "Unlock Premium"
+        return isPremium ? AppBrand.premiumName : "Upgrade to PRO"
     }
     }
 
 
     private var premiumButtonTitle: String {
     private var premiumButtonTitle: String {
-        if !isPremium { return "Upgrade Now" }
+        if !isPremium { return "Upgrade to Pro" }
         return hasManageableSubscription ? "Manage Subscription" : "View Premium"
         return hasManageableSubscription ? "Manage Subscription" : "View Premium"
     }
     }
 
 
@@ -24,7 +24,7 @@ struct PremiumCardView: View {
             return "Verifying your plan with the App Store."
             return "Verifying your plan with the App Store."
         }
         }
         if isPremium {
         if isPremium {
-            return "You have unlimited checks, advanced features and an ad-free experience."
+            return "You have unlimited checks and access to all advanced features."
         }
         }
         if remainingFreeAIUses > 0 {
         if remainingFreeAIUses > 0 {
             let useLabel = remainingFreeAIUses == 1 ? "use" : "uses"
             let useLabel = remainingFreeAIUses == 1 ? "use" : "uses"

+ 3 - 3
gramora/Views/SettingsView.swift

@@ -161,8 +161,8 @@ struct SettingsView: View {
 
 
                         SettingsActionRow(
                         SettingsActionRow(
                             iconName: "crown.fill",
                             iconName: "crown.fill",
-                            title: "Upgrade to Premium",
-                            subtitle: "Unlock all premium features",
+                            title: "Upgrade to PRO",
+                            subtitle: "Unlock unlimited AI and advanced features",
                             actionTitle: "View Plans",
                             actionTitle: "View Plans",
                             isDestructive: false
                             isDestructive: false
                         ) {
                         ) {
@@ -197,7 +197,7 @@ struct SettingsView: View {
             }
             }
             return "Premium access is active"
             return "Premium access is active"
         }
         }
-        return "Upgrade to unlock all premium features"
+        return "Upgrade to PRO for unlimited AI and advanced features"
     }
     }
 
 
     private var appearanceSection: some View {
     private var appearanceSection: some View {