Skip to main content

Onboarding

1. Summary

Goal: Гейтинг доступа к приложению через проверку подписок на Telegram бота и канал. Обеспечивает контролируемую воронку входа пользователей.

User Value: Быстрый и понятный старт с пошаговым гайдом. Пользователь чётко видит, что нужно сделать для получения доступа.


2. Business Logic

Activation Flow

Subscription Checks

Проверка: User.botStatus

Допустимые значения:

  • ACTIVE — пользователь написал /start боту
  • REACTIVATED — пользователь разблокировал бота после блокировки

Недопустимые:

  • NEW_USER — пользователь создан в TMA, но не писал боту
  • BLOCKED — пользователь заблокировал бота

Как обновляется: При получении Telegram webhook от бота.

Onboarding Steps Interface

interface OnboardingStep {
name: string; // Текущие значения: 'bot_subscription', 'channel_subscription'
description: string; // Локализованное описание
completed: boolean; // Выполнен ли шаг
required: boolean; // Обязателен для активации (всегда true)
}

interface OnboardingStatus {
isComplete: boolean; // Все шаги выполнены
canActivate: boolean; // Можно завершить онбординг
nextStep?: string; // Следующий необходимый шаг (undefined если все выполнены)
steps: OnboardingStep[]; // Список всех шагов
}

Core Logic

getOnboardingStatus(telegramId: string, forceRefresh?: boolean):

  1. Найти пользователя по telegramId
  2. Проверить botStatus (ACTIVE или REACTIVATED)
  3. Проверить подписку на канал через Telegram API
  4. Сформировать список шагов с их статусами
  5. Определить canActivate = allRequired.every(completed)

completeOnboarding(telegramId: string):

  1. Получить актуальный статус онбординга
  2. Проверить canActivate === true
  3. Если да — вернуть welcome message
  4. Если нет — вернуть { wasActivated: false, message: "Onboarding incomplete. Next step: ..." }
Важно

OnboardingService НЕ активирует InviteSession. Активация выполняется через ActivationRewardService в Telegram bot flow. Онбординг только проверяет подписки. См. Activation.

Protection

ДействиеRate LimitAuthValidation
Get status20 req/minnone (public)GetOnboardingStatusSchema
Complete20 req/minnone (public)CompleteOnboardingSchema
Refresh subscriptions20 req/minnone (public)RefreshSubscriptionsSchema
Почему нет auth?

Эндпоинты публичные, потому что пользователь ещё не аутентифицирован на этом этапе. Аутентификация происходит после успешного онбординга.

Edge Cases

СитуацияПоведениеUI
Прямой вход в TMA без invite linkВсё равно проверяем подпискиПоказываем шаги как обычно
Force refresh (?force=true)Bypass кэша, актуальная проверкаИспользуется кнопкой "Обновить"
Telegram API недоступенcheckChannelSubscription возвращает false (блокирует). Исключение: если credentials не настроены → true (пропуск)Показываем шаг как невыполненный
Bot status = BLOCKEDШаг bot_subscription = incomplete"Разблокируйте бота"
Канал не существуетОшибка при проверкеЛогируем, не блокируем
force query param — dead code

Параметр ?force=true работает в сервисе (forceRefresh), но не объявлен в GetOnboardingStatusSchema — это undeclared query param, не проходящий Zod валидацию. Более того, Fastify с опцией removeAdditional в JSON Schema валидации удаляет неизвестные query-параметры до попадания в контроллер, поэтому request.query.force будет всегда undefined — фактически dead code.

| Бэкенд недоступен (сетевая ошибка) | onboardingState = 'server_unavailable' | ServerUnavailableScreen с retry | | Бэкенд не отвечает (TCP timeout >15с) | Timeout safety net → server_unavailable | Тот же ServerUnavailableScreen | | Сессия Telegram устарела | onboardingState = 'session_expired' | SessionExpiredScreen — перезапуск Mini App |

Gate Screens (LoadingOrchestrator)

LoadingOrchestrator управляет инициализацией и может показать блокирующий экран вместо приложения:

ЭкранУсловиеКнопки
BrandedLoadingScreenИдёт инициализация— (fake progress bar)
ServerUnavailableScreenОба API-запроса (maintenance + season) упали с network error, либо timeout 15с"Повторить попытку", "Закрыть приложение"
SessionExpiredScreeninitData Telegram отсутствует или невалиден"Перезапустить приложение"
BannedScreenПользователь забанен"Закрыть приложение"
Bot/Channel activationПодписки не выполненыСсылки на бота/канал
ServerUnavailableScreen — детекция

Детекция backend unavailability происходит в initialize() через networkError: boolean флаг в return type обоих check-функций (checkMaintenanceStatus, checkSeasonStatus). Если хотя бы одна вернула networkError: true и при этом maintenance не активен — показывается ServerUnavailableScreen. Retry полностью сбрасывает state machine и запускает инициализацию заново.


