Создайте мини-приложение Telegram с React: проверка initData, @twa-dev/sdk и MainButton
Мини-приложение Telegram - это веб-страница, которая запускается в браузере приложения Telegram на iOS, Android и рабочем столе. С точки зрения React, это просто обычный SPA; различия - мост JS (window.Telegram.WebApp), способ аутентификации пользователя (подпись initData на бэкенде), а нативные элементы пользовательского интерфейса (MainButton, BackButton, переменные темы).
В этом уроке мы создадим небольшое приложение Vite + React Mini, которое:
1. Читает window.Telegram.WebApp (с @twa-dev/sdk как набранная обертка). 2. Отправляет initData на сервер PHP, который проверяет подпись HMAC-SHA-256 по маркеру бота. 3. Провода MainButton к обработчику React, чтобы собственная нижняя кнопка управляла состоянием React.
Мы не будем покрывать публикацию в Telegram, tapps.co каталог, или BackButton истории — только то, что обещает заголовок.
1. Запустите проект
npm create vite@latest miniapp-react -- --template react-ts
cd miniapp-react
npm install @twa-dev/sdk
npm run dev
Для локального тестирования в Telegram нужен HTTPS и публичный URL; npm run dev где ngrok http 5173 это обычная настройка. Официальные документы объясняют тоннель.
2. window.Telegram.WebApp ИЛИ @twa-dev/sdk
Когда Telegram открывает ваш URL-адрес, он вводит скрипт, который раскрывает глобальный:
interface TelegramWebApp {
initData: string; // raw query string, used for backend auth
initDataUnsafe: WebAppUser; // already-parsed user object, UNTRUSTED
ready(): void; // tell Telegram the UI is mounted
expand(): void; // grow to full height
close(): void;
MainButton: {
text: string;
show(): void;
hide(): void;
onClick(cb: () => void): void;
offClick(cb: () => void): void;
setText(t: string): void;
enable(): void;
disable(): void;
};
colorScheme: 'light' | 'dark';
themeParams: Record<string, string>;
}
Два свойства выглядят похожими, но не являются взаимозаменяемыми:
- initData - необработанная строка запроса (auth_date=...&user=...&hash=...). Это то, что вы отправляете на сервер. Сервер пересчитывает HMAC и сравнивает его с хэшей используя ваш токен бота в качестве секрета. - initDataUnsafe.user является уже проанализированным объектом. Удобно, но вы не должны доверять ни одному полю в нем для авторизации. Любой может создать страницу, которая устанавливает window.Telegram = { WebApp: { initDataUnsafe: { user: { id: 42 } } } }. Рассматривайте это как подсказку UX, а не как идентификацию.
Использовать @twa-dev/sdk дает вам тот же API с типами TypeScript и небольшой границей пакета, поэтому ваш код React не достигает окно непосредственно
import WebApp from '@twa-dev/sdk';
WebApp.ready();
WebApp.expand();
console.log(WebApp.initData); // raw string
console.log(WebApp.initDataUnsafe); // typed object
Для остальной части учебника мы используем SDK; переключение на глобальный просто удаляет импорт.
3. Проверить initData на бэкэнде
Это шаг, который фактически аутентифицирует пользователя. Договор задокументирован Telegram: принимайте каждый initData поле кроме хэшей, постройте data-check-string коллегиальных значение ключа линий, объединенных \nВычисления (compute) HMAC-SHA-256(data-check-string, "WebAppData") с ключом, являющимся SHA-256(bot_token), и сравните с хэшей с использованием безопасного по времени компаратора. Отклоните все, что старше ~5 минут, проверив Дата авторизации.
Минимальная конечная точка PHP:
<?php
// public/auth.php
declare(strict_types=1);
header('Content-Type: application/json');
$raw = file_get_contents('php://input') ?: '';
$payload = json_decode($raw, true);
if (!is_array($payload) || !isset($payload['initData'])) {
http_response_code(400);
echo json_encode(['error' => 'initData missing']);
return;
}
$botToken = getenv('BOT_TOKEN'); // never hardcode
$secret = hash('sha256', $botToken, true);
parse_str($payload['initData'], $data);
if (!isset($data['hash'], $data['auth_date'], $data['user'])) {
http_response_code(400);
echo json_encode(['error' => 'malformed initData']);
return;
}
$check = [];
foreach ($data as $k => $v) {
if ($k === 'hash') continue;
$check[] = $k . '=' . $v;
}
$checkString = implode("\n", $check);
$calc = hash_hmac('sha256', $checkString, $secret);
if (!hash_equals($calc, (string) $data['hash'])) {
http_response_code(401);
echo json_encode(['error' => 'bad signature']);
return;
}
if (time() - (int) $data['auth_date'] > 300) {
http_response_code(401);
echo json_encode(['error' => 'initData expired']);
return;
}
$user = json_decode($data['user'], true);
$tid = is_array($user) && isset($user['id']) ? (int) $user['id'] : 0;
// At this point you have a verified telegram_id.
// Bind it to your local session/JWT and respond.
echo json_encode(['ok' => true, 'telegram_id' => $tid]);
Ключевые моменты: parse_str обрабатывает URL-декодирование, hash_equals является безопасным по времени компаратором, маркер бота остается в переменной env, и Дата авторизации дает вам окно повтора. Более ранняя статья DEV.to о валидации HMAC более подробно описывает тот же алгоритм — рассматривайте его как компаньона на стороне React.
4. React side: вызов бэкенда
Держите звонок маленьким и набранным. Нам нужно только initData; initDataUnsafe это подсказка UX, которую мы показываем, а не источник истины.
// src/api.ts
export type AuthUser = { id: number; first_name: string; username?: string };
export async function authWithTelegram(initData: string): Promise<AuthUser> {
const res = await fetch('/auth.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ initData }),
});
if (!res.ok) throw new Error(`auth failed: ${res.status}`);
const json = await res.json();
return json.user as AuthUser;
}
Вызовите его один раз, когда компонент монтируется, после WebApp.ready():
// src/App.tsx
import { useEffect, useState } from 'react';
import WebApp from '@twa-dev/sdk';
import { authWithTelegram, AuthUser } from './api';
export default function App() {
const [user, setUser] = useState<AuthUser | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
WebApp.ready();
authWithTelegram(WebApp.initData)
.then(setUser)
.catch((e) => setError(String(e)));
}, []);
if (error) return <p>Auth error: {error}</p>;
if (!user) return <p>Loading…</p>;
return <p>Hello, {user.first_name} ({user.id})</p>;
}
WebApp.initDataUnsafe.user доступно сразу, поэтому вы можете показать имя, пока сетевой запрос находится в полете — просто помните, что любое поле, исходящее от него, не является доверенным до тех пор, пока бэкэнд не подтвердит.
5-проводная: MainButton в реактивное состояние
MainButton постоянная кнопка в нижней части мини-приложения. Два правила сохраняют здравомыслие:
- Настройте его внутри эффектов React, а не во время рендеринга. - Используйте offClick в функции очистки, чтобы useEffect повторные запуски не складывают обработчики.
import { useEffect, useState } from 'react';
import WebApp from '@twa-dev/sdk';
export function ConfirmButton({ onConfirm }: { onConfirm: () => void }) {
const [busy, setBusy] = useState(false);
useEffect(() => {
const mb = WebApp.MainButton;
mb.text = 'CONFIRM';
mb.show();
const handler = async () => {
if (busy) return;
setBusy(true);
mb.showProgress(true);
try {
await onConfirm();
WebApp.close();
} finally {
mb.showProgress(false);
setBusy(false);
}
};
mb.onClick(handler);
return () => {
mb.offClick(handler);
mb.hide();
};
}, [onConfirm, busy]);
return null;
}
Несколько вещей, которые стоит знать:
- MainButton.showProgress(true) показывает неопределенный вращатель; соедините его с отключить если вы также хотите игнорировать дальнейшие клики. - WebApp.close() это единственный способ закрыть мини-приложение из JS. Не пытайтесь уходить; браузер в приложении просто снова откроет вас. - MainButton.setText принимает до 64 видимых символов; более длинные строки усекаются.
6. Производственные заметки (необязательно)
- Используйте относительный путь для бэкенда и обслуживайте SPA + API из того же источника, в противном случае CSP Telegram заблокирует выборку. - Pin a против строка запроса или хэш сборки в вашем HTML-коде, чтобы Telegram не кэшировал устаревшую оболочку после развертывания. - Кэшировать проверенные Телеграм ID в файле cookie HttpOnly или недолговечном JWT; не храните его в localStorage если вы можете этого избежать. - Если ваше мини-приложение запускается с startapp параметр или пуск глубокая ссылка, чтение WebApp.initDataUnsafe.start_param после аутентификации и отправки на нем — но все равно закройте сторону сервера действий. - Для аналитики подсчитайте события на бэкэнде, набранные проверенным Телеграм ID. WebApp.initDataUnsafe поля подходят для воронки «на какую кнопку они нажали», а не для выставления счетов.
Если вы хотите пропустить шаблон и отправить мини-приложение вместе с ботом Telegram, BotCreator отправляет сквозную работу продукта — бота, мини-приложение и бэкенд. Справочник Telegram Bot API, который они поддерживают на botservice.biz/telegram-bot-api, является хорошим компаньоном, пока вы читаете официальные документы.