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:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { isGooglePlayBuild } from './distribution';
|
||||
import { afterEach, describe, it, expect, vi } from 'vitest';
|
||||
import { isGooglePlayBuild, purchasesHidden } from './distribution';
|
||||
|
||||
describe('isGooglePlayBuild', () => {
|
||||
it('is false by default (no VITE_GP_BUILD flag, not the mock ?gp force)', () => {
|
||||
@@ -8,3 +8,21 @@ describe('isGooglePlayBuild', () => {
|
||||
expect(isGooglePlayBuild()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('purchasesHidden', () => {
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
|
||||
it('is false by default (the build sells normally)', () => {
|
||||
expect(purchasesHidden()).toBe(false);
|
||||
});
|
||||
|
||||
it('is true for the native MVP that defers billing (VITE_PAYMENTS_DISABLED)', () => {
|
||||
vi.stubEnv('VITE_PAYMENTS_DISABLED', '1');
|
||||
expect(purchasesHidden()).toBe(true);
|
||||
});
|
||||
|
||||
it('is true for the Google Play build (folds isGooglePlayBuild)', () => {
|
||||
vi.stubEnv('VITE_GP_BUILD', '1');
|
||||
expect(purchasesHidden()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Distribution channel of the native build. Google Play forbids selling in-app currency through
|
||||
// an external gate, so the Google Play build hides the purchase actions and shows a stub pointing
|
||||
// the user to the RuStore build; every other build (web, VK, Telegram, RuStore native) sells
|
||||
// normally. The channel is a build-time flag the Google Play build sets, not a runtime guess.
|
||||
// Distribution flags for the native builds. Two independent build-time flags hide the in-app-currency
|
||||
// purchase actions: VITE_GP_BUILD marks the Google Play build (Google forbids selling in-app currency
|
||||
// through an external gate — it hides the actions and shows a stub pointing at the RuStore build), and
|
||||
// VITE_PAYMENTS_DISABLED marks the thin native MVP that defers store billing (it hides the actions with
|
||||
// no store pointer). Every other build (web, VK, Telegram, a billing-enabled RuStore native build) sells
|
||||
// normally. purchasesHidden() folds both; isGooglePlayBuild() stays separate for the RuStore-pointer stub.
|
||||
|
||||
/**
|
||||
* isGooglePlayBuild reports whether this is the Google Play native build, where in-app-currency
|
||||
@@ -19,3 +21,22 @@ export function isGooglePlayBuild(): boolean {
|
||||
}
|
||||
return (import.meta.env as Record<string, string | undefined>).VITE_GP_BUILD === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* purchasesHidden reports whether this build hides the in-app-currency purchase actions. It is true
|
||||
* for the Google Play build (external-payment policy — see isGooglePlayBuild) and for the thin native
|
||||
* MVP that defers store billing (the build-time flag VITE_PAYMENTS_DISABLED set to "1"). Every other
|
||||
* build sells normally. Under the mock e2e it can be forced on with a `?nopay` query parameter so the
|
||||
* hidden-purchases state is exercisable without a separate build.
|
||||
*/
|
||||
export function purchasesHidden(): boolean {
|
||||
if (isGooglePlayBuild()) return true;
|
||||
if (
|
||||
import.meta.env.MODE === 'mock' &&
|
||||
typeof window !== 'undefined' &&
|
||||
new URLSearchParams(window.location.search).has('nopay')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return (import.meta.env as Record<string, string | undefined>).VITE_PAYMENTS_DISABLED === '1';
|
||||
}
|
||||
|
||||
@@ -252,6 +252,7 @@ export const en = {
|
||||
'wallet.iosBlockedLink': 'other version',
|
||||
'wallet.iosBlockedPost': ' of the game.',
|
||||
'wallet.gpStub': 'To buy chips, install the RuStore build.',
|
||||
'wallet.purchasesSoon': 'Purchases will be available in a future update.',
|
||||
'wallet.cur.RUB': '₽',
|
||||
'wallet.cur.VOTE': 'votes',
|
||||
'wallet.cur.XTR': '⭐',
|
||||
|
||||
@@ -252,6 +252,7 @@ export const ru: Record<MessageKey, string> = {
|
||||
'wallet.iosBlockedLink': 'другой версией',
|
||||
'wallet.iosBlockedPost': ' игры.',
|
||||
'wallet.gpStub': 'Чтобы покупать фишки, установите версию из RuStore.',
|
||||
'wallet.purchasesSoon': 'Покупки появятся в одном из следующих обновлений.',
|
||||
'wallet.cur.RUB': '₽',
|
||||
'wallet.cur.VOTE': 'голосов',
|
||||
'wallet.cur.XTR': '⭐',
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { gatewayOrigin } from './origin';
|
||||
|
||||
describe('gatewayOrigin', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('returns the configured VITE_GATEWAY_URL (the packaged native build)', () => {
|
||||
vi.stubEnv('VITE_GATEWAY_URL', 'https://erudit-game.ru');
|
||||
// Even with a page origin present, the configured gateway wins (native loads from a local origin).
|
||||
vi.stubGlobal('location', { origin: 'https://localhost' });
|
||||
expect(gatewayOrigin()).toBe('https://erudit-game.ru');
|
||||
});
|
||||
|
||||
it('falls back to the page origin when VITE_GATEWAY_URL is unset (web/mock)', () => {
|
||||
vi.stubEnv('VITE_GATEWAY_URL', '');
|
||||
vi.stubGlobal('location', { origin: 'https://erudit-game.ru' });
|
||||
expect(gatewayOrigin()).toBe('https://erudit-game.ru');
|
||||
});
|
||||
|
||||
it('returns an empty string with neither a configured gateway nor a location (server-side)', () => {
|
||||
vi.stubEnv('VITE_GATEWAY_URL', '');
|
||||
// No location stub — location is undefined in the node test env, so the path degrades to relative.
|
||||
expect(gatewayOrigin()).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
// The absolute origin the client talks to. A packaged native build (Capacitor) loads the SPA from
|
||||
// its own local origin (https://localhost / file://), so an absolute URL built from location.origin
|
||||
// would point at the app itself, not the gateway. The native build therefore sets VITE_GATEWAY_URL
|
||||
// to the real gateway; web/mock leave it empty and fall back to the page origin. Centralised so every
|
||||
// absolute-URL construction resolves identically. transport.ts computes the same fallback inline for
|
||||
// the Connect base URL and is intentionally left untouched.
|
||||
|
||||
/**
|
||||
* gatewayOrigin returns the absolute origin every client build talks to: the configured
|
||||
* VITE_GATEWAY_URL when set (the native build), otherwise the current page origin. It returns an
|
||||
* empty string when there is no location (server-side), so callers appending a path degrade to a
|
||||
* relative URL.
|
||||
*/
|
||||
export function gatewayOrigin(): string {
|
||||
const configured = import.meta.env.VITE_GATEWAY_URL ?? '';
|
||||
return configured || (typeof location !== 'undefined' ? location.origin : '');
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
import { isIosSafari, isStandalone, resolveInstallMode, type InstallMode } from './pwa';
|
||||
import { insideTelegram } from './telegram';
|
||||
import { insideVK } from './vk';
|
||||
import { clientChannel } from './channel';
|
||||
|
||||
/** inMiniApp reports whether the app runs inside a Telegram or VK Mini App webview, where a web
|
||||
* install makes no sense (the platform manages its own launcher). Kept here, out of the pure
|
||||
@@ -79,12 +80,16 @@ export async function promptInstall(): Promise<void> {
|
||||
* registerServiceWorker registers the app-shell service worker (built from ui/src/sw.ts to
|
||||
* dist/sw.js by vite-plugin-pwa): it satisfies Chromium's installability requirement and precaches
|
||||
* the shell + assets so an installed PWA cold-launches offline. Web-only: skipped in the mock build
|
||||
* (a real worker would perturb the Playwright run) and inside a Telegram/VK Mini App. Best-effort —
|
||||
* a failed registration only means the app is not installable and cannot launch offline.
|
||||
* (a real worker would perturb the Playwright run), inside a Telegram/VK Mini App, and inside the
|
||||
* Capacitor native shell (the bundle is already local, and a worker would risk serving stale assets
|
||||
* across store updates). Best-effort — a failed registration only means the app is not installable
|
||||
* and cannot launch offline.
|
||||
*/
|
||||
export function registerServiceWorker(): void {
|
||||
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return;
|
||||
if (import.meta.env.MODE === 'mock') return;
|
||||
if (inMiniApp()) return;
|
||||
const ch = clientChannel();
|
||||
if (ch === 'android' || ch === 'ios') return;
|
||||
void navigator.serviceWorker.register('sw.js').catch(() => {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user