Розробка 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 допоможуть реалізувати проєкт будь-якої складності.