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']>;