Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: oidc setup login/logout #17746

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 271 additions & 40 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import React from 'react';

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import TradingHubLogout from '../tradinghub-logout';

jest.mock('@deriv/hooks', () => ({
...jest.requireActual('@deriv/hooks'),
useOauth2: jest.fn(({ handleLogout }) => ({
isOAuth2Enabled: true,
oAuthLogout: jest.fn(() => handleLogout && handleLogout()),
})),
}));
describe('TradingHubLogout', () => {
const mock_props: React.ComponentProps<typeof TradingHubLogout> = {
handleOnLogout: jest.fn(),
Expand All @@ -13,10 +22,10 @@ describe('TradingHubLogout', () => {
expect(screen.getByText('Log out')).toBeInTheDocument();
});

it('should invoke handleOnLogout when logout tab is clicked', () => {
it('should invoke handleOnLogout when logout tab is clicked', async () => {
render(<TradingHubLogout {...mock_props} />);
const el_tab = screen.getByTestId('dt_logout_tab');
userEvent.click(el_tab);
expect(mock_props.handleOnLogout).toBeCalledTimes(1);
await userEvent.click(el_tab);
expect(mock_props.handleOnLogout).toHaveBeenCalledTimes(1);
});
});
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"@deriv-com/quill-tokens": "2.0.4",
"@deriv-com/quill-ui": "1.24.2",
"@deriv-com/translations": "1.3.9",
"@deriv-com/auth-client": "1.3.3",
"@deriv-com/ui": "1.36.4",
"@deriv-com/utils": "^0.0.40",
"@deriv/account": "^1.0.0",
Expand Down
44 changes: 34 additions & 10 deletions packages/core/src/App/AppContent.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
import React from 'react';

import { useRemoteConfig } from '@deriv/api';
import { useDevice } from '@deriv-com/ui';
import {
useGrowthbookGetFeatureValue,
useGrowthbookIsOn,
useLiveChat,
useOauth2,
useSilentLoginAndLogout,
} from '@deriv/hooks';
import { WALLETS_UNSUPPORTED_LANGUAGES } from '@deriv/shared';
import { observer, useStore } from '@deriv/stores';
import { ThemeProvider } from '@deriv-com/quill-ui';
import { useTranslations } from '@deriv-com/translations';
import { useDevice } from '@deriv-com/ui';
import { browserSupportsWebAuthn } from '@simplewebauthn/browser';

import P2PIFrame from 'Modules/P2PIFrame';
import SmartTraderIFrame from 'Modules/SmartTraderIFrame';

import initDatadog from '../Utils/Datadog';
import initHotjar from '../Utils/Hotjar';

import ErrorBoundary from './Components/Elements/Errors/error-boundary.jsx';
import LandscapeBlocker from './Components/Elements/LandscapeBlocker';
import AppToastMessages from './Containers/app-toast-messages.jsx';
import AppContents from './Containers/Layout/app-contents.jsx';
import Footer from './Containers/Layout/footer.jsx';
import Header from './Containers/Layout/header';
import AppModals from './Containers/Modals';
import Routes from './Containers/Routes/routes.jsx';
import Devtools from './Devtools';
import LandscapeBlocker from './Components/Elements/LandscapeBlocker';
import initDatadog from '../Utils/Datadog';
import { ThemeProvider } from '@deriv-com/quill-ui';
import { useGrowthbookGetFeatureValue, useGrowthbookIsOn, useLiveChat, useOauth2 } from '@deriv/hooks';
import { useTranslations } from '@deriv-com/translations';
import initHotjar from '../Utils/Hotjar';
import { WALLETS_UNSUPPORTED_LANGUAGES } from '@deriv/shared';

const AppContent: React.FC<{ passthrough: unknown }> = observer(({ passthrough }) => {
const store = useStore();
Expand All @@ -31,6 +41,7 @@ const AppContent: React.FC<{ passthrough: unknown }> = observer(({ passthrough }
landing_company_shortcode,
currency,
residence,
logout,
email,
setIsPasskeySupported,
account_settings,
Expand All @@ -44,7 +55,18 @@ const AppContent: React.FC<{ passthrough: unknown }> = observer(({ passthrough }
const { isMobile } = useDevice();
const { switchLanguage } = useTranslations();

const { isOAuth2Enabled } = useOauth2();
const { isOAuth2Enabled, oAuthLogout } = useOauth2({
handleLogout: async () => {
await logout();
},
});

useSilentLoginAndLogout({
thisyahlen-deriv marked this conversation as resolved.
Show resolved Hide resolved
is_client_store_initialized,
isOAuth2Enabled,
oAuthLogout,
});

const [isWebPasskeysFFEnabled, isGBLoaded] = useGrowthbookIsOn({
featureFlag: 'web_passkeys',
});
Expand Down Expand Up @@ -134,10 +156,12 @@ const AppContent: React.FC<{ passthrough: unknown }> = observer(({ passthrough }
is_wallets_unsupported_language,
]);

const isCallBackPage = window.location.pathname.includes('callback');
thisyahlen-deriv marked this conversation as resolved.
Show resolved Hide resolved

return (
<ThemeProvider theme={is_dark_mode_on ? 'dark' : 'light'}>
<LandscapeBlocker />
<Header />
{!isCallBackPage && <Header />}
<ErrorBoundary root_store={store}>
<AppContents>
{/* TODO: [trader-remove-client-base] */}
Expand Down
39 changes: 25 additions & 14 deletions packages/core/src/App/Components/Layout/Header/login-button.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import PropTypes from 'prop-types';
import React from 'react';
import PropTypes from 'prop-types';

import { Button } from '@deriv/components';
import { useOauth2 } from '@deriv/hooks';
import { redirectToLogin } from '@deriv/shared';
import { getLanguage, localize } from '@deriv/translations';
import { requestOidcAuthentication } from '@deriv-com/auth-client';

const LoginButton = ({ className }) => (
<Button
id='dt_login_button'
className={className}
has_effect
text={localize('Log in')}
onClick={() => {
window.LiveChatWidget?.call('hide');
redirectToLogin(false, getLanguage());
}}
tertiary
/>
);
const LoginButton = ({ className }) => {
const { isOAuth2Enabled } = useOauth2();
return (
<Button
id='dt_login_button'
className={className}
has_effect
text={localize('Log in')}
onClick={async () => {
if (isOAuth2Enabled) {
await requestOidcAuthentication({
redirectCallbackUri: `${window.location.origin}/callback`,
});
}
window.LiveChatWidget?.call('hide');
redirectToLogin(false, getLanguage());
}}
tertiary
/>
);
};

LoginButton.propTypes = {
className: PropTypes.string,
Expand Down
17 changes: 14 additions & 3 deletions packages/core/src/App/Constants/routes-config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import React from 'react';
import { Redirect as RouterRedirect } from 'react-router-dom';
import { makeLazyLoader, routes, moduleLoader } from '@deriv/shared';

import { Loading } from '@deriv/components';
import { makeLazyLoader, moduleLoader, routes } from '@deriv/shared';
import { localize } from '@deriv/translations';

import Redirect from 'App/Containers/Redirect';
import RootComponent from 'App/Containers/RootComponent';
import Endpoint from 'Modules/Endpoint';

const CFDCompareAccounts = React.lazy(() =>
import(/* webpackChunkName: "cfd-compare-accounts" */ '@deriv/cfd/src/Containers/cfd-compare-accounts')
import CallbackPage from '../../Modules/Callback/CallbackPage.tsx';

const CFDCompareAccounts = React.lazy(
() => import(/* webpackChunkName: "cfd-compare-accounts" */ '@deriv/cfd/src/Containers/cfd-compare-accounts')
);

// Error Routes
Expand Down Expand Up @@ -356,6 +360,12 @@ const getModules = () => {
is_authenticated: false,
getTitle: () => localize("Trader's Hub"),
},
{
path: routes.callback_page,
component: CallbackPage,
is_authenticated: false,
getTitle: () => localize('Callback'),
thisyahlen-deriv marked this conversation as resolved.
Show resolved Hide resolved
},
];

return modules;
Expand All @@ -372,6 +382,7 @@ const initRoutesConfig = () => [
{ path: routes.index, component: RouterRedirect, getTitle: () => '', to: routes.traders_hub },
{ path: routes.endpoint, component: Endpoint, getTitle: () => 'Endpoint' }, // doesn't need localization as it's for internal use
{ path: routes.redirect, component: Redirect, getTitle: () => localize('Redirect') },
{ path: routes.callback_page, component: CallbackPage, getTitle: () => localize('Callback') },
thisyahlen-deriv marked this conversation as resolved.
Show resolved Hide resolved
{
path: routes.complaints_policy,
component: lazyLoadComplaintsPolicy(),
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/Modules/Callback/CallbackPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { withRouter } from 'react-router-dom';

import { ButtonLink } from '@deriv/components';
import { routes } from '@deriv/shared';
import { Callback } from '@deriv-com/auth-client';

const CallbackPage = () => {
return (
<Callback
onSignInSuccess={tokens => {
localStorage.setItem('config.tokens', JSON.stringify(tokens));
localStorage.setItem('config.account1', tokens.token1);
localStorage.setItem('active_loginid', tokens.acct1);
thisyahlen-deriv marked this conversation as resolved.
Show resolved Hide resolved

window.location.href = routes.traders_hub;
}}
renderReturnButton={() => {
return (
<ButtonLink to={routes.traders_hub}>
<p>{"Return to Trader's Hub"}</p>
thisyahlen-deriv marked this conversation as resolved.
Show resolved Hide resolved
</ButtonLink>
);
}}
/>
);
};

export default withRouter(CallbackPage);
3 changes: 3 additions & 0 deletions packages/core/src/Modules/Callback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import CallbackPage from './CallbackPage';

export default CallbackPage;
2 changes: 2 additions & 0 deletions packages/core/src/Services/logout.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const doLogout = response => {
localStorage.removeItem('closed_toast_notifications');
localStorage.removeItem('is_wallet_migration_modal_closed');
localStorage.removeItem('active_wallet_loginid');
localStorage.removeItem('config.account1');
localStorage.removeItem('config.tokens');
thisyahlen-deriv marked this conversation as resolved.
Show resolved Hide resolved
localStorage.removeItem('verification_code.system_email_change');
localStorage.removeItem('verification_code.request_email');
localStorage.removeItem('new_email.system_email_change');
Expand Down
50 changes: 30 additions & 20 deletions packages/core/src/Stores/client-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,34 @@ import {
deriv_urls,
excludeParamsFromUrlQuery,
filterUrlQuery,
getAppId,
getPropertyValue,
getUrlP2P,
getUrlSmartTrader,
isCryptocurrency,
isDesktopOs,
isMobile,
isEmptyObject,
isLocal,
isMobile,
isProduction,
isStaging,
isTestLink,
isTestDerivApp,
isTestLink,
LocalStore,
redirectToLogin,
removeCookies,
routes,
SessionStore,
setCurrencies,
sortApiData,
State,
toMoment,
sortApiData,
urlForLanguage,
getAppId,
getUrlP2P,
} from '@deriv/shared';
import { getLanguage, getRedirectionLanguage, localize } from '@deriv/translations';
import { getCountry } from '@deriv/utils';
import { Analytics } from '@deriv-com/analytics';
import { URLConstants } from '@deriv-com/utils';
import { getCountry } from '@deriv/utils';

import { getLanguage, localize, getRedirectionLanguage } from '@deriv/translations';

import { requestLogout, WS } from 'Services';
import BinarySocketGeneral from 'Services/socket-general';
Expand All @@ -44,6 +43,7 @@ import { getAccountTitle, getAvailableAccount, getClientAccountType } from './He
import { setDeviceDataCookie } from './Helpers/device';
import { buildCurrenciesList } from './Modules/Trading/Helpers/currency';
import BaseStore from './base-store';

import BinarySocket from '_common/base/socket_base';
import * as SocketCache from '_common/base/socket_cache';
import { getRegion, isEuCountry, isMultipliersOnly, isOptionsBlocked } from '_common/utility';
Expand Down Expand Up @@ -1526,14 +1526,16 @@ export default class ClientStore extends BaseStore {
const authorize_response = await this.setUserLogin(login_new_user);

if (search) {
if (code_param && action_param) this.setVerificationCode(code_param, action_param);
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
// timeout is needed to get the token (code) from the URL before we hide it from the URL
// and from LiveChat that gets the URL from Window, particularly when initialized via HTML script on mobile
history.replaceState(null, null, window.location.search.replace(/&?code=[^&]*/i, ''));
}, 0);
});
if (window.location.pathname !== routes.callback_page) {
if (code_param && action_param) this.setVerificationCode(code_param, action_param);
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
// timeout is needed to get the token (code) from the URL before we hide it from the URL
// and from LiveChat that gets the URL from Window, particularly when initialized via HTML script on mobile
history.replaceState(null, null, window.location.search.replace(/&?code=[^&]*/i, ''));
}, 0);
});
}
}

this.setDeviceData();
Expand Down Expand Up @@ -1567,7 +1569,7 @@ export default class ClientStore extends BaseStore {
this.root_store.ui.toggleResetEmailModal(true);
}
}
const client = this.accounts[this.loginid];
const client = this.accounts[this.loginid] || { token: localStorage.getItem('config.account1') };
thisyahlen-deriv marked this conversation as resolved.
Show resolved Hide resolved
// If there is an authorize_response, it means it was the first login
if (authorize_response) {
// If this fails, it means the landing company check failed
Expand Down Expand Up @@ -2186,7 +2188,7 @@ export default class ClientStore extends BaseStore {

let is_social_signup_provider = false;

if (search) {
if (search && window.location.pathname !== routes.callback_page) {
let search_params = new URLSearchParams(window.location.search);

search_params.forEach((value, key) => {
Expand All @@ -2212,8 +2214,9 @@ export default class ClientStore extends BaseStore {
}

const is_client_logging_in = login_new_user ? login_new_user.token1 : obj_params.token1;
const is_callback_page_client_logging_in = localStorage.getItem('config.account1') || '';

if (is_client_logging_in) {
if (is_client_logging_in || is_callback_page_client_logging_in) {
this.setIsLoggingIn(true);

const redirect_url = sessionStorage.getItem('redirect_url');
Expand All @@ -2233,13 +2236,20 @@ export default class ClientStore extends BaseStore {
SocketCache.clear();
// is_populating_account_list is used for socket general to know not to filter the first-time logins
this.is_populating_account_list = true;
const authorize_response = await BinarySocket.authorize(is_client_logging_in);
const authorize_response = await BinarySocket.authorize(
is_client_logging_in || is_callback_page_client_logging_in
);

if (login_new_user) {
// overwrite obj_params if login is for new virtual account
obj_params = login_new_user;
}

if (localStorage.getItem('config.tokens')) {
const tokens = JSON.parse(localStorage.getItem('config.tokens'));
obj_params = tokens;
}

if (authorize_response.error) {
return authorize_response;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@binary-com/binary-document-uploader": "^2.4.8",
"@deriv-com/analytics": "1.26.2",
"@deriv/api": "^1.0.0",
"@deriv-com/auth-client": "1.0.29",
"@deriv-com/auth-client": "1.3.3",
"@deriv/stores": "^1.0.0",
"@deriv/utils": "^1.0.0",
"@deriv/shared": "^1.0.0",
Expand Down
Loading
Loading