diff --git a/resources/lang/en.json b/resources/lang/en.json index b1f4913cc..66fa02726 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1295,6 +1295,8 @@ "change_tier_success": "Switched to {tier}.", "checkout_failed": "Failed to create checkout session.", "confirm_downgrade": "Downgrade to {tier}? You'll get account credit for the unused portion of your current plan.", + "confirm_purchase_body": "Buy {item} for {amount, number} {currency}?", + "confirm_purchase_title": "Confirm Purchase", "confirm_upgrade": "Upgrade to {tier}? You'll be charged the prorated difference now.", "currency_pack_purchase_success": "Currency pack purchase successful!", "effects": "Effects", diff --git a/src/client/components/CosmeticContainer.ts b/src/client/components/CosmeticContainer.ts index e262ea4fe..04038275e 100644 --- a/src/client/components/CosmeticContainer.ts +++ b/src/client/components/CosmeticContainer.ts @@ -344,6 +344,19 @@ export class CosmeticContainer extends LitElement { this.onPurchaseSoft, ].filter(Boolean); if (handlers.length === 1 && !this._loading) { + // Currency purchases go through the confirmation dialog instead of + // firing immediately. + if ( + handlers[0] === this.onPurchaseHard || + handlers[0] === this.onPurchaseSoft + ) { + ( + this.querySelector("purchase-button") as PurchaseButton | null + )?.requestCurrencyPurchase( + handlers[0] === this.onPurchaseHard ? "hard" : "soft", + ); + return; + } this._loading = true; this._showLoadingOverlay(); Promise.resolve(handlers[0]!()) @@ -468,6 +481,7 @@ export class CosmeticContainer extends LitElement { .rarity=${this.rarity} .dollarLabelKey=${this.dollarLabelKey} .priceSuffix=${this.priceSuffix} + .itemName=${this.name} .onPurchaseDollar=${this.onPurchaseDollar} .onPurchaseHard=${this.onPurchaseHard} .onPurchaseSoft=${this.onPurchaseSoft} diff --git a/src/client/components/PurchaseButton.ts b/src/client/components/PurchaseButton.ts index 8f390ad0d..19bea97c9 100644 --- a/src/client/components/PurchaseButton.ts +++ b/src/client/components/PurchaseButton.ts @@ -4,6 +4,7 @@ import { Product } from "../../core/CosmeticSchemas"; import type { InsufficientCurrency, PurchaseResult } from "../Cosmetics"; import { translateText } from "../Utils"; import "./CapIcon"; +import "./ConfirmDialog"; import "./InsufficientCurrencyDialog"; import "./PlutoniumIcon"; @@ -201,6 +202,10 @@ export class PurchaseButton extends LitElement { @property({ type: String }) priceSuffix: string = ""; + /** Display name of the item, used in the currency confirmation dialog. */ + @property({ type: String }) + itemName: string = ""; + @property({ type: Function }) onPurchaseDollar?: () => Promise; @@ -212,6 +217,8 @@ export class PurchaseButton extends LitElement { /** Set when a purchase fails for lack of funds; drives the dialog. */ @state() private insufficient: InsufficientCurrency | null = null; + /** Which currency purchase is awaiting confirmation, if any. */ + @state() private confirmingCurrency: "hard" | "soft" | null = null; private busy = false; createRenderRoot() { @@ -220,6 +227,18 @@ export class PurchaseButton extends LitElement { private handleClick(e: Event, handler?: () => Promise) { e.stopPropagation(); + this.executePurchase(handler); + } + + /** Opens the currency confirmation dialog; the purchase runs on confirm. */ + requestCurrencyPurchase(method: "hard" | "soft") { + const handler = + method === "hard" ? this.onPurchaseHard : this.onPurchaseSoft; + if (!handler || this.busy) return; + this.confirmingCurrency = method; + } + + private executePurchase(handler?: () => Promise) { if (!handler || this.busy) return; this.busy = true; const container = this.closest("cosmetic-container") as HTMLElement | null; @@ -263,7 +282,10 @@ export class PurchaseButton extends LitElement {