Skip to content

Commit b87c04e

Browse files
GBOYEEroot
andauthored
feat(mobile): add send max amount helper (#84) (#477)
Co-authored-by: root <root@vmi3321457.contaboserver.net>
1 parent 9a15209 commit b87c04e

3 files changed

Lines changed: 90 additions & 2 deletions

File tree

app/send.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
validateMemo,
2828
} from "../src/utils/validation";
2929
import { resolveAddressLabel } from "../src/utils/contacts";
30-
import { formatAmount } from "../src/utils/amount";
30+
import { formatAmount, getMaxSendableAmount } from "../src/utils/amount";
3131
import { WALLET_SECRET_ACCESS_MESSAGE } from "../src/utils/walletStorageErrors";
3232
import {
3333
Send as SendIcon,
@@ -140,6 +140,16 @@ export default function SendScreen() {
140140
const handleScanClose = () => {
141141
setIsScanning(false);
142142
};
143+
144+
const handleSetMaxAmount = () => {
145+
const maxAmount = getMaxSendableAmount(balance);
146+
setAmount(maxAmount);
147+
setErrors((prev) => ({
148+
...prev,
149+
amount: validateAmount(maxAmount, balance) ?? undefined,
150+
}));
151+
};
152+
143153
const handleSend = () => {
144154
const fieldErrors: FieldErrors = {
145155
destination: validateAddress(destination, publicKey) ?? undefined,
@@ -245,6 +255,18 @@ export default function SendScreen() {
245255
error={errors.amount}
246256
keyboardType="decimal-pad"
247257
helperText={`Available balance: ${formatAmount(balance)} XLM`}
258+
rightIcon={
259+
<TouchableOpacity
260+
onPress={handleSetMaxAmount}
261+
accessibilityLabel="Send maximum amount"
262+
accessibilityRole="button"
263+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
264+
>
265+
<Text style={{ color: colors.primary, fontWeight: "600", fontSize: 13 }}>
266+
Send Max
267+
</Text>
268+
</TouchableOpacity>
269+
}
248270
/>
249271

250272
<FormField

src/utils/amount.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,38 @@
1-
import { formatAmount } from './amount';
1+
import { formatAmount, getMaxSendableAmount } from './amount';
2+
3+
describe('getMaxSendableAmount', () => {
4+
it('returns 0 for null/undefined/empty balance', () => {
5+
expect(getMaxSendableAmount(null)).toBe('0');
6+
expect(getMaxSendableAmount(undefined)).toBe('0');
7+
expect(getMaxSendableAmount('')).toBe('0');
8+
});
9+
10+
it('returns 0 for zero or negative balance', () => {
11+
expect(getMaxSendableAmount('0')).toBe('0');
12+
expect(getMaxSendableAmount('-10')).toBe('0');
13+
});
14+
15+
it('subtracts the default MIN_XLM_RESERVE (1) from the balance', () => {
16+
expect(getMaxSendableAmount('100')).toBe('99');
17+
expect(getMaxSendableAmount('1.5')).toBe('0.5');
18+
expect(getMaxSendableAmount('1')).toBe('0');
19+
});
20+
21+
it('returns 0 when balance is less than reserve', () => {
22+
expect(getMaxSendableAmount('0.5')).toBe('0');
23+
expect(getMaxSendableAmount('0.0000001')).toBe('0');
24+
});
25+
26+
it('accepts a custom reserve parameter', () => {
27+
expect(getMaxSendableAmount('100', 2)).toBe('98');
28+
expect(getMaxSendableAmount('1', 0.5)).toBe('0.5');
29+
});
30+
31+
it('handles number inputs', () => {
32+
expect(getMaxSendableAmount(100)).toBe('99');
33+
expect(getMaxSendableAmount(0.5)).toBe('0');
34+
});
35+
});
236

337
describe('formatAmount', () => {
438
it('formats integers correctly', () => {

src/utils/amount.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
/**
2+
* Minimum XLM reserve that must stay in the wallet.
3+
* Accounts must keep this balance to remain active on the network.
4+
*/
5+
export const MIN_XLM_RESERVE = 1;
6+
7+
/**
8+
* Calculate the maximum amount that can be sent from a wallet,
9+
* accounting for the minimum XLM reserve requirement.
10+
*
11+
* @param balance The current wallet balance as a string or number
12+
* @param reserve Optional reserve override (defaults to MIN_XLM_RESERVE)
13+
* @returns The max sendable amount as a string, or '0' if balance is insufficient
14+
*/
15+
export function getMaxSendableAmount(
16+
balance: string | number | null | undefined,
17+
reserve: number = MIN_XLM_RESERVE,
18+
): string {
19+
if (balance === null || balance === undefined || balance === '') {
20+
return '0';
21+
}
22+
23+
const num = typeof balance === 'number' ? balance : parseFloat(balance);
24+
if (isNaN(num) || num <= 0) {
25+
return '0';
26+
}
27+
28+
const maxSendable = Math.max(0, num - reserve);
29+
// Return with up to 7 decimal places, stripping trailing zeros
30+
return maxSendable.toFixed(7).replace(/\.?0+$/, '');
31+
}
32+
133
/**
234
* Formats a numeric amount for display.
335
* Standardises displaying XLM and other asset amounts.

0 commit comments

Comments
 (0)