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

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 недель назад
Родитель
Сommit
9c0cb48a25

+ 11 - 46
gramora/Managers/SubscriptionManager.swift

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

+ 0 - 2
gramora/Models/PaywallModels.swift

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

+ 3 - 17
gramora/Paywall.storekit

@@ -14,21 +14,7 @@
 
 
   ],
   ],
   "products" : [
   "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" : {
   "settings" : {
     "_applicationInternalID" : "6785216263",
     "_applicationInternalID" : "6785216263",
@@ -38,7 +24,7 @@
     "_developerTeamID" : "LY29MGDPPD",
     "_developerTeamID" : "LY29MGDPPD",
     "_disableDialogs" : false,
     "_disableDialogs" : false,
     "_failTransactionsEnabled" : false,
     "_failTransactionsEnabled" : false,
-    "_lastSynchronizedDate" : 804341970.10229504,
+    "_lastSynchronizedDate" : 804604882.87108397,
     "_locale" : "en_US",
     "_locale" : "en_US",
     "_renewalBillingIssuesEnabled" : false,
     "_renewalBillingIssuesEnabled" : false,
     "_storefront" : "USA",
     "_storefront" : "USA",
@@ -123,7 +109,7 @@
           "groupNumber" : 3,
           "groupNumber" : 3,
           "internalID" : "6785220317",
           "internalID" : "6785220317",
           "introductoryOffer" : {
           "introductoryOffer" : {
-            "internalID" : "1455D143",
+            "internalID" : "77C335A0",
             "numberOfPeriods" : 1,
             "numberOfPeriods" : 1,
             "paymentMode" : "free",
             "paymentMode" : "free",
             "subscriptionPeriod" : "P3D"
             "subscriptionPeriod" : "P3D"

+ 1 - 3
gramora/Utilities/SubscriptionProductID.swift

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

+ 17 - 56
gramora/Views/PaywallView.swift

@@ -18,32 +18,24 @@ struct PaywallView: View {
         subscriptions.isPremiumActive
         subscriptions.isPremiumActive
     }
     }
 
 
-    private var isSelectedPlanLifetime: Bool {
-        viewModel.selectedPlan == .lifetime
-    }
-
     private var canPurchaseSelectedPlan: Bool {
     private var canPurchaseSelectedPlan: Bool {
         guard isPremium else { return true }
         guard isPremium else { return true }
         guard subscriptions.hasManageableSubscription else { return false }
         guard subscriptions.hasManageableSubscription else { return false }
         guard let activePlan = subscriptions.activeRecurringPlan ?? subscriptions.activePlan else {
         guard let activePlan = subscriptions.activeRecurringPlan ?? subscriptions.activePlan else {
-            return viewModel.selectedPlan == .lifetime
+            return false
         }
         }
         return viewModel.selectedPlan != activePlan
         return viewModel.selectedPlan != activePlan
     }
     }
 
 
     private var ctaDescription: String {
     private var ctaDescription: String {
         if isPremium, !subscriptions.hasManageableSubscription {
         if isPremium, !subscriptions.hasManageableSubscription {
-            return "You already have lifetime access."
+            return "You already have premium access."
         }
         }
         if subscriptions.hasManageableSubscription,
         if subscriptions.hasManageableSubscription,
            let activePlan = subscriptions.activeRecurringPlan ?? subscriptions.activePlan,
            let activePlan = subscriptions.activeRecurringPlan ?? subscriptions.activePlan,
            viewModel.selectedPlan == activePlan {
            viewModel.selectedPlan == activePlan {
             return "You're currently on the \(subscriptions.displayTitle(for: activePlan)) plan."
             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)
         return subscriptions.billingDescription(for: viewModel.selectedPlan)
     }
     }
 
 
@@ -107,9 +99,6 @@ struct PaywallView: View {
         .clearHostingBackground()
         .clearHostingBackground()
         .task {
         .task {
             await subscriptions.loadProducts(presentLoadingUI: true)
             await subscriptions.loadProducts(presentLoadingUI: true)
-            if subscriptions.hasManageableSubscription {
-                viewModel.selectPlan(.lifetime)
-            }
         }
         }
         .alert(
         .alert(
             "Subscriptions",
             "Subscriptions",
@@ -137,27 +126,6 @@ struct PaywallView: View {
         } message: {
         } message: {
             Text(subscriptions.subscriptionSuccessMessage ?? "")
             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 {
     private var premiumTitleSection: some View {
@@ -181,12 +149,12 @@ struct PaywallView: View {
 
 
     private var premiumStatusSubtitle: String {
     private var premiumStatusSubtitle: String {
         if subscriptions.hasManageableSubscription, let plan = subscriptions.activeRecurringPlan ?? subscriptions.activePlan {
         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 {
         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 {
     private var closeButton: some View {
@@ -257,13 +225,8 @@ struct PaywallView: View {
             Button {
             Button {
                 Task {
                 Task {
                     let purchased = await subscriptions.purchase(viewModel.selectedPlan)
                     let purchased = await subscriptions.purchase(viewModel.selectedPlan)
-                    if purchased {
-                        if subscriptions.showsCancelSubscriptionReminder {
-                            return
-                        }
-                        if subscriptions.isPremiumActive {
-                            onClose()
-                        }
+                    if purchased, subscriptions.isPremiumActive {
+                        onClose()
                     }
                     }
                 }
                 }
             } label: {
             } label: {
@@ -306,7 +269,7 @@ struct PaywallView: View {
             .buttonStyle(.plain)
             .buttonStyle(.plain)
             .onHover { isCTAHovered = $0 }
             .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))
                 .font(.system(size: 13, weight: .semibold))
                 .foregroundStyle(Color(red: 0.58, green: 0.61, blue: 0.66))
                 .foregroundStyle(Color(red: 0.58, green: 0.61, blue: 0.66))
         }
         }
@@ -314,16 +277,14 @@ struct PaywallView: View {
 
 
     private var legalSection: some View {
     private var legalSection: some View {
         VStack(spacing: 12) {
         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 {
             HStack {
                 PaywallFooterLinkView(
                 PaywallFooterLinkView(
@@ -469,10 +430,10 @@ private struct PaywallPlanCardView: View {
                     if let tagText {
                     if let tagText {
                         Text(tagText)
                         Text(tagText)
                             .font(.system(size: 12, weight: .semibold))
                             .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(.horizontal, 12)
                             .padding(.vertical, 5)
                             .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())
                             .clipShape(Capsule())
                     }
                     }
                 }
                 }

+ 2 - 2
gramora/Views/SettingsView.swift

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

+ 4 - 4
website/privacy-policy.html

@@ -77,10 +77,10 @@
           the app settings.
           the app settings.
         </p>
         </p>
 
 
-        <h3>1.3 Subscription and Purchase Data</h3>
+        <h3>1.3 Subscription Data</h3>
         <p>
         <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.
           to manage your access to premium features. We do not collect or store your payment card details.
         </p>
         </p>
 
 
@@ -114,7 +114,7 @@
             <a href="https://openai.com/policies/privacy-policy">OpenAI's Privacy Policy</a>.
             <a href="https://openai.com/policies/privacy-policy">OpenAI's Privacy Policy</a>.
           </li>
           </li>
           <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>.
             <a href="https://www.apple.com/legal/privacy/">Apple's Privacy Policy</a>.
           </li>
           </li>
         </ul>
         </ul>

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

@@ -79,13 +79,13 @@
           Certain features of Grammar Pro are available for free, including unlimited word
           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
           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
           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.
           change at our discretion.
         </p>
         </p>
 
 
         <h3>3.2 Billing</h3>
         <h3>3.2 Billing</h3>
         <p>
         <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
           billing cycles, free trials, and refunds are governed by Apple's terms and policies. We do
           not directly process payments.
           not directly process payments.
         </p>
         </p>