diff --git a/src/components/AddTraderForm.jsx b/src/components/AddTraderForm.jsx index cf823c4..53bd609 100644 --- a/src/components/AddTraderForm.jsx +++ b/src/components/AddTraderForm.jsx @@ -1,14 +1,16 @@ import { useState, useEffect } from "react"; import { Text, Button, TextField, Snackbar } from "@deriv-com/quill-ui"; import PropTypes from "prop-types"; -import useDerivWebSocket from "../hooks/useDerivWebSocket"; +import useWebSocket from "../hooks/useWebSocket"; +import useAuthorize from "../hooks/useAuthorize"; const AddTraderForm = ({ onAddTrader }) => { const [traderData, setTraderData] = useState({ token: "", }); - const { sendRequest, wsResponse } = useDerivWebSocket(); + const { sendMessage, lastMessage } = useWebSocket(); + const { isAuthorized, isConnected } = useAuthorize(); const [isProcessing, setIsProcessing] = useState(false); const [snackbar, setSnackbar] = useState({ isVisible: false, @@ -17,16 +19,16 @@ const AddTraderForm = ({ onAddTrader }) => { }); useEffect(() => { - if (!wsResponse || !isProcessing) return; + if (!lastMessage || !isProcessing) return; - if (wsResponse.msg_type === "copy_start") { + if (lastMessage.msg_type === "copy_start") { setIsProcessing(false); - if (wsResponse.error) { + if (lastMessage.error) { setSnackbar({ isVisible: true, message: - wsResponse.error.message || + lastMessage.error.message || "Failed to start copy trading", status: "fail", }); @@ -40,12 +42,20 @@ const AddTraderForm = ({ onAddTrader }) => { }); } } - }, [wsResponse, isProcessing, onAddTrader, traderData]); + }, [lastMessage, isProcessing, onAddTrader, traderData]); const handleSubmit = (e) => { e.preventDefault(); + if (!isConnected || !isAuthorized) { + setSnackbar({ + isVisible: true, + message: "Not connected to server", + status: "fail", + }); + return; + } setIsProcessing(true); - sendRequest({ + sendMessage({ copy_start: traderData.token, }); }; diff --git a/src/components/CopierDashboard.jsx b/src/components/CopierDashboard.jsx index b258d93..ee0cd94 100644 --- a/src/components/CopierDashboard.jsx +++ b/src/components/CopierDashboard.jsx @@ -1,12 +1,15 @@ import { useEffect, useState } from "react"; import { Text, Snackbar } from "@deriv-com/quill-ui"; -import useDerivWebSocket from "../hooks/useDerivWebSocket"; +import useWebSocket from "../hooks/useWebSocket"; +import useAuthorize from "../hooks/useAuthorize"; import useCopyTradersList from "../hooks/useCopyTradersList"; import AddTraderForm from "./AddTraderForm"; import TraderCard from "./TraderCard"; const CopierDashboard = () => { - const { sendRequest, wsResponse } = useDerivWebSocket(); + const { sendMessage, isConnected } = useWebSocket(); + const { isAuthorized } = useAuthorize(); + const [wsResponse, setWsResponse] = useState(null); const { traders: apiTraders, isLoading, @@ -22,8 +25,6 @@ const CopierDashboard = () => { }); }, [apiTraders, isLoading, error]); const [processingTrader, setProcessingTrader] = useState(null); - const [copiedTrader, setCopiedTrader] = useState(null); - const [failedCopyTrader, setFailedCopyTrader] = useState(null); const [snackbar, setSnackbar] = useState({ isVisible: false, message: "", @@ -50,7 +51,6 @@ const CopierDashboard = () => { "Error starting copy trade", status: "fail", }); - setFailedCopyTrader(processingTrader); setProcessingTrader(null); } else { const trader = processingTrader; @@ -58,11 +58,10 @@ const CopierDashboard = () => { "Showing success snackbar for trader:", trader.name ); - setCopiedTrader(trader); setProcessingTrader(null); setSnackbar({ isVisible: true, - message: `Successfully started copying ${trader.name}`, + message: `Successfully started copying ${trader.id}`, status: "neutral", }); } @@ -78,7 +77,6 @@ const CopierDashboard = () => { setProcessingTrader(null); } else { const trader = processingTrader; - setCopiedTrader(null); setProcessingTrader(null); setSnackbar({ isVisible: true, @@ -98,19 +96,19 @@ const CopierDashboard = () => { setSnackbar((prev) => ({ ...prev, isVisible: false })); }; - const handleCopyClick = (trader) => { - console.log("Copy clicked for trader:", trader); - setProcessingTrader(trader); - sendRequest({ - copy_start: trader.token, - }); - }; - const handleStopCopy = (trader) => { console.log("Stop copy clicked for trader:", trader); + if (!isConnected || !isAuthorized) { + setSnackbar({ + isVisible: true, + message: "Connection not ready. Please try again.", + status: "fail", + }); + return; + } setProcessingTrader(trader); - sendRequest({ - copy_stop: trader.token, + sendMessage({ copy_stop: trader.token }, (response) => { + setWsResponse(response); }); }; @@ -120,17 +118,6 @@ const CopierDashboard = () => { refreshList(); }; - const handleRemoveTrader = (trader) => { - // Show feedback - setSnackbar({ - isVisible: true, - message: `Removed ${trader.name}`, - status: "neutral", - }); - // Refresh the traders list from API - refreshList(); - }; - return (
diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 6bd101b..111ac7a 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -1,40 +1,23 @@ import { useState } from "react"; -import { - Button, - Text, - SegmentedControlSingleChoice, -} from "@deriv-com/quill-ui"; -import useDerivWebSocket from "../hooks/useDerivWebSocket"; +import { Button } from "@deriv-com/quill-ui"; +import useWebSocket from "../hooks/useWebSocket"; import Header from "./Header"; import TraderDashboard from "./TraderDashboard"; import CopierDashboard from "./CopierDashboard"; const Dashboard = () => { - const { settings, isLoading, sendRequest } = useDerivWebSocket(); - const [userType, setUserType] = useState("trader"); - - const items = [ - { label: "Trader", value: "trader" }, - { label: "Copier", value: "copier" }, - ]; + const { isConnected } = useWebSocket(); + const [userType, setUserType] = useState("copier"); const handleBecomeTrader = () => { - sendRequest({ - set_settings: 1, - allow_copiers: 1, - }); setUserType("trader"); }; const handleBecomeCopier = () => { - sendRequest({ - set_settings: 1, - allow_copiers: 0, - }); setUserType("copier"); }; - if (isLoading) { + if (!isConnected) { return
Loading...
; } @@ -46,21 +29,21 @@ const Dashboard = () => {
@@ -70,7 +53,7 @@ const Dashboard = () => { ) : userType === "copier" ? ( ) : ( - + )}
diff --git a/src/hooks/useAuthorize.js b/src/hooks/useAuthorize.js new file mode 100644 index 0000000..aa760e0 --- /dev/null +++ b/src/hooks/useAuthorize.js @@ -0,0 +1,50 @@ +import { useEffect } from 'react'; +import useWebSocket from './useWebSocket'; +import useDerivAccounts from './useDerivAccounts'; + +// Singleton state +let isAuthorizedGlobal = false; +let authErrorGlobal = null; + +const useAuthorize = () => { + const { defaultAccount, clearAccounts } = useDerivAccounts(); + const { isConnected, sendMessage, close } = useWebSocket(); + + // Handle authorization + useEffect(() => { + if (isConnected && defaultAccount?.token && !isAuthorizedGlobal) { + console.log('Sending authorize request'); + sendMessage( + { authorize: defaultAccount.token }, + (response) => { + if (response.error) { + console.error('Authorization failed:', response.error); + authErrorGlobal = response.error; + isAuthorizedGlobal = false; + clearAccounts(); + close(); + } else { + console.log('Authorization successful'); + authErrorGlobal = null; + isAuthorizedGlobal = true; + } + } + ); + } + }, [isConnected, defaultAccount, sendMessage, clearAccounts, close]); + + // Reset auth state when connection is lost + useEffect(() => { + if (!isConnected) { + isAuthorizedGlobal = false; + } + }, [isConnected]); + + return { + isAuthorized: isAuthorizedGlobal, + authError: authErrorGlobal, + isConnected, + }; +}; + +export default useAuthorize; diff --git a/src/hooks/useCopyTradersList.js b/src/hooks/useCopyTradersList.js index 7a3d1c5..22457c5 100644 --- a/src/hooks/useCopyTradersList.js +++ b/src/hooks/useCopyTradersList.js @@ -1,50 +1,77 @@ -import { useEffect, useState } from 'react' -import useDerivWebSocket from './useDerivWebSocket' +import { useEffect, useState, useCallback } from 'react' +import useWebSocket from './useWebSocket' +import useAuthorize from './useAuthorize' const useCopyTradersList = () => { - const { sendRequest, wsResponse } = useDerivWebSocket() + const { sendMessage, lastMessage } = useWebSocket() + const { isAuthorized, isConnected } = useAuthorize() const [traders, setTraders] = useState([]) const [copiers, setCopiers] = useState([]) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) - useEffect(() => { - // Make the initial request - sendRequest({ - copytrading_list: 1, - // Optional parameters can be added here: - // sort_fields: ["performance", "monthly_profitable_trades"], - // sort_order: ["DESC", "DESC"] - }) - }, [sendRequest]) + const fetchList = useCallback(() => { + if (!isConnected || !isAuthorized) return + + sendMessage( + { + copytrading_list: 1, + // Optional parameters can be added here: + // sort_fields: ["performance", "monthly_profitable_trades"], + // sort_order: ["DESC", "DESC"] + }, + (response) => { + console.log('Copy Trading List Response:', response) + if (response.error) { + setError(response.error.message) + setIsLoading(false) + return + } + + if (response.copytrading_list) { + setTraders(response.copytrading_list.traders) + setCopiers(response.copytrading_list.copiers) + setIsLoading(false) + setError(null) + } + } + ) + }, [sendMessage, isConnected, isAuthorized]) + + // Initial fetch when authorized and connected useEffect(() => { - if (!wsResponse) return + if (isAuthorized && isConnected) { + fetchList() + } + }, [isAuthorized, isConnected, fetchList]) - if (wsResponse.msg_type === 'copytrading_list') { - console.log('Copy Trading List Response:', wsResponse) + // Handle broadcast messages + useEffect(() => { + if (!lastMessage) return - if (wsResponse.error) { - setError(wsResponse.error.message) + if (lastMessage.msg_type === 'copytrading_list') { + if (lastMessage.error) { + setError(lastMessage.error.message) setIsLoading(false) return } - if (wsResponse.copytrading_list) { - setTraders(wsResponse.copytrading_list.traders) - setCopiers(wsResponse.copytrading_list.copiers) + if (lastMessage.copytrading_list) { + setTraders(lastMessage.copytrading_list.traders) + setCopiers(lastMessage.copytrading_list.copiers) setIsLoading(false) setError(null) } } - }, [wsResponse]) + }, [lastMessage]) return { traders, copiers, isLoading, error, - refreshList: () => sendRequest({ copytrading_list: 1 }) + refreshList: fetchList } } diff --git a/src/hooks/useCopyTradingStats.js b/src/hooks/useCopyTradingStats.js index e4fddfa..480ee3e 100644 --- a/src/hooks/useCopyTradingStats.js +++ b/src/hooks/useCopyTradingStats.js @@ -1,42 +1,46 @@ import { useState, useEffect } from 'react'; -import useDerivWebSocket from './useDerivWebSocket'; +import useWebSocket from './useWebSocket'; const useCopyTradingStats = (traderId) => { const [stats, setStats] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const { sendRequest, wsResponse } = useDerivWebSocket(); + const { sendMessage, lastMessage } = useWebSocket(); useEffect(() => { if (!traderId) return; const fetchStats = () => { setIsLoading(true); - sendRequest({ + sendMessage({ copytrading_statistics: 1, trader_id: traderId, + passthrough: { + trader_id: traderId + } }); }; fetchStats(); - }, [traderId, sendRequest]); + }, [traderId, sendMessage]); useEffect(() => { - if (!wsResponse || !traderId) return; + if (!lastMessage || !traderId) return; - if (wsResponse.echo_req?.trader_id === traderId) { - if (wsResponse.error) { - setError(wsResponse.error.message); + // Check if this response is for the current trader + if (lastMessage.passthrough?.trader_id === traderId) { + if (lastMessage.error) { + setError(lastMessage.error.message); setIsLoading(false); return; } - if (wsResponse.msg_type === 'copytrading_statistics') { - setStats(wsResponse.copytrading_statistics); + if (lastMessage.msg_type === 'copytrading_statistics') { + setStats(lastMessage.copytrading_statistics); setIsLoading(false); } } - }, [wsResponse, traderId]); + }, [lastMessage, traderId]); return { stats, @@ -44,12 +48,15 @@ const useCopyTradingStats = (traderId) => { error, refetch: () => { setError(null); - sendRequest({ + sendMessage({ copytrading_statistics: 1, trader_id: traderId, + passthrough: { + trader_id: traderId + } }); }, }; }; -export default useCopyTradingStats; \ No newline at end of file +export default useCopyTradingStats; diff --git a/src/hooks/useDerivWebSocket.js b/src/hooks/useDerivWebSocket.js index 921c189..35dd960 100644 --- a/src/hooks/useDerivWebSocket.js +++ b/src/hooks/useDerivWebSocket.js @@ -10,9 +10,10 @@ console.log('WebSocket Configuration:', { FINAL_URL: WEBSOCKET_URL }) -// Singleton WebSocket instance +// Singleton WebSocket instance and state let globalWs = null let responseHandlers = new Set() +let isAuthorizedGlobal = false const useDerivWebSocket = () => { const [socket, setSocket] = useState(null) @@ -56,6 +57,7 @@ const useDerivWebSocket = () => { } else { console.log('Authorization successful') authRetryCountRef.current = 0 // Reset retry counter on success + isAuthorizedGlobal = true setIsConnected(true) globalWs.send(JSON.stringify({ get_settings: 1 @@ -91,10 +93,12 @@ const useDerivWebSocket = () => { globalWs.onopen = () => { console.log('WebSocket connected, readyState:', globalWs.readyState) setIsConnected(true) - console.log('Sending authorize request with token:', defaultAccount.token) - globalWs.send(JSON.stringify({ - authorize: defaultAccount.token - })) + if (!isAuthorizedGlobal) { + console.log('Sending authorize request with token:', defaultAccount.token) + globalWs.send(JSON.stringify({ + authorize: defaultAccount.token + })) + } } globalWs.onerror = (error) => { @@ -104,6 +108,7 @@ const useDerivWebSocket = () => { globalWs.onclose = () => { console.log('WebSocket connection closed') globalWs = null + isAuthorizedGlobal = false responseHandlers.forEach(handler => handler({ type: 'connection', status: 'disconnected' })) setIsConnected(false) } @@ -118,7 +123,7 @@ const useDerivWebSocket = () => { responseHandlers.add(handleResponseRef.current) setSocket(globalWs) - if (globalWs.readyState === WebSocket.OPEN) { + if (globalWs.readyState === WebSocket.OPEN && !isAuthorizedGlobal) { setIsConnected(true) console.log('WebSocket already open, sending authorize request') globalWs.send(JSON.stringify({ @@ -159,4 +164,4 @@ const useDerivWebSocket = () => { } } -export default useDerivWebSocket +export default useDerivWebSocket diff --git a/src/hooks/useSettings.js b/src/hooks/useSettings.js new file mode 100644 index 0000000..79a6e93 --- /dev/null +++ b/src/hooks/useSettings.js @@ -0,0 +1,73 @@ +import { useState, useEffect, useCallback } from 'react'; +import useWebSocket from './useWebSocket'; +import useAuthorize from './useAuthorize'; + +const useSettings = () => { + const [settings, setSettings] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const { isAuthorized } = useAuthorize(); + const { isConnected, sendMessage } = useWebSocket(); + + // Fetch settings when authorized + useEffect(() => { + if (isConnected && isAuthorized && !settings) { + console.log('Fetching user settings'); + sendMessage( + { get_settings: 1 }, + (response) => { + if (response.error) { + console.error('Failed to fetch settings:', response.error); + setError(response.error); + } else { + console.log('Settings received:', response.get_settings); + setSettings(response.get_settings); + setError(null); + } + setIsLoading(false); + } + ); + } + }, [isConnected, isAuthorized, settings, sendMessage]); + + // Reset settings when connection is lost + useEffect(() => { + if (!isConnected) { + setSettings(null); + setIsLoading(true); + } + }, [isConnected]); + + const updateSettings = useCallback(async (newSettings) => { + if (!isConnected || !isAuthorized) { + throw new Error('Not connected or authorized'); + } + + return new Promise((resolve, reject) => { + sendMessage( + { set_settings: 1, ...newSettings }, + (response) => { + if (response.error) { + console.error('Failed to update settings:', response.error); + setError(response.error); + reject(response.error); + } else { + console.log('Settings updated:', response.set_settings); + setSettings(response.set_settings); + setError(null); + resolve(response.set_settings); + } + } + ); + }); + }, [isConnected, isAuthorized, sendMessage]); + + return { + settings, + error, + isLoading, + updateSettings + }; +}; + +export default useSettings; diff --git a/src/hooks/useWebSocket.js b/src/hooks/useWebSocket.js new file mode 100644 index 0000000..f4f3dcb --- /dev/null +++ b/src/hooks/useWebSocket.js @@ -0,0 +1,150 @@ +import { useState, useEffect, useCallback } from 'react'; +import { getConfig } from '../config'; + +// Singleton WebSocket instance +let wsInstance = null; +let subscribers = new Set(); +let messageHandlers = new Map(); +let currentReqId = 1; +let pingInterval = null; + +// Ping interval in milliseconds (30 seconds) +const PING_INTERVAL = 30000; + +const sendPing = () => { + if (wsInstance?.readyState === WebSocket.OPEN) { + const reqId = generateReqId(); + wsInstance.send(JSON.stringify({ ping: 1, req_id: reqId })); + } +}; + +const generateReqId = () => { + return currentReqId++; +}; + +const createWebSocket = () => { + const config = getConfig(); + const { WS_URL, APP_ID } = config; + const wsUrl = `${WS_URL}?app_id=${APP_ID}`; + + if (wsInstance) { + return wsInstance; + } + wsInstance = new WebSocket(wsUrl); + + wsInstance.onopen = () => { + subscribers.forEach(subscriber => subscriber.onOpen?.()); + // Start ping interval + if (pingInterval) { + clearInterval(pingInterval); + } + pingInterval = setInterval(sendPing, PING_INTERVAL); + }; + + wsInstance.onclose = () => { + subscribers.forEach(subscriber => subscriber.onClose?.()); + wsInstance = null; + messageHandlers.clear(); + // Clear ping interval + if (pingInterval) { + clearInterval(pingInterval); + pingInterval = null; + } + }; + + wsInstance.onerror = (error) => { + subscribers.forEach(subscriber => subscriber.onError?.(error)); + }; + + wsInstance.onmessage = (event) => { + try { + const response = JSON.parse(event.data); + const reqId = response.req_id; + + // Handle specific message handler if exists + if (reqId && messageHandlers.has(reqId)) { + messageHandlers.get(reqId)(response); + messageHandlers.delete(reqId); + } + + // Broadcast to all subscribers + subscribers.forEach(subscriber => subscriber.onMessage?.(response)); + } catch (error) { + console.error('Failed to parse WebSocket message:', error); + } + }; + + return wsInstance; +}; + +const useWebSocket = () => { + const [isConnected, setIsConnected] = useState(false); + const [error, setError] = useState(null); + const [lastMessage, setLastMessage] = useState(null); + + useEffect(() => { + const subscriber = { + onOpen: () => { + setIsConnected(true); + setError(null); + }, + onClose: () => { + setIsConnected(false); + }, + onError: (error) => { + setError(error); + setIsConnected(false); + }, + onMessage: (message) => { + setLastMessage(message); + } + }; + + subscribers.add(subscriber); + const ws = createWebSocket(); + + // If WebSocket is already open when hook is initialized + if (ws.readyState === WebSocket.OPEN) { + setIsConnected(true); + } + + return () => { + subscribers.delete(subscriber); + }; + }, []); + + const sendMessage = useCallback((message, callback) => { + if (!wsInstance || wsInstance.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket is not connected'); + } + + const reqId = generateReqId(); + const messageWithId = { + ...message, + req_id: reqId + }; + + if (callback) { + messageHandlers.set(reqId, callback); + } + + wsInstance.send(JSON.stringify(messageWithId)); + return reqId; + }, []); + + const close = useCallback(() => { + if (wsInstance) { + wsInstance.close(); + } + }, []); + + return { + isConnected, + error, + lastMessage, + sendMessage, + close + }; +}; + +export default useWebSocket;