diff --git a/apps/web-wallet/app/features/send/balance-check.ts b/apps/web-wallet/app/features/send/balance-check.ts new file mode 100644 index 000000000..c3d7fe1c5 --- /dev/null +++ b/apps/web-wallet/app/features/send/balance-check.ts @@ -0,0 +1,23 @@ +import { type Account, getAccountBalance } from '~/features/accounts/account'; +import type { Money } from '~/lib/money'; + +/** + * Returns true when the amount exceeds the account's available balance. + * Does not consider potential fees — those are checked in the quote services. + */ +export const exceedsAccountBalance = ( + amount: Money, + amountInOtherCurrency: Money | undefined, + account: Account, +): boolean => { + const amountInAccountCurrency = + amount.currency === account.currency ? amount : amountInOtherCurrency; + if (!amountInAccountCurrency || amountInAccountCurrency.isZero()) { + return false; + } + const balance = getAccountBalance(account); + if (!balance) { + return false; + } + return amountInAccountCurrency.amount().gt(balance.amount()); +}; diff --git a/apps/web-wallet/app/features/send/send-input.tsx b/apps/web-wallet/app/features/send/send-input.tsx index fbd6c280c..9cf90ee7d 100644 --- a/apps/web-wallet/app/features/send/send-input.tsx +++ b/apps/web-wallet/app/features/send/send-input.tsx @@ -6,7 +6,7 @@ import { X, ZapIcon, } from 'lucide-react'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { MoneyInputDisplay } from '~/components/money-display'; import { Numpad } from '~/components/numpad'; import { @@ -48,6 +48,7 @@ import { AddContactDrawer, ContactsList } from '../contacts'; import type { Contact } from '../contacts/contact'; import { getDefaultUnit } from '../shared/currencies'; import { DomainError, getErrorMessage } from '../shared/error'; +import { exceedsAccountBalance } from './balance-check'; import { useSendStore } from './send-provider'; export function SendInput() { @@ -91,6 +92,18 @@ export function SendInput() { initialOtherCurrency: initialInputCurrency === 'BTC' ? 'USD' : 'BTC', }); + const insufficientBalance = exceedsAccountBalance( + inputValue, + convertedValue, + sendAccount, + ); + + const wasInsufficient = useRef(false); + useEffect(() => { + if (insufficientBalance && !wasInsufficient.current) startShakeAnimation(); + wasInsufficient.current = insufficientBalance; + }, [insufficientBalance, startShakeAnimation]); + const handleContinue = async ( inputValue: Money, convertedValue: Money | undefined, @@ -160,6 +173,22 @@ export function SendInput() { newInputValue: latestInputValue, newConvertedValue: latestConvertedValue, } = setInputValue(amount.toString(defaultUnit), amount.currency)); + + if ( + exceedsAccountBalance( + latestInputValue, + latestConvertedValue, + sendAccount, + ) + ) { + toast({ + title: 'Insufficient balance', + description: + 'The invoice amount is greater than your available balance.', + variant: 'destructive', + }); + return true; + } } await handleContinue(latestInputValue, latestConvertedValue); @@ -196,11 +225,15 @@ export function SendInput() { /> - {!exchangeRateError && ( - + {insufficientBalance ? ( +

Insufficient balance

+ ) : ( + !exchangeRateError && ( + + ) )} @@ -262,7 +295,7 @@ export function SendInput() {