Skip to content

Commit

Permalink
feat: useDisplayUsdMode hook
Browse files Browse the repository at this point in the history
  • Loading branch information
rin-st committed Jun 3, 2024
1 parent 4819cb2 commit 14aa5c9
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 63 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
13 changes: 9 additions & 4 deletions packages/nextjs/components/ScaffoldEthAppWithProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ import { useGlobalState } from "~~/services/store/store";
import { wagmiConfig } from "~~/services/web3/wagmiConfig";

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

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

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

return (
<>
Expand Down
22 changes: 7 additions & 15 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 price = useGlobalState(state => state.nativeCurrency.price);
const isPriceFetching = 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 || (isPriceFetching && price === 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,7 +54,7 @@ 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 ? (
Expand Down
30 changes: 11 additions & 19 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,21 @@ 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);

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 +76,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,24 +91,18 @@ 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={`${
Expand All @@ -122,8 +114,8 @@ export const EtherInput = ({
>
<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
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 };
};
27 changes: 13 additions & 14 deletions packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useTargetNetwork } from "./useTargetNetwork";
import { useInterval } from "usehooks-ts";
import scaffoldConfig from "~~/scaffold.config";
Expand All @@ -11,24 +11,23 @@ const enablePolling = false;
*/
export const useNativeCurrencyPrice = () => {
const { targetNetwork } = useTargetNetwork();
const [isFetching, setIsFetching] = useState(true);
const [nativeCurrencyPrice, setNativeCurrencyPrice] = useState(0);

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

// Get the price of ETH from Uniswap on mount
useEffect(() => {
(async () => {
const price = await fetchPriceFromUniswap(targetNetwork);
setNativeCurrencyPrice(price);
})();
}, [targetNetwork]);
fetchPrice();
}, [fetchPrice]);

// Get the price of ETH from Uniswap at a given interval
useInterval(
async () => {
const price = await fetchPriceFromUniswap(targetNetwork);
setNativeCurrencyPrice(price);
},
enablePolling ? scaffoldConfig.pollingInterval : null,
);
useInterval(fetchPrice, enablePolling ? scaffoldConfig.pollingInterval : null);

return nativeCurrencyPrice;
return { nativeCurrencyPrice, isFetching };
};
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 14aa5c9

Please sign in to comment.