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

[DTRA]feat: added growthbook feature flag for dtrader-v2 #17160

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
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';
25 changes: 25 additions & 0 deletions packages/hooks/src/useDtraderV2Flag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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 [dtrader_v2_enabled, setDTraderV2Enabled] = useState(false);
const { isMobile } = useDevice();
useEffect(() => {
if (is_growthbook_loaded || isDTraderV2()) {
setDTraderV2Enabled(
(isDTraderV2() || Boolean(Analytics?.getFeatureValue('dtrader_v2_enabled', false))) &&
isMobile &&
(location.pathname.startsWith(routes.trade) || location.pathname.startsWith('/contract/'))
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion to refactor those complex if statemetns, for example like below:

const is_dtrader_v2 = isDTraderV2();
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 is_mobile = isMobile;
const should_load_dtrader_module = is_growthbook_loaded || is_dtrader_v2;

if (should_load_dtrader_module) {
    setLoadDTraderModule(true);
    const should_enable_dtrader_v2 = (is_dtrader_v2 || is_feature_flag_active) && is_mobile && is_trade_or_contract_path;
    setDTraderV2Enabled(should_enable_dtrader_v2);
}

complex if stataments are complexity points,

}
}, [isMobile, 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
Loading