3. ADR (Architectural Decisions)

Почему OnboardingService НЕ активирует сессии?

Проблема: Изначально OnboardingService и активировал подписки, и активировал InviteSession. Это нарушало Single Responsibility.

Решение: Разделение ответственности:

  • OnboardingService — только проверка подписок
  • ActivationRewardService — активация сессий и распределение наград

Альтернативы (отклонены):

  • Всё в одном сервисе — сложнее тестировать, нарушает SRP

Последствия:

  • Чистая архитектура
  • Проще unit-тесты
  • POST /onboarding/complete не имеет side effects кроме возврата сообщения

Почему проверки для ВСЕХ пользователей?

Проблема: Пользователи могут войти в TMA напрямую (без invite link), минуя создание PENDING InviteSession.

Решение: Проверять подписки для всех, независимо от наличия PENDING сессии.

Последствия:

  • Консистентный гейтинг для всех способов входа
  • Нет "обходного пути" через direct access

Почему публичные эндпоинты без auth?

Проблема: Как аутентифицировать пользователя, который ещё не прошёл онбординг?

Решение: Эндпоинты /onboarding/* публичные, используют только telegramId из query/body.

Риски и митигация:

  • Риск: Спам запросов → Митигация: Rate limit 20 req/min (rateLimitConfigs.onboarding)
  • Риск: Утечка данных → Митигация: Эндпоинты возвращают только boolean статусы, никаких sensitive данных

Последствия:

  • Простой flow для нового пользователя
  • Нет chicken-and-egg проблемы с auth

4. Architecture

Service Dependencies

Key Components

КомпонентПутьОписание
OnboardingServicebackend/src/domains/users/services/onboarding.service.tsПроверка подписок
OnboardingControllerbackend/src/domains/users/controllers/user-onboarding.controller.tsHTTP handlers
TelegramSubscriptionServicebackend/src/domains/telegram/services/telegram-subscription.service.tsTelegram API интеграция
Routesbackend/src/domains/users/routes/user-onboarding.routes.tsAPI endpoints
Schemasbackend/src/domains/users/schemas/user-onboarding.schemas.tsRequest/Response validation

Method Details

OnboardingService Methods

getOnboardingStatus(telegramId: string, forceRefresh?: boolean)

  • Возвращает: OnboardingStatus
  • Кэширование: Нет (проверки реал-тайм)
  • Force refresh: Bypass any implicit caching

completeOnboarding(telegramId: string)

  • Возвращает: { wasActivated: boolean, message: string }
  • wasActivated всегда false (OnboardingService не активирует сессии)
  • Если canActivate = false: возвращает { wasActivated: false, message: "Onboarding incomplete. Next step: ..." }

checkBotSubscription(telegramId: string)

  • Возвращает: boolean
  • Проверяет: user.botStatus in ['ACTIVE', 'REACTIVATED']

checkChannelSubscription(telegramId: string)

  • Возвращает: boolean
  • Использует: TelegramSubscriptionService

refreshSubscriptionStatus(telegramId: string)

  • Возвращает: { botSubscribed: boolean, channelSubscribed: boolean, lastChecked: Date }
  • Для: Troubleshooting и testing

5. Database Schema

МодельОписаниеКлючевые поля
UserСодержит botStatusbotStatus (BotRegistrationState enum)
InviteSessionТрекинг перехода по ссылкеstate, telegramId, metadata, expiresAt

BotRegistrationState Enum

enum BotRegistrationState {
NEW_USER // Создан в TMA, никогда не писал боту
ACTIVE // Написал /start боту
REACTIVATED // Разблокировал бота после блокировки
BLOCKED // Заблокировал бота
}

InviteSession States

Relationships


6. API Endpoints

Public API

МетодЭндпоинтОписаниеDocs
GET/onboarding/statusПроверка статуса онбординга
POST/onboarding/completeЗавершение онбординга
POST/onboarding/refresh-subscriptionsПринудительное обновление статуса
Request/Response Examples

GET /onboarding/status?telegramId=123456789

// Response 200
{
"success": true,
"data": {
"isComplete": false,
"canActivate": false,
"nextStep": "channel_subscription",
"steps": [
{
"name": "bot_subscription",
"description": "Subscribe to Telegram bot",
"completed": true,
"required": true
},
{
"name": "channel_subscription",
"description": "Subscribe to Telegram channel",
"completed": false,
"required": true
}
]
}
}

POST /onboarding/complete

// Request Body
{
"telegramId": 123456789
}

// Response 200 (success)
{
"success": true,
"data": {
"wasActivated": false,
"message": "Добро пожаловать, Иван! Все проверки пройдены!"
}
}

// Response 200 (not ready — подписки не выполнены)
{
"success": true,
"data": {
"wasActivated": false,
"message": "Onboarding incomplete. Next step: channel_subscription"
}
}

POST /onboarding/refresh-subscriptions

// Request Body
{
"telegramId": 123456789
}

// Response 200
{
"success": true,
"data": {
"botSubscribed": true,
"channelSubscribed": false,
"lastChecked": "2026-02-21T10:30:00.000Z"
}
}

Stub Endpoints (не реализованы)

МетодЭндпоинтОписаниеСтатус
GET/onboarding/notificationsПолучение уведомленийStub (empty array)
POST/onboarding/notifications/mark-readПометить прочитаннымиStub (always success)

7. Interactive Guide

Изменение от 29.01.2026

Добавлена система интерактивного тура для новых пользователей после успешной активации. Использует react-joyride для пошагового обучения.

Goal

После прохождения activation flow (подписки на бота/канал), новым пользователям показывается интерактивный тур по приложению. Цель — познакомить с основными возможностями за ~1 минуту.

Tour System Architecture

Components

КомпонентПутьОписание
OnboardingGuidefrontend/src/components/onboarding/OnboardingGuide.tsxReact-joyride wrapper, управление туром
TourPromptModalfrontend/src/components/onboarding/TourPromptModal.tsxМодалка "Хочешь пройти тур?"
ConfirmSkipModalfrontend/src/components/onboarding/ConfirmSkipModal.tsxПодтверждение пропуска тура
onboardingStorefrontend/src/stores/onboardingStore.tsZustand store для UI состояния
tourStepsfrontend/src/config/tourSteps.tsКонфигурация 12 шагов тура

Tour Steps (12 шагов)

Шаги нацелены на основные UI элементы через data-tour атрибуты:

#TargetОписание
1balance-scrapБаланс Scrap — игровая валюта
2balance-xpXP — опыт для прокачки уровня
3daily-caseЕжедневный бесплатный кейс
4quests-sectionКвесты — задания для наград
5quest-filtersФильтры квестов по типам
6nav-spinНавигация: Рулетка
7nav-casesНавигация: Кейсы
8nav-profileНавигация: Профиль
9inventory-buttonКнопка инвентаря (скины)
10steam-trade-urlSteam Trade URL для вывода
11settings-buttonКнопка настроек
12faq-buttonFAQ для помощи

User Flow

Новый пользователь:

  1. Проходит activation (подписки)
  2. После успешной активации → onboardingGuideCompleted = false (default)
  3. App.tsx показывает TourPromptModal
  4. Пользователь выбирает:
    • "Начать тур" → Joyride показывает 12 шагов
    • "Не сейчас" → ConfirmSkipModal → подтверждение → сохранение флага в БД

Повторный запуск:

  1. Settings → "Пройти тур заново"
  2. setOnboardingGuideCompleted(false)showPrompt()
  3. TourPromptModal появляется снова

Backend Integration

Поле: UserSettings.onboardingGuideCompleted (Boolean, default: false)

Тип синхронизации: Backend DB + localStorage (localStorage = кеш для быстрой проверки, Backend = source of truth)

Обновление:

  • При завершении тура: PUT /api/users/settings { onboardingGuideCompleted: true }
  • При пропуске тура: то же самое
  • При "Пройти заново": локальный setOnboardingGuideCompleted(false) → модалка появляется
Важно

onboardingGuideCompleted сохраняется в localStorage через Zustand partialize (аналогично soundEnabled). localStorage служит кешем для быстрого старта, Backend DB является source of truth. При инициализации значение из Backend перезаписывает localStorage.

Edge Cases

СитуацияПоведение
Пользователь закрывает модалку (X)Тур не засчитывается, модалка появится при следующем запуске
Пользователь нажимает ESC во время тураJoyride останавливается, тур не засчитывается
Target элемент не найденJoyride пропускает шаг (event: TARGET_NOT_FOUND)
Пользователь переключил таб во время тураТур останавливается (элементы скрыты)
Переход из TourPromptModal в mini-tourАтомарный set() одновременно скрывает prompt и запускает mini-tour — scroll lock без разрыва (lockCount 1→1)
Scroll Lock: атомарный переход prompt → mini-tour

При пропуске тура (skipTour() и startMiniTour()) навигация происходит под промптом (промпт остаётся видимым как overlay), после чего единый вызов set() атомарно скрывает prompt и запускает mini-tour. Это гарантирует, что scroll lock переходит от prompt к mini-tour без видимого разрыва — lockCount остаётся 1→1 в рамках одного React commit (до paint).


  • Activation — активация InviteSession и награды
  • Profile — профиль после активации
  • Settings — настройки пользователя (включая onboardingGuideCompleted)
  • Referrals — реферальная система
  • Telegram Bot — интеграция с Telegram
  • UTM Tracking — трекинг источников при регистрации
  • Security Matrix — обзор защит