Skip to content

Commit

Permalink
[WALL] Nijil / WALL-3488 / Application crashes after trading accounts…
Browse files Browse the repository at this point in the history
… creation in Trader's Hub (deriv-com#14183)

* feat: useAccountList hook

* fix: infinite useEffect loop

* chore: remove useAccountList hook

* chore: handle useEffect infinite loops by removing the useModal() dependancies

* chore: update useDerivAccountsList hook

* fix: undefined balance caused application crash

* chore: remove unused import

* feat: select default account

* feat: remove unneded condition

* feat: missing else

* feat: fixed default behavriour

* feat: removed unnecesay tests

* feat: removed unused imports

* chore: remove unwanted imports and variables

* fix: types and other fixes

* fix: type error

* fix: eslint error

* fix: memory leak due to useDerivAccountList hook

* fix: options listing not updating after adding trading account

* fix: apply changes based on comments received

* fix: missing item in dependancy array

* fix: handle default wallet if there's no USD wallet

* chore: cleanup

* fix: account creation success modal is not showin in responsive

* fix: apply review comments

* fix: remove unused  type

* chore: rename variables for better clarity

---------

Co-authored-by: Wojciech Brygola <[email protected]>
  • Loading branch information
nijil-deriv and wojciech-deriv authored Mar 28, 2024
1 parent 58001e6 commit fea435f
Show file tree
Hide file tree
Showing 20 changed files with 196 additions and 156 deletions.
26 changes: 18 additions & 8 deletions packages/api-v2/src/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { createContext, useState, useContext, useCallback, useEffect, useMemo } from 'react';
import { useAPIContext } from './APIProvider';

import { getActiveLoginIDFromLocalStorage, getToken } from '@deriv/utils';
import { getAccountsFromLocalStorage, getActiveLoginIDFromLocalStorage, getToken } from '@deriv/utils';
import useMutation from './useMutation';
import { TSocketResponseData } from '../types';

Expand All @@ -23,16 +23,21 @@ type LoginToken = {
token: string;
};

// Create the context
const AuthContext = createContext<AuthContextType | undefined>(undefined);

type AuthProviderProps = {
children: React.ReactNode;
cookieTimeout?: number;
loginIDKey?: string;
selectDefaultAccount?: (loginids: NonNullable<ReturnType<typeof getAccountsFromLocalStorage>>) => string;
};

function waitForLoginAndTokenWithTimeout(loginIDKey?: string, cookieTimeout = 10000) {
// Create the context
const AuthContext = createContext<AuthContextType | undefined>(undefined);

function waitForLoginAndTokenWithTimeout(
cookieTimeout = 10000,
loginIDKey?: string,
selectDefaultAccount?: (loginids: NonNullable<ReturnType<typeof getAccountsFromLocalStorage>>) => string
) {
// Default timeout of 10 seconds
let timeoutHandle: NodeJS.Timeout | undefined,
cookieTimeoutHandle: NodeJS.Timeout | undefined, // Handle for the cookieTimeout
Expand All @@ -44,10 +49,16 @@ function waitForLoginAndTokenWithTimeout(loginIDKey?: string, cookieTimeout = 10
) => {
const loginId = getActiveLoginIDFromLocalStorage(loginIDKey);
const token = getToken(loginId as string);
const storedAccounts = getAccountsFromLocalStorage();
if (loginId && token) {
clearTimeout(timeoutHandle); // Clear the checkLogin timeout as we've succeeded
clearTimeout(cookieTimeoutHandle); // Clear the cookieTimeout as well
resolve({ loginId, token });
} else if (selectDefaultAccount && storedAccounts && Object.keys(storedAccounts).length > 0) {
const selectedLoginId = selectDefaultAccount(storedAccounts);
clearTimeout(timeoutHandle); // Clear the checkLogin timeout as we've succeeded
clearTimeout(cookieTimeoutHandle); // Clear the cookieTimeout as well
resolve({ loginId: selectedLoginId, token: getToken(selectedLoginId) || '' });
} else {
timeoutHandle = setTimeout(checkLogin, 100, resolve, reject);
}
Expand Down Expand Up @@ -78,7 +89,7 @@ function waitForLoginAndTokenWithTimeout(loginIDKey?: string, cookieTimeout = 10
};
}

const AuthProvider = ({ loginIDKey, children, cookieTimeout }: AuthProviderProps) => {
const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccount }: AuthProviderProps) => {
const [loginid, setLoginid] = useState<string | null>(null);

const { mutateAsync } = useMutation('authorize');
Expand Down Expand Up @@ -122,7 +133,7 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout }: AuthProviderProps
setIsLoading(true);
setIsSuccess(false);

const { promise, cleanup } = waitForLoginAndTokenWithTimeout(loginIDKey, cookieTimeout);
const { promise, cleanup } = waitForLoginAndTokenWithTimeout(cookieTimeout, loginIDKey, selectDefaultAccount);

let isMounted = true;

Expand Down Expand Up @@ -164,7 +175,6 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout }: AuthProviderProps
if (newLoginId === loginid && !forceRefresh) {
return;
}

