Telegram Mini App на React: SDK, валидация initData на PHP и управление MainButton

Разработка 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 состояит из следующих шагов:

  1. Разбор query-строки в массив параметров.
  2. Извлечение и удаление из массива параметра hash.
  3. Сортировка оставшихся ключей по алфавиту и формирование строки вида key=value key2=value2.
  4. Генерация HMAC-SHA-256 хеша от строки WebAppData с использованием токена бота в качестве ключа.
  5. Вычисление итогового HMAC-SHA-256 от сформированной строки параметров с использованием секретного ключа из шага 4.
  6. Сравнение полученного хеша с переданным в параметре hash через hash_equals() (тайминг-сейф проверка).
  7. Проверка актуальности поля 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.

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

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