Compare commits
17 Commits
16349f5ad9
..
v1.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 45957bdcd6 | |||
| 829e29a726 | |||
| 399508f2f0 | |||
| 3a18e683ca | |||
| 93d086a8a3 | |||
| 8fe1bdba6b | |||
| 7923b3cc09 | |||
| 4891216749 | |||
| f1b8769c89 | |||
| b6f28a2423 | |||
| e32ee9ce68 | |||
| dc946a1faf | |||
| 384bd143d0 | |||
| c5d22fceca | |||
| deaa7a29c5 | |||
| 24017bcb7f | |||
| 2c4f4b10dc |
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -113,9 +113,7 @@ Telegram держит **единого бота**: все игроки поль
|
|||||||
|
|
||||||
В профиле перечислены **способы входа** аккаунта. В вебе игрок может добавить
|
В профиле перечислены **способы входа** аккаунта. В вебе игрок может добавить
|
||||||
Telegram (попап логин-виджета) или VK (веб-вход VK ID — редирект на страницу входа VK и
|
Telegram (попап логин-виджета) или VK (веб-вход VK ID — редирект на страницу входа VK и
|
||||||
обратно); внутри Mini App платформа-хозяин уже привязана, и её собственная плашка способа
|
обратно); внутри Mini App платформа-хозяин уже привязана. Привязка провайдера, уже
|
||||||
входа скрыта — платформу, через которую игрок сейчас вошёл, отсюда отвязать нельзя,
|
|
||||||
показывается только плашка другого провайдера. Привязка провайдера, уже
|
|
||||||
принадлежащего другому аккаунту, предлагает то же необратимое **слияние**, что и привязка
|
принадлежащего другому аккаунту, предлагает то же необратимое **слияние**, что и привязка
|
||||||
email. Привязанного провайдера можно **отвязать** — кроме последнего
|
email. Привязанного провайдера можно **отвязать** — кроме последнего
|
||||||
оставшегося способа входа: он не отвязывается, чтобы аккаунт оставался достижимым.
|
оставшегося способа входа: он не отвязывается, чтобы аккаунт оставался достижимым.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user