mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-08 13:02:10 +00:00
Subscription upgrade/downgrade + tier management (#3927)
## Summary - Tier upgrade/downgrade in the Store. The Subscriptions tab now shows all tiers including the user's current one. Other tiers swap "Subscribe" → "Switch" when the user already has a sub, and clicking them calls the new `POST /subscriptions/@me/change-tier` endpoint with a direction-aware confirm (upgrade charges prorated diff now, downgrade gives account credit). - Owned-tier card renders a **Current Plan** badge in place of the purchase button. Resolution logic in `resolveCosmetics` now reads `userMeResponse.player.subscription.tier` (with flare fallback) and marks that tier as `owned`. - AccountModal's `<subscription-panel>` reworked into a proper two-column layout: - **Left**: tier name, `$X.XX/mo` price, description, daily Pu/Caps amounts. - **Right**: status badge (Active / Renews date / Cancels date), `[Manage] [Change Tier]` button row, `[Cancel]` centered underneath. When `cancelAtPeriodEnd === true`, the row collapses to a single `[Reactivate]` button (opens the Stripe portal). - New `<o-button size="xs">` variant (`py-2 px-3 text-xs`) for the compact panel buttons. - Store dollar-purchase price label now supports an optional suffix (`/mo` for subs only) via a `priceSuffix` prop plumbed through `CosmeticContainer` → `PurchaseButton`. - `Api.ts` gains `changeSubscriptionTier(tierName)` with the same 401-handling pattern as the existing subscription helpers. <img width="1114" height="728" alt="Screenshot 2026-05-14 at 7 09 20 PM" src="https://github.com/user-attachments/assets/688f83d5-4010-4580-9214-6885af8ec98e" /> <img width="1038" height="276" alt="Screenshot 2026-05-14 at 7 09 33 PM" src="https://github.com/user-attachments/assets/458197f5-a0d4-4c32-bc55-31e5679629b5" /> <img width="887" height="286" alt="Screenshot 2026-05-14 at 7 09 55 PM" src="https://github.com/user-attachments/assets/8149ed82-89cc-4bbe-83de-3614f886b331" /> ## Discord evan
This commit is contained in:
@@ -34,6 +34,10 @@ export class SubscriptionPanel extends LitElement {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
private handleChangeTier = (): void => {
|
||||
window.location.hash = "modal=store&tab=subscriptions";
|
||||
};
|
||||
|
||||
private handleCancel = async (): Promise<void> => {
|
||||
const confirmed = window.confirm(
|
||||
translateText("account_modal.cancel_subscription_confirm"),
|
||||
@@ -103,56 +107,87 @@ export class SubscriptionPanel extends LitElement {
|
||||
${translateText("account_modal.your_subscription")}
|
||||
</h3>
|
||||
<div
|
||||
class="flex flex-col gap-3 p-4 rounded-lg bg-white/5 border border-white/10"
|
||||
class="flex flex-wrap items-start justify-between gap-4 p-4 rounded-lg bg-white/5 border border-white/10"
|
||||
>
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<div class="text-base font-bold text-white">
|
||||
${translateCosmetic("subscriptions", cosmetic?.name ?? sub.tier)}
|
||||
<div class="flex flex-col gap-3 flex-1 min-w-0">
|
||||
<div class="flex items-baseline gap-2 flex-wrap">
|
||||
<div class="text-base font-bold text-white">
|
||||
${translateCosmetic(
|
||||
"subscriptions",
|
||||
cosmetic?.name ?? sub.tier,
|
||||
)}
|
||||
</div>
|
||||
${cosmetic?.product?.price
|
||||
? html`<div class="text-xs text-white/60">
|
||||
${translateText("account_modal.sub_price_monthly", {
|
||||
price: cosmetic.product.price,
|
||||
})}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
${this.renderStatus()}
|
||||
${cosmetic?.description
|
||||
? html`<div class="text-sm text-white/70">
|
||||
${cosmetic.description}
|
||||
</div>`
|
||||
: ""}
|
||||
${cosmetic
|
||||
? html`<div class="flex flex-wrap gap-4 mt-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<plutonium-icon .size=${20}></plutonium-icon>
|
||||
<span class="text-sm font-bold text-green-400"
|
||||
>${cosmetic.dailyHardCurrency.toLocaleString()}</span
|
||||
>
|
||||
<span class="text-[10px] text-white/50 uppercase"
|
||||
>${translateText("cosmetics.per_day")}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<cap-icon .size=${20}></cap-icon>
|
||||
<span class="text-sm font-bold text-amber-700"
|
||||
>${cosmetic.dailySoftCurrency.toLocaleString()}</span
|
||||
>
|
||||
<span class="text-[10px] text-white/50 uppercase"
|
||||
>${translateText("cosmetics.per_day")}</span
|
||||
>
|
||||
</div>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
${cosmetic?.description
|
||||
? html`<div class="text-sm text-white/70">
|
||||
${cosmetic.description}
|
||||
</div>`
|
||||
: ""}
|
||||
${cosmetic
|
||||
? html`<div class="flex flex-wrap gap-4 mt-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<plutonium-icon .size=${20}></plutonium-icon>
|
||||
<span class="text-sm font-bold text-green-400"
|
||||
>${cosmetic.dailyHardCurrency.toLocaleString()}</span
|
||||
>
|
||||
<span class="text-[10px] text-white/50 uppercase"
|
||||
>${translateText("cosmetics.per_day")}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<cap-icon .size=${20}></cap-icon>
|
||||
<span class="text-sm font-bold text-amber-700"
|
||||
>${cosmetic.dailySoftCurrency.toLocaleString()}</span
|
||||
>
|
||||
<span class="text-[10px] text-white/50 uppercase"
|
||||
>${translateText("cosmetics.per_day")}</span
|
||||
>
|
||||
</div>
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<o-button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
translationKey="account_modal.manage_subscription"
|
||||
@click=${this.handleManage}
|
||||
></o-button>
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
${this.renderStatus()}
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
${sub.cancelAtPeriodEnd
|
||||
? html`<o-button
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
translationKey="account_modal.reactivate_subscription"
|
||||
@click=${this.handleManage}
|
||||
></o-button>`
|
||||
: html`
|
||||
<o-button
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
translationKey="account_modal.manage_subscription"
|
||||
@click=${this.handleManage}
|
||||
></o-button>
|
||||
<o-button
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
translationKey="account_modal.change_tier"
|
||||
@click=${this.handleChangeTier}
|
||||
></o-button>
|
||||
`}
|
||||
</div>
|
||||
${sub.cancelAtPeriodEnd
|
||||
? ""
|
||||
: html`<o-button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
translationKey="account_modal.cancel_subscription"
|
||||
@click=${this.handleCancel}
|
||||
></o-button>`}
|
||||
: html`<div class="flex justify-center w-full">
|
||||
<o-button
|
||||
variant="danger"
|
||||
size="xs"
|
||||
translationKey="account_modal.cancel_subscription"
|
||||
@click=${this.handleCancel}
|
||||
></o-button>
|
||||
</div>`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user