Работа с данными
saasApi клиент
Все HTTP-запросы к saas-api проходят через единый синглтон saasApi (src/features/saas-api/client.ts).
Особенности клиента
- JWT-токены хранятся в
expo-secure-store - Автоматический refresh токена за 5 минут до истечения
- Retry логика: при 401 — refresh токена и повтор запроса
- Таймаут: 30 секунд (обычные запросы), 120 секунд (upload)
- Заголовок
x-request-idна каждом запросе
API-группы
| Группа | Методы |
|---|---|
| Auth | signIn, signUp, getMe, signOut, forgotPassword, resetPassword, verifyCode, getGoogleAuthUrl, handleOAuthCallback |
| Companies | getCompanies, createCompany, updateCompany, deleteCompany, getCompanyUsers, sendInvitation, getAccessTokens |
| Assistants | getAssistants, getAssistant, createAssistant, updateAssistant, deleteAssistant, duplicateAssistant, applyTemplate |
| Knowledge | getKnowledgeItems, createKnowledgeItem, updateKnowledgeItem, deleteKnowledgeItem |
| Tools | getAssistantTools, createAssistantTool, updateAssistantTool, deleteAssistantTool |
| Folders | getFolders, createFolder, updateFolder, deleteFolder |
| Chats | getChats, getChat, toggleAiControl, getMessages, createMessage, deleteMessage |
| Phones | getPhones, createPhone, deletePhone, attachPhone, detachPhone |
| Integrations | getIntegrations, connectIntegration, Telegram (QR/code), Instagram/Messenger OAuth, WhatsApp, Viber, ElevenLabs, Google Sheets |
| Billing | getTariffs, createSubscription, createPayment, setTariff, cancelSubscription |
| Files | uploadFile (FormData, 120s timeout) |
Обработка ошибок
- Сервисный слой возвращает
nullпри ошибке (не бросает исключения) - 401: автоматический refresh + повторный запрос; при повторном 401 — принудительный логаут
- 403: помечается
isForbidden: true - Try-catch только для HTTP-запросов и операций сохранения
WebSocket (socket.io)
Подключение
WebSocket клиент (src/features/chat/websocket/WebSocketClient.ts) — синглтон.
| Параметр | Значение |
|---|---|
| URL | EXPO_PUBLIC_WS_URL |
| Path | /ws |
| Transport | websocket only |
| Auth | token + companyId (headers + query) |
| Reconnect | auto, exponential backoff (1s → 10s) |
Жизненный цикл
connect(companyId)— подключение с контекстом компании- Автоматическое присоединение к комнате компании
joinChat(chatId)— вход в комнату чата- Получение событий → обновление Zustand stores
leaveChat(chatId)— выход из комнаты чатаdisconnect()— при смене компании или логауте
Оптимизация батареи
- WebSocket отключается при уходе приложения в фон
- Автоматическое переподключение при возврате в foreground
События
Отправляемые (клиент → сервер):
| Событие | Описание |
|---|---|
chat:join | Вход в комнату чата |
chat:leave | Выход из комнаты чата |
typing:start | Начало набора |
typing:stop | Конец набора |
Получаемые (сервер → клиент):
| Событие | Payload |
|---|---|
message:new | { message: Message } |
message:updated | { message: Message } |
message:deleted | { messageId, chatId } |
chat:updated | { chat: Chat } |
typing:start | { chatId, userId } |
typing:stop | { chatId, userId } |
telegram:qr-updated | { integrationId, qrCodeUrl } |
telegram:qr-needs-2fa | { integrationId } |
telegram:qr-completed | { integrationId } |
Паттерн query + mutation
typescript
// Запрос данных
const { data, isLoading } = useQuery({
queryKey: ['chats', companyId],
queryFn: () => saasApi.getChats({ companyId }),
enabled: !!companyId,
});
// Мутация с инвалидацией
const toggleAi = useMutation({
mutationFn: ({ chatId, enabled }) =>
saasApi.toggleAiControl(chatId, enabled),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['chats'] });
},
});