Repository: deriv-com/p2p
Pull request #539: farrah/fix: tmb integration
Author: login: farrah-deriv
Created: 2025-05-06T10:16:06Z | Merged: 2025-05-09T06:59:44Z



Diff:
diff --git a/package-lock.json b/package-lock.json
index 3fde8566..42c59ac2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,7 @@
                 "@datadog/browser-rum": "^5.11.0",
                 "@deriv-com/analytics": "^1.27.0",
                 "@deriv-com/api-hooks": "^1.7.1",
-                "@deriv-com/auth-client": "1.4.6",
+                "@deriv-com/auth-client": "1.5.1",
                 "@deriv-com/translations": "^1.3.9",
                 "@deriv-com/ui": "^1.35.6",
                 "@deriv-com/utils": "^0.0.39",
@@ -3895,9 +3895,9 @@
             ]
         },
         "node_modules/@deriv-com/auth-client": {
-            "version": "1.4.6",
-            "resolved": "https://registry.npmjs.org/@deriv-com/auth-client/-/auth-client-1.4.6.tgz",
-            "integrity": "sha512-oRrrdT9UVeFrATm0sNr8cgMF0dIlxrymjRMyZ/kbR5f+Ofxm9VGalNzO6R2ZjcazoMvdbP0PRkQZpRH7IJMPfA==",
+            "version": "1.5.1",
+            "resolved": "https://registry.npmjs.org/@deriv-com/auth-client/-/auth-client-1.5.1.tgz",
+            "integrity": "sha512-zeRwiSRPxjJyF74+Oq//E93VTrGL1OpWzo4RMCeKNoqrwm/Mz077I2eQMG6nujvqBY8ygXzz5BowZumxmQaH9g==",
             "dependencies": {
                 "@deriv-com/utils": "^0.0.42",
                 "js-cookie": "3.0.5",
diff --git a/package.json b/package.json
index 633ca8b9..24ee86d7 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
         "@datadog/browser-rum": "^5.11.0",
         "@deriv-com/analytics": "^1.27.0",
         "@deriv-com/api-hooks": "^1.7.1",
-        "@deriv-com/auth-client": "1.4.6",
+        "@deriv-com/auth-client": "1.5.1",
         "@deriv-com/translations": "^1.3.9",
         "@deriv-com/ui": "^1.35.6",
         "@deriv-com/utils": "^0.0.39",
@@ -114,4 +114,4 @@
     "optionalDependencies": {
         "@rollup/rollup-linux-x64-gnu": "4.9.5"
     }
-}
+}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 0e42dce2..795c211a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom';
 import { QueryParamProvider } from 'use-query-params';
 import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
 import { AppFooter, AppHeader, DerivIframe, ErrorBoundary } from '@/components';
-import { useDatadog, useDerivAnalytics, useOAuth, useTrackjs } from '@/hooks';
+import { useDatadog, useDerivAnalytics, useOAuth, useTMB, useTrackjs } from '@/hooks';
 import AppContent from '@/routes/AppContent';
 import { initializeI18n, TranslationProvider } from '@deriv-com/translations';
 import { Loader, useDevice } from '@deriv-com/ui';
@@ -17,6 +17,7 @@ const i18nInstance = initializeI18n({
 
 const App = () => {
     const { onRenderAuthCheck } = useOAuth();
+    const { onRenderTMBCheck } = useTMB();
     const { init: initTrackJS } = useTrackjs();
     const { initialise: initDatadog } = useDatadog();
     const { isDesktop } = useDevice();
@@ -26,14 +27,19 @@ const App = () => {
     const isProduction = process.env.VITE_NODE_ENV === 'production' || origin === URLConstants.derivP2pProduction;
     const isStaging = process.env.VITE_NODE_ENV === 'staging' || origin === URLConstants.derivP2pStaging;
     const isOAuth2Enabled = isProduction || isStaging;
+    const isTMBEnabled = localStorage.getItem('is_tmb_enabled');
 
     initTrackJS();
     initDerivAnalytics();
     initDatadog();
 
     useEffect(() => {
-        onRenderAuthCheck();
-    }, [onRenderAuthCheck]);
+        if (isTMBEnabled) {
+            onRenderTMBCheck();
+        } else {
+            onRenderAuthCheck();
+        }
+    }, [onRenderAuthCheck, onRenderTMBCheck, isTMBEnabled]);
 
     return (
         <BrowserRouter>
diff --git a/src/components/AppHeader/AppHeader.tsx b/src/components/AppHeader/AppHeader.tsx
index f9e946ce..712e2733 100644
--- a/src/components/AppHeader/AppHeader.tsx
+++ b/src/components/AppHeader/AppHeader.tsx
@@ -31,6 +31,7 @@ const AppHeader = () => {
     const isProduction = process.env.VITE_NODE_ENV === 'production' || origin === URLConstants.derivP2pProduction;
     const isStaging = process.env.VITE_NODE_ENV === 'staging' || origin === URLConstants.derivP2pStaging;
     const isOAuth2Enabled = isProduction || isStaging;
+    const isTMBEnabled = localStorage.getItem('is_tmb_enabled');
     const redirectLink = useShouldRedirectToLowCodeHub('personal-details');
 
     useEffect(() => {
@@ -94,9 +95,15 @@ const AppHeader = () => {
                 color='primary-light'
                 onClick={async () => {
                     if (isOAuth2Enabled) {
-                        await requestOidcAuthentication({
-                            redirectCallbackUri: `${window.location.origin}/callback`,
-                        });
+                        if (isTMBEnabled) {
+                            await requestOidcAuthentication({
+                                redirectCallbackUri: window.location.origin,
+                            });
+                        } else {
+                            await requestOidcAuthentication({
+                                redirectCallbackUri: `${window.location.origin}/callback`,
+                            });
+                        }
                     } else {
                         window.open(oauthUrl, '_self');
                     }
diff --git a/src/hooks/custom-hooks/index.ts b/src/hooks/custom-hooks/index.ts
index f7c0bc06..fda747ea 100644
--- a/src/hooks/custom-hooks/index.ts
+++ b/src/hooks/custom-hooks/index.ts
@@ -29,4 +29,5 @@ export { default as useQueryString } from './useQueryString';
 export { default as useSendbird } from './useSendbird';
 export { default as useShouldRedirectToLowCodeHub } from './useShouldRedirectToLowCodeHub';
 export { default as useSyncedTime } from './useSyncedTime';
+export { default as useTMB } from './useTMB';
 export { default as useTrackjs } from './useTrackjs';
diff --git a/src/hooks/custom-hooks/useTMB.ts b/src/hooks/custom-hooks/useTMB.ts
new file mode 100644
index 00000000..9997b948
--- /dev/null
+++ b/src/hooks/custom-hooks/useTMB.ts
@@ -0,0 +1,132 @@
+import { useCallback } from 'react';
+import Cookies from 'js-cookie';
+import { getOauthUrl } from '@/constants';
+import { getCurrentRoute, removeCookies } from '@/utils';
+import { useAuthData } from '@deriv-com/api-hooks';
+import { OAuth2Logout, requestOidcAuthentication, requestSessionActive } from '@deriv-com/auth-client';
+import { URLConstants } from '@deriv-com/utils';
+
+type UseTMBReturn = {
+    isOAuth2Enabled: boolean;
+    oAuthLogout: () => void;
+    onRenderTMBCheck: () => void;
+};
+
+/**
+ * useTMB - hooks to help with TMB function such getting the active sessions and tokens
+ * @returns {UseOAuthReturn}
+ */
+const useTMB = (options: { showErrorModal?: () => void } = {}): UseTMBReturn => {
+    const { showErrorModal } = options;
+    const origin = window.location.origin;
+    const isProduction = process.env.VITE_NODE_ENV === 'production' || origin === URLConstants.derivP2pProduction;
+    const isStaging = process.env.VITE_NODE_ENV === 'staging' || origin === URLConstants.derivP2pStaging;
+    const isOAuth2Enabled = isProduction || isStaging;
+
+    const { error, isAuthorized, isAuthorizing, logout } = useAuthData();
+    const isEndpointPage = getCurrentRoute() === 'endpoint';
+    const isCallbackPage = getCurrentRoute() === 'callback';
+    const isRedirectPage = getCurrentRoute() === 'redirect';
+    const oauthUrl = getOauthUrl();
+    const authTokenLocalStorage = localStorage.getItem('authToken');
+
+    const WSLogoutAndRedirect = async () => {
+        try {
+            await logout();
+        } catch (error) {
+            // eslint-disable-next-line no-console
+            console.error('Failed to logout', error);
+        }
+        removeCookies('affiliate_token', 'affiliate_tracking', 'utm_data', 'onfido_token', 'gclid');
+        redirectToAuth();
+    };
+
+    const handleLogout = async () => {
+        try {
+            await OAuth2Logout({
+                postLogoutRedirectUri: window.location.origin,
+                redirectCallbackUri: window.location.origin,
+                WSLogoutAndRedirect,
+            });
+        } catch (error) {
+            // eslint-disable-next-line no-console
+            console.error('Failed to handle logout', error);
+            showErrorModal?.();
+        }
+    };
+
+    const redirectToAuth = async () => {
+        if (isOAuth2Enabled) {
+            try {
+                await requestOidcAuthentication({
+                    redirectCallbackUri: window.location.origin,
+                });
+            } catch (error) {
+                // eslint-disable-next-line no-console
+                console.error('Failed to redirect to auth', error);
+                showErrorModal?.();
+            }
+        } else {
+            window.open(oauthUrl, '_self');
+        }
+    };
+
+    const getActiveSessions = async () => {
+        try {
+            const data = await requestSessionActive();
+
+            return data;
+        } catch (error) {
+            // eslint-disable-next-line no-console
+            console.error('Failed to get active sessions', error);
+            showErrorModal?.();
+        }
+    };
+
+    const onRenderTMBCheck = useCallback(async () => {
+        const activeSessions = await getActiveSessions();
+        localStorage.setItem('clientAccounts', JSON.stringify(activeSessions?.tokens));
+
+        if (!isEndpointPage && !isCallbackPage) {
+            // NOTE: we only do single logout using logged_state cookie checks only in Safari
+            // because front channels do not work in Safari, front channels (front-channel.html) would already help us automatically log out
+            const shouldSingleLogoutWithLoggedState = !activeSessions?.active;
+            if ((shouldSingleLogoutWithLoggedState && isOAuth2Enabled) || error?.code === 'InvalidToken') {
+                try {
+                    await handleLogout();
+                } catch (error) {
+                    // eslint-disable-next-line no-console
+                    console.error('Failed to handle logout', error);
+                    showErrorModal?.();
+                }
+            } else if (isRedirectPage) {
+                const params = new URLSearchParams(location.search);
+                const from = params.get('from');
+                const authTokenCookie = Cookies.get('authtoken');
+
+                if (from === 'tradershub' && authTokenCookie) {
+                    const cleanedAuthToken = decodeURIComponent(authTokenCookie).replace(/^"|"$/g, '');
+                    localStorage.setItem('authToken', cleanedAuthToken);
+                    Cookies.remove('authtoken');
+                    window.location.href = window.location.origin;
+                }
+            } else if (!isAuthorized && !isAuthorizing && !authTokenLocalStorage) {
+                await redirectToAuth();
+            }
+        }
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [
+        error?.code,
+        isEndpointPage,
+        isCallbackPage,
+        isOAuth2Enabled,
+        isRedirectPage,
+        isAuthorized,
+        isAuthorizing,
+        authTokenLocalStorage,
+    ]);
+
+    return { isOAuth2Enabled, oAuthLogout: handleLogout, onRenderTMBCheck };
+};
+
+export default useTMB;


Reviews:
user: login: shahzaib-deriv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T03:31:53Z
user: login: adrienne-deriv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T06:59:31Z
user: login: shahzaib-deriv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T07:01:09Z
user: login: shahzaib-deriv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T07:02:33Z
user: login: adrienne-deriv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T07:07:07Z
user: login: shahzaib-deriv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T07:14:21Z
user: login: shahzaib-deriv
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T06:57:32Z

Review comments:
diff_hunk: @@ -26,14 +27,21 @@ const App = () => {
     const isProduction = process.env.VITE_NODE_ENV === 'production' || origin === URLConstants.derivP2pProduction;
     const isStaging = process.env.VITE_NODE_ENV === 'staging' || origin === URLConstants.derivP2pStaging;
     const isOAuth2Enabled = isProduction || isStaging;
+    const [isTMBEnabled] = useGrowthbookGetFeatureValue({
+        featureFlag: 'tmb_enabled',
+    });
 
     initTrackJS();
     initDerivAnalytics();
     initDatadog();
 
     useEffect(() => {
-        onRenderAuthCheck();
-    }, [onRenderAuthCheck]);
+        if (isTMBEnabled) {
+            onRenderTMBCheck();
+        } else {
+            onRenderAuthCheck();
+        }
+    }, [onRenderAuthCheck, onRenderTMBCheck]);
path: src/App.tsx
user: login: shahzaib-deriv
body: ```suggestion
    }, [onRenderAuthCheck, onRenderTMBCheck, isTMBEnabled]);
```
created_at: 2025-05-07T03:31:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+import { useCallback } from 'react';
+import Cookies from 'js-cookie';
+import { getOauthUrl } from '@/constants';
+import { getCurrentRoute, removeCookies } from '@/utils';
+import { useAuthData } from '@deriv-com/api-hooks';
+import { OAuth2Logout, requestOidcAuthentication, requestSessionActive } from '@deriv-com/auth-client';
+import { URLConstants } from '@deriv-com/utils';
+
+type UseTMBReturn = {
+    isOAuth2Enabled: boolean;
+    oAuthLogout: () => void;
+    onRenderTMBCheck: () => void;
+};
+
+/**
+ * useTMB - hooks to help with TMB function such getting the active sessions and tokens
+ * @returns {UseOAuthReturn}
+ */
+const useTMB = (options: { showErrorModal?: () => void } = {}): UseTMBReturn => {
+    const { showErrorModal } = options;
+    const origin = window.location.origin;
+    const isProduction = process.env.VITE_NODE_ENV === 'production' || origin === URLConstants.derivP2pProduction;
+    const isStaging = process.env.VITE_NODE_ENV === 'staging' || origin === URLConstants.derivP2pStaging;
+    const isOAuth2Enabled = isProduction || isStaging;
+
+    const { logout } = useAuthData();
+    const { error, isAuthorized, isAuthorizing } = useAuthData();
path: src/hooks/custom-hooks/useTMB.ts
user: login: adrienne-deriv
body: ```suggestion
    const { logout, error, isAuthorized, isAuthorizing } = useAuthData();
```
created_at: 2025-05-08T06:59:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,13 +25,25 @@ const CallbackPage = () => {
     return (
         <Callback
             onSignInSuccess={tokens => {
-                const groupedTokens = groupTokens(tokens);
-                localStorage.setItem('clientAccounts', JSON.stringify(groupedTokens));
-
-                const selectedAuthToken =
-                    groupedTokens.find(item => item.cur === 'USD' && item.acct?.includes('CR'))?.token || tokens.token1;
-
-                localStorage.setItem('authToken', selectedAuthToken);
+                const isTMBEnabled = localStorage.getItem('is_tmb_enabled');
path: src/routes/CallbackPage.tsx
user: login: shahzaib-deriv
body: In my understanding, we don't need to go to `/callback` page. 
In this case, we don't need to use this `Callback` component from `auth-client` library & this `onSignInSuccess` method is not required. 


We'll be getting the tokens from `sessions/active` endpoint, which are returned by `requestSessionActive` from auth-client library
created_at: 2025-05-08T07:01:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+import { useCallback } from 'react';
+import Cookies from 'js-cookie';
+import { getOauthUrl } from '@/constants';
+import { getCurrentRoute, removeCookies } from '@/utils';
+import { useAuthData } from '@deriv-com/api-hooks';
+import { OAuth2Logout, requestOidcAuthentication, requestSessionActive } from '@deriv-com/auth-client';
+import { URLConstants } from '@deriv-com/utils';
+
+type UseTMBReturn = {
+    isOAuth2Enabled: boolean;
+    oAuthLogout: () => void;
+    onRenderTMBCheck: () => void;
+};
+
+/**
+ * useTMB - hooks to help with TMB function such getting the active sessions and tokens
+ * @returns {UseOAuthReturn}
+ */
+const useTMB = (options: { showErrorModal?: () => void } = {}): UseTMBReturn => {
+    const { showErrorModal } = options;
+    const origin = window.location.origin;
+    const isProduction = process.env.VITE_NODE_ENV === 'production' || origin === URLConstants.derivP2pProduction;
+    const isStaging = process.env.VITE_NODE_ENV === 'staging' || origin === URLConstants.derivP2pStaging;
+    const isOAuth2Enabled = isProduction || isStaging;
+
+    const { logout } = useAuthData();
+    const { error, isAuthorized, isAuthorizing } = useAuthData();
+    const isEndpointPage = getCurrentRoute() === 'endpoint';
+    const isCallbackPage = getCurrentRoute() === 'callback';
+    const isRedirectPage = getCurrentRoute() === 'redirect';
+    const oauthUrl = getOauthUrl();
+    const authTokenLocalStorage = localStorage.getItem('authToken');
+
+    const WSLogoutAndRedirect = async () => {
+        try {
+            await logout();
+        } catch (error) {
+            // eslint-disable-next-line no-console
+            console.error('Failed to logout', error);
+        }
+        removeCookies('affiliate_token', 'affiliate_tracking', 'utm_data', 'onfido_token', 'gclid');
+        redirectToAuth();
+    };
+
+    const handleLogout = async () => {
+        try {
+            await OAuth2Logout({
+                postLogoutRedirectUri: window.location.origin,
+                redirectCallbackUri: `${window.location.origin}/callback`,
+                WSLogoutAndRedirect,
+            });
+        } catch (error) {
+            // eslint-disable-next-line no-console
+            console.error('Failed to handle logout', error);
+            showErrorModal?.();
+        }
+    };
+
+    const redirectToAuth = async () => {
+        if (isOAuth2Enabled) {
+            try {
+                await requestOidcAuthentication({
+                    redirectCallbackUri: `${window.location.origin}/callback`,
+                });
+            } catch (error) {
+                // eslint-disable-next-line no-console
+                console.error('Failed to redirect to auth', error);
+                showErrorModal?.();
+            }
+        } else {
+            window.open(oauthUrl, '_self');
+        }
+    };
+
+    const getActiveSessions = async () => {
+        try {
+            const data = await requestSessionActive();
path: src/hooks/custom-hooks/useTMB.ts
user: login: shahzaib-deriv
body: this data will have `array of objects` containing token pairs. 

We don't need to utilize `/callback` page with the callback function. 
created_at: 2025-05-08T07:02:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,13 +25,25 @@ const CallbackPage = () => {
     return (
         <Callback
             onSignInSuccess={tokens => {
-                const groupedTokens = groupTokens(tokens);
-                localStorage.setItem('clientAccounts', JSON.stringify(groupedTokens));
-
-                const selectedAuthToken =
-                    groupedTokens.find(item => item.cur === 'USD' && item.acct?.includes('CR'))?.token || tokens.token1;
-
-                localStorage.setItem('authToken', selectedAuthToken);
+                const isTMBEnabled = localStorage.getItem('is_tmb_enabled');
path: src/routes/CallbackPage.tsx
user: login: adrienne-deriv
body: we will still need to go to the callback page for first time login, but we dont need anymore changes here in the callback page, its only in the SSO logic
created_at: 2025-05-08T07:07:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,13 +25,25 @@ const CallbackPage = () => {
     return (
         <Callback
             onSignInSuccess={tokens => {
-                const groupedTokens = groupTokens(tokens);
-                localStorage.setItem('clientAccounts', JSON.stringify(groupedTokens));
-
-                const selectedAuthToken =
-                    groupedTokens.find(item => item.cur === 'USD' && item.acct?.includes('CR'))?.token || tokens.token1;
-
-                localStorage.setItem('authToken', selectedAuthToken);
+                const isTMBEnabled = localStorage.getItem('is_tmb_enabled');
path: src/routes/CallbackPage.tsx
user: login: shahzaib-deriv
body: correct @adrienne-deriv 
the logic can stay intact for backward compatibly, however when the flag for TMB is enabled, we don't want the user to land on /callback page as we don't need the token exchanges. `/session/active` provides everything we need.
created_at: 2025-05-08T07:14:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-08T09:49:54Z
body: **Preview Link**: https://tmb-integration.p2p-standalone.pages.dev
| Name | Result |
| :--- | :------ |
| **Build status**  | Completed ✅ |
| **Preview URL**  | [Visit Preview](https://tmb-integration.p2p-standalone.pages.dev) |
| **Action URL**  | [Visit Action](https://github.com/deriv-com/p2p/actions/runs/14903618689) |
<!-- Sticky Pull Request CommentCloudflare Pages Preview Comment -->
author_association: NONE