Telegram Mini App на Vue 3: WebApp SDK, темы, sendData и валидация initData на PHP

Разработка Telegram Mini Apps (TWA) на современном стекe Vue 3 требует глубокого понимания специфики взаимодействия клиентского WebView с клиентом Telegram и серверным бэкендом. В отличие от стандартных SPA, мини-приложения функционируют внутри изолированного контейнера, где необходимо корректно обрабатывать изменения viewport, подстраиваться под системную цветовую схему пользователя и защищать передаваемые данные от подделки.

В этой статье мы разберем построение клиентской части на Vue 3 с применением Composition API, управление устойчивостью интерфейса, передачу данных боту и строгую валидацию подлинности сессии на стороне PHP.

1. Инициализация Telegram WebApp SDK и Composition API в Vue 3

Подключить Telegram WebApp JS SDK можно через скрипт https://telegram.org/js/telegram-web-app.js в index.html или установив npm-пакет. Для обеспечения реактивности в Vue 3 целесообразно вынести работу с window.Telegram.WebApp в отдельный Composable-модуль useTelegram.

Создадим реактивный хук, который инициализирует WebApp, отслеживает состояние разворачивания экрана (isExpanded) и предоставляет интерфейс для вызова нативных методов.

import { ref, onMounted, onUnmounted } from 'vue';

export function useTelegram() {
const tg = window.Telegram?.WebApp;
const user = ref(tg?.initDataUnsafe?.user || null);
const isExpanded = ref(tg?.isExpanded || false);
const themeParams = ref(tg?.themeParams || {});

const handleViewportChanged = () => {
isExpanded.value = tg?.isExpanded || false;
};

onMounted(() => {
if (!tg) {
console.warn('Telegram WebApp SDK не найден');
return;
}

tg.ready();
tg.expand();
tg.onEvent('viewportChanged', handleViewportChanged);
});

onUnmounted(() => {
if (tg) {
tg.offEvent('viewportChanged', handleViewportChanged);
}
});

const sendDataToBot = (data) => {
if (!tg) return;
tg.sendData(JSON.stringify(data));
};

return {
tg,
user,
isExpanded,
themeParams,
sendDataToBot
};
}

2. Управление Viewport и защита от прыгающего интерфейса

Одной из главных проблем при разработке Mini Apps на мобильных устройствах является нестабильность высоты области просмотра (viewport). Вызов клавиатуры или свайп пользователя может приводить к свёртыванию приложения или появлению пустых областей.

Чтобы предотвратить случайное закрытие приложения свайпом вниз и зафиксировать интерфейс, необходимо при монтировании главного компонента вызывать метод expand(), а также применить CSS-переменные для динамической высоты экрана.

import { onMounted } from 'vue';
import { useTelegram } from './useTelegram';

export default {
name: 'App',
setup() {
const { tg, themeParams } = useTelegram();

const applyThemeVariables = () => {
if (!themeParams.value) return;
const root = document.documentElement;
const tp = themeParams.value;

if (tp.bg_color) root.style.setProperty('--tg-theme-bg-color', tp.bg_color);
if (tp.text_color) root.style.setProperty('--tg-theme-text-color', tp.text_color);
if (tp.button_color) root.style.setProperty('--tg-theme-button-color', tp.button_color);
if (tp.button_text_color) root.style.setProperty('--tg-theme-button-text-color', tp.button_text_color);
if (tp.secondary_bg_color) root.style.setProperty('--tg-theme-secondary-bg-color', tp.secondary_bg_color);
};

onMounted(() => {
if (tg) {
tg.enableClosingConfirmation();
applyThemeVariables();
}
});

return {};
}
};

3. Передача данных в чат с ботом через sendData

Метод Telegram.WebApp.sendData() позволяет мгновенно отправить строковую полезную нагрузку в диалог с ботом и автоматически закрыть Mini App. Важное техническое ограничение: метод sendData() работает только если веб-приложение было открыто через обычную кнопку клавиатуры (KeyboardButton типа web_app). Если приложение открыто из Inline-кнопки или системного меню, sendData() вызовет ошибку SDK.

