Skip to content

Commit

Permalink
Create a basic TokenBalanceInput component
Browse files Browse the repository at this point in the history
  • Loading branch information
kkosiorowska committed Dec 15, 2023
1 parent 38fd08c commit 113bf2e
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 1 deletion.
3 changes: 2 additions & 1 deletion dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"@ledgerhq/wallet-api-client-react": "^1.1.2",
"framer-motion": "^10.16.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-number-format": "^5.3.1"
},
"devDependencies": {
"@thesis-co/eslint-config": "^0.6.1",
Expand Down
47 changes: 47 additions & 0 deletions dapp/src/components/shared/NumberFormatInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react"
import { NumericFormat } from "react-number-format"
import { InputProps, chakra, useMultiStyleConfig } from "@chakra-ui/react"

const ChakraWrapper = chakra(NumericFormat)

export type NumberFormatInputValues = {
formattedValue: string
value: string
floatValue: number
}

type NumberFormatInputProps = {
onValueChange: (values: NumberFormatInputValues) => void
decimalScale?: number
} & InputProps

/**
* Component is from the Threshold Network React Components repository.
* It has been used because it supports the thousandth separator
* and can be easily integrated with Chakra UI.
*
* More info:
* https://github.com/threshold-network/components/blob/main/src/components/NumberFormatInput/index.tsx
*/
const NumberFormatInput = React.forwardRef<
HTMLInputElement,
NumberFormatInputProps
>((props, ref) => {
const { field: css } = useMultiStyleConfig("Input", props)

const { decimalScale, isDisabled, ...restProps } = props

return (
<ChakraWrapper
allowLeadingZeros={false}
thousandSeparator
decimalScale={decimalScale}
__css={css}
disabled={isDisabled}
getInputRef={ref}
{...restProps}
/>
)
})

export default NumberFormatInput
121 changes: 121 additions & 0 deletions dapp/src/components/shared/TokenBalanceInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { useMemo } from "react"
import {
Box,
Button,
HStack,
InputGroup,
InputProps,
InputRightElement,
createStylesContext,
useMultiStyleConfig,
} from "@chakra-ui/react"
import { fixedPointNumberToString } from "../../../utils"
import { CurrencyType } from "../../../types"
import { CURRENCIES_BY_TYPE } from "../../../constants"
import NumberFormatInput, {
NumberFormatInputValues,
} from "../NumberFormatInput"
import { CurrencyBalance } from "../CurrencyBalance"

const VARIANT = "balance"
const [StylesProvider, useStyles] = createStylesContext("TokenBalanceInput")

type HelperErrorTextProps = {
errorMsgText?: string | JSX.Element
hasError?: boolean
helperText?: string | JSX.Element
}

function HelperErrorText({
helperText,
errorMsgText,
hasError,
}: HelperErrorTextProps) {
const styles = useStyles()
const text = hasError ? errorMsgText : helperText

if (!text) return null

return (
<Box as="span" __css={hasError ? styles.errorMsgText : styles.helperText}>
{text}
</Box>
)
}

type TokenBalanceInputProps = {
amount?: string
currencyType: CurrencyType
tokenBalance: string | number
placeholder?: string
size?: "lg" | "md"
setAmount: (value: string) => void
} & InputProps &
HelperErrorTextProps

export default function TokenBalanceInput({
amount,
currencyType,
tokenBalance,
placeholder,
size = "lg",
setAmount,
errorMsgText,
helperText,
hasError = false,
...inputProps
}: TokenBalanceInputProps) {
const styles = useMultiStyleConfig("TokenBalanceInput", { size })

const tokenBalanceAmount = useMemo(() => {
const currency = CURRENCIES_BY_TYPE[currencyType]
return fixedPointNumberToString(
BigInt(tokenBalance || 0),
currency.decimals,
)
}, [currencyType, tokenBalance])

return (
<Box __css={styles.container}>
<Box __css={styles.labelContainer}>
<Box as="span" __css={styles.label}>
Amount
</Box>
<HStack>
<Box as="span" __css={styles.balance}>
Balance
</Box>
<CurrencyBalance
size={size === "lg" ? "md" : "sm"}
amount={tokenBalance}
currencyType={currencyType}
/>
</HStack>
</Box>
<InputGroup variant={VARIANT}>
<NumberFormatInput
size={size}
value={amount}
variant={VARIANT}
placeholder={placeholder}
onValueChange={(values: NumberFormatInputValues) =>
setAmount(values.value)
}
{...inputProps}
/>
<InputRightElement>
<Button h="70%" onClick={() => setAmount(tokenBalanceAmount)}>
Max
</Button>
</InputRightElement>
</InputGroup>
<StylesProvider value={styles}>
<HelperErrorText
helperText={helperText}
errorMsgText={errorMsgText}
hasError={hasError}
/>
</StylesProvider>
</Box>
)
}
40 changes: 40 additions & 0 deletions dapp/src/theme/Input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { inputAnatomy as parts } from "@chakra-ui/anatomy"
import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react"

const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys)

