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

ui(Jar): hide currency symbol of frozen balance #697

Merged
merged 4 commits into from
Nov 30, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions src/components/Balance.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,35 @@
color: var(--jam-balance-color);
}

.frozenSymbol {
order: -2;
.hideSymbol {
padding-left: 0.1em;
color: var(--jam-balance-deemphasize-color);
}

.bitcoinSymbol {
padding-right: 0.1em;
order: -1;
width: 1em;
padding-right: 0.1em;
}

.satsSymbol {
padding-right: 0.1em;
order: 6;
order: 5;
}

.hideSymbol {
padding-left: 0.1em;
color: var(--jam-balance-deemphasize-color);
.frozenSymbol {
order: 5;
}
.bitcoinAmount + .frozenSymbol {
order: -2;
width: 1em;
height: 1em;
}

.frozenSymbol,
.bitcoinSymbol,
.satsSymbol {
display: flex;
justify-content: center;
}

.bitcoinAmount .fractionalPart :nth-child(3)::before,
Expand Down
38 changes: 37 additions & 1 deletion src/components/Balance.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,46 @@ describe('<Balance />', () => {
expect(screen.getByText(`NaN`)).toBeInTheDocument()
})

it('should render BTC using satscomma formatting', () => {
it('should render balance in BTC', () => {
render(<Balance valueString={'123.456'} convertToUnit={BTC} showBalance={true} />)
expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`123.45600000`)
expect(screen.getByTestId('bitcoin-symbol')).toBeVisible()
expect(screen.queryByTestId('sats-symbol')).not.toBeInTheDocument()
expect(screen.queryByTestId('frozen-symbol')).not.toBeInTheDocument()
})

it('should render balance in SATS', () => {
render(<Balance valueString={'123.456'} convertToUnit={SATS} showBalance={true} />)
expect(screen.getByTestId('sats-amount')).toHaveTextContent(`12,345,600,000`)
expect(screen.getByTestId('sats-symbol')).toBeVisible()
expect(screen.queryByTestId('bitcoin-symbol')).not.toBeInTheDocument()
expect(screen.queryByTestId('frozen-symbol')).not.toBeInTheDocument()
})

it('should hide balance for BTC by default', () => {
render(<Balance valueString={'123.456'} convertToUnit={BTC} />)
expect(screen.getByText(`*****`)).toBeInTheDocument()
expect(screen.queryByTestId('bitcoin-amount')).not.toBeInTheDocument()
expect(screen.queryByTestId('bitcoin-symbol')).not.toBeInTheDocument()
})

it('should hide balance for SATS by default', () => {
render(<Balance valueString={'123'} convertToUnit={SATS} />)
expect(screen.getByText(`*****`)).toBeInTheDocument()
expect(screen.queryByTestId(`sats-amount`)).not.toBeInTheDocument()
expect(screen.queryByTestId('sats-symbol')).not.toBeInTheDocument()
})

it('should render a string BTC value correctly as BTC', () => {
render(<Balance valueString={'123.03224961'} convertToUnit={BTC} showBalance={true} />)
expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`123.03224961`)
expect(screen.getByTestId('bitcoin-symbol')).toBeVisible()
})

it('should render a string BTC value correctly as SATS', () => {
render(<Balance valueString={'123.03224961'} convertToUnit={SATS} showBalance={true} />)
expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`12,303,224,961`)
expect(screen.getByTestId('sats-symbol')).toBeVisible()
})

it('should render a zero string BTC value correctly as BTC', () => {
Expand Down Expand Up @@ -108,6 +123,27 @@ describe('<Balance />', () => {
expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,100,000,000,000,000`)
})

it('should render frozen balance in BTC', () => {
render(<Balance valueString={'123.456'} convertToUnit={BTC} showBalance={true} frozen={true} />)
expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`123.45600000`)
expect(screen.getByTestId('bitcoin-symbol')).toBeVisible()
expect(screen.getByTestId('frozen-symbol')).toBeVisible()
})

it('should render frozen balance in SATS', () => {
render(<Balance valueString={'123.456'} convertToUnit={SATS} showBalance={true} frozen={true} />)
expect(screen.getByTestId('sats-amount')).toHaveTextContent(`12,345,600,000`)
expect(screen.getByTestId('sats-symbol')).toBeVisible()
expect(screen.getByTestId('frozen-symbol')).toBeVisible()
})

it('should render balance without symbol', () => {
render(<Balance valueString={'123.456'} convertToUnit={SATS} showBalance={true} frozen={true} showSymbol={false} />)
expect(screen.getByTestId('sats-amount')).toBeVisible()
expect(screen.getByTestId('frozen-symbol')).toBeVisible()
expect(screen.queryByTestId('sats-symbol')).not.toBeInTheDocument()
})

it('should toggle visibility of initially hidden balance on click by default', () => {
render(<Balance valueString={`21`} convertToUnit={SATS} showBalance={false} />)
expect(screen.queryByTestId(`sats-amount`)).not.toBeInTheDocument()
Expand Down
171 changes: 89 additions & 82 deletions src/components/Balance.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MouseEventHandler, useCallback, useEffect, useMemo, useState } from 'react'
import { PropsWithChildren, MouseEventHandler, useEffect, useMemo, useState } from 'react'
import classNames from 'classnames'
import Sprite from './Sprite'
import { SATS, BTC, btcToSats, satsToBtc, isValidNumber, formatBtc, formatSats } from '../utils'
Expand All @@ -15,74 +15,99 @@ 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 (
<span
className={`${styles.bitcoinAmount} slashed-zeroes`}
data-testid="bitcoin-amount"
data-integer-part-is-zero={integerPartIsZero}
data-fractional-part-starts-with-zero={fractionalPartStartsWithZero}
data-raw-value={value}
data-formatted-value={numberString}
>
<span className={styles.integerPart}>{integerPart}</span>
<span className={styles.decimalPoint}>{DECIMAL_POINT_CHAR}</span>
<span className={styles.fractionalPart}>
{fractionPartArray.map((digit, index) => (
<span key={index} data-digit={digit}>
{digit}
</span>
))}
</span>
</span>
)
}

const SatsAmountComponent = ({ value }: { value: number }) => {
return (
<span className={`${styles.satsAmount} slashed-zeroes`} data-testid="sats-amount" data-raw-value={value}>
{formatSats(value)}
</span>
)
}

const BTC_SYMBOL = <span className={styles.bitcoinSymbol}>{'\u20BF'}</span>
const BTC_SYMBOL = (
<span data-testid="bitcoin-symbol" className={styles.bitcoinSymbol}>
{'\u20BF'}
</span>
)

const SAT_SYMBOL = <Sprite className={styles.satsSymbol} symbol="sats" width="1.2em" height="1.2em" />
const SAT_SYMBOL = (
<Sprite data-testid="sats-symbol" className={styles.satsSymbol} symbol="sats" width="1.2em" height="1.2em" />
)

const FROZEN_SYMBOL = (
<Sprite className={`${styles.frozenSymbol} frozen-symbol-hook`} symbol="snowflake" width="1.2em" height="1.2em" />
<Sprite
data-testid="frozen-symbol"
className={`${styles.frozenSymbol} frozen-symbol-hook`}
symbol="snowflake"
width="1.2em"
height="1.2em"
/>
)

interface BalanceComponentProps {
symbol: JSX.Element
value: JSX.Element
symbol?: JSX.Element
showSymbol?: boolean
frozen?: boolean
}

const BalanceComponent = ({ symbol, value, frozen = false }: BalanceComponentProps) => {
const BalanceComponent = ({
symbol,
showSymbol = true,
frozen = false,
children,
}: PropsWithChildren<BalanceComponentProps>) => {
return (
<span
className={classNames(styles.balance, 'balance-hook', 'd-inline-flex align-items-center', {
[styles.frozen]: frozen,
})}
>
{children}
{showSymbol && symbol}
{frozen && FROZEN_SYMBOL}
{value}
{symbol}
</span>
)
}

const DECIMAL_POINT_CHAR = '.'

type BitcoinBalanceProps = Omit<BalanceComponentProps, 'symbol'> & { value: number }

const BitcoinBalance = ({ value, ...props }: BitcoinBalanceProps) => {
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 (
<BalanceComponent symbol={BTC_SYMBOL} {...props}>
<span
className={`${styles.bitcoinAmount} slashed-zeroes`}
data-testid="bitcoin-amount"
data-integer-part-is-zero={integerPartIsZero}
data-fractional-part-starts-with-zero={fractionalPartStartsWithZero}
data-raw-value={value}
data-formatted-value={numberString}
>
<span className={styles.integerPart}>{integerPart}</span>
<span className={styles.decimalPoint}>{DECIMAL_POINT_CHAR}</span>
<span className={styles.fractionalPart}>
{fractionPartArray.map((digit, index) => (
<span key={index} data-digit={digit}>
{digit}
</span>
))}
</span>
</span>
</BalanceComponent>
)
}

type SatsBalanceProps = Omit<BalanceComponentProps, 'symbol'> & { value: number }

const SatsBalance = ({ value, ...props }: SatsBalanceProps) => {
return (
<BalanceComponent symbol={SAT_SYMBOL} {...props}>
<span className={`${styles.satsAmount} slashed-zeroes`} data-testid="sats-amount" data-raw-value={value}>
{formatSats(value)}
</span>
</BalanceComponent>
)
}

/**
* Options argument for Balance component.
*
Expand All @@ -97,12 +122,11 @@ const BalanceComponent = ({ symbol, value, frozen = false }: BalanceComponentPro
* @param {loading}: A loading flag that renders a placeholder while true.
* @param {enableVisibilityToggle}: A flag that controls whether the balance can mask/unmask when clicked
*/
interface BalanceProps {
type BalanceProps = Omit<BalanceComponentProps, 'symbol'> & {
valueString: string
convertToUnit: Unit
showBalance?: boolean
enableVisibilityToggle?: boolean
frozen?: boolean
}

/**
Expand All @@ -113,7 +137,7 @@ export default function Balance({
convertToUnit,
showBalance = false,
enableVisibilityToggle = !showBalance,
frozen = false,
...props
}: BalanceProps) {
const [isBalanceVisible, setIsBalanceVisible] = useState(showBalance)
const displayMode = useMemo(() => getDisplayMode(convertToUnit, isBalanceVisible), [convertToUnit, isBalanceVisible])
Expand All @@ -122,64 +146,47 @@ export default function Balance({
setIsBalanceVisible(showBalance)
}, [showBalance])

const toggleVisibility: MouseEventHandler = useCallback((e) => {
const toggleVisibility: MouseEventHandler = (e) => {
e.preventDefault()
e.stopPropagation()

setIsBalanceVisible((current) => !current)
}, [])
}

const balanceComponent = useMemo(() => {
if (displayMode === DISPLAY_MODE_HIDDEN) {
return (
<BalanceComponent
symbol={<Sprite symbol="hide" width="1.2em" height="1.2em" className={styles.hideSymbol} />}
value={<span className="slashed-zeroes">{'*****'}</span>}
frozen={frozen}
/>
{...props}
>
<span className="slashed-zeroes">{'*****'}</span>
</BalanceComponent>
)
}

const valueNumber = parseFloat(valueString)
if (!isValidNumber(valueNumber)) {
console.warn('<Balance /> component expects number input as string')
return <BalanceComponent symbol={<></>} value={<>{valueString}</>} frozen={frozen} />
return <BalanceComponent {...props}>{valueString}</BalanceComponent>
}

// Treat integers as sats.
const valueIsSats = valueString === parseInt(valueString, 10).toString()
// Treat decimal numbers as btc.
const valueIsBtc = !valueIsSats && valueString.indexOf('.') > -1

if (valueIsBtc && displayMode === DISPLAY_MODE_BTC)
return (
<BalanceComponent symbol={BTC_SYMBOL} value={<BitcoinAmountComponent value={valueNumber} />} frozen={frozen} />
)
if (valueIsSats && displayMode === DISPLAY_MODE_SATS)
return (
<BalanceComponent symbol={SAT_SYMBOL} value={<SatsAmountComponent value={valueNumber} />} frozen={frozen} />
)
if (valueIsBtc && displayMode === DISPLAY_MODE_BTC) return <BitcoinBalance value={valueNumber} {...props} />
if (valueIsSats && displayMode === DISPLAY_MODE_SATS) return <SatsBalance value={valueNumber} {...props} />

if (valueIsBtc && displayMode === DISPLAY_MODE_SATS)
return (
<BalanceComponent
symbol={SAT_SYMBOL}
value={<SatsAmountComponent value={btcToSats(valueString)} />}
frozen={frozen}
/>
)
return <SatsBalance value={btcToSats(valueString)} {...props} />
if (valueIsSats && displayMode === DISPLAY_MODE_BTC)
return (
<BalanceComponent
symbol={BTC_SYMBOL}
value={<BitcoinAmountComponent value={satsToBtc(valueString)} />}
frozen={frozen}
/>
)
return <BitcoinBalance value={satsToBtc(valueString)} {...props} />

console.warn('<Balance /> component cannot determine balance format')
return <BalanceComponent symbol={<></>} value={<>{valueString}</>} frozen={frozen} />
}, [valueString, displayMode, frozen])
return <BalanceComponent {...props}>{valueString}</BalanceComponent>
}, [valueString, displayMode, props])

if (!enableVisibilityToggle) {
return <>{balanceComponent}</>
Expand Down
4 changes: 0 additions & 4 deletions src/components/jars/Jar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,6 @@
font-size: 0.8rem;
}

.frozen.jarBalance {
font-size: 0.7rem;
}

.selectableJarContainer {
display: flex;
flex-direction: column;
Expand Down
1 change: 1 addition & 0 deletions src/components/jars/Jar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ const Jar = ({ index, balance, frozenBalance, fillLevel, isOpen = false }: JarPr
convertToUnit={settings.unit}
showBalance={settings.showBalance}
frozen={true}
showSymbol={false}
/>
)}
</div>
Expand Down