Skip to content

Commit

Permalink
Amount Preset inputs (#1806)
Browse files Browse the repository at this point in the history
  • Loading branch information
rustam-cb authored Jan 21, 2025
1 parent bbaceed commit 567dd02
Show file tree
Hide file tree
Showing 18 changed files with 484 additions and 90 deletions.
5 changes: 5 additions & 0 deletions playground/nextjs-app-router/components/demo/FundCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { FundCard } from '@coinbase/onchainkit/fund';
import type { PresetAmountInputs } from '../../onchainkit/esm/fund/types';

export default function FundCardDemo() {
const presetAmountInputs: PresetAmountInputs = ['10', '20', '100'];

return (
<div className="mx-auto grid w-[500px] gap-8">
<FundCard
assetSymbol="ETH"
country="US"
presetAmountInputs={presetAmountInputs}
onError={(error) => {
console.log('FundCard onError', error);
}}
Expand Down
2 changes: 1 addition & 1 deletion playground/nextjs-app-router/onchainkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/onchainkit",
"version": "0.36.5",
"version": "0.36.7",
"type": "module",
"repository": "https://github.com/coinbase/onchainkit.git",
"license": "MIT",
Expand Down
33 changes: 30 additions & 3 deletions src/fund/components/FundCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
import { useAccount } from 'wagmi';
import { useFundCardFundingUrl } from '../hooks/useFundCardFundingUrl';
import type { PresetAmountInputs } from '../types';
import { fetchOnrampQuote } from '../utils/fetchOnrampQuote';
import { getFundingPopupSize } from '../utils/getFundingPopupSize';
import { FundCard } from './FundCard';
Expand Down Expand Up @@ -110,10 +111,18 @@ const TestComponent = () => {
);
};

const renderComponent = () =>
const renderComponent = (presetAmountInputs?: PresetAmountInputs) =>
render(
<FundCardProvider asset="BTC" country="US">
<FundCard assetSymbol="BTC" country="US" />
<FundCardProvider
asset="BTC"
country="US"
presetAmountInputs={presetAmountInputs}
>
<FundCard
assetSymbol="BTC"
country="US"
presetAmountInputs={presetAmountInputs}
/>
<TestComponent />
</FundCardProvider>,
);
Expand Down Expand Up @@ -293,4 +302,22 @@ describe('FundCard', () => {
expect(screen.getByTestId('custom-child')).toBeInTheDocument();
expect(screen.queryByTestId('ockFundCardHeader')).not.toBeInTheDocument();
});

it('handles preset amount input click correctly', async () => {
const presetAmountInputs: PresetAmountInputs = ['12345', '20', '30'];

renderComponent(presetAmountInputs);

await waitFor(() => {
expect(screen.getByTestId('loading-state').textContent).toBe(
'not-loading',
);

// Click the preset amount input
const presetAmountInput = screen.getByText('12345 USD');

// Verify the input value was updated
expect(presetAmountInput).toBeInTheDocument();
});
});
});
4 changes: 4 additions & 0 deletions src/fund/components/FundCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import FundCardAmountInput from './FundCardAmountInput';
import FundCardAmountInputTypeSwitch from './FundCardAmountInputTypeSwitch';
import { FundCardHeader } from './FundCardHeader';
import { FundCardPaymentMethodDropdown } from './FundCardPaymentMethodDropdown';
import { FundCardPresetAmountInputList } from './FundCardPresetAmountInputList';
import { FundCardProvider } from './FundCardProvider';
import { FundCardSubmitButton } from './FundCardSubmitButton';

