Conversation
WalkthroughAdds client-side subscription tier-change support: a new API helper, purchase flow to confirm and call tier changes, owned-subscription detection, and button/ container props to show switch/subscribe labels and monthly price suffixes. ChangesSubscription Tier Changes
sequenceDiagram
participant User
participant Store as Store UI
participant CosmeticButton
participant Cosmetics as Cosmetics.ts
participant Api as API Client
participant Backend
User->>Store: Open subscription grid
Store->>CosmeticButton: Render with userHasSubscription flag
User->>CosmeticButton: Click "Switch/Subscribe"
CosmeticButton->>Cosmetics: purchaseCosmetic(subscription)
Cosmetics->>Cosmetics: read current tier, compare price
Cosmetics->>User: show upgrade/downgrade confirmation
User->>Cosmetics: confirm
Cosmetics->>Api: changeSubscriptionTier(newTier)
Api->>Backend: POST /subscriptions/@me/change-tier
Backend->>Api: 200 OK
Api->>Cosmetics: success
Cosmetics->>Store: refresh user state
Store->>User: updated subscription shown
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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.
Actionable comments posted: 3
🤖 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.
Inline comments:
In `@src/client/components/baseComponents/Button.ts`:
- Around line 59-60: The "xs" size case in Button.ts returns classes that are
taller than "sm" (case "xs" currently returns "py-2 px-3 text-xs"), so update
the "xs" case in the size switch (the case labeled "xs" in the function that
computes size classes) to use smaller vertical padding than "sm" (e.g., change
to a smaller padding set such as py-1 and smaller horizontal padding like px-2
while keeping text-xs) so the xs button renders smaller than sm.
In `@src/client/components/SubscriptionPanel.ts`:
- Around line 128-131: SubscriptionPanel currently renders cosmetic.description
directly; wrap that value in the translateText() call (i.e. replace direct
rendering of cosmetic.description with translateText(cosmetic.description) or a
translation key derived from the cosmetic item) so all UI text goes through
i18n, and add the matching entry/key to resources/lang/en.json (e.g.
"subscription.cosmetic.<id>.description" or the exact string key you use) so
translations load correctly; update any unit/preview that asserts the rendered
text if present.
In `@src/client/Cosmetics.ts`:
- Around line 58-67: The code currently treats a missing currentCosmetic as an
upgrade (isUpgrade true) which can display incorrect billing text; change the
logic so isUpgrade is only computed when currentCosmetic exists (use
currentCosmetic !== null && currentCosmetic !== undefined), otherwise set a
neutral state (e.g., isUpgrade = null or a separate flag like directionUnknown)
and choose a neutral confirmKey (add a new translation key like
"store.confirm_change" or similar) instead of "store.confirm_upgrade" or
"store.confirm_downgrade"; update the confirmKey assignment to select
upgrade/downgrade only when isUpgrade is boolean and fall back to the neutral
key when currentCosmetic is missing, referencing fetchCosmetics(),
currentCosmetic, isUpgrade, and confirmKey in your changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6d2b1ab0-8016-4ff2-81ad-587cdaf4a4db
📒 Files selected for processing (9)
resources/lang/en.jsonsrc/client/Api.tssrc/client/Cosmetics.tssrc/client/Store.tssrc/client/components/CosmeticButton.tssrc/client/components/CosmeticContainer.tssrc/client/components/PurchaseButton.tssrc/client/components/SubscriptionPanel.tssrc/client/components/baseComponents/Button.ts
| const currentCosmetic = | ||
| (await fetchCosmetics())?.subscriptions?.[currentSub.tier] ?? null; | ||
| const isUpgrade = | ||
| currentCosmetic !== undefined && currentCosmetic !== null | ||
| ? sub.priceMonthly > currentCosmetic.priceMonthly | ||
| : true; | ||
| const targetName = translateCosmetic("subscriptions", sub.name); | ||
| const confirmKey = isUpgrade | ||
| ? "store.confirm_upgrade" | ||
| : "store.confirm_downgrade"; |
There was a problem hiding this comment.
Avoid defaulting unknown tier comparisons to “upgrade” messaging.
On Line 60–67, when the current tier is missing from cosmetics.subscriptions, isUpgrade defaults to true, so downgrade paths can show upgrade billing text. Use a neutral fallback confirm message when direction is unknown.
💡 Suggested fix
- const isUpgrade =
- currentCosmetic !== undefined && currentCosmetic !== null
- ? sub.priceMonthly > currentCosmetic.priceMonthly
- : true;
+ const isUpgrade =
+ currentCosmetic !== undefined && currentCosmetic !== null
+ ? sub.priceMonthly > currentCosmetic.priceMonthly
+ : null;
const targetName = translateCosmetic("subscriptions", sub.name);
- const confirmKey = isUpgrade
- ? "store.confirm_upgrade"
- : "store.confirm_downgrade";
+ const confirmKey =
+ isUpgrade === null
+ ? "store.confirm_change_tier"
+ : isUpgrade
+ ? "store.confirm_upgrade"
+ : "store.confirm_downgrade";Add a neutral translation key:
"store": {
+ "confirm_change_tier": "Switch to {tier}?",
"confirm_upgrade": "Upgrade to {tier}? You'll be charged the prorated difference now.",
"confirm_downgrade": "Downgrade to {tier}? You'll get account credit for the unused portion of your current plan.",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const currentCosmetic = | |
| (await fetchCosmetics())?.subscriptions?.[currentSub.tier] ?? null; | |
| const isUpgrade = | |
| currentCosmetic !== undefined && currentCosmetic !== null | |
| ? sub.priceMonthly > currentCosmetic.priceMonthly | |
| : true; | |
| const targetName = translateCosmetic("subscriptions", sub.name); | |
| const confirmKey = isUpgrade | |
| ? "store.confirm_upgrade" | |
| : "store.confirm_downgrade"; | |
| const currentCosmetic = | |
| (await fetchCosmetics())?.subscriptions?.[currentSub.tier] ?? null; | |
| const isUpgrade = | |
| currentCosmetic !== undefined && currentCosmetic !== null | |
| ? sub.priceMonthly > currentCosmetic.priceMonthly | |
| : null; | |
| const targetName = translateCosmetic("subscriptions", sub.name); | |
| const confirmKey = | |
| isUpgrade === null | |
| ? "store.confirm_change_tier" | |
| : isUpgrade | |
| ? "store.confirm_upgrade" | |
| : "store.confirm_downgrade"; |
🤖 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/client/Cosmetics.ts` around lines 58 - 67, The code currently treats a
missing currentCosmetic as an upgrade (isUpgrade true) which can display
incorrect billing text; change the logic so isUpgrade is only computed when
currentCosmetic exists (use currentCosmetic !== null && currentCosmetic !==
undefined), otherwise set a neutral state (e.g., isUpgrade = null or a separate
flag like directionUnknown) and choose a neutral confirmKey (add a new
translation key like "store.confirm_change" or similar) instead of
"store.confirm_upgrade" or "store.confirm_downgrade"; update the confirmKey
assignment to select upgrade/downgrade only when isUpgrade is boolean and fall
back to the neutral key when currentCosmetic is missing, referencing
fetchCosmetics(), currentCosmetic, isUpgrade, and confirmKey in your changes.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/client/Cosmetics.ts (1)
56-69:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse a neutral confirm message when the current tier cannot be resolved.
If
fetchCosmetics()fails or the current tier is missing fromcosmetics.subscriptions,isUpgradefalls back totrue, so downgrade flows can still show the prorated-upgrade copy.Proposed change
const currentCosmetic = (await fetchCosmetics())?.subscriptions?.[currentSub.tier] ?? null; const isUpgrade = currentCosmetic !== null ? sub.priceMonthly > currentCosmetic.priceMonthly - : true; + : null; const targetName = translateCosmetic("subscriptions", sub.name); - const confirmKey = isUpgrade - ? "store.confirm_upgrade" - : "store.confirm_downgrade"; + const confirmKey = + isUpgrade === null + ? "store.confirm_change_tier" + : isUpgrade + ? "store.confirm_upgrade" + : "store.confirm_downgrade";🤖 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/client/Cosmetics.ts` around lines 56 - 69, The confirm dialog assumes an upgrade when currentCosmetic is null, causing downgrade flows to show upgrade copy; change the logic in the block that calls fetchCosmetics() (referencing currentCosmetic, isUpgrade, currentSub.tier, sub.priceMonthly) to detect the unresolved case and use a neutral translation key (e.g., "store.confirm_change") instead of defaulting to the upgrade key; update the confirmKey selection (used in translateText/translateCosmetic and assigned to confirmed) to choose "store.confirm_upgrade" or "store.confirm_downgrade" only when currentCosmetic is present, otherwise choose the neutral key.
🤖 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.
Inline comments:
In `@src/client/components/PurchaseButton.ts`:
- Around line 199-201: The priceSuffix property on the PurchaseButton component
is currently raw display text and must be passed/used as a translation key (or
as a fully translated template) so it goes through translateText() and
resources/lang/en.json; change the `@property` priceSuffix: string to represent a
translation key (e.g., "priceSuffixKey") or document that callers must pass a
translated string, then update the render logic in PurchaseButton (where
priceSuffix is interpolated) to call translateText(this.priceSuffix) before
rendering and add the corresponding key/value to resources/lang/en.json; apply
the same change to the other similar prop references noted around the 239-242
area so all user-visible suffixes go through translateText().
In `@src/client/Cosmetics.ts`:
- Around line 395-402: The ownership logic can mark both the actual subscription
and a flare as owned when flares lag; update the isCurrent calculation in the
loop over cosmetics.subscriptions so that subscription ownership uses
player.subscription.tier as the sole source of truth (currentSubTier) when it is
non-null, and only fall back to checking flares.includes(key) if currentSubTier
is null/undefined; adjust the evaluation that sets isCurrent (referencing
currentSubTier, flares, subKey, and key) so flares are considered only when
userMeResponse.player.subscription?.tier is not present.
---
Duplicate comments:
In `@src/client/Cosmetics.ts`:
- Around line 56-69: The confirm dialog assumes an upgrade when currentCosmetic
is null, causing downgrade flows to show upgrade copy; change the logic in the
block that calls fetchCosmetics() (referencing currentCosmetic, isUpgrade,
currentSub.tier, sub.priceMonthly) to detect the unresolved case and use a
neutral translation key (e.g., "store.confirm_change") instead of defaulting to
the upgrade key; update the confirmKey selection (used in
translateText/translateCosmetic and assigned to confirmed) to choose
"store.confirm_upgrade" or "store.confirm_downgrade" only when currentCosmetic
is present, otherwise choose the neutral key.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b4dbb685-2160-470a-bbd5-a457b476c516
📒 Files selected for processing (9)
resources/lang/en.jsonsrc/client/Api.tssrc/client/Cosmetics.tssrc/client/Store.tssrc/client/components/CosmeticButton.tssrc/client/components/CosmeticContainer.tssrc/client/components/PurchaseButton.tssrc/client/components/SubscriptionPanel.tssrc/client/components/baseComponents/Button.ts
✅ Files skipped from review due to trivial changes (1)
- resources/lang/en.json
🚧 Files skipped from review as they are similar to previous changes (6)
- src/client/components/CosmeticButton.ts
- src/client/components/CosmeticContainer.ts
- src/client/components/baseComponents/Button.ts
- src/client/Store.ts
- src/client/components/SubscriptionPanel.ts
- src/client/Api.ts
| const currentSubTier = | ||
| userMeResponse === false | ||
| ? null | ||
| : (userMeResponse.player.subscription?.tier ?? null); | ||
| for (const [subKey, sub] of Object.entries(cosmetics.subscriptions ?? {})) { | ||
| const key = `subscription:${subKey}`; | ||
| const rel = flares.includes(key) | ||
| const isCurrent = subKey === currentSubTier || flares.includes(key); | ||
| const rel: ResolvedCosmetic["relationship"] = isCurrent |
There was a problem hiding this comment.
Use flare ownership only as a fallback.
subKey === currentSubTier || flares.includes(key) can mark two plans as "owned" if the flare list lags behind a tier change. When player.subscription.tier exists, it should be the only source of truth.
Proposed change
- const isCurrent = subKey === currentSubTier || flares.includes(key);
+ const isCurrent =
+ currentSubTier !== null ? subKey === currentSubTier : flares.includes(key);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const currentSubTier = | |
| userMeResponse === false | |
| ? null | |
| : (userMeResponse.player.subscription?.tier ?? null); | |
| for (const [subKey, sub] of Object.entries(cosmetics.subscriptions ?? {})) { | |
| const key = `subscription:${subKey}`; | |
| const rel = flares.includes(key) | |
| const isCurrent = subKey === currentSubTier || flares.includes(key); | |
| const rel: ResolvedCosmetic["relationship"] = isCurrent | |
| const currentSubTier = | |
| userMeResponse === false | |
| ? null | |
| : (userMeResponse.player.subscription?.tier ?? null); | |
| for (const [subKey, sub] of Object.entries(cosmetics.subscriptions ?? {})) { | |
| const key = `subscription:${subKey}`; | |
| const isCurrent = | |
| currentSubTier !== null ? subKey === currentSubTier : flares.includes(key); | |
| const rel: ResolvedCosmetic["relationship"] = isCurrent |
🤖 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/client/Cosmetics.ts` around lines 395 - 402, The ownership logic can mark
both the actual subscription and a flare as owned when flares lag; update the
isCurrent calculation in the loop over cosmetics.subscriptions so that
subscription ownership uses player.subscription.tier as the sole source of truth
(currentSubTier) when it is non-null, and only fall back to checking
flares.includes(key) if currentSubTier is null/undefined; adjust the evaluation
that sets isCurrent (referencing currentSubTier, flares, subKey, and key) so
flares are considered only when userMeResponse.player.subscription?.tier is not
present.
Summary
POST /subscriptions/@me/change-tierendpoint with a direction-aware confirm (upgrade charges prorated diff now, downgrade gives account credit).resolveCosmeticsnow readsuserMeResponse.player.subscription.tier(with flare fallback) and marks that tier asowned.<subscription-panel>reworked into a proper two-column layout:$X.XX/moprice, description, daily Pu/Caps amounts.[Manage] [Change Tier]button row,[Cancel]centered underneath. WhencancelAtPeriodEnd === true, the row collapses to a single[Reactivate]button (opens the Stripe portal).<o-button size="xs">variant (py-2 px-3 text-xs) for the compact panel buttons./mofor subs only) via apriceSuffixprop plumbed throughCosmeticContainer→PurchaseButton.Api.tsgainschangeSubscriptionTier(tierName)with the same 401-handling pattern as the existing subscription helpers.Discord
evan