feat(ui): one-tap confirm deeplink screen + client language

Add the /confirm/<token> SPA route and Confirm screen: a prefetch-safe button
POSTs the token via a new confirmEmailLink RPC (client/transport/mock/codec +
ConfirmLinkResult model). A login adopts the minted session and enters the app; a
link shows confirmed / merge-in-the-app; an invalid or expired token asks for a new
code. Exempt /confirm from the no-session /login redirect. Forward the client
locale on the email request (authEmailRequest gains language → app.locale) so a
fresh web login email is localised. Handle the new 'profile' live-event sub-kind by
re-fetching the profile, so a link confirmed in another browser reflects in-app at
once. i18n en+ru.
This commit is contained in:
Ilia Denisov
2026-07-03 04:30:16 +02:00
parent 762155a55e
commit 409462fc09
12 changed files with 184 additions and 8 deletions
+22 -1
View File
@@ -31,6 +31,7 @@ import type {
Invitation,
InvitationInvitee,
InvitationSettings,
ConfirmLinkResult,
LinkResult,
MatchResult,
MoveRecord,
@@ -212,13 +213,15 @@ export function encodeGuestLogin(locale: string, browserTz: string): Uint8Array
return finish(b, fb.GuestLoginRequest.endGuestLoginRequest(b));
}
export function encodeEmailRequest(email: string, browserTz: string): Uint8Array {
export function encodeEmailRequest(email: string, browserTz: string, language: string): Uint8Array {
const b = new Builder(128);
const e = b.createString(email);
const tz = b.createString(browserTz);
const l = b.createString(language);
fb.EmailRequestRequest.startEmailRequestRequest(b);
fb.EmailRequestRequest.addEmail(b, e);
fb.EmailRequestRequest.addBrowserTz(b, tz);
fb.EmailRequestRequest.addLanguage(b, l);
return finish(b, fb.EmailRequestRequest.endEmailRequestRequest(b));
}
@@ -232,6 +235,14 @@ export function encodeEmailLogin(email: string, code: string): Uint8Array {
return finish(b, fb.EmailLoginRequest.endEmailLoginRequest(b));
}
export function encodeEmailConfirmLink(token: string): Uint8Array {
const b = new Builder(128);
const t = b.createString(token);
fb.EmailConfirmLinkRequest.startEmailConfirmLinkRequest(b);
fb.EmailConfirmLinkRequest.addToken(b, t);
return finish(b, fb.EmailConfirmLinkRequest.endEmailConfirmLinkRequest(b));
}
// --- response decoders ---
function s(v: string | null): string {
@@ -730,6 +741,16 @@ export function decodeLinkResult(buf: Uint8Array): LinkResult {
};
}
export function decodeConfirmLinkResult(buf: Uint8Array): ConfirmLinkResult {
const r = fb.EmailConfirmLinkResult.getRootAsEmailConfirmLinkResult(new ByteBuffer(buf));
const sess = r.session();
return {
purpose: (s(r.purpose()) || 'link') as ConfirmLinkResult['purpose'],
status: (s(r.status()) || 'confirmed') as ConfirmLinkResult['status'],
session: sess ? sessionFromTable(sess) : null,
};
}
// --- social decoders ---
function decodeAccountRef(r: fb.AccountRef): AccountRef {