Browse Source

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 tháng trước cách đây
mục cha
commit
00332297e3
1 tập tin đã thay đổi với 35 bổ sung3 xóa
  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
 }
 
+/// 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 {
     static let section = "dashboard.settings.section"
     static let sectionHeader = "dashboard.settings.sectionHeader"
@@ -597,6 +617,17 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
         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() {
         guard let hostWindow = window else { return }
 
@@ -1998,7 +2029,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     }
 
     @objc private func didSubmitSearch() {
-        guard ensureProAccess() else { return }
+        guard ensureProAccessForJobSearch() else { return }
         let prompt = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
         guard !prompt.isEmpty, !isAwaitingResponse else { return }
         let isContinuation = isContinuationPrompt(prompt)
@@ -2116,7 +2147,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     }
 
     private func focusSearchField(seed: String) {
-        guard ensureProAccess() else { return }
+        guard ensureProAccessForJobSearch() else { return }
         jobKeywordsField.stringValue = seed
         window?.makeFirstResponder(jobKeywordsField)
         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() {
-        guard ensureProAccess() else { return }
+        guard ensureProAccessForJobSearch() else { return }
         let prompt = "Show more jobs"
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
         if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
@@ -2136,6 +2167,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
     }
 
     private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
+        FreeTierJobSearchQuota.recordUserMessageSent(isProActive: SubscriptionStore.shared.isProActive)
         isAwaitingResponse = true
         addInlineChatThinkingRow()
         setInputEnabled(false)