feat(ui): hide Telegram/VK link buttons on the native build

The native (Capacitor) sign-in surface is guest + email only: VK ID
web-login is a full-page redirect to id.vk.com that cannot return into the
WebView, and the Telegram Login Widget is unreliable there. Profile now
gates telegramLinkable/vkLinkable on !nativeShell (clientChannel android/
ios), hiding both LINK buttons on native; email and account management —
including an existing link's redirect-free UNLINK — stay. Native tg/vk
login (native SDKs / deep-link OAuth) is a separate later stage.

Covered by e2e/native.spec.ts (the reconcile test opens Profile and asserts
no Link Telegram / Link VK buttons, email present).
This commit is contained in:
Ilia Denisov
2026-07-12 18:09:11 +02:00
parent e077258567
commit 0eb72ba955
3 changed files with 46 additions and 19 deletions
+27 -17
View File
@@ -91,7 +91,10 @@ Kept current as parts land so a fresh session resumes without re-deriving. Verif
`clientver`/config/`connectsrv` tests green, `svelte-check` 0, `vitest` 591, e2e 232 (incl. `clientver`/config/`connectsrv` tests green, `svelte-check` 0, `vitest` 591, e2e 232 (incl.
`update.spec.ts`), client build clean. Silent reconciliation seam deferred to D (owner); in-app `update.spec.ts`), client build clean. Silent reconciliation seam deferred to D (owner); in-app
store-update SDK noted Out of scope. store-update SDK noted Out of scope.
- **D. Offline-first — 🚧 IN PROGRESS (D.1D.4 done; D.6 + the D-close verification remain).** - **D. Offline-first — ✅ CODE-COMPLETE & e2e-verified (D.1D.6 done, 2026-07-12).** Remaining: the on-device
emulator **airplane-mode smoke** (owner / stage G) and the **deferred local-game-visibility decision** (below).
D.5 (both offline modes render + play for the local guest) is verified by `e2e/native.spec.ts` (vs_ai move +
hotseat start).
- **D.1 + D.2 (foundations) — ✅ DONE & committed `bcd5a1d` (2026-07-12).** `ui/scripts/bundle-dicts.mjs` - **D.1 + D.2 (foundations) — ✅ DONE & committed `bcd5a1d` (2026-07-12).** `ui/scripts/bundle-dicts.mjs`
(release DAWGs → `dist/dict/<variant>@<version>.dawg`, keyed on `VITE_DICT_VERSION`, `OUT_DIR` override (release DAWGs → `dist/dict/<variant>@<version>.dawg`, keyed on `VITE_DICT_VERSION`, `OUT_DIR` override
for the e2e); the `dict/loader.ts` **bundled tier** (between IndexedDB and network, **native-gated** for the e2e); the `dict/loader.ts` **bundled tier** (between IndexedDB and network, **native-gated**
@@ -110,7 +113,11 @@ Kept current as parts land so a fresh session resumes without re-deriving. Verif
the dawgs into `dist-e2e/dict/`. `svelte-check` 0, `vitest` 591, web + native builds clean; e2e runs in CI. the dawgs into `dist-e2e/dict/`. `svelte-check` 0, `vitest` 591, web + native builds clean; e2e runs in CI.
See §D.3/§D.4 for the as-built corrections (shouldBootOffline **not** changed; `allowOffline` + See §D.3/§D.4 for the as-built corrections (shouldBootOffline **not** changed; `allowOffline` +
checkReachable-token findings; the `__native.reconcile` e2e hook). checkReachable-token findings; the `__native.reconcile` e2e hook).
- **D.6 (Profile tg/vk hide on native) — ▢ TODO** (owner deferred to a follow-up this session). - **D.6 (Profile tg/vk hide on native) — ✅ DONE & verified (2026-07-12).** `Profile.svelte` gates
`telegramLinkable`/`vkLinkable` on `!nativeShell` (`clientChannel()` android/ios), so the Telegram + VK
LINK buttons are hidden on the native build; email + account management (incl. any existing link's UNLINK,
a redirect-free gateway call) stay. Covered by the `e2e/native.spec.ts` reconcile test (Profile → no Link
Telegram / Link VK buttons, email present).
- **DEFERRED DECISION (owner-agreed 2026-07-12) — native local-game visibility.** The online lobby lists - **DEFERRED DECISION (owner-agreed 2026-07-12) — native local-game visibility.** The online lobby lists
only server games (`Lobby.svelte:42/54`), so a native guest's **device-local vs_ai/hotseat games hide from only server games (`Lobby.svelte:42/54`), so a native guest's **device-local vs_ai/hotseat games hide from
the lobby once reconciled online** (the pre-existing offline↔online split). For native this IS the primary the lobby once reconciled online** (the pre-existing offline↔online split). For native this IS the primary
@@ -420,16 +427,18 @@ purchase actions are absent; `pnpm check` + `pnpm test:unit` pass.
**Done when:** Go `clientver` + gate tests pass; `pnpm check`/`test:unit`/`test:e2e` pass; a local **Done when:** Go `clientver` + gate tests pass; `pnpm check`/`test:unit`/`test:e2e` pass; a local
gateway with `GATEWAY_MIN_CLIENT_VERSION=v99.0.0` shows the overlay, unset ⇒ unchanged. gateway with `GATEWAY_MIN_CLIENT_VERSION=v99.0.0` shows the overlay, unset ⇒ unchanged.
### D. Offline-first (bundled dicts + local guest + cold boot + reconciliation) — 🚧 IN PROGRESS ### D. Offline-first (bundled dicts + local guest + cold boot + reconciliation) — ✅ CODE-COMPLETE
Both offline modes already exist and are gated on `offlineMode.active`: local vs_ai and 2-4-player Both offline modes already exist and are gated on `offlineMode.active`: local vs_ai and 2-4-player
hotseat (pass-and-play with a host PIN referee) — see `ui/src/lib/localgame/source.hotseat.test.ts` hotseat (pass-and-play with a host PIN referee) — see `ui/src/lib/localgame/source.hotseat.test.ts`
and `ui/src/screens/NewGame.svelte:177-431`. This phase makes them reachable on a **cold first launch and `ui/src/screens/NewGame.svelte:177-431`. This phase makes them reachable on a **cold first launch
with no network**. with no network**.
**Status:** the additive foundations (D.1, D.2) are **done & committed (`bcd5a1d`)** and inert on the **Status:** D.1D.6 are **done & verified** (foundations committed `bcd5a1d`; boot + reconciliation +
web; the boot rewrite (D.3), reconciliation + the silent seam (D.4), the Profile soft-sign-in (D.6) and Profile committed on `feature/android-native`, 2026-07-12), with `e2e/native.spec.ts` green locally
the tests remain. **Verify every line ref below against current code** (they are as of 2026-07-12). (chromium + webkit) and in CI. Remaining for D: the on-device emulator airplane-mode smoke (owner /
stage G) and the deferred local-game-visibility decision (Progress §D / §G). **Verify every line ref
below against current code.**
**Decisions locked this session (owner-approved) — bake these before implementing:** **Decisions locked this session (owner-approved) — bake these before implementing:**
- **The blocking Login is bypassed on native only.** Web / PWA / Telegram / VK keep the current - **The blocking Login is bypassed on native only.** Web / PWA / Telegram / VK keep the current
@@ -514,17 +523,18 @@ the tests remain. **Verify every line ref below against current code** (they are
offline flows on `guest || offlineMode.active`; the local guest makes `offlineMode.active` true, so both offline flows on `guest || offlineMode.active`; the local guest makes `offlineMode.active` true, so both
"quick" (vs_ai) and "with friends" (hotseat) show. The vs_ai human seat already uses the local-guest id "quick" (vs_ai) and "with friends" (hotseat) show. The vs_ai human seat already uses the local-guest id
(D.2). (D.2).
6. **Soft registration = reuse the `Profile` screen — ▢ TODO.** 6. **Soft registration = reuse the `Profile` screen — ✅ DONE & verified (2026-07-12).** As built:
- Profile is already reachable for an **online** guest (guests keep the Profile tab - **Reaching Profile needed no change:** guests keep the Profile tab (`SettingsHub.svelte:27` only hides
`SettingsHub.svelte:27` only hides friends/wallet for guests; `:33` hides profile/friends/wallet in friends/wallet for guests; `:33` hides profile/friends/wallet in **offline** mode), so once the native
**offline** mode). So once the native guest is online and reconciled (D.4 clears the auto-offline), guest is online and reconciled (D.4 clears the auto-offline) Profile shows and email sign-in works.
Profile shows and email sign-in works. Offline you cannot register anyway (every method needs the Offline you cannot register anyway (every method needs the server), so the offline-hidden behaviour is
server), so the offline-hidden behaviour is correct — **no change is needed to reach Profile**, provided correct.
D.4 clears the auto-offline when online. - **Hid tg/vk on native:** `Profile.svelte` gates `telegramLinkable = loginWidgetAvailable() && !nativeShell`
- **Hide tg/vk on native:** `Profile.svelte` — the Telegram link button (`~line 485`, gated on and `vkLinkable = vkWebLinkAvailable() && !nativeShell` (`nativeShell = clientChannel()` android/ios), so
`telegramLinkable = loginWidgetAvailable()`, `:62`) and the VK link button (`~line 499`). Add a both LINK buttons are hidden on native while email + account management (and an existing link's UNLINK — a
`clientChannel()` android/ios check to hide both on the native build; keep email + account management. redirect-free `link.unlink` call) stay. Verified in `e2e/native.spec.ts` (the reconcile test opens Profile
(See the decision block above for why.) and asserts no Link Telegram / Link VK, email present; `loginWidgetAvailable`/`vkWebLinkAvailable` are true
under the mock, so the buttons show on web — the native count-0 proves the gate).
**Tests — ✅ DONE for D.3/D.4 (2026-07-12):** **Tests — ✅ DONE for D.3/D.4 (2026-07-12):**
- **Unit (vitest, node):** no NEW unit test was added for D.3/D.4 — the boot decision, reconciliation and - **Unit (vitest, node):** no NEW unit test was added for D.3/D.4 — the boot decision, reconciliation and
+11
View File
@@ -73,6 +73,17 @@ test.describe('native offline-first', () => {
await expect(page.locator('header.nav.offline')).toHaveCount(0); await expect(page.locator('header.nav.offline')).toHaveCount(0);
await expect(page.getByText('Ann', { exact: false }).first()).toBeVisible({ timeout: 15000 }); await expect(page.getByText('Ann', { exact: false }).first()).toBeVisible({ timeout: 15000 });
await expect(page.locator('button.tab').nth(1)).toBeEnabled(); await expect(page.locator('button.tab').nth(1)).toBeEnabled();
// The native sign-in surface is guest + email only: on Profile the Telegram + VK LINK buttons are
// hidden (their login widget / full-page redirect cannot return into the WebView), while email account
// management stays. loginWidgetAvailable/vkWebLinkAvailable are true under the mock, so on web these
// buttons DO show (social.spec links Telegram there) — a count of 0 here proves the native gate, not a
// merely-absent button.
await page.getByRole('button', { name: /Settings/ }).click();
await page.getByRole('button', { name: 'Profile', exact: true }).click();
await expect(page.getByRole('button', { name: /Link Telegram/i })).toHaveCount(0);
await expect(page.getByRole('button', { name: /Link VK/i })).toHaveCount(0);
await expect(page.getByText('you@example.com')).toBeVisible();
}); });
test('starts a 2-player hotseat game as an offline guest', async ({ page }) => { test('starts a 2-player hotseat game as an offline guest', async ({ page }) => {
+8 -2
View File
@@ -16,6 +16,7 @@
import { gateway } from '../lib/gateway'; import { gateway } from '../lib/gateway';
import { insideTelegram, loginWidgetAvailable, requestTelegramLogin } from '../lib/telegram'; import { insideTelegram, loginWidgetAvailable, requestTelegramLogin } from '../lib/telegram';
import { insideVK } from '../lib/vk'; import { insideVK } from '../lib/vk';
import { clientChannel } from '../lib/channel';
import { kickDictPreload } from '../lib/offline.svelte'; import { kickDictPreload } from '../lib/offline.svelte';
import { startVKLink, vkWebLinkAvailable } from '../lib/vkid'; import { startVKLink, vkWebLinkAvailable } from '../lib/vkid';
import { t } from '../lib/i18n/index.svelte'; import { t } from '../lib/i18n/index.svelte';
@@ -59,8 +60,13 @@
let deleting = $state<null | { method: 'email' | 'phrase' }>(null); let deleting = $state<null | { method: 'email' | 'phrase' }>(null);
let deleteCode = $state(''); let deleteCode = $state('');
let deletePhrase = $state(''); let deletePhrase = $state('');
const telegramLinkable = loginWidgetAvailable(); // The native build hides the Telegram + VK LINK buttons: VK ID web-login is a full-page redirect to
const vkLinkable = vkWebLinkAvailable(); // id.vk.com that cannot return into the Capacitor WebView, and the Telegram Login Widget is unreliable
// there — so guest + email is the native sign-in surface (native tg/vk login is a later stage). An
// existing link's UNLINK button stays (a pure gateway call, no redirect), as does all account management.
const nativeShell = clientChannel() === 'android' || clientChannel() === 'ios';
const telegramLinkable = loginWidgetAvailable() && !nativeShell;
const vkLinkable = vkWebLinkAvailable() && !nativeShell;
// Inside a host Mini App the current platform's own identity must not be managed from the // Inside a host Mini App the current platform's own identity must not be managed from the
// profile — unlinking the very platform you are signed in through is meaningless — so that // profile — unlinking the very platform you are signed in through is meaningless — so that
// provider's linked-account row is hidden here (the web / native builds still show both). // provider's linked-account row is hidden here (the web / native builds still show both).