Skip to content

Commit e2dae20

Browse files
Gabry848claude
andcommitted
docs(openspec): aggiunge specifiche per il sistema di quota chat
Proposta, design, spec e task per il sistema di limiti piano: - chat-quota-enforcement: gestione HTTP 429 e WS 4029 - plan-usage: visualizzazione piano e utilizzo in settings - quota-indicator: indicatore messaggi rimanenti nella chat Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 61ccc23 commit e2dae20

7 files changed

Lines changed: 234 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-04-07
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
## Context
2+
3+
The server has introduced a monthly quota system for AI chat (text and voice). The app currently has no awareness of user plans or usage limits. The bot service (`src/services/botservice.ts`) sends requests to `POST /chat/text` and opens WebSockets to `/chat/voice-bot-websocket` without handling quota-related error codes. The Settings screen has no plan/usage information.
4+
5+
Current state:
6+
- `botservice.ts` does not read rate-limit response headers
7+
- No `planService` exists
8+
- Text chat catches generic errors but not HTTP 429 specifically
9+
- Voice WebSocket does not differentiate close code 4029 from other failures
10+
- Settings screen has no plan section
11+
12+
## Goals / Non-Goals
13+
14+
**Goals:**
15+
- Introduce `planService.ts` to fetch and cache `GET /auth/me/plan`
16+
- Display plan, text/voice quotas with progress bars, and reset date in Settings
17+
- Handle HTTP `429` in text chat with an inline, localized message and CTA
18+
- Handle WS close `4029` and error frame in voice chat with a modal and CTA
19+
- Show a live `X-RateLimit-Remaining` counter in the chat input area; disable send at 0
20+
- Add all user-facing strings to `en.json` and `it.json`
21+
22+
**Non-Goals:**
23+
- In-app upgrade/payment flow (CTA navigates to Plan screen only)
24+
- Admin plan management UI
25+
- Push notification when quota resets
26+
- Caching plan data across app restarts (re-fetch on open is sufficient)
27+
28+
## Decisions
29+
30+
### D1 — New `planService.ts` instead of extending existing services
31+
Quota data is orthogonal to tasks and auth. A dedicated service keeps concerns separate and is easier to test.
32+
Alternative considered: extending `authService.ts`. Rejected because auth service is already complex and quota data has its own refresh lifecycle.
33+
34+
### D2 — Store remaining counter in React state, not AsyncStorage
35+
`X-RateLimit-Remaining` is up-to-date after every message send. Persisting it across app restarts adds complexity for little gain; `GET /auth/me/plan` on next open gives accurate data.
36+
Alternative: AsyncStorage persistence. Rejected as overkill.
37+
38+
### D3 — Inline error message in chat (not toast) for 429
39+
The server spec explicitly requests an inline error message. This also provides a persistent, tappable CTA to the Plan screen.
40+
Alternative: bottom toast. Rejected per server integration guide.
41+
42+
### D4 — Voice quota modal (not inline) for close 4029
43+
Voice chat UI doesn't have a chat message thread. A modal is the appropriate pattern for a session-ending error.
44+
45+
### D5 — Threshold `>= 9999` treated as unlimited
46+
ENTERPRISE plan returns `999999`. Any value >= 9999 is treated as unlimited in the UI (counter hidden, send never disabled).
47+
48+
### D6 — `X-RateLimit-Remaining` read from SSE/fetch response headers
49+
`botservice.ts` already wraps the `/chat/text` call. The remaining count will be extracted from the response headers at that call site and returned alongside the message response so the caller can update UI state.
50+
51+
## Risks / Trade-offs
52+
53+
- [Risk] SSE streaming responses may not expose headers via the Fetch API in React Native → Mitigation: test on Android/iOS; fall back to `GET /auth/me/plan` if headers are inaccessible in the streaming path.
54+
- [Risk] Plan screen navigation assumes a named route exists → Mitigation: verify route name in `RootStackParamList` before wiring CTAs; add route if missing.
55+
- [Risk] WS close code `4029` may be swallowed by reconnect logic → Mitigation: check existing reconnect guard in `VoiceBotWebSocket` and add an explicit `4029` branch that skips reconnect.
56+
57+
## Migration Plan
58+
59+
No data migration needed. All changes are additive:
60+
1. Add `planService.ts`
61+
2. Update `botservice.ts` (header reading + 429 handling)
62+
3. Update `VoiceBotWebSocket` in `botservice.ts` (4029 handling)
63+
4. Add Plan section to Settings screen
64+
5. Update BotChat screen (inline error + counter + disabled state)
65+
6. Add i18n strings
66+
67+
Rollback: revert the above files. No server-side changes required from the client side.
68+
69+
## Open Questions
70+
71+
- Is there an existing "Plan" or "Subscription" screen in the navigation stack, or must it be created? (Affects CTA navigation target.)
72+
- Does the voice chat UI live in a dedicated screen or is it a modal within BotChat? (Affects where to show the modal.)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## Why
2+
3+
The server now enforces monthly usage quotas on AI chat (text and voice) per user plan (FREE/PRO/ENTERPRISE). The client must surface quota status, handle exhaustion errors gracefully, and guide users toward upgrading — otherwise users will hit silent failures or confusing errors when their quota runs out.
4+
5+
## What Changes
6+
7+
- **New**: `GET /auth/me/plan` API call to fetch plan info and usage counters
8+
- **New**: Plan & Usage UI section in Settings showing text/voice quotas with progress bars and reset date
9+
- **Modified**: Text chat handler now catches HTTP `429` and shows an inline quota-exceeded message with CTA to Plan screen
10+
- **Modified**: Voice WebSocket handler now catches close code `4029` and error frame `{"type":"error","message":"Voice request quota exceeded..."}`, showing a modal with CTA to Plan screen
11+
- **New**: Live quota counter in chat UI reading `X-RateLimit-Remaining` from response headers; disables send button at 0
12+
- **New**: `planService.ts` service for fetching and caching plan/quota data
13+
14+
## Capabilities
15+
16+
### New Capabilities
17+
18+
- `plan-usage`: Fetch and display user plan info, text/voice quota counters, and reset date from `GET /auth/me/plan`
19+
- `chat-quota-enforcement`: Handle `429` HTTP errors in text chat and WS close code `4029` in voice chat with inline messages and Plan screen CTAs
20+
- `quota-indicator`: Live remaining-messages counter in chat UI derived from `X-RateLimit-Remaining` response headers
21+
22+
### Modified Capabilities
23+
24+
<!-- No existing spec-level requirements are changing; new behavior is additive -->
25+
26+
## Impact
27+
28+
- `src/services/botservice.ts`: Add header reading for `X-RateLimit-Remaining`; handle `429` in text send; handle WS close `4029` and error frame in voice
29+
- `src/navigation/screens/Settings.tsx`: Add Plan & Usage section with progress bars
30+
- `src/navigation/screens/BotChat.tsx` (or equivalent): Show inline quota error, soft warning, disabled send button
31+
- `src/navigation/screens/VoiceChat.tsx` (or equivalent): Show voice quota modal
32+
- New file: `src/services/planService.ts` — wraps `GET /auth/me/plan`
33+
- `src/locales/en.json` / `src/locales/it.json`: Add quota-related strings
34+
- No new native dependencies required
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Handle HTTP 429 in text chat
4+
The text chat send handler SHALL catch HTTP `429` responses from `POST /chat/text` and display an inline error message in the chat thread. The message SHALL include the plan name and quota reset date. The system SHALL NOT retry automatically on 429.
5+
6+
#### Scenario: Text quota exceeded
7+
- **WHEN** the user sends a text message and the server responds with HTTP `429`
8+
- **THEN** an inline bot-style message appears in the chat: "Hai raggiunto il limite mensile di messaggi per il tuo piano {PLAN}. I contatori si resettano il {RESET_DATE}."
9+
- **AND** the message includes a tappable CTA that navigates to the Plan & Usage screen
10+
- **AND** the send button remains accessible (user is not locked out permanently)
11+
12+
#### Scenario: No automatic retry on 429
13+
- **WHEN** the server returns `429`
14+
- **THEN** the client does NOT resend the message automatically
15+
16+
### Requirement: Handle WebSocket close code 4029 in voice chat
17+
The voice WebSocket handler SHALL detect the `{"type":"error","message":"Voice request quota exceeded..."}` frame and the close code `4029`, and SHALL display a quota-exceeded modal. The system SHALL NOT attempt automatic reconnection after a `4029` close.
18+
19+
#### Scenario: Voice quota exceeded — error frame received
20+
- **WHEN** the WebSocket receives a JSON frame with `type === "error"` and a message containing "quota exceeded"
21+
- **THEN** the voice session ends and a modal is shown: "Hai esaurito le richieste vocali mensili per il piano {PLAN}. Puoi continuare a usare la chat testuale."
22+
- **AND** the modal includes a CTA to navigate to the Plan & Usage screen
23+
24+
#### Scenario: Voice quota exceeded — close code 4029
25+
- **WHEN** the WebSocket closes with code `4029`
26+
- **THEN** the voice session ends and the same quota-exceeded modal is shown
27+
- **AND** the client does NOT attempt to reconnect
28+
29+
#### Scenario: Normal WebSocket close is unaffected
30+
- **WHEN** the WebSocket closes with any code other than `4029`
31+
- **THEN** existing reconnect and error-handling logic is unchanged
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Fetch user plan and quota data
4+
The system SHALL provide a `planService.ts` that calls `GET /auth/me/plan` with the user's Bearer token and API key, returning plan name, text/voice usage counters, and reset date.
5+
6+
#### Scenario: Successful fetch
7+
- **WHEN** `planService.getUserPlan()` is called with a valid token
8+
- **THEN** it returns an object with `plan`, `text_messages_limit`, `text_messages_used`, `voice_requests_limit`, `voice_requests_used`, and `reset_date`
9+
10+
#### Scenario: Network or auth failure
11+
- **WHEN** the request fails (network error or 401)
12+
- **THEN** `planService.getUserPlan()` throws an error that the caller can handle
13+
14+
### Requirement: Display plan and usage in Settings
15+
The Settings screen SHALL display a "Piano & Utilizzo" section that shows the user's current plan, text and voice usage progress bars, and the quota reset date. The section SHALL be populated by calling `planService.getUserPlan()` on screen mount.
16+
17+
#### Scenario: Data loaded successfully
18+
- **WHEN** the Settings screen mounts and the plan fetch succeeds
19+
- **THEN** the screen displays the plan badge (e.g. "FREE"), two progress bars (text messages and voice requests) with used/limit labels, and the reset date formatted as a localized date string
20+
21+
#### Scenario: FREE plan shown with upgrade CTA
22+
- **WHEN** the fetched plan is `"FREE"`
23+
- **THEN** an "Upgrade" CTA button is visible below the usage section
24+
25+
#### Scenario: ENTERPRISE plan — unlimited display
26+
- **WHEN** either `text_messages_limit` or `voice_requests_limit` is >= 9999
27+
- **THEN** the corresponding counter displays "Illimitato" instead of a numeric limit and the progress bar is hidden
28+
29+
#### Scenario: Loading state
30+
- **WHEN** the plan fetch is in progress
31+
- **THEN** placeholder/skeleton UI is shown for the usage section
32+
33+
#### Scenario: Fetch error
34+
- **WHEN** the plan fetch fails
35+
- **THEN** a retry button or error message is shown; the rest of Settings remains usable
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Live remaining-messages counter in chat UI
4+
After each successful text message send, the chat screen SHALL read the `X-RateLimit-Remaining` header from the response, store it in local state, and display a subtle counter near the chat input. The counter SHALL be hidden for ENTERPRISE users (limit >= 9999).
5+
6+
#### Scenario: Counter updates after successful send
7+
- **WHEN** `POST /chat/text` returns `200 OK` with an `X-RateLimit-Remaining` header
8+
- **THEN** the counter in the chat UI updates to show "{N} messaggi rimasti questo mese"
9+
10+
#### Scenario: Counter hidden for ENTERPRISE users
11+
- **WHEN** the user's `text_messages_limit` is >= 9999
12+
- **THEN** no remaining-messages counter is displayed
13+
14+
#### Scenario: Soft warning at low quota
15+
- **WHEN** `X-RateLimit-Remaining` is <= 5
16+
- **THEN** the counter text changes to a warning style: "Ti restano solo {N} messaggi questo mese."
17+
18+
### Requirement: Disable send button at zero remaining
19+
When `X-RateLimit-Remaining` reaches `0`, the chat send button SHALL be disabled and the exhausted state SHALL be shown (matching the inline error from the chat-quota-enforcement spec).
20+
21+
#### Scenario: Send disabled at quota zero
22+
- **WHEN** `X-RateLimit-Remaining` is `0` (or the last response returned `429`)
23+
- **THEN** the send button is visually disabled and tapping it does not send a message
24+
- **AND** the inline quota-exceeded message is visible in the chat thread
25+
26+
#### Scenario: Send re-enabled after plan refresh
27+
- **WHEN** the user navigates to the Plan & Usage screen and returns to chat
28+
- **THEN** if the quota has been refreshed (new month), the send button becomes active again
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## 1. Plan Service
2+
3+
- [x] 1.1 Create `src/services/planService.ts` with `getUserPlan()` that calls `GET /auth/me/plan` using the existing axios instance (with Bearer token and API key headers)
4+
- [x] 1.2 Define and export `UserPlan` TypeScript interface matching the API response fields
5+
- [x] 1.3 Add localized strings for plan/quota UI to `src/locales/en.json` and `src/locales/it.json` (plan badge labels, progress bar labels, reset date label, upgrade CTA, "Illimitato" text)
6+
7+
## 2. Settings — Plan & Usage Section
8+
9+
- [x] 2.1 Add a "Piano & Utilizzo" section to `src/navigation/screens/Settings.tsx` that calls `planService.getUserPlan()` on mount
10+
- [x] 2.2 Render plan badge (FREE / PRO / ENTERPRISE), two progress bars (text messages and voice requests) with used/limit labels, and formatted reset date
11+
- [x] 2.3 Show "Upgrade" CTA button when `plan === "FREE"`
12+
- [x] 2.4 Display "Illimitato" and hide progress bar when limit >= 9999
13+
- [x] 2.5 Handle loading and error states in the Settings section (skeleton UI + retry button)
14+
15+
## 3. Text Chat — 429 Handling
16+
17+
- [x] 3.1 In the text message send handler (BotChat screen or `botservice.ts`), catch HTTP `429` responses and extract plan name and reset date for the error message
18+
- [x] 3.2 Insert an inline bot-style error message into the chat thread with the localized quota-exceeded text and a tappable CTA that navigates to the Plan & Usage screen (Settings)
19+
- [x] 3.3 Ensure no automatic retry occurs on 429
20+
21+
## 4. Quota Indicator in Chat UI
22+
23+
- [x] 4.1 In `botservice.ts`, extract `X-RateLimit-Remaining` from the `POST /chat/text` response headers and return it alongside the message data
24+
- [x] 4.2 In the BotChat screen, store `remainingMessages` in local state and update it after each successful send
25+
- [x] 4.3 Render the remaining-messages counter near the chat input; apply warning style when <= 5; hide for ENTERPRISE (limit >= 9999)
26+
- [x] 4.4 Disable the send button and show the exhausted state when `remainingMessages === 0` (or last response was 429)
27+
28+
## 5. Voice Chat — 4029 Handling
29+
30+
- [x] 5.1 In `VoiceBotWebSocket` (`src/services/botservice.ts`), add a handler for incoming JSON frames with `type === "error"` containing a quota-exceeded message
31+
- [x] 5.2 Add a handler for WebSocket close code `4029` that suppresses automatic reconnection and invokes a `onVoiceQuotaExceeded` callback
32+
- [x] 5.3 In the voice chat UI (VoiceChat screen or BotChat voice modal), show a modal with the localized voice-quota-exceeded message and a CTA to the Plan & Usage screen when `onVoiceQuotaExceeded` fires

0 commit comments

Comments
 (0)