diff --git a/components/apps/peanut/claim/1.LinkDetails.tsx b/components/apps/peanut/claim/1.LinkDetails.tsx new file mode 100644 index 0000000..adb1fe2 --- /dev/null +++ b/components/apps/peanut/claim/1.LinkDetails.tsx @@ -0,0 +1,172 @@ +import {useEffect, useState} from 'react'; +import {IconSpinner} from '@icons/IconSpinner'; +import {CHAIN_DETAILS, claimLinkGasless} from '@squirrel-labs/peanut-sdk'; +import {waitForTransaction} from '@wagmi/core'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; + +import {useClaimLinkPeanut} from './useClaimLinkPeanut'; + +import type {ReactElement} from 'react'; + +function ViewLinkDetails({onProceed}: {onProceed: VoidFunction}): ReactElement { + const {linkDetails, set_claimTxHash, claimTxHash} = useClaimLinkPeanut(); + const {address} = useWeb3(); + const [isClient, set_isClient] = useState(false); + const [isLoading, set_isLoading] = useState(false); + + /* + * Claim link + * Function that handles peanut functionality to claim a link. + * This function is the gasless function, it will not require any wallet interaction by the user. Only the link, and the address are required. + */ + async function onClaimLink(): Promise { + set_isLoading(true); + + if (!address) { + alert('Please connect a wallet to claim to.'); + set_isLoading(false); + return; + } + + try { + const claimLinkGaslessResp = await claimLinkGasless({ + link: linkDetails.link, + recipientAddress: address ? address.toString() : '', + APIKey: process.env.NEXT_PUBLIC_PEANUT_API_KEY ?? '' + }); + waitForTransaction({hash: claimLinkGaslessResp.txHash}); + set_claimTxHash(claimLinkGaslessResp.txHash); + set_isLoading(false); + onProceed(); + } catch (error) { + set_isLoading(false); + console.log('error', error); + } + } + + useEffect(() => { + set_isClient(true); + }, []); // to prevent hydration error + + return ( +
+
+
+
+ {'Here are some details:'} +
+
+ +
+
+

{'Chain:'}

+
+
+

+ {linkDetails.chainId ? ( + CHAIN_DETAILS[linkDetails.chainId]?.name + ) : ( + + )} +

+
+ +
+

{'Amount:'}

+
+
+

+ {linkDetails.tokenAmount ? ( + linkDetails.tokenAmount + ) : ( + + )} +

+
+
+

{'Token:'}

+
+
+

+ {linkDetails.tokenName ? ( + linkDetails.tokenName + ) : ( + + )} +

+
+
+

{'Address:'}

+
+
+

+ {linkDetails.tokenAddress ? ( + linkDetails.tokenAddress + ) : ( + + )} +

+
+
+ +
+
+ {isClient + ? linkDetails.claimed || claimTxHash + ? 'This link has already been claimed' + : address + ? 'You are claiming to ' + address + : 'Please connect a wallet to claim to.' + : ''} +
+ +
+ +
+
+
+
+ ); +} +export default ViewLinkDetails; diff --git a/components/apps/peanut/claim/2.ClaimSuccess.tsx b/components/apps/peanut/claim/2.ClaimSuccess.tsx new file mode 100644 index 0000000..391493d --- /dev/null +++ b/components/apps/peanut/claim/2.ClaimSuccess.tsx @@ -0,0 +1,38 @@ +import {useMemo} from 'react'; +import {CHAIN_DETAILS} from '@squirrel-labs/peanut-sdk'; +import ViewSectionHeading from '@common/ViewSectionHeading'; + +import {useClaimLinkPeanut} from './useClaimLinkPeanut'; + +import type {ReactElement} from 'react'; + +function ViewClaimSuccess(): ReactElement { + const {linkDetails, claimTxHash} = useClaimLinkPeanut(); + + const blockExplorerUrl = useMemo(() => { + return CHAIN_DETAILS[linkDetails.chainId]?.explorers[0].url; + }, [linkDetails.chainId, CHAIN_DETAILS]); + + return ( +
+
+ + +
+ +
+
+
+ ); +} +export default ViewClaimSuccess; diff --git a/components/apps/peanut/claim/useClaimLinkPeanut.tsx b/components/apps/peanut/claim/useClaimLinkPeanut.tsx new file mode 100644 index 0000000..eae2eef --- /dev/null +++ b/components/apps/peanut/claim/useClaimLinkPeanut.tsx @@ -0,0 +1,124 @@ +import React, {createContext, type Dispatch, type SetStateAction, useEffect, useMemo, useState} from 'react'; +import {useUpdateEffect} from '@react-hookz/web'; +import {getLinkDetails} from '@squirrel-labs/peanut-sdk'; +import {scrollToTargetAdjusted} from '@utils/animations'; +import {HEADER_HEIGHT} from '@utils/constants'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; + +export enum Step { + LINKDETAILS = 'link_details', + CLAIMSUCCESS = 'claim_success' +} + +export type TClaimLink = { + linkDetails: any; + set_linkDetails: Dispatch>; + currentStep: Step; + set_currentStep: Dispatch>; + claimTxHash: string; + set_claimTxHash: Dispatch>; + claimUrl: string; + set_claimUrl: Dispatch>; +}; + +const defaultProps: TClaimLink = { + linkDetails: {}, + set_linkDetails: (): void => undefined, + currentStep: Step.LINKDETAILS, + set_currentStep: (): void => undefined, + claimTxHash: '', + set_claimTxHash: (): void => undefined, + claimUrl: '', + set_claimUrl: (): void => undefined +}; + +const ClaimLinkPeanutContext = createContext(defaultProps); +export const ClaimLinkPeanutContextApp = ({children}: {children: React.ReactElement}): React.ReactElement => { + const {address, isActive, isWalletSafe, isWalletLedger, onConnect} = useWeb3(); + const [currentStep, set_currentStep] = useState(Step.LINKDETAILS); + const [linkDetails, set_linkDetails] = useState({}); + const [claimTxHash, set_claimTxHash] = useState(''); + const [claimUrl, set_claimUrl] = useState(''); + + /********************************************************************************************** + ** This effect is used to directly ask the user to connect its wallet if it's not connected + **********************************************************************************************/ + useEffect((): void => { + if (!isActive && !address) { + onConnect(); + return; + } + }, [address, isActive, onConnect]); + + /********************************************************************************************** + ** This effect is used to handle some UI transitions and sections jumps. Once the current step + ** changes, we need to scroll to the correct section. + ** This effect is ignored on mount but will be triggered on every update to set the correct + ** scroll position. + **********************************************************************************************/ + useUpdateEffect((): void => { + setTimeout((): void => { + let currentStepContainer; + const scalooor = document?.getElementById('scalooor'); + + if (currentStep === Step.LINKDETAILS) { + currentStepContainer = document?.getElementById('linkDetails'); + } else if (currentStep === Step.CLAIMSUCCESS) { + currentStepContainer = document?.getElementById('claimSuccess'); + } + const currentElementHeight = currentStepContainer?.offsetHeight; + if (scalooor?.style) { + scalooor.style.height = `calc(100vh - ${currentElementHeight}px - ${HEADER_HEIGHT}px + 36px)`; + } + if (currentStepContainer) { + scrollToTargetAdjusted(currentStepContainer); + } + }, 0); + }, [currentStep, isWalletLedger, isWalletSafe]); + + useEffect(() => { + if (claimUrl) { + peanutGetLinkDetails({claimUrl}); + } + }, [claimUrl]); + + /********************************************************************************************** + ** This function is used to get the details of the link (amount, token, chain). + **********************************************************************************************/ + async function peanutGetLinkDetails({claimUrl}: {claimUrl: string}): Promise { + try { + const linkDetails = await getLinkDetails({ + link: claimUrl + }); + console.log('linkDetails', linkDetails); + set_linkDetails(linkDetails); + } catch (error) { + console.error(error); + } + } + + const contextValue = useMemo( + (): TClaimLink => ({ + currentStep, + set_currentStep, + linkDetails, + set_linkDetails, + claimTxHash, + set_claimTxHash, + claimUrl, + set_claimUrl + }), + [currentStep, set_currentStep, linkDetails, set_linkDetails, claimTxHash, set_claimTxHash] + ); + + return ( + +
+ {children} +
+
+ + ); +}; + +export const useClaimLinkPeanut = (): TClaimLink => React.useContext(ClaimLinkPeanutContext); diff --git a/components/apps/peanut/create/1.ViewChainToSend.tsx b/components/apps/peanut/create/1.ViewChainToSend.tsx new file mode 100644 index 0000000..f6ef3cc --- /dev/null +++ b/components/apps/peanut/create/1.ViewChainToSend.tsx @@ -0,0 +1,48 @@ +import {NetworkSelector} from 'components/common/HeaderElements'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import ViewSectionHeading from '@common/ViewSectionHeading'; + +import type {ReactElement} from 'react'; + +function ViewChainToSend({onProceed}: {onProceed: VoidFunction}): ReactElement { + return ( +
+
+ + +
+
=> e.preventDefault()} + className={ + 'grid w-full grid-cols-12 flex-row items-center justify-between gap-4 md:w-3/4 md:gap-6' + }> +
+
+ +
{' '} +
+
+ +
+
+
+
+
+ ); +} +export default ViewChainToSend; diff --git a/components/apps/peanut/create/2.ViewTokenToSend.tsx b/components/apps/peanut/create/2.ViewTokenToSend.tsx new file mode 100644 index 0000000..a1e81f1 --- /dev/null +++ b/components/apps/peanut/create/2.ViewTokenToSend.tsx @@ -0,0 +1,142 @@ +import React, {useState} from 'react'; +import ComboboxAddressInput from 'components/common/ComboboxAddressInput'; +import {useTokenList} from 'contexts/useTokenList'; +import {Step} from '@disperse/useDisperse'; +import {useDeepCompareEffect, useUpdateEffect} from '@react-hookz/web'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useChainID} from '@yearn-finance/web-lib/hooks/useChainID'; +import {isZeroAddress, toAddress} from '@yearn-finance/web-lib/utils/address'; +import {ETH_TOKEN_ADDRESS, ZERO_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; +import {getNetwork} from '@yearn-finance/web-lib/utils/wagmi/utils'; +import ViewSectionHeading from '@common/ViewSectionHeading'; + +import {useCreateLinkPeanut} from './useCreateLinkPeanut'; + +import type {ReactElement} from 'react'; +import type {TDict} from '@yearn-finance/web-lib/types'; +import type {TToken} from '@utils/types/types'; + +function ViewTokenToSend({onProceed}: {onProceed: VoidFunction}): ReactElement { + const {safeChainID} = useChainID(); + const {currentStep, tokenToSend, set_tokenToSend} = useCreateLinkPeanut(); + const {tokenList} = useTokenList(); + const [localTokenToSend, set_localTokenToSend] = useState(ETH_TOKEN_ADDRESS); + const [isValidTokenToReceive, set_isValidTokenToReceive] = useState(true); + const [possibleTokenToReceive, set_possibleTokenToReceive] = useState>({}); + + /* 🔵 - Yearn Finance ************************************************************************** + ** On mount, fetch the token list from the tokenlistooor repo for the cowswap token list, which + ** will be used to populate the tokenToDisperse token combobox. + ** Only the tokens in that list will be displayed as possible destinations. + **********************************************************************************************/ + useDeepCompareEffect((): void => { + const possibleDestinationsTokens: TDict = {}; + const {wrappedToken} = getNetwork(safeChainID).contracts; + if (wrappedToken) { + possibleDestinationsTokens[ETH_TOKEN_ADDRESS] = { + address: ETH_TOKEN_ADDRESS, + chainID: safeChainID, + name: wrappedToken.coinName, + symbol: wrappedToken.coinSymbol, + decimals: wrappedToken.decimals, + logoURI: `${process.env.SMOL_ASSETS_URL}/token/${safeChainID}/${ETH_TOKEN_ADDRESS}/logo-128.png` + }; + } + for (const eachToken of Object.values(tokenList)) { + if (eachToken.chainID === safeChainID) { + possibleDestinationsTokens[toAddress(eachToken.address)] = eachToken; + } + } + set_possibleTokenToReceive(possibleDestinationsTokens); + }, [tokenList, safeChainID]); + + /* 🔵 - Yearn Finance ************************************************************************** + ** When the tokenToDisperse token changes, check if it is a valid tokenToDisperse token. The check is + ** trivial as we only check if the address is valid. + **********************************************************************************************/ + useUpdateEffect((): void => { + set_isValidTokenToReceive('undetermined'); + if (!isZeroAddress(toAddress(localTokenToSend))) { + set_isValidTokenToReceive(true); + } + }, [tokenToSend]); + + return ( +
+
+ +
+
=> e.preventDefault()} + className={ + 'grid w-full grid-cols-12 flex-row items-center justify-between gap-4 md:w-3/4 md:gap-6' + }> +
+ { + if ([Step.SELECTOR].includes(currentStep)) { + set_localTokenToSend(newToken); + set_tokenToSend({ + address: toAddress(newToken as string), + chainID: safeChainID, + name: possibleTokenToReceive[toAddress(newToken as string)]?.name || '', + symbol: possibleTokenToReceive[toAddress(newToken as string)]?.symbol || '', + decimals: + possibleTokenToReceive[toAddress(newToken as string)]?.decimals || 0, + logoURI: + possibleTokenToReceive[toAddress(newToken as string)]?.logoURI || '' + }); + } else { + set_localTokenToSend(newToken); + set_tokenToSend({ + address: toAddress(newToken as string), + chainID: safeChainID, + name: possibleTokenToReceive[toAddress(newToken as string)]?.name || '', + symbol: possibleTokenToReceive[toAddress(newToken as string)]?.symbol || '', + decimals: + possibleTokenToReceive[toAddress(newToken as string)]?.decimals || 0, + logoURI: + possibleTokenToReceive[toAddress(newToken as string)]?.logoURI || '' + }); + } + }} + /> +
+
+ +
+
+
+
+
+ ); +} + +export default ViewTokenToSend; diff --git a/components/apps/peanut/create/3.ViewAmountToSend.tsx b/components/apps/peanut/create/3.ViewAmountToSend.tsx new file mode 100644 index 0000000..9130834 --- /dev/null +++ b/components/apps/peanut/create/3.ViewAmountToSend.tsx @@ -0,0 +1,231 @@ +import React, {memo, useCallback, useMemo, useState} from 'react'; +import {useWallet} from 'contexts/useWallet'; +import {handleInputChangeEventValue} from 'utils/handleInputChangeEventValue'; +import {IconSpinner} from '@icons/IconSpinner'; +import {CHAIN_DETAILS, getLinksFromTx, getRandomString, prepareDepositTxs} from '@squirrel-labs/peanut-sdk'; +import {prepareSendTransaction, sendTransaction, waitForTransaction} from '@wagmi/core'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {useChainID} from '@yearn-finance/web-lib/hooks/useChainID'; +import {isZeroAddress, toAddress} from '@yearn-finance/web-lib/utils/address'; +import {formatAmount} from '@yearn-finance/web-lib/utils/format.number'; + +import {useCreateLinkPeanut} from './useCreateLinkPeanut'; + +import type {ChangeEvent, ReactElement} from 'react'; +import type {TNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import type {TToken} from '@utils/types/types'; + +function AmountToSendInput({ + token, + amount, + onChange +}: { + token: TToken | undefined; + amount: TNormalizedBN | undefined; + onChange: (amount: TNormalizedBN) => void; +}): ReactElement { + /********************************************************************************************** + ** onInputChange is triggered when the user is typing in the input field. It updates the + ** amount in the state and triggers the debounced retrieval of the quote from the Cowswap API. + ** It is set as callback to avoid unnecessary re-renders. + **********************************************************************************************/ + const onInputChange = useCallback( + (e: ChangeEvent): void => { + onChange(handleInputChangeEventValue(e, token?.decimals || 18)); + }, + [onChange, token?.decimals] + ); + + return ( +
+
+ e.preventDefault()} + min={0} + step={1 / 10 ** (token?.decimals || 18)} + inputMode={'numeric'} + placeholder={'0'} + pattern={'^((?:0|[1-9]+)(?:.(?:d+?[1-9]|[1-9]))?)$'} + onChange={onInputChange} + value={amount?.normalized} + /> +
+
+ ); +} + +const ViewAmountToSend = memo(function ViewAmountToSend({onProceed}: {onProceed: VoidFunction}): ReactElement { + const {safeChainID} = useChainID(); + const {address} = useWeb3(); + const {balances} = useWallet(); + const {tokenToSend, amountToSend, set_amountToSend, createdLink, set_createdLink, onResetCreateLink} = + useCreateLinkPeanut(); + const [loadingStates, set_loadingStates] = useState<'idle' | 'Confirm in wallet' | 'Creating'>('idle'); + + const isLoading = useMemo(() => loadingStates !== 'idle', [loadingStates]); + + const balanceOf = useMemo((): number => { + if (isZeroAddress(tokenToSend?.address)) { + return 0; + } + const balance = balances?.[toAddress(tokenToSend?.address)]?.normalized; + return balance || 0; + }, [balances, tokenToSend]); + + const isAboveBalance = (Number(amountToSend?.normalized) ?? 0) > balanceOf; + + /* + * Function to handle the creation of a link. ChainId, tokenAmount and tokenAddress are required. + * Done using advanced implementation (prepare, signing and getting the link are done seperately) + */ + const onCreateLink = async (): Promise => { + try { + set_loadingStates('Creating'); + const tokenType = + CHAIN_DETAILS[safeChainID as keyof typeof CHAIN_DETAILS]?.nativeCurrency.symbol == tokenToSend.symbol + ? 0 + : 1; + + const linkDetails = { + chainId: safeChainID.toString(), + tokenAmount: Number(amountToSend?.normalized), + tokenAddress: tokenToSend?.address, + tokenDecimals: tokenToSend?.decimals, + tokenType: tokenType, + baseUrl: `${window.location.href}/claim`, + trackId: 'smoldapp' + }; + + const password = await getRandomString(16); + + const preparedTxs = await prepareDepositTxs({ + address: address ?? '', + linkDetails: linkDetails, + passwords: [password] + }); + const signedTxsResponse = []; + + for (const tx of preparedTxs.unsignedTxs) { + set_loadingStates('Confirm in wallet'); + + const config = await prepareSendTransaction({ + to: tx.to ?? undefined, + data: (tx.data as `0x${string}`) ?? undefined, + value: tx.value?.valueOf() ?? undefined + }); + const sendTxResponse = await sendTransaction(config); + set_loadingStates('Creating'); + await waitForTransaction({hash: sendTxResponse.hash, confirmations: 4}); + + signedTxsResponse.push(sendTxResponse); + } + + const getLinkFromTxResponse = await getLinksFromTx({ + linkDetails: linkDetails, + txHash: signedTxsResponse[signedTxsResponse.length - 1].hash, + passwords: [password] + }); + + set_createdLink({ + link: getLinkFromTxResponse.links[0], + hash: signedTxsResponse[signedTxsResponse.length - 1].hash + }); + set_loadingStates('idle'); + onProceed(); + } catch (error) { + console.error(error); + set_loadingStates('idle'); + } + }; + + return ( +
+
+
+
+ {'How much do you want to send?'} +

+ {'Drop the amount of tokens you want the link to hold.'} +

+
+
+ +
+
+

{'Amount'}

+
+
+ { + set_amountToSend(amount); + }} + /> +
+
+
+
+
+
{'You have'}
+ +
+ {`${formatAmount(balanceOf, tokenToSend?.decimals || 18)} ${tokenToSend?.symbol || ''}`} +
+
+
+
{'You are sending'}
+ +
+ {`${formatAmount(amountToSend?.normalized ?? '0', tokenToSend?.decimals || 18)} ${ + tokenToSend?.symbol || '' + }`} +
+
+
+
+ +
+
+
+
+ ); +}); + +export default ViewAmountToSend; diff --git a/components/apps/peanut/create/4.ViewSuccesToSend.tsx b/components/apps/peanut/create/4.ViewSuccesToSend.tsx new file mode 100644 index 0000000..3d91097 --- /dev/null +++ b/components/apps/peanut/create/4.ViewSuccesToSend.tsx @@ -0,0 +1,55 @@ +import {useMemo} from 'react'; +import {CHAIN_DETAILS} from '@squirrel-labs/peanut-sdk'; +import {useChainID} from '@yearn-finance/web-lib/hooks/useChainID'; + +import {useCreateLinkPeanut} from './useCreateLinkPeanut'; + +import type {ReactElement} from 'react'; + +function ViewSuccesToSend(): ReactElement { + const {createdLink} = useCreateLinkPeanut(); + const {safeChainID} = useChainID(); + + const blockExplorerUrl = useMemo(() => { + return CHAIN_DETAILS[safeChainID]?.explorers[0].url; + }, [safeChainID, CHAIN_DETAILS]); + + return ( +
+
+
+
+ {'The link has been created!'} +
+

+ {'Click'} +

+ + {'here'} + +

+ {'to see the transaction confirmation.'} +

+
+
+
+ +
+
+ ); +} +export default ViewSuccesToSend; diff --git a/components/apps/peanut/create/Logo.tsx b/components/apps/peanut/create/Logo.tsx new file mode 100644 index 0000000..d0e9627 --- /dev/null +++ b/components/apps/peanut/create/Logo.tsx @@ -0,0 +1,119 @@ +import React from 'react'; + +import type {ReactElement} from 'react'; + +function LogoPeanutCreator(props: React.SVGProps): ReactElement { + const st0 = {fill: '#FFFFFF', stroke: '#000000', strokeWidth: 12, strokeMiterlimit: 10}; + const st2 = {fillOpacity: 0, stroke: '#000000', strokeWidth: 12}; + const st4 = {fill: '#000000'}; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default LogoPeanutCreator; diff --git a/components/apps/peanut/create/useCreateLinkPeanut.tsx b/components/apps/peanut/create/useCreateLinkPeanut.tsx new file mode 100644 index 0000000..c518e72 --- /dev/null +++ b/components/apps/peanut/create/useCreateLinkPeanut.tsx @@ -0,0 +1,142 @@ +import React, {createContext, type Dispatch, type SetStateAction, useEffect, useMemo, useState} from 'react'; +import {useUpdateEffect} from '@react-hookz/web'; +import {scrollToTargetAdjusted} from '@utils/animations'; +import {HEADER_HEIGHT} from '@utils/constants'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {ETH_TOKEN_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; +import {getNetwork} from '@yearn-finance/web-lib/utils/wagmi/utils'; + +import type {TNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import type {TToken} from '@utils/types/types'; + +export enum Step { + TOSENDCHAIN = 'destination_chain', + TOSENDTOKEN = 'destination_token', + TOSENDAMOUNT = 'destination_amount', + TOSENDSUCCESS = 'destination_success' +} + +export type TCreatedLink = { + hash: string; + link: string; +}; + +export type TSelected = { + tokenToSend: TToken; + currentStep: Step; + amountToSend: TNormalizedBN | undefined; + createdLink: TCreatedLink; + set_createdLink: Dispatch>; + set_tokenToSend: Dispatch>; + set_currentStep: Dispatch>; + set_amountToSend: Dispatch>; + onResetCreateLink: () => void; +}; + +const {wrappedToken: defaultTokens} = getNetwork(1).contracts; +const defaultProps: TSelected = { + tokenToSend: { + address: ETH_TOKEN_ADDRESS, + chainID: 1, + name: defaultTokens?.coinName || 'Ether', + symbol: defaultTokens?.coinSymbol || 'ETH', + decimals: defaultTokens?.decimals || 18, + logoURI: `${process.env.SMOL_ASSETS_URL}/token/1/${ETH_TOKEN_ADDRESS}/logo-128.png` + }, + currentStep: Step.TOSENDCHAIN, + amountToSend: undefined, + createdLink: { + hash: '', + link: '' + }, + set_createdLink: (): void => undefined, + set_tokenToSend: (): void => undefined, + set_currentStep: (): void => undefined, + set_amountToSend: (): void => undefined, + onResetCreateLink: (): void => undefined +}; + +const CreateLinkPeanutContext = createContext(defaultProps); +export const CreateLinkPeanutContextApp = ({children}: {children: React.ReactElement}): React.ReactElement => { + const {address, isActive, isWalletSafe, isWalletLedger, onConnect} = useWeb3(); + const [currentStep, set_currentStep] = useState(Step.TOSENDCHAIN); + const [tokenToSend, set_tokenToSend] = useState(defaultProps.tokenToSend); + const [createdLink, set_createdLink] = useState(defaultProps.createdLink); + + const [amountToSend, set_amountToSend] = useState(undefined); + + const onResetCreateLink = (): void => { + setTimeout(() => { + set_currentStep(Step.TOSENDCHAIN); + set_tokenToSend(defaultProps.tokenToSend); + set_amountToSend(undefined); + set_createdLink(defaultProps.createdLink); + }, 500); + }; + + /********************************************************************************************** + ** This effect is used to directly ask the user to connect its wallet if it's not connected + **********************************************************************************************/ + useEffect((): void => { + if (!isActive && !address) { + onConnect(); + return; + } + }, [address, isActive, onConnect]); + + /********************************************************************************************** + ** This effect is used to handle some UI transitions and sections jumps. Once the current step + ** changes, we need to scroll to the correct section. + ** This effect is ignored on mount but will be triggered on every update to set the correct + ** scroll position. + **********************************************************************************************/ + useUpdateEffect((): void => { + setTimeout((): void => { + let currentStepContainer; + const scalooor = document?.getElementById('scalooor'); + + if (currentStep === Step.TOSENDCHAIN) { + currentStepContainer = document?.getElementById('chainToSend'); + } else if (currentStep === Step.TOSENDTOKEN) { + currentStepContainer = document?.getElementById('tokenToSend'); + } else if (currentStep === Step.TOSENDAMOUNT) { + currentStepContainer = document?.getElementById('amountToSend'); + } else if (currentStep === Step.TOSENDSUCCESS) { + currentStepContainer = document?.getElementById('successToSend'); + } + const currentElementHeight = currentStepContainer?.offsetHeight; + if (scalooor?.style) { + scalooor.style.height = `calc(100vh - ${currentElementHeight}px - ${HEADER_HEIGHT}px + 36px)`; + } + if (currentStepContainer) { + scrollToTargetAdjusted(currentStepContainer); + } + }, 0); + }, [currentStep, isWalletLedger, isWalletSafe]); + + const contextValue = useMemo( + (): TSelected => ({ + currentStep, + set_currentStep, + tokenToSend, + set_tokenToSend, + amountToSend, + set_amountToSend, + onResetCreateLink, + createdLink, + set_createdLink + }), + [currentStep, tokenToSend, amountToSend] + ); + + return ( + +
+ {children} +
+
+ + ); +}; + +export const useCreateLinkPeanut = (): TSelected => React.useContext(CreateLinkPeanutContext); diff --git a/components/common/HeaderElements.tsx b/components/common/HeaderElements.tsx index 821d0c6..00421ee 100644 --- a/components/common/HeaderElements.tsx +++ b/components/common/HeaderElements.tsx @@ -1,10 +1,10 @@ import React, {Fragment, useEffect, useMemo, useState} from 'react'; import Image from 'next/image'; -import {usePublicClient} from 'wagmi'; +import assert from 'assert'; +import {useConnect, usePublicClient} from 'wagmi'; import {Listbox, Transition} from '@headlessui/react'; import {useAccountModal, useChainModal} from '@rainbow-me/rainbowkit'; import {useIsMounted} from '@react-hookz/web'; -import {SUPPORTED_SMOL_CHAINS} from '@utils/constants'; import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; import {toSafeChainID} from '@yearn-finance/web-lib/hooks/useChainID'; import {IconChevronBottom} from '@yearn-finance/web-lib/icons/IconChevronBottom'; @@ -40,7 +40,17 @@ function NetworkButton({ ); } -function CurrentNetworkButton({label, value, isOpen}: {label: string; value: number; isOpen: boolean}): ReactElement { +function CurrentNetworkButton({ + label, + value, + isOpen, + fullWidth +}: { + label: string; + value: number; + isOpen: boolean; + fullWidth?: boolean; +}): ReactElement { const [src, set_src] = useState(undefined); useEffect((): void => { @@ -52,7 +62,10 @@ function CurrentNetworkButton({label, value, isOpen}: {label: string; value: num return ( + className={ + 'yearn--header-nav-item flex flex-row items-center border-0 p-0 text-xs md:flex md:text-sm ' + + (fullWidth ? ' justify-between w-full ' : '') + }>
@@ -73,7 +86,7 @@ function CurrentNetworkButton({label, value, isOpen}: {label: string; value: num
@@ -81,18 +94,25 @@ function CurrentNetworkButton({label, value, isOpen}: {label: string; value: num } type TNetwork = {value: number; label: string}; -export function NetworkSelector({networks}: {networks: number[]}): ReactElement { +export function NetworkSelector({networks, fullWidth}: {networks: number[]; fullWidth?: boolean}): ReactElement { const {onSwitchChain} = useWeb3(); const publicClient = usePublicClient(); + const {connectors} = useConnect(); const safeChainID = toSafeChainID(publicClient?.chain.id, Number(process.env.BASE_CHAINID)); const isMounted = useIsMounted(); const supportedNetworks = useMemo((): TNetwork[] => { - return SUPPORTED_SMOL_CHAINS.filter( - ({id}): boolean => - ![5, 1337, 84531].includes(id) && ((networks.length > 0 && networks.includes(id)) || true) - ).map((network: Chain): TNetwork => ({value: network.id, label: network.name})); - }, [networks]); + const injectedConnector = connectors.find((e): boolean => e.id.toLocaleLowerCase() === 'injected'); + assert(injectedConnector, 'No injected connector found'); + const chainsForInjected = injectedConnector.chains; + + return chainsForInjected + .filter( + ({id}): boolean => + ![5, 1337, 84531].includes(id) && ((networks.length > 0 && networks.includes(id)) || true) + ) + .map((network: Chain): TNetwork => ({value: network.id, label: network.name})); + }, [connectors, networks]); const currentNetwork = useMemo( (): TNetwork | undefined => supportedNetworks.find((network): boolean => network.value === safeChainID), @@ -131,7 +151,7 @@ export function NetworkSelector({networks}: {networks: number[]}): ReactElement } return ( -
+
onSwitchChain((value as {value: number}).value)}> @@ -141,6 +161,7 @@ export function NetworkSelector({networks}: {networks: number[]}): ReactElement label={currentNetwork?.label || 'Ethereum'} value={currentNetwork?.value || 1} isOpen={open} + fullWidth={fullWidth} />
{supportedNetworks.map( (network): ReactElement => ( @@ -243,7 +266,7 @@ export function WalletSelector(): ReactElement { walletIdentity ) : ( - +