diff --git a/src/fund/components/FundCard.tsx b/src/fund/components/FundCard.tsx index db70c4ebf6..7860cea5a1 100644 --- a/src/fund/components/FundCard.tsx +++ b/src/fund/components/FundCard.tsx @@ -3,7 +3,6 @@ import { Children, useMemo } from 'react'; import { useTheme } from '../../core-react/internal/hooks/useTheme'; import { background, border, cn, color, text } from '../../styles/theme'; import { DEFAULT_PAYMENT_METHODS } from '../constants'; -import { useExchangeRate } from '../hooks/useExchangeRate'; import { useFundCardFundingUrl } from '../hooks/useFundCardFundingUrl'; import { useFundCardSetupOnrampEventListeners } from '../hooks/useFundCardSetupOnrampEventListeners'; import type { FundCardContentPropsReact, FundCardPropsReact } from '../types'; @@ -88,11 +87,6 @@ function FundCardContent({ paymentMethods = DEFAULT_PAYMENT_METHODS, submitButtonComponent, }: FundCardContentPropsReact) { - /** - * Fetches and sets the exchange rate for the asset - */ - useExchangeRate(assetSymbol); - const { setFundAmountFiat, fundAmountFiat, diff --git a/src/fund/components/FundCardAmountInput.tsx b/src/fund/components/FundCardAmountInput.tsx index 1f70b652d9..3156635228 100644 --- a/src/fund/components/FundCardAmountInput.tsx +++ b/src/fund/components/FundCardAmountInput.tsx @@ -107,7 +107,7 @@ export const FundCardAmountInput = ({ `} -
+
{/* Display the fiat currency sign before the input*/} {inputType === 'fiat' && currencySign && ( {!hideImage && ( - + )} {paymentMethod.name} diff --git a/src/fund/components/FundCardPaymentMethodSelectorToggle.tsx b/src/fund/components/FundCardPaymentMethodSelectorToggle.tsx index fec8d70d8b..64ff9c8ab5 100644 --- a/src/fund/components/FundCardPaymentMethodSelectorToggle.tsx +++ b/src/fund/components/FundCardPaymentMethodSelectorToggle.tsx @@ -31,7 +31,7 @@ export const FundCardPaymentMethodSelectorToggle = forwardRef( data-testid="ockFundCardPaymentMethodSelectorToggle" >
- +
('default'); + const fetchExchangeRate = useDebounce(async () => { + setExchangeRateLoading(true); + const quote = await fetchOnrampQuote({ + purchaseCurrency: selectedAsset, + paymentCurrency: 'USD', + paymentAmount: '100', + paymentMethod: 'CARD', + country: 'US', + }); + + setExchangeRateLoading(false); + + setExchangeRate( + Number(quote.purchaseAmount.value) / Number(quote.paymentSubtotal.value), + ); + }, 1000); + + // biome-ignore lint/correctness/useExhaustiveDependencies: One time effect + useEffect(() => { + fetchExchangeRate(); + }, []); + const value = useValue({ selectedAsset, setSelectedAsset, diff --git a/src/fund/components/FundProvider.test.tsx b/src/fund/components/FundProvider.test.tsx index 70f8071d6e..4c75dba348 100644 --- a/src/fund/components/FundProvider.test.tsx +++ b/src/fund/components/FundProvider.test.tsx @@ -1,18 +1,47 @@ -import { render, screen } from '@testing-library/react'; +import { setOnchainKitConfig } from '@/core/OnchainKitConfig'; +import { render, screen, waitFor } from '@testing-library/react'; import { act } from 'react'; -import { describe, expect, it } from 'vitest'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; import { FundCardProvider, useFundContext } from './FundCardProvider'; +const mockResponseData = { + payment_total: { value: '100.00', currency: 'USD' }, + payment_subtotal: { value: '120.00', currency: 'USD' }, + purchase_amount: { value: '0.1', currency: 'BTC' }, + coinbase_fee: { value: '2.00', currency: 'USD' }, + network_fee: { value: '1.00', currency: 'USD' }, + quote_id: 'quote-id-123', +}; + +global.fetch = vi.fn(() => + Promise.resolve({ + json: () => Promise.resolve(mockResponseData), + }), +) as Mock; + +vi.mock('../../core-react/internal/hooks/useDebounce', () => ({ + useDebounce: vi.fn((callback) => callback), +})); + const TestComponent = () => { const context = useFundContext(); return (
{context.selectedAsset} + {context.exchangeRate} + + {context.exchangeRateLoading ? 'loading' : 'not-loading'} +
); }; describe('FundCardProvider', () => { + beforeEach(() => { + setOnchainKitConfig({ apiKey: '123456789' }); + vi.clearAllMocks(); + }); + it('provides default context values', () => { render( @@ -48,6 +77,34 @@ describe('FundCardProvider', () => { expect(screen.getByTestId('selected-asset').textContent).toBe('ETH'); }); + it('fetches and sets exchange rate on mount', async () => { + act(() => { + render( + + + , + ); + }); + + // Check initial loading state + expect(screen.getByTestId('loading-state').textContent).toBe('loading'); + + // Wait for exchange rate to be set + await waitFor(() => { + expect(screen.getByTestId('exchange-rate').textContent).toBe('0.0008333333333333334'); + expect(screen.getByTestId('loading-state').textContent).toBe('not-loading'); + }); + + // Verify fetch was called with correct parameters + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/quote'), + expect.objectContaining({ + method: 'POST', + body: expect.stringContaining('"purchase_currency":"BTC"'), + }) + ); + }); + it('throws error when useFundContext is used outside of FundCardProvider', () => { const TestOutsideProvider = () => { useFundContext(); diff --git a/src/fund/hooks/useExchangeRate.test.ts b/src/fund/hooks/useExchangeRate.test.ts deleted file mode 100644 index 2bdc1a6361..0000000000 --- a/src/fund/hooks/useExchangeRate.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { setOnchainKitConfig } from '@/core/OnchainKitConfig'; -import { renderHook } from '@testing-library/react'; -import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; -import { useFundContext } from '../components/FundCardProvider'; -import { useExchangeRate } from './useExchangeRate'; - -const mockResponseData = { - payment_total: { value: '100.00', currency: 'USD' }, - payment_subtotal: { value: '120.00', currency: 'USD' }, - purchase_amount: { value: '0.1', currency: 'BTC' }, - coinbase_fee: { value: '2.00', currency: 'USD' }, - network_fee: { value: '1.00', currency: 'USD' }, - quote_id: 'quote-id-123', -}; - -global.fetch = vi.fn(() => - Promise.resolve({ - json: () => Promise.resolve(mockResponseData), - }), -) as Mock; - -vi.mock('../../core-react/internal/hooks/useDebounce', () => ({ - useDebounce: vi.fn((callback) => callback), -})); - -vi.mock('../components/FundCardProvider', () => ({ - useFundContext: vi.fn(), -})); - -let mockSetExchangeRate = vi.fn(); -let mockSetExchangeRateLoading = vi.fn(); - -describe('useExchangeRate', () => { - beforeEach(() => { - setOnchainKitConfig({ apiKey: '123456789' }); - mockSetExchangeRate = vi.fn(); - mockSetExchangeRateLoading = vi.fn(); - (useFundContext as Mock).mockReturnValue({ - exchangeRateLoading: false, - setExchangeRate: mockSetExchangeRate, - setExchangeRateLoading: mockSetExchangeRateLoading, - }); - }); - - it('should fetch and set exchange rate correctly', async () => { - // Mock dependencies - - renderHook(() => useExchangeRate('BTC')); - - // Assert loading state - expect(mockSetExchangeRateLoading).toHaveBeenCalledWith(true); - - // Wait for the exchange rate to be fetched - await new Promise((resolve) => setTimeout(resolve, 0)); - - // Assert loading state is false and exchange rate is set correctly - expect(mockSetExchangeRateLoading).toHaveBeenCalledWith(false); - expect(mockSetExchangeRate).toHaveBeenCalledWith(0.0008333333333333334); - }); - - it('should not fetch exchange rate if already loading', () => { - // Mock exchangeRateLoading as true - (useFundContext as Mock).mockReturnValue({ - exchangeRateLoading: true, - setExchangeRate: mockSetExchangeRate, - setExchangeRateLoading: mockSetExchangeRateLoading, - }); - - // Render the hook - renderHook(() => useExchangeRate('BTC')); - - // Assert that setExchangeRateLoading was not called - expect(mockSetExchangeRateLoading).not.toHaveBeenCalled(); - - // Assert that setExchangeRate was not called - expect(mockSetExchangeRate).not.toHaveBeenCalled(); - }); -}); diff --git a/src/fund/hooks/useExchangeRate.ts b/src/fund/hooks/useExchangeRate.ts deleted file mode 100644 index 54c84c784b..0000000000 --- a/src/fund/hooks/useExchangeRate.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useMemo } from 'react'; -import { useDebounce } from '../../core-react/internal/hooks/useDebounce'; -import { useFundContext } from '../components/FundCardProvider'; -import { fetchOnrampQuote } from '../utils/fetchOnrampQuote'; - -export const useExchangeRate = (assetSymbol: string) => { - const { setExchangeRate, exchangeRateLoading, setExchangeRateLoading } = - useFundContext(); - - const fetchExchangeRate = useDebounce(async () => { - if (exchangeRateLoading) { - return; - } - - setExchangeRateLoading(true); - const quote = await fetchOnrampQuote({ - purchaseCurrency: assetSymbol, - paymentCurrency: 'USD', - paymentAmount: '100', - paymentMethod: 'CARD', - country: 'US', - }); - - setExchangeRateLoading(false); - - setExchangeRate( - Number(quote.purchaseAmount.value) / Number(quote.paymentSubtotal.value), - ); - }, 1000); - - // biome-ignore lint/correctness/useExhaustiveDependencies: One time effect - useEffect(() => { - fetchExchangeRate(); - }, []); - - return useMemo(() => ({ fetchExchangeRate }), [fetchExchangeRate]); -}; diff --git a/src/internal/components/TextInput.tsx b/src/internal/components/TextInput.tsx index dd01b3b2dc..198b2ae30e 100644 --- a/src/internal/components/TextInput.tsx +++ b/src/internal/components/TextInput.tsx @@ -13,7 +13,6 @@ type TextInputReact = { setValue: (s: string) => void; value: string; inputValidator?: (s: string) => boolean; - style?: React.CSSProperties; }; export function TextInput({ @@ -27,7 +26,6 @@ export function TextInput({ setValue, value, inputValidator = () => true, - style, }: TextInputReact) { const handleDebounce = useDebounce((value) => { onChange(value); @@ -51,7 +49,6 @@ export function TextInput({ return ( (