diff --git a/packages/wallets/src/features/cashier/modules/Transfer/components/TransferForm/TransferForm.tsx b/packages/wallets/src/features/cashier/modules/Transfer/components/TransferForm/TransferForm.tsx index b403a4e836c4..3a9d9a62ea71 100644 --- a/packages/wallets/src/features/cashier/modules/Transfer/components/TransferForm/TransferForm.tsx +++ b/packages/wallets/src/features/cashier/modules/Transfer/components/TransferForm/TransferForm.tsx @@ -6,6 +6,7 @@ import { useTransfer } from '../../provider'; import type { TInitialTransferFormValues } from '../../types'; import { TransferFormAmountInput } from '../TransferFormAmountInput'; import { TransferFormDropdown } from '../TransferFormDropdown'; +import { TransferMessages } from '../TransferMessages'; import './TransferForm.scss'; const TransferForm = () => { @@ -41,7 +42,7 @@ const TransferForm = () => { mobileAccountsListRef={mobileAccountsListRef} /> -
+
{ + const { values } = useFormikContext(); + + const messages = useTransferMessages(values.fromAccount, values.toAccount, values); + + return ( + + {messages.map(message => ( + + ))} + + ); +}; + +export default TransferMessages; diff --git a/packages/wallets/src/features/cashier/modules/Transfer/components/TransferMessages/index.ts b/packages/wallets/src/features/cashier/modules/Transfer/components/TransferMessages/index.ts new file mode 100644 index 000000000000..25b578844971 --- /dev/null +++ b/packages/wallets/src/features/cashier/modules/Transfer/components/TransferMessages/index.ts @@ -0,0 +1 @@ +export { default as TransferMessages } from './TransferMessages'; diff --git a/packages/wallets/src/features/cashier/modules/Transfer/hooks/index.ts b/packages/wallets/src/features/cashier/modules/Transfer/hooks/index.ts index 6528e2633dbf..7c2aac9420cd 100644 --- a/packages/wallets/src/features/cashier/modules/Transfer/hooks/index.ts +++ b/packages/wallets/src/features/cashier/modules/Transfer/hooks/index.ts @@ -1,2 +1,3 @@ export { default as useExtendedTransferAccountProperties } from './useExtendedTransferAccountProperties'; export { default as useSortedTransferAccounts } from './useSortedTransferAccounts'; +export { default as useTransferMessages } from './useTransferMessages'; diff --git a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/index.ts b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/index.ts new file mode 100644 index 000000000000..bcc52fda1f1c --- /dev/null +++ b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/index.ts @@ -0,0 +1,3 @@ +import useTransferMessages from './useTransferMessages'; + +export default useTransferMessages; diff --git a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/types/index.ts b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/types/index.ts new file mode 100644 index 000000000000..fcb073fefcd6 --- /dev/null +++ b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/types/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/types/types.ts b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/types/types.ts new file mode 100644 index 000000000000..416ea0d23d34 --- /dev/null +++ b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/types/types.ts @@ -0,0 +1,17 @@ +import { THooks } from '../../../../../../../types'; +import { TAccount } from '../../../types'; + +export type TMessage = { + text: string; + type: 'error' | 'success'; +}; + +export type TMessageFnProps = { + activeWallet: THooks.ActiveWalletAccount; + displayMoney?: (amount: number, currency: string, fractionalDigits: number) => string; + exchangeRates?: THooks.ExchangeRate; + limits?: THooks.AccountLimits; + sourceAccount: NonNullable; + sourceAmount: number; + targetAccount: NonNullable; +}; diff --git a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/useTransferMessages.ts b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/useTransferMessages.ts new file mode 100644 index 000000000000..39e364c35634 --- /dev/null +++ b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/useTransferMessages.ts @@ -0,0 +1,110 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useAccountLimits, useActiveWalletAccount, useAuthorize, useExchangeRate, usePOI } from '@deriv/api'; +import { displayMoney as displayMoney_ } from '@deriv/api/src/utils'; +import { THooks } from '../../../../../../types'; +import { TAccount, TInitialTransferFormValues } from '../../types'; +import { + cumulativeAccountLimitsMessageFn, + lifetimeAccountLimitsBetweenWalletsMessageFn, +} from './utils/messageFunctions'; +import { TMessage, TMessageFnProps } from './types'; + +const useTransferMessages = ( + fromAccount: NonNullable | undefined, + toAccount: NonNullable | undefined, + formData: TInitialTransferFormValues +) => { + const { data: authorizeData } = useAuthorize(); + const { data: activeWallet } = useActiveWalletAccount(); + const { preferred_language: preferredLanguage } = authorizeData; + const { data: poi } = usePOI(); + const { data: accountLimits } = useAccountLimits(); + const { data: exchangeRatesRaw, subscribe, unsubscribe } = useExchangeRate(); + + const [exchangeRates, setExchangeRates] = useState(); + + const isTransferBetweenWallets = + fromAccount?.account_category === 'wallet' && toAccount?.account_category === 'wallet'; + const isAccountVerified = poi?.is_verified; + + useEffect( + () => setExchangeRates(prev => ({ ...prev, rates: { ...prev?.rates, ...exchangeRatesRaw?.rates } })), + [exchangeRatesRaw?.rates] + ); + + useEffect(() => { + if (!fromAccount?.currency || !toAccount?.currency || !activeWallet?.currency || !activeWallet?.loginid) return; + unsubscribe(); + if (!isAccountVerified && isTransferBetweenWallets) { + subscribe({ + base_currency: activeWallet.currency, + loginid: activeWallet.loginid, + target_currency: + activeWallet.loginid === fromAccount.loginid ? toAccount.currency : fromAccount.currency, + }); + } else { + subscribe({ + base_currency: 'USD', + loginid: activeWallet.loginid, + target_currency: toAccount.currency, + }); + if (fromAccount.currency !== toAccount.currency) + subscribe({ + base_currency: 'USD', + loginid: activeWallet.loginid, + target_currency: fromAccount.currency, + }); + return unsubscribe; + } + }, [ + activeWallet?.currency, + activeWallet?.loginid, + fromAccount?.currency, + fromAccount?.loginid, + isAccountVerified, + isTransferBetweenWallets, + subscribe, + toAccount?.currency, + unsubscribe, + ]); + + const displayMoney = useCallback( + (amount: number, currency: string, fractionalDigits: number) => + displayMoney_(amount, currency, { + fractional_digits: fractionalDigits, + preferred_language: preferredLanguage, + }), + [preferredLanguage] + ); + + if (!activeWallet || !fromAccount || !toAccount) return []; + + const sourceAmount = formData.fromAmount; + + const messageFns: ((props: TMessageFnProps) => TMessage | null)[] = []; + const messages: TMessage[] = []; + + if (isAccountVerified || (!isAccountVerified && !isTransferBetweenWallets)) { + messageFns.push(cumulativeAccountLimitsMessageFn); + } + if (!isAccountVerified && isTransferBetweenWallets) { + messageFns.push(lifetimeAccountLimitsBetweenWalletsMessageFn); + } + + messageFns.forEach(messageFn => { + const message = messageFn({ + activeWallet, + displayMoney, + exchangeRates, + limits: accountLimits, + sourceAccount: fromAccount, + sourceAmount, + targetAccount: toAccount, + }); + if (message) messages.push(message); + }); + + return messages; +}; + +export default useTransferMessages; diff --git a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/utils/messageFunctions.ts b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/utils/messageFunctions.ts new file mode 100644 index 000000000000..be917cbfa8c6 --- /dev/null +++ b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/utils/messageFunctions.ts @@ -0,0 +1,177 @@ +import { TMessageFnProps } from '../types'; + +// this function should work once BE WALL-1440 is delivered +const lifetimeAccountLimitsBetweenWalletsMessageFn = ({ + activeWallet, + displayMoney, + exchangeRates, + limits, + sourceAccount, + sourceAmount, + targetAccount, +}: TMessageFnProps) => { + if (sourceAccount?.account_category !== 'wallet' || targetAccount?.account_category !== 'wallet') return null; + + const sourceWalletType = sourceAccount.account_type === 'crypto' ? 'crypto' : 'fiat'; + const targetWalletType = targetAccount.account_type === 'crypto' ? 'crypto' : 'fiat'; + const limitsCaseKey = `${sourceWalletType}_to_${targetWalletType}` as const; + + //@ts-expect-error needs backend type + const allowedSumActiveWalletCurrency = limits?.lifetime_transfers?.[limitsCaseKey].allowed as number; + //@ts-expect-error needs backend type + const availableSumActiveWalletCurrency = limits?.lifetime_transfers?.[limitsCaseKey].available as number; + + if ( + !sourceAccount.currency || + !exchangeRates?.rates?.[sourceAccount.currency] || + !targetAccount.currency || + !exchangeRates?.rates?.[targetAccount.currency] || + !sourceAccount.currencyConfig || + !targetAccount.currencyConfig + ) + return null; + + const transferDirection = activeWallet.loginid === sourceAccount.loginid ? 'from' : 'to'; + + const allowedSumConverted = + allowedSumActiveWalletCurrency * + (exchangeRates?.rates[transferDirection === 'from' ? targetAccount.currency : sourceAccount.currency] ?? 1); + const availableSumConverted = + availableSumActiveWalletCurrency * + (exchangeRates?.rates[transferDirection === 'from' ? targetAccount.currency : sourceAccount.currency] ?? 1); + + const sourceCurrencyLimit = transferDirection === 'from' ? allowedSumActiveWalletCurrency : allowedSumConverted; + const targetCurrencyLimit = transferDirection === 'from' ? allowedSumConverted : allowedSumActiveWalletCurrency; + + const sourceCurrencyRemainder = + transferDirection === 'from' ? availableSumActiveWalletCurrency : availableSumConverted; + const targetCurrencyRemainder = + transferDirection === 'from' ? availableSumConverted : availableSumActiveWalletCurrency; + + const formattedSourceCurrencyLimit = displayMoney?.( + sourceCurrencyLimit, + sourceAccount.currencyConfig.display_code, + sourceAccount.currencyConfig.fractional_digits + ); + const formattedTargetCurrencyLimit = displayMoney?.( + targetCurrencyLimit, + targetAccount.currencyConfig.display_code, + targetAccount.currencyConfig?.fractional_digits + ); + + const formattedSourceCurrencyRemainder = displayMoney?.( + sourceCurrencyRemainder, + sourceAccount.currencyConfig.display_code, + sourceAccount.currencyConfig.fractional_digits + ); + const formattedTargetCurrencyRemainder = displayMoney?.( + targetCurrencyRemainder, + targetAccount.currencyConfig?.display_code, + targetAccount.currencyConfig?.fractional_digits + ); + + if (availableSumActiveWalletCurrency === 0) + return { + text: `You've reached the lifetime transfer limit from your ${sourceAccount.accountName} to any ${targetWalletType} Wallet. Verify your account to upgrade the limit.`, + type: 'error' as const, + }; + + if (allowedSumActiveWalletCurrency === availableSumActiveWalletCurrency) + return { + text: `The lifetime transfer limit from ${sourceAccount.accountName} to any ${targetWalletType} Wallet is ${formattedSourceCurrencyLimit} (${formattedTargetCurrencyLimit}).`, + type: sourceAmount > sourceCurrencyRemainder ? ('error' as const) : ('success' as const), + }; + + return { + text: `Remaining lifetime transfer limit is ${formattedSourceCurrencyRemainder} (${formattedTargetCurrencyRemainder}). Verify your account to upgrade the limit.`, + type: sourceAmount > sourceCurrencyRemainder ? ('error' as const) : ('success' as const), + }; +}; + +const cumulativeAccountLimitsMessageFn = ({ + displayMoney, + exchangeRates, + limits, + sourceAccount, + sourceAmount, + targetAccount, +}: TMessageFnProps) => { + const isTransferBetweenWallets = + sourceAccount.account_category === 'wallet' && targetAccount.account_category === 'wallet'; + const isSameCurrency = sourceAccount.currency === targetAccount.currency; + + const keyAccountType = + [sourceAccount, targetAccount].find(acc => acc.account_category !== 'wallet')?.account_type ?? 'wallets'; + + const platformKey = keyAccountType === 'standard' ? 'dtrade' : keyAccountType; + + //@ts-expect-error needs backend type + const allowedSumUSD = limits?.daily_cumulative_amount_transfers?.[platformKey].allowed as number; + //@ts-expect-error needs backend type + const availableSumUSD = limits?.daily_cumulative_amount_transfers?.[platformKey].available as number; + + if ( + !sourceAccount.currency || + !exchangeRates?.rates?.[sourceAccount.currency] || + !targetAccount.currency || + !exchangeRates?.rates?.[targetAccount.currency] || + !sourceAccount.currencyConfig || + !targetAccount.currencyConfig + ) + return null; + + const sourceCurrencyLimit = allowedSumUSD * (exchangeRates.rates[sourceAccount.currency] ?? 1); + const targetCurrencyLimit = allowedSumUSD * (exchangeRates.rates[targetAccount.currency] ?? 1); + + const sourceCurrencyRemainder = availableSumUSD * (exchangeRates.rates[sourceAccount.currency] ?? 1); + const targetCurrencyRemainder = availableSumUSD * (exchangeRates.rates[targetAccount.currency] ?? 1); + + const formattedSourceCurrencyLimit = displayMoney?.( + sourceCurrencyLimit, + sourceAccount.currencyConfig.display_code, + sourceAccount.currencyConfig.fractional_digits + ); + const formattedTargetCurrencyLimit = displayMoney?.( + targetCurrencyLimit, + targetAccount.currencyConfig.display_code, + targetAccount.currencyConfig?.fractional_digits + ); + + const formattedSourceCurrencyRemainder = displayMoney?.( + sourceCurrencyRemainder, + sourceAccount.currencyConfig.display_code, + sourceAccount.currencyConfig.fractional_digits + ); + const formattedTargetCurrencyRemainder = displayMoney?.( + targetCurrencyRemainder, + targetAccount.currencyConfig?.display_code, + targetAccount.currencyConfig?.fractional_digits + ); + + if (availableSumUSD === 0) + return { + text: `You have reached your daily transfer limit of ${formattedSourceCurrencyLimit} ${ + !isSameCurrency ? ` (${formattedTargetCurrencyLimit})` : '' + } between your ${ + isTransferBetweenWallets ? 'Wallets' : `${sourceAccount.accountName} and ${targetAccount.accountName}` + }. The limit will reset at 00:00 GMT.`, + type: 'error' as const, + }; + + if (allowedSumUSD === availableSumUSD) + return { + text: `The daily transfer limit between your ${ + isTransferBetweenWallets ? 'Wallets' : `${sourceAccount.accountName} and ${targetAccount.accountName}` + } is ${formattedSourceCurrencyLimit}${!isSameCurrency ? ` (${formattedTargetCurrencyLimit})` : ''}.`, + type: sourceAmount > sourceCurrencyRemainder ? ('error' as const) : ('success' as const), + }; + + return { + text: `The remaining daily transfer limit between ${ + isTransferBetweenWallets ? 'Wallets' : `your ${sourceAccount.accountName} and ${targetAccount.accountName}` + } is ${formattedSourceCurrencyRemainder}${!isSameCurrency ? ` (${formattedTargetCurrencyRemainder})` : ''}.`, + type: sourceAmount > sourceCurrencyRemainder ? ('error' as const) : ('success' as const), + }; +}; + +export { cumulativeAccountLimitsMessageFn, lifetimeAccountLimitsBetweenWalletsMessageFn }; diff --git a/packages/wallets/src/types.ts b/packages/wallets/src/types.ts index ab5fdf2e1977..44f82c5f1b03 100644 --- a/packages/wallets/src/types.ts +++ b/packages/wallets/src/types.ts @@ -1,4 +1,5 @@ import type { + useAccountLimits, useActiveAccount, useActiveTradingAccount, useActiveWalletAccount, @@ -28,6 +29,7 @@ import type { // eslint-disable-next-line @typescript-eslint/no-namespace export namespace THooks { + export type AccountLimits = NonNullable['data']>; export type Authentication = NonNullable['data']>; export type AvailableMT5Accounts = NonNullable['data']>[number]; export type CreateWallet = NonNullable['data']>;