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 && ( - + {insufficientBalance ? ( +

Insufficient balance

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