From 17d6b579a986f62f6d96f6b7e190da1b3118712a Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 21 Jan 2025 22:07:51 -0800 Subject: [PATCH 001/115] send scaffolding --- package.json | 6 ++ .../nextjs-app-router/components/Demo.tsx | 2 + .../components/demo/Send.tsx | 5 + .../components/form/active-component.tsx | 3 + .../nextjs-app-router/onchainkit/package.json | 6 ++ .../nextjs-app-router/types/onchainkit.ts | 1 + src/send/components/AddressInput.tsx | 37 ++++++++ src/send/components/Send.tsx | 53 +++++++++++ src/send/components/SendHeader.tsx | 20 ++++ src/send/components/SendProvider.tsx | 92 +++++++++++++++++++ src/send/index.ts | 1 + 11 files changed, 226 insertions(+) create mode 100644 playground/nextjs-app-router/components/demo/Send.tsx create mode 100644 src/send/components/AddressInput.tsx create mode 100644 src/send/components/Send.tsx create mode 100644 src/send/components/SendHeader.tsx create mode 100644 src/send/components/SendProvider.tsx create mode 100644 src/send/index.ts diff --git a/package.json b/package.json index aa6a89e914..ea0e27be58 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,12 @@ "import": "./esm/nft/components/mint/index.js", "default": "./esm/nft/components/mint/index.js" }, + "./send": { + "types": "./esm/send/index.d.ts", + "module": "./esm/send/index.js", + "import": "./esm/send/index.js", + "default": "./esm/send/index.js" + }, "./swap": { "types": "./esm/swap/index.d.ts", "module": "./esm/swap/index.js", diff --git a/playground/nextjs-app-router/components/Demo.tsx b/playground/nextjs-app-router/components/Demo.tsx index 498d195e55..b4de5e88da 100644 --- a/playground/nextjs-app-router/components/Demo.tsx +++ b/playground/nextjs-app-router/components/Demo.tsx @@ -23,6 +23,7 @@ import WalletDemo from './demo/Wallet'; import WalletAdvancedDefaultDemo from './demo/WalletAdvancedDefault'; import WalletDefaultDemo from './demo/WalletDefault'; import WalletIslandDemo from './demo/WalletIsland'; +import SendDemo from '@/components/demo/Send'; const activeComponentMapping: Record = { [OnchainKitComponent.FundButton]: FundButtonDemo, @@ -43,6 +44,7 @@ const activeComponentMapping: Record = { [OnchainKitComponent.NFTMintCardDefault]: NFTMintCardDefaultDemo, [OnchainKitComponent.NFTCardDefault]: NFTCardDefaultDemo, [OnchainKitComponent.IdentityCard]: IdentityCardDemo, + [OnchainKitComponent.Send]: SendDemo, }; export default function Demo() { diff --git a/playground/nextjs-app-router/components/demo/Send.tsx b/playground/nextjs-app-router/components/demo/Send.tsx new file mode 100644 index 0000000000..797358ebb6 --- /dev/null +++ b/playground/nextjs-app-router/components/demo/Send.tsx @@ -0,0 +1,5 @@ +import { Send } from '@coinbase/onchainkit/send'; + +export default function SendDemo() { + return ; +} diff --git a/playground/nextjs-app-router/components/form/active-component.tsx b/playground/nextjs-app-router/components/form/active-component.tsx index 292f831ab5..fd7f15577e 100644 --- a/playground/nextjs-app-router/components/form/active-component.tsx +++ b/playground/nextjs-app-router/components/form/active-component.tsx @@ -68,6 +68,9 @@ export function ActiveComponent() { NFT Mint Card Default + + Send + diff --git a/playground/nextjs-app-router/onchainkit/package.json b/playground/nextjs-app-router/onchainkit/package.json index 876438b38a..6e02ce9867 100644 --- a/playground/nextjs-app-router/onchainkit/package.json +++ b/playground/nextjs-app-router/onchainkit/package.json @@ -169,6 +169,12 @@ "import": "./esm/nft/components/mint/index.js", "default": "./esm/nft/components/mint/index.js" }, + "./send": { + "types": "./esm/send/index.d.ts", + "module": "./esm/send/index.js", + "import": "./esm/send/index.js", + "default": "./esm/send/index.js" + }, "./swap": { "types": "./esm/swap/index.d.ts", "module": "./esm/swap/index.js", diff --git a/playground/nextjs-app-router/types/onchainkit.ts b/playground/nextjs-app-router/types/onchainkit.ts index 00d1c8a5b3..cb98b6b74c 100644 --- a/playground/nextjs-app-router/types/onchainkit.ts +++ b/playground/nextjs-app-router/types/onchainkit.ts @@ -17,6 +17,7 @@ export enum OnchainKitComponent { NFTCardDefault = 'nft-card-default', NFTMintCard = 'nft-mint-card', NFTMintCardDefault = 'nft-mint-card-default', + Send = 'send', } export enum TransactionTypes { diff --git a/src/send/components/AddressInput.tsx b/src/send/components/AddressInput.tsx new file mode 100644 index 0000000000..ab874db713 --- /dev/null +++ b/src/send/components/AddressInput.tsx @@ -0,0 +1,37 @@ +import { TextInput } from '@/internal/components/TextInput'; +import { useSendContext } from '@/send/components/SendProvider'; +import { border, cn, color } from '@/styles/theme'; + +/*** + delayMs?: number; + disabled?: boolean; + onBlur?: () => void; + inputValidator?: (s: string) => boolean; +}; + */ + +export function AddressInput() { + const { recipientInput, setRecipientInput } = useSendContext(); + + return ( +
+ To + +
+ ); +} diff --git a/src/send/components/Send.tsx b/src/send/components/Send.tsx new file mode 100644 index 0000000000..8605dd3f92 --- /dev/null +++ b/src/send/components/Send.tsx @@ -0,0 +1,53 @@ +import { useTheme } from '@/core-react/internal/hooks/useTheme'; +import { background, border, cn, color } from '@/styles/theme'; +import type { ReactNode } from 'react'; +import { SendHeader } from './SendHeader'; +import { SendProvider } from './SendProvider'; +import { AddressInput } from '@/send/components/AddressInput'; + +type SendReact = { + children?: ReactNode; + className?: string; +}; + +export function Send({ children, className }: SendReact) { + const componentTheme = useTheme(); + + if (!children) { + return ( + + + + + + + ); + } + + return ( + + + {children} + + + ); +} + +function SendContent({ children, className }: SendReact) { + return ( +
+ {children} +
+ ); +} diff --git a/src/send/components/SendHeader.tsx b/src/send/components/SendHeader.tsx new file mode 100644 index 0000000000..fc659b3fc1 --- /dev/null +++ b/src/send/components/SendHeader.tsx @@ -0,0 +1,20 @@ +import { cn, text } from '@/styles/theme'; +import type { ReactNode } from 'react'; + +export function SendHeader({ + label = 'Send', + leftContent, + rightContent, +}: { + label?: string; + leftContent?: ReactNode; + rightContent?: ReactNode; +}) { + return ( +
+
{leftContent}
+
{label}
+
{rightContent}
+
+ ); +} diff --git a/src/send/components/SendProvider.tsx b/src/send/components/SendProvider.tsx new file mode 100644 index 0000000000..f2554553e4 --- /dev/null +++ b/src/send/components/SendProvider.tsx @@ -0,0 +1,92 @@ +import { useValue } from '@/core-react/internal/hooks/useValue'; +import { usePortfolioTokenBalances } from '@/core-react/wallet/hooks/usePortfolioTokenBalances'; +import type { PortfolioTokenWithFiatValue } from '@/core/api/types'; +import { useGetETHBalance } from '@/wallet/hooks/useGetETHBalance'; +// import { useWalletAdvancedContext } from '@/wallet/components/WalletAdvancedProvider'; +// import { useWalletContext } from '@/wallet/components/WalletProvider'; +import { + createContext, + type Dispatch, + type ReactNode, + type SetStateAction, + useContext, + useState, +} from 'react'; +import type { Address, Chain } from 'viem'; +import { useAccount } from 'wagmi'; + +type SendContextType = { + address: Address | undefined; + chain: Chain | undefined; + ethBalance: number | undefined; + tokenBalances: PortfolioTokenWithFiatValue[] | undefined; + recipientInput: string | null; + setRecipientInput: Dispatch>; + recipientAddress: Address | null; + setRecipientAddress: Dispatch>; +}; + +type SendProviderReact = { + children: ReactNode; + address?: Address; + chain?: Chain; + tokenBalances?: PortfolioTokenWithFiatValue[]; + ethBalance?: number; +}; + +const emptyContext = {} as SendContextType; + +const SendContext = createContext(emptyContext); + +export function useSendContext() { + return useContext(SendContext); +} + +export function SendProvider({ + children, + address, + chain, + tokenBalances, +}: SendProviderReact) { + // const { address: senderAddress, chain: senderChain } = useWalletContext() ?? {}; + // const { tokenBalances: senderTokenBalances } = useWalletAdvancedContext() ?? {}; + const [recipientInput, setRecipientInput] = useState(null); + const [recipientAddress, setRecipientAddress] = useState
( + null, + ); + + let ethBalance: number | undefined; + const { response } = useGetETHBalance(address); + if (response?.data?.value) { + ethBalance = Number(response.data.value) / 10 ** response.data.decimals; + } + + let senderAddress = address; + let senderChain = chain; + if (!senderAddress || !senderChain) { + const { address: userAddress, chain: userChain } = useAccount(); + senderAddress = userAddress; + senderChain = userChain; + } + + let senderTokenBalances = tokenBalances; + if (!senderTokenBalances) { + const { data } = usePortfolioTokenBalances({ + address: senderAddress, + }); + senderTokenBalances = data?.tokenBalances ?? []; + } + + const value = useValue({ + address: senderAddress, + chain: senderChain, + tokenBalances: senderTokenBalances, + ethBalance, + recipientInput, + setRecipientInput, + recipientAddress, + setRecipientAddress, + }); + + return {children}; +} diff --git a/src/send/index.ts b/src/send/index.ts new file mode 100644 index 0000000000..d11e0b3251 --- /dev/null +++ b/src/send/index.ts @@ -0,0 +1 @@ +export { Send } from './components/Send'; From 29ce8e2a3275facb762cfc1b4013f15b7ad2fa57 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 21 Jan 2025 22:18:22 -0800 Subject: [PATCH 002/115] remove ethbalance from provider props --- src/send/components/SendProvider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/send/components/SendProvider.tsx b/src/send/components/SendProvider.tsx index f2554553e4..4c7d5268e8 100644 --- a/src/send/components/SendProvider.tsx +++ b/src/send/components/SendProvider.tsx @@ -31,7 +31,6 @@ type SendProviderReact = { address?: Address; chain?: Chain; tokenBalances?: PortfolioTokenWithFiatValue[]; - ethBalance?: number; }; const emptyContext = {} as SendContextType; From d5f399f4b12c1df7fb33ee6c5d1b37835e33f361 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 22 Jan 2025 15:03:28 -0800 Subject: [PATCH 003/115] update utils --- src/identity/utils/getName.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/identity/utils/getName.ts b/src/identity/utils/getName.ts index b772f834e1..fed8f0ff12 100644 --- a/src/identity/utils/getName.ts +++ b/src/identity/utils/getName.ts @@ -29,9 +29,10 @@ export const getName = async ({ const client = getChainPublicClient(chain); if (chainIsBase) { + const baseClient = getChainPublicClient(chain); const addressReverseNode = convertReverseNodeToBytes(address, base.id); try { - const basename = await client.readContract({ + const basename = await baseClient.readContract({ abi: L2ResolverAbi, address: RESOLVER_ADDRESSES_BY_CHAIN_ID[chain.id], functionName: 'name', From cb646396e536eb6747234db67ae661dc73925e42 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 22 Jan 2025 15:04:04 -0800 Subject: [PATCH 004/115] refactor to remove sendprovider dep --- src/send/components/AddressInput.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/send/components/AddressInput.tsx b/src/send/components/AddressInput.tsx index ab874db713..828d9f7967 100644 --- a/src/send/components/AddressInput.tsx +++ b/src/send/components/AddressInput.tsx @@ -1,6 +1,6 @@ import { TextInput } from '@/internal/components/TextInput'; -import { useSendContext } from '@/send/components/SendProvider'; -import { border, cn, color } from '@/styles/theme'; +import { background, border, cn, color } from '@/styles/theme'; +import type { Dispatch, SetStateAction } from 'react'; /*** delayMs?: number; @@ -10,9 +10,17 @@ import { border, cn, color } from '@/styles/theme'; }; */ -export function AddressInput() { - const { recipientInput, setRecipientInput } = useSendContext(); +type AddressInputProps = { + addressInput: string | null; + setAddressInput: Dispatch>; + className?: string; +}; +export function AddressInput({ + addressInput, + setAddressInput, + className, +}: AddressInputProps) { return (
To @@ -28,9 +37,9 @@ export function AddressInput() { inputMode="text" placeholder="Basename, ENS, or Address" aria-label="Input Receiver Address" - value={recipientInput ?? ''} - onChange={setRecipientInput} - className="w-full outline-none" + value={addressInput ?? ''} + onChange={setAddressInput} + className={cn(background.default, 'w-full outline-none')} />
); From 971dc6a4e7eeeedaffabd8c751673509967a6fa0 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 22 Jan 2025 15:04:38 -0800 Subject: [PATCH 005/115] starting address selection --- src/send/components/Send.tsx | 53 ++++++++- src/send/components/SendProvider.tsx | 165 +++++++++++++++++++-------- src/send/validateAddressInput.ts | 17 +++ 3 files changed, 183 insertions(+), 52 deletions(-) create mode 100644 src/send/validateAddressInput.ts diff --git a/src/send/components/Send.tsx b/src/send/components/Send.tsx index 8605dd3f92..bfba1b17da 100644 --- a/src/send/components/Send.tsx +++ b/src/send/components/Send.tsx @@ -2,7 +2,7 @@ import { useTheme } from '@/core-react/internal/hooks/useTheme'; import { background, border, cn, color } from '@/styles/theme'; import type { ReactNode } from 'react'; import { SendHeader } from './SendHeader'; -import { SendProvider } from './SendProvider'; +import { SendProvider, useSendContext } from './SendProvider'; import { AddressInput } from '@/send/components/AddressInput'; type SendReact = { @@ -16,10 +16,7 @@ export function Send({ children, className }: SendReact) { if (!children) { return ( - - - - + ); } @@ -34,6 +31,50 @@ export function Send({ children, className }: SendReact) { } function SendContent({ children, className }: SendReact) { + const { + senderAddress, + senderChain, + tokenBalances, + ethBalance, + recipientInput, + setRecipientInput, + validatedRecipientAddress, + selectedRecipientAddress, + } = useSendContext(); + + console.log({ + senderAddress, + senderChain, + tokenBalances, + ethBalance, + recipientInput, + validatedRecipientAddress, + selectedRecipientAddress, + }); + + if (!children) { + return ( +
+ + +
+ ); + } + return (
>; - recipientAddress: Address | null; - setRecipientAddress: Dispatch>; + validatedRecipientAddress: Address | null; + setValidatedRecipientAddress: Dispatch>; + selectedRecipientAddress: Address | null; + setSelectedRecipientAddress: Dispatch>; }; type SendProviderReact = { children: ReactNode; - address?: Address; - chain?: Chain; - tokenBalances?: PortfolioTokenWithFiatValue[]; }; +type LifecycleStatus = + | { + statusName: 'init'; + statusData: { + isMissingRequiredField: true; + }; + } + | { + statusName: 'addressSelected'; + statusData: { + isMissingRequiredField: boolean; + }; + } + | { + statusName: 'amountChange'; + statusData: { + isMissingRequiredField: boolean; + }; + } + | { + statusName: 'transactionPending'; + statusData: { + isMissingRequiredField: false; + }; + } + | { + statusName: 'transactionApproved'; + statusData: { + isMissingRequiredField: boolean; + callsId?: Hex; + transactionHash?: Hex; + }; + } + | { + statusName: 'success'; + statusData: { + isMissingRequiredField: false; + transactionReceipt: TransactionReceipt; + }; + }; + const emptyContext = {} as SendContextType; const SendContext = createContext(emptyContext); @@ -41,50 +82,82 @@ export function useSendContext() { return useContext(SendContext); } -export function SendProvider({ - children, - address, - chain, - tokenBalances, -}: SendProviderReact) { - // const { address: senderAddress, chain: senderChain } = useWalletContext() ?? {}; - // const { tokenBalances: senderTokenBalances } = useWalletAdvancedContext() ?? {}; - const [recipientInput, setRecipientInput] = useState(null); - const [recipientAddress, setRecipientAddress] = useState
( - null, +export function SendProvider({ children }: SendProviderReact) { + const [senderAddress, setSenderAddress] = useState
( + undefined, ); + const [senderChain, setSenderChain] = useState(undefined); + const [ethBalance, setEthBalance] = useState(undefined); + const [tokenBalances, setTokenBalances] = useState< + PortfolioTokenWithFiatValue[] | undefined + >(undefined); + const [recipientInput, setRecipientInput] = useState(null); + const [validatedRecipientAddress, setValidatedRecipientAddress] = + useState
(null); + const [selectedRecipientAddress, setSelectedRecipientAddress] = + useState
(null); + const [lifecycleStatus, setLifecycleStatus] = useState({ + statusName: 'init', + statusData: { + isMissingRequiredField: true, + }, + }); - let ethBalance: number | undefined; - const { response } = useGetETHBalance(address); - if (response?.data?.value) { - ethBalance = Number(response.data.value) / 10 ** response.data.decimals; - } + const { address, chain } = useAccount(); + useEffect(() => { + if (address) { + setSenderAddress(address); + } + if (chain) { + setSenderChain(chain); + } + }, [address, chain]); - let senderAddress = address; - let senderChain = chain; - if (!senderAddress || !senderChain) { - const { address: userAddress, chain: userChain } = useAccount(); - senderAddress = userAddress; - senderChain = userChain; - } + const { response: ethBalanceResponse } = useGetETHBalance(senderAddress); + useEffect(() => { + if (ethBalanceResponse?.data?.value) { + setEthBalance( + Number(ethBalanceResponse.data.value) / + 10 ** ethBalanceResponse.data.decimals, + ); + } + }, [ethBalanceResponse]); - let senderTokenBalances = tokenBalances; - if (!senderTokenBalances) { - const { data } = usePortfolioTokenBalances({ - address: senderAddress, - }); - senderTokenBalances = data?.tokenBalances ?? []; - } + const { data } = usePortfolioTokenBalances({ + address: senderAddress, + }); + useEffect(() => { + if (data?.tokenBalances) { + setTokenBalances(data.tokenBalances); + } + }, [data]); + + useEffect(() => { + async function validateRecipientInput() { + if (recipientInput) { + const validatedInput = await validateAddressInput(recipientInput); + if (validatedInput) { + setValidatedRecipientAddress(validatedInput); + } else { + setValidatedRecipientAddress(null); + } + } + } + validateRecipientInput(); + }, [recipientInput]); const value = useValue({ - address: senderAddress, - chain: senderChain, - tokenBalances: senderTokenBalances, + lifecycleStatus, + senderAddress, + senderChain, + tokenBalances, ethBalance, recipientInput, setRecipientInput, - recipientAddress, - setRecipientAddress, + validatedRecipientAddress, + setValidatedRecipientAddress, + selectedRecipientAddress, + setSelectedRecipientAddress, }); return {children}; diff --git a/src/send/validateAddressInput.ts b/src/send/validateAddressInput.ts new file mode 100644 index 0000000000..be9bd1bd23 --- /dev/null +++ b/src/send/validateAddressInput.ts @@ -0,0 +1,17 @@ +import { isBasename } from '@/identity/utils/isBasename'; +import { isEns } from '@/identity/utils/isEns'; +import { getAddress } from '@/identity/utils/getAddress'; +import { isAddress } from 'viem'; +import { base } from 'viem/chains'; + +export async function validateAddressInput(input: string) { + if (isBasename(input) || isEns(input)) { + const address = await getAddress({ name: input, chain: base }); + if (address) { + return address; + } + } else if (isAddress(input, { strict: false })) { + return input; + } + return null; +} From 05eb4165782fd0818ef9531eb4ec5f23eaa9eaeb Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 22 Jan 2025 20:54:09 -0800 Subject: [PATCH 006/115] update getAddress to support basename reverse res --- src/identity/utils/getAddress.ts | 2 ++ src/send/validateAddressInput.ts | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/identity/utils/getAddress.ts b/src/identity/utils/getAddress.ts index b30a0ecdac..cebb314443 100644 --- a/src/identity/utils/getAddress.ts +++ b/src/identity/utils/getAddress.ts @@ -5,6 +5,8 @@ import { RESOLVER_ADDRESSES_BY_CHAIN_ID } from '@/identity/constants'; import type { GetAddress, GetAddressReturnType } from '@/identity/types'; import { isBasename } from '@/identity/utils/isBasename'; import { mainnet } from 'viem/chains'; +import { RESOLVER_ADDRESSES_BY_CHAIN_ID } from '@/identity/constants'; +import { isBasename } from '@/identity/utils/isBasename'; /** * Get address from ENS name or Basename. diff --git a/src/send/validateAddressInput.ts b/src/send/validateAddressInput.ts index be9bd1bd23..2e73c5fc87 100644 --- a/src/send/validateAddressInput.ts +++ b/src/send/validateAddressInput.ts @@ -2,11 +2,14 @@ import { isBasename } from '@/identity/utils/isBasename'; import { isEns } from '@/identity/utils/isEns'; import { getAddress } from '@/identity/utils/getAddress'; import { isAddress } from 'viem'; -import { base } from 'viem/chains'; +import { base, mainnet } from 'viem/chains'; export async function validateAddressInput(input: string) { if (isBasename(input) || isEns(input)) { - const address = await getAddress({ name: input, chain: base }); + const address = await getAddress({ + name: input, + chain: isBasename(input) ? base : mainnet, + }); if (address) { return address; } From a89983cb5107dd357f407234cc643581f70e3884 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 22 Jan 2025 21:16:21 -0800 Subject: [PATCH 007/115] utils + tests --- src/identity/utils/getName.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/identity/utils/getName.ts b/src/identity/utils/getName.ts index fed8f0ff12..b772f834e1 100644 --- a/src/identity/utils/getName.ts +++ b/src/identity/utils/getName.ts @@ -29,10 +29,9 @@ export const getName = async ({ const client = getChainPublicClient(chain); if (chainIsBase) { - const baseClient = getChainPublicClient(chain); const addressReverseNode = convertReverseNodeToBytes(address, base.id); try { - const basename = await baseClient.readContract({ + const basename = await client.readContract({ abi: L2ResolverAbi, address: RESOLVER_ADDRESSES_BY_CHAIN_ID[chain.id], functionName: 'name', From 033c4b8881d79883537095b5ec8916c31b0348f3 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 22 Jan 2025 22:47:24 -0800 Subject: [PATCH 008/115] early send flows --- src/send/components/AddressSelector.tsx | 43 +++++++++ src/send/components/Send.tsx | 45 ++++----- src/send/components/SendProvider.tsx | 116 ++++++++++++++++++++++-- src/send/components/TokenSelector.tsx | 31 +++++++ src/token/components/TokenRow.tsx | 9 +- 5 files changed, 211 insertions(+), 33 deletions(-) create mode 100644 src/send/components/AddressSelector.tsx create mode 100644 src/send/components/TokenSelector.tsx diff --git a/src/send/components/AddressSelector.tsx b/src/send/components/AddressSelector.tsx new file mode 100644 index 0000000000..305f8ffaa8 --- /dev/null +++ b/src/send/components/AddressSelector.tsx @@ -0,0 +1,43 @@ +import { Address, Avatar, Identity, Name } from '@/identity'; +import { useSendContext } from '@/send/components/SendProvider'; +import { background, border, cn, pressable } from '@/styles/theme'; + +export function AddressSelector() { + const { + senderChain, + validatedRecipientAddress, + handleAddressSelection, + lifecycleStatus, + } = useSendContext(); + + if ( + !validatedRecipientAddress || + lifecycleStatus.statusName !== 'selectingAddress' + ) { + return null; + } + + return ( + + ); +} diff --git a/src/send/components/Send.tsx b/src/send/components/Send.tsx index bfba1b17da..17f0328399 100644 --- a/src/send/components/Send.tsx +++ b/src/send/components/Send.tsx @@ -4,6 +4,9 @@ import type { ReactNode } from 'react'; import { SendHeader } from './SendHeader'; import { SendProvider, useSendContext } from './SendProvider'; import { AddressInput } from '@/send/components/AddressInput'; +import { AddressSelector } from '@/send/components/AddressSelector'; +import { TokenSelector } from '@/send/components/TokenSelector'; +import { ConnectWallet } from '@/wallet'; type SendReact = { children?: ReactNode; @@ -31,25 +34,10 @@ export function Send({ children, className }: SendReact) { } function SendContent({ children, className }: SendReact) { - const { - senderAddress, - senderChain, - tokenBalances, - ethBalance, - recipientInput, - setRecipientInput, - validatedRecipientAddress, - selectedRecipientAddress, - } = useSendContext(); + const context = useSendContext(); console.log({ - senderAddress, - senderChain, - tokenBalances, - ethBalance, - recipientInput, - validatedRecipientAddress, - selectedRecipientAddress, + context }); if (!children) { @@ -61,16 +49,29 @@ function SendContent({ children, className }: SendReact) { border.lineDefault, color.foreground, 'h-96 w-88', - 'flex flex-col', + 'flex flex-col items-center', 'p-4', className, )} > - + { + context.lifecycleStatus.statusName === 'connectingWallet' && ( +
+ +
+ ) + } + { + context.lifecycleStatus.statusName !== 'connectingWallet' && ( + + ) + } + +
); } diff --git a/src/send/components/SendProvider.tsx b/src/send/components/SendProvider.tsx index d54e4a04e3..7e7c6e1902 100644 --- a/src/send/components/SendProvider.tsx +++ b/src/send/components/SendProvider.tsx @@ -7,6 +7,7 @@ import { type Dispatch, type ReactNode, type SetStateAction, + useCallback, useContext, useEffect, useState, @@ -14,6 +15,8 @@ import { import type { Address, Chain, Hex, TransactionReceipt } from 'viem'; import { useAccount } from 'wagmi'; import { validateAddressInput } from '@/send/validateAddressInput'; +import { useLifecycleStatus } from '@/core-react/internal/hooks/useLifecycleStatus'; +import type { Token } from '@/token'; type SendContextType = { lifecycleStatus: LifecycleStatus; @@ -27,6 +30,10 @@ type SendContextType = { setValidatedRecipientAddress: Dispatch>; selectedRecipientAddress: Address | null; setSelectedRecipientAddress: Dispatch>; + handleAddressSelection: (address: Address) => void; + selectedToken: Token | null; + setSelectedToken: Dispatch>; + handleTokenSelection: (token: Token) => void; }; type SendProviderReact = { @@ -40,12 +47,36 @@ type LifecycleStatus = isMissingRequiredField: true; }; } + | { + statusName: 'connectingWallet'; + statusData: { + isMissingRequiredField: true; + }; + } + | { + statusName: 'selectingAddress'; + statusData: { + isMissingRequiredField: true; + }; + } | { statusName: 'addressSelected'; statusData: { isMissingRequiredField: boolean; }; } + | { + statusName: 'selectingToken'; + statusData: { + isMissingRequiredField: true; + }; + } + | { + statusName: 'tokenSelected'; + statusData: { + isMissingRequiredField: boolean; + }; + } | { statusName: 'amountChange'; statusData: { @@ -61,7 +92,7 @@ type LifecycleStatus = | { statusName: 'transactionApproved'; statusData: { - isMissingRequiredField: boolean; + isMissingRequiredField: false; callsId?: Hex; transactionHash?: Hex; }; @@ -88,20 +119,22 @@ export function SendProvider({ children }: SendProviderReact) { ); const [senderChain, setSenderChain] = useState(undefined); const [ethBalance, setEthBalance] = useState(undefined); - const [tokenBalances, setTokenBalances] = useState< - PortfolioTokenWithFiatValue[] | undefined - >(undefined); const [recipientInput, setRecipientInput] = useState(null); const [validatedRecipientAddress, setValidatedRecipientAddress] = useState
(null); const [selectedRecipientAddress, setSelectedRecipientAddress] = useState
(null); - const [lifecycleStatus, setLifecycleStatus] = useState({ - statusName: 'init', - statusData: { - isMissingRequiredField: true, - }, - }); + const [tokenBalances, setTokenBalances] = useState< + PortfolioTokenWithFiatValue[] | undefined + >(undefined); + const [selectedToken, setSelectedToken] = useState(null); + const [lifecycleStatus, updateLifecycleStatus] = + useLifecycleStatus({ + statusName: 'init', + statusData: { + isMissingRequiredField: true, + }, + }); const { address, chain } = useAccount(); useEffect(() => { @@ -146,6 +179,65 @@ export function SendProvider({ children }: SendProviderReact) { validateRecipientInput(); }, [recipientInput]); + useEffect(() => { + if (lifecycleStatus.statusName === 'init') { + if (senderAddress) { + updateLifecycleStatus({ + statusName: 'selectingAddress', + statusData: { + isMissingRequiredField: true, + }, + }); + } else { + updateLifecycleStatus({ + statusName: 'connectingWallet', + statusData: { + isMissingRequiredField: true, + }, + }); + } + } + }, [senderAddress, lifecycleStatus, updateLifecycleStatus]); + + useEffect(() => { + if (lifecycleStatus.statusName === 'connectingWallet') { + if (senderAddress) { + updateLifecycleStatus({ + statusName: 'selectingAddress', + statusData: { + isMissingRequiredField: true, + }, + }); + } + } + }, [senderAddress, lifecycleStatus, updateLifecycleStatus]); + + const handleAddressSelection = useCallback( + (address: Address) => { + setSelectedRecipientAddress(address); + updateLifecycleStatus({ + statusName: 'selectingToken', + statusData: { + isMissingRequiredField: true, + }, + }); + }, + [updateLifecycleStatus], + ); + + const handleTokenSelection = useCallback( + (token: Token) => { + setSelectedToken(token); + updateLifecycleStatus({ + statusName: 'tokenSelected', + statusData: { + isMissingRequiredField: true, + }, + }); + }, + [updateLifecycleStatus], + ); + const value = useValue({ lifecycleStatus, senderAddress, @@ -158,6 +250,10 @@ export function SendProvider({ children }: SendProviderReact) { setValidatedRecipientAddress, selectedRecipientAddress, setSelectedRecipientAddress, + handleAddressSelection, + selectedToken, + setSelectedToken, + handleTokenSelection, }); return {children}; diff --git a/src/send/components/TokenSelector.tsx b/src/send/components/TokenSelector.tsx new file mode 100644 index 0000000000..eea85af09b --- /dev/null +++ b/src/send/components/TokenSelector.tsx @@ -0,0 +1,31 @@ +import { useSendContext } from '@/send/components/SendProvider'; +import { TokenRow } from '@/token'; + +export function TokenSelector() { + const { tokenBalances, handleTokenSelection, lifecycleStatus } = + useSendContext(); + + if (lifecycleStatus.statusName !== 'selectingToken') { + return null; + } + + return ( +
+ {tokenBalances?.map((token) => ( + + ))} +
+ ); +} diff --git a/src/token/components/TokenRow.tsx b/src/token/components/TokenRow.tsx index 7360bedb98..f56077da4e 100644 --- a/src/token/components/TokenRow.tsx +++ b/src/token/components/TokenRow.tsx @@ -32,7 +32,14 @@ export const TokenRow = memo(function TokenRow({ {!hideImage && } - {token.name} + + {token.name.trim()} + {!hideSymbol && ( {token.symbol} From 6ede2248f72c4bd887f39c22ca74d99a08406071 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 23 Jan 2025 11:47:51 -0800 Subject: [PATCH 009/115] move send into wallet --- .../nextjs-app-router/components/Demo.tsx | 2 - .../components/demo/Send.tsx | 5 - .../components/form/active-component.tsx | 3 - .../nextjs-app-router/types/onchainkit.ts | 1 - src/send/index.ts | 1 - .../components/AddressInput.tsx | 0 .../components/AddressSelector.tsx | 2 +- .../WalletAdvancedSend}/components/Send.tsx | 32 ++--- .../components/SendHeader.tsx | 0 .../components/SendProvider.tsx | 128 ++++++++---------- .../components/TokenSelector.tsx | 2 +- .../validateAddressInput.ts | 0 12 files changed, 69 insertions(+), 107 deletions(-) delete mode 100644 playground/nextjs-app-router/components/demo/Send.tsx delete mode 100644 src/send/index.ts rename src/{send => wallet/components/WalletAdvancedSend}/components/AddressInput.tsx (100%) rename src/{send => wallet/components/WalletAdvancedSend}/components/AddressSelector.tsx (90%) rename src/{send => wallet/components/WalletAdvancedSend}/components/Send.tsx (69%) rename src/{send => wallet/components/WalletAdvancedSend}/components/SendHeader.tsx (100%) rename src/{send => wallet/components/WalletAdvancedSend}/components/SendProvider.tsx (71%) rename src/{send => wallet/components/WalletAdvancedSend}/components/TokenSelector.tsx (88%) rename src/{send => wallet/components/WalletAdvancedSend}/validateAddressInput.ts (100%) diff --git a/playground/nextjs-app-router/components/Demo.tsx b/playground/nextjs-app-router/components/Demo.tsx index b4de5e88da..498d195e55 100644 --- a/playground/nextjs-app-router/components/Demo.tsx +++ b/playground/nextjs-app-router/components/Demo.tsx @@ -23,7 +23,6 @@ import WalletDemo from './demo/Wallet'; import WalletAdvancedDefaultDemo from './demo/WalletAdvancedDefault'; import WalletDefaultDemo from './demo/WalletDefault'; import WalletIslandDemo from './demo/WalletIsland'; -import SendDemo from '@/components/demo/Send'; const activeComponentMapping: Record = { [OnchainKitComponent.FundButton]: FundButtonDemo, @@ -44,7 +43,6 @@ const activeComponentMapping: Record = { [OnchainKitComponent.NFTMintCardDefault]: NFTMintCardDefaultDemo, [OnchainKitComponent.NFTCardDefault]: NFTCardDefaultDemo, [OnchainKitComponent.IdentityCard]: IdentityCardDemo, - [OnchainKitComponent.Send]: SendDemo, }; export default function Demo() { diff --git a/playground/nextjs-app-router/components/demo/Send.tsx b/playground/nextjs-app-router/components/demo/Send.tsx deleted file mode 100644 index 797358ebb6..0000000000 --- a/playground/nextjs-app-router/components/demo/Send.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Send } from '@coinbase/onchainkit/send'; - -export default function SendDemo() { - return ; -} diff --git a/playground/nextjs-app-router/components/form/active-component.tsx b/playground/nextjs-app-router/components/form/active-component.tsx index fd7f15577e..292f831ab5 100644 --- a/playground/nextjs-app-router/components/form/active-component.tsx +++ b/playground/nextjs-app-router/components/form/active-component.tsx @@ -68,9 +68,6 @@ export function ActiveComponent() { NFT Mint Card Default - - Send - diff --git a/playground/nextjs-app-router/types/onchainkit.ts b/playground/nextjs-app-router/types/onchainkit.ts index cb98b6b74c..00d1c8a5b3 100644 --- a/playground/nextjs-app-router/types/onchainkit.ts +++ b/playground/nextjs-app-router/types/onchainkit.ts @@ -17,7 +17,6 @@ export enum OnchainKitComponent { NFTCardDefault = 'nft-card-default', NFTMintCard = 'nft-mint-card', NFTMintCardDefault = 'nft-mint-card-default', - Send = 'send', } export enum TransactionTypes { diff --git a/src/send/index.ts b/src/send/index.ts deleted file mode 100644 index d11e0b3251..0000000000 --- a/src/send/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Send } from './components/Send'; diff --git a/src/send/components/AddressInput.tsx b/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx similarity index 100% rename from src/send/components/AddressInput.tsx rename to src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx diff --git a/src/send/components/AddressSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx similarity index 90% rename from src/send/components/AddressSelector.tsx rename to src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx index 305f8ffaa8..50a37142cd 100644 --- a/src/send/components/AddressSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx @@ -1,5 +1,5 @@ import { Address, Avatar, Identity, Name } from '@/identity'; -import { useSendContext } from '@/send/components/SendProvider'; +import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; import { background, border, cn, pressable } from '@/styles/theme'; export function AddressSelector() { diff --git a/src/send/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx similarity index 69% rename from src/send/components/Send.tsx rename to src/wallet/components/WalletAdvancedSend/components/Send.tsx index 17f0328399..70f48f5d50 100644 --- a/src/send/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -3,9 +3,9 @@ import { background, border, cn, color } from '@/styles/theme'; import type { ReactNode } from 'react'; import { SendHeader } from './SendHeader'; import { SendProvider, useSendContext } from './SendProvider'; -import { AddressInput } from '@/send/components/AddressInput'; -import { AddressSelector } from '@/send/components/AddressSelector'; -import { TokenSelector } from '@/send/components/TokenSelector'; +import { AddressInput } from '@/wallet/components/WalletAdvancedSend/components/AddressInput'; +import { AddressSelector } from '@/wallet/components/WalletAdvancedSend/components/AddressSelector'; +import { TokenSelector } from '@/wallet/components/WalletAdvancedSend/components/TokenSelector'; import { ConnectWallet } from '@/wallet'; type SendReact = { @@ -37,7 +37,7 @@ function SendContent({ children, className }: SendReact) { const context = useSendContext(); console.log({ - context + context, }); if (!children) { @@ -55,21 +55,17 @@ function SendContent({ children, className }: SendReact) { )} > - { - context.lifecycleStatus.statusName === 'connectingWallet' && ( -
+ {context.lifecycleStatus.statusName === 'connectingWallet' && ( +
-
- ) - } - { - context.lifecycleStatus.statusName !== 'connectingWallet' && ( - - ) - } +
+ )} + {context.lifecycleStatus.statusName !== 'connectingWallet' && ( + + )} diff --git a/src/send/components/SendHeader.tsx b/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx similarity index 100% rename from src/send/components/SendHeader.tsx rename to src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx diff --git a/src/send/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx similarity index 71% rename from src/send/components/SendProvider.tsx rename to src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index 7e7c6e1902..caf7a6c44d 100644 --- a/src/send/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -1,7 +1,5 @@ import { useValue } from '@/core-react/internal/hooks/useValue'; -import { usePortfolioTokenBalances } from '@/core-react/wallet/hooks/usePortfolioTokenBalances'; import type { PortfolioTokenWithFiatValue } from '@/api/types'; -import { useGetETHBalance } from '@/wallet/hooks/useGetETHBalance'; import { createContext, type Dispatch, @@ -13,15 +11,16 @@ import { useState, } from 'react'; import type { Address, Chain, Hex, TransactionReceipt } from 'viem'; -import { useAccount } from 'wagmi'; -import { validateAddressInput } from '@/send/validateAddressInput'; +import { validateAddressInput } from '@/wallet/components/WalletAdvancedSend/validateAddressInput'; import { useLifecycleStatus } from '@/core-react/internal/hooks/useLifecycleStatus'; import type { Token } from '@/token'; +import { useWalletContext } from '@/wallet/components/WalletProvider'; +import { useWalletAdvancedContext } from '@/wallet/components/WalletAdvancedProvider'; type SendContextType = { lifecycleStatus: LifecycleStatus; - senderAddress: Address | undefined; - senderChain: Chain | undefined; + senderAddress: Address | null | undefined; + senderChain: Chain | null | undefined; ethBalance: number | undefined; tokenBalances: PortfolioTokenWithFiatValue[] | undefined; recipientInput: string | null; @@ -47,8 +46,14 @@ type LifecycleStatus = isMissingRequiredField: true; }; } + // | { + // statusName: 'connectingWallet'; + // statusData: { + // isMissingRequiredField: true; + // }; + // } | { - statusName: 'connectingWallet'; + statusName: 'fundingWallet'; statusData: { isMissingRequiredField: true; }; @@ -59,12 +64,12 @@ type LifecycleStatus = isMissingRequiredField: true; }; } - | { - statusName: 'addressSelected'; - statusData: { - isMissingRequiredField: boolean; - }; - } + // | { + // statusName: 'addressSelected'; + // statusData: { + // isMissingRequiredField: boolean; + // }; + // } | { statusName: 'selectingToken'; statusData: { @@ -81,6 +86,7 @@ type LifecycleStatus = statusName: 'amountChange'; statusData: { isMissingRequiredField: boolean; + sufficientBalance: boolean; }; } | { @@ -114,19 +120,12 @@ export function useSendContext() { } export function SendProvider({ children }: SendProviderReact) { - const [senderAddress, setSenderAddress] = useState
( - undefined, - ); - const [senderChain, setSenderChain] = useState(undefined); const [ethBalance, setEthBalance] = useState(undefined); const [recipientInput, setRecipientInput] = useState(null); const [validatedRecipientAddress, setValidatedRecipientAddress] = useState
(null); const [selectedRecipientAddress, setSelectedRecipientAddress] = useState
(null); - const [tokenBalances, setTokenBalances] = useState< - PortfolioTokenWithFiatValue[] | undefined - >(undefined); const [selectedToken, setSelectedToken] = useState(null); const [lifecycleStatus, updateLifecycleStatus] = useLifecycleStatus({ @@ -136,35 +135,47 @@ export function SendProvider({ children }: SendProviderReact) { }, }); - const { address, chain } = useAccount(); - useEffect(() => { - if (address) { - setSenderAddress(address); - } - if (chain) { - setSenderChain(chain); - } - }, [address, chain]); + const { address: senderAddress, chain: senderChain } = useWalletContext(); + const { tokenBalances } = useWalletAdvancedContext(); + - const { response: ethBalanceResponse } = useGetETHBalance(senderAddress); useEffect(() => { - if (ethBalanceResponse?.data?.value) { - setEthBalance( - Number(ethBalanceResponse.data.value) / - 10 ** ethBalanceResponse.data.decimals, - ); + if (lifecycleStatus.statusName === 'init') { + if (senderAddress) { + updateLifecycleStatus({ + statusName: 'selectingAddress', + statusData: { + isMissingRequiredField: true, + }, + }); + } } - }, [ethBalanceResponse]); + }, [senderAddress, lifecycleStatus, updateLifecycleStatus]); - const { data } = usePortfolioTokenBalances({ - address: senderAddress, - }); + // Set Lifecycle Status after fetching token balances useEffect(() => { - if (data?.tokenBalances) { - setTokenBalances(data.tokenBalances); + const ethBalance = tokenBalances?.filter( + (token) => token.symbol === 'ETH', + )[0]; + if (!ethBalance || ethBalance.cryptoBalance === 0) { + updateLifecycleStatus({ + statusName: 'fundingWallet', + statusData: { + isMissingRequiredField: true, + }, + }); + } else if (ethBalance.cryptoBalance > 0) { + setEthBalance(ethBalance.cryptoBalance); + updateLifecycleStatus({ + statusName: 'selectingAddress', + statusData: { + isMissingRequiredField: true, + }, + }); } - }, [data]); + }, [tokenBalances, updateLifecycleStatus]); + // Validate Recipient Input useEffect(() => { async function validateRecipientInput() { if (recipientInput) { @@ -179,39 +190,6 @@ export function SendProvider({ children }: SendProviderReact) { validateRecipientInput(); }, [recipientInput]); - useEffect(() => { - if (lifecycleStatus.statusName === 'init') { - if (senderAddress) { - updateLifecycleStatus({ - statusName: 'selectingAddress', - statusData: { - isMissingRequiredField: true, - }, - }); - } else { - updateLifecycleStatus({ - statusName: 'connectingWallet', - statusData: { - isMissingRequiredField: true, - }, - }); - } - } - }, [senderAddress, lifecycleStatus, updateLifecycleStatus]); - - useEffect(() => { - if (lifecycleStatus.statusName === 'connectingWallet') { - if (senderAddress) { - updateLifecycleStatus({ - statusName: 'selectingAddress', - statusData: { - isMissingRequiredField: true, - }, - }); - } - } - }, [senderAddress, lifecycleStatus, updateLifecycleStatus]); - const handleAddressSelection = useCallback( (address: Address) => { setSelectedRecipientAddress(address); diff --git a/src/send/components/TokenSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx similarity index 88% rename from src/send/components/TokenSelector.tsx rename to src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx index eea85af09b..63eee2ea8a 100644 --- a/src/send/components/TokenSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx @@ -1,4 +1,4 @@ -import { useSendContext } from '@/send/components/SendProvider'; +import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; import { TokenRow } from '@/token'; export function TokenSelector() { diff --git a/src/send/validateAddressInput.ts b/src/wallet/components/WalletAdvancedSend/validateAddressInput.ts similarity index 100% rename from src/send/validateAddressInput.ts rename to src/wallet/components/WalletAdvancedSend/validateAddressInput.ts From b9bba5566c0be2c9bff7f2c211b7d5fc41fcae7b Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 23 Jan 2025 12:54:59 -0800 Subject: [PATCH 010/115] send in walletisland --- .../components/WalletAdvancedContent.tsx | 136 +++++++++++------- .../components/WalletAdvancedProvider.tsx | 6 + .../components/AddressSelector.tsx | 2 +- .../WalletAdvancedSend/components/Send.tsx | 8 +- .../components/SendProvider.tsx | 12 -- .../WalletAdvancedTransactionActions.tsx | 6 +- src/wallet/types.ts | 4 + 7 files changed, 97 insertions(+), 77 deletions(-) diff --git a/src/wallet/components/WalletAdvancedContent.tsx b/src/wallet/components/WalletAdvancedContent.tsx index 9ac314c824..2593666a31 100644 --- a/src/wallet/components/WalletAdvancedContent.tsx +++ b/src/wallet/components/WalletAdvancedContent.tsx @@ -1,6 +1,8 @@ import { background, border, cn, text } from '@/styles/theme'; +import { useCallback, useMemo } from 'react'; import { WALLET_ADVANCED_DEFAULT_SWAPPABLE_TOKENS } from '../constants'; import type { WalletAdvancedReact } from '../types'; +import { Send } from './WalletAdvancedSend/components/Send'; import { useWalletAdvancedContext } from './WalletAdvancedProvider'; import { WalletAdvancedQrReceive } from './WalletAdvancedQrReceive'; import { WalletAdvancedSwap } from './WalletAdvancedSwap'; @@ -11,13 +13,69 @@ export function WalletAdvancedContent({ swappableTokens, }: WalletAdvancedReact) { const { - isSubComponentClosing, setIsSubComponentOpen, + isSubComponentClosing, setIsSubComponentClosing, } = useWalletContext(); - const { showQr, showSwap, tokenBalances, animations } = + + const { showQr, showSwap, showSend, tokenBalances, animations } = useWalletAdvancedContext(); + const handleAnimationEnd = useCallback(() => { + if (isSubComponentClosing) { + setIsSubComponentOpen(false); + setIsSubComponentClosing(false); + } + }, [isSubComponentClosing, setIsSubComponentOpen, setIsSubComponentClosing]); + + const content = useMemo(() => { + if (showSend) { + return ( + + + + ); + } + + if (showQr) { + return ( + + + + ); + } + + if (showSwap) { + return ( + + + Swap + + } + to={swappableTokens ?? WALLET_ADVANCED_DEFAULT_SWAPPABLE_TOKENS} + from={ + tokenBalances?.map((token) => ({ + address: token.address, + chainId: token.chainId, + symbol: token.symbol, + decimals: token.decimals, + image: token.image, + name: token.name, + })) ?? [] + } + className="w-full px-4 pt-3 pb-4" + /> + + ); + } + + return {children}; + }, [showQr, showSwap, showSend, swappableTokens, tokenBalances, children]); + return (
{ - if (isSubComponentClosing) { - setIsSubComponentOpen(false); - setIsSubComponentClosing(false); - } - }} + onAnimationEnd={handleAnimationEnd} + > + {content} +
+ ); +} + +function ContentWrapper({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
-
- -
-
- - Swap -
- } - to={swappableTokens ?? WALLET_ADVANCED_DEFAULT_SWAPPABLE_TOKENS} - from={ - tokenBalances?.map((token) => ({ - address: token.address, - chainId: token.chainId, - symbol: token.symbol, - decimals: token.decimals, - image: token.image, - name: token.name, - })) ?? [] - } - className="w-full px-4 pt-3 pb-4" - /> -
-
- {children} -
+ {children} ); } diff --git a/src/wallet/components/WalletAdvancedProvider.tsx b/src/wallet/components/WalletAdvancedProvider.tsx index bd88debc12..f1be98d70b 100644 --- a/src/wallet/components/WalletAdvancedProvider.tsx +++ b/src/wallet/components/WalletAdvancedProvider.tsx @@ -32,6 +32,8 @@ export function WalletAdvancedProvider({ const [isSwapClosing, setIsSwapClosing] = useState(false); const [showQr, setShowQr] = useState(false); const [isQrClosing, setIsQrClosing] = useState(false); + const [showSend, setShowSend] = useState(false); + const [isSendClosing, setIsSendClosing] = useState(false); const { data: portfolioData, refetch: refetchPortfolioData, @@ -56,6 +58,10 @@ export function WalletAdvancedProvider({ setShowQr, isQrClosing, setIsQrClosing, + showSend, + setShowSend, + isSendClosing, + setIsSendClosing, tokenBalances, portfolioFiatValue, isFetchingPortfolioData, diff --git a/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx index 50a37142cd..9a1d98d65c 100644 --- a/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx @@ -25,7 +25,7 @@ export function AddressSelector() { > - {context.lifecycleStatus.statusName === 'connectingWallet' && ( -
- -
- )} - {context.lifecycleStatus.statusName !== 'connectingWallet' && ( + {context.lifecycleStatus.statusName !== 'init' && ( { @@ -48,8 +48,8 @@ export function WalletAdvancedTransactionActions() { }, [address, chain?.name, projectId]); const handleSend = useCallback(() => { - window.open('https://wallet.coinbase.com', '_blank'); - }, []); + setShowSend(true); + }, [setShowSend]); const handleSwap = useCallback(() => { setShowSwap(true); diff --git a/src/wallet/types.ts b/src/wallet/types.ts index d141d82314..3c974bc5ab 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -216,6 +216,10 @@ export type WalletAdvancedContextType = { setShowQr: Dispatch>; isQrClosing: boolean; setIsQrClosing: Dispatch>; + showSend: boolean; + setShowSend: Dispatch>; + isSendClosing: boolean; + setIsSendClosing: Dispatch>; tokenBalances: PortfolioTokenWithFiatValue[] | undefined; portfolioFiatValue: number | undefined; isFetchingPortfolioData: boolean; From c50f9a87124c49ef152e0d7706e74f54fd705e8b Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 23 Jan 2025 13:34:22 -0800 Subject: [PATCH 011/115] state drives active screen --- .../components/AddressSelector.tsx | 13 +-- .../WalletAdvancedSend/components/Send.tsx | 89 +++++++++++-------- .../components/SendAmountInput.tsx | 3 + .../components/TokenSelector.tsx | 43 +++++---- 4 files changed, 81 insertions(+), 67 deletions(-) create mode 100644 src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx diff --git a/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx index 9a1d98d65c..3dbf979e7e 100644 --- a/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx @@ -3,17 +3,10 @@ import { useSendContext } from '@/wallet/components/WalletAdvancedSend/component import { background, border, cn, pressable } from '@/styles/theme'; export function AddressSelector() { - const { - senderChain, - validatedRecipientAddress, - handleAddressSelection, - lifecycleStatus, - } = useSendContext(); + const { senderChain, validatedRecipientAddress, handleAddressSelection } = + useSendContext(); - if ( - !validatedRecipientAddress || - lifecycleStatus.statusName !== 'selectingAddress' - ) { + if (!validatedRecipientAddress) { return null; } diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index cbf6faac69..1faa58bf77 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -1,11 +1,12 @@ import { useTheme } from '@/core-react/internal/hooks/useTheme'; import { background, border, cn, color } from '@/styles/theme'; -import type { ReactNode } from 'react'; +import { useMemo, type ReactNode } from 'react'; import { SendHeader } from './SendHeader'; import { SendProvider, useSendContext } from './SendProvider'; import { AddressInput } from '@/wallet/components/WalletAdvancedSend/components/AddressInput'; import { AddressSelector } from '@/wallet/components/WalletAdvancedSend/components/AddressSelector'; import { TokenSelector } from '@/wallet/components/WalletAdvancedSend/components/TokenSelector'; +import { SendAmountInput } from '@/wallet/components/WalletAdvancedSend/components/SendAmountInput'; type SendReact = { children?: ReactNode; @@ -32,40 +33,10 @@ export function Send({ children, className }: SendReact) { ); } -function SendContent({ children, className }: SendReact) { - const context = useSendContext(); - - console.log({ - context, - }); - - if (!children) { - return ( -
- - {context.lifecycleStatus.statusName !== 'init' && ( - - )} - - -
- ); - } - +function SendContent({ + children = , + className, +}: SendReact) { return (
); } + +function SendDefaultChildren() { + const context = useSendContext(); + + console.log({ + context, + }); + + const activeStep = useMemo(() => { + if (!context.selectedRecipientAddress) { + return ( + <> + + + + ); + } + + if (!context.selectedToken) { + return ( + <> + + + + ); + } + + return ; + }, [ + context.selectedRecipientAddress, + context.selectedToken, + context.recipientInput, + context.setRecipientInput, + ]); + + return ( + <> + + {activeStep} + + ); +} diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx new file mode 100644 index 0000000000..abb418d7f5 --- /dev/null +++ b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx @@ -0,0 +1,3 @@ +export function SendAmountInput() { + return
SendAmountInput
; +} diff --git a/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx index 63eee2ea8a..fc199079c9 100644 --- a/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx @@ -1,31 +1,30 @@ import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; import { TokenRow } from '@/token'; +import { cn, text } from '@/styles/theme'; export function TokenSelector() { - const { tokenBalances, handleTokenSelection, lifecycleStatus } = - useSendContext(); - - if (lifecycleStatus.statusName !== 'selectingToken') { - return null; - } + const { tokenBalances, handleTokenSelection } = useSendContext(); return ( -
- {tokenBalances?.map((token) => ( - - ))} +
+ Select a token +
+ {tokenBalances?.map((token) => ( + + ))} +
); } From ebe1a68005f5f6332544996313fdbe1169e01a28 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 23 Jan 2025 14:46:54 -0800 Subject: [PATCH 012/115] use amount input in send --- .../internal/hooks/useAmountInput.tsx | 70 ++++++++ src/core/utils/truncateDecimalPlaces.ts | 20 +++ .../components/AmountInput/AmountInput.tsx | 139 ++++++++++++++++ .../AmountInput/AmountInputTypeSwitch.tsx | 85 ++++++++++ .../components/AmountInput/CurrencyLabel.tsx | 26 +++ .../WalletAdvancedSend/components/Send.tsx | 6 + .../components/SendAmountInput.tsx | 46 +++++- .../components/SendFundingWallet.tsx | 3 + .../components/SendProvider.tsx | 23 +++ src/wallet/hooks/useInputResize.test.ts | 149 ++++++++++++++++++ src/wallet/hooks/useInputResize.ts | 41 +++++ 11 files changed, 606 insertions(+), 2 deletions(-) create mode 100644 src/core-react/internal/hooks/useAmountInput.tsx create mode 100644 src/core/utils/truncateDecimalPlaces.ts create mode 100644 src/internal/components/AmountInput/AmountInput.tsx create mode 100644 src/internal/components/AmountInput/AmountInputTypeSwitch.tsx create mode 100644 src/internal/components/AmountInput/CurrencyLabel.tsx create mode 100644 src/wallet/components/WalletAdvancedSend/components/SendFundingWallet.tsx create mode 100644 src/wallet/hooks/useInputResize.test.ts create mode 100644 src/wallet/hooks/useInputResize.ts diff --git a/src/core-react/internal/hooks/useAmountInput.tsx b/src/core-react/internal/hooks/useAmountInput.tsx new file mode 100644 index 0000000000..e46f35805e --- /dev/null +++ b/src/core-react/internal/hooks/useAmountInput.tsx @@ -0,0 +1,70 @@ +import { useCallback, useMemo } from 'react'; +import { truncateDecimalPlaces } from '@/core/utils/truncateDecimalPlaces'; + +type UseAmountInputParams = { + setFiatAmount: (value: string) => void; + setCryptoAmount: (value: string) => void; + selectedInputType: 'fiat' | 'crypto'; + exchangeRate: string; +}; + +export const useAmountInput = ({ + setFiatAmount, + setCryptoAmount, + selectedInputType, + exchangeRate, +}: UseAmountInputParams) => { + + const handleFiatChange = useCallback( + (value: string) => { + const fiatValue = truncateDecimalPlaces(value, 2); + setFiatAmount(fiatValue); + + const calculatedCryptoValue = String( + Number(fiatValue) * Number(exchangeRate), + ); + const resultCryptoValue = truncateDecimalPlaces(calculatedCryptoValue, 8); + setCryptoAmount( + calculatedCryptoValue === '0' ? '' : resultCryptoValue, + ); + }, + [exchangeRate, setFiatAmount, setCryptoAmount], + ); + + const handleCryptoChange = useCallback( + (value: string) => { + const truncatedValue = truncateDecimalPlaces(value, 8); + setCryptoAmount(truncatedValue); + + const calculatedFiatValue = String( + Number(truncatedValue) / Number(exchangeRate), + ); + + const resultFiatValue = truncateDecimalPlaces(calculatedFiatValue, 2); + setFiatAmount(resultFiatValue === '0' ? '' : resultFiatValue); + }, + [exchangeRate, setFiatAmount, setCryptoAmount], + ); + + const handleChange = useCallback( + (value: string, onChange?: (value: string) => void) => { + if (selectedInputType === 'fiat') { + handleFiatChange(value); + } else { + handleCryptoChange(value); + } + + onChange?.(value); + }, + [handleFiatChange, handleCryptoChange, selectedInputType], + ); + + return useMemo( + () => ({ + handleChange, + handleFiatChange, + handleCryptoChange, + }), + [handleChange, handleFiatChange, handleCryptoChange], + ); +}; diff --git a/src/core/utils/truncateDecimalPlaces.ts b/src/core/utils/truncateDecimalPlaces.ts new file mode 100644 index 0000000000..8bb03ab678 --- /dev/null +++ b/src/core/utils/truncateDecimalPlaces.ts @@ -0,0 +1,20 @@ +/** + * Limit the value to N decimal places + */ +export const truncateDecimalPlaces = ( + value: string | number, + decimalPlaces: number, +) => { + const stringValue = String(value); + const decimalIndex = stringValue.indexOf('.'); + let resultValue = stringValue; + + if ( + decimalIndex !== -1 && + stringValue.length - decimalIndex - 1 > decimalPlaces + ) { + resultValue = stringValue.substring(0, decimalIndex + decimalPlaces + 1); + } + + return resultValue; +}; diff --git a/src/internal/components/AmountInput/AmountInput.tsx b/src/internal/components/AmountInput/AmountInput.tsx new file mode 100644 index 0000000000..501661b154 --- /dev/null +++ b/src/internal/components/AmountInput/AmountInput.tsx @@ -0,0 +1,139 @@ +import { isValidAmount } from '@/core/utils/isValidAmount'; +import { TextInput } from '@/internal/components/TextInput'; +import { useCallback, useEffect, useRef } from 'react'; +import { cn, text } from '@/styles/theme'; +import { useAmountInput } from '@/core-react/internal/hooks/useAmountInput'; +import { useInputResize } from '@/core-react/wallet/hooks/useInputResize'; +import { CurrencyLabel } from '@/internal/components/AmountInput/CurrencyLabel'; + +type AmountInputProps = { + asset: string; + currency: string; + fiatAmount: string; + cryptoAmount: string; + selectedInputType: 'fiat' | 'crypto'; + setFiatAmount: (value: string) => void; + setCryptoAmount: (value: string) => void; + exchangeRate: string; + className?: string; +}; + +export function AmountInput({ + fiatAmount, + cryptoAmount, + asset, + selectedInputType, + currency, + className, + setFiatAmount, + setCryptoAmount, + exchangeRate, +}: AmountInputProps) { + const currencyOrAsset = selectedInputType === 'fiat' ? currency : asset; + + const containerRef = useRef(null); + const inputRef = useRef(null); + const hiddenSpanRef = useRef(null); + const currencySpanRef = useRef(null); + + const value = selectedInputType === 'fiat' ? fiatAmount : cryptoAmount; + + const updateInputWidth = useInputResize( + containerRef, + inputRef, + hiddenSpanRef, + currencySpanRef, + ); + + const { handleChange } = useAmountInput({ + setFiatAmount, + setCryptoAmount, + selectedInputType, + exchangeRate, + }); + + const handleAmountChange = useCallback( + (value: string) => { + handleChange(value, () => { + if (inputRef.current) { + inputRef.current.focus(); + } + }); + }, + [handleChange], + ); + + // biome-ignore lint/correctness/useExhaustiveDependencies: When value changes, we want to update the input width + useEffect(() => { + updateInputWidth(); + }, [value, updateInputWidth]); + + const selectedInputTypeRef = useRef(selectedInputType); + + useEffect(() => { + /** + * We need to focus the input when the input type changes + * but not on the initial render. + */ + if (selectedInputTypeRef.current !== selectedInputType) { + selectedInputTypeRef.current = selectedInputType; + handleFocusInput(); + } + }, [selectedInputType]); + + const handleFocusInput = () => { + if (inputRef.current) { + inputRef.current.focus(); + } + }; + + return ( +
+
+ + + +
+ + {/* Hidden span for measuring text width + Without this span the input field would not adjust its width based on the text width and would look like this: + [0.12--------Empty Space-------][ETH] - As you can see the currency symbol is far away from the inputed value + + With this span we can measure the width of the text in the input field and set the width of the input field to match the text width + [0.12][ETH] - Now the currency symbol is displayed next to the input field + */} + + {value ? `${value}.` : '0.'} + +
+ ); +} diff --git a/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx b/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx new file mode 100644 index 0000000000..14420a63ba --- /dev/null +++ b/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx @@ -0,0 +1,85 @@ +import { formatFiatAmount } from '@/core/utils/formatFiatAmount'; +import { useCallback, useMemo } from 'react'; +import { useIcon } from '@/core-react/internal/hooks/useIcon'; +import { Skeleton } from '@/internal/components/Skeleton'; +import { cn, pressable, text } from '@/styles/theme'; +import { truncateDecimalPlaces } from '@/core/utils/truncateDecimalPlaces'; + +type AmountInputTypeSwitchPropsReact = { + selectedInputType: 'fiat' | 'crypto'; + setSelectedInputType: (type: 'fiat' | 'crypto') => void; + asset: string; + fundAmountFiat: string; + fundAmountCrypto: string; + exchangeRate: number; + exchangeRateLoading: boolean; + currency: string; + className?: string; +}; + +export function AmountInputTypeSwitch({ + selectedInputType, + setSelectedInputType, + asset, + fundAmountFiat, + fundAmountCrypto, + exchangeRate, + exchangeRateLoading, + currency, + className, +}: AmountInputTypeSwitchPropsReact) { + const iconSvg = useIcon({ icon: 'toggle' }); + + const handleToggle = useCallback(() => { + setSelectedInputType(selectedInputType === 'fiat' ? 'crypto' : 'fiat'); + }, [selectedInputType, setSelectedInputType]); + + const formatCrypto = useCallback( + (amount: string) => { + return `${truncateDecimalPlaces(amount || '0', 8)} ${asset}`; + }, + [asset], + ); + + const amountLine = useMemo(() => { + return ( + + {selectedInputType === 'fiat' + ? formatCrypto(fundAmountCrypto) + : formatFiatAmount({ + amount: fundAmountFiat, + currency: currency, + minimumFractionDigits: 0, + })} + + ); + }, [ + fundAmountCrypto, + fundAmountFiat, + selectedInputType, + formatCrypto, + currency, + ]); + + if (exchangeRateLoading || !exchangeRate) { + return ; + } + + return ( +
+ +
{amountLine}
+
+ ); +} diff --git a/src/internal/components/AmountInput/CurrencyLabel.tsx b/src/internal/components/AmountInput/CurrencyLabel.tsx new file mode 100644 index 0000000000..37ef0ac0d9 --- /dev/null +++ b/src/internal/components/AmountInput/CurrencyLabel.tsx @@ -0,0 +1,26 @@ +import { forwardRef } from 'react'; +import { cn, color, text } from '@/styles/theme'; + +type CurrencyLabelProps = { + label: string; +}; + +export const CurrencyLabel = forwardRef< + HTMLSpanElement, + CurrencyLabelProps +>(({ label }, ref) => { + return ( + + {label} + + ); +}); diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index 1faa58bf77..5f334af654 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -7,6 +7,7 @@ import { AddressInput } from '@/wallet/components/WalletAdvancedSend/components/ import { AddressSelector } from '@/wallet/components/WalletAdvancedSend/components/AddressSelector'; import { TokenSelector } from '@/wallet/components/WalletAdvancedSend/components/TokenSelector'; import { SendAmountInput } from '@/wallet/components/WalletAdvancedSend/components/SendAmountInput'; +import { SendFundingWallet } from '@/wallet/components/WalletAdvancedSend/components/SendFundingWallet'; type SendReact = { children?: ReactNode; @@ -63,6 +64,10 @@ function SendDefaultChildren() { }); const activeStep = useMemo(() => { + if (!context.ethBalance) { + return ; + } + if (!context.selectedRecipientAddress) { return ( <> @@ -89,6 +94,7 @@ function SendDefaultChildren() { return ; }, [ + context.ethBalance, context.selectedRecipientAddress, context.selectedToken, context.recipientInput, diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx index abb418d7f5..c2f9543b41 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx @@ -1,3 +1,45 @@ -export function SendAmountInput() { - return
SendAmountInput
; +import { AmountInput } from '@/internal/components/AmountInput/AmountInput'; +import { AmountInputTypeSwitch } from '@/internal/components/AmountInput/AmountInputTypeSwitch'; +// import { cn } from '@/styles/theme'; +import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; +import { useState } from 'react'; + +export function SendAmountInput({ className }: { className?: string }) { + const [selectedInputType, setSelectedInputType] = useState<'fiat' | 'crypto'>( + 'crypto', + ); + const { + fiatAmount, + cryptoAmount, + selectedToken, + setFiatAmount, + setCryptoAmount, + exchangeRate, + } = useSendContext(); + + return ( +
+ + +
+ ); } diff --git a/src/wallet/components/WalletAdvancedSend/components/SendFundingWallet.tsx b/src/wallet/components/WalletAdvancedSend/components/SendFundingWallet.tsx new file mode 100644 index 0000000000..0f2f0cc4f9 --- /dev/null +++ b/src/wallet/components/WalletAdvancedSend/components/SendFundingWallet.tsx @@ -0,0 +1,3 @@ +export function SendFundingWallet() { + return
SendFundingWallet
; +} diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index 79aba66792..5b759eb163 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -33,6 +33,14 @@ type SendContextType = { selectedToken: Token | null; setSelectedToken: Dispatch>; handleTokenSelection: (token: Token) => void; + fiatAmount: string | null; + setFiatAmount: Dispatch>; + cryptoAmount: string | null; + setCryptoAmount: Dispatch>; + exchangeRate: number; + setExchangeRate: Dispatch>; + exchangeRateLoading: boolean; + setExchangeRateLoading: Dispatch>; }; type SendProviderReact = { @@ -115,6 +123,13 @@ export function SendProvider({ children }: SendProviderReact) { const [selectedRecipientAddress, setSelectedRecipientAddress] = useState
(null); const [selectedToken, setSelectedToken] = useState(null); + const [fiatAmount, setFiatAmount] = useState(null); + const [cryptoAmount, setCryptoAmount] = useState(null); + const [exchangeRate, setExchangeRate] = useState(100); + const [exchangeRateLoading, setExchangeRateLoading] = useState(true); + + // TODO FETCH EXCHANGE RATE + const [lifecycleStatus, updateLifecycleStatus] = useLifecycleStatus({ statusName: 'init', @@ -220,6 +235,14 @@ export function SendProvider({ children }: SendProviderReact) { selectedToken, setSelectedToken, handleTokenSelection, + fiatAmount, + setFiatAmount, + cryptoAmount, + setCryptoAmount, + exchangeRate, + setExchangeRate, + exchangeRateLoading, + setExchangeRateLoading, }); return {children}; diff --git a/src/wallet/hooks/useInputResize.test.ts b/src/wallet/hooks/useInputResize.test.ts new file mode 100644 index 0000000000..d1d4832c80 --- /dev/null +++ b/src/wallet/hooks/useInputResize.test.ts @@ -0,0 +1,149 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useInputResize } from './useInputResize'; + +describe('useInputResize', () => { + let resizeCallback: (entries: ResizeObserverEntry[]) => void; + + beforeEach(() => { + // Mock ResizeObserver with callback capture + global.ResizeObserver = vi.fn().mockImplementation((callback) => { + resizeCallback = callback; + return { + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + }; + }); + }); + + it('handles null refs', () => { + const containerRef = { current: null }; + const inputRef = { current: null }; + const hiddenSpanRef = { current: null }; + const currencySpanRef = { current: null }; + + const { result } = renderHook(() => + useInputResize(containerRef, inputRef, hiddenSpanRef, currencySpanRef), + ); + + expect(() => result.current()).not.toThrow(); + }); + + it('updates input width based on measurements', () => { + const containerRef = { + current: { + getBoundingClientRect: () => ({ width: 300 }), + }, + }; + const inputRef = { + current: { + style: {}, + }, + }; + const hiddenSpanRef = { + current: { + offsetWidth: 100, + }, + }; + const currencySpanRef = { + current: { + getBoundingClientRect: () => ({ width: 20 }), + }, + }; + const { result } = renderHook(() => + useInputResize( + containerRef as React.RefObject, + inputRef as React.RefObject, + hiddenSpanRef as React.RefObject, + currencySpanRef as React.RefObject, + ), + ); + + result.current(); + + expect((inputRef.current as HTMLInputElement).style.width).toBe('100px'); + expect((inputRef.current as HTMLInputElement).style.maxWidth).toBe('280px'); + }); + + it('calls updateInputWidth when ResizeObserver triggers', () => { + const containerRef = { + current: { + getBoundingClientRect: () => ({ width: 300 }), + }, + }; + const inputRef = { + current: { + style: {}, + }, + }; + const hiddenSpanRef = { + current: { + offsetWidth: 100, + }, + }; + const currencySpanRef = { + current: { + getBoundingClientRect: () => ({ width: 20 }), + }, + }; + + renderHook(() => + useInputResize( + containerRef as React.RefObject, + inputRef as React.RefObject, + hiddenSpanRef as React.RefObject, + currencySpanRef as React.RefObject, + ), + ); + + // Trigger the ResizeObserver callback + resizeCallback([ + { + contentRect: { width: 300 } as DOMRectReadOnly, + target: document.createElement('div'), + borderBoxSize: [], + contentBoxSize: [], + devicePixelContentBoxSize: [], + }, + ]); + + // Verify the input width was updated + expect((inputRef.current as HTMLInputElement).style.width).toBe('100px'); + expect((inputRef.current as HTMLInputElement).style.maxWidth).toBe('280px'); + }); + + it('handles missing currency ref but present other refs', () => { + const containerRef = { + current: { + getBoundingClientRect: () => ({ width: 300 }), + }, + }; + const inputRef = { + current: { + style: {}, + }, + }; + const hiddenSpanRef = { + current: { + offsetWidth: 100, + }, + }; + const currencySpanRef = { current: null }; + + const { result } = renderHook(() => + useInputResize( + containerRef as React.RefObject, + inputRef as React.RefObject, + hiddenSpanRef as React.RefObject, + currencySpanRef as React.RefObject, + ), + ); + + result.current(); + + // Should still work but with 0 currency width + expect((inputRef.current as HTMLInputElement).style.width).toBe('100px'); + expect((inputRef.current as HTMLInputElement).style.maxWidth).toBe('300px'); // full container width since currency width is 0 + }); +}); diff --git a/src/wallet/hooks/useInputResize.ts b/src/wallet/hooks/useInputResize.ts new file mode 100644 index 0000000000..4681d3b4ae --- /dev/null +++ b/src/wallet/hooks/useInputResize.ts @@ -0,0 +1,41 @@ +import { type RefObject, useCallback, useEffect } from 'react'; + +export const useInputResize = ( + containerRef: RefObject, + inputRef: RefObject, + hiddenSpanRef: RefObject, + currencySpanRef: RefObject, +) => { + const updateInputWidth = useCallback(() => { + if (hiddenSpanRef.current && inputRef.current && containerRef.current) { + const textWidth = Math.max(42, hiddenSpanRef.current.offsetWidth); + const currencyWidth = + currencySpanRef.current?.getBoundingClientRect().width || 0; + const containerWidth = containerRef.current.getBoundingClientRect().width; + + // Set the input width based on available space + inputRef.current.style.width = `${textWidth}px`; + inputRef.current.style.maxWidth = `${containerWidth - currencyWidth}px`; + } + }, [containerRef, inputRef, hiddenSpanRef, currencySpanRef]); + + // Set up resize observer + useEffect(() => { + if (!containerRef.current) { + return; + } + + const resizeObserver = new ResizeObserver(() => { + updateInputWidth(); + }); + + resizeObserver.observe(containerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, [containerRef, updateInputWidth]); + + // Update width when value changes + return updateInputWidth; +}; From b0a97f54eb078b79083b78f347daf4a14195f127 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 23 Jan 2025 19:43:23 -0800 Subject: [PATCH 013/115] send amount input --- .../internal/hooks/useExchangeRate.tsx | 56 +++++++++ .../AmountInput/AmountInputTypeSwitch.tsx | 16 +-- .../components/AddressInput.tsx | 10 +- .../components/AddressSelector.tsx | 25 ++-- .../WalletAdvancedSend/components/Send.tsx | 5 +- .../components/SendAmountInput.tsx | 10 +- .../components/SendProvider.tsx | 113 ++++++++++++------ 7 files changed, 160 insertions(+), 75 deletions(-) create mode 100644 src/core-react/internal/hooks/useExchangeRate.tsx diff --git a/src/core-react/internal/hooks/useExchangeRate.tsx b/src/core-react/internal/hooks/useExchangeRate.tsx new file mode 100644 index 0000000000..a5918e0e64 --- /dev/null +++ b/src/core-react/internal/hooks/useExchangeRate.tsx @@ -0,0 +1,56 @@ +import { getSwapQuote } from '@/api'; +import { isApiError } from '@/core/utils/isApiResponseError'; +import type { Token } from '@/token'; +import { usdcToken } from '@/token/constants'; +import type { Dispatch, SetStateAction } from 'react'; + +type UseExchangeRateParams = { + token: Token; + selectedInputType: 'crypto' | 'fiat'; + setExchangeRate: Dispatch>; + setExchangeRateLoading: Dispatch>; +}; + +export async function useExchangeRate({ + token, + selectedInputType, + setExchangeRate, + setExchangeRateLoading, +}: UseExchangeRateParams) { + console.log('useing exchange rate hook'); + if (!token) { + return; + } + + if (token.address === usdcToken.address) { + setExchangeRate(1); + return; + } + + setExchangeRateLoading(true); + + const fromToken = selectedInputType === 'crypto' ? token : usdcToken; + const toToken = selectedInputType === 'crypto' ? usdcToken : token; + + try { + const response = await getSwapQuote({ + amount: '1', // hardcoded amount because we only need the exchange rate + from: fromToken, + to: toToken, + useAggregator: false, + }); + if (isApiError(response)) { + console.error('Error fetching exchange rate:', response.error); + return; + } + const rate = + selectedInputType === 'crypto' + ? 1 / Number(response.fromAmountUSD) + : Number(response.toAmount) / 10 ** response.to.decimals; + setExchangeRate(rate); + } catch (error) { + console.error('Uncaught error fetching exchange rate:', error); + } finally { + setExchangeRateLoading(false); + } +} diff --git a/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx b/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx index 14420a63ba..ee0bb5ed10 100644 --- a/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx +++ b/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx @@ -9,8 +9,8 @@ type AmountInputTypeSwitchPropsReact = { selectedInputType: 'fiat' | 'crypto'; setSelectedInputType: (type: 'fiat' | 'crypto') => void; asset: string; - fundAmountFiat: string; - fundAmountCrypto: string; + fiatAmount: string; + cryptoAmount: string; exchangeRate: number; exchangeRateLoading: boolean; currency: string; @@ -21,8 +21,8 @@ export function AmountInputTypeSwitch({ selectedInputType, setSelectedInputType, asset, - fundAmountFiat, - fundAmountCrypto, + fiatAmount, + cryptoAmount, exchangeRate, exchangeRateLoading, currency, @@ -45,17 +45,17 @@ export function AmountInputTypeSwitch({ return ( {selectedInputType === 'fiat' - ? formatCrypto(fundAmountCrypto) + ? formatCrypto(cryptoAmount) : formatFiatAmount({ - amount: fundAmountFiat, + amount: fiatAmount, currency: currency, minimumFractionDigits: 0, })} ); }, [ - fundAmountCrypto, - fundAmountFiat, + cryptoAmount, + fiatAmount, selectedInputType, formatCrypto, currency, diff --git a/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx b/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx index 828d9f7967..6b947255c5 100644 --- a/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx @@ -2,14 +2,6 @@ import { TextInput } from '@/internal/components/TextInput'; import { background, border, cn, color } from '@/styles/theme'; import type { Dispatch, SetStateAction } from 'react'; -/*** - delayMs?: number; - disabled?: boolean; - onBlur?: () => void; - inputValidator?: (s: string) => boolean; -}; - */ - type AddressInputProps = { addressInput: string | null; setAddressInput: Dispatch>; @@ -36,9 +28,9 @@ export function AddressInput({
diff --git a/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx index 3dbf979e7e..756bb409ef 100644 --- a/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx @@ -1,23 +1,24 @@ import { Address, Avatar, Identity, Name } from '@/identity'; import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; import { background, border, cn, pressable } from '@/styles/theme'; +import { useCallback } from 'react'; +import type { Address as AddressType } from 'viem'; -export function AddressSelector() { - const { senderChain, validatedRecipientAddress, handleAddressSelection } = - useSendContext(); +type AddressSelectorProps = { + address: AddressType; +}; - if (!validatedRecipientAddress) { - return null; - } +export function AddressSelector({ address }: AddressSelectorProps) { + const { senderChain, handleAddressSelection } = useSendContext(); + + const handleClick = useCallback(() => { + handleAddressSelection(address); + }, [handleAddressSelection, address]); return ( - + )} +
+ ); +} From f37900175d0ede507d6c72fe53bc27aeb24bb3a1 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 24 Jan 2025 17:20:10 -0800 Subject: [PATCH 022/115] enable actions on token balance --- src/internal/components/TokenBalance.tsx | 37 +++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/internal/components/TokenBalance.tsx b/src/internal/components/TokenBalance.tsx index e675cd175c..b78b9fdeb4 100644 --- a/src/internal/components/TokenBalance.tsx +++ b/src/internal/components/TokenBalance.tsx @@ -3,6 +3,7 @@ import { TokenImage, type Token } from '@/token'; type TokenBalanceProps = { className?: string; + onClick?: () => void; onActionPress: () => void; token: Token; amount: string; @@ -19,7 +20,41 @@ export function TokenBalance({ subtitle, showAction = false, showImage = true, + onClick, }: TokenBalanceProps) { + if (onClick) { + return ( + + )} + + ); + } return (
{showImage && }
From f3854dacbd436f8a6ff077a45cd52a2511dd4b5c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sun, 26 Jan 2025 15:34:23 -0800 Subject: [PATCH 023/115] token selection --- src/internal/components/TokenBalance.tsx | 140 +++++++++++------- .../WalletAdvancedSend/components/Send.tsx | 14 +- .../components/SendProvider.tsx | 12 ++ .../components/TokenSelector.tsx | 61 +++++--- 4 files changed, 138 insertions(+), 89 deletions(-) diff --git a/src/internal/components/TokenBalance.tsx b/src/internal/components/TokenBalance.tsx index b78b9fdeb4..23cc2b76b2 100644 --- a/src/internal/components/TokenBalance.tsx +++ b/src/internal/components/TokenBalance.tsx @@ -1,85 +1,117 @@ -import { background, border, cn, color, text } from '@/styles/theme'; -import { TokenImage, type Token } from '@/token'; +import type { PortfolioTokenWithFiatValue } from '@/api/types'; +import { truncateDecimalPlaces } from '@/internal/utils/truncateDecimalPlaces'; +import { cn, color, text } from '@/styles/theme'; +import { TokenImage } from '@/token'; +import { useMemo } from 'react'; type TokenBalanceProps = { - className?: string; - onClick?: () => void; - onActionPress: () => void; - token: Token; - amount: string; + token: PortfolioTokenWithFiatValue; subtitle: string; - showAction?: boolean; showImage?: boolean; -}; + onClick?: (token: PortfolioTokenWithFiatValue) => void; + className?: string; +} & ( + | { showAction?: true; onActionPress?: () => void } + | { showAction?: false; onActionPress?: never } +); export function TokenBalance({ - className, - onActionPress, token, - amount, subtitle, - showAction = false, showImage = true, onClick, + showAction = false, + onActionPress, + className, }: TokenBalanceProps) { + const formattedValueInFiat = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(token.fiatBalance); + + const tokenContent = useMemo(() => { + return ( + <> + {showImage && } +
+ + {token.name?.trim()} + + + {`${truncateDecimalPlaces( + token.cryptoBalance / 10 ** token.decimals, + 2, + )} ${token.symbol} ${subtitle}`} + +
+ {showAction ? ( + { + e.stopPropagation(); + onActionPress?.(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.stopPropagation(); + onActionPress?.(); + } + }} + className={cn( + text.label2, + color.primary, + 'ml-auto p-0.5 hover:font-bold', + )} + aria-label="Use max" + > + Use max + + ) : ( + + {formattedValueInFiat} + + )} + + ); + }, [ + showAction, + token, + formattedValueInFiat, + showImage, + onActionPress, + subtitle, + ]); + if (onClick) { return ( - )} + {tokenContent} ); } + return (
- {showImage && } -
-
{`${amount} ${token.symbol}`}
-
{subtitle}
-
- {showAction && ( - - )} + {tokenContent}
); } diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index c21ed87c5e..ebfd2af011 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -8,7 +8,6 @@ import { AddressSelector } from '@/wallet/components/WalletAdvancedSend/componen import { TokenSelector } from '@/wallet/components/WalletAdvancedSend/components/TokenSelector'; import { SendAmountInput } from '@/wallet/components/WalletAdvancedSend/components/SendAmountInput'; import { SendFundingWallet } from '@/wallet/components/WalletAdvancedSend/components/SendFundingWallet'; -import { TokenBalance } from '@/internal/components/TokenBalance'; type SendReact = { children?: ReactNode; @@ -90,19 +89,10 @@ function SendDefaultChildren() { } return ( -
+
- { - console.log('clicked token'); - }} - /> +
); }, [ diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index 9d45a72d72..0bdb6456c9 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -33,6 +33,7 @@ type SendContextType = { selectedToken: PortfolioTokenWithFiatValue | null; setSelectedToken: Dispatch>; handleTokenSelection: (token: PortfolioTokenWithFiatValue) => void; + handleResetTokenSelection: () => void; fiatAmount: string | null; setFiatAmount: Dispatch>; cryptoAmount: string | null; @@ -195,6 +196,16 @@ export function SendProvider({ children }: SendProviderReact) { [updateLifecycleStatus], ); + const handleResetTokenSelection = useCallback(() => { + setSelectedToken(null); + updateLifecycleStatus({ + statusName: 'selectingAddress', + statusData: { + isMissingRequiredField: true, + }, + }); + }, [updateLifecycleStatus]); + useEffect(() => { if (!selectedToken) { return; @@ -224,6 +235,7 @@ export function SendProvider({ children }: SendProviderReact) { selectedToken, setSelectedToken, handleTokenSelection, + handleResetTokenSelection, fiatAmount, setFiatAmount, cryptoAmount, diff --git a/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx index fc199079c9..f305e9311c 100644 --- a/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx @@ -1,30 +1,45 @@ import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; -import { TokenRow } from '@/token'; -import { cn, text } from '@/styles/theme'; +import { TokenBalance } from '@/internal/components/TokenBalance'; +import { border, cn, pressable, text } from '@/styles/theme'; export function TokenSelector() { - const { tokenBalances, handleTokenSelection } = useSendContext(); + const { + tokenBalances, + selectedToken, + handleTokenSelection, + handleResetTokenSelection, + } = useSendContext(); - return ( -
- Select a token -
- {tokenBalances?.map((token) => ( - - ))} + if (!selectedToken) { + return ( +
+ Select a token +
+ {tokenBalances?.map((token) => ( + + ))} +
-
+ ); + } + + return ( + { + console.log('clicked max'); + }} + className={cn(pressable.default, border.radius)} + /> ); } From b1df7ac753bad1cd09f3d681b34d98e4bd3e348b Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sun, 26 Jan 2025 19:17:13 -0800 Subject: [PATCH 024/115] improved token reselection --- .../components/AmountInput/AmountInput.tsx | 3 +-- .../WalletAdvancedSend/components/Send.tsx | 8 ++++--- .../components/SendAmountInput.tsx | 22 ++++++++++--------- .../components/SendButton.tsx | 15 +++++++++++++ .../components/SendProvider.tsx | 3 +++ ...okenSelector.tsx => SendTokenSelector.tsx} | 16 +++++++++++--- 6 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 src/wallet/components/WalletAdvancedSend/components/SendButton.tsx rename src/wallet/components/WalletAdvancedSend/components/{TokenSelector.tsx => SendTokenSelector.tsx} (72%) diff --git a/src/internal/components/AmountInput/AmountInput.tsx b/src/internal/components/AmountInput/AmountInput.tsx index 8f0ef5268c..efa4bd6fe0 100644 --- a/src/internal/components/AmountInput/AmountInput.tsx +++ b/src/internal/components/AmountInput/AmountInput.tsx @@ -29,13 +29,12 @@ export function AmountInput({ setCryptoAmount, exchangeRate, }: AmountInputProps) { - const currencyOrAsset = selectedInputType === 'fiat' ? currency : asset; - const containerRef = useRef(null); const inputRef = useRef(null); const hiddenSpanRef = useRef(null); const currencySpanRef = useRef(null); + const currencyOrAsset = selectedInputType === 'fiat' ? currency : asset; const value = selectedInputType === 'fiat' ? fiatAmount : cryptoAmount; const updateInputWidth = useInputResize( diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index ebfd2af011..aee223b38c 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -5,9 +5,10 @@ import { SendHeader } from './SendHeader'; import { SendProvider, useSendContext } from './SendProvider'; import { AddressInput } from '@/wallet/components/WalletAdvancedSend/components/AddressInput'; import { AddressSelector } from '@/wallet/components/WalletAdvancedSend/components/AddressSelector'; -import { TokenSelector } from '@/wallet/components/WalletAdvancedSend/components/TokenSelector'; +import { SendTokenSelector } from '@/wallet/components/WalletAdvancedSend/components/SendTokenSelector'; import { SendAmountInput } from '@/wallet/components/WalletAdvancedSend/components/SendAmountInput'; import { SendFundingWallet } from '@/wallet/components/WalletAdvancedSend/components/SendFundingWallet'; +import { SendButton } from '@/wallet/components/WalletAdvancedSend/components/SendButton'; type SendReact = { children?: ReactNode; @@ -83,7 +84,7 @@ function SendDefaultChildren() { return ( <> - + ); } @@ -92,7 +93,8 @@ function SendDefaultChildren() {
- + +
); }, [ diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx index 8d89051c57..9766adce24 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx @@ -28,16 +28,18 @@ export function SendAmountInput({ className }: { className?: string }) { setCryptoAmount={setCryptoAmount} exchangeRate={String(exchangeRate)} /> - + {exchangeRate > 0 && ( + + )}
); diff --git a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx new file mode 100644 index 0000000000..15236a19a8 --- /dev/null +++ b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx @@ -0,0 +1,15 @@ +import { border, cn, pressable } from '@/styles/theme'; + +export function SendButton() { + return ( + + ); +} diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index 0bdb6456c9..52ac6f2c84 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -198,6 +198,9 @@ export function SendProvider({ children }: SendProviderReact) { const handleResetTokenSelection = useCallback(() => { setSelectedToken(null); + setFiatAmount(null); + setCryptoAmount(null); + setExchangeRate(0); updateLifecycleStatus({ statusName: 'selectingAddress', statusData: { diff --git a/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx similarity index 72% rename from src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx rename to src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx index f305e9311c..c96f1a3b2c 100644 --- a/src/wallet/components/WalletAdvancedSend/components/TokenSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx @@ -2,12 +2,15 @@ import { useSendContext } from '@/wallet/components/WalletAdvancedSend/component import { TokenBalance } from '@/internal/components/TokenBalance'; import { border, cn, pressable, text } from '@/styles/theme'; -export function TokenSelector() { +export function SendTokenSelector() { const { tokenBalances, selectedToken, handleTokenSelection, handleResetTokenSelection, + setSelectedInputType, + setCryptoAmount, + setFiatAmount, } = useSendContext(); if (!selectedToken) { @@ -37,9 +40,16 @@ export function TokenSelector() { onClick={handleResetTokenSelection} showAction={true} onActionPress={() => { - console.log('clicked max'); + setSelectedInputType('crypto'); + setFiatAmount(String(selectedToken.fiatBalance)); + setCryptoAmount( + String( + Number(selectedToken.cryptoBalance) / + 10 ** Number(selectedToken.decimals), + ), + ); }} - className={cn(pressable.default, border.radius)} + className={cn(pressable.alternate, border.radius)} /> ); } From da50b4990fb47568e2b5cb313c4067a0c4efcb85 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sun, 26 Jan 2025 19:47:03 -0800 Subject: [PATCH 025/115] improved asset type switch --- .../components/SendAmountInput.tsx | 69 +++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx index 9766adce24..45923a6b15 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx @@ -1,5 +1,8 @@ +import type { PortfolioTokenWithFiatValue } from '@/api/types'; import { AmountInput } from '@/internal/components/AmountInput/AmountInput'; import { AmountInputTypeSwitch } from '@/internal/components/AmountInput/AmountInputTypeSwitch'; +import { Skeleton } from '@/internal/components/Skeleton'; +import { cn, color, text } from '@/styles/theme'; import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; export function SendAmountInput({ className }: { className?: string }) { @@ -10,6 +13,7 @@ export function SendAmountInput({ className }: { className?: string }) { setFiatAmount, setCryptoAmount, exchangeRate, + exchangeRateLoading, selectedInputType, setSelectedInputType, } = useSendContext(); @@ -28,19 +32,60 @@ export function SendAmountInput({ className }: { className?: string }) { setCryptoAmount={setCryptoAmount} exchangeRate={String(exchangeRate)} /> - {exchangeRate > 0 && ( - - )} + +
); } + +function SendAmountInputTypeSwitch({ + exchangeRateLoading, + exchangeRate, + selectedToken, + fiatAmount, + cryptoAmount, + selectedInputType, + setSelectedInputType, +}: { + exchangeRateLoading: boolean; + exchangeRate: number; + selectedToken: PortfolioTokenWithFiatValue | null; + fiatAmount: string; + cryptoAmount: string; + selectedInputType: 'fiat' | 'crypto'; + setSelectedInputType: (type: 'fiat' | 'crypto') => void; +}) { + if (exchangeRateLoading) { + return ; + } + + if (exchangeRate <= 0) { + return ( +
+ Exchange rate unavailable +
+ ); + } + + return ( + + ); +} From 393fc342a7d6d6d7bbba44e304adea894e5a01b7 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sun, 26 Jan 2025 20:20:22 -0800 Subject: [PATCH 026/115] update send button --- .../components/SendButton.tsx | 104 ++++++++++++++++-- 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx index 15236a19a8..dff2276620 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx @@ -1,15 +1,97 @@ -import { border, cn, pressable } from '@/styles/theme'; +import { cn, color, text } from '@/styles/theme'; +import { + type TransactionButtonReact, + Transaction, + TransactionButton, + TransactionSponsor, + TransactionStatus, + TransactionStatusAction, + TransactionStatusLabel, +} from '@/transaction'; +import type { Call } from '@/transaction/types'; +import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; +import { useMemo, useState } from 'react'; + +type SendButtonProps = { + label?: string; + className?: string; +} & Pick< + TransactionButtonReact, + 'disabled' | 'pendingOverride' | 'successOverride' | 'errorOverride' +>; + +export function SendButton({ + label = 'Continue', + className, + disabled, + successOverride, + pendingOverride, + errorOverride, +}: SendButtonProps) { + const isSponsored = false; + const chainId = 8453; + const [callData, setCallData] = useState([]); + const [sendError, setSendError] = useState(null); + + const { cryptoAmount, selectedToken } = useSendContext(); + + const handleOnStatus = () => {}; + + const sendButtonLabel = useMemo(() => { + if ( + Number(cryptoAmount) > + Number(selectedToken?.cryptoBalance) / + 10 ** Number(selectedToken?.decimals) + ) { + return 'Insufficient balance'; + } + return label; + }, [cryptoAmount, label, selectedToken]); + + const isDisabled = useMemo(() => { + if (disabled) { + return true; + } + if (Number(cryptoAmount) <= 0) { + return true; + } + if ( + Number(cryptoAmount) > + Number(selectedToken?.cryptoBalance) / + 10 ** Number(selectedToken?.decimals) + ) { + return true; + } + return false; + }, [cryptoAmount, disabled, selectedToken]); -export function SendButton() { return ( - + <> + + + {!sendError && } + + + + + + {sendError && ( +
+ {sendError} +
+ )} + ); } From d029daf503bf816740314eb4ae63b60365cc2eea Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sun, 26 Jan 2025 20:26:26 -0800 Subject: [PATCH 027/115] header back button --- .../components/SendHeader.tsx | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx b/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx index 4e4a568551..7054b48822 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx @@ -1,25 +1,47 @@ import { cn, text } from '@/styles/theme'; import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; -import { useCallback, type ReactNode } from 'react'; +import { useCallback } from 'react'; import { PressableIcon } from '@/internal/components/PressableIcon'; import { CloseSvg } from '@/internal/svg/closeSvg'; +import { backArrowSvg } from '@/internal/svg/backArrowSvg'; +import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; -export function SendHeader({ - label = 'Send', - leftContent, -}: { - label?: string; - leftContent?: ReactNode; -}) { +export function SendHeader({ label = 'Send' }: { label?: string }) { const { setShowSend } = useWalletAdvancedContext(); + const { + selectedRecipientAddress, + setSelectedRecipientAddress, + selectedToken, + setSelectedToken, + } = useSendContext(); + + const handleBack = useCallback(() => { + if (selectedToken) { + setSelectedToken(null); + } else if (selectedRecipientAddress) { + setSelectedRecipientAddress(null); + } + }, [ + selectedRecipientAddress, + selectedToken, + setSelectedRecipientAddress, + setSelectedToken, + ]); + const handleClose = useCallback(() => { setShowSend(false); }, [setShowSend]); return (
-
{leftContent}
+
+ {selectedRecipientAddress && ( + + {backArrowSvg} + + )} +
{label}
From bfbb95a2b1b1d542af1445348753cd4808717f04 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 27 Jan 2025 14:05:32 -0800 Subject: [PATCH 028/115] fix imports --- .../components/AmountInput/AmountInputTypeSwitch.tsx | 12 +++--------- .../WalletAdvancedSend/components/Send.tsx | 2 +- .../WalletAdvancedSend/components/SendProvider.tsx | 11 +++++++---- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx b/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx index db041cca7e..ca33a6d78d 100644 --- a/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx +++ b/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx @@ -1,9 +1,9 @@ import { formatFiatAmount } from '@/internal/utils/formatFiatAmount'; import { useCallback, useMemo } from 'react'; -import { useIcon } from '@/core-react/internal/hooks/useIcon'; import { Skeleton } from '@/internal/components/Skeleton'; +import { useIcon } from '@/internal/hooks/useIcon'; +import { truncateDecimalPlaces } from '@/internal/utils/truncateDecimalPlaces'; import { cn, pressable, text } from '@/styles/theme'; -import { truncateDecimalPlaces } from '@/core/utils/truncateDecimalPlaces'; type AmountInputTypeSwitchPropsReact = { selectedInputType: 'fiat' | 'crypto'; @@ -53,13 +53,7 @@ export function AmountInputTypeSwitch({ })} ); - }, [ - cryptoAmount, - fiatAmount, - selectedInputType, - formatCrypto, - currency, - ]); + }, [cryptoAmount, fiatAmount, selectedInputType, formatCrypto, currency]); if (exchangeRateLoading || !exchangeRate) { return ; diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index aee223b38c..9bea6efeaa 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@/core-react/internal/hooks/useTheme'; +import { useTheme } from '@/internal/hooks/useTheme'; import { background, border, cn, color } from '@/styles/theme'; import { useMemo, type ReactNode } from 'react'; import { SendHeader } from './SendHeader'; diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index 52ac6f2c84..6182b6be2c 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -1,4 +1,3 @@ -import { useValue } from '@/core-react/internal/hooks/useValue'; import type { PortfolioTokenWithFiatValue } from '@/api/types'; import { createContext, @@ -12,7 +11,8 @@ import { } from 'react'; import type { Address, Chain, Hex, TransactionReceipt } from 'viem'; import { validateAddressInput } from '@/wallet/components/WalletAdvancedSend/validateAddressInput'; -import { useLifecycleStatus } from '@/core-react/internal/hooks/useLifecycleStatus'; +import { useLifecycleStatus } from '@/internal/hooks/useLifecycleStatus'; +import { useValue } from '@/internal/hooks/useValue'; import { useWalletContext } from '@/wallet/components/WalletProvider'; import { useWalletAdvancedContext } from '@/wallet/components/WalletAdvancedProvider'; import { useExchangeRate } from '@/internal/hooks/useExchangeRate'; @@ -31,7 +31,9 @@ type SendContextType = { setSelectedRecipientAddress: Dispatch>; handleAddressSelection: (address: Address) => void; selectedToken: PortfolioTokenWithFiatValue | null; - setSelectedToken: Dispatch>; + setSelectedToken: Dispatch< + SetStateAction + >; handleTokenSelection: (token: PortfolioTokenWithFiatValue) => void; handleResetTokenSelection: () => void; fiatAmount: string | null; @@ -125,7 +127,8 @@ export function SendProvider({ children }: SendProviderReact) { useState
(null); const [selectedRecipientAddress, setSelectedRecipientAddress] = useState
(null); - const [selectedToken, setSelectedToken] = useState(null); + const [selectedToken, setSelectedToken] = + useState(null); const [selectedInputType, setSelectedInputType] = useState<'fiat' | 'crypto'>( 'crypto', ); From ada978b04faa09c8db4c2992c70674b453bd4668 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 27 Jan 2025 15:14:52 -0800 Subject: [PATCH 029/115] refactor provider for clarity --- src/api/buildTransferTransaction.ts | 39 +++ src/internal/hooks/useTransferTransaction.tsx | 44 ++++ .../components/AddressInput.tsx | 40 ++- .../WalletAdvancedSend/components/Send.tsx | 4 +- ...ssSelector.tsx => SendAddressSelector.tsx} | 4 +- .../components/SendAmountInput.tsx | 16 +- .../components/SendButton.tsx | 14 +- .../components/SendHeader.tsx | 12 +- .../components/SendProvider.tsx | 239 +++++++++--------- .../components/SendTokenSelector.tsx | 8 +- .../components/WalletAdvancedSend/types.ts | 104 ++++++++ 11 files changed, 359 insertions(+), 165 deletions(-) create mode 100644 src/api/buildTransferTransaction.ts create mode 100644 src/internal/hooks/useTransferTransaction.tsx rename src/wallet/components/WalletAdvancedSend/components/{AddressSelector.tsx => SendAddressSelector.tsx} (90%) create mode 100644 src/wallet/components/WalletAdvancedSend/types.ts diff --git a/src/api/buildTransferTransaction.ts b/src/api/buildTransferTransaction.ts new file mode 100644 index 0000000000..b9426a20f5 --- /dev/null +++ b/src/api/buildTransferTransaction.ts @@ -0,0 +1,39 @@ +import type { Call } from '@/transaction/types'; +import { type Address, encodeFunctionData, erc20Abi } from 'viem'; + +type BuildTransferTransactionParams = { + recipientAddress: Address; + tokenAddress: Address | null; + amount: bigint; +}; + +export function buildTransferTransaction({ + recipientAddress, + tokenAddress, + amount, +}: BuildTransferTransactionParams): Call[] { + // if no token address, we are sending native ETH + // and the data prop is empty + if (!tokenAddress) { + return [ + { + to: recipientAddress, + data: '0x', + value: amount, + }, + ]; + } + + const transferCallData = encodeFunctionData({ + abi: erc20Abi, + functionName: 'transfer', + args: [recipientAddress, amount], + }); + + return [ + { + to: tokenAddress, + data: transferCallData, + }, + ]; +} diff --git a/src/internal/hooks/useTransferTransaction.tsx b/src/internal/hooks/useTransferTransaction.tsx new file mode 100644 index 0000000000..897a24bd75 --- /dev/null +++ b/src/internal/hooks/useTransferTransaction.tsx @@ -0,0 +1,44 @@ +import { buildTransferTransaction } from '@/api/buildTransferTransaction'; +import type { Token } from '@/token'; +import type { Call } from '@/transaction/types'; +import { type Address, parseUnits } from 'viem'; + +type UseTransferTransactionParams = { + recipientAddress: Address; + token: Token | null; + amount: string; +}; + +export function useTransferTransaction({ + recipientAddress, + token, + amount, +}: UseTransferTransactionParams): { calls: Call[] } { + if (!token) { + return { calls: [] }; + } + + if (!token.address) { + if (token.symbol !== 'ETH') { + return { calls: [] }; + } + const parsedAmount = parseUnits(amount, token.decimals); + return { + calls: buildTransferTransaction({ + recipientAddress, + tokenAddress: null, + amount: parsedAmount, + }), + }; + } + + const parsedAmount = parseUnits(amount.toString(), token.decimals); + + return { + calls: buildTransferTransaction({ + recipientAddress, + tokenAddress: token.address, + amount: parsedAmount, + }), + }; +} diff --git a/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx b/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx index 430c5f1218..9065f82b41 100644 --- a/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx @@ -11,34 +11,30 @@ type AddressInputProps = { export function AddressInput({ className }: AddressInputProps) { const { selectedRecipientAddress, - setSelectedRecipientAddress, - setValidatedRecipientAddress, recipientInput, - setRecipientInput, + handleRecipientInputChange, } = useSendContext(); const inputValue = selectedRecipientAddress ? getSlicedAddress(selectedRecipientAddress) : recipientInput; - const handleFocus = useCallback((e: React.FocusEvent) => { - if (selectedRecipientAddress) { - setRecipientInput(selectedRecipientAddress); - setTimeout(() => { - // TODO: This is a hack to get the cursor to the end of the input, how to remove timeout? - e.target.setSelectionRange(selectedRecipientAddress.length, selectedRecipientAddress.length); - e.target.scrollLeft = e.target.scrollWidth; - - }, 0); - setSelectedRecipientAddress(null); - setValidatedRecipientAddress(null); - } - }, [ - selectedRecipientAddress, - setSelectedRecipientAddress, - setValidatedRecipientAddress, - setRecipientInput, - ]); + const handleFocus = useCallback( + (e: React.FocusEvent) => { + if (selectedRecipientAddress) { + handleRecipientInputChange(selectedRecipientAddress); + setTimeout(() => { + // TODO: This is a hack to get the cursor to the end of the input, how to remove timeout? + e.target.setSelectionRange( + selectedRecipientAddress.length, + selectedRecipientAddress.length, + ); + e.target.scrollLeft = e.target.scrollWidth; + }, 0); + } + }, + [selectedRecipientAddress, handleRecipientInputChange], + ); return (
{context.validatedRecipientAddress && ( - + )} ); diff --git a/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx similarity index 90% rename from src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx rename to src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx index 743a091415..bcf20763b4 100644 --- a/src/wallet/components/WalletAdvancedSend/components/AddressSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx @@ -4,11 +4,11 @@ import { background, border, cn, pressable } from '@/styles/theme'; import { useCallback } from 'react'; import type { Address as AddressType } from 'viem'; -type AddressSelectorProps = { +type SendAddressSelectorProps = { address: AddressType; }; -export function AddressSelector({ address }: AddressSelectorProps) { +export function SendAddressSelector({ address }: SendAddressSelectorProps) { const { senderChain, handleAddressSelection } = useSendContext(); const handleClick = useCallback(() => { diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx index 45923a6b15..a41cb3130a 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx @@ -7,15 +7,15 @@ import { useSendContext } from '@/wallet/components/WalletAdvancedSend/component export function SendAmountInput({ className }: { className?: string }) { const { - fiatAmount, - cryptoAmount, selectedToken, - setFiatAmount, - setCryptoAmount, - exchangeRate, - exchangeRateLoading, + cryptoAmount, + handleCryptoAmountChange, + fiatAmount, + handleFiatAmountChange, selectedInputType, setSelectedInputType, + exchangeRate, + exchangeRateLoading, } = useSendContext(); return ( @@ -28,8 +28,8 @@ export function SendAmountInput({ className }: { className?: string }) { currency={'USD'} selectedInputType={selectedInputType} className={className} - setFiatAmount={setFiatAmount} - setCryptoAmount={setCryptoAmount} + setFiatAmount={handleFiatAmountChange} + setCryptoAmount={handleCryptoAmountChange} exchangeRate={String(exchangeRate)} /> diff --git a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx index dff2276620..84186d63cc 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx @@ -8,9 +8,8 @@ import { TransactionStatusAction, TransactionStatusLabel, } from '@/transaction'; -import type { Call } from '@/transaction/types'; import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; type SendButtonProps = { label?: string; @@ -30,10 +29,9 @@ export function SendButton({ }: SendButtonProps) { const isSponsored = false; const chainId = 8453; - const [callData, setCallData] = useState([]); - const [sendError, setSendError] = useState(null); - const { cryptoAmount, selectedToken } = useSendContext(); + const { cryptoAmount, selectedToken, callData, sendTransactionError } = + useSendContext(); const handleOnStatus = () => {}; @@ -81,15 +79,15 @@ export function SendButton({ errorOverride={errorOverride} disabled={isDisabled} /> - {!sendError && } + {!sendTransactionError && } - {sendError && ( + {sendTransactionError && (
- {sendError} + {sendTransactionError}
)} diff --git a/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx b/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx index 7054b48822..57abc64811 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx @@ -11,22 +11,22 @@ export function SendHeader({ label = 'Send' }: { label?: string }) { const { selectedRecipientAddress, - setSelectedRecipientAddress, selectedToken, - setSelectedToken, + handleResetTokenSelection, + handleRecipientInputChange, } = useSendContext(); const handleBack = useCallback(() => { if (selectedToken) { - setSelectedToken(null); + handleResetTokenSelection(); } else if (selectedRecipientAddress) { - setSelectedRecipientAddress(null); + handleRecipientInputChange(selectedRecipientAddress); } }, [ selectedRecipientAddress, selectedToken, - setSelectedRecipientAddress, - setSelectedToken, + handleResetTokenSelection, + handleRecipientInputChange, ]); const handleClose = useCallback(() => { diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index 6182b6be2c..160b324a43 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -1,116 +1,22 @@ import type { PortfolioTokenWithFiatValue } from '@/api/types'; import { createContext, - type Dispatch, - type ReactNode, - type SetStateAction, useCallback, useContext, useEffect, useState, } from 'react'; -import type { Address, Chain, Hex, TransactionReceipt } from 'viem'; +import type { Address } from 'viem'; import { validateAddressInput } from '@/wallet/components/WalletAdvancedSend/validateAddressInput'; import { useLifecycleStatus } from '@/internal/hooks/useLifecycleStatus'; import { useValue } from '@/internal/hooks/useValue'; import { useWalletContext } from '@/wallet/components/WalletProvider'; import { useWalletAdvancedContext } from '@/wallet/components/WalletAdvancedProvider'; import { useExchangeRate } from '@/internal/hooks/useExchangeRate'; - -type SendContextType = { - lifecycleStatus: LifecycleStatus; - senderAddress: Address | null | undefined; - senderChain: Chain | null | undefined; - ethBalance: number | undefined; - tokenBalances: PortfolioTokenWithFiatValue[] | undefined; - recipientInput: string | null; - setRecipientInput: Dispatch>; - validatedRecipientAddress: Address | null; - setValidatedRecipientAddress: Dispatch>; - selectedRecipientAddress: Address | null; - setSelectedRecipientAddress: Dispatch>; - handleAddressSelection: (address: Address) => void; - selectedToken: PortfolioTokenWithFiatValue | null; - setSelectedToken: Dispatch< - SetStateAction - >; - handleTokenSelection: (token: PortfolioTokenWithFiatValue) => void; - handleResetTokenSelection: () => void; - fiatAmount: string | null; - setFiatAmount: Dispatch>; - cryptoAmount: string | null; - setCryptoAmount: Dispatch>; - exchangeRate: number; - setExchangeRate: Dispatch>; - exchangeRateLoading: boolean; - setExchangeRateLoading: Dispatch>; - selectedInputType: 'fiat' | 'crypto'; - setSelectedInputType: Dispatch>; -}; - -type SendProviderReact = { - children: ReactNode; -}; - -type LifecycleStatus = - | { - statusName: 'init'; - statusData: { - isMissingRequiredField: true; - }; - } - | { - statusName: 'fundingWallet'; - statusData: { - isMissingRequiredField: true; - }; - } - | { - statusName: 'selectingAddress'; - statusData: { - isMissingRequiredField: true; - }; - } - | { - statusName: 'selectingToken'; - statusData: { - isMissingRequiredField: true; - }; - } - | { - statusName: 'tokenSelected'; - statusData: { - isMissingRequiredField: boolean; - }; - } - | { - statusName: 'amountChange'; - statusData: { - isMissingRequiredField: boolean; - sufficientBalance: boolean; - }; - } - | { - statusName: 'transactionPending'; - statusData: { - isMissingRequiredField: false; - }; - } - | { - statusName: 'transactionApproved'; - statusData: { - isMissingRequiredField: false; - callsId?: Hex; - transactionHash?: Hex; - }; - } - | { - statusName: 'success'; - statusData: { - isMissingRequiredField: false; - transactionReceipt: TransactionReceipt; - }; - }; +import type { Call } from '@/transaction/types'; +import { useTransferTransaction } from '@/internal/hooks/useTransferTransaction'; +import type { LifecycleStatus, SendProviderReact } from '../types'; +import type { SendContextType } from '../types'; const emptyContext = {} as SendContextType; @@ -121,12 +27,17 @@ export function useSendContext() { } export function SendProvider({ children }: SendProviderReact) { + // state for ETH balance const [ethBalance, setEthBalance] = useState(undefined); + + // state for recipient address selection const [recipientInput, setRecipientInput] = useState(null); const [validatedRecipientAddress, setValidatedRecipientAddress] = useState
(null); const [selectedRecipientAddress, setSelectedRecipientAddress] = useState
(null); + + // state for token selection const [selectedToken, setSelectedToken] = useState(null); const [selectedInputType, setSelectedInputType] = useState<'fiat' | 'crypto'>( @@ -138,6 +49,13 @@ export function SendProvider({ children }: SendProviderReact) { const [exchangeRateLoading, setExchangeRateLoading] = useState(false); + // state for transaction data + const [callData, setCallData] = useState([]); + const [sendTransactionError, setSendTransactionError] = useState< + string | null + >(null); + + // data and utils from hooks const [lifecycleStatus, updateLifecycleStatus] = useLifecycleStatus({ statusName: 'init', @@ -145,7 +63,6 @@ export function SendProvider({ children }: SendProviderReact) { isMissingRequiredField: true, }, }); - const { address: senderAddress, chain: senderChain } = useWalletContext(); const { tokenBalances } = useWalletAdvancedContext(); @@ -155,8 +72,15 @@ export function SendProvider({ children }: SendProviderReact) { setEthBalance( Number(ethBalance.cryptoBalance / 10 ** ethBalance.decimals), ); + } else { + updateLifecycleStatus({ + statusName: 'fundingWallet', + statusData: { + isMissingRequiredField: true, + }, + }); } - }, [tokenBalances]); + }, [tokenBalances, updateLifecycleStatus]); // Validate recipient input and set validated recipient address useEffect(() => { @@ -173,6 +97,21 @@ export function SendProvider({ children }: SendProviderReact) { validateRecipientInput(); }, [recipientInput]); + const handleRecipientInputChange = useCallback( + (input: string) => { + setRecipientInput(input); + setSelectedRecipientAddress(null); + setValidatedRecipientAddress(null); + updateLifecycleStatus({ + statusName: 'selectingAddress', + statusData: { + isMissingRequiredField: true, + }, + }); + }, + [updateLifecycleStatus], + ); + const handleAddressSelection = useCallback( (address: Address) => { setSelectedRecipientAddress(address); @@ -190,9 +129,10 @@ export function SendProvider({ children }: SendProviderReact) { (token: PortfolioTokenWithFiatValue) => { setSelectedToken(token); updateLifecycleStatus({ - statusName: 'tokenSelected', + statusName: 'amountChange', statusData: { isMissingRequiredField: true, + sufficientBalance: false, }, }); }, @@ -205,18 +145,18 @@ export function SendProvider({ children }: SendProviderReact) { setCryptoAmount(null); setExchangeRate(0); updateLifecycleStatus({ - statusName: 'selectingAddress', + statusName: 'selectingToken', statusData: { isMissingRequiredField: true, }, }); }, [updateLifecycleStatus]); + // fetch & set exchange rate useEffect(() => { if (!selectedToken) { return; } - useExchangeRate({ token: selectedToken, selectedInputType, @@ -225,33 +165,106 @@ export function SendProvider({ children }: SendProviderReact) { }); }, [selectedToken, selectedInputType]); - const value = useValue({ + const handleFiatAmountChange = useCallback( + (value: string) => { + setFiatAmount(value); + updateLifecycleStatus({ + statusName: 'amountChange', + statusData: { + isMissingRequiredField: true, + sufficientBalance: + Number(value) <= Number(selectedToken?.fiatBalance) ?? 0, + }, + }); + }, + [updateLifecycleStatus, selectedToken], + ); + + const handleCryptoAmountChange = useCallback( + (value: string) => { + setCryptoAmount(value); + updateLifecycleStatus({ + statusName: 'amountChange', + statusData: { + isMissingRequiredField: true, + sufficientBalance: + Number(value) <= + Number(selectedToken?.cryptoBalance) / + 10 ** Number(selectedToken?.decimals) ?? 0, + }, + }); + }, + [updateLifecycleStatus, selectedToken], + ); + + const handleTransactionError = useCallback( + (error: string) => { + updateLifecycleStatus({ + statusName: 'error', + statusData: { + error: 'Error building send transaction', + code: 'SmWBc01', // Send module SendButton component 01 error + message: error, + }, + }); + setSendTransactionError(error); + }, + [updateLifecycleStatus], + ); + + const fetchTransactionData = useCallback(() => { + if (!selectedRecipientAddress || !selectedToken || !cryptoAmount) { + return; + } + + try { + setCallData([]); + setSendTransactionError(null); + const { calls } = useTransferTransaction({ + recipientAddress: selectedRecipientAddress, + token: selectedToken, + amount: cryptoAmount, + }); + setCallData(calls); + } catch (error) { + handleTransactionError(error as string); + } + }, [ + selectedRecipientAddress, + selectedToken, + cryptoAmount, + handleTransactionError, + ]); + + useEffect(() => { + fetchTransactionData(); + }, [fetchTransactionData]); + + const value = useValue({ lifecycleStatus, + updateLifecycleStatus, senderAddress, senderChain, tokenBalances, ethBalance, recipientInput, - setRecipientInput, validatedRecipientAddress, - setValidatedRecipientAddress, selectedRecipientAddress, - setSelectedRecipientAddress, handleAddressSelection, selectedToken, - setSelectedToken, + handleRecipientInputChange, handleTokenSelection, handleResetTokenSelection, fiatAmount, - setFiatAmount, + handleFiatAmountChange, cryptoAmount, - setCryptoAmount, + handleCryptoAmountChange, exchangeRate, - setExchangeRate, exchangeRateLoading, - setExchangeRateLoading, selectedInputType, setSelectedInputType, + callData, + sendTransactionError, }); return {children}; diff --git a/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx index c96f1a3b2c..c9327fcccc 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx @@ -9,8 +9,8 @@ export function SendTokenSelector() { handleTokenSelection, handleResetTokenSelection, setSelectedInputType, - setCryptoAmount, - setFiatAmount, + handleCryptoAmountChange, + handleFiatAmountChange, } = useSendContext(); if (!selectedToken) { @@ -41,8 +41,8 @@ export function SendTokenSelector() { showAction={true} onActionPress={() => { setSelectedInputType('crypto'); - setFiatAmount(String(selectedToken.fiatBalance)); - setCryptoAmount( + handleFiatAmountChange(String(selectedToken.fiatBalance)); + handleCryptoAmountChange( String( Number(selectedToken.cryptoBalance) / 10 ** Number(selectedToken.decimals), diff --git a/src/wallet/components/WalletAdvancedSend/types.ts b/src/wallet/components/WalletAdvancedSend/types.ts new file mode 100644 index 0000000000..5a8cf54298 --- /dev/null +++ b/src/wallet/components/WalletAdvancedSend/types.ts @@ -0,0 +1,104 @@ +import type { APIError, PortfolioTokenWithFiatValue } from '../../../api/types'; +import type { Call } from '../../../transaction/types'; +import type { Dispatch, ReactNode, SetStateAction } from 'react'; +import type { Address, Chain, Hex, TransactionReceipt } from 'viem'; + +export type SendProviderReact = { + children: ReactNode; +}; + +export type SendContextType = { + // Lifecycle Status Context + lifecycleStatus: LifecycleStatus; + updateLifecycleStatus: (newStatus: LifecycleStatus) => void; + + // Wallet Context + senderAddress: Address | null | undefined; + senderChain: Chain | null | undefined; + ethBalance: number | undefined; + tokenBalances: PortfolioTokenWithFiatValue[] | undefined; + + // Recipient Address Context + recipientInput: string | null; + validatedRecipientAddress: Address | null; + selectedRecipientAddress: Address | null; + handleAddressSelection: (address: Address) => void; + handleRecipientInputChange: (input: string) => void; + + // Token Context + selectedToken: PortfolioTokenWithFiatValue | null; + handleTokenSelection: (token: PortfolioTokenWithFiatValue) => void; + handleResetTokenSelection: () => void; + + // Amount Context + selectedInputType: 'fiat' | 'crypto'; + setSelectedInputType: Dispatch>; + exchangeRate: number; + exchangeRateLoading: boolean; + fiatAmount: string | null; + handleFiatAmountChange: (value: string) => void; + cryptoAmount: string | null; + handleCryptoAmountChange: (value: string) => void; + + // Transaction Context + callData: Call[]; + sendTransactionError: string | null; +}; + +export type LifecycleStatus = + | { + statusName: 'init'; + statusData: { + isMissingRequiredField: true; + }; + } + | { + statusName: 'fundingWallet'; + statusData: { + isMissingRequiredField: true; + }; + } + | { + statusName: 'selectingAddress'; + statusData: { + isMissingRequiredField: true; + }; + } + | { + statusName: 'selectingToken'; + statusData: { + isMissingRequiredField: true; + }; + } + | { + statusName: 'amountChange'; + statusData: { + isMissingRequiredField: boolean; + sufficientBalance: boolean; + }; + } + | { + statusName: 'transactionPending'; + statusData: { + isMissingRequiredField: false; + }; + } + | { + statusName: 'transactionApproved'; + statusData: { + isMissingRequiredField: false; + callsId?: Hex; + transactionHash?: Hex; + }; + } + | { + statusName: 'success'; + statusData: { + isMissingRequiredField: false; + transactionReceipt: TransactionReceipt; + }; + } + | { + statusName: 'error'; + statusData: APIError; + }; From 58d9600b4c2f00818e1faa7b2df59b2530a2310f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 27 Jan 2025 15:29:29 -0800 Subject: [PATCH 030/115] update filename --- .../components/WalletAdvancedSend/components/Send.tsx | 8 ++++---- .../components/{AddressInput.tsx => SendAddressInput.tsx} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/wallet/components/WalletAdvancedSend/components/{AddressInput.tsx => SendAddressInput.tsx} (96%) diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index c0eda066c9..1f7bc2d88c 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -3,7 +3,7 @@ import { background, border, cn, color } from '@/styles/theme'; import { useMemo, type ReactNode } from 'react'; import { SendHeader } from './SendHeader'; import { SendProvider, useSendContext } from './SendProvider'; -import { AddressInput } from '@/wallet/components/WalletAdvancedSend/components/AddressInput'; +import { SendAddressInput } from '@/wallet/components/WalletAdvancedSend/components/SendAddressInput'; import { SendAddressSelector } from '@/wallet/components/WalletAdvancedSend/components/SendAddressSelector'; import { SendTokenSelector } from '@/wallet/components/WalletAdvancedSend/components/SendTokenSelector'; import { SendAmountInput } from '@/wallet/components/WalletAdvancedSend/components/SendAmountInput'; @@ -72,7 +72,7 @@ function SendDefaultChildren() { if (!context.selectedRecipientAddress) { return ( <> - + {context.validatedRecipientAddress && ( )} @@ -83,7 +83,7 @@ function SendDefaultChildren() { if (!context.selectedToken) { return ( <> - + ); @@ -91,7 +91,7 @@ function SendDefaultChildren() { return (
- + diff --git a/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx similarity index 96% rename from src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx rename to src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx index 9065f82b41..0e597946e0 100644 --- a/src/wallet/components/WalletAdvancedSend/components/AddressInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx @@ -8,7 +8,7 @@ type AddressInputProps = { className?: string; }; -export function AddressInput({ className }: AddressInputProps) { +export function SendAddressInput({ className }: AddressInputProps) { const { selectedRecipientAddress, recipientInput, From 69e5ab99e032dd449a02a0296ef8c0b4cd1daf3f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 28 Jan 2025 10:58:39 -0800 Subject: [PATCH 031/115] fix subtitle --- .../WalletAdvancedSend/components/SendTokenSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx index c9327fcccc..43a78c2006 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx @@ -36,7 +36,7 @@ export function SendTokenSelector() { { From ad5af090d89b679637acb37304976251ba9215cd Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 28 Jan 2025 11:36:33 -0800 Subject: [PATCH 032/115] add fundcard when no eth balance --- .../components/SendFundingWallet.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendFundingWallet.tsx b/src/wallet/components/WalletAdvancedSend/components/SendFundingWallet.tsx index 0f2f0cc4f9..91453c5801 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendFundingWallet.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendFundingWallet.tsx @@ -1,3 +1,19 @@ +import { FundCard } from '@/fund'; + export function SendFundingWallet() { - return
SendFundingWallet
; + return ( +
+ {}} + onStatus={() => {}} + onSuccess={() => {}} + className="w-88 border-none" + /> +
+ ); } From 35efd87f6556855fb10af97b3d840606ee57aaf3 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 28 Jan 2025 11:41:23 -0800 Subject: [PATCH 033/115] minor refactor --- .../WalletAdvancedSend/components/Send.tsx | 53 +++++++------------ 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index 1f7bc2d88c..94830bae22 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -15,48 +15,33 @@ type SendReact = { className?: string; }; -export function Send({ children, className }: SendReact) { +export function Send({ + children = , + className, +}: SendReact) { const componentTheme = useTheme(); - if (!children) { - return ( - - - - ); - } - return ( - +
{children} - +
); } -function SendContent({ - children = , - className, -}: SendReact) { - return ( -
- {children} -
- ); -} - function SendDefaultChildren() { const context = useSendContext(); @@ -65,7 +50,7 @@ function SendDefaultChildren() { }); const activeStep = useMemo(() => { - if (!context.ethBalance) { + if (!context.ethBalance || context.ethBalance < 0.0000001) { return ; } From 620554b33b39bee2a14aaaeb7ac0d932a6b595a4 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 28 Jan 2025 21:23:50 -0800 Subject: [PATCH 034/115] design review --- .../nextjs-app-router/onchainkit/package.json | 2 +- .../components/AmountInput/AmountInput.tsx | 16 +++- .../components/AmountInput/CurrencyLabel.tsx | 4 +- src/internal/components/TokenBalance.tsx | 86 +++++++++++-------- .../components/WalletAdvancedContent.tsx | 2 +- .../WalletAdvancedSend/components/Send.tsx | 4 +- .../components/SendAmountInput.tsx | 13 ++- .../components/SendTokenSelector.tsx | 6 +- 8 files changed, 81 insertions(+), 52 deletions(-) diff --git a/playground/nextjs-app-router/onchainkit/package.json b/playground/nextjs-app-router/onchainkit/package.json index 6e02ce9867..ce290f8936 100644 --- a/playground/nextjs-app-router/onchainkit/package.json +++ b/playground/nextjs-app-router/onchainkit/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/onchainkit", - "version": "0.36.8", + "version": "0.36.9", "type": "module", "repository": "https://github.com/coinbase/onchainkit.git", "license": "MIT", diff --git a/src/internal/components/AmountInput/AmountInput.tsx b/src/internal/components/AmountInput/AmountInput.tsx index efa4bd6fe0..3f2b5b24c4 100644 --- a/src/internal/components/AmountInput/AmountInput.tsx +++ b/src/internal/components/AmountInput/AmountInput.tsx @@ -16,18 +16,20 @@ type AmountInputProps = { setCryptoAmount: (value: string) => void; exchangeRate: string; className?: string; + textClassName?: string; }; export function AmountInput({ + asset, + currency, fiatAmount, cryptoAmount, - asset, selectedInputType, - currency, - className, setFiatAmount, setCryptoAmount, exchangeRate, + className, + textClassName, }: AmountInputProps) { const containerRef = useRef(null); const inputRef = useRef(null); @@ -101,6 +103,7 @@ export function AmountInput({ '[appearance:textfield]', '[&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none', '[&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none', + textClassName, )} value={value} onChange={handleAmountChange} @@ -110,7 +113,11 @@ export function AmountInput({ placeholder="0" /> - +
{/* Hidden span for measuring text width @@ -129,6 +136,7 @@ export function AmountInput({ 'text-6xl leading-none outline-none', 'pointer-events-none absolute whitespace-nowrap opacity-0', 'left-[-9999px]', // Hide the span from the DOM + textClassName, )} > {value ? `${value}.` : '0.'} diff --git a/src/internal/components/AmountInput/CurrencyLabel.tsx b/src/internal/components/AmountInput/CurrencyLabel.tsx index 37ef0ac0d9..bb6a8de35a 100644 --- a/src/internal/components/AmountInput/CurrencyLabel.tsx +++ b/src/internal/components/AmountInput/CurrencyLabel.tsx @@ -3,12 +3,13 @@ import { cn, color, text } from '@/styles/theme'; type CurrencyLabelProps = { label: string; + className?: string; }; export const CurrencyLabel = forwardRef< HTMLSpanElement, CurrencyLabelProps ->(({ label }, ref) => { +>(({ label, className }, ref) => { return ( diff --git a/src/internal/components/TokenBalance.tsx b/src/internal/components/TokenBalance.tsx index 23cc2b76b2..f15fcec109 100644 --- a/src/internal/components/TokenBalance.tsx +++ b/src/internal/components/TokenBalance.tsx @@ -11,8 +11,8 @@ type TokenBalanceProps = { onClick?: (token: PortfolioTokenWithFiatValue) => void; className?: string; } & ( - | { showAction?: true; onActionPress?: () => void } - | { showAction?: false; onActionPress?: never } + | { showAction?: true; actionText?: string; onActionPress?: () => void } + | { showAction?: false; actionText?: never; onActionPress?: never } ); export function TokenBalance({ @@ -21,6 +21,7 @@ export function TokenBalance({ showImage = true, onClick, showAction = false, + actionText = 'Use max', onActionPress, className, }: TokenBalanceProps) { @@ -31,14 +32,16 @@ export function TokenBalance({ const tokenContent = useMemo(() => { return ( - <> - {showImage && } -
+
+
+ {showImage && } +
+
{token.name?.trim()} @@ -50,41 +53,50 @@ export function TokenBalance({ )} ${token.symbol} ${subtitle}`}
- {showAction ? ( - { - e.stopPropagation(); - onActionPress?.(); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { +
+ {showAction ? ( + { e.stopPropagation(); onActionPress?.(); - } - }} - className={cn( - text.label2, - color.primary, - 'ml-auto p-0.5 hover:font-bold', - )} - aria-label="Use max" - > - Use max - - ) : ( - - {formattedValueInFiat} - - )} - + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.stopPropagation(); + onActionPress?.(); + } + }} + className={cn( + text.label2, + color.primary, + 'ml-auto p-0.5 font-bold transition-brightness hover:brightness-110', + )} + aria-label={actionText} + > + {actionText} + + ) : ( + + {formattedValueInFiat} + + )} +
+
); }, [ - showAction, - token, - formattedValueInFiat, showImage, - onActionPress, + token, subtitle, + showAction, + actionText, + onActionPress, + formattedValueInFiat, ]); if (onClick) { @@ -93,7 +105,7 @@ export function TokenBalance({ type="button" onClick={() => onClick(token)} className={cn( - 'flex w-full items-center justify-start gap-4 p-3 px-4', + 'flex w-full items-center justify-start gap-4 px-2 py-1', className, )} data-testid="ockTokenBalanceButton" @@ -106,7 +118,7 @@ export function TokenBalance({ return (
+
- +
diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx index a41cb3130a..9f07d35b95 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx @@ -5,7 +5,13 @@ import { Skeleton } from '@/internal/components/Skeleton'; import { cn, color, text } from '@/styles/theme'; import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; -export function SendAmountInput({ className }: { className?: string }) { +export function SendAmountInput({ + className, + textClassName, +}: { + className?: string; + textClassName?: string; +}) { const { selectedToken, cryptoAmount, @@ -19,8 +25,8 @@ export function SendAmountInput({ className }: { className?: string }) { } = useSendContext(); return ( -
-
+
+
- Select a token + Select a token
{tokenBalances?.map((token) => ( ); } From c3d3e84f8e486b7f038c232a2070988421d00320 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 28 Jan 2025 21:44:49 -0800 Subject: [PATCH 035/115] fix ethBalance race condition --- .../WalletAdvancedSend/components/Send.tsx | 5 +++++ .../WalletAdvancedSend/components/SendProvider.tsx | 12 ++++++++---- src/wallet/components/WalletAdvancedSend/types.ts | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index d52a89d8cf..b568a9387b 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -50,6 +50,10 @@ function SendDefaultChildren() { }); const activeStep = useMemo(() => { + if (!context.isInitialized) { + return null; + } + if (!context.ethBalance || context.ethBalance < 0.0000001) { return ; } @@ -83,6 +87,7 @@ function SendDefaultChildren() {
); }, [ + context.isInitialized, context.ethBalance, context.selectedRecipientAddress, context.selectedToken, diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index 160b324a43..ab1eff5756 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -27,8 +27,10 @@ export function useSendContext() { } export function SendProvider({ children }: SendProviderReact) { + const [isInitialized, setIsInitialized] = useState(false); + // state for ETH balance - const [ethBalance, setEthBalance] = useState(undefined); + const [ethBalance, setEthBalance] = useState(0); // state for recipient address selection const [recipientInput, setRecipientInput] = useState(null); @@ -80,6 +82,7 @@ export function SendProvider({ children }: SendProviderReact) { }, }); } + setIsInitialized(true); }, [tokenBalances, updateLifecycleStatus]); // Validate recipient input and set validated recipient address @@ -173,7 +176,7 @@ export function SendProvider({ children }: SendProviderReact) { statusData: { isMissingRequiredField: true, sufficientBalance: - Number(value) <= Number(selectedToken?.fiatBalance) ?? 0, + Number(value) <= Number(selectedToken?.fiatBalance), }, }); }, @@ -189,8 +192,8 @@ export function SendProvider({ children }: SendProviderReact) { isMissingRequiredField: true, sufficientBalance: Number(value) <= - Number(selectedToken?.cryptoBalance) / - 10 ** Number(selectedToken?.decimals) ?? 0, + Number(selectedToken?.cryptoBalance) / + 10 ** Number(selectedToken?.decimals), }, }); }, @@ -241,6 +244,7 @@ export function SendProvider({ children }: SendProviderReact) { }, [fetchTransactionData]); const value = useValue({ + isInitialized, lifecycleStatus, updateLifecycleStatus, senderAddress, diff --git a/src/wallet/components/WalletAdvancedSend/types.ts b/src/wallet/components/WalletAdvancedSend/types.ts index 5a8cf54298..cafea94144 100644 --- a/src/wallet/components/WalletAdvancedSend/types.ts +++ b/src/wallet/components/WalletAdvancedSend/types.ts @@ -9,6 +9,7 @@ export type SendProviderReact = { export type SendContextType = { // Lifecycle Status Context + isInitialized: boolean; lifecycleStatus: LifecycleStatus; updateLifecycleStatus: (newStatus: LifecycleStatus) => void; From 77771aac204f8cd7291adc04c65124a2f3e7d38d Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 28 Jan 2025 22:36:42 -0800 Subject: [PATCH 036/115] fix lints --- .../AmountInput/AmountInputTypeSwitch.tsx | 4 +- .../components/AmountInput/CurrencyLabel.tsx | 41 +++++++++---------- .../components/WalletAdvancedContent.tsx | 2 +- .../WalletAdvancedSend/components/Send.tsx | 14 +++---- .../components/SendAddressSelector.tsx | 2 +- .../components/SendButton.tsx | 4 +- .../components/SendHeader.tsx | 10 ++--- .../components/SendProvider.tsx | 16 ++++---- .../components/SendTokenSelector.tsx | 6 ++- .../components/WalletAdvancedSend/types.ts | 4 +- .../validateAddressInput.ts | 20 --------- 11 files changed, 52 insertions(+), 71 deletions(-) delete mode 100644 src/wallet/components/WalletAdvancedSend/validateAddressInput.ts diff --git a/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx b/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx index ca33a6d78d..30f7d8d1d8 100644 --- a/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx +++ b/src/internal/components/AmountInput/AmountInputTypeSwitch.tsx @@ -1,9 +1,9 @@ -import { formatFiatAmount } from '@/internal/utils/formatFiatAmount'; -import { useCallback, useMemo } from 'react'; import { Skeleton } from '@/internal/components/Skeleton'; import { useIcon } from '@/internal/hooks/useIcon'; +import { formatFiatAmount } from '@/internal/utils/formatFiatAmount'; import { truncateDecimalPlaces } from '@/internal/utils/truncateDecimalPlaces'; import { cn, pressable, text } from '@/styles/theme'; +import { useCallback, useMemo } from 'react'; type AmountInputTypeSwitchPropsReact = { selectedInputType: 'fiat' | 'crypto'; diff --git a/src/internal/components/AmountInput/CurrencyLabel.tsx b/src/internal/components/AmountInput/CurrencyLabel.tsx index bb6a8de35a..eb8fdbd06a 100644 --- a/src/internal/components/AmountInput/CurrencyLabel.tsx +++ b/src/internal/components/AmountInput/CurrencyLabel.tsx @@ -1,28 +1,27 @@ -import { forwardRef } from 'react'; import { cn, color, text } from '@/styles/theme'; +import { forwardRef } from 'react'; type CurrencyLabelProps = { label: string; className?: string; }; -export const CurrencyLabel = forwardRef< - HTMLSpanElement, - CurrencyLabelProps ->(({ label, className }, ref) => { - return ( - - {label} - - ); -}); +export const CurrencyLabel = forwardRef( + ({ label, className }, ref) => { + return ( + + {label} + + ); + }, +); diff --git a/src/wallet/components/WalletAdvancedContent.tsx b/src/wallet/components/WalletAdvancedContent.tsx index 21af44eebf..9cd643aec8 100644 --- a/src/wallet/components/WalletAdvancedContent.tsx +++ b/src/wallet/components/WalletAdvancedContent.tsx @@ -2,9 +2,9 @@ import { background, border, cn, text } from '@/styles/theme'; import { useCallback, useMemo } from 'react'; import { WALLET_ADVANCED_DEFAULT_SWAPPABLE_TOKENS } from '../constants'; import type { WalletAdvancedReact } from '../types'; -import { Send } from './WalletAdvancedSend/components/Send'; import { useWalletAdvancedContext } from './WalletAdvancedProvider'; import { WalletAdvancedQrReceive } from './WalletAdvancedQrReceive'; +import { Send } from './WalletAdvancedSend/components/Send'; import { WalletAdvancedSwap } from './WalletAdvancedSwap'; import { useWalletContext } from './WalletProvider'; diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index b568a9387b..7d71151319 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -1,14 +1,14 @@ import { useTheme } from '@/internal/hooks/useTheme'; import { background, border, cn, color } from '@/styles/theme'; -import { useMemo, type ReactNode } from 'react'; +import { type ReactNode, useMemo } from 'react'; +import { SendAddressInput } from './SendAddressInput'; +import { SendAddressSelector } from './SendAddressSelector'; +import { SendAmountInput } from './SendAmountInput'; +import { SendButton } from './SendButton'; +import { SendFundingWallet } from './SendFundingWallet'; import { SendHeader } from './SendHeader'; import { SendProvider, useSendContext } from './SendProvider'; -import { SendAddressInput } from '@/wallet/components/WalletAdvancedSend/components/SendAddressInput'; -import { SendAddressSelector } from '@/wallet/components/WalletAdvancedSend/components/SendAddressSelector'; -import { SendTokenSelector } from '@/wallet/components/WalletAdvancedSend/components/SendTokenSelector'; -import { SendAmountInput } from '@/wallet/components/WalletAdvancedSend/components/SendAmountInput'; -import { SendFundingWallet } from '@/wallet/components/WalletAdvancedSend/components/SendFundingWallet'; -import { SendButton } from '@/wallet/components/WalletAdvancedSend/components/SendButton'; +import { SendTokenSelector } from './SendTokenSelector'; type SendReact = { children?: ReactNode; diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx index bcf20763b4..b47e0c529e 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx @@ -1,8 +1,8 @@ import { Address, Avatar, Identity, Name } from '@/identity'; -import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; import { background, border, cn, pressable } from '@/styles/theme'; import { useCallback } from 'react'; import type { Address as AddressType } from 'viem'; +import { useSendContext } from './SendProvider'; type SendAddressSelectorProps = { address: AddressType; diff --git a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx index 84186d63cc..52df53bd51 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx @@ -1,15 +1,15 @@ import { cn, color, text } from '@/styles/theme'; import { - type TransactionButtonReact, Transaction, TransactionButton, + type TransactionButtonReact, TransactionSponsor, TransactionStatus, TransactionStatusAction, TransactionStatusLabel, } from '@/transaction'; -import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; import { useMemo } from 'react'; +import { useSendContext } from './SendProvider'; type SendButtonProps = { label?: string; diff --git a/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx b/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx index 57abc64811..8b63b77510 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendHeader.tsx @@ -1,10 +1,10 @@ -import { cn, text } from '@/styles/theme'; -import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; -import { useCallback } from 'react'; import { PressableIcon } from '@/internal/components/PressableIcon'; -import { CloseSvg } from '@/internal/svg/closeSvg'; import { backArrowSvg } from '@/internal/svg/backArrowSvg'; -import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; +import { CloseSvg } from '@/internal/svg/closeSvg'; +import { cn, text } from '@/styles/theme'; +import { useCallback } from 'react'; +import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; +import { useSendContext } from './SendProvider'; export function SendHeader({ label = 'Send' }: { label?: string }) { const { setShowSend } = useWalletAdvancedContext(); diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index ab1eff5756..a8270df22b 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -1,4 +1,9 @@ import type { PortfolioTokenWithFiatValue } from '@/api/types'; +import { useExchangeRate } from '@/internal/hooks/useExchangeRate'; +import { useLifecycleStatus } from '@/internal/hooks/useLifecycleStatus'; +import { useTransferTransaction } from '@/internal/hooks/useTransferTransaction'; +import { useValue } from '@/internal/hooks/useValue'; +import type { Call } from '@/transaction/types'; import { createContext, useCallback, @@ -7,14 +12,9 @@ import { useState, } from 'react'; import type { Address } from 'viem'; -import { validateAddressInput } from '@/wallet/components/WalletAdvancedSend/validateAddressInput'; -import { useLifecycleStatus } from '@/internal/hooks/useLifecycleStatus'; -import { useValue } from '@/internal/hooks/useValue'; -import { useWalletContext } from '@/wallet/components/WalletProvider'; -import { useWalletAdvancedContext } from '@/wallet/components/WalletAdvancedProvider'; -import { useExchangeRate } from '@/internal/hooks/useExchangeRate'; -import type { Call } from '@/transaction/types'; -import { useTransferTransaction } from '@/internal/hooks/useTransferTransaction'; +import { validateAddressInput } from '../../../utils/validateAddressInput'; +import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; +import { useWalletContext } from '../../WalletProvider'; import type { LifecycleStatus, SendProviderReact } from '../types'; import type { SendContextType } from '../types'; diff --git a/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx index 8adaa64bc6..778a9fd915 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx @@ -1,6 +1,6 @@ -import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; import { TokenBalance } from '@/internal/components/TokenBalance'; import { border, cn, color, pressable, text } from '@/styles/theme'; +import { useSendContext } from './SendProvider'; export function SendTokenSelector() { const { @@ -16,7 +16,9 @@ export function SendTokenSelector() { if (!selectedToken) { return (
- Select a token + + Select a token +
{tokenBalances?.map((token) => ( Date: Tue, 28 Jan 2025 22:45:18 -0800 Subject: [PATCH 037/115] use common files --- src/api/buildTransferTransaction.ts | 39 ---------------- src/core/utils/truncateDecimalPlaces.ts | 20 --------- src/internal/hooks/useTransferTransaction.tsx | 44 ------------------- .../components/SendButton.tsx | 2 +- .../components/SendProvider.tsx | 14 +++--- .../components/WalletAdvancedSend/types.ts | 2 +- 6 files changed, 11 insertions(+), 110 deletions(-) delete mode 100644 src/api/buildTransferTransaction.ts delete mode 100644 src/core/utils/truncateDecimalPlaces.ts delete mode 100644 src/internal/hooks/useTransferTransaction.tsx diff --git a/src/api/buildTransferTransaction.ts b/src/api/buildTransferTransaction.ts deleted file mode 100644 index b9426a20f5..0000000000 --- a/src/api/buildTransferTransaction.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Call } from '@/transaction/types'; -import { type Address, encodeFunctionData, erc20Abi } from 'viem'; - -type BuildTransferTransactionParams = { - recipientAddress: Address; - tokenAddress: Address | null; - amount: bigint; -}; - -export function buildTransferTransaction({ - recipientAddress, - tokenAddress, - amount, -}: BuildTransferTransactionParams): Call[] { - // if no token address, we are sending native ETH - // and the data prop is empty - if (!tokenAddress) { - return [ - { - to: recipientAddress, - data: '0x', - value: amount, - }, - ]; - } - - const transferCallData = encodeFunctionData({ - abi: erc20Abi, - functionName: 'transfer', - args: [recipientAddress, amount], - }); - - return [ - { - to: tokenAddress, - data: transferCallData, - }, - ]; -} diff --git a/src/core/utils/truncateDecimalPlaces.ts b/src/core/utils/truncateDecimalPlaces.ts deleted file mode 100644 index 8bb03ab678..0000000000 --- a/src/core/utils/truncateDecimalPlaces.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Limit the value to N decimal places - */ -export const truncateDecimalPlaces = ( - value: string | number, - decimalPlaces: number, -) => { - const stringValue = String(value); - const decimalIndex = stringValue.indexOf('.'); - let resultValue = stringValue; - - if ( - decimalIndex !== -1 && - stringValue.length - decimalIndex - 1 > decimalPlaces - ) { - resultValue = stringValue.substring(0, decimalIndex + decimalPlaces + 1); - } - - return resultValue; -}; diff --git a/src/internal/hooks/useTransferTransaction.tsx b/src/internal/hooks/useTransferTransaction.tsx deleted file mode 100644 index 897a24bd75..0000000000 --- a/src/internal/hooks/useTransferTransaction.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { buildTransferTransaction } from '@/api/buildTransferTransaction'; -import type { Token } from '@/token'; -import type { Call } from '@/transaction/types'; -import { type Address, parseUnits } from 'viem'; - -type UseTransferTransactionParams = { - recipientAddress: Address; - token: Token | null; - amount: string; -}; - -export function useTransferTransaction({ - recipientAddress, - token, - amount, -}: UseTransferTransactionParams): { calls: Call[] } { - if (!token) { - return { calls: [] }; - } - - if (!token.address) { - if (token.symbol !== 'ETH') { - return { calls: [] }; - } - const parsedAmount = parseUnits(amount, token.decimals); - return { - calls: buildTransferTransaction({ - recipientAddress, - tokenAddress: null, - amount: parsedAmount, - }), - }; - } - - const parsedAmount = parseUnits(amount.toString(), token.decimals); - - return { - calls: buildTransferTransaction({ - recipientAddress, - tokenAddress: token.address, - amount: parsedAmount, - }), - }; -} diff --git a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx index 52df53bd51..1d7834aaf8 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx @@ -68,7 +68,7 @@ export function SendButton({ (false); // state for transaction data - const [callData, setCallData] = useState([]); + const [callData, setCallData] = useState(null); const [sendTransactionError, setSendTransactionError] = useState< string | null >(null); @@ -221,14 +221,18 @@ export function SendProvider({ children }: SendProviderReact) { } try { - setCallData([]); + setCallData(null); setSendTransactionError(null); - const { calls } = useTransferTransaction({ + const calls = useSendTransaction({ recipientAddress: selectedRecipientAddress, token: selectedToken, amount: cryptoAmount, }); - setCallData(calls); + if ('error' in calls) { + handleTransactionError(calls.error); + } else { + setCallData(calls); + } } catch (error) { handleTransactionError(error as string); } diff --git a/src/wallet/components/WalletAdvancedSend/types.ts b/src/wallet/components/WalletAdvancedSend/types.ts index fa1c881622..6ad9640a13 100644 --- a/src/wallet/components/WalletAdvancedSend/types.ts +++ b/src/wallet/components/WalletAdvancedSend/types.ts @@ -42,7 +42,7 @@ export type SendContextType = { handleCryptoAmountChange: (value: string) => void; // Transaction Context - callData: Call[]; + callData: Call | null; sendTransactionError: string | null; }; From 89992460b63098a25216ad7579db3739dd2ac464 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 11:10:11 -0800 Subject: [PATCH 038/115] subcomponents use props --- .../WalletAdvancedSend/components/Send.tsx | 67 +++++++++++++++++-- .../components/SendAddressInput.tsx | 67 +++++++++++++++---- .../components/SendAddressSelector.tsx | 17 +++-- .../components/SendAmountInput.tsx | 2 + .../components/SendButton.tsx | 19 ++++-- .../components/SendHeader.tsx | 2 + .../components/SendTokenSelector.tsx | 34 ++++++---- 7 files changed, 164 insertions(+), 44 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index 7d71151319..8d68fc51a2 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -61,9 +61,18 @@ function SendDefaultChildren() { if (!context.selectedRecipientAddress) { return ( <> - + {context.validatedRecipientAddress && ( - + )} ); @@ -72,18 +81,49 @@ function SendDefaultChildren() { if (!context.selectedToken) { return ( <> - - + + ); } return (
- + - - + +
); }, [ @@ -92,6 +132,19 @@ function SendDefaultChildren() { context.selectedRecipientAddress, context.selectedToken, context.validatedRecipientAddress, + context.tokenBalances, + context.setSelectedInputType, + context.handleTokenSelection, + context.handleResetTokenSelection, + context.handleCryptoAmountChange, + context.handleFiatAmountChange, + context.handleRecipientInputChange, + context.handleAddressSelection, + context.recipientInput, + context.senderChain, + context.cryptoAmount, + context.callData, + context.sendTransactionError, ]); return ( diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx index 0e597946e0..1a34423ce0 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx @@ -1,28 +1,42 @@ +'use client'; + +import { getName } from '@/identity'; import { getSlicedAddress } from '@/identity/utils/getSlicedAddress'; import { TextInput } from '@/internal/components/TextInput'; import { background, border, cn, color } from '@/styles/theme'; -import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import type { Address, Chain } from 'viem'; +import { base } from 'viem/chains'; type AddressInputProps = { + selectedRecipientAddress: Address | null; + recipientInput: string | null; + handleRecipientInputChange: (input: string) => void; + senderChain: Chain | null | undefined; className?: string; }; -export function SendAddressInput({ className }: AddressInputProps) { - const { - selectedRecipientAddress, - recipientInput, - handleRecipientInputChange, - } = useSendContext(); +export function SendAddressInput({ + selectedRecipientAddress, + recipientInput, + handleRecipientInputChange, + senderChain, + className, +}: AddressInputProps) { + const [inputValue, setInputValue] = useState(null); - const inputValue = selectedRecipientAddress - ? getSlicedAddress(selectedRecipientAddress) - : recipientInput; + useEffect(() => { + getInputValue(selectedRecipientAddress, recipientInput, senderChain) + .then(setInputValue) + .catch(console.error); + }, [selectedRecipientAddress, recipientInput, senderChain]); const handleFocus = useCallback( (e: React.FocusEvent) => { if (selectedRecipientAddress) { - handleRecipientInputChange(selectedRecipientAddress); + getInputValue(selectedRecipientAddress, recipientInput, senderChain) + .then((value) => handleRecipientInputChange(value ?? '')) + .catch(console.error); setTimeout(() => { // TODO: This is a hack to get the cursor to the end of the input, how to remove timeout? e.target.setSelectionRange( @@ -33,7 +47,12 @@ export function SendAddressInput({ className }: AddressInputProps) { }, 0); } }, - [selectedRecipientAddress, handleRecipientInputChange], + [ + selectedRecipientAddress, + handleRecipientInputChange, + recipientInput, + senderChain, + ], ); return ( @@ -47,7 +66,7 @@ export function SendAddressInput({ className }: AddressInputProps) { className, )} > - To + To ); } + +async function getInputValue( + selectedRecipientAddress: Address | null, + recipientInput: string | null, + senderChain: Chain | null | undefined, +) { + if (!selectedRecipientAddress) { + return recipientInput; + } + + const name = await getName({ + address: selectedRecipientAddress, + chain: senderChain ?? base, + }); + if (name) { + return name; + } + + return getSlicedAddress(selectedRecipientAddress); +} diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx index b47e0c529e..45a13bfebc 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx @@ -1,16 +1,21 @@ +'use client'; + import { Address, Avatar, Identity, Name } from '@/identity'; import { background, border, cn, pressable } from '@/styles/theme'; import { useCallback } from 'react'; -import type { Address as AddressType } from 'viem'; -import { useSendContext } from './SendProvider'; +import type { Address as AddressType, Chain } from 'viem'; type SendAddressSelectorProps = { address: AddressType; + senderChain: Chain | null | undefined; + handleAddressSelection: (address: AddressType) => void; }; -export function SendAddressSelector({ address }: SendAddressSelectorProps) { - const { senderChain, handleAddressSelection } = useSendContext(); - +export function SendAddressSelector({ + address, + senderChain, + handleAddressSelection, +}: SendAddressSelectorProps) { const handleClick = useCallback(() => { handleAddressSelection(address); }, [handleAddressSelection, address]); @@ -19,7 +24,7 @@ export function SendAddressSelector({ address }: SendAddressSelectorProps) { -
{amountLine}
-
- ); -} diff --git a/src/internal/components/AmountInput/CurrencyLabel.tsx b/src/internal/components/AmountInput/CurrencyLabel.tsx deleted file mode 100644 index eb8fdbd06a..0000000000 --- a/src/internal/components/AmountInput/CurrencyLabel.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { cn, color, text } from '@/styles/theme'; -import { forwardRef } from 'react'; - -type CurrencyLabelProps = { - label: string; - className?: string; -}; - -export const CurrencyLabel = forwardRef( - ({ label, className }, ref) => { - return ( - - {label} - - ); - }, -); diff --git a/src/internal/components/amount-input/AmountInput.tsx b/src/internal/components/amount-input/AmountInput.tsx index 2f40216d39..ec041b4c30 100644 --- a/src/internal/components/amount-input/AmountInput.tsx +++ b/src/internal/components/amount-input/AmountInput.tsx @@ -16,6 +16,7 @@ type AmountInputProps = { setCryptoAmount: (value: string) => void; exchangeRate: string; className?: string; + textClassName?: string; }; export function AmountInput({ @@ -24,10 +25,11 @@ export function AmountInput({ asset, selectedInputType, currency, - className, setFiatAmount, setCryptoAmount, exchangeRate, + className, + textClassName, }: AmountInputProps) { const containerRef = useRef(null); const inputRef = useRef(null); @@ -101,6 +103,7 @@ export function AmountInput({ '[appearance:textfield]', '[&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none', '[&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none', + textClassName, )} value={value} onChange={handleAmountChange} @@ -110,12 +113,17 @@ export function AmountInput({ placeholder="0" /> - +
{/* Hidden span for measuring text width Without this span the input field would not adjust its width based on the text width and would look like this: [0.12--------Empty Space-------][ETH] - As you can see the currency symbol is far away from the inputed value + With this span we can measure the width of the text in the input field and set the width of the input field to match the text width [0.12][ETH] - Now the currency symbol is displayed next to the input field */} @@ -128,6 +136,7 @@ export function AmountInput({ 'text-6xl leading-none outline-none', 'pointer-events-none absolute whitespace-nowrap opacity-0', 'left-[-9999px]', // Hide the span from the DOM + textClassName, )} > {value ? `${value}.` : '0.'} diff --git a/src/internal/components/amount-input/CurrencyLabel.tsx b/src/internal/components/amount-input/CurrencyLabel.tsx index fc5a228522..eb8fdbd06a 100644 --- a/src/internal/components/amount-input/CurrencyLabel.tsx +++ b/src/internal/components/amount-input/CurrencyLabel.tsx @@ -3,10 +3,11 @@ import { forwardRef } from 'react'; type CurrencyLabelProps = { label: string; + className?: string; }; export const CurrencyLabel = forwardRef( - ({ label }, ref) => { + ({ label, className }, ref) => { return ( ( color.disabled, 'flex items-center justify-center bg-transparent', 'text-6xl leading-none outline-none', + className, )} data-testid="ockCurrencySpan" > diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx index 3d8d0e1168..c69d0b8508 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx @@ -1,8 +1,8 @@ 'use client'; import type { PortfolioTokenWithFiatValue } from '@/api/types'; -import { AmountInput } from '@/internal/components/AmountInput/AmountInput'; -import { AmountInputTypeSwitch } from '@/internal/components/AmountInput/AmountInputTypeSwitch'; +import { AmountInput } from '@/internal/components/amount-input/AmountInput'; +import { AmountInputTypeSwitch } from '@/internal/components/amount-input/AmountInputTypeSwitch'; import { Skeleton } from '@/internal/components/Skeleton'; import { cn, color, text } from '@/styles/theme'; import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; @@ -35,11 +35,11 @@ export function SendAmountInput({ asset={selectedToken?.symbol ?? ''} currency={'USD'} selectedInputType={selectedInputType} - className={className} - textClassName={textClassName} setFiatAmount={handleFiatAmountChange} setCryptoAmount={handleCryptoAmountChange} exchangeRate={String(exchangeRate)} + className={className} + textClassName={textClassName} /> Date: Wed, 29 Jan 2025 12:25:25 -0800 Subject: [PATCH 043/115] remove send exportsc --- package.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/package.json b/package.json index ea0e27be58..aa6a89e914 100644 --- a/package.json +++ b/package.json @@ -170,12 +170,6 @@ "import": "./esm/nft/components/mint/index.js", "default": "./esm/nft/components/mint/index.js" }, - "./send": { - "types": "./esm/send/index.d.ts", - "module": "./esm/send/index.js", - "import": "./esm/send/index.js", - "default": "./esm/send/index.js" - }, "./swap": { "types": "./esm/swap/index.d.ts", "module": "./esm/swap/index.js", From f15c9b6a3c50c7c62fbc502c972447a9df3b93da Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 12:50:08 -0800 Subject: [PATCH 044/115] update token balance display --- src/internal/components/TokenBalance.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/internal/components/TokenBalance.tsx b/src/internal/components/TokenBalance.tsx index f15fcec109..341de63f00 100644 --- a/src/internal/components/TokenBalance.tsx +++ b/src/internal/components/TokenBalance.tsx @@ -32,21 +32,21 @@ export function TokenBalance({ const tokenContent = useMemo(() => { return ( -
-
- {showImage && } +
+
+ {showImage && }
{token.name?.trim()} - + {`${truncateDecimalPlaces( token.cryptoBalance / 10 ** token.decimals, 2, From 430e3d2a8818e71c18da07366c3134261bac1f9e Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 12:57:01 -0800 Subject: [PATCH 045/115] remove max width from token name --- src/internal/components/TokenBalance.tsx | 2 +- src/token/components/TokenRow.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/internal/components/TokenBalance.tsx b/src/internal/components/TokenBalance.tsx index 341de63f00..79c9050313 100644 --- a/src/internal/components/TokenBalance.tsx +++ b/src/internal/components/TokenBalance.tsx @@ -41,7 +41,7 @@ export function TokenBalance({ className={cn( text.headline, color.foreground, - 'max-w-52 overflow-hidden text-ellipsis whitespace-nowrap', + 'overflow-hidden text-ellipsis whitespace-nowrap', )} > {token.name?.trim()} diff --git a/src/token/components/TokenRow.tsx b/src/token/components/TokenRow.tsx index f56077da4e..d0e0670bf9 100644 --- a/src/token/components/TokenRow.tsx +++ b/src/token/components/TokenRow.tsx @@ -35,7 +35,7 @@ export const TokenRow = memo(function TokenRow({ {token.name.trim()} From 588a8a4baa1060308770e91e373ff0105808fdd4 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 13:27:05 -0800 Subject: [PATCH 046/115] fix use max --- .../nextjs-app-router/onchainkit/package.json | 6 ------ src/internal/components/TokenBalance.tsx | 19 ++++++++----------- .../components/SendProvider.tsx | 4 +++- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/playground/nextjs-app-router/onchainkit/package.json b/playground/nextjs-app-router/onchainkit/package.json index ea0e27be58..aa6a89e914 100644 --- a/playground/nextjs-app-router/onchainkit/package.json +++ b/playground/nextjs-app-router/onchainkit/package.json @@ -170,12 +170,6 @@ "import": "./esm/nft/components/mint/index.js", "default": "./esm/nft/components/mint/index.js" }, - "./send": { - "types": "./esm/send/index.d.ts", - "module": "./esm/send/index.js", - "import": "./esm/send/index.js", - "default": "./esm/send/index.js" - }, "./swap": { "types": "./esm/swap/index.d.ts", "module": "./esm/swap/index.js", diff --git a/src/internal/components/TokenBalance.tsx b/src/internal/components/TokenBalance.tsx index 79c9050313..07e80a2721 100644 --- a/src/internal/components/TokenBalance.tsx +++ b/src/internal/components/TokenBalance.tsx @@ -1,6 +1,6 @@ import type { PortfolioTokenWithFiatValue } from '@/api/types'; import { truncateDecimalPlaces } from '@/internal/utils/truncateDecimalPlaces'; -import { cn, color, text } from '@/styles/theme'; +import { border, cn, color, text } from '@/styles/theme'; import { TokenImage } from '@/token'; import { useMemo } from 'react'; @@ -55,26 +55,23 @@ export function TokenBalance({
{showAction ? ( - { e.stopPropagation(); onActionPress?.(); }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.stopPropagation(); - onActionPress?.(); - } - }} className={cn( text.label2, color.primary, - 'ml-auto p-0.5 font-bold transition-brightness hover:brightness-110', + border.radius, + 'ml-auto p-0.5 font-bold', + 'border border-transparent hover:border-[--ock-line-primary]', )} - aria-label={actionText} > {actionText} - + ) : ( { - setCryptoAmount(value); + const truncatedValue = truncateDecimalPlaces(value, 8); + setCryptoAmount(truncatedValue); updateLifecycleStatus({ statusName: 'amountChange', statusData: { From 67e9726ea5cbe5236a6e1224ca702ee420e62f9f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 13:29:20 -0800 Subject: [PATCH 047/115] hardcode input autocomplete props --- src/internal/components/TextInput.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/internal/components/TextInput.tsx b/src/internal/components/TextInput.tsx index 1d7d6b3bda..238ec03dd3 100644 --- a/src/internal/components/TextInput.tsx +++ b/src/internal/components/TextInput.tsx @@ -20,10 +20,6 @@ type TextInputReact = { setValue?: (s: string) => void; value: string; inputValidator?: (s: string) => boolean; - /** autocomplete attribute handles browser autocomplete, defaults to 'off' */ - autoComplete?: string; - /** data-1p-ignore attribute handles password manager autocomplete, defaults to true */ - 'data-1p-ignore'?: boolean; }; export const TextInput = forwardRef( @@ -41,8 +37,6 @@ export const TextInput = forwardRef( inputMode, value, inputValidator = () => true, - autoComplete = 'off', - 'data-1p-ignore': data1pIgnore = true, }, ref, ) => { @@ -80,8 +74,8 @@ export const TextInput = forwardRef( onChange={handleChange} onFocus={onFocus} disabled={disabled} - autoComplete={autoComplete} - data-1p-ignore={data1pIgnore} + autoComplete="off" // autocomplete attribute handles browser autocomplete + data-1p-ignore={true} // data-1p-ignore attribute handles password manager autocomplete /> ); }, From 7a253925bb7c33df84faf63d9c7830f53e3669c6 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 13:58:09 -0800 Subject: [PATCH 048/115] send on base and mainnet --- .../components/WalletAdvancedSend/components/Send.tsx | 4 +--- .../WalletAdvancedSend/components/SendButton.tsx | 10 ++++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index 15c075022f..cc12a1631d 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -62,7 +62,6 @@ function SendDefaultChildren() { return ( <> diff --git a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx index 674bc62bad..c6199d4391 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx @@ -13,11 +13,12 @@ import { } from '@/transaction'; import type { Call } from '@/transaction/types'; import { useMemo } from 'react'; -import { base } from 'viem/chains'; +import { type Chain, base } from 'viem/chains'; type SendButtonProps = { cryptoAmount: string | null; selectedToken: PortfolioTokenWithFiatValue | null; + senderChain?: Chain | null; callData: Call | null; sendTransactionError: string | null; label?: string; @@ -29,6 +30,7 @@ type SendButtonProps = { export function SendButton({ label = 'Continue', + senderChain, className, disabled, successOverride, @@ -40,9 +42,6 @@ export function SendButton({ sendTransactionError, }: SendButtonProps) { const isSponsored = false; - const chainId = base.id; - - const handleOnStatus = () => {}; const sendButtonLabel = useMemo(() => { if ( @@ -76,9 +75,8 @@ export function SendButton({ <> Date: Wed, 29 Jan 2025 14:39:48 -0800 Subject: [PATCH 049/115] fix button-within-button bug --- src/internal/components/TokenBalance.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/internal/components/TokenBalance.tsx b/src/internal/components/TokenBalance.tsx index 07e80a2721..df049eff48 100644 --- a/src/internal/components/TokenBalance.tsx +++ b/src/internal/components/TokenBalance.tsx @@ -55,13 +55,16 @@ export function TokenBalance({
{showAction ? ( - + ) : ( Date: Wed, 29 Jan 2025 14:39:59 -0800 Subject: [PATCH 050/115] implement succcessOverride --- .../components/SendButton.tsx | 139 ++++++++++++++---- 1 file changed, 111 insertions(+), 28 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx index c6199d4391..18633b011c 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx @@ -1,7 +1,7 @@ 'use client'; import type { PortfolioTokenWithFiatValue } from '@/api/types'; -import { cn, color, text } from '@/styles/theme'; +import { getChainExplorer } from '@/core/network/getChainExplorer'; import { Transaction, TransactionButton, @@ -11,9 +11,14 @@ import { TransactionStatusAction, TransactionStatusLabel, } from '@/transaction'; +import { useTransactionContext } from '@/transaction/components/TransactionProvider'; import type { Call } from '@/transaction/types'; -import { useMemo } from 'react'; +import { useWalletAdvancedContext } from '@/wallet/components/WalletAdvancedProvider'; +import { useWalletContext } from '@/wallet/components/WalletProvider'; +import { useCallback, useMemo } from 'react'; +import type { TransactionReceipt } from 'viem'; import { type Chain, base } from 'viem/chains'; +import { useChainId } from 'wagmi'; type SendButtonProps = { cryptoAmount: string | null; @@ -72,31 +77,109 @@ export function SendButton({ }, [cryptoAmount, disabled, selectedToken]); return ( - <> - - - {!sendTransactionError && } - - - - - - {sendTransactionError && ( -
- {sendTransactionError} -
- )} - + + + {!sendTransactionError && } + + + + + + ); +} + +/** + * SendTransactionButton required to be a nested component in order to pull from TransactionContext. + * Need to pull from TransactionContext in order to get transactionHash and transactionId. + * Need transactionHash and transactionId in order to determine where to open the transaction in the wallet or explorer. + */ +function SendTransactionButton({ + className, + senderChain, + pendingOverride, + errorOverride, + successOverride, + disabled, + label, +}: { + className?: string; + senderChain?: Chain | null; + pendingOverride?: TransactionButtonReact['pendingOverride']; + errorOverride?: TransactionButtonReact['errorOverride']; + successOverride?: TransactionButtonReact['successOverride']; + disabled?: boolean; + label: string; +}) { + const { address } = useWalletContext(); + const { setShowSend } = useWalletAdvancedContext(); + + const { transactionHash, transactionId } = useTransactionContext(); + + const accountChainId = senderChain?.id ?? useChainId(); + + const defaultSuccessHandler = useCallback( + (receipt: TransactionReceipt | undefined) => { + // SW will have txn id so open in wallet + if ( + receipt && + transactionId && + transactionHash && + senderChain?.id && + address + ) { + const url = new URL('https://wallet.coinbase.com/assets/transactions'); + url.searchParams.set('contentParams[txHash]', transactionHash); + url.searchParams.set( + 'contentParams[chainId]', + JSON.stringify(senderChain?.id), + ); + url.searchParams.set('contentParams[fromAddress]', address); + window.open(url, '_blank', 'noopener,noreferrer'); + } else { + // EOA will not have txn id so open in explorer + const chainExplorer = getChainExplorer(accountChainId); + window.open( + `${chainExplorer}/tx/${transactionHash}`, + '_blank', + 'noopener,noreferrer', + ); + } + setShowSend(false); + }, + [ + address, + senderChain, + transactionId, + transactionHash, + accountChainId, + setShowSend, + ], + ); + + const defaultSuccessOverride = { + onClick: defaultSuccessHandler, + }; + + return ( + ); } From 3ad97f7d2decdfab2c8af09ce6d6226f150945c4 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 15:22:23 -0800 Subject: [PATCH 051/115] fix lints --- .../WalletAdvancedSend/components/SendAmountInput.tsx | 4 ++-- .../WalletAdvancedSend/components/SendProvider.tsx | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx index c69d0b8508..e67ccfdde7 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAmountInput.tsx @@ -1,11 +1,11 @@ 'use client'; import type { PortfolioTokenWithFiatValue } from '@/api/types'; +import { Skeleton } from '@/internal/components/Skeleton'; import { AmountInput } from '@/internal/components/amount-input/AmountInput'; import { AmountInputTypeSwitch } from '@/internal/components/amount-input/AmountInputTypeSwitch'; -import { Skeleton } from '@/internal/components/Skeleton'; import { cn, color, text } from '@/styles/theme'; -import { useSendContext } from '@/wallet/components/WalletAdvancedSend/components/SendProvider'; +import { useSendContext } from './SendProvider'; export function SendAmountInput({ className, diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index b881d19c15..0de8993298 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -3,6 +3,7 @@ import { useExchangeRate } from '@/internal/hooks/useExchangeRate'; import { useLifecycleStatus } from '@/internal/hooks/useLifecycleStatus'; import { useSendTransaction } from '@/internal/hooks/useSendTransaction'; import { useValue } from '@/internal/hooks/useValue'; +import { truncateDecimalPlaces } from '@/internal/utils/truncateDecimalPlaces'; import type { Call } from '@/transaction/types'; import { createContext, @@ -15,9 +16,11 @@ import type { Address } from 'viem'; import { validateAddressInput } from '../../../utils/validateAddressInput'; import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; import { useWalletContext } from '../../WalletProvider'; -import type { LifecycleStatus, SendProviderReact } from '../types'; -import type { SendContextType } from '../types'; -import { truncateDecimalPlaces } from '@/internal/utils/truncateDecimalPlaces'; +import type { + LifecycleStatus, + SendContextType, + SendProviderReact, +} from '../types'; const emptyContext = {} as SendContextType; From 02aed37cc29a1bd2acb22032de77dc62caa0be84 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 16:05:02 -0800 Subject: [PATCH 052/115] implement lifecycle tracking --- .../WalletAdvancedSend/components/Send.tsx | 2 + .../components/SendButton.tsx | 39 +++++++++--------- .../components/SendProvider.tsx | 38 +++++++++-------- .../components/WalletAdvancedSend/types.ts | 41 +++++-------------- 4 files changed, 54 insertions(+), 66 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index cc12a1631d..04f1c4e406 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -124,6 +124,7 @@ function SendDefaultChildren() { senderChain={context.senderChain} callData={context.callData} sendTransactionError={context.sendTransactionError} + onStatus={context.updateLifecycleStatus} />
); @@ -147,6 +148,7 @@ function SendDefaultChildren() { context.cryptoAmount, context.callData, context.sendTransactionError, + context.updateLifecycleStatus, ]); return ( diff --git a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx index 18633b011c..56c69b0601 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx @@ -2,31 +2,30 @@ import type { PortfolioTokenWithFiatValue } from '@/api/types'; import { getChainExplorer } from '@/core/network/getChainExplorer'; -import { - Transaction, - TransactionButton, - type TransactionButtonReact, - TransactionSponsor, - TransactionStatus, - TransactionStatusAction, - TransactionStatusLabel, -} from '@/transaction'; +import { Transaction } from '@/transaction/components/Transaction'; +import { TransactionButton } from '@/transaction/components/TransactionButton'; +import { TransactionSponsor } from '@/transaction/components/TransactionSponsor'; +import { TransactionStatus } from '@/transaction/components/TransactionStatus'; +import { TransactionStatusAction } from '@/transaction/components/TransactionStatusAction'; +import { TransactionStatusLabel } from '@/transaction/components/TransactionStatusLabel'; import { useTransactionContext } from '@/transaction/components/TransactionProvider'; -import type { Call } from '@/transaction/types'; -import { useWalletAdvancedContext } from '@/wallet/components/WalletAdvancedProvider'; -import { useWalletContext } from '@/wallet/components/WalletProvider'; +import type { Call, TransactionButtonReact } from '@/transaction/types'; import { useCallback, useMemo } from 'react'; import type { TransactionReceipt } from 'viem'; import { type Chain, base } from 'viem/chains'; import { useChainId } from 'wagmi'; +import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; +import { useWalletContext } from '../../WalletProvider'; +import type { SendLifecycleStatus } from '../types'; type SendButtonProps = { + label?: string; + senderChain?: Chain | null; cryptoAmount: string | null; selectedToken: PortfolioTokenWithFiatValue | null; - senderChain?: Chain | null; callData: Call | null; sendTransactionError: string | null; - label?: string; + onStatus?: (status: SendLifecycleStatus) => void; className?: string; } & Pick< TransactionButtonReact, @@ -36,15 +35,16 @@ type SendButtonProps = { export function SendButton({ label = 'Continue', senderChain, - className, disabled, - successOverride, - pendingOverride, - errorOverride, - cryptoAmount, selectedToken, + cryptoAmount, callData, + onStatus, + pendingOverride, + successOverride, sendTransactionError, + errorOverride, + className, }: SendButtonProps) { const isSponsored = false; @@ -81,6 +81,7 @@ export function SendButton({ isSponsored={isSponsored} chainId={senderChain?.id ?? base.id} calls={callData ? [callData] : []} + onStatus={onStatus} > ({ + useLifecycleStatus({ statusName: 'init', statusData: { isMissingRequiredField: true, @@ -78,6 +78,12 @@ export function SendProvider({ children }: SendProviderReact) { setEthBalance( Number(ethBalance.cryptoBalance / 10 ** ethBalance.decimals), ); + updateLifecycleStatus({ + statusName: 'selectingAddress', + statusData: { + isMissingRequiredField: true, + }, + }); } else { updateLifecycleStatus({ statusName: 'fundingWallet', @@ -104,6 +110,19 @@ export function SendProvider({ children }: SendProviderReact) { validateRecipientInput(); }, [recipientInput]); + // fetch & set exchange rate + useEffect(() => { + if (!selectedToken) { + return; + } + useExchangeRate({ + token: selectedToken, + selectedInputType, + setExchangeRate, + setExchangeRateLoading, + }); + }, [selectedToken, selectedInputType]); + const handleRecipientInputChange = useCallback( (input: string) => { setRecipientInput(input); @@ -159,19 +178,6 @@ export function SendProvider({ children }: SendProviderReact) { }); }, [updateLifecycleStatus]); - // fetch & set exchange rate - useEffect(() => { - if (!selectedToken) { - return; - } - useExchangeRate({ - token: selectedToken, - selectedInputType, - setExchangeRate, - setExchangeRateLoading, - }); - }, [selectedToken, selectedInputType]); - const handleFiatAmountChange = useCallback( (value: string) => { setFiatAmount(value); @@ -211,7 +217,7 @@ export function SendProvider({ children }: SendProviderReact) { statusName: 'error', statusData: { error: 'Error building send transaction', - code: 'SmWBc01', // Send module SendButton component 01 error + code: 'SmSeBc01', // Send module SendButton component 01 error message: error, }, }); diff --git a/src/wallet/components/WalletAdvancedSend/types.ts b/src/wallet/components/WalletAdvancedSend/types.ts index 38825c1a94..f142dcfc04 100644 --- a/src/wallet/components/WalletAdvancedSend/types.ts +++ b/src/wallet/components/WalletAdvancedSend/types.ts @@ -1,7 +1,10 @@ import type { Dispatch, ReactNode, SetStateAction } from 'react'; -import type { Address, Chain, Hex, TransactionReceipt } from 'viem'; -import type { APIError, PortfolioTokenWithFiatValue } from '../../../api/types'; -import type { Call } from '../../../transaction/types'; +import type { Address, Chain } from 'viem'; +import type { PortfolioTokenWithFiatValue } from '../../../api/types'; +import type { + Call, + LifecycleStatus as TransactionLifecycleStatus, +} from '../../../transaction/types'; export type SendProviderReact = { children: ReactNode; @@ -10,8 +13,8 @@ export type SendProviderReact = { export type SendContextType = { // Lifecycle Status Context isInitialized: boolean; - lifecycleStatus: LifecycleStatus; - updateLifecycleStatus: (newStatus: LifecycleStatus) => void; + lifecycleStatus: SendLifecycleStatus; + updateLifecycleStatus: (newStatus: SendLifecycleStatus) => void; // Wallet Context senderAddress: Address | null | undefined; @@ -47,7 +50,7 @@ export type SendContextType = { sendTransactionError: string | null; }; -export type LifecycleStatus = +export type SendLifecycleStatus = | { statusName: 'init'; statusData: { @@ -79,28 +82,4 @@ export type LifecycleStatus = sufficientBalance: boolean; }; } - | { - statusName: 'transactionPending'; - statusData: { - isMissingRequiredField: false; - }; - } - | { - statusName: 'transactionApproved'; - statusData: { - isMissingRequiredField: false; - callsId?: Hex; - transactionHash?: Hex; - }; - } - | { - statusName: 'success'; - statusData: { - isMissingRequiredField: false; - transactionReceipt: TransactionReceipt; - }; - } - | { - statusName: 'error'; - statusData: APIError; - }; + | TransactionLifecycleStatus; From 930add18ff07e11ea2b2022815b1a296c549ad63 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 16:05:27 -0800 Subject: [PATCH 053/115] update types to conform to standard useLifecycleStatus non-null expectation --- src/transaction/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/transaction/types.ts b/src/transaction/types.ts index 9bcdb7280a..177a3bd795 100644 --- a/src/transaction/types.ts +++ b/src/transaction/types.ts @@ -27,7 +27,7 @@ type TransactionButtonOverride = { export type LifecycleStatus = | { statusName: 'init'; - statusData: null; + statusData: Record; } | { statusName: 'error'; @@ -35,15 +35,15 @@ export type LifecycleStatus = } | { statusName: 'transactionIdle'; // initial status prior to the mutation function executing - statusData: null; + statusData: Record; } | { statusName: 'buildingTransaction'; - statusData: null; + statusData: Record; } | { statusName: 'transactionPending'; // if the mutation is currently executing - statusData: null; + statusData: Record; } | { statusName: 'transactionLegacyExecuted'; From 66cd24800b59f93861cfb6c58f5a722e4ae6b232 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 16:07:22 -0800 Subject: [PATCH 054/115] fix lints --- .../components/WalletAdvancedSend/components/SendButton.tsx | 2 +- .../components/WalletAdvancedSend/components/SendProvider.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx index 56c69b0601..6b02bd6d11 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx @@ -4,11 +4,11 @@ import type { PortfolioTokenWithFiatValue } from '@/api/types'; import { getChainExplorer } from '@/core/network/getChainExplorer'; import { Transaction } from '@/transaction/components/Transaction'; import { TransactionButton } from '@/transaction/components/TransactionButton'; +import { useTransactionContext } from '@/transaction/components/TransactionProvider'; import { TransactionSponsor } from '@/transaction/components/TransactionSponsor'; import { TransactionStatus } from '@/transaction/components/TransactionStatus'; import { TransactionStatusAction } from '@/transaction/components/TransactionStatusAction'; import { TransactionStatusLabel } from '@/transaction/components/TransactionStatusLabel'; -import { useTransactionContext } from '@/transaction/components/TransactionProvider'; import type { Call, TransactionButtonReact } from '@/transaction/types'; import { useCallback, useMemo } from 'react'; import type { TransactionReceipt } from 'viem'; diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index 5eb554c348..bc6d2428ab 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -17,8 +17,8 @@ import { validateAddressInput } from '../../../utils/validateAddressInput'; import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; import { useWalletContext } from '../../WalletProvider'; import type { - SendLifecycleStatus, SendContextType, + SendLifecycleStatus, SendProviderReact, } from '../types'; From c1061d02719a9673b94c0e99d58feeac9321d4a1 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 19:14:54 -0800 Subject: [PATCH 055/115] fix playground package.json --- playground/nextjs-app-router/onchainkit/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/playground/nextjs-app-router/onchainkit/package.json b/playground/nextjs-app-router/onchainkit/package.json index aa6a89e914..876438b38a 100644 --- a/playground/nextjs-app-router/onchainkit/package.json +++ b/playground/nextjs-app-router/onchainkit/package.json @@ -1,11 +1,11 @@ { "name": "@coinbase/onchainkit", - "version": "0.36.9", + "version": "0.36.8", "type": "module", "repository": "https://github.com/coinbase/onchainkit.git", "license": "MIT", "scripts": { - "build": "packemon build --addEngines --addFiles --loadConfigs --declaration && tscpaths -p tsconfig.esm.json -s ./src -o ./esm && npx packemon validate --no-license --no-people --no-repo && tailwindcss -i ./src/styles/index.css -o ./src/tailwind.css --minify && tailwindcss -i ./src/styles/index-with-tailwind.css -o ./src/styles.css --minify", + "build": "packemon build --addEngines --addFiles --loadConfigs --declaration && npx packemon validate --no-license --no-people --no-repo && tailwindcss -i ./src/styles/index.css -o ./src/tailwind.css --minify && tailwindcss -i ./src/styles/index-with-tailwind.css -o ./src/styles.css --minify", "check": "biome check --write .", "check:unsafe": "biome check . --fix --unsafe", "ci:check": "biome ci --formatter-enabled=false --linter-enabled=false", @@ -77,7 +77,6 @@ "rimraf": "^5.0.5", "storybook": "^8.2.9", "tailwindcss": "^3.4.3", - "tscpaths": "^0.0.9", "tsup": "^8.3.5", "typescript": "~5.3.3", "vite": "^5.3.3", From adbbe2ec2b57b099566620d5de5a73b8907e685a Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 20:42:42 -0800 Subject: [PATCH 056/115] remove error export, improve number transforms --- .../components/SendProvider.tsx | 20 +++++++++---------- .../components/WalletAdvancedSend/types.ts | 1 - 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index bc6d2428ab..4c36bf50cb 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -12,7 +12,7 @@ import { useEffect, useState, } from 'react'; -import type { Address } from 'viem'; +import { formatUnits, type Address } from 'viem'; import { validateAddressInput } from '../../../utils/validateAddressInput'; import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; import { useWalletContext } from '../../WalletProvider'; @@ -57,9 +57,6 @@ export function SendProvider({ children }: SendProviderReact) { // state for transaction data const [callData, setCallData] = useState(null); - const [sendTransactionError, setSendTransactionError] = useState< - string | null - >(null); // data and utils from hooks const [lifecycleStatus, updateLifecycleStatus] = @@ -76,7 +73,9 @@ export function SendProvider({ children }: SendProviderReact) { const ethBalance = tokenBalances?.find((token) => token.address === ''); if (ethBalance && ethBalance.cryptoBalance > 0) { setEthBalance( - Number(ethBalance.cryptoBalance / 10 ** ethBalance.decimals), + Number( + formatUnits(BigInt(ethBalance.cryptoBalance), ethBalance.decimals), + ), ); updateLifecycleStatus({ statusName: 'selectingAddress', @@ -203,8 +202,12 @@ export function SendProvider({ children }: SendProviderReact) { isMissingRequiredField: true, sufficientBalance: Number(value) <= - Number(selectedToken?.cryptoBalance) / - 10 ** Number(selectedToken?.decimals), + Number( + formatUnits( + BigInt(selectedToken?.cryptoBalance ?? 0), + selectedToken?.decimals ?? 0, + ), + ), }, }); }, @@ -221,7 +224,6 @@ export function SendProvider({ children }: SendProviderReact) { message: error, }, }); - setSendTransactionError(error); }, [updateLifecycleStatus], ); @@ -233,7 +235,6 @@ export function SendProvider({ children }: SendProviderReact) { try { setCallData(null); - setSendTransactionError(null); const calls = useSendTransaction({ recipientAddress: selectedRecipientAddress, token: selectedToken, @@ -284,7 +285,6 @@ export function SendProvider({ children }: SendProviderReact) { selectedInputType, setSelectedInputType, callData, - sendTransactionError, }); return {children}; diff --git a/src/wallet/components/WalletAdvancedSend/types.ts b/src/wallet/components/WalletAdvancedSend/types.ts index f142dcfc04..c9c5b3be46 100644 --- a/src/wallet/components/WalletAdvancedSend/types.ts +++ b/src/wallet/components/WalletAdvancedSend/types.ts @@ -47,7 +47,6 @@ export type SendContextType = { // Transaction Context callData: Call | null; - sendTransactionError: string | null; }; export type SendLifecycleStatus = From 240a3f7d6b9fb74aaa1cebe0fb8b58a84bbdb826 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 20:42:58 -0800 Subject: [PATCH 057/115] improve number transforms --- src/wallet/components/WalletAdvancedTokenHoldings.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/wallet/components/WalletAdvancedTokenHoldings.tsx b/src/wallet/components/WalletAdvancedTokenHoldings.tsx index 8d84bd98a0..ad5282e1af 100644 --- a/src/wallet/components/WalletAdvancedTokenHoldings.tsx +++ b/src/wallet/components/WalletAdvancedTokenHoldings.tsx @@ -2,6 +2,7 @@ import { cn, color, text } from '@/styles/theme'; import { type Token, TokenImage } from '@/token'; +import { formatUnits } from 'viem'; import { useWalletAdvancedContext } from './WalletAdvancedProvider'; type TokenDetailsProps = { @@ -44,10 +45,12 @@ export function WalletAdvancedTokenHoldings() { name: tokenBalance.name, symbol: tokenBalance.symbol, }} - balance={ - Number(tokenBalance.cryptoBalance) / - 10 ** Number(tokenBalance.decimals) - } + balance={Number( + formatUnits( + BigInt(tokenBalance.cryptoBalance), + tokenBalance.decimals, + ), + )} valueInFiat={Number(tokenBalance.fiatBalance)} /> ))} From 84d73d1a132e93704d58a79b46dacc58b0807b30 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 20:43:24 -0800 Subject: [PATCH 058/115] improved number transforms --- .../WalletAdvancedSend/components/SendTokenSelector.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx index 5996750fa3..8b71732ea4 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendTokenSelector.tsx @@ -4,6 +4,7 @@ import type { PortfolioTokenWithFiatValue } from '@/api/types'; import { TokenBalance } from '@/internal/components/TokenBalance'; import { border, cn, color, pressable, text } from '@/styles/theme'; import type { Dispatch, SetStateAction } from 'react'; +import { formatUnits } from 'viem'; type SendTokenSelectorProps = { selectedToken: PortfolioTokenWithFiatValue | null; @@ -56,8 +57,10 @@ export function SendTokenSelector({ handleFiatAmountChange(String(selectedToken.fiatBalance)); handleCryptoAmountChange( String( - Number(selectedToken.cryptoBalance) / - 10 ** Number(selectedToken.decimals), + formatUnits( + BigInt(selectedToken.cryptoBalance), + selectedToken.decimals, + ), ), ); }} From 640615afa68d7149c7b0259f3b72692e7bd89935 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 20:43:42 -0800 Subject: [PATCH 059/115] improved number transforms --- src/internal/components/TokenBalance.tsx | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/internal/components/TokenBalance.tsx b/src/internal/components/TokenBalance.tsx index df049eff48..bcb5c95954 100644 --- a/src/internal/components/TokenBalance.tsx +++ b/src/internal/components/TokenBalance.tsx @@ -1,8 +1,10 @@ import type { PortfolioTokenWithFiatValue } from '@/api/types'; +import { formatFiatAmount } from '@/internal/utils/formatFiatAmount'; import { truncateDecimalPlaces } from '@/internal/utils/truncateDecimalPlaces'; import { border, cn, color, text } from '@/styles/theme'; import { TokenImage } from '@/token'; import { useMemo } from 'react'; +import { formatUnits } from 'viem'; type TokenBalanceProps = { token: PortfolioTokenWithFiatValue; @@ -25,10 +27,15 @@ export function TokenBalance({ onActionPress, className, }: TokenBalanceProps) { - const formattedValueInFiat = new Intl.NumberFormat('en-US', { - style: 'currency', + const formattedFiatValue = formatFiatAmount({ + amount: token.fiatBalance, currency: 'USD', - }).format(token.fiatBalance); + }); + + const formattedCryptoValue = truncateDecimalPlaces( + formatUnits(BigInt(token.cryptoBalance), token.decimals), + 3, + ); const tokenContent = useMemo(() => { return ( @@ -47,10 +54,7 @@ export function TokenBalance({ {token.name?.trim()} - {`${truncateDecimalPlaces( - token.cryptoBalance / 10 ** token.decimals, - 2, - )} ${token.symbol} ${subtitle}`} + {`${formattedCryptoValue} ${token.symbol} ${subtitle}`}
@@ -83,7 +87,7 @@ export function TokenBalance({ 'whitespace-nowrap', )} > - {formattedValueInFiat} + {formattedFiatValue} )}
@@ -96,7 +100,8 @@ export function TokenBalance({ showAction, actionText, onActionPress, - formattedValueInFiat, + formattedFiatValue, + formattedCryptoValue, ]); if (onClick) { From 4f5452e19f294b1fc2a77e960ff02e6920b5c737 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 20:44:15 -0800 Subject: [PATCH 060/115] utils for readability and reuse --- .../WalletAdvancedSend/components/Send.tsx | 23 +++- .../components/SendButton.tsx | 114 +++--------------- .../utils/defaultSendTxSuccessHandler.ts | 50 ++++++++ .../utils/validateAmountInput.ts | 46 +++++++ 4 files changed, 137 insertions(+), 96 deletions(-) create mode 100644 src/wallet/components/WalletAdvancedSend/utils/defaultSendTxSuccessHandler.ts create mode 100644 src/wallet/components/WalletAdvancedSend/utils/validateAmountInput.ts diff --git a/src/wallet/components/WalletAdvancedSend/components/Send.tsx b/src/wallet/components/WalletAdvancedSend/components/Send.tsx index 04f1c4e406..7c70a15ef2 100644 --- a/src/wallet/components/WalletAdvancedSend/components/Send.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/Send.tsx @@ -9,6 +9,8 @@ import { SendFundWallet } from './SendFundWallet'; import { SendHeader } from './SendHeader'; import { SendProvider, useSendContext } from './SendProvider'; import { SendTokenSelector } from './SendTokenSelector'; +import { validateAmountInput } from '../utils/validateAmountInput'; +import { parseUnits } from 'viem'; type SendReact = { children?: ReactNode; @@ -44,6 +46,21 @@ export function Send({ function SendDefaultChildren() { const context = useSendContext(); + const disableSendButton = !validateAmountInput({ + cryptoAmount: context.cryptoAmount ?? '', + selectedToken: context.selectedToken ?? undefined, + }); + const buttonLabel = useMemo(() => { + if ( + parseUnits( + context.cryptoAmount ?? '', + context.selectedToken?.decimals ?? 0, + ) > (context.selectedToken?.cryptoBalance ?? 0n) + ) { + return 'Insufficient balance'; + } + return 'Continue'; + }, [context.cryptoAmount, context.selectedToken]); console.log({ context, @@ -123,8 +140,9 @@ function SendDefaultChildren() { selectedToken={context.selectedToken} senderChain={context.senderChain} callData={context.callData} - sendTransactionError={context.sendTransactionError} onStatus={context.updateLifecycleStatus} + disabled={disableSendButton} + label={buttonLabel} />
); @@ -147,8 +165,9 @@ function SendDefaultChildren() { context.senderChain, context.cryptoAmount, context.callData, - context.sendTransactionError, context.updateLifecycleStatus, + disableSendButton, + buttonLabel, ]); return ( diff --git a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx index 6b02bd6d11..457dc15c1d 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendButton.tsx @@ -1,30 +1,26 @@ 'use client'; import type { PortfolioTokenWithFiatValue } from '@/api/types'; -import { getChainExplorer } from '@/core/network/getChainExplorer'; import { Transaction } from '@/transaction/components/Transaction'; import { TransactionButton } from '@/transaction/components/TransactionButton'; import { useTransactionContext } from '@/transaction/components/TransactionProvider'; -import { TransactionSponsor } from '@/transaction/components/TransactionSponsor'; import { TransactionStatus } from '@/transaction/components/TransactionStatus'; import { TransactionStatusAction } from '@/transaction/components/TransactionStatusAction'; import { TransactionStatusLabel } from '@/transaction/components/TransactionStatusLabel'; import type { Call, TransactionButtonReact } from '@/transaction/types'; -import { useCallback, useMemo } from 'react'; -import type { TransactionReceipt } from 'viem'; import { type Chain, base } from 'viem/chains'; -import { useChainId } from 'wagmi'; import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; import { useWalletContext } from '../../WalletProvider'; import type { SendLifecycleStatus } from '../types'; +import { defaultSendTxSuccessHandler } from '@/wallet/components/WalletAdvancedSend/utils/defaultSendTxSuccessHandler'; type SendButtonProps = { label?: string; senderChain?: Chain | null; cryptoAmount: string | null; selectedToken: PortfolioTokenWithFiatValue | null; + isSponsored?: boolean; callData: Call | null; - sendTransactionError: string | null; onStatus?: (status: SendLifecycleStatus) => void; className?: string; } & Pick< @@ -36,46 +32,14 @@ export function SendButton({ label = 'Continue', senderChain, disabled, - selectedToken, - cryptoAmount, + isSponsored = false, callData, onStatus, pendingOverride, successOverride, - sendTransactionError, errorOverride, className, }: SendButtonProps) { - const isSponsored = false; - - const sendButtonLabel = useMemo(() => { - if ( - Number(cryptoAmount) > - Number(selectedToken?.cryptoBalance) / - 10 ** Number(selectedToken?.decimals) - ) { - return 'Insufficient balance'; - } - return label; - }, [cryptoAmount, label, selectedToken]); - - const isDisabled = useMemo(() => { - if (disabled) { - return true; - } - if (Number(cryptoAmount) <= 0) { - return true; - } - if ( - Number(cryptoAmount) > - Number(selectedToken?.cryptoBalance) / - 10 ** Number(selectedToken?.decimals) - ) { - return true; - } - return false; - }, [cryptoAmount, disabled, selectedToken]); - return ( - {!sendTransactionError && } @@ -107,70 +70,33 @@ export function SendButton({ * Need transactionHash and transactionId in order to determine where to open the transaction in the wallet or explorer. */ function SendTransactionButton({ - className, + label, senderChain, + disabled, pendingOverride, - errorOverride, successOverride, - disabled, - label, + errorOverride, + className, }: { - className?: string; + label: string; senderChain?: Chain | null; + disabled?: boolean; pendingOverride?: TransactionButtonReact['pendingOverride']; - errorOverride?: TransactionButtonReact['errorOverride']; successOverride?: TransactionButtonReact['successOverride']; - disabled?: boolean; - label: string; + errorOverride?: TransactionButtonReact['errorOverride']; + className?: string; }) { const { address } = useWalletContext(); const { setShowSend } = useWalletAdvancedContext(); - const { transactionHash, transactionId } = useTransactionContext(); - - const accountChainId = senderChain?.id ?? useChainId(); - - const defaultSuccessHandler = useCallback( - (receipt: TransactionReceipt | undefined) => { - // SW will have txn id so open in wallet - if ( - receipt && - transactionId && - transactionHash && - senderChain?.id && - address - ) { - const url = new URL('https://wallet.coinbase.com/assets/transactions'); - url.searchParams.set('contentParams[txHash]', transactionHash); - url.searchParams.set( - 'contentParams[chainId]', - JSON.stringify(senderChain?.id), - ); - url.searchParams.set('contentParams[fromAddress]', address); - window.open(url, '_blank', 'noopener,noreferrer'); - } else { - // EOA will not have txn id so open in explorer - const chainExplorer = getChainExplorer(accountChainId); - window.open( - `${chainExplorer}/tx/${transactionHash}`, - '_blank', - 'noopener,noreferrer', - ); - } - setShowSend(false); - }, - [ - address, - senderChain, + const defaultSuccessOverride = { + onClick: defaultSendTxSuccessHandler({ transactionId, transactionHash, - accountChainId, - setShowSend, - ], - ); - - const defaultSuccessOverride = { - onClick: defaultSuccessHandler, + senderChain: senderChain ?? undefined, + address: address ?? undefined, + onComplete: () => setShowSend(false), + }), }; return ( diff --git a/src/wallet/components/WalletAdvancedSend/utils/defaultSendTxSuccessHandler.ts b/src/wallet/components/WalletAdvancedSend/utils/defaultSendTxSuccessHandler.ts new file mode 100644 index 0000000000..73a168e5b1 --- /dev/null +++ b/src/wallet/components/WalletAdvancedSend/utils/defaultSendTxSuccessHandler.ts @@ -0,0 +1,50 @@ +import { getChainExplorer } from '@/core/network/getChainExplorer'; +import type { Address, Chain, TransactionReceipt } from 'viem'; +import { useChainId } from 'wagmi'; + +export function defaultSendTxSuccessHandler({ + transactionId, + transactionHash, + senderChain, + address, + onComplete, +}: { + transactionId: string | undefined; + transactionHash: string | undefined; + senderChain: Chain | undefined; + address: Address | undefined; + onComplete?: () => void; +}) { + return (receipt: TransactionReceipt | undefined) => { + const accountChainId = senderChain?.id ?? useChainId(); + + // SW will have txn id so open in wallet + if ( + receipt && + transactionId && + transactionHash && + senderChain?.id && + address + ) { + const url = new URL('https://wallet.coinbase.com/assets/transactions'); + url.searchParams.set('contentParams[txHash]', transactionHash); + url.searchParams.set( + 'contentParams[chainId]', + JSON.stringify(senderChain?.id), + ); + url.searchParams.set('contentParams[fromAddress]', address); + window.open(url, '_blank', 'noopener,noreferrer'); + } else { + // EOA will not have txn id so open in explorer + const chainExplorer = getChainExplorer(accountChainId); + window.open( + `${chainExplorer}/tx/${transactionHash}`, + '_blank', + 'noopener,noreferrer', + ); + } + + // After opening the transaction in the wallet or explorer, close the send modal + onComplete?.(); + }; +} diff --git a/src/wallet/components/WalletAdvancedSend/utils/validateAmountInput.ts b/src/wallet/components/WalletAdvancedSend/utils/validateAmountInput.ts new file mode 100644 index 0000000000..174c2012ac --- /dev/null +++ b/src/wallet/components/WalletAdvancedSend/utils/validateAmountInput.ts @@ -0,0 +1,46 @@ +import type { PortfolioTokenWithFiatValue } from '@/api/types'; +import { parseUnits } from 'viem'; + +// const parsedCryptoAmount = parseUnits( +// cryptoAmount ?? '', +// selectedToken?.decimals ?? 0, +// ); +// const parsedCryptoBalance = parseUnits( +// String(selectedToken?.cryptoBalance) ?? '', +// selectedToken?.decimals ?? 0, +// ); + +// const sendButtonLabel = useMemo(() => { +// if (parsedCryptoAmount > parsedCryptoBalance) { +// return 'Insufficient balance'; +// } +// return label; +// }, [parsedCryptoAmount, parsedCryptoBalance, label]); + +// const isDisabled = useMemo(() => { +// if (disabled) { +// return true; +// } +// if (parsedCryptoAmount <= 0n || parsedCryptoAmount > parsedCryptoBalance) { +// return true; +// } +// return false; +// }, [parsedCryptoAmount, parsedCryptoBalance, disabled]); + +export function validateAmountInput({ + cryptoAmount, + selectedToken, +}: { + cryptoAmount?: string; + selectedToken?: PortfolioTokenWithFiatValue; +}) { + if (!cryptoAmount || !selectedToken) { + return false; + } + + const parsedCryptoAmount = parseUnits(cryptoAmount, selectedToken.decimals); + + return ( + parsedCryptoAmount > 0n && parsedCryptoAmount <= selectedToken.cryptoBalance + ); +} From 6be3572e1c1177fdeafa2689ea5c34e94969b0c9 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 21:14:55 -0800 Subject: [PATCH 061/115] remove identity wrapper --- .../components/SendAddressSelector.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx index 45a13bfebc..144add067a 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Address, Avatar, Identity, Name } from '@/identity'; +import { Address, Avatar, Name } from '@/identity'; import { background, border, cn, pressable } from '@/styles/theme'; import { useCallback } from 'react'; import type { Address as AddressType, Chain } from 'viem'; @@ -22,10 +22,7 @@ export function SendAddressSelector({ return (
); } From 4358eaaf627856fbd096021e927b2a60ddbb4d69 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 22:15:34 -0800 Subject: [PATCH 062/115] utility for resolving the displayed address/name --- .../utils/resolveAddressInput.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/wallet/components/WalletAdvancedSend/utils/resolveAddressInput.ts diff --git a/src/wallet/components/WalletAdvancedSend/utils/resolveAddressInput.ts b/src/wallet/components/WalletAdvancedSend/utils/resolveAddressInput.ts new file mode 100644 index 0000000000..bb18b26b5e --- /dev/null +++ b/src/wallet/components/WalletAdvancedSend/utils/resolveAddressInput.ts @@ -0,0 +1,37 @@ +import { getName, isBasename } from '@/identity'; +import { getSlicedAddress } from '@/identity/utils/getSlicedAddress'; +import { type Address, isAddress } from 'viem'; +import { base, mainnet } from 'viem/chains'; + +export async function resolveAddressInput( + selectedRecipientAddress: Address | null, + recipientInput: string | null, +) { + // if there is no user input, return null + if (!recipientInput) { + return null; + } + + // if the user hasn't selected an address yet, just return their input + if (!selectedRecipientAddress) { + return recipientInput; + } + + // we now have a selected address + // so if the user's input is address-format, then return the sliced address + if (isAddress(recipientInput)) { + return getSlicedAddress(selectedRecipientAddress); + } + + // if the user's input wasn't address-format, then try to get and return the name + const name = await getName({ + address: selectedRecipientAddress, + chain: isBasename(recipientInput) ? base : mainnet, + }); + if (name) { + return name; + } + + // as a last resort, just return the user's input + return recipientInput; +} From ca305b2745ab89ab7632d9d0ff549be54787da65 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 22:16:37 -0800 Subject: [PATCH 063/115] use utility --- .../components/SendAddressInput.tsx | 31 ++----------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx index a3231e4e32..877e9f7457 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAddressInput.tsx @@ -1,7 +1,5 @@ 'use client'; -import { getName, isBasename } from '@/identity'; -import { getSlicedAddress } from '@/identity/utils/getSlicedAddress'; import { TextInput } from '@/internal/components/TextInput'; import { background, border, cn, color } from '@/styles/theme'; import { @@ -11,7 +9,7 @@ import { useEffect, } from 'react'; import type { Address } from 'viem'; -import { base, mainnet } from 'viem/chains'; +import { resolveAddressInput } from '../utils/resolveAddressInput'; type AddressInputProps = { selectedRecipientAddress: Address | null; @@ -29,14 +27,14 @@ export function SendAddressInput({ className, }: AddressInputProps) { useEffect(() => { - resolveInputDisplay(selectedRecipientAddress, recipientInput) + resolveAddressInput(selectedRecipientAddress, recipientInput) .then(setRecipientInput) .catch(console.error); }, [selectedRecipientAddress, recipientInput, setRecipientInput]); const handleFocus = useCallback(() => { if (selectedRecipientAddress) { - resolveInputDisplay(selectedRecipientAddress, recipientInput) + resolveAddressInput(selectedRecipientAddress, recipientInput) .then((value) => handleRecipientInputChange(value ?? '')) .catch(console.error); } @@ -66,26 +64,3 @@ export function SendAddressInput({
); } - -async function resolveInputDisplay( - selectedRecipientAddress: Address | null, - recipientInput: string | null, -) { - if (!recipientInput) { - return null; - } - - if (!selectedRecipientAddress) { - return recipientInput; - } - - const name = await getName({ - address: selectedRecipientAddress, - chain: isBasename(recipientInput) ? base : mainnet, - }); - if (name) { - return name; - } - - return getSlicedAddress(selectedRecipientAddress); -} From be0826e8fb5759f4755d43f522ed814df23cfb03 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 29 Jan 2025 22:16:55 -0800 Subject: [PATCH 064/115] revert type change --- src/transaction/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/transaction/types.ts b/src/transaction/types.ts index 177a3bd795..9bcdb7280a 100644 --- a/src/transaction/types.ts +++ b/src/transaction/types.ts @@ -27,7 +27,7 @@ type TransactionButtonOverride = { export type LifecycleStatus = | { statusName: 'init'; - statusData: Record; + statusData: null; } | { statusName: 'error'; @@ -35,15 +35,15 @@ export type LifecycleStatus = } | { statusName: 'transactionIdle'; // initial status prior to the mutation function executing - statusData: Record; + statusData: null; } | { statusName: 'buildingTransaction'; - statusData: Record; + statusData: null; } | { statusName: 'transactionPending'; // if the mutation is currently executing - statusData: Record; + statusData: null; } | { statusName: 'transactionLegacyExecuted'; From 1bc72ffa17dd168231513736bdb1b2a406714f89 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 11:29:20 -0800 Subject: [PATCH 065/115] update lifecycle types --- .../components/WalletAdvancedSend/types.ts | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/types.ts b/src/wallet/components/WalletAdvancedSend/types.ts index c9c5b3be46..33314e40f8 100644 --- a/src/wallet/components/WalletAdvancedSend/types.ts +++ b/src/wallet/components/WalletAdvancedSend/types.ts @@ -1,10 +1,8 @@ import type { Dispatch, ReactNode, SetStateAction } from 'react'; -import type { Address, Chain } from 'viem'; -import type { PortfolioTokenWithFiatValue } from '../../../api/types'; -import type { - Call, - LifecycleStatus as TransactionLifecycleStatus, -} from '../../../transaction/types'; +import type { Address, Chain, TransactionReceipt } from 'viem'; +import type { APIError, PortfolioTokenWithFiatValue } from '../../../api/types'; +import type { Call } from '../../../transaction/types'; +import type { LifecycleStatusUpdate } from '@/internal/types'; export type SendProviderReact = { children: ReactNode; @@ -14,7 +12,9 @@ export type SendContextType = { // Lifecycle Status Context isInitialized: boolean; lifecycleStatus: SendLifecycleStatus; - updateLifecycleStatus: (newStatus: SendLifecycleStatus) => void; + updateLifecycleStatus: ( + status: LifecycleStatusUpdate, + ) => void; // Wallet Context senderAddress: Address | null | undefined; @@ -81,4 +81,23 @@ export type SendLifecycleStatus = sufficientBalance: boolean; }; } - | TransactionLifecycleStatus; + | { + statusName: 'transactionPending'; // if the mutation is currently executing + statusData: null; + } + | { + statusName: 'transactionLegacyExecuted'; + statusData: { + transactionHashList: Address[]; + }; + } + | { + statusName: 'success'; // if the last mutation attempt was successful + statusData: { + transactionReceipts: TransactionReceipt[]; + }; + } + | { + statusName: 'error'; + statusData: APIError; + }; From 00f920d9788f4cc7d2367154447d279e24caaebd Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 14:20:57 -0800 Subject: [PATCH 066/115] update and create utils --- .../utils/resolveAddressInput.ts | 43 +++++++++++++------ .../utils/validateAddressInput.test.ts | 0 .../utils/validateAddressInput.ts | 13 ++++-- .../utils/validateAmountInput.ts | 26 ----------- 4 files changed, 41 insertions(+), 41 deletions(-) rename src/wallet/{ => components/WalletAdvancedSend}/utils/validateAddressInput.test.ts (100%) rename src/wallet/{ => components/WalletAdvancedSend}/utils/validateAddressInput.ts (62%) diff --git a/src/wallet/components/WalletAdvancedSend/utils/resolveAddressInput.ts b/src/wallet/components/WalletAdvancedSend/utils/resolveAddressInput.ts index bb18b26b5e..3e1437649d 100644 --- a/src/wallet/components/WalletAdvancedSend/utils/resolveAddressInput.ts +++ b/src/wallet/components/WalletAdvancedSend/utils/resolveAddressInput.ts @@ -1,37 +1,56 @@ import { getName, isBasename } from '@/identity'; import { getSlicedAddress } from '@/identity/utils/getSlicedAddress'; +import type { RecipientAddress } from '@/wallet/components/WalletAdvancedSend/types'; +import { validateAddressInput } from '@/wallet/components/WalletAdvancedSend/utils/validateAddressInput'; import { type Address, isAddress } from 'viem'; import { base, mainnet } from 'viem/chains'; export async function resolveAddressInput( selectedRecipientAddress: Address | null, recipientInput: string | null, -) { - // if there is no user input, return null +): Promise { + // if there is no user input, return nullish values if (!recipientInput) { - return null; + return { + display: '', + value: null, + }; } - // if the user hasn't selected an address yet, just return their input + // if the user hasn't selected an address yet, return their input and a validated address if (!selectedRecipientAddress) { - return recipientInput; + const validatedAddress = await validateAddressInput(recipientInput); + return { + display: recipientInput, + value: validatedAddress, + }; } - // we now have a selected address - // so if the user's input is address-format, then return the sliced address + // we now have a selected recipient + // if the user's input is address-format, then return the sliced address if (isAddress(recipientInput)) { - return getSlicedAddress(selectedRecipientAddress); + return { + display: getSlicedAddress(recipientInput), + value: selectedRecipientAddress, + }; } - // if the user's input wasn't address-format, then try to get and return the name + // if the user's input wasn't address-format, then it must have been name-format + // so try to get and return the name const name = await getName({ address: selectedRecipientAddress, chain: isBasename(recipientInput) ? base : mainnet, }); if (name) { - return name; + return { + display: name, + value: selectedRecipientAddress, + }; } - // as a last resort, just return the user's input - return recipientInput; + // as a last resort, display the user's input and set the value to null + return { + display: recipientInput, + value: null, + }; } diff --git a/src/wallet/utils/validateAddressInput.test.ts b/src/wallet/components/WalletAdvancedSend/utils/validateAddressInput.test.ts similarity index 100% rename from src/wallet/utils/validateAddressInput.test.ts rename to src/wallet/components/WalletAdvancedSend/utils/validateAddressInput.test.ts diff --git a/src/wallet/utils/validateAddressInput.ts b/src/wallet/components/WalletAdvancedSend/utils/validateAddressInput.ts similarity index 62% rename from src/wallet/utils/validateAddressInput.ts rename to src/wallet/components/WalletAdvancedSend/utils/validateAddressInput.ts index 7b6bd97942..45234c837f 100644 --- a/src/wallet/utils/validateAddressInput.ts +++ b/src/wallet/components/WalletAdvancedSend/utils/validateAddressInput.ts @@ -4,15 +4,22 @@ import { isEns } from '@/identity/utils/isEns'; import { isAddress } from 'viem'; import { base, mainnet } from 'viem/chains'; -export async function validateAddressInput(input: string) { +export async function validateAddressInput(input: string | null) { + if (!input) { + return null; + } + if (isAddress(input, { strict: false })) { return input; } - if (isBasename(input) || isEns(input)) { + const inputIsBasename = isBasename(input); + const inputIsEns = isEns(input); + + if (inputIsBasename || inputIsEns) { const address = await getAddress({ name: input, - chain: isBasename(input) ? base : mainnet, + chain: inputIsBasename ? base : mainnet, }); if (address) { return address; diff --git a/src/wallet/components/WalletAdvancedSend/utils/validateAmountInput.ts b/src/wallet/components/WalletAdvancedSend/utils/validateAmountInput.ts index 174c2012ac..3dc11b5832 100644 --- a/src/wallet/components/WalletAdvancedSend/utils/validateAmountInput.ts +++ b/src/wallet/components/WalletAdvancedSend/utils/validateAmountInput.ts @@ -1,32 +1,6 @@ import type { PortfolioTokenWithFiatValue } from '@/api/types'; import { parseUnits } from 'viem'; -// const parsedCryptoAmount = parseUnits( -// cryptoAmount ?? '', -// selectedToken?.decimals ?? 0, -// ); -// const parsedCryptoBalance = parseUnits( -// String(selectedToken?.cryptoBalance) ?? '', -// selectedToken?.decimals ?? 0, -// ); - -// const sendButtonLabel = useMemo(() => { -// if (parsedCryptoAmount > parsedCryptoBalance) { -// return 'Insufficient balance'; -// } -// return label; -// }, [parsedCryptoAmount, parsedCryptoBalance, label]); - -// const isDisabled = useMemo(() => { -// if (disabled) { -// return true; -// } -// if (parsedCryptoAmount <= 0n || parsedCryptoAmount > parsedCryptoBalance) { -// return true; -// } -// return false; -// }, [parsedCryptoAmount, parsedCryptoBalance, disabled]); - export function validateAmountInput({ cryptoAmount, selectedToken, From 441795b8a0bbbd387bfb89a5dc20438101cd37db Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 14:21:17 -0800 Subject: [PATCH 067/115] button label --- .../utils/getDefaultSendButtonLabel.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/wallet/components/WalletAdvancedSend/utils/getDefaultSendButtonLabel.ts diff --git a/src/wallet/components/WalletAdvancedSend/utils/getDefaultSendButtonLabel.ts b/src/wallet/components/WalletAdvancedSend/utils/getDefaultSendButtonLabel.ts new file mode 100644 index 0000000000..567c835329 --- /dev/null +++ b/src/wallet/components/WalletAdvancedSend/utils/getDefaultSendButtonLabel.ts @@ -0,0 +1,23 @@ +import type { PortfolioTokenWithFiatValue } from '@/api/types'; +import { parseUnits } from 'viem'; + +export function getDefaultSendButtonLabel( + cryptoAmount: string | null, + selectedToken: PortfolioTokenWithFiatValue | null, +) { + if (!cryptoAmount) { + return 'Input amount'; + } + + if (!selectedToken) { + return 'Select token'; + } + + if ( + parseUnits(cryptoAmount, selectedToken.decimals) > + selectedToken.cryptoBalance + ) { + return 'Insufficient balance'; + } + return 'Continue'; +} From fe0de1c7907429985febee342eec7e47db38ea51 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 14:33:20 -0800 Subject: [PATCH 068/115] update provider and types --- .../components/SendProvider.tsx | 71 +++++++------------ .../components/WalletAdvancedSend/types.ts | 21 +++--- 2 files changed, 34 insertions(+), 58 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index 4c36bf50cb..63d2fb98b5 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -12,14 +12,13 @@ import { useEffect, useState, } from 'react'; -import { formatUnits, type Address } from 'viem'; -import { validateAddressInput } from '../../../utils/validateAddressInput'; +import { formatUnits } from 'viem'; import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; -import { useWalletContext } from '../../WalletProvider'; import type { SendContextType, SendLifecycleStatus, SendProviderReact, + RecipientAddress, } from '../types'; const emptyContext = {} as SendContextType; @@ -37,11 +36,11 @@ export function SendProvider({ children }: SendProviderReact) { const [ethBalance, setEthBalance] = useState(0); // state for recipient address selection - const [recipientInput, setRecipientInput] = useState(null); - const [validatedRecipientAddress, setValidatedRecipientAddress] = - useState
(null); const [selectedRecipientAddress, setSelectedRecipientAddress] = - useState
(null); + useState({ + display: '', + value: null, + }); // state for token selection const [selectedToken, setSelectedToken] = @@ -66,7 +65,7 @@ export function SendProvider({ children }: SendProviderReact) { isMissingRequiredField: true, }, }); - const { address: senderAddress, chain: senderChain } = useWalletContext(); + // const { address: senderAddress, chain: senderChain } = useWalletContext(); const { tokenBalances } = useWalletAdvancedContext(); useEffect(() => { @@ -94,21 +93,6 @@ export function SendProvider({ children }: SendProviderReact) { setIsInitialized(true); }, [tokenBalances, updateLifecycleStatus]); - // Validate recipient input and set validated recipient address - useEffect(() => { - async function validateRecipientInput() { - if (recipientInput) { - const validatedInput = await validateAddressInput(recipientInput); - if (validatedInput) { - setValidatedRecipientAddress(validatedInput); - } else { - setValidatedRecipientAddress(null); - } - } - } - validateRecipientInput(); - }, [recipientInput]); - // fetch & set exchange rate useEffect(() => { if (!selectedToken) { @@ -122,24 +106,23 @@ export function SendProvider({ children }: SendProviderReact) { }); }, [selectedToken, selectedInputType]); - const handleRecipientInputChange = useCallback( - (input: string) => { - setRecipientInput(input); - setSelectedRecipientAddress(null); - setValidatedRecipientAddress(null); - updateLifecycleStatus({ - statusName: 'selectingAddress', - statusData: { - isMissingRequiredField: true, - }, - }); - }, - [updateLifecycleStatus], - ); + const handleRecipientInputChange = useCallback(() => { + console.log('provider handleRecipientInputChange'); + setSelectedRecipientAddress({ + display: '', + value: null, + }); + updateLifecycleStatus({ + statusName: 'selectingAddress', + statusData: { + isMissingRequiredField: true, + }, + }); + }, [updateLifecycleStatus]); const handleAddressSelection = useCallback( - (address: Address) => { - setSelectedRecipientAddress(address); + async (selection: RecipientAddress) => { + setSelectedRecipientAddress(selection); updateLifecycleStatus({ statusName: 'selectingToken', statusData: { @@ -229,14 +212,14 @@ export function SendProvider({ children }: SendProviderReact) { ); const fetchTransactionData = useCallback(() => { - if (!selectedRecipientAddress || !selectedToken || !cryptoAmount) { + if (!selectedRecipientAddress.value || !selectedToken || !cryptoAmount) { return; } try { setCallData(null); const calls = useSendTransaction({ - recipientAddress: selectedRecipientAddress, + recipientAddress: selectedRecipientAddress.value, token: selectedToken, amount: cryptoAmount, }); @@ -263,13 +246,7 @@ export function SendProvider({ children }: SendProviderReact) { isInitialized, lifecycleStatus, updateLifecycleStatus, - senderAddress, - senderChain, - tokenBalances, ethBalance, - recipientInput, - setRecipientInput, - validatedRecipientAddress, selectedRecipientAddress, handleAddressSelection, selectedToken, diff --git a/src/wallet/components/WalletAdvancedSend/types.ts b/src/wallet/components/WalletAdvancedSend/types.ts index 33314e40f8..fee9d59dd1 100644 --- a/src/wallet/components/WalletAdvancedSend/types.ts +++ b/src/wallet/components/WalletAdvancedSend/types.ts @@ -1,5 +1,5 @@ import type { Dispatch, ReactNode, SetStateAction } from 'react'; -import type { Address, Chain, TransactionReceipt } from 'viem'; +import type { Address, TransactionReceipt } from 'viem'; import type { APIError, PortfolioTokenWithFiatValue } from '../../../api/types'; import type { Call } from '../../../transaction/types'; import type { LifecycleStatusUpdate } from '@/internal/types'; @@ -16,19 +16,13 @@ export type SendContextType = { status: LifecycleStatusUpdate, ) => void; - // Wallet Context - senderAddress: Address | null | undefined; - senderChain: Chain | null | undefined; + // Sender Context ethBalance: number | undefined; - tokenBalances: PortfolioTokenWithFiatValue[] | undefined; // Recipient Address Context - recipientInput: string | null; - setRecipientInput: Dispatch>; - validatedRecipientAddress: Address | null; - selectedRecipientAddress: Address | null; - handleAddressSelection: (address: Address) => void; - handleRecipientInputChange: (input: string) => void; + selectedRecipientAddress: RecipientAddress; + handleAddressSelection: (selection: RecipientAddress) => void; + handleRecipientInputChange: () => void; // Token Context selectedToken: PortfolioTokenWithFiatValue | null; @@ -49,6 +43,11 @@ export type SendContextType = { callData: Call | null; }; +export type RecipientAddress = { + display: string; + value: Address | null; +}; + export type SendLifecycleStatus = | { statusName: 'init'; From 266afbf47b35a7c3a38baed3f79ffacad49b360f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 15:11:39 -0800 Subject: [PATCH 069/115] clean provider --- .../WalletAdvancedSend/components/SendProvider.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx index 63d2fb98b5..8d3c392572 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendProvider.tsx @@ -57,7 +57,7 @@ export function SendProvider({ children }: SendProviderReact) { // state for transaction data const [callData, setCallData] = useState(null); - // data and utils from hooks + // lifecycle status const [lifecycleStatus, updateLifecycleStatus] = useLifecycleStatus({ statusName: 'init', @@ -65,9 +65,9 @@ export function SendProvider({ children }: SendProviderReact) { isMissingRequiredField: true, }, }); - // const { address: senderAddress, chain: senderChain } = useWalletContext(); - const { tokenBalances } = useWalletAdvancedContext(); + // fetch & set ETH balance + const { tokenBalances } = useWalletAdvancedContext(); useEffect(() => { const ethBalance = tokenBalances?.find((token) => token.address === ''); if (ethBalance && ethBalance.cryptoBalance > 0) { @@ -106,8 +106,8 @@ export function SendProvider({ children }: SendProviderReact) { }); }, [selectedToken, selectedInputType]); + // handlers const handleRecipientInputChange = useCallback(() => { - console.log('provider handleRecipientInputChange'); setSelectedRecipientAddress({ display: '', value: null, From 4be81d10968fe7058f40deae63473fbd245d3ae3 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 15:12:15 -0800 Subject: [PATCH 070/115] handle null args --- .../components/SendAddressSelector.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx b/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx index 144add067a..22bb420484 100644 --- a/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx +++ b/src/wallet/components/WalletAdvancedSend/components/SendAddressSelector.tsx @@ -2,23 +2,22 @@ import { Address, Avatar, Name } from '@/identity'; import { background, border, cn, pressable } from '@/styles/theme'; -import { useCallback } from 'react'; import type { Address as AddressType, Chain } from 'viem'; type SendAddressSelectorProps = { - address: AddressType; + address: AddressType | null; senderChain: Chain | null | undefined; - handleAddressSelection: (address: AddressType) => void; + handleClick: () => Promise; }; export function SendAddressSelector({ address, senderChain, - handleAddressSelection, + handleClick, }: SendAddressSelectorProps) { - const handleClick = useCallback(() => { - handleAddressSelection(address); - }, [handleAddressSelection, address]); + if (!address || !senderChain) { + return null; + } return ( - ); - } - - return ( -
- {tokenContent} -
- ); -} diff --git a/src/token/components/TokenBalance.tsx b/src/token/components/TokenBalance.tsx index f2c485fdfb..f09275eba1 100644 --- a/src/token/components/TokenBalance.tsx +++ b/src/token/components/TokenBalance.tsx @@ -2,33 +2,59 @@ import { formatFiatAmount } from '@/internal/utils/formatFiatAmount'; import { truncateDecimalPlaces } from '@/internal/utils/truncateDecimalPlaces'; import { border, cn, color, text } from '@/styles/theme'; import { TokenImage } from '@/token'; -import { useCallback } from 'react'; import { formatUnits } from 'viem'; import type { TokenBalanceProps } from '../types'; +import { useMemo } from 'react'; export function TokenBalance({ token, onClick, + onActionPress, + actionText = 'Use max', classNames, + 'aria-label': ariaLabel, ...contentProps }: TokenBalanceProps) { if (onClick) { return ( - + {onActionPress && ( + )} - data-testid="ockTokenBalanceButton" - > - - +
); } @@ -53,31 +79,26 @@ function TokenBalanceContent({ token, subtitle, showImage = true, - actionText = 'Use max', onActionPress, tokenSize = 40, classNames, }: TokenBalanceProps) { - const formattedFiatValue = formatFiatAmount({ - amount: token.fiatBalance, - currency: 'USD', - }); - - const formattedCryptoValue = truncateDecimalPlaces( - formatUnits(BigInt(token.cryptoBalance), token.decimals), - 3, + const formattedFiatValue = useMemo( + () => + formatFiatAmount({ + amount: token.fiatBalance, + currency: 'USD', + }), + [token.fiatBalance], ); - const handleActionPress = useCallback( - ( - e: - | React.MouseEvent - | React.KeyboardEvent, - ) => { - e.stopPropagation(); - onActionPress?.(); - }, - [onActionPress], + const formattedCryptoValue = useMemo( + () => + truncateDecimalPlaces( + formatUnits(BigInt(token.cryptoBalance), token.decimals), + 3, + ), + [token.cryptoBalance, token.decimals], ); return ( @@ -107,25 +128,7 @@ function TokenBalanceContent({
- {onActionPress ? ( -
- {actionText} -
- ) : ( + {!onActionPress && ( void; - /** Size of the token image in px (default: 40) */ - tokenSize?: number; + /** Optional aria label for the component */ + 'aria-label'?: string; /** Optional additional CSS classes to apply to the component */ classNames?: { container?: string; @@ -146,13 +144,27 @@ export type TokenBalanceProps = { }; } & ( | { - /** Hide the action button (default)*/ - actionText?: never; - onActionPress?: never; + /** Show the token image (default: true) */ + showImage?: true; + /** Size of the token image in px (default: 40) */ + tokenSize?: number; } | { - /** Show an additional action button (eg. "Use max") */ - actionText?: string; - onActionPress: () => void; + /** Hide the token image */ + showImage: false; + /** Size of the token image in px (default: 40) */ + tokenSize?: never; } -); +) & + ( + | { + /** Hide the action button (default)*/ + onActionPress?: never; + actionText?: never; + } + | { + /** Show an additional action button (eg. "Use max") */ + onActionPress: () => void; + actionText?: string; + } + ); From 6c82789cb3da0359d23496241489b50dd94c285f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 7 Feb 2025 13:32:25 -0800 Subject: [PATCH 092/115] fix button validation --- .../components/SendButton.tsx | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/wallet/components/wallet-advanced-send/components/SendButton.tsx b/src/wallet/components/wallet-advanced-send/components/SendButton.tsx index aaba05dd23..80a3ba4ec9 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendButton.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendButton.tsx @@ -40,18 +40,23 @@ export function SendButton({ errorOverride, }: SendButtonProps) { const { chain: senderChain } = useWalletContext(); - const { callData, cryptoAmount, selectedToken, updateLifecycleStatus } = - useSendContext(); + const { + callData, + cryptoAmount: inputAmount, + selectedToken, + updateLifecycleStatus, + } = useSendContext(); const disableSendButton = disabled ?? !validateAmountInput({ - cryptoAmount: cryptoAmount ?? '', + inputAmount: inputAmount ?? '', + balance: BigInt(selectedToken?.cryptoBalance ?? 0), selectedToken: selectedToken ?? undefined, }); const buttonLabel = - label ?? getDefaultSendButtonLabel(cryptoAmount, selectedToken); + label ?? getDefaultSendButtonLabel(inputAmount, selectedToken); const handleStatus = useCallback( (status: LifecycleStatus) => { @@ -168,19 +173,20 @@ function getDefaultSendButtonLabel( } function validateAmountInput({ - cryptoAmount, + inputAmount, + balance, selectedToken, }: { - cryptoAmount?: string; + inputAmount?: string; + balance?: bigint; selectedToken?: PortfolioTokenWithFiatValue; }) { - if (!cryptoAmount || !selectedToken) { + if (!inputAmount || !selectedToken || !balance) { return false; } - const parsedCryptoAmount = parseUnits(cryptoAmount, selectedToken.decimals); + const parsedCryptoAmount = parseUnits(inputAmount, selectedToken.decimals); + console.log({ inputAmount, balance, parsedCryptoAmount }); - return ( - parsedCryptoAmount > 0n && parsedCryptoAmount <= selectedToken.cryptoBalance - ); + return parsedCryptoAmount > 0n && parsedCryptoAmount <= balance; } From 3624ea5edc8524808d57295fb640719e33b6243e Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 7 Feb 2025 13:32:58 -0800 Subject: [PATCH 093/115] fix lints --- src/token/components/TokenBalance.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/token/components/TokenBalance.tsx b/src/token/components/TokenBalance.tsx index f09275eba1..5f0c6a0c16 100644 --- a/src/token/components/TokenBalance.tsx +++ b/src/token/components/TokenBalance.tsx @@ -2,9 +2,9 @@ import { formatFiatAmount } from '@/internal/utils/formatFiatAmount'; import { truncateDecimalPlaces } from '@/internal/utils/truncateDecimalPlaces'; import { border, cn, color, text } from '@/styles/theme'; import { TokenImage } from '@/token'; +import { useMemo } from 'react'; import { formatUnits } from 'viem'; import type { TokenBalanceProps } from '../types'; -import { useMemo } from 'react'; export function TokenBalance({ token, From 3c19c3b6435b688b096f651625287d78b25d1748 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 7 Feb 2025 13:56:10 -0800 Subject: [PATCH 094/115] refactor walletadvanced active feature --- .../components/WalletAdvancedContent.tsx | 18 ++++------- .../components/WalletAdvancedProvider.tsx | 30 +++++++------------ .../components/WalletAdvancedQrReceive.tsx | 20 ++++++++----- src/wallet/components/WalletAdvancedSwap.tsx | 21 +++++++------ .../WalletAdvancedTransactionActions.tsx | 10 +++---- .../WalletAdvancedWalletActions.tsx | 6 ++-- .../components/SendButton.tsx | 4 +-- .../components/SendHeader.tsx | 6 ++-- src/wallet/types.ts | 18 ++++------- 9 files changed, 59 insertions(+), 74 deletions(-) diff --git a/src/wallet/components/WalletAdvancedContent.tsx b/src/wallet/components/WalletAdvancedContent.tsx index 1571cf7126..11d39a917e 100644 --- a/src/wallet/components/WalletAdvancedContent.tsx +++ b/src/wallet/components/WalletAdvancedContent.tsx @@ -24,7 +24,7 @@ export function WalletAdvancedContent({ breakpoint, } = useWalletContext(); - const { showQr, showSwap, showSend, tokenBalances, animations } = + const { activeFeature, tokenBalances, animations } = useWalletAdvancedContext(); const handleBottomSheetClose = useCallback(() => { @@ -39,7 +39,7 @@ export function WalletAdvancedContent({ }, [isSubComponentClosing, setIsSubComponentOpen, setIsSubComponentClosing]); const content = useMemo(() => { - if (showSend) { + if (activeFeature === 'send') { return ( @@ -47,7 +47,7 @@ export function WalletAdvancedContent({ ); } - if (showQr) { + if (activeFeature === 'qr') { return ( @@ -55,7 +55,7 @@ export function WalletAdvancedContent({ ); } - if (showSwap) { + if (activeFeature === 'swap') { return ( {children}; - }, [ - showQr, - showSwap, - showSend, - swappableTokens, - tokenBalances, - children, - classNames, - ]); + }, [activeFeature, swappableTokens, tokenBalances, children, classNames]); if (breakpoint === 'sm') { return ( diff --git a/src/wallet/components/WalletAdvancedProvider.tsx b/src/wallet/components/WalletAdvancedProvider.tsx index da5abe7728..eea153ce22 100644 --- a/src/wallet/components/WalletAdvancedProvider.tsx +++ b/src/wallet/components/WalletAdvancedProvider.tsx @@ -2,7 +2,10 @@ import { RequestContext } from '@/core/network/constants'; import { useValue } from '@/internal/hooks/useValue'; import { usePortfolio } from '@/wallet/hooks/usePortfolio'; import { type ReactNode, createContext, useContext, useState } from 'react'; -import type { WalletAdvancedContextType } from '../types'; +import type { + WalletAdvancedContextType, + WalletAdvancedFeature, +} from '../types'; import { useWalletContext } from './WalletProvider'; type WalletAdvancedProviderReact = { @@ -29,12 +32,9 @@ export function WalletAdvancedProvider({ }: WalletAdvancedProviderReact) { const { address, isSubComponentClosing, showSubComponentAbove } = useWalletContext(); - const [showSwap, setShowSwap] = useState(false); - const [isSwapClosing, setIsSwapClosing] = useState(false); - const [showQr, setShowQr] = useState(false); - const [isQrClosing, setIsQrClosing] = useState(false); - const [showSend, setShowSend] = useState(false); - const [isSendClosing, setIsSendClosing] = useState(false); + const [activeFeature, setActiveFeature] = + useState(null); + const [isActiveFeatureClosing, setIsActiveFeatureClosing] = useState(false); const { data: portfolioData, refetch: refetchPortfolioData, @@ -51,18 +51,10 @@ export function WalletAdvancedProvider({ ); const value = useValue({ - showSwap, - setShowSwap, - isSwapClosing, - setIsSwapClosing, - showQr, - setShowQr, - isQrClosing, - setIsQrClosing, - showSend, - setShowSend, - isSendClosing, - setIsSendClosing, + activeFeature, + setActiveFeature, + isActiveFeatureClosing, + setIsActiveFeatureClosing, tokenBalances, portfolioFiatValue, isFetchingPortfolioData, diff --git a/src/wallet/components/WalletAdvancedQrReceive.tsx b/src/wallet/components/WalletAdvancedQrReceive.tsx index e8f11ec819..c19cbe5378 100644 --- a/src/wallet/components/WalletAdvancedQrReceive.tsx +++ b/src/wallet/components/WalletAdvancedQrReceive.tsx @@ -16,20 +16,24 @@ export function WalletAdvancedQrReceive({ classNames, }: WalletAdvancedQrReceiveProps) { const { address } = useWalletContext(); - const { setShowQr, isQrClosing, setIsQrClosing } = useWalletAdvancedContext(); + const { + setActiveFeature, + isActiveFeatureClosing, + setIsActiveFeatureClosing, + } = useWalletAdvancedContext(); const [copyText, setCopyText] = useState('Copy'); const [copyButtonText, setCopyButtonText] = useState('Copy address'); const handleCloseQr = useCallback(() => { - setIsQrClosing(true); - }, [setIsQrClosing]); + setIsActiveFeatureClosing(true); + }, [setIsActiveFeatureClosing]); const handleAnimationEnd = useCallback(() => { - if (isQrClosing) { - setShowQr(false); - setIsQrClosing(false); + if (isActiveFeatureClosing) { + setActiveFeature(null); + setIsActiveFeatureClosing(false); } - }, [isQrClosing, setShowQr, setIsQrClosing]); + }, [isActiveFeatureClosing, setActiveFeature, setIsActiveFeatureClosing]); const resetAffordanceText = useCallback(() => { setTimeout(() => { @@ -68,7 +72,7 @@ export function WalletAdvancedQrReceive({ 'flex flex-col items-center justify-between', 'h-full w-full', 'px-4 pt-3 pb-4', - isQrClosing + isActiveFeatureClosing ? 'fade-out slide-out-to-left-5 animate-out fill-mode-forwards ease-in-out' : 'fade-in slide-in-from-left-5 linear animate-in duration-150', classNames?.container, diff --git a/src/wallet/components/WalletAdvancedSwap.tsx b/src/wallet/components/WalletAdvancedSwap.tsx index 5bba7cebbc..65fae95fab 100644 --- a/src/wallet/components/WalletAdvancedSwap.tsx +++ b/src/wallet/components/WalletAdvancedSwap.tsx @@ -30,19 +30,22 @@ export function WalletAdvancedSwap({ title, to, }: WalletAdvancedSwapProps) { - const { setShowSwap, isSwapClosing, setIsSwapClosing } = - useWalletAdvancedContext(); + const { + setActiveFeature, + isActiveFeatureClosing, + setIsActiveFeatureClosing, + } = useWalletAdvancedContext(); const handleCloseSwap = useCallback(() => { - setIsSwapClosing(true); - }, [setIsSwapClosing]); + setIsActiveFeatureClosing(true); + }, [setIsActiveFeatureClosing]); const handleAnimationEnd = useCallback(() => { - if (isSwapClosing) { - setShowSwap(false); - setIsSwapClosing(false); + if (isActiveFeatureClosing) { + setActiveFeature(null); + setIsActiveFeatureClosing(false); } - }, [isSwapClosing, setShowSwap, setIsSwapClosing]); + }, [isActiveFeatureClosing, setActiveFeature, setIsActiveFeatureClosing]); const backButton = ( @@ -55,7 +58,7 @@ export function WalletAdvancedSwap({ className={cn( 'h-full', border.radius, - isSwapClosing + isActiveFeatureClosing ? 'fade-out slide-out-to-right-5 animate-out fill-mode-forwards ease-in-out' : 'fade-in slide-in-from-right-5 linear animate-in duration-150', 'relative', diff --git a/src/wallet/components/WalletAdvancedTransactionActions.tsx b/src/wallet/components/WalletAdvancedTransactionActions.tsx index f1061e8853..ec26d9b0c7 100644 --- a/src/wallet/components/WalletAdvancedTransactionActions.tsx +++ b/src/wallet/components/WalletAdvancedTransactionActions.tsx @@ -34,7 +34,7 @@ export function WalletAdvancedTransactionActions({ }: WalletAdvancedTransactionActionsProps) { const { address, chain } = useWalletContext(); const { projectId } = useOnchainKit(); - const { isFetchingPortfolioData, setShowSwap, setShowSend, animations } = + const { isFetchingPortfolioData, setActiveFeature, animations } = useWalletAdvancedContext(); const handleBuy = useCallback(() => { @@ -64,12 +64,12 @@ export function WalletAdvancedTransactionActions({ }, [address, chain?.name, projectId]); const handleSend = useCallback(() => { - setShowSend(true); - }, [setShowSend]); + setActiveFeature('send'); + }, [setActiveFeature]); const handleSwap = useCallback(() => { - setShowSwap(true); - }, [setShowSwap]); + setActiveFeature('swap'); + }, [setActiveFeature]); if (isFetchingPortfolioData) { return ( diff --git a/src/wallet/components/WalletAdvancedWalletActions.tsx b/src/wallet/components/WalletAdvancedWalletActions.tsx index 059638fdee..dc507251c8 100644 --- a/src/wallet/components/WalletAdvancedWalletActions.tsx +++ b/src/wallet/components/WalletAdvancedWalletActions.tsx @@ -25,7 +25,7 @@ export function WalletAdvancedWalletActions({ classNames, }: WalletAdvancedWalletActionsProps) { const { address, handleClose } = useWalletContext(); - const { setShowQr, refetchPortfolioData, animations } = + const { setActiveFeature, refetchPortfolioData, animations } = useWalletAdvancedContext(); const { disconnect, connectors } = useDisconnect(); @@ -41,8 +41,8 @@ export function WalletAdvancedWalletActions({ }, [disconnect, connectors, handleClose]); const handleQr = useCallback(() => { - setShowQr(true); - }, [setShowQr]); + setActiveFeature('qr'); + }, [setActiveFeature]); const handleRefreshPortfolioData = useCallback(async () => { await refetchPortfolioData(); diff --git a/src/wallet/components/wallet-advanced-send/components/SendButton.tsx b/src/wallet/components/wallet-advanced-send/components/SendButton.tsx index 80a3ba4ec9..4e569c8cc0 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendButton.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendButton.tsx @@ -126,7 +126,7 @@ function SendTransactionButton({ className?: string; }) { const { address } = useWalletContext(); - const { setShowSend } = useWalletAdvancedContext(); + const { setActiveFeature } = useWalletAdvancedContext(); const { transactionHash, transactionId } = useTransactionContext(); const defaultSuccessOverride = { onClick: defaultSendTxSuccessHandler({ @@ -134,7 +134,7 @@ function SendTransactionButton({ transactionHash, senderChain: senderChain ?? undefined, address: address ?? undefined, - onComplete: () => setShowSend(false), + onComplete: () => setActiveFeature(null), }), }; diff --git a/src/wallet/components/wallet-advanced-send/components/SendHeader.tsx b/src/wallet/components/wallet-advanced-send/components/SendHeader.tsx index 4afb08f785..39f1dce4c0 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendHeader.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendHeader.tsx @@ -19,7 +19,7 @@ type SendHeaderProps = { }; export function SendHeader({ label = 'Send', classNames }: SendHeaderProps) { - const { setShowSend } = useWalletAdvancedContext(); + const { setActiveFeature } = useWalletAdvancedContext(); const { selectedRecipientAddress, @@ -42,8 +42,8 @@ export function SendHeader({ label = 'Send', classNames }: SendHeaderProps) { ]); const handleClose = useCallback(() => { - setShowSend(false); - }, [setShowSend]); + setActiveFeature(null); + }, [setActiveFeature]); return (
>; - isSwapClosing: boolean; - setIsSwapClosing: Dispatch>; - showQr: boolean; - setShowQr: Dispatch>; - isQrClosing: boolean; - setIsQrClosing: Dispatch>; - showSend: boolean; - setShowSend: Dispatch>; - isSendClosing: boolean; - setIsSendClosing: Dispatch>; + activeFeature: WalletAdvancedFeature | null; + setActiveFeature: Dispatch>; + isActiveFeatureClosing: boolean; + setIsActiveFeatureClosing: Dispatch>; tokenBalances: PortfolioTokenWithFiatValue[] | undefined; portfolioFiatValue: number | undefined; isFetchingPortfolioData: boolean; From e75515050e2f780c8898fc87b5cfe5bd9ad7dc49 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 7 Feb 2025 13:57:20 -0800 Subject: [PATCH 095/115] use skeleton for loading state --- .../components/WalletAdvancedTransactionActions.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/wallet/components/WalletAdvancedTransactionActions.tsx b/src/wallet/components/WalletAdvancedTransactionActions.tsx index ec26d9b0c7..35deb20a9a 100644 --- a/src/wallet/components/WalletAdvancedTransactionActions.tsx +++ b/src/wallet/components/WalletAdvancedTransactionActions.tsx @@ -1,5 +1,6 @@ 'use client'; +import { Skeleton } from '@/internal/components/Skeleton'; import { addSvgForeground } from '@/internal/svg/addForegroundSvg'; import { arrowUpRightSvg } from '@/internal/svg/arrowUpRightSvg'; import { toggleSvg } from '@/internal/svg/toggleSvg'; @@ -72,12 +73,7 @@ export function WalletAdvancedTransactionActions({ }, [setActiveFeature]); if (isFetchingPortfolioData) { - return ( -
- ); // Prevent layout shift + return ; } return ( From d06060ce430f06af05f11115090cdfdf0eef6a8c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 10 Feb 2025 15:08:46 -0800 Subject: [PATCH 096/115] address commentsc --- .../wallet-advanced-send/components/SendAddressInput.tsx | 4 +++- .../wallet-advanced-send/components/SendAmountInput.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/wallet-advanced-send/components/SendAddressInput.tsx b/src/wallet/components/wallet-advanced-send/components/SendAddressInput.tsx index e544546ecc..b59e6a4b5a 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAddressInput.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAddressInput.tsx @@ -55,7 +55,9 @@ export function SendAddressInput({ inputMode="text" placeholder="Basename, ENS, or Address" value={displayValue} - inputValidator={() => !!validateAddressInput(recipientInput)} + inputValidator={(recipientInput) => + !!validateAddressInput(recipientInput) + } setValue={setRecipientInput} onChange={handleSetValue} onFocus={handleFocus} diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx index 77482ad80f..1756f21d5e 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx @@ -29,7 +29,7 @@ export function SendAmountInput({ fiatAmount={fiatAmount ?? ''} cryptoAmount={cryptoAmount ?? ''} asset={selectedToken?.symbol ?? ''} - currency={'USD'} + currency="USD" selectedInputType={selectedInputType} setFiatAmount={handleFiatAmountChange} setCryptoAmount={handleCryptoAmountChange} From f51ce9b1d6024ab5547b7d1101e16f0bec73a1a1 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 25 Feb 2025 21:07:38 -0800 Subject: [PATCH 097/115] fix tests --- .../components/SendAmountInput.test.tsx | 100 +++++++++------- .../SendAmountInputTypeSwitch.test.tsx | 107 ++++++++++-------- .../components/SendTokenSelector.test.tsx | 86 +++++++------- 3 files changed, 159 insertions(+), 134 deletions(-) diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx index 5a6b4128f3..1ba9e5252e 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx @@ -1,11 +1,15 @@ import { AmountInput } from '@/internal/components/amount-input/AmountInput'; import { render } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { SendAmountInput } from './SendAmountInput'; import { SendAmountInputTypeSwitch } from './SendAmountInputTypeSwitch'; +import { useSendContext } from './SendProvider'; vi.mock('@/internal/components/amount-input/AmountInput'); vi.mock('./SendAmountInputTypeSwitch'); +vi.mock('./SendProvider', () => ({ + useSendContext: vi.fn(), +})); const mockToken = { symbol: 'ETH', @@ -18,36 +22,41 @@ const mockToken = { fiatBalance: 3300, }; +const defaultContext = { + selectedToken: mockToken, + cryptoAmount: '1.0', + handleCryptoAmountChange: vi.fn(), + fiatAmount: '2000', + handleFiatAmountChange: vi.fn(), + selectedInputType: 'crypto' as const, + setSelectedInputType: vi.fn(), + exchangeRate: 2000, + exchangeRateLoading: false, + className: 'test-class', + textClassName: 'test-text-class', +}; + describe('SendAmountInput', () => { beforeEach(() => { vi.clearAllMocks(); + (useSendContext as Mock).mockReturnValue(defaultContext); }); - const defaultProps = { - selectedToken: mockToken, - cryptoAmount: '1.0', - handleCryptoAmountChange: vi.fn(), - fiatAmount: '2000', - handleFiatAmountChange: vi.fn(), - selectedInputType: 'crypto' as const, - setSelectedInputType: vi.fn(), - exchangeRate: 2000, - exchangeRateLoading: false, - className: 'test-class', - textClassName: 'test-text-class', - }; - it('passes correct props to AmountInput', () => { - render(); + (useSendContext as Mock).mockReturnValue({ + ...defaultContext, + }); + + render(); expect(AmountInput).toHaveBeenCalledWith( { - fiatAmount: defaultProps.fiatAmount, - cryptoAmount: defaultProps.cryptoAmount, - asset: defaultProps.selectedToken.symbol, + fiatAmount: defaultContext.fiatAmount, + cryptoAmount: defaultContext.cryptoAmount, + asset: defaultContext.selectedToken.symbol, currency: 'USD', - selectedInputType: defaultProps.selectedInputType, - setFiatAmount: defaultProps.handleFiatAmountChange, - setCryptoAmount: defaultProps.handleCryptoAmountChange, + selectedInputType: defaultContext.selectedInputType, + setFiatAmount: defaultContext.handleFiatAmountChange, + setCryptoAmount: defaultContext.handleCryptoAmountChange, exchangeRate: '2000', className: 'test-class', textClassName: 'test-text-class', @@ -57,31 +66,34 @@ describe('SendAmountInput', () => { }); it('passes correct props to SendAmountInputTypeSwitch', () => { - render(); + (useSendContext as Mock).mockReturnValue({ + ...defaultContext, + }); + + render(); expect(SendAmountInputTypeSwitch).toHaveBeenCalledWith( { - selectedToken: defaultProps.selectedToken, - fiatAmount: defaultProps.fiatAmount, - cryptoAmount: defaultProps.cryptoAmount, - selectedInputType: defaultProps.selectedInputType, - setSelectedInputType: defaultProps.setSelectedInputType, - exchangeRate: defaultProps.exchangeRate, - exchangeRateLoading: defaultProps.exchangeRateLoading, + selectedToken: defaultContext.selectedToken, + fiatAmount: defaultContext.fiatAmount, + cryptoAmount: defaultContext.cryptoAmount, + selectedInputType: defaultContext.selectedInputType, + setSelectedInputType: defaultContext.setSelectedInputType, + exchangeRate: defaultContext.exchangeRate, + exchangeRateLoading: defaultContext.exchangeRateLoading, }, {}, ); }); it('handles null/undefined values correctly', () => { - render( - , - ); + (useSendContext as Mock).mockReturnValue({ + ...defaultContext, + selectedToken: null, + fiatAmount: null, + cryptoAmount: null, + }); + render(); expect(AmountInput).toHaveBeenCalledWith( { fiatAmount: '', @@ -89,8 +101,8 @@ describe('SendAmountInput', () => { asset: '', currency: 'USD', selectedInputType: 'crypto', - setFiatAmount: defaultProps.handleFiatAmountChange, - setCryptoAmount: defaultProps.handleCryptoAmountChange, + setFiatAmount: defaultContext.handleFiatAmountChange, + setCryptoAmount: defaultContext.handleCryptoAmountChange, exchangeRate: '2000', className: 'test-class', textClassName: 'test-text-class', @@ -103,10 +115,10 @@ describe('SendAmountInput', () => { selectedToken: null, fiatAmount: '', cryptoAmount: '', - selectedInputType: defaultProps.selectedInputType, - setSelectedInputType: defaultProps.setSelectedInputType, - exchangeRate: defaultProps.exchangeRate, - exchangeRateLoading: defaultProps.exchangeRateLoading, + selectedInputType: defaultContext.selectedInputType, + setSelectedInputType: defaultContext.setSelectedInputType, + exchangeRate: defaultContext.exchangeRate, + exchangeRateLoading: defaultContext.exchangeRateLoading, }, {}, ); diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx index 57fac66afe..af24dfd58e 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx @@ -1,11 +1,15 @@ import { Skeleton } from '@/internal/components/Skeleton'; import { AmountInputTypeSwitch } from '@/internal/components/amount-input/AmountInputTypeSwitch'; import { render } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { SendAmountInputTypeSwitch } from './SendAmountInputTypeSwitch'; +import { useSendContext } from './SendProvider'; vi.mock('@/internal/components/Skeleton'); vi.mock('@/internal/components/amount-input/AmountInputTypeSwitch'); +vi.mock('./SendProvider', () => ({ + useSendContext: vi.fn(), +})); const mockToken = { symbol: 'ETH', @@ -18,28 +22,29 @@ const mockToken = { fiatBalance: 3300, }; +const defaultContext = { + selectedToken: mockToken, + cryptoAmount: '1.0', + fiatAmount: '2000', + selectedInputType: 'crypto' as const, + setSelectedInputType: vi.fn(), + exchangeRate: 2000, + exchangeRateLoading: false, +}; + describe('SendAmountInputTypeSwitch', () => { beforeEach(() => { vi.clearAllMocks(); + (useSendContext as Mock).mockReturnValue(defaultContext); }); - const defaultProps = { - selectedToken: mockToken, - cryptoAmount: '1.0', - handleCryptoAmountChange: vi.fn(), - fiatAmount: '2000', - handleFiatAmountChange: vi.fn(), - selectedInputType: 'crypto' as const, - setSelectedInputType: vi.fn(), - exchangeRate: 2000, - exchangeRateLoading: false, - className: 'test-class', - textClassName: 'test-text-class', - loadingDisplay:
test-loading-display
, - }; - it('passes an error state when exchange rate is invalid', () => { - render(); + (useSendContext as Mock).mockReturnValue({ + ...defaultContext, + exchangeRate: 0, + }); + + render(); expect(AmountInputTypeSwitch).toHaveBeenCalledWith( expect.objectContaining({ loadingDisplay:
test-loading-display
, @@ -50,56 +55,64 @@ describe('SendAmountInputTypeSwitch', () => { }); it('shows skeleton when exchange rate is loading', () => { - render( - , - ); + (useSendContext as Mock).mockReturnValue({ + ...defaultContext, + exchangeRateLoading: true, + }); + + render(); expect(Skeleton).toHaveBeenCalled(); }); it('passes correct props to AmountInput', () => { - render(); + (useSendContext as Mock).mockReturnValue({ + ...defaultContext, + className: 'test-class', + loadingDisplay:
test-loading-display
, + }); + + render(); expect(AmountInputTypeSwitch).toHaveBeenCalledWith( { - asset: defaultProps.selectedToken.symbol, - fiatAmount: defaultProps.fiatAmount, - cryptoAmount: defaultProps.cryptoAmount, - exchangeRate: defaultProps.exchangeRate, - exchangeRateLoading: false, + asset: defaultContext.selectedToken.symbol, + fiatAmount: defaultContext.fiatAmount, + cryptoAmount: defaultContext.cryptoAmount, + exchangeRate: defaultContext.exchangeRate, + exchangeRateLoading: defaultContext.exchangeRateLoading, currency: 'USD', - selectedInputType: defaultProps.selectedInputType, - setSelectedInputType: defaultProps.setSelectedInputType, - className: defaultProps.className, - loadingDisplay: defaultProps.loadingDisplay, + selectedInputType: defaultContext.selectedInputType, + setSelectedInputType: defaultContext.setSelectedInputType, + className: 'test-class', + loadingDisplay:
test-loading-display
, }, {}, ); }); it('handles null/undefined values correctly', () => { - render( - , - ); + (useSendContext as Mock).mockReturnValue({ + selectedToken: null, + fiatAmount: null, + cryptoAmount: null, + exchangeRate: 3300, + exchangeRateLoading: false, + selectedInputType: 'fiat', + setSelectedInputType: vi.fn(), + }); + + render(); expect(AmountInputTypeSwitch).toHaveBeenCalledWith( { asset: '', fiatAmount: '', cryptoAmount: '', - exchangeRate: defaultProps.exchangeRate, - exchangeRateLoading: defaultProps.exchangeRateLoading, + exchangeRate: 3300, + exchangeRateLoading: false, currency: 'USD', - selectedInputType: defaultProps.selectedInputType, - setSelectedInputType: defaultProps.setSelectedInputType, - className: defaultProps.className, - loadingDisplay: defaultProps.loadingDisplay, + selectedInputType: 'fiat', + setSelectedInputType: vi.fn(), + className: '', }, {}, ); diff --git a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx index 8358529ee2..0eccfccf57 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx @@ -3,12 +3,17 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; import { SendTokenSelector } from './SendTokenSelector'; +import { useSendContext } from './SendProvider'; // Mock the context hook vi.mock('../../WalletAdvancedProvider', () => ({ useWalletAdvancedContext: vi.fn(), })); +vi.mock('./SendProvider', () => ({ + useSendContext: vi.fn(), +})); + const mockTokenBalances: PortfolioTokenWithFiatValue[] = [ { address: '0x1230000000000000000000000000000000000000', @@ -32,25 +37,26 @@ const mockTokenBalances: PortfolioTokenWithFiatValue[] = [ }, ]; -describe('SendTokenSelector', () => { - const defaultProps = { - selectedToken: null, - handleTokenSelection: vi.fn(), - handleResetTokenSelection: vi.fn(), - setSelectedInputType: vi.fn(), - handleCryptoAmountChange: vi.fn(), - handleFiatAmountChange: vi.fn(), - }; +const defaultContext = { + selectedToken: null, + handleTokenSelection: vi.fn(), + handleResetTokenSelection: vi.fn(), + setSelectedInputType: vi.fn(), + handleCryptoAmountChange: vi.fn(), + handleFiatAmountChange: vi.fn(), +}; +describe('SendTokenSelector', () => { beforeEach(() => { vi.clearAllMocks(); (useWalletAdvancedContext as Mock).mockReturnValue({ tokenBalances: mockTokenBalances, }); + (useSendContext as Mock).mockReturnValue(defaultContext); }); it('renders token selection list when no token is selected', () => { - render(); + render(); expect(screen.getByText('Select a token')).toBeInTheDocument(); expect(screen.getAllByRole('button')).toHaveLength( @@ -59,54 +65,54 @@ describe('SendTokenSelector', () => { }); it('calls handleTokenSelection when a token is clicked from the list', () => { - render(); + render(); fireEvent.click(screen.getAllByTestId('ockTokenBalanceButton')[0]); - expect(defaultProps.handleTokenSelection).toHaveBeenCalledWith( + expect(defaultContext.handleTokenSelection).toHaveBeenCalledWith( mockTokenBalances[0], ); }); it('renders selected token with max button when token is selected', () => { - render( - , - ); + (useSendContext as Mock).mockReturnValue({ + ...defaultContext, + selectedToken: mockTokenBalances[0], + }); + + render(); expect(screen.getByText('Test Token')).toBeInTheDocument(); expect(screen.getByText(/0\.000 TEST available/)).toBeInTheDocument(); }); it('handles max button click correctly', () => { - render( - , - ); + (useSendContext as Mock).mockReturnValue({ + ...defaultContext, + selectedToken: mockTokenBalances[0], + }); + + render(); const maxButton = screen.getByRole('button', { name: 'Use max' }); fireEvent.click(maxButton); - expect(defaultProps.setSelectedInputType).toHaveBeenCalledWith('crypto'); - expect(defaultProps.handleFiatAmountChange).toHaveBeenCalledWith('100'); - expect(defaultProps.handleCryptoAmountChange).toHaveBeenCalledWith( + expect(defaultContext.setSelectedInputType).toHaveBeenCalledWith('crypto'); + expect(defaultContext.handleFiatAmountChange).toHaveBeenCalledWith('100'); + expect(defaultContext.handleCryptoAmountChange).toHaveBeenCalledWith( '0.000000001', ); }); it('calls handleResetTokenSelection when selected token is clicked', () => { - render( - , - ); + (useSendContext as Mock).mockReturnValue({ + ...defaultContext, + selectedToken: mockTokenBalances[0], + }); + + render(); fireEvent.click(screen.getByTestId('ockTokenBalanceButton')); - expect(defaultProps.handleResetTokenSelection).toHaveBeenCalled(); + expect(defaultContext.handleResetTokenSelection).toHaveBeenCalled(); }); it('handles empty tokenBalances gracefully', () => { @@ -114,7 +120,7 @@ describe('SendTokenSelector', () => { tokenBalances: [], }); - render(); + render(); expect(screen.getByText('Select a token')).toBeInTheDocument(); }); @@ -124,19 +130,13 @@ describe('SendTokenSelector', () => { }; const { rerender } = render( - , + , ); const buttons = screen.getAllByTestId('ockTokenBalanceButton'); expect(buttons[0]).toHaveClass(customClassNames.container); expect(buttons[1]).toHaveClass(customClassNames.container); - rerender( - , - ); + rerender(); const button = screen.getByTestId('ockTokenBalanceButton'); expect(button).toHaveClass(customClassNames.container); }); From c157748c350d669d803ab8f47e09d69b4d371630 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 25 Feb 2025 22:45:14 -0800 Subject: [PATCH 098/115] allow action button on non-actionable balance row --- src/token/components/TokenBalance.tsx | 62 +++++++++++++++++++-------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/src/token/components/TokenBalance.tsx b/src/token/components/TokenBalance.tsx index 4f02754a2b..e7d4232b7f 100644 --- a/src/token/components/TokenBalance.tsx +++ b/src/token/components/TokenBalance.tsx @@ -36,23 +36,11 @@ export function TokenBalance({ /> {onActionPress && ( - + )}
); @@ -71,6 +59,13 @@ export function TokenBalance({ {...contentProps} classNames={classNames} /> + {onActionPress && ( + + )}
); } @@ -144,3 +139,36 @@ function TokenBalanceContent({
); } + +function TokenBalanceActionButton({ + actionText, + onActionPress, + className, +}: Pick & { + className?: string; +}) { + return ( + + ); +} From 35c2c1b184afccb44a8bdc9be28a57224fcb4ed7 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 25 Feb 2025 22:45:20 -0800 Subject: [PATCH 099/115] update tests --- src/token/components/TokenBalance.test.tsx | 8 ++- .../components/WalletAdvancedContent.test.tsx | 61 ++++++++++++++----- .../WalletAdvancedProvider.test.tsx | 12 ++-- .../WalletAdvancedQrReceive.test.tsx | 40 ++++++------ .../components/WalletAdvancedSwap.test.tsx | 31 ++++------ .../WalletAdvancedTokenHoldings.test.tsx | 12 ++-- .../WalletAdvancedTransactionActions.test.tsx | 36 +++++------ .../WalletAdvancedWalletActions.test.tsx | 30 +++------ .../components/SendAmountInput.test.tsx | 48 ++++----------- .../SendAmountInputTypeSwitch.test.tsx | 31 ++++++---- .../components/SendTokenSelector.test.tsx | 5 ++ 11 files changed, 157 insertions(+), 157 deletions(-) diff --git a/src/token/components/TokenBalance.test.tsx b/src/token/components/TokenBalance.test.tsx index fed23a5d6f..e04bed1233 100644 --- a/src/token/components/TokenBalance.test.tsx +++ b/src/token/components/TokenBalance.test.tsx @@ -112,7 +112,13 @@ describe('TokenBalance', () => { render(); const actionButton = screen.getByRole('button', { name: 'Use max' }); - fireEvent.keyDown(actionButton); + fireEvent.keyDown(actionButton, { key: 'Escape' }); + expect(onActionPress).not.toHaveBeenCalled(); + + fireEvent.keyDown(actionButton, { key: ' ' }); + expect(onActionPress).toHaveBeenCalled(); + + fireEvent.keyDown(actionButton, { key: 'Enter' }); expect(onActionPress).toHaveBeenCalled(); }); diff --git a/src/wallet/components/WalletAdvancedContent.test.tsx b/src/wallet/components/WalletAdvancedContent.test.tsx index 158f321707..fc9c3ce8cf 100644 --- a/src/wallet/components/WalletAdvancedContent.test.tsx +++ b/src/wallet/components/WalletAdvancedContent.test.tsx @@ -42,6 +42,14 @@ vi.mock('./WalletAdvancedSwap', () => ({ ), })); +vi.mock('./wallet-advanced-send/components/Send', () => ({ + Send: ({ className }: { className?: string }) => ( +
+ WalletAdvancedSend +
+ ), +})); + vi.mock('./WalletProvider', () => ({ useWalletContext: vi.fn(), WalletProvider: ({ children }: { children: React.ReactNode }) => ( @@ -56,10 +64,8 @@ describe('WalletAdvancedContent', () => { >; const defaultMockUseWalletAdvancedContext = { - showSwap: false, - isSwapClosing: false, - showQr: false, - isQrClosing: false, + activeFeature: null, + isActiveFeatureClosing: false, tokenHoldings: [], animations: { container: '', @@ -250,10 +256,10 @@ describe('WalletAdvancedContent', () => { expect(setIsSubComponentClosing).toHaveBeenCalledWith(false); }); - it('renders WalletAdvancedQrReceive when showQr is true', () => { + it('renders WalletAdvancedQrReceive when activeFeature is qr', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showQr: true, + activeFeature: 'qr', }); render( @@ -269,12 +275,15 @@ describe('WalletAdvancedContent', () => { expect( screen.queryByTestId('ockWalletAdvancedSwap'), ).not.toBeInTheDocument(); + expect( + screen.queryByTestId('ockWalletAdvancedSend'), + ).not.toBeInTheDocument(); }); - it('renders WalletAdvancedSwap when showSwap is true', () => { + it('renders WalletAdvancedSwap when activeFeature is swap', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showSwap: true, + activeFeature: 'swap', }); render( @@ -288,6 +297,31 @@ describe('WalletAdvancedContent', () => { expect( screen.queryByTestId('ockWalletAdvancedQrReceive'), ).not.toBeInTheDocument(); + expect( + screen.queryByTestId('ockWalletAdvancedSend'), + ).not.toBeInTheDocument(); + }); + + it('renders WalletAdvancedSend when activeFeature is send', () => { + mockUseWalletAdvancedContext.mockReturnValue({ + ...defaultMockUseWalletAdvancedContext, + activeFeature: 'send', + }); + + render( + +
WalletAdvancedContent
+
, + ); + + expect(screen.getByTestId('ockWalletAdvancedSend')).toBeDefined(); + expect(screen.queryByTestId('ockWalletAdvancedSend')).toBeInTheDocument(); + expect( + screen.queryByTestId('ockWalletAdvancedQrReceive'), + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId('ockWalletAdvancedSwap'), + ).not.toBeInTheDocument(); }); it('correctly maps token balances to the swap component', () => { @@ -312,7 +346,7 @@ describe('WalletAdvancedContent', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showSwap: true, + activeFeature: 'swap', tokenBalances: mockTokenBalances, }); @@ -349,8 +383,7 @@ describe('WalletAdvancedContent', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showQr: true, - showSwap: false, + activeFeature: 'qr', }); const customClassNames = { @@ -380,8 +413,7 @@ describe('WalletAdvancedContent', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showQr: false, - showSwap: true, + activeFeature: 'swap', }); rerender( @@ -419,8 +451,7 @@ describe('WalletAdvancedContent', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showQr: true, - showSwap: false, + activeFeature: 'qr', }); const customClassNames = { diff --git a/src/wallet/components/WalletAdvancedProvider.test.tsx b/src/wallet/components/WalletAdvancedProvider.test.tsx index da8bbb5262..4885e5c8ee 100644 --- a/src/wallet/components/WalletAdvancedProvider.test.tsx +++ b/src/wallet/components/WalletAdvancedProvider.test.tsx @@ -57,14 +57,10 @@ describe('useWalletAdvancedContext', () => { }); expect(result.current).toEqual({ - showSwap: false, - setShowSwap: expect.any(Function), - isSwapClosing: false, - setIsSwapClosing: expect.any(Function), - showQr: false, - setShowQr: expect.any(Function), - isQrClosing: false, - setIsQrClosing: expect.any(Function), + activeFeature: null, + setActiveFeature: expect.any(Function), + isActiveFeatureClosing: false, + setIsActiveFeatureClosing: expect.any(Function), tokenBalances: expect.any(Array), portfolioFiatValue: expect.any(Number), refetchPortfolioData: expect.any(Function), diff --git a/src/wallet/components/WalletAdvancedQrReceive.test.tsx b/src/wallet/components/WalletAdvancedQrReceive.test.tsx index 8c594dd64f..536bab3bcd 100644 --- a/src/wallet/components/WalletAdvancedQrReceive.test.tsx +++ b/src/wallet/components/WalletAdvancedQrReceive.test.tsx @@ -56,8 +56,10 @@ describe('WalletAdvancedQrReceive', () => { >; const defaultMockUseWalletAdvancedContext = { - showQr: false, - setShowQr: vi.fn(), + activeFeature: null, + setActiveFeature: vi.fn(), + isActiveFeatureClosing: false, + setIsActiveFeatureClosing: vi.fn(), animationClasses: { qr: 'animate-slideInFromLeft', }, @@ -79,7 +81,7 @@ describe('WalletAdvancedQrReceive', () => { it('should render correctly based on isQrClosing state', () => { mockUseWalletAdvancedContext.mockReturnValue({ - isQrClosing: false, + isActiveFeatureClosing: false, }); const { rerender } = render(); @@ -91,7 +93,7 @@ describe('WalletAdvancedQrReceive', () => { ); mockUseWalletAdvancedContext.mockReturnValue({ - isQrClosing: true, + isActiveFeatureClosing: true, }); rerender(); expect(screen.getByTestId('ockWalletAdvancedQrReceive')).toHaveClass( @@ -100,27 +102,23 @@ describe('WalletAdvancedQrReceive', () => { }); it('should close when back button is clicked', () => { - const mockSetShowQr = vi.fn(); - const mockSetIsQrClosing = vi.fn(); mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showQr: true, - setShowQr: mockSetShowQr, - setIsQrClosing: mockSetIsQrClosing, + activeFeature: 'qr', }); const { rerender } = render(); const backButton = screen.getByRole('button', { name: /back button/i }); fireEvent.click(backButton); - expect(mockSetIsQrClosing).toHaveBeenCalledWith(true); + expect( + defaultMockUseWalletAdvancedContext.setIsActiveFeatureClosing, + ).toHaveBeenCalledWith(true); mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showQr: true, - setShowQr: mockSetShowQr, - setIsQrClosing: mockSetIsQrClosing, - isQrClosing: true, + activeFeature: 'qr', + isActiveFeatureClosing: true, }); rerender(); @@ -128,8 +126,12 @@ describe('WalletAdvancedQrReceive', () => { const qrContainer = screen.getByTestId('ockWalletAdvancedQrReceive'); fireEvent.animationEnd(qrContainer); - expect(mockSetShowQr).toHaveBeenCalledWith(false); - expect(mockSetIsQrClosing).toHaveBeenCalledWith(false); + expect( + defaultMockUseWalletAdvancedContext.setActiveFeature, + ).toHaveBeenCalledWith(null); + expect( + defaultMockUseWalletAdvancedContext.setIsActiveFeatureClosing, + ).toHaveBeenCalledWith(false); }); it('should copy address when the copy icon is clicked', async () => { @@ -143,7 +145,7 @@ describe('WalletAdvancedQrReceive', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showQr: true, + activeFeature: 'qr', }); render(); @@ -185,7 +187,7 @@ describe('WalletAdvancedQrReceive', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showQr: true, + activeFeature: 'qr', }); render(); @@ -227,7 +229,7 @@ describe('WalletAdvancedQrReceive', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showQr: true, + activeFeature: 'qr', }); render(); diff --git a/src/wallet/components/WalletAdvancedSwap.test.tsx b/src/wallet/components/WalletAdvancedSwap.test.tsx index 2e1295e952..8d85c93d00 100644 --- a/src/wallet/components/WalletAdvancedSwap.test.tsx +++ b/src/wallet/components/WalletAdvancedSwap.test.tsx @@ -84,9 +84,10 @@ describe('WalletAdvancedSwap', () => { const mockUseAccount = useAccount as ReturnType; const defaultMockUseWalletAdvancedContext = { - showSwap: false, - setShowSwap: vi.fn(), - setIsSwapClosing: vi.fn(), + activeFeature: null, + setActiveFeature: vi.fn(), + isActiveFeatureClosing: false, + setIsActiveFeatureClosing: vi.fn(), animationClasses: { swap: 'animate-slideInFromLeft', }, @@ -145,7 +146,7 @@ describe('WalletAdvancedSwap', () => { it('should render correctly', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showSwap: true, + activeFeature: 'swap', }); render( @@ -163,7 +164,7 @@ describe('WalletAdvancedSwap', () => { it('should render correctly based on isSwapClosing state', () => { mockUseWalletAdvancedContext.mockReturnValue({ - isSwapClosing: false, + isActiveFeatureClosing: false, }); const { rerender } = render( @@ -182,7 +183,7 @@ describe('WalletAdvancedSwap', () => { ); mockUseWalletAdvancedContext.mockReturnValue({ - isSwapClosing: true, + isActiveFeatureClosing: true, }); rerender( { }); it('should close swap when back button is clicked', () => { - const mockSetShowSwap = vi.fn(); - const mockSetIsSwapClosing = vi.fn(); mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showSwap: true, + activeFeature: 'swap', tokenHoldings: [tokens], - setShowSwap: mockSetShowSwap, - setIsSwapClosing: mockSetIsSwapClosing, }); const { rerender } = render( @@ -223,14 +220,12 @@ describe('WalletAdvancedSwap', () => { const backButton = screen.getByRole('button', { name: /back button/i }); fireEvent.click(backButton); - expect(mockSetIsSwapClosing).toHaveBeenCalledWith(true); + expect(defaultMockUseWalletAdvancedContext.setIsActiveFeatureClosing).toHaveBeenCalledWith(true); mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, tokenHoldings: [tokens], - setShowSwap: mockSetShowSwap, - setIsSwapClosing: mockSetIsSwapClosing, - isSwapClosing: true, + isActiveFeatureClosing: true, }); rerender( @@ -247,14 +242,14 @@ describe('WalletAdvancedSwap', () => { const swapContainer = screen.getByTestId('ockWalletAdvancedSwap'); fireEvent.animationEnd(swapContainer); - expect(mockSetShowSwap).toHaveBeenCalledWith(false); - expect(mockSetIsSwapClosing).toHaveBeenCalledWith(false); + expect(defaultMockUseWalletAdvancedContext.setActiveFeature).toHaveBeenCalledWith(null); + expect(defaultMockUseWalletAdvancedContext.setIsActiveFeatureClosing).toHaveBeenCalledWith(false); }); it('should apply custom classNames to all elements', () => { mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - showSwap: true, + activeFeature: 'swap', }); const customClassNames = { diff --git a/src/wallet/components/WalletAdvancedTokenHoldings.test.tsx b/src/wallet/components/WalletAdvancedTokenHoldings.test.tsx index 827c65bf33..79d61c2dc1 100644 --- a/src/wallet/components/WalletAdvancedTokenHoldings.test.tsx +++ b/src/wallet/components/WalletAdvancedTokenHoldings.test.tsx @@ -65,8 +65,8 @@ describe('WalletAdvancedTokenHoldings', () => { 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', chainId: 8453, }, - balance: 0.42, - valueInFiat: 1386, + cryptoBalance: 420000000000000, + fiatBalance: 1386, }, { token: { @@ -78,8 +78,8 @@ describe('WalletAdvancedTokenHoldings', () => { 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', chainId: 8453, }, - balance: 69, - valueInFiat: 69, + cryptoBalance: 69000000, + fiatBalance: 69, }, { token: { @@ -91,8 +91,8 @@ describe('WalletAdvancedTokenHoldings', () => { 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', chainId: 8453, }, - balance: 0.42, - valueInFiat: 1386, + cryptoBalance: 420000000000000, + fiatBalance: 1386, }, ]; diff --git a/src/wallet/components/WalletAdvancedTransactionActions.test.tsx b/src/wallet/components/WalletAdvancedTransactionActions.test.tsx index f9e6d0aad7..896cddb029 100644 --- a/src/wallet/components/WalletAdvancedTransactionActions.test.tsx +++ b/src/wallet/components/WalletAdvancedTransactionActions.test.tsx @@ -38,7 +38,7 @@ describe('WalletAdvancedTransactionActons', () => { const mockUseWalletContext = useWalletContext as ReturnType; const defaultMockUseWalletAdvancedContext = { - setShowSwap: vi.fn(), + setActiveFeature: vi.fn(), animations: { content: '', }, @@ -142,36 +142,34 @@ describe('WalletAdvancedTransactionActons', () => { expect(window.open).not.toHaveBeenCalled(); }); - it('sets showSend to true when the send button is clicked', () => { - const setShowSendMock = vi.fn(); - - mockUseWalletAdvancedContext.mockReturnValue({ - ...defaultMockUseWalletAdvancedContext, - setShowSend: setShowSendMock, - }); + it('sets activeFeature to send when the send button is clicked', () => { + mockUseWalletAdvancedContext.mockReturnValue( + defaultMockUseWalletAdvancedContext, + ); render(); const sendButton = screen.getByRole('button', { name: 'Send' }); fireEvent.click(sendButton); - expect(setShowSendMock).toHaveBeenCalledWith(true); + expect( + defaultMockUseWalletAdvancedContext.setActiveFeature, + ).toHaveBeenCalledWith('send'); }); - it('sets showSwap to true when the swap button is clicked', () => { - const setShowSwapMock = vi.fn(); - - mockUseWalletAdvancedContext.mockReturnValue({ - ...defaultMockUseWalletAdvancedContext, - setShowSwap: setShowSwapMock, - }); + it('sets activeFeature to swap when the swap button is clicked', () => { + mockUseWalletAdvancedContext.mockReturnValue( + defaultMockUseWalletAdvancedContext, + ); render(); const swapButton = screen.getByRole('button', { name: 'Swap' }); fireEvent.click(swapButton); - expect(setShowSwapMock).toHaveBeenCalledWith(true); + expect( + defaultMockUseWalletAdvancedContext.setActiveFeature, + ).toHaveBeenCalledWith('swap'); }); it('renders a placeholder when fetcher is loading', () => { @@ -182,9 +180,7 @@ describe('WalletAdvancedTransactionActons', () => { render(); - const placeholder = screen.getByTestId( - 'ockWalletAdvanced_LoadingPlaceholder', - ); + const placeholder = screen.getByTestId('ockSkeleton'); expect(placeholder).toHaveClass('my-3 h-16 w-80'); }); diff --git a/src/wallet/components/WalletAdvancedWalletActions.test.tsx b/src/wallet/components/WalletAdvancedWalletActions.test.tsx index b3b9a4cf13..7e279bb88f 100644 --- a/src/wallet/components/WalletAdvancedWalletActions.test.tsx +++ b/src/wallet/components/WalletAdvancedWalletActions.test.tsx @@ -43,6 +43,8 @@ describe('WalletAdvancedWalletActions', () => { const mockSendAnalytics = vi.fn(); const defaultMockUseWalletAdvancedContext = { + setActiveFeature: vi.fn(), + refetchPortfolioData: vi.fn(), animations: { content: '', }, @@ -63,12 +65,6 @@ describe('WalletAdvancedWalletActions', () => { const handleCloseMock = vi.fn(); mockUseWalletContext.mockReturnValue({ handleClose: handleCloseMock }); - const setShowQrMock = vi.fn(); - mockUseWalletAdvancedContext.mockReturnValue({ - ...defaultMockUseWalletAdvancedContext, - setShowQr: setShowQrMock, - }); - (useDisconnect as Mock).mockReturnValue({ disconnect: vi.fn(), connectors: [], @@ -93,12 +89,6 @@ describe('WalletAdvancedWalletActions', () => { handleClose: handleCloseMock, }); - const setShowQrMock = vi.fn(); - mockUseWalletAdvancedContext.mockReturnValue({ - ...defaultMockUseWalletAdvancedContext, - setShowQr: setShowQrMock, - }); - const disconnectMock = vi.fn(); (useDisconnect as Mock).mockReturnValue({ disconnect: disconnectMock, @@ -117,10 +107,8 @@ describe('WalletAdvancedWalletActions', () => { }); it('sets showQr to true when qr button is clicked', () => { - const setShowQrMock = vi.fn(); mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - setShowQr: setShowQrMock, }); render(); @@ -128,14 +116,14 @@ describe('WalletAdvancedWalletActions', () => { const qrButton = screen.getByTestId('ockWalletAdvanced_QrButton'); fireEvent.click(qrButton); - expect(setShowQrMock).toHaveBeenCalled(); + expect( + defaultMockUseWalletAdvancedContext.setActiveFeature, + ).toHaveBeenCalledWith('qr'); }); it('refreshes portfolio data when refresh button is clicked', () => { - const refetchPortfolioDataMock = vi.fn(); mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - refetchPortfolioData: refetchPortfolioDataMock, }); render(); @@ -143,7 +131,9 @@ describe('WalletAdvancedWalletActions', () => { const refreshButton = screen.getByTestId('ockWalletAdvanced_RefreshButton'); fireEvent.click(refreshButton); - expect(refetchPortfolioDataMock).toHaveBeenCalled(); + expect( + defaultMockUseWalletAdvancedContext.refetchPortfolioData, + ).toHaveBeenCalled(); }); it('opens transaction history when transactions button is clicked', () => { @@ -250,10 +240,8 @@ describe('WalletAdvancedWalletActions', () => { }); it('sends analytics when QR button is clicked', () => { - const setShowQrMock = vi.fn(); mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - setShowQr: setShowQrMock, }); render(); @@ -270,10 +258,8 @@ describe('WalletAdvancedWalletActions', () => { }); it('sends analytics when refresh button is clicked', () => { - const refetchPortfolioDataMock = vi.fn(); mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, - refetchPortfolioData: refetchPortfolioDataMock, }); render(); diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx index 1ba9e5252e..0d05a2d1ba 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx @@ -2,7 +2,6 @@ import { AmountInput } from '@/internal/components/amount-input/AmountInput'; import { render } from '@testing-library/react'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { SendAmountInput } from './SendAmountInput'; -import { SendAmountInputTypeSwitch } from './SendAmountInputTypeSwitch'; import { useSendContext } from './SendProvider'; vi.mock('@/internal/components/amount-input/AmountInput'); @@ -47,7 +46,12 @@ describe('SendAmountInput', () => { ...defaultContext, }); - render(); + render( + , + ); expect(AmountInput).toHaveBeenCalledWith( { fiatAmount: defaultContext.fiatAmount, @@ -65,26 +69,6 @@ describe('SendAmountInput', () => { ); }); - it('passes correct props to SendAmountInputTypeSwitch', () => { - (useSendContext as Mock).mockReturnValue({ - ...defaultContext, - }); - - render(); - expect(SendAmountInputTypeSwitch).toHaveBeenCalledWith( - { - selectedToken: defaultContext.selectedToken, - fiatAmount: defaultContext.fiatAmount, - cryptoAmount: defaultContext.cryptoAmount, - selectedInputType: defaultContext.selectedInputType, - setSelectedInputType: defaultContext.setSelectedInputType, - exchangeRate: defaultContext.exchangeRate, - exchangeRateLoading: defaultContext.exchangeRateLoading, - }, - {}, - ); - }); - it('handles null/undefined values correctly', () => { (useSendContext as Mock).mockReturnValue({ ...defaultContext, @@ -93,7 +77,12 @@ describe('SendAmountInput', () => { cryptoAmount: null, }); - render(); + render( + , + ); expect(AmountInput).toHaveBeenCalledWith( { fiatAmount: '', @@ -109,18 +98,5 @@ describe('SendAmountInput', () => { }, {}, ); - - expect(SendAmountInputTypeSwitch).toHaveBeenCalledWith( - { - selectedToken: null, - fiatAmount: '', - cryptoAmount: '', - selectedInputType: defaultContext.selectedInputType, - setSelectedInputType: defaultContext.setSelectedInputType, - exchangeRate: defaultContext.exchangeRate, - exchangeRateLoading: defaultContext.exchangeRateLoading, - }, - {}, - ); }); }); diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx index af24dfd58e..18433cbb5b 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx @@ -39,15 +39,16 @@ describe('SendAmountInputTypeSwitch', () => { }); it('passes an error state when exchange rate is invalid', () => { + const mockLoadingDisplay =
test-loading-display
; (useSendContext as Mock).mockReturnValue({ ...defaultContext, exchangeRate: 0, }); - render(); + render(); expect(AmountInputTypeSwitch).toHaveBeenCalledWith( expect.objectContaining({ - loadingDisplay:
test-loading-display
, + loadingDisplay: mockLoadingDisplay, exchangeRate: 0, }), {}, @@ -65,13 +66,15 @@ describe('SendAmountInputTypeSwitch', () => { }); it('passes correct props to AmountInput', () => { - (useSendContext as Mock).mockReturnValue({ - ...defaultContext, - className: 'test-class', - loadingDisplay:
test-loading-display
, - }); + const mockLoadingDisplay =
test-loading-display
; + (useSendContext as Mock).mockReturnValue(defaultContext); - render(); + render( + , + ); expect(AmountInputTypeSwitch).toHaveBeenCalledWith( { asset: defaultContext.selectedToken.symbol, @@ -90,6 +93,9 @@ describe('SendAmountInputTypeSwitch', () => { }); it('handles null/undefined values correctly', () => { + const mockSetSelectedInputType = vi.fn(); + const mockLoadingDisplay =
test-loading-display
; + (useSendContext as Mock).mockReturnValue({ selectedToken: null, fiatAmount: null, @@ -97,10 +103,10 @@ describe('SendAmountInputTypeSwitch', () => { exchangeRate: 3300, exchangeRateLoading: false, selectedInputType: 'fiat', - setSelectedInputType: vi.fn(), + setSelectedInputType: mockSetSelectedInputType, }); - render(); + render(); expect(AmountInputTypeSwitch).toHaveBeenCalledWith( { @@ -111,8 +117,9 @@ describe('SendAmountInputTypeSwitch', () => { exchangeRateLoading: false, currency: 'USD', selectedInputType: 'fiat', - setSelectedInputType: vi.fn(), - className: '', + setSelectedInputType: mockSetSelectedInputType, + className: undefined, + loadingDisplay: mockLoadingDisplay, }, {}, ); diff --git a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx index 0eccfccf57..a4f2b616bf 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx @@ -136,6 +136,11 @@ describe('SendTokenSelector', () => { expect(buttons[0]).toHaveClass(customClassNames.container); expect(buttons[1]).toHaveClass(customClassNames.container); + (useSendContext as Mock).mockReturnValue({ + ...defaultContext, + selectedToken: mockTokenBalances[0], + }); + rerender(); const button = screen.getByTestId('ockTokenBalanceButton'); expect(button).toHaveClass(customClassNames.container); From 49545c0a6be62c22b0d37fd81aa80a486332d7c4 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 25 Feb 2025 22:46:26 -0800 Subject: [PATCH 100/115] fix lints --- src/wallet/components/WalletAdvancedSwap.test.tsx | 12 +++++++++--- .../components/SendTokenSelector.test.tsx | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/wallet/components/WalletAdvancedSwap.test.tsx b/src/wallet/components/WalletAdvancedSwap.test.tsx index 8d85c93d00..2b88753088 100644 --- a/src/wallet/components/WalletAdvancedSwap.test.tsx +++ b/src/wallet/components/WalletAdvancedSwap.test.tsx @@ -220,7 +220,9 @@ describe('WalletAdvancedSwap', () => { const backButton = screen.getByRole('button', { name: /back button/i }); fireEvent.click(backButton); - expect(defaultMockUseWalletAdvancedContext.setIsActiveFeatureClosing).toHaveBeenCalledWith(true); + expect( + defaultMockUseWalletAdvancedContext.setIsActiveFeatureClosing, + ).toHaveBeenCalledWith(true); mockUseWalletAdvancedContext.mockReturnValue({ ...defaultMockUseWalletAdvancedContext, @@ -242,8 +244,12 @@ describe('WalletAdvancedSwap', () => { const swapContainer = screen.getByTestId('ockWalletAdvancedSwap'); fireEvent.animationEnd(swapContainer); - expect(defaultMockUseWalletAdvancedContext.setActiveFeature).toHaveBeenCalledWith(null); - expect(defaultMockUseWalletAdvancedContext.setIsActiveFeatureClosing).toHaveBeenCalledWith(false); + expect( + defaultMockUseWalletAdvancedContext.setActiveFeature, + ).toHaveBeenCalledWith(null); + expect( + defaultMockUseWalletAdvancedContext.setIsActiveFeatureClosing, + ).toHaveBeenCalledWith(false); }); it('should apply custom classNames to all elements', () => { diff --git a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx index a4f2b616bf..85a72061b3 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx @@ -2,8 +2,8 @@ import type { PortfolioTokenWithFiatValue } from '@/api/types'; import { fireEvent, render, screen } from '@testing-library/react'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; -import { SendTokenSelector } from './SendTokenSelector'; import { useSendContext } from './SendProvider'; +import { SendTokenSelector } from './SendTokenSelector'; // Mock the context hook vi.mock('../../WalletAdvancedProvider', () => ({ From 5a404056c1b1bf240861cccb4bc10ad8d2564f95 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 28 Feb 2025 17:01:50 -0800 Subject: [PATCH 101/115] remove theme, import type --- .../wallet-advanced-send/components/Send.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/wallet/components/wallet-advanced-send/components/Send.tsx b/src/wallet/components/wallet-advanced-send/components/Send.tsx index dc7322d3eb..db34c7218a 100644 --- a/src/wallet/components/wallet-advanced-send/components/Send.tsx +++ b/src/wallet/components/wallet-advanced-send/components/Send.tsx @@ -1,8 +1,7 @@ import { Skeleton } from '@/internal/components/Skeleton'; -import { useTheme } from '@/internal/hooks/useTheme'; import { background, border, cn, color } from '@/styles/theme'; -import type { ReactNode } from 'react'; import { ETH_REQUIRED_FOR_SEND } from '../constants'; +import type { SendReact } from '../types'; import { SendAddressSelection } from './SendAddressSelection'; import { SendAmountInput } from './SendAmountInput'; import { SendButton } from './SendButton'; @@ -11,22 +10,15 @@ import { SendHeader } from './SendHeader'; import { SendProvider, useSendContext } from './SendProvider'; import { SendTokenSelector } from './SendTokenSelector'; -type SendReact = { - children?: ReactNode; - className?: string; -}; - export function Send({ children = , className, }: SendReact) { - const componentTheme = useTheme(); - return (
Date: Fri, 28 Feb 2025 17:02:45 -0800 Subject: [PATCH 102/115] add data-testids --- .../wallet-advanced-send/components/SendAddressInput.tsx | 1 + .../components/SendAddressSelector.tsx | 7 ++++++- .../wallet-advanced-send/components/SendHeader.tsx | 6 ++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/wallet/components/wallet-advanced-send/components/SendAddressInput.tsx b/src/wallet/components/wallet-advanced-send/components/SendAddressInput.tsx index b59e6a4b5a..764a95d68a 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAddressInput.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAddressInput.tsx @@ -41,6 +41,7 @@ export function SendAddressInput({ return (
+ + )), +})); + +vi.mock('@/transaction/components/TransactionStatus', () => ({ + TransactionStatus: vi.fn(({ children }) => ( +
{children}
+ )), +})); + +vi.mock('@/transaction/components/TransactionStatusLabel', () => ({ + TransactionStatusLabel: vi.fn(() => ( +
Status
+ )), +})); + +vi.mock('@/transaction/components/TransactionStatusAction', () => ({ + TransactionStatusAction: vi.fn(() => ( +
Action
+ )), +})); + +vi.mock('../utils/defaultSendTxSuccessHandler', () => ({ + defaultSendTxSuccessHandler: vi.fn(() => vi.fn()), +})); + +const mockChain = { + id: 8453, + name: 'Base', + nativeCurrency: { + name: 'Ethereum', + symbol: 'ETH', + decimals: 18, + }, +} as Chain; + +const mockSelectedtoken = { + name: 'USD Coin', + address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' as Address, + symbol: 'USDC', + decimals: 6, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjMZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2', + chainId: 8453, + cryptoBalance: 69000000, + fiatBalance: 69000000, +}; + +describe('SendButton', () => { + const mockUseWalletContext = useWalletContext as ReturnType; + const mockUseWalletAdvancedContext = useWalletAdvancedContext as ReturnType< + typeof vi.fn + >; + const mockUseSendContext = useSendContext as ReturnType; + const mockUseTransactionContext = useTransactionContext as ReturnType< + typeof vi.fn + >; + + const mockWalletContext = { + chain: mockChain, + address: '0x1234567890123456789012345678901234567890', + }; + + const mockWalletAdvancedContext = { + setActiveFeature: vi.fn(), + }; + + const mockSendContext = { + callData: { to: '0x9876543210987654321098765432109876543210', data: '0x' }, + cryptoAmount: '1.0', + selectedToken: mockSelectedtoken, + updateLifecycleStatus: vi.fn(), + }; + + const mockTransactionContext = { + transactionHash: '0xabcdef', + transactionId: '123', + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseWalletContext.mockReturnValue(mockWalletContext); + mockUseWalletAdvancedContext.mockReturnValue(mockWalletAdvancedContext); + mockUseSendContext.mockReturnValue(mockSendContext); + mockUseTransactionContext.mockReturnValue(mockTransactionContext); + }); + + it('renders with default props', () => { + render(); + + expect(Transaction).toHaveBeenCalledWith( + expect.objectContaining({ + isSponsored: false, + chainId: mockChain.id, + calls: [mockSendContext.callData], + }), + {}, + ); + + expect(screen.getByTestId('mock-transaction')).toBeInTheDocument(); + expect(screen.getByTestId('mock-transaction-button')).toBeInTheDocument(); + expect(screen.getByTestId('mock-transaction-status')).toBeInTheDocument(); + expect( + screen.getByTestId('mock-transaction-status-label'), + ).toBeInTheDocument(); + expect( + screen.getByTestId('mock-transaction-status-action'), + ).toBeInTheDocument(); + }); + + it('passes custom label to TransactionButton', () => { + render(); + + expect(TransactionButton).toHaveBeenCalledWith( + expect.objectContaining({ + text: 'Custom Send', + }), + {}, + ); + }); + + it('passes isSponsored prop to Transaction', () => { + render(); + + expect(Transaction).toHaveBeenCalledWith( + expect.objectContaining({ + isSponsored: true, + }), + {}, + ); + }); + + it('passes className to TransactionButton', () => { + render(); + + expect(TransactionButton).toHaveBeenCalledWith( + expect.objectContaining({ + className: 'custom-button-class', + }), + {}, + ); + }); + + it('disables button when input amount is invalid', () => { + mockUseSendContext.mockReturnValue({ + ...mockSendContext, + cryptoAmount: '3.0', // More than balance + }); + + render(); + + expect(TransactionButton).toHaveBeenCalledWith( + expect.objectContaining({ + disabled: true, + }), + {}, + ); + }); + + it('disables button when disabled prop is true', () => { + render(); + + expect(TransactionButton).toHaveBeenCalledWith( + expect.objectContaining({ + disabled: true, + }), + {}, + ); + }); + + it('uses default chain when wallet chain is null', () => { + mockUseWalletContext.mockReturnValue({ + ...mockWalletContext, + chain: null, + }); + + render(); + + expect(Transaction).toHaveBeenCalledWith( + expect.objectContaining({ + chainId: base.id, + }), + {}, + ); + }); + + it('uses empty calls array when callData is null', () => { + mockUseSendContext.mockReturnValue({ + ...mockSendContext, + callData: null, + }); + + render(); + + expect(Transaction).toHaveBeenCalledWith( + expect.objectContaining({ + calls: [], + }), + {}, + ); + }); + + it('sets button label to "Input amount" when cryptoAmount is null', () => { + mockUseSendContext.mockReturnValue({ + ...mockSendContext, + cryptoAmount: null, + }); + + render(); + + expect(TransactionButton).toHaveBeenCalledWith( + expect.objectContaining({ + text: 'Input amount', + }), + {}, + ); + }); + + it('sets button label to "Select token" when selectedToken is null', () => { + mockUseSendContext.mockReturnValue({ + ...mockSendContext, + selectedToken: null, + }); + + render(); + + expect(TransactionButton).toHaveBeenCalledWith( + expect.objectContaining({ + text: 'Select token', + }), + {}, + ); + }); + + it('sets button label to "Insufficient balance" when amount exceeds balance', () => { + mockUseSendContext.mockReturnValue({ + ...mockSendContext, + cryptoAmount: '3.0', // More than the 2 ETH balance + }); + + vi.mocked(parseUnits).mockImplementation(() => { + return 3000000000000000000n; + }); + + render(); + + expect(TransactionButton).toHaveBeenCalledWith( + expect.objectContaining({ + text: 'Insufficient balance', + }), + {}, + ); + }); + + it('sets button label to "Continue" when all conditions are met', () => { + vi.mocked(parseUnits).mockImplementation(() => { + return 1000000n; + }); + + render(); + + expect(TransactionButton).toHaveBeenCalledWith( + expect.objectContaining({ + text: 'Continue', + }), + {}, + ); + }); + + it('calls updateLifecycleStatus when transaction status changes', () => { + render(); + + // Extract the onStatus handler from Transaction props + const { onStatus } = vi.mocked(Transaction).mock.calls[0][0]; + + // Call with valid status + onStatus?.({ statusName: 'transactionPending', statusData: null }); + + expect(mockSendContext.updateLifecycleStatus).toHaveBeenCalledWith({ + statusName: 'transactionPending', + statusData: null, + }); + }); + + it('does not call updateLifecycleStatus for non-tracked statuses', () => { + render(); + + // Extract the onStatus handler from Transaction props + const { onStatus } = vi.mocked(Transaction).mock.calls[0][0]; + + // @ts-expect-error - setting invalid status name for testing + onStatus?.({ statusName: 'someOtherStatus', statusData: null }); + + expect(mockSendContext.updateLifecycleStatus).not.toHaveBeenCalled(); + }); + + it('configures defaultSuccessOverride with correct parameters', () => { + render(); + + expect(defaultSendTxSuccessHandler).toHaveBeenCalledWith({ + transactionId: '123', + transactionHash: '0xabcdef', + senderChain: mockWalletContext.chain, + address: mockWalletContext.address, + onComplete: expect.any(Function), + }); + }); + + it('passes custom overrides to TransactionButton', () => { + const pendingOverride = { text: 'Sending...' }; + const successOverride = { text: 'Sent!' }; + const errorOverride = { text: 'Failed!' }; + + render( + , + ); + + expect(TransactionButton).toHaveBeenCalledWith( + expect.objectContaining({ + pendingOverride, + successOverride, + errorOverride, + }), + {}, + ); + }); + + it('handles null wallet address correctly', () => { + mockUseWalletContext.mockReturnValue({ + ...mockWalletContext, + address: null, + }); + + render(); + + expect(defaultSendTxSuccessHandler).toHaveBeenCalledWith( + expect.objectContaining({ + address: undefined, + }), + ); + }); +}); diff --git a/src/wallet/components/wallet-advanced-send/components/SendHeader.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendHeader.test.tsx new file mode 100644 index 0000000000..bc52d90a4b --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/components/SendHeader.test.tsx @@ -0,0 +1,157 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { SendHeader } from './SendHeader'; +import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; +import { useSendContext } from './SendProvider'; + +vi.mock('../../WalletAdvancedProvider', () => ({ + useWalletAdvancedContext: vi.fn(), +})); + +vi.mock('./SendProvider', () => ({ + useSendContext: vi.fn(), +})); + +vi.mock('@/internal/components/PressableIcon', () => ({ + PressableIcon: vi.fn(({ children, onClick, className }) => ( + + )), +})); + +vi.mock('@/internal/svg/backArrowSvg', () => ({ + backArrowSvg:
Back Arrow
, +})); + +vi.mock('@/internal/svg/closeSvg', () => ({ + CloseSvg: () =>
Close
, +})); + +describe('SendHeader', () => { + const mockUseWalletAdvancedContext = useWalletAdvancedContext as ReturnType< + typeof vi.fn + >; + const mockUseSendContext = useSendContext as ReturnType; + + const mockWalletAdvancedContext = { + setActiveFeature: vi.fn(), + }; + + const mockSendContext = { + selectedRecipientAddress: { value: null, display: null }, + selectedToken: null, + handleResetTokenSelection: vi.fn(), + handleRecipientInputChange: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseWalletAdvancedContext.mockReturnValue(mockWalletAdvancedContext); + mockUseSendContext.mockReturnValue(mockSendContext); + }); + + it('renders with default label', () => { + render(); + + expect(screen.getByText('Send')).toBeInTheDocument(); + expect(screen.getByTestId('mock-close-svg')).toBeInTheDocument(); + expect(screen.queryByTestId('mock-back-arrow')).not.toBeInTheDocument(); + }); + + it('renders with custom label', () => { + render(); + + expect(screen.getByText('Custom Send')).toBeInTheDocument(); + }); + + it('applies custom classNames', () => { + const customClassNames = { + container: 'custom-container', + label: 'custom-label', + close: 'custom-close', + back: 'custom-back', + }; + + mockUseSendContext.mockReturnValue({ + ...mockSendContext, + selectedRecipientAddress: { value: '0x123', display: 'user.eth' }, + }); + + render(); + + const container = screen.queryByTestId('ockSendHeader'); + expect(container).toHaveClass('custom-container'); + + const label = screen.queryByTestId('ockSendHeader_label'); + expect(label).toHaveClass('custom-label'); + + const backButton = screen.queryByTestId('ockSendHeader_back'); + expect(backButton?.firstElementChild).toHaveClass('custom-back'); + + const closeButton = screen.queryByTestId('ockSendHeader_close'); + expect(closeButton?.firstElementChild).toHaveClass('custom-close'); + }); + + it('shows back button when recipient address is selected', () => { + mockUseSendContext.mockReturnValue({ + ...mockSendContext, + selectedRecipientAddress: { + value: '0x1234567890123456789012345678901234567890', + display: 'user.eth', + }, + }); + + render(); + + expect(screen.getByTestId('mock-back-arrow')).toBeInTheDocument(); + }); + + it('calls handleClose when close button is clicked', () => { + render(); + + const closeButton = screen.getByTestId('mock-pressable-icon'); + fireEvent.click(closeButton); + + expect(mockWalletAdvancedContext.setActiveFeature).toHaveBeenCalledWith( + null, + ); + }); + + it('calls handleResetTokenSelection when back button is clicked and token is selected', () => { + mockUseSendContext.mockReturnValue({ + ...mockSendContext, + selectedRecipientAddress: { value: '0x123', display: 'user.eth' }, + selectedToken: { symbol: 'ETH' }, + }); + + render(); + + const backButton = screen.getByTestId('mock-back-arrow'); + fireEvent.click(backButton); + + expect(mockSendContext.handleResetTokenSelection).toHaveBeenCalled(); + expect(mockSendContext.handleRecipientInputChange).not.toHaveBeenCalled(); + }); + + it('calls handleRecipientInputChange when back button is clicked and no token is selected', () => { + mockUseSendContext.mockReturnValue({ + ...mockSendContext, + selectedRecipientAddress: { value: '0x123', display: 'user.eth' }, + selectedToken: null, + }); + + render(); + + const backButton = screen.getByTestId('mock-back-arrow'); + fireEvent.click(backButton); + + expect(mockSendContext.handleRecipientInputChange).toHaveBeenCalled(); + expect(mockSendContext.handleResetTokenSelection).not.toHaveBeenCalled(); + }); +}); diff --git a/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts b/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts deleted file mode 100644 index 6c49cbf080..0000000000 --- a/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -// import type { PortfolioTokenWithFiatValue } from '@/api/types'; -// import { describe, expect, it } from 'vitest'; -// import { getDefaultSendButtonLabel } from './getDefaultSendButtonLabel'; - -// describe('getDefaultSendButtonLabel', () => { -// const mockToken = { -// address: '0x1230000000000000000000000000000000000000', -// symbol: 'TEST', -// name: 'Test Token', -// decimals: 18, -// cryptoBalance: 1000000000000000, -// fiatBalance: 100, -// image: 'test.png', -// chainId: 8453, -// } as PortfolioTokenWithFiatValue; - -// it('returns "Input amount" when cryptoAmount is null', () => { -// expect(getDefaultSendButtonLabel(null, mockToken)).toBe('Input amount'); -// }); - -// it('returns "Input amount" when cryptoAmount is empty string', () => { -// expect(getDefaultSendButtonLabel('', mockToken)).toBe('Input amount'); -// }); - -// it('returns "Select token" when token is null', () => { -// expect(getDefaultSendButtonLabel('1.0', null)).toBe('Select token'); -// }); - -// it('returns "Insufficient balance" when amount exceeds balance', () => { -// expect(getDefaultSendButtonLabel('2.0', mockToken)).toBe( -// 'Insufficient balance', -// ); -// }); - -// it('returns "Continue" when amount is valid and within balance', () => { -// expect(getDefaultSendButtonLabel('0.0001', mockToken)).toBe('Continue'); -// }); - -// it('returns "Continue" when amount equals balance exactly', () => { -// expect(getDefaultSendButtonLabel('0.001', mockToken)).toBe('Continue'); -// }); -// }); From 9b7c0d06e907a5c6a292796c473408d6c2d41739 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Sun, 2 Mar 2025 09:31:57 -0800 Subject: [PATCH 106/115] add getPriceQuote --- src/api/getPriceQuote.test.ts | 106 +++++++++++++++++++++++++ src/api/getPriceQuote.ts | 59 ++++++++++++++ src/api/types.ts | 33 ++++++++ src/core/network/definitions/wallet.ts | 1 + 4 files changed, 199 insertions(+) create mode 100644 src/api/getPriceQuote.test.ts create mode 100644 src/api/getPriceQuote.ts diff --git a/src/api/getPriceQuote.test.ts b/src/api/getPriceQuote.test.ts new file mode 100644 index 0000000000..6df5ece562 --- /dev/null +++ b/src/api/getPriceQuote.test.ts @@ -0,0 +1,106 @@ +import { RequestContext } from '@/core/network/constants'; +import { CDP_GET_PRICE_QUOTE } from '@/core/network/definitions/wallet'; +import { sendRequest } from '@/core/network/request'; +import { type Mock, describe, expect, it, vi } from 'vitest'; +import { getPriceQuote } from './getPriceQuote'; +import type { + GetPriceQuoteParams, + GetPriceQuoteResponse, + PriceQuoteToken, +} from './types'; + +vi.mock('@/core/network/request', () => ({ + sendRequest: vi.fn(), +})); + +const mockTokens: PriceQuoteToken[] = [ + 'ETH', + '0x1234567890123456789012345678901234567890', +]; + +const mockParams: GetPriceQuoteParams = { + tokens: mockTokens, +}; + +const mockSuccessResponse: GetPriceQuoteResponse = { + priceQuote: [ + { + name: 'Ethereum', + symbol: 'ETH', + contractAddress: '', + price: '2400', + timestamp: 1714761600, + }, + { + name: 'Test Token', + symbol: 'TEST', + contractAddress: '0x1234567890123456789012345678901234567890', + price: '3.14', + timestamp: 1714761600, + }, + ], +}; + +describe('getPriceQuote', () => { + const mockSendRequest = sendRequest as Mock; + + it('should return the price quote for a successful request', async () => { + mockSendRequest.mockResolvedValueOnce({ + result: mockSuccessResponse, + }); + + const result = await getPriceQuote(mockParams); + + expect(result).toEqual(mockSuccessResponse); + expect(mockSendRequest).toHaveBeenCalledWith( + CDP_GET_PRICE_QUOTE, + [mockParams], + RequestContext.API, + ); + }); + + it('should return an error if no tokens are provided', async () => { + const result = await getPriceQuote({ + tokens: [], + }); + + expect(result).toEqual({ + code: 'INVALID_INPUT', + error: 'Invalid input: tokens must be an array of at least one token', + message: '', + }); + }); + + it('should handle API error response', async () => { + const mockError = { + code: 500, + error: 'Internal Server Error', + message: 'Internal Server Error', + }; + + mockSendRequest.mockResolvedValueOnce({ + error: mockError, + }); + + const result = await getPriceQuote(mockParams); + + expect(result).toEqual({ + code: `${mockError.code}`, + error: 'Error fetching price quote', + message: mockError.message, + }); + }); + + it('should handle unexpected errors', async () => { + const errorMessage = 'Network Error'; + mockSendRequest.mockRejectedValueOnce(new Error(errorMessage)); + + const result = await getPriceQuote(mockParams); + + expect(result).toEqual({ + code: 'UNCAUGHT_PRICE_QUOTE_ERROR', + error: 'Something went wrong', + message: `Error fetching price quote: Error: ${errorMessage}`, + }); + }); +}); diff --git a/src/api/getPriceQuote.ts b/src/api/getPriceQuote.ts new file mode 100644 index 0000000000..6f0ce02878 --- /dev/null +++ b/src/api/getPriceQuote.ts @@ -0,0 +1,59 @@ +import { RequestContext } from '@/core/network/constants'; +import { CDP_GET_PRICE_QUOTE } from '@/core/network/definitions/wallet'; +import { sendRequest } from '@/core/network/request'; + +import type { GetPriceQuoteParams, GetPriceQuoteResponse } from './types'; + +/** + * Retrieves a price quote for a token + * + * @param params - The parameters for the price quote. The property `tokens` + * must be an array of contract addresses or 'ETH'. + * @param _context - The context in which the price quote is retrieved + * @returns The price quote for the token + */ +export async function getPriceQuote( + params: GetPriceQuoteParams, + _context: RequestContext = RequestContext.API, +): Promise { + const apiParams = validateGetPriceQuoteParams(params); + if ('error' in apiParams) { + return apiParams; + } + + try { + const res = await sendRequest( + CDP_GET_PRICE_QUOTE, + [apiParams], + _context, + ); + if (res.error) { + return { + code: `${res.error.code}`, + error: 'Error fetching price quote', + message: res.error.message, + }; + } + return res.result; + } catch (error) { + return { + code: 'UNCAUGHT_PRICE_QUOTE_ERROR', + error: 'Something went wrong', + message: `Error fetching price quote: ${error}`, + }; + } +} + +function validateGetPriceQuoteParams(params: GetPriceQuoteParams) { + const { tokens } = params; + + if (!tokens || tokens.length === 0) { + return { + code: 'INVALID_INPUT', + error: 'Invalid input: tokens must be an array of at least one token', + message: '', + }; + } + + return params; +} diff --git a/src/api/types.ts b/src/api/types.ts index 6d588633c2..651b97b800 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -384,3 +384,36 @@ export type BuildSendTransactionParams = { * Note: exported as public Type */ export type BuildSendTransactionResponse = Call | APIError; + +export type PriceQuoteToken = Address | 'ETH'; + +/** + * Note: exported as public Type + */ +export type GetPriceQuoteParams = { + /** The token to get the price quote for */ + tokens: PriceQuoteToken[]; +}; + +type PriceQuote = { + /** The name of the token */ + name: string | ''; + /** The symbol of the token */ + symbol: string | ''; + /** The contract address of the token */ + contractAddress: Address | ''; + /** The price of the token */ + price: string | ''; + /** The timestamp of the price quote */ + timestamp: number | 0; +}; + +/** + * Note: exported as public Type + */ +export type GetPriceQuoteResponse = + | { + /** The array of price quotes for the tokens */ + priceQuote: PriceQuote[]; + } + | APIError; diff --git a/src/core/network/definitions/wallet.ts b/src/core/network/definitions/wallet.ts index 129cd6573e..131e1409f6 100644 --- a/src/core/network/definitions/wallet.ts +++ b/src/core/network/definitions/wallet.ts @@ -1 +1,2 @@ export const CDP_GET_PORTFOLIO_TOKEN_BALANCES = 'cdp_getTokensForAddresses'; +export const CDP_GET_PRICE_QUOTE = 'cdp_getPriceQuote'; From d8a948dffafa209dfabd27a4d224673c6dc091b8 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 3 Mar 2025 10:05:10 -0800 Subject: [PATCH 107/115] export getPriceQuote --- src/api/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/index.ts b/src/api/index.ts index c60f6aedb2..c2f0c1a92d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -7,6 +7,7 @@ export { getSwapQuote } from './getSwapQuote'; export { getTokenDetails } from './getTokenDetails'; export { getTokens } from './getTokens'; export { getPortfolios } from './getPortfolios'; +export { getPriceQuote } from './getPriceQuote'; export type { APIError, BuildMintTransactionParams, @@ -18,6 +19,8 @@ export type { BuildSwapTransactionResponse, GetMintDetailsParams, GetMintDetailsResponse, + GetPriceQuoteParams, + GetPriceQuoteResponse, GetSwapQuoteParams, GetSwapQuoteResponse, GetTokenDetailsParams, From 4207b23b181018b4d6c8ce04cf09dd8846942520 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 3 Mar 2025 10:05:58 -0800 Subject: [PATCH 108/115] useExchangeRate uses getPriceQuote --- src/internal/hooks/useExchangeRate.test.tsx | 94 ++++++++++----------- src/internal/hooks/useExchangeRate.tsx | 35 +++----- 2 files changed, 58 insertions(+), 71 deletions(-) diff --git a/src/internal/hooks/useExchangeRate.test.tsx b/src/internal/hooks/useExchangeRate.test.tsx index 66a99ade24..152f229aa6 100644 --- a/src/internal/hooks/useExchangeRate.test.tsx +++ b/src/internal/hooks/useExchangeRate.test.tsx @@ -1,12 +1,12 @@ -import { getSwapQuote } from '@/api'; -import type { Token } from '@/token'; -import { ethToken, usdcToken } from '@/token/constants'; +import { getPriceQuote } from '@/api'; +import type { PriceQuoteToken } from '@/api/types'; +import { ethToken } from '@/token/constants'; import { renderHook, waitFor } from '@testing-library/react'; import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { useExchangeRate } from './useExchangeRate'; vi.mock('@/api', () => ({ - getSwapQuote: vi.fn(), + getPriceQuote: vi.fn(), })); describe('useExchangeRate', () => { @@ -18,7 +18,7 @@ describe('useExchangeRate', () => { const mockSetExchangeRate = vi.fn(); const { result } = renderHook(() => useExchangeRate({ - token: undefined as unknown as Token, + token: undefined as unknown as PriceQuoteToken, selectedInputType: 'crypto', setExchangeRate: mockSetExchangeRate, setExchangeRateLoading: vi.fn(), @@ -30,35 +30,25 @@ describe('useExchangeRate', () => { expect(mockSetExchangeRate).not.toHaveBeenCalled(); }); - it('should return 1 if a token is usdc', async () => { - const mockSetExchangeRate = vi.fn(); - renderHook(() => - useExchangeRate({ - token: usdcToken, - selectedInputType: 'crypto', - setExchangeRate: mockSetExchangeRate, - setExchangeRateLoading: vi.fn(), - }), - ); - - expect(mockSetExchangeRate).toHaveBeenCalledWith(1); - }); - it('should set the correct exchange rate when the selected input type is crypto', async () => { const mockSetExchangeRate = vi.fn(); const mockSetExchangeRateLoading = vi.fn(); - (getSwapQuote as Mock).mockResolvedValue({ - fromAmountUSD: '200', - toAmount: '100000000', - to: { - decimals: 18, - }, + (getPriceQuote as Mock).mockResolvedValue({ + priceQuote: [ + { + name: 'ETH', + symbol: 'ETH', + contractAddress: '', + price: '2400', + timestamp: 1714761600, + }, + ], }); renderHook(() => useExchangeRate({ - token: ethToken, + token: ethToken.symbol as PriceQuoteToken, selectedInputType: 'crypto', setExchangeRate: mockSetExchangeRate, setExchangeRateLoading: mockSetExchangeRateLoading, @@ -66,7 +56,7 @@ describe('useExchangeRate', () => { ); await waitFor(() => { - expect(mockSetExchangeRate).toHaveBeenCalledWith(1 / 200); + expect(mockSetExchangeRate).toHaveBeenCalledWith(1 / 2400); }); }); @@ -74,17 +64,21 @@ describe('useExchangeRate', () => { const mockSetExchangeRate = vi.fn(); const mockSetExchangeRateLoading = vi.fn(); - (getSwapQuote as Mock).mockResolvedValue({ - fromAmountUSD: '200', - toAmount: '100000000', - to: { - decimals: 18, - }, + (getPriceQuote as Mock).mockResolvedValue({ + priceQuote: [ + { + name: 'ETH', + symbol: 'ETH', + contractAddress: '', + price: '2400', + timestamp: 1714761600, + }, + ], }); renderHook(() => useExchangeRate({ - token: ethToken, + token: ethToken.symbol as PriceQuoteToken, selectedInputType: 'fiat', setExchangeRate: mockSetExchangeRate, setExchangeRateLoading: mockSetExchangeRateLoading, @@ -92,7 +86,7 @@ describe('useExchangeRate', () => { ); await waitFor(() => { - expect(mockSetExchangeRate).toHaveBeenCalledWith(100000000 / 10 ** 18); + expect(mockSetExchangeRate).toHaveBeenCalledWith(2400); }); }); @@ -101,13 +95,13 @@ describe('useExchangeRate', () => { const mockSetExchangeRateLoading = vi.fn(); const consoleSpy = vi.spyOn(console, 'error'); - (getSwapQuote as Mock).mockResolvedValue({ + (getPriceQuote as Mock).mockResolvedValue({ error: 'test error', }); renderHook(() => useExchangeRate({ - token: ethToken, + token: ethToken.symbol as PriceQuoteToken, selectedInputType: 'fiat', setExchangeRate: mockSetExchangeRate, setExchangeRateLoading: mockSetExchangeRateLoading, @@ -116,7 +110,7 @@ describe('useExchangeRate', () => { await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith( - 'Error fetching exchange rate:', + 'Error fetching price quote:', 'test error', ); expect(mockSetExchangeRate).not.toHaveBeenCalled(); @@ -128,11 +122,11 @@ describe('useExchangeRate', () => { const mockSetExchangeRateLoading = vi.fn(); const consoleSpy = vi.spyOn(console, 'error'); - (getSwapQuote as Mock).mockRejectedValue(new Error('test error')); + (getPriceQuote as Mock).mockRejectedValue(new Error('test error')); renderHook(() => useExchangeRate({ - token: ethToken, + token: ethToken.symbol as PriceQuoteToken, selectedInputType: 'fiat', setExchangeRate: mockSetExchangeRate, setExchangeRateLoading: mockSetExchangeRateLoading, @@ -141,7 +135,7 @@ describe('useExchangeRate', () => { await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith( - 'Uncaught error fetching exchange rate:', + 'Uncaught error fetching price quote:', expect.any(Error), ); expect(mockSetExchangeRate).not.toHaveBeenCalled(); @@ -152,17 +146,21 @@ describe('useExchangeRate', () => { const mockSetExchangeRate = vi.fn(); const mockSetExchangeRateLoading = vi.fn(); - (getSwapQuote as Mock).mockResolvedValue({ - fromAmountUSD: '1', - toAmount: '1', - to: { - decimals: 18, - }, + (getPriceQuote as Mock).mockResolvedValue({ + priceQuote: [ + { + name: 'ETH', + symbol: 'ETH', + contractAddress: '', + price: '2400', + timestamp: 1714761600, + }, + ], }); renderHook(() => useExchangeRate({ - token: ethToken, + token: ethToken.symbol as PriceQuoteToken, selectedInputType: 'crypto', setExchangeRate: mockSetExchangeRate, setExchangeRateLoading: mockSetExchangeRateLoading, diff --git a/src/internal/hooks/useExchangeRate.tsx b/src/internal/hooks/useExchangeRate.tsx index 467528de40..bbaf9b2ddb 100644 --- a/src/internal/hooks/useExchangeRate.tsx +++ b/src/internal/hooks/useExchangeRate.tsx @@ -1,12 +1,11 @@ -import { getSwapQuote } from '@/api'; +import { getPriceQuote } from '@/api'; import { RequestContext } from '@/core/network/constants'; import { isApiError } from '@/internal/utils/isApiResponseError'; -import type { Token } from '@/token'; -import { usdcToken } from '@/token/constants'; +import type { PriceQuoteToken } from '@/api/types'; import type { Dispatch, SetStateAction } from 'react'; type UseExchangeRateParams = { - token: Token; + token: PriceQuoteToken; selectedInputType: 'crypto' | 'fiat'; setExchangeRate: Dispatch>; setExchangeRateLoading?: Dispatch>; @@ -22,37 +21,27 @@ export async function useExchangeRate({ return; } - if (token.address.toLowerCase() === usdcToken.address.toLowerCase()) { - setExchangeRate(1); - return; - } - setExchangeRateLoading?.(true); - const fromToken = selectedInputType === 'crypto' ? token : usdcToken; - const toToken = selectedInputType === 'crypto' ? usdcToken : token; - try { - const response = await getSwapQuote( - { - amount: '1', // hardcoded amount of 1 because we only need the exchange rate - from: fromToken, - to: toToken, - useAggregator: false, - }, + const response = await getPriceQuote( + { tokens: [token] }, RequestContext.Wallet, ); if (isApiError(response)) { - console.error('Error fetching exchange rate:', response.error); + console.error('Error fetching price quote:', response.error); return; } + const priceQuote = response.priceQuote[0]; + const rate = selectedInputType === 'crypto' - ? 1 / Number(response.fromAmountUSD) - : Number(response.toAmount) / 10 ** response.to.decimals; + ? 1 / Number(priceQuote.price) + : Number(priceQuote.price); + setExchangeRate(rate); } catch (error) { - console.error('Uncaught error fetching exchange rate:', error); + console.error('Uncaught error fetching price quote:', error); } finally { setExchangeRateLoading?.(false); } From e83fcd8dd480302ac8f1cef6ad3f27017f2db6ae Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 3 Mar 2025 10:06:08 -0800 Subject: [PATCH 109/115] add and update tests --- .../components/SendAddressInput.test.tsx | 11 +- .../components/SendButton.test.tsx | 1 - .../components/SendProvider.test.tsx | 670 ++++++++++++++++++ .../components/SendProvider.tsx | 43 +- 4 files changed, 705 insertions(+), 20 deletions(-) create mode 100644 src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx diff --git a/src/wallet/components/wallet-advanced-send/components/SendAddressInput.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendAddressInput.test.tsx index dbddf32530..9816f1ebd7 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAddressInput.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAddressInput.test.tsx @@ -107,7 +107,6 @@ describe('SendAddressInput', () => { const { onFocus } = vi.mocked(TextInput).mock.calls[0][0]; - // Call the onFocus handler onFocus?.({} as React.FocusEvent); expect(props.handleRecipientInputChange).toHaveBeenCalled(); @@ -116,10 +115,8 @@ describe('SendAddressInput', () => { it('does not call handleRecipientInputChange on focus when selectedRecipientAddress.value does not exist', () => { render(); - // Extract the onFocus handler const { onFocus } = vi.mocked(TextInput).mock.calls[0][0]; - // Call the onFocus handler onFocus?.({} as React.FocusEvent); expect(mockProps.handleRecipientInputChange).not.toHaveBeenCalled(); @@ -128,10 +125,8 @@ describe('SendAddressInput', () => { it('calls setRecipientInput when TextInput setValue is called', () => { render(); - // Extract the setValue handler const { setValue } = vi.mocked(TextInput).mock.calls[0][0]; - // Call the setValue handler setValue?.('new-input'); expect(mockProps.setRecipientInput).toHaveBeenCalledWith('new-input'); @@ -145,11 +140,9 @@ describe('SendAddressInput', () => { render(); - // Extract the onChange handler const { onChange } = vi.mocked(TextInput).mock.calls[0][0]; - // Call the onChange handler - onChange && (await onChange('new-input')); + await onChange?.('new-input'); expect(resolveAddressInput).toHaveBeenCalledWith(null, 'new-input'); expect(mockProps.setValidatedInput).toHaveBeenCalledWith({ @@ -165,10 +158,8 @@ describe('SendAddressInput', () => { render(); - // Extract the inputValidator const { inputValidator } = vi.mocked(TextInput).mock.calls[0][0]; - // Call the validator inputValidator?.('test-input'); expect(validateAddressInput).toHaveBeenCalledWith('test-input'); diff --git a/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx index 363fe5beb7..4f8a96066e 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx @@ -329,7 +329,6 @@ describe('SendButton', () => { it('does not call updateLifecycleStatus for non-tracked statuses', () => { render(); - // Extract the onStatus handler from Transaction props const { onStatus } = vi.mocked(Transaction).mock.calls[0][0]; // @ts-expect-error - setting invalid status name for testing diff --git a/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx new file mode 100644 index 0000000000..2d4ccb24d6 --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx @@ -0,0 +1,670 @@ +import type { APIError } from '@/api/types'; +import { render, renderHook, act } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { SendProvider, useSendContext } from './SendProvider'; +import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; +import { useSendTransaction } from '@/internal/hooks/useSendTransaction'; +import { formatUnits } from 'viem'; +import { useExchangeRate } from '@/internal/hooks/useExchangeRate'; + +// Mock dependencies +vi.mock('../../WalletAdvancedProvider', () => ({ + useWalletAdvancedContext: vi.fn(), +})); + +vi.mock('@/internal/hooks/useExchangeRate', () => ({ + useExchangeRate: vi.fn().mockReturnValue(Promise.resolve()), +})); + +vi.mock('@/internal/hooks/useSendTransaction', () => ({ + useSendTransaction: vi.fn(), +})); + +vi.mock('viem', () => ({ + formatUnits: vi.fn(), +})); + +describe('useSendContext', () => { + const mockUseWalletAdvancedContext = useWalletAdvancedContext as ReturnType< + typeof vi.fn + >; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseWalletAdvancedContext.mockReturnValue({ + tokenBalances: [ + { + address: '', + symbol: 'ETH', + decimals: 18, + cryptoBalance: '2000000000000000000', + fiatBalance: 4000, + }, + ], + }); + + vi.mocked(formatUnits).mockReturnValue('2'); + vi.mocked(useSendTransaction).mockReturnValue({ + to: '0x1234', + data: '0x', + }); + }); + + it('should provide send context when used within provider', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + expect(result.current).toEqual({ + isInitialized: expect.any(Boolean), + lifecycleStatus: expect.any(Object), + updateLifecycleStatus: expect.any(Function), + ethBalance: expect.any(Number), + selectedRecipientAddress: expect.any(Object), + handleAddressSelection: expect.any(Function), + selectedToken: null, + handleRecipientInputChange: expect.any(Function), + handleTokenSelection: expect.any(Function), + handleResetTokenSelection: expect.any(Function), + fiatAmount: null, + handleFiatAmountChange: expect.any(Function), + cryptoAmount: null, + handleCryptoAmountChange: expect.any(Function), + exchangeRate: expect.any(Number), + exchangeRateLoading: expect.any(Boolean), + selectedInputType: 'crypto', + setSelectedInputType: expect.any(Function), + callData: null, + }); + }); + + it('should throw an error when used outside of SendProvider', () => { + const TestComponent = () => { + useSendContext(); + return null; + }; + + const originalError = console.error; + console.error = vi.fn(); + + expect(() => { + render(); + }).toThrow(); + + console.error = originalError; + }); + + it('should initialize and set lifecycle status when the user has an ETH balance', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + expect(result.current.ethBalance).toBe(2); + expect(result.current.lifecycleStatus.statusName).toBe('selectingAddress'); + }); + + it('should initialize and set lifecycle status when the user does not have an ETH balance', () => { + mockUseWalletAdvancedContext.mockReturnValue({ + tokenBalances: [ + { + address: '0x0000000000000000000000000000000000000000', + symbol: 'USDC', + decimals: 6, + cryptoBalance: '2000000000000000000', + fiatBalance: 4000, + }, + ], + }); + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + expect(result.current.ethBalance).toBe(0); + expect(result.current.lifecycleStatus.statusName).toBe('fundingWallet'); + }); + + it('should handle address selection', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + act(() => { + result.current.handleAddressSelection({ + display: 'user.eth', + value: '0x1234', + }); + }); + + expect(result.current.selectedRecipientAddress).toEqual({ + display: 'user.eth', + value: '0x1234', + }); + expect(result.current.lifecycleStatus.statusName).toBe('selectingToken'); + }); + + it('should handle recipient input change', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + act(() => { + result.current.handleAddressSelection({ + display: 'user.eth', + value: '0x1234', + }); + }); + + expect(result.current.selectedRecipientAddress).toEqual({ + display: 'user.eth', + value: '0x1234', + }); + + act(() => { + result.current.handleRecipientInputChange(); + }); + + expect(result.current.selectedRecipientAddress).toEqual({ + display: '', + value: null, + }); + + expect(result.current.lifecycleStatus.statusName).toBe('selectingAddress'); + expect(result.current.lifecycleStatus.statusData).toEqual({ + isMissingRequiredField: true, + }); + }); + + it('should handle token selection', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const token = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleTokenSelection(token); + }); + + expect(result.current.selectedToken).toEqual(token); + expect(result.current.lifecycleStatus.statusName).toBe('amountChange'); + }); + + it('should handle reset token selection', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const token = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleTokenSelection(token); + result.current.handleResetTokenSelection(); + }); + + expect(result.current.selectedToken).toBeNull(); + expect(result.current.fiatAmount).toBeNull(); + expect(result.current.cryptoAmount).toBeNull(); + expect(result.current.lifecycleStatus.statusName).toBe('selectingToken'); + }); + + it('should handle crypto amount change', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const token = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleTokenSelection(token); + result.current.handleCryptoAmountChange('1.0'); + }); + + expect(result.current.cryptoAmount).toBe('1.0'); + expect(result.current.lifecycleStatus.statusName).toBe('amountChange'); + }); + + it('should handle fiat amount change', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const token = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleTokenSelection(token); + result.current.handleFiatAmountChange('1000'); + }); + + expect(result.current.fiatAmount).toBe('1000'); + expect(result.current.lifecycleStatus.statusName).toBe('amountChange'); + }); + + it('should handle BigInt conversion in crypto amount change', () => { + const formatUnitsSpy = vi.mocked(formatUnits); + formatUnitsSpy.mockClear(); + + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const token = { + name: 'Test Token', + symbol: 'TEST', + address: '0x123' as const, + decimals: 18, + cryptoBalance: 100000000000000, + fiatBalance: 1000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleTokenSelection(token); + result.current.handleCryptoAmountChange('1.0'); + }); + + expect(formatUnitsSpy).toHaveBeenCalledWith(expect.any(BigInt), 18); + + const tokenWithNullValues = { + name: 'Test Token', + symbol: 'TEST', + address: '0x123' as const, + decimals: undefined as unknown as number, + cryptoBalance: undefined as unknown as number, + fiatBalance: undefined as unknown as number, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleTokenSelection(tokenWithNullValues); + }); + + formatUnitsSpy.mockClear(); + + act(() => { + result.current.handleCryptoAmountChange('1.0'); + }); + + expect(formatUnitsSpy).toHaveBeenCalledWith(BigInt(0), 0); + }); + + it('should set sufficientBalance correctly when token has fiat balance', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + act(() => { + result.current.handleFiatAmountChange('1000'); + }); + + expect(result.current.lifecycleStatus.statusData?.sufficientBalance).toBe( + false, + ); + + const token = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleTokenSelection(token); + }); + + act(() => { + result.current.handleFiatAmountChange('3999.99'); + }); + expect(result.current.fiatAmount).toBe('3999.99'); + // @ts-ignore - test is not type narrowing + expect(result.current.lifecycleStatus.statusData?.sufficientBalance).toBe( + true, + ); + + act(() => { + result.current.handleFiatAmountChange('4000'); + }); + expect(result.current.fiatAmount).toBe('4000'); + // @ts-ignore - test is not type narrowing + expect(result.current.lifecycleStatus.statusData?.sufficientBalance).toBe( + true, + ); + + act(() => { + result.current.handleFiatAmountChange('4000.01'); + }); + expect(result.current.fiatAmount).toBe('4000.01'); + // @ts-ignore - test is not type narrowing + expect(result.current.lifecycleStatus.statusData?.sufficientBalance).toBe( + false, + ); + }); + + it('should set sufficientBalance correctly in handleFiatAmountChange', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + act(() => { + result.current.handleFiatAmountChange('1000'); + }); + // @ts-ignore - test is not type narrowing + expect(result.current.lifecycleStatus.statusData?.sufficientBalance).toBe( + false, + ); + + const tokenWithNullFiatBalance = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: undefined as unknown as number, + chainId: 8453, + image: '', + }; + + const tokenWithoutBalance = { + ...tokenWithNullFiatBalance, + }; + + act(() => { + result.current.handleTokenSelection(tokenWithoutBalance); + result.current.handleFiatAmountChange('100'); + }); + expect(result.current.fiatAmount).toBe('100'); + // @ts-ignore - test is not type narrowing + expect(result.current.lifecycleStatus.statusData?.sufficientBalance).toBe( + false, + ); + }); + + it('should handle input type change', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + act(() => { + result.current.setSelectedInputType('fiat'); + }); + + expect(result.current.selectedInputType).toBe('fiat'); + }); + + it('should call useExchangeRate with correct parameters when ETH token is selected', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const ethToken = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleTokenSelection(ethToken); + }); + + expect(useExchangeRate).toHaveBeenCalledWith({ + token: 'ETH', + selectedInputType: 'crypto', + setExchangeRate: expect.any(Function), + setExchangeRateLoading: expect.any(Function), + }); + }); + + it('should call useExchangeRate with token address for non-ETH tokens', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const usdcToken = { + name: 'USD Coin', + symbol: 'USDC', + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as const, + decimals: 6, + cryptoBalance: 5000000, + fiatBalance: 5, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleTokenSelection(usdcToken); + }); + + expect(useExchangeRate).toHaveBeenCalledWith({ + token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + selectedInputType: 'crypto', + setExchangeRate: expect.any(Function), + setExchangeRateLoading: expect.any(Function), + }); + }); + + it('should call useExchangeRate when selectedInputType changes', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const ethToken = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleTokenSelection(ethToken); + }); + + vi.clearAllMocks(); + + act(() => { + result.current.setSelectedInputType('fiat'); + }); + + expect(useExchangeRate).toHaveBeenCalledWith({ + token: 'ETH', + selectedInputType: 'fiat', + setExchangeRate: expect.any(Function), + setExchangeRateLoading: expect.any(Function), + }); + }); + + it('should not call useExchangeRate if selectedToken has invalid parameters', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const mockToken = { + name: 'Mock Token', + symbol: 'MOCK', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + vi.clearAllMocks(); + + act(() => { + result.current.handleTokenSelection(mockToken); + }); + + expect(useExchangeRate).not.toHaveBeenCalled(); + }); + + it('should fetch transaction data when all required fields are set', () => { + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const token = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleAddressSelection({ + display: 'user.eth', + value: '0x1234', + }); + result.current.handleTokenSelection(token); + result.current.handleCryptoAmountChange('1.0'); + }); + + expect(useSendTransaction).toHaveBeenCalledWith({ + recipientAddress: '0x1234', + token, + amount: '1.0', + }); + + expect(result.current.callData).toEqual({ + to: '0x1234', + data: '0x', + }); + }); + + it('should handle transaction error when useSendTransaction returns an error', () => { + vi.mocked(useSendTransaction).mockReturnValue({ + code: 'SeMSeBC01', // Send module SendButton component 01 error + error: 'Transaction failed', + message: 'Error: Transaction failed', + }); + + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const token = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleAddressSelection({ + display: 'user.eth', + value: '0x1234', + }); + result.current.handleTokenSelection(token); + result.current.handleCryptoAmountChange('1.0'); + }); + + expect(result.current.lifecycleStatus.statusName).toBe('error'); + expect((result.current.lifecycleStatus.statusData as APIError).code).toBe( + 'SeMSeBC01', + ); + expect((result.current.lifecycleStatus.statusData as APIError).error).toBe( + 'Error building send transaction: Transaction failed', + ); + expect( + (result.current.lifecycleStatus.statusData as APIError).message, + ).toBe('Error: Transaction failed'); + }); + + it('should handle transaction error when useSendTransaction throws', () => { + vi.mocked(useSendTransaction).mockImplementation(() => { + throw new Error('Uncaught send transaction error'); + }); + + const { result } = renderHook(() => useSendContext(), { + wrapper: SendProvider, + }); + + const token = { + name: 'Ethereum', + symbol: 'ETH', + address: '' as const, + decimals: 18, + cryptoBalance: 200000000000000, + fiatBalance: 4000, + chainId: 8453, + image: '', + }; + + act(() => { + result.current.handleAddressSelection({ + display: 'user.eth', + value: '0x1234', + }); + result.current.handleTokenSelection(token); + result.current.handleCryptoAmountChange('1.0'); + }); + + expect(result.current.lifecycleStatus.statusName).toBe('error'); + expect((result.current.lifecycleStatus.statusData as APIError).code).toBe( + 'UNCAUGHT_SEND_TRANSACTION_ERROR', + ); + expect((result.current.lifecycleStatus.statusData as APIError).error).toBe( + 'Error building send transaction: Uncaught send transaction error', + ); + expect( + (result.current.lifecycleStatus.statusData as APIError).message, + ).toBe('Error: Uncaught send transaction error'); + }); +}); diff --git a/src/wallet/components/wallet-advanced-send/components/SendProvider.tsx b/src/wallet/components/wallet-advanced-send/components/SendProvider.tsx index cee7fd4ada..7d4f1d6464 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendProvider.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendProvider.tsx @@ -1,4 +1,8 @@ -import type { PortfolioTokenWithFiatValue } from '@/api/types'; +import type { + APIError, + PortfolioTokenWithFiatValue, + PriceQuoteToken, +} from '@/api/types'; import { useExchangeRate } from '@/internal/hooks/useExchangeRate'; import { useLifecycleStatus } from '@/internal/hooks/useLifecycleStatus'; import { useSendTransaction } from '@/internal/hooks/useSendTransaction'; @@ -26,7 +30,11 @@ const emptyContext = {} as SendContextType; const SendContext = createContext(emptyContext); export function useSendContext() { - return useContext(SendContext); + const sendContext = useContext(SendContext); + if (sendContext === emptyContext) { + throw new Error('useSendContext must be used within a SendProvider'); + } + return sendContext; } export function SendProvider({ children }: SendProviderReact) { @@ -98,8 +106,21 @@ export function SendProvider({ children }: SendProviderReact) { if (!selectedToken) { return; } + + const tokenSymbol = selectedToken.symbol; + const tokenAddress = selectedToken.address; + let tokenParam: PriceQuoteToken; + + if (tokenSymbol === 'ETH') { + tokenParam = 'ETH' as const; + } else if (tokenAddress !== '') { + tokenParam = tokenAddress; + } else { + return; + } + useExchangeRate({ - token: selectedToken, + token: tokenParam, selectedInputType, setExchangeRate, setExchangeRateLoading, @@ -198,13 +219,13 @@ export function SendProvider({ children }: SendProviderReact) { ); const handleTransactionError = useCallback( - (error: string) => { + (error: APIError) => { updateLifecycleStatus({ statusName: 'error', statusData: { - error: 'Error building send transaction', - code: 'SeMSeBC01', // Send module SendButton component 01 error - message: error, + code: error.code, + error: `Error building send transaction: ${error.error}`, + message: error.message, }, }); }, @@ -224,12 +245,16 @@ export function SendProvider({ children }: SendProviderReact) { amount: cryptoAmount, }); if ('error' in calls) { - handleTransactionError(calls.error); + handleTransactionError(calls); } else { setCallData(calls); } } catch (error) { - handleTransactionError(error as string); + handleTransactionError({ + code: 'UNCAUGHT_SEND_TRANSACTION_ERROR', + error: 'Uncaught send transaction error', + message: String(error), + }); } }, [ selectedRecipientAddress, From ec0ef5b43ba38c84f1fec99d58d8de3dd2b44759 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 3 Mar 2025 10:07:00 -0800 Subject: [PATCH 110/115] fix test --- .../wallet-advanced-send/components/SendProvider.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx index 2d4ccb24d6..87a04d7817 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx @@ -334,6 +334,7 @@ describe('useSendContext', () => { result.current.handleFiatAmountChange('1000'); }); + // @ts-ignore - test is not type narrowing expect(result.current.lifecycleStatus.statusData?.sufficientBalance).toBe( false, ); From 219fccab49a41aa2a9408024028b758576b1a759 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 3 Mar 2025 10:12:55 -0800 Subject: [PATCH 111/115] fix linters --- src/internal/hooks/useExchangeRate.tsx | 2 +- .../components/SendAddressInput.test.tsx | 5 ++--- .../components/SendAddressSelection.test.tsx | 3 +-- .../components/SendAddressSelector.test.tsx | 15 +++++---------- .../components/SendButton.test.tsx | 17 +++++++---------- .../components/SendHeader.test.tsx | 6 +++--- .../components/SendProvider.test.tsx | 11 +++++------ 7 files changed, 24 insertions(+), 35 deletions(-) diff --git a/src/internal/hooks/useExchangeRate.tsx b/src/internal/hooks/useExchangeRate.tsx index bbaf9b2ddb..b3cb10137b 100644 --- a/src/internal/hooks/useExchangeRate.tsx +++ b/src/internal/hooks/useExchangeRate.tsx @@ -1,7 +1,7 @@ import { getPriceQuote } from '@/api'; +import type { PriceQuoteToken } from '@/api/types'; import { RequestContext } from '@/core/network/constants'; import { isApiError } from '@/internal/utils/isApiResponseError'; -import type { PriceQuoteToken } from '@/api/types'; import type { Dispatch, SetStateAction } from 'react'; type UseExchangeRateParams = { diff --git a/src/wallet/components/wallet-advanced-send/components/SendAddressInput.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendAddressInput.test.tsx index 9816f1ebd7..98b690f7f9 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAddressInput.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAddressInput.test.tsx @@ -1,12 +1,11 @@ import { TextInput } from '@/internal/components/TextInput'; import { render, screen } from '@testing-library/react'; import type { Address } from 'viem'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { SendAddressInput } from './SendAddressInput'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { resolveAddressInput } from '../utils/resolveAddressInput'; import { validateAddressInput } from '../utils/validateAddressInput'; +import { SendAddressInput } from './SendAddressInput'; -// Mock dependencies vi.mock('@/internal/components/TextInput', () => ({ TextInput: vi.fn(() => ), })); diff --git a/src/wallet/components/wallet-advanced-send/components/SendAddressSelection.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendAddressSelection.test.tsx index 79c824da99..fdd1b7ef2c 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAddressSelection.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAddressSelection.test.tsx @@ -1,6 +1,6 @@ import { act, render, screen } from '@testing-library/react'; import type { Chain } from 'viem'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useWalletContext } from '../../WalletProvider'; import { resolveAddressInput } from '../utils/resolveAddressInput'; import { SendAddressInput } from './SendAddressInput'; @@ -8,7 +8,6 @@ import { SendAddressSelection } from './SendAddressSelection'; import { SendAddressSelector } from './SendAddressSelector'; import { useSendContext } from './SendProvider'; -// Mock dependencies vi.mock('../../WalletProvider', () => ({ useWalletContext: vi.fn(), })); diff --git a/src/wallet/components/wallet-advanced-send/components/SendAddressSelector.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendAddressSelector.test.tsx index 0b7c759a24..ec0fb1f68c 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAddressSelector.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAddressSelector.test.tsx @@ -1,10 +1,9 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { Address, Avatar, Name } from '@/identity'; +import { fireEvent, render, screen } from '@testing-library/react'; import type { Address as AddressType, Chain } from 'viem'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SendAddressSelector } from './SendAddressSelector'; -import { Address, Avatar, Name } from '@/identity'; -// Mock dependencies vi.mock('@/identity', () => ({ Address: vi.fn(() =>
Address Component
), Avatar: vi.fn(() =>
Avatar Component
), @@ -39,9 +38,7 @@ describe('SendAddressSelector', () => { }); it('returns null when address is not provided', () => { - render( - , - ); + render(); const container = screen.queryByTestId('ockSendAddressSelector_container'); @@ -49,9 +46,7 @@ describe('SendAddressSelector', () => { }); it('returns null when senderChain is not provided', () => { - render( - , - ); + render(); const container = screen.queryByTestId('ockSendAddressSelector_container'); diff --git a/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx index 4f8a96066e..9aa611e40d 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx @@ -1,17 +1,16 @@ +import { Transaction } from '@/transaction/components/Transaction'; +import { TransactionButton } from '@/transaction/components/TransactionButton'; +import { useTransactionContext } from '@/transaction/components/TransactionProvider'; import { render, screen } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; import { type Address, type Chain, parseUnits } from 'viem'; import { base } from 'viem/chains'; -import { SendButton } from './SendButton'; -import { useWalletContext } from '../../WalletProvider'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; -import { useSendContext } from './SendProvider'; -import { useTransactionContext } from '@/transaction/components/TransactionProvider'; -import { Transaction } from '@/transaction/components/Transaction'; -import { TransactionButton } from '@/transaction/components/TransactionButton'; +import { useWalletContext } from '../../WalletProvider'; import { defaultSendTxSuccessHandler } from '../utils/defaultSendTxSuccessHandler'; +import { SendButton } from './SendButton'; +import { useSendContext } from './SendProvider'; -// Mock dependencies vi.mock('viem', () => ({ parseUnits: vi.fn(), })); @@ -314,10 +313,8 @@ describe('SendButton', () => { it('calls updateLifecycleStatus when transaction status changes', () => { render(); - // Extract the onStatus handler from Transaction props const { onStatus } = vi.mocked(Transaction).mock.calls[0][0]; - // Call with valid status onStatus?.({ statusName: 'transactionPending', statusData: null }); expect(mockSendContext.updateLifecycleStatus).toHaveBeenCalledWith({ diff --git a/src/wallet/components/wallet-advanced-send/components/SendHeader.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendHeader.test.tsx index bc52d90a4b..ad59da5fc9 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendHeader.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendHeader.test.tsx @@ -1,7 +1,7 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { SendHeader } from './SendHeader'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; +import { SendHeader } from './SendHeader'; import { useSendContext } from './SendProvider'; vi.mock('../../WalletAdvancedProvider', () => ({ diff --git a/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx index 87a04d7817..c811a3cf42 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendProvider.test.tsx @@ -1,13 +1,12 @@ import type { APIError } from '@/api/types'; -import { render, renderHook, act } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SendProvider, useSendContext } from './SendProvider'; -import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; +import { useExchangeRate } from '@/internal/hooks/useExchangeRate'; import { useSendTransaction } from '@/internal/hooks/useSendTransaction'; +import { act, render, renderHook } from '@testing-library/react'; import { formatUnits } from 'viem'; -import { useExchangeRate } from '@/internal/hooks/useExchangeRate'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; +import { SendProvider, useSendContext } from './SendProvider'; -// Mock dependencies vi.mock('../../WalletAdvancedProvider', () => ({ useWalletAdvancedContext: vi.fn(), })); From 373099418aed622bfe2060d6f3ba7e0aca27d483 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 3 Mar 2025 10:55:06 -0800 Subject: [PATCH 112/115] update tests --- .../components/SendButton.test.tsx | 42 +++++++++++++------ .../components/SendButton.tsx | 7 +++- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx index 9aa611e40d..cdd0f9e779 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendButton.test.tsx @@ -98,6 +98,8 @@ describe('SendButton', () => { const mockUseTransactionContext = useTransactionContext as ReturnType< typeof vi.fn >; + const mockDefaultSendTxSuccessHandler = + defaultSendTxSuccessHandler as ReturnType; const mockWalletContext = { chain: mockChain, @@ -334,18 +336,6 @@ describe('SendButton', () => { expect(mockSendContext.updateLifecycleStatus).not.toHaveBeenCalled(); }); - it('configures defaultSuccessOverride with correct parameters', () => { - render(); - - expect(defaultSendTxSuccessHandler).toHaveBeenCalledWith({ - transactionId: '123', - transactionHash: '0xabcdef', - senderChain: mockWalletContext.chain, - address: mockWalletContext.address, - onComplete: expect.any(Function), - }); - }); - it('passes custom overrides to TransactionButton', () => { const pendingOverride = { text: 'Sending...' }; const successOverride = { text: 'Sent!' }; @@ -383,4 +373,32 @@ describe('SendButton', () => { }), ); }); + + it('configures defaultSuccessOverride with correct parameters', () => { + render(); + + expect(defaultSendTxSuccessHandler).toHaveBeenCalledWith({ + transactionId: '123', + transactionHash: '0xabcdef', + senderChain: mockWalletContext.chain, + address: mockWalletContext.address, + onComplete: expect.any(Function), + }); + }); + + it('calls setActiveFeature when completionHandler is triggered', () => { + const setActiveFeature = vi.fn(); + mockUseWalletAdvancedContext.mockReturnValue({ + setActiveFeature, + }); + + render(); + + const { onComplete } = mockDefaultSendTxSuccessHandler.mock.calls[0][0]; + + // Call the callback + onComplete(); + + expect(setActiveFeature).toHaveBeenCalledWith(null); + }); }); diff --git a/src/wallet/components/wallet-advanced-send/components/SendButton.tsx b/src/wallet/components/wallet-advanced-send/components/SendButton.tsx index c352fc6df0..8a6b624348 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendButton.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendButton.tsx @@ -128,13 +128,18 @@ function SendTransactionButton({ const { address } = useWalletContext(); const { setActiveFeature } = useWalletAdvancedContext(); const { transactionHash, transactionId } = useTransactionContext(); + + const completionHandler = useCallback(() => { + setActiveFeature(null); + }, [setActiveFeature]); + const defaultSuccessOverride = { onClick: defaultSendTxSuccessHandler({ transactionId, transactionHash, senderChain: senderChain ?? undefined, address: address ?? undefined, - onComplete: () => setActiveFeature(null), + onComplete: completionHandler, }), }; From 8813fa8c443550c7ae65850e1ee340e162213f8f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 3 Mar 2025 11:21:58 -0800 Subject: [PATCH 113/115] bugfix --- playground/nextjs-app-router/onchainkit/package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/playground/nextjs-app-router/onchainkit/package.json b/playground/nextjs-app-router/onchainkit/package.json index e73b09cd0e..0679f21097 100644 --- a/playground/nextjs-app-router/onchainkit/package.json +++ b/playground/nextjs-app-router/onchainkit/package.json @@ -146,6 +146,12 @@ "import": "./esm/core/index.js", "default": "./esm/core/index.js" }, + "./earn": { + "types": "./esm/earn/index.d.ts", + "module": "./esm/earn/index.js", + "import": "./esm/earn/index.js", + "default": "./esm/earn/index.js" + }, "./fund": { "types": "./esm/fund/index.d.ts", "module": "./esm/fund/index.js", From 631bc06b1b4d8c6487627465b7bcf926c13d5cf9 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 4 Mar 2025 09:54:06 -0800 Subject: [PATCH 114/115] getPriceQuote nits --- src/api/getPriceQuote.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/getPriceQuote.ts b/src/api/getPriceQuote.ts index 6f0ce02878..e22c26d3ac 100644 --- a/src/api/getPriceQuote.ts +++ b/src/api/getPriceQuote.ts @@ -29,7 +29,7 @@ export async function getPriceQuote( ); if (res.error) { return { - code: `${res.error.code}`, + code: String(res.error.code), error: 'Error fetching price quote', message: res.error.message, }; @@ -51,7 +51,7 @@ function validateGetPriceQuoteParams(params: GetPriceQuoteParams) { return { code: 'INVALID_INPUT', error: 'Invalid input: tokens must be an array of at least one token', - message: '', + message: 'Tokens must be an array of at least one token', }; } From c3140a113626b320ac7acf506a7e5a7ce9d5d1a2 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Tue, 4 Mar 2025 10:08:19 -0800 Subject: [PATCH 115/115] fix test --- src/api/getPriceQuote.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/getPriceQuote.test.ts b/src/api/getPriceQuote.test.ts index 6df5ece562..f78157eee6 100644 --- a/src/api/getPriceQuote.test.ts +++ b/src/api/getPriceQuote.test.ts @@ -67,7 +67,7 @@ describe('getPriceQuote', () => { expect(result).toEqual({ code: 'INVALID_INPUT', error: 'Invalid input: tokens must be an array of at least one token', - message: '', + message: 'Tokens must be an array of at least one token', }); });