Settings
1. Summary
Goal: Персонализация пользовательского опыта через настройки приложения. Система обеспечивает сохранение предпочтений между сессиями и устройствами, а также управление прогрессивным онбордингом через контекстные подсказки.
User Value: Настройки звука, вибрации и темы сохраняются автоматически. Контекстные подсказки показываются один раз и синхронизируются между устройствами. Пользователь получает консистентный опыт при каждом запуске приложения.
2. Business Logic
Types of Settings
- Synced (Backend)
- Local Only
Сохраняется: Backend DB (source of truth)
Настройки:
soundEnabled— звуковые эффекты в приложенииonboardingGuideCompleted— интерактивный тур пройденdismissedHints— закрытые контекстные подсказки (JSON)
Поведение:
- При изменении — debounced save (500ms) в Backend
- При старте — загрузка из Backend (source of truth)
soundEnabledдублируется в localStorage для быстрого стартаonboardingGuideCompletedНЕ сохраняется в localStorage (защита от bypass)dismissedHintsсинхронизируется с localStorage для offline работы
Сохраняется: Только localStorage
Настройки:
vibration— тактильный откликtheme— тема интерфейса (dark/light)
Поведение:
- Не синхронизируется между устройствами
- Theme fallback:
Telegram.WebApp.colorScheme
Sync Mechanics
Приоритет загрузки при старте:
- localStorage — Zustand persist (мгновенный старт)
- Telegram.colorScheme — fallback для темы (первый запуск)
- Backend API — source of truth для
soundEnabled
При переключении звука сохранение в Backend откладывается на 500ms. Это предотвращает spam-запросы при быстром переключении туда-обратно.
Auto-create: Если UserSettings не существует — создаётся автоматически с дефолтами при первом GET/PUT запросе.
Contextual Hints Mechanics
Система прогрессивного онбординга через контекстные подсказки, показываемые в ключевые моменты.
Типы подсказок:
| Hint ID | Trigger | Описание |
|---|---|---|
achievement_intro | Первое открытие экрана достижений | Знакомство с системой достижений |
streak_milestone_3 | Достижение 3 дней стрика | Мотивация продолжать стрик |
raffle_sp_reached_100 | 100+ Streak Points | Доступность розыгрыша |
season_active_info | Активный сезон | Информация о сезонных наградах |
referral_first_activated | 1+ активированных рефералов | Первая реферальная награда |
Поведение:
- Подсказка показывается один раз при первом триггере
- После закрытия — сохраняется в
dismissedHints(Backend + localStorage) - Синхронизация между устройствами — пользователь не увидит дважды
- Offline режим — работает через localStorage
- Данные хранятся как JSON:
{ [hintId]: { dismissedAt: ISO date, viewCount: number } }
Подсказки показываются постепенно, по мере взаимодействия с функциями. Это предотвращает перегрузку информацией и повышает retention.
Protection
| Действие | Rate Limit | Auth | Validation |
|---|---|---|---|
| Get settings | general (100/min) | Telegram | GetSettingsSchema |
| Update settings | general (100/min) | Telegram | UpdateSettingsSchema |
| Dismiss hint | general (100/min) | Telegram | Request body: { hintId: string } |
| Sync hints | general (100/min) | Telegram | Request body: { hints: Record<string, object> } |
См. Security Matrix для полного обзора защит.
Edge Cases
| Ситуация | Поведение | UI |
|---|---|---|
| ✅ Первый запуск | Auto-create с defaults | Настройки по умолчанию |
| 🔄 Offline mode | Сохраняется в localStorage | Работает без Backend |
| ❌ Backend недоступен | localStorage fallback | Ошибка скрыта от пользователя |
| 🔄 Быстрое переключение | Debounce 500ms | Мгновенный UI, отложенный save |
| 💡 Первая подсказка | Показывается при триггере | Modal с backdrop |
| 🔄 Повторный триггер | Не показывается (dismissed) | Нет UI |
| 📱 Смена устройства | Sync с Backend | Подсказки остаются закрытыми |
| ⚠️ Ошибка dismiss API | Сохраняется в localStorage | Подсказка скрывается локально |
Planned Features
Планируется расширение synced настроек:
| Настройка | Описание | Статус |
|---|---|---|
notifications | Push-уведомления | 🔜 Planned |
language | Язык интерфейса | 🔜 Planned |
При добавлении новых полей:
- Добавить в Prisma модель
UserSettings - Обновить
SettingsServiceи schemas - Добавить в Zustand store (synced или local)
3. ADR (Architectural Decisions)
Почему только sound синхронизируется с Backend?
Проблема: Какие настройки сохранять в БД, а какие локально?
Решение: Только soundEnabled синхронизируется. Vibration и theme — localStorage only.
Альтернативы (отклонены):
- Все настройки в БД — overhead, theme зависит от устройства
- Всё локально — потеря при смене устройства
Последствия:
- Минимальная нагрузка на Backend (1 boolean поле)
- Theme адаптируется к устройству (Telegram colorScheme)
- Vibration device-specific (не все устройства поддерживают)
Почему Zustand persist + Backend?
Проблема: Нужен быстрый старт приложения, но и консистентность между устройствами.
Решение: Двухуровневое хранение:
- localStorage (Zustand persist) — instant load
- Backend — source of truth, загружается async
Последствия:
- UI отрисовывается мгновенно с cached значениями
- Backend синхронизация происходит в фоне
- При конфликте — Backend побеждает
Почему убран переключатель языка?
Проблема: Нужна ли настройка языка интерфейса?
Решение: Убрано из MVP. Приложение только на русском.
Альтернативы (отклонены):
- Полноценная i18n — overhead для MVP, нет аудитории на других языках
- Переключатель без локализации — бессмысленно
Последствия (YAGNI):
- Меньше кода, проще поддержка
- При расширении на другие рынки — добавить
languageв UserSettings
Почему убран переключатель уведомлений?
Проблема: Должен ли пользователь управлять push-уведомлениями?
Решение: Убрано намеренно. Пользователь не может отключить уведомления в приложении.
Причина:
- Уведомления используются для маркетинга и retention
- Если дать возможность отключить — большинство отключит
- Telegram уже имеет системные настройки mute для ботов
Последствия:
- Выше engagement через push-уведомления
- Пользователь может заглушить бота через Telegram если нужно
Почему JSON для dismissedHints?
Проблема: Как хранить информацию о закрытых контекстных подсказках?
Решение: JSON поле в UserSettings вместо отдельной таблицы.
Альтернативы (отклонены):
- Отдельная таблица
DismissedHintsс релейшеном — overhead, JOIN запросы - localStorage только — нет синхронизации между устройствами
- Bitfield (flags) — не расширяемо, нет метаданных (dismissedAt, viewCount)
Последствия:
- Гибкость: легко добавлять новые подсказки без миграций
- Простота: одно поле вместо JOIN запросов
- Performance: одно SELECT возвращает все данные
- Масштабируемость: JSON поддерживает неограниченное количество подсказок
JSON оптимален для "словарей" с динамическим набором ключей. Если бы требовалась сложная фильтрация по dismissedAt или viewCount — выбрали бы отдельную таблицу.
4. Architecture
Data Flow
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| SettingsService | backend/src/domains/users/services/settings.service.ts | Бизнес-логика, auto-create, hints management |
| SettingsController | backend/src/domains/users/controllers/user-settings.controller.ts | HTTP handlers (settings + hints) |
| Routes | backend/src/domains/users/routes/user-settings.routes.ts | GET/PUT/POST endpoints |
| Schemas | backend/src/domains/users/schemas/user-settings.schemas.ts | Request/Response validation |
| Settings Store | frontend/src/stores/settingsStore.ts | State management + persist |
| Hints Store | frontend/src/stores/hintsStore.ts | Hints state + localStorage persistence |
| Frontend Service | frontend/src/services/settings.service.ts | API client (settings + hints sync) |
| ContextualHint Component | frontend/src/components/ContextualHint.tsx | Reusable hint modal |
| useShowHint Hook | frontend/src/hooks/useShowHint.ts | Conditional hint display logic |
| Hints Config | frontend/src/config/hints.ts | Hints definitions (5 hints) |
| UI Screen | frontend/src/components/screens/SettingsScreen.tsx | Settings interface |
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| UserSettings | Настройки пользователя | userId, soundEnabled |
Fields
| Поле | Тип | Default | Описание |
|---|---|---|---|
id | String (cuid) | auto | Primary key |
userId | String | — | FK to User (unique) |
soundEnabled | Boolean | true | Звуковые эффекты |
onboardingGuideCompleted | Boolean | false | Интерактивный тур пройден |
dismissedHints | Json (nullable) | "{}" | Закрытые контекстные подсказки |
createdAt | DateTime | now() | Дата создания |
updatedAt | DateTime | auto | Дата обновления |
{
"achievement_intro": {
"dismissedAt": "2024-01-29T12:00:00.000Z",
"viewCount": 1
},
"streak_milestone_3": {
"dismissedAt": "2024-01-30T15:30:00.000Z",
"viewCount": 1
}
}
Relationships
При удалении User автоматически удаляется связанный UserSettings (onDelete: Cascade).
6. API Endpoints
User API
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /api/users/settings | Получить настройки | → |
| PUT | /api/users/settings | Обновить настройки | → |
| POST | /api/users/settings/hints/dismiss | Закрыть контекстную подсказку | — |
| POST | /api/users/settings/hints/sync | Синхронизировать закрытые подсказки | — |
Request/Response Examples
GET /api/users/settings
// Response 200
{
"success": true,
"data": {
"soundEnabled": true,
"onboardingGuideCompleted": false,
"dismissedHints": {
"achievement_intro": {
"dismissedAt": "2024-01-29T12:00:00.000Z",
"viewCount": 1
}
}
}
}
PUT /api/users/settings
// Request Body (partial update)
{
"soundEnabled": false
}
// OR
{
"onboardingGuideCompleted": true
}
// Response 200
{
"success": true,
"data": {
"soundEnabled": false,
"onboardingGuideCompleted": true,
"dismissedHints": {}
}
}
POST /api/users/settings/hints/dismiss
// Request Body
{
"hintId": "achievement_intro"
}
// Response 200
{
"success": true,
"data": {
"soundEnabled": true,
"onboardingGuideCompleted": false,
"dismissedHints": {
"achievement_intro": {
"dismissedAt": "2024-01-29T12:00:00.000Z",
"viewCount": 1
}
}
}
}
POST /api/users/settings/hints/sync
// Request Body
{
"hints": {
"achievement_intro": {
"dismissedAt": "2024-01-29T12:00:00.000Z",
"viewCount": 1
},
"streak_milestone_3": {
"dismissedAt": "2024-01-30T15:30:00.000Z",
"viewCount": 1
}
}
}
// Response 200
{
"success": true,
"data": {
"soundEnabled": true,
"onboardingGuideCompleted": false,
"dismissedHints": {
"achievement_intro": {
"dismissedAt": "2024-01-29T12:00:00.000Z",
"viewCount": 1
},
"streak_milestone_3": {
"dismissedAt": "2024-01-30T15:30:00.000Z",
"viewCount": 1
}
}
}
}
7. Related
- Profile — основной профиль пользователя, балансы и статистика
- Onboarding — активация аккаунта через подписки
- Streaks — система лояльности (влияет на звук наград)
- Steam Trade — Trade URL для вывода
- Security Matrix — обзор защит