From de8e8bcf3e65fbfdeadb0087a9be4878f0fd084d Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 00:00:28 +0530 Subject: [PATCH 01/13] Add websocket reports listener --- src/components/WebSocketMessages.js | 441 ++++++++++++++++++ src/components/WebSocketStatus.js | 186 ++++++++ src/hooks/useWebSocket.js | 244 ++++++++++ src/screens/WebSocketDemoScreen.js | 306 +++++++++++++ src/services/WebSocketService.js | 671 ++++++++++++++++++++++++++++ 5 files changed, 1848 insertions(+) create mode 100644 src/components/WebSocketMessages.js create mode 100644 src/components/WebSocketStatus.js create mode 100644 src/hooks/useWebSocket.js create mode 100644 src/screens/WebSocketDemoScreen.js create mode 100644 src/services/WebSocketService.js diff --git a/src/components/WebSocketMessages.js b/src/components/WebSocketMessages.js new file mode 100644 index 0000000..a64cf9a --- /dev/null +++ b/src/components/WebSocketMessages.js @@ -0,0 +1,441 @@ +import React, { useEffect, useState } from "react"; +import { + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import { theme } from "../services/Common/theme"; + +const WebSocketMessages = ({ + url, + messageTypes = [], + isConnected, + subscribe, + unsubscribe, +}) => { + const [messages, setMessages] = useState([]); + const [filteredMessages, setFilteredMessages] = useState([]); + const [selectedType, setSelectedType] = useState("all"); + + // Subscribe to report messages from Go backend + useEffect(() => { + console.log("📱 [WebSocketMessages] useEffect triggered:", { + isConnected, + url, + messageTypes, + hasSubscribe: !!subscribe, + hasUnsubscribe: !!unsubscribe, + subscribeType: typeof subscribe, + unsubscribeType: typeof unsubscribe, + timestamp: new Date().toISOString(), + // Add detailed dependency tracking + isConnectedValue: isConnected, + urlValue: url, + messageTypesValue: messageTypes, + messageTypesLength: messageTypes.length, + messageTypesString: JSON.stringify(messageTypes), + }); + + if (isConnected && subscribe && unsubscribe) { + console.log( + "📱 [WebSocketMessages] WebSocket connected, subscribing to 'reports' type" + ); + + // Subscribe to the "reports" message type that Go backend sends + const subscriptionId = subscribe("reports", (payload, metadata) => { + console.log( + "📱 [WebSocketMessages] Reports subscription callback triggered:", + { + payload, + metadata, + payloadType: typeof payload, + payloadKeys: payload ? Object.keys(payload) : "N/A", + payloadIsNull: payload === null, + payloadIsUndefined: payload === undefined, + timestamp: new Date().toISOString(), + } + ); + + const newMessage = { + id: Date.now(), + type: "reports", + timestamp: new Date(), + data: payload, + metadata, + }; + + console.log("📱 [WebSocketMessages] Created new message object:", { + newMessage, + dataField: newMessage.data, + dataType: typeof newMessage.data, + }); + + setMessages((prev) => { + const updatedMessages = [newMessage, ...prev.slice(0, 99)]; // Keep last 100 + console.log("📱 [WebSocketMessages] Updated messages state:", { + previousCount: prev.length, + newCount: updatedMessages.length, + firstMessage: updatedMessages[0], + }); + return updatedMessages; + }); + }); + + console.log( + "📱 [WebSocketMessages] Subscription created with ID:", + subscriptionId + ); + + return () => { + console.log( + "📱 [WebSocketMessages] useEffect cleanup running, removing subscription:", + subscriptionId, + "Reason: useEffect dependencies changed or component unmounting" + ); + unsubscribe("reports", subscriptionId); + }; + } else { + console.log( + "📱 [WebSocketMessages] WebSocket not connected or missing functions, skipping subscription" + ); + } + }, [isConnected, url, messageTypes]); // Restored dependencies now that re-render issue is fixed + + // Filter messages based on selected type + useEffect(() => { + if (selectedType === "all") { + setFilteredMessages(messages); + } else { + setFilteredMessages(messages.filter((msg) => msg.type === selectedType)); + } + }, [messages, selectedType]); + + const clearMessages = () => { + // clearMessageHistory(); // This function is no longer available from useWebSocket + setMessages([]); + setFilteredMessages([]); + }; + + const formatTimestamp = (timestamp) => { + return new Date(timestamp).toLocaleTimeString(); + }; + + const renderReportMessage = (message) => { + console.log( + "📱 [WebSocketMessages] renderReportMessage() called with message:", + { + message, + messageType: typeof message, + messageKeys: message ? Object.keys(message) : "N/A", + } + ); + + const { data } = message; + + console.log("📱 [WebSocketMessages] Extracted data from message:", { + data, + dataType: typeof data, + dataIsNull: data === null, + dataIsUndefined: data === undefined, + dataKeys: data ? Object.keys(data) : "N/A", + }); + + if (!data || !data.reports) { + console.log("📱 [WebSocketMessages] No report data found in message"); + return No report data; + } + + console.log("📱 [WebSocketMessages] Report data found:", { + reportsCount: data.reports.length, + reportsType: typeof data.reports, + firstReport: data.reports[0], + dataKeys: Object.keys(data), + }); + + return ( + + + 📊 Reports Batch ({data.count} reports) + + + Sequence: {data.fromSeq} - {data.toSeq} + + {data.reports.slice(0, 3).map((reportWithAnalysis, index) => { + console.log("📱 [WebSocketMessages] Rendering report:", { + index, + reportWithAnalysis, + reportKeys: reportWithAnalysis + ? Object.keys(reportWithAnalysis) + : "N/A", + }); + + return ( + + + ID: {reportWithAnalysis.report.id} + + + 📍 {reportWithAnalysis.report.latitude.toFixed(4)},{" "} + {reportWithAnalysis.report.longitude.toFixed(4)} + + + ⏰{" "} + {new Date(reportWithAnalysis.report.timestamp).toLocaleString()} + + {reportWithAnalysis.analysis && + reportWithAnalysis.analysis.length > 0 && ( + + 🔍 Analysis: {reportWithAnalysis.analysis[0].classification} + (Severity: {reportWithAnalysis.analysis[0].severityLevel}) + + )} + + ); + })} + {data.reports.length > 3 && ( + + ... and {data.reports.length - 3} more reports + + )} + + ); + }; + + const renderMessage = (message) => { + switch (message.type) { + case "reports": + return renderReportMessage(message); + default: + return ( + + {JSON.stringify(message.data || message, null, 2)} + + ); + } + }; + + if (!isConnected) { + return ( + + + 🔌 WebSocket not connected. Connect first to receive messages. + + + ); + } + + return ( + + {/* Message Type Filter */} + + setSelectedType("all")} + > + + All ({messages.length}) + + + setSelectedType("reports")} + > + + Reports ({messages.filter((m) => m.type === "reports").length}) + + + + + {/* Messages */} + + {filteredMessages.length === 0 ? ( + + 📭 No messages received yet. Reports will appear here when + broadcasted by the server. + + ) : ( + filteredMessages.map((message) => ( + + + {message.type} + + {formatTimestamp(message.timestamp)} + + + {renderMessage(message)} + + )) + )} + + + {/* Clear Button */} + {messages.length > 0 && ( + + Clear Messages + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.COLORS.BG || "#FFFFFF", + padding: 16, + }, + disconnectedText: { + textAlign: "center", + color: theme.COLORS.GRAY || "#9E9E9E", + fontSize: 16, + marginTop: 20, + }, + filterContainer: { + flexDirection: "row", + justifyContent: "space-around", + marginBottom: 12, + backgroundColor: theme.COLORS.GRAY || "#F5F5F5", + borderRadius: 8, + padding: 4, + }, + filterButton: { + paddingHorizontal: 15, + paddingVertical: 8, + borderRadius: 6, + }, + filterButtonActive: { + backgroundColor: theme.COLORS.PRIMARY || "#2196F3", + }, + filterButtonText: { + fontSize: 14, + fontWeight: "500", + color: theme.COLORS.TEXT || "#333333", + }, + filterButtonTextActive: { + color: "#FFFFFF", + }, + messagesContainer: { + flex: 1, + }, + messageContainer: { + backgroundColor: theme.COLORS.BG || "#F8F9FA", + borderRadius: 8, + padding: 12, + marginBottom: 10, + borderLeftWidth: 4, + borderLeftColor: theme.COLORS.PRIMARY || "#2196F3", + }, + messageHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 8, + }, + messageType: { + fontSize: 14, + fontWeight: "600", + color: theme.COLORS.PRIMARY || "#2196F3", + textTransform: "uppercase", + }, + messageTimestamp: { + fontSize: 12, + color: theme.COLORS.GRAY || "#9E9E9E", + }, + messageText: { + fontSize: 14, + color: theme.COLORS.TEXT || "#333333", + lineHeight: 20, + }, + reportContainer: { + backgroundColor: theme.COLORS.BG || "#F8F9FA", + borderRadius: 8, + padding: 12, + marginBottom: 10, + borderLeftWidth: 4, + borderLeftColor: theme.COLORS.PRIMARY || "#2196F3", + }, + reportHeader: { + fontSize: 16, + fontWeight: "600", + color: theme.COLORS.PRIMARY || "#2196F3", + marginBottom: 8, + }, + reportDetails: { + fontSize: 13, + color: theme.COLORS.GRAY || "#9E9E9E", + marginBottom: 12, + }, + singleReport: { + backgroundColor: theme.COLORS.BG || "#E0E0E0", + borderRadius: 6, + padding: 10, + marginBottom: 8, + }, + reportId: { + fontSize: 14, + fontWeight: "500", + color: theme.COLORS.TEXT || "#333333", + marginBottom: 4, + }, + reportLocation: { + fontSize: 13, + color: theme.COLORS.GRAY || "#9E9E9E", + marginBottom: 4, + }, + reportTime: { + fontSize: 12, + color: theme.COLORS.GRAY || "#9E9E9E", + marginBottom: 4, + }, + analysisInfo: { + fontSize: 12, + color: theme.COLORS.GRAY || "#9E9E9E", + }, + moreReports: { + fontSize: 12, + color: theme.COLORS.GRAY || "#9E9E9E", + marginTop: 8, + }, + noMessagesText: { + textAlign: "center", + color: theme.COLORS.GRAY || "#9E9E9E", + fontSize: 16, + marginTop: 20, + }, + clearButton: { + backgroundColor: theme.COLORS.ERROR || "#F44336", + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 6, + alignSelf: "center", + marginTop: 10, + }, + clearButtonText: { + color: "#FFFFFF", + fontSize: 14, + fontWeight: "600", + }, +}); + +export default WebSocketMessages; diff --git a/src/components/WebSocketStatus.js b/src/components/WebSocketStatus.js new file mode 100644 index 0000000..c994194 --- /dev/null +++ b/src/components/WebSocketStatus.js @@ -0,0 +1,186 @@ +import React from "react"; +import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native"; +import { theme } from "../services/Common/theme"; + +const WebSocketStatus = ({ + url, + onConnect, + onDisconnect, + isConnected, + connectionStatus, + isReconnecting, + error, + stats, + connect, + disconnect, +}) => { + const handleConnect = async () => { + console.log("🔌 [WebSocketStatus] handleConnect() called"); + try { + console.log("🔌 [WebSocketStatus] Attempting to connect to:", url); + await connect(); + console.log( + "🔌 [WebSocketStatus] Connection successful, calling onConnect callback" + ); + if (onConnect) onConnect(); + } catch (err) { + console.error("❌ [WebSocketStatus] Connection failed:", err); + Alert.alert("Connection Error", err.message || "Failed to connect"); + } + }; + + const handleDisconnect = () => { + console.log("🔌 [WebSocketStatus] handleDisconnect() called"); + disconnect(); + console.log( + "🔌 [WebSocketStatus] Disconnected, calling onDisconnect callback" + ); + if (onDisconnect) onDisconnect(); + }; + + const getStatusColor = () => { + switch (connectionStatus) { + case "connected": + return theme.COLORS.SUCCESS || "#4CAF50"; + case "connecting": + return theme.COLORS.WARNING || "#FF9800"; + case "disconnected": + return theme.COLORS.ERROR || "#F44336"; + case "failed": + return theme.COLORS.ERROR || "#F44336"; + default: + return theme.COLORS.GRAY || "#9E9E9E"; + } + }; + + const getStatusText = () => { + if (isReconnecting) { + return `Reconnecting... (${stats.reconnectAttempts || 0}/${ + stats.maxReconnectAttempts || 5 + })`; + } + + switch (connectionStatus) { + case "connected": + return "Connected"; + case "connecting": + return "Connecting..."; + case "disconnected": + return "Disconnected"; + case "failed": + return "Connection Failed"; + default: + return "Unknown"; + } + }; + + return ( + + + + {getStatusText()} + + + {error && Error: {error}} + + + + Queue: {stats.messageQueueSize || 0} | Subscriptions:{" "} + {stats.subscriptionCount || 0} + + + + + {!isConnected ? ( + + + {isReconnecting ? "Connecting..." : "Connect"} + + + ) : ( + + Disconnect + + )} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + padding: 16, + backgroundColor: theme.COLORS.BG || "#FFFFFF", + borderRadius: 8, + marginVertical: 8, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + statusRow: { + flexDirection: "row", + alignItems: "center", + marginBottom: 8, + }, + statusIndicator: { + width: 12, + height: 12, + borderRadius: 6, + marginRight: 8, + }, + statusText: { + fontSize: 16, + fontWeight: "600", + color: theme.COLORS.TEXT || "#333333", + }, + errorText: { + color: theme.COLORS.ERROR || "#F44336", + fontSize: 14, + marginBottom: 8, + }, + statsContainer: { + marginBottom: 16, + }, + statsText: { + fontSize: 12, + color: theme.COLORS.GRAY || "#9E9E9E", + }, + buttonContainer: { + flexDirection: "row", + justifyContent: "center", + }, + button: { + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 6, + minWidth: 100, + alignItems: "center", + }, + connectButton: { + backgroundColor: theme.COLORS.SUCCESS || "#4CAF50", + }, + disconnectButton: { + backgroundColor: theme.COLORS.ERROR || "#F44336", + }, + buttonText: { + color: "#FFFFFF", + fontSize: 14, + fontWeight: "600", + }, +}); + +export default WebSocketStatus; diff --git a/src/hooks/useWebSocket.js b/src/hooks/useWebSocket.js new file mode 100644 index 0000000..b5c30cc --- /dev/null +++ b/src/hooks/useWebSocket.js @@ -0,0 +1,244 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import webSocketService from "../services/WebSocketService"; + +/** + * React hook for using WebSocket service in components + * @param {string} url - WebSocket server URL + * @param {Object} options - Connection options + * @returns {Object} WebSocket state and methods + */ +export const useWebSocket = (url, options = {}) => { + const [connectionStatus, setConnectionStatus] = useState("disconnected"); + const [isConnected, setIsConnected] = useState(false); + const [isReconnecting, setIsReconnecting] = useState(false); + const [lastMessage, setLastMessage] = useState(null); + const [error, setError] = useState(null); + const [stats, setStats] = useState({}); + + const messageHistory = useRef([]); + const subscriptions = useRef(new Map()); + const reconnectAttempts = useRef(0); + + // Default options + const defaultOptions = { + autoConnect: true, + reconnect: true, + // Go backend handles ping/pong, so disable client heartbeat + heartbeat: false, + heartbeatInterval: 30000, + heartbeatTimeout: 5000, + maxReconnectAttempts: 5, + ...options, + }; + + // Connect to WebSocket + const connect = useCallback(async () => { + try { + setError(null); + await webSocketService.connect(url, defaultOptions); + } catch (err) { + setError(err.message || "Failed to connect"); + console.error("WebSocket connection error:", err); + } + }, [url, defaultOptions]); + + // Disconnect from WebSocket + const disconnect = useCallback(() => { + webSocketService.disconnect(); + }, []); + + // Send message + const send = useCallback((message, queueIfDisconnected = true) => { + return webSocketService.send(message, queueIfDisconnected); + }, []); + + // Subscribe to message type + const subscribe = useCallback( + (type, callback, id = null) => { + console.log("🔔 [useWebSocket] subscribe() called:", { + type, + callbackType: typeof callback, + id, + url, + hookRenderCount: Date.now(), // Track when this function is called + }); + + const subscriptionId = webSocketService.subscribe( + type, + (payload, metadata) => { + console.log("🔔 [useWebSocket] Subscription callback triggered:", { + type, + payload, + metadata, + payloadType: typeof payload, + payloadKeys: payload ? Object.keys(payload) : "N/A", + timestamp: Date.now(), + }); + + setLastMessage({ type, payload, metadata, timestamp: Date.now() }); + + // Add to message history + messageHistory.current.push({ + type, + payload, + metadata, + timestamp: Date.now(), + }); + + // Keep only last 100 messages + if (messageHistory.current.length > 100) { + messageHistory.current.shift(); + } + + // Call the callback + if (callback) { + console.log( + "🔔 [useWebSocket] Calling user callback with payload:", + payload + ); + callback(payload, metadata); + } + }, + id + ); + + console.log( + "🔔 [useWebSocket] Subscription created with ID:", + subscriptionId + ); + + // Store subscription reference + subscriptions.current.set(type, subscriptionId); + + return subscriptionId; + }, + [url] + ); + + // Unsubscribe from message type + const unsubscribe = useCallback( + (type, id = null) => { + const subscriptionId = subscriptions.current.get(type); + if (subscriptionId) { + webSocketService.unsubscribe(type, id || subscriptionId); + subscriptions.current.delete(type); + } + }, + [url] + ); + + // Get message history + const getMessageHistory = useCallback((type = null, limit = 50) => { + let messages = messageHistory.current; + + if (type) { + messages = messages.filter((msg) => msg.type === type); + } + + return messages.slice(-limit); + }, []); + + // Clear message history + const clearMessageHistory = useCallback(() => { + messageHistory.current = []; + }, []); + + // Get connection stats + const getStats = useCallback(() => { + return webSocketService.getStats(); + }, []); + + // Update stats periodically - REMOVED to prevent constant re-renders + // useEffect(() => { + // const statsInterval = setInterval(() => { + // setStats(webSocketService.getStats()); + // }, 1000); + + // return () => clearInterval(statsInterval); + // }, []); + + // Set up event listeners + useEffect(() => { + const handleConnected = (data) => { + setIsConnected(true); + setConnectionStatus("connected"); + setIsReconnecting(false); + setError(null); + reconnectAttempts.current = 0; + }; + + const handleDisconnected = (data) => { + setIsConnected(false); + setConnectionStatus("disconnected"); + setIsReconnecting(false); + }; + + const handleReconnecting = () => { + setIsReconnecting(true); + setConnectionStatus("connecting"); + reconnectAttempts.current++; + }; + + const handleError = (errorData) => { + setError(errorData.error || "WebSocket error"); + }; + + const handleReconnectFailed = (data) => { + setIsReconnecting(false); + setConnectionStatus("failed"); + setError(`Reconnection failed after ${data.attempts} attempts`); + }; + + // Subscribe to WebSocket events + webSocketService.on("connected", handleConnected); + webSocketService.on("disconnected", handleDisconnected); + webSocketService.on("reconnecting", handleReconnecting); + webSocketService.on("error", handleError); + webSocketService.on("reconnectFailed", handleReconnectFailed); + + // Auto-connect if enabled + if (defaultOptions.autoConnect && url) { + connect(); + } + + // Cleanup function + return () => { + webSocketService.off("connected", handleConnected); + webSocketService.off("disconnected", handleDisconnected); + webSocketService.off("reconnecting", handleReconnecting); + webSocketService.off("error", handleError); + webSocketService.off("reconnectFailed", handleReconnectFailed); + + // Unsubscribe from all message types + subscriptions.current.forEach((subscriptionId, type) => { + webSocketService.unsubscribe(type, subscriptionId); + }); + subscriptions.current.clear(); + }; + }, [url, defaultOptions.autoConnect, connect]); + + return { + // State + connectionStatus, + isConnected, + isReconnecting, + lastMessage, + error, + stats, + + // Methods + connect, + disconnect, + send, + subscribe, + unsubscribe, + getMessageHistory, + clearMessageHistory, + getStats, + + // Utility + messageHistory: messageHistory.current, + }; +}; + +export default useWebSocket; diff --git a/src/screens/WebSocketDemoScreen.js b/src/screens/WebSocketDemoScreen.js new file mode 100644 index 0000000..dc0021e --- /dev/null +++ b/src/screens/WebSocketDemoScreen.js @@ -0,0 +1,306 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import { + Alert, + SafeAreaView, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import WebSocketMessages from '../components/WebSocketMessages'; +import WebSocketStatus from '../components/WebSocketStatus'; +import {useWebSocket} from '../hooks/useWebSocket'; +import {theme} from '../services/Common/theme'; + +const WebSocketDemoScreen = () => { + const [serverUrl, setServerUrl] = useState( + 'wss://live.cleanapp.io/api/v3/reports/listen', + ); + const [messageTypes, setMessageTypes] = useState([ + 'chat', + 'notification', + 'update', + ]); + const [customMessageType, setCustomMessageType] = useState(''); + + // Render counter removed - was causing noise in logs + + // Stabilize messageTypes array to prevent unnecessary re-renders + const stableMessageTypes = useMemo(() => messageTypes, [messageTypes]); + + // Centralized WebSocket connection + const { + connectionStatus, + isConnected, + isReconnecting, + error, + stats, + connect, + disconnect, + subscribe: originalSubscribe, + unsubscribe: originalUnsubscribe, + } = useWebSocket(serverUrl, { + autoConnect: false, + reconnect: true, + heartbeat: false, // Go backend handles ping/pong + }); + + useEffect(() => { + connect(); + }, []); + + // Stabilize function references to prevent unnecessary re-renders + const subscribe = useCallback(originalSubscribe, [originalSubscribe]); + const unsubscribe = useCallback(originalUnsubscribe, [originalUnsubscribe]); + + const handleConnect = () => { + console.log('🖥️ [WebSocketDemoScreen] handleConnect() called'); + console.log('🖥️ [WebSocketDemoScreen] Server URL:', serverUrl); + }; + + const handleDisconnect = () => { + console.log('🖥️ [WebSocketDemoScreen] handleDisconnect() called'); + console.log('🖥️ [WebSocketDemoScreen] Server URL:', serverUrl); + }; + + const addMessageType = () => { + if ( + customMessageType.trim() && + !messageTypes.includes(customMessageType.trim()) + ) { + setMessageTypes([...messageTypes, customMessageType.trim()]); + setCustomMessageType(''); + } + }; + + const removeMessageType = typeToRemove => { + Alert.alert( + 'Remove Message Type', + `Are you sure you want to remove "${typeToRemove}"?`, + [ + {text: 'Cancel', style: 'cancel'}, + { + text: 'Remove', + style: 'destructive', + onPress: () => { + setMessageTypes(messageTypes.filter(type => type !== typeToRemove)); + }, + }, + ], + ); + }; + + const resetToDefaults = () => { + setMessageTypes(['chat', 'notification', 'update']); + setServerUrl(Constants.expoConfig?.extra?.devWebsocketUrl); + }; + + return ( + + + {/* WebSocket Status */} + + Connection Status + + + + {/* WebSocket Messages */} + + Real-time Messages + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.COLORS.BG || '#F5F5F5', + }, + scrollView: { + flex: 1, + }, + header: { + padding: 20, + backgroundColor: theme.COLORS.PRIMARY || '#2196F3', + alignItems: 'center', + }, + title: { + fontSize: 24, + fontWeight: 'bold', + color: '#FFFFFF', + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: '#FFFFFF', + textAlign: 'center', + opacity: 0.9, + }, + section: { + margin: 16, + padding: 16, + backgroundColor: '#FFFFFF', + borderRadius: 8, + shadowColor: '#000', + shadowOffset: {width: 0, height: 2}, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + color: theme.COLORS.TEXT || '#333333', + marginBottom: 16, + }, + inputRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 16, + }, + label: { + fontSize: 16, + fontWeight: '500', + color: theme.COLORS.TEXT || '#333333', + marginRight: 12, + minWidth: 80, + }, + urlInput: { + flex: 1, + borderWidth: 1, + borderColor: theme.COLORS.BORDER || '#E0E0E0', + borderRadius: 6, + padding: 12, + fontSize: 14, + color: theme.COLORS.TEXT || '#333333', + }, + resetButton: { + alignSelf: 'flex-end', + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: theme.COLORS.GRAY || '#9E9E9E', + borderRadius: 4, + }, + resetButtonText: { + color: '#FFFFFF', + fontSize: 12, + fontWeight: '500', + }, + messageTypesContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 16, + }, + messageTypeItem: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.COLORS.PRIMARY || '#E3F2FD', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, + marginRight: 8, + marginBottom: 8, + }, + messageTypeText: { + fontSize: 12, + color: theme.COLORS.PRIMARY || '#1976D2', + fontWeight: '500', + marginRight: 6, + }, + removeTypeButton: { + width: 18, + height: 18, + borderRadius: 9, + backgroundColor: theme.COLORS.ERROR || '#F44336', + alignItems: 'center', + justifyContent: 'center', + }, + removeTypeButtonText: { + color: '#FFFFFF', + fontSize: 12, + fontWeight: 'bold', + lineHeight: 18, + }, + addTypeContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + addTypeInput: { + flex: 1, + borderWidth: 1, + borderColor: theme.COLORS.BORDER || '#E0E0E0', + borderRadius: 6, + padding: 12, + marginRight: 8, + fontSize: 14, + color: theme.COLORS.TEXT || '#333333', + }, + addTypeButton: { + backgroundColor: theme.COLORS.SUCCESS || '#4CAF50', + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 6, + }, + addTypeButtonText: { + color: '#FFFFFF', + fontSize: 14, + fontWeight: '600', + }, + instructionItem: { + flexDirection: 'row', + marginBottom: 12, + alignItems: 'flex-start', + }, + instructionNumber: { + fontSize: 16, + fontWeight: 'bold', + color: theme.COLORS.PRIMARY || '#2196F3', + marginRight: 12, + minWidth: 20, + }, + instructionText: { + flex: 1, + fontSize: 14, + color: theme.COLORS.TEXT || '#333333', + lineHeight: 20, + }, + featureItem: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12, + }, + featureIcon: { + fontSize: 20, + marginRight: 12, + }, + featureText: { + flex: 1, + fontSize: 14, + color: theme.COLORS.TEXT || '#333333', + lineHeight: 20, + }, +}); + +export default WebSocketDemoScreen; diff --git a/src/services/WebSocketService.js b/src/services/WebSocketService.js new file mode 100644 index 0000000..a280048 --- /dev/null +++ b/src/services/WebSocketService.js @@ -0,0 +1,671 @@ +// Custom EventEmitter implementation for React Native +class EventEmitter { + constructor() { + this.events = {}; + } + + on(event, listener) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(listener); + return this; + } + + off(event, listener) { + if (!this.events[event]) return this; + + if (listener) { + this.events[event] = this.events[event].filter((l) => l !== listener); + } else { + delete this.events[event]; + } + return this; + } + + emit(event, ...args) { + if (!this.events[event]) return false; + + this.events[event].forEach((listener) => { + try { + listener(...args); + } catch (error) { + console.error(`Error in event listener for ${event}:`, error); + } + }); + + return true; + } + + removeAllListeners(event) { + if (event) { + delete this.events[event]; + } else { + this.events = {}; + } + return this; + } +} + +class WebSocketService extends EventEmitter { + constructor() { + super(); + this.ws = null; + this.url = null; + this.isConnected = false; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 5; + this.reconnectDelay = 1000; // Start with 1 second + this.maxReconnectDelay = 30000; // Max 30 seconds + this.heartbeatInterval = null; + this.heartbeatTimeout = null; + this.messageQueue = []; + this.subscriptions = new Map(); + this.isReconnecting = false; + } + + /** + * Connect to WebSocket server + * @param {string} url - WebSocket server URL + * @param {Object} options - Connection options + */ + connect(url, options = {}) { + console.log("🔌 [WebSocketService] connect() called with URL:", url); + console.log("🔌 [WebSocketService] connect() options:", options); + + if (this.isConnected || this.isReconnecting) { + console.log( + "🔌 [WebSocketService] Already connected or reconnecting, skipping connection" + ); + return Promise.resolve(); + } + + this.url = url; + const { + protocols = [], + headers = {}, + reconnect = true, + // Go backend handles ping/pong, so disable client heartbeat + heartbeat = false, + heartbeatInterval = 30000, + heartbeatTimeout = 5000, + } = options; + + console.log("🔌 [WebSocketService] Connection parameters:", { + url: this.url, + protocols, + headers, + reconnect, + heartbeat, + heartbeatInterval, + heartbeatTimeout, + }); + + return new Promise((resolve, reject) => { + try { + console.log("🔌 [WebSocketService] Creating new WebSocket instance..."); + this.ws = new WebSocket(url, protocols); + + // Set up event handlers + this.ws.onopen = () => { + console.log( + "🔌 [WebSocketService] WebSocket connection opened successfully!" + ); + console.log("🔌 [WebSocketService] Connection details:", { + url: this.ws.url, + protocol: this.ws.protocol, + readyState: this.ws.readyState, + bufferedAmount: this.ws.bufferedAmount, + }); + + this.isConnected = true; + this.isReconnecting = false; + this.reconnectAttempts = 0; + this.reconnectDelay = 1000; + + // Go backend handles ping/pong, so don't start client heartbeat + // if (heartbeat) { + // this.startHeartbeat(heartbeatInterval, heartbeatTimeout); + // } + + // Process queued messages + this.processMessageQueue(); + + // Emit connection event + this.emit("connected", { url, timestamp: Date.now() }); + + console.log( + "🔌 [WebSocketService] Connection established and ready for messages" + ); + resolve(); + }; + + this.ws.onmessage = (event) => { + console.log("📨 [WebSocketService] Raw message received:", { + data: event.data, + type: event.type, + timestamp: new Date().toISOString(), + }); + + try { + console.log( + "📨 [WebSocketService] Attempting to parse message as JSON..." + ); + const data = JSON.parse(event.data); + console.log( + "📨 [WebSocketService] Successfully parsed JSON message:", + { + parsedData: data, + dataType: typeof data, + hasType: "type" in data, + hasPayload: "payload" in data, + hasData: "data" in data, + keys: Object.keys(data), + } + ); + + // Handle double-encoded JSON from Go backend + if (data.data && typeof data.data === "string") { + console.log( + "📨 [WebSocketService] Detected double-encoded JSON, parsing inner data..." + ); + try { + const innerData = JSON.parse(data.data); + console.log( + "📨 [WebSocketService] Successfully parsed inner JSON:", + innerData + ); + this.handleMessage(innerData); + } catch (innerError) { + console.error( + "❌ [WebSocketService] Failed to parse inner JSON:", + innerError + ); + // Fall back to original data + this.handleMessage(data); + } + } else { + this.handleMessage(data); + } + } catch (error) { + console.error( + "❌ [WebSocketService] Error parsing WebSocket message:", + error + ); + console.error( + "❌ [WebSocketService] Raw message that failed to parse:", + event.data + ); + this.emit("error", { + error: "Invalid message format", + data: event.data, + parseError: error.message, + }); + } + }; + + this.ws.onclose = (event) => { + console.log("🔌 [WebSocketService] WebSocket connection closed:", { + code: event.code, + reason: event.reason, + wasClean: event.wasClean, + timestamp: new Date().toISOString(), + }); + + this.isConnected = false; + this.stopHeartbeat(); + + // Emit disconnection event + this.emit("disconnected", { + code: event.code, + reason: event.reason, + timestamp: Date.now(), + }); + + // Attempt reconnection if enabled + if (reconnect && !this.isReconnecting) { + console.log("🔌 [WebSocketService] Attempting to reconnect..."); + this.scheduleReconnect(); + } + }; + + this.ws.onerror = (error) => { + console.error( + "❌ [WebSocketService] WebSocket error occurred:", + error + ); + console.error("❌ [WebSocketService] Error details:", { + message: error.message, + type: error.type, + target: error.target, + timestamp: new Date().toISOString(), + }); + + this.emit("error", { error: error.message || "WebSocket error" }); + reject(error); + }; + } catch (error) { + console.error( + "❌ [WebSocketService] Error creating WebSocket connection:", + error + ); + reject(error); + } + }); + } + + /** + * Disconnect from WebSocket server + */ + disconnect() { + if (this.ws) { + this.stopHeartbeat(); + this.ws.close(1000, "Client disconnecting"); + this.ws = null; + this.isConnected = false; + this.isReconnecting = false; + this.messageQueue = []; + this.subscriptions.clear(); + console.log("WebSocket disconnected by client"); + } + } + + /** + * Send message to WebSocket server + * @param {Object|string} message - Message to send + * @param {boolean} queueIfDisconnected - Queue message if disconnected + */ + send(message, queueIfDisconnected = true) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + if (queueIfDisconnected) { + this.messageQueue.push(message); + console.log("Message queued, WebSocket not connected"); + return false; + } + throw new Error("WebSocket not connected"); + } + + try { + const messageStr = + typeof message === "string" ? message : JSON.stringify(message); + this.ws.send(messageStr); + this.emit("messageSent", { message, timestamp: Date.now() }); + return true; + } catch (error) { + console.error("Error sending WebSocket message:", error); + this.emit("error", { + error: "Failed to send message", + originalError: error, + }); + return false; + } + } + + /** + * Subscribe to a specific message type + * @param {string} type - Message type to subscribe to + * @param {Function} callback - Callback function + * @param {string} id - Optional subscription ID + */ + subscribe(type, callback, id = null) { + const subscriptionId = id || `${type}_${Date.now()}_${Math.random()}`; + + if (!this.subscriptions.has(type)) { + this.subscriptions.set(type, new Map()); + } + + this.subscriptions.get(type).set(subscriptionId, callback); + + // Emit subscription event + this.emit("subscribed", { type, id: subscriptionId }); + + return subscriptionId; + } + + /** + * Unsubscribe from a specific message type + * @param {string} type - Message type to unsubscribe from + * @param {string} id - Subscription ID (if null, unsubscribes all for type) + */ + unsubscribe(type, id = null) { + console.log("🔔 [WebSocketService] unsubscribe() called:", { + type, + id, + hasType: this.subscriptions.has(type), + currentSubscriptions: Array.from(this.subscriptions.keys()), + }); + + if (!this.subscriptions.has(type)) { + console.log( + "🔔 [WebSocketService] No subscriptions found for type:", + type + ); + return false; + } + + if (id === null) { + // Unsubscribe all for this type + const count = this.subscriptions.get(type).size; + this.subscriptions.delete(type); + console.log( + "🔔 [WebSocketService] Unsubscribed all subscriptions for type:", + { + type, + removedCount: count, + } + ); + this.emit("unsubscribed", { type, id: null }); + return true; + } else { + // Unsubscribe specific subscription + const typeSubscriptions = this.subscriptions.get(type); + if (typeSubscriptions && typeSubscriptions.has(id)) { + typeSubscriptions.delete(id); + console.log( + "🔔 [WebSocketService] Unsubscribed specific subscription:", + { + type, + id, + remainingForType: typeSubscriptions.size, + } + ); + + // Remove type if no more subscriptions + if (typeSubscriptions.size === 0) { + this.subscriptions.delete(type); + console.log( + "🔔 [WebSocketService] Removed empty subscription type:", + type + ); + } + + this.emit("unsubscribed", { type, id }); + return true; + } + } + + return false; + } + + /** + * Get connection status + */ + getConnectionStatus() { + if (!this.ws) { + return "disconnected"; + } + + switch (this.ws.readyState) { + case WebSocket.CONNECTING: + return "connecting"; + case WebSocket.OPEN: + return "connected"; + case WebSocket.CLOSING: + return "closing"; + case WebSocket.CLOSED: + return "closed"; + default: + return "unknown"; + } + } + + /** + * Get connection statistics + */ + getStats() { + return { + isConnected: this.isConnected, + isReconnecting: this.isReconnecting, + reconnectAttempts: this.reconnectAttempts, + messageQueueSize: this.messageQueue.length, + subscriptionCount: Array.from(this.subscriptions.values()).reduce( + (total, typeSubs) => total + typeSubs.size, + 0 + ), + url: this.url, + connectionStatus: this.getConnectionStatus(), + }; + } + + // Private methods + + handleMessage(data) { + console.log("📨 [WebSocketService] handleMessage() called with data:", { + data, + dataType: typeof data, + isNull: data === null, + isUndefined: data === undefined, + keys: data ? Object.keys(data) : "N/A", + }); + + // Additional validation for the data structure + if (!data || typeof data !== "object") { + console.error( + "❌ [WebSocketService] Invalid data received in handleMessage:", + data + ); + return; + } + + // Extract message properties with detailed logging + const { type, payload, id, timestamp, data: messageData } = data; + + console.log("📨 [WebSocketService] Extracted message properties:", { + type, + payload, + id, + timestamp, + messageData, + hasType: type !== undefined, + hasPayload: payload !== undefined, + hasId: id !== undefined, + hasTimestamp: timestamp !== undefined, + hasMessageData: messageData !== undefined, + }); + + // Emit raw message event with Go backend compatible structure + console.log("📨 [WebSocketService] Emitting 'message' event with data:", { + type, + payload: messageData, // Use messageData (Go's 'data' field) as payload + id, + timestamp, + raw: data, + }); + this.emit("message", { + type, + payload: messageData, + id, + timestamp, + raw: data, + }); + + // Handle subscriptions with detailed logging + if (type && this.subscriptions.has(type)) { + console.log("📨 [WebSocketService] Found subscriptions for type:", type); + const typeSubscriptions = this.subscriptions.get(type); + console.log("📨 [WebSocketService] Type subscriptions:", { + subscriptionCount: typeSubscriptions.size, + subscriptionIds: Array.from(typeSubscriptions.keys()), + }); + + console.log( + "📨 [WebSocketService] About to call subscription callbacks with data:", + { + type, + messageData, + payload, + actualPayload: messageData || payload, + } + ); + + typeSubscriptions.forEach((callback, subscriptionId) => { + // Use messageData (Go's 'data' field) as the actual payload for callbacks + const actualPayload = messageData || payload; + console.log("📨 [WebSocketService] Calling subscription callback:", { + subscriptionId, + type, + originalPayload: payload, + actualPayload: actualPayload, + callbackType: typeof callback, + }); + + try { + // Use messageData (Go's 'data' field) as the actual payload for callbacks + const actualPayload = messageData || payload; + callback(actualPayload, { type, id, timestamp }); + console.log( + "📨 [WebSocketService] Subscription callback executed successfully" + ); + } catch (error) { + console.error( + "❌ [WebSocketService] Error in subscription callback:", + { + error: error.message, + stack: error.stack, + subscriptionId, + type, + payload: messageData || payload, + } + ); + this.emit("error", { + error: "Subscription callback error", + type, + originalError: error, + }); + } + }); + } else { + console.log( + "📨 [WebSocketService] No subscriptions found for type:", + type + ); + console.log( + "📨 [WebSocketService] Available subscription types:", + Array.from(this.subscriptions.keys()) + ); + } + + // Handle specific message types from Go backend + console.log("📨 [WebSocketService] Processing message type:", type); + switch (type) { + case "pong": + console.log("📨 [WebSocketService] Handling pong message"); + // Go backend sends pong responses to our pings + this.handlePong(); + break; + case "reports": + console.log( + "📨 [WebSocketService] Handling reports message from Go backend" + ); + console.log( + "📨 [WebSocketService] Go backend data field:", + messageData + ); + console.log("📨 [WebSocketService] Legacy payload field:", payload); + + // Go backend uses 'data' field, so use messageData as the primary source + const reportData = messageData || payload; + console.log( + "📨 [WebSocketService] Final report data to emit:", + reportData, + { reportDataString: reportData.toString() } + ); + + console.log( + "📨 [WebSocketService] Emitting 'reports' event with data:+++++++++++", + { + reports: reportData.reports, + reportsString: reportData.reports.toString(), + reportsAnalysis: reportData.reports[0].analysis, + reportsAnalysisString: reportData.reports[0].analysis.toString(), + } + ); + + // Go backend broadcasts report data + this.emit("reports", reportData); + break; + case "error": + console.log("📨 [WebSocketService] Handling error message from server"); + this.emit("serverError", payload); + break; + default: + console.log( + "📨 [WebSocketService] Handling default message type:", + type + ); + // Emit typed event with Go backend compatible data + const defaultData = messageData || payload; + this.emit(type, defaultData); + } + + console.log("📨 [WebSocketService] handleMessage() completed"); + } + + scheduleReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + this.emit("reconnectFailed", { + attempts: this.reconnectAttempts, + maxAttempts: this.maxReconnectAttempts, + }); + return; + } + + this.isReconnecting = true; + this.reconnectAttempts++; + + const delay = Math.min( + this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), + this.maxReconnectDelay + ); + + setTimeout(() => { + if (this.isReconnecting) { + this.connect(this.url, { reconnect: true }); + } + }, delay); + } + + startHeartbeat(interval, timeout) { + this.heartbeatInterval = setInterval(() => { + if (this.isConnected) { + this.send({ type: "ping", timestamp: Date.now() }); + + // Set timeout for pong response + this.heartbeatTimeout = setTimeout(() => { + this.ws.close(1000, "Heartbeat timeout"); + }, timeout); + } + }, interval); + } + + stopHeartbeat() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + + if (this.heartbeatTimeout) { + clearTimeout(this.heartbeatTimeout); + this.heartbeatTimeout = null; + } + } + + handlePong() { + if (this.heartbeatTimeout) { + clearTimeout(this.heartbeatTimeout); + this.heartbeatTimeout = null; + } + } + + processMessageQueue() { + while (this.messageQueue.length > 0) { + const message = this.messageQueue.shift(); + this.send(message, false); + } + } +} + +// Create singleton instance +const webSocketService = new WebSocketService(); + +export default webSocketService; From 423004828c718cc019786df8ebd14bee209e9c54 Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 00:25:41 +0530 Subject: [PATCH 02/13] Remove unused components and refactor code --- src/components/WebSocketMessages.js | 12 ++++---- src/components/WebSocketStatus.js | 11 ++----- src/screens/WebSocketDemoScreen.js | 47 ++--------------------------- 3 files changed, 10 insertions(+), 60 deletions(-) diff --git a/src/components/WebSocketMessages.js b/src/components/WebSocketMessages.js index a64cf9a..d81d05e 100644 --- a/src/components/WebSocketMessages.js +++ b/src/components/WebSocketMessages.js @@ -10,7 +10,7 @@ import { theme } from "../services/Common/theme"; const WebSocketMessages = ({ url, - messageTypes = [], + // messageTypes = [], isConnected, subscribe, unsubscribe, @@ -24,7 +24,7 @@ const WebSocketMessages = ({ console.log("📱 [WebSocketMessages] useEffect triggered:", { isConnected, url, - messageTypes, + // messageTypes, hasSubscribe: !!subscribe, hasUnsubscribe: !!unsubscribe, subscribeType: typeof subscribe, @@ -33,9 +33,9 @@ const WebSocketMessages = ({ // Add detailed dependency tracking isConnectedValue: isConnected, urlValue: url, - messageTypesValue: messageTypes, - messageTypesLength: messageTypes.length, - messageTypesString: JSON.stringify(messageTypes), + // messageTypesValue: messageTypes, + // messageTypesLength: messageTypes.length, + // messageTypesString: JSON.stringify(messageTypes), }); if (isConnected && subscribe && unsubscribe) { @@ -101,7 +101,7 @@ const WebSocketMessages = ({ "📱 [WebSocketMessages] WebSocket not connected or missing functions, skipping subscription" ); } - }, [isConnected, url, messageTypes]); // Restored dependencies now that re-render issue is fixed + }, [isConnected, url]); // Restored dependencies now that re-render issue is fixed // Filter messages based on selected type useEffect(() => { diff --git a/src/components/WebSocketStatus.js b/src/components/WebSocketStatus.js index c994194..8b3abc3 100644 --- a/src/components/WebSocketStatus.js +++ b/src/components/WebSocketStatus.js @@ -88,13 +88,6 @@ const WebSocketStatus = ({ {error && Error: {error}} - - - Queue: {stats.messageQueueSize || 0} | Subscriptions:{" "} - {stats.subscriptionCount || 0} - - - {!isConnected ? ( { const [serverUrl, setServerUrl] = useState( 'wss://live.cleanapp.io/api/v3/reports/listen', ); - const [messageTypes, setMessageTypes] = useState([ - 'chat', - 'notification', - 'update', - ]); - const [customMessageType, setCustomMessageType] = useState(''); - - // Render counter removed - was causing noise in logs - - // Stabilize messageTypes array to prevent unnecessary re-renders - const stableMessageTypes = useMemo(() => messageTypes, [messageTypes]); - // Centralized WebSocket connection const { connectionStatus, @@ -56,43 +44,13 @@ const WebSocketDemoScreen = () => { const handleConnect = () => { console.log('🖥️ [WebSocketDemoScreen] handleConnect() called'); console.log('🖥️ [WebSocketDemoScreen] Server URL:', serverUrl); + connect(); }; const handleDisconnect = () => { console.log('🖥️ [WebSocketDemoScreen] handleDisconnect() called'); console.log('🖥️ [WebSocketDemoScreen] Server URL:', serverUrl); - }; - - const addMessageType = () => { - if ( - customMessageType.trim() && - !messageTypes.includes(customMessageType.trim()) - ) { - setMessageTypes([...messageTypes, customMessageType.trim()]); - setCustomMessageType(''); - } - }; - - const removeMessageType = typeToRemove => { - Alert.alert( - 'Remove Message Type', - `Are you sure you want to remove "${typeToRemove}"?`, - [ - {text: 'Cancel', style: 'cancel'}, - { - text: 'Remove', - style: 'destructive', - onPress: () => { - setMessageTypes(messageTypes.filter(type => type !== typeToRemove)); - }, - }, - ], - ); - }; - - const resetToDefaults = () => { - setMessageTypes(['chat', 'notification', 'update']); - setServerUrl(Constants.expoConfig?.extra?.devWebsocketUrl); + disconnect(); }; return ( @@ -122,7 +80,6 @@ const WebSocketDemoScreen = () => { Real-time Messages Date: Wed, 27 Aug 2025 01:04:45 +0530 Subject: [PATCH 03/13] Setup local notifications --- package.json | 1 + src/components/WebSocketMessages.js | 210 ++++++++++++++++------------ src/screens/WebSocketDemoScreen.js | 3 +- yarn.lock | 20 ++- 4 files changed, 140 insertions(+), 94 deletions(-) diff --git a/package.json b/package.json index 0b925a6..b7d4e58 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@babel/preset-typescript": "^7.23.3", "@bam.tech/react-native-image-resizer": "^3.0.11", "@ethersproject/shims": "^5.8.0", + "@notifee/react-native": "^9.1.8", "@react-native-async-storage/async-storage": "^2.1.2", "@react-native-clipboard/clipboard": "^1.16.1", "@react-native-community/blur": "^4.4.0", diff --git a/src/components/WebSocketMessages.js b/src/components/WebSocketMessages.js index d81d05e..3cccf89 100644 --- a/src/components/WebSocketMessages.js +++ b/src/components/WebSocketMessages.js @@ -1,12 +1,13 @@ -import React, { useEffect, useState } from "react"; +import React, {useEffect, useState} from 'react'; import { ScrollView, StyleSheet, Text, TouchableOpacity, View, -} from "react-native"; -import { theme } from "../services/Common/theme"; +} from 'react-native'; +import {theme} from '../services/Common/theme'; +import notifee from '@notifee/react-native'; const WebSocketMessages = ({ url, @@ -17,11 +18,11 @@ const WebSocketMessages = ({ }) => { const [messages, setMessages] = useState([]); const [filteredMessages, setFilteredMessages] = useState([]); - const [selectedType, setSelectedType] = useState("all"); + const [selectedType, setSelectedType] = useState('all'); // Subscribe to report messages from Go backend useEffect(() => { - console.log("📱 [WebSocketMessages] useEffect triggered:", { + console.log('📱 [WebSocketMessages] useEffect triggered:', { isConnected, url, // messageTypes, @@ -40,41 +41,43 @@ const WebSocketMessages = ({ if (isConnected && subscribe && unsubscribe) { console.log( - "📱 [WebSocketMessages] WebSocket connected, subscribing to 'reports' type" + "📱 [WebSocketMessages] WebSocket connected, subscribing to 'reports' type", ); // Subscribe to the "reports" message type that Go backend sends - const subscriptionId = subscribe("reports", (payload, metadata) => { + const subscriptionId = subscribe('reports', (payload, metadata) => { console.log( - "📱 [WebSocketMessages] Reports subscription callback triggered:", + '📱 [WebSocketMessages] Reports subscription callback triggered:', { payload, metadata, payloadType: typeof payload, - payloadKeys: payload ? Object.keys(payload) : "N/A", + payloadKeys: payload ? Object.keys(payload) : 'N/A', payloadIsNull: payload === null, payloadIsUndefined: payload === undefined, timestamp: new Date().toISOString(), - } + }, ); const newMessage = { id: Date.now(), - type: "reports", + type: 'reports', timestamp: new Date(), data: payload, metadata, }; - console.log("📱 [WebSocketMessages] Created new message object:", { + console.log('📱 [WebSocketMessages] Created new message object:', { newMessage, dataField: newMessage.data, dataType: typeof newMessage.data, }); - setMessages((prev) => { + onDisplayNotification(); + + setMessages(prev => { const updatedMessages = [newMessage, ...prev.slice(0, 99)]; // Keep last 100 - console.log("📱 [WebSocketMessages] Updated messages state:", { + console.log('📱 [WebSocketMessages] Updated messages state:', { previousCount: prev.length, newCount: updatedMessages.length, firstMessage: updatedMessages[0], @@ -84,31 +87,31 @@ const WebSocketMessages = ({ }); console.log( - "📱 [WebSocketMessages] Subscription created with ID:", - subscriptionId + '📱 [WebSocketMessages] Subscription created with ID:', + subscriptionId, ); return () => { console.log( - "📱 [WebSocketMessages] useEffect cleanup running, removing subscription:", + '📱 [WebSocketMessages] useEffect cleanup running, removing subscription:', subscriptionId, - "Reason: useEffect dependencies changed or component unmounting" + 'Reason: useEffect dependencies changed or component unmounting', ); - unsubscribe("reports", subscriptionId); + unsubscribe('reports', subscriptionId); }; } else { console.log( - "📱 [WebSocketMessages] WebSocket not connected or missing functions, skipping subscription" + '📱 [WebSocketMessages] WebSocket not connected or missing functions, skipping subscription', ); } }, [isConnected, url]); // Restored dependencies now that re-render issue is fixed // Filter messages based on selected type useEffect(() => { - if (selectedType === "all") { + if (selectedType === 'all') { setFilteredMessages(messages); } else { - setFilteredMessages(messages.filter((msg) => msg.type === selectedType)); + setFilteredMessages(messages.filter(msg => msg.type === selectedType)); } }, [messages, selectedType]); @@ -118,36 +121,36 @@ const WebSocketMessages = ({ setFilteredMessages([]); }; - const formatTimestamp = (timestamp) => { + const formatTimestamp = timestamp => { return new Date(timestamp).toLocaleTimeString(); }; - const renderReportMessage = (message) => { + const renderReportMessage = message => { console.log( - "📱 [WebSocketMessages] renderReportMessage() called with message:", + '📱 [WebSocketMessages] renderReportMessage() called with message:', { message, messageType: typeof message, - messageKeys: message ? Object.keys(message) : "N/A", - } + messageKeys: message ? Object.keys(message) : 'N/A', + }, ); - const { data } = message; + const {data} = message; - console.log("📱 [WebSocketMessages] Extracted data from message:", { + console.log('📱 [WebSocketMessages] Extracted data from message:', { data, dataType: typeof data, dataIsNull: data === null, dataIsUndefined: data === undefined, - dataKeys: data ? Object.keys(data) : "N/A", + dataKeys: data ? Object.keys(data) : 'N/A', }); if (!data || !data.reports) { - console.log("📱 [WebSocketMessages] No report data found in message"); + console.log('📱 [WebSocketMessages] No report data found in message'); return No report data; } - console.log("📱 [WebSocketMessages] Report data found:", { + console.log('📱 [WebSocketMessages] Report data found:', { reportsCount: data.reports.length, reportsType: typeof data.reports, firstReport: data.reports[0], @@ -163,12 +166,12 @@ const WebSocketMessages = ({ Sequence: {data.fromSeq} - {data.toSeq} {data.reports.slice(0, 3).map((reportWithAnalysis, index) => { - console.log("📱 [WebSocketMessages] Rendering report:", { + console.log('📱 [WebSocketMessages] Rendering report:', { index, reportWithAnalysis, reportKeys: reportWithAnalysis ? Object.keys(reportWithAnalysis) - : "N/A", + : 'N/A', }); return ( @@ -177,11 +180,11 @@ const WebSocketMessages = ({ ID: {reportWithAnalysis.report.id} - 📍 {reportWithAnalysis.report.latitude.toFixed(4)},{" "} + 📍 {reportWithAnalysis.report.latitude.toFixed(4)},{' '} {reportWithAnalysis.report.longitude.toFixed(4)} - ⏰{" "} + ⏰{' '} {new Date(reportWithAnalysis.report.timestamp).toLocaleString()} {reportWithAnalysis.analysis && @@ -203,9 +206,9 @@ const WebSocketMessages = ({ ); }; - const renderMessage = (message) => { + const renderMessage = message => { switch (message.type) { - case "reports": + case 'reports': return renderReportMessage(message); default: return ( @@ -216,6 +219,34 @@ const WebSocketMessages = ({ } }; + async function onDisplayNotification() { + // Request permissions (required for iOS) + await notifee.requestPermission(); + + // Create a channel (required for Android) + const channelId = await notifee.createChannel({ + id: 'default', + name: 'Default Channel', + }); + + try { + // Display a notification + await notifee.displayNotification({ + title: 'Notification Title', + body: 'Main body content of the notification', + android: { + channelId, + // pressAction is needed if you want the notification to open the app when pressed + pressAction: { + id: 'default', + }, + }, + }); + } catch (e) { + console.log('📱 [WebSocketMessages] Error displaying notification:', e); + } + } + if (!isConnected) { return ( @@ -233,33 +264,29 @@ const WebSocketMessages = ({ setSelectedType("all")} - > + onPress={() => setSelectedType('all')}> + selectedType === 'all' && styles.filterButtonTextActive, + ]}> All ({messages.length}) setSelectedType("reports")} - > + onPress={() => setSelectedType('reports')}> - Reports ({messages.filter((m) => m.type === "reports").length}) + selectedType === 'reports' && styles.filterButtonTextActive, + ]}> + Reports ({messages.filter(m => m.type === 'reports').length}) @@ -267,15 +294,14 @@ const WebSocketMessages = ({ {/* Messages */} + showsVerticalScrollIndicator={false}> {filteredMessages.length === 0 ? ( 📭 No messages received yet. Reports will appear here when broadcasted by the server. ) : ( - filteredMessages.map((message) => ( + filteredMessages.map(message => ( {message.type} @@ -302,20 +328,20 @@ const WebSocketMessages = ({ const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: theme.COLORS.BG || "#FFFFFF", + backgroundColor: theme.COLORS.BG || '#FFFFFF', padding: 16, }, disconnectedText: { - textAlign: "center", - color: theme.COLORS.GRAY || "#9E9E9E", + textAlign: 'center', + color: theme.COLORS.GRAY || '#9E9E9E', fontSize: 16, marginTop: 20, }, filterContainer: { - flexDirection: "row", - justifyContent: "space-around", + flexDirection: 'row', + justifyContent: 'space-around', marginBottom: 12, - backgroundColor: theme.COLORS.GRAY || "#F5F5F5", + backgroundColor: theme.COLORS.GRAY || '#F5F5F5', borderRadius: 8, padding: 4, }, @@ -325,116 +351,116 @@ const styles = StyleSheet.create({ borderRadius: 6, }, filterButtonActive: { - backgroundColor: theme.COLORS.PRIMARY || "#2196F3", + backgroundColor: theme.COLORS.PRIMARY || '#2196F3', }, filterButtonText: { fontSize: 14, - fontWeight: "500", - color: theme.COLORS.TEXT || "#333333", + fontWeight: '500', + color: theme.COLORS.TEXT || '#333333', }, filterButtonTextActive: { - color: "#FFFFFF", + color: '#FFFFFF', }, messagesContainer: { flex: 1, }, messageContainer: { - backgroundColor: theme.COLORS.BG || "#F8F9FA", + backgroundColor: theme.COLORS.BG || '#F8F9FA', borderRadius: 8, padding: 12, marginBottom: 10, borderLeftWidth: 4, - borderLeftColor: theme.COLORS.PRIMARY || "#2196F3", + borderLeftColor: theme.COLORS.PRIMARY || '#2196F3', }, messageHeader: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', marginBottom: 8, }, messageType: { fontSize: 14, - fontWeight: "600", - color: theme.COLORS.PRIMARY || "#2196F3", - textTransform: "uppercase", + fontWeight: '600', + color: theme.COLORS.PRIMARY || '#2196F3', + textTransform: 'uppercase', }, messageTimestamp: { fontSize: 12, - color: theme.COLORS.GRAY || "#9E9E9E", + color: theme.COLORS.GRAY || '#9E9E9E', }, messageText: { fontSize: 14, - color: theme.COLORS.TEXT || "#333333", + color: theme.COLORS.TEXT || '#333333', lineHeight: 20, }, reportContainer: { - backgroundColor: theme.COLORS.BG || "#F8F9FA", + backgroundColor: theme.COLORS.BG || '#F8F9FA', borderRadius: 8, padding: 12, marginBottom: 10, borderLeftWidth: 4, - borderLeftColor: theme.COLORS.PRIMARY || "#2196F3", + borderLeftColor: theme.COLORS.PRIMARY || '#2196F3', }, reportHeader: { fontSize: 16, - fontWeight: "600", - color: theme.COLORS.PRIMARY || "#2196F3", + fontWeight: '600', + color: theme.COLORS.PRIMARY || '#2196F3', marginBottom: 8, }, reportDetails: { fontSize: 13, - color: theme.COLORS.GRAY || "#9E9E9E", + color: theme.COLORS.GRAY || '#9E9E9E', marginBottom: 12, }, singleReport: { - backgroundColor: theme.COLORS.BG || "#E0E0E0", + backgroundColor: theme.COLORS.BG || '#E0E0E0', borderRadius: 6, padding: 10, marginBottom: 8, }, reportId: { fontSize: 14, - fontWeight: "500", - color: theme.COLORS.TEXT || "#333333", + fontWeight: '500', + color: theme.COLORS.TEXT || '#333333', marginBottom: 4, }, reportLocation: { fontSize: 13, - color: theme.COLORS.GRAY || "#9E9E9E", + color: theme.COLORS.GRAY || '#9E9E9E', marginBottom: 4, }, reportTime: { fontSize: 12, - color: theme.COLORS.GRAY || "#9E9E9E", + color: theme.COLORS.GRAY || '#9E9E9E', marginBottom: 4, }, analysisInfo: { fontSize: 12, - color: theme.COLORS.GRAY || "#9E9E9E", + color: theme.COLORS.GRAY || '#9E9E9E', }, moreReports: { fontSize: 12, - color: theme.COLORS.GRAY || "#9E9E9E", + color: theme.COLORS.GRAY || '#9E9E9E', marginTop: 8, }, noMessagesText: { - textAlign: "center", - color: theme.COLORS.GRAY || "#9E9E9E", + textAlign: 'center', + color: theme.COLORS.GRAY || '#9E9E9E', fontSize: 16, marginTop: 20, }, clearButton: { - backgroundColor: theme.COLORS.ERROR || "#F44336", + backgroundColor: theme.COLORS.ERROR || '#F44336', paddingHorizontal: 20, paddingVertical: 12, borderRadius: 6, - alignSelf: "center", + alignSelf: 'center', marginTop: 10, }, clearButtonText: { - color: "#FFFFFF", + color: '#FFFFFF', fontSize: 14, - fontWeight: "600", + fontWeight: '600', }, }); diff --git a/src/screens/WebSocketDemoScreen.js b/src/screens/WebSocketDemoScreen.js index 28903aa..dcebf38 100644 --- a/src/screens/WebSocketDemoScreen.js +++ b/src/screens/WebSocketDemoScreen.js @@ -13,8 +13,9 @@ import {useWebSocket} from '../hooks/useWebSocket'; import {theme} from '../services/Common/theme'; const WebSocketDemoScreen = () => { + // TODO: Handle prod vs dev env const [serverUrl, setServerUrl] = useState( - 'wss://live.cleanapp.io/api/v3/reports/listen', + 'wss://devlive.cleanapp.io/api/v3/reports/listen', ); // Centralized WebSocket connection const { diff --git a/yarn.lock b/yarn.lock index f6176e6..88ad8ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1008,7 +1008,20 @@ "@babel/parser" "^7.26.9" "@babel/types" "^7.26.9" -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.8", "@babel/traverse@^7.26.9": +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3": + version "7.26.9" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz" + integrity sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.9" + "@babel/parser" "^7.26.9" + "@babel/template" "^7.26.9" + "@babel/types" "^7.26.9" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.8", "@babel/traverse@^7.26.9": version "7.26.9" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz" integrity sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg== @@ -1475,6 +1488,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@notifee/react-native@^9.1.8": + version "9.1.8" + resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-9.1.8.tgz#3d55cb3fbcc21f9e35931e366afdf64b294da891" + integrity sha512-Az/dueoPerJsbbjRxu8a558wKY+gONUrfoy3Hs++5OqbeMsR0dYe6P+4oN6twrLFyzAhEA1tEoZRvQTFDRmvQg== + "@react-native-async-storage/async-storage@^2.1.2": version "2.2.0" resolved "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz" From 074f9bd47fd195bda0bd9d9eaa8555c628fe1dd1 Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 01:15:23 +0530 Subject: [PATCH 04/13] Add websocket and notification components to the first tab --- src/components/WebSocketMessages.js | 77 +---------- src/screens/CacheScreen.js | 203 ++++++++++++++++------------ src/screens/WebSocketDemoScreen.js | 43 +++--- 3 files changed, 137 insertions(+), 186 deletions(-) diff --git a/src/components/WebSocketMessages.js b/src/components/WebSocketMessages.js index 3cccf89..627ad1e 100644 --- a/src/components/WebSocketMessages.js +++ b/src/components/WebSocketMessages.js @@ -247,82 +247,7 @@ const WebSocketMessages = ({ } } - if (!isConnected) { - return ( - - - 🔌 WebSocket not connected. Connect first to receive messages. - - - ); - } - - return ( - - {/* Message Type Filter */} - - setSelectedType('all')}> - - All ({messages.length}) - - - setSelectedType('reports')}> - - Reports ({messages.filter(m => m.type === 'reports').length}) - - - - - {/* Messages */} - - {filteredMessages.length === 0 ? ( - - 📭 No messages received yet. Reports will appear here when - broadcasted by the server. - - ) : ( - filteredMessages.map(message => ( - - - {message.type} - - {formatTimestamp(message.timestamp)} - - - {renderMessage(message)} - - )) - )} - - - {/* Clear Button */} - {messages.length > 0 && ( - - Clear Messages - - )} - - ); + return <>; }; const styles = StyleSheet.create({ diff --git a/src/screens/CacheScreen.js b/src/screens/CacheScreen.js index 9eff0e1..2ef67cf 100644 --- a/src/screens/CacheScreen.js +++ b/src/screens/CacheScreen.js @@ -1,9 +1,21 @@ -import React, { useState } from 'react'; -import { useFocusEffect } from '@react-navigation/native'; -import { Linking, Modal, Pressable, ScrollView, StyleSheet, View, Text, TextInput, ToastAndroid, Alert, Platform } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { fontFamilies } from '../utils/fontFamilies'; -import { theme } from '../services/Common/theme'; +import React, {useState} from 'react'; +import {useFocusEffect} from '@react-navigation/native'; +import { + Linking, + Modal, + Pressable, + ScrollView, + StyleSheet, + View, + Text, + TextInput, + ToastAndroid, + Alert, + Platform, +} from 'react-native'; +import {SafeAreaView} from 'react-native-safe-area-context'; +import {fontFamilies} from '../utils/fontFamilies'; +import {theme} from '../services/Common/theme'; import Ripple from '../components/Ripple'; import WalletSettingsIcon from '../assets/ico_cache_settings.svg'; @@ -12,8 +24,8 @@ import BottomSheetDialog from '../components/BotomSheetDialog'; import CopySmallIcon from '../assets/ico_copy_small.svg'; import EyeSmallIcon from '../assets/ico_eye_small.svg'; import EyeOffIcon from '../assets/ico_eye_off.svg'; -import { BlurView } from '@react-native-community/blur'; -import { Row } from '../components/Row'; +import {BlurView} from '@react-native-community/blur'; +import {Row} from '../components/Row'; import Clipboard from '@react-native-clipboard/clipboard'; import { getPrivacySetting, @@ -24,17 +36,18 @@ import { setPrivacySetting, setUserName, } from '../services/DataManager'; -import { useTranslation } from 'react-i18next'; +import {useTranslation} from 'react-i18next'; import { getBlockchainLink, getRewardStats, updateOrCreateUser, updatePrivacyAndTOC, } from '../services/API/APIManager'; -import { useStateValue } from '../services/State/State'; -import { actions } from '../services/State/Reducer'; +import {useStateValue} from '../services/State/State'; +import {actions} from '../services/State/Reducer'; +import WebSocketDemoScreen from './WebSocketDemoScreen'; -const CacheScreen = (props) => { +const CacheScreen = props => { const [avatarOpened, setAvatarOpened] = useState(false); const [privacyOpened, setPrivacyOpened] = useState(false); const [cacheSettingOpened, setCacheSettingOpened] = useState(false); @@ -43,10 +56,10 @@ const CacheScreen = (props) => { const [avatarName, setAvatarName] = useState(''); const [shareDataStatus, setShareDataStatus] = useState(0); - const [{ cacheVault }, dispatch] = useStateValue(); + const [{cacheVault}, dispatch] = useStateValue(); const [blockchainLink, setBlockchainLink] = useState(''); - const { t } = useTranslation(); + const {t} = useTranslation(); const privacy_values = [ { @@ -80,31 +93,39 @@ const CacheScreen = (props) => { React.useCallback(() => { const internalFunc = async () => { await initWallet(); - getUserName().then((resp) => { + getUserName().then(resp => { if (resp) { setAvatarName(resp.userName); } }); - getPrivacySetting().then((resp) => { + getPrivacySetting().then(resp => { if (resp) { setShareDataStatus(resp); } }); const wa = await getWalletAddress(); setWalletAddress(await getWalletAddress(wa)); - getRewardStats(wa).then((resp) => { + getRewardStats(wa).then(resp => { if (resp && resp.ok) { let cacheResult = { reports: resp.stats.kitns_daily + resp.stats.kitns_disbursed || 0, - referrals: resp.stats.kitns_ref_daily + resp.stats.kitns_ref_disbursed || 0, + referrals: + resp.stats.kitns_ref_daily + resp.stats.kitns_ref_disbursed || + 0, dailyReports: resp.stats.kitns_daily || 0, dailyReferrals: resp.stats.kitns_ref_daily || 0, disbursedReports: resp.stats.kitns_disbursed || 0, disbursedReferrals: resp.stats.kitns_ref_disbursed || 0, - disbursedTotal: resp.stats.kitns_disbursed + resp.stats.kitns_ref_disbursed || 0, - dailyTotal: resp.stats.kitns_daily + resp.stats.kitns_ref_daily || 0, - total: resp.stats.kitns_daily + resp.stats.kitns_ref_daily + - resp.stats.kitns_disbursed + resp.stats.kitns_ref_disbursed || 0, + disbursedTotal: + resp.stats.kitns_disbursed + resp.stats.kitns_ref_disbursed || + 0, + dailyTotal: + resp.stats.kitns_daily + resp.stats.kitns_ref_daily || 0, + total: + resp.stats.kitns_daily + + resp.stats.kitns_ref_daily + + resp.stats.kitns_disbursed + + resp.stats.kitns_ref_disbursed || 0, }; dispatch({ type: actions.SET_CACHE_VAULT, @@ -113,32 +134,30 @@ const CacheScreen = (props) => { setCacheVault(cacheResult); } }); - getBlockchainLink(wa).then((resp) => { + getBlockchainLink(wa).then(resp => { if (resp && resp.ok) { setBlockchainLink(resp.blockchainLink); } }); - } + }; internalFunc(); }, []), ); const AvatarSheet = ({ isVisible = false, - onClose = () => { }, - onUpdateUser = async () => { }, + onClose = () => {}, + onUpdateUser = async () => {}, }) => { - const [localAvatarName, setLocalAvatarName] = useState(avatarName === walletAddress ? '' : avatarName); + const [localAvatarName, setLocalAvatarName] = useState( + avatarName === walletAddress ? '' : avatarName, + ); return ( - + {t('cachescreen.editavatar')} { onChangeText={setLocalAvatarName} /> { const updateUserResult = await onUpdateUser(localAvatarName); if (updateUserResult) { @@ -160,7 +179,7 @@ const CacheScreen = (props) => { {t('cachescreen.submit')} { onClose(); }}> @@ -169,23 +188,23 @@ const CacheScreen = (props) => { ); - } + }; const PrivacySheet = ({ isVisible = false, dataSharingOption = 0, - onClose = () => { }, + onClose = () => {}, }) => { const [privacySelected, setPrivacySelected] = useState(dataSharingOption); - const selectPrivacy = (privacy_index) => { + const selectPrivacy = privacy_index => { setPrivacySelected(privacy_index); }; const setPrivacy = async () => { await updatePrivacyAndTOC( walletAddress, - privacySelected === 0 ? 'share_data_live' : 'not_share_data_live' + privacySelected === 0 ? 'share_data_live' : 'not_share_data_live', ); setPrivacySetting(privacySelected); setShareDataStatus(privacySelected); @@ -193,13 +212,11 @@ const CacheScreen = (props) => { }; return ( - + - {t('cachescreen.privacysettings')} + + {t('cachescreen.privacysettings')} + {privacy_values.map((element, index) => ( { width: '80%', }}> selectPrivacy(index)}> - + {element.value} {element.sub_value} @@ -228,7 +245,7 @@ const CacheScreen = (props) => { ))} {t('cachescreen.Confirm')} @@ -237,7 +254,7 @@ const CacheScreen = (props) => { ); }; - const CacheSettingSheet = ({ isVisible = false, onClose = () => { } }) => { + const CacheSettingSheet = ({isVisible = false, onClose = () => {}}) => { const [isShowPrivateKey, setIsShowPrivateKey] = useState(true); const [isShowMnemonics, setIsShowMnemonics] = useState(true); @@ -249,7 +266,7 @@ const CacheScreen = (props) => { setIsShowPrivateKey(!isShowPrivateKey); }; - const onCopyWalletAddress = (address) => { + const onCopyWalletAddress = address => { Clipboard.setString(address); if (Platform.OS === 'ios') { Alert.alert('Copied to clipboard'); @@ -273,7 +290,7 @@ const CacheScreen = (props) => { {t('cachescreen.youraddress')} - {walletAddress} + {walletAddress} onCopyWalletAddress(walletAddress)}> @@ -287,12 +304,12 @@ const CacheScreen = (props) => { paddingHorizontal: 8, }}> - + {t('cachescreen.privatekey')} - + {walletInfo.privateKey} {isShowPrivateKey && ( @@ -311,7 +328,7 @@ const CacheScreen = (props) => { - + {isShowPrivateKey ? t('cachescreen.taptoreveal') : t('cachescreen.taptohide')} @@ -323,14 +340,14 @@ const CacheScreen = (props) => { + style={{...styles.blue30Card, marginTop: 24, paddingHorizontal: 8}}> {t('cachescreen.keepyourkitnsafe')} - + - + {t('cachescreen.mnemonicphrase')} @@ -358,7 +375,7 @@ const CacheScreen = (props) => { - + {isShowMnemonics ? t('cachescreen.taptoreveal') : t('cachescreen.taptohide')} @@ -370,7 +387,7 @@ const CacheScreen = (props) => { {t('cachescreen.back')} @@ -385,7 +402,6 @@ const CacheScreen = (props) => { onClose={onClose} title={t('cachescreen.cachesettings')} headerIcon={}> - ); @@ -399,38 +415,47 @@ const CacheScreen = (props) => { {t('cachescreen.total')} - - {cacheVault.total || 0} {'KITN'} + + {cacheVault.total || 0}{' '} + {'KITN'} {t('cachescreen.fromReports')} - {cacheVault.reports || 0} {'KITN'} + {cacheVault.reports || 0}{' '} + {'KITN'} {t('cachescreen.fromReferrals')} - {cacheVault.referrals || 0} {'KITN'} + {cacheVault.referrals || 0}{' '} + {'KITN'} {t('cachescreen.todaysReports')} - {cacheVault.dailyReports} {'KITN'} + {cacheVault.dailyReports}{' '} + {'KITN'} - {t('cachescreen.todaysReferrals')} - {cacheVault.dailyReferrals} {'KITN'} + {t('cachescreen.todaysReferrals')} + + + {cacheVault.dailyReferrals}{' '} + {'KITN'} - {t('cachescreen.todaysLitterbox')} + + {t('cachescreen.todaysLitterbox')} + {`${cacheVault.dailyTotal || 0} `} @@ -439,13 +464,16 @@ const CacheScreen = (props) => { - {t('cachescreen.blockchainLink')} + + {t('cachescreen.blockchainLink')} + - + Linking.openURL(blockchainLink)} - >{blockchainLink} + style={{...styles.txt12bold, color: theme.COLORS.GREEN_LINK}} + onPress={() => Linking.openURL(blockchainLink)}> + {blockchainLink} + {/** Avatar */} @@ -453,14 +481,18 @@ const CacheScreen = (props) => { {t('cachescreen.avatar')} - + - {avatarName === walletAddress ? t('cachescreen.avatarnotset') : avatarName} + {avatarName === walletAddress + ? t('cachescreen.avatarnotset') + : avatarName} - {avatarName === walletAddress ? t('cachescreen.create') : t('cachescreen.edit')} + {avatarName === walletAddress + ? t('cachescreen.create') + : t('cachescreen.edit')} @@ -471,20 +503,18 @@ const CacheScreen = (props) => { {t('cachescreen.privacy')} - + {shareDataStatus == 0 ? t('cachescreen.Mapreportswithavatar') - : t('cachescreen.Sharereportsanonymously') - } + : t('cachescreen.Sharereportsanonymously')} {shareDataStatus == 0 ? t('cachescreen.Traceable') - : t('cachescreen.Nodatacollectedwhileflagging') - } + : t('cachescreen.Nodatacollectedwhileflagging')} @@ -492,7 +522,7 @@ const CacheScreen = (props) => { - + {t('cachescreen.cachesettings')} @@ -503,6 +533,7 @@ const CacheScreen = (props) => { + { onClose={() => { setAvatarOpened(false); }} - onUpdateUser={async (userName) => { + onUpdateUser={async userName => { if (userName.length == 0) { Alert.alert( t('onboarding.Error'), t('cachescreen.errAvatarEmpty'), - [{ text: t('onboarding.Ok'), type: 'cancel' }], + [{text: t('onboarding.Ok'), type: 'cancel'}], ); return false; } @@ -525,11 +556,11 @@ const CacheScreen = (props) => { Alert.alert( t('onboarding.Error'), t('onboarding.ErrSameUsernameExists'), - [{ text: t('onboarding.Ok'), type: 'cancel' }], + [{text: t('onboarding.Ok'), type: 'cancel'}], ); return false; } - setUserName({ userName: userName }); + setUserName({userName: userName}); return true; } else { return false; @@ -742,7 +773,7 @@ const styles = StyleSheet.create({ padding: 10, color: 'white', fontSize: 20, - fontWeight: 'bold' + fontWeight: 'bold', }, }); diff --git a/src/screens/WebSocketDemoScreen.js b/src/screens/WebSocketDemoScreen.js index dcebf38..32b7336 100644 --- a/src/screens/WebSocketDemoScreen.js +++ b/src/screens/WebSocketDemoScreen.js @@ -60,32 +60,27 @@ const WebSocketDemoScreen = () => { style={styles.scrollView} showsVerticalScrollIndicator={false}> {/* WebSocket Status */} - - Connection Status - - + {/* WebSocket Messages */} - - Real-time Messages - - + + ); From c8bef6353b7c2c75e472014e62f64937bbb4cabf Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 02:04:33 +0530 Subject: [PATCH 05/13] Handle prod vs dev websocket url --- src/screens/WebSocketDemoScreen.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/screens/WebSocketDemoScreen.js b/src/screens/WebSocketDemoScreen.js index 32b7336..7205d11 100644 --- a/src/screens/WebSocketDemoScreen.js +++ b/src/screens/WebSocketDemoScreen.js @@ -13,9 +13,10 @@ import {useWebSocket} from '../hooks/useWebSocket'; import {theme} from '../services/Common/theme'; const WebSocketDemoScreen = () => { - // TODO: Handle prod vs dev env const [serverUrl, setServerUrl] = useState( - 'wss://devlive.cleanapp.io/api/v3/reports/listen', + process.env.APP_MODE === 'dev' + ? 'wss://devlive.cleanapp.io/api/v3/reports/listen' + : 'wss://live.cleanapp.io/api/v3/reports/listen', ); // Centralized WebSocket connection const { From cd36d6852646dee58a0d0bd8c309cb8433609e1c Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 02:09:39 +0530 Subject: [PATCH 06/13] Ask notification permission on app start --- App.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/App.tsx b/App.tsx index b7371bf..3fd542b 100644 --- a/App.tsx +++ b/App.tsx @@ -33,6 +33,7 @@ import {I18nextProvider} from 'react-i18next'; import {MenuProvider} from 'react-native-popup-menu'; import {ethers} from 'ethers'; import SplashScreen from 'react-native-splash-screen'; +import notifee from '@notifee/react-native'; enableScreens(); @@ -151,7 +152,8 @@ const App = () => { useEffect(() => { SplashScreen.hide(); - }) + notifee.requestPermission(); + }); return ( From 70a3a0aed71380a4f4afe822bed4933dc318ecc8 Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 11:54:45 +0530 Subject: [PATCH 07/13] Remove notification permission request on app start --- App.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/App.tsx b/App.tsx index 3fd542b..e72268e 100644 --- a/App.tsx +++ b/App.tsx @@ -33,7 +33,6 @@ import {I18nextProvider} from 'react-i18next'; import {MenuProvider} from 'react-native-popup-menu'; import {ethers} from 'ethers'; import SplashScreen from 'react-native-splash-screen'; -import notifee from '@notifee/react-native'; enableScreens(); @@ -152,7 +151,6 @@ const App = () => { useEffect(() => { SplashScreen.hide(); - notifee.requestPermission(); }); return ( From f6812586ae7225e532bd593fcf3eb62795769ea7 Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 12:07:53 +0530 Subject: [PATCH 08/13] Handle camera permissions --- src/screens/CameraScreen.js | 94 +++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/src/screens/CameraScreen.js b/src/screens/CameraScreen.js index b90e135..3e10aba 100644 --- a/src/screens/CameraScreen.js +++ b/src/screens/CameraScreen.js @@ -133,8 +133,33 @@ const CameraScreen = (props) => { const flashScale = useState(0); const flashScaleStatic = useState(1); const tapScale = useState(0); - const { t } = useTranslation(); - const { hasPermission, requestPermission } = useCameraPermission(); + const {t} = useTranslation(); + const {hasPermission, requestPermission} = useCameraPermission(); + + // Enhanced camera permission handler + const handleCameraPermission = async () => { + try { + if (!hasPermission) { + const result = await requestPermission(); + if (!result) { + Alert.alert( + 'Camera Permission Required', + 'This app needs camera access to take photos. Please grant camera permission in Settings.', + [ + {text: 'Cancel', style: 'cancel'}, + {text: 'Settings', onPress: () => Linking.openSettings()}, + ], + ); + } + } + } catch (error) { + console.error('Error handling camera permission:', error); + Alert.alert( + 'Camera Permission Error', + 'There was an error requesting camera permission. Please try again or check your device settings.', + ); + } + }; const isFocused = useIsFocused(); @@ -247,7 +272,8 @@ const CameraScreen = (props) => { }, [phototaken, flashAnimatedValue]); useEffect(() => { - requestPermission(); + // Request camera permission when component mounts + handleCameraPermission(); }, []); const checkPhotoLibraryPermission = async () => { @@ -611,6 +637,26 @@ const CameraScreen = (props) => { animatedProps={animatedProps} /> )} + + {/* Camera Permission Status */} + {!hasPermission && ( + + + Camera Permission Required + + + This app needs camera access to take photos. Please grant + camera permission. + + + + Grant Permission + + + + )} {isInAnnotationMode && photoData && ( { )} - {!phototaken && !isInAnnotationMode && ( + {!phototaken && !isInAnnotationMode && hasPermission && ( <> Date: Wed, 27 Aug 2025 12:09:29 +0530 Subject: [PATCH 09/13] Handle permissions --- src/components/PermissionRequest.js | 114 +++++++++++++++++++ src/services/PermissionManager.js | 163 ++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 src/components/PermissionRequest.js create mode 100644 src/services/PermissionManager.js diff --git a/src/components/PermissionRequest.js b/src/components/PermissionRequest.js new file mode 100644 index 0000000..a6b3561 --- /dev/null +++ b/src/components/PermissionRequest.js @@ -0,0 +1,114 @@ +import React, {useState, useEffect} from 'react'; +import {View, Text, TouchableOpacity, Alert, StyleSheet} from 'react-native'; +import PermissionManager from '../services/PermissionManager'; + +const PermissionRequest = () => { + const [permissions, setPermissions] = useState({ + location: {granted: false, reason: ''}, + camera: {granted: false, reason: ''}, + notifications: {granted: false, reason: ''}, + }); + + useEffect(() => { + checkPermissions(); + }, []); + + const checkPermissions = async () => { + const result = await PermissionManager.checkAllPermissions(); + setPermissions(result); + }; + + const requestPermission = async type => { + let result; + + switch (type) { + case 'location': + result = await PermissionManager.requestLocationPermission(); + break; + case 'camera': + result = await PermissionManager.requestCameraPermission(); + break; + case 'notifications': + result = await PermissionManager.requestNotificationPermission(); + break; + } + + if (result.granted) { + Alert.alert('Success', `${type} permission granted!`); + } else if (result.reason === 'blocked') { + Alert.alert( + 'Permission Required', + `${type} permission is required. Please enable it in Settings.`, + [ + {text: 'Cancel', style: 'cancel'}, + {text: 'Settings', onPress: PermissionManager.openSettings}, + ], + ); + } + + checkPermissions(); // Refresh permissions + }; + + const renderPermissionButton = (type, permission) => ( + requestPermission(type)} + disabled={permission.granted}> + + {permission.granted ? `${type} Granted` : `Request ${type}`} + + + ); + + return ( + + App Permissions + {renderPermissionButton('location', permissions.location)} + {renderPermissionButton('camera', permissions.camera)} + {renderPermissionButton('notifications', permissions.notifications)} + + {permissions.allGranted && ( + All permissions granted! 🎉 + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + padding: 20, + }, + title: { + fontSize: 24, + fontWeight: 'bold', + marginBottom: 20, + textAlign: 'center', + }, + permissionButton: { + backgroundColor: '#007AFF', + padding: 15, + borderRadius: 8, + marginBottom: 10, + }, + grantedButton: { + backgroundColor: '#34C759', + }, + buttonText: { + color: 'white', + textAlign: 'center', + fontSize: 16, + fontWeight: '600', + }, + successText: { + color: '#34C759', + textAlign: 'center', + fontSize: 18, + fontWeight: '600', + marginTop: 20, + }, +}); + +export default PermissionRequest; diff --git a/src/services/PermissionManager.js b/src/services/PermissionManager.js new file mode 100644 index 0000000..7dbf334 --- /dev/null +++ b/src/services/PermissionManager.js @@ -0,0 +1,163 @@ +import {Platform} from 'react-native'; +import { + check, + request, + PERMISSIONS, + RESULTS, + openSettings, +} from 'react-native-permissions'; + +class PermissionManager { + // Location Permissions + static async requestLocationPermission() { + try { + let permission; + + if (Platform.OS === 'ios') { + permission = PERMISSIONS.IOS.LOCATION_WHEN_IN_USE; + } else { + permission = PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION; + } + + const result = await check(permission); + + if (result === RESULTS.UNAVAILABLE) { + return {granted: false, reason: 'unavailable'}; + } + + if (result === RESULTS.DENIED) { + const requestResult = await request(permission); + return { + granted: requestResult === RESULTS.GRANTED, + reason: requestResult, + }; + } + + if (result === RESULTS.BLOCKED) { + return {granted: false, reason: 'blocked'}; + } + + return {granted: result === RESULTS.GRANTED, reason: result}; + } catch (error) { + return {granted: false, reason: 'error', error}; + } + } + + // Camera Permissions + static async requestCameraPermission() { + try { + let permission; + + if (Platform.OS === 'ios') { + permission = PERMISSIONS.IOS.CAMERA; + } else { + permission = PERMISSIONS.ANDROID.CAMERA; + } + + const result = await check(permission); + + if (result === RESULTS.UNAVAILABLE) { + return {granted: false, reason: 'unavailable'}; + } + + if (result === RESULTS.DENIED) { + const requestResult = await request(permission); + return { + granted: requestResult === RESULTS.GRANTED, + reason: requestResult, + }; + } + + if (result === RESULTS.BLOCKED) { + return {granted: false, reason: 'blocked'}; + } + + return {granted: result === RESULTS.GRANTED, reason: result}; + } catch (error) { + console.error('Camera permission error:', error); + return {granted: false, reason: 'error', error}; + } + } + + // Check camera permission without requesting + static async checkCameraPermission() { + try { + let permission; + + if (Platform.OS === 'ios') { + permission = PERMISSIONS.IOS.CAMERA; + } else { + permission = PERMISSIONS.ANDROID.CAMERA; + } + + const result = await check(permission); + return {granted: result === RESULTS.GRANTED, reason: result}; + } catch (error) { + console.error('Check camera permission error:', error); + return {granted: false, reason: 'error', error}; + } + } + + // Notification Permissions (iOS 13+ and Android 13+) + static async requestNotificationPermission() { + try { + let permission; + + if (Platform.OS === 'ios') { + permission = PERMISSIONS.IOS.NOTIFICATIONS; + } else { + // Android 13+ (API level 33+) + if (Platform.Version >= 33) { + permission = PERMISSIONS.ANDROID.POST_NOTIFICATIONS; + } else { + return {granted: true, reason: 'not_required'}; + } + } + + const result = await check(permission); + + if (result === RESULTS.UNAVAILABLE) { + return {granted: false, reason: 'unavailable'}; + } + + if (result === RESULTS.DENIED) { + const requestResult = await request(permission); + return { + granted: requestResult === RESULTS.GRANTED, + reason: requestResult, + }; + } + + if (result === RESULTS.BLOCKED) { + return {granted: false, reason: 'blocked'}; + } + + return {granted: result === RESULTS.GRANTED, reason: result}; + } catch (error) { + return {granted: false, reason: 'error', error}; + } + } + + // Check all permissions at once + static async checkAllPermissions() { + const [location, camera, notifications] = await Promise.all([ + this.requestLocationPermission(), + this.requestCameraPermission(), + this.requestNotificationPermission(), + ]); + + return { + location, + camera, + notifications, + allGranted: location.granted && camera.granted && notifications.granted, + }; + } + + // Open app settings + static openSettings() { + openSettings(); + } +} + +export default PermissionManager; From cdeb122d225629aca932b1b612b2587f463b8daf Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 14:56:36 +0530 Subject: [PATCH 10/13] Update notification title, description from websocket payload and also check distance --- src/components/PermissionRequest.js | 13 +- src/components/PermissionTest.js | 216 ++++++++++++++++ src/components/WebSocketMessages.js | 279 +++++++++++++++++--- src/screens/CacheScreen.js | 2 + src/services/PermissionManager.js | 126 +++++++-- src/services/WebSocketService.js | 380 +++++++++++++++++++--------- 6 files changed, 841 insertions(+), 175 deletions(-) create mode 100644 src/components/PermissionTest.js diff --git a/src/components/PermissionRequest.js b/src/components/PermissionRequest.js index a6b3561..861805b 100644 --- a/src/components/PermissionRequest.js +++ b/src/components/PermissionRequest.js @@ -63,18 +63,7 @@ const PermissionRequest = () => { ); - return ( - - App Permissions - {renderPermissionButton('location', permissions.location)} - {renderPermissionButton('camera', permissions.camera)} - {renderPermissionButton('notifications', permissions.notifications)} - - {permissions.allGranted && ( - All permissions granted! 🎉 - )} - - ); + return <>; }; const styles = StyleSheet.create({ diff --git a/src/components/PermissionTest.js b/src/components/PermissionTest.js new file mode 100644 index 0000000..af253eb --- /dev/null +++ b/src/components/PermissionTest.js @@ -0,0 +1,216 @@ +import React, {useState, useEffect} from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + Alert, + Platform, +} from 'react-native'; +import PermissionManager from '../services/PermissionManager'; + +const PermissionTest = () => { + const [permissions, setPermissions] = useState({ + location: {granted: false, reason: ''}, + camera: {granted: false, reason: ''}, + notifications: {granted: false, reason: ''}, + }); + + useEffect(() => { + checkAllPermissions(); + }, []); + + const checkAllPermissions = async () => { + try { + const result = await PermissionManager.checkAllPermissions(); + setPermissions(result); + console.log('Permission check result:', result); + } catch (error) { + console.error('Error checking permissions:', error); + } + }; + + const initializeNotifications = async () => { + try { + const result = await PermissionManager.initializeNotifications(); + console.log('Notification initialization result:', result); + checkAllPermissions(); // Refresh permissions after initialization + } catch (error) { + console.error('Error initializing notifications:', error); + } + }; + + const requestSpecificPermission = async type => { + try { + let result; + + switch (type) { + case 'location': + result = await PermissionManager.requestLocationPermission(); + break; + case 'camera': + result = await PermissionManager.requestCameraPermission(); + break; + case 'notifications': + result = await PermissionManager.requestNotificationPermission(); + break; + } + + console.log(`${type} permission result:`, result); + + if (result.granted) { + Alert.alert('Success', `${type} permission granted!`); + } else { + Alert.alert('Permission Result', `${type}: ${result.reason}`, [ + {text: 'OK', style: 'default'}, + ...(result.reason === 'blocked' + ? [{text: 'Settings', onPress: PermissionManager.openSettings}] + : []), + ]); + } + + // Refresh permissions + checkAllPermissions(); + } catch (error) { + console.error(`Error requesting ${type} permission:`, error); + Alert.alert('Error', `Failed to request ${type} permission`); + } + }; + + const getPermissionStatus = permission => { + if (permission.granted) return '✅ Granted'; + if (permission.reason === 'blocked') return '🚫 Blocked'; + if (permission.reason === 'denied') return '❌ Denied'; + if (permission.reason === 'unavailable') return '⚠️ Unavailable'; + return `❓ ${permission.reason}`; + }; + + return ( + + Permission Test + Platform: {Platform.OS} + + + Location + + {getPermissionStatus(permissions.location)} + + requestSpecificPermission('location')}> + Request Location + + + + + Camera + + {getPermissionStatus(permissions.camera)} + + requestSpecificPermission('camera')}> + Request Camera + + + + + Notifications + + {getPermissionStatus(permissions.notifications)} + + requestSpecificPermission('notifications')}> + Request Notifications + + + + + Refresh All + + + + Open Settings + + + + Initialize Notifications + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 20, + backgroundColor: '#f5f5f5', + }, + title: { + fontSize: 24, + fontWeight: 'bold', + textAlign: 'center', + marginBottom: 10, + color: '#333', + }, + subtitle: { + fontSize: 16, + textAlign: 'center', + marginBottom: 30, + color: '#666', + }, + permissionSection: { + backgroundColor: 'white', + padding: 20, + borderRadius: 10, + marginBottom: 15, + shadowColor: '#000', + shadowOffset: {width: 0, height: 2}, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + marginBottom: 10, + color: '#333', + }, + statusText: { + fontSize: 16, + marginBottom: 15, + color: '#666', + }, + button: { + backgroundColor: '#007AFF', + padding: 12, + borderRadius: 8, + alignItems: 'center', + }, + buttonText: { + color: 'white', + fontSize: 16, + fontWeight: '600', + }, + refreshButton: { + backgroundColor: '#34C759', + marginTop: 10, + }, + settingsButton: { + backgroundColor: '#FF9500', + marginTop: 10, + }, + initButton: { + backgroundColor: '#AF52DE', + marginTop: 10, + }, +}); + +export default PermissionTest; diff --git a/src/components/WebSocketMessages.js b/src/components/WebSocketMessages.js index 627ad1e..fc65e1a 100644 --- a/src/components/WebSocketMessages.js +++ b/src/components/WebSocketMessages.js @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useState, useRef, useCallback} from 'react'; import { ScrollView, StyleSheet, @@ -8,41 +8,73 @@ import { } from 'react-native'; import {theme} from '../services/Common/theme'; import notifee from '@notifee/react-native'; +import {getLocation} from '../functions/geolocation'; +import webSocketService from '../services/WebSocketService'; const WebSocketMessages = ({ url, // messageTypes = [], isConnected, subscribe, - unsubscribe, }) => { const [messages, setMessages] = useState([]); const [filteredMessages, setFilteredMessages] = useState([]); const [selectedType, setSelectedType] = useState('all'); + // Use refs to track subscription and prevent duplicate subscriptions + const subscriptionRef = useRef(null); + const isSubscribedRef = useRef(false); + // Subscribe to report messages from Go backend + // + // SOLUTION: Never unsubscribe - keep subscription active permanently + // This prevents the immediate unsubscribe issue that was causing + // the subscription to be removed right after creation + // + // IMPORTANT: Even though we removed the cleanup function, React or the parent + // component might still call unsubscribe. We handle this by blocking + // unsubscribe calls for 'reports' type in WebSocketService. useEffect(() => { console.log('📱 [WebSocketMessages] useEffect triggered:', { isConnected, url, - // messageTypes, hasSubscribe: !!subscribe, - hasUnsubscribe: !!unsubscribe, subscribeType: typeof subscribe, - unsubscribeType: typeof unsubscribe, timestamp: new Date().toISOString(), - // Add detailed dependency tracking - isConnectedValue: isConnected, - urlValue: url, - // messageTypesValue: messageTypes, - // messageTypesLength: messageTypes.length, - // messageTypesString: JSON.stringify(messageTypes), + isAlreadySubscribed: isSubscribedRef.current, + currentSubscriptionId: subscriptionRef.current, }); - if (isConnected && subscribe && unsubscribe) { + // Only proceed if we have all required functions and are connected + if (!isConnected || !subscribe) { + console.log( + '📱 [WebSocketMessages] WebSocket not connected or missing subscribe function, skipping subscription', + { + isConnected, + hasSubscribe: !!subscribe, + subscribeValue: subscribe, + subscribeType: typeof subscribe, + subscribeIsFunction: typeof subscribe === 'function', + }, + ); + return; + } + + // Prevent multiple subscriptions + if (isSubscribedRef.current && subscriptionRef.current) { console.log( - "📱 [WebSocketMessages] WebSocket connected, subscribing to 'reports' type", + '📱 [WebSocketMessages] Already subscribed, skipping duplicate subscription', + {subscriptionId: subscriptionRef.current}, ); + return; + } + + console.log( + "📱 [WebSocketMessages] WebSocket connected, subscribing to 'reports' type", + ); + + try { + console.log('📱 [WebSocketMessages] About to call subscribe function...'); // Subscribe to the "reports" message type that Go backend sends const subscriptionId = subscribe('reports', (payload, metadata) => { @@ -73,7 +105,16 @@ const WebSocketMessages = ({ dataType: typeof newMessage.data, }); - onDisplayNotification(); + // check if the report is within the user's radius and display notification + checkUserLocation(payload).then(isInRange => { + if (isInRange) { + onDisplayNotification(payload); + } else { + console.log( + "📱 [WebSocketMessages] Report is not within the user's radius", + ); + } + }); setMessages(prev => { const updatedMessages = [newMessage, ...prev.slice(0, 99)]; // Keep last 100 @@ -86,25 +127,91 @@ const WebSocketMessages = ({ }); }); - console.log( - '📱 [WebSocketMessages] Subscription created with ID:', + console.log('📱 [WebSocketMessages] Subscribe function returned:', { subscriptionId, - ); + subscriptionIdType: typeof subscriptionId, + subscriptionIdIsNull: subscriptionId === null, + subscriptionIdIsUndefined: subscriptionId === undefined, + }); + + // Store subscription info in refs + subscriptionRef.current = subscriptionId; + isSubscribedRef.current = true; + + console.log('📱 [WebSocketMessages] Subscription created and stored:', { + subscriptionId, + subscriptionRef: subscriptionRef.current, + isSubscribed: isSubscribedRef.current, + }); - return () => { + // Debug: Check if subscription was actually stored in WebSocketService + if (typeof subscribe === 'function' && subscribe.name === 'subscribe') { console.log( - '📱 [WebSocketMessages] useEffect cleanup running, removing subscription:', - subscriptionId, - 'Reason: useEffect dependencies changed or component unmounting', + '📱 [WebSocketMessages] Subscribe function appears to be from WebSocketService', ); - unsubscribe('reports', subscriptionId); - }; - } else { + } else { + console.log( + '📱 [WebSocketMessages] Subscribe function is NOT from WebSocketService:', + { + functionName: subscribe.name, + functionType: typeof subscribe, + }, + ); + } + + // Debug: Check WebSocketService subscription state + console.log( + '📱 [WebSocketMessages] WebSocketService subscription state:', + ); + console.log( + '📱 [WebSocketMessages] Has reports subscription type:', + webSocketService.hasSubscriptionType('reports'), + ); console.log( - '📱 [WebSocketMessages] WebSocket not connected or missing functions, skipping subscription', + '📱 [WebSocketMessages] Reports subscription count:', + webSocketService.getSubscriptionCount('reports'), ); + webSocketService.debugSubscriptions(); + } catch (error) { + console.error( + '📱 [WebSocketMessages] Error creating subscription:', + error, + ); + return; } - }, [isConnected, url]); // Restored dependencies now that re-render issue is fixed + + // NO CLEANUP FUNCTION - subscription stays active forever + // This prevents the immediate unsubscribe issue + }, [isConnected, url, subscribe]); // Removed unsubscribe from dependencies + + // NO CLEANUP ON UNMOUNT - subscription stays active forever + // This prevents any unsubscribe calls that could cause issues + + // Debug effect to track subscription state changes + useEffect(() => { + console.log( + '📱 [WebSocketMessages] Debug effect - checking subscription state:', + { + isConnected, + url, + hasSubscribe: !!subscribe, + subscribeType: typeof subscribe, + subscriptionRef: subscriptionRef.current, + isSubscribed: isSubscribedRef.current, + }, + ); + }, [isConnected, url, subscribe]); + + // Debug effect to track subscription state changes + useEffect(() => { + console.log('📱 [WebSocketMessages] Subscription state changed:', { + isConnected, + url, + hasSubscription: !!subscriptionRef.current, + subscriptionId: subscriptionRef.current, + isSubscribed: isSubscribedRef.current, + }); + }, [isConnected, url, subscriptionRef.current, isSubscribedRef.current]); // Filter messages based on selected type useEffect(() => { @@ -219,7 +326,103 @@ const WebSocketMessages = ({ } }; - async function onDisplayNotification() { + const calculateDistance = (location, reportLocation) => { + const R = 6371; // Radius of the earth in km + const dLat = + (reportLocation.latitude - location.latitude) * (Math.PI / 180); + const dLon = + (reportLocation.longitude - location.longitude) * (Math.PI / 180); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(location.latitude * (Math.PI / 180)) * + Math.cos(reportLocation.latitude * (Math.PI / 180)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const d = R * c; // Distance in km + return d; + }; + + const checkUserLocation = async payload => { + // { + // "reports": [ + // { + // "report": { + // "seq": 131, + // "timestamp": "2025-08-27T05:51:31Z", + // "id": "0xdff9426058ccA89C0297fb45E0620bc052899A5c", + // "latitude": 3.320606, + // "longitude": 23.530489 + // }, + // "analysis": [ + // { + // "seq": 131, + // "source": "ChatGPT", + // "analysis_text": "```json\n{\n \"title\": \"Litter Detected on Tram Tracks in Urban Area\",\n \"description\": \"A piece of litter, specifically a small food wrapper, is observed on tram tracks in an urban area, increasing risk to tram operations and contributing to environmental impact.\",\n \"classification\": \"Physical\",\n \"user_info\": {\n \"name\": null,\n \"email\": null,\n \"company\": null,\n \"role\": null,\n \"company_size\": null\n },\n \"location\": \"Intersection near visible tram stop and To Let sign, likely urban setting\",\n \"brand_name\": null,\n \"responsible_party\": \"Municipality Public Works\",\n \"inferred_contact_emails\": [\"cityworks@municipality.gov\", \"cleaning@municipality.gov\", \"trammaintenance@municipality.gov\"],\n \"suggested_remediation\": [\n \"Deploy cleanup crew to the reported location to remove litter from tram tracks.\",\n \"Enhance surveillance and monitoring for littering in public transport areas.\",\n \"Implement public awareness campaigns about the impact of littering on public transport safety.\",\n \"Introduce regular patrols on tram lines to ensure cleanliness and safety.\"\n ],\n \"litter_probability\": 0.95,\n \"hazard_probability\": 0.6,\n \"digital_bug_probabilty\": 0.0,\n \"severity_level\": 0.4,\n \"is_valid\": true\n}\n```", + // "title": "Litter Detected on Tram Tracks in Urban Area", + // "description": "A piece of litter, specifically a small food wrapper, is observed on tram tracks in an urban area, increasing risk to tram operations and contributing to environmental impact.", + // "brand_name": "", + // "brand_display_name": "", + // "litter_probability": 0.95, + // "hazard_probability": 0.6, + // "digital_bug_probability": 0, + // "severity_level": 0.4, + // "summary": "Litter Detected on Tram Tracks in Urban Area: A piece of litter, specifically a small food wrapper, is observed on tram tracks in an urban area, increasing risk to tram operations and contributing to environmental impact.", + // "language": "en", + // "classification": "physical", + // "created_at": "2025-08-27T05:51:42Z", + // "updated_at": "0001-01-01T00:00:00Z" + // } + // ] + // } + // ], + // "count": 1, + // "from_seq": 131, + // "to_seq": 131 + // } + + let isInRange = false; + + try { + const location = await getLocation(); + if (location) { + console.log('Location obtained:', location); + console.log('Latitude:', location.latitude); + console.log('Longitude:', location.longitude); + + if (!payload.reports || payload.reports.length === 0) { + console.log('No reports found in websocket message'); + return false; + } + + const report = payload.reports[0].report; + + const reportLocation = { + latitude: report.latitude, + longitude: report.longitude, + }; + + // Calculate the distance between the user's location and the report's location + const distance = calculateDistance(location, reportLocation); + console.log('Distance:', distance); + if (distance < 100) { + isInRange = true; + } + } else { + console.log('Location permission denied or failed'); + isInRange = false; + } + } catch (error) { + console.log('In catch'); + console.error('Error getting location:', error); + isInRange = false; + } finally { + console.log('In finally'); + return isInRange; + } + }; + + async function onDisplayNotification(payload) { // Request permissions (required for iOS) await notifee.requestPermission(); @@ -230,10 +433,28 @@ const WebSocketMessages = ({ }); try { + if (!payload.reports || payload.reports.length === 0) { + console.log('No reports found in websocket message'); + return false; + } + + const report = payload.reports[0].report; + + let analysisText = ''; + if ( + !payload.reports[0].analysis || + payload.reports[0].analysis.length === 0 + ) { + console.log('No analysis found in report'); + analysisText = ''; + } else { + analysisText = payload.reports[0].analysis[0].summary; + } + // Display a notification await notifee.displayNotification({ - title: 'Notification Title', - body: 'Main body content of the notification', + title: 'A new report has been detected', + body: analysisText, android: { channelId, // pressAction is needed if you want the notification to open the app when pressed diff --git a/src/screens/CacheScreen.js b/src/screens/CacheScreen.js index 2ef67cf..19f236e 100644 --- a/src/screens/CacheScreen.js +++ b/src/screens/CacheScreen.js @@ -37,6 +37,7 @@ import { setUserName, } from '../services/DataManager'; import {useTranslation} from 'react-i18next'; +import PermissionRequest from '../components/PermissionRequest'; import { getBlockchainLink, getRewardStats, @@ -533,6 +534,7 @@ const CacheScreen = props => { + diff --git a/src/services/PermissionManager.js b/src/services/PermissionManager.js index 7dbf334..6d345d4 100644 --- a/src/services/PermissionManager.js +++ b/src/services/PermissionManager.js @@ -6,6 +6,10 @@ import { RESULTS, openSettings, } from 'react-native-permissions'; +import notifee, { + AuthorizationStatus, + AndroidImportance, +} from '@notifee/react-native'; class PermissionManager { // Location Permissions @@ -98,11 +102,37 @@ class PermissionManager { } } - // Notification Permissions (iOS 13+ and Android 13+) + // Notification Permissions using notifee static async requestNotificationPermission() { try { - let permission; + // Request permission using notifee + const settings = await notifee.requestPermission(); + + // Log the authorization status + if (settings.authorizationStatus === AuthorizationStatus.DENIED) { + console.log('User denied notification permissions request'); + return {granted: false, reason: 'denied'}; + } else if ( + settings.authorizationStatus === AuthorizationStatus.AUTHORIZED + ) { + console.log('User granted notification permissions request'); + return {granted: true, reason: 'granted'}; + } else if ( + settings.authorizationStatus === AuthorizationStatus.PROVISIONAL + ) { + console.log( + 'User provisionally granted notification permissions request', + ); + return {granted: true, reason: 'provisional'}; + } else if ( + settings.authorizationStatus === AuthorizationStatus.NOT_DETERMINED + ) { + console.log('Notification permission not determined'); + return {granted: false, reason: 'not_determined'}; + } + // Fallback: check system permission status + let permission; if (Platform.OS === 'ios') { permission = PERMISSIONS.IOS.NOTIFICATIONS; } else { @@ -115,31 +145,58 @@ class PermissionManager { } const result = await check(permission); + return {granted: result === RESULTS.GRANTED, reason: result}; + } catch (error) { + console.error('Notification permission error:', error); + return {granted: false, reason: 'error', error}; + } + } - if (result === RESULTS.UNAVAILABLE) { - return {granted: false, reason: 'unavailable'}; - } - - if (result === RESULTS.DENIED) { - const requestResult = await request(permission); - return { - granted: requestResult === RESULTS.GRANTED, - reason: requestResult, - }; - } + // Check notification permission status without requesting + static async checkNotificationPermission() { + try { + // Check current notification settings + const settings = await notifee.getNotificationSettings(); - if (result === RESULTS.BLOCKED) { - return {granted: false, reason: 'blocked'}; + if (settings.authorizationStatus === AuthorizationStatus.AUTHORIZED) { + return {granted: true, reason: 'authorized'}; + } else if ( + settings.authorizationStatus === AuthorizationStatus.PROVISIONAL + ) { + return {granted: true, reason: 'provisional'}; + } else if (settings.authorizationStatus === AuthorizationStatus.DENIED) { + return {granted: false, reason: 'denied'}; + } else if ( + settings.authorizationStatus === AuthorizationStatus.NOT_DETERMINED + ) { + return {granted: false, reason: 'not_determined'}; } - return {granted: result === RESULTS.GRANTED, reason: result}; + return {granted: false, reason: 'unknown'}; } catch (error) { + console.error('Check notification permission error:', error); return {granted: false, reason: 'error', error}; } } // Check all permissions at once static async checkAllPermissions() { + const [location, camera, notifications] = await Promise.all([ + this.requestLocationPermission(), + this.requestCameraPermission(), + this.checkNotificationPermission(), // Use check instead of request to avoid prompting + ]); + + return { + location, + camera, + notifications, + allGranted: location.granted && camera.granted && notifications.granted, + }; + } + + // Request all permissions at once (use this when you want to prompt user) + static async requestAllPermissions() { const [location, camera, notifications] = await Promise.all([ this.requestLocationPermission(), this.requestCameraPermission(), @@ -158,6 +215,43 @@ class PermissionManager { static openSettings() { openSettings(); } + + // Create notification channel for Android (required for notifications to work) + static async createNotificationChannel() { + if (Platform.OS === 'android') { + try { + const channelId = await notifee.createChannel({ + id: 'default', + name: 'Default Channel', + sound: 'default', + importance: AndroidImportance.HIGH, + }); + console.log('Notification channel created:', channelId); + return channelId; + } catch (error) { + console.error('Error creating notification channel:', error); + return null; + } + } + return null; + } + + // Initialize notification system + static async initializeNotifications() { + try { + // Create notification channel for Android + await this.createNotificationChannel(); + + // Check current notification settings + const settings = await this.checkNotificationPermission(); + console.log('Notification settings:', settings); + + return settings; + } catch (error) { + console.error('Error initializing notifications:', error); + return {granted: false, reason: 'error', error}; + } + } } export default PermissionManager; diff --git a/src/services/WebSocketService.js b/src/services/WebSocketService.js index a280048..c07bba4 100644 --- a/src/services/WebSocketService.js +++ b/src/services/WebSocketService.js @@ -16,7 +16,7 @@ class EventEmitter { if (!this.events[event]) return this; if (listener) { - this.events[event] = this.events[event].filter((l) => l !== listener); + this.events[event] = this.events[event].filter(l => l !== listener); } else { delete this.events[event]; } @@ -26,7 +26,7 @@ class EventEmitter { emit(event, ...args) { if (!this.events[event]) return false; - this.events[event].forEach((listener) => { + this.events[event].forEach(listener => { try { listener(...args); } catch (error) { @@ -70,12 +70,12 @@ class WebSocketService extends EventEmitter { * @param {Object} options - Connection options */ connect(url, options = {}) { - console.log("🔌 [WebSocketService] connect() called with URL:", url); - console.log("🔌 [WebSocketService] connect() options:", options); + console.log('🔌 [WebSocketService] connect() called with URL:', url); + console.log('🔌 [WebSocketService] connect() options:', options); if (this.isConnected || this.isReconnecting) { console.log( - "🔌 [WebSocketService] Already connected or reconnecting, skipping connection" + '🔌 [WebSocketService] Already connected or reconnecting, skipping connection', ); return Promise.resolve(); } @@ -91,7 +91,7 @@ class WebSocketService extends EventEmitter { heartbeatTimeout = 5000, } = options; - console.log("🔌 [WebSocketService] Connection parameters:", { + console.log('🔌 [WebSocketService] Connection parameters:', { url: this.url, protocols, headers, @@ -103,15 +103,15 @@ class WebSocketService extends EventEmitter { return new Promise((resolve, reject) => { try { - console.log("🔌 [WebSocketService] Creating new WebSocket instance..."); + console.log('🔌 [WebSocketService] Creating new WebSocket instance...'); this.ws = new WebSocket(url, protocols); // Set up event handlers this.ws.onopen = () => { console.log( - "🔌 [WebSocketService] WebSocket connection opened successfully!" + '🔌 [WebSocketService] WebSocket connection opened successfully!', ); - console.log("🔌 [WebSocketService] Connection details:", { + console.log('🔌 [WebSocketService] Connection details:', { url: this.ws.url, protocol: this.ws.protocol, readyState: this.ws.readyState, @@ -132,16 +132,16 @@ class WebSocketService extends EventEmitter { this.processMessageQueue(); // Emit connection event - this.emit("connected", { url, timestamp: Date.now() }); + this.emit('connected', {url, timestamp: Date.now()}); console.log( - "🔌 [WebSocketService] Connection established and ready for messages" + '🔌 [WebSocketService] Connection established and ready for messages', ); resolve(); }; - this.ws.onmessage = (event) => { - console.log("📨 [WebSocketService] Raw message received:", { + this.ws.onmessage = event => { + console.log('📨 [WebSocketService] Raw message received:', { data: event.data, type: event.type, timestamp: new Date().toISOString(), @@ -149,37 +149,37 @@ class WebSocketService extends EventEmitter { try { console.log( - "📨 [WebSocketService] Attempting to parse message as JSON..." + '📨 [WebSocketService] Attempting to parse message as JSON...', ); const data = JSON.parse(event.data); console.log( - "📨 [WebSocketService] Successfully parsed JSON message:", + '📨 [WebSocketService] Successfully parsed JSON message:', { parsedData: data, dataType: typeof data, - hasType: "type" in data, - hasPayload: "payload" in data, - hasData: "data" in data, + hasType: 'type' in data, + hasPayload: 'payload' in data, + hasData: 'data' in data, keys: Object.keys(data), - } + }, ); // Handle double-encoded JSON from Go backend - if (data.data && typeof data.data === "string") { + if (data.data && typeof data.data === 'string') { console.log( - "📨 [WebSocketService] Detected double-encoded JSON, parsing inner data..." + '📨 [WebSocketService] Detected double-encoded JSON, parsing inner data...', ); try { const innerData = JSON.parse(data.data); console.log( - "📨 [WebSocketService] Successfully parsed inner JSON:", - innerData + '📨 [WebSocketService] Successfully parsed inner JSON:', + innerData, ); this.handleMessage(innerData); } catch (innerError) { console.error( - "❌ [WebSocketService] Failed to parse inner JSON:", - innerError + '❌ [WebSocketService] Failed to parse inner JSON:', + innerError, ); // Fall back to original data this.handleMessage(data); @@ -189,23 +189,23 @@ class WebSocketService extends EventEmitter { } } catch (error) { console.error( - "❌ [WebSocketService] Error parsing WebSocket message:", - error + '❌ [WebSocketService] Error parsing WebSocket message:', + error, ); console.error( - "❌ [WebSocketService] Raw message that failed to parse:", - event.data + '❌ [WebSocketService] Raw message that failed to parse:', + event.data, ); - this.emit("error", { - error: "Invalid message format", + this.emit('error', { + error: 'Invalid message format', data: event.data, parseError: error.message, }); } }; - this.ws.onclose = (event) => { - console.log("🔌 [WebSocketService] WebSocket connection closed:", { + this.ws.onclose = event => { + console.log('🔌 [WebSocketService] WebSocket connection closed:', { code: event.code, reason: event.reason, wasClean: event.wasClean, @@ -216,7 +216,7 @@ class WebSocketService extends EventEmitter { this.stopHeartbeat(); // Emit disconnection event - this.emit("disconnected", { + this.emit('disconnected', { code: event.code, reason: event.reason, timestamp: Date.now(), @@ -224,30 +224,30 @@ class WebSocketService extends EventEmitter { // Attempt reconnection if enabled if (reconnect && !this.isReconnecting) { - console.log("🔌 [WebSocketService] Attempting to reconnect..."); + console.log('🔌 [WebSocketService] Attempting to reconnect...'); this.scheduleReconnect(); } }; - this.ws.onerror = (error) => { + this.ws.onerror = error => { console.error( - "❌ [WebSocketService] WebSocket error occurred:", - error + '❌ [WebSocketService] WebSocket error occurred:', + error, ); - console.error("❌ [WebSocketService] Error details:", { + console.error('❌ [WebSocketService] Error details:', { message: error.message, type: error.type, target: error.target, timestamp: new Date().toISOString(), }); - this.emit("error", { error: error.message || "WebSocket error" }); + this.emit('error', {error: error.message || 'WebSocket error'}); reject(error); }; } catch (error) { console.error( - "❌ [WebSocketService] Error creating WebSocket connection:", - error + '❌ [WebSocketService] Error creating WebSocket connection:', + error, ); reject(error); } @@ -260,13 +260,13 @@ class WebSocketService extends EventEmitter { disconnect() { if (this.ws) { this.stopHeartbeat(); - this.ws.close(1000, "Client disconnecting"); + this.ws.close(1000, 'Client disconnecting'); this.ws = null; this.isConnected = false; this.isReconnecting = false; this.messageQueue = []; - this.subscriptions.clear(); - console.log("WebSocket disconnected by client"); + // Don't clear subscriptions on disconnect - preserve them for reconnection + console.log('WebSocket disconnected by client'); } } @@ -279,22 +279,22 @@ class WebSocketService extends EventEmitter { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { if (queueIfDisconnected) { this.messageQueue.push(message); - console.log("Message queued, WebSocket not connected"); + console.log('Message queued, WebSocket not connected'); return false; } - throw new Error("WebSocket not connected"); + throw new Error('WebSocket not connected'); } try { const messageStr = - typeof message === "string" ? message : JSON.stringify(message); + typeof message === 'string' ? message : JSON.stringify(message); this.ws.send(messageStr); - this.emit("messageSent", { message, timestamp: Date.now() }); + this.emit('messageSent', {message, timestamp: Date.now()}); return true; } catch (error) { - console.error("Error sending WebSocket message:", error); - this.emit("error", { - error: "Failed to send message", + console.error('Error sending WebSocket message:', error); + this.emit('error', { + error: 'Failed to send message', originalError: error, }); return false; @@ -308,17 +308,57 @@ class WebSocketService extends EventEmitter { * @param {string} id - Optional subscription ID */ subscribe(type, callback, id = null) { + console.log('🔔 [WebSocketService] subscribe() called with:', { + type, + callbackType: typeof callback, + callbackName: callback.name || 'anonymous', + id, + currentSubscriptionsForType: this.subscriptions.has(type) + ? this.subscriptions.get(type).size + : 0, + }); + const subscriptionId = id || `${type}_${Date.now()}_${Math.random()}`; if (!this.subscriptions.has(type)) { this.subscriptions.set(type, new Map()); + console.log( + '🔔 [WebSocketService] Created new subscription type map for:', + type, + ); } - this.subscriptions.get(type).set(subscriptionId, callback); + // Check if this exact callback is already subscribed + const typeSubscriptions = this.subscriptions.get(type); + for (const [existingId, existingCallback] of typeSubscriptions) { + if (existingCallback === callback) { + console.log( + '🔔 [WebSocketService] Callback already subscribed, returning existing ID:', + existingId, + ); + return existingId; + } + } + + typeSubscriptions.set(subscriptionId, callback); + + console.log('🔔 [WebSocketService] New subscription created:', { + type, + id: subscriptionId, + totalSubscriptionsForType: typeSubscriptions.size, + totalSubscriptions: Array.from(this.subscriptions.values()).reduce( + (total, typeSubs) => total + typeSubs.size, + 0, + ), + }); // Emit subscription event - this.emit("subscribed", { type, id: subscriptionId }); + this.emit('subscribed', {type, id: subscriptionId}); + console.log( + '🔔 [WebSocketService] Subscription completed, returning ID:', + subscriptionId, + ); return subscriptionId; } @@ -328,17 +368,31 @@ class WebSocketService extends EventEmitter { * @param {string} id - Subscription ID (if null, unsubscribes all for type) */ unsubscribe(type, id = null) { - console.log("🔔 [WebSocketService] unsubscribe() called:", { + console.log('🔔 [WebSocketService] unsubscribe() called:', { type, id, hasType: this.subscriptions.has(type), currentSubscriptions: Array.from(this.subscriptions.keys()), + stack: new Error().stack, // This will show the call stack }); + // SPECIAL CASE: Never unsubscribe from 'reports' type + // This prevents React or parent components from accidentally removing + // the reports subscription that we want to keep active forever + if (type === 'reports') { + console.log( + '🔔 [WebSocketService] BLOCKED: Cannot unsubscribe from reports type', + ); + console.log( + '🔔 [WebSocketService] Reports subscriptions are permanent and cannot be removed', + ); + return true; // Pretend unsubscribe was successful + } + if (!this.subscriptions.has(type)) { console.log( - "🔔 [WebSocketService] No subscriptions found for type:", - type + '🔔 [WebSocketService] No subscriptions found for type:', + type, ); return false; } @@ -348,13 +402,13 @@ class WebSocketService extends EventEmitter { const count = this.subscriptions.get(type).size; this.subscriptions.delete(type); console.log( - "🔔 [WebSocketService] Unsubscribed all subscriptions for type:", + '🔔 [WebSocketService] Unsubscribed all subscriptions for type:', { type, removedCount: count, - } + }, ); - this.emit("unsubscribed", { type, id: null }); + this.emit('unsubscribed', {type, id: null}); return true; } else { // Unsubscribe specific subscription @@ -362,24 +416,24 @@ class WebSocketService extends EventEmitter { if (typeSubscriptions && typeSubscriptions.has(id)) { typeSubscriptions.delete(id); console.log( - "🔔 [WebSocketService] Unsubscribed specific subscription:", + '🔔 [WebSocketService] Unsubscribed specific subscription:', { type, id, remainingForType: typeSubscriptions.size, - } + }, ); // Remove type if no more subscriptions if (typeSubscriptions.size === 0) { this.subscriptions.delete(type); console.log( - "🔔 [WebSocketService] Removed empty subscription type:", - type + '🔔 [WebSocketService] Removed empty subscription type:', + type, ); } - this.emit("unsubscribed", { type, id }); + this.emit('unsubscribed', {type, id}); return true; } } @@ -392,20 +446,20 @@ class WebSocketService extends EventEmitter { */ getConnectionStatus() { if (!this.ws) { - return "disconnected"; + return 'disconnected'; } switch (this.ws.readyState) { case WebSocket.CONNECTING: - return "connecting"; + return 'connecting'; case WebSocket.OPEN: - return "connected"; + return 'connected'; case WebSocket.CLOSING: - return "closing"; + return 'closing'; case WebSocket.CLOSED: - return "closed"; + return 'closed'; default: - return "unknown"; + return 'unknown'; } } @@ -420,37 +474,110 @@ class WebSocketService extends EventEmitter { messageQueueSize: this.messageQueue.length, subscriptionCount: Array.from(this.subscriptions.values()).reduce( (total, typeSubs) => total + typeSubs.size, - 0 + 0, ), url: this.url, connectionStatus: this.getConnectionStatus(), }; } + /** + * Check if a subscription exists for a given type and callback + * @param {string} type - Message type + * @param {Function} callback - Callback function + * @returns {string|null} - Subscription ID if exists, null otherwise + */ + hasSubscription(type, callback) { + if (!this.subscriptions.has(type)) { + return null; + } + + const typeSubscriptions = this.subscriptions.get(type); + for (const [id, existingCallback] of typeSubscriptions) { + if (existingCallback === callback) { + return id; + } + } + return null; + } + + /** + * Get all active subscriptions + */ + getSubscriptions() { + const result = {}; + for (const [type, typeSubs] of this.subscriptions) { + result[type] = Array.from(typeSubs.keys()); + } + return result; + } + + /** + * Debug method to log current subscription state + */ + debugSubscriptions() { + console.log('🔍 [WebSocketService] Current subscription state:'); + console.log( + '🔍 [WebSocketService] Total subscription types:', + this.subscriptions.size, + ); + + for (const [type, typeSubs] of this.subscriptions) { + console.log(`🔍 [WebSocketService] Type '${type}':`, { + count: typeSubs.size, + ids: Array.from(typeSubs.keys()), + callbacks: Array.from(typeSubs.values()).map( + cb => cb.name || 'anonymous', + ), + }); + } + } + + /** + * Check if a specific subscription type exists + * @param {string} type - Message type to check + * @returns {boolean} - True if subscriptions exist for this type + */ + hasSubscriptionType(type) { + return ( + this.subscriptions.has(type) && this.subscriptions.get(type).size > 0 + ); + } + + /** + * Get subscription count for a specific type + * @param {string} type - Message type + * @returns {number} - Number of subscriptions for this type + */ + getSubscriptionCount(type) { + if (!this.subscriptions.has(type)) return 0; + return this.subscriptions.get(type).size; + } + // Private methods handleMessage(data) { - console.log("📨 [WebSocketService] handleMessage() called with data:", { + console.log('📨 [WebSocketService] handleMessage() called with data:', { data, dataType: typeof data, isNull: data === null, isUndefined: data === undefined, - keys: data ? Object.keys(data) : "N/A", + keys: data ? Object.keys(data) : 'N/A', }); // Additional validation for the data structure - if (!data || typeof data !== "object") { + if (!data || typeof data !== 'object') { console.error( - "❌ [WebSocketService] Invalid data received in handleMessage:", - data + '❌ [WebSocketService] Invalid data received in handleMessage:', + data, ); return; } // Extract message properties with detailed logging - const { type, payload, id, timestamp, data: messageData } = data; + const {type, payload, id, timestamp, data: messageData} = data; - console.log("📨 [WebSocketService] Extracted message properties:", { + console.log('📨 [WebSocketService] Extracted message properties:', { type, payload, id, @@ -471,7 +598,7 @@ class WebSocketService extends EventEmitter { timestamp, raw: data, }); - this.emit("message", { + this.emit('message', { type, payload: messageData, id, @@ -481,27 +608,27 @@ class WebSocketService extends EventEmitter { // Handle subscriptions with detailed logging if (type && this.subscriptions.has(type)) { - console.log("📨 [WebSocketService] Found subscriptions for type:", type); + console.log('📨 [WebSocketService] Found subscriptions for type:', type); const typeSubscriptions = this.subscriptions.get(type); - console.log("📨 [WebSocketService] Type subscriptions:", { + console.log('📨 [WebSocketService] Type subscriptions:', { subscriptionCount: typeSubscriptions.size, subscriptionIds: Array.from(typeSubscriptions.keys()), }); console.log( - "📨 [WebSocketService] About to call subscription callbacks with data:", + '📨 [WebSocketService] About to call subscription callbacks with data:', { type, messageData, payload, actualPayload: messageData || payload, - } + }, ); typeSubscriptions.forEach((callback, subscriptionId) => { // Use messageData (Go's 'data' field) as the actual payload for callbacks const actualPayload = messageData || payload; - console.log("📨 [WebSocketService] Calling subscription callback:", { + console.log('📨 [WebSocketService] Calling subscription callback:', { subscriptionId, type, originalPayload: payload, @@ -512,23 +639,23 @@ class WebSocketService extends EventEmitter { try { // Use messageData (Go's 'data' field) as the actual payload for callbacks const actualPayload = messageData || payload; - callback(actualPayload, { type, id, timestamp }); + callback(actualPayload, {type, id, timestamp}); console.log( - "📨 [WebSocketService] Subscription callback executed successfully" + '📨 [WebSocketService] Subscription callback executed successfully', ); } catch (error) { console.error( - "❌ [WebSocketService] Error in subscription callback:", + '❌ [WebSocketService] Error in subscription callback:', { error: error.message, stack: error.stack, subscriptionId, type, payload: messageData || payload, - } + }, ); - this.emit("error", { - error: "Subscription callback error", + this.emit('error', { + error: 'Subscription callback error', type, originalError: error, }); @@ -536,39 +663,39 @@ class WebSocketService extends EventEmitter { }); } else { console.log( - "📨 [WebSocketService] No subscriptions found for type:", - type + '📨 [WebSocketService] No subscriptions found for type:', + type, ); console.log( - "📨 [WebSocketService] Available subscription types:", - Array.from(this.subscriptions.keys()) + '📨 [WebSocketService] Available subscription types:', + Array.from(this.subscriptions.keys()), ); } // Handle specific message types from Go backend - console.log("📨 [WebSocketService] Processing message type:", type); + console.log('📨 [WebSocketService] Processing message type:', type); switch (type) { - case "pong": - console.log("📨 [WebSocketService] Handling pong message"); + case 'pong': + console.log('📨 [WebSocketService] Handling pong message'); // Go backend sends pong responses to our pings this.handlePong(); break; - case "reports": + case 'reports': console.log( - "📨 [WebSocketService] Handling reports message from Go backend" + '📨 [WebSocketService] Handling reports message from Go backend', ); console.log( - "📨 [WebSocketService] Go backend data field:", - messageData + '📨 [WebSocketService] Go backend data field:', + messageData, ); - console.log("📨 [WebSocketService] Legacy payload field:", payload); + console.log('📨 [WebSocketService] Legacy payload field:', payload); // Go backend uses 'data' field, so use messageData as the primary source const reportData = messageData || payload; console.log( - "📨 [WebSocketService] Final report data to emit:", + '📨 [WebSocketService] Final report data to emit:', reportData, - { reportDataString: reportData.toString() } + {reportDataString: reportData.toString()}, ); console.log( @@ -578,32 +705,32 @@ class WebSocketService extends EventEmitter { reportsString: reportData.reports.toString(), reportsAnalysis: reportData.reports[0].analysis, reportsAnalysisString: reportData.reports[0].analysis.toString(), - } + }, ); // Go backend broadcasts report data - this.emit("reports", reportData); + this.emit('reports', reportData); break; - case "error": - console.log("📨 [WebSocketService] Handling error message from server"); - this.emit("serverError", payload); + case 'error': + console.log('📨 [WebSocketService] Handling error message from server'); + this.emit('serverError', payload); break; default: console.log( - "📨 [WebSocketService] Handling default message type:", - type + '📨 [WebSocketService] Handling default message type:', + type, ); // Emit typed event with Go backend compatible data const defaultData = messageData || payload; this.emit(type, defaultData); } - console.log("📨 [WebSocketService] handleMessage() completed"); + console.log('📨 [WebSocketService] handleMessage() completed'); } scheduleReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { - this.emit("reconnectFailed", { + this.emit('reconnectFailed', { attempts: this.reconnectAttempts, maxAttempts: this.maxReconnectAttempts, }); @@ -615,12 +742,29 @@ class WebSocketService extends EventEmitter { const delay = Math.min( this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), - this.maxReconnectDelay + this.maxReconnectDelay, ); setTimeout(() => { if (this.isReconnecting) { - this.connect(this.url, { reconnect: true }); + console.log('🔌 [WebSocketService] Attempting reconnection...'); + this.connect(this.url, {reconnect: true}) + .then(() => { + console.log( + '🔌 [WebSocketService] Reconnection successful, restoring subscriptions...', + ); + // Emit event to notify components that subscriptions are restored + this.emit('reconnected', { + timestamp: Date.now(), + subscriptionCount: Array.from(this.subscriptions.values()).reduce( + (total, typeSubs) => total + typeSubs.size, + 0, + ), + }); + }) + .catch(error => { + console.error('🔌 [WebSocketService] Reconnection failed:', error); + }); } }, delay); } @@ -628,11 +772,11 @@ class WebSocketService extends EventEmitter { startHeartbeat(interval, timeout) { this.heartbeatInterval = setInterval(() => { if (this.isConnected) { - this.send({ type: "ping", timestamp: Date.now() }); + this.send({type: 'ping', timestamp: Date.now()}); // Set timeout for pong response this.heartbeatTimeout = setTimeout(() => { - this.ws.close(1000, "Heartbeat timeout"); + this.ws.close(1000, 'Heartbeat timeout'); }, timeout); } }, interval); From 0a145890b17bd59c483102dfb6727e756561bdda Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 14:58:58 +0530 Subject: [PATCH 11/13] Reduce report radius to 1 km --- src/components/WebSocketMessages.js | 3 ++- src/utils/constants.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 src/utils/constants.ts diff --git a/src/components/WebSocketMessages.js b/src/components/WebSocketMessages.js index fc65e1a..f0a8b4c 100644 --- a/src/components/WebSocketMessages.js +++ b/src/components/WebSocketMessages.js @@ -10,6 +10,7 @@ import {theme} from '../services/Common/theme'; import notifee from '@notifee/react-native'; import {getLocation} from '../functions/geolocation'; import webSocketService from '../services/WebSocketService'; +import {RADIUS_IN_KILOMETERS} from '../utils/constants'; const WebSocketMessages = ({ url, @@ -405,7 +406,7 @@ const WebSocketMessages = ({ // Calculate the distance between the user's location and the report's location const distance = calculateDistance(location, reportLocation); console.log('Distance:', distance); - if (distance < 100) { + if (distance < RADIUS_IN_KILOMETERS) { isInRange = true; } } else { diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..37d2f2a --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1 @@ +export const RADIUS_IN_KILOMETERS = 1; \ No newline at end of file From 88c0efd77562b15281207c134b642317d2c66b88 Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 15:32:47 +0530 Subject: [PATCH 12/13] Remove debug logs --- src/components/WebSocketMessages.js | 220 +----------------- src/services/WebSocketService.js | 334 +--------------------------- 2 files changed, 13 insertions(+), 541 deletions(-) diff --git a/src/components/WebSocketMessages.js b/src/components/WebSocketMessages.js index f0a8b4c..d96205c 100644 --- a/src/components/WebSocketMessages.js +++ b/src/components/WebSocketMessages.js @@ -27,71 +27,23 @@ const WebSocketMessages = ({ const isSubscribedRef = useRef(false); // Subscribe to report messages from Go backend - // - // SOLUTION: Never unsubscribe - keep subscription active permanently - // This prevents the immediate unsubscribe issue that was causing - // the subscription to be removed right after creation - // // IMPORTANT: Even though we removed the cleanup function, React or the parent // component might still call unsubscribe. We handle this by blocking // unsubscribe calls for 'reports' type in WebSocketService. useEffect(() => { - console.log('📱 [WebSocketMessages] useEffect triggered:', { - isConnected, - url, - hasSubscribe: !!subscribe, - subscribeType: typeof subscribe, - timestamp: new Date().toISOString(), - isAlreadySubscribed: isSubscribedRef.current, - currentSubscriptionId: subscriptionRef.current, - }); - // Only proceed if we have all required functions and are connected if (!isConnected || !subscribe) { - console.log( - '📱 [WebSocketMessages] WebSocket not connected or missing subscribe function, skipping subscription', - { - isConnected, - hasSubscribe: !!subscribe, - subscribeValue: subscribe, - subscribeType: typeof subscribe, - subscribeIsFunction: typeof subscribe === 'function', - }, - ); return; } // Prevent multiple subscriptions if (isSubscribedRef.current && subscriptionRef.current) { - console.log( - '📱 [WebSocketMessages] Already subscribed, skipping duplicate subscription', - {subscriptionId: subscriptionRef.current}, - ); return; } - console.log( - "📱 [WebSocketMessages] WebSocket connected, subscribing to 'reports' type", - ); - try { - console.log('📱 [WebSocketMessages] About to call subscribe function...'); - // Subscribe to the "reports" message type that Go backend sends const subscriptionId = subscribe('reports', (payload, metadata) => { - console.log( - '📱 [WebSocketMessages] Reports subscription callback triggered:', - { - payload, - metadata, - payloadType: typeof payload, - payloadKeys: payload ? Object.keys(payload) : 'N/A', - payloadIsNull: payload === null, - payloadIsUndefined: payload === undefined, - timestamp: new Date().toISOString(), - }, - ); - const newMessage = { id: Date.now(), type: 'reports', @@ -100,84 +52,24 @@ const WebSocketMessages = ({ metadata, }; - console.log('📱 [WebSocketMessages] Created new message object:', { - newMessage, - dataField: newMessage.data, - dataType: typeof newMessage.data, - }); - // check if the report is within the user's radius and display notification checkUserLocation(payload).then(isInRange => { if (isInRange) { onDisplayNotification(payload); - } else { - console.log( - "📱 [WebSocketMessages] Report is not within the user's radius", - ); } }); setMessages(prev => { const updatedMessages = [newMessage, ...prev.slice(0, 99)]; // Keep last 100 - console.log('📱 [WebSocketMessages] Updated messages state:', { - previousCount: prev.length, - newCount: updatedMessages.length, - firstMessage: updatedMessages[0], - }); return updatedMessages; }); }); - console.log('📱 [WebSocketMessages] Subscribe function returned:', { - subscriptionId, - subscriptionIdType: typeof subscriptionId, - subscriptionIdIsNull: subscriptionId === null, - subscriptionIdIsUndefined: subscriptionId === undefined, - }); - // Store subscription info in refs subscriptionRef.current = subscriptionId; isSubscribedRef.current = true; - - console.log('📱 [WebSocketMessages] Subscription created and stored:', { - subscriptionId, - subscriptionRef: subscriptionRef.current, - isSubscribed: isSubscribedRef.current, - }); - - // Debug: Check if subscription was actually stored in WebSocketService - if (typeof subscribe === 'function' && subscribe.name === 'subscribe') { - console.log( - '📱 [WebSocketMessages] Subscribe function appears to be from WebSocketService', - ); - } else { - console.log( - '📱 [WebSocketMessages] Subscribe function is NOT from WebSocketService:', - { - functionName: subscribe.name, - functionType: typeof subscribe, - }, - ); - } - - // Debug: Check WebSocketService subscription state - console.log( - '📱 [WebSocketMessages] WebSocketService subscription state:', - ); - console.log( - '📱 [WebSocketMessages] Has reports subscription type:', - webSocketService.hasSubscriptionType('reports'), - ); - console.log( - '📱 [WebSocketMessages] Reports subscription count:', - webSocketService.getSubscriptionCount('reports'), - ); - webSocketService.debugSubscriptions(); } catch (error) { - console.error( - '📱 [WebSocketMessages] Error creating subscription:', - error, - ); + console.error('Error creating subscription:', error); return; } @@ -188,32 +80,6 @@ const WebSocketMessages = ({ // NO CLEANUP ON UNMOUNT - subscription stays active forever // This prevents any unsubscribe calls that could cause issues - // Debug effect to track subscription state changes - useEffect(() => { - console.log( - '📱 [WebSocketMessages] Debug effect - checking subscription state:', - { - isConnected, - url, - hasSubscribe: !!subscribe, - subscribeType: typeof subscribe, - subscriptionRef: subscriptionRef.current, - isSubscribed: isSubscribedRef.current, - }, - ); - }, [isConnected, url, subscribe]); - - // Debug effect to track subscription state changes - useEffect(() => { - console.log('📱 [WebSocketMessages] Subscription state changed:', { - isConnected, - url, - hasSubscription: !!subscriptionRef.current, - subscriptionId: subscriptionRef.current, - isSubscribed: isSubscribedRef.current, - }); - }, [isConnected, url, subscriptionRef.current, isSubscribedRef.current]); - // Filter messages based on selected type useEffect(() => { if (selectedType === 'all') { @@ -234,37 +100,12 @@ const WebSocketMessages = ({ }; const renderReportMessage = message => { - console.log( - '📱 [WebSocketMessages] renderReportMessage() called with message:', - { - message, - messageType: typeof message, - messageKeys: message ? Object.keys(message) : 'N/A', - }, - ); - const {data} = message; - console.log('📱 [WebSocketMessages] Extracted data from message:', { - data, - dataType: typeof data, - dataIsNull: data === null, - dataIsUndefined: data === undefined, - dataKeys: data ? Object.keys(data) : 'N/A', - }); - if (!data || !data.reports) { - console.log('📱 [WebSocketMessages] No report data found in message'); return No report data; } - console.log('📱 [WebSocketMessages] Report data found:', { - reportsCount: data.reports.length, - reportsType: typeof data.reports, - firstReport: data.reports[0], - dataKeys: Object.keys(data), - }); - return ( @@ -274,14 +115,6 @@ const WebSocketMessages = ({ Sequence: {data.fromSeq} - {data.toSeq} {data.reports.slice(0, 3).map((reportWithAnalysis, index) => { - console.log('📱 [WebSocketMessages] Rendering report:', { - index, - reportWithAnalysis, - reportKeys: reportWithAnalysis - ? Object.keys(reportWithAnalysis) - : 'N/A', - }); - return ( @@ -345,54 +178,12 @@ const WebSocketMessages = ({ }; const checkUserLocation = async payload => { - // { - // "reports": [ - // { - // "report": { - // "seq": 131, - // "timestamp": "2025-08-27T05:51:31Z", - // "id": "0xdff9426058ccA89C0297fb45E0620bc052899A5c", - // "latitude": 3.320606, - // "longitude": 23.530489 - // }, - // "analysis": [ - // { - // "seq": 131, - // "source": "ChatGPT", - // "analysis_text": "```json\n{\n \"title\": \"Litter Detected on Tram Tracks in Urban Area\",\n \"description\": \"A piece of litter, specifically a small food wrapper, is observed on tram tracks in an urban area, increasing risk to tram operations and contributing to environmental impact.\",\n \"classification\": \"Physical\",\n \"user_info\": {\n \"name\": null,\n \"email\": null,\n \"company\": null,\n \"role\": null,\n \"company_size\": null\n },\n \"location\": \"Intersection near visible tram stop and To Let sign, likely urban setting\",\n \"brand_name\": null,\n \"responsible_party\": \"Municipality Public Works\",\n \"inferred_contact_emails\": [\"cityworks@municipality.gov\", \"cleaning@municipality.gov\", \"trammaintenance@municipality.gov\"],\n \"suggested_remediation\": [\n \"Deploy cleanup crew to the reported location to remove litter from tram tracks.\",\n \"Enhance surveillance and monitoring for littering in public transport areas.\",\n \"Implement public awareness campaigns about the impact of littering on public transport safety.\",\n \"Introduce regular patrols on tram lines to ensure cleanliness and safety.\"\n ],\n \"litter_probability\": 0.95,\n \"hazard_probability\": 0.6,\n \"digital_bug_probabilty\": 0.0,\n \"severity_level\": 0.4,\n \"is_valid\": true\n}\n```", - // "title": "Litter Detected on Tram Tracks in Urban Area", - // "description": "A piece of litter, specifically a small food wrapper, is observed on tram tracks in an urban area, increasing risk to tram operations and contributing to environmental impact.", - // "brand_name": "", - // "brand_display_name": "", - // "litter_probability": 0.95, - // "hazard_probability": 0.6, - // "digital_bug_probability": 0, - // "severity_level": 0.4, - // "summary": "Litter Detected on Tram Tracks in Urban Area: A piece of litter, specifically a small food wrapper, is observed on tram tracks in an urban area, increasing risk to tram operations and contributing to environmental impact.", - // "language": "en", - // "classification": "physical", - // "created_at": "2025-08-27T05:51:42Z", - // "updated_at": "0001-01-01T00:00:00Z" - // } - // ] - // } - // ], - // "count": 1, - // "from_seq": 131, - // "to_seq": 131 - // } - let isInRange = false; try { const location = await getLocation(); if (location) { - console.log('Location obtained:', location); - console.log('Latitude:', location.latitude); - console.log('Longitude:', location.longitude); - if (!payload.reports || payload.reports.length === 0) { - console.log('No reports found in websocket message'); return false; } @@ -405,20 +196,17 @@ const WebSocketMessages = ({ // Calculate the distance between the user's location and the report's location const distance = calculateDistance(location, reportLocation); - console.log('Distance:', distance); + if (distance < RADIUS_IN_KILOMETERS) { isInRange = true; } } else { - console.log('Location permission denied or failed'); isInRange = false; } } catch (error) { - console.log('In catch'); console.error('Error getting location:', error); isInRange = false; } finally { - console.log('In finally'); return isInRange; } }; @@ -435,7 +223,6 @@ const WebSocketMessages = ({ try { if (!payload.reports || payload.reports.length === 0) { - console.log('No reports found in websocket message'); return false; } @@ -446,7 +233,6 @@ const WebSocketMessages = ({ !payload.reports[0].analysis || payload.reports[0].analysis.length === 0 ) { - console.log('No analysis found in report'); analysisText = ''; } else { analysisText = payload.reports[0].analysis[0].summary; @@ -465,7 +251,7 @@ const WebSocketMessages = ({ }, }); } catch (e) { - console.log('📱 [WebSocketMessages] Error displaying notification:', e); + console.error('Error displaying notification:', e); } } diff --git a/src/services/WebSocketService.js b/src/services/WebSocketService.js index c07bba4..41846e1 100644 --- a/src/services/WebSocketService.js +++ b/src/services/WebSocketService.js @@ -70,13 +70,7 @@ class WebSocketService extends EventEmitter { * @param {Object} options - Connection options */ connect(url, options = {}) { - console.log('🔌 [WebSocketService] connect() called with URL:', url); - console.log('🔌 [WebSocketService] connect() options:', options); - if (this.isConnected || this.isReconnecting) { - console.log( - '🔌 [WebSocketService] Already connected or reconnecting, skipping connection', - ); return Promise.resolve(); } @@ -91,33 +85,12 @@ class WebSocketService extends EventEmitter { heartbeatTimeout = 5000, } = options; - console.log('🔌 [WebSocketService] Connection parameters:', { - url: this.url, - protocols, - headers, - reconnect, - heartbeat, - heartbeatInterval, - heartbeatTimeout, - }); - return new Promise((resolve, reject) => { try { - console.log('🔌 [WebSocketService] Creating new WebSocket instance...'); this.ws = new WebSocket(url, protocols); // Set up event handlers this.ws.onopen = () => { - console.log( - '🔌 [WebSocketService] WebSocket connection opened successfully!', - ); - console.log('🔌 [WebSocketService] Connection details:', { - url: this.ws.url, - protocol: this.ws.protocol, - readyState: this.ws.readyState, - bufferedAmount: this.ws.bufferedAmount, - }); - this.isConnected = true; this.isReconnecting = false; this.reconnectAttempts = 0; @@ -134,53 +107,19 @@ class WebSocketService extends EventEmitter { // Emit connection event this.emit('connected', {url, timestamp: Date.now()}); - console.log( - '🔌 [WebSocketService] Connection established and ready for messages', - ); resolve(); }; this.ws.onmessage = event => { - console.log('📨 [WebSocketService] Raw message received:', { - data: event.data, - type: event.type, - timestamp: new Date().toISOString(), - }); - try { - console.log( - '📨 [WebSocketService] Attempting to parse message as JSON...', - ); const data = JSON.parse(event.data); - console.log( - '📨 [WebSocketService] Successfully parsed JSON message:', - { - parsedData: data, - dataType: typeof data, - hasType: 'type' in data, - hasPayload: 'payload' in data, - hasData: 'data' in data, - keys: Object.keys(data), - }, - ); // Handle double-encoded JSON from Go backend if (data.data && typeof data.data === 'string') { - console.log( - '📨 [WebSocketService] Detected double-encoded JSON, parsing inner data...', - ); try { const innerData = JSON.parse(data.data); - console.log( - '📨 [WebSocketService] Successfully parsed inner JSON:', - innerData, - ); this.handleMessage(innerData); } catch (innerError) { - console.error( - '❌ [WebSocketService] Failed to parse inner JSON:', - innerError, - ); // Fall back to original data this.handleMessage(data); } @@ -188,14 +127,6 @@ class WebSocketService extends EventEmitter { this.handleMessage(data); } } catch (error) { - console.error( - '❌ [WebSocketService] Error parsing WebSocket message:', - error, - ); - console.error( - '❌ [WebSocketService] Raw message that failed to parse:', - event.data, - ); this.emit('error', { error: 'Invalid message format', data: event.data, @@ -205,13 +136,6 @@ class WebSocketService extends EventEmitter { }; this.ws.onclose = event => { - console.log('🔌 [WebSocketService] WebSocket connection closed:', { - code: event.code, - reason: event.reason, - wasClean: event.wasClean, - timestamp: new Date().toISOString(), - }); - this.isConnected = false; this.stopHeartbeat(); @@ -224,31 +148,15 @@ class WebSocketService extends EventEmitter { // Attempt reconnection if enabled if (reconnect && !this.isReconnecting) { - console.log('🔌 [WebSocketService] Attempting to reconnect...'); this.scheduleReconnect(); } }; this.ws.onerror = error => { - console.error( - '❌ [WebSocketService] WebSocket error occurred:', - error, - ); - console.error('❌ [WebSocketService] Error details:', { - message: error.message, - type: error.type, - target: error.target, - timestamp: new Date().toISOString(), - }); - this.emit('error', {error: error.message || 'WebSocket error'}); reject(error); }; } catch (error) { - console.error( - '❌ [WebSocketService] Error creating WebSocket connection:', - error, - ); reject(error); } }); @@ -266,7 +174,6 @@ class WebSocketService extends EventEmitter { this.isReconnecting = false; this.messageQueue = []; // Don't clear subscriptions on disconnect - preserve them for reconnection - console.log('WebSocket disconnected by client'); } } @@ -279,7 +186,7 @@ class WebSocketService extends EventEmitter { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { if (queueIfDisconnected) { this.messageQueue.push(message); - console.log('Message queued, WebSocket not connected'); + return false; } throw new Error('WebSocket not connected'); @@ -308,57 +215,25 @@ class WebSocketService extends EventEmitter { * @param {string} id - Optional subscription ID */ subscribe(type, callback, id = null) { - console.log('🔔 [WebSocketService] subscribe() called with:', { - type, - callbackType: typeof callback, - callbackName: callback.name || 'anonymous', - id, - currentSubscriptionsForType: this.subscriptions.has(type) - ? this.subscriptions.get(type).size - : 0, - }); - const subscriptionId = id || `${type}_${Date.now()}_${Math.random()}`; if (!this.subscriptions.has(type)) { this.subscriptions.set(type, new Map()); - console.log( - '🔔 [WebSocketService] Created new subscription type map for:', - type, - ); } // Check if this exact callback is already subscribed const typeSubscriptions = this.subscriptions.get(type); for (const [existingId, existingCallback] of typeSubscriptions) { if (existingCallback === callback) { - console.log( - '🔔 [WebSocketService] Callback already subscribed, returning existing ID:', - existingId, - ); return existingId; } } typeSubscriptions.set(subscriptionId, callback); - console.log('🔔 [WebSocketService] New subscription created:', { - type, - id: subscriptionId, - totalSubscriptionsForType: typeSubscriptions.size, - totalSubscriptions: Array.from(this.subscriptions.values()).reduce( - (total, typeSubs) => total + typeSubs.size, - 0, - ), - }); - // Emit subscription event this.emit('subscribed', {type, id: subscriptionId}); - console.log( - '🔔 [WebSocketService] Subscription completed, returning ID:', - subscriptionId, - ); return subscriptionId; } @@ -368,32 +243,14 @@ class WebSocketService extends EventEmitter { * @param {string} id - Subscription ID (if null, unsubscribes all for type) */ unsubscribe(type, id = null) { - console.log('🔔 [WebSocketService] unsubscribe() called:', { - type, - id, - hasType: this.subscriptions.has(type), - currentSubscriptions: Array.from(this.subscriptions.keys()), - stack: new Error().stack, // This will show the call stack - }); - // SPECIAL CASE: Never unsubscribe from 'reports' type // This prevents React or parent components from accidentally removing // the reports subscription that we want to keep active forever if (type === 'reports') { - console.log( - '🔔 [WebSocketService] BLOCKED: Cannot unsubscribe from reports type', - ); - console.log( - '🔔 [WebSocketService] Reports subscriptions are permanent and cannot be removed', - ); return true; // Pretend unsubscribe was successful } if (!this.subscriptions.has(type)) { - console.log( - '🔔 [WebSocketService] No subscriptions found for type:', - type, - ); return false; } @@ -401,13 +258,6 @@ class WebSocketService extends EventEmitter { // Unsubscribe all for this type const count = this.subscriptions.get(type).size; this.subscriptions.delete(type); - console.log( - '🔔 [WebSocketService] Unsubscribed all subscriptions for type:', - { - type, - removedCount: count, - }, - ); this.emit('unsubscribed', {type, id: null}); return true; } else { @@ -415,22 +265,10 @@ class WebSocketService extends EventEmitter { const typeSubscriptions = this.subscriptions.get(type); if (typeSubscriptions && typeSubscriptions.has(id)) { typeSubscriptions.delete(id); - console.log( - '🔔 [WebSocketService] Unsubscribed specific subscription:', - { - type, - id, - remainingForType: typeSubscriptions.size, - }, - ); // Remove type if no more subscriptions if (typeSubscriptions.size === 0) { this.subscriptions.delete(type); - console.log( - '🔔 [WebSocketService] Removed empty subscription type:', - type, - ); } this.emit('unsubscribed', {type, id}); @@ -512,92 +350,19 @@ class WebSocketService extends EventEmitter { return result; } - /** - * Debug method to log current subscription state - */ - debugSubscriptions() { - console.log('🔍 [WebSocketService] Current subscription state:'); - console.log( - '🔍 [WebSocketService] Total subscription types:', - this.subscriptions.size, - ); - - for (const [type, typeSubs] of this.subscriptions) { - console.log(`🔍 [WebSocketService] Type '${type}':`, { - count: typeSubs.size, - ids: Array.from(typeSubs.keys()), - callbacks: Array.from(typeSubs.values()).map( - cb => cb.name || 'anonymous', - ), - }); - } - } - - /** - * Check if a specific subscription type exists - * @param {string} type - Message type to check - * @returns {boolean} - True if subscriptions exist for this type - */ - hasSubscriptionType(type) { - return ( - this.subscriptions.has(type) && this.subscriptions.get(type).size > 0 - ); - } - - /** - * Get subscription count for a specific type - * @param {string} type - Message type - * @returns {number} - Number of subscriptions for this type - */ - getSubscriptionCount(type) { - if (!this.subscriptions.has(type)) return 0; - return this.subscriptions.get(type).size; - } - // Private methods handleMessage(data) { - console.log('📨 [WebSocketService] handleMessage() called with data:', { - data, - dataType: typeof data, - isNull: data === null, - isUndefined: data === undefined, - keys: data ? Object.keys(data) : 'N/A', - }); - // Additional validation for the data structure if (!data || typeof data !== 'object') { - console.error( - '❌ [WebSocketService] Invalid data received in handleMessage:', - data, - ); + console.error('Invalid data received in handleMessage:', data); return; } - // Extract message properties with detailed logging + // Extract message properties const {type, payload, id, timestamp, data: messageData} = data; - console.log('📨 [WebSocketService] Extracted message properties:', { - type, - payload, - id, - timestamp, - messageData, - hasType: type !== undefined, - hasPayload: payload !== undefined, - hasId: id !== undefined, - hasTimestamp: timestamp !== undefined, - hasMessageData: messageData !== undefined, - }); - // Emit raw message event with Go backend compatible structure - console.log("📨 [WebSocketService] Emitting 'message' event with data:", { - type, - payload: messageData, // Use messageData (Go's 'data' field) as payload - id, - timestamp, - raw: data, - }); this.emit('message', { type, payload: messageData, @@ -606,54 +371,21 @@ class WebSocketService extends EventEmitter { raw: data, }); - // Handle subscriptions with detailed logging + // Handle subscriptions if (type && this.subscriptions.has(type)) { - console.log('📨 [WebSocketService] Found subscriptions for type:', type); const typeSubscriptions = this.subscriptions.get(type); - console.log('📨 [WebSocketService] Type subscriptions:', { - subscriptionCount: typeSubscriptions.size, - subscriptionIds: Array.from(typeSubscriptions.keys()), - }); - - console.log( - '📨 [WebSocketService] About to call subscription callbacks with data:', - { - type, - messageData, - payload, - actualPayload: messageData || payload, - }, - ); typeSubscriptions.forEach((callback, subscriptionId) => { - // Use messageData (Go's 'data' field) as the actual payload for callbacks - const actualPayload = messageData || payload; - console.log('📨 [WebSocketService] Calling subscription callback:', { - subscriptionId, - type, - originalPayload: payload, - actualPayload: actualPayload, - callbackType: typeof callback, - }); - try { // Use messageData (Go's 'data' field) as the actual payload for callbacks const actualPayload = messageData || payload; callback(actualPayload, {type, id, timestamp}); - console.log( - '📨 [WebSocketService] Subscription callback executed successfully', - ); } catch (error) { - console.error( - '❌ [WebSocketService] Error in subscription callback:', - { - error: error.message, - stack: error.stack, - subscriptionId, - type, - payload: messageData || payload, - }, - ); + console.error('Error in subscription callback:', { + error: error.message, + subscriptionId, + type, + }); this.emit('error', { error: 'Subscription callback error', type, @@ -661,71 +393,29 @@ class WebSocketService extends EventEmitter { }); } }); - } else { - console.log( - '📨 [WebSocketService] No subscriptions found for type:', - type, - ); - console.log( - '📨 [WebSocketService] Available subscription types:', - Array.from(this.subscriptions.keys()), - ); } // Handle specific message types from Go backend - console.log('📨 [WebSocketService] Processing message type:', type); switch (type) { case 'pong': - console.log('📨 [WebSocketService] Handling pong message'); // Go backend sends pong responses to our pings this.handlePong(); break; case 'reports': - console.log( - '📨 [WebSocketService] Handling reports message from Go backend', - ); - console.log( - '📨 [WebSocketService] Go backend data field:', - messageData, - ); - console.log('📨 [WebSocketService] Legacy payload field:', payload); - // Go backend uses 'data' field, so use messageData as the primary source const reportData = messageData || payload; - console.log( - '📨 [WebSocketService] Final report data to emit:', - reportData, - {reportDataString: reportData.toString()}, - ); - - console.log( - "📨 [WebSocketService] Emitting 'reports' event with data:+++++++++++", - { - reports: reportData.reports, - reportsString: reportData.reports.toString(), - reportsAnalysis: reportData.reports[0].analysis, - reportsAnalysisString: reportData.reports[0].analysis.toString(), - }, - ); // Go backend broadcasts report data this.emit('reports', reportData); break; case 'error': - console.log('📨 [WebSocketService] Handling error message from server'); this.emit('serverError', payload); break; default: - console.log( - '📨 [WebSocketService] Handling default message type:', - type, - ); // Emit typed event with Go backend compatible data const defaultData = messageData || payload; this.emit(type, defaultData); } - - console.log('📨 [WebSocketService] handleMessage() completed'); } scheduleReconnect() { @@ -747,12 +437,8 @@ class WebSocketService extends EventEmitter { setTimeout(() => { if (this.isReconnecting) { - console.log('🔌 [WebSocketService] Attempting reconnection...'); this.connect(this.url, {reconnect: true}) .then(() => { - console.log( - '🔌 [WebSocketService] Reconnection successful, restoring subscriptions...', - ); // Emit event to notify components that subscriptions are restored this.emit('reconnected', { timestamp: Date.now(), @@ -763,7 +449,7 @@ class WebSocketService extends EventEmitter { }); }) .catch(error => { - console.error('🔌 [WebSocketService] Reconnection failed:', error); + console.error('Reconnection failed:', error); }); } }, delay); From 7b949f7a737e001db939715aa31ef8ed90ac79b3 Mon Sep 17 00:00:00 2001 From: Ravi Singh Lodhi Date: Wed, 27 Aug 2025 16:04:40 +0530 Subject: [PATCH 13/13] Remove debug UI for websocket --- src/components/WebSocketStatus.js | 123 +++++++++++------------------- 1 file changed, 44 insertions(+), 79 deletions(-) diff --git a/src/components/WebSocketStatus.js b/src/components/WebSocketStatus.js index 8b3abc3..aa13edf 100644 --- a/src/components/WebSocketStatus.js +++ b/src/components/WebSocketStatus.js @@ -1,6 +1,6 @@ -import React from "react"; -import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native"; -import { theme } from "../services/Common/theme"; +import React from 'react'; +import {Alert, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; +import {theme} from '../services/Common/theme'; const WebSocketStatus = ({ url, @@ -15,41 +15,41 @@ const WebSocketStatus = ({ disconnect, }) => { const handleConnect = async () => { - console.log("🔌 [WebSocketStatus] handleConnect() called"); + console.log('🔌 [WebSocketStatus] handleConnect() called'); try { - console.log("🔌 [WebSocketStatus] Attempting to connect to:", url); + console.log('🔌 [WebSocketStatus] Attempting to connect to:', url); await connect(); console.log( - "🔌 [WebSocketStatus] Connection successful, calling onConnect callback" + '🔌 [WebSocketStatus] Connection successful, calling onConnect callback', ); if (onConnect) onConnect(); } catch (err) { - console.error("❌ [WebSocketStatus] Connection failed:", err); - Alert.alert("Connection Error", err.message || "Failed to connect"); + console.error('❌ [WebSocketStatus] Connection failed:', err); + Alert.alert('Connection Error', err.message || 'Failed to connect'); } }; const handleDisconnect = () => { - console.log("🔌 [WebSocketStatus] handleDisconnect() called"); + console.log('🔌 [WebSocketStatus] handleDisconnect() called'); disconnect(); console.log( - "🔌 [WebSocketStatus] Disconnected, calling onDisconnect callback" + '🔌 [WebSocketStatus] Disconnected, calling onDisconnect callback', ); if (onDisconnect) onDisconnect(); }; const getStatusColor = () => { switch (connectionStatus) { - case "connected": - return theme.COLORS.SUCCESS || "#4CAF50"; - case "connecting": - return theme.COLORS.WARNING || "#FF9800"; - case "disconnected": - return theme.COLORS.ERROR || "#F44336"; - case "failed": - return theme.COLORS.ERROR || "#F44336"; + case 'connected': + return theme.COLORS.SUCCESS || '#4CAF50'; + case 'connecting': + return theme.COLORS.WARNING || '#FF9800'; + case 'disconnected': + return theme.COLORS.ERROR || '#F44336'; + case 'failed': + return theme.COLORS.ERROR || '#F44336'; default: - return theme.COLORS.GRAY || "#9E9E9E"; + return theme.COLORS.GRAY || '#9E9E9E'; } }; @@ -61,55 +61,20 @@ const WebSocketStatus = ({ } switch (connectionStatus) { - case "connected": - return "Connected"; - case "connecting": - return "Connecting..."; - case "disconnected": - return "Disconnected"; - case "failed": - return "Connection Failed"; + case 'connected': + return 'Connected'; + case 'connecting': + return 'Connecting...'; + case 'disconnected': + return 'Disconnected'; + case 'failed': + return 'Connection Failed'; default: - return "Unknown"; + return 'Unknown'; } }; - return ( - - - - {getStatusText()} - - - {error && Error: {error}} - - - {!isConnected ? ( - - - {isReconnecting ? "Connecting..." : "Connect"} - - - ) : ( - - Disconnect - - )} - - - ); + return <>; }; const styles = StyleSheet.create({ @@ -118,15 +83,15 @@ const styles = StyleSheet.create({ backgroundColor: theme.COLORS.BG, borderRadius: 8, marginVertical: 8, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, + shadowColor: '#000', + shadowOffset: {width: 0, height: 2}, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, statusRow: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', marginBottom: 8, }, statusIndicator: { @@ -137,11 +102,11 @@ const styles = StyleSheet.create({ }, statusText: { fontSize: 16, - fontWeight: "600", - color: theme.COLORS.TEXT || "#ffffff", + fontWeight: '600', + color: theme.COLORS.TEXT || '#ffffff', }, errorText: { - color: theme.COLORS.ERROR || "#F44336", + color: theme.COLORS.ERROR || '#F44336', fontSize: 14, marginBottom: 8, }, @@ -150,29 +115,29 @@ const styles = StyleSheet.create({ }, statsText: { fontSize: 12, - color: theme.COLORS.GRAY || "#9E9E9E", + color: theme.COLORS.GRAY || '#9E9E9E', }, buttonContainer: { - flexDirection: "row", - justifyContent: "center", + flexDirection: 'row', + justifyContent: 'center', }, button: { paddingHorizontal: 24, paddingVertical: 12, borderRadius: 6, minWidth: 100, - alignItems: "center", + alignItems: 'center', }, connectButton: { - backgroundColor: theme.COLORS.SUCCESS || "#4CAF50", + backgroundColor: theme.COLORS.SUCCESS || '#4CAF50', }, disconnectButton: { - backgroundColor: theme.COLORS.ERROR || "#F44336", + backgroundColor: theme.COLORS.ERROR || '#F44336', }, buttonText: { - color: "#FFFFFF", + color: '#FFFFFF', fontSize: 14, - fontWeight: "600", + fontWeight: '600', }, });