diff --git a/dapp/src/components/Modals/ConnectWalletModal.tsx b/dapp/src/components/Modals/ConnectWalletModal.tsx index 2658ac432..c0ce3de8a 100644 --- a/dapp/src/components/Modals/ConnectWalletModal.tsx +++ b/dapp/src/components/Modals/ConnectWalletModal.tsx @@ -6,27 +6,34 @@ import { ModalFooter, ModalHeader, VStack, + Text, } from "@chakra-ui/react" -import { useRequestBitcoinAccount } from "../../hooks" import BaseModal from "./BaseModal" -import ConnectBTCAccount from "../../static/images/ConnectBTCAccount.png" -import { TextLg, TextMd } from "../Typography" +import { Currency, RequestAccountParams } from "../../types" -export default function ConnectWalletModal() { - const { requestAccount } = useRequestBitcoinAccount() +type ConnectWalletModalProps = { + currency: Currency + image: string + requestAccount: (...params: RequestAccountParams) => Promise +} +export default function ConnectWalletModal({ + currency, + image, + requestAccount, +}: ConnectWalletModalProps) { return ( - Bitcoin account not installed + {currency.name} account not installed - - - - - Bitcoin account is required to make transactions for depositing and - staking your BTC. - + + + + + {currency.name} account is required to make transactions + for depositing and staking your {currency.symbol}. + diff --git a/dapp/src/components/Modals/StakeModal.tsx b/dapp/src/components/Modals/StakeModal.tsx index 581431e08..3f4aef654 100644 --- a/dapp/src/components/Modals/StakeModal.tsx +++ b/dapp/src/components/Modals/StakeModal.tsx @@ -9,15 +9,13 @@ import { Tab, TabPanels, TabPanel, - NumberInput, - NumberInputField, - NumberInputStepper, } from "@chakra-ui/react" -import { useStakingFlowContext } from "../../hooks" +import { useStakingFlowContext, useWalletContext } from "../../hooks" import BaseModal from "./BaseModal" import { TokenBalance } from "../TokenBalance" import { BITCOIN } from "../../constants" import { TextMd } from "../Typography" +import TokenBalanceInput from "../TokenBalanceInput" function StakeDetails({ text, @@ -41,7 +39,8 @@ function StakeDetails({ } export default function StakeModal() { - const { closeModal } = useStakingFlowContext() + const { amount, setAmount, closeModal } = useStakingFlowContext() + const { btcAccount } = useWalletContext() return ( @@ -55,11 +54,16 @@ export default function StakeModal() { - {/* TODO: Create a custom number input component */} - - - - + {/* TODO: Add a validation */} + setAmount(value)} + /> {/* TODO: Use the real data */} + if (!btcAccount) + return ( + + ) + + if (!ethAccount) + return ( + + ) if (modalType === "overview") return diff --git a/dapp/src/components/TokenBalanceInput/index.tsx b/dapp/src/components/TokenBalanceInput/index.tsx new file mode 100644 index 000000000..1c7a1ba28 --- /dev/null +++ b/dapp/src/components/TokenBalanceInput/index.tsx @@ -0,0 +1,78 @@ +import React, { useMemo } from "react" +import { + Button, + Flex, + HStack, + Icon, + InputGroup, + InputRightElement, + NumberInput, + NumberInputField, + Tooltip, + useColorModeValue, +} from "@chakra-ui/react" +import { TokenBalance } from "../TokenBalance" +import { Currency } from "../../types" +import { fixedPointNumberToString } from "../../utils" +import { Alert } from "../../static/icons" +import { USD } from "../../constants" +import { TextMd, TextSm } from "../Typography" + +export default function TokenBalanceInput({ + amount, + usdAmount, + currency, + tokenBalance, + placeholder, + onChange, +}: { + amount?: string + usdAmount?: string + currency: Currency + tokenBalance: string | number + placeholder?: string + onChange: (value: string) => void +}) { + // TODO: Set the correct color + const colorInfo = useColorModeValue("grey.200", "grey.200") + + const tokenBalanceAmount = useMemo( + () => + fixedPointNumberToString(BigInt(tokenBalance || 0), currency.decimals), + [currency.decimals, tokenBalance], + ) + + return ( + + + Amount + + Balance + + + + + onChange(valueString)} + > + + + + + + + + {/* TODO: Add correct text for tooltip */} + + + + {`${usdAmount} ${USD.symbol}`} + + + ) +} diff --git a/dapp/src/contexts/StakingFlowContext.tsx b/dapp/src/contexts/StakingFlowContext.tsx index a58446787..449e5a936 100644 --- a/dapp/src/contexts/StakingFlowContext.tsx +++ b/dapp/src/contexts/StakingFlowContext.tsx @@ -2,15 +2,17 @@ import React, { createContext, useCallback, useMemo, useState } from "react" import { ModalType } from "../types" type StakingFlowContextValue = { - modalType: ModalType | undefined + modalType?: ModalType + amount?: string closeModal: () => void setModalType: React.Dispatch> + setAmount: React.Dispatch> } export const StakingFlowContext = createContext({ - modalType: undefined, setModalType: () => {}, closeModal: () => {}, + setAmount: () => {}, }) export function StakingFlowProvider({ @@ -19,19 +21,23 @@ export function StakingFlowProvider({ children: React.ReactNode }): React.ReactElement { const [modalType, setModalType] = useState(undefined) + const [amount, setAmount] = useState(undefined) const closeModal = useCallback(() => { setModalType(undefined) + setAmount(undefined) }, []) const contextValue: StakingFlowContextValue = useMemo( () => ({ modalType, + amount, closeModal, setModalType, + setAmount, }), - [modalType, closeModal], + [modalType, amount, closeModal], ) return ( diff --git a/dapp/src/static/icons/Alert.tsx b/dapp/src/static/icons/Alert.tsx new file mode 100644 index 000000000..e76365d89 --- /dev/null +++ b/dapp/src/static/icons/Alert.tsx @@ -0,0 +1,24 @@ +import React from "react" +import { createIcon } from "@chakra-ui/react" + +export const Alert = createIcon({ + displayName: "Alert", + viewBox: "0 0 16 16", + path: [ + + + , + + + + + , + ], +}) diff --git a/dapp/src/static/icons/index.ts b/dapp/src/static/icons/index.ts index 66c6e3aa9..6e64faaba 100644 --- a/dapp/src/static/icons/index.ts +++ b/dapp/src/static/icons/index.ts @@ -2,3 +2,4 @@ export * from "./Info" export * from "./Bitcoin" export * from "./Ethereum" export * from "./ChevronRight" +export * from "./Alert" diff --git a/dapp/src/static/images/ConnectETHAccount.png b/dapp/src/static/images/ConnectETHAccount.png new file mode 100644 index 000000000..e1ad2974d Binary files /dev/null and b/dapp/src/static/images/ConnectETHAccount.png differ diff --git a/dapp/src/types/ledger-live-app.ts b/dapp/src/types/ledger-live-app.ts index c63368d3e..8e82cb1a3 100644 --- a/dapp/src/types/ledger-live-app.ts +++ b/dapp/src/types/ledger-live-app.ts @@ -1,6 +1,8 @@ import { WalletAPIClient } from "@ledgerhq/wallet-api-client" -type RequestAccountParams = Parameters +export type RequestAccountParams = Parameters< + WalletAPIClient["account"]["request"] +> export type UseRequestAccountReturn = { requestAccount: (...params: RequestAccountParams) => Promise diff --git a/dapp/src/utils/numbers.ts b/dapp/src/utils/numbers.ts index 5e44e49db..f292f41b9 100644 --- a/dapp/src/utils/numbers.ts +++ b/dapp/src/utils/numbers.ts @@ -4,8 +4,6 @@ const toLocaleString = (value: number): string => /** * Convert a fixed point bigint with precision `fixedPointDecimals` to a * floating point number truncated to `desiredDecimals`. - * If `formattedAmount` is less than the minimum amount to display - * for the specified precision return information about this. * * 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#L216-L239 @@ -14,11 +12,7 @@ export function bigIntToUserAmount( fixedPoint: bigint, fixedPointDecimals: number, desiredDecimals = 2, -): string { - if (fixedPoint === BigInt(0)) { - return `0.${"0".repeat(desiredDecimals)}` - } - +): number { const fixedPointDesiredDecimalsAmount = fixedPoint / 10n ** BigInt(Math.max(1, fixedPointDecimals - desiredDecimals)) @@ -26,8 +20,39 @@ export function bigIntToUserAmount( const formattedAmount = Number(fixedPointDesiredDecimalsAmount) / 10 ** Math.min(desiredDecimals, fixedPointDecimals) - const minAmountToDisplay = - 1 / 10 ** Math.min(desiredDecimals, fixedPointDecimals) + + return formattedAmount +} + +/** + * Display a token amount correctly with desired decimals. + * The function returns a string with a language-sensitive representation of this number. + * + * - If the amount entered is zero, return the result with the desired decimals. + * For example, 0.00 for a precision of 2. + * - If `formattedAmount` is less than the minimum amount to display + * for the specified precision return information about this. + * For example, <0.01 for a precision of 2. + * - Other amounts are formatted according to the use of the `bigIntToUserAmount` function. + * + */ +export const formatTokenAmount = ( + amount: number | string, + decimals = 18, + desiredDecimals = 2, +) => { + const fixedPoint = BigInt(amount) + + if (fixedPoint === BigInt(0)) { + return `0.${"0".repeat(desiredDecimals)}` + } + + const formattedAmount = bigIntToUserAmount( + fixedPoint, + decimals, + desiredDecimals, + ) + const minAmountToDisplay = 1 / 10 ** Math.min(desiredDecimals, decimals) if (minAmountToDisplay > formattedAmount) { return `<0.${"0".repeat(desiredDecimals - 1)}1` @@ -35,13 +60,47 @@ export function bigIntToUserAmount( return toLocaleString(formattedAmount) } -export const formatTokenAmount = ( - amount: number | string, - decimals = 18, - desiredDecimals = 2, -) => bigIntToUserAmount(BigInt(amount), decimals, desiredDecimals) 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}` +}