Stage 11: account linking & merge (email + Telegram Login Widget) (#12)
This commit was merged in pull request #12.
This commit is contained in:
@@ -65,3 +65,96 @@ export function onTelegramPath(): boolean {
|
||||
if (typeof location === 'undefined') return false;
|
||||
return location.pathname.startsWith('/telegram/');
|
||||
}
|
||||
|
||||
// --- Login Widget (web sign-in for account linking, Stage 11) ---
|
||||
|
||||
// The Login Widget is the web (non-Mini-App) Telegram sign-in. It is used only to
|
||||
// attach a Telegram identity to an existing account from a browser; inside the Mini
|
||||
// App the session is already a Telegram identity. It needs the bot id (numeric,
|
||||
// VITE_TELEGRAM_BOT_ID) and, in production, the site domain registered with BotFather
|
||||
// (/setdomain) — without that Telegram refuses to render. The connector validates the
|
||||
// returned data (HMAC under SHA-256(bot_token)).
|
||||
|
||||
const widgetScriptSrc = 'https://telegram.org/js/telegram-widget.js?22';
|
||||
|
||||
interface telegramAuthUser {
|
||||
id: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
username?: string;
|
||||
photo_url?: string;
|
||||
auth_date: number;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
interface telegramLoginSDK {
|
||||
auth(opts: { bot_id: string; request_access?: string }, cb: (user: telegramAuthUser | false) => void): void;
|
||||
}
|
||||
|
||||
function isMock(): boolean {
|
||||
return import.meta.env.MODE === 'mock';
|
||||
}
|
||||
|
||||
function botID(): string {
|
||||
return (import.meta.env.VITE_TELEGRAM_BOT_ID as string | undefined) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* loginWidgetAvailable reports whether the "Link Telegram" control should be shown:
|
||||
* not already inside the Mini App, and either the mock build or a configured bot id.
|
||||
*/
|
||||
export function loginWidgetAvailable(): boolean {
|
||||
if (insideTelegram()) return false;
|
||||
return isMock() || botID() !== '';
|
||||
}
|
||||
|
||||
let widgetLoad: Promise<void> | null = null;
|
||||
|
||||
function loadWidget(): Promise<void> {
|
||||
if (typeof document === 'undefined') return Promise.reject(new Error('telegram: no document'));
|
||||
const sdk = (window as unknown as { Telegram?: { Login?: telegramLoginSDK } }).Telegram?.Login;
|
||||
if (sdk) return Promise.resolve();
|
||||
if (!widgetLoad) {
|
||||
widgetLoad = new Promise<void>((resolve, reject) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = widgetScriptSrc;
|
||||
s.async = true;
|
||||
s.onload = () => resolve();
|
||||
s.onerror = () => reject(new Error('telegram: widget load failed'));
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
return widgetLoad;
|
||||
}
|
||||
|
||||
/**
|
||||
* requestTelegramLogin drives the Login Widget popup and resolves with the auth data
|
||||
* serialized as a URL query string (id=...&auth_date=...&hash=...) — the form the
|
||||
* connector validates — or null when the user cancels. In the mock build it returns
|
||||
* a fixed payload without loading the real widget (telegram.org is blocked in tests).
|
||||
*/
|
||||
export async function requestTelegramLogin(): Promise<string | null> {
|
||||
if (isMock()) {
|
||||
return `id=42&first_name=Telegram&auth_date=${Math.floor(Date.now() / 1000)}&hash=mock`;
|
||||
}
|
||||
await loadWidget();
|
||||
const login = (window as unknown as { Telegram?: { Login?: telegramLoginSDK } }).Telegram?.Login;
|
||||
if (!login) throw new Error('telegram: login unavailable');
|
||||
const user = await new Promise<telegramAuthUser | false>((resolve) => {
|
||||
login.auth({ bot_id: botID(), request_access: 'write' }, resolve);
|
||||
});
|
||||
if (!user) return null;
|
||||
return serializeTelegramAuth(user);
|
||||
}
|
||||
|
||||
function serializeTelegramAuth(u: telegramAuthUser): string {
|
||||
const params = new URLSearchParams();
|
||||
params.set('id', String(u.id));
|
||||
if (u.first_name) params.set('first_name', u.first_name);
|
||||
if (u.last_name) params.set('last_name', u.last_name);
|
||||
if (u.username) params.set('username', u.username);
|
||||
if (u.photo_url) params.set('photo_url', u.photo_url);
|
||||
params.set('auth_date', String(u.auth_date));
|
||||
params.set('hash', u.hash);
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user