Tribe name boost purchases in the store - #4739
Conversation
Adds the boost flow to the store's Tribes tab: each active name gets a Boost button priced from cosmetics.json (tribeNames.boostPriceHard — never hardcoded; the button hides when the block is absent), a "N× boosted · until <date>" line from the list endpoint's activeBoosts / boostExpiresAt, and a confirm dialog whose copy sells what boosts actually do (appear more often in other players' games). Purchases POST /users/@me/tribe_names/:id/boosts with a fresh Idempotency-Key UUID per click — the endpoint has no rate limit and boosts stack, so the key is what stops a double-submit from charging twice. Balance is pre-checked against /users/@me and shortfalls reuse the InsufficientCurrencyDialog top-up path; a 400 (balance moved), 404 (name taken down), or failure each map to the flow the API doc recommends. Boost state is re-fetched after purchase, never reconstructed client-side. Verified live against stubbed API routes: list rendering, confirm, 201 refresh, idempotency header, and the no-POST insufficient path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
WalkthroughAdds tribe-name boost schemas, API handling, cosmetics pricing, and a complete ChangesTribe boost feature
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
actor Player
participant TribesPanel
participant boostTribeName
participant TribeBoostEndpoint
Player->>TribesPanel: Select boost
TribesPanel->>TribesPanel: Check balance and confirm purchase
TribesPanel->>boostTribeName: Submit tribe id and idempotency key
boostTribeName->>TribeBoostEndpoint: POST request
TribeBoostEndpoint-->>boostTribeName: Return boost response or error status
boostTribeName-->>TribesPanel: Return typed result
TribesPanel->>TribesPanel: Refresh tribe data and show notice
Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/core/CosmeticSchemas.ts (1)
435-444: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueOptional: consider guarding against malformed pricing config.
priceHard,boostPriceHard, andboostDurationDaysaccept any number, including negative or zero values. A typo incosmetics.json(e.g.boostDurationDays: -30) would still pass validation and surface a nonsensical boost duration/price in the UI.🛡️ Optional tightening
tribeNames: z .object({ - priceHard: z.number(), - boostPriceHard: z.number(), - boostDurationDays: z.number(), + priceHard: z.number().positive(), + boostPriceHard: z.number().positive(), + boostDurationDays: z.number().positive(), }) .optional(),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/core/CosmeticSchemas.ts` around lines 435 - 444, Strengthen the `tribeNames` schema in `CosmeticSchemas` by validating `priceHard`, `boostPriceHard`, and `boostDurationDays` as strictly positive numbers, rejecting zero and negative configuration values while preserving the existing optional handling for older `cosmetics.json` files.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/core/CosmeticSchemas.ts`:
- Around line 435-444: Strengthen the `tribeNames` schema in `CosmeticSchemas`
by validating `priceHard`, `boostPriceHard`, and `boostDurationDays` as strictly
positive numbers, rejecting zero and negative configuration values while
preserving the existing optional handling for older `cosmetics.json` files.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2042c94a-ccc8-496f-984e-c549828f7f6e
📒 Files selected for processing (7)
resources/lang/en.jsonsrc/client/Api.tssrc/client/components/TribesPanel.tssrc/core/ApiSchemas.tssrc/core/CosmeticSchemas.tstests/ApiSchemas.test.tstests/CosmeticSchemas.test.ts
The API moved the name purchase price to cosmetics.json and dropped priceHard from GET /users/@me/tribe_names; the required z.coerce.number() coerced the absent field to NaN and failed the whole list parse, breaking the tab. Make the field optional and prefer the cosmetics.json price, keeping the response field as a legacy fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Boosted rows now read "2 boosts · next expires Aug 23" (count instead of the multiplier, per-repo singular/_plural convention) and the list sorts most-boosted first — stable, so equal counts keep the API's newest-first order and rejected names sink with the unboosted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The list endpoint builds boostExpiresAt with a raw SQL max() that
bypasses the ORM's Date mapping, so the wire carried pg text
("2026-08-23 18:04:11+00") and the strict z.iso.datetime() failed the
whole list parse. The field is now a plain string and the renderer
skips dates the browser can't parse, showing the count alone.
Also relabel the date from "next expires" to "until": the API serves
the LAST boost's lapse (max), the moment the name stops being boosted
entirely, not the next one to drop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cosmetics.json (tribeNames.priceHard) is the only price source now; the list endpoint serves just the player's names. A legacy response with priceHard still parses — the unknown key is stripped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
Adds the boost purchase flow to the store's Tribes tab (follow-up to #4715, per the boost handoff doc):
cosmetics.json→tribeNames.boostPriceHard— never hardcoded. If the config block is absent (older API), boost purchasing hides entirely; rejected/revoked names never show the button (they'd 404).activeBoosts(+1 = displayed multiplier, since draw weight is1 + activeBoosts) andboostExpiresAt. Both fields are optional in the schema so older responses parse.Purchase flow (mirrors the API doc's recommendation)
/users/@mecurrency.hard— a shortfall opens the existingInsufficientCurrencyDialogwith its top-up CTA instead of submitting into a guaranteed 400.POST /users/@me/tribe_names/:id/boostswith a freshIdempotency-KeyUUID per click and all boost buttons disabled until it settles — the endpoint has no rate limit and boosts stack, so this is what prevents double-charges.201→ success notice, then re-fetch the name list and/users/@me(boost state is only served by the list endpoint — never reconstructed client-side).400insufficient (balance moved under us) → refresh + top-up dialog.404(name taken down mid-session) → notice + list refresh so the rejected state and review reason appear.Schemas
ApiSchemas:TribeNameSchema+activeBoosts/boostExpiresAt(optional), newPostTribeBoostResponseSchema(stringified bigints stay strings).CosmeticSchemas: optionaltribeNames { priceHard, boostPriceHard, boostDurationDays }block onCosmeticsSchema.Verification
Idempotency-Key, and the insufficient path showing the top-up dialog without issuing a request.tsc --noEmit, eslint, prettier all green; en.json keys sorted (EnJsonSorted).🤖 Generated with Claude Code