feat(payments): wallet screen with balances, benefits and storefront
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Failing after 12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Failing after 12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
Add the "Кошелёк" section to the settings hub: context-visible chip balances, active benefits (no-ads term/forever, hints) and a storefront of chip-priced values and money-priced chip packs. Guests have no wallet; the Google Play build hides the money purchases behind a RuStore stub; a web purchase that would draw VK/Telegram chips warns first. Add the catalog read path the storefront needs — a context-projected GET /api/v1/user/wallet/catalog (payments service + store, gateway op wallet.catalog, FBS Catalog/CatalogProduct/CatalogAtom, client decode) — plus the client leg for the existing wallet.get/buy ops. Value spends reuse the existing spend path; the chip-pack purchase (money order flow) arrives with payment intake, so its action is a disabled placeholder for now. Covered by Go unit (catalog projection) + integration (/wallet/catalog over Postgres), vitest (formatting, spendable selection, web-spend warning, GP flag, codec + gateway encode round-trips) and Playwright mock e2e (render, guest-hidden, GP stub, warning) on Chromium + WebKit.
This commit is contained in:
@@ -32,7 +32,7 @@ status — without re-deriving decisions.
|
||||
| E0 | Payments data foundation | 1 | DONE |
|
||||
| E1 | Trusted platform signal | 1 | DONE |
|
||||
| E2 | Currency + benefit core | 1 | DONE |
|
||||
| E3 | Wallet UI | 1 | TODO |
|
||||
| E3 | Wallet UI | 1 | DONE |
|
||||
| E4 | Durability (PITR) | 2 | TODO |
|
||||
| E5 | Payment intake | 2 | TODO |
|
||||
| E6 | Ads | 2 | TODO |
|
||||
@@ -375,43 +375,72 @@ refactors. The legacy flip is expand-contract: reads move first, DROP much later
|
||||
|
||||
## E3 — Wallet UI
|
||||
|
||||
**Status:** TODO · **Release 1** · depends on: E2 · mechanics: PAYMENTS §1, §7, §6, §13, §4.
|
||||
**Status:** DONE · **Release 1** · depends on: E2 · mechanics: PAYMENTS §1, §7, §6, §13, §4.
|
||||
|
||||
**Goal.** The user-facing "Кошелёк" (Wallet) section: balances + active benefits + the
|
||||
catalog storefront, honouring guest-hidden, GP-stub, and the web-spend warning.
|
||||
|
||||
**Backend + wire (catalog read — new).** E2 shipped `wallet.get` / `wallet.buy` (segments +
|
||||
benefits + a chip spend) but no way to *list* the catalog; the storefront needs one, so E3 adds a
|
||||
read path following the E2 shape:
|
||||
|
||||
- `payments.Service.Catalog(ctx, cxt)` + `store.loadCatalog` (`internal/payments/{catalog,
|
||||
store_catalog}.go`) read every **active** product with atoms and prices and project them to the
|
||||
context (`projectCatalog`, pure + unit-tested): a **value** (no `chips` atom) carries its CHIP
|
||||
price and shows everywhere; a **chip pack** (`chips` atom) carries the money price for the
|
||||
context method (`cxt.Kind`: vk→VOTE, telegram→XTR, direct→RUB) and shows only where that method
|
||||
is priced. Read **uncached** (the catalog is small and rarely edited — unlike the per-account
|
||||
balances/benefits the read cache fronts).
|
||||
- REST `GET /api/v1/user/wallet/catalog` (`handleWalletCatalog`, gated by `walletGate`) →
|
||||
`catalogDTO`; gateway op `wallet.catalog` (`transcode.go` + `encodeCatalog`), `backendclient.
|
||||
Catalog`; FBS `Catalog`/`CatalogProduct`/`CatalogAtom` (`pkg/fbs/scrabble.fbs`), client
|
||||
`decodeCatalog`.
|
||||
|
||||
**UI (`ui/`).**
|
||||
|
||||
- New `'wallet'` tab in `ui/src/screens/SettingsHub.svelte` — `SettingsTab` union, tab
|
||||
button **between Friends (:65) and About (:66)**, body branch in the `:42-51` switch,
|
||||
`'wallet'` route in `ui/src/lib/routeparse.ts` + `ui/src/App.svelte`. Reuse the hub's
|
||||
guest/offline gating.
|
||||
- `Wallet.svelte` screen: **minimal** — context-available chip balances + active benefits
|
||||
(no-ads until date, hints count). **No history feed** (PAYMENTS §11 — noise). Storefront:
|
||||
products from the configurable catalog, prices in chips (values) / per-method (chip packs).
|
||||
- New `'wallet'` tab in `ui/src/screens/SettingsHub.svelte` — `SettingsTab` union, tab button
|
||||
**between Friends and About** (guest-hidden like Friends, offline-disabled like Profile/Friends),
|
||||
body branch, `'wallet'` route in `ui/src/lib/routeparse.ts` + `ui/src/App.svelte`.
|
||||
- `Wallet.svelte` screen: context-available chip balances + active benefits (no-ads until date /
|
||||
forever, hints count). **No history feed** (PAYMENTS §11 — noise). Storefront: values priced in
|
||||
chips (buyable with `wallet.buy`), chip packs priced per method.
|
||||
- **Guest:** section hidden entirely (durable-only).
|
||||
- **GP build:** purchase CTA replaced by a stub ("install the RuStore build to make
|
||||
purchases"); rewarded + spending earned chips still work. Detect the GP native build.
|
||||
- **Web-spend warning:** before spending vk/tg chips in a web/native (direct) context, show
|
||||
an own `Modal` (not `showPopup` — that eats the user-activation gesture) warning the value
|
||||
will be web/native-only.
|
||||
- Extract all logic into `ui/src/lib/*` (unit-testable); keep `.svelte` thin.
|
||||
- **GP build:** the chip-pack purchases are hidden behind a RuStore stub; spending earned chips on
|
||||
values still works (PAYMENTS §13). Detected by a build-time flag `VITE_GP_BUILD`
|
||||
(`ui/src/lib/distribution.ts`), forcible under the mock e2e with `?gp`.
|
||||
- **Web-spend warning:** before a value spend that would draw vk/tg chips in a direct context, an
|
||||
own `Modal` (not `showPopup`) warns the benefit will be web/native-only. The context is inferred
|
||||
client-side (`executionContext` — VK/Telegram/direct); the server enforces the real gate on
|
||||
`wallet.buy` (fail-closed), so no wallet-DTO change was needed.
|
||||
- Logic extracted to `ui/src/lib/{wallet,distribution}.ts` (unit-testable); `.svelte` stays thin.
|
||||
|
||||
**Pack purchase is display-only in E3.** Buying a chip pack needs the money order flow, which is
|
||||
**E5**; here a pack card shows its price with a disabled **"Soon"** action. E5 replaces that with the
|
||||
launch/order CTA. Value spends are wired now (E2 `wallet.buy`), though a Release-1 durable account
|
||||
has 0 chips until funding exists (E5/E6), so a real value spend returns insufficient-funds until then.
|
||||
|
||||
**Tests.**
|
||||
|
||||
- unit (vitest): storefront/price formatting, context-available-segment selection, warning
|
||||
trigger condition.
|
||||
- UI (Playwright mock, Chromium+WebKit): Wallet renders between Friends/About; guest hides
|
||||
it; GP stub; warning modal on web vk/tg spend. Mock overlay must stay instant under
|
||||
`MODE==='mock'` (or it intercepts taps).
|
||||
- unit (Go): `projectCatalog` context matrix (value everywhere; pack per context method; misconfig
|
||||
omitted). unit (vitest): money/price formatting, spendable-segment selection, warning trigger,
|
||||
the GP flag; `decodeCatalog` codec round-trip (mock e2e bypasses the codec).
|
||||
- integration (`inttest`): `/wallet/catalog` returns active products with the context-correct price
|
||||
over Postgres; soft-deleted excluded.
|
||||
- UI (Playwright mock, Chromium+WebKit): Wallet renders between Friends/About; guest hides it
|
||||
(`?guest` seam); GP stub (`?gp`); warning modal on a vk-drawing web spend, no warning on a
|
||||
direct-covered spend. Mock overlay stays instant under `MODE==='mock'` (or it intercepts taps).
|
||||
|
||||
**Done-criteria.** Owner can review the Wallet on the deployed test contour (visual sign-off
|
||||
is on the contour, not local); balances/benefits/storefront correct per context; guest/GP/
|
||||
warning paths verified. Release 1 is now demonstrable end-to-end via `admin_grant`.
|
||||
**Done-criteria (met).** Balances/benefits/storefront render per context; guest/GP/warning paths
|
||||
verified by unit + integration + mock e2e on both engines. **A *populated* contour review depends
|
||||
on later stages** (no products until the E7 catalog editor, no chip funding until E5/E6, no grant
|
||||
UI until E7): on the contour E3 shows the correct **empty** wallet/storefront states; the populated
|
||||
UI is proven by the mock e2e. (This replaces the earlier "demonstrable end-to-end via `admin_grant`"
|
||||
line — the grant UI is E7.)
|
||||
|
||||
**Notes/risks.** No global `.btn`/`.ghost` in `ui/` — style per-component with scoped CSS +
|
||||
tokens (mirror NewGame `.invite` for a CTA). Svelte whitespace/`$state` naming gotchas
|
||||
apply. iOS WebView download/gesture caveats are irrelevant here (no file delivery).
|
||||
**Notes/risks.** No global `.btn`/`.ghost` in `ui/` — styled per-component with scoped CSS + tokens
|
||||
(mirror NewGame `.invite`). Product titles are single-language catalog data (E0 `product.title`),
|
||||
shown verbatim; currency/chip words are i18n, counts follow the app's label+number convention (no
|
||||
noun agreement). Svelte whitespace/`$state` naming gotchas apply.
|
||||
|
||||
---
|
||||
|
||||
@@ -466,7 +495,9 @@ receipts, and refunds.
|
||||
|
||||
- `POST /api/v1/user/wallet/order` → create `order(pending)` (account/platform/product/
|
||||
expected amount/origin), return the provider-specific launch payload with `order_id`
|
||||
threaded in (Robokassa `InvId` / TG `invoice_payload` / VK `item`).
|
||||
threaded in (Robokassa `InvId` / TG `invoice_payload` / VK `item`). The storefront's chip-pack
|
||||
card wires its purchase CTA to this here, **replacing the disabled "Soon" placeholder E3 left**
|
||||
(`ui/src/screens/Wallet.svelte`).
|
||||
- Robokassa + VK **public webhooks**: new edge routes (add to `deploy/caddy/Caddyfile`
|
||||
`@gateway` matcher — or they fall to the landing catch-all), signature/HMAC verified,
|
||||
proxied into a payments intake handler. Match by `order_id`, verify amount, credit
|
||||
|
||||
Reference in New Issue
Block a user