feat(email): one-tap confirm deeplink + client language (PR1b) #162
+11
-2
@@ -239,7 +239,13 @@ arrive from a platform rather than completing a mandatory registration).
|
||||
development log mailer when no relay is configured) and, once
|
||||
verified, attaches a confirmed email identity. Sends are throttled per recipient
|
||||
(a 60-second cooldown and a five-per-hour cap). Links in the email are built from
|
||||
a configured public base URL, never a request Host header (anti-injection). An
|
||||
a configured public base URL, never a request Host header (anti-injection). The email
|
||||
also carries a **one-tap confirm deeplink** (`/app/#/confirm/<token>`, an opaque
|
||||
256-bit token stored only as its SHA-256, 12-hour TTL): a login mints a session in the
|
||||
browser that opens it (magic-link), a link confirms the identity and emits a `notify`
|
||||
profile-refresh to the in-app session, and a would-be merge is deferred to the
|
||||
interactive flow. The confirm page is prefetch-safe — it confirms only on a button
|
||||
press, so a mail scanner's GET does not consume the single-use token. An
|
||||
**email-login** account is created flagged `is_guest` and stays reapable until the
|
||||
code is confirmed — so an abandoned, never-confirmed login frees its reserved
|
||||
address — with confirming clearing the flag. Accounts and identities use
|
||||
@@ -933,7 +939,10 @@ edge-pause, scroll speed, and the fade-out → gap → fade-in transition) are o
|
||||
eligibility inputs (grants hints, grants/revokes `no_banner`; a future payment flow sets
|
||||
`paid_account`), the backend emits a `notify` **`banner`** sub-kind (a payload-free re-poll signal),
|
||||
and the open client re-fetches `profile.get` to show or hide the banner in place. Operator *content*
|
||||
edits take effect on the next `profile.get` (open/reconnect/foreground), not mid-session.
|
||||
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
|
||||
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.
|
||||
|
||||
> 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
|
||||
|
||||
+4
-1
@@ -66,7 +66,10 @@ client's light/dark scheme, and the layout clears the VK mobile home bar. The sa
|
||||
is provisioned on the first request but only becomes a durable account once the code
|
||||
is confirmed, so an abandoned, never-confirmed attempt is cleaned up and its address
|
||||
freed. Sends are rate-limited (a short cooldown between codes and a small hourly cap
|
||||
per address), so a mistyped address or an impatient tap cannot flood an inbox.
|
||||
per address), so a mistyped address or an impatient tap cannot flood an inbox. The email
|
||||
also carries a **one-tap link** that confirms the address — or signs the player straight
|
||||
in — in a single tap; opening it in another browser reflects in the app at once, and a
|
||||
mail scanner that merely fetches the link never triggers it (it acts only on a button press).
|
||||
Telegram runs a **single bot**: every player uses
|
||||
the same bot, and all of its chat and out-of-app notifications are written in the
|
||||
player's own **interface language** (en/ru). A separate optional **promo bot** can run alongside the
|
||||
|
||||
@@ -71,7 +71,10 @@ launch-параметрам VK (их проверяет gateway), и при пе
|
||||
запросе, но становится постоянным аккаунтом только после подтверждения кода — так брошенная,
|
||||
неподтверждённая попытка вычищается, а адрес освобождается. Отправки ограничены по частоте (короткая
|
||||
пауза между кодами и небольшой часовой лимит на адрес), чтобы опечатка в адресе или нетерпеливый тап
|
||||
не завалили почтовый ящик.
|
||||
не завалили почтовый ящик. В письме также есть **ссылка одного нажатия**, которая подтверждает
|
||||
адрес — или сразу выполняет вход — в один тап; открытие её в другом браузере тут же отражается в
|
||||
приложении, а почтовый сканер, который просто загружает ссылку, её не срабатывает (действие
|
||||
только по нажатию кнопки).
|
||||
Telegram держит **единого бота**: все игроки пользуются одним
|
||||
и тем же ботом, а весь его чат и внеприложенческие уведомления пишутся на **языке
|
||||
интерфейса** самого игрока (en/ru). Рядом с основным может работать отдельный опциональный
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
decodeStats,
|
||||
encodeCheckWord,
|
||||
encodeEmailRequest,
|
||||
encodeEmailConfirmLink,
|
||||
encodeFeedbackSubmit,
|
||||
encodeDraftSave,
|
||||
encodeEnqueue,
|
||||
@@ -121,6 +122,12 @@ describe('codec', () => {
|
||||
);
|
||||
expect(email.email()).toBe('a@example.com');
|
||||
expect(email.browserTz()).toBe('+00:00');
|
||||
expect(email.language()).toBe('en');
|
||||
|
||||
const confirm = fb.EmailConfirmLinkRequest.getRootAsEmailConfirmLinkRequest(
|
||||
new ByteBuffer(encodeEmailConfirmLink('tok-abc')),
|
||||
);
|
||||
expect(confirm.token()).toBe('tok-abc');
|
||||
|
||||
const vk = fb.VKLoginRequest.getRootAsVKLoginRequest(
|
||||
new ByteBuffer(encodeVKLogin('vk_user_id=494075&vk_ts=1&sign=abc', '+03:00', 'Иван Петров')),
|
||||
|
||||
Reference in New Issue
Block a user