Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

frontend/coins: improve sats amount formatting #2059

Merged
merged 1 commit into from Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,8 @@
# Changelog

## Unreleased
- Improve sats amount readability adding thousands separator

## 4.37.0
- Bundle BitBox02 firmware version v9.14.0
- Enable auto HiDPI scaling to correctly manage scale factor on high density screens
Expand Down
10 changes: 2 additions & 8 deletions frontends/web/src/api/account.ts
Expand Up @@ -25,13 +25,7 @@ export type Fiat = 'AUD' | 'BRL' | 'BTC' | 'CAD' | 'CHF' | 'CNY' | 'EUR' | 'GBP'

export type ConversionUnit = Fiat | 'sat'

export type MainnetCoin = 'BTC' | 'LTC' | 'ETH';

export type TestnetCoin = 'TBTC' | 'TLTC' | 'GOETH';

export type Coin = MainnetCoin | TestnetCoin;

export type CoinWithSAT = Coin | 'sat' | 'tsat';
export type CoinUnit = 'BTC' | 'sat' | 'LTC' | 'ETH' | 'TBTC' | 'tsat' | 'TLTC' | 'GOETH';

export interface IActiveToken {
tokenCode: string;
Expand Down Expand Up @@ -141,7 +135,7 @@ export type Conversions = {
export interface IAmount {
amount: string;
conversions?: Conversions;
unit: Coin;
unit: CoinUnit;
}

export interface IBalance {
Expand Down
3 changes: 3 additions & 0 deletions frontends/web/src/components/amount/amount.module.css
@@ -0,0 +1,3 @@
.space {
margin-left: 0.5ch;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I ended up with 0.5 because 1 seemed a bit too much imho

}
179 changes: 179 additions & 0 deletions frontends/web/src/components/amount/amount.test.tsx
@@ -0,0 +1,179 @@
/**
* Copyright 2023 Shift Crypto AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { render } from '@testing-library/react';
import { Amount } from './amount';
import { CoinUnit, ConversionUnit } from './../../api/account';

describe('Amount formatting', () => {

describe('sat amounts', () => {
let coins: CoinUnit[] = ['sat', 'tsat'];
coins.forEach(coin => {
it('12345678901234 ' + coin + ' with removeBtcTrailingZeroes enabled gets spaced', () => {
const { getByTestId } = render(<Amount amount="12345678901234" unit={coin} removeBtcTrailingZeroes/>);
const blocks = getByTestId('amountBlocks');
expect(blocks.innerHTML).toBe(
'<span class="">12</span>' +
'<span class="space">345</span>' +
'<span class="space">678</span>' +
'<span class="space">901</span>' +
'<span class="space">234</span>');
});

it('1234567 ' + coin + ' with removeBtcTrailingZeroes enabled gets spaced', () => {
const { getByTestId } = render(<Amount amount="1234567" unit={coin} removeBtcTrailingZeroes/>);
const blocks = getByTestId('amountBlocks');
expect(blocks.innerHTML).toBe(
'<span class="">1</span>' +
'<span class="space">234</span>' +
'<span class="space">567</span>');
});

it('12345 ' + coin + ' with removeBtcTrailingZeroes enabled gets spaced', () => {
const { getByTestId } = render(<Amount amount="12345" unit={coin} removeBtcTrailingZeroes/>);
const blocks = getByTestId('amountBlocks');
expect(blocks.innerHTML).toBe(
'<span class="">12</span>' +
'<span class="space">345</span>');
});

it('21 ' + coin + ' with removeBtcTrailingZeroes enabled gets spaced', () => {
const { getByTestId } = render(<Amount amount="21" unit={coin} removeBtcTrailingZeroes/>);
const blocks = getByTestId('amountBlocks');
expect(blocks.innerHTML).toBe('<span class="">21</span>');
});

it('12345678901234 ' + coin + ' with removeBtcTrailingZeroes disabled gets spaced', () => {
const { getByTestId } = render(<Amount amount="12345678901234" unit={coin}/>);
const blocks = getByTestId('amountBlocks');
expect(blocks.innerHTML).toBe(
'<span class="">12</span>' +
'<span class="space">345</span>' +
'<span class="space">678</span>' +
'<span class="space">901</span>' +
'<span class="space">234</span>');
});

it('1234567 ' + coin + ' with removeBtcTrailingZeroes disabled gets spaced', () => {
const { getByTestId } = render(<Amount amount="1234567" unit={coin}/>);
const blocks = getByTestId('amountBlocks');
expect(blocks.innerHTML).toBe(
'<span class="">1</span>' +
'<span class="space">234</span>' +
'<span class="space">567</span>');
});

it('12345 ' + coin + ' with removeBtcTrailingZeroes disabled gets spaced', () => {
const { getByTestId } = render(<Amount amount="12345" unit={coin}/>);
const blocks = getByTestId('amountBlocks');
expect(blocks.innerHTML).toBe(
'<span class="">12</span>' +
'<span class="space">345</span>');
});

it('21 ' + coin + ' with removeBtcTrailingZeroes disabled gets spaced', () => {
const { getByTestId } = render(<Amount amount="21" unit={coin}/>);
const blocks = getByTestId('amountBlocks');
expect(blocks.innerHTML).toBe('<span class="">21</span>');
});
});
});

describe('BTC/LTC coins amounts', () => {
let coins: CoinUnit[] = ['BTC', 'TBTC', 'LTC', 'TLTC'];
coins.forEach(coin => {
it('10.00000000 ' + coin + ' with removeBtcTrailingZeroes enabled becomes 10', () => {
const { container } = render(<Amount amount="10.00000000" unit={coin} removeBtcTrailingZeroes/>);
expect(container).toHaveTextContent('10');
});
it('10.12300000 ' + coin + ' with removeBtcTrailingZeroes enabled becomes 10.123', () => {
const { container } = render(<Amount amount="10.12300000" unit={coin} removeBtcTrailingZeroes/>);
expect(container).toHaveTextContent('10.123');
});
it('42 ' + coin + ' with removeBtcTrailingZeroes enabled stays 42', () => {
const { container } = render(<Amount amount="42" unit={coin} removeBtcTrailingZeroes/>);
expect(container).toHaveTextContent('42');
});
it('10.00000000 ' + coin + ' with removeBtcTrailingZeroes disabled stays 10.00000000', () => {
const { container } = render(<Amount amount="10.00000000" unit={coin}/>);
expect(container).toHaveTextContent('10.00000000');
});
it('10.12300000 ' + coin + ' with removeBtcTrailingZeroes disabled stays 10.12300000', () => {
const { container } = render(<Amount amount="10.12300000" unit={coin}/>);
expect(container).toHaveTextContent('10.12300000');
});
it('42 ' + coin + ' with removeBtcTrailingZeroes disabled stays 42', () => {
const { container } = render(<Amount amount="42" unit={coin}/>);
expect(container).toHaveTextContent('42');
});

});
});

describe('non BTC coins amounts', () => {
let coins: CoinUnit[] = ['ETH', 'GOETH'];
coins.forEach(coin => {
it('10.00000000 ' + coin + ' with removeBtcTrailingZeroes enabled stays 10.00000000', () => {
const { container } = render(<Amount amount="10.00000000" unit={coin} removeBtcTrailingZeroes/>);
expect(container).toHaveTextContent('10.00000000');
});
it('10.12300000 ' + coin + ' with removeBtcTrailingZeroes enabled stays 10.12300000', () => {
const { container } = render(<Amount amount="10.12300000" unit={coin} removeBtcTrailingZeroes/>);
expect(container).toHaveTextContent('10.12300000');
});
it('42 ' + coin + ' with removeBtcTrailingZeroes enabled stays 42', () => {
const { container } = render(<Amount amount="42" unit={coin} removeBtcTrailingZeroes/>);
expect(container).toHaveTextContent('42');
});
it('10.00000000 ' + coin + ' with removeBtcTrailingZeroes disabled stays 10.00000000', () => {
const { container } = render(<Amount amount="10.00000000" unit={coin}/>);
expect(container).toHaveTextContent('10.00000000');
});
it('10.12300000 ' + coin + ' with removeBtcTrailingZeroes disabled stays 10.12300000', () => {
const { container } = render(<Amount amount="10.12300000" unit={coin}/>);
expect(container).toHaveTextContent('10.12300000');
});
it('42 ' + coin + ' with removeBtcTrailingZeroes disabled stays 42', () => {
const { container } = render(<Amount amount="42" unit={coin}/>);
expect(container).toHaveTextContent('42');
});
});
});

describe('fiat amounts', () => {
let fiatCoins: ConversionUnit[] = ['USD', 'EUR', 'CHF'];
fiatCoins.forEach(coin => {
it('1\'340.25 ' + coin + ' with removeBtcTrailingZeroes enabled stays 1\'340.25', () => {
const { container } = render(<Amount amount="1'340.25" unit={coin} removeBtcTrailingZeroes/>);
expect(container).toHaveTextContent('1\'340.25');
});
it('218.00 ' + coin + ' with removeBtcTrailingZeroes enabled stays 218.00', () => {
const { container } = render(<Amount amount="218.00" unit={coin} removeBtcTrailingZeroes/>);
expect(container).toHaveTextContent('218.00');
});
it('1\'340.25 ' + coin + ' with removeBtcTrailingZeroes disabled stays 1\'340.25', () => {
const { container } = render(<Amount amount="1'340.25" unit={coin}/>);
expect(container).toHaveTextContent('1\'340.25');
});
it('218.00 ' + coin + ' with removeBtcTrailingZeroes disabled stays 218.00', () => {
const { container } = render(<Amount amount="218.00" unit={coin}/>);
expect(container).toHaveTextContent('218.00');
});

});
});
});
58 changes: 58 additions & 0 deletions frontends/web/src/components/amount/amount.tsx
@@ -0,0 +1,58 @@
/**
* Copyright 2023 Shift Crypto AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import style from './amount.module.css';
import { CoinUnit, ConversionUnit } from './../../api/account';

type TProps = {
amount: string;
unit: CoinUnit | ConversionUnit;
removeBtcTrailingZeroes?: boolean;
};

export const Amount = ({ amount, unit, removeBtcTrailingZeroes }: TProps) => {
const formatSats = (amount: string): JSX.Element => {
const blocks: JSX.Element[] = [];
const blockSize = 3;

for (let i = amount.length; i > 0 ; i -= blockSize) {
const start = Math.max(0, i - blockSize);

blocks.push(
<span key={'block_' + blocks.length} className={start === 0 ? '' : style.space}>
{amount.slice(start, i)}
</span>
);
}

return <span data-testid={'amountBlocks'}>{blocks.reverse()}</span>;
};

switch (unit) {
case 'BTC':
case 'TBTC':
case 'LTC':
case 'TLTC':
if (removeBtcTrailingZeroes && amount.includes('.')) {
return <>{amount.replace(/\.?0+$/, '')}</>;
}
break;
case 'sat':
case 'tsat':
return formatSats(amount);
}
return <>{amount}</>;

};
32 changes: 20 additions & 12 deletions frontends/web/src/components/balance/balance.tsx
@@ -1,6 +1,6 @@
/**
* Copyright 2018 Shift Devices AG
* Copyright 2021 Shift Crypto AG
* Copyright 2023 Shift Crypto AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,7 +18,7 @@
import { useTranslation } from 'react-i18next';
import { IBalance } from '../../api/account';
import { FiatConversion } from '../../components/rates/rates';
import { bitcoinRemoveTrailingZeroes } from '../../utils/trailing-zeroes';
import { Amount } from '../../components/amount/amount';
import style from './balance.module.css';

type TProps = {
Expand All @@ -37,28 +37,36 @@ export const Balance = ({
);
}

// remove trailing zeroes from Bitcoin balance
const availableBalance = bitcoinRemoveTrailingZeroes(balance.available.amount, balance.available.unit);
const incomingBalance = bitcoinRemoveTrailingZeroes(balance.incoming.amount, balance.incoming.unit);

return (
<header className={style.balance}>
<table className={style.balanceTable}>
<tbody>
<tr data-testid="availableBalance">
<td className={style.availableAmount}>{availableBalance}</td>
<td className={style.availableAmount}>
<Amount
amount={balance.available.amount}
unit={balance.available.unit}
removeBtcTrailingZeroes/>
</td>
<td className={style.availableUnit}>{balance.available.unit}</td>
</tr>
<FiatConversion amount={balance.available} tableRow noAction={noRotateFiat} noBtcZeroes/>
</tbody>
</table>
{
balance.hasIncoming && (
<p data-testid="incomingBalance" className={style.pendingBalance}>
{t('account.incoming')} +{incomingBalance} {balance.incoming.unit} /
<span className={style.incomingConversion}>
{' '}
<FiatConversion amount={balance.incoming} noBtcZeroes/>
<p className={style.pendingBalance}>
{t('account.incoming')}
<span data-testid="incomingBalance">
+<Amount
amount={balance.incoming.amount}
unit={balance.incoming.unit}
removeBtcTrailingZeroes/>
{' '}{balance.incoming.unit} /
<span className={style.incomingConversion}>
{' '}
<FiatConversion amount={balance.incoming} noBtcZeroes/>
</span>
</span>
</p>
)
Expand Down
22 changes: 10 additions & 12 deletions frontends/web/src/components/rates/rates.tsx
Expand Up @@ -21,7 +21,7 @@ import { reinitializeAccounts } from '../../api/backend';
import { share } from '../../decorators/share';
import { Store } from '../../decorators/store';
import { setConfig } from '../../utils/config';
import { bitcoinRemoveTrailingZeroes } from '../../utils/trailing-zeroes';
import { Amount } from '../../components/amount/amount';
import { equal } from '../../utils/equal';
import { apiGet } from '../../utils/request';
import style from './rates.module.css';
Expand Down Expand Up @@ -128,27 +128,25 @@ function Conversion({
btcUnit,
}: TProps) {

let formattedValue = '---';
let formattedAmount = <>{'---'}</>;
let isAvailable = false;

var activeUnit: ConversionUnit = active;
if (active === 'BTC' && btcUnit === 'sat') {
activeUnit = 'sat';
}

// amount.conversions[active] can be empty in recent transactions.
if (amount && amount.conversions && amount.conversions[active] && amount.conversions[active] !== '') {
isAvailable = true;
formattedValue = amount.conversions[active];
if (noBtcZeroes) {
formattedValue = bitcoinRemoveTrailingZeroes(formattedValue, active);
}
formattedAmount = <Amount amount={amount.conversions[active]} unit={activeUnit} removeBtcTrailingZeroes={!!noBtcZeroes}/>;
}

var activeUnit: ConversionUnit = active;
if (active === 'BTC' && btcUnit === 'sat') {
activeUnit = 'sat';
}

if (tableRow) {
return (
<tr className={unstyled ? '' : style.fiatRow}>
<td className={unstyled ? '' : style.availableFiatAmount}>{formattedValue}</td>
<td className={unstyled ? '' : style.availableFiatAmount}>{formattedAmount}</td>
{
!noAction && (
<td className={unstyled ? '' : style.availableFiatUnit} onClick={rotateFiat}>{activeUnit}</td>
Expand All @@ -165,7 +163,7 @@ function Conversion({
return (
<span className={ `${style.rates} ${!isAvailable ? style.notAvailable : ''}`}>
{isAvailable ? sign : ''}
{formattedValue}
{formattedAmount}
{' '}
{
!skipUnit && !noAction && (
Expand Down