fix(ui): scope the wallet buy-button busy dim to the tapped button
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Failing after 11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped

Tapping one pack's Buy dimmed every buy button at once (the shared busy
flag drove disabled + the :disabled opacity on all of them), so it looked
like both packs reacted to one tap. Track the in-flight product id
(busyId): the concurrency guard stays global, but the pressed/dimmed look
is now scoped to the tapped button. Payments were never affected.
This commit is contained in:
Ilia Denisov
2026-07-09 22:26:28 +02:00
parent 219fc7c827
commit 55dbb1005e
+16 -8
View File
@@ -21,7 +21,10 @@
let wallet = $state<Wallet | null>(null); let wallet = $state<Wallet | null>(null);
let catalog = $state<Catalog | null>(null); let catalog = $state<Catalog | null>(null);
let loading = $state(true); let loading = $state(true);
let busy = $state(false); // The product whose purchase is in flight, or null. It both guards against a concurrent purchase
// (busy) and scopes the pressed/dimmed visual to the tapped button — not every buy button.
let busyId = $state<string | null>(null);
const busy = $derived(busyId !== null);
// The value awaiting a web-spend warning confirmation (a spend that would draw vk/tg chips). // The value awaiting a web-spend warning confirmation (a spend that would draw vk/tg chips).
let warnProduct = $state<CatalogProduct | null>(null); let warnProduct = $state<CatalogProduct | null>(null);
@@ -99,13 +102,13 @@
async function doBuy(p: CatalogProduct) { async function doBuy(p: CatalogProduct) {
warnProduct = null; warnProduct = null;
if (busy) return; if (busy) return;
busy = true; busyId = p.productId;
try { try {
wallet = await gateway.walletBuy(p.productId); wallet = await gateway.walletBuy(p.productId);
} catch (e) { } catch (e) {
handleError(e); handleError(e);
} finally { } finally {
busy = false; busyId = null;
} }
} }
@@ -114,7 +117,7 @@
// paying accepts the public offer (linked below the packs). // paying accepts the public offer (linked below the packs).
async function onOrder(p: CatalogProduct) { async function onOrder(p: CatalogProduct) {
if (busy) return; if (busy) return;
busy = true; busyId = p.productId;
try { try {
const order = await gateway.walletOrder(p.productId); const order = await gateway.walletOrder(p.productId);
if (context === 'vk') { if (context === 'vk') {
@@ -134,7 +137,7 @@
} catch (e) { } catch (e) {
handleError(e); handleError(e);
} finally { } finally {
busy = false; busyId = null;
} }
} }
</script> </script>
@@ -172,7 +175,9 @@
<div class="row product" data-testid="product" data-kind="value" data-pid={p.productId}> <div class="row product" data-testid="product" data-kind="value" data-pid={p.productId}>
<span class="name">{p.title}</span> <span class="name">{p.title}</span>
<span class="price">🪙 {p.chips}</span> <span class="price">🪙 {p.chips}</span>
<button class="buy" data-testid="buy" disabled={busy} onclick={() => onBuy(p)}>{t('wallet.buy')}</button> <button class="buy" class:working={busyId === p.productId} data-testid="buy" disabled={busy} onclick={() => onBuy(p)}
>{t('wallet.buy')}</button
>
</div> </div>
{/each} {/each}
@@ -186,6 +191,7 @@
<button <button
class="buy" class="buy"
class:blocked={purchaseBlocked} class:blocked={purchaseBlocked}
class:working={busyId === p.productId}
data-testid="buy-pack" data-testid="buy-pack"
disabled={busy} disabled={busy}
onclick={() => { onclick={() => {
@@ -271,10 +277,12 @@
cursor: pointer; cursor: pointer;
} }
.buy:disabled { .buy:disabled {
opacity: 0.5;
cursor: default; cursor: default;
} }
.buy.blocked { /* The dimmed look is scoped: the tapped button while its purchase is in flight (working), or a
blocked pack on VK iOS — never every buy button at once when one purchase is busy. */
.buy.blocked,
.buy.working {
opacity: 0.5; opacity: 0.5;
} }
.ios-note { .ios-note {