Разработка Telegram Mini Apps (TWA) на современном фронтенд-стеке требует соблюдения строгих правил работы с контекстом мессенджера и безопасностью данных. При создании SPA-приложений на React разработчики часто сталкиваются с вопросами выбора SDK, корректной работы с системными компонентами UI (такими как MainButton) и валидации передаваемых данных на бэкенде.
@twa-dev/sdk против window.Telegram.WebApp: что выбрать для React?
При интеграции React-приложения с Telegram Mini Apps у вас есть два основных способа взаимодействия с клиентским API мессенджера:
- Прямое обращение к
window.Telegram.WebApp: требует подключения скриптаhttps://telegram.org/js/telegram-web-app.jsвindex.html. Подход не даёт из коробки типизации TypeScript, требует самостоятельной проверки наличия глобального объекта и создания обёрток. - Использование библиотеки
@twa-dev/sdk(или@telegram-apps/sdk): npm-пакет, предоставляющий готовые TypeScript-типы, методы-хелперы и удобную обёртку над объектомwindow.Telegram.WebApp. Это упрощает работу в компонентной структуре React и предотвращает ошибки периода выполнения (runtime errors) при сборке.
Для production-приложений на React предпочтительно использовать npm-пакеты, так как они упрощают автодополнение в IDE и изолируют работу с глобальным окном.
Инициализация SDK и управление MainButton в React
Главная кнопка Telegram (MainButton) находится внизу экрана мессенджера вне контейнера webview. Управление ее состоянием (текст, видимость, лоадер) должно быть синхронизировано с жизненным циклом React-компонента.
Ниже представлен рабочий пример компонента формы заказа, который инициализирует TWA, раскрывает приложение на весь экран через expand(), подписывается на клик по MainButton и отправляет сырую строку initData на бэкенд.
import React, { useEffect, useState } from 'react';
import WebApp from '@twa-dev/sdk';
export const ServiceOrderForm: React.FC = () => {
const [service, setService] = useState('audit');
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
// Сообщаем Telegram, что Mini App готово к отображению
WebApp.ready();
WebApp.expand();
// Настраиваем MainButton
WebApp.MainButton.setText('ОФОРМИТЬ ЗАЯВКУ');
WebApp.MainButton.show();
const handleSubmit = async () => {
WebApp.MainButton.showProgress();
setErrorMessage('');
try {
const response = await fetch('/api/lead.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initData: WebApp.initData, // Передаем подлинную строку
service: service
})
});
const result = await response.json();
if (response.ok && result.ok) {
WebApp.close(); // Закрываем Mini App при успехе
} else {
setErrorMessage(result.error || 'Неизвестная ошибка сервера');
}
} catch (err) {
setErrorMessage('Ошибка сети при отправке данных');
} finally {
WebApp.MainButton.hideProgress();
}
};
// Подписываемся на событие нажатия
WebApp.MainButton.onClick(handleSubmit);
// Очищаем обработчики при размонтировании компонента
return () => {
WebApp.MainButton.offClick(handleSubmit);
WebApp.MainButton.hide();
};
}, [service]);
return (
<div style={{ padding: '16px', color: 'var(--tg-theme-text-color)' }}>
<h2>Выбор услуги</h2>
<select
value={service}
onChange={(e) => setService(e.target.value)}
style={{ width: '100%', padding: '8px', margin: '12px 0' }}
>
<option value="audit">Технический аудит</option>
<option value="development">Разработка Telegram бота</option>
</select>
{errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
</div>
);
};initDataUnsafe vs initData: в чём разница и где кроется угроза
Объект WebApp.initDataUnsafe представляет собой уже распарсенный JavaScript-объект с данными пользователя (user.id, first_name, username и т.д.).
Критически важно: Никогда не используйте
initDataUnsafeдля аутентификации пользователя или передачи бизнес-данных на сервер! Любой пользователь может открыть DevTools в Telegram Desktop или подменить JS-объект в памяти, выдав себя за администратора или другого клиента.
Строка WebApp.initData содержит подписанный Telegram набор параметров в формате query string. В неё входит параметр hash, который генерируется серверами Telegram с использованием HMAC-SHA-256 и секретного ключа вашего бота. Только проверка этой подписи на стороне PHP гарантирует, что данные не были скомпрометированы.
Валидация initData на PHP
Алгоритм проверки initData на PHP состояит из следующих шагов:
- Разбор query-строки в массив параметров.
- Извлечение и удаление из массива параметра
hash. - Сортировка оставшихся ключей по алфавиту и формирование строки вида
key=value key2=value2. - Генерация HMAC-SHA-256 хеша от строки
WebAppDataс использованием токена бота в качестве ключа. - Вычисление итогового HMAC-SHA-256 от сформированной строки параметров с использованием секретного ключа из шага 4.
- Сравнение полученного хеша с переданным в параметре
hashчерезhash_equals()(тайминг-сейф проверка). - Проверка актуальности поля
auth_date(защита от атак повторного воспроизведения Replay Attack).
Функция валидации initData на PHP:
<?php
function validateTelegramInitData(string $initData, string $botToken): ?array
{
parse_str($initData, $data);
if (!isset($data['hash'])) {
return null;
}
$checkHash = $data['hash'];
unset($data['hash']);
// Сортировка ключей по алфавиту
$dataCheckArray = [];
foreach ($data as $key => $value) {
$dataCheckArray[] = $key . '=' . $value;
}
sort($dataCheckArray);
$dataCheckString = implode("
", $dataCheckArray);
// Вычисление secret_key = HMAC-SHA256("WebAppData", bot_token)
$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);
// Вычисление итогового хеша
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);
// Timing-safe сравнение
if (!hash_equals($calculatedHash, $checkHash)) {
return null;
}
// Проверка срока жизни данных (например, не старше 24 часов)
if (isset($data['auth_date']) && (time() - (int)$data['auth_date'] > 86400)) {
return null;
}
// Декодируем JSON-объект пользователя, если он присутствует
if (isset($data['user'])) {
$data['user'] = json_decode($data['user'], true);
}
return $data;
}Приём лида и обратный вызов к Telegram Bot API через cURL
После успешной валидации `initData` создаем уникальный идентификатор заявки, сохраняем данные и отправляем пользователю подтверждение через Telegram Bot API.
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/validate.php';
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE || empty($input['initData'])) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Bad request']);
exit;
}
$botToken = getenv('TELEGRAM_BOT_TOKEN');
if (!$botToken) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Server config error']);
exit;
}
$validatedData = validateTelegramInitData($input['initData'], $botToken);
if (!$validatedData || empty($validatedData['user']['id'])) {
http_response_code(403);
echo json_encode(['ok' => false, 'error' => 'Unauthorized: initData invalid']);
exit;
}
$userId = (int)$validatedData['user']['id'];
$service = filter_var($input['service'] ?? 'default', FILTER_SANITIZE_SPECIAL_CHARS);
// Генерация защищенного ID лида
$leadId = bin2hex(random_bytes(7));
// Отправка текстового сообщения в чат с пользователем через Telegram Bot API
$payload = [
'chat_id' => $userId,
'text' => "Заявка принята!
Номер заявки: {$leadId}
Услуга: {$service}",
'parse_mode' => 'HTML'
];
$ch = curl_init("https://api.telegram.org/bot{$botToken}/sendMessage");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 5
]);
$responseRaw = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$response = json_decode($responseRaw, true);
if ($httpCode !== 200 || empty($response['ok'])) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Failed to send Telegram notification']);
exit;
}
echo json_encode(['ok' => true, 'lead_id' => $leadId]);Частые ошибки при разработке React Mini Apps
- Забытая отписка от событий MainButton: если в
useEffectне вызыватьWebApp.MainButton.offClick(handler), при каждом перерендере компонента будут навешиваться дублирующие обработчики. Это приводит к нескольким параллельным запросам к API при одном клике. - Отсутствие вызова
ready()иexpand(): без вызоваready()мессенджер может не скрыть стартовый индикатор загрузки, а безexpand()окно приложения останется в свернутом режиме (half-screen). - Завязка на CSS-размеры 100vh: экран высоты на мобильных устройствах изменяется при появлении виртуальной клавиатуры. Используйте переменные Telegram CSS (например,
var(--tg-viewport-height)) или вызовыWebApp.onEvent('viewportChanged', ...).
Разработка надёжных Telegram Mini Apps и сложных интеграций под ключ — в сервисе BotCreator.