Repository: deriv-com/deriv-app
Pull request #18114: Adrienne / Fix SSO triggering multiple times due to useeffect dependencies
Author: login: adrienne-deriv
Created: 2025-03-07T06:31:54Z | Merged: 2025-03-07T08:33:17Z

## Changes:

Please provide a summary of the change.

### Screenshots:

Please provide some screenshots of the change.



Diff:
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 }) => (
+        <StoreProvider store={mockStoreData}>{children}</StoreProvider>
+    );
 
     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<void>;
-    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,
     ]);


Reviews:
user: login: amam-deriv
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T06:35:35Z
user: login: adrienne-deriv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T07:02:42Z
user: login: adrienne-deriv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T07:02:54Z
user: login: suisin-deriv
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T08:32:42Z

Review comments:
diff_hunk: @@ -1,7 +1,11 @@
-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';
+import { useDebounceCallback } from 'usehooks-ts';
+
+const DEFAULT_DEBOUNCE_TIME = 10000;
path: packages/hooks/src/useSilentLoginAndLogout.ts
user: login: amam-deriv
body: do we still need debounce with useRef, and 10s for debounce is too long, normally its 500ms max
created_at: 2025-03-07T06:35:35Z
author_association: COLLABORATOR
diff_hunk: @@ -16,60 +16,53 @@ const useSilentLoginAndLogout = ({
     is_client_store_initialized,
     isOAuth2Enabled,
     oAuthLogout,
-    prevent_single_login,
 }: {
     is_client_store_initialized: boolean;
     isOAuth2Enabled: boolean;
     oAuthLogout: () => Promise<void>;
-    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');
         if (isUsingLegacyFlow && loggedState === 'false' && isOAuth2Enabled) {
             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();
         }
     }, [
         loggedState,
         isClientAccountsPopulated,
         is_client_store_initialized,
         isOAuth2Enabled,
-        oAuthLogout,
path: packages/hooks/src/useSilentLoginAndLogout.ts
user: login: adrienne-deriv
body: This is actually one of the dependencies that's causing the 9 times rerendering
created_at: 2025-03-07T07:02:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,60 +16,53 @@ const useSilentLoginAndLogout = ({
     is_client_store_initialized,
     isOAuth2Enabled,
     oAuthLogout,
-    prevent_single_login,
 }: {
     is_client_store_initialized: boolean;
     isOAuth2Enabled: boolean;
     oAuthLogout: () => Promise<void>;
-    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');
         if (isUsingLegacyFlow && loggedState === 'false' && isOAuth2Enabled) {
             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();
         }
     }, [
         loggedState,
         isClientAccountsPopulated,
         is_client_store_initialized,
         isOAuth2Enabled,
-        oAuthLogout,
path: packages/hooks/src/useSilentLoginAndLogout.ts
user: login: adrienne-deriv
body: Because the `oAuthLogout` passed was not useCallback'ed
created_at: 2025-03-07T07:02:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-07T06:31:58Z
body: [vc]: #1p1qqZKdHJQlnxpreq1fOvoiBApZcQR8ojTQRwyiHRk=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJkZXJpdi1hcHAiLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vZGVyaXYvZGVyaXYtYXBwL0hBaGZMNFd4aGc0M3BwNXY3SzJSNzhSMndNdmQiLCJwcmV2aWV3VXJsIjoiZGVyaXYtYXBwLWdpdC1mb3JrLWFkcmllbm5lLWRlcml2LWZpeC1zc28tdHJpZ2dlcmluZy1sb3RzLmJpbmFyeS5zeCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **deriv-app** | ✅ Ready ([Inspect](https://vercel.com/deriv/deriv-app/HAhfL4Wxhg43pp5v7K2R78R2wMvd)) | [Visit Preview](https://deriv-app-git-fork-adrienne-deriv-fix-sso-triggering-lots.binary.sx) | Mar 7, 2025 8:05am |


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-07T06:53:20Z
body: A production App ID was automatically generated for this PR. ([log](https://github.com/deriv-com/deriv-app/actions/runs/13716372291))

- **PR**: [https://github.com/deriv-com/deriv-app/pull/18114](https://github.com/deriv-com/deriv-app/pull/18114)
- **URLs**:
    - **w/ App ID + Server**: https://deriv-app-git-fork-adrienne-deriv-fix-sso-triggering-lots.binary.sx?qa_server=red.derivws.com&app_id=31586
    - **Original**: https://deriv-app-git-fork-adrienne-deriv-fix-sso-triggering-lots.binary.sx
- **App ID**: `31586`

<details>
  <summary>Click here to copy & paste above information.</summary>

  ```
  - **PR**: [https://github.com/deriv-com/deriv-app/pull/18114](https://github.com/deriv-com/deriv-app/pull/18114)
  - **URLs**:
      - **w/ App ID + Server**: https://deriv-app-git-fork-adrienne-deriv-fix-sso-triggering-lots.binary.sx?qa_server=red.derivws.com&app_id=31586
      - **Original**: https://deriv-app-git-fork-adrienne-deriv-fix-sso-triggering-lots.binary.sx
  - **App ID**: `31586`
  ```
</details>
<!-- Sticky Pull Request Commentderiv-app-id-action -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-07T06:53:24Z
body: ⏳ **Generating Lighthouse report...**
<!-- Sticky Pull Request Commentgenerate_lighthouse_comment -->
author_association: CONTRIBUTOR