const variantBalanceField = defineStyle({
border: "1px solid",
borderColor: "gold.300",
color: "grey.700",
fontWeight: "bold",
bg: "opacity.white.5",
paddingRight: 20,
// TODO: Set the color correctly without using the chakra variable.
caretColor: "var(--chakra-colors-brand-400)",

_placeholder: {
color: "grey.300",
fontWeight: "medium",
},
})

const variantBalanceElement = defineStyle({
h: "100%",
width: 14,
mr: 2,
})

const variantBalance = definePartsStyle({
field: variantBalanceField,
element: variantBalanceElement,
})

const variants = {
balance: variantBalance,
}

const Input = defineMultiStyleConfig({ variants })

export default Input
99 changes: 99 additions & 0 deletions dapp/src/theme/TokenBalanceInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react"

const PARTS = [
"container",
"labelContainer",
"label",
"balance",
"helperText",
"errorMsgText",
]

const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(PARTS)

const baseStyleContainer = defineStyle({
display: "flex",
flexDirection: "column",
gap: 1,
})

const baseStyleLabelContainer = defineStyle({
display: "flex",
justifyContent: "space-between",
})

const baseStyleLabel = defineStyle({
fontWeight: "semibold",
})

const baseStyleBalance = defineStyle({
fontWeight: "normal",
color: "grey.500",
})

const baseStyleHelperText = defineStyle({
fontWeight: "normal",
color: "grey.500",
})

const baseStyleErrorMsgText = defineStyle({
fontWeight: "normal",
color: "red.400",
})

const baseStyle = definePartsStyle({
container: baseStyleContainer,
labelContainer: baseStyleLabelContainer,
label: baseStyleLabel,
balance: baseStyleBalance,
helperText: baseStyleHelperText,
errorMsgText: baseStyleErrorMsgText,
})

const sizeMd = definePartsStyle({
label: {
fontSize: "sm",
lineHeight: "sm",
},
balance: {
fontSize: "sm",
lineHeight: "sm",
},
helperText: {
fontSize: "sm",
lineHeight: "sm",
},
errorMsgText: {
fontSize: "sm",
lineHeight: "sm",
},
})

const sizeLg = definePartsStyle({
label: {
fontSize: "md",
lineHeight: "md",
},
balance: {
fontSize: "md",
lineHeight: "md",
},
helperText: {
fontSize: "sm",
lineHeight: "sm",
},
errorMsgText: {
fontSize: "sm",
lineHeight: "sm",
},
})

const sizes = {
md: sizeMd,
lg: sizeLg,
}

const TokenBalanceInput = defineMultiStyleConfig({ baseStyle, sizes })

export default TokenBalanceInput
4 changes: 4 additions & 0 deletions dapp/src/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import Tooltip from "./Tooltip"
import { colors, fonts, lineHeights } from "./utils"
import Heading from "./Heading"
import CurrencyBalance from "./CurrencyBalance"
import TokenBalanceInput from "./TokenBalanceInput"
import Input from "./Input"

const defaultTheme = {
colors,
Expand All @@ -28,6 +30,8 @@ const defaultTheme = {
CurrencyBalance,
Card,
Tooltip,
Input,
TokenBalanceInput,
},
}

Expand Down
39 changes: 39 additions & 0 deletions dapp/src/utils/numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,42 @@ export const formatSatoshiAmount = (
amount: number | string,
desiredDecimals = 2,
) => formatTokenAmount(amount, 8, desiredDecimals)

/**
* Converts a fixed point number with a bigint amount and a decimals field
* indicating the orders of magnitude in `amount` behind the decimal point into
* a string in US decimal format (no thousands separators, . for the decimal
* separator).
*
* Used in cases where precision is critical.
*
* This function is based on the solution used by the Taho extension.
* More info: https://github.com/tahowallet/extension/blob/main/background/lib/fixed-point.ts#L172-L214
*/
export function fixedPointNumberToString(
amount: bigint,
decimals: number,
trimTrailingZeros = true,
): string {
const undecimaledAmount = amount.toString()
const preDecimalLength = undecimaledAmount.length - decimals

const preDecimalCharacters =
preDecimalLength > 0
? undecimaledAmount.substring(0, preDecimalLength)
: "0"
const postDecimalCharacters =
"0".repeat(Math.max(-preDecimalLength, 0)) +
undecimaledAmount.substring(preDecimalLength)

const trimmedPostDecimalCharacters = trimTrailingZeros
? postDecimalCharacters.replace(/0*$/, "")
: postDecimalCharacters

const decimalString =
trimmedPostDecimalCharacters.length > 0
? `.${trimmedPostDecimalCharacters}`
: ""

return `${preDecimalCharacters}${decimalString}`
}
Loading

0 comments on commit 113bf2e

Please sign in to comment.