Обзор
Лендинги (Landing Pages) — публичные страницы покупки VPN-подписки, доступные по прямой ссылке без необходимости регистрации в Telegram. Покупатель выбирает тариф, период, способ оплаты, вводит email или Telegram-контакт и оплачивает подписку.
Возможности
- Публичная страница по уникальному slug:
https://cabinet.example.com/quick/{slug}
- Выбор из разрешённых тарифов и периодов
- Все подключённые платёжные системы (с возможностью ограничения и sub-options)
- Покупка для себя или в подарок
- Мультиязычный интерфейс (заголовки, описания, баджи — JSON с ключами
ru, en, uz, uk, es)
- Система скидок с таймером обратного отсчёта и переопределениями по тарифам
- Кастомный CSS
- SEO meta-теги
- Автосоздание аккаунта в Cabinet с отправкой пароля на email
- Уведомления администраторов в Telegram о новых покупках
Требования
Для работы лендингов необходимо:
CABINET_ENABLED=true
CABINET_URL заполнен (используется для формирования ссылок в email)
- SMTP настроен и работает — для отправки данных подписки и пароля от Cabinet на email покупателя
- Хотя бы одна платёжная система подключена и настроена
- Настроены тарифы (
SALES_MODE=tariffs)
- Права на управление лендингами (если включён RBAC)
Почему SMTP обязателен
Когда пользователь покупает подписку через лендинг по email:
- Создаётся аккаунт в Cabinet с автосгенерированным паролем
- На email отправляется письмо с данными для входа, ссылкой подписки и инструкциями
- Без SMTP пользователь не получит ни пароль, ни ссылку подписки
Настройка SMTP: Настройка SMTP
Для улучшения доставляемости email бот автоматически добавляет заголовки Message-ID и Date ко всем исходящим письмам.
Создание лендинга
Через Admin Cabinet
- Откройте Cabinet → Админ-панель → Лендинги
- Нажмите Создать лендинг
- Заполните поля:
| Поле | Описание | Обязательно |
|---|
| Slug | URL-путь лендинга (латиница, цифры, дефис). Уникальный | Да |
| Заголовок | Мультиязычный заголовок страницы (JSON) | Да |
| Подзаголовок | Мультиязычный подзаголовок | Нет |
| Тарифы | Выбор из доступных тарифов (макс. 50) | Да |
| Периоды | Доступные периоды по тарифам (по умолчанию — все) | Нет |
| Способы оплаты | Разрешённые платёжные методы с sub-options | Да |
| Подарки | Включить покупку в подарок | Нет |
| Особенности | Список фич с иконками (промо-блок) | Нет |
| Footer | Мультиязычный текст внизу страницы | Нет |
| CSS | Кастомный CSS для стилизации | Нет |
| Meta title | SEO заголовок (мультиязычный) | Нет |
| Meta description | SEO описание (мультиязычный) | Нет |
| Порядок отображения | Числовой порядок в списке лендингов | Нет |
- Нажмите Сохранить
Лендинг будет доступен по адресу: https://cabinet.example.com/quick/{slug}
Мультиязычные поля
Все текстовые поля поддерживают мультиязычность в формате JSON:
{
"ru": "Быстрый VPN",
"en": "Fast VPN",
"uz": "Tez VPN"
}
Язык выбирается автоматически по языку браузера пользователя. Если перевод не найден — используется русский (ru).
Ограничение периодов
По умолчанию на лендинге доступны все периоды тарифа. Можно ограничить через поле Допустимые периоды:
{
"15": [30, 90],
"23": [30, 90, 180]
}
Где ключ — ID тарифа (строка), значение — список доступных периодов в днях. Максимум 50 тарифов, до 20 периодов на каждый.
Способы оплаты и sub-options
Выберите платёжные системы, доступные на конкретном лендинге. Некоторые платёжные системы поддерживают sub-options — отдельные кнопки для разных методов внутри одного провайдера:
| Провайдер | Sub-options |
|---|
| YooKassa | Карта, СБП |
| PayPalych (Pal24) | Карта, СБП |
| Freekassa | Карта, СБП (NSPK) |
| Platega | Динамические (из PLATEGA_ACTIVE_METHODS) |
| Telegram Stars, CryptoBot, CloudPayments и др. | Нет sub-options |
На публичной странице лендинга sub-options отображаются как отдельные кнопки выбора способа оплаты.
Особенности (промо-блок)
Список фич для промо-блока на странице лендинга. Каждая фича содержит:
[
{
"icon": "shield",
"title": {"ru": "Безопасность", "en": "Security"},
"description": {"ru": "Шифрование трафика", "en": "Traffic encryption"}
}
]
Система скидок
Лендинги поддерживают временные скидки с таймером обратного отсчёта.
Настройка скидки
В редакторе лендинга заполните блок Скидка:
| Поле | Описание | Ограничения |
|---|
| Процент скидки | Глобальная скидка на все тарифы | 1-99% |
| Начало скидки | Дата и время начала действия | UTC, datetime-local с секундами |
| Окончание скидки | Дата и время окончания | UTC, должно быть позже начала |
| Текст баджа | Мультиязычный текст на баннере | JSON, опционально |
| Переопределения | Индивидуальный процент по тарифам | JSON, ключи должны быть из списка тарифов |
Дата начала должна быть строго раньше даты окончания. Это проверяется на бэкенде и на уровне базы данных (CHECK constraint).
Переопределения по тарифам
Если для тарифа задано индивидуальное значение скидки, оно используется вместо глобального:
Тариф 15 получит скидку 30%, тариф 23 — 50%, остальные — глобальный процент.
Ключи переопределений должны быть ID тарифов из списка разрешённых тарифов лендинга. Несуществующие ID будут отклонены при сохранении.
Каскадное удаление
При сбросе процента скидки (discount_percent = null) автоматически очищаются все связанные поля:
discount_overrides → null
discount_starts_at → null
discount_ends_at → null
discount_badge_text → null
Таймер обратного отсчёта
На публичной странице лендинга отображается анимированный баннер скидки:
- Текст баджа (или «Скидка N%» по умолчанию)
- Таймер обратного отсчёта: дни : часы : минуты : секунды
- Перечёркнутая старая цена и новая цена со скидкой
Когда таймер истекает:
- Баннер скидки плавно исчезает (анимация
AnimatePresence)
- Данные лендинга автоматически перезагружаются с сервера
- Цены обновляются до полных
- Повторная загрузка подтверждает, что скидка действительно закончилась на сервере
Расчёт цены со скидкой
Формула (одинаковая на бэкенде и фронтенде):
discounted_price = max(1, price - floor(price * discount_percent / 100))
- Целочисленное деление —
// в Python, Math.floor() в JavaScript
- Все расчёты в копейках
- Минимальная цена — 1 копейка (защита от бесплатных покупок)
- Переопределение по тарифу имеет приоритет над глобальным процентом
Процесс покупки
Покупка по email (полный lifecycle)
Покупатель Лендинг API бота Remnawave
│ │ │ │
│ 1. Открывает │ │ │
│ /quick/{slug} │ │ │
│──────────────────────────>│ │ │
│ │ 2. GET /landing/ │ │
│ │ config/{slug} │ │
│ │──────────────────────────>│ │
│ │ {tariffs, prices, │ │
│ │ discount, payments} │ │
│ │<──────────────────────────│ │
│ 3. Выбирает тариф, │ │ │
│ период, оплату, │ │ │
│ вводит email │ │ │
│──────────────────────────>│ │ │
│ │ 4. POST /landing/ │ │
│ │ purchase │ │
│ │──────────────────────────>│ │
│ │ {token, payment_url} │ │
│ │<──────────────────────────│ │
│ 5. Переход на │ │ │
│ платёжную систему │ │ │
│──────────────────────────>│ │ │
│ │ │ │
│ │ 6. Webhook: │ │
│ │ оплата успешна │ │
│ │ │ │
│ │ │ 7. Создание подписки │
│ │ │──────────────────────────>│
│ │ │ {subscription_url} │
│ │ │<──────────────────────────│
│ │ │ │
│ │ │ 8. Создание аккаунта │
│ │ │ Cabinet + пароль │
│ │ │ │
│ 9. Email: пароль, │ │ │
│ ссылка подписки, │ │ │
│ ссылка на Cabinet │ │ │
│<─────────────────────────────────────────────────────│ │
│ │ │ │
│ 10. Вход в Cabinet │ │ │
│ по email + пароль │ │ │
Детали шагов 7-9:
- Бот проверяет цену с учётом активной скидки (пересчитывает на момент оплаты)
- Создаёт пользователя в Remnawave с указанным тарифом и периодом
- Если аккаунт Cabinet с таким email не существует:
- Создаёт аккаунт с автосгенерированным паролем
- Генерирует одноразовый auto-login токен
- Отправляет email с:
- Паролем от Cabinet
- Ссылкой подписки (subscription URL)
- Ссылкой для автоматического входа в Cabinet
- Инструкциями по подключению
Покупка по Telegram
- Покупатель вводит свой Telegram username (без
@)
- После оплаты бот отправляет сообщение в Telegram с данными подписки
- Если у пользователя нет аккаунта бота — подписка ожидает активации:
- Пользователь запускает бот через
/start
- Бот автоматически активирует ожидающую подписку
Подарочная покупка
- Покупатель включает переключатель «В подарок»
- Вводит свои контакты (email или Telegram)
- Вводит контакты получателя (email или Telegram)
- Опционально добавляет сообщение для получателя
- После оплаты подписка активируется на аккаунте получателя
Управление лендингами
Список лендингов
Admin Cabinet → Лендинги — список всех лендингов:
- Slug и заголовок
- Статус (активен / неактивен)
- Количество покупок
- Дата создания
- Действия: редактирование, toggle, удаление
Активация / деактивация
Кнопка toggle в списке лендингов. Неактивные лендинги возвращают 404 при попытке открыть публичную страницу.
Удаление
Удаление доступно только для лендингов без покупок (для сохранения истории платежей).
RBAC права
Если включён RBAC, для управления лендингами требуются соответствующие разрешения:
| Право | Описание |
|---|
| Просмотр лендингов | Список и детали лендингов |
| Создание лендингов | Создание новых лендингов |
| Редактирование лендингов | Изменение настроек, скидок, контента |
| Удаление лендингов | Удаление лендингов без покупок |
Настройка прав: RBAC
Уведомления администраторов
При новых покупках через лендинг:
- Telegram: администраторы получают уведомление в настроенный чат/топик (если
ADMIN_NOTIFICATIONS_ENABLED=true)
- Email покупателю: письмо с данными подписки и паролем от Cabinet
- Telegram покупателю: сообщение в Telegram (если покупка по username)
Уведомления отправляются в топик, указанный в ADMIN_NOTIFICATIONS_TOPIC_ID. Если топик не задан — в основной чат ADMIN_NOTIFICATIONS_CHAT_ID.
Email-шаблоны
Шаблоны писем для покупок через лендинг можно настроить в Admin Cabinet → Email шаблоны. Доступные шаблоны:
| Шаблон | Описание |
|---|
| Quick Purchase — результат покупки | Письмо покупателю после успешной оплаты |
Шаблоны поддерживают переменные-плейсхолдеры, которые автоматически подставляются при отправке (ссылка подписки, пароль, имя сервиса и др.). Предварительный просмотр доступен в редакторе шаблонов.
API эндпоинты
GET /landing/config/
Публичный эндпоинт. Возвращает конфигурацию лендинга для отображения.
Ответ:
{
"slug": "promo-2025",
"title": "Быстрый VPN",
"subtitle": "Безопасное подключение",
"features": [...],
"footer_text": "...",
"gift_enabled": true,
"custom_css": null,
"tariffs": [
{
"id": 15,
"name": "Стандарт",
"periods": [
{"days": 30, "price_kopeks": 30000},
{"days": 90, "price_kopeks": 80000}
]
}
],
"payment_methods": [
{
"method_id": "yookassa",
"display_name": "ЮKassa",
"sub_options": [
{"id": "card", "name": "Карта"},
{"id": "sbp", "name": "СБП"}
]
}
],
"discount": {
"percent": 20,
"ends_at": "2025-12-31T23:59:59Z",
"badge_text": "Новогодняя скидка!"
}
}
Переопределения скидок по тарифам (discount_overrides) не передаются в публичный API. Скидка уже «вшита» в цены тарифов — каждый тариф получает свою цену с учётом индивидуальной или глобальной скидки.
POST /landing/purchase
Создание покупки. Возвращает токен покупки и URL для оплаты.
GET /landing/purchase//status
Проверка статуса покупки по токену.
Пример .env
# === ОБЯЗАТЕЛЬНЫЕ ДЛЯ ЛЕНДИНГОВ ===
CABINET_ENABLED=true
CABINET_URL=https://cabinet.example.com
SALES_MODE=tariffs
# === SMTP (обязательно для email-покупок) ===
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASSWORD=your_password
SMTP_FROM_EMAIL=noreply@example.com
SMTP_FROM_NAME=My VPN Service
# === Платёжная система (хотя бы одна) ===
YOOKASSA_ENABLED=true
YOOKASSA_SHOP_ID=123456
YOOKASSA_SECRET_KEY=your_secret
# === Уведомления администраторов (рекомендуется) ===
ADMIN_NOTIFICATIONS_ENABLED=true
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890
ADMIN_NOTIFICATIONS_TOPIC_ID=123
Устранение проблем
Лендинг возвращает 404
- Лендинг активен? Проверьте статус в Admin Cabinet → Лендинги
- Slug указан верно в URL? (регистрозависимый)
CABINET_ENABLED=true?
Покупатель не получает email
- SMTP настроен? Проверьте Настройка SMTP
- Письмо в спаме? Проверьте SPF, DKIM, DMARC записи домена
SMTP_FROM_EMAIL заполнен?
- Проверьте логи бота:
docker logs <bot_container> 2>&1 | grep -i smtp
Скидка не отображается
- Текущее время сервера между
discount_starts_at и discount_ends_at?
discount_percent задан (1-99)?
- Даты указаны в UTC (не в локальном времени)?
- Проверьте API:
curl -s https://cabinet.example.com/api/landing/config/{slug} | jq '.discount'
Подписка не создаётся после оплаты
- Remnawave панель доступна? Проверьте
REMNAWAVE_API_URL
- Тариф существует в панели? ID тарифа совпадает?
- Проверьте логи:
docker logs <bot_container> 2>&1 | grep -i "guest_purchase"
Неверная цена со скидкой
- Скидка пересчитывается на момент оплаты — если скидка закончилась между выбором и оплатой, будет полная цена
- Формула:
max(1, price - floor(price * percent / 100)) — целочисленное деление, не округление
- Переопределение по тарифу имеет приоритет над глобальным процентом
Ошибка при сохранении скидки
| Ошибка | Причина |
|---|
discount_starts_at must be before discount_ends_at | Дата начала >= дата окончания |
| Невалидные ключи переопределений | ID тарифа не из списка разрешённых тарифов лендинга |
| Процент вне диапазона | Значение должно быть 1-99 (CHECK constraint в БД) |