4aa6174968
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
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.
79 lines
3.4 KiB
TypeScript
79 lines
3.4 KiB
TypeScript
// Wallet storefront logic, kept pure and out of the .svelte screen so it unit-tests in the node
|
|
// environment: money/price formatting, the spendable-segment selection, and the web-spend warning
|
|
// trigger. The server owns the real store-compliance gate; these helpers drive display only.
|
|
|
|
import type { Wallet, WalletSegment } from './model';
|
|
import { insideVK } from './vk';
|
|
import { insideTelegram } from './telegram';
|
|
|
|
/** The client's execution context for the wallet UI, mirroring the server's platform kind. */
|
|
export type SpendContext = 'vk' | 'telegram' | 'direct';
|
|
|
|
/**
|
|
* executionContext reports the client's best-effort context: a VK Mini App, a Telegram Mini App,
|
|
* else a web/native (direct) session. It only drives the display-only web-spend warning; the
|
|
* server enforces the real gate on every spend (fail-closed), never trusting this.
|
|
*/
|
|
export function executionContext(): SpendContext {
|
|
if (insideVK()) return 'vk';
|
|
if (insideTelegram()) return 'telegram';
|
|
return 'direct';
|
|
}
|
|
|
|
/**
|
|
* majorAmount converts a money amount in a currency's minor units to its major unit — the rouble
|
|
* has 100 kopecks; Votes and Stars are whole units (scale 1).
|
|
*/
|
|
export function majorAmount(minor: number, currency: string): number {
|
|
return minor / (currency === 'RUB' ? 100 : 1);
|
|
}
|
|
|
|
/**
|
|
* formatAmount renders a money price in major units: two fractional digits for the rouble, a whole
|
|
* number for the whole-unit currencies (Votes / Stars). The currency label is added by the caller
|
|
* (it is localized), so this stays a pure number formatter.
|
|
*/
|
|
export function formatAmount(minor: number, currency: string): string {
|
|
return currency === 'RUB' ? (minor / 100).toFixed(2) : String(minor);
|
|
}
|
|
|
|
/**
|
|
* spendableChips totals the chips the player can actually spend in the current context — the sum of
|
|
* the segments the server marked spendable (zero for a VK-iOS-frozen or untrusted wallet, where no
|
|
* segment is spendable).
|
|
*/
|
|
export function spendableChips(w: Wallet): number {
|
|
return w.segments.reduce((sum, s) => sum + (s.spendable ? s.chips : 0), 0);
|
|
}
|
|
|
|
// drawPriority is the segment draw order on the web, mirroring the backend spend: the home direct
|
|
// segment first, then the store-funded segments.
|
|
const drawPriority: readonly string[] = ['direct', 'vk', 'telegram'];
|
|
|
|
/**
|
|
* needsWebSpendWarning reports whether buying a chip-priced value in the current context would draw
|
|
* store-funded (vk / telegram) chips — the case the UI must warn about, because the benefit it buys
|
|
* (origin=direct) is then usable on the web/native only, by the store-compliance rule. It fires only
|
|
* in a direct context and only when the priority draw (direct→vk→tg) actually reaches a store
|
|
* segment to cover the price; a spend that the direct segment covers alone, or a spend in a VK/
|
|
* Telegram context (which stays within the same store segment), never warns.
|
|
*/
|
|
export function needsWebSpendWarning(
|
|
context: SpendContext,
|
|
segments: WalletSegment[],
|
|
chipPrice: number,
|
|
): boolean {
|
|
if (context !== 'direct') return false;
|
|
let remaining = chipPrice;
|
|
let reachesStore = false;
|
|
for (const src of drawPriority) {
|
|
if (remaining <= 0) break;
|
|
const seg = segments.find((s) => s.source === src && s.spendable);
|
|
if (!seg || seg.chips <= 0) continue;
|
|
const take = Math.min(seg.chips, remaining);
|
|
if (src !== 'direct' && take > 0) reachesStore = true;
|
|
remaining -= take;
|
|
}
|
|
return remaining <= 0 && reachesStore;
|
|
}
|