diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 673cb9b..ce7e4cc 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -30,7 +30,7 @@ It assumes: - [x] Backend migrated into `backend/` and passing typecheck, build, test, and backend verification gates - [x] Web migrated into `web/` with shared runtime, shared kill-switch gate, shared telemetry bootstrap, and normalized backend URL resolution - [x] Mobile migrated into `mobile/` with product identity, shared runtime bootstrap, launch-time kill-switch gate, transitional Supabase auth, live backend polling plus websocket-backed updates, startup/error telemetry capture, secure session storage with invalidation handling, and explicit degraded/offline status surfacing -- [-] DRY cleanup completed for runtime/config/bootstrap concerns and shared Supabase bootstrap, but not yet for all auth/session internals +- [-] DRY cleanup completed for runtime/config/bootstrap concerns, shared Supabase bootstrap, and shared websocket auth helpers, but not yet for all auth/session internals - [!] Full common-platform auth replacement remains a follow-up for web and mobile; current implementation uses transitional Supabase-backed auth to stay compatible with the backend's current JWT boundary ## 3. Guiding Rules @@ -261,7 +261,7 @@ Move the web dashboard onto the new repo and onto shared platform bootstrap patt - [x] Move runtime config to common conventions - [x] Define product config - [x] Define API client and websocket client -- [ ] Standardize websocket token propagation +- [x] Standardize websocket token propagation - [x] Integrate maintenance and kill-switch UX states - [x] Define shell-level maintenance and kill-switch behavior - [ ] Classify each current web tab as ship, defer, or redesign diff --git a/mobile/providers/TradingDataProvider.tsx b/mobile/providers/TradingDataProvider.tsx index 50081cc..7b77cb4 100644 --- a/mobile/providers/TradingDataProvider.tsx +++ b/mobile/providers/TradingDataProvider.tsx @@ -4,6 +4,7 @@ import { io, type Socket } from 'socket.io-client'; import { mobileRuntime } from '@/lib/runtime'; import { mobileTelemetry, trackMobileError } from '@/lib/telemetry'; import { useMobileAuth } from '@/providers/MobileAuthProvider'; +import { buildTradingSocketOptions, isUnauthorizedSocketError } from '../../shared/realtime.js'; type HealthSnapshot = { tradingControl?: { @@ -223,10 +224,7 @@ export function TradingDataProvider({ children }: { children: ReactNode }) { })); }; - socket = io(tradingSocketUrl, { - transports: ['polling', 'websocket'], - auth: { token: accessToken }, - }); + socket = io(tradingSocketUrl, buildTradingSocketOptions(accessToken)); socket.on('connect', () => { setConnected(true); @@ -242,7 +240,7 @@ export function TradingDataProvider({ children }: { children: ReactNode }) { socket.on('connect_error', (socketError) => { setError(socketError.message); trackMobileError('realtime', 'socket_connect_failed', socketError); - if (socketError.message.toLowerCase().includes('unauthorized')) { + if (isUnauthorizedSocketError(socketError.message)) { void invalidateSession(socketError.message); } }); diff --git a/shared/realtime.ts b/shared/realtime.ts new file mode 100644 index 0000000..a2a8498 --- /dev/null +++ b/shared/realtime.ts @@ -0,0 +1,12 @@ +export function buildTradingSocketOptions(token: string, socketPath?: string) { + return { + transports: ['polling', 'websocket'] as ('polling' | 'websocket')[], + auth: { token }, + ...(socketPath ? { path: socketPath } : {}), + }; +} + +export function isUnauthorizedSocketError(message: string) { + const normalizedMessage = message.toLowerCase(); + return normalizedMessage.includes('unauthorized') || normalizedMessage.includes('invalid token'); +} diff --git a/web/src/hooks/useWebSocket.ts b/web/src/hooks/useWebSocket.ts index 1c89fb6..7ffc2b3 100644 --- a/web/src/hooks/useWebSocket.ts +++ b/web/src/hooks/useWebSocket.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { io, Socket } from 'socket.io-client'; +import { buildTradingSocketOptions } from '../../../shared/realtime.js'; import { supabase } from '../lib/supabaseClient'; export interface TradingControlSnapshot { @@ -284,11 +285,7 @@ export const useWebSocket = (url: string) => { return; } - const socketOptions = { - transports: ['polling', 'websocket'] as ('polling' | 'websocket')[], - auth: { token }, - ...(import.meta.env.VITE_SOCKET_PATH ? { path: import.meta.env.VITE_SOCKET_PATH } : {}) - }; + const socketOptions = buildTradingSocketOptions(token, import.meta.env.VITE_SOCKET_PATH); newSocket = io(url, socketOptions);