Expand All @@ -17,6 +18,7 @@ export function FundCard({
headerText,
country = 'US',
subdivision,
presetAmountInputs,
children = <DefaultFundCardContent />,
className,
onError,
Expand All @@ -36,6 +38,7 @@ export function FundCard({
onError={onError}
onStatus={onStatus}
onSuccess={onSuccess}
presetAmountInputs={presetAmountInputs}
>
<div
className={cn(
Expand Down Expand Up @@ -71,6 +74,7 @@ function DefaultFundCardContent() {
<FundCardHeader />
<FundCardAmountInput />
<FundCardAmountInputTypeSwitch />
<FundCardPresetAmountInputList />
<FundCardPaymentMethodDropdown />
<FundCardSubmitButton />
</>
Expand Down
1 change: 1 addition & 0 deletions src/fund/components/FundCardAmountInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ describe('FundCardAmountInput', () => {
expect(valueFiat.textContent).toBe('0');
});
});

it('handles non zero values in fiat mode', async () => {
act(() => {
render(
Expand Down
78 changes: 18 additions & 60 deletions src/fund/components/FundCardAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,20 @@ import { isValidAmount } from '@/core/utils/isValidAmount';
import { TextInput } from '@/internal/components/TextInput';
import { useCallback, useEffect, useRef } from 'react';
import { cn, text } from '../../styles/theme';
import { useAmountInput } from '../hooks/useAmountInput';
import { useInputResize } from '../hooks/useInputResize';
import type { FundCardAmountInputPropsReact } from '../types';
import { truncateDecimalPlaces } from '../utils/truncateDecimalPlaces';
import { FundCardCurrencyLabel } from './FundCardCurrencyLabel';
import { useFundContext } from './FundCardProvider';

export const FundCardAmountInput = ({
className,
}: FundCardAmountInputPropsReact) => {
// TODO: Get currency label from country (This is coming in the follow up PRs)
const currencyLabel = 'USD';
const { fundAmountFiat, fundAmountCrypto, asset, selectedInputType } =
useFundContext();

const {
fundAmountFiat,
setFundAmountFiat,
fundAmountCrypto,
setFundAmountCrypto,
asset,
selectedInputType,
exchangeRate,
} = useFundContext();
// Next PR will include a support for any currency
const currencyOrAsset = selectedInputType === 'fiat' ? 'USD' : asset;

const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
Expand All @@ -39,50 +32,20 @@ export const FundCardAmountInput = ({
currencySpanRef,
);

const handleFiatChange = useCallback(
(value: string) => {
const fiatValue = truncateDecimalPlaces(value, 2);
setFundAmountFiat(fiatValue);

const calculatedCryptoValue = String(
Number(fiatValue) * Number(exchangeRate),
);
const resultCryptoValue = truncateDecimalPlaces(calculatedCryptoValue, 8);
setFundAmountCrypto(
calculatedCryptoValue === '0' ? '' : resultCryptoValue,
);
},
[exchangeRate, setFundAmountFiat, setFundAmountCrypto],
);
const { handleChange } = useAmountInput();

const handleCryptoChange = useCallback(
const handleAmountChange = useCallback(
(value: string) => {
const truncatedValue = truncateDecimalPlaces(value, 8);
setFundAmountCrypto(truncatedValue);

const calculatedFiatValue = String(
Number(truncatedValue) / Number(exchangeRate),
);

const resultFiatValue = truncateDecimalPlaces(calculatedFiatValue, 2);
setFundAmountFiat(resultFiatValue === '0' ? '' : resultFiatValue);
handleChange(value, () => {
if (inputRef.current) {
inputRef.current.focus();
}
});
},
[exchangeRate, setFundAmountFiat, setFundAmountCrypto],
[handleChange],
);

const handleChange = useCallback(
(value: string) => {
if (selectedInputType === 'fiat') {
handleFiatChange(value);
} else {
handleCryptoChange(value);
}
},
[handleFiatChange, handleCryptoChange, selectedInputType],
);

// Update width when value changes
// biome-ignore lint/correctness/useExhaustiveDependencies: We want to update the input width when the value changes
// biome-ignore lint/correctness/useExhaustiveDependencies: When value changes, we want to update the input width
useEffect(() => {
updateInputWidth();
}, [value, updateInputWidth]);
Expand All @@ -103,11 +66,9 @@ export const FundCardAmountInput = ({
<div
ref={containerRef}
data-testid="ockFundCardAmountInputContainer"
className={cn('flex cursor-text py-6', className)}
onClick={handleFocusInput}
onKeyUp={handleFocusInput}
className={cn('flex cursor-text pt-6 pb-4', className)}
>
<div className="flex h-20">
<div className="flex h-14">
<TextInput
className={cn(
text.body,
Expand All @@ -118,17 +79,14 @@ export const FundCardAmountInput = ({
'[&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none',
)}
value={value}
onChange={handleChange}
onChange={handleAmountChange}
inputValidator={isValidAmount}
ref={inputRef}
inputMode="decimal"
placeholder="0"
/>

<FundCardCurrencyLabel
ref={currencySpanRef}
label={selectedInputType === 'crypto' ? asset : currencyLabel}
/>
<FundCardCurrencyLabel ref={currencySpanRef} label={currencyOrAsset} />
</div>

{/* Hidden span for measuring text width
Expand Down
6 changes: 3 additions & 3 deletions src/fund/components/FundCardPaymentMethodDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { background, border, cn } from '../../styles/theme';
import type {
FundCardPaymentMethodDropdownPropsReact,
PaymentMethodReact,
PaymentMethod,
} from '../types';
import { FundCardPaymentMethodSelectRow } from './FundCardPaymentMethodSelectRow';
import { FundCardPaymentMethodSelectorToggle } from './FundCardPaymentMethodSelectorToggle';
Expand All @@ -31,7 +31,7 @@ export function FundCardPaymentMethodDropdown({
}, [paymentMethods]);

const isPaymentMethodDisabled = useCallback(
(method: PaymentMethodReact) => {
(method: PaymentMethod) => {
const amount = Number(fundAmountFiat);
return (
(method.id === 'APPLE_PAY' || method.id === 'ACH_BANK_ACCOUNT') &&
Expand Down Expand Up @@ -60,7 +60,7 @@ export function FundCardPaymentMethodDropdown({
]);

const handlePaymentMethodSelect = useCallback(
(paymentMethod: PaymentMethodReact) => {
(paymentMethod: PaymentMethod) => {
if (!isPaymentMethodDisabled(paymentMethod)) {
setSelectedPaymentMethod(paymentMethod);
setIsOpen(false);
Expand Down
4 changes: 2 additions & 2 deletions src/fund/components/FundCardPaymentMethodSelectRow.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { PaymentMethodReact } from '../types';
import type { PaymentMethod } from '../types';
import { FundCardPaymentMethodSelectRow } from './FundCardPaymentMethodSelectRow';

describe('FundCardPaymentMethodSelectRow', () => {
const mockPaymentMethod: PaymentMethodReact = {
const mockPaymentMethod: PaymentMethod = {
id: 'APPLE_PAY',
name: 'Apple Pay',
description: 'Up to $500/week',
Expand Down
Loading

0 comments on commit 567dd02

Please sign in to comment.