Skip to content

Commit

Permalink
feat: useDisplayUsdMode hook (#859)
Browse files Browse the repository at this point in the history
  • Loading branch information
rin-st authored Jun 5, 2024
1 parent fa16ebf commit 18dd946
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 92 deletions.
2 changes: 1 addition & 1 deletion packages/nextjs/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useGlobalState } from "~~/services/store/store";
* Site footer
*/
export const Footer = () => {
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrencyPrice);
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
const { targetNetwork } = useTargetNetwork();
const isLocalNetwork = targetNetwork.id === hardhat.id;

Expand Down
12 changes: 2 additions & 10 deletions packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,11 @@ import { Footer } from "~~/components/Footer";
import { Header } from "~~/components/Header";
import { BlockieAvatar } from "~~/components/scaffold-eth";
import { ProgressBar } from "~~/components/scaffold-eth/ProgressBar";
import { useNativeCurrencyPrice } from "~~/hooks/scaffold-eth";
import { useGlobalState } from "~~/services/store/store";
import { useInitializeNativeCurrencyPrice } from "~~/hooks/scaffold-eth";
import { wagmiConfig } from "~~/services/web3/wagmiConfig";

const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
const price = useNativeCurrencyPrice();
const setNativeCurrencyPrice = useGlobalState(state => state.setNativeCurrencyPrice);

useEffect(() => {
if (price > 0) {
setNativeCurrencyPrice(price);
}
}, [setNativeCurrencyPrice, price]);
useInitializeNativeCurrencyPrice();

