import { makeAutoObservable, reaction, runInAction } from 'mobx'; import { AppState, type AppStateStatus } from 'react-native'; import { themeStore } from '@/shared/config'; import { appEvents, currentPosition, hydrateStorage, startGeoTracking, stopGeoTracking, storage } from '@/shared/lib'; import type { Driver, MrmAdmin } from '@/entities/driver/@x/session'; import { MessageStore } from '@/entities/message/@x/session'; import { loginByPin, loginMrm as loginMrmRequest, logout as logoutRequest, sendHeartbeat } from '@/entities/driver/@x/session'; import type { AppScreen, ThemeMode } from './types'; import { clearMrmAdmin, clearSession, persistDriver, persistMrmAdmin, persistSession, sessionStartedAt, storedDriver, storedMrmAdmin, storedToken, } from './sessionStorage'; const HEARTBEAT_INTERVAL = 10_000; const SPEED_INTERVAL = 3_000; function themeStorageKey(driverId: string): string { return `kiosk:theme:${driverId}`; } export class SessionStore { readonly messageStore = new MessageStore(); screen: AppScreen = 'login'; driver: Driver | null = null; mrmAdmin: MrmAdmin | null = null; sessionKicked = false; ready = false; theme: ThemeMode = 'dark'; isDark = true; isMoving = true; speed = 32; currentStopIndex = 3; loginError: string | null = null; loginLoading = false; private heartbeatTimer: ReturnType | null = null; private speedTimer: ReturnType | null = null; private appStateSub: { remove: () => void } | null = null; constructor() { makeAutoObservable(this, {}, { autoBind: true }); reaction( () => this.isDark, (isDark) => themeStore.setDark(isDark), { fireImmediately: true }, ); } async init(): Promise { if (this.ready) return; await hydrateStorage(); runInAction(() => { this.driver = storedDriver(); this.mrmAdmin = storedMrmAdmin(); this.restore(); this.ready = true; }); this.start(); } get messages() { return this.messageStore.messages; } get connection() { return this.messageStore.connection; } get pendingImportant() { return this.messageStore.pendingImportant; } get pendingCount() { return this.messageStore.pendingCount; } get unreadCount() { return this.messageStore.unreadCount; } private restore(): void { if (this.mrmAdmin) { this.screen = 'main'; } else if (storedToken() && this.driver) { this.screen = 'main'; this.messageStore.hydrateFromCache(); } if (this.driver) this.applyStoredTheme(this.driver.id); this.isDark = this.theme === 'dark'; } start(): void { this.speedTimer = setInterval(() => { if (this.screen !== 'main') return; runInAction(() => { this.speed = Math.max(0, Math.min(60, this.speed + (Math.random() - 0.5) * 8)); }); }, SPEED_INTERVAL); this.startSessionLoop(); } stop(): void { if (this.speedTimer) clearInterval(this.speedTimer); this.stopSessionLoop(); this.messageStore.stop(); } private startSessionLoop(): void { if (this.screen === 'login' || this.screen === 'welcome') return; startGeoTracking(); this.messageStore.start(); void this.heartbeat(); this.heartbeatTimer = setInterval(() => void this.heartbeat(), HEARTBEAT_INTERVAL); this.appStateSub = AppState.addEventListener('change', (state: AppStateStatus) => { if (state === 'active') void this.heartbeat(); }); } private stopSessionLoop(): void { if (this.heartbeatTimer) clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; this.appStateSub?.remove(); this.appStateSub = null; stopGeoTracking(); } private async heartbeat(): Promise { const result = await sendHeartbeat( storedToken(), currentPosition(), this.driver?.deviationSettings?.updatedAt ?? null, ); if (result.sessionExpired) { runInAction(() => { this.sessionKicked = true; this.driver = null; this.screen = 'login'; }); clearSession(); this.stopSessionLoop(); return; } if (result.deviationSettings && this.driver) { runInAction(() => { this.driver = { ...this.driver!, deviationSettings: result.deviationSettings! }; }); persistDriver(this.driver); } } private applyStoredTheme(driverId: string): void { const raw = storage.get(themeStorageKey(driverId)); if (raw === 'light' || raw === 'dark') this.theme = raw; } private mapDriver(raw: Record): Driver { return { id: String(raw.id), name: raw.name as string, vehicleType: raw.vehicleType as Driver['vehicleType'], vehicleNumber: raw.vehicleNumber as string, routeNumber: raw.routeNumber as string, routeId: (raw.routeId as string) ?? undefined, shiftStart: (raw.shiftStart as string) || '08:00', dispatcherName: (raw.dispatcherName as string) ?? undefined, deviationSettings: (raw.deviationSettings as Driver['deviationSettings']) ?? undefined, }; } async login(employeeId: string, pin: string): Promise { this.loginError = null; this.loginLoading = true; this.sessionKicked = false; try { const { driver, token } = await loginByPin(employeeId, pin); persistSession(token, driver); runInAction(() => { this.driver = driver; this.messageStore.reset(); }); this.startShift(); return true; } catch (error) { runInAction(() => { this.loginError = error instanceof Error ? error.message : 'Ошибка входа'; }); return false; } finally { runInAction(() => (this.loginLoading = false)); } } loginSmsVerified(data: { token: string; driver: Record }): void { const driver = this.mapDriver(data.driver); persistSession(data.token, driver); this.driver = driver; this.messageStore.reset(); this.loginError = null; this.startShift(); } async loginMrm(login: string, password: string): Promise { this.loginError = null; this.loginLoading = true; try { const admin = await loginMrmRequest(login, password); persistMrmAdmin(admin); runInAction(() => { this.mrmAdmin = admin; this.screen = 'main'; }); this.startSessionLoop(); } catch (error) { runInAction(() => { this.loginError = error instanceof Error ? error.message : 'Ошибка входа'; }); } finally { runInAction(() => (this.loginLoading = false)); } } startShift(): void { this.screen = 'main'; this.isMoving = true; this.startSessionLoop(); } setScreen(screen: AppScreen): void { this.screen = screen; } async logout(): Promise { const token = storedToken(); clearSession(); clearMrmAdmin(); this.stopSessionLoop(); this.messageStore.stop(); runInAction(() => { this.driver = null; this.mrmAdmin = null; this.messageStore.reset(); this.screen = 'login'; this.isMoving = false; }); await logoutRequest(token); } dismissSessionKicked(): void { this.sessionKicked = false; } toggleTheme(): void { this.theme = this.theme === 'light' ? 'dark' : 'light'; this.isDark = this.theme === 'dark'; this.persistTheme(); appEvents.emit('theme:changed', this.theme); } setTheme(theme: ThemeMode): void { this.theme = theme; this.isDark = this.theme === 'dark'; this.persistTheme(); } private persistTheme(): void { if (!this.driver) return; storage.set(themeStorageKey(this.driver.id), this.theme); } get sessionStartedAt(): number | null { return sessionStartedAt(); } } export const sessionStore = new SessionStore();