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;