return (
<>
Expand Down
24 changes: 8 additions & 16 deletions packages/nextjs/components/scaffold-eth/Balance.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useEffect, useState } from "react";
import { Address, formatEther } from "viem";
import { useDisplayUsdMode } from "~~/hooks/scaffold-eth/useDisplayUsdMode";
import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork";
import { useWatchBalance } from "~~/hooks/scaffold-eth/useWatchBalance";
import { useGlobalState } from "~~/services/store/store";
Expand All @@ -17,7 +17,9 @@ type BalanceProps = {
*/
export const Balance = ({ address, className = "", usdMode }: BalanceProps) => {
const { targetNetwork } = useTargetNetwork();
const price = useGlobalState(state => state.nativeCurrencyPrice);
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
const isNativeCurrencyPriceFetching = useGlobalState(state => state.nativeCurrency.isFetching);

const {
data: balance,
isError,
Expand All @@ -26,19 +28,9 @@ export const Balance = ({ address, className = "", usdMode }: BalanceProps) => {
address,
});

const [displayUsdMode, setDisplayUsdMode] = useState(price > 0 ? Boolean(usdMode) : false);

useEffect(() => {
setDisplayUsdMode(price > 0 ? Boolean(usdMode) : false);
}, [usdMode, price]);

const toggleBalanceMode = () => {
if (price > 0) {
setDisplayUsdMode(prevMode => !prevMode);
}
};
const { displayUsdMode, toggleDisplayUsdMode } = useDisplayUsdMode({ defaultUsdMode: usdMode });

if (!address || isLoading || balance === null) {
if (!address || isLoading || balance === null || (isNativeCurrencyPriceFetching && nativeCurrencyPrice === 0)) {
return (
<div className="animate-pulse flex space-x-4">
<div className="rounded-md bg-slate-300 h-6 w-6"></div>
Expand All @@ -62,13 +54,13 @@ export const Balance = ({ address, className = "", usdMode }: BalanceProps) => {
return (
<button
className={`btn btn-sm btn-ghost flex flex-col font-normal items-center hover:bg-transparent ${className}`}
onClick={toggleBalanceMode}
onClick={toggleDisplayUsdMode}
>
<div className="w-full flex items-center justify-center">
{displayUsdMode ? (
<>
<span className="text-[0.8em] font-bold mr-1">$</span>
<span>{(formattedBalance * price).toFixed(2)}</span>
<span>{(formattedBalance * nativeCurrencyPrice).toFixed(2)}</span>
</>
) : (
<>
Expand Down
33 changes: 13 additions & 20 deletions packages/nextjs/components/scaffold-eth/Input/EtherInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react";
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;
Expand Down Expand Up @@ -52,24 +53,22 @@ export const EtherInput = ({
usdMode,
}: CommonInputProps & { usdMode?: boolean }) => {
const [transitoryDisplayValue, setTransitoryDisplayValue] = useState<string>();
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrencyPrice);
const [internalUsdMode, setInternalUSDMode] = useState(nativeCurrencyPrice > 0 ? Boolean(usdMode) : false);
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
const isNativeCurrencyPriceFetching = useGlobalState(state => state.nativeCurrency.isFetching);

useEffect(() => {
setInternalUSDMode(nativeCurrencyPrice > 0 ? Boolean(usdMode) : false);
}, [usdMode, nativeCurrencyPrice]);
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(internalUsdMode, value, nativeCurrencyPrice);
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, internalUsdMode, value]);
}, [nativeCurrencyPrice, transitoryDisplayValue, displayUsdMode, value]);

const handleChangeNumber = (newValue: string) => {
if (newValue && !SIGNED_NUMBER_REGEX.test(newValue)) {
Expand All @@ -78,7 +77,7 @@ export const EtherInput = ({

// 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 (internalUsdMode) {
if (displayUsdMode) {
const decimals = newValue.split(".")[1];
if (decimals && decimals.length > MAX_DECIMALS_USD) {
return;
Expand All @@ -93,37 +92,31 @@ export const EtherInput = ({
setTransitoryDisplayValue(undefined);
}

const newEthValue = displayValueToEtherValue(internalUsdMode, newValue, nativeCurrencyPrice);
const newEthValue = displayValueToEtherValue(displayUsdMode, newValue, nativeCurrencyPrice || 0);
onChange(newEthValue);
};

const toggleMode = () => {
if (nativeCurrencyPrice > 0) {
setInternalUSDMode(!internalUsdMode);
}
};

return (
<InputBase
name={name}
value={displayValue}
placeholder={placeholder}
onChange={handleChangeNumber}
disabled={disabled}
prefix={<span className="pl-4 -mr-2 text-accent self-center">{internalUsdMode ? "$" : "Ξ"}</span>}
prefix={<span className="pl-4 -mr-2 text-accent self-center">{displayUsdMode ? "$" : "Ξ"}</span>}
suffix={
<div
className={`${
nativeCurrencyPrice > 0
? ""
: "tooltip tooltip-secondary before:content-[attr(data-tip)] before:right-[-10px] before:left-auto before:transform-none"
}`}
data-tip="Unable to fetch price"
data-tip={isNativeCurrencyPriceFetching ? "Fetching price" : "Unable to fetch price"}
>
<button
className="btn btn-primary h-[2.2rem] min-h-[2.2rem]"
onClick={toggleMode}
disabled={!internalUsdMode && !nativeCurrencyPrice}
onClick={toggleDisplayUsdMode}
disabled={!displayUsdMode && !nativeCurrencyPrice}
>
<ArrowsRightLeftIcon className="h-3 w-3 cursor-pointer" aria-hidden="true" />
</button>
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/hooks/scaffold-eth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export * from "./useBurnerWallet";
export * from "./useContractLogs";
export * from "./useDeployedContractInfo";
export * from "./useFetchBlocks";
export * from "./useNativeCurrencyPrice";
export * from "./useInitializeNativeCurrencyPrice";
export * from "./useNetworkColor";
export * from "./useOutsideClick";
export * from "./useScaffoldContract";
Expand Down
21 changes: 21 additions & 0 deletions packages/nextjs/hooks/scaffold-eth/useDisplayUsdMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useCallback, useEffect, useState } from "react";
import { useGlobalState } from "~~/services/store/store";

export const useDisplayUsdMode = ({ defaultUsdMode = false }: { defaultUsdMode?: boolean }) => {
const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrency.price);
const isPriceFetched = nativeCurrencyPrice > 0;
const predefinedUsdMode = isPriceFetched ? Boolean(defaultUsdMode) : false;
const [displayUsdMode, setDisplayUsdMode] = useState(predefinedUsdMode);

useEffect(() => {
setDisplayUsdMode(predefinedUsdMode);
}, [predefinedUsdMode]);

const toggleDisplayUsdMode = useCallback(() => {
if (isPriceFetched) {
setDisplayUsdMode(!displayUsdMode);
}
}, [displayUsdMode, isPriceFetched]);

return { displayUsdMode, toggleDisplayUsdMode };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useCallback, useEffect } from "react";
import { useTargetNetwork } from "./useTargetNetwork";
import { useInterval } from "usehooks-ts";
import scaffoldConfig from "~~/scaffold.config";
import { useGlobalState } from "~~/services/store/store";
import { fetchPriceFromUniswap } from "~~/utils/scaffold-eth";

const enablePolling = false;

/**
* Get the price of Native Currency based on Native Token/DAI trading pair from Uniswap SDK
*/
export const useInitializeNativeCurrencyPrice = () => {
const setNativeCurrencyPrice = useGlobalState(state => state.setNativeCurrencyPrice);
const setIsNativeCurrencyFetching = useGlobalState(state => state.setIsNativeCurrencyFetching);
const { targetNetwork } = useTargetNetwork();

const fetchPrice = useCallback(async () => {
setIsNativeCurrencyFetching(true);
const price = await fetchPriceFromUniswap(targetNetwork);
setNativeCurrencyPrice(price);
setIsNativeCurrencyFetching(false);
}, [setIsNativeCurrencyFetching, setNativeCurrencyPrice, targetNetwork]);

// Get the price of ETH from Uniswap on mount
useEffect(() => {
fetchPrice();
}, [fetchPrice]);

// Get the price of ETH from Uniswap at a given interval
useInterval(fetchPrice, enablePolling ? scaffoldConfig.pollingInterval : null);
};
34 changes: 0 additions & 34 deletions packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts

This file was deleted.

17 changes: 10 additions & 7 deletions packages/nextjs/hooks/scaffold-eth/useTargetNetwork.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { useAccount } from "wagmi";
import scaffoldConfig from "~~/scaffold.config";
import { useGlobalState } from "~~/services/store/store";
Expand All @@ -20,10 +20,13 @@ export function useTargetNetwork(): { targetNetwork: ChainWithAttributes } {
}
}, [chain?.id, setTargetNetwork, targetNetwork.id]);

return {
targetNetwork: {
...targetNetwork,
...NETWORKS_EXTRA_DATA[targetNetwork.id],
},
};
return useMemo(
() => ({
targetNetwork: {
...targetNetwork,
...NETWORKS_EXTRA_DATA[targetNetwork.id],
},
}),
[targetNetwork],
);
}
16 changes: 13 additions & 3 deletions packages/nextjs/services/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,25 @@ import { ChainWithAttributes } from "~~/utils/scaffold-eth";
*/

type GlobalState = {
nativeCurrencyPrice: number;
nativeCurrency: {
price: number;
isFetching: boolean;
};
setNativeCurrencyPrice: (newNativeCurrencyPriceState: number) => void;
setIsNativeCurrencyFetching: (newIsNativeCurrencyFetching: boolean) => void;
targetNetwork: ChainWithAttributes;
setTargetNetwork: (newTargetNetwork: ChainWithAttributes) => void;
};

export const useGlobalState = create<GlobalState>(set => ({
nativeCurrencyPrice: 0,
setNativeCurrencyPrice: (newValue: number): void => set(() => ({ nativeCurrencyPrice: newValue })),
nativeCurrency: {
price: 0,
isFetching: true,
},
setNativeCurrencyPrice: (newValue: number): void =>
set(state => ({ nativeCurrency: { ...state.nativeCurrency, price: newValue } })),
setIsNativeCurrencyFetching: (newValue: boolean): void =>
set(state => ({ nativeCurrency: { ...state.nativeCurrency, isFetching: newValue } })),
targetNetwork: scaffoldConfig.targetNetworks[0],
setTargetNetwork: (newTargetNetwork: ChainWithAttributes) => set(() => ({ targetNetwork: newTargetNetwork })),
}));

0 comments on commit 18dd946

Please sign in to comment.