From da6432c76c3871c4d7602cab72d2450cd93f4cfc Mon Sep 17 00:00:00 2001
From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com>
Date: Fri, 5 Jun 2026 15:07:31 -0700
Subject: [PATCH] feat(send): preflight balance check at input time
Rebased onto post-monorepo master; paths relocated app/ -> apps/web-wallet/app/. Original commits:
- feat(send): preflight balance check at input time
- refactor(send): shake inline on numpad keypress
- chore(send): drop balance-check unit tests
- refactor(send): drop dead check + tighten balance-check JSDoc
- docs(send): reword balance-check JSDoc
- refactor(send): expose previewNext, shake on balance-transition only
- Revert "refactor(send): expose previewNext, shake on balance-transition only"
- fix(send): shake on balance-transition via useEffect
- revert(send): restore numpad onButtonClick arrow-body formatting
---
.../app/features/send/balance-check.ts | 23 +++++++++
.../app/features/send/send-input.tsx | 47 ++++++++++++++++---
.../app/features/send/send-scanner.tsx | 15 ++++++
3 files changed, 78 insertions(+), 7 deletions(-)
create mode 100644 apps/web-wallet/app/features/send/balance-check.ts
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 && ( +