Переглянути джерело

Remove lifetime purchase support now that the app is subscription-only.

Drop the lifetime product from StoreKit, paywall, and subscription logic, and align website legal copy with subscriptions-only billing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hussain Afzal 3 тижнів тому
батько
коміт
9c0cb48a25

+ 11 - 46
gramora/Managers/SubscriptionManager.swift

@@ -53,8 +53,6 @@ final class SubscriptionManager: ObservableObject {
     @Published private(set) var freeAIUsesConsumed = 0
     @Published var purchaseError: PurchaseError?
     @Published var subscriptionSuccessMessage: String?
-    @Published var showsCancelSubscriptionReminder = false
-    @Published private(set) var recurringPlanPendingCancellation: PaywallPlan?
 
     var hasAllProductsLoaded: Bool {
         SubscriptionProductID.all.allSatisfy { productsByID[$0] != nil }
@@ -151,9 +149,6 @@ final class SubscriptionManager: ObservableObject {
         case .yearly:
             let weekly = product.price / Decimal(52)
             return "\(weekly.formatted(formatStyle)) / week"
-        case .lifetime:
-            let doubled = product.price * 2
-            return doubled.formatted(formatStyle)
         }
     }
 
@@ -167,8 +162,6 @@ final class SubscriptionManager: ObservableObject {
             return "Basic"
         case .yearly:
             return savingsTagText(for: plan)
-        case .lifetime:
-            return "Pay Once"
         case .monthly:
             return nil
         }
@@ -183,7 +176,6 @@ final class SubscriptionManager: ObservableObject {
         case .weekly: return "START WEEKLY PLAN"
         case .monthly: return "START MONTHLY PLAN"
         case .yearly: return "START YEARLY PLAN"
-        case .lifetime: return "UNLOCK LIFETIME"
         }
     }
 
