Skip to main content

Settings

1. Summary

Goal: Персонализация пользовательского опыта через настройки приложения. Система обеспечивает сохранение предпочтений между сессиями и устройствами, а также управление прогрессивным онбордингом через контекстные подсказки.

User Value: Настройки звука, вибрации и темы сохраняются автоматически. Контекстные подсказки показываются один раз и синхронизируются между устройствами. Пользователь получает консистентный опыт при каждом запуске приложения.


2. Business Logic

Types of Settings

Сохраняется: 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 работы

Sync Mechanics

Приоритет загрузки при старте:

  1. localStorage — Zustand persist (мгновенный старт)
  2. Telegram.colorScheme — fallback для темы (первый запуск)
  3. Backend API — source of truth для soundEnabled
Debounced Save

При переключении звука сохранение в Backend откладывается на 500ms. Это предотвращает spam-запросы при быстром переключении туда-обратно.

Auto-create: Если UserSettings не существует — создаётся автоматически с дефолтами при первом GET/PUT запросе.

Contextual Hints Mechanics

Система прогрессивного онбординга через контекстные подсказки, показываемые в ключевые моменты.

Типы подсказок:

Hint IDTriggerОписание
achievement_introПервое открытие экрана достиженийЗнакомство с системой достижений
streak_milestone_3Достижение 3 дней стрикаМотивация продолжать стрик
raffle_sp_reached_100100+ Streak PointsДоступность розыгрыша
season_active_infoАктивный сезонИнформация о сезонных наградах
referral_first_activated1+ активированных рефераловПервая реферальная награда

Поведение:

  • Подсказка показывается один раз при первом триггере
  • После закрытия — сохраняется в dismissedHints (Backend + localStorage)
  • Синхронизация между устройствами — пользователь не увидит дважды
  • Offline режим — работает через localStorage
  • Данные хранятся как JSON: { [hintId]: { dismissedAt: ISO date, viewCount: number } }
Progressive Disclosure

Подсказки показываются постепенно, по мере взаимодействия с функциями. Это предотвращает перегрузку информацией и повышает retention.

Protection

ДействиеRate LimitAuthValidation
Get settingsgeneral (100/min)TelegramGetSettingsSchema
Update settingsgeneral (100/min)TelegramUpdateSettingsSchema
Dismiss hintgeneral (100/min)TelegramRequest body: { hintId: string }
Sync hintsgeneral (100/min)TelegramRequest 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

Roadmap

Планируется расширение synced настроек:

НастройкаОписаниеСтатус
notificationsPush-уведомления🔜 Planned
languageЯзык интерфейса🔜 Planned

При добавлении новых полей:

  1. Добавить в Prisma модель UserSettings
  2. Обновить SettingsService и schemas
  3. Добавить в 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?

Проблема: Нужен быстрый старт приложения, но и консистентность между устройствами.

Решение: Двухуровневое хранение:

  1. localStorage (Zustand persist) — instant load
  2. 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 vs Separate Table

JSON оптимален для "словарей" с динамическим набором ключей. Если бы требовалась сложная фильтрация по dismissedAt или viewCount — выбрали бы отдельную таблицу.


4. Architecture

Data Flow

Key Components

КомпонентПутьОписание
SettingsServicebackend/src/domains/users/services/settings.service.tsБизнес-логика, auto-create, hints management
SettingsControllerbackend/src/domains/users/controllers/user-settings.controller.tsHTTP handlers (settings + hints)
Routesbackend/src/domains/users/routes/user-settings.routes.tsGET/PUT/POST endpoints
Schemasbackend/src/domains/users/schemas/user-settings.schemas.tsRequest/Response validation
Settings Storefrontend/src/stores/settingsStore.tsState management + persist
Hints Storefrontend/src/stores/hintsStore.tsHints state + localStorage persistence
Frontend Servicefrontend/src/services/settings.service.tsAPI client (settings + hints sync)
ContextualHint Componentfrontend/src/components/ContextualHint.tsxReusable hint modal
useShowHint Hookfrontend/src/hooks/useShowHint.tsConditional hint display logic
Hints Configfrontend/src/config/hints.tsHints definitions (5 hints)
UI Screenfrontend/src/components/screens/SettingsScreen.tsxSettings interface

5. Database Schema

Models

МодельОписаниеКлючевые поля
UserSettingsНастройки пользователяuserId, soundEnabled

Fields

ПолеТипDefaultОписание
idString (cuid)autoPrimary key
userIdStringFK to User (unique)
soundEnabledBooleantrueЗвуковые эффекты
onboardingGuideCompletedBooleanfalseИнтерактивный тур пройден
dismissedHintsJson (nullable)"{}"Закрытые контекстные подсказки
createdAtDateTimenow()Дата создания
updatedAtDateTimeautoДата обновления
dismissedHints JSON Structure
{
"achievement_intro": {
"dismissedAt": "2024-01-29T12:00:00.000Z",
"viewCount": 1
},
"streak_milestone_3": {
"dismissedAt": "2024-01-30T15:30:00.000Z",
"viewCount": 1
}
}

Relationships

Cascade Delete

При удалении 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
}
}
}
}

  • Profile — основной профиль пользователя, балансы и статистика
  • Onboarding — активация аккаунта через подписки
  • Streaks — система лояльности (влияет на звук наград)
  • Steam Trade — Trade URL для вывода
  • Security Matrix — обзор защит