feat(ui): wallet storefront redesign — split buy/spend, compact balance, exchange confirm
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m55s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m55s
Reworks the Wallet screen to be more compact and to separate the two flows, and fixes a
reported bug.
Bug: buying a value with too few chips showed the generic "Something went wrong" toast — the
backend's `insufficient_chips` (409) code was propagated correctly but had no i18n mapping, so
`errorKey` fell back to `error.generic`. Add `error.insufficient_chips` / `error.product_not_found`.
Also disable a value's Exchange button up front when the spendable balance cannot cover it (the
server stays authoritative).
Redesign:
- a compact **balance** row leads the screen — the context's own chips (🪙 N) then any linked
other-platform chips behind that platform's logo (monospaced digits); inside a VK/TG store only
that store's own segment shows;
- the benefits section is renamed **Active**, moved above the store, shows one inline line
(hints + ad-free) and hides when nothing is active;
- the **store** splits by a two-way toggle into **Buy chips** (money packs + the watch-an-ad row at
the top) and **Spend chips** (values, exchanged for chips);
- a value's action reads **Exchange** and opens a single confirmation dialog (chip spends are
instant, with no provider window to confirm them); the existing cross-platform store-compliance
warning folds into that same dialog when the spend would draw VK/TG chips.
No payments-model or wire change; the money→chips→values model is unchanged (verified: money buys
only chip packs, values are chips-only — "no-ads for money" is structurally impossible).
Tests: wallet e2e updated for the new layout (chromium + webkit green). Docs: FUNCTIONAL (+_ru)
wallet story rewritten. App bundle budget 126 → 127 KB (always-loaded settings hub).
This commit is contained in:
+41
-24
@@ -1,9 +1,10 @@
|
||||
import { expect, test, type Page } from './fixtures';
|
||||
|
||||
// The Wallet section against the mock transport (no backend). The mock seeds a web/native (direct)
|
||||
// account holding a direct + vk chip segment and a small catalog (two chip-priced values and one
|
||||
// rouble-priced chip pack — see lib/mock/client.ts), so the storefront, the store-compliance
|
||||
// warning and the Google Play stub are all exercisable without a backend.
|
||||
// account holding a direct (120) + vk (400) chip segment and a small catalog (two chip-priced values
|
||||
// and one rouble-priced chip pack — see lib/mock/client.ts), so the compact balance, the split
|
||||
// storefront, the exchange-confirm dialog (with its store-compliance warning) and the Google Play
|
||||
// stub are all exercisable without a backend.
|
||||
|
||||
async function loginLobby(page: Page): Promise<void> {
|
||||
await page.goto('/');
|
||||
@@ -22,24 +23,33 @@ async function openWallet(page: Page): Promise<void> {
|
||||
await expect(page.getByTestId('wallet')).toBeVisible();
|
||||
}
|
||||
|
||||
test('wallet: balances, benefits and the storefront render for a durable account', async ({ page }) => {
|
||||
test('wallet: balance, active benefits and the split storefront render for a durable account', async ({ page }) => {
|
||||
await loginLobby(page);
|
||||
await openWallet(page);
|
||||
|
||||
// Balances: the seeded direct ("Web") and vk segments.
|
||||
await expect(page.getByRole('heading', { name: 'Balance' })).toBeVisible();
|
||||
await expect(page.getByText('Web', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('VK', { exact: true })).toBeVisible();
|
||||
// The compact balance leads with the context's own (direct/"Web") chips, then the linked vk
|
||||
// segment behind its logo.
|
||||
const balance = page.getByTestId('balance');
|
||||
await expect(balance).toContainText('120');
|
||||
await expect(balance).toContainText('400');
|
||||
await expect(balance.locator('.plogo')).toHaveCount(1); // the one linked (vk) platform
|
||||
|
||||
// Benefits + the storefront: two values priced in chips, one pack priced in roubles.
|
||||
await expect(page.getByRole('heading', { name: 'Benefits' })).toBeVisible();
|
||||
// The active benefits (the seeded 5 hints) sit above the store.
|
||||
await expect(page.getByRole('heading', { name: 'Active' })).toBeVisible();
|
||||
await expect(page.getByTestId('active')).toContainText('Hints: 5');
|
||||
await expect(page.getByRole('heading', { name: 'Store' })).toBeVisible();
|
||||
await expect(page.getByTestId('product')).toHaveCount(3);
|
||||
const pack = page.locator('[data-kind="pack"]');
|
||||
await expect(pack).toContainText('₽');
|
||||
// The pack purchase is wired to money intake: an enabled Buy action, with the public-offer link.
|
||||
await expect(pack.getByTestId('buy-pack')).toBeEnabled();
|
||||
|
||||
// Default tab — Buy chips: one rouble-priced pack + the public offer, and no values.
|
||||
await expect(page.locator('[data-kind="pack"]')).toHaveCount(1);
|
||||
await expect(page.locator('[data-kind="pack"]')).toContainText('₽');
|
||||
await expect(page.locator('[data-kind="pack"]').getByTestId('buy-pack')).toBeEnabled();
|
||||
await expect(page.getByTestId('offer')).toContainText('Public offer');
|
||||
await expect(page.locator('[data-kind="value"]')).toHaveCount(0);
|
||||
|
||||
// Spend chips tab: the two chip-priced values, exchanged (not bought) with the "Exchange" button.
|
||||
await page.getByTestId('tab-spend').click();
|
||||
await expect(page.locator('[data-kind="value"]')).toHaveCount(2);
|
||||
await expect(page.locator('[data-kind="value"]').first().getByTestId('buy')).toHaveText('Exchange');
|
||||
});
|
||||
|
||||
test('wallet: buying a chip pack opens the provider payment page', async ({ page }) => {
|
||||
@@ -87,32 +97,39 @@ test('wallet: the Google Play build hides the money purchases behind the RuStore
|
||||
await expect(page.getByText('Your turn')).toBeVisible();
|
||||
await openWallet(page);
|
||||
|
||||
// The chip pack is gone; the stub points at the RuStore build. Spending earned chips still works.
|
||||
// Buy chips: the chip pack is gone; the stub points at the RuStore build.
|
||||
await expect(page.getByTestId('gp-stub')).toBeVisible();
|
||||
await expect(page.locator('[data-kind="pack"]')).toHaveCount(0);
|
||||
// Spending earned chips still works — the values live in the Spend tab.
|
||||
await page.getByTestId('tab-spend').click();
|
||||
await expect(page.locator('[data-kind="value"]').first().getByTestId('buy')).toBeVisible();
|
||||
});
|
||||
|
||||
test('wallet: a web spend that would draw store chips warns first, then completes', async ({ page }) => {
|
||||
test('wallet: a web spend that would draw store chips confirms with a warning, then completes', async ({ page }) => {
|
||||
await loginLobby(page);
|
||||
await openWallet(page);
|
||||
await page.getByTestId('tab-spend').click();
|
||||
|
||||
// The 300-chip value drains direct (120) then reaches into vk (180) → a store segment → warn.
|
||||
// The 300-chip value drains direct (120) then reaches into vk (180) → a store segment → the confirm
|
||||
// dialog carries the cross-platform warning.
|
||||
await page.locator('[data-pid="val-noads-30"]').getByTestId('buy').click();
|
||||
await expect(page.getByTestId('warn')).toBeVisible();
|
||||
await page.getByTestId('warn-confirm').click();
|
||||
await expect(page.getByTestId('warn')).toBeHidden();
|
||||
// The spend applied: the no-ads benefit now shows an end date.
|
||||
await page.getByTestId('spend-confirm').click();
|
||||
// The spend applied: the no-ads benefit now shows an end date in the Active section.
|
||||
await expect(page.getByTestId('wallet')).toContainText('No ads until');
|
||||
});
|
||||
|
||||
test('wallet: a spend the direct segment covers alone does not warn', async ({ page }) => {
|
||||
test('wallet: a spend the direct segment covers alone confirms without a warning', async ({ page }) => {
|
||||
await loginLobby(page);
|
||||
await openWallet(page);
|
||||
await page.getByTestId('tab-spend').click();
|
||||
|
||||
// The 100-chip value is covered by the direct segment (120) alone → no store draw, no warning.
|
||||
// The 100-chip value is covered by the direct segment (120) alone → the confirm dialog shows, but
|
||||
// with no cross-platform warning.
|
||||
await page.locator('[data-pid="val-hints-50"]').getByTestId('buy').click();
|
||||
await expect(page.getByTestId('spend-body')).toBeVisible();
|
||||
await expect(page.getByTestId('warn')).toBeHidden();
|
||||
await page.getByTestId('spend-confirm').click();
|
||||
// The hints benefit rose from 5 to 55.
|
||||
await expect(page.getByTestId('wallet')).toContainText('Hints 55');
|
||||
await expect(page.getByTestId('wallet')).toContainText('Hints: 55');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user