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

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.