import { useMemo, useState } from "react"; import { ArrowsRightLeftIcon } from "@heroicons/react/24/outline"; import { CommonInputProps, InputBase, SIGNED_NUMBER_REGEX } from "~~/components/scaffold-eth"; import { useDisplayUsdMode } from "~~/hooks/scaffold-eth/useDisplayUsdMode"; import { useGlobalState } from "~~/services/store/store"; const MAX_DECIMALS_USD = 2; function etherValueToDisplayValue(usdMode: boolean, etherValue: string, nativeCurrencyPrice: number) { if (usdMode && nativeCurrencyPrice) { const parsedEthValue = parseFloat(etherValue); if (Number.isNaN(parsedEthValue)) { return etherValue; } else { // We need to round the value rather than use toFixed, // since otherwise a user would not be able to modify the decimal value return ( Math.round(parsedEthValue * nativeCurrencyPrice * 10 ** MAX_DECIMALS_USD) / 10 ** MAX_DECIMALS_USD ).toString(); } } else { return etherValue; } } function displayValueToEtherValue(usdMode: boolean, displayValue: string, nativeCurrencyPrice: number) { if (usdMode && nativeCurrencyPrice) { const parsedDisplayValue = parseFloat(displayValue); if (Number.isNaN(parsedDisplayValue)) { // Invalid number. return displayValue; } else { // Compute the ETH value if a valid number. return (parsedDisplayValue / nativeCurrencyPrice).toString(); } } else { return displayValue; } } /** * Input for ETH amount with USD conversion. * * onChange will always be called with the value in ETH */ export const EtherInput = ({ value, name, placeholder, onChange, disabled, usdMode, }: CommonInputProps & { usdMode?: boolean }) => { const [transitoryDisplayValue, setTransitoryDisplayValue] = useState(); const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price); const isNativeCurrencyPriceFetching = useGlobalState(state => state.nativeCurrency.isFetching); const { displayUsdMode, toggleDisplayUsdMode } = useDisplayUsdMode({ defaultUsdMode: usdMode }); // The displayValue is derived from the ether value that is controlled outside of the component // In usdMode, it is converted to its usd value, in regular mode it is unaltered const displayValue = useMemo(() => { const newDisplayValue = etherValueToDisplayValue(displayUsdMode, value, nativeCurrencyPrice || 0); if (transitoryDisplayValue && parseFloat(newDisplayValue) === parseFloat(transitoryDisplayValue)) { return transitoryDisplayValue; } // Clear any transitory display values that might be set setTransitoryDisplayValue(undefined); return newDisplayValue; }, [nativeCurrencyPrice, transitoryDisplayValue, displayUsdMode, value]); const handleChangeNumber = (newValue: string) => { if (newValue && !SIGNED_NUMBER_REGEX.test(newValue)) { return; } // Following condition is a fix to prevent usdMode from experiencing different display values // than what the user entered. This can happen due to floating point rounding errors that are introduced in the back and forth conversion if (displayUsdMode) { const decimals = newValue.split(".")[1]; if (decimals && decimals.length > MAX_DECIMALS_USD) { return; } } // Since the display value is a derived state (calculated from the ether value), usdMode would not allow introducing a decimal point. // This condition handles a transitory state for a display value with a trailing decimal sign if (newValue.endsWith(".") || newValue.endsWith(".0")) { setTransitoryDisplayValue(newValue); } else { setTransitoryDisplayValue(undefined); } const newEthValue = displayValueToEtherValue(displayUsdMode, newValue, nativeCurrencyPrice || 0); onChange(newEthValue); }; return ( {displayUsdMode ? "$" : "Ξ"}} suffix={
0 ? "" : "tooltip tooltip-secondary before:content-[attr(data-tip)] before:right-[-10px] before:left-auto before:transform-none" }`} data-tip={isNativeCurrencyPriceFetching ? "Fetching price" : "Unable to fetch price"} >
} /> ); };