From f6f87e29d746a2a2a888476a76cc6b68e9f32a99 Mon Sep 17 00:00:00 2001 From: wojciech-deriv <141034155+wojciech-deriv@users.noreply.github.com> Date: Fri, 7 Jun 2024 14:34:49 +0100 Subject: [PATCH] [WALL-3905] wojtek/do not subscribe for not authorized websocket (#15047) * feat: make sure subscrpitions are not called when websocket is not authorized or not opened * feat: typo * feat: fixed test * feat: removed unnecessary file * feat: fixed loader handling --- packages/api-v2/src/AuthProvider.tsx | 58 +++++++++++++++++-- .../src/__tests__/useSubscription.spec.tsx | 6 +- packages/api-v2/src/useAPI.ts | 4 +- packages/api-v2/src/useSubscription.ts | 6 +- .../TransactionStatus/TransactionStatus.tsx | 4 +- .../__tests__/TransactionStatus.spec.tsx | 9 +++ 6 files changed, 74 insertions(+), 13 deletions(-) diff --git a/packages/api-v2/src/AuthProvider.tsx b/packages/api-v2/src/AuthProvider.tsx index a62fc6c0c0a7..e41775c0d018 100644 --- a/packages/api-v2/src/AuthProvider.tsx +++ b/packages/api-v2/src/AuthProvider.tsx @@ -3,7 +3,8 @@ import { useAPIContext } from './APIProvider'; import { getAccountsFromLocalStorage, getActiveLoginIDFromLocalStorage, getToken } from '@deriv/utils'; import useMutation from './useMutation'; -import { TSocketResponseData } from '../types'; +import { TSocketSubscribableEndpointNames, TSocketResponseData, TSocketRequestPayload } from '../types'; +import useAPI from './useAPI'; // Define the type for the context state type AuthContextType = { @@ -19,6 +20,17 @@ type AuthContextType = { error: unknown; isSwitching: boolean; isInitializing: boolean; + subscribe: ( + name: T, + payload?: TSocketRequestPayload + ) => + | { + subscribe: ( + onData: (response: any) => void, + onError: (response: any) => void + ) => { unsubscribe?: VoidFunction }; + } + | undefined; }; type LoginToken = { @@ -97,7 +109,7 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun const { mutateAsync } = useMutation('authorize'); - const { queryClient, setOnReconnected, setOnConnected } = useAPIContext(); + const { queryClient, setOnReconnected, setOnConnected, derivAPI } = useAPIContext(); const [isLoading, setIsLoading] = useState(true); const [isSwitching, setIsSwitching] = useState(false); @@ -105,9 +117,35 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun const [isSuccess, setIsSuccess] = useState(false); const [isError, setIsError] = useState(false); const [isFetching, setIsFetching] = useState(false); + const [isAuthorized, setIsAuthorized] = useState(false); const [data, setData] = useState | null>(); + const { subscribe: _subscribe } = useAPI(); + + const subscribe = useCallback( + ( + name: T, + payload?: TSocketRequestPayload + ): + | { + subscribe: ( + // The type will be handled by the `useSubscription` hook. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onData: (response: any) => void, + // The type will be handled by the `useSubscription` hook. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onError: (response: any) => void + ) => { unsubscribe?: VoidFunction }; + } + | undefined => { + if (isAuthorized && derivAPI.connection.readyState == 1) { + return derivAPI?.subscribe({ [name]: 1, subscribe: 1, ...(payload || {}) }); + } + }, + [derivAPI, isAuthorized] + ); + const processAuthorizeResponse = useCallback( (authorizeResponse: TSocketResponseData<'authorize'>) => { setData(authorizeResponse); @@ -135,8 +173,10 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun }, []); useEffect(() => { - setOnReconnected(() => { - mutateAsync({ payload: { authorize: getToken(loginid || '') ?? '' } }); + setOnReconnected(async () => { + setIsAuthorized(false); + await mutateAsync({ payload: { authorize: getToken(loginid || '') ?? '' } }); + setIsAuthorized(true); }); }, [loginid]); @@ -154,8 +194,10 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun setIsLoading(true); setIsInitializing(true); setIsFetching(true); + setIsAuthorized(false); await mutateAsync({ payload: { authorize: token || '' } }) .then(res => { + setIsAuthorized(true); processAuthorizeResponse(res); setIsLoading(false); setIsInitializing(false); @@ -163,6 +205,7 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun setLoginid(res?.authorize?.loginid ?? ''); }) .catch(() => { + setIsAuthorized(false); setIsLoading(false); setIsInitializing(false); setIsError(true); @@ -175,6 +218,7 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun }) .catch(() => { if (isMounted) { + setIsAuthorized(false); setIsLoading(false); setIsInitializing(false); setIsError(true); @@ -197,7 +241,9 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun setIsLoading(true); setIsSwitching(true); + setIsAuthorized(false); const authorizeResponse = await mutateAsync({ payload: { authorize: getToken(newLoginId) ?? '' } }); + setIsAuthorized(true); setLoginid(newLoginId); processAuthorizeResponse(authorizeResponse); @@ -224,8 +270,9 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun loginid, isSwitching, isInitializing, + subscribe, }; - }, [data, switchAccount, refetch, isLoading, isError, isFetching, isSuccess, loginid]); + }, [data, switchAccount, refetch, isLoading, isError, isFetching, isSuccess, loginid, subscribe]); return {children}; }; @@ -234,6 +281,7 @@ export default AuthProvider; export const useAuthContext = () => { const context = useContext(AuthContext); + if (!context) { throw new Error('useAuthContext must be used within APIProvider'); } diff --git a/packages/api-v2/src/__tests__/useSubscription.spec.tsx b/packages/api-v2/src/__tests__/useSubscription.spec.tsx index 6dbfb19baf56..d8e5a7bf63c1 100644 --- a/packages/api-v2/src/__tests__/useSubscription.spec.tsx +++ b/packages/api-v2/src/__tests__/useSubscription.spec.tsx @@ -4,9 +4,11 @@ import APIProvider from '../APIProvider'; import AuthProvider from '../AuthProvider'; import useSubscription from '../useSubscription'; -jest.mock('./../useAPI', () => ({ +jest.mock('../AuthProvider', () => ({ __esModule: true, - default() { + ...jest.requireActual('../AuthProvider'), + + useAuthContext: () => { return { subscribe() { return { diff --git a/packages/api-v2/src/useAPI.ts b/packages/api-v2/src/useAPI.ts index 65e035b1037a..fc4a5efe7955 100644 --- a/packages/api-v2/src/useAPI.ts +++ b/packages/api-v2/src/useAPI.ts @@ -41,7 +41,9 @@ const useAPI = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any onError: (response: any) => void ) => { unsubscribe?: VoidFunction }; - } => derivAPI?.subscribe({ [name]: 1, subscribe: 1, ...(payload || {}) }), + } => { + return derivAPI?.subscribe({ [name]: 1, subscribe: 1, ...(payload || {}) }); + }, [derivAPI] ); diff --git a/packages/api-v2/src/useSubscription.ts b/packages/api-v2/src/useSubscription.ts index 80a2311a2da9..cd448a9743b5 100644 --- a/packages/api-v2/src/useSubscription.ts +++ b/packages/api-v2/src/useSubscription.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import useAPI from './useAPI'; +import { useAuthContext } from './AuthProvider'; import type { TSocketAcceptableProps, TSocketError, @@ -16,7 +16,7 @@ const useSubscription = (name: T, id const [data, setData] = useState>(); const subscriber = useRef<{ unsubscribe?: VoidFunction }>(); const idle_timeout = useRef(); - const { subscribe: _subscribe } = useAPI(); + const { subscribe: _subscribe } = useAuthContext(); const subscribe = useCallback( (...props: TSocketAcceptableProps) => { @@ -33,7 +33,7 @@ const useSubscription = (name: T, id }, idle_time); try { - subscriber.current = _subscribe(name, payload).subscribe( + subscriber.current = _subscribe(name, payload)?.subscribe( response => { setData(response); setIsLoading(false); diff --git a/packages/wallets/src/features/cashier/modules/TransactionStatus/TransactionStatus.tsx b/packages/wallets/src/features/cashier/modules/TransactionStatus/TransactionStatus.tsx index f1f3de2dbbf5..0a5ac81ef43d 100644 --- a/packages/wallets/src/features/cashier/modules/TransactionStatus/TransactionStatus.tsx +++ b/packages/wallets/src/features/cashier/modules/TransactionStatus/TransactionStatus.tsx @@ -54,12 +54,12 @@ const TransactionStatus: React.FC = ({ transactionType }) => {/* --color-grey-5 */}
- {!isError && isLoading && ( + {isLoading && (
)} - {isError && } + {isError && !isLoading && } {isTransactionStatusSuccessVisible && ( { it('should render error state correctly for useActiveWalletAccount', async () => { const mockError = new Error('Test error'); + (useCryptoTransactions as jest.Mock).mockImplementation(() => ({ + data: [], + error: null, + isLoading: false, + resetData: jest.fn(), + subscribe: jest.fn(), + unsubscribe: jest.fn(), + })); + (useActiveWalletAccount as jest.Mock).mockImplementation(() => ({ data: null, error: mockError,