Skip to content

Commit 9216243

Browse files
authored
feat(ui): add TokenBalanceField component for displaying token balances (#142)
Introduce a new reusable component to display token balances with truncation and tooltip functionality. The component replaces inline token balance display logic in permissionHandlerContent and includes utility function for decimal truncation.
1 parent e9e5c88 commit 9216243

File tree

6 files changed

+116
-6
lines changed

6 files changed

+116
-6
lines changed

packages/gator-permissions-snap/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snap-7715-permissions.git"
88
},
99
"source": {
10-
"shasum": "nODLXP/oHwoGcAqLt3fXCkTszV4MiIPEL55PFWvw2us=",
10+
"shasum": "2vOYwTM30DDpYzGG2NRfWFDCJ+jxKa36N5T9C1BnyWk=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/gator-permissions-snap/src/core/permissionHandlerContent.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
TextField,
1818
TooltipIcon,
1919
TokenField,
20+
TokenBalanceField,
2021
} from '../ui/components';
2122

2223
export const ACCOUNT_SELECTOR_NAME = 'account-selector';
@@ -81,11 +82,7 @@ export const PermissionHandlerContent = ({
8182
chainId,
8283
explorerUrl,
8384
}: PermissionHandlerContentProps): SnapElement => {
84-
const tokenBalanceComponent = tokenBalance ? (
85-
<Text>{tokenBalance} available</Text>
86-
) : (
87-
<Skeleton />
88-
);
85+
const tokenBalanceComponent = TokenBalanceField({ tokenBalance });
8986

9087
const fiatBalanceComponent = tokenBalanceFiat ? (
9188
<Text>{tokenBalanceFiat}</Text>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Skeleton, Text, Tooltip } from '@metamask/snaps-sdk/jsx';
2+
3+
import { truncateDecimalPlaces } from '../../utils/string';
4+
5+
type TokenBalanceFieldProps = {
6+
tokenBalance: string | undefined;
7+
};
8+
9+
/**
10+
* A component that displays the token balance.
11+
* @param props - The component props.
12+
* @param props.tokenBalance - The token balance to display.
13+
* @returns A JSX element containing the token balance or a skeleton if not available.
14+
*/
15+
export const TokenBalanceField = ({ tokenBalance }: TokenBalanceFieldProps) => {
16+
if (!tokenBalance) {
17+
return <Skeleton />;
18+
}
19+
const truncatedTokenBalance = truncateDecimalPlaces(tokenBalance);
20+
if (truncatedTokenBalance === tokenBalance) {
21+
return <Text>{truncatedTokenBalance} available</Text>;
22+
}
23+
return (
24+
<Tooltip content={<Text>{tokenBalance}</Text>}>
25+
<Text>{truncatedTokenBalance} available</Text>
26+
</Tooltip>
27+
);
28+
};

packages/gator-permissions-snap/src/ui/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export { TextField } from './TextField';
77
export { TokenIcon } from './TokenIcon';
88
export { TooltipIcon } from './TooltipIcon';
99
export { TokenField } from './TokenField';
10+
export { TokenBalanceField } from './TokenBalanceField';

packages/gator-permissions-snap/src/utils/string.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,34 @@ export function shortenAddress(address = '') {
5858
skipCharacterInEnd: false,
5959
});
6060
}
61+
62+
/**
63+
* Truncates the decimal part of a numeric string to a specified number of digits.
64+
* If the input is not a valid number, returns the original value.
65+
*
66+
* @param value - The numeric string to truncate (e.g., "123.45678").
67+
* @param decimalPlaces - The maximum number of decimal places to keep. Defaults to 5.
68+
* @returns The truncated numeric string.
69+
* @throws {Error} If the input value is not a valid number.
70+
*
71+
* @example
72+
* truncateDecimalPlaces("123.456789123", 4); // "123.4567"
73+
* truncateDecimalPlaces("100", 4); // "100"
74+
* truncateDecimalPlaces("abc", 4); // "abc"
75+
*/
76+
export function truncateDecimalPlaces(
77+
value: string,
78+
decimalPlaces = 5,
79+
): string {
80+
const trimmedValue = value.trim();
81+
// Only allow optional minus, digits, optional decimal and digits
82+
if (!/^[-+]?\d+(\.\d+)?$/u.test(trimmedValue)) {
83+
throw new Error(`Invalid number: ${value}`);
84+
}
85+
86+
const [whole = '', decimals = ''] = trimmedValue.split('.');
87+
if (!decimals) {
88+
return whole;
89+
}
90+
return `${whole}.${decimals.slice(0, decimalPlaces)}`;
91+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { truncateDecimalPlaces } from '../../src/utils/string';
2+
3+
describe('truncateDecimalPlaces', () => {
4+
it('truncates decimals to default 5 places', () => {
5+
expect(truncateDecimalPlaces('123.456789123')).toBe('123.45678');
6+
});
7+
8+
it('truncates decimals to specified places', () => {
9+
expect(truncateDecimalPlaces('123.456789123', 4)).toBe('123.4567');
10+
});
11+
12+
it('returns original if decimals are less than or equal to specified', () => {
13+
expect(truncateDecimalPlaces('123.45', 8)).toBe('123.45');
14+
expect(truncateDecimalPlaces('123.4567', 4)).toBe('123.4567');
15+
});
16+
17+
it('returns integer part if no decimals', () => {
18+
expect(truncateDecimalPlaces('100')).toBe('100');
19+
});
20+
21+
it('trims input before processing', () => {
22+
expect(truncateDecimalPlaces(' 123.456789123 ', 3)).toBe('123.456');
23+
});
24+
25+
it('handles negative numbers', () => {
26+
expect(truncateDecimalPlaces('-123.456789', 2)).toBe('-123.45');
27+
});
28+
29+
it('handles positive sign', () => {
30+
expect(truncateDecimalPlaces('+123.456789', 2)).toBe('+123.45');
31+
});
32+
33+
it('throws error for invalid input', () => {
34+
expect(() => truncateDecimalPlaces('abc.def')).toThrow(
35+
'Invalid number: abc.def',
36+
);
37+
expect(() => truncateDecimalPlaces('foo')).toThrow('Invalid number: foo');
38+
expect(() => truncateDecimalPlaces('')).toThrow('Invalid number: ');
39+
});
40+
41+
it('handles scientific notation', () => {
42+
expect(() => truncateDecimalPlaces('1e-8')).toThrow('Invalid number: 1e-8');
43+
expect(() => truncateDecimalPlaces('1.23e2')).toThrow(
44+
'Invalid number: 1.23e2',
45+
);
46+
});
47+
48+
it('throws error for more than one dot', () => {
49+
expect(() => truncateDecimalPlaces('1.2.3')).toThrow(
50+
'Invalid number: 1.2.3',
51+
);
52+
});
53+
});

0 commit comments

Comments
 (0)