Skip to content

Commit

Permalink
[DTRA]feat: added growthbook feature flag for dtrader-v2 (deriv-com#1…
Browse files Browse the repository at this point in the history
…7160)

* feat: added growthbook feature flag for dtrader-v2

* feat: enable dtrader v2 when feature flag is enabled

* feat: dtrader-v2 added behind feature flag

* fix: implement contract header inside dtrader v2

* fix: fixed failing test cases in contract-details

* refactor: centeralised the logic for dtrader v2 in one place

* fix: failing test case

* fix: addressed review comments and removed loader from app.jsx

* refactor: refactor dtrader-v2 hook

* refactor: small change in useDtraderV2 flag
  • Loading branch information
vinu-deriv authored Oct 28, 2024
1 parent 595fdb0 commit 97d01e2
Show file tree
Hide file tree
Showing 23 changed files with 185 additions and 91 deletions.
8 changes: 4 additions & 4 deletions packages/core/src/App/Components/Routes/binary-routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ import { Loading } from '@deriv/components';
import getRoutesConfig from 'App/Constants/routes-config';
import RouteWithSubRoutes from './route-with-sub-routes.jsx';
import { observer, useStore } from '@deriv/stores';
import { getPositionsV2TabIndexFromURL, isDTraderV2, routes } from '@deriv/shared';
import { getPositionsV2TabIndexFromURL, routes } from '@deriv/shared';
import { useDtraderV2Flag } from '@deriv/hooks';