Пример отправки структуры заказа из компонента Vue 3:

import { ref } from 'vue';
import { useTelegram } from './useTelegram';

export default {
name: 'OrderForm',
setup() {
const { sendDataToBot } = useTelegram();
const serviceId = ref(102);
const comment = ref('');

const submitForm = () => {
const payload = {
action: 'create_order',
lead_id: Date.now().toString(36),
service_id: serviceId.value,
comment: comment.value
};

sendDataToBot(payload);
};

return { serviceId, comment, submitForm };
}
};

4. Валидация initData на бэкенде (PHP HMAC-SHA256)

Объект initDataUnsafe на клиенте может быть легко подделан пользователем через DevTools. Для безопасной аутентификации бэкенд должен получать сырую строку initData (переданную через HTTP-заголовок Authorization или в тела POST-запроса) и валидировать её с использованием токена бота.

Ниже представлен рабочий PHP-скрипт проверку криптографической подписи initData с применением бинарного HMAC-SHA256 и функций hash_equals для защиты от атак по времени (timing attacks):

<?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);

// 1. Создаем секретный ключ из токена бота
$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);

// 2. Вычисляем контрольный хэш от строки данных
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);

// 3. Timing-safe сравнение хэшей
if (!hash_equals($calculatedHash, $checkHash)) {
return null;
}

// 4. Проверка устаревания данных (например, не старше 24 часов)
if (isset($data['auth_date']) && (time() - (int)$data['auth_date'] > 86400)) {
return null;
}

return $data;
}

// Пример обработки входящего запроса
header('Content-Type: application/json');

$headers = getallheaders();
$initData = $headers['X-Telegram-Init-Data'] ?? $_POST['init_data'] ?? '';
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: '';

if (empty($initData) || empty($botToken)) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Missing required parameters']);
exit;
}

$validated = validateTelegramInitData($initData, $botToken);

if ($validated === null) {
http_response_code(403);
echo json_encode(['ok' => false, 'error' => 'Invalid initData signature or data expired']);
exit;
}

$userData = json_decode($validated['user'] ?? '{}', true);

echo json_encode([
'ok' => true,
'user_id' => $userData['id'] ?? null,
'username' => $userData['username'] ?? null
]);

5. Отправка ответов через Bot API на PHP при работе через Inline Mini Apps

Если Mini App запущен из Inline-кнопки, отправка sendData() не сработает. В этом случае фронтенд отправляет AJAX-запрос на ваш PHP-бэкенд, а бэкенд совершает прямой вызов метода Telegram Bot API sendMessage через cURL.

<?php

function sendTelegramMessage(int $chatId, string $text): bool
{
$botToken = getenv('TELEGRAM_BOT_TOKEN');
if (!$botToken) {
error_log('TELEGRAM_BOT_TOKEN is not defined');
return false;
}

$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
$payload = [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML'
];

$ch = curl_init($url);
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 ($curlError || $httpCode !== 200) {
error_log("Telegram API Error: HTTP {$httpCode}, cURL: {$curlError}");
return false;
}

$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || !($result['ok'] ?? false)) {
error_log("Telegram API non-ok response: " . $response);
return false;
}

return true;
}

6. Типичные ошибки и чеклист перед деплоем

  • Завязка на sendData везде: Помните, что sendData не работает для Mini Apps, открытых по прямой ссылку (t.me/bot/app) или из InlineKeyboardMarkup. Всегда закладывайте fallback на свой REST API.
  • Отсутствие валидации auth_date: Обязательно проверяйте поле auth_date из initData на сервере, иначе старая строка initData может быть повторно использована третьими лицами (Replay-атака).
  • Прямое использование window.Telegram: При серверном рендеринге (SSR / Nuxt) или при локальной разработке в браузере объект window.Telegram.WebApp может быть недоступен. Всегда проверяйте его существование перед вызовом методов.

Разрабатывать отказоустойчивые вебхуки и архитектуру Mini Apps помогает команда BotCreator.

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

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