Разработка Telegram Mini Apps (TWA) на фронтенде часто начинается с выбора между нативным объектом window.Telegram.WebApp и сторонними библиотеками вроде @twa-dev/sdk. Главная задача разработчика — правильно связать жизненный цикл React-компонентов с глобальными событиями Telegram и обеспечить строгую валидацию данных на бэкенде.
@twa-dev/sdk или window.Telegram.WebApp: что выбрать
Прямое обращение к window.Telegram.WebApp работает без установки дополнительных пакетов, однако усложняет типизацию в TypeScript и требует постоянных проверок на существование объекта при SSR или локальном запуске в браузере. Пакет @twa-dev/sdk (или современные альтернативы из экосистемы Telegram Apps) предоставляет обертку над глобальным объектом, гарантируя тип-безопасность и корректную инициализацию.
Для React-приложений оптимальным решением является создание изолированного контекста или пользовательского хука, который скрывает детали доступа к WebApp API и предотвращает ошибки при монтировании компонентов.
import { useEffect, useState } from 'react';
export function useTelegram() {
const [webApp, setWebApp] = useState(null);
useEffect(() => {
const app = window.Telegram?.WebApp;
if (app) {
app.ready();
setWebApp(app);
}
}, []);
return {
webApp,
user: webApp?.initDataUnsafe?.user ?? null,
initData: webApp?.initData ?? '',
mainButton: webApp?.MainButton ?? null,
close: () => webApp?.close(),
};
}Управление MainButton и предотвращение дублирования обработчиков
Объект MainButton управляется синглтоном на стороне клиента Telegram. Частая ошибка React-разработчиков — вешать обработчик onClick внутри компонента без его удаления при размонтировании (unmount). Это приводит к повторным вызовам функций, отправке дублирующих заказов и утечкам памяти.
Жизненный цикл кнопки должен быть строго привязан к `useEffect` с функциями очистки через `offClick`:
import { useEffect } from 'react';
import { useTelegram } from './useTelegram';
export function OrderSubmitButton({ onSubmit, isLoading, text = 'ОФОРМИТЬ ЗАКАЗ' }) {
const { mainButton } = useTelegram();
useEffect(() => {
if (!mainButton) return;
mainButton.setText(text);
mainButton.show();
const handleClick = () => {
onSubmit();
};
mainButton.onClick(handleClick);
return () => {
mainButton.offClick(handleClick);
mainButton.hide();
};
}, [mainButton, text, onSubmit]);
useEffect(() => {
if (!mainButton) return;
if (isLoading) {
mainButton.showProgress(false);
mainButton.disable();
} else {
mainButton.hideProgress();
mainButton.enable();
}
}, [mainButton, isLoading]);
return null;
}Уязвимость initDataUnsafe и зачем нужна сырая строка initData
Объект initDataUnsafe представляет собой распарсенный JSON, доступный в JavaScript. Любой пользователь может открыть DevTools или подменить этот объект в памяти браузера, изменив id пользователя, логин или параметры подписки. Использовать initDataUnsafe для принятия бизнес-решений, выдачи прав или списания средств на бэкенде категорически запрещено.
Для безопасной аутентификации React-приложение передает на бэкенд сырую строку initData через HTTP-заголовок (например, Authorization: Bearer <initData>). Бэкенд проверяет подлинность всей строки с помощью HMAC-SHA-256 и токена бота.
Валидация initData на стороне PHP API
Алгоритм валидации на бэкенде состоит из следующих шагов:
- Декодирование URL-encoded строки и извлечение подписи
hash. - Сортировка оставшихся параметров по алфавиту и сборка строки вида
key=value. - Вычисление секретного ключа:
HMAC-SHA256("WebAppData", bot_token). - Вычисление хэша от собранной строки и проверка через timing-safe функцию
hash_equals. - Проверка актуальности данных по полю
auth_dateдля защиты от атак повторного воспроизведения (Replay Attack).
<?php
declare(strict_types=1);
class TelegramAuthService
{
private string $botToken;
public function __construct(?string $botToken = null)
{
$this->botToken = $botToken ?? (string) getenv('TELEGRAM_BOT_TOKEN');
if (empty($this->botToken)) {
throw new RuntimeException('TELEGRAM_BOT_TOKEN не задан');
}
}
public function validateInitData(string $initData, int $maxAgeSeconds = 86400): array
{
parse_str($initData, $data);
if (!isset($data['hash'])) {
throw new InvalidArgumentException('Параметр hash отсутствует');
}
$receivedHash = $data['hash'];
unset($data['hash']);
ksort($data);
$dataCheckArr = [];
foreach ($data as $key => $value) {
$dataCheckArr[] = $key . '=' . $value;
}
$dataCheckString = implode("
", $dataCheckArr);
$secretKey = hash_hmac('sha256', $this->botToken, 'WebAppData', true);
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);
if (!hash_equals($calculatedHash, $receivedHash)) {
throw new SecurityException('Недействительная подпись initData');
}
if (isset($data['auth_date']) && (time() - (int)$data['auth_date'] > $maxAgeSeconds)) {
throw new SecurityException('Данные устарели (Replay Attack)');
}
if (isset($data['user'])) {
$data['user'] = json_decode($data['user'], true);
}
return $data;
}
}Создание контроллера и отправка ответа в Telegram Bot API
После успешной валидации `initData` контроллер бэкенда может обработать целевое действие (например, создание заявки) и уведомить пользователя через Telegram Bot API cURL-запросом.
<?php
declare(strict_types=1);
class OrderController
{
public function createLead(): void
{
header('Content-Type: application/json');
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? '';
$initData = trim(str_replace('Bearer ', '', $authHeader));
if (empty($initData)) {
http_response_code(401);
echo json_encode(['ok' => false, 'error' => 'Токен авторизации отсутствует']);
return;
}
try {
$authService = new TelegramAuthService();
$validatedData = $authService->validateInitData($initData);
} catch (Exception $e) {
http_response_code(403);
echo json_encode(['ok' => false, 'error' => 'Ошибка авторизации: ' . $e->getMessage()]);
return;
}
$userId = $validatedData['user']['id'] ?? null;
$leadId = bin2hex(random_bytes(7));
$botToken = getenv('TELEGRAM_BOT_TOKEN');
$ch = curl_init("https://api.telegram.org/bot{$botToken}/sendMessage");
$payload = [
'chat_id' => $userId,
'text' => "<b>Заявка #" . htmlspecialchars($leadId) . " принята!</b>
Скоро мы свяжемся с вами.",
'parse_mode' => 'HTML',
];
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_TIMEOUT => 5,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'cURL error: ' . $curlError]);
return;
}
$responseData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || empty($responseData['ok'])) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Ошибка Telegram API']);
return;
}
echo json_encode(['ok' => true, 'lead_id' => $leadId]);
}
}Частые ошибки при разработке Mini Apps
- Использование
file_get_contentsвместо cURL: приводит к блокировке потока при сетевых задержках и отсутствии тонкой настройки таймаутов. - Сравнение строк через
===: подпись hash необходимо сравнивать исключительно черезhash_equalsво избежание атак по времени (Timing Attacks). - Отсутствие отписки от событий: если не вызывать
offClickв React-эффектах, повторный клик по кнопке вызовет стейт прошлых монтирований. - Неправильный порядок ключей: если перед генерацией SHA256 не отсортировать массивы через
ksort, бэкенд всегда будет отклонять подлинные данные Telegram.
Если вам требуется профессиональная разработка сложных Mini Apps или интеграция CRM-систем с Telegram, специалисты BotCreator помогут реализовать проект любой сложности.