Pārlūkot izejas kodu

Allow two free Home AI job search messages before paywall.

Free users can run the Indeed-oriented AI job search on Home for up to two
user-initiated requests (typed searches, Role/Company/Skill shortcuts, and
“Show more jobs” each count). After that, the Premium sheet appears as for
other Pro-only flows. Pro subscribers remain unlimited. Usage is persisted in
UserDefaults so the cap survives app restarts. CV Maker, profiles, and filled
CV preview stay behind full Pro access unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
Uzair Tahir 2 mēneši atpakaļ
vecāks
revīzija
00332297e3
1 mainītis faili ar 35 papildinājumiem un 3 dzēšanām
  1. 35 3
      App for Indeed/Views/DashboardView.swift

+ 35 - 3
App for Indeed/Views/DashboardView.swift

@@ -19,6 +19,26 @@ private enum PremiumSheetLayout {
     static let overscanExtraTop: CGFloat = 0.5
     static let overscanExtraTop: CGFloat = 0.5
 }
 }
 
 
+/// Free-tier cap for Home AI job search (user messages only; Pro is unlimited).
+private enum FreeTierJobSearchQuota {
+    static let maxUserMessages = 2
+    private static let userDefaultsKey = "com.appforindeed.freeJobSearchUserMessageCount"
+
+    static var userMessageCount: Int {
+        get { UserDefaults.standard.integer(forKey: userDefaultsKey) }
+        set { UserDefaults.standard.set(newValue, forKey: userDefaultsKey) }
+    }
+
+    static func canSendAnotherMessage(isProActive: Bool) -> Bool {
+        isProActive || userMessageCount < maxUserMessages
+    }
+
+    static func recordUserMessageSent(isProActive: Bool) {
+        guard !isProActive else { return }
+        userMessageCount += 1
+    }
+}
+
 private enum SettingsAppearanceID {
 private enum SettingsAppearanceID {
     static let section = "dashboard.settings.section"
     static let section = "dashboard.settings.section"
     static let sectionHeader = "dashboard.settings.sectionHeader"
     static let sectionHeader = "dashboard.settings.sectionHeader"
@@ -597,6 +617,17 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         return true
         return true
     }
     }
 
 
+    /// Home AI job search: Pro is unlimited; free users may send up to `FreeTierJobSearchQuota.maxUserMessages` user messages.
+    @discardableResult
+    private func ensureProAccessForJobSearch() -> Bool {
+        if SubscriptionStore.shared.isProActive { return true }
+        guard FreeTierJobSearchQuota.canSendAnotherMessage(isProActive: false) else {
+            presentPremiumPlansSheet()
+            return false
+        }
+        return true
+    }
+
     private func presentPremiumPlansSheet() {
     private func presentPremiumPlansSheet() {
         guard let hostWindow = window else { return }
         guard let hostWindow = window else { return }
 
 
@@ -1998,7 +2029,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     }
     }
 
 
     @objc private func didSubmitSearch() {
     @objc private func didSubmitSearch() {
-        guard ensureProAccess() else { return }
+        guard ensureProAccessForJobSearch() else { return }
         let prompt = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
         let prompt = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
         guard !prompt.isEmpty, !isAwaitingResponse else { return }
         guard !prompt.isEmpty, !isAwaitingResponse else { return }
         let isContinuation = isContinuationPrompt(prompt)
         let isContinuation = isContinuationPrompt(prompt)
@@ -2116,7 +2147,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     }
     }
 
 
     private func focusSearchField(seed: String) {
     private func focusSearchField(seed: String) {
-        guard ensureProAccess() else { return }
+        guard ensureProAccessForJobSearch() else { return }
         jobKeywordsField.stringValue = seed
         jobKeywordsField.stringValue = seed
         window?.makeFirstResponder(jobKeywordsField)
         window?.makeFirstResponder(jobKeywordsField)
         if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
         if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
@@ -2125,7 +2156,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     }
     }
 
 
     @objc private func didTapLoadMoreJobs() {
     @objc private func didTapLoadMoreJobs() {
-        guard ensureProAccess() else { return }
+        guard ensureProAccessForJobSearch() else { return }
         let prompt = "Show more jobs"
         let prompt = "Show more jobs"
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
         if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
         if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
@@ -2136,6 +2167,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     }
     }
 
 
     private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
     private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
+        FreeTierJobSearchQuota.recordUserMessageSent(isProActive: SubscriptionStore.shared.isProActive)
         isAwaitingResponse = true
         isAwaitingResponse = true
         addInlineChatThinkingRow()
         addInlineChatThinkingRow()
         setInputEnabled(false)
         setInputEnabled(false)