"use client" import React, { createContext, useContext, useReducer, useEffect, useCallback } from 'react' import { DeviceRegistrationSession, RegistrationStatus, DeviceDto, InitiateRegistrationRequest, WebSocketDeviceEvent } from '../types/device' import { devicesApi } from '../services/devices' import { deviceWebSocketService, WebSocketEventHandlers } from '../services/websocket' import { useAuth } from './auth-context' // State interface interface DeviceRegistrationState { currentSession: DeviceRegistrationSession | null registeredDevices: DeviceDto[] isLoading: boolean error: string | null wsConnected: boolean qrCodeUrl: string | null } // Action types type DeviceRegistrationAction = | { type: 'SET_LOADING'; payload: boolean } | { type: 'SET_ERROR'; payload: string | null } | { type: 'SET_CURRENT_SESSION'; payload: DeviceRegistrationSession | null } | { type: 'SET_REGISTERED_DEVICES'; payload: DeviceDto[] } | { type: 'ADD_DEVICE'; payload: DeviceDto } | { type: 'UPDATE_DEVICE'; payload: DeviceDto } | { type: 'REMOVE_DEVICE'; payload: string } | { type: 'SET_WS_CONNECTED'; payload: boolean } | { type: 'SET_QR_CODE_URL'; payload: string | null } | { type: 'UPDATE_SESSION_STATUS'; payload: RegistrationStatus } | { type: 'RESET_STATE' } // Initial state const initialState: DeviceRegistrationState = { currentSession: null, registeredDevices: [], isLoading: false, error: null, wsConnected: false, qrCodeUrl: null, } // Reducer function deviceRegistrationReducer( state: DeviceRegistrationState, action: DeviceRegistrationAction ): DeviceRegistrationState { switch (action.type) { case 'SET_LOADING': return { ...state, isLoading: action.payload } case 'SET_ERROR': return { ...state, error: action.payload, isLoading: false } case 'SET_CURRENT_SESSION': return { ...state, currentSession: action.payload } case 'SET_REGISTERED_DEVICES': return { ...state, registeredDevices: action.payload } case 'ADD_DEVICE': return { ...state, registeredDevices: [...state.registeredDevices, action.payload] } case 'UPDATE_DEVICE': return { ...state, registeredDevices: state.registeredDevices.map(device => device.id === action.payload.id ? action.payload : device ) } case 'REMOVE_DEVICE': return { ...state, registeredDevices: state.registeredDevices.filter( device => device.id !== action.payload ) } case 'SET_WS_CONNECTED': return { ...state, wsConnected: action.payload } case 'SET_QR_CODE_URL': return { ...state, qrCodeUrl: action.payload } case 'UPDATE_SESSION_STATUS': return { ...state, currentSession: state.currentSession ? { ...state.currentSession, status: action.payload } : null } case 'RESET_STATE': return initialState default: return state } } // Context interface interface DeviceRegistrationContextType { state: DeviceRegistrationState // Registration actions initiateRegistration: (request: InitiateRegistrationRequest) => Promise cancelRegistration: () => Promise refreshSession: () => Promise // Device management actions refreshDevices: () => Promise updateDevice: (deviceId: string, updates: Partial) => Promise deleteDevice: (deviceId: string) => Promise // WebSocket actions connectWebSocket: () => Promise disconnectWebSocket: () => void // Utility actions clearError: () => void resetState: () => void } const DeviceRegistrationContext = createContext(undefined) // Provider component interface DeviceRegistrationProviderProps { children: React.ReactNode } export function DeviceRegistrationProvider({ children }: DeviceRegistrationProviderProps) { const [state, dispatch] = useReducer(deviceRegistrationReducer, initialState) const { user, isAuthenticated } = useAuth() // WebSocket event handlers const wsEventHandlers: WebSocketEventHandlers = { onConnect: () => { dispatch({ type: 'SET_WS_CONNECTED', payload: true }) }, onDisconnect: () => { dispatch({ type: 'SET_WS_CONNECTED', payload: false }) }, onError: (error) => { console.error('WebSocket error:', error) dispatch({ type: 'SET_ERROR', payload: error.message }) }, onDeviceConnected: (data) => { if (state.currentSession && data.sessionId === state.currentSession.id) { dispatch({ type: 'UPDATE_SESSION_STATUS', payload: RegistrationStatus.DEVICE_CONNECTED }) } }, onRegistrationCompleted: (data) => { if (state.currentSession && data.sessionId === state.currentSession.id) { dispatch({ type: 'UPDATE_SESSION_STATUS', payload: RegistrationStatus.COMPLETED }) // Add the new device to the list if (data.device) { dispatch({ type: 'ADD_DEVICE', payload: data.device }) } // Clear current session after a delay setTimeout(() => { dispatch({ type: 'SET_CURRENT_SESSION', payload: null }) dispatch({ type: 'SET_QR_CODE_URL', payload: null }) }, 2000) } }, onRegistrationFailed: (data) => { if (state.currentSession && data.sessionId === state.currentSession.id) { dispatch({ type: 'UPDATE_SESSION_STATUS', payload: RegistrationStatus.FAILED }) dispatch({ type: 'SET_ERROR', payload: data.error || 'Registration failed' }) } }, onDeviceStatusChanged: (data) => { // Update device status in the list dispatch({ type: 'UPDATE_DEVICE', payload: data.device }) }, } // Initialize WebSocket event handlers useEffect(() => { deviceWebSocketService.setEventHandlers(wsEventHandlers) }, [state.currentSession?.id]) // Connect WebSocket when authenticated useEffect(() => { if (isAuthenticated && !state.wsConnected) { connectWebSocket() } else if (!isAuthenticated && state.wsConnected) { disconnectWebSocket() } }, [isAuthenticated]) // Load devices on mount when authenticated useEffect(() => { if (isAuthenticated) { refreshDevices() } }, [isAuthenticated]) // Cleanup on unmount useEffect(() => { return () => { disconnectWebSocket() } }, []) // Actions implementation const initiateRegistration = useCallback(async (request: InitiateRegistrationRequest) => { dispatch({ type: 'SET_LOADING', payload: true }) dispatch({ type: 'SET_ERROR', payload: null }) try { const response = await devicesApi.initiateRegistration(request) // Transform the response into a session object const session: DeviceRegistrationSession = { id: response.claim_id, userProfileId: user?.id || '', registrationCode: response.claim_token, pinCode: response.fallback_pin, status: RegistrationStatus.WAITING_FOR_DEVICE, expiresAt: response.expires_at, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } dispatch({ type: 'SET_CURRENT_SESSION', payload: session }) dispatch({ type: 'SET_QR_CODE_URL', payload: response.qr_code_url }) // Join the registration session room if (state.wsConnected) { deviceWebSocketService.joinRegistrationSession(response.claim_id) } } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to initiate registration' }) } finally { dispatch({ type: 'SET_LOADING', payload: false }) } }, [state.wsConnected, user?.id]) const cancelRegistration = useCallback(async () => { if (!state.currentSession) return dispatch({ type: 'SET_LOADING', payload: true }) try { await devicesApi.cancelRegistration(state.currentSession.id) // Leave the registration session room if (state.wsConnected) { deviceWebSocketService.leaveRegistrationSession(state.currentSession.id) } dispatch({ type: 'SET_CURRENT_SESSION', payload: null }) dispatch({ type: 'SET_QR_CODE_URL', payload: null }) } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to cancel registration' }) } finally { dispatch({ type: 'SET_LOADING', payload: false }) } }, [state.currentSession?.id, state.wsConnected]) const refreshSession = useCallback(async () => { if (!state.currentSession) return try { const session = await devicesApi.getRegistrationSession(state.currentSession.id) dispatch({ type: 'SET_CURRENT_SESSION', payload: session }) } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to refresh session' }) } }, [state.currentSession?.id]) const refreshDevices = useCallback(async () => { if (!isAuthenticated) return try { const response = await devicesApi.getDevices() dispatch({ type: 'SET_REGISTERED_DEVICES', payload: response.devices }) } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to load devices' }) } }, [isAuthenticated]) const updateDevice = useCallback(async (deviceId: string, updates: Partial) => { dispatch({ type: 'SET_LOADING', payload: true }) try { const updatedDevice = await devicesApi.updateDevice(deviceId, updates) dispatch({ type: 'UPDATE_DEVICE', payload: updatedDevice }) } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to update device' }) } finally { dispatch({ type: 'SET_LOADING', payload: false }) } }, []) const deleteDevice = useCallback(async (deviceId: string) => { dispatch({ type: 'SET_LOADING', payload: true }) try { await devicesApi.deleteDevice(deviceId) dispatch({ type: 'REMOVE_DEVICE', payload: deviceId }) } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to delete device' }) } finally { dispatch({ type: 'SET_LOADING', payload: false }) } }, []) const connectWebSocket = useCallback(async () => { try { const token = localStorage.getItem('accessToken') if (!token) throw new Error('No authentication token') await deviceWebSocketService.connect(token) } catch (error) { dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Failed to connect to real-time updates' }) } }, []) const disconnectWebSocket = useCallback(() => { deviceWebSocketService.disconnect() dispatch({ type: 'SET_WS_CONNECTED', payload: false }) }, []) const clearError = useCallback(() => { dispatch({ type: 'SET_ERROR', payload: null }) }, []) const resetState = useCallback(() => { dispatch({ type: 'RESET_STATE' }) }, []) const contextValue: DeviceRegistrationContextType = { state, initiateRegistration, cancelRegistration, refreshSession, refreshDevices, updateDevice, deleteDevice, connectWebSocket, disconnectWebSocket, clearError, resetState, } return ( {children} ) } // Hook for using the context export function useDeviceRegistration() { const context = useContext(DeviceRegistrationContext) if (context === undefined) { throw new Error('useDeviceRegistration must be used within a DeviceRegistrationProvider') } return context }