Розробка 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.