ソースを参照

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 週間 前
コミット
00332297e3
共有1 個のファイルを変更した35 個の追加3 個の削除を含む
  1. 35 3
      App for Indeed/Views/DashboardView.swift

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

@@ -19,6 +19,26 @@ private enum PremiumSheetLayout {
19 19
     static let overscanExtraTop: CGFloat = 0.5
20 20
 }
21 21
 
22
+/// Free-tier cap for Home AI job search (user messages only; Pro is unlimited).
23
+private enum FreeTierJobSearchQuota {
24
+    static let maxUserMessages = 2
25
+    private static let userDefaultsKey = "com.appforindeed.freeJobSearchUserMessageCount"
26
+
27
+    static var userMessageCount: Int {
28
+        get { UserDefaults.standard.integer(forKey: userDefaultsKey) }
29
+        set { UserDefaults.standard.set(newValue, forKey: userDefaultsKey) }
30
+    }
31
+
32
+    static func canSendAnotherMessage(isProActive: Bool) -> Bool {
33
+        isProActive || userMessageCount < maxUserMessages
34
+    }
35
+
36
+    static func recordUserMessageSent(isProActive: Bool) {
37
+        guard !isProActive else { return }
38
+        userMessageCount += 1
39
+    }
40
+}
41
+
22 42
 private enum SettingsAppearanceID {
23 43
     static let section = "dashboard.settings.section"
24 44
     static let sectionHeader = "dashboard.settings.sectionHeader"
@@ -597,6 +617,17 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
597 617
         return true
598 618
     }
599 619
 
620
+    /// Home AI job search: Pro is unlimited; free users may send up to `FreeTierJobSearchQuota.maxUserMessages` user messages.
621
+    @discardableResult
622
+    private func ensureProAccessForJobSearch() -> Bool {
623
+        if SubscriptionStore.shared.isProActive { return true }
624
+        guard FreeTierJobSearchQuota.canSendAnotherMessage(isProActive: false) else {
625
+            presentPremiumPlansSheet()
626
+            return false
627
+        }
628
+        return true
629
+    }
630
+
600 631
     private func presentPremiumPlansSheet() {
601 632
         guard let hostWindow = window else { return }
602 633
 
@@ -1998,7 +2029,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1998 2029
     }
1999 2030
 
2000 2031
     @objc private func didSubmitSearch() {
2001
-        guard ensureProAccess() else { return }
2032
+        guard ensureProAccessForJobSearch() else { return }
2002 2033
         let prompt = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
2003 2034
         guard !prompt.isEmpty, !isAwaitingResponse else { return }
2004 2035
         let isContinuation = isContinuationPrompt(prompt)
@@ -2116,7 +2147,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2116 2147
     }
2117 2148
 
2118 2149
     private func focusSearchField(seed: String) {
2119
-        guard ensureProAccess() else { return }
2150
+        guard ensureProAccessForJobSearch() else { return }
2120 2151
         jobKeywordsField.stringValue = seed
2121 2152
         window?.makeFirstResponder(jobKeywordsField)
2122 2153
         if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
@@ -2125,7 +2156,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2125 2156
     }
2126 2157
 
2127 2158
     @objc private func didTapLoadMoreJobs() {
2128
-        guard ensureProAccess() else { return }
2159
+        guard ensureProAccessForJobSearch() else { return }
2129 2160
         let prompt = "Show more jobs"
2130 2161
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
2131 2162
         if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
@@ -2136,6 +2167,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2136 2167
     }
2137 2168
 
2138 2169
     private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
2170
+        FreeTierJobSearchQuota.recordUserMessageSent(isProActive: SubscriptionStore.shared.isProActive)
2139 2171
         isAwaitingResponse = true
2140 2172
         addInlineChatThinkingRow()
2141 2173
         setInputEnabled(false)