From bd25379713bc36bdff385fb2cf4cae3d008c8ba3 Mon Sep 17 00:00:00 2001 From: adrienne-deriv <103016120+adrienne-deriv@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:33:17 +0800 Subject: [PATCH] Adrienne / Fix SSO triggering multiple times due to useeffect dependencies (#18114) * feat: store redirect metadata for traders hub from os-redirect * Merge branch 'master' of github.com:deriv-com/deriv-app * chore: refactored sso and slo conditions * chore: remove debouncing in sso and slo handler * chore: resolve test cases --- packages/core/src/App/AppContent.tsx | 1 - .../RootComponent/root-component.jsx | 12 ++- ...ec.ts => useSilentLoginAndLogout.spec.tsx} | 78 ++++++++++++------- packages/hooks/src/useSilentLoginAndLogout.ts | 37 ++++----- 4 files changed, 73 insertions(+), 55 deletions(-) rename packages/hooks/src/__tests__/{useSilentLoginAndLogout.spec.ts => useSilentLoginAndLogout.spec.tsx} (65%) diff --git a/packages/core/src/App/AppContent.tsx b/packages/core/src/App/AppContent.tsx index 85d5ebcb9683..d876b23cf2b4 100644 --- a/packages/core/src/App/AppContent.tsx +++ b/packages/core/src/App/AppContent.tsx @@ -73,7 +73,6 @@ const AppContent: React.FC<{ passthrough: unknown }> = observer(({ passthrough } is_client_store_initialized, isOAuth2Enabled, oAuthLogout, - prevent_single_login, }); const [isWebPasskeysFFEnabled, isGBLoaded] = useGrowthbookIsOn({ diff --git a/packages/core/src/App/Containers/RootComponent/root-component.jsx b/packages/core/src/App/Containers/RootComponent/root-component.jsx index b16740822c5d..784b911c0a02 100644 --- a/packages/core/src/App/Containers/RootComponent/root-component.jsx +++ b/packages/core/src/App/Containers/RootComponent/root-component.jsx @@ -25,7 +25,14 @@ const RootComponent = observer(props => { setIsWalletsOnboardingTourGuideVisible, notification_messages_ui, } = ui; - const { has_wallet, logout, prevent_redirect_to_hub, is_client_store_initialized, setPreventSingleLogin } = client; + const { + has_wallet, + logout, + prevent_redirect_to_hub, + is_client_store_initialized, + prevent_single_login, + setPreventSingleLogin, + } = client; const { oAuthLogout } = useOauth2({ handleLogout: logout }); @@ -56,7 +63,7 @@ const RootComponent = observer(props => { } const shouldStayInDerivApp = !isHubRedirectionEnabled || !has_wallet || prevent_redirect_to_hub; - if (isHubRedirectionLoaded && is_client_store_initialized && shouldStayInDerivApp) { + if (prevent_single_login && isHubRedirectionLoaded && is_client_store_initialized && shouldStayInDerivApp) { setPreventSingleLogin(false); } }, [ @@ -64,6 +71,7 @@ const RootComponent = observer(props => { isHubRedirectionEnabled, has_wallet, prevent_redirect_to_hub, + prevent_single_login, is_client_store_initialized, ]); diff --git a/packages/hooks/src/__tests__/useSilentLoginAndLogout.spec.ts b/packages/hooks/src/__tests__/useSilentLoginAndLogout.spec.tsx similarity index 65% rename from packages/hooks/src/__tests__/useSilentLoginAndLogout.spec.ts rename to packages/hooks/src/__tests__/useSilentLoginAndLogout.spec.tsx index 8c39bc695d85..cae3f7924e0f 100644 --- a/packages/hooks/src/__tests__/useSilentLoginAndLogout.spec.ts +++ b/packages/hooks/src/__tests__/useSilentLoginAndLogout.spec.tsx @@ -1,9 +1,11 @@ +import React from 'react'; import Cookies from 'js-cookie'; import { requestOidcAuthentication } from '@deriv-com/auth-client'; import { renderHook } from '@testing-library/react-hooks'; import useSilentLoginAndLogout from '../useSilentLoginAndLogout'; +import { mockStore, StoreProvider } from '@deriv/stores'; jest.mock('js-cookie', () => ({ get: jest.fn(), @@ -15,6 +17,12 @@ jest.mock('@deriv-com/auth-client', () => ({ describe('useSilentLoginAndLogout', () => { const mockOAuthLogout = jest.fn(); + const mockStoreData = mockStore({ + client: { prevent_single_login: false }, + }); + const wrapper = ({ children }: { children: JSX.Element }) => ( + {children} + ); beforeEach(() => { jest.clearAllMocks(); @@ -42,12 +50,14 @@ describe('useSilentLoginAndLogout', () => { jest.spyOn(Storage.prototype, 'getItem').mockReturnValue(JSON.stringify({})); - renderHook(() => - useSilentLoginAndLogout({ - is_client_store_initialized: true, - isOAuth2Enabled: true, - oAuthLogout: mockOAuthLogout, - }) + renderHook( + () => + useSilentLoginAndLogout({ + is_client_store_initialized: true, + isOAuth2Enabled: true, + oAuthLogout: mockOAuthLogout, + }), + { wrapper } ); expect(requestOidcAuthentication).toHaveBeenCalledWith({ @@ -61,12 +71,14 @@ describe('useSilentLoginAndLogout', () => { jest.spyOn(Storage.prototype, 'getItem').mockReturnValue(JSON.stringify({})); - renderHook(() => - useSilentLoginAndLogout({ - is_client_store_initialized: true, - isOAuth2Enabled: true, - oAuthLogout: mockOAuthLogout, - }) + renderHook( + () => + useSilentLoginAndLogout({ + is_client_store_initialized: true, + isOAuth2Enabled: true, + oAuthLogout: mockOAuthLogout, + }), + { wrapper } ); expect(requestOidcAuthentication).not.toHaveBeenCalled(); @@ -83,12 +95,14 @@ describe('useSilentLoginAndLogout', () => { value: { pathname: '/callback' }, }); - renderHook(() => - useSilentLoginAndLogout({ - is_client_store_initialized: true, - isOAuth2Enabled: true, - oAuthLogout: mockOAuthLogout, - }) + renderHook( + () => + useSilentLoginAndLogout({ + is_client_store_initialized: true, + isOAuth2Enabled: true, + oAuthLogout: mockOAuthLogout, + }), + { wrapper } ); expect(requestOidcAuthentication).not.toHaveBeenCalled(); @@ -100,12 +114,14 @@ describe('useSilentLoginAndLogout', () => { jest.spyOn(Storage.prototype, 'getItem').mockReturnValue(JSON.stringify({ account1: {}, account2: {} })); - renderHook(() => - useSilentLoginAndLogout({ - is_client_store_initialized: true, - isOAuth2Enabled: true, - oAuthLogout: mockOAuthLogout, - }) + renderHook( + () => + useSilentLoginAndLogout({ + is_client_store_initialized: true, + isOAuth2Enabled: true, + oAuthLogout: mockOAuthLogout, + }), + { wrapper } ); expect(requestOidcAuthentication).not.toHaveBeenCalled(); @@ -117,12 +133,14 @@ describe('useSilentLoginAndLogout', () => { jest.spyOn(Storage.prototype, 'getItem').mockReturnValue(JSON.stringify({ account1: {}, account2: {} })); - renderHook(() => - useSilentLoginAndLogout({ - is_client_store_initialized: true, - isOAuth2Enabled: true, - oAuthLogout: mockOAuthLogout, - }) + renderHook( + () => + useSilentLoginAndLogout({ + is_client_store_initialized: true, + isOAuth2Enabled: true, + oAuthLogout: mockOAuthLogout, + }), + { wrapper } ); expect(requestOidcAuthentication).not.toHaveBeenCalled(); diff --git a/packages/hooks/src/useSilentLoginAndLogout.ts b/packages/hooks/src/useSilentLoginAndLogout.ts index 591887f2f321..bd99776f29c9 100644 --- a/packages/hooks/src/useSilentLoginAndLogout.ts +++ b/packages/hooks/src/useSilentLoginAndLogout.ts @@ -1,8 +1,8 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import Cookies from 'js-cookie'; import { requestOidcAuthentication } from '@deriv-com/auth-client'; - +import { useStore } from '@deriv/stores'; /** * Handles silent login and single logout logic for OAuth2. * @@ -16,22 +16,26 @@ const useSilentLoginAndLogout = ({ is_client_store_initialized, isOAuth2Enabled, oAuthLogout, - prevent_single_login, }: { is_client_store_initialized: boolean; isOAuth2Enabled: boolean; oAuthLogout: () => Promise; - prevent_single_login?: boolean; }) => { const loggedState = Cookies.get('logged_state'); + const { client } = useStore(); const clientAccounts = JSON.parse(localStorage.getItem('client.accounts') || '{}'); const isClientAccountsPopulated = Object.keys(clientAccounts).length > 0; const isSilentLoginExcluded = window.location.pathname.includes('callback') || window.location.pathname.includes('endpoint'); + // state to manage and ensure OIDC callback functions are invoked once only + const isAuthenticating = useRef(false); + const isLoggingOut = useRef(false); + const { prevent_single_login } = client; + useEffect(() => { - if (prevent_single_login) return; + if (prevent_single_login || !isOAuth2Enabled || !is_client_store_initialized || isSilentLoginExcluded) return; // NOTE: Remove this logic once social signup is intergated with OIDC const params = new URLSearchParams(window.location.search); const isUsingLegacyFlow = params.has('token1') && params.has('acct1'); @@ -39,29 +43,19 @@ const useSilentLoginAndLogout = ({ return; } - if ( - !isUsingLegacyFlow && - loggedState === 'true' && - !isClientAccountsPopulated && - isOAuth2Enabled && - is_client_store_initialized && - !isSilentLoginExcluded - ) { + if (!isUsingLegacyFlow && loggedState === 'true' && !isClientAccountsPopulated) { // Perform silent login + if (isAuthenticating.current) return; + isAuthenticating.current = true; requestOidcAuthentication({ redirectCallbackUri: `${window.location.origin}/callback`, }); } - if ( - !isUsingLegacyFlow && - loggedState === 'false' && - is_client_store_initialized && - isOAuth2Enabled && - isClientAccountsPopulated && - !window.location.pathname.includes('callback') - ) { + if (!isUsingLegacyFlow && loggedState === 'false' && isClientAccountsPopulated) { // Perform single logout + if (isLoggingOut.current) return; + isLoggingOut.current = true; oAuthLogout(); } }, [ @@ -69,7 +63,6 @@ const useSilentLoginAndLogout = ({ isClientAccountsPopulated, is_client_store_initialized, isOAuth2Enabled, - oAuthLogout, isSilentLoginExcluded, prevent_single_login, ]);