@@ -207,13 +199,11 @@ final class SubscriptionManager: ObservableObject {
             return "Billed at \(product.displayPrice) per month"
         case .yearly:
             return "Billed at \(product.displayPrice) every year"
-        case .lifetime:
-            return "One-time payment of \(product.displayPrice)"
         }
     }
 
     func showsStrikethroughSecondaryPrice(for plan: PaywallPlan) -> Bool {
-        plan == .weekly || plan == .lifetime
+        plan == .weekly
     }
 
     private func savingsTagText(for plan: PaywallPlan) -> String? {
@@ -279,9 +269,6 @@ final class SubscriptionManager: ObservableObject {
         purchasingPlan = plan
         defer { purchasingPlan = nil }
 
-        let shouldRemindToCancelSubscription = plan == .lifetime && hasManageableSubscription
-        let recurringPlanBeforePurchase = activeRecurringPlan ?? activePlan
-
         do {
             let result = try await product.purchase()
             switch result {
@@ -295,14 +282,10 @@ final class SubscriptionManager: ObservableObject {
                 await refreshPremiumAccessWithRetry()
                 if hasPremiumAccess {
                     NotificationCenter.default.post(name: .subscriptionPurchased, object: nil)
-                }
-                if shouldRemindToCancelSubscription {
-                    recurringPlanPendingCancellation = recurringPlanBeforePurchase
-                    showsCancelSubscriptionReminder = true
-                } else if !hasPremiumAccess {
+                } else {
                     subscriptionSuccessMessage = "Purchase successful. If premium features don't unlock right away, tap Restore Purchases."
                 }
-                return hasPremiumAccess || shouldRemindToCancelSubscription
+                return hasPremiumAccess
             case .userCancelled:
                 return false
             case .pending:
@@ -346,19 +329,6 @@ final class SubscriptionManager: ObservableObject {
         subscriptionSuccessMessage = nil
     }
 
-    func clearCancelSubscriptionReminder() {
-        showsCancelSubscriptionReminder = false
-        recurringPlanPendingCancellation = nil
-    }
-
-    var cancelSubscriptionReminderMessage: String {
-        if let plan = recurringPlanPendingCancellation {
-            let planName = displayTitle(for: plan)
-            return "Your lifetime access is active. Your \(planName) subscription is still active and will renew automatically. Cancel it in the App Store to avoid future charges."
-        }
-        return "Your lifetime access is active. Your recurring subscription is still active and will renew automatically. Cancel it in the App Store to avoid future charges."
-    }
-
     private func performLoadProducts(presentLoadingUI: Bool?) async {
         let missingAny = SubscriptionProductID.all.contains { productsByID[$0] == nil }
         let shouldShowLoading = presentLoadingUI ?? missingAny
@@ -431,18 +401,15 @@ final class SubscriptionManager: ObservableObject {
             guard transaction.revocationDate == nil else { continue }
 
             guard let plan = SubscriptionProductID.plan(for: transaction.productID) else { continue }
-
-            if SubscriptionProductID.recurring.contains(transaction.productID) {
-                guard await isRecurringSubscriptionEntitled(transaction: transaction) else {
-                    continue
-                }
-                manageableSubscription = true
-                if resolvedRecurringPlan == nil {
-                    resolvedRecurringPlan = plan
-                }
+            guard await isRecurringSubscriptionEntitled(transaction: transaction) else {
+                continue
             }
 
+            manageableSubscription = true
             hasPremium = true
+            if resolvedRecurringPlan == nil {
+                resolvedRecurringPlan = plan
+            }
             if resolvedPlan == nil {
                 resolvedPlan = plan
             }
@@ -463,10 +430,8 @@ final class SubscriptionManager: ObservableObject {
         hasPremiumAccess = true
         hasResolvedPremiumStatus = true
         activePlan = plan
-        if SubscriptionProductID.recurring.contains(transaction.productID) {
-            hasManageableSubscription = true
-            activeRecurringPlan = plan
-        }
+        hasManageableSubscription = true
+        activeRecurringPlan = plan
     }
 
     private func refreshIntroOfferEligibility() async {

+ 0 - 2
gramora/Models/PaywallModels.swift

@@ -12,7 +12,6 @@ enum PaywallPlan: String, CaseIterable, Identifiable {
     case weekly
     case monthly
     case yearly
-    case lifetime
 
     var id: String { rawValue }
 
@@ -21,7 +20,6 @@ enum PaywallPlan: String, CaseIterable, Identifiable {
         case .weekly: SubscriptionProductID.weekly
         case .monthly: SubscriptionProductID.monthly
         case .yearly: SubscriptionProductID.yearly
-        case .lifetime: SubscriptionProductID.lifetime
         }
     }
 }

+ 3 - 17
gramora/Paywall.storekit

@@ -14,21 +14,7 @@
 
   ],
   "products" : [
-    {
-      "displayPrice" : "59.99",
-      "familyShareable" : false,
-      "internalID" : "6785220646",
-      "localizations" : [
-        {
-          "description" : "Unlock Grammar Pro Premium forever.",
-          "displayName" : "Lifetime",
-          "locale" : "en_US"
-        }
-      ],
-      "productID" : "com.grammarpro.pro.lifetime",
-      "referenceName" : "Lifetime",
-      "type" : "NonConsumable"
-    }
+
   ],
   "settings" : {
     "_applicationInternalID" : "6785216263",
@@ -38,7 +24,7 @@
     "_developerTeamID" : "LY29MGDPPD",
     "_disableDialogs" : false,
     "_failTransactionsEnabled" : false,
-    "_lastSynchronizedDate" : 804341970.10229504,
+    "_lastSynchronizedDate" : 804604882.87108397,
     "_locale" : "en_US",
     "_renewalBillingIssuesEnabled" : false,
     "_storefront" : "USA",
@@ -123,7 +109,7 @@
           "groupNumber" : 3,
           "internalID" : "6785220317",
           "introductoryOffer" : {
-            "internalID" : "1455D143",
+            "internalID" : "77C335A0",
             "numberOfPeriods" : 1,
             "paymentMode" : "free",
             "subscriptionPeriod" : "P3D"

+ 1 - 3
gramora/Utilities/SubscriptionProductID.swift

@@ -4,9 +4,8 @@ enum SubscriptionProductID {
     static let weekly = "com.grammarpro.pro.weekly"
     static let monthly = "com.grammarpro.pro.monthly"
     static let yearly = "com.grammarpro.pro.yearly"
-    static let lifetime = "com.grammarpro.pro.lifetime"
 
-    static let all: Set<String> = [weekly, monthly, yearly, lifetime]
+    static let all: Set<String> = [weekly, monthly, yearly]
 
     static let recurring: Set<String> = [weekly, monthly, yearly]
 
@@ -15,7 +14,6 @@ enum SubscriptionProductID {
         case weekly: .weekly
         case monthly: .monthly
         case yearly: .yearly
-        case lifetime: .lifetime
         default: nil
         }
     }

+ 17 - 56
gramora/Views/PaywallView.swift

@@ -18,32 +18,24 @@ struct PaywallView: View {
         subscriptions.isPremiumActive
     }
 
-    private var isSelectedPlanLifetime: Bool {
-        viewModel.selectedPlan == .lifetime
-    }
-
     private var canPurchaseSelectedPlan: Bool {
         guard isPremium else { return true }
         guard subscriptions.hasManageableSubscription else { return false }
         guard let activePlan = subscriptions.activeRecurringPlan ?? subscriptions.activePlan else {
-            return viewModel.selectedPlan == .lifetime
+            return false
         }
         return viewModel.selectedPlan != activePlan
     }
 
     private var ctaDescription: String {
         if isPremium, !subscriptions.hasManageableSubscription {
-            return "You already have lifetime access."
+            return "You already have premium access."
         }
         if subscriptions.hasManageableSubscription,
            let activePlan = subscriptions.activeRecurringPlan ?? subscriptions.activePlan,
            viewModel.selectedPlan == activePlan {
             return "You're currently on the \(subscriptions.displayTitle(for: activePlan)) plan."
         }
-        if subscriptions.hasManageableSubscription,
-           viewModel.selectedPlan == .lifetime {
-            return "Upgrade to lifetime with a one-time payment. Your current subscription won't cancel automatically — cancel it after purchasing to avoid further charges."
-        }
         return subscriptions.billingDescription(for: viewModel.selectedPlan)
     }
 
@@ -107,9 +99,6 @@ struct PaywallView: View {
         .clearHostingBackground()
         .task {
             await subscriptions.loadProducts(presentLoadingUI: true)
-            if subscriptions.hasManageableSubscription {
-                viewModel.selectPlan(.lifetime)
-            }
         }
         .alert(
             "Subscriptions",
@@ -137,27 +126,6 @@ struct PaywallView: View {
         } message: {
             Text(subscriptions.subscriptionSuccessMessage ?? "")
         }
-        .alert(
-            "Cancel your subscription",
-            isPresented: Binding(
-                get: { subscriptions.showsCancelSubscriptionReminder },
-                set: { presented in
-                    if !presented { subscriptions.clearCancelSubscriptionReminder() }
-                }
-            )
-        ) {
-            Button("Manage Subscription") {
-                subscriptions.openSubscriptionManagement()
-                subscriptions.clearCancelSubscriptionReminder()
-                onClose()
-            }
-            Button("Not Now", role: .cancel) {
-                subscriptions.clearCancelSubscriptionReminder()
-                onClose()
-            }
-        } message: {
-            Text(subscriptions.cancelSubscriptionReminderMessage)
-        }
     }
 
     private var premiumTitleSection: some View {
@@ -181,12 +149,12 @@ struct PaywallView: View {
 
     private var premiumStatusSubtitle: String {
         if subscriptions.hasManageableSubscription, let plan = subscriptions.activeRecurringPlan ?? subscriptions.activePlan {
-            return "Your \(subscriptions.displayTitle(for: plan)) plan is active. Upgrade to lifetime or switch plans below."
+            return "Your \(subscriptions.displayTitle(for: plan)) plan is active. Switch plans below."
         }
         if subscriptions.hasManageableSubscription {
-            return "Your subscription is active. Upgrade to lifetime or switch plans below."
+            return "Your subscription is active. Switch plans below."
         }
-        return "You have lifetime access to all premium features."
+        return "You have premium access to all features."
     }
 
     private var closeButton: some View {
@@ -257,13 +225,8 @@ struct PaywallView: View {
             Button {
                 Task {
                     let purchased = await subscriptions.purchase(viewModel.selectedPlan)
-                    if purchased {
-                        if subscriptions.showsCancelSubscriptionReminder {
-                            return
-                        }
-                        if subscriptions.isPremiumActive {
-                            onClose()
-                        }
+                    if purchased, subscriptions.isPremiumActive {
+                        onClose()
                     }
                 }
             } label: {
@@ -306,7 +269,7 @@ struct PaywallView: View {
             .buttonStyle(.plain)
             .onHover { isCTAHovered = $0 }
 
-            Text(isSelectedPlanLifetime ? "One-time purchase, yours forever." : "No commitment, cancel anytime.")
+            Text("No commitment, cancel anytime.")
                 .font(.system(size: 13, weight: .semibold))
                 .foregroundStyle(Color(red: 0.58, green: 0.61, blue: 0.66))
         }
@@ -314,16 +277,14 @@ struct PaywallView: View {
 
     private var legalSection: some View {
         VStack(spacing: 12) {
-            if !isSelectedPlanLifetime {
-                VStack(spacing: 2) {
-                    Text("Your subscription will automatically renew unless auto-renew is turned off at least 24-hours before the end of the current subscription period.")
-                    Text("Payment will be charged to your Apple ID at confirmation of purchase.")
-                }
-                .padding(.top, -8)
-                .font(.system(size: 12, weight: .medium))
-                .foregroundStyle(Color(red: 0.59, green: 0.62, blue: 0.68))
-                .multilineTextAlignment(.center)
+            VStack(spacing: 2) {
+                Text("Your subscription will automatically renew unless auto-renew is turned off at least 24-hours before the end of the current subscription period.")
+                Text("Payment will be charged to your Apple ID at confirmation of purchase.")
             }
+            .padding(.top, -8)
+            .font(.system(size: 12, weight: .medium))
+            .foregroundStyle(Color(red: 0.59, green: 0.62, blue: 0.68))
+            .multilineTextAlignment(.center)
 
             HStack {
                 PaywallFooterLinkView(
@@ -469,10 +430,10 @@ private struct PaywallPlanCardView: View {
                     if let tagText {
                         Text(tagText)
                             .font(.system(size: 12, weight: .semibold))
-                            .foregroundStyle(tagText == "Pay Once" ? Color(red: 0.53, green: 0.39, blue: 0.90) : Color(red: 0.07, green: 0.60, blue: 0.46))
+                            .foregroundStyle(Color(red: 0.07, green: 0.60, blue: 0.46))
                             .padding(.horizontal, 12)
                             .padding(.vertical, 5)
-                            .background(tagText == "Pay Once" ? Color(red: 0.94, green: 0.91, blue: 0.99) : Color(red: 0.91, green: 0.98, blue: 0.95))
+                            .background(Color(red: 0.91, green: 0.98, blue: 0.95))
                             .clipShape(Capsule())
                     }
                 }

+ 2 - 2
gramora/Views/SettingsView.swift

@@ -150,7 +150,7 @@ struct SettingsView: View {
                         SettingsActionRow(
                             iconName: "crown.fill",
                             title: subscriptions.hasManageableSubscription ? "Manage Subscription" : "View Premium",
-                            subtitle: "View plans, upgrade to lifetime, or manage billing",
+                            subtitle: "View plans or manage billing",
                             actionTitle: "Open",
                             isDestructive: false
                         ) {
@@ -195,7 +195,7 @@ struct SettingsView: View {
             if subscriptions.hasManageableSubscription {
                 return "Subscription is active"
             }
-            return "Lifetime access is active"
+            return "Premium access is active"
         }
         return "Upgrade to unlock all premium features"
     }

+ 4 - 4
website/privacy-policy.html

@@ -77,10 +77,10 @@
           the app settings.
         </p>
 
-        <h3>1.3 Subscription and Purchase Data</h3>
+        <h3>1.3 Subscription Data</h3>
         <p>
-          Subscriptions and in-app purchases are processed through Apple's App Store. We receive
-          limited purchase information (such as subscription status and product identifiers) from Apple
+          Subscriptions are processed through Apple's App Store. We receive
+          limited subscription information (such as subscription status and product identifiers) from Apple
           to manage your access to premium features. We do not collect or store your payment card details.
         </p>
 
@@ -114,7 +114,7 @@
             <a href="https://openai.com/policies/privacy-policy">OpenAI's Privacy Policy</a>.
           </li>
           <li>
-            <strong>Apple App Store</strong> — processes subscriptions and in-app purchases. See
+            <strong>Apple App Store</strong> — processes subscriptions. See
             <a href="https://www.apple.com/legal/privacy/">Apple's Privacy Policy</a>.
           </li>
         </ul>

+ 2 - 2
website/terms-and-condition.html

@@ -79,13 +79,13 @@
           Certain features of Grammar Pro are available for free, including unlimited word
           counting. Free users may use AI-powered tools up to four (4) times in total across
           all tools (for example, grammar checking, spell checking, or paraphrasing). Premium
-          features require an active subscription or one-time purchase. Feature availability may
+          features require an active subscription. Feature availability may
           change at our discretion.
         </p>
 
         <h3>3.2 Billing</h3>
         <p>
-          All purchases and subscriptions are processed through the Apple App Store. Payment terms,
+          All subscriptions are processed through the Apple App Store. Payment terms,
           billing cycles, free trials, and refunds are governed by Apple's terms and policies. We do
           not directly process payments.
         </p>