Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/web-wallet/app/features/send/balance-check.ts
Original file line number Diff line number Diff line change
@@ -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());
};
47 changes: 40 additions & 7 deletions apps/web-wallet/app/features/send/send-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -91,6 +92,18 @@ export function SendInput() {
initialOtherCurrency: initialInputCurrency === 'BTC' ? 'USD' : 'BTC',
});

const insufficientBalance = exceedsAccountBalance(
inputValue,
convertedValue,
sendAccount,
);

const wasInsufficient = useRef(false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is ref needed?

useEffect(() => {
if (insufficientBalance && !wasInsufficient.current) startShakeAnimation();
wasInsufficient.current = insufficientBalance;
}, [insufficientBalance, startShakeAnimation]);

const handleContinue = async (
inputValue: Money,
convertedValue: Money | undefined,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -196,11 +225,15 @@ export function SendInput() {
/>
</div>

{!exchangeRateError && (
<ConvertedMoneySwitcher
onSwitch={switchInputCurrency}
money={convertedValue}
/>
{insufficientBalance ? (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would rather show this somehow without hiding exchange rate

<p className="text-destructive text-sm">Insufficient balance</p>
) : (
!exchangeRateError && (
<ConvertedMoneySwitcher
onSwitch={switchInputCurrency}
money={convertedValue}
/>
)
)}
</div>

Expand Down Expand Up @@ -262,7 +295,7 @@ export function SendInput() {
<div className="flex items-center justify-end">
<Button
onClick={() => handleContinue(inputValue, convertedValue)}
disabled={inputValue.isZero()}
disabled={inputValue.isZero() || insufficientBalance}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if we should allow click but then show insufficient balance toast instead of showing it on every amount change?

loading={status === 'quoting' || isContinuing}
>
Continue
Expand Down
15 changes: 15 additions & 0 deletions apps/web-wallet/app/features/send/send-scanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Money } from '~/lib/money';
import { useNavigateWithViewTransition } from '~/lib/transitions/view-transition';
import type { Account } from '../accounts/account';
import { DomainError, getErrorMessage } from '../shared/error';
import { exceedsAccountBalance } from './balance-check';
import { useSendStore } from './send-provider';

/**
Expand Down Expand Up @@ -68,6 +69,20 @@ export default function SendScanner() {

const convertedAmount =
amount.currency !== sendAccount.currency ? convert(amount) : undefined;

if (exceedsAccountBalance(amount, convertedAmount, sendAccount)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is related to my comment on that other pr that changes send scanner and it kind of proves my point there. now scanner becomes even more aware of something it shouldn't be its resposibility

toast({
title: 'Insufficient balance',
description:
'The invoice amount is greater than your available balance.',
variant: 'destructive',
});
return navigate(buildLinkWithSearchParams('/send'), {
applyTo: 'oldView',
transition: 'slideDown',
});
}

const result = await continueSend(amount, convertedAmount);

if (!result.success) {
Expand Down
Loading