Skip to content

Commit

Permalink
ONRAMP-4897 [OnchainKit] Fund Card - Refresh exchange rate regularly
Browse files Browse the repository at this point in the history
  • Loading branch information
rustam-cb committed Jan 29, 2025
1 parent 797b7f0 commit 01ab0be
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 31 deletions.
46 changes: 15 additions & 31 deletions src/fund/components/FundCardProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from 'react';
import { useValue } from '../../internal/hooks/useValue';
import { useEmitLifecycleStatus } from '../hooks/useEmitLifecycleStatus';
import { useOnrampExchangeRate } from '../hooks/useOnrampExhangeRate';
import { usePaymentMethods } from '../hooks/usePaymentMethods';
import type {
AmountInputType,
Expand All @@ -17,7 +18,6 @@ import type {
PaymentMethod,
PresetAmountInputs,
} from '../types';
import { fetchOnrampQuote } from '../utils/fetchOnrampQuote';

type FundCardContextType = {
asset: string;
Expand Down Expand Up @@ -92,40 +92,24 @@ export function FundCardProvider({
onStatus,
});

const fetchExchangeRate = useCallback(async () => {
setExchangeRateLoading(true);

try {
const quote = await fetchOnrampQuote({
purchaseCurrency: asset,
paymentCurrency: currency,
paymentAmount: '100',
paymentMethod: 'CARD',
country,
subdivision,
});
const { fetchExchangeRate } = useOnrampExchangeRate({
asset,
currency,
country,
subdivision,
setExchangeRate,
onError,
});

setExchangeRate(
Number(quote.purchaseAmount.value) /
Number(quote.paymentSubtotal.value),
);
} catch (err) {
if (err instanceof Error) {
console.error('Error fetching exchange rate:', err);
onError?.({
errorType: 'handled_error',
code: 'EXCHANGE_RATE_ERROR',
debugMessage: err.message,
});
}
} finally {
setExchangeRateLoading(false);
}
}, [asset, country, subdivision, currency, onError]);
const handleFetchExchangeRate = useCallback(async () => {
setExchangeRateLoading(true);
await fetchExchangeRate();
setExchangeRateLoading(false);
}, [fetchExchangeRate]);

// biome-ignore lint/correctness/useExhaustiveDependencies: One time effect
useEffect(() => {
fetchExchangeRate();
handleFetchExchangeRate();
}, []);

// Fetches and sets the payment methods to the context
Expand Down
84 changes: 84 additions & 0 deletions src/fund/hooks/useOnrampExchangeRate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { renderHook } from '@testing-library/react';
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
import { quoteResponseDataMock } from '../mocks';
import type { OnrampError } from '../types';
import { fetchOnrampQuote } from '../utils/fetchOnrampQuote';
import { useOnrampExchangeRate } from './useOnrampExhangeRate';

vi.mock('../utils/fetchOnrampQuote');

describe('useOnrampExchangeRate', () => {
const mockSetExchangeRate = vi.fn();
const mockOnError = vi.fn();

beforeEach(() => {
vi.clearAllMocks();
(fetchOnrampQuote as Mock).mockResolvedValue(quoteResponseDataMock);
});

it('fetches and calculates exchange rate correctly', async () => {
const { result } = renderHook(() =>
useOnrampExchangeRate({
asset: 'ETH',
currency: 'USD',
country: 'US',
setExchangeRate: mockSetExchangeRate,
}),
);

await result.current.fetchExchangeRate();

// Verify exchange rate calculation
expect(mockSetExchangeRate).toHaveBeenCalledWith(
Number(quoteResponseDataMock.purchaseAmount.value) /
Number(quoteResponseDataMock.paymentSubtotal.value),
);
});

it('handles API errors', async () => {
const error = new Error('API Error');
(fetchOnrampQuote as Mock).mockRejectedValue(error);

const { result } = renderHook(() =>
useOnrampExchangeRate({
asset: 'ETH',
currency: 'USD',
country: 'US',
setExchangeRate: mockSetExchangeRate,
onError: mockOnError,
}),
);

await result.current.fetchExchangeRate();

// Should call onError with correct error object
expect(mockOnError).toHaveBeenCalledWith({
errorType: 'handled_error',
code: 'EXCHANGE_RATE_ERROR',
debugMessage: 'API Error',
} satisfies OnrampError);
});

it('includes subdivision in API call when provided', async () => {
const { result } = renderHook(() =>
useOnrampExchangeRate({
asset: 'ETH',
currency: 'USD',
country: 'US',
subdivision: 'CA',
setExchangeRate: mockSetExchangeRate,
}),
);

await result.current.fetchExchangeRate();

expect(fetchOnrampQuote).toHaveBeenCalledWith({
purchaseCurrency: 'ETH',
paymentCurrency: 'USD',
paymentAmount: '100',
paymentMethod: 'CARD',
country: 'US',
subdivision: 'CA',
});
});
});
48 changes: 48 additions & 0 deletions src/fund/hooks/useOnrampExhangeRate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useCallback, useMemo } from 'react';
import type { OnrampError } from '../types';
import { fetchOnrampQuote } from '../utils/fetchOnrampQuote';

export const useOnrampExchangeRate = ({
asset,
currency,
country,
subdivision,
setExchangeRate,
onError,
}: {
asset: string;
currency: string;
country: string;
subdivision?: string;
setExchangeRate: (exchangeRate: number) => void;
onError?: (error: OnrampError) => void;
}) => {
const fetchExchangeRate = useCallback(async () => {
try {
const quote = await fetchOnrampQuote({
purchaseCurrency: asset,
paymentCurrency: currency,
paymentAmount: '100',
paymentMethod: 'CARD',
country,
subdivision,
});

setExchangeRate(
Number(quote.purchaseAmount.value) /
Number(quote.paymentSubtotal.value),
);
} catch (err) {
if (err instanceof Error) {
console.error('Error fetching exchange rate:', err);
onError?.({
errorType: 'handled_error',
code: 'EXCHANGE_RATE_ERROR',
debugMessage: err.message,
});
}
}
}, [asset, country, subdivision, currency, onError, setExchangeRate]);

return useMemo(() => ({ fetchExchangeRate }), [fetchExchangeRate]);
};

0 comments on commit 01ab0be

Please sign in to comment.