Limit floating bar to 50 queries/week for free users#6407
Conversation
Tracks weekly query timestamps locally, fetches subscription plan from backend to bypass limits for paid users. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shows upgrade message when limit is reached instead of sending query. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR adds a 50-queries-per-week rolling limit for free (Basic) users of the floating bar, using Confidence Score: 4/5Mostly safe to merge, but the early recordQuery() call will silently drain free-user quota on failed requests. The premature recordQuery() call is a present defect on the changed code path that negatively impacts free users with no recovery mechanism. The cached-status inconsistency is narrower but still affects correctness. Both warrant addressing before shipping. FloatingBarUsageLimiter.swift (init cache logic, dead planFetched) and FloatingControlBarWindow.swift (placement of recordQuery() call).
|
| Filename | Overview |
|---|---|
| desktop/Desktop/Sources/FloatingControlBar/FloatingBarUsageLimiter.swift | New class implementing rolling 7-day quota tracking via UserDefaults; has a cached-status inconsistency on init and dead planFetched field. |
| desktop/Desktop/Sources/FloatingControlBar/FloatingControlBarWindow.swift | Integrates limit check into sendAIQuery; recordQuery() is called before the query is dispatched, causing lost quota on failure. |
| desktop/Desktop/Sources/OmiApp.swift | Adds fetchPlan() call on app startup; minimal and correct change. |
Sequence Diagram
sequenceDiagram
participant User
participant FloatingBar as FloatingControlBarWindow
participant Limiter as FloatingBarUsageLimiter
participant Provider as ChatProvider
participant API as Backend API
User->>FloatingBar: sendAIQuery(message)
FloatingBar->>Limiter: isLimitReached?
alt limit reached
Limiter-->>FloatingBar: true
FloatingBar-->>User: Show upgrade message
else under limit
Limiter-->>FloatingBar: false
FloatingBar->>Limiter: recordQuery() ⚠️ counted before request
FloatingBar->>API: capture screenshot
FloatingBar->>Provider: sendMessage(query + screenshot)
alt API succeeds
Provider-->>FloatingBar: streaming response
FloatingBar-->>User: Display AI response
else API fails
Provider-->>FloatingBar: error
FloatingBar-->>User: Show error
Note over Limiter: query already counted — slot lost
end
end
Reviews (1): Last reviewed commit: "Fetch subscription plan on startup for u..." | Re-trigger Greptile
| return | ||
| } | ||
|
|
||
| limiter.recordQuery() |
There was a problem hiding this comment.
Query counted before API call succeeds
recordQuery() is called immediately before the network request is dispatched. If the API call fails — network error, server 5xx, timeout — the slot is permanently consumed without the user receiving any response. Over a 50/week budget this erodes the quota silently.
Move recordQuery() to after the provider confirms it has received and started streaming the response (inside the chatCancellable sink, once aiMessage.isStreaming == true is first observed).
| private var planFetched = false | ||
|
|
||
| private init() { | ||
| hasPaidPlan = UserDefaults.standard.string(forKey: Self.cachedPlanKey).map { $0 != "basic" } ?? false |
There was a problem hiding this comment.
Cached plan ignores subscription status
The init() cache restore uses only $0 != "basic" to determine hasPaidPlan, while fetchPlan() also requires status == .active. A user whose Pro subscription has lapsed will have "pro" cached in UserDefaults and bypass the weekly limit until the async fetchPlan() completes. Store a single boolean in the cache instead so the initial value is consistent with the live check.
|
|
||
| @Published private(set) var weeklyQueriesUsed: Int = 0 | ||
| @Published private(set) var hasPaidPlan: Bool = false | ||
| private var planFetched = false |
| } | ||
|
|
||
| /// Fetch the user's subscription plan from the backend (call on app launch / sign-in). | ||
| func fetchPlan() async { |
There was a problem hiding this comment.
fetchPlan() not called on sign-in
The doc comment says call on app launch / sign-in, but the only call site in OmiApp.swift fires only on startup. A user who signs into a Pro account mid-session will remain rate-limited until the next cold launch. Consider also calling FloatingBarUsageLimiter.shared.fetchPlan() wherever auth sign-in is confirmed.
) ## Summary - Free (Basic) users limited to **50 floating bar queries per week** (rolling 7-day window) - Pro/Unlimited users have no limit - When limit is reached, shows upgrade message instead of sending query - Subscription plan fetched on startup and cached locally - Query timestamps tracked in UserDefaults ## Test plan - [ ] Free user can send queries up to the limit - [ ] At limit, shows upgrade message instead of sending - [ ] Pro/Unlimited users are never limited - [ ] Weekly count resets as old queries age out past 7 days 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
Test plan
🤖 Generated with Claude Code