Prechádzať zdrojové kódy

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 týždňov pred
rodič
commit
0871e3e78f

+ 40 - 1
gramora/Managers/SubscriptionManager.swift

@@ -74,6 +74,9 @@ final class SubscriptionManager: ObservableObject {
     private var inFlightProductLoad: Task<Void, Never>?
     private var cancellables = Set<AnyCancellable>()
     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) {
         self.defaults = defaults
@@ -394,11 +397,19 @@ final class SubscriptionManager: ObservableObject {
         var manageableSubscription = false
         var resolvedPlan: PaywallPlan?
         var resolvedRecurringPlan: PaywallPlan?
+        var revokedPendingPlan = false
 
         for await result in Transaction.currentEntitlements {
             guard case .verified(let transaction) = result 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 await isRecurringSubscriptionEntitled(transaction: transaction) else {
@@ -407,6 +418,7 @@ final class SubscriptionManager: ObservableObject {
 
             manageableSubscription = true
             hasPremium = true
+            pendingVerifiedPurchase = nil
             if resolvedRecurringPlan == nil {
                 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
         hasManageableSubscription = manageableSubscription
         activePlan = resolvedPlan
@@ -427,6 +449,7 @@ final class SubscriptionManager: ObservableObject {
               transaction.revocationDate == nil,
               let plan = SubscriptionProductID.plan(for: transaction.productID) else { return }
 
+        pendingVerifiedPurchase = (plan: plan, purchasedAt: Date())
         hasPremiumAccess = true
         hasResolvedPremiumStatus = true
         activePlan = plan
@@ -458,9 +481,20 @@ final class SubscriptionManager: ObservableObject {
             await refreshPremiumAccess()
             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 {
+        guard transaction.revocationDate == nil else { return false }
+
         let productID = transaction.productID
         let product: Product?
         if let cached = productsByID[productID] {
@@ -514,6 +548,11 @@ final class SubscriptionManager: ObservableObject {
         for await update in Transaction.updates {
             switch update {
             case .verified(let transaction):
+                if transaction.revocationDate != nil,
+                   let plan = SubscriptionProductID.plan(for: transaction.productID),
+                   pendingVerifiedPurchase?.plan == plan {
+                    pendingVerifiedPurchase = nil
+                }
                 await transaction.finish()
                 await refreshPremiumAccess()
             case .unverified(_, let error):

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

@@ -11,11 +11,11 @@ struct PremiumCardView: View {
 
     private var title: String {
         if isCheckingSubscription { return "Checking subscription…" }
-        return isPremium ? AppBrand.premiumName : "Unlock Premium"
+        return isPremium ? AppBrand.premiumName : "Upgrade to PRO"
     }
 
     private var premiumButtonTitle: String {
-        if !isPremium { return "Upgrade Now" }
+        if !isPremium { return "Upgrade to Pro" }
         return hasManageableSubscription ? "Manage Subscription" : "View Premium"
     }
 
@@ -24,7 +24,7 @@ struct PremiumCardView: View {
             return "Verifying your plan with the App Store."
         }
         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 {
             let useLabel = remainingFreeAIUses == 1 ? "use" : "uses"

+ 3 - 3
gramora/Views/SettingsView.swift

@@ -161,8 +161,8 @@ struct SettingsView: View {
 
                         SettingsActionRow(
                             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",
                             isDestructive: false
                         ) {
@@ -197,7 +197,7 @@ struct SettingsView: View {
             }
             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 {