// 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; }