feat(android): resolve gateway origin on native, skip SW, hide MVP purchases

Make the web SPA behave correctly inside the Capacitor native shell, where the
bundle loads from a local origin:

- New lib/origin.ts gatewayOrigin(): absolute URLs resolve to VITE_GATEWAY_URL on
  native (the finished-game export/share URL in Game.svelte and the Wallet
  site-root link), falling back to the page origin on web. transport.ts already
  resolved via VITE_GATEWAY_URL and is left as-is.
- Skip the PWA service worker on the native channel (the assets are already local;
  a worker would risk serving stale content across store updates).
- Hide the money-purchase UI in the MVP: new distribution.purchasesHidden() folds
  VITE_PAYMENTS_DISABLED and the Google Play flag. The Wallet "buy" tab shows a
  neutral, pointer-free note (wallet.purchasesSoon) for the RuStore MVP and keeps
  the RuStore stub Google-Play-only. A ?nopay mock force mirrors ?gp for the e2e.
- Native env types in vite-env.d.ts.

Client-only, contour-safe: no wire/proto/schema change; web/VK/Telegram unchanged.
Tests: origin + purchasesHidden unit tests, a ?nopay wallet e2e. svelte-check
clean, vitest green, web + native vite build clean.
This commit is contained in:
Ilia Denisov
2026-07-12 14:58:35 +02:00
parent aaf2825260
commit de003e862a
13 changed files with 163 additions and 20 deletions
+15 -6
View File
@@ -6,8 +6,9 @@
import { normalizeSubtype } from '../lib/platform';
import { t, i18n, type MessageKey } from '../lib/i18n/index.svelte';
import { executionContext, formatAmount, needsWebSpendWarning, spendableChips, type SpendContext } from '../lib/wallet';
import { isGooglePlayBuild } from '../lib/distribution';
import { isGooglePlayBuild, purchasesHidden } from '../lib/distribution';
import { onExternalLinkClick, openExternalUrl } from '../lib/links';
import { gatewayOrigin } from '../lib/origin';
import { vkPlatform, vkShowOrderBox } from '../lib/vk';
import { telegramOpenInvoice } from '../lib/telegram';
import { rewardedReady, showRewarded } from '../lib/ads';
@@ -16,8 +17,9 @@
// The Wallet section: a compact chip balance, the active benefits, and the storefront. The store
// splits into "buy chips" (chip packs, funded with money — a provider redirect) and "spend chips"
// (values bought with chips — an instant exchange). On the Google Play build the money purchases
// are hidden behind a RuStore stub (store policy), while spending already-earned chips still works.
// (values bought with chips — an instant exchange). When purchases are hidden — the Google Play
// build (behind a RuStore stub, store policy) or the native MVP that defers billing (a neutral
// note) — the money purchases disappear while spending already-earned chips still works.
let wallet = $state<Wallet | null>(null);
let catalog = $state<Catalog | null>(null);
@@ -38,13 +40,16 @@
);
const gpBuild = isGooglePlayBuild();
// Money purchases are hidden in the Google Play build (policy) and in the native MVP that defers
// billing; the RuStore-pointer stub shows only for the former (gpBuild).
const noPurchases = purchasesHidden();
// Money purchases are not permitted inside the VK iOS app (Apple ToS); the pack CTA is shown
// muted and taps explain why, rather than hiding the storefront. VK's device family is not on the
// client platformSubtype (server-derived) — read it from the signed vk_platform launch param.
const purchaseBlocked = context === 'vk' && normalizeSubtype(vkPlatform()) === 'ios';
// The "other version" link points at this deployment's own site root (the landing lists every
// platform), so it follows prod vs the contour without a hardcoded host.
const siteRoot = typeof location !== 'undefined' ? `${location.origin}/` : '/';
const siteRoot = `${gatewayOrigin()}/`;
const values = $derived(catalog?.products.filter((p) => p.kind === 'value') ?? []);
const packs = $derived(catalog?.products.filter((p) => p.kind === 'pack') ?? []);
@@ -237,8 +242,12 @@
>
</div>
{/if}
{#if gpBuild}
<p class="stub" data-testid="gp-stub">{t('wallet.gpStub')}</p>
{#if noPurchases}
{#if gpBuild}
<p class="stub" data-testid="gp-stub">{t('wallet.gpStub')}</p>
{:else}
<p class="stub" data-testid="purchases-hidden">{t('wallet.purchasesSoon')}</p>
{/if}
{:else}
{#each packs as p (p.productId)}
<div class="row product" data-testid="product" data-kind="pack" data-pid={p.productId}>