queryClient.cancelQueries();

setIsLoading(true);
Expand Down
15 changes: 8 additions & 7 deletions packages/api-v2/src/hooks/useActiveLinkedToTradingAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ const useActiveLinkedToTradingAccount = () => {
const { data: account_list_data } = useDerivAccountsList();
const { data: wallet_account_data } = useActiveWalletAccount();

const linkedDtradeLoginId = wallet_account_data?.linked_to?.find(
linked => linked.loginid && linked?.platform === 'dtrade'
)?.loginid;

const matchingTradingAccount = account_list_data?.filter(account => account.loginid === linkedDtradeLoginId)[0];

const modified_account = useMemo(() => {
if (!account_list_data || !wallet_account_data) return undefined;

const linkedDtradeLoginId = account_list_data
?.filter(account => account.loginid === wallet_account_data?.loginid)[0]
?.linked_to?.find(linked => linked.loginid && linked?.platform === 'dtrade')?.loginid;

const matchingTradingAccount = account_list_data?.filter(account => account.loginid === linkedDtradeLoginId)[0];

return { ...matchingTradingAccount };
}, [account_list_data, matchingTradingAccount, wallet_account_data]);
}, [account_list_data, wallet_account_data]);

return {
/** The active linked trading account for the current user. */
Expand Down
2 changes: 1 addition & 1 deletion packages/api-v2/src/hooks/useCreateNewRealAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const useCreateNewRealAccount = () => {
const invalidate = useInvalidateQuery();
const { data, ...rest } = useMutation('new_account_real', {
onSuccess: () => {
invalidate('authorize');
invalidate('account_list');
},
});

Expand Down
17 changes: 12 additions & 5 deletions packages/api-v2/src/hooks/useDerivAccountsList.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { useMemo } from 'react';
import useQuery from '../useQuery';
import useAuthorize from './useAuthorize';
import useBalance from './useBalance';
import useCurrencyConfig from './useCurrencyConfig';
import { displayMoney } from '../utils';

/** A custom hook that returns the list of accounts for the current user. */
const useDerivAccountsList = () => {
const { data: authorize_data, ...rest } = useAuthorize();
const { data: authorize_data, isSuccess } = useAuthorize();
const { data: account_list_data, ...rest } = useQuery('account_list', {
options: {
enabled: isSuccess,
refetchOnWindowFocus: false,
},
});
const { data: balance_data } = useBalance();
const { getConfig } = useCurrencyConfig();

// Add additional information to the authorize response.
const modified_accounts = useMemo(() => {
return authorize_data.account_list?.map(account => {
return account_list_data?.account_list?.map(account => {
return {
...account,
/** Creation time of the account. */
Expand All @@ -22,10 +29,10 @@ const useDerivAccountsList = () => {
/** Date till client has excluded him/herself from the website, only present if client is self excluded. */
excluded_until: account.excluded_until ? new Date(account.excluded_until) : undefined,
/** Indicating whether the wallet is the currently active account. */
is_active: account.loginid === authorize_data.loginid,
is_active: account.loginid === authorize_data?.loginid,
/** Indicating whether any linked account is active */
is_linked_account_active: account.linked_to?.some(
account => account.loginid === authorize_data.loginid
account => account.loginid === authorize_data?.loginid
),
/** indicating whether the account is marked as disabled or not. */
is_disabled: Boolean(account.is_disabled),
Expand All @@ -43,7 +50,7 @@ const useDerivAccountsList = () => {
is_mf: account.loginid?.startsWith('MF'),
} as const;
});
}, [authorize_data.account_list, authorize_data.loginid, getConfig]);
}, [account_list_data?.account_list, authorize_data?.loginid, getConfig]);

// Add balance to each account
const modified_accounts_with_balance = useMemo(
Expand Down
68 changes: 56 additions & 12 deletions packages/api-v2/src/hooks/useWalletAccountsList.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,87 @@
import { useMemo } from 'react';
import useDerivAccountsList from './useDerivAccountsList';
import useAuthorize from './useAuthorize';
import useBalance from './useBalance';
import useCurrencyConfig from './useCurrencyConfig';
import { displayMoney } from '../utils';

/** A custom hook that gets the list of all wallet accounts for the current user. */
const useWalletAccountsList = () => {
const { data: account_list_data, ...rest } = useDerivAccountsList();
const { data: authorize_data, ...rest } = useAuthorize();
const { data: balance_data } = useBalance();
const { getConfig } = useCurrencyConfig();

// Filter out non-wallet accounts.
const filtered_accounts = useMemo(
() => account_list_data?.filter(account => account.is_wallet),
[account_list_data]
() => authorize_data?.account_list?.filter(account => account.account_category === 'wallet'),
[authorize_data]
);

// Add additional information to each wallet account.
const modified_accounts = useMemo(() => {
return filtered_accounts?.map(wallet => {
const wallet_currency_type = wallet.is_virtual ? 'Demo' : wallet.currency || '';
const dtrade_loginid = wallet.linked_to?.find(account => account.platform === 'dtrade')?.loginid;
const currency_config = wallet.currency ? getConfig(wallet.currency) : undefined;

return {
...wallet,
/** The DTrade account ID of this wallet */
dtrade_loginid,
/** Wallet account's currency config information */
currency_config,
/** Returns the wallet's currency type. ex: `Demo`, `USD`, etc. */
wallet_currency_type,
/** Returns if the wallet is a crypto wallet. */
is_crypto: currency_config?.is_crypto,
/** Creation time of the wallet account. */
created_at: wallet.created_at ? new Date(wallet.created_at) : undefined,
/** Date till client has excluded him/herself from the website, only present if client is self excluded. */
excluded_until: wallet.excluded_until ? new Date(wallet.excluded_until) : undefined,
/** Indicating whether the wallet account is the currently active account. */
is_active: wallet.loginid === authorize_data?.loginid,
/** Indicating whether any linked account is active */
is_linked_account_active: wallet.linked_to?.some(
account => account.loginid === authorize_data?.loginid
),
/** indicating whether the account is marked as disabled or not. */
is_disabled: Boolean(wallet.is_disabled),
/** indicating whether the wallet account is a virtual-money account. */
is_virtual: Boolean(wallet.is_virtual),
/** The account ID of specified wallet account. */
loginid: `${wallet.loginid}`,
/** Landing company shortcode the account belongs to. */
landing_company_name: wallet.landing_company_name?.replace('maltainvest', 'malta'),
/** Indicating whether the wallet is a maltainvest wallet. */
is_malta_wallet: wallet.landing_company_name === 'maltainvest',
/** The DTrade account ID of this wallet */
dtrade_loginid,
/** Returns if the wallet is a crypto wallet. */
is_crypto: wallet.currency_config?.is_crypto,
} as const;
});
}, [filtered_accounts]);
}, [filtered_accounts, getConfig]);

// Add balance to each wallet account
const modified_accounts_with_balance = useMemo(
() =>
modified_accounts?.map(wallet => {
const balance = balance_data?.accounts?.[wallet.loginid]?.balance || 0;

return {
...wallet,
/** The balance of the wallet account. */
balance,
/** The balance of the wallet account in currency format. */
display_balance: displayMoney(balance, wallet.currency_config?.display_code || 'USD', {
fractional_digits: wallet.currency_config?.fractional_digits,
preferred_language: authorize_data?.preferred_language,
}),
};
}),
[balance_data?.accounts, modified_accounts, authorize_data?.preferred_language]
);

// Sort wallet accounts alphabetically by fiat, crypto, then virtual.
const sorted_accounts = useMemo(() => {
if (!modified_accounts) return;
if (!modified_accounts_with_balance) return;

return [...modified_accounts].sort((a, b) => {
return [...modified_accounts_with_balance].sort((a, b) => {
if (a.is_virtual !== b.is_virtual) {
return a.is_virtual ? 1 : -1;
} else if (a.currency_config?.is_crypto !== b.currency_config?.is_crypto) {
Expand All @@ -46,7 +90,7 @@ const useWalletAccountsList = () => {

return (a.currency || 'USD').localeCompare(b.currency || 'USD');
});
}, [modified_accounts]);
}, [modified_accounts_with_balance]);

return {
/** The list of wallet accounts for the current user. */
Expand Down
12 changes: 12 additions & 0 deletions packages/api-v2/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type {
AccountLimitsRequest,
AccountLimitsResponse,
AccountListRequest,
AccountListResponse,
AccountStatusRequest,
AccountStatusResponse,
ActiveSymbolsRequest,
Expand Down Expand Up @@ -2144,11 +2146,21 @@ type TPrivateSocketEndpoints = {
};
};

type TAccountList = NonNullable<AccountListResponse['account_list']>[number] & { excluded_until: Date };

interface IExtendedAccountListResponse extends AccountListResponse {
account_list?: TAccountList[];
}

type TSocketEndpoints = {
active_symbols: {
request: ActiveSymbolsRequest;
response: ActiveSymbolsResponse;
};
account_list: {
request: AccountListRequest;
response: IExtendedAccountListResponse;
};
api_token: {
request: APITokenRequest;
response: APITokenResponse;
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/getActiveAuthTokenIDFromLocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import getActiveLoginIDFromLocalStorage from './getActiveLoginIDFromLocalStorage
/**
* Gets the current user's auth `token` for the active `loginid` from the `localStorage`.
*/
const getActiveAuthTokenIDFromLocalStorage = (loginIDKey?: string) => {
const getActiveAuthTokenIDFromLocalStorage = (loginid_key?: string) => {
const accounts = getAccountsFromLocalStorage();
const active_loginid = getActiveLoginIDFromLocalStorage(loginIDKey);
const active_loginid = getActiveLoginIDFromLocalStorage(loginid_key);

// If there is no active loginid or no accounts list, return undefined.
if (!active_loginid || !accounts) return;
Expand Down
8 changes: 3 additions & 5 deletions packages/utils/src/getActiveLoginIDFromLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
/**
* Gets the active `loginid` for the current user from the `localStorage`.
*/
const getActiveLoginIDFromLocalStorage = (loginIDKey?: string) => {
const active_loginid = localStorage.getItem('active_loginid');
const active_custom_loginid = loginIDKey ? localStorage.getItem(loginIDKey) : undefined;

return active_custom_loginid ?? active_loginid ?? undefined;
const getActiveLoginIDFromLocalStorage = (loginid_key = 'active_loginid') => {
const active_custom_loginid = localStorage.getItem(loginid_key);
return active_custom_loginid ?? undefined;
};

export default getActiveLoginIDFromLocalStorage;
24 changes: 23 additions & 1 deletion packages/wallets/src/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,29 @@ import { AuthProvider } from '@deriv/api-v2';
const WALLETS_LOGINID_LOCALSTORAGE_KEY = 'active_wallet_loginid';

const WalletsAuthProvider = ({ children, ...rest }: Omit<ComponentProps<typeof AuthProvider>, 'loginIDKey'>) => (
<AuthProvider {...rest} loginIDKey={WALLETS_LOGINID_LOCALSTORAGE_KEY}>
<AuthProvider
{...rest}
loginIDKey={WALLETS_LOGINID_LOCALSTORAGE_KEY}
selectDefaultAccount={accountsObject => {
const loginIds = Object.keys(accountsObject);
const defaultFiatWallet = loginIds.filter((loginId: string) => {
const { account_category: accountCategory, account_type: accountType } = accountsObject[loginId];
const isWallet = accountCategory == 'wallet';
const isFiat = accountType == 'doughflow';
return isWallet && isFiat;
})[0];

if (!defaultFiatWallet) {
const defaultWallet = loginIds.filter((loginId: string) => {
const { account_category: accountCategory } = accountsObject[loginId];
const isWallet = accountCategory == 'wallet';
return isWallet;
})[0];
return defaultWallet;
}
return defaultFiatWallet;
}}
>
{children}
</AuthProvider>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/wallets/src/components/AppCard/AppCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { WalletsAppLinkedWithWalletIcon } from '../WalletsAppLinkedWithWalletIco
import './AppCard.scss';

type TProps = {
activeWalletCurrency: THooks.ActiveWalletAccount['currency'];
activeWalletCurrency?: THooks.ActiveWalletAccount['currency'];
appIcon: React.ComponentProps<typeof WalletsAppLinkedWithWalletIcon>['appIcon'];
appName?: string;
balance?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const ModalStepWrapper: FC<PropsWithChildren<TModalStepWrapperProps>> = ({
setModalOptions({
shouldHideDerivAppHeader,
});
}, [shouldHideDerivAppHeader, setModalOptions]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldHideDerivAppHeader]);

const Footer = () =>
hasRenderFooter ? (
Expand Down
Loading

0 comments on commit fea435f

Please sign in to comment.