const BinaryRoutes = observer(props => {
const { ui, gtm } = useStore();
const { promptFn, prompt_when } = ui;
const { pushDataLayer } = gtm;
const location = useLocation();
const is_dtrader_v2 =
isDTraderV2() && (location.pathname.startsWith(routes.trade) || location.pathname.startsWith('/contract/'));
const { dtrader_v2_enabled } = useDtraderV2Flag();

React.useEffect(() => {
pushDataLayer({ event: 'page_load' });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location]);

const getLoader = () => {
if (is_dtrader_v2)
if (dtrader_v2_enabled)
return (
<Loading.DTraderV2
initial_app_loading
Expand Down
7 changes: 1 addition & 6 deletions packages/core/src/App/Constants/routes-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,7 @@ const CFDCompareAccounts = React.lazy(() =>
// Error Routes
const Page404 = React.lazy(() => import(/* webpackChunkName: "404" */ 'Modules/Page404'));

const Trader = React.lazy(() =>
moduleLoader(() => {
// eslint-disable-next-line import/no-unresolved
return import(/* webpackChunkName: "trader" */ '@deriv/trader');
})
);
const Trader = React.lazy(() => import(/* webpackChunkName: "trader" */ '@deriv/trader'));

const Reports = React.lazy(() => {
// eslint-disable-next-line import/no-unresolved
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/App/Containers/Layout/app-contents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import React from 'react';
import { useLocation, withRouter } from 'react-router';
import { Analytics } from '@deriv-com/analytics';
import { ThemedScrollbars } from '@deriv/components';
import { CookieStorage, TRACKING_STATUS_KEY, platforms, routes, WS, isDTraderV2 } from '@deriv/shared';
import { CookieStorage, TRACKING_STATUS_KEY, platforms, routes, WS } from '@deriv/shared';
import { useStore, observer } from '@deriv/stores';
import { useDtraderV2Flag } from '@deriv/hooks';
import CookieBanner from '../../Components/Elements/CookieBanner/cookie-banner.jsx';
import { useDevice } from '@deriv-com/ui';

Expand Down Expand Up @@ -36,12 +37,12 @@ const AppContents = observer(({ children }) => {
} = ui;

const tracking_status = tracking_status_cookie.get(TRACKING_STATUS_KEY);
const is_dtrader_v2 =
isDTraderV2() && (location.pathname.startsWith(routes.trade) || location.pathname.startsWith('/contract/'));

const scroll_ref = React.useRef(null);
const child_ref = React.useRef(null);

const { dtrader_v2_enabled } = useDtraderV2Flag();

React.useEffect(() => {
if (scroll_ref.current) setAppContentsScrollRef(scroll_ref);
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -112,7 +113,7 @@ const AppContents = observer(({ children }) => {
'app-contents--is-scrollable': is_cfd_page || is_cashier_visible,
'app-contents--is-hidden': platforms[platform],
'app-contents--is-onboarding': window.location.pathname === routes.onboarding,
'app-contents--is-dtrader-v2': is_dtrader_v2,
'app-contents--is-dtrader-v2': dtrader_v2_enabled,
})}
ref={scroll_ref}
>
Expand Down
10 changes: 0 additions & 10 deletions packages/core/src/App/Containers/Layout/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
import { useFeatureFlags } from '@deriv/hooks';
import { useReadLocalStorage } from 'usehooks-ts';
import { makeLazyLoader, moduleLoader, routes } from '@deriv/shared';
import { observer, useStore } from '@deriv/stores';
import { useDevice } from '@deriv-com/ui';
import classNames from 'classnames';
import DTraderContractDetailsHeader from './dtrader-v2-contract-detail-header';

const HeaderFallback = () => {
return <div className={classNames('header')} />;
Expand Down Expand Up @@ -47,7 +45,6 @@ const Header = observer(() => {
const { client } = useStore();
const { accounts, has_wallet, is_logged_in, setAccounts, loginid, switchAccount } = client;
const { pathname } = useLocation();
const { isMobile } = useDevice();

const is_wallets_cashier_route = pathname.includes(routes.wallets);

Expand All @@ -63,8 +60,6 @@ const Header = observer(() => {
is_wallets_cashier_route;

const client_accounts = useReadLocalStorage('client.accounts');
const { is_dtrader_v2_enabled } = useFeatureFlags();

React.useEffect(() => {
if (has_wallet && is_logged_in) {
const accounts_keys = Object.keys(accounts ?? {});
Expand All @@ -83,11 +78,6 @@ const Header = observer(() => {
case pathname === routes.onboarding:
result = null;
break;
case is_dtrader_v2_enabled &&
isMobile &&
pathname.startsWith('/contract/') === routes.contract.startsWith('/contract/'):
result = <DTraderContractDetailsHeader />;
break;
case traders_hub_routes:
result = has_wallet ? <TradersHubHeaderWallets /> : <TradersHubHeader />;
break;
Expand Down
21 changes: 2 additions & 19 deletions packages/core/src/App/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import { CFDStore } from '@deriv/cfd';
import { Loading } from '@deriv/components';
import {
POIProvider,
getPositionsV2TabIndexFromURL,
initFormErrorMessages,
isDTraderV2,
routes,
setSharedCFDText,
setUrlLanguage,
setWebsocket,
Expand Down Expand Up @@ -47,8 +44,6 @@ const AppWithoutTranslation = ({ root_store }) => {
const { preferred_language } = root_store.client;
const { is_dark_mode_on } = root_store.ui;
const is_dark_mode = is_dark_mode_on || JSON.parse(localStorage.getItem('ui_store'))?.is_dark_mode_on;
const is_dtrader_v2 =
isDTraderV2() && (location.pathname.startsWith(routes.trade) || location.pathname.startsWith('/contract/'));
const language = preferred_language ?? getInitialLanguage();

React.useEffect(() => {
Expand Down Expand Up @@ -90,22 +85,10 @@ const AppWithoutTranslation = ({ root_store }) => {
}
}, [root_store.client.email]);

const getLoader = () =>
is_dtrader_v2 ? (
<Loading.DTraderV2
initial_app_loading
is_contract_details={location.pathname.startsWith('/contract/')}
is_positions={location.pathname === routes.trader_positions}
is_closed_tab={getPositionsV2TabIndexFromURL() === 1}
/>
) : (
<Loading />
);

React.useEffect(() => {
const html = document?.querySelector('html');

if (!html || !is_dtrader_v2) return;
if (!html) return;
if (is_dark_mode) {
html.classList?.remove('light');
html.classList?.add('dark');
Expand All @@ -127,7 +110,7 @@ const AppWithoutTranslation = ({ root_store }) => {
<P2PSettingsProvider>
<TranslationProvider defaultLang={language} i18nInstance={i18nInstance}>
{/* This is required as translation provider uses suspense to reload language */}
<React.Suspense fallback={getLoader()}>
<React.Suspense fallback={<Loading />}>
<AppContent passthrough={platform_passthrough} />
</React.Suspense>
</TranslationProvider>
Expand Down
3 changes: 2 additions & 1 deletion packages/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"dayjs": "^1.11.11",
"@types/js-cookie": "^3.0.1",
"js-cookie": "^2.2.1",
"@deriv-com/utils": "^0.0.36"
"@deriv-com/utils": "^0.0.36",
"@deriv-com/ui": "1.36.4"
},
"devDependencies": {
"typescript": "^4.6.3",
Expand Down
54 changes: 54 additions & 0 deletions packages/hooks/src/__tests__/useDtraderV2Flag.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { renderHook } from '@testing-library/react-hooks';
import { useDtraderV2Flag } from '..';
import useIsGrowthbookIsLoaded from '../useIsGrowthbookLoaded';
import { useDevice } from '@deriv-com/ui';
import { Analytics } from '@deriv-com/analytics';

jest.mock('@deriv-com/analytics', () => ({
Analytics: {
getFeatureValue: jest.fn().mockReturnValue(true),
},
}));

jest.mock('@deriv-com/ui', () => ({
useDevice: jest.fn(() => ({ isMobile: true })),
}));

jest.mock('../useIsGrowthbookLoaded');

describe('useDtraderV2Flag', () => {
const originalLocation = window.location;
beforeAll(() => {
const mockLocation = {
...originalLocation,
pathname: '/dtrader',
};

Object.defineProperty(window, 'location', {
value: mockLocation,
writable: true,
});
});

afterAll(() => {
Object.defineProperty(window, 'location', {
value: originalLocation,
writable: true,
});
});

it('should initially set load_dtrader_module and dtrader_v2_enabled to false', () => {
(useIsGrowthbookIsLoaded as jest.Mock).mockReturnValue({ isGBLoaded: false, isGBAvailable: true });
const { result } = renderHook(() => useDtraderV2Flag());
expect(result.current.load_dtrader_module).toBe(false);
expect(result.current.dtrader_v2_enabled).toBe(false);
});

it('should set load_dtrader_module and dtrader_v2_enabled to true when dtrader is enabled', () => {
(useIsGrowthbookIsLoaded as jest.Mock).mockReturnValue({ isGBLoaded: true, isGBAvailable: true });
(useDevice as jest.Mock).mockReturnValueOnce({ isMobile: true });
(Analytics.getFeatureValue as jest.Mock).mockReturnValue(true);
const { result } = renderHook(() => useDtraderV2Flag());
expect(result.current.dtrader_v2_enabled).toBe(true);
});
});
6 changes: 3 additions & 3 deletions packages/hooks/src/__tests__/useGrowthbookIsOn.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('useGrowthbookIsOn', () => {

it('should return initial state correctly', () => {
(useRemoteConfig as jest.Mock).mockReturnValue({ data: {} });
(useIsGrowthbookIsLoaded as jest.Mock).mockReturnValue(false);
(useIsGrowthbookIsLoaded as jest.Mock).mockReturnValue({ isGBLoaded: false });
Analytics.isFeatureOn = jest.fn(() => false);

const { result } = renderHook(() => useGrowthbookIsOn({ featureFlag: mockFeatureFlag }));
Expand All @@ -28,7 +28,7 @@ describe('useGrowthbookIsOn', () => {

it('should update state when data.marketing_growthbook and isGBLoaded change', () => {
(useRemoteConfig as jest.Mock).mockReturnValue({ data: { marketing_growthbook: true } });
(useIsGrowthbookIsLoaded as jest.Mock).mockReturnValue(true);
(useIsGrowthbookIsLoaded as jest.Mock).mockReturnValue({ isGBLoaded: true });
Analytics.isFeatureOn = jest.fn(() => true);
Analytics.getInstances = jest.fn(
() =>
Expand All @@ -50,7 +50,7 @@ describe('useGrowthbookIsOn', () => {

it('should set feature value when Analytics instances are available', () => {
(useRemoteConfig as jest.Mock).mockReturnValue({ data: { marketing_growthbook: true } });
(useIsGrowthbookIsLoaded as jest.Mock).mockReturnValue(true);
(useIsGrowthbookIsLoaded as jest.Mock).mockReturnValue({ isGBLoaded: true });
Analytics.isFeatureOn = jest.fn(() => false);
const setRendererMock = jest.fn();

Expand Down
10 changes: 5 additions & 5 deletions packages/hooks/src/__tests__/useIsGrowthbookLoaded.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('useIsGrowthbookIsLoaded', () => {

const { result } = renderHook(() => useIsGrowthbookIsLoaded());

expect(result.current).toBe(false); // isGBLoaded
expect(result.current.isGBLoaded).toBe(false); // isGBLoaded
});

it('should update state when data.marketing_growthbook is true and Analytics instance is available', () => {
Expand All @@ -35,7 +35,7 @@ describe('useIsGrowthbookIsLoaded', () => {
);
const { result, rerender } = renderHook(() => useIsGrowthbookIsLoaded());

expect(result.current).toBe(false); // isGBLoaded initially false
expect(result.current.isGBLoaded).toBe(false); // isGBLoaded initially false

act(() => {
(Analytics.getInstances as jest.Mock).mockReturnValueOnce({ ab: true });
Expand All @@ -44,7 +44,7 @@ describe('useIsGrowthbookIsLoaded', () => {
rerender();
});

expect(result.current).toBe(true); // isGBLoaded should be true
expect(result.current.isGBLoaded).toBe(true); // isGBLoaded should be true
expect(clearInterval).toHaveBeenCalledTimes(1);
});

Expand All @@ -65,13 +65,13 @@ describe('useIsGrowthbookIsLoaded', () => {

const { result } = renderHook(() => useIsGrowthbookIsLoaded());

expect(result.current).toBe(false); // isGBLoaded initially false
expect(result.current.isGBLoaded).toBe(false); // isGBLoaded initially false

act(() => {
jest.advanceTimersByTime(11000); // Move timer forward by 11 seconds
});

expect(result.current).toBe(false); // isGBLoaded should still be false
expect(result.current.isGBLoaded).toBe(false); // isGBLoaded should still be false
});

it('should clear interval on unmount', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,7 @@ export { default as usePhoneNumberVerificationSessionTimer } from './usePhoneNum
export { default as useIsPhoneNumberVerified } from './useIsPhoneNumberVerified';
export { default as usePhoneVerificationAnalytics } from './usePhoneVerificationAnalytics';
export { default as useTradingPlatformStatus } from './useTradingPlatformStatus';
export { default as useDtraderV2Flag } from './useDtraderV2Flag';
export { default as useIsGrowthbookIsLoaded } from './useIsGrowthbookLoaded';
export { default as useOauth2 } from './useOauth2';
export type { TradingPlatformStatus } from './useTradingPlatformStatus';
28 changes: 28 additions & 0 deletions packages/hooks/src/useDtraderV2Flag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useState, useEffect } from 'react';
import useIsGrowthbookIsLoaded from './useIsGrowthbookLoaded';
import { isDTraderV2, routes } from '@deriv/shared';
import { useDevice } from '@deriv-com/ui';
import { Analytics } from '@deriv-com/analytics';

const useDtraderV2Flag = () => {
const { isGBLoaded: is_growthbook_loaded, isGBAvailable: is_gb_available } = useIsGrowthbookIsLoaded();
const load_dtrader_module = is_growthbook_loaded || !is_gb_available;

const is_dtrader_v2 = isDTraderV2();
const { isMobile: is_mobile } = useDevice();
const is_feature_flag_active = Boolean(Analytics?.getFeatureValue('dtrader_v2_enabled', false));
const is_trade_or_contract_path =
location.pathname.startsWith(routes.trade) || location.pathname.startsWith('/contract/');

const [dtrader_v2_enabled, setDTraderV2Enabled] = useState(false);
useEffect(() => {
if (is_growthbook_loaded || isDTraderV2()) {
setDTraderV2Enabled((is_dtrader_v2 || is_feature_flag_active) && is_mobile && is_trade_or_contract_path);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [is_mobile, is_growthbook_loaded]);

return { dtrader_v2_enabled, load_dtrader_module };
};

export default useDtraderV2Flag;
2 changes: 1 addition & 1 deletion packages/hooks/src/useGrowthbookGetFeatureValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const useGrowthbookGetFeatureValue = <T extends string | boolean>({
const [featureFlagValue, setFeatureFlagValue] = useState(
Analytics?.getFeatureValue(featureFlag, resolvedDefaultValue) ?? resolvedDefaultValue
);
const isGBLoaded = useIsGrowthbookIsLoaded();
const { isGBLoaded } = useIsGrowthbookIsLoaded();
const isMounted = useIsMounted();

// Required for debugging Growthbook, this will be removed after Freshchat launch
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/src/useGrowthbookIsOn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface UseGrowthbookIsOneArgs {

const useGrowthbookIsOn = ({ featureFlag }: UseGrowthbookIsOneArgs) => {
const [featureIsOn, setFeatureIsOn] = useState(Analytics?.isFeatureOn(featureFlag));
const isGBLoaded = useIsGrowthbookIsLoaded();
const { isGBLoaded } = useIsGrowthbookIsLoaded();

useEffect(() => {
if (isGBLoaded) {
Expand Down
7 changes: 6 additions & 1 deletion packages/hooks/src/useIsGrowthbookLoaded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import { useRemoteConfig } from '@deriv/api';
const useIsGrowthbookIsLoaded = () => {
const [isGBLoaded, setIsGBLoaded] = useState(false);
const { data } = useRemoteConfig(true);
const [isGBAvailable, setisGBAvailable] = useState<boolean>(true);

useEffect(() => {
let analytics_interval: NodeJS.Timeout;

if (data?.marketing_growthbook) {
let checksCounter = 0;
analytics_interval = setInterval(() => {
// Check if the analytics instance is available for 10 seconds before setting the feature flag value
if (checksCounter > 20) {
// If the analytics instance is not available after 10 seconds, clear the interval
clearInterval(analytics_interval);
setisGBAvailable(false);
return;
}
checksCounter += 1;
Expand All @@ -23,13 +26,15 @@ const useIsGrowthbookIsLoaded = () => {
clearInterval(analytics_interval);
}
}, 500);
} else {
setisGBAvailable(false);
}
return () => {
clearInterval(analytics_interval);
};
}, [data.marketing_growthbook]);

return isGBLoaded;
return { isGBLoaded, isGBAvailable };
};

export default useIsGrowthbookIsLoaded;
Loading

0 comments on commit 97d01e2

Please sign in to comment.