From 821b227fa763ca3b6caa28d8cda2f038a46d237b Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Mon, 13 Nov 2023 11:10:20 +0100 Subject: [PATCH] ui: improve balance spacing, coloring and grouping (#691) * chore: continue loop on errors in setIntervalDebounced * ui(balance): improve displaying balance value in BTC * chore: use cursor-pointer style class where applicable * ui(style): use only css to style bitcoin amount * refactor: add frozen flag to balance component * ui(balance): align colors with copy * ui(balance): deemphasize symbol on zero balance * test(balance): use testid to get balance element --- src/components/Balance.module.css | 72 ++++++++++ src/components/Balance.test.tsx | 70 +++++----- src/components/Balance.tsx | 125 +++++++++++++----- src/components/Cheatsheet.tsx | 2 +- src/components/MainWalletView.tsx | 2 +- src/components/Send/FeeBreakdown.tsx | 2 +- .../fb/FidelityBondSteps.module.css | 8 +- .../jar_details/UtxoList.module.css | 3 + src/components/jar_details/UtxoList.tsx | 1 + src/components/jars/Jar.module.css | 7 +- src/components/jars/Jar.tsx | 18 ++- src/utils.test.ts | 42 +++++- src/utils.ts | 33 ++--- 13 files changed, 272 insertions(+), 113 deletions(-) create mode 100644 src/components/Balance.module.css diff --git a/src/components/Balance.module.css b/src/components/Balance.module.css new file mode 100644 index 000000000..ced0b1632 --- /dev/null +++ b/src/components/Balance.module.css @@ -0,0 +1,72 @@ +:root { + --jam-balance-color: #212529; + --jam-balance-deemphasize-color: #9eacba; +} + +:root[data-theme='dark'] { + --jam-balance-color: #ffffff; + --jam-balance-deemphasize-color: #555c62; +} + +.frozen { + --jam-balance-color: #0d6efd; + --jam-balance-deemphasize-color: #7eb2ff; +} + +:root[data-theme='dark'] .frozen { + --jam-balance-color: #1372ff; + --jam-balance-deemphasize-color: #1153b5; +} + +.balance { + color: var(--jam-balance-color); +} + +.frozenSymbol { + order: -2; +} + +.bitcoinSymbol { + padding-right: 0.1em; + order: -1; +} + +.satsSymbol { + padding-right: 0.1em; + order: 6; +} + +.hideSymbol { + padding-left: 0.1em; + color: var(--jam-balance-deemphasize-color); +} + +.bitcoinAmount .fractionalPart :nth-child(3)::before, +.bitcoinAmount .fractionalPart :nth-child(6)::before { + content: '\202F'; +} + +/** Integer Part **/ +.bitcoinAmount[data-integer-part-is-zero="true"] .integerPart, +/** Decimal Point **/ +.bitcoinAmount[data-integer-part-is-zero="true"] .decimalPoint, +.bitcoinAmount[data-fractional-part-starts-with-zero="true"] .decimalPoint, +/** Fractional Part **/ +.bitcoinAmount[data-integer-part-is-zero="false"] .fractionalPart, +.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]), +.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"], +.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], +/** Symbol */ +.bitcoinAmount[data-raw-value="0"] + .bitcoinSymbol { + color: var(--jam-balance-deemphasize-color); +} + +.satsAmount[data-raw-value='0'], +.satsAmount[data-raw-value='0'] + .satsSymbol { + color: var(--jam-balance-deemphasize-color); +} diff --git a/src/components/Balance.test.tsx b/src/components/Balance.test.tsx index 08430cd38..9429387c6 100644 --- a/src/components/Balance.test.tsx +++ b/src/components/Balance.test.tsx @@ -1,8 +1,8 @@ -import { act } from 'react-dom/test-utils' +import { render } from '@testing-library/react' import user from '@testing-library/user-event' -import { render, screen } from '../testUtils' +import { act } from 'react-dom/test-utils' +import { screen } from '../testUtils' import { BTC, SATS } from '../utils' - import Balance from './Balance' describe('', () => { @@ -13,164 +13,164 @@ describe('', () => { it('should render BTC using satscomma formatting', () => { render() - expect(screen.getByText(`123.45 600 000`)).toBeInTheDocument() + expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`123.45600000`) }) it('should hide balance for BTC by default', () => { render() expect(screen.getByText(`*****`)).toBeInTheDocument() - expect(screen.queryByText(`123.45 600 000`)).not.toBeInTheDocument() + expect(screen.queryByTestId('bitcoin-amount')).not.toBeInTheDocument() }) it('should hide balance for SATS by default', () => { render() expect(screen.getByText(`*****`)).toBeInTheDocument() - expect(screen.queryByText(`123`)).not.toBeInTheDocument() + expect(screen.queryByTestId(`sats-amount`)).not.toBeInTheDocument() }) it('should render a string BTC value correctly as BTC', () => { render() - expect(screen.getByText(`123.03 224 961`)).toBeInTheDocument() + expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`123.03224961`) }) it('should render a string BTC value correctly as SATS', () => { render() - expect(screen.getByText(`12,303,224,961`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`12,303,224,961`) }) it('should render a zero string BTC value correctly as BTC', () => { render() - expect(screen.getByText(`0.00 000 000`)).toBeInTheDocument() + expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`0.00000000`) }) it('should render a zero string BTC value correctly as SATS', () => { render() - expect(screen.getByText(`0`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`0`) }) it('should render a large string BTC value correctly as BTC', () => { render() - expect(screen.getByText(`20,999,999.97 690 000`)).toBeInTheDocument() + expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`20,999,999.97690000`) }) it('should render a large string BTC value correctly as SATS', () => { render() - expect(screen.getByText(`2,099,999,997,690,000`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,099,999,997,690,000`) }) it('should render a max string BTC value correctly as BTC', () => { render() - expect(screen.getByText(`21,000,000.00 000 000`)).toBeInTheDocument() + expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`21,000,000.00000000`) }) it('should render a max string BTC value correctly as SATS', () => { render() - expect(screen.getByText(`2,100,000,000,000,000`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,100,000,000,000,000`) }) it('should render a string SATS value correctly as SATS', () => { render() - expect(screen.getByText(`43,000`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`43,000`) }) it('should render a string SATS value correctly as BTC', () => { render() - expect(screen.getByText(`0.00 043 000`)).toBeInTheDocument() + expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`0.00043000`) }) it('should render a zero string SATS value correctly as BTC', () => { render() - expect(screen.getByText(`0.00 000 000`)).toBeInTheDocument() + expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`0.00000000`) }) it('should render a zero string SATS value correctly as SATS', () => { render() - expect(screen.getByText(`0`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`0`) }) it('should render a large string SATS value correctly as BTC', () => { render() - expect(screen.getByText(`20,999,999.97 690 000`)).toBeInTheDocument() + expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`20,999,999.97690000`) }) it('should render a large string SATS value correctly as SATS', () => { render() - expect(screen.getByText(`2,099,999,997,690,000`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,099,999,997,690,000`) }) it('should render a max string SATS value correctly as BTC', () => { render() - expect(screen.getByText(`21,000,000.00 000 000`)).toBeInTheDocument() + expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`21,000,000.00000000`) }) it('should render a max string SATS value correctly as SATS', () => { render() - expect(screen.getByText(`2,100,000,000,000,000`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,100,000,000,000,000`) }) it('should toggle visibility of initially hidden balance on click by default', () => { render() - expect(screen.queryByText(`21`)).not.toBeInTheDocument() + expect(screen.queryByTestId(`sats-amount`)).not.toBeInTheDocument() expect(screen.getByText(`*****`)).toBeInTheDocument() act(() => { user.click(screen.getByText(`*****`)) }) - expect(screen.getByText(`21`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toBeInTheDocument() expect(screen.queryByText(`*****`)).not.toBeInTheDocument() act(() => { - user.click(screen.getByText(`21`)) + user.click(screen.getByTestId(`sats-amount`)) }) - expect(screen.queryByText(`21`)).not.toBeInTheDocument() + expect(screen.queryByTestId(`sats-amount`)).not.toBeInTheDocument() expect(screen.getByText(`*****`)).toBeInTheDocument() }) it('should NOT toggle visibility of initially hidden balance on click when disabled via flag', () => { render() - expect(screen.queryByText(`21`)).not.toBeInTheDocument() + expect(screen.queryByTestId(`sats-amount`)).not.toBeInTheDocument() expect(screen.getByText(`*****`)).toBeInTheDocument() act(() => { user.click(screen.getByText(`*****`)) }) - expect(screen.queryByText(`21`)).not.toBeInTheDocument() + expect(screen.queryByTestId(`sats-amount`)).not.toBeInTheDocument() expect(screen.getByText(`*****`)).toBeInTheDocument() }) it('should NOT toggle visibility of initially visible balance on click by default', () => { render() - expect(screen.getByText(`21`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toBeInTheDocument() expect(screen.queryByText(`*****`)).not.toBeInTheDocument() act(() => { - user.click(screen.getByText(`21`)) + user.click(screen.getByTestId(`sats-amount`)) }) - expect(screen.getByText(`21`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toBeInTheDocument() expect(screen.queryByText(`*****`)).not.toBeInTheDocument() }) it('should toggle visibility of initially visible balance on click when enabled via flag', () => { render() - expect(screen.getByText(`21`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toBeInTheDocument() expect(screen.queryByText(`*****`)).not.toBeInTheDocument() act(() => { - user.click(screen.getByText(`21`)) + user.click(screen.getByTestId(`sats-amount`)) }) - expect(screen.queryByText(`21`)).not.toBeInTheDocument() + expect(screen.queryByTestId(`sats-amount`)).not.toBeInTheDocument() expect(screen.getByText(`*****`)).toBeInTheDocument() act(() => { user.click(screen.getByText(`*****`)) }) - expect(screen.getByText(`21`)).toBeInTheDocument() + expect(screen.getByTestId(`sats-amount`)).toBeInTheDocument() expect(screen.queryByText(`*****`)).not.toBeInTheDocument() }) }) diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx index 19ba8194f..9d4943759 100644 --- a/src/components/Balance.tsx +++ b/src/components/Balance.tsx @@ -1,6 +1,8 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { SATS, BTC, btcToSats, satsToBtc, formatBtc, formatSats, isValidNumber } from '../utils' +import { MouseEventHandler, useCallback, useEffect, useMemo, useState } from 'react' +import classNames from 'classnames' import Sprite from './Sprite' +import { SATS, BTC, btcToSats, satsToBtc, isValidNumber, formatBtc, formatSats } from '../utils' +import styles from './Balance.module.css' const DISPLAY_MODE_BTC = 0 const DISPLAY_MODE_SATS = 1 @@ -13,18 +15,70 @@ const getDisplayMode = (unit: Unit, showBalance: boolean) => { return DISPLAY_MODE_HIDDEN } +const DECIMAL_POINT_CHAR = '.' + +const BitcoinAmountComponent = ({ value }: { value: number }) => { + const numberString = formatBtc(value) + const [integerPart, fractionalPart] = numberString.split(DECIMAL_POINT_CHAR) + + const fractionPartArray = fractionalPart.split('') + const integerPartIsZero = integerPart === '0' + const fractionalPartStartsWithZero = fractionPartArray[0] === '0' + + return ( + + {integerPart} + {DECIMAL_POINT_CHAR} + + {fractionPartArray.map((digit, index) => ( + + {digit} + + ))} + + + ) +} + +const SatsAmountComponent = ({ value }: { value: number }) => { + return ( + + {formatSats(value)} + + ) +} + +const BTC_SYMBOL = {'\u20BF'} + +const SAT_SYMBOL = + +const FROZEN_SYMBOL = ( + +) + interface BalanceComponentProps { symbol: JSX.Element - value: any - symbolIsPrefix: boolean + value: JSX.Element + frozen?: boolean } -const BalanceComponent = ({ symbol, value, symbolIsPrefix }: BalanceComponentProps) => { +const BalanceComponent = ({ symbol, value, frozen = false }: BalanceComponentProps) => { return ( - - {symbolIsPrefix && symbol} - {value} - {!symbolIsPrefix && symbol} + + {frozen && FROZEN_SYMBOL} + {value} + {symbol} ) } @@ -48,6 +102,7 @@ interface BalanceProps { convertToUnit: Unit showBalance?: boolean enableVisibilityToggle?: boolean + frozen?: boolean } /** @@ -58,6 +113,7 @@ export default function Balance({ convertToUnit, showBalance = false, enableVisibilityToggle = !showBalance, + frozen = false, }: BalanceProps) { const [isBalanceVisible, setIsBalanceVisible] = useState(showBalance) const displayMode = useMemo(() => getDisplayMode(convertToUnit, isBalanceVisible), [convertToUnit, isBalanceVisible]) @@ -66,7 +122,7 @@ export default function Balance({ setIsBalanceVisible(showBalance) }, [showBalance]) - const toggleVisibility = useCallback((e) => { + const toggleVisibility: MouseEventHandler = useCallback((e) => { e.preventDefault() e.stopPropagation() @@ -77,13 +133,9 @@ export default function Balance({ if (displayMode === DISPLAY_MODE_HIDDEN) { return ( - - - } - value={'*****'} - symbolIsPrefix={false} + symbol={} + value={{'*****'}} + frozen={frozen} /> ) } @@ -91,7 +143,7 @@ export default function Balance({ const valueNumber = parseFloat(valueString) if (!isValidNumber(valueNumber)) { console.warn(' component expects number input as string') - return } value={valueString} symbolIsPrefix={false} /> + return } value={<>{valueString}} frozen={frozen} /> } // Treat integers as sats. @@ -99,32 +151,41 @@ export default function Balance({ // Treat decimal numbers as btc. const valueIsBtc = !valueIsSats && valueString.indexOf('.') > -1 - const btcSymbol = ( - - {'\u20BF'} - - ) - const satSymbol = - if (valueIsBtc && displayMode === DISPLAY_MODE_BTC) - return + return ( + } frozen={frozen} /> + ) if (valueIsSats && displayMode === DISPLAY_MODE_SATS) - return + return ( + } frozen={frozen} /> + ) if (valueIsBtc && displayMode === DISPLAY_MODE_SATS) - return + return ( + } + frozen={frozen} + /> + ) if (valueIsSats && displayMode === DISPLAY_MODE_BTC) - return + return ( + } + frozen={frozen} + /> + ) console.warn(' component cannot determine balance format') - return } value={valueString} symbolIsPrefix={false} /> - }, [valueString, displayMode]) + return } value={<>{valueString}} frozen={frozen} /> + }, [valueString, displayMode, frozen]) if (!enableVisibilityToggle) { return <>{balanceComponent} } else { return ( - + {balanceComponent} ) diff --git a/src/components/Cheatsheet.tsx b/src/components/Cheatsheet.tsx index 4faaa2638..f72be2876 100644 --- a/src/components/Cheatsheet.tsx +++ b/src/components/Cheatsheet.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren } from 'react' +import { PropsWithChildren } from 'react' import * as rb from 'react-bootstrap' import { Link } from 'react-router-dom' import { Trans, useTranslation } from 'react-i18next' diff --git a/src/components/MainWalletView.tsx b/src/components/MainWalletView.tsx index 7af7f68ca..3e3f074e4 100644 --- a/src/components/MainWalletView.tsx +++ b/src/components/MainWalletView.tsx @@ -149,7 +149,7 @@ export default function MainWalletView({ wallet }: MainWalletViewProps) { ) : ( - settingsDispatch({ showBalance: !settings.showBalance })} style={{ cursor: 'pointer' }}> + settingsDispatch({ showBalance: !settings.showBalance })} className="cursor-pointer"> {!currentWalletInfo || isLoading ? ( ) : ( diff --git a/src/components/Send/FeeBreakdown.tsx b/src/components/Send/FeeBreakdown.tsx index a44242806..0b6cb46cc 100644 --- a/src/components/Send/FeeBreakdown.tsx +++ b/src/components/Send/FeeBreakdown.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames' import Balance from '../Balance' import * as rb from 'react-bootstrap' import { useSettings } from '../../context/SettingsContext' -import { SATS, formatSats, factorToPercentage } from '../../utils' +import { SATS, factorToPercentage, formatSats } from '../../utils' import { FeeValues } from '../../hooks/Fees' import { AmountSats } from '../../libs/JmWalletApi' diff --git a/src/components/fb/FidelityBondSteps.module.css b/src/components/fb/FidelityBondSteps.module.css index 0123dd2dd..5c486338d 100644 --- a/src/components/fb/FidelityBondSteps.module.css +++ b/src/components/fb/FidelityBondSteps.module.css @@ -11,12 +11,8 @@ gap: 2rem; } -.jarsContainer :global .balance-value-hook { - font-size: 0.7rem; -} - -.jarsContainer :global .balance-symbol-hook { - font-size: 0.7rem; +.jarsContainer :global .balance-hook { + font-size: 0.75rem; } @media only screen and (min-width: 768px) { diff --git a/src/components/jar_details/UtxoList.module.css b/src/components/jar_details/UtxoList.module.css index 8c50e6d62..81a0d1648 100644 --- a/src/components/jar_details/UtxoList.module.css +++ b/src/components/jar_details/UtxoList.module.css @@ -32,6 +32,9 @@ .utxoList tr.frozen .quickFreezeUnfreezeBtn { color: var(--bs-red); } +.utxoList tr.frozen :global .balance-hook .frozen-symbol-hook { + display: none !important; +} .utxoList .utxoIcon > .iconLocked { margin-bottom: 3px; diff --git a/src/components/jar_details/UtxoList.tsx b/src/components/jar_details/UtxoList.tsx index 6f7f1bbf3..2178ef82b 100644 --- a/src/components/jar_details/UtxoList.tsx +++ b/src/components/jar_details/UtxoList.tsx @@ -324,6 +324,7 @@ const UtxoList = ({ valueString={utxo.value.toString()} convertToUnit={settings.unit} showBalance={settings.showBalance} + frozen={utxo.frozen} /> diff --git a/src/components/jars/Jar.module.css b/src/components/jars/Jar.module.css index 126bf799e..b24d8b0a1 100644 --- a/src/components/jars/Jar.module.css +++ b/src/components/jars/Jar.module.css @@ -54,15 +54,12 @@ - 1.5ch for the dot and spaces */ min-width: 11.5ch; - + min-height: 1rem; font-size: 0.8rem; } .frozen.jarBalance { - --bs-code-color: var(--bs-blue); - color: var(--bs-blue); - font-size: 0.75rem; - min-height: 1rem; + font-size: 0.7rem; } .selectableJarContainer { diff --git a/src/components/jars/Jar.tsx b/src/components/jars/Jar.tsx index cdf487a27..4def150e3 100644 --- a/src/components/jars/Jar.tsx +++ b/src/components/jars/Jar.tsx @@ -129,16 +129,14 @@ const Jar = ({ index, balance, frozenBalance, fillLevel, isOpen = false }: JarPr
- {frozenBalance && frozenBalance > 0 ? ( - <> - - - - ) : null} + {frozenBalance > 0 && ( + + )}
diff --git a/src/utils.test.ts b/src/utils.test.ts index a2ab28f5e..c10b50981 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,4 +1,12 @@ -import { shortenStringMiddle, percentageToFactor, factorToPercentage, toSemVer, UNKNOWN_VERSION } from './utils' +import { + shortenStringMiddle, + percentageToFactor, + factorToPercentage, + toSemVer, + UNKNOWN_VERSION, + formatSats, + formatBtc, +} from './utils' describe('shortenStringMiddle', () => { it('should shorten string in the middle', () => { @@ -113,3 +121,35 @@ describe('toSemVer', () => { expect(toSemVer('21million')).toBe(UNKNOWN_VERSION) }) }) + +describe('formatSats', () => { + it('should format given value as amount in sats', () => { + expect(formatSats(-1_000)).toBe('-1,000') + expect(formatSats(-21)).toBe('-21') + expect(formatSats(-1)).toBe('-1') + expect(formatSats(0)).toBe('0') + expect(formatSats(1)).toBe('1') + expect(formatSats(999)).toBe('999') + expect(formatSats(1_000)).toBe('1,000') + expect(formatSats(2099999997690000)).toBe('2,099,999,997,690,000') + expect(formatSats(2100000000000000)).toBe('2,100,000,000,000,000') + }) +}) + +describe('formatBtc', () => { + it('should format given value as amount in BTC', () => { + expect(formatBtc(-1_000)).toBe('-1,000.00000000') + expect(formatBtc(-21)).toBe('-21.00000000') + expect(formatBtc(-1)).toBe('-1.00000000') + expect(formatBtc(0)).toBe('0.00000000') + expect(formatBtc(1)).toBe('1.00000000') + expect(formatBtc(123.03224961)).toBe('123.03224961') + expect(formatBtc(123.456)).toBe('123.45600000') + expect(formatBtc(1_000)).toBe('1,000.00000000') + expect(formatBtc(20999999.9769)).toBe('20,999,999.97690000') + expect(formatBtc(20999999.9769)).toBe('20,999,999.97690000') + expect(formatBtc(21000000)).toBe('21,000,000.00000000') + expect(formatBtc(21000000.0)).toBe('21,000,000.00000000') + expect(formatBtc(21000000.0)).toBe('21,000,000.00000000') + }) +}) diff --git a/src/utils.ts b/src/utils.ts index 5f53de08e..48b2e0159 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,11 +3,13 @@ import { OfferType, WalletFileName } from './libs/JmWalletApi' const BTC_FORMATTER = new Intl.NumberFormat('en-US', { minimumIntegerDigits: 1, minimumFractionDigits: 8, + maximumFractionDigits: 8, }) const SATS_FORMATTER = new Intl.NumberFormat('en-US', { minimumIntegerDigits: 1, minimumFractionDigits: 0, + maximumFractionDigits: 0, }) export const BTC: Unit = 'BTC' @@ -32,27 +34,11 @@ export const btcToSats = (value: string) => Math.round(parseFloat(value) * 10000 export const satsToBtc = (value: string) => parseInt(value, 10) / 100000000 -export const formatBtc = (value: number) => { - const decimalPoint = '\u002E' - const nbHalfSpace = '\u202F' +export const formatBtc = (value: number) => BTC_FORMATTER.format(value) - const numberString = BTC_FORMATTER.format(value) +export const formatSats = (value: number) => SATS_FORMATTER.format(value) - const [integerPart, fractionalPart] = numberString.split(decimalPoint) - - const formattedFractionalPart = fractionalPart - .split('') - .map((char, idx) => (idx === 2 || idx === 5 ? `${nbHalfSpace}${char}` : char)) - .join('') - - return integerPart + decimalPoint + formattedFractionalPart -} - -export const formatSats = (value: number) => { - return SATS_FORMATTER.format(value) -} - -export const shortenStringMiddle = (value: string, chars = 8, separator = '…') => { +export const shortenStringMiddle = (value: string, chars = 8, separator = '\u2026' /* \u2026 = … */) => { const prefixLength = Math.max(Math.floor(chars / 2), 1) if (value.length <= prefixLength * 2) { return `${value}` @@ -116,12 +102,17 @@ export const setIntervalDebounced = ( callback: () => Promise, delay: Milliseconds, onTimerIdChanged: (timerId: NodeJS.Timer) => void, + onError: (error: any, loop: () => void) => void = (_, loop) => loop(), ) => { ;(function loop() { onTimerIdChanged( setTimeout(async () => { - await callback() - loop() + try { + await callback() + loop() + } catch (e: any) { + onError(e, loop) + } }, delay), ) })()