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

Add a shared free AI tier so users can try core features before subscribing.

Offer four free AI uses across all tools, keep word count fully free, and align the terms with the new access model for App Store review.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hussain Afzal 4 недель назад
Родитель
Сommit
592cbc4120

+ 42 - 4
gramora/Managers/SubscriptionManager.swift

@@ -34,6 +34,9 @@ enum PurchaseError: Equatable {
 
 
 @MainActor
 @MainActor
 final class SubscriptionManager: ObservableObject {
 final class SubscriptionManager: ObservableObject {
+    static let maxFreeAIUses = 4
+    private static let freeAIUsesConsumedKey = "freeAIUsesConsumed"
+
     private enum ProductLoadError: Error {
     private enum ProductLoadError: Error {
         case timeout
         case timeout
     }
     }
@@ -49,6 +52,7 @@ final class SubscriptionManager: ObservableObject {
     @Published private(set) var activePlan: PaywallPlan?
     @Published private(set) var activePlan: PaywallPlan?
     @Published private(set) var activeRecurringPlan: PaywallPlan?
     @Published private(set) var activeRecurringPlan: PaywallPlan?
     @Published private(set) var introOfferEligibleByPlan: [PaywallPlan: Bool] = [:]
     @Published private(set) var introOfferEligibleByPlan: [PaywallPlan: Bool] = [:]
+    @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 var showsCancelSubscriptionReminder = false
@@ -58,17 +62,51 @@ final class SubscriptionManager: ObservableObject {
         SubscriptionProductID.all.allSatisfy { productsByID[$0] != nil }
         SubscriptionProductID.all.allSatisfy { productsByID[$0] != nil }
     }
     }
 
 
+    var remainingFreeAIUses: Int {
+        max(0, Self.maxFreeAIUses - freeAIUsesConsumed)
+    }
+
+    var canUseAIFeatures: Bool {
+        hasPremiumAccess || remainingFreeAIUses > 0
+    }
+
     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
+
+    init(defaults: UserDefaults = .standard) {
+        self.defaults = defaults
+        hasPremiumAccess = defaults.bool(forKey: Self.cachedPremiumAccessKey)
+        freeAIUsesConsumed = min(
+            defaults.integer(forKey: Self.freeAIUsesConsumedKey),
+            Self.maxFreeAIUses
+        )
+
+        observeAIFeatureUsage()
 
 
-    init() {
-        hasPremiumAccess = UserDefaults.standard.bool(forKey: Self.cachedPremiumAccessKey)
         Task { await listenForTransactionUpdates() }
         Task { await listenForTransactionUpdates() }
         Task { await refreshPremiumAccess() }
         Task { await refreshPremiumAccess() }
         Task { await loadProducts(presentLoadingUI: false) }
         Task { await loadProducts(presentLoadingUI: false) }
         observeAppLifecycle()
         observeAppLifecycle()
     }
     }
 
 
+    func recordSuccessfulAIUse() {
+        guard !hasPremiumAccess else { return }
+        guard freeAIUsesConsumed < Self.maxFreeAIUses else { return }
+
+        freeAIUsesConsumed += 1
+        defaults.set(freeAIUsesConsumed, forKey: Self.freeAIUsesConsumedKey)
+    }
+
+    private func observeAIFeatureUsage() {
+        NotificationCenter.default.publisher(for: .aiFeatureDidSucceed)
+            .receive(on: DispatchQueue.main)
+            .sink { [weak self] _ in
+                self?.recordSuccessfulAIUse()
+            }
+            .store(in: &cancellables)
+    }
+
     func loadProducts(presentLoadingUI: Bool? = nil) async {
     func loadProducts(presentLoadingUI: Bool? = nil) async {
         if let existing = inFlightProductLoad {
         if let existing = inFlightProductLoad {
             await existing.value
             await existing.value
@@ -413,7 +451,7 @@ final class SubscriptionManager: ObservableObject {
         activePlan = resolvedPlan
         activePlan = resolvedPlan
         activeRecurringPlan = resolvedRecurringPlan
         activeRecurringPlan = resolvedRecurringPlan
         hasResolvedPremiumStatus = true
         hasResolvedPremiumStatus = true
-        UserDefaults.standard.set(hasPremium, forKey: Self.cachedPremiumAccessKey)
+        defaults.set(hasPremium, forKey: Self.cachedPremiumAccessKey)
     }
     }
 
 
     private func applyOptimisticPremiumAccess(for transaction: Transaction) {
     private func applyOptimisticPremiumAccess(for transaction: Transaction) {
@@ -427,7 +465,7 @@ final class SubscriptionManager: ObservableObject {
             hasManageableSubscription = true
             hasManageableSubscription = true
             activeRecurringPlan = plan
             activeRecurringPlan = plan
         }
         }
-        UserDefaults.standard.set(true, forKey: Self.cachedPremiumAccessKey)
+        defaults.set(true, forKey: Self.cachedPremiumAccessKey)
     }
     }
 
 
     private func refreshIntroOfferEligibility() async {
     private func refreshIntroOfferEligibility() async {

+ 11 - 0
gramora/Utilities/AIFeatureUsage.swift

@@ -0,0 +1,11 @@
+import Foundation
+
+extension Notification.Name {
+    static let aiFeatureDidSucceed = Notification.Name("aiFeatureDidSucceed")
+}
+
+enum AIFeatureUsage {
+    static func recordSuccessfulUse() {
+        NotificationCenter.default.post(name: .aiFeatureDidSucceed, object: nil)
+    }
+}

+ 1 - 1
gramora/Utilities/SettingsEnvironment.swift

@@ -14,7 +14,7 @@ extension EnvironmentValues {
         set { self[SettingsActionKey.self] = newValue }
         set { self[SettingsActionKey.self] = newValue }
     }
     }
 
 
-    /// Returns `true` when the user has premium access. Otherwise presents the paywall and returns `false`.
+    /// Returns `true` when the user can run an AI feature (premium or free uses remaining). Otherwise presents the paywall and returns `false`.
     var requirePremiumAccess: () -> Bool {
     var requirePremiumAccess: () -> Bool {
         get { self[RequirePremiumAccessKey.self] }
         get { self[RequirePremiumAccessKey.self] }
         set { self[RequirePremiumAccessKey.self] = newValue }
         set { self[RequirePremiumAccessKey.self] = newValue }

+ 1 - 0
gramora/ViewModels/DictionaryViewModel.swift

@@ -132,6 +132,7 @@ final class DictionaryViewModel: ObservableObject {
             entry = result
             entry = result
             query = result.word
             query = result.word
             addRecentSearch(result.word)
             addRecentSearch(result.word)
+            AIFeatureUsage.recordSuccessfulUse()
         } catch {
         } catch {
             guard !Task.isCancelled, activeLookupID == lookupID else { return }
             guard !Task.isCancelled, activeLookupID == lookupID else { return }
             errorMessage = UserFacingError.message(for: error)
             errorMessage = UserFacingError.message(for: error)

+ 1 - 0
gramora/ViewModels/EmailWriterViewModel.swift

@@ -102,6 +102,7 @@ final class EmailWriterViewModel: ObservableObject {
                     emailType: requestEmailType,
                     emailType: requestEmailType,
                     tone: requestTone
                     tone: requestTone
                 )
                 )
+                AIFeatureUsage.recordSuccessfulUse()
             } catch {
             } catch {
                 errorMessage = EmailWriterErrors.message(for: error)
                 errorMessage = EmailWriterErrors.message(for: error)
             }
             }

+ 1 - 0
gramora/ViewModels/GrammarCheckerViewModel.swift

@@ -104,6 +104,7 @@ final class GrammarCheckerViewModel: ObservableObject {
                 correctedText = result.correctedText
                 correctedText = result.correctedText
                 hasResults = true
                 hasResults = true
                 saveToHistory(text: content, issues: result.issues, correctedText: result.correctedText)
                 saveToHistory(text: content, issues: result.issues, correctedText: result.correctedText)
+                AIFeatureUsage.recordSuccessfulUse()
             } catch {
             } catch {
                 errorMessage = UserFacingError.message(for: error)
                 errorMessage = UserFacingError.message(for: error)
             }
             }

+ 1 - 0
gramora/ViewModels/JournalFinderViewModel.swift

@@ -114,6 +114,7 @@ final class JournalFinderViewModel: ObservableObject {
                 result = searchResult
                 result = searchResult
                 hasResults = true
                 hasResults = true
                 saveToHistory(criteria: searchCriteria, result: searchResult)
                 saveToHistory(criteria: searchCriteria, result: searchResult)
+                AIFeatureUsage.recordSuccessfulUse()
             } catch {
             } catch {
                 errorMessage = UserFacingError.message(for: error)
                 errorMessage = UserFacingError.message(for: error)
             }
             }

+ 1 - 0
gramora/ViewModels/LanguageTranslatorViewModel.swift

@@ -118,6 +118,7 @@ final class LanguageTranslatorViewModel: ObservableObject {
                     sourceLanguage: requestSourceLanguage,
                     sourceLanguage: requestSourceLanguage,
                     targetLanguage: requestTargetLanguage
                     targetLanguage: requestTargetLanguage
                 )
                 )
+                AIFeatureUsage.recordSuccessfulUse()
             } catch {
             } catch {
                 errorMessage = UserFacingError.message(for: error)
                 errorMessage = UserFacingError.message(for: error)
             }
             }

+ 1 - 0
gramora/ViewModels/ParaphrasingViewModel.swift

@@ -95,6 +95,7 @@ final class ParaphrasingViewModel: ObservableObject {
                 let result = try await paraphrasingService.paraphrase(content)
                 let result = try await paraphrasingService.paraphrase(content)
                 paraphrasedText = result
                 paraphrasedText = result
                 saveToHistory(text: content, paraphrasedText: result)
                 saveToHistory(text: content, paraphrasedText: result)
+                AIFeatureUsage.recordSuccessfulUse()
             } catch {
             } catch {
                 errorMessage = UserFacingError.message(for: error)
                 errorMessage = UserFacingError.message(for: error)
             }
             }

+ 1 - 0
gramora/ViewModels/PunctuationCheckerViewModel.swift

@@ -99,6 +99,7 @@ final class PunctuationCheckerViewModel: ObservableObject {
                 issues = result.issues
                 issues = result.issues
                 hasResults = true
                 hasResults = true
                 saveToHistory(text: content, issues: result.issues, correctedText: result.correctedText)
                 saveToHistory(text: content, issues: result.issues, correctedText: result.correctedText)
+                AIFeatureUsage.recordSuccessfulUse()
             } catch {
             } catch {
                 errorMessage = UserFacingError.message(for: error)
                 errorMessage = UserFacingError.message(for: error)
             }
             }

+ 1 - 0
gramora/ViewModels/SpellCheckerViewModel.swift

@@ -104,6 +104,7 @@ final class SpellCheckerViewModel: ObservableObject {
                 correctedText = result.correctedText
                 correctedText = result.correctedText
                 hasResults = true
                 hasResults = true
                 saveToHistory(text: content, mistakes: result.mistakes, correctedText: result.correctedText)
                 saveToHistory(text: content, mistakes: result.mistakes, correctedText: result.correctedText)
+                AIFeatureUsage.recordSuccessfulUse()
             } catch {
             } catch {
                 errorMessage = UserFacingError.message(for: error)
                 errorMessage = UserFacingError.message(for: error)
             }
             }

+ 1 - 0
gramora/ViewModels/SummarizerViewModel.swift

@@ -95,6 +95,7 @@ final class SummarizerViewModel: ObservableObject {
                 let result = try await summarizerService.summarize(content)
                 let result = try await summarizerService.summarize(content)
                 summary = result
                 summary = result
                 saveToHistory(text: content, summary: result)
                 saveToHistory(text: content, summary: result)
+                AIFeatureUsage.recordSuccessfulUse()
             } catch {
             } catch {
                 errorMessage = UserFacingError.message(for: error)
                 errorMessage = UserFacingError.message(for: error)
             }
             }

+ 13 - 5
gramora/Views/Components/PremiumCardView.swift

@@ -3,6 +3,7 @@ import SwiftUI
 struct PremiumCardView: View {
 struct PremiumCardView: View {
     let isPremium: Bool
     let isPremium: Bool
     let hasManageableSubscription: Bool
     let hasManageableSubscription: Bool
+    let remainingFreeAIUses: Int
     let onUpgradeTapped: () -> Void
     let onUpgradeTapped: () -> Void
     let onManageSubscriptionTapped: () -> Void
     let onManageSubscriptionTapped: () -> Void
     @State private var isUpgradeHovered = false
     @State private var isUpgradeHovered = false
@@ -12,6 +13,17 @@ struct PremiumCardView: View {
         return hasManageableSubscription ? "Manage Subscription" : "View Premium"
         return hasManageableSubscription ? "Manage Subscription" : "View Premium"
     }
     }
 
 
+    private var subtitle: String {
+        if isPremium {
+            return "You have unlimited checks, advanced features and an ad-free experience."
+        }
+        if remainingFreeAIUses > 0 {
+            let useLabel = remainingFreeAIUses == 1 ? "use" : "uses"
+            return "\(remainingFreeAIUses) free AI \(useLabel) remaining across all tools. Upgrade for unlimited access."
+        }
+        return "Free AI uses used. Upgrade for unlimited checks and advanced features."
+    }
+
     var body: some View {
     var body: some View {
         VStack(alignment: .leading, spacing: 10) {
         VStack(alignment: .leading, spacing: 10) {
             HStack(spacing: 8) {
             HStack(spacing: 8) {
@@ -24,11 +36,7 @@ struct PremiumCardView: View {
                     .foregroundStyle(AppTheme.textPrimary)
                     .foregroundStyle(AppTheme.textPrimary)
             }
             }
 
 
-            Text(
-                isPremium
-                    ? "You have unlimited checks, advanced features and an ad-free experience."
-                    : "Get unlimited checks, advanced features and an ad-free experience."
-            )
+            Text(subtitle)
                 .font(.system(size: 11))
                 .font(.system(size: 11))
                 .foregroundStyle(AppTheme.textSecondary)
                 .foregroundStyle(AppTheme.textSecondary)
                 .lineSpacing(2)
                 .lineSpacing(2)

+ 2 - 4
gramora/Views/MainView.swift

@@ -18,6 +18,7 @@ struct MainView: View {
                 isShowingSettings: viewModel.isShowingSettings,
                 isShowingSettings: viewModel.isShowingSettings,
                 isPremium: subscriptions.hasPremiumAccess,
                 isPremium: subscriptions.hasPremiumAccess,
                 hasManageableSubscription: subscriptions.hasManageableSubscription,
                 hasManageableSubscription: subscriptions.hasManageableSubscription,
+                remainingFreeAIUses: subscriptions.remainingFreeAIUses,
                 onUpgradeTapped: viewModel.showPaywall,
                 onUpgradeTapped: viewModel.showPaywall,
                 onManageSubscriptionTapped: viewModel.showPaywall
                 onManageSubscriptionTapped: viewModel.showPaywall
             )
             )
@@ -56,10 +57,7 @@ struct MainView: View {
             }
             }
             .environment(\.onSettingsTapped, viewModel.showSettings)
             .environment(\.onSettingsTapped, viewModel.showSettings)
             .environment(\.requirePremiumAccess, {
             .environment(\.requirePremiumAccess, {
-                if !subscriptions.hasResolvedPremiumStatus {
-                    return subscriptions.hasPremiumAccess
-                }
-                if subscriptions.hasPremiumAccess { return true }
+                if subscriptions.canUseAIFeatures { return true }
                 viewModel.showPaywall()
                 viewModel.showPaywall()
                 return false
                 return false
             })
             })

+ 2 - 0
gramora/Views/SidebarView.swift

@@ -5,6 +5,7 @@ struct SidebarView: View {
     let isShowingSettings: Bool
     let isShowingSettings: Bool
     let isPremium: Bool
     let isPremium: Bool
     let hasManageableSubscription: Bool
     let hasManageableSubscription: Bool
+    let remainingFreeAIUses: Int
     let onUpgradeTapped: () -> Void
     let onUpgradeTapped: () -> Void
     let onManageSubscriptionTapped: () -> Void
     let onManageSubscriptionTapped: () -> Void
 
 
@@ -32,6 +33,7 @@ struct SidebarView: View {
             PremiumCardView(
             PremiumCardView(
                 isPremium: isPremium,
                 isPremium: isPremium,
                 hasManageableSubscription: hasManageableSubscription,
                 hasManageableSubscription: hasManageableSubscription,
+                remainingFreeAIUses: remainingFreeAIUses,
                 onUpgradeTapped: onUpgradeTapped,
                 onUpgradeTapped: onUpgradeTapped,
                 onManageSubscriptionTapped: onManageSubscriptionTapped
                 onManageSubscriptionTapped: onManageSubscriptionTapped
             )
             )

+ 23 - 73
gramora/Views/WordsCountView.swift

@@ -3,13 +3,6 @@ import SwiftUI
 struct WordsCountView: View {
 struct WordsCountView: View {
     @StateObject private var viewModel = WordsCountViewModel()
     @StateObject private var viewModel = WordsCountViewModel()
     @Environment(\.onSettingsTapped) private var onSettingsTapped
     @Environment(\.onSettingsTapped) private var onSettingsTapped
-    @Environment(\.requirePremiumAccess) private var requirePremiumAccess
-    @EnvironmentObject private var subscriptions: SubscriptionManager
-    @State private var didPromptWordCountPaywall = false
-
-    private var hasPremium: Bool {
-        subscriptions.hasPremiumAccess
-    }
 
 
     var body: some View {
     var body: some View {
         GeometryReader { proxy in
         GeometryReader { proxy in
@@ -37,10 +30,8 @@ struct WordsCountView: View {
             .padding(.bottom, dynamicBottomPadding)
             .padding(.bottom, dynamicBottomPadding)
         }
         }
         .clearHostingBackground()
         .clearHostingBackground()
-        .onChange(of: subscriptions.hasPremiumAccess) { isPremium in
-            if isPremium {
-                viewModel.handleTextChange()
-            }
+        .onChange(of: viewModel.text) { _ in
+            viewModel.handleTextChange()
         }
         }
     }
     }
 
 
@@ -76,43 +67,31 @@ struct WordsCountView: View {
         VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
         VStack(alignment: .leading, spacing: AppTheme.contentCardSpacing) {
             YourContentHeader()
             YourContentHeader()
 
 
-            Text(hasPremium ? viewModel.wordCountLabel : "Words Count")
+            Text(viewModel.wordCountLabel)
                 .font(.system(size: 18, weight: .semibold))
                 .font(.system(size: 18, weight: .semibold))
                 .foregroundStyle(AppTheme.textPrimary)
                 .foregroundStyle(AppTheme.textPrimary)
 
 
             statsRow
             statsRow
 
 
             VStack(alignment: .leading, spacing: 6) {
             VStack(alignment: .leading, spacing: 6) {
-                ZStack(alignment: .topLeading) {
-                    ThinCaretTextEditor(
-                        text: premiumGatedText,
-                        placeholder: "Type or paste your text here to count words...",
-                        maxWords: WordsCountViewModel.maxWords,
-                        onWordLimitReached: viewModel.notifyWordLimitReached
-                    )
-                        .font(.system(size: 14))
-                        .foregroundStyle(AppTheme.textPrimary)
-                        .frame(maxWidth: .infinity, maxHeight: .infinity)
-                        .frame(minHeight: 96)
-                        .background(AppTheme.surface)
-                        .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
-                        .overlay(
-                            RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
-                                .stroke(AppTheme.inputBorder, lineWidth: 1)
-                        )
-
-                    if !hasPremium {
+                ThinCaretTextEditor(
+                    text: $viewModel.text,
+                    placeholder: "Type or paste your text here to count words...",
+                    maxWords: WordsCountViewModel.maxWords,
+                    onWordLimitReached: viewModel.notifyWordLimitReached
+                )
+                    .font(.system(size: 14))
+                    .foregroundStyle(AppTheme.textPrimary)
+                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    .frame(minHeight: 96)
+                    .background(AppTheme.surface)
+                    .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
+                    .overlay(
                         RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
                         RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
-                            .fill(Color.clear)
-                            .contentShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
-                            .onTapGesture {
-                                didPromptWordCountPaywall = false
-                                _ = requirePremiumAccess()
-                            }
-                    }
-                }
+                            .stroke(AppTheme.inputBorder, lineWidth: 1)
+                    )
 
 
-                Text(hasPremium ? viewModel.wordCountLimitLabel : "Upgrade to count words")
+                Text(viewModel.wordCountLimitLabel)
                     .font(.system(size: 11))
                     .font(.system(size: 11))
                     .foregroundStyle(AppTheme.textMuted)
                     .foregroundStyle(AppTheme.textMuted)
                     .frame(maxWidth: .infinity, alignment: .trailing)
                     .frame(maxWidth: .infinity, alignment: .trailing)
@@ -121,12 +100,10 @@ struct WordsCountView: View {
 
 
             HStack(spacing: 8) {
             HStack(spacing: 8) {
                 ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
                 ToolbarButton(title: "Paste Text", iconName: "doc.on.clipboard") {
-                    guard requirePremiumAccess() else { return }
                     viewModel.pasteText()
                     viewModel.pasteText()
                 }
                 }
 
 
                 ToolbarButton(title: "Upload File", iconName: "square.and.arrow.up") {
                 ToolbarButton(title: "Upload File", iconName: "square.and.arrow.up") {
-                    guard requirePremiumAccess() else { return }
                     viewModel.uploadFile()
                     viewModel.uploadFile()
                 }
                 }
 
 
@@ -134,7 +111,6 @@ struct WordsCountView: View {
 
 
                 ClearButton {
                 ClearButton {
                     viewModel.clearText()
                     viewModel.clearText()
-                    didPromptWordCountPaywall = false
                 }
                 }
             }
             }
         }
         }
@@ -149,38 +125,12 @@ struct WordsCountView: View {
         .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
         .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
     }
     }
 
 
-    private var premiumGatedText: Binding<String> {
-        Binding(
-            get: { viewModel.text },
-            set: { newValue in
-                let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
-                if trimmed.isEmpty {
-                    didPromptWordCountPaywall = false
-                    viewModel.text = newValue
-                    viewModel.handleTextChange()
-                    return
-                }
-
-                guard subscriptions.hasPremiumAccess else {
-                    if !didPromptWordCountPaywall {
-                        didPromptWordCountPaywall = true
-                        requirePremiumAccess()
-                    }
-                    return
-                }
-
-                viewModel.text = newValue
-                viewModel.handleTextChange()
-            }
-        )
-    }
-
     private var statsRow: some View {
     private var statsRow: some View {
         HStack(spacing: 12) {
         HStack(spacing: 12) {
-            StatItem(title: "Words", value: hasPremium ? "\(viewModel.wordCount)" : "—")
-            StatItem(title: "Sentences", value: hasPremium ? "\(viewModel.sentenceCount)" : "—")
-            StatItem(title: "Paragraphs", value: hasPremium ? "\(viewModel.paragraphCount)" : "—")
-            StatItem(title: "Characters", value: hasPremium ? "\(viewModel.characterCount)" : "—")
+            StatItem(title: "Words", value: "\(viewModel.wordCount)")
+            StatItem(title: "Sentences", value: "\(viewModel.sentenceCount)")
+            StatItem(title: "Paragraphs", value: "\(viewModel.paragraphCount)")
+            StatItem(title: "Characters", value: "\(viewModel.characterCount)")
             Spacer(minLength: 0)
             Spacer(minLength: 0)
         }
         }
         .padding(.vertical, 6)
         .padding(.vertical, 6)

+ 5 - 2
website/terms/index.html

@@ -76,8 +76,11 @@
 
 
         <h3>3.1 Free and Premium Access</h3>
         <h3>3.1 Free and Premium Access</h3>
         <p>
         <p>
-          Certain features of Grammar Pro are available for free. Premium features require an active
-          subscription or one-time purchase. Feature availability may change at our discretion.
+          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
+          change at our discretion.
         </p>
         </p>
 
 
         <h3>3.2 Billing</h3>
         <h3>3.2 Billing</h3>