POS Desktop · Регистрация устройства

Wizard первого запуска. Пользователь видит его если в localStorage нет pos.registered=true и нет fallback’а через VITE_DEV_STORE_ID.

Триггер показа (в App.tsx)

const cfg = getDeviceConfig();
const hasEnvFallback = !!import.meta.env.VITE_DEV_STORE_ID;
if (cfg.registered || hasEnvFallback || config.useMockBff) {
  // обычный flow с PIN-логином
} else {
  // <RegistrationScreen />
}

То есть dev-разработчик с env-настройками wizard не видит — старый workflow продолжает работать.

Шаги wizard

Шаг 0 — URL франшизы

┌─ ERP POS · Шаг 1/4 ─┐
│ URL франшизы:        │
│ [https://...........] │
│ Например:            │
│ https://erp-test.    │
│ nirbi.ru/pos         │
│                      │
│ [Далее]              │
└──────────────────────┘

При нажатии «Далее» — pingHealth(bffUrl)GET /health. Если 200 → шаг 1, иначе ошибка «Сервер не отвечает».

URL должен содержать префикс /pos

На test VPS nginx роутит /pos/api/... → pos-bff. Без префикса попадаем в default health-check fallback.

Шаг 1 — Логин админа

┌─ Шаг 2/4 ─┐
│ Email менеджера: │
│ [............] │
│ Пароль:         │
│ [............] │
│                 │
│ [Назад] [Далее] │
└─────────────────┘

Запросы:

  1. POST /api/v1/admin/auth/login (admin-bff) — body { email, password }. Ответ: { data: { access_token, user: { permissions: [...] } } }.
  2. Проверка permissions.includes("pos.settings.edit"). Если нет → ошибка «У вас нет прав для регистрации кассы».
  3. GET /api/v1/admin/stores — список доступных пользователю ТТ.

Шаг 2 — Выбор ТТ

┌─ Шаг 3/4 ─┐
│ Куда устанавливаете кассу? │
│ ◉ Кафе на Тверской 1       │
│ ○ Кафе на Покровке         │
│                            │
│ [Назад] [Далее]            │
└────────────────────────────┘

Если ТТ только одна — выбирается автоматически. Кнопка «Далее» disabled пока ТТ не выбрана.

Шаг 3 — Имя устройства

┌─ Шаг 4/4 ─┐
│ Имя кассы:                 │
│ [POS-a3f29b...............]│
│ Имя видно админу в списке  │
│ устройств. Например, «Касса│
│ бар Тверская».             │
│                            │
│ [Назад] [Зарегистрировать] │
└────────────────────────────┘

При нажатии «Зарегистрировать»:

  1. POST /api/v1/admin/pos/devices/register (admin-bff → user-service). Body: { device_id, store_id, name, app_version } + Bearer admin-токен.
  2. На успех: setRegistered(bffUrl, storeId) в localStorage → window.location.reload().
  3. После reload App.tsx видит registered=true и пропускает к LoginScreen (PIN-логин кассира).

localStorage ключи

КлючЗначениеКогда выставляется
pos.device_idUUID v4при первом запуске (ensureDeviceId())
pos.bff_urlURL франшизыпосле успешной регистрации
pos.store_idUUID ТТпосле успешной регистрации
pos.registered"true"после успешной регистрации

Force-logout (revoke)

Когда админ удаляет кассу через админку:

  1. На следующем API-запросе pos-bff/middleware/auth.ts heartbeat-проверка возвращает 404 DEVICE_NOT_FOUND от user-service.
  2. Pos-bff отвечает клиенту 401 с кодом DEVICE_REVOKED.
  3. BffClient.onDeviceRevoked callback: clearRegistration() + window.location.reload().
  4. После reload App.tsx снова показывает <RegistrationScreen>.

device_id остаётся в localStorage — переиспользуется при повторной регистрации (опционально админ может одобрить заново).

Технические детали

  • Файлы:
    • apps/desktop/src/screens/RegistrationScreen.tsx — wizard
    • apps/desktop/src/api/registration.ts — клиент admin-bff
    • apps/desktop/src/lib/storage.ts — localStorage обёртка
    • apps/desktop/src/config.ts — runtime getter (localStorage с fallback на VITE_*)
    • packages/api-client/src/bff-client.ts — добавлены опции getDeviceId, getAppVersion, onDeviceRevoked
  • HTTP headers, отправляемые с каждым запросом после регистрации:
    • X-Device-Id: <pos.device_id>
    • X-App-Version: 0.1.1
    • X-Active-Store: <pos.store_id> (старая фича для multi-store кассиров)
    • Authorization: Bearer <jwt>

Связи