From 1ebece6bad0aff7d9ab225c35e42c70d82fee5bb Mon Sep 17 00:00:00 2001 From: Jim Daniels Wasswa <104334373+jim-deriv@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:06:26 +0800 Subject: [PATCH] Jim/wall 5227/switch ws connection when clients switch between real and demo wallets 1 (#17949) * chore: update generic error message component for cashier and wallets * feat: add endpoint switching * revert: revert unrelated changes * style: rename variables to more meaningful names * style: sort imports * style: rename variable and remove unnecessary type attribute --- packages/api-v2/src/APIProvider.tsx | 31 +++++++++++----- packages/api-v2/src/AuthProvider.tsx | 41 +++++++++++++++++----- packages/api-v2/src/constants/index.ts | 1 + packages/api-v2/src/constants/platforms.ts | 1 + packages/api-v2/src/ws-client/ws-client.ts | 16 ++++++--- packages/shared/src/utils/config/config.ts | 8 +++-- packages/wallets/src/App.tsx | 2 +- 7 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 packages/api-v2/src/constants/platforms.ts diff --git a/packages/api-v2/src/APIProvider.tsx b/packages/api-v2/src/APIProvider.tsx index 3ecb76ef7b4a..d8f77d2833d3 100644 --- a/packages/api-v2/src/APIProvider.tsx +++ b/packages/api-v2/src/APIProvider.tsx @@ -1,10 +1,14 @@ -import React, { PropsWithChildren, createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'; +import React, { createContext, PropsWithChildren, useCallback, useContext, useEffect, useRef, useState } from 'react'; + import { getAppId, getSocketURL } from '@deriv/shared'; import { getInitialLanguage } from '@deriv-com/translations'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + import { TSocketRequestPayload, TSocketResponseData, TSocketSubscribableEndpointNames } from '../types'; -import { hashObject } from './utils'; + import WSClient from './ws-client/ws-client'; +import { PLATFORMS } from './constants'; +import { hashObject } from './utils'; type TSubscribeFunction = ( name: T, @@ -27,14 +31,14 @@ type APIContextData = { setOnConnected: (onConnected: () => void) => void; connection: WebSocket; wsClient: WSClient; + createNewWSConnection: () => void; }; /** * Retrieves the WebSocket URL based on the current environment. * @returns {string} The WebSocket URL. */ -const getWebSocketURL = () => { - const endpoint = getSocketURL(); +const getWebSocketURL = (endpoint: string) => { const app_id = getAppId(); const language = getInitialLanguage(); return `wss://${endpoint}/websockets/v3?app_id=${app_id}&l=${language}&brand=deriv`; @@ -45,8 +49,8 @@ const APIContext = createContext(null); /** * @returns {WebSocket} The initialized WebSocket instance. */ -const initializeConnection = (onWSClose: () => void, onOpen?: () => void): WebSocket => { - const wss_url = getWebSocketURL(); +const initializeConnection = (endpoint: string, onWSClose: () => void, onOpen?: () => void): WebSocket => { + const wss_url = getWebSocketURL(endpoint); const connection = new WebSocket(wss_url); connection.addEventListener('close', () => { @@ -67,12 +71,13 @@ const initializeConnection = (onWSClose: () => void, onOpen?: () => void): WebSo type TAPIProviderProps = { /** If set to true, the APIProvider will instantiate it's own socket connection. */ standalone?: boolean; + platform?: string; }; type SubscribeReturnType = ReturnType; // This captures the entire return type of TSubscribeFunction type UnwrappedSubscription = Awaited; -const APIProvider = ({ children }: PropsWithChildren) => { +const APIProvider = ({ children, platform }: PropsWithChildren) => { const [reconnect, setReconnect] = useState(false); const connectionRef = useRef(); const subscriptionsRef = useRef>(); @@ -87,6 +92,7 @@ const APIProvider = ({ children }: PropsWithChildren) => { const language = getInitialLanguage(); const [prevLanguage, setPrevLanguage] = useState(language); + const endpoint = getSocketURL(platform === PLATFORMS.WALLETS); useEffect(() => { isMounted.current = true; @@ -109,6 +115,7 @@ const APIProvider = ({ children }: PropsWithChildren) => { // have to be here and not inside useEffect as there are places in code expecting this to be available if (!connectionRef.current) { connectionRef.current = initializeConnection( + endpoint, () => { if (isMounted.current) setReconnect(true); }, @@ -118,6 +125,7 @@ const APIProvider = ({ children }: PropsWithChildren) => { } wsClientRef.current.setWs(connectionRef.current); + wsClientRef.current.setEndpoint(endpoint); if (isMounted.current) { isOpenRef.current = true; if (onConnectedRef.current) { @@ -197,6 +205,7 @@ const APIProvider = ({ children }: PropsWithChildren) => { let reconnectTimerId: NodeJS.Timeout; if (reconnect) { connectionRef.current = initializeConnection( + endpoint, () => { reconnectTimerId = setTimeout(() => { if (isMounted.current) { @@ -209,6 +218,7 @@ const APIProvider = ({ children }: PropsWithChildren) => { throw new Error('Connection is not set'); } wsClientRef.current.setWs(connectionRef.current); + wsClientRef.current.setEndpoint(endpoint); if (onReconnectedRef.current) { onReconnectedRef.current(); } @@ -218,7 +228,7 @@ const APIProvider = ({ children }: PropsWithChildren) => { } return () => clearTimeout(reconnectTimerId); - }, [reconnect]); + }, [endpoint, reconnect]); // reconnects to latest WS url for new language only when language changes useEffect(() => { @@ -229,10 +239,15 @@ const APIProvider = ({ children }: PropsWithChildren) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [language]); + const createNewWSConnection = useCallback(() => { + setReconnect(true); + }, []); + return ( ) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any subscribe: (onData: (response: any) => void) => Promise<{ unsubscribe: () => Promise }>; }; }; @@ -108,7 +112,7 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun const { mutateAsync } = useMutation('authorize'); - const { queryClient, setOnReconnected, setOnConnected, wsClient } = useAPIContext(); + const { queryClient, setOnReconnected, setOnConnected, wsClient, createNewWSConnection } = useAPIContext(); const [isLoading, setIsLoading] = useState(true); const [isSwitching, setIsSwitching] = useState(false); @@ -125,6 +129,7 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun const subscribe = useCallback( (name: T, payload?: TSocketRequestPayload) => { return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any subscribe: (onData: (response: any) => void) => { return wsClient?.subscribe(name, payload, onData); }, @@ -149,8 +154,15 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun if (!activeAccount) return; localStorage.setItem(loginIDKey ?? 'active_loginid', activeLoginID); + const isDemo = activeAccount.is_virtual; + const shouldCreateNewWSConnection = + (isDemo && wsClient?.endpoint === AppIDConstants.environments.real) || + (!isDemo && wsClient?.endpoint === AppIDConstants.environments.demo); + if (shouldCreateNewWSConnection) { + createNewWSConnection(); + } }, - [loginIDKey] + [loginIDKey, wsClient?.endpoint, createNewWSConnection] ); useEffect(() => { @@ -267,8 +279,21 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun isInitializing, subscribe, logout, + createNewWSConnection, }; - }, [data, switchAccount, refetch, isLoading, isError, isFetching, isSuccess, loginid, logout, subscribe]); + }, [ + data, + switchAccount, + refetch, + isLoading, + isError, + isFetching, + isSuccess, + loginid, + logout, + createNewWSConnection, + subscribe, + ]); return {children}; }; diff --git a/packages/api-v2/src/constants/index.ts b/packages/api-v2/src/constants/index.ts index aa7dd211925f..4460fc78c0de 100644 --- a/packages/api-v2/src/constants/index.ts +++ b/packages/api-v2/src/constants/index.ts @@ -1,3 +1,4 @@ export * from './countries'; export * from './onfido'; export * from './errorCodes'; +export * from './platforms'; diff --git a/packages/api-v2/src/constants/platforms.ts b/packages/api-v2/src/constants/platforms.ts new file mode 100644 index 000000000000..ff77f49b7f7b --- /dev/null +++ b/packages/api-v2/src/constants/platforms.ts @@ -0,0 +1 @@ +export const PLATFORMS = { WALLETS: 'wallets' } as const; diff --git a/packages/api-v2/src/ws-client/ws-client.ts b/packages/api-v2/src/ws-client/ws-client.ts index 6c153bba4890..76990332f1fb 100644 --- a/packages/api-v2/src/ws-client/ws-client.ts +++ b/packages/api-v2/src/ws-client/ws-client.ts @@ -1,12 +1,13 @@ -import SubscriptionsManager from './subscriptions-manager'; -import request from './request'; import { - TSocketResponse, - TSocketRequestPayload, TSocketEndpointNames, + TSocketRequestPayload, + TSocketResponse, TSocketSubscribableEndpointNames, } from '../../types'; +import request from './request'; +import SubscriptionsManager from './subscriptions-manager'; + /** * WSClient as main instance */ @@ -15,6 +16,7 @@ export default class WSClient { subscriptionManager: SubscriptionsManager; isAuthorized = false; onAuthorized?: () => void; + endpoint?: string; constructor(onAuthorized?: () => void) { this.onAuthorized = onAuthorized; @@ -28,6 +30,10 @@ export default class WSClient { } } + setEndpoint(endpoint: string) { + this.endpoint = endpoint; + } + private onWebsocketAuthorized() { if (!this.ws) { return; @@ -47,7 +53,7 @@ export default class WSClient { return Promise.reject(new Error('WS is not set')); } return request(this.ws, name, payload).then((response: TSocketResponse) => { - if ((response as unknown as any).msg_type === 'authorize') { + if ('msg_type' in response && response.msg_type === 'authorize') { this.onWebsocketAuthorized(); } diff --git a/packages/shared/src/utils/config/config.ts b/packages/shared/src/utils/config/config.ts index 6d51850df093..742ed41d1f8c 100644 --- a/packages/shared/src/utils/config/config.ts +++ b/packages/shared/src/utils/config/config.ts @@ -77,7 +77,7 @@ export const getAppId = () => { return app_id; }; -export const getSocketURL = () => { +export const getSocketURL = (is_wallets = false) => { const local_storage_server_url = window.localStorage.getItem('config.server_url'); if (local_storage_server_url) return local_storage_server_url; @@ -87,8 +87,10 @@ export const getSocketURL = () => { const params = new URLSearchParams(document.location.search.substring(1)); active_loginid_from_url = params.get('acct1'); } - - const loginid = window.localStorage.getItem('active_loginid') || active_loginid_from_url; + const local_storage_loginid = is_wallets + ? window.localStorage.getItem('active_wallet_loginid') + : window.localStorage.getItem('active_loginid'); + const loginid = local_storage_loginid || active_loginid_from_url; const is_real = loginid && !/^(VRT|VRW)/.test(loginid); const server = is_real ? 'green' : 'blue'; diff --git a/packages/wallets/src/App.tsx b/packages/wallets/src/App.tsx index 41793dc31d1b..ead52b96cb88 100644 --- a/packages/wallets/src/App.tsx +++ b/packages/wallets/src/App.tsx @@ -44,7 +44,7 @@ const App: React.FC = ({ const defaultLanguage = preferredLanguage ?? language; return ( - + }>