Skip to content

Commit

Permalink
Jim/wall 5227/switch ws connection when clients switch between real a…
Browse files Browse the repository at this point in the history
…nd 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
  • Loading branch information
jim-deriv authored Jan 8, 2025
1 parent 2ac55e6 commit 1ebece6
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 25 deletions.
31 changes: 23 additions & 8 deletions packages/api-v2/src/APIProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 = <T extends TSocketSubscribableEndpointNames>(
name: T,
Expand All @@ -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`;
Expand All @@ -45,8 +49,8 @@ const APIContext = createContext<APIContextData | null>(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', () => {
Expand All @@ -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<TSubscribeFunction>; // This captures the entire return type of TSubscribeFunction
type UnwrappedSubscription = Awaited<SubscribeReturnType>;

const APIProvider = ({ children }: PropsWithChildren<TAPIProviderProps>) => {
const APIProvider = ({ children, platform }: PropsWithChildren<TAPIProviderProps>) => {
const [reconnect, setReconnect] = useState(false);
const connectionRef = useRef<WebSocket>();
const subscriptionsRef = useRef<Record<string, UnwrappedSubscription['subscription']>>();
Expand All @@ -87,6 +92,7 @@ const APIProvider = ({ children }: PropsWithChildren<TAPIProviderProps>) => {

const language = getInitialLanguage();
const [prevLanguage, setPrevLanguage] = useState<string>(language);
const endpoint = getSocketURL(platform === PLATFORMS.WALLETS);

useEffect(() => {
isMounted.current = true;
Expand All @@ -109,6 +115,7 @@ const APIProvider = ({ children }: PropsWithChildren<TAPIProviderProps>) => {
// 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);
},
Expand All @@ -118,6 +125,7 @@ const APIProvider = ({ children }: PropsWithChildren<TAPIProviderProps>) => {
}

wsClientRef.current.setWs(connectionRef.current);
wsClientRef.current.setEndpoint(endpoint);
if (isMounted.current) {
isOpenRef.current = true;
if (onConnectedRef.current) {
Expand Down Expand Up @@ -197,6 +205,7 @@ const APIProvider = ({ children }: PropsWithChildren<TAPIProviderProps>) => {
let reconnectTimerId: NodeJS.Timeout;
if (reconnect) {
connectionRef.current = initializeConnection(
endpoint,
() => {
reconnectTimerId = setTimeout(() => {
if (isMounted.current) {
Expand All @@ -209,6 +218,7 @@ const APIProvider = ({ children }: PropsWithChildren<TAPIProviderProps>) => {
throw new Error('Connection is not set');
}
wsClientRef.current.setWs(connectionRef.current);
wsClientRef.current.setEndpoint(endpoint);
if (onReconnectedRef.current) {
onReconnectedRef.current();
}
Expand All @@ -218,7 +228,7 @@ const APIProvider = ({ children }: PropsWithChildren<TAPIProviderProps>) => {
}

return () => clearTimeout(reconnectTimerId);
}, [reconnect]);
}, [endpoint, reconnect]);

// reconnects to latest WS url for new language only when language changes
useEffect(() => {
Expand All @@ -229,10 +239,15 @@ const APIProvider = ({ children }: PropsWithChildren<TAPIProviderProps>) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [language]);

const createNewWSConnection = useCallback(() => {
setReconnect(true);
}, []);

return (
<APIContext.Provider
value={{
subscribe,
createNewWSConnection,
unsubscribe,
queryClient: reactQueryRef.current,
setOnReconnected,
Expand Down
41 changes: 33 additions & 8 deletions packages/api-v2/src/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React, { createContext, useState, useContext, useCallback, useEffect, useMemo } from 'react';
import { useAPIContext } from './APIProvider';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';

import { getAccountsFromLocalStorage, getActiveLoginIDFromLocalStorage, getToken } from '@deriv/utils';
import useMutation from './useMutation';
import { TSocketSubscribableEndpointNames, TSocketResponseData, TSocketRequestPayload } from '../types';
import useAPI from './useAPI';
import { AppIDConstants } from '@deriv-com/utils';

import { TSocketRequestPayload, TSocketResponseData, TSocketSubscribableEndpointNames } from '../types';

import { useAPIContext } from './APIProvider';
import { API_ERROR_CODES } from './constants';
import useAPI from './useAPI';
import useMutation from './useMutation';

// Define the type for the context state
type AuthContextType = {
Expand All @@ -25,6 +28,7 @@ type AuthContextType = {
name: T,
payload?: TSocketRequestPayload<T>
) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subscribe: (onData: (response: any) => void) => Promise<{ unsubscribe: () => Promise<void> }>;
};
};
Expand Down Expand Up @@ -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);
Expand All @@ -125,6 +129,7 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun
const subscribe = useCallback(
<T extends TSocketSubscribableEndpointNames>(name: T, payload?: TSocketRequestPayload<T>) => {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subscribe: (onData: (response: any) => void) => {
return wsClient?.subscribe(name, payload, onData);
},
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
Expand Down
1 change: 1 addition & 0 deletions packages/api-v2/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './countries';
export * from './onfido';
export * from './errorCodes';
export * from './platforms';
1 change: 1 addition & 0 deletions packages/api-v2/src/constants/platforms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PLATFORMS = { WALLETS: 'wallets' } as const;
16 changes: 11 additions & 5 deletions packages/api-v2/src/ws-client/ws-client.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand All @@ -15,6 +16,7 @@ export default class WSClient {
subscriptionManager: SubscriptionsManager;
isAuthorized = false;
onAuthorized?: () => void;
endpoint?: string;

constructor(onAuthorized?: () => void) {
this.onAuthorized = onAuthorized;
Expand All @@ -28,6 +30,10 @@ export default class WSClient {
}
}

setEndpoint(endpoint: string) {
this.endpoint = endpoint;
}

private onWebsocketAuthorized() {
if (!this.ws) {
return;
Expand All @@ -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<TSocketEndpointNames>) => {
if ((response as unknown as any).msg_type === 'authorize') {
if ('msg_type' in response && response.msg_type === 'authorize') {
this.onWebsocketAuthorized();
}

Expand Down
8 changes: 5 additions & 3 deletions packages/shared/src/utils/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/wallets/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const App: React.FC<TProps> = ({
const defaultLanguage = preferredLanguage ?? language;

return (
<APIProvider standalone>
<APIProvider platform='wallets' standalone>
<WalletsAuthProvider logout={logout}>
<TranslationProvider defaultLang={defaultLanguage} i18nInstance={i18nInstance}>
<React.Suspense fallback={<Loader />}>
Expand Down

0 comments on commit 1ebece6

Please sign in to comment.