diff --git a/src/components/RealTimeSettings.jsx b/src/components/RealTimeSettings.jsx index 882a770..85ea585 100644 --- a/src/components/RealTimeSettings.jsx +++ b/src/components/RealTimeSettings.jsx @@ -1,46 +1,26 @@ import { Sync, SyncDisabled } from '@mui/icons-material' -import { - Box, - Card, - Chip, - FormControl, - FormHelperText, - FormLabel, - Option, - Select, - Typography, -} from '@mui/joy' +import { Box, Card, Chip, FormHelperText, Switch, Typography } from '@mui/joy' import { useState } from 'react' -import { useWebSocketContext } from '../contexts/WebSocketContext' import { useSSEContext } from '../hooks/useSSEContext' import { useUserProfile } from '../queries/UserQueries' import { isPlusAccount } from '../utils/Helpers' import SSEConnectionStatus from './SSEConnectionStatus' -import WebSocketConnectionStatus from './WebSocketConnectionStatus' const REALTIME_TYPES = { DISABLED: 'disabled', - WEBSOCKET: 'websocket', SSE: 'sse', } const RealTimeSettings = () => { const { data: userProfile } = useUserProfile() - // WebSocket context - const webSocketContext = useWebSocketContext() - // SSE context const sseContext = useSSEContext() // Get current realtime type from localStorage const getCurrentRealtimeType = () => { - const wsEnabled = localStorage.getItem('websocket_enabled') !== 'false' const sseEnabled = localStorage.getItem('sse_enabled') === 'true' - - if (sseEnabled) return REALTIME_TYPES.SSE - if (wsEnabled) return REALTIME_TYPES.WEBSOCKET - return REALTIME_TYPES.DISABLED + return sseEnabled ? REALTIME_TYPES.SSE : REALTIME_TYPES.DISABLED } const [realtimeType, setRealtimeType] = useState(getCurrentRealtimeType()) @@ -55,21 +35,11 @@ const RealTimeSettings = () => { // Update localStorage and toggle connections switch (newValue) { case REALTIME_TYPES.DISABLED: - localStorage.setItem('websocket_enabled', 'false') - localStorage.setItem('sse_enabled', 'false') - webSocketContext.disconnect() - sseContext.disconnect() - break - case REALTIME_TYPES.WEBSOCKET: - localStorage.setItem('websocket_enabled', 'true') localStorage.setItem('sse_enabled', 'false') sseContext.disconnect() - webSocketContext.connect() break case REALTIME_TYPES.SSE: - localStorage.setItem('websocket_enabled', 'false') localStorage.setItem('sse_enabled', 'true') - webSocketContext.disconnect() sseContext.connect() break } @@ -77,8 +47,6 @@ const RealTimeSettings = () => { const getCurrentContext = () => { switch (realtimeType) { - case REALTIME_TYPES.WEBSOCKET: - return webSocketContext case REALTIME_TYPES.SSE: return sseContext default: @@ -99,31 +67,26 @@ const RealTimeSettings = () => { } if (realtimeType === REALTIME_TYPES.DISABLED) { - return 'Real-time updates are disabled. Enable WebSocket or SSE to see live changes when you or other circle members complete, skip, or modify chores.' + return 'Real-time updates are disabled. Enable them to see live changes when you or other circle members complete, skip, or modify chores.' } - const typeLabel = - realtimeType === REALTIME_TYPES.WEBSOCKET ? 'WebSocket' : 'SSE' - if (context.isConnected) { - return `Real-time updates (${typeLabel}) are working. You'll see live changes when you or other circle members complete, skip, or modify chores.` + return "Real-time updates are working. You'll see live changes when you or other circle members complete, skip, or modify chores." } if (context.isConnecting) { - return `Connecting to real-time updates (${typeLabel})...` + return 'Connecting to real-time updates...' } if (context.error) { - return `Real-time updates (${typeLabel}) are enabled but not working: ${context.error}` + return `Real-time updates are enabled but not working: ${context.error}` } - return `Real-time updates (${typeLabel}) are enabled but not currently connected.` + return 'Real-time updates are enabled but not currently connected.' } const getConnectionStatusComponent = () => { switch (realtimeType) { - case REALTIME_TYPES.WEBSOCKET: - return case REALTIME_TYPES.SSE: return default: @@ -133,7 +96,7 @@ const RealTimeSettings = () => { return ( - + {realtimeType !== REALTIME_TYPES.DISABLED && isPlusAccount(userProfile) ? ( @@ -141,24 +104,44 @@ const RealTimeSettings = () => { )} - - Real-time Updates - {!isPlusAccount(userProfile) && ( - - Plus Feature - - )} - + + + Real-time Updates + {!isPlusAccount(userProfile) && ( + + Plus Feature + + )} + + + { + handleRealtimeTypeChange( + null, + e.target.checked + ? REALTIME_TYPES.SSE + : REALTIME_TYPES.DISABLED, + ) + }} + disabled={!isPlusAccount(userProfile)} + inputProps={{ 'aria-label': 'Enable Real-time Updates' }} + /> + Get instant notifications when chores are updated - {realtimeType !== REALTIME_TYPES.DISABLED && - isPlusAccount(userProfile) && - getConnectionStatusComponent()} - + {/* Real-time Connection Type @@ -175,7 +158,7 @@ const RealTimeSettings = () => { - + */} {getStatusDescription()} @@ -185,19 +168,7 @@ const RealTimeSettings = () => { Status: - - {context.getConnectionStatus()} - + {getConnectionStatusComponent()} {context.error && ( {context.error} @@ -213,30 +184,6 @@ const RealTimeSettings = () => { complete, skip, or modify chores. )} - - {realtimeType !== REALTIME_TYPES.DISABLED && - isPlusAccount(userProfile) && ( - - - Connection Types: - - - • WebSocket: Traditional bi-directional real-time - connection - - - • SSE: Server-Sent Events - lighter weight, - one-way updates from server - - - )} ) } diff --git a/src/hooks/useSSE.js b/src/hooks/useSSE.js index 455f482..4b29207 100644 --- a/src/hooks/useSSE.js +++ b/src/hooks/useSSE.js @@ -1,8 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' import { EventSourcePolyfill } from 'event-source-polyfill' import { useCallback, useEffect, useRef, useState } from 'react' -import { useUserProfile } from '../queries/UserQueries' -import { isPlusAccount } from '../utils/Helpers' import { apiManager, isTokenValid } from '../utils/TokenManager' const SSE_STATES = { @@ -29,16 +27,8 @@ export const useSSE = () => { const heartbeatMonitorRef = useRef(null) const queryClient = useQueryClient() - const { data: userProfile } = useUserProfile() const getSSEUrl = useCallback(() => { - if (!userProfile?.circleID) { - console.log( - 'SSE: User not part of any circle - real-time features unavailable', - ) - return null - } - const token = localStorage.getItem('ca_token') if (!token || !isTokenValid()) { console.log('SSE: No valid authentication token') @@ -48,12 +38,11 @@ export const useSSE = () => { // Get the API URL from apiManager const apiUrl = apiManager.getApiURL() // e.g., "http://localhost:8080/api/v1" - // Build SSE URL - const sseUrl = `${apiUrl}/realtime/sse?circleId=${userProfile.circleID}` + // Build SSE URL - let backend determine circle from authenticated user + const sseUrl = `${apiUrl}/realtime/sse` - console.log('SSE: Generated URL:', sseUrl) return { url: sseUrl, token } - }, [userProfile]) + }, []) const handleSSEMessage = useCallback( event => { @@ -61,8 +50,6 @@ export const useSSE = () => { const eventData = JSON.parse(event.data) setLastEvent(eventData) - console.log('SSE event received:', eventData.type, eventData) - // Update heartbeat timestamp if (eventData.type === 'heartbeat') { lastHeartbeatRef.current = Date.now() @@ -152,14 +139,8 @@ export const useSSE = () => { // Create connect function that can be called from anywhere const connect = useCallback(() => { - console.log('SSE connect called') - console.log('SSE current state:', eventSourceRef.current?.readyState) - - // Circuit breaker: prevent infinite reconnection loops if (isCircuitBreakerOpen) { - console.warn( - 'SSE: Circuit breaker is open, preventing connection attempt', - ) + console.log('SSE: Circuit breaker is open, preventing connection attempt') setError( 'Connection blocked due to repeated failures. Please try again later.', ) @@ -209,8 +190,9 @@ export const useSSE = () => { setConnectionState(SSE_STATES.CONNECTING) isManuallyClosedRef.current = false - // Option 1: Use EventSource polyfill with Authorization header (Recommended) - // This is the most secure and standard way to authenticate SSE connections + // here use EventSource polyfill with Authorization header as the native EventSource does not support headers + // the other option was to pass via query param which is less secure and also there. + // TODO: use cookie-based once/if at all i move from local storage to httpOnly cookies. eventSourceRef.current = new EventSourcePolyfill(sseConfig.url, { headers: { Authorization: `Bearer ${sseConfig.token}`, @@ -306,7 +288,7 @@ export const useSSE = () => { setError('Failed to establish connection') setConnectionState(SSE_STATES.CLOSED) } - }, [getSSEUrl, handleSSEMessage, stopHeartbeatMonitor]) + }, [getSSEUrl, handleSSEMessage, stopHeartbeatMonitor, isCircuitBreakerOpen]) const disconnect = useCallback(() => { isManuallyClosedRef.current = true @@ -330,12 +312,10 @@ export const useSSE = () => { enabled => { console.log('SSE toggleSSEEnabled called:', { enabled, - userProfile: userProfile?.circleID, isTokenValid: isTokenValid(), - isPlusAccount: isPlusAccount(userProfile), }) localStorage.setItem('sse_enabled', enabled.toString()) - if (enabled && userProfile?.circleID && isTokenValid()) { + if (enabled && isTokenValid()) { console.log('SSE toggleSSEEnabled: Calling connect()') connect() } else { @@ -343,38 +323,27 @@ export const useSSE = () => { disconnect() } }, - [connect, disconnect, userProfile], + [connect, disconnect], ) const isSSEEnabled = useCallback(() => { return localStorage.getItem('sse_enabled') === 'true' }, []) - // Auto-connect when user profile is available and token is valid + // Auto-connect when SSE is enabled and token is valid useEffect(() => { console.log('SSE auto-connect effect triggered') - console.log('UserProfile:', userProfile) - console.log('circleID:', userProfile?.circleID) console.log('Token valid:', isTokenValid()) - console.log('Is Plus account:', isPlusAccount(userProfile)) // Check if SSE is enabled in settings const isSSEEnabledSetting = localStorage.getItem('sse_enabled') === 'true' console.log('SSE enabled in settings:', isSSEEnabledSetting) - if ( - userProfile?.circleID && - isTokenValid() && - isSSEEnabledSetting && - isPlusAccount(userProfile) - ) { + if (isTokenValid() && isSSEEnabledSetting) { console.log('SSE: Conditions met, attempting to connect') connect() } else { console.log('SSE: Conditions not met, disconnecting') - if (!isPlusAccount(userProfile)) { - console.log('SSE: Not a Plus account - feature unavailable') - } disconnect() } @@ -383,7 +352,7 @@ export const useSSE = () => { disconnect() } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userProfile?.circleID, userProfile?.expiration]) // Only depend on essential userProfile fields + }, []) // Only run once on mount // Cleanup timeouts on unmount useEffect(() => { @@ -410,10 +379,8 @@ export const useSSE = () => { const isSSEEnabledSetting = localStorage.getItem('sse_enabled') === 'true' if ( - userProfile?.circleID && isTokenValid() && isSSEEnabledSetting && - isPlusAccount(userProfile) && connectionState !== SSE_STATES.OPEN ) { connect() @@ -426,8 +393,7 @@ export const useSSE = () => { return () => { document.removeEventListener('visibilitychange', handleVisibilityChange) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userProfile?.circleID, userProfile?.expiration, connectionState]) + }, [connectionState, connect]) return { connectionState, diff --git a/src/hooks/useWebSocket.js b/src/hooks/useWebSocket.js index 7644401..2850258 100644 --- a/src/hooks/useWebSocket.js +++ b/src/hooks/useWebSocket.js @@ -1,7 +1,5 @@ import { useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useRef, useState } from 'react' -import { useUserProfile } from '../queries/UserQueries' -import { isPlusAccount } from '../utils/Helpers' import { apiManager, isTokenValid } from '../utils/TokenManager' const WEBSOCKET_STATES = { @@ -26,16 +24,8 @@ export const useWebSocket = () => { const isManuallyClosedRef = useRef(false) const queryClient = useQueryClient() - const { data: userProfile } = useUserProfile() const getWebSocketUrl = useCallback(() => { - if (!userProfile?.circleID) { - console.log( - 'WebSocket: User not part of any circle - real-time features unavailable', - ) - return null - } - const token = localStorage.getItem('ca_token') if (!token || !isTokenValid()) { console.log('WebSocket: No valid authentication token') @@ -55,10 +45,11 @@ export const useWebSocket = () => { wsUrl = `${isHttps ? 'wss:' : 'ws:'}//${wsUrl}` } - wsUrl = `${wsUrl}/api/v1/realtime/ws?token=${token}&circleId=${userProfile.circleID}` + // Let backend determine circle from authenticated user + wsUrl = `${wsUrl}/api/v1/realtime/ws?token=${token}` return wsUrl - }, [userProfile]) + }, []) const handleWebSocketMessage = useCallback( event => { @@ -250,39 +241,31 @@ export const useWebSocket = () => { const toggleWebSocketEnabled = useCallback( enabled => { localStorage.setItem('websocket_enabled', enabled.toString()) - if (enabled && userProfile?.circleID && isTokenValid()) { + if (enabled && isTokenValid()) { connect() } else { disconnect() } }, - [connect, disconnect, userProfile], + [connect, disconnect], ) const isWebSocketEnabled = useCallback(() => { return localStorage.getItem('websocket_enabled') !== 'false' }, []) - // Auto-connect when user profile is available and token is valid + // Auto-connect when WebSocket is enabled and token is valid useEffect(() => { // Check if WebSocket is enabled in settings const isWebSocketEnabledSetting = localStorage.getItem('websocket_enabled') !== 'false' console.log('WebSocket enabled in settings:', isWebSocketEnabledSetting) - if ( - userProfile?.circleID && - isTokenValid() && - isWebSocketEnabledSetting && - isPlusAccount(userProfile) - ) { + if (isTokenValid() && isWebSocketEnabledSetting) { console.log('WebSocket: Conditions met, attempting to connect') connect() } else { console.log('WebSocket: Conditions not met, disconnecting') - if (!isPlusAccount(userProfile)) { - console.log('WebSocket: Not a Plus account - feature unavailable') - } disconnect() } @@ -290,7 +273,8 @@ export const useWebSocket = () => { return () => { disconnect() } - }, [userProfile, connect, disconnect]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) // Only run once on mount // Cleanup timeouts on unmount useEffect(() => { diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx index b6720ff..3399497 100644 --- a/src/views/Authorization/LoginView.jsx +++ b/src/views/Authorization/LoginView.jsx @@ -21,7 +21,6 @@ import { LoginSocialGoogle } from 'reactjs-social-login' import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config' import Logo from '../../Logo' import { useResource } from '../../queries/ResourceQueries' -import { useUserProfile } from '../../queries/UserQueries' import { useNotification } from '../../service/NotificationProvider' import { login } from '../../utils/Fetcher' import { apiManager } from '../../utils/TokenManager' @@ -29,7 +28,7 @@ import MFAVerificationModal from './MFAVerificationModal' const LoginView = () => { // Only fetch user profile if token is valid to prevent unnecessary queries - const { data: userProfileData } = useUserProfile() + // const { data: userProfileData } = useUserProfile() const [userProfile, setUserProfile] = useState(null) const [username, setUsername] = useState('') const [password, setPassword] = useState('') diff --git a/src/views/ChoreEdit/ThingTriggerSection.jsx b/src/views/ChoreEdit/ThingTriggerSection.jsx index 981f84b..e4c4d24 100644 --- a/src/views/ChoreEdit/ThingTriggerSection.jsx +++ b/src/views/ChoreEdit/ThingTriggerSection.jsx @@ -7,9 +7,6 @@ import { Chip, FormControl, Input, - ListItem, - ListItemContent, - ListItemDecorator, Option, Select, TextField, @@ -113,7 +110,7 @@ const ThingTriggerSection = ({ onChange={(e, newValue) => setSelectedThing(newValue)} getOptionLabel={option => option.name} renderOption={(props, option) => ( - + - + {option.name} - - + + type: {option.type}{' '} state: {option.state} - + - + )} renderInput={params => ( diff --git a/src/views/Chores/MultiSelectHelp.jsx b/src/views/Chores/MultiSelectHelp.jsx index 18d6616..11df46e 100644 --- a/src/views/Chores/MultiSelectHelp.jsx +++ b/src/views/Chores/MultiSelectHelp.jsx @@ -110,7 +110,7 @@ const MultiSelectHelp = ({ isVisible = true }) => { description='Mark selected tasks as completed' /> diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx index 96a0fd5..723b8cf 100644 --- a/src/views/Chores/MyChores.jsx +++ b/src/views/Chores/MyChores.jsx @@ -825,18 +825,6 @@ const MyChores = () => { return ( <> - - {JSON.stringify(userProfile) === 'null'} - - - {userLabelsLoading} - - - {performers.length === 0} - - - {choresLoading} - ) @@ -1227,7 +1215,6 @@ const MyChores = () => { }, }} > - {/* Primary Actions - Safe operations */} - - - {/* Visual separator for destructive actions */} - - - {/* Secondary Actions - Less destructive */} - {/* Most destructive action - visually distinct */}