Skip to content

Limit floating bar to 50 queries/week for free users#6407

Merged
kodjima33 merged 3 commits into
mainfrom
worktree-voice-response-default
Apr 7, 2026
Merged

Limit floating bar to 50 queries/week for free users#6407
kodjima33 merged 3 commits into
mainfrom
worktree-voice-response-default

Conversation

@kodjima33
Copy link
Copy Markdown
Collaborator

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

kodjima33 and others added 3 commits April 7, 2026 18:02
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>
@kodjima33 kodjima33 merged commit ec576da into main Apr 7, 2026
1 check passed
@kodjima33 kodjima33 deleted the worktree-voice-response-default branch April 7, 2026 22:03
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 7, 2026

Greptile Summary

This PR adds a 50-queries-per-week rolling limit for free (Basic) users of the floating bar, using UserDefaults to persist query timestamps and the backend subscription API to determine plan tier. The implementation is straightforward, but there is one meaningful defect: recordQuery() is called before the actual API call is dispatched, meaning a failed query still consumes a slot.

Confidence Score: 4/5

Mostly 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).

Vulnerabilities

No security concerns identified. Query timestamps are stored in UserDefaults (local to the device/user account), and the subscription check uses the existing authenticated APIClient. No secrets are logged or exposed.

Important Files Changed

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
Loading

Reviews (1): Last reviewed commit: "Fetch subscription plan on startup for u..." | Re-trigger Greptile

return
}

limiter.recordQuery()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 planFetched is dead code

This property is set to true inside fetchPlan() but is never read anywhere in the file or broader codebase. Either wire it up or remove it to avoid misleading future readers.

}

/// Fetch the user's subscription plan from the backend (call on app launch / sign-in).
func fetchPlan() async {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Glucksberg pushed a commit to Glucksberg/omi-local that referenced this pull request Apr 28, 2026
)

## 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant