From 392485073b5cb74624390d7fc22dbb061bebd377 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 20:52:03 -0800 Subject: [PATCH 01/31] hardcode autocomplete values --- 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 f0e26269d4b8e7579e5f38a0cc933078046a9504 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 20:52:28 -0800 Subject: [PATCH 02/31] textClassName override --- .../components/amount-input/AmountInput.tsx | 14 +++++++++++--- .../components/amount-input/CurrencyLabel.tsx | 4 +++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/internal/components/amount-input/AmountInput.tsx b/src/internal/components/amount-input/AmountInput.tsx index 119d532c1b..22b6ec95d4 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 wrapperRef = useRef(null); @@ -105,7 +107,8 @@ 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} inputValidator={isValidAmount} @@ -114,7 +117,11 @@ export function AmountInput({ placeholder="0" />
- +
@@ -123,6 +130,7 @@ export function AmountInput({ {/* 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 */} 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" > From 11f8904baac76001a7a826f17bf246c74f3c8aa8 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 20:52:42 -0800 Subject: [PATCH 03/31] actionable token balance component --- src/token/components/TokenBalance.tsx | 137 ++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/token/components/TokenBalance.tsx diff --git a/src/token/components/TokenBalance.tsx b/src/token/components/TokenBalance.tsx new file mode 100644 index 0000000000..29f16ab822 --- /dev/null +++ b/src/token/components/TokenBalance.tsx @@ -0,0 +1,137 @@ +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 { formatUnits } from 'viem'; + +type TokenBalanceProps = { + token: PortfolioTokenWithFiatValue; + subtitle: string; + showImage?: boolean; + onClick?: (token: PortfolioTokenWithFiatValue) => void; + className?: string; + tokenSize?: number; + tokenNameClassName?: string; + tokenValueClassName?: string; + fiatValueClassName?: string; + actionClassName?: string; +} & ( + | { showAction?: true; actionText?: string; onActionPress?: () => void } + | { showAction?: false; actionText?: never; onActionPress?: never } +); + +export function TokenBalance({ + onClick, + className, + token, + ...contentProps +}: TokenBalanceProps) { + const Wrapper = onClick ? 'button' : 'div'; + + return ( + onClick(token), + })} + className={cn( + 'flex w-full items-center justify-start gap-4 px-2 py-1', + className, + )} + data-testid="ockTokenBalanceButton" + > + + + ); +} + +function TokenBalanceContent({ + token, + subtitle, + showImage = true, + showAction = false, + actionText = 'Use max', + onActionPress, + tokenSize = 40, + tokenNameClassName, + tokenValueClassName, + fiatValueClassName, + actionClassName, +}: TokenBalanceProps) { + const formattedFiatValue = formatFiatAmount({ + amount: token.fiatBalance, + currency: 'USD', + }); + + const formattedCryptoValue = truncateDecimalPlaces( + formatUnits(BigInt(token.cryptoBalance), token.decimals), + 3, + ); + + return ( +
+
+ {showImage && } +
+
+ + {token.name?.trim()} + + + {`${formattedCryptoValue} ${token.symbol} ${subtitle}`} + +
+
+ {showAction ? ( +
{ + e.stopPropagation(); + onActionPress?.(); + }} + onKeyDown={(e) => { + e.stopPropagation(); + onActionPress?.(); + }} + className={cn( + text.label2, + color.primary, + border.radius, + 'ml-auto cursor-pointer p-0.5 font-bold', + 'border border-transparent hover:border-[--ock-line-primary]', + actionClassName, + )} + > + {actionText} +
+ ) : ( + + {formattedFiatValue} + + )} +
+
+ ); +} From 4309e09bf36823a4a15c19edd7ea78427fbcdf3c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 20:54:00 -0800 Subject: [PATCH 04/31] trim and hide long token names --- src/token/components/TokenRow.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/token/components/TokenRow.tsx b/src/token/components/TokenRow.tsx index 7360bedb98..d0e0670bf9 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 0717b1c1f978d135ab1d5649dd1b25402bea3e22 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 21:18:25 -0800 Subject: [PATCH 05/31] export token balance --- src/token/components/TokenBalance.tsx | 18 +-------------- src/token/index.ts | 1 + src/token/types.ts | 32 +++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/token/components/TokenBalance.tsx b/src/token/components/TokenBalance.tsx index 29f16ab822..e18620de2c 100644 --- a/src/token/components/TokenBalance.tsx +++ b/src/token/components/TokenBalance.tsx @@ -1,25 +1,9 @@ -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 { formatUnits } from 'viem'; - -type TokenBalanceProps = { - token: PortfolioTokenWithFiatValue; - subtitle: string; - showImage?: boolean; - onClick?: (token: PortfolioTokenWithFiatValue) => void; - className?: string; - tokenSize?: number; - tokenNameClassName?: string; - tokenValueClassName?: string; - fiatValueClassName?: string; - actionClassName?: string; -} & ( - | { showAction?: true; actionText?: string; onActionPress?: () => void } - | { showAction?: false; actionText?: never; onActionPress?: never } -); +import type { TokenBalanceProps } from '../types'; export function TokenBalance({ onClick, diff --git a/src/token/index.ts b/src/token/index.ts index 52b38ffa12..ede0b37c27 100644 --- a/src/token/index.ts +++ b/src/token/index.ts @@ -6,6 +6,7 @@ export { TokenRow } from './components/TokenRow'; export { TokenSearch } from './components/TokenSearch'; export { TokenSelectDropdown } from './components/TokenSelectDropdown'; export { TokenSelectModal } from './components/TokenSelectModal'; +export { TokenBalance } from './components/TokenBalance'; // Utils export { formatAmount } from './utils/formatAmount'; diff --git a/src/token/types.ts b/src/token/types.ts index bcf8dc93dc..f804097b82 100644 --- a/src/token/types.ts +++ b/src/token/types.ts @@ -1,5 +1,6 @@ // 🌲☀🌲 import type { Address } from 'viem'; +import type { PortfolioTokenWithFiatValue } from '../api/types'; /** * Note: exported as public Type @@ -120,3 +121,34 @@ export type TokenSelectModalReact = { /** Selected token */ token?: Token; }; + +/** + * Note: exported as public Type + */ +export type TokenBalanceProps = { + /** Token with fiat and crypto balance*/ + token: PortfolioTokenWithFiatValue; + /** Subtitle to display next to the token name (eg. "available") */ + subtitle: string; + /** Show the token image (default: true) */ + showImage?: boolean; + /** Click handler for the whole component*/ + onClick?: (token: PortfolioTokenWithFiatValue) => void; + /** Size of the token image in px (default: 40) */ + tokenSize?: number; + /** Optional additional CSS class to apply to the component */ + className?: string; + /** Optional additional CSS class to apply to the token name */ + tokenNameClassName?: string; + /** Optional additional CSS class to apply to the token value */ + tokenValueClassName?: string; + /** Optional additional CSS class to apply to the fiat value */ + fiatValueClassName?: string; + /** Optional additional CSS class to apply to the action button */ + actionClassName?: string; +} & ( + /** Hide the action button (default)*/ + | { showAction?: false; actionText?: never; onActionPress?: never } + /** Show an additional action button (eg. "Use max") */ + | { showAction?: true; actionText?: string; onActionPress?: () => void } +); From d1b36cd7d7d3b9cf6f77ca43920ff1eb99f7bc6c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 21:18:50 -0800 Subject: [PATCH 06/31] send components --- .../components/SendAmountInput.tsx | 94 ++++++++++++ .../components/SendFundWallet.tsx | 47 ++++++ .../components/SendTokenSelector.tsx | 62 ++++++++ .../components/wallet-advanced-send/types.ts | 135 ++++++++++++++++++ 4 files changed, 338 insertions(+) create mode 100644 src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx create mode 100644 src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx create mode 100644 src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx create mode 100644 src/wallet/components/wallet-advanced-send/types.ts diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx new file mode 100644 index 0000000000..17170c873c --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx @@ -0,0 +1,94 @@ +'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 { cn, color, text } from '@/styles/theme'; +import type { SendAmountInputProps } from '../types'; + +export function SendAmountInput({ + selectedToken, + cryptoAmount, + handleCryptoAmountChange, + fiatAmount, + handleFiatAmountChange, + selectedInputType, + setSelectedInputType, + exchangeRate, + exchangeRateLoading, + className, + textClassName, +}: SendAmountInputProps) { + return ( +
+
+ + + +
+
+ ); +} + +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 ( + + ); +} diff --git a/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx b/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx new file mode 100644 index 0000000000..39cc2bcead --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx @@ -0,0 +1,47 @@ +import { + FundCard, + FundCardAmountInput, + FundCardAmountInputTypeSwitch, + FundCardPaymentMethodDropdown, + FundCardPresetAmountInputList, + FundCardSubmitButton, +} from '@/fund'; +import { cn, color, text } from '@/styles/theme'; +import type { SendFundingWalletProps } from '../types'; + +export function SendFundWallet({ + onError, + onStatus, + onSuccess, + className, +}: SendFundingWalletProps) { + return ( +
+
+ Insufficient ETH balance to send transaction. Fund your wallet to + continue. +
+ + + + + + + +
+ ); +} diff --git a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx new file mode 100644 index 0000000000..34b5320340 --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { TokenBalance } from '@/token'; +import { border, cn, color, pressable, text } from '@/styles/theme'; +import { formatUnits } from 'viem'; +import type { SendTokenSelectorProps } from '../types'; +import { useWalletAdvancedContext } from '@/wallet/components/WalletAdvancedProvider'; + +export function SendTokenSelector({ + selectedToken, + handleTokenSelection, + handleResetTokenSelection, + setSelectedInputType, + handleCryptoAmountChange, + handleFiatAmountChange, +}: SendTokenSelectorProps) { + const { tokenBalances } = useWalletAdvancedContext(); + + if (!selectedToken) { + return ( +
+ + Select a token + +
+ {tokenBalances?.map((token) => ( + + ))} +
+
+ ); + } + + return ( + { + setSelectedInputType('crypto'); + handleFiatAmountChange(String(selectedToken.fiatBalance)); + handleCryptoAmountChange( + String( + formatUnits( + BigInt(selectedToken.cryptoBalance), + selectedToken.decimals, + ), + ), + ); + }} + className={cn(pressable.alternate, border.radius, 'p-2')} + /> + ); +} diff --git a/src/wallet/components/wallet-advanced-send/types.ts b/src/wallet/components/wallet-advanced-send/types.ts new file mode 100644 index 0000000000..638b7f4033 --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/types.ts @@ -0,0 +1,135 @@ +import type { Dispatch, ReactNode, SetStateAction } from 'react'; +import type { Address, TransactionReceipt } from 'viem'; +import type { APIError, PortfolioTokenWithFiatValue } from '../../../api/types'; +import type { LifecycleStatusUpdate } from '../../../internal/types'; +import type { Call } from '../../../transaction/types'; + +export type SendProviderReact = { + children: ReactNode; +}; + +export type SendContextType = { + // Lifecycle Status Context + isInitialized: boolean; + lifecycleStatus: SendLifecycleStatus; + updateLifecycleStatus: ( + status: LifecycleStatusUpdate, + ) => void; + + // Sender Context + ethBalance: number | undefined; + + // Recipient Address Context + selectedRecipientAddress: RecipientAddress; + handleAddressSelection: (selection: RecipientAddress) => void; + handleRecipientInputChange: () => 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 | null; +}; + +export type RecipientAddress = { + display: string; + value: Address | null; +}; + +export type SendLifecycleStatus = + | { + 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'; // 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; + }; + +export type SendAmountInputProps = { + className?: string; + textClassName?: string; +} & Pick< + SendContextType, + | 'selectedToken' + | 'cryptoAmount' + | 'handleCryptoAmountChange' + | 'fiatAmount' + | 'handleFiatAmountChange' + | 'selectedInputType' + | 'setSelectedInputType' + | 'exchangeRate' + | 'exchangeRateLoading' +>; + +export type SendFundingWalletProps = { + onError?: () => void; + onStatus?: () => void; + onSuccess?: () => void; + className?: string; +}; + +export type SendTokenSelectorProps = Pick< + SendContextType, + | 'selectedToken' + | 'handleTokenSelection' + | 'handleResetTokenSelection' + | 'setSelectedInputType' + | 'handleCryptoAmountChange' + | 'handleFiatAmountChange' +>; From f829c34ade8c8d21c05efeb553c066015c48a918 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 21:33:03 -0800 Subject: [PATCH 07/31] send utils --- .../utils/defaultSendTxSuccessHandler.ts | 50 +++++++++++++++++ .../utils/getDefaultSendButtonLabel.ts | 24 ++++++++ .../utils/resolveAddressInput.ts | 56 +++++++++++++++++++ .../utils/validateAddressInput.test.ts | 0 .../utils/validateAddressInput.ts | 0 5 files changed, 130 insertions(+) create mode 100644 src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.ts create mode 100644 src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.ts create mode 100644 src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.ts rename src/wallet/{ => components/wallet-advanced-send}/utils/validateAddressInput.test.ts (100%) rename src/wallet/{ => components/wallet-advanced-send}/utils/validateAddressInput.ts (100%) diff --git a/src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.ts b/src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.ts new file mode 100644 index 0000000000..96046d6070 --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/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, take an action (default: close the send modal) + onComplete?.(); + }; +} diff --git a/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.ts b/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.ts new file mode 100644 index 0000000000..883b20bf81 --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.ts @@ -0,0 +1,24 @@ +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'; +} diff --git a/src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.ts b/src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.ts new file mode 100644 index 0000000000..7d63c3e02e --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.ts @@ -0,0 +1,56 @@ +import { getName, isBasename } from '@/identity'; +import { getSlicedAddress } from '@/identity/utils/getSlicedAddress'; +import { type Address, isAddress } from 'viem'; +import { base, mainnet } from 'viem/chains'; +import type { RecipientAddress } from '../types'; +import { validateAddressInput } from './validateAddressInput'; + +export async function resolveAddressInput( + selectedRecipientAddress: Address | null, + recipientInput: string | null, +): Promise { + if (!recipientInput) { + return { + display: '', + value: null, + }; + } + + // if the user hasn't selected an address yet, return their input and a validated address (or null) + if (!selectedRecipientAddress) { + const validatedAddress = await validateAddressInput(recipientInput); + return { + display: recipientInput, + value: validatedAddress, + }; + } + + // we now have a selected recipient + // if the user's input is address-format, then return the sliced address + if (isAddress(recipientInput)) { + return { + display: getSlicedAddress(recipientInput), + value: selectedRecipientAddress, + }; + } + + // if the user's input wasn't address-format, then it must have been name-format + // so try to get and return the name + // TODO: do i need to do this fetch? can i just display the recipientInput? + const name = await getName({ + address: selectedRecipientAddress, + chain: isBasename(recipientInput) ? base : mainnet, + }); + if (name) { + return { + display: name, + value: selectedRecipientAddress, + }; + } + + // 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/wallet-advanced-send/utils/validateAddressInput.test.ts similarity index 100% rename from src/wallet/utils/validateAddressInput.test.ts rename to src/wallet/components/wallet-advanced-send/utils/validateAddressInput.test.ts diff --git a/src/wallet/utils/validateAddressInput.ts b/src/wallet/components/wallet-advanced-send/utils/validateAddressInput.ts similarity index 100% rename from src/wallet/utils/validateAddressInput.ts rename to src/wallet/components/wallet-advanced-send/utils/validateAddressInput.ts From 35cf20af4fe55dfcffa083b5dc9eee66a5efa13c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 21:50:48 -0800 Subject: [PATCH 08/31] simplify address resolution --- .../utils/resolveAddressInput.ts | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.ts b/src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.ts index 7d63c3e02e..14fd8d289c 100644 --- a/src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.ts +++ b/src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.ts @@ -1,56 +1,41 @@ -import { getName, isBasename } from '@/identity'; import { getSlicedAddress } from '@/identity/utils/getSlicedAddress'; import { type Address, isAddress } from 'viem'; -import { base, mainnet } from 'viem/chains'; import type { RecipientAddress } from '../types'; import { validateAddressInput } from './validateAddressInput'; export async function resolveAddressInput( - selectedRecipientAddress: Address | null, - recipientInput: string | null, + selectedAddress: Address | null, + input: string | null, ): Promise { - if (!recipientInput) { + // if there is no user input, return nullish values + if (!input) { return { display: '', value: null, }; } - // if the user hasn't selected an address yet, return their input and a validated address (or null) - if (!selectedRecipientAddress) { - const validatedAddress = await validateAddressInput(recipientInput); + // if the user hasn't selected an address yet, return their input and a validated address + if (!selectedAddress) { + const validatedAddress = await validateAddressInput(input); return { - display: recipientInput, + display: input, value: validatedAddress, }; } - // we now have a selected recipient + // we now have a selected recipient, so the value will always be the selected address // if the user's input is address-format, then return the sliced address - if (isAddress(recipientInput)) { + if (isAddress(input)) { return { - display: getSlicedAddress(recipientInput), - value: selectedRecipientAddress, + display: getSlicedAddress(input), + value: selectedAddress, }; } - // if the user's input wasn't address-format, then it must have been name-format - // so try to get and return the name - // TODO: do i need to do this fetch? can i just display the recipientInput? - const name = await getName({ - address: selectedRecipientAddress, - chain: isBasename(recipientInput) ? base : mainnet, - }); - if (name) { - return { - display: name, - value: selectedRecipientAddress, - }; - } - - // as a last resort, display the user's input and set the value to null + // otherwise, the user's input is a name, so display the name return { - display: recipientInput, - value: null, + display: input, + value: selectedAddress, }; } From 321f471782e54c5f91884989116e4b57d0b60141 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 21:54:13 -0800 Subject: [PATCH 09/31] fix lints --- src/token/types.ts | 16 ++++++++++++---- .../components/SendFundWallet.tsx | 3 ++- .../components/SendTokenSelector.tsx | 4 ++-- .../components/wallet-advanced-send/types.ts | 5 +++-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/token/types.ts b/src/token/types.ts index f804097b82..736efb295e 100644 --- a/src/token/types.ts +++ b/src/token/types.ts @@ -147,8 +147,16 @@ export type TokenBalanceProps = { /** Optional additional CSS class to apply to the action button */ actionClassName?: string; } & ( - /** Hide the action button (default)*/ - | { showAction?: false; actionText?: never; onActionPress?: never } - /** Show an additional action button (eg. "Use max") */ - | { showAction?: true; actionText?: string; onActionPress?: () => void } + | { + /** Hide the action button (default)*/ + showAction?: false; + actionText?: never; + onActionPress?: never; + } + | { + /** Show an additional action button (eg. "Use max") */ + showAction?: true; + actionText?: string; + onActionPress?: () => void; + } ); diff --git a/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx b/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx index 39cc2bcead..579c1550ed 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx @@ -14,6 +14,7 @@ export function SendFundWallet({ onStatus, onSuccess, className, + subtitleClassName, }: SendFundingWalletProps) { return (
Insufficient ETH balance to send transaction. Fund your wallet to continue. diff --git a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx index 34b5320340..e51f1b3d34 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx @@ -1,10 +1,10 @@ 'use client'; -import { TokenBalance } from '@/token'; import { border, cn, color, pressable, text } from '@/styles/theme'; +import { TokenBalance } from '@/token'; import { formatUnits } from 'viem'; +import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; import type { SendTokenSelectorProps } from '../types'; -import { useWalletAdvancedContext } from '@/wallet/components/WalletAdvancedProvider'; export function SendTokenSelector({ selectedToken, diff --git a/src/wallet/components/wallet-advanced-send/types.ts b/src/wallet/components/wallet-advanced-send/types.ts index 638b7f4033..bd0c327835 100644 --- a/src/wallet/components/wallet-advanced-send/types.ts +++ b/src/wallet/components/wallet-advanced-send/types.ts @@ -81,7 +81,7 @@ export type SendLifecycleStatus = }; } | { - statusName: 'transactionPending'; // if the mutation is currently executing + statusName: 'transactionPending'; statusData: null; } | { @@ -91,7 +91,7 @@ export type SendLifecycleStatus = }; } | { - statusName: 'success'; // if the last mutation attempt was successful + statusName: 'success'; statusData: { transactionReceipts: TransactionReceipt[]; }; @@ -122,6 +122,7 @@ export type SendFundingWalletProps = { onStatus?: () => void; onSuccess?: () => void; className?: string; + subtitleClassName?: string; }; export type SendTokenSelectorProps = Pick< From 37ddab5e91defb4688ca1898acfcbad42cbd0740 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 22:54:02 -0800 Subject: [PATCH 10/31] separate components, add tests --- .../components/SendAmountInput.test.tsx | 114 ++++++++++++++++++ .../components/SendAmountInput.tsx | 48 +------- .../SendAmountInputTypeSwitch.test.tsx | 99 +++++++++++++++ .../components/SendAmountInputTypeSwitch.tsx | 48 ++++++++ 4 files changed, 262 insertions(+), 47 deletions(-) create mode 100644 src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx create mode 100644 src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx create mode 100644 src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx new file mode 100644 index 0000000000..6b75414fe2 --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx @@ -0,0 +1,114 @@ +import { render } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { AmountInput } from '@/internal/components/amount-input/AmountInput'; +import { SendAmountInput } from './SendAmountInput'; +import { SendAmountInputTypeSwitch } from './SendAmountInputTypeSwitch'; + +vi.mock('@/internal/components/amount-input/AmountInput'); +vi.mock('./SendAmountInputTypeSwitch'); + +const mockToken = { + symbol: 'ETH', + address: '' as const, + chainId: 8543, + decimals: 18, + image: null, + name: 'Ethereum', + cryptoBalance: 1, + fiatBalance: 3300, +}; + +describe('SendAmountInput', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + 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(); + expect(AmountInput).toHaveBeenCalledWith( + { + fiatAmount: defaultProps.fiatAmount, + cryptoAmount: defaultProps.cryptoAmount, + asset: defaultProps.selectedToken.symbol, + currency: 'USD', + selectedInputType: defaultProps.selectedInputType, + setFiatAmount: defaultProps.handleFiatAmountChange, + setCryptoAmount: defaultProps.handleCryptoAmountChange, + exchangeRate: '2000', + className: 'test-class', + textClassName: 'test-text-class', + }, + {}, + ); + }); + + it('passes correct props to SendAmountInputTypeSwitch', () => { + 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, + }, + {}, + ); + }); + + it('handles null/undefined values correctly', () => { + render( + , + ); + + expect(AmountInput).toHaveBeenCalledWith( + { + fiatAmount: '', + cryptoAmount: '', + asset: '', + currency: 'USD', + selectedInputType: 'crypto', + setFiatAmount: defaultProps.handleFiatAmountChange, + setCryptoAmount: defaultProps.handleCryptoAmountChange, + exchangeRate: '2000', + className: 'test-class', + textClassName: 'test-text-class', + }, + {}, + ); + + expect(SendAmountInputTypeSwitch).toHaveBeenCalledWith( + { + selectedToken: null, + fiatAmount: '', + cryptoAmount: '', + selectedInputType: defaultProps.selectedInputType, + setSelectedInputType: defaultProps.setSelectedInputType, + exchangeRate: defaultProps.exchangeRate, + exchangeRateLoading: defaultProps.exchangeRateLoading, + }, + {}, + ); + }); +}); diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx index 17170c873c..1b8146ddb8 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx @@ -1,11 +1,8 @@ '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 { cn, color, text } from '@/styles/theme'; import type { SendAmountInputProps } from '../types'; +import { SendAmountInputTypeSwitch } from './SendAmountInputTypeSwitch'; export function SendAmountInput({ selectedToken, @@ -49,46 +46,3 @@ export function SendAmountInput({
); } - -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 ( - - ); -} diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx new file mode 100644 index 0000000000..2f7e232af6 --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx @@ -0,0 +1,99 @@ +import { render } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Skeleton } from '@/internal/components/Skeleton'; +import { AmountInputTypeSwitch } from '@/internal/components/amount-input/AmountInputTypeSwitch'; +import { SendAmountInputTypeSwitch } from './SendAmountInputTypeSwitch'; + +vi.mock('@/internal/components/Skeleton'); +vi.mock('@/internal/components/amount-input/AmountInputTypeSwitch'); + +const mockToken = { + symbol: 'ETH', + address: '' as const, + chainId: 8543, + decimals: 18, + image: null, + name: 'Ethereum', + cryptoBalance: 1, + fiatBalance: 3300, +}; + +describe('SendAmountInput', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + 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('shows error message when exchange rate is invalid', () => { + const { container } = render( + , + ); + expect(container).toHaveTextContent('Exchange rate unavailable'); + expect(AmountInputTypeSwitch).not.toHaveBeenCalled(); + }); + + it('shows skeleton when exchange rate is loading', () => { + render( + , + ); + expect(Skeleton).toHaveBeenCalled(); + }); + + it('passes correct props to AmountInput', () => { + render(); + expect(AmountInputTypeSwitch).toHaveBeenCalledWith( + { + asset: defaultProps.selectedToken.symbol, + fiatAmount: defaultProps.fiatAmount, + cryptoAmount: defaultProps.cryptoAmount, + exchangeRate: defaultProps.exchangeRate, + exchangeRateLoading: false, + currency: 'USD', + selectedInputType: defaultProps.selectedInputType, + setSelectedInputType: defaultProps.setSelectedInputType, + }, + {}, + ); + }); + + it('handles null/undefined values correctly', () => { + render( + , + ); + + expect(AmountInputTypeSwitch).toHaveBeenCalledWith( + { + asset: '', + fiatAmount: '', + cryptoAmount: '', + exchangeRate: defaultProps.exchangeRate, + exchangeRateLoading: defaultProps.exchangeRateLoading, + currency: 'USD', + selectedInputType: defaultProps.selectedInputType, + setSelectedInputType: defaultProps.setSelectedInputType, + }, + {}, + ); + }); +}); diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx new file mode 100644 index 0000000000..ffff38642f --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx @@ -0,0 +1,48 @@ +import { AmountInputTypeSwitch } from '@/internal/components/amount-input/AmountInputTypeSwitch'; +import { Skeleton } from '@/internal/components/Skeleton'; +import { cn, color, text } from '@/styles/theme'; +import type { SendAmountInputProps } from '../types'; + +export function SendAmountInputTypeSwitch({ + exchangeRateLoading, + exchangeRate, + selectedToken, + fiatAmount, + cryptoAmount, + selectedInputType, + setSelectedInputType, +}: Pick< + SendAmountInputProps, + | 'exchangeRateLoading' + | 'exchangeRate' + | 'selectedToken' + | 'fiatAmount' + | 'cryptoAmount' + | 'selectedInputType' + | 'setSelectedInputType' +>) { + if (exchangeRateLoading) { + return ; + } + + if (exchangeRate <= 0) { + return ( +
+ Exchange rate unavailable +
+ ); + } + + return ( + + ); +} From e0f30cea01330b0ae5de30bb909e22a82f1c6822 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 23:02:19 -0800 Subject: [PATCH 11/31] add test --- .../components/SendFundWallet.test.tsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/wallet/components/wallet-advanced-send/components/SendFundWallet.test.tsx diff --git a/src/wallet/components/wallet-advanced-send/components/SendFundWallet.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendFundWallet.test.tsx new file mode 100644 index 0000000000..5a953ffcbb --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/components/SendFundWallet.test.tsx @@ -0,0 +1,86 @@ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + FundCard, + FundCardAmountInput, + FundCardAmountInputTypeSwitch, + FundCardPaymentMethodDropdown, + FundCardPresetAmountInputList, + FundCardSubmitButton, +} from '@/fund'; +import { SendFundWallet } from './SendFundWallet'; + +// Mock all fund components +vi.mock('@/fund/components/FundCard'); +vi.mock('@/fund/components/FundCardAmountInput'); +vi.mock('@/fund/components/FundCardAmountInputTypeSwitch'); +vi.mock('@/fund/components/FundCardPaymentMethodDropdown'); +vi.mock('@/fund/components/FundCardPresetAmountInputList'); +vi.mock('@/fund/components/FundCardSubmitButton'); + +describe('SendFundWallet', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const defaultProps = { + onError: vi.fn(), + onStatus: vi.fn(), + onSuccess: vi.fn(), + className: 'test-class', + subtitleClassName: 'test-subtitle-class', + }; + + it('renders with correct base structure', () => { + const { container } = render(); + expect(screen.getByTestId('ockSendFundWallet')).toBeInTheDocument(); + expect(container.firstChild).toHaveClass( + 'flex', + 'flex-col', + 'items-center', + 'justify-between', + ); + }); + + it('passes correct props to FundCard', () => { + render(); + expect(FundCard).toHaveBeenCalledWith( + { + assetSymbol: 'ETH', + country: 'US', + currency: 'USD', + presetAmountInputs: ['2', '5', '10'], + onError: defaultProps.onError, + onStatus: defaultProps.onStatus, + onSuccess: defaultProps.onSuccess, + className: expect.stringContaining('test-class'), + children: expect.any(Array), + }, + {}, + ); + }); + + it('renders all child components in correct order', () => { + render(); + const fundCardCall = vi.mocked(FundCard).mock.calls[0][0]; + const children = fundCardCall.children as React.ReactElement[]; + + expect(children).toHaveLength(5); + expect(children?.[0].type).toBe(FundCardAmountInput); + expect(children?.[1].type).toBe(FundCardAmountInputTypeSwitch); + expect(children?.[2].type).toBe(FundCardPresetAmountInputList); + expect(children?.[3].type).toBe(FundCardPaymentMethodDropdown); + expect(children?.[4].type).toBe(FundCardSubmitButton); + }); + + it('applies correct className to subtitle', () => { + const { container } = render(); + const subtitle = container.querySelector( + `.${defaultProps.subtitleClassName}`, + ); + expect(subtitle).toBeInTheDocument(); + expect(subtitle).toHaveTextContent( + 'Insufficient ETH balance to send transaction. Fund your wallet to continue.', + ); + }); +}); From eeaedeb3f48bb55d40dae88eff639f0cf0a8638f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 23:16:32 -0800 Subject: [PATCH 12/31] add tests --- .../components/SendTokenSelector.test.tsx | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx diff --git a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx new file mode 100644 index 0000000000..082d6c6fa5 --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx @@ -0,0 +1,120 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useWalletAdvancedContext } from '../../WalletAdvancedProvider'; +import { SendTokenSelector } from './SendTokenSelector'; +import type { PortfolioTokenWithFiatValue } from '@/api/types'; + +// Mock the context hook +vi.mock('../../WalletAdvancedProvider', () => ({ + useWalletAdvancedContext: vi.fn(), +})); + +const mockTokenBalances: PortfolioTokenWithFiatValue[] = [ + { + address: '0x1230000000000000000000000000000000000000', + symbol: 'TEST', + name: 'Test Token', + decimals: 18, + cryptoBalance: 1000000000, + fiatBalance: 100, + image: 'test.png', + chainId: 8543, + }, + { + address: '0x4560000000000000000000000000000000000000', + symbol: 'TEST2', + name: 'Test Token 2', + decimals: 18, + cryptoBalance: 2000000000000, + fiatBalance: 200, + image: 'test2.png', + chainId: 8543, + }, +]; + +describe('SendTokenSelector', () => { + const defaultProps = { + selectedToken: null, + handleTokenSelection: vi.fn(), + handleResetTokenSelection: vi.fn(), + setSelectedInputType: vi.fn(), + handleCryptoAmountChange: vi.fn(), + handleFiatAmountChange: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + (useWalletAdvancedContext as Mock).mockReturnValue({ + tokenBalances: mockTokenBalances, + }); + }); + + it('renders token selection list when no token is selected', () => { + render(); + + expect(screen.getByText('Select a token')).toBeInTheDocument(); + expect(screen.getAllByRole('button')).toHaveLength( + mockTokenBalances.length, + ); + }); + + it('calls handleTokenSelection when a token is clicked from the list', () => { + render(); + + fireEvent.click(screen.getAllByTestId('ockTokenBalanceButton')[0]); + expect(defaultProps.handleTokenSelection).toHaveBeenCalledWith( + mockTokenBalances[0], + ); + }); + + it('renders selected token with max button when token is selected', () => { + render( + , + ); + + expect(screen.getByText('Test Token')).toBeInTheDocument(); + expect(screen.getByText(/0\.000 TEST available/)).toBeInTheDocument(); + }); + + it('handles max button click correctly', () => { + 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( + '0.000000001', + ); + }); + + it('calls handleResetTokenSelection when selected token is clicked', () => { + render( + , + ); + + fireEvent.click(screen.getByTestId('ockTokenBalanceButton')); + expect(defaultProps.handleResetTokenSelection).toHaveBeenCalled(); + }); + + it('handles empty tokenBalances gracefully', () => { + (useWalletAdvancedContext as Mock).mockReturnValue({ + tokenBalances: [], + }); + + render(); + expect(screen.getByText('Select a token')).toBeInTheDocument(); + }); +}); From 367f325976127754b28599f46879341f369e8ece Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 23:27:40 -0800 Subject: [PATCH 13/31] fix typos --- .../wallet-advanced-send/components/SendAmountInput.test.tsx | 4 ++-- .../components/SendAmountInputTypeSwitch.test.tsx | 4 ++-- .../components/SendTokenSelector.test.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 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 6b75414fe2..a26fd113de 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx @@ -10,10 +10,10 @@ vi.mock('./SendAmountInputTypeSwitch'); const mockToken = { symbol: 'ETH', address: '' as const, - chainId: 8543, + chainId: 8453, decimals: 18, image: null, - name: 'Ethereum', + name: 'Base', cryptoBalance: 1, fiatBalance: 3300, }; 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 2f7e232af6..4095449591 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx @@ -10,10 +10,10 @@ vi.mock('@/internal/components/amount-input/AmountInputTypeSwitch'); const mockToken = { symbol: 'ETH', address: '' as const, - chainId: 8543, + chainId: 8453, decimals: 18, image: null, - name: 'Ethereum', + name: 'Base', cryptoBalance: 1, fiatBalance: 3300, }; 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 082d6c6fa5..cb299b7447 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx @@ -18,7 +18,7 @@ const mockTokenBalances: PortfolioTokenWithFiatValue[] = [ cryptoBalance: 1000000000, fiatBalance: 100, image: 'test.png', - chainId: 8543, + chainId: 8453, }, { address: '0x4560000000000000000000000000000000000000', @@ -28,7 +28,7 @@ const mockTokenBalances: PortfolioTokenWithFiatValue[] = [ cryptoBalance: 2000000000000, fiatBalance: 200, image: 'test2.png', - chainId: 8543, + chainId: 8453, }, ]; From 8d3c4e33ecef7a559779c907f966b241a7eceabd Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 23:27:46 -0800 Subject: [PATCH 14/31] add tests --- .../utils/defaultSendTxSuccessHandler.test.ts | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.test.ts diff --git a/src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.test.ts b/src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.test.ts new file mode 100644 index 0000000000..346d93ff00 --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.test.ts @@ -0,0 +1,101 @@ +import { getChainExplorer } from '@/core/network/getChainExplorer'; +import type { Address, Chain, TransactionReceipt } from 'viem'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { defaultSendTxSuccessHandler } from './defaultSendTxSuccessHandler'; + +vi.mock('@/core/network/getChainExplorer'); +vi.mock('wagmi', () => ({ + useChainId: () => 8453, +})); + +describe('defaultSendTxSuccessHandler', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(window, 'open').mockImplementation(() => null); + vi.mocked(getChainExplorer).mockReturnValue('https://basescan.org'); + }); + + it('opens Coinbase Wallet URL when all SW params are present', () => { + const handler = defaultSendTxSuccessHandler({ + transactionId: 'txn123', + transactionHash: '0xabc', + senderChain: { id: 1, name: 'Ethereum' } as Chain, + address: '0x123' as Address, + onComplete: vi.fn(), + }); + + handler({} as TransactionReceipt); + + const [[url]] = vi.mocked(window.open).mock.calls; + expect(url?.toString()).toBe( + 'https://wallet.coinbase.com/assets/transactions?contentParams%5BtxHash%5D=0xabc&contentParams%5BchainId%5D=1&contentParams%5BfromAddress%5D=0x123', + ); + expect(window.open).toHaveBeenCalledWith( + expect.any(URL), + '_blank', + 'noopener,noreferrer', + ); + }); + + it('opens block explorer when SW params are missing', () => { + const handler = defaultSendTxSuccessHandler({ + transactionId: undefined, + transactionHash: '0xabc', + senderChain: undefined, + address: undefined, + }); + + handler({} as TransactionReceipt); + + const [[url]] = vi.mocked(window.open).mock.calls; + expect(url?.toString()).toBe('https://basescan.org/tx/0xabc'); + expect(window.open).toHaveBeenCalledWith( + 'https://basescan.org/tx/0xabc', + '_blank', + 'noopener,noreferrer', + ); + }); + + it('calls onComplete callback if provided', () => { + const onComplete = vi.fn(); + const handler = defaultSendTxSuccessHandler({ + transactionId: undefined, + transactionHash: '0xabc', + senderChain: undefined, + address: undefined, + onComplete, + }); + + handler({} as TransactionReceipt); + + expect(onComplete).toHaveBeenCalled(); + }); + + it('uses senderChain.id when available', () => { + const handler = defaultSendTxSuccessHandler({ + transactionId: undefined, + transactionHash: '0xabc', + senderChain: { id: 1, name: 'Ethereum' } as Chain, + address: undefined, + }); + + vi.mocked(getChainExplorer).mockReturnValue('https://basescan.org'); + + handler({} as TransactionReceipt); + + expect(getChainExplorer).toHaveBeenCalledWith(1); + }); + + it('falls back to useChainId when senderChain is undefined', () => { + const handler = defaultSendTxSuccessHandler({ + transactionId: undefined, + transactionHash: '0xabc', + senderChain: undefined, + address: undefined, + }); + + handler({} as TransactionReceipt); + + expect(getChainExplorer).toHaveBeenCalledWith(8453); + }); +}); From 1d6b9230a597524e7f0bf1bfcd76b003b461dec9 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 23:32:58 -0800 Subject: [PATCH 15/31] add tests --- .../utils/getDefaultSendButtonLabel.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts diff --git a/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts b/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts new file mode 100644 index 0000000000..7bc1f8688f --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts @@ -0,0 +1,44 @@ +import type { PortfolioTokenWithFiatValue } from '@/api/types'; +import { describe, it, expect } 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 db5deaff3f3a43fd62fdb6138ed02f14661f4b38 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 23:35:02 -0800 Subject: [PATCH 16/31] add tests --- .../utils/resolveAddressInput.test.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.test.ts diff --git a/src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.test.ts b/src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.test.ts new file mode 100644 index 0000000000..abcca859cc --- /dev/null +++ b/src/wallet/components/wallet-advanced-send/utils/resolveAddressInput.test.ts @@ -0,0 +1,69 @@ +import { getSlicedAddress } from '@/identity/utils/getSlicedAddress'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { resolveAddressInput } from './resolveAddressInput'; +import { validateAddressInput } from './validateAddressInput'; + +vi.mock('@/identity/utils/getSlicedAddress'); +vi.mock('./validateAddressInput'); + +describe('resolveAddressInput', () => { + const mockAddress = '0x1234567890123456789012345678901234567890'; + const mockSlicedAddress = '0x1234...7890'; + + beforeEach(() => { + vi.mocked(validateAddressInput).mockResolvedValue(mockAddress); + vi.mocked(getSlicedAddress).mockReturnValue(mockSlicedAddress); + }); + + it('returns empty values when input is null', async () => { + const result = await resolveAddressInput(null, null); + expect(result).toEqual({ + display: '', + value: null, + }); + }); + + it('returns empty values when input is empty string', async () => { + const result = await resolveAddressInput(null, ''); + expect(result).toEqual({ + display: '', + value: null, + }); + }); + + it('validates input when no address is selected', async () => { + const input = '0xabcd'; + const result = await resolveAddressInput(null, input); + + expect(validateAddressInput).toHaveBeenCalledWith(input); + expect(result).toEqual({ + display: input, + value: mockAddress, + }); + }); + + it('returns sliced address when input is address format and address is selected', async () => { + const input = '0x1234567890123456789012345678901234567890'; + const selectedAddress = '0x9876543210987654321098765432109876543210'; + + const result = await resolveAddressInput(selectedAddress, input); + + expect(getSlicedAddress).toHaveBeenCalledWith(input); + expect(result).toEqual({ + display: mockSlicedAddress, + value: selectedAddress, + }); + }); + + it('returns input name when input is not address format and address is selected', async () => { + const input = 'Vitalik'; + const selectedAddress = '0x9876543210987654321098765432109876543210'; + + const result = await resolveAddressInput(selectedAddress, input); + + expect(result).toEqual({ + display: input, + value: selectedAddress, + }); + }); +}); From 34d0a1dbbacbaf67728f42417bebea74e8668053 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 23:38:43 -0800 Subject: [PATCH 17/31] fix lints --- .../components/SendAmountInput.test.tsx | 4 ++-- .../components/SendAmountInputTypeSwitch.test.tsx | 4 ++-- .../components/SendAmountInputTypeSwitch.tsx | 2 +- .../wallet-advanced-send/components/SendFundWallet.test.tsx | 5 ++--- .../components/SendTokenSelector.test.tsx | 4 ++-- .../utils/defaultSendTxSuccessHandler.test.ts | 2 +- .../utils/getDefaultSendButtonLabel.test.ts | 6 ++---- 7 files changed, 12 insertions(+), 15 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 a26fd113de..201a4c9d2a 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx @@ -1,6 +1,6 @@ -import { render } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AmountInput } from '@/internal/components/amount-input/AmountInput'; +import { render } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SendAmountInput } from './SendAmountInput'; import { SendAmountInputTypeSwitch } from './SendAmountInputTypeSwitch'; 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 4095449591..7e42f86af4 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx @@ -1,7 +1,7 @@ -import { render } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; 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 { SendAmountInputTypeSwitch } from './SendAmountInputTypeSwitch'; vi.mock('@/internal/components/Skeleton'); diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx index ffff38642f..a6fdce8f0e 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx @@ -1,5 +1,5 @@ -import { AmountInputTypeSwitch } from '@/internal/components/amount-input/AmountInputTypeSwitch'; import { Skeleton } from '@/internal/components/Skeleton'; +import { AmountInputTypeSwitch } from '@/internal/components/amount-input/AmountInputTypeSwitch'; import { cn, color, text } from '@/styles/theme'; import type { SendAmountInputProps } from '../types'; diff --git a/src/wallet/components/wallet-advanced-send/components/SendFundWallet.test.tsx b/src/wallet/components/wallet-advanced-send/components/SendFundWallet.test.tsx index 5a953ffcbb..2c00fc6dd1 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendFundWallet.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendFundWallet.test.tsx @@ -1,5 +1,3 @@ -import { render, screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; import { FundCard, FundCardAmountInput, @@ -8,9 +6,10 @@ import { FundCardPresetAmountInputList, FundCardSubmitButton, } from '@/fund'; +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SendFundWallet } from './SendFundWallet'; -// Mock all fund components vi.mock('@/fund/components/FundCard'); vi.mock('@/fund/components/FundCardAmountInput'); vi.mock('@/fund/components/FundCardAmountInputTypeSwitch'); 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 cb299b7447..0c1a8115ea 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx @@ -1,8 +1,8 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +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 type { PortfolioTokenWithFiatValue } from '@/api/types'; // Mock the context hook vi.mock('../../WalletAdvancedProvider', () => ({ diff --git a/src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.test.ts b/src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.test.ts index 346d93ff00..123e1794a7 100644 --- a/src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.test.ts +++ b/src/wallet/components/wallet-advanced-send/utils/defaultSendTxSuccessHandler.test.ts @@ -1,6 +1,6 @@ import { getChainExplorer } from '@/core/network/getChainExplorer'; import type { Address, Chain, TransactionReceipt } from 'viem'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { defaultSendTxSuccessHandler } from './defaultSendTxSuccessHandler'; vi.mock('@/core/network/getChainExplorer'); diff --git a/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts b/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts index 7bc1f8688f..e108d75322 100644 --- a/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts +++ b/src/wallet/components/wallet-advanced-send/utils/getDefaultSendButtonLabel.test.ts @@ -1,5 +1,5 @@ import type { PortfolioTokenWithFiatValue } from '@/api/types'; -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { getDefaultSendButtonLabel } from './getDefaultSendButtonLabel'; describe('getDefaultSendButtonLabel', () => { @@ -33,9 +33,7 @@ describe('getDefaultSendButtonLabel', () => { }); it('returns "Continue" when amount is valid and within balance', () => { - expect(getDefaultSendButtonLabel('0.0001', mockToken)).toBe( - 'Continue', - ); + expect(getDefaultSendButtonLabel('0.0001', mockToken)).toBe('Continue'); }); it('returns "Continue" when amount equals balance exactly', () => { From 037d9a6b5d34ae650f5e74a3e15eef26727634f2 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 23:58:04 -0800 Subject: [PATCH 18/31] hanlde optional subtitle --- src/token/components/TokenBalance.tsx | 2 +- src/token/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/token/components/TokenBalance.tsx b/src/token/components/TokenBalance.tsx index e18620de2c..6311a6a082 100644 --- a/src/token/components/TokenBalance.tsx +++ b/src/token/components/TokenBalance.tsx @@ -76,7 +76,7 @@ function TokenBalanceContent({ tokenValueClassName, )} > - {`${formattedCryptoValue} ${token.symbol} ${subtitle}`} + {`${formattedCryptoValue} ${token.symbol} ${subtitle ?? ''}`}
diff --git a/src/token/types.ts b/src/token/types.ts index 736efb295e..a8279d2972 100644 --- a/src/token/types.ts +++ b/src/token/types.ts @@ -129,7 +129,7 @@ export type TokenBalanceProps = { /** Token with fiat and crypto balance*/ token: PortfolioTokenWithFiatValue; /** Subtitle to display next to the token name (eg. "available") */ - subtitle: string; + subtitle?: string; /** Show the token image (default: true) */ showImage?: boolean; /** Click handler for the whole component*/ From 4bf85a0a0c1480de49aecae1fd0c02145be4be1f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 23:58:17 -0800 Subject: [PATCH 19/31] add tests --- src/token/components/TokenBalance.test.tsx | 159 +++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/token/components/TokenBalance.test.tsx diff --git a/src/token/components/TokenBalance.test.tsx b/src/token/components/TokenBalance.test.tsx new file mode 100644 index 0000000000..5865aa1d0a --- /dev/null +++ b/src/token/components/TokenBalance.test.tsx @@ -0,0 +1,159 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { TokenBalance } from './TokenBalance'; + +const mockToken = { + symbol: 'ETH', + address: '' as const, + chainId: 8453, + decimals: 18, + image: null, + name: 'Ethereum', + cryptoBalance: 1, + fiatBalance: 3300, +}; + +describe('TokenBalance', () => { + describe('Main TokenBalance component', () => { + it('renders as div when no onClick provided', () => { + render(); + expect(screen.queryByRole('button')).toBeNull(); + }); + + it('renders as button with onClick handler', () => { + const onClick = vi.fn(); + render(); + + const button = screen.getByRole('button'); + fireEvent.click(button); + expect(onClick).toHaveBeenCalledWith(mockToken); + }); + + it('applies custom className', () => { + const customClass = 'custom-class'; + render(); + expect(screen.getByTestId('ockTokenBalanceButton')).toHaveClass( + customClass, + ); + }); + }); + + describe('TokenBalanceContent', () => { + it('renders token details correctly', () => { + render(); + + expect(screen.getByText('Ethereum')).toBeInTheDocument(); + expect(screen.getByText('0.000 ETH Test subtitle')).toBeInTheDocument(); + expect(screen.getByText('$3,300.00')).toBeInTheDocument(); + }); + + it('shows/hides token image based on showImage prop', () => { + const { rerender } = render( + , + ); + expect(screen.getByTestId('ockTokenImage_NoImage')).toBeInTheDocument(); + + rerender( + , + ); + expect(screen.queryByTestId('ockTokenImage_NoImage')).toBeNull(); + }); + + it('renders subtitle when provided', () => { + const subtitle = '(50% of balance)'; + render(); + expect(screen.getByText(`0.000 ETH ${subtitle}`)).toBeInTheDocument(); + }); + + it('renders action button when showAction is true', () => { + const onActionPress = vi.fn(); + render( + , + ); + + const actionButton = screen.getByRole('button', { + name: 'Custom Action', + }); + expect(actionButton).toBeInTheDocument(); + + fireEvent.click(actionButton); + expect(onActionPress).toHaveBeenCalled(); + }); + + it('handles keyboard events on action button', () => { + const onActionPress = vi.fn(); + render( + , + ); + + const actionButton = screen.getByRole('button', { name: 'Use max' }); + fireEvent.keyDown(actionButton); + expect(onActionPress).toHaveBeenCalled(); + }); + + it('applies custom class names to token elements', () => { + const customClasses = { + tokenNameClassName: 'custom-name', + tokenValueClassName: 'custom-value', + fiatValueClassName: 'custom-fiat', + actionClassName: 'custom-action', + }; + + render(); + + expect(screen.getByText('Ethereum')).toHaveClass('custom-name'); + expect(screen.getByText('0.000 ETH')).toHaveClass('custom-value'); + expect(screen.getByText('$3,300.00')).toHaveClass('custom-fiat'); + }); + + it('handles token with empty/null name', () => { + const tokenWithoutName = { + ...mockToken, + name: null as unknown as string, + }; + render( + , + ); + + const nameElement = screen.getByText('', { + selector: 'span.ock-font-family.font-semibold', + }); + expect(nameElement).toBeInTheDocument(); + }); + + it('handles token size prop correctly', () => { + const customSize = 60; + render( + , + ); + const imageContainer = screen.getByTestId('ockTokenImage_NoImage'); + expect(imageContainer).toHaveStyle({ + width: `${customSize}px`, + height: `${customSize}px`, + minWidth: `${customSize}px`, + minHeight: `${customSize}px`, + }); + }); + }); +}); From 46b70b818bd1d96810533453da030a9d617bbcb0 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Thu, 30 Jan 2025 23:58:24 -0800 Subject: [PATCH 20/31] fix typos --- .../wallet-advanced-send/components/SendAmountInput.test.tsx | 2 +- .../components/SendAmountInputTypeSwitch.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 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 201a4c9d2a..5a6b4128f3 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.test.tsx @@ -13,7 +13,7 @@ const mockToken = { chainId: 8453, decimals: 18, image: null, - name: 'Base', + name: 'Ethereum', cryptoBalance: 1, fiatBalance: 3300, }; 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 7e42f86af4..9a4aceb15e 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx @@ -13,7 +13,7 @@ const mockToken = { chainId: 8453, decimals: 18, image: null, - name: 'Base', + name: 'Ethereum', cryptoBalance: 1, fiatBalance: 3300, }; From ae4a93bfd907473bde41054ff748b3166da18093 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 31 Jan 2025 10:00:00 -0800 Subject: [PATCH 21/31] add loadingDisplay override to AmountInputTypeSwitch --- .../amount-input/AmountInputTypeSwitch.tsx | 4 +++- .../components/SendAmountInputTypeSwitch.tsx | 13 ++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/internal/components/amount-input/AmountInputTypeSwitch.tsx b/src/internal/components/amount-input/AmountInputTypeSwitch.tsx index a192e725a0..712d78e47c 100644 --- a/src/internal/components/amount-input/AmountInputTypeSwitch.tsx +++ b/src/internal/components/amount-input/AmountInputTypeSwitch.tsx @@ -13,6 +13,7 @@ type AmountInputTypeSwitchPropsReact = { cryptoAmount: string; exchangeRate: number; exchangeRateLoading: boolean; + loadingDisplay?: React.ReactNode; currency: string; className?: string; }; @@ -26,6 +27,7 @@ export function AmountInputTypeSwitch({ exchangeRate, exchangeRateLoading, currency, + loadingDisplay = , className, }: AmountInputTypeSwitchPropsReact) { const iconSvg = useIcon({ icon: 'toggle' }); @@ -56,7 +58,7 @@ export function AmountInputTypeSwitch({ }, [cryptoAmount, fiatAmount, selectedInputType, formatCrypto, currency]); if (exchangeRateLoading || !exchangeRate) { - return ; + return loadingDisplay; } return ( diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx index a6fdce8f0e..c0380b55b8 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx @@ -25,13 +25,11 @@ export function SendAmountInputTypeSwitch({ return ; } - if (exchangeRate <= 0) { - return ( -
- Exchange rate unavailable -
- ); - } + const loadingDisplay = ( +
+ Exchange rate unavailable +
+ ); return ( Date: Fri, 31 Jan 2025 10:04:44 -0800 Subject: [PATCH 22/31] refactor TokenBaance for readability --- src/token/components/TokenBalance.tsx | 28 ++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/token/components/TokenBalance.tsx b/src/token/components/TokenBalance.tsx index 6311a6a082..36a6aae7fb 100644 --- a/src/token/components/TokenBalance.tsx +++ b/src/token/components/TokenBalance.tsx @@ -6,27 +6,37 @@ import { formatUnits } from 'viem'; import type { TokenBalanceProps } from '../types'; export function TokenBalance({ + token, onClick, className, - token, ...contentProps }: TokenBalanceProps) { - const Wrapper = onClick ? 'button' : 'div'; + if (onClick) { + return ( + + ); + } return ( - onClick(token), - })} +
- +
); } From a27d1942110caf62c70ba44cddea322cb646f69c Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 31 Jan 2025 10:13:40 -0800 Subject: [PATCH 23/31] add classname override and remove unnecessary object --- .../components/SendAmountInputTypeSwitch.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx index c0380b55b8..4203e371f1 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx @@ -11,7 +11,10 @@ export function SendAmountInputTypeSwitch({ cryptoAmount, selectedInputType, setSelectedInputType, -}: Pick< + className, +}: { + className?: string; +} & Pick< SendAmountInputProps, | 'exchangeRateLoading' | 'exchangeRate' @@ -39,9 +42,10 @@ export function SendAmountInputTypeSwitch({ exchangeRate={exchangeRate} exchangeRateLoading={false} loadingDisplay={loadingDisplay} - currency={'USD'} + currency="USD" selectedInputType={selectedInputType} setSelectedInputType={setSelectedInputType} + className={className} /> ); } From e20e4f1a6567736225a0e1c9c78e171cb3d55b2f Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 31 Jan 2025 10:42:11 -0800 Subject: [PATCH 24/31] classNames object --- src/token/components/TokenBalance.tsx | 45 ++++++++++--------- src/token/types.ts | 22 ++++----- .../components/SendTokenSelector.tsx | 20 +++++++-- .../components/wallet-advanced-send/types.ts | 10 ++++- 4 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/token/components/TokenBalance.tsx b/src/token/components/TokenBalance.tsx index 36a6aae7fb..165bbd41aa 100644 --- a/src/token/components/TokenBalance.tsx +++ b/src/token/components/TokenBalance.tsx @@ -4,11 +4,12 @@ import { border, cn, color, text } from '@/styles/theme'; import { TokenImage } from '@/token'; import { formatUnits } from 'viem'; import type { TokenBalanceProps } from '../types'; +import { useCallback } from 'react'; export function TokenBalance({ token, onClick, - className, + classNames, ...contentProps }: TokenBalanceProps) { if (onClick) { @@ -18,7 +19,7 @@ export function TokenBalance({ onClick={() => onClick(token)} className={cn( 'flex w-full items-center justify-start gap-4 px-2 py-1', - className, + classNames?.container, )} data-testid="ockTokenBalanceButton" > @@ -31,7 +32,7 @@ export function TokenBalance({
@@ -44,14 +45,10 @@ function TokenBalanceContent({ token, subtitle, showImage = true, - showAction = false, actionText = 'Use max', onActionPress, tokenSize = 40, - tokenNameClassName, - tokenValueClassName, - fiatValueClassName, - actionClassName, + classNames, }: TokenBalanceProps) { const formattedFiatValue = formatFiatAmount({ amount: token.fiatBalance, @@ -63,6 +60,18 @@ function TokenBalanceContent({ 3, ); + const handleActionPress = useCallback( + ( + e: + | React.MouseEvent + | React.KeyboardEvent, + ) => { + e.stopPropagation(); + onActionPress?.(); + }, + [onActionPress], + ); + return (
@@ -74,7 +83,7 @@ function TokenBalanceContent({ text.headline, color.foreground, 'overflow-hidden text-ellipsis whitespace-nowrap', - tokenNameClassName, + classNames?.tokenName, )} > {token.name?.trim()} @@ -83,32 +92,26 @@ function TokenBalanceContent({ className={cn( text.label2, color.foregroundMuted, - tokenValueClassName, + classNames?.tokenValue, )} > {`${formattedCryptoValue} ${token.symbol} ${subtitle ?? ''}`}
- {showAction ? ( + {onActionPress ? (
{ - e.stopPropagation(); - onActionPress?.(); - }} - onKeyDown={(e) => { - e.stopPropagation(); - onActionPress?.(); - }} + onClick={handleActionPress} + onKeyDown={handleActionPress} className={cn( text.label2, color.primary, border.radius, 'ml-auto cursor-pointer p-0.5 font-bold', 'border border-transparent hover:border-[--ock-line-primary]', - actionClassName, + classNames?.action, )} > {actionText} @@ -119,7 +122,7 @@ function TokenBalanceContent({ text.label2, color.foregroundMuted, 'whitespace-nowrap', - fiatValueClassName, + classNames?.fiatValue, )} > {formattedFiatValue} diff --git a/src/token/types.ts b/src/token/types.ts index a8279d2972..0a29f5a084 100644 --- a/src/token/types.ts +++ b/src/token/types.ts @@ -136,27 +136,23 @@ export type TokenBalanceProps = { onClick?: (token: PortfolioTokenWithFiatValue) => void; /** Size of the token image in px (default: 40) */ tokenSize?: number; - /** Optional additional CSS class to apply to the component */ - className?: string; - /** Optional additional CSS class to apply to the token name */ - tokenNameClassName?: string; - /** Optional additional CSS class to apply to the token value */ - tokenValueClassName?: string; - /** Optional additional CSS class to apply to the fiat value */ - fiatValueClassName?: string; - /** Optional additional CSS class to apply to the action button */ - actionClassName?: string; + /** Optional additional CSS classes to apply to the component */ + classNames?: { + container?: string; + tokenName?: string; + tokenValue?: string; + fiatValue?: string; + action?: string; + }; } & ( | { /** Hide the action button (default)*/ - showAction?: false; actionText?: never; onActionPress?: never; } | { /** Show an additional action button (eg. "Use max") */ - showAction?: true; actionText?: string; - onActionPress?: () => void; + onActionPress: () => void; } ); diff --git a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx index e51f1b3d34..b2e4e5c69c 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.tsx @@ -13,6 +13,7 @@ export function SendTokenSelector({ setSelectedInputType, handleCryptoAmountChange, handleFiatAmountChange, + classNames, }: SendTokenSelectorProps) { const { tokenBalances } = useWalletAdvancedContext(); @@ -29,7 +30,14 @@ export function SendTokenSelector({ token={token} onClick={handleTokenSelection} subtitle="" - className={cn(pressable.default, border.radius)} + classNames={{ + container: cn( + pressable.default, + border.radius, + classNames?.container, + ), + ...classNames, + }} /> ))}
@@ -43,7 +51,6 @@ export function SendTokenSelector({ showImage={true} subtitle="available" onClick={handleResetTokenSelection} - showAction={true} onActionPress={() => { setSelectedInputType('crypto'); handleFiatAmountChange(String(selectedToken.fiatBalance)); @@ -56,7 +63,14 @@ export function SendTokenSelector({ ), ); }} - className={cn(pressable.alternate, border.radius, 'p-2')} + classNames={{ + container: cn( + pressable.alternate, + border.radius, + classNames?.container, + ), + ...classNames, + }} /> ); } diff --git a/src/wallet/components/wallet-advanced-send/types.ts b/src/wallet/components/wallet-advanced-send/types.ts index bd0c327835..a3b78e5822 100644 --- a/src/wallet/components/wallet-advanced-send/types.ts +++ b/src/wallet/components/wallet-advanced-send/types.ts @@ -125,7 +125,15 @@ export type SendFundingWalletProps = { subtitleClassName?: string; }; -export type SendTokenSelectorProps = Pick< +export type SendTokenSelectorProps = { + classNames?: { + container?: string; + tokenName?: string; + tokenValue?: string; + fiatValue?: string; + action?: string; + }; +} & Pick< SendContextType, | 'selectedToken' | 'handleTokenSelection' From 79c64d48ffd2c3ca109a6e5ed6f395f20cd33cae Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 31 Jan 2025 10:44:59 -0800 Subject: [PATCH 25/31] classNames object --- .../components/SendFundWallet.tsx | 12 +++++++----- src/wallet/components/wallet-advanced-send/types.ts | 7 +++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx b/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx index 579c1550ed..607d586116 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendFundWallet.tsx @@ -13,16 +13,18 @@ export function SendFundWallet({ onError, onStatus, onSuccess, - className, - subtitleClassName, + classNames, }: SendFundingWalletProps) { return (
Insufficient ETH balance to send transaction. Fund your wallet to continue. @@ -35,7 +37,7 @@ export function SendFundWallet({ onError={onError} onStatus={onStatus} onSuccess={onSuccess} - className={cn('mt-3 w-88 border-none py-0', className)} + className={cn('mt-3 w-88 border-none py-0', classNames?.fundCard)} > diff --git a/src/wallet/components/wallet-advanced-send/types.ts b/src/wallet/components/wallet-advanced-send/types.ts index a3b78e5822..becf90ff0b 100644 --- a/src/wallet/components/wallet-advanced-send/types.ts +++ b/src/wallet/components/wallet-advanced-send/types.ts @@ -121,8 +121,11 @@ export type SendFundingWalletProps = { onError?: () => void; onStatus?: () => void; onSuccess?: () => void; - className?: string; - subtitleClassName?: string; + classNames?: { + container?: string; + subtitle?: string; + fundCard?: string; + }; }; export type SendTokenSelectorProps = { From 86b19c15819d00d2842197f480a4208aa8273d72 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 31 Jan 2025 11:09:27 -0800 Subject: [PATCH 26/31] add classNames overrides, tests --- src/token/components/TokenBalance.test.tsx | 54 ++++++++++++++++------ src/token/components/TokenBalance.tsx | 5 +- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/token/components/TokenBalance.test.tsx b/src/token/components/TokenBalance.test.tsx index 5865aa1d0a..2a7a641451 100644 --- a/src/token/components/TokenBalance.test.tsx +++ b/src/token/components/TokenBalance.test.tsx @@ -29,11 +29,20 @@ describe('TokenBalance', () => { expect(onClick).toHaveBeenCalledWith(mockToken); }); - it('applies custom className', () => { - const customClass = 'custom-class'; - render(); + it('applies custom classNames to the div container', () => { + const customClassNames = { container: 'custom-class' }; + render(); + expect(screen.getByTestId('ockTokenBalance')).toHaveClass( + customClassNames.container, + ); + }); + + it('applies custom classNames to the button container', () => { + const customClassNames = { container: 'custom-class' }; + const handleClick = vi.fn(); + render(); expect(screen.getByTestId('ockTokenBalanceButton')).toHaveClass( - customClass, + customClassNames.container, ); }); }); @@ -78,7 +87,6 @@ describe('TokenBalance', () => { render( , @@ -98,7 +106,6 @@ describe('TokenBalance', () => { render( , ); @@ -108,21 +115,42 @@ describe('TokenBalance', () => { expect(onActionPress).toHaveBeenCalled(); }); - it('applies custom class names to token elements', () => { - const customClasses = { - tokenNameClassName: 'custom-name', - tokenValueClassName: 'custom-value', - fiatValueClassName: 'custom-fiat', - actionClassName: 'custom-action', + it('applies custom class names to token elements when no action is provided', () => { + const customClassNames = { + tokenName: 'custom-name', + tokenValue: 'custom-value', + fiatValue: 'custom-fiat', }; - render(); + render(); expect(screen.getByText('Ethereum')).toHaveClass('custom-name'); expect(screen.getByText('0.000 ETH')).toHaveClass('custom-value'); expect(screen.getByText('$3,300.00')).toHaveClass('custom-fiat'); }); + it('applies custom class names to token elements when action is provided', () => { + const customClassNames = { + tokenName: 'custom-name', + tokenValue: 'custom-value', + action: 'custom-action', + }; + + render( + {}} + />, + ); + + expect(screen.getByText('Ethereum')).toHaveClass('custom-name'); + expect(screen.getByText('0.000 ETH')).toHaveClass('custom-value'); + expect(screen.getByTestId('ockTokenBalanceAction')).toHaveClass( + 'custom-action', + ); + }); + it('handles token with empty/null name', () => { const tokenWithoutName = { ...mockToken, diff --git a/src/token/components/TokenBalance.tsx b/src/token/components/TokenBalance.tsx index 165bbd41aa..468a7e8205 100644 --- a/src/token/components/TokenBalance.tsx +++ b/src/token/components/TokenBalance.tsx @@ -23,7 +23,7 @@ export function TokenBalance({ )} data-testid="ockTokenBalanceButton" > - + ); } @@ -36,7 +36,7 @@ export function TokenBalance({ )} data-testid="ockTokenBalance" > - +
); } @@ -102,6 +102,7 @@ function TokenBalanceContent({ {onActionPress ? (
Date: Fri, 31 Jan 2025 11:34:27 -0800 Subject: [PATCH 27/31] update tests --- .../SendAmountInputTypeSwitch.test.tsx | 20 +++++++++----- .../components/SendAmountInputTypeSwitch.tsx | 12 ++++----- .../components/SendFundWallet.test.tsx | 20 ++++++++++---- .../components/SendTokenSelector.test.tsx | 26 +++++++++++++++++++ 4 files changed, 61 insertions(+), 17 deletions(-) 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 9a4aceb15e..57fac66afe 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.test.tsx @@ -18,7 +18,7 @@ const mockToken = { fiatBalance: 3300, }; -describe('SendAmountInput', () => { +describe('SendAmountInputTypeSwitch', () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -35,14 +35,18 @@ describe('SendAmountInput', () => { exchangeRateLoading: false, className: 'test-class', textClassName: 'test-text-class', + loadingDisplay:
test-loading-display
, }; - it('shows error message when exchange rate is invalid', () => { - const { container } = render( - , + it('passes an error state when exchange rate is invalid', () => { + render(); + expect(AmountInputTypeSwitch).toHaveBeenCalledWith( + expect.objectContaining({ + loadingDisplay:
test-loading-display
, + exchangeRate: 0, + }), + {}, ); - expect(container).toHaveTextContent('Exchange rate unavailable'); - expect(AmountInputTypeSwitch).not.toHaveBeenCalled(); }); it('shows skeleton when exchange rate is loading', () => { @@ -67,6 +71,8 @@ describe('SendAmountInput', () => { currency: 'USD', selectedInputType: defaultProps.selectedInputType, setSelectedInputType: defaultProps.setSelectedInputType, + className: defaultProps.className, + loadingDisplay: defaultProps.loadingDisplay, }, {}, ); @@ -92,6 +98,8 @@ describe('SendAmountInput', () => { currency: 'USD', selectedInputType: defaultProps.selectedInputType, setSelectedInputType: defaultProps.setSelectedInputType, + className: defaultProps.className, + loadingDisplay: defaultProps.loadingDisplay, }, {}, ); diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx index 4203e371f1..68f98cbb68 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx @@ -5,6 +5,11 @@ import type { SendAmountInputProps } from '../types'; export function SendAmountInputTypeSwitch({ exchangeRateLoading, + loadingDisplay = ( +
+ Exchange rate unavailable +
+ ), exchangeRate, selectedToken, fiatAmount, @@ -14,6 +19,7 @@ export function SendAmountInputTypeSwitch({ className, }: { className?: string; + loadingDisplay?: React.ReactNode; } & Pick< SendAmountInputProps, | 'exchangeRateLoading' @@ -28,12 +34,6 @@ export function SendAmountInputTypeSwitch({ return ; } - const loadingDisplay = ( -
- Exchange rate unavailable -
- ); - return ( { onError: vi.fn(), onStatus: vi.fn(), onSuccess: vi.fn(), - className: 'test-class', - subtitleClassName: 'test-subtitle-class', + classNames: { + container: 'test-class', + subtitle: 'test-subtitle-class', + fundCard: 'test-fund-card-class', + }, }; it('renders with correct base structure', () => { @@ -38,6 +41,7 @@ describe('SendFundWallet', () => { 'flex-col', 'items-center', 'justify-between', + defaultProps.classNames.container, ); }); @@ -52,8 +56,14 @@ describe('SendFundWallet', () => { onError: defaultProps.onError, onStatus: defaultProps.onStatus, onSuccess: defaultProps.onSuccess, - className: expect.stringContaining('test-class'), - children: expect.any(Array), + className: expect.stringContaining(defaultProps.classNames.fundCard), + children: [ + expect.any(Object), // FundCardAmountInput + expect.any(Object), // FundCardAmountInputTypeSwitch + expect.any(Object), // FundCardPresetAmountInputList + expect.any(Object), // FundCardPaymentMethodDropdown + expect.any(Object), // FundCardSubmitButton + ], }, {}, ); @@ -75,7 +85,7 @@ describe('SendFundWallet', () => { it('applies correct className to subtitle', () => { const { container } = render(); const subtitle = container.querySelector( - `.${defaultProps.subtitleClassName}`, + `.${defaultProps.classNames.subtitle}`, ); expect(subtitle).toBeInTheDocument(); expect(subtitle).toHaveTextContent( 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 0c1a8115ea..79bf8c5749 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx @@ -117,4 +117,30 @@ describe('SendTokenSelector', () => { render(); expect(screen.getByText('Select a token')).toBeInTheDocument(); }); + + it('applies custom classNames when provided', () => { + const customClassNames = { + container: 'custom-button', + }; + + const { rerender } = render( + , + ); + const buttons = screen.getAllByTestId('ockTokenBalanceButton'); + expect(buttons[0]).toHaveClass(customClassNames.container); + expect(buttons[1]).toHaveClass(customClassNames.container); + + rerender( + , + ); + const button = screen.getByTestId('ockTokenBalanceButton'); + expect(button).toHaveClass(customClassNames.container); + }); }); From 30decf8617c86c3dda4923c232476d4203392cb3 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Fri, 31 Jan 2025 11:35:18 -0800 Subject: [PATCH 28/31] fix lints --- src/token/components/TokenBalance.test.tsx | 15 ++++++++------- src/token/components/TokenBalance.tsx | 14 +++++++++++--- .../components/SendTokenSelector.test.tsx | 5 +---- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/token/components/TokenBalance.test.tsx b/src/token/components/TokenBalance.test.tsx index 2a7a641451..fed23a5d6f 100644 --- a/src/token/components/TokenBalance.test.tsx +++ b/src/token/components/TokenBalance.test.tsx @@ -40,7 +40,13 @@ describe('TokenBalance', () => { it('applies custom classNames to the button container', () => { const customClassNames = { container: 'custom-class' }; const handleClick = vi.fn(); - render(); + render( + , + ); expect(screen.getByTestId('ockTokenBalanceButton')).toHaveClass( customClassNames.container, ); @@ -103,12 +109,7 @@ describe('TokenBalance', () => { it('handles keyboard events on action button', () => { const onActionPress = vi.fn(); - render( - , - ); + render(); const actionButton = screen.getByRole('button', { name: 'Use max' }); fireEvent.keyDown(actionButton); diff --git a/src/token/components/TokenBalance.tsx b/src/token/components/TokenBalance.tsx index 468a7e8205..f2c485fdfb 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 { useCallback } from 'react'; import { formatUnits } from 'viem'; import type { TokenBalanceProps } from '../types'; -import { useCallback } from 'react'; export function TokenBalance({ token, @@ -23,7 +23,11 @@ export function TokenBalance({ )} data-testid="ockTokenBalanceButton" > - + ); } @@ -36,7 +40,11 @@ export function TokenBalance({ )} data-testid="ockTokenBalance" > - +
); } 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 79bf8c5749..8358529ee2 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendTokenSelector.test.tsx @@ -124,10 +124,7 @@ describe('SendTokenSelector', () => { }; const { rerender } = render( - , + , ); const buttons = screen.getAllByTestId('ockTokenBalanceButton'); expect(buttons[0]).toHaveClass(customClassNames.container); From 1aa46ca158398acbb2586e233753268115da14e4 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 3 Feb 2025 14:31:26 -0800 Subject: [PATCH 29/31] add comments --- .../components/SendAmountInputTypeSwitch.tsx | 3 +++ .../components/wallet-advanced-send/types.ts | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx index 68f98cbb68..fecf313b56 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInputTypeSwitch.tsx @@ -30,6 +30,9 @@ export function SendAmountInputTypeSwitch({ | 'selectedInputType' | 'setSelectedInputType' >) { + // AmountInputTypeSwitch uses a skeleton for both loading and error states + // SendAmountInputTypeSwitch uses skeleton for the loading display + // SendAmountInputTypeSwitch uses a custom error display (see loadingDisplay default) if (exchangeRateLoading) { return ; } diff --git a/src/wallet/components/wallet-advanced-send/types.ts b/src/wallet/components/wallet-advanced-send/types.ts index becf90ff0b..86796fb1e5 100644 --- a/src/wallet/components/wallet-advanced-send/types.ts +++ b/src/wallet/components/wallet-advanced-send/types.ts @@ -10,41 +10,62 @@ export type SendProviderReact = { export type SendContextType = { // Lifecycle Status Context + /** Whether the send component data has been initialized */ isInitialized: boolean; + /** The current lifecycle status of the send component */ lifecycleStatus: SendLifecycleStatus; + /** Handler for updating the lifecycle status of the send component */ updateLifecycleStatus: ( status: LifecycleStatusUpdate, ) => void; // Sender Context + /** The balance of the sender's ETH wallet */ ethBalance: number | undefined; // Recipient Address Context + /** The selected recipient address */ selectedRecipientAddress: RecipientAddress; + /** Handler for the selection of a recipient address */ handleAddressSelection: (selection: RecipientAddress) => void; + /** Handler for the change of a recipient address */ handleRecipientInputChange: () => void; // Token Context + /** The token selected by the user for the send transaction */ selectedToken: PortfolioTokenWithFiatValue | null; + /** Handler for the selection of a token */ handleTokenSelection: (token: PortfolioTokenWithFiatValue) => void; + /** Handler for the reset of a token selection */ handleResetTokenSelection: () => void; // Amount Context + /** The type of input selected by the user for the send transaction, defaults to crypto */ selectedInputType: 'fiat' | 'crypto'; + /** Handler for the selection of an input type */ setSelectedInputType: Dispatch>; + /** The exchange rate for the selected token */ exchangeRate: number; + /** Whether the exchange rate is loading */ exchangeRateLoading: boolean; + /** The fiat amount selected by the user for the send transaction */ fiatAmount: string | null; + /** Handler for the change of a fiat amount */ handleFiatAmountChange: (value: string) => void; + /** The crypto amount selected by the user for the send transaction */ cryptoAmount: string | null; + /** Handler for the change of a crypto amount */ handleCryptoAmountChange: (value: string) => void; // Transaction Context + /** The call data for the send transaction */ callData: Call | null; }; export type RecipientAddress = { + /** The value to display in the input field of the recipient address */ display: string; + /** The address of the recipient */ value: Address | null; }; From 517068276c674716564da875a03d64fd86b247a9 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 3 Feb 2025 14:37:49 -0800 Subject: [PATCH 30/31] fix lints --- src/internal/components/amount-input/AmountInput.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/internal/components/amount-input/AmountInput.tsx b/src/internal/components/amount-input/AmountInput.tsx index 22b6ec95d4..7c1ab26dee 100644 --- a/src/internal/components/amount-input/AmountInput.tsx +++ b/src/internal/components/amount-input/AmountInput.tsx @@ -108,7 +108,7 @@ export function AmountInput({ '[&::-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} inputValidator={isValidAmount} @@ -118,10 +118,10 @@ export function AmountInput({ />
+ ref={labelRef} + label={currencyOrAsset} + className={textClassName} + />
From 7cfb954459223b085730a1fa612f5c9ece2f1ef3 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Mon, 3 Feb 2025 14:50:52 -0800 Subject: [PATCH 31/31] remove extraneous div --- .../components/SendAmountInput.tsx | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx index 1b8146ddb8..8a6455bd3d 100644 --- a/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx +++ b/src/wallet/components/wallet-advanced-send/components/SendAmountInput.tsx @@ -18,31 +18,29 @@ export function SendAmountInput({ textClassName, }: SendAmountInputProps) { return ( -
-
- +
+ - -
+
); }