support for purchasing currency packs (#3629)

## Description:

Adds a currency pack system to the store. Players can purchase packs of
in-game currency (Plutonium and Caps) via Stripe checkout.

What's new:

* Pack schema (PackSchema) — new cosmetic type with currency
(hard/soft), amount, and displayName
* "Packs" tab in the Store — renders purchasable currency packs using
existing CosmeticButton infrastructure
* Stripe checkout flow — new createCurrencyPackCheckout API call and
handlePackPurchase handler
* Currency display in Account modal — shows Plutonium and Caps balances
when logged in
I* con components — <plutonium-icon> (animated green glow + rotate) and
<cap-icon> with new SVG assets
* Currency in UserMeResponse — player.currency.hard /
player.currency.soft added to the API schema

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
Evan
2026-04-10 15:07:47 -07:00
committed by GitHub
parent de92a2721a
commit 696e727a39
13 changed files with 274 additions and 12 deletions
+36 -3
View File
@@ -1,12 +1,14 @@
import { html, LitElement, nothing, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Flag, Pattern } from "../../core/CosmeticSchemas";
import { Flag, Pack, Pattern } from "../../core/CosmeticSchemas";
import { PlayerPattern } from "../../core/Schemas";
import { ResolvedCosmetic, translateCosmetic } from "../Cosmetics";
import { translateText } from "../Utils";
import "./CapIcon";
import "./CosmeticContainer";
import "./CosmeticInfo";
import { renderPatternPreview } from "./PatternPreview";
import "./PlutoniumIcon";
@customElement("cosmetic-button")
export class CosmeticButton extends LitElement {
@@ -38,6 +40,9 @@ export class CosmeticButton extends LitElement {
if (this.resolved.type === "pattern") {
return translateCosmetic("territory_patterns.pattern", c.name);
}
if (this.resolved.type === "pack") {
return (c as Pack).displayName;
}
return translateCosmetic("flags", c.name);
}
@@ -55,6 +60,33 @@ export class CosmeticButton extends LitElement {
return renderPatternPreview(playerPattern, 150, 150);
}
if (this.resolved.type === "pack") {
const pack = this.resolved.cosmetic as Pack;
const isHard = pack.currency === "hard";
const icon = isHard
? html`<plutonium-icon
class="flex-1 flex items-center"
.size=${100}
></plutonium-icon>`
: html`<cap-icon
class="flex-1 flex items-center"
.size=${100}
></cap-icon>`;
const colorClass = isHard ? "text-green-400" : "text-amber-700";
const currencyKey = isHard ? "cosmetics.hard" : "cosmetics.soft";
return html`<div
class="flex flex-col items-center justify-end h-full w-full text-center gap-1 pb-1"
>
${icon}
<span class="text-lg font-black ${colorClass}"
>${pack.amount.toLocaleString()}</span
>
<span class="text-[10px] font-bold text-white/50 uppercase"
>${translateText(currencyKey)}</span
>
</div>`;
}
const c = this.resolved.cosmetic as Flag;
return html`<img
src=${c.url}
@@ -75,8 +107,9 @@ export class CosmeticButton extends LitElement {
render() {
const c = this.resolved.cosmetic;
const isPurchasable = this.resolved.relationship === "purchasable";
const isPattern = this.resolved.type === "pattern";
const sizeClass = isPattern ? "gap-2 p-3 w-48" : "gap-1 p-1.5 w-36";
const type = this.resolved.type;
const isPattern = type === "pattern";
const sizeClass = type === "flag" ? "gap-1 p-1.5 w-36" : "gap-2 p-3 w-48";
const crazygamesClass = isPattern ? "no-crazygames " : "";
return html`