React-компоненты для Telegram Mini App: обертка над WebApp SDK, управление MainButton и PHP-бэкенд

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

Алгоритм валидации на бэкенде состоит из следующих шагов:

  1. Декодирование URL-encoded строки и извлечение подписи hash.
  2. Сортировка оставшихся параметров по алфавиту и сборка строки вида key=value .
  3. Вычисление секретного ключа: HMAC-SHA256("WebAppData", bot_token).
  4. Вычисление хэша от собранной строки и проверка через timing-safe функцию hash_equals.
  5. Проверка актуальности данных по полю 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 помогут реализовать проект любой сложности.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.