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 && (
-
Insufficient balance
+ ) : ( + !exchangeRateError && ( +