Compare commits

..

17 Commits

Author SHA1 Message Date
developer 45957bdcd6 Merge pull request 'Release: promote development → master (old Android WebView support + unsupported-engine telemetry)' (#178) from development into master 2026-07-04 21:23:29 +00:00
developer 829e29a726 Merge pull request 'Release v1.8.0: prod build fix (re-promote)' (#175) from development into master 2026-07-03 21:48:13 +00:00
developer 399508f2f0 Merge pull request 'Release v1.8.0: promote development → master' (#173) from development into master 2026-07-03 21:31:25 +00:00
developer 3a18e683ca Merge pull request 'release: VK Mini App + landscape UI + dict v1.3.1 seed (development→master)' (#144) from development into master 2026-06-30 05:37:43 +00:00
developer 93d086a8a3 Merge pull request 'release: v1.7.0 — Telegram Mini App embedding enhancements' (#138) from development into master 2026-06-24 11:49:44 +00:00
developer 8fe1bdba6b Merge pull request 'release: v1.6.0 — promo deep-link seeds EN variant (+ UI nits)' (#135) from development into master 2026-06-23 21:02:19 +00:00
developer 7923b3cc09 Merge pull request 'release v1.5.1: support-relay card + topic-reopen fixes' (#133) from development into master 2026-06-23 16:54:01 +00:00
developer 4891216749 Merge pull request 'release v1.5.0: Telegram bot support relay' (#131) from development into master 2026-06-23 16:16:04 +00:00
developer f1b8769c89 Merge pull request 'release: v1.4.1 — Telegram nav (windowed, own back button, debug panel)' (#129) from development into master 2026-06-23 13:27:31 +00:00
developer b6f28a2423 Merge pull request 'release: v1.4.0 — Telegram launch diagnostic + dynamic SDK load' (#127) from development into master 2026-06-23 08:40:09 +00:00
developer e32ee9ce68 Merge pull request 'Release: development → master' (#125) from development into master 2026-06-22 22:36:42 +00:00
developer dc946a1faf Merge pull request 'release v1.2.2: edge HTTP/3 stall fix + db-size dashboard threshold' (#121) from development into master 2026-06-22 19:50:58 +00:00
developer 384bd143d0 Merge pull request 'Promote development → master: banner tip set + banner/push language fix' (#114) from development into master 2026-06-22 18:28:00 +00:00
developer c5d22fceca Merge pull request 'Promote development → master: Erudit blank star + dictionary v1.3.0 pin' (#111) from development into master 2026-06-22 13:12:01 +00:00
developer deaa7a29c5 Merge pull request 'Promote development → master (docs finalize + UI tweaks + Telegram name fallback)' (#108) from development into master 2026-06-22 07:27:40 +00:00
developer 24017bcb7f Merge pull request 'Promote development → master (deploy v2: versioning + visible jobs + rollback)' (#106) from development into master 2026-06-22 06:01:03 +00:00
developer 2c4f4b10dc Merge pull request 'Promote development → master (initial production release: pre-release line + Stage 18)' (#104) from development into master 2026-06-22 05:05:48 +00:00
7 changed files with 8 additions and 90 deletions
+1 -4
View File
@@ -983,10 +983,7 @@ and the open client re-fetches `profile.get` to show or hide the banner in place
edits take effect on the next `profile.get` (open/reconnect/foreground), not mid-session. The same edits take effect on the next `profile.get` (open/reconnect/foreground), not mid-session. The same
mechanism carries a **`profile`** sub-kind — a payload-free re-fetch signal emitted when a viewer's mechanism carries a **`profile`** sub-kind — a payload-free re-fetch signal emitted when a viewer's
own account changed out of band (an email confirmed through the one-tap deeplink opened in another own account changed out of band (an email confirmed through the one-tap deeplink opened in another
browser), so an open in-app session reflects it at once. The live stream is single-shot with no browser), so an open in-app session reflects it at once.
replay, so a **backgrounded Mini App** — suspended while the user is in their mail app tapping the
link — misses the event; while an add-email confirmation is pending the client therefore also
**polls `profile.get`** (and on foreground regain) as a fallback until the address lands.
> A single `app.load` bootstrap aggregator (collapsing `profile.get` + lobby + badge fetches into > A single `app.load` bootstrap aggregator (collapsing `profile.get` + lobby + badge fetches into
> one round-trip) was **considered and deferred**: client↔gateway is HTTP/2 (h2c), so the bootstrap > one round-trip) was **considered and deferred**: client↔gateway is HTTP/2 (h2c), so the bootstrap
+1 -3
View File
@@ -109,9 +109,7 @@ two accounts share a game still in progress.
The profile lists the account's **sign-in methods**. On the web a player can add The profile lists the account's **sign-in methods**. On the web a player can add
Telegram (a login-widget popup) or VK (VK ID web login — a redirect to VK's sign-in and Telegram (a login-widget popup) or VK (VK ID web login — a redirect to VK's sign-in and
back); inside a Mini App the host platform is already linked, and its own sign-in-method back); inside a Mini App the host platform is already linked. Linking a provider that
row is hidden — the platform the player is currently signed in through cannot be unlinked
from there, only the other provider's row is shown. Linking a provider that
already belongs to another account offers the same irreversible **merge** as email already belongs to another account offers the same irreversible **merge** as email
linking. A linked provider can be **unlinked** — except the last linking. A linked provider can be **unlinked** — except the last
remaining sign-in method, which is refused so the account stays reachable. **Email is remaining sign-in method, which is refused so the account stays reachable. **Email is
+1 -3
View File
@@ -113,9 +113,7 @@ Telegram держит **единого бота**: все игроки поль
В профиле перечислены **способы входа** аккаунта. В вебе игрок может добавить В профиле перечислены **способы входа** аккаунта. В вебе игрок может добавить
Telegram (попап логин-виджета) или VK (веб-вход VK ID — редирект на страницу входа VK и Telegram (попап логин-виджета) или VK (веб-вход VK ID — редирект на страницу входа VK и
обратно); внутри Mini App платформа-хозяин уже привязана, и её собственная плашка способа обратно); внутри Mini App платформа-хозяин уже привязана. Привязка провайдера, уже
входа скрыта — платформу, через которую игрок сейчас вошёл, отсюда отвязать нельзя,
показывается только плашка другого провайдера. Привязка провайдера, уже
принадлежащего другому аккаунту, предлагает то же необратимое **слияние**, что и привязка принадлежащего другому аккаунту, предлагает то же необратимое **слияние**, что и привязка
email. Привязанного провайдера можно **отвязать** — кроме последнего email. Привязанного провайдера можно **отвязать** — кроме последнего
оставшегося способа входа: он не отвязывается, чтобы аккаунт оставался достижимым. оставшегося способа входа: он не отвязывается, чтобы аккаунт оставался достижимым.
-30
View File
@@ -327,36 +327,6 @@ test('change email: a free address replaces the current one', async ({ page }) =
await expect(page.getByText('you@example.com')).toHaveCount(0); await expect(page.getByText('you@example.com')).toHaveCount(0);
}); });
// The add-email confirmation can complete out of band: the recipient taps the one-tap link in the
// email, which confirms in another browser/session. A backgrounded Mini App misses the live
// 'profile' event (the stream is single-shot, no replay), so the open code form falls back to
// polling the profile until the address lands. The mock attaches the email WITHOUT emitting an
// event, so only the poll can surface it.
test('add email: an out-of-band confirmation surfaces on the open form via polling', async ({ page }) => {
await loginLobby(page);
await openProfile(page);
// Drop the seeded email so the sign-in section offers the add-email flow.
await page.evaluate(() => (window as unknown as { __mock: { clearEmail(): void } }).__mock.clearEmail());
const emailInput = page.locator('.accounts input[type="email"]');
await expect(emailInput).toBeVisible();
await emailInput.fill('linked@example.com');
await page.getByRole('button', { name: 'Send code' }).click();
await expect(page.locator('.accounts .codein')).toBeVisible();
// Confirmed elsewhere via the one-tap link, with no live event delivered: only the poll surfaces it.
await page.evaluate(() =>
(
window as unknown as { __mock: { confirmEmailOutOfBand(email: string): void } }
).__mock.confirmEmailOutOfBand('linked@example.com'),
);
// The confirmed address surfaces as the email row, and the code form is gone.
await expect(page.locator('.acctrow').filter({ hasText: 'linked@example.com' })).toBeVisible({ timeout: 10000 });
await expect(page.locator('.accounts .codein')).toHaveCount(0);
});
test('link then unlink Telegram from the sign-in methods', async ({ page }) => { test('link then unlink Telegram from the sign-in methods', async ({ page }) => {
await loginLobby(page); await loginLobby(page);
await openProfile(page); await openProfile(page);
-4
View File
@@ -39,8 +39,6 @@ if (isMock && typeof window !== 'undefined') {
joinOpponentSilently(): void; joinOpponentSilently(): void;
adminReply(): void; adminReply(): void;
setGameLimit(v: boolean): void; setGameLimit(v: boolean): void;
clearEmail(): void;
confirmEmailOutOfBand(email: string): void;
}; };
} }
).__mock = { ).__mock = {
@@ -48,7 +46,5 @@ if (isMock && typeof window !== 'undefined') {
joinOpponentSilently: () => (gateway as MockGateway).joinPendingOpponentSilently(), joinOpponentSilently: () => (gateway as MockGateway).joinPendingOpponentSilently(),
adminReply: () => (gateway as MockGateway).mockAdminReply(), adminReply: () => (gateway as MockGateway).mockAdminReply(),
setGameLimit: (v: boolean) => (gateway as MockGateway).setGameLimit(v), setGameLimit: (v: boolean) => (gateway as MockGateway).setGameLimit(v),
clearEmail: () => (gateway as MockGateway).mockClearEmail(),
confirmEmailOutOfBand: (email: string) => (gateway as MockGateway).mockConfirmEmailOutOfBand(email),
}; };
} }
-11
View File
@@ -659,17 +659,6 @@ export class MockGateway implements GatewayClient {
this.profile.email = email; this.profile.email = email;
return emptyLinked(); return emptyLinked();
} }
// e2e hooks (window.__mock, see lib/gateway.ts): mockClearEmail drops the seeded email so the
// sign-in section offers the add-email flow; mockConfirmEmailOutOfBand attaches the address
// WITHOUT a live event — standing in for the one-tap confirm link opened in another browser,
// so the open code form must reflect it by polling the profile, not by a push.
mockClearEmail(): void {
this.profile.email = '';
this.emit({ kind: 'notify', sub: 'profile' });
}
mockConfirmEmailOutOfBand(email: string): void {
this.profile.email = email;
}
async linkEmailMerge(email: string, _code: string): Promise<LinkResult> { async linkEmailMerge(email: string, _code: string): Promise<LinkResult> {
this.profile.isGuest = false; this.profile.isGuest = false;
this.profile.email = email; this.profile.email = email;
+5 -35
View File
@@ -7,15 +7,13 @@
confirmDeleteAccount, confirmDeleteAccount,
handleError, handleError,
logout, logout,
refreshProfile,
requestDeleteAccount, requestDeleteAccount,
showToast, showToast,
} from '../lib/app.svelte'; } from '../lib/app.svelte';
import { GatewayError } from '../lib/client'; import { GatewayError } from '../lib/client';
import { connection } from '../lib/connection.svelte'; import { connection } from '../lib/connection.svelte';
import { gateway } from '../lib/gateway'; import { gateway } from '../lib/gateway';
import { insideTelegram, loginWidgetAvailable, requestTelegramLogin } from '../lib/telegram'; import { loginWidgetAvailable, requestTelegramLogin } from '../lib/telegram';
import { insideVK } from '../lib/vk';
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';
import { import {
@@ -60,11 +58,6 @@
let deletePhrase = $state(''); let deletePhrase = $state('');
const telegramLinkable = loginWidgetAvailable(); const telegramLinkable = loginWidgetAvailable();
const vkLinkable = vkWebLinkAvailable(); const vkLinkable = vkWebLinkAvailable();
// 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
// provider's linked-account row is hidden here (the web / native builds still show both).
const hostTelegram = insideTelegram();
const hostVK = insideVK();
function defaultTz(): string { function defaultTz(): string {
const b = browserOffset(); const b = browserOffset();
@@ -113,27 +106,6 @@
); );
const canUnlink = $derived(linkedCount >= 2); const canUnlink = $derived(linkedCount >= 2);
// While an add-email confirmation is pending, the address may be confirmed out of band — the
// one-tap link in the email confirms in another browser/session. A backgrounded Mini App drops
// the single-shot live stream and misses the 'profile' event (there is no replay), so poll the
// profile as a fallback until the address lands; the live push still updates it instantly while
// the app stays foregrounded. Scoped to the add-email wait (a code sent, no email yet).
const awaitingEmailConfirm = $derived(emailSent && !app.profile?.email);
$effect(() => {
if (!awaitingEmailConfirm) return;
const poll = (): void => void refreshProfile();
const id = setInterval(poll, 4000);
// Re-check the moment the Mini App returns to the foreground, where the missed event landed.
const onVisible = (): void => {
if (document.visibilityState === 'visible') poll();
};
document.addEventListener('visibilitychange', onVisible);
return () => {
clearInterval(id);
document.removeEventListener('visibilitychange', onVisible);
};
});
// toggleVariant flips a variant in the preference set, refusing to remove the last one — // toggleVariant flips a variant in the preference set, refusing to remove the last one —
// the player must allow themselves at least one variant. // the player must allow themselves at least one variant.
function toggleVariant(id: Variant) { function toggleVariant(id: Variant) {
@@ -420,10 +392,8 @@
<!-- Sign-in methods: bind/change an email and link/unlink providers. A returning email <!-- Sign-in methods: bind/change an email and link/unlink providers. A returning email
(add) triggers the merge dialog below. On the web an account can add Telegram via the (add) triggers the merge dialog below. On the web an account can add Telegram via the
login widget or VK via VK ID web login (a full-page redirect); inside a Mini App the login widget or VK via VK ID web login (a full-page redirect); inside a Mini App the
host provider is already linked, so its own row is hidden — the platform you are signed host provider is already linked. Email is never unlinked — it is changed. Unlink is
in through cannot be unlinked from here (the other provider's row still shows). Email is offered only when another identity remains (the backend also refuses the last one). -->
never unlinked — it is changed. Unlink is offered only when another identity remains
(the backend also refuses the last one). -->
<section class="accounts"> <section class="accounts">
<h3>{t('profile.accountsTitle')}</h3> <h3>{t('profile.accountsTitle')}</h3>
@@ -470,7 +440,7 @@
</div> </div>
{/if} {/if}
{#if p.telegramLinked && !hostTelegram} {#if p.telegramLinked}
<div class="acctrow"> <div class="acctrow">
<span class="prov"><img src="telegram-logo.svg" alt="" width="18" height="18" />Telegram</span> <span class="prov"><img src="telegram-logo.svg" alt="" width="18" height="18" />Telegram</span>
{#if canUnlink} {#if canUnlink}
@@ -483,7 +453,7 @@
</button> </button>
{/if} {/if}
{#if p.vkLinked && !hostVK} {#if p.vkLinked}
<div class="acctrow"> <div class="acctrow">
<span class="prov"><img src="vk-logo.svg" alt="" width="18" height="18" />VK</span> <span class="prov"><img src="vk-logo.svg" alt="" width="18" height="18" />VK</span>
{#if canUnlink} {#if canUnlink}