Skip to content

Commit

Permalink
Merge branch 'master' into WALL-5147-Gold-MT5-account-creation-flow-o…
Browse files Browse the repository at this point in the history
…n-Wallets
  • Loading branch information
heorhi-deriv authored Nov 21, 2024
2 parents 7d99698 + fc71941 commit 2bca5aa
Show file tree
Hide file tree
Showing 39 changed files with 387 additions and 670 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ type PageOverlayWrapperProps = {
*/
const PageOverlayWrapper = observer(({ routes, subroutes }: PageOverlayWrapperProps) => {
const history = useHistory();
const { client, common } = useStore();
const { client, common, ui } = useStore();
const { logout } = client;
const { is_from_derivgo } = common;
const { setIsForcedToExitPnv } = ui;
const { isDesktop } = useDevice();

const passkeysMenuCloseActionEventTrack = React.useCallback(() => {
Expand Down Expand Up @@ -54,6 +55,11 @@ const PageOverlayWrapper = observer(({ routes, subroutes }: PageOverlayWrapperPr
const selected_route = getSelectedRoute({ routes: subroutes, pathname: location.pathname });

const onClickLogout = async () => {
if (window.location.pathname.startsWith(shared_routes.phone_verification)) {
setIsForcedToExitPnv(true);
// Add a small delay to ensure state is updated before navigation because adding await doesn't work here
await new Promise(resolve => setTimeout(resolve, 0));
}
history.push(shared_routes.traders_hub);
await logout();
};
Expand Down
6 changes: 5 additions & 1 deletion packages/api-v2/src/APIProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,11 @@ const APIProvider = ({ children }: PropsWithChildren<TAPIProviderProps>) => {
if (reconnect) {
connectionRef.current = initializeConnection(
() => {
reconnectTimerId = setTimeout(() => setReconnect(true), 500);
reconnectTimerId = setTimeout(() => {
if (isMounted.current) {
setReconnect(true);
}
}, 500);
},
() => {
if (!connectionRef.current) {
Expand Down
18 changes: 14 additions & 4 deletions packages/api/src/hooks/useRemoteConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { ObjectUtils } from '@deriv-com/utils';
import initData from '../remote_config.json';

Expand All @@ -17,22 +17,32 @@ const remoteConfigQuery = async function () {

function useRemoteConfig(enabled = false) {
const [data, setData] = useState(initData);
const isMounted = useRef(false);

useEffect(() => {
enabled &&
isMounted.current = true;

return () => {
isMounted.current = false;
};
}, []);

useEffect(() => {
if (enabled) {
remoteConfigQuery()
.then(async res => {
const resHash = await ObjectUtils.hashObject(res);
const dataHash = await ObjectUtils.hashObject(data);
if (resHash !== dataHash) {
if (resHash !== dataHash && isMounted.current) {
setData(res);
}
})
.catch(error => {
// eslint-disable-next-line no-console
console.log('Remote Config error: ', error);
});
}, [enabled]);
}
}, [enabled, data]);

return { data };
}
Expand Down
1 change: 1 addition & 0 deletions packages/appstore/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const default_plugins = [
new Dotenv(),
new DefinePlugin({
'process.env.TRUSTPILOT_API_KEY': JSON.stringify(process.env.TRUSTPILOT_API_KEY),
'process.env.REMOTE_CONFIG_URL': JSON.stringify(process.env.REMOTE_CONFIG_URL),
}),
new IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ }),
new CircularDependencyPlugin({ exclude: /node_modules/, failOnError: true }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const AccountSwitcher = observer(({ history, is_mobile, is_visible }) => {
toggleSetCurrencyModal,
should_show_real_accounts_list,
setShouldShowCooldownModal,
setIsForcedToExitPnv,
} = ui;
const [active_tab_index, setActiveTabIndex] = React.useState(!is_virtual || should_show_real_accounts_list ? 0 : 1);
const [is_deriv_demo_visible, setDerivDemoVisible] = React.useState(true);
Expand Down Expand Up @@ -99,6 +100,9 @@ const AccountSwitcher = observer(({ history, is_mobile, is_visible }) => {
await logoutClient();
history.push(routes.traders_hub);
} else {
if (window.location.pathname.startsWith(routes.phone_verification)) {
await setIsForcedToExitPnv(true);
}
history.push(routes.traders_hub);
await logoutClient();
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/Stores/ui-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ export default class UIStore extends BaseStore {
this.should_show_phone_number_otp = should_show_phone_number_otp;
}

setIsForcedToExitPnv(is_forced_to_exit_pnv) {
async setIsForcedToExitPnv(is_forced_to_exit_pnv) {
this.is_forced_to_exit_pnv = is_forced_to_exit_pnv;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React from 'react';

import { mockStore, StoreProvider } from '@deriv/stores';
import { renderHook } from '@testing-library/react-hooks';

import usePhoneNumberVerificationSessionTimer from '../usePhoneNumberVerificationSessionTimer';
import useSettings from '../useSettings';

jest.mock('@deriv/shared', () => ({
...jest.requireActual('@deriv/shared'),
Expand All @@ -12,34 +10,31 @@ jest.mock('@deriv/shared', () => ({
},
}));

const mock_store = mockStore({
client: {
account_settings: {
phone_number_verification: {
session_timestamp: undefined,
},
},
},
});
jest.mock('../useSettings');

describe('usePhoneNumberVerificationSetTimer', () => {
const wrapper = ({ children }: { children: JSX.Element }) => (
<StoreProvider store={mock_store}>{children}</StoreProvider>
);
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers({ legacyFakeTimers: true });
(useSettings as jest.Mock).mockReturnValue({
data: {
phone_number_verification: { session_timestamp: undefined },
},
});
});

afterEach(() => {
jest.useRealTimers();
});

it('should set should_show_session_timeout_modal to true if session_timestap is same with WS response time', async () => {
if (mock_store.client.account_settings.phone_number_verification)
mock_store.client.account_settings.phone_number_verification.session_timestamp = 1620000000;
(useSettings as jest.Mock).mockReturnValue({
data: {
phone_number_verification: { session_timestamp: 1620000000 },
},
});

const { result, waitForNextUpdate } = renderHook(() => usePhoneNumberVerificationSessionTimer(), { wrapper });
const { result, waitForNextUpdate } = renderHook(() => usePhoneNumberVerificationSessionTimer());

expect(result.current.should_show_session_timeout_modal).toBe(false);

Expand All @@ -49,10 +44,13 @@ describe('usePhoneNumberVerificationSetTimer', () => {
});

it('should set should_show_session_timeout_modal to false if session_timestap more than WS response time', async () => {
if (mock_store.client.account_settings.phone_number_verification)
mock_store.client.account_settings.phone_number_verification.session_timestamp = 1620000003;
(useSettings as jest.Mock).mockReturnValue({
data: {
phone_number_verification: { session_timestamp: 1620000003 },
},
});

const { result, waitForNextUpdate } = renderHook(() => usePhoneNumberVerificationSessionTimer(), { wrapper });
const { result, waitForNextUpdate } = renderHook(() => usePhoneNumberVerificationSessionTimer());

expect(result.current.should_show_session_timeout_modal).toBe(false);

Expand All @@ -62,21 +60,27 @@ describe('usePhoneNumberVerificationSetTimer', () => {
});

it('should set formatted_time value to be 00:00 if the session_timestamp has no difference', async () => {
if (mock_store.client.account_settings.phone_number_verification)
mock_store.client.account_settings.phone_number_verification.session_timestamp = 1620000000;
(useSettings as jest.Mock).mockReturnValue({
data: {
phone_number_verification: { session_timestamp: 1620000000 },
},
});

const { result, waitForNextUpdate } = renderHook(() => usePhoneNumberVerificationSessionTimer(), { wrapper });
const { result, waitForNextUpdate } = renderHook(() => usePhoneNumberVerificationSessionTimer());

await waitForNextUpdate();

expect(result.current.formatted_time).toBe('00:00');
});

it('should set formatted_time value if the session_timestamp has any value', async () => {
if (mock_store.client.account_settings.phone_number_verification)
mock_store.client.account_settings.phone_number_verification.session_timestamp = 1620000003;
(useSettings as jest.Mock).mockReturnValue({
data: {
phone_number_verification: { session_timestamp: 1620000003 },
},
});

const { result, waitForNextUpdate } = renderHook(() => usePhoneNumberVerificationSessionTimer(), { wrapper });
const { result, waitForNextUpdate } = renderHook(() => usePhoneNumberVerificationSessionTimer());

await waitForNextUpdate();

Expand Down
10 changes: 7 additions & 3 deletions packages/hooks/src/useGrowthbookGetFeatureValue.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { Analytics } from '@deriv-com/analytics';
import { getFeatureFlag } from '@deriv/utils';
import { useIsMounted } from 'usehooks-ts';

interface UseGrowthbookGetFeatureValueArgs<T> {
featureFlag: string;
Expand All @@ -14,6 +15,7 @@ const useGrowthbookGetFeatureValue = <T extends string | boolean>({
const resolvedDefaultValue: T = defaultValue !== undefined ? defaultValue : (false as T);
const [featureFlagValue, setFeatureFlagValue] = useState<boolean>(false);
const [isGBLoaded, setIsGBLoaded] = useState(false);
const isMounted = useIsMounted();

// Required for debugging Growthbook, this will be removed after this is added in the Analytics directly.
if (typeof window !== 'undefined') {
Expand All @@ -23,12 +25,14 @@ const useGrowthbookGetFeatureValue = <T extends string | boolean>({
useEffect(() => {
const fetchFeatureFlag = async () => {
const is_enabled = await getFeatureFlag(featureFlag, resolvedDefaultValue);
setFeatureFlagValue(is_enabled);
setIsGBLoaded(true);
if (isMounted()) {
setFeatureFlagValue(is_enabled);
setIsGBLoaded(true);
}
};

fetchFeatureFlag();
}, []);
}, [featureFlag, resolvedDefaultValue, isMounted]);

return [featureFlagValue, isGBLoaded];
};
Expand Down
18 changes: 11 additions & 7 deletions packages/hooks/src/usePhoneNumberVerificationSessionTimer.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { useCallback, useEffect, useState } from 'react';
import { useIsMounted, WS } from '@deriv/shared';
import { useStore } from '@deriv/stores';
import dayjs from 'dayjs';
import useSettings from './useSettings';

const usePhoneNumberVerificationSessionTimer = () => {
const [session_timer, setSessionTimer] = useState<number | undefined>();
const [formatted_time, setFormattedTime] = useState('00:00');
const [should_show_session_timeout_modal, setShouldShowSessionTimeoutModal] = useState(false);
const { client } = useStore();
const { account_settings } = client;
const { phone_number_verification } = account_settings;
const { data: account_settings } = useSettings();
const isMounted = useIsMounted();

const formatTime = useCallback((totalSeconds: number) => {
Expand All @@ -29,9 +27,13 @@ const usePhoneNumberVerificationSessionTimer = () => {
WS.send({ time: 1 }).then((response: { error?: Error; time: number }) => {
if (response.error) return;

if (response.time && phone_number_verification?.session_timestamp) {
//@ts-expect-error will remove this once GetSettings is updated
if (response.time && account_settings?.phone_number_verification?.session_timestamp) {
// request_in_miliseconds is to convert session_timestamp from get_settings * it with 1000 to make it into miliseconds and convert the time using dayjs package
const request_in_milliseconds = dayjs(phone_number_verification?.session_timestamp * 1000);
const request_in_milliseconds = dayjs(
//@ts-expect-error will remove this once GetSettings is updated
account_settings?.phone_number_verification?.session_timestamp * 1000
);
// next_request is to compare request_in_miliseconds with server's response time
const next_request = Math.round(request_in_milliseconds.diff(response.time * 1000) / 1000);

Expand All @@ -42,7 +44,8 @@ const usePhoneNumberVerificationSessionTimer = () => {
}
}
});
}, [phone_number_verification?.session_timestamp]);
//@ts-expect-error will remove this once GetSettings is updated
}, [account_settings?.phone_number_verification?.session_timestamp]);

useEffect(() => {
let countdown: ReturnType<typeof setInterval>;
Expand All @@ -63,6 +66,7 @@ const usePhoneNumberVerificationSessionTimer = () => {
return {
formatted_time,
should_show_session_timeout_modal,
setSessionTimer,
setShouldShowSessionTimeoutModal,
};
};
Expand Down
20 changes: 14 additions & 6 deletions packages/trader/src/AppV2/Components/Guide/guide.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import React from 'react';
import { Button, Text } from '@deriv-com/quill-ui';

import { LabelPairedPresentationScreenSmRegularIcon } from '@deriv/quill-icons';
import { Localize } from '@deriv/translations';
import { observer, useStore } from '@deriv/stores';
import { useTraderStore } from 'Stores/useTraderStores';
import { Localize } from '@deriv/translations';
import { Button, Text } from '@deriv-com/quill-ui';

import useContractsForCompany from 'AppV2/Hooks/useContractsForCompany';
import { AVAILABLE_CONTRACTS, CONTRACT_LIST } from 'AppV2/Utils/trade-types-utils';
import { useTraderStore } from 'Stores/useTraderStores';

import { sendOpenGuideToAnalytics } from '../../../Analytics';

import GuideDefinitionModal from './guide-definition-modal';
import GuideDescriptionModal from './guide-description-modal';
import useContractsForCompany from 'AppV2/Hooks/useContractsForCompany';
import { sendOpenGuideToAnalytics } from '../../../Analytics';

type TGuide = {
has_label?: boolean;
Expand Down Expand Up @@ -81,7 +85,11 @@ const Guide = observer(({ has_label, show_guide_for_selected_contract }: TGuide)
contract_list={ordered_contract_list}
is_dark_mode_on={is_dark_mode_on}
is_open={is_description_opened}
onChipSelect={onChipSelect}
onChipSelect={(id: string) => {
const selected_trade_type = ordered_contract_list.find(item => item.id === id);
sendOpenGuideToAnalytics(selected_trade_type?.for?.[0] ?? '', 'trade_type_page');
onChipSelect(id);
}}
onClose={onClose}
onTermClick={setSelectedTerm}
selected_contract_type={selected_contract_type}
Expand Down
6 changes: 4 additions & 2 deletions packages/trader/src/AppV2/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import type { TWebSocket } from 'Types';
import initStore from 'App/init-store';
import type { TCoreStores } from '@deriv/stores/types';
import { routes } from '@deriv/shared';
import ModulesProvider from 'Stores/Providers/modules-providers';
import TraderProviders from '../trader-providers';
import { ReportsStoreProvider } from '../../../reports/src/Stores/useReportsStores';
Expand Down Expand Up @@ -41,10 +42,11 @@ const App = ({ passthrough }: Apptypes) => {
}, []);

React.useEffect(() => {
if (!window.location.pathname.startsWith('/contract')) {
if (window.location.pathname === routes.trade) {
sendDtraderV2OpenToAnalytics();
}
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [window.location.pathname]);

return (
<TraderProviders store={root_store}>
Expand Down
Loading

0 comments on commit 2bca5aa

Please sign in to comment.