Quellcode durchsuchen

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 vor 2 Wochen
Ursprung
Commit
00332297e3
1 geänderte Dateien mit 35 neuen und 3 gelöschten Zeilen
  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
     static let overscanExtraTop: CGFloat = 0.5
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
 private enum SettingsAppearanceID {
42
 private enum SettingsAppearanceID {
23
     static let section = "dashboard.settings.section"
43
     static let section = "dashboard.settings.section"
24
     static let sectionHeader = "dashboard.settings.sectionHeader"
44
     static let sectionHeader = "dashboard.settings.sectionHeader"
@@ -597,6 +617,17 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
597
         return true
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
     private func presentPremiumPlansSheet() {
631
     private func presentPremiumPlansSheet() {
601
         guard let hostWindow = window else { return }
632
         guard let hostWindow = window else { return }
602
 
633
 
@@ -1998,7 +2029,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
1998
     }
2029
     }
1999
 
2030
 
2000
     @objc private func didSubmitSearch() {
2031
     @objc private func didSubmitSearch() {
2001
-        guard ensureProAccess() else { return }
2032
+        guard ensureProAccessForJobSearch() else { return }
2002
         let prompt = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
2033
         let prompt = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
2003
         guard !prompt.isEmpty, !isAwaitingResponse else { return }
2034
         guard !prompt.isEmpty, !isAwaitingResponse else { return }
2004
         let isContinuation = isContinuationPrompt(prompt)
2035
         let isContinuation = isContinuationPrompt(prompt)
@@ -2116,7 +2147,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2116
     }
2147
     }
2117
 
2148
 
2118
     private func focusSearchField(seed: String) {
2149
     private func focusSearchField(seed: String) {
2119
-        guard ensureProAccess() else { return }
2150
+        guard ensureProAccessForJobSearch() else { return }
2120
         jobKeywordsField.stringValue = seed
2151
         jobKeywordsField.stringValue = seed
2121
         window?.makeFirstResponder(jobKeywordsField)
2152
         window?.makeFirstResponder(jobKeywordsField)
2122
         if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
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
     @objc private func didTapLoadMoreJobs() {
2158
     @objc private func didTapLoadMoreJobs() {
2128
-        guard ensureProAccess() else { return }
2159
+        guard ensureProAccessForJobSearch() else { return }
2129
         let prompt = "Show more jobs"
2160
         let prompt = "Show more jobs"
2130
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
2161
         guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
2131
         if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
2162
         if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
@@ -2136,6 +2167,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
2136
     }
2167
     }
2137
 
2168
 
2138
     private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
2169
     private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
2170
+        FreeTierJobSearchQuota.recordUserMessageSent(isProActive: SubscriptionStore.shared.isProActive)
2139
         isAwaitingResponse = true
2171
         isAwaitingResponse = true
2140
         addInlineChatThinkingRow()
2172
         addInlineChatThinkingRow()
2141
         setInputEnabled(false)
2173
         setInputEnabled(false)