Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Fund Card component #1718

Merged
merged 106 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
9fe2fde
Onramp - event subscriptions migration
rustam-cb Nov 14, 2024
5c95ff9
Fund form
rustam-cb Nov 15, 2024
7c3fba9
Merge branch 'main' into fund-form
rustam-cb Nov 19, 2024
a0486e4
Initial utils
rustam-cb Nov 20, 2024
bffb90f
Update
rustam-cb Dec 5, 2024
a56f488
Fix lint issues
rustam-cb Dec 5, 2024
34916c7
Update
rustam-cb Dec 5, 2024
455e4bd
Merge branch 'main' into onramp-api-utils
rustam-cb Dec 5, 2024
d40eca0
Merge branch 'main' into fund-form
rustam-cb Dec 5, 2024
fc9db1f
Update
rustam-cb Dec 6, 2024
595f54e
Update
rustam-cb Dec 6, 2024
486d618
Update
rustam-cb Dec 6, 2024
6ed6c16
Update
rustam-cb Dec 6, 2024
b16da4a
Update
rustam-cb Dec 6, 2024
b7b2cd2
Update
rustam-cb Dec 6, 2024
bb348df
Update
rustam-cb Dec 6, 2024
e92e933
Update
rustam-cb Dec 6, 2024
2651a5f
Update
rustam-cb Dec 6, 2024
e91f671
Update
rustam-cb Dec 6, 2024
e5b0cae
Merge branch 'onramp-api-utils' into fund-form
rustam-cb Dec 6, 2024
a27489b
Update
rustam-cb Dec 9, 2024
51d7041
Merge branch 'main' into onramp-api-utils
rustam-cb Dec 9, 2024
eefc65d
Use Api key from the config
rustam-cb Dec 9, 2024
dbb6b80
Merge branch 'main' into fund-form
rustam-cb Dec 9, 2024
eb92aa5
Merge branch 'onramp-api-utils' into fund-form
rustam-cb Dec 9, 2024
edd3766
Update
rustam-cb Dec 10, 2024
839a045
Update
rustam-cb Dec 10, 2024
c7521d7
Update
rustam-cb Dec 10, 2024
45e7a77
Merge branch 'main' into fund-form
rustam-cb Dec 11, 2024
c14a5a0
Intermediate changes
rustam-cb Dec 16, 2024
9583b11
Merge branch 'main' into fund-form
rustam-cb Dec 16, 2024
6ee7143
Fund card implementation
rustam-cb Dec 17, 2024
cd499ba
Merge branch 'main' into fund-form
rustam-cb Dec 17, 2024
535b2f3
Fund form
rustam-cb Dec 18, 2024
6b9c6ae
Organize code
rustam-cb Dec 18, 2024
8669f40
Merge branch 'main' into fund-form
rustam-cb Dec 18, 2024
4dc16e1
Update
rustam-cb Dec 18, 2024
8991cdb
Update
rustam-cb Dec 18, 2024
eb9a993
Update
rustam-cb Dec 18, 2024
4ffe559
Update
rustam-cb Dec 18, 2024
1b3f1fc
Update
rustam-cb Dec 18, 2024
423843a
Update
rustam-cb Dec 18, 2024
2fabd85
Update
rustam-cb Dec 18, 2024
9032c5e
Update
rustam-cb Dec 18, 2024
b14bc3c
Format
rustam-cb Dec 18, 2024
c143dcb
Update naming
rustam-cb Dec 18, 2024
302806e
Format
rustam-cb Dec 18, 2024
c99194f
Address comments
rustam-cb Dec 18, 2024
950504e
Address comments
rustam-cb Dec 18, 2024
ba15289
Address comments
rustam-cb Dec 19, 2024
cd67c2e
Address comments
rustam-cb Dec 19, 2024
0d3e6db
Merge branch 'main' into fund-form
rustam-cb Dec 19, 2024
4669aeb
Update
rustam-cb Dec 19, 2024
ee55dc8
Update
rustam-cb Dec 19, 2024
72fcbb2
Fix lint issue
rustam-cb Dec 19, 2024
ef17061
Address comments
rustam-cb Dec 19, 2024
1b06d8d
Address comments
rustam-cb Dec 19, 2024
af2203e
Update
rustam-cb Dec 19, 2024
af24c30
Address comments
rustam-cb Dec 20, 2024
f6f1845
Format
rustam-cb Dec 20, 2024
98c055d
Format
rustam-cb Dec 20, 2024
461e136
Address comments
rustam-cb Jan 6, 2025
8f529c4
Organize import statements
rustam-cb Jan 6, 2025
9204930
Fix typo
rustam-cb Jan 6, 2025
0b1e28d
Add type annotations
rustam-cb Jan 6, 2025
46ee540
Upadte className for FundCardHeader
rustam-cb Jan 6, 2025
d11e78a
Update css class
rustam-cb Jan 6, 2025
5272942
Address comments
rustam-cb Jan 6, 2025
c028137
Remove pressable.error
rustam-cb Jan 6, 2025
492b911
Address comments
rustam-cb Jan 7, 2025
a417a96
Address comments...
rustam-cb Jan 8, 2025
3969c11
Address comments
rustam-cb Jan 8, 2025
849a18a
Address comments
rustam-cb Jan 8, 2025
a1f3b32
Merge branch 'main' into fund-form
rustam-cb Jan 8, 2025
0c95bb0
Address comments
rustam-cb Jan 8, 2025
eb784a5
Address comments
rustam-cb Jan 9, 2025
0309e65
Show connect wallet when there is no address only
rustam-cb Jan 10, 2025
1e0d236
Make children default prop
rustam-cb Jan 10, 2025
3a61814
Create FundCardSubmitButton component
rustam-cb Jan 10, 2025
fdd28fb
Update
rustam-cb Jan 10, 2025
d8f2561
Update
rustam-cb Jan 10, 2025
1200f79
Remove debounce
rustam-cb Jan 11, 2025
2df11f6
Add Lifecycle methods
rustam-cb Jan 11, 2025
5a3552e
Default payment methods
rustam-cb Jan 13, 2025
394c34e
Rename fundProvider to fundCardProvider
rustam-cb Jan 13, 2025
4439b28
Fix tests
rustam-cb Jan 13, 2025
226d5ca
Fix test
rustam-cb Jan 13, 2025
4ceb0ac
Fix tests
rustam-cb Jan 13, 2025
928cfa6
Update testIds
rustam-cb Jan 13, 2025
9d47368
Address comments
rustam-cb Jan 13, 2025
95876dd
Update
rustam-cb Jan 14, 2025
1852142
Update
rustam-cb Jan 14, 2025
e47fac5
Address more comments
rustam-cb Jan 14, 2025
84820ee
Fix tests
rustam-cb Jan 14, 2025
bf65ed2
Merge branch 'main' into fund-form
rustam-cb Jan 14, 2025
eb538ca
Merge branch 'main' into fund-form
rustam-cb Jan 14, 2025
f4269a8
Fix tests
rustam-cb Jan 14, 2025
e152292
Update width of amount type swith
rustam-cb Jan 14, 2025
84a281b
Fix flaky tests
rustam-cb Jan 14, 2025
871cf5d
Add lifecycle hooks to demo
rustam-cb Jan 15, 2025
9a842d1
Address comments
rustam-cb Jan 15, 2025
3383b14
Address comments
rustam-cb Jan 16, 2025
05f2610
Format
rustam-cb Jan 16, 2025
7850ac4
Fix tests
rustam-cb Jan 16, 2025
f785493
Remove paymentMethods props
rustam-cb Jan 16, 2025
59c99d0
FOrmt
rustam-cb Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions playground/nextjs-app-router/components/Demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { useContext, useEffect, useState } from 'react';
import DemoOptions from './DemoOptions';
import BuyDemo from './demo/Buy';
import CheckoutDemo from './demo/Checkout';
import FundDemo from './demo/Fund';
import FundButtonDemo from './demo/FundButton';
import FundCardDemo from './demo/FundCard';
import IdentityDemo from './demo/Identity';
import { IdentityCardDemo } from './demo/IdentityCard';
import NFTCardDemo from './demo/NFTCard';
Expand All @@ -24,8 +25,9 @@ import WalletDefaultDemo from './demo/WalletDefault';
import WalletIslandDemo from './demo/WalletIsland';

const activeComponentMapping: Record<OnchainKitComponent, React.FC> = {
[OnchainKitComponent.FundButton]: FundButtonDemo,
[OnchainKitComponent.FundCard]: FundCardDemo,
[OnchainKitComponent.Buy]: BuyDemo,
[OnchainKitComponent.Fund]: FundDemo,
[OnchainKitComponent.Identity]: IdentityDemo,
[OnchainKitComponent.Transaction]: TransactionDemo,
[OnchainKitComponent.Checkout]: CheckoutDemo,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FundButton } from '@coinbase/onchainkit/fund';

export default function FundDemo() {
export default function FundButtonDemo() {
return (
<div className="mx-auto grid w-1/2 gap-8">
<FundButton />
Expand Down
20 changes: 20 additions & 0 deletions playground/nextjs-app-router/components/demo/FundCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FundCard } from '@coinbase/onchainkit/fund';
export default function FundCardDemo() {
return (
<div className="mx-auto grid w-[500px] gap-8">
<FundCard
assetSymbol="ETH"
country="US"
onError={(error) => {
console.log('FundCard onError', error);
}}
onStatus={(status) => {
console.log('FundCard onStatus', status);
}}
onSuccess={() => {
console.log('FundCard onSuccess');
}}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,13 @@ export function ActiveComponent() {
<SelectValue placeholder="Select component" />
</SelectTrigger>
<SelectContent>
<SelectItem value={OnchainKitComponent.FundButton}>
Fund Button
</SelectItem>
<SelectItem value={OnchainKitComponent.FundCard}>
Fund Card
</SelectItem>
<SelectItem value={OnchainKitComponent.Buy}>Buy</SelectItem>
<SelectItem value={OnchainKitComponent.Fund}>Fund</SelectItem>
<SelectItem value={OnchainKitComponent.Identity}>Identity</SelectItem>
<SelectItem value={OnchainKitComponent.IdentityCard}>
IdentityCard
Expand Down
3 changes: 2 additions & 1 deletion playground/nextjs-app-router/types/onchainkit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum OnchainKitComponent {
FundButton = 'fund-button',
FundCard = 'fund-card',
Buy = 'buy',
Fund = 'fund',
Identity = 'identity',
IdentityCard = 'identity-card',
Checkout = 'checkout',
Expand Down
4 changes: 2 additions & 2 deletions src/buy/components/BuyOnrampItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ describe('BuyOnrampItem', () => {
);

expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.getByText('Apple Pay')).toBeInTheDocument();
expect(screen.getByTestId('ock-applePayOnrampItem')).toBeInTheDocument();
expect(screen.getByText('Fast and secure payments.')).toBeInTheDocument();
expect(screen.getByTestId('appleSvg')).toBeInTheDocument();
expect(screen.getByTestId('ock-applePaySvg')).toBeInTheDocument();
});

it('handles icon rendering based on the icon prop', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/buy/components/BuyOnrampItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useMemo } from 'react';
import { appleSvg } from '../../internal/svg/appleSvg';
import { applePaySvg } from '../../internal/svg/applePaySvg';
import { cardSvg } from '../../internal/svg/cardSvg';
import { coinbaseLogoSvg } from '../../internal/svg/coinbaseLogoSvg';
import { cn, color, pressable, text } from '../../styles/theme';
Expand All @@ -15,7 +15,7 @@ type OnrampItemReact = {
};

const ONRAMP_ICON_MAP: Record<string, React.ReactNode> = {
applePay: appleSvg,
applePay: applePaySvg,
coinbasePay: coinbaseLogoSvg,
creditCard: cardSvg,
};
Expand Down
26 changes: 25 additions & 1 deletion src/core-react/internal/hooks/useIcon.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { applePaySvg } from '../../../internal/svg/applePaySvg';
import { appleSvg } from '../../../internal/svg/appleSvg';
import { coinbasePaySvg } from '../../../internal/svg/coinbasePaySvg';
import { creditCardSvg } from '../../../internal/svg/creditCardSvg';
import { fundWalletSvg } from '../../../internal/svg/fundWallet';
import { swapSettingsSvg } from '../../../internal/svg/swapSettings';
import { toggleSvg } from '../../../internal/svg/toggleSvg';
import { walletSvg } from '../../../internal/svg/walletSvg';
import { useIcon } from './useIcon';

Expand All @@ -17,7 +21,27 @@ describe('useIcon', () => {
expect(result.current).toBe(walletSvg);
});

it('should return coinbasePaySvg when icon is "coinbasePay"', () => {
it('should return toggleSvg when icon is "toggle"', () => {
const { result } = renderHook(() => useIcon({ icon: 'toggle' }));
expect(result.current).toBe(toggleSvg);
});

it('should return appleSvg when icon is "apple"', () => {
const { result } = renderHook(() => useIcon({ icon: 'apple' }));
expect(result.current).toBe(appleSvg);
});

it('should return applePaySvg when icon is "applePay"', () => {
const { result } = renderHook(() => useIcon({ icon: 'applePay' }));
expect(result.current).toBe(applePaySvg);
});

it('should return creditCardSvg when icon is "creditCard"', () => {
const { result } = renderHook(() => useIcon({ icon: 'creditCard' }));
expect(result.current).toBe(creditCardSvg);
});

it('should return CoinbasePaySvg when icon is "coinbasePay"', () => {
const { result } = renderHook(() => useIcon({ icon: 'coinbasePay' }));
expect(result.current).toBe(coinbasePaySvg);
});
Expand Down
15 changes: 15 additions & 0 deletions src/core-react/internal/hooks/useIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { applePaySvg } from '@/internal/svg/applePaySvg';
import { isValidElement, useMemo } from 'react';
import { appleSvg } from '../../../internal/svg/appleSvg';
import { coinbaseLogoSvg } from '../../../internal/svg/coinbaseLogoSvg';
import { coinbasePaySvg } from '../../../internal/svg/coinbasePaySvg';
import { creditCardSvg } from '../../../internal/svg/creditCardSvg';
import { fundWalletSvg } from '../../../internal/svg/fundWallet';
import { swapSettingsSvg } from '../../../internal/svg/swapSettings';
import { toggleSvg } from '../../../internal/svg/toggleSvg';
import { walletSvg } from '../../../internal/svg/walletSvg';

export const useIcon = ({ icon }: { icon?: React.ReactNode }) => {
Expand All @@ -12,12 +17,22 @@ export const useIcon = ({ icon }: { icon?: React.ReactNode }) => {
switch (icon) {
case 'coinbasePay':
return coinbasePaySvg;
case 'coinbaseLogo':
return coinbaseLogoSvg;
case 'fundWallet':
return fundWalletSvg;
case 'swapSettings':
return swapSettingsSvg;
case 'wallet':
return walletSvg;
case 'toggle':
return toggleSvg;
case 'applePay':
return applePaySvg;
case 'apple':
return appleSvg;
case 'creditCard':
return creditCardSvg;
}
if (isValidElement(icon)) {
return icon;
Expand Down
162 changes: 145 additions & 17 deletions src/fund/components/FundButton.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import '@testing-library/jest-dom';
import { pressable } from '@/styles/theme';
import { openPopup } from '@/ui-react/internal/utils/openPopup';
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { type Mock, afterEach, describe, expect, it, vi } from 'vitest';
import {
type Mock,
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import { useAccount } from 'wagmi';
import { useGetFundingUrl } from '../hooks/useGetFundingUrl';
import { getFundingPopupSize } from '../utils/getFundingPopupSize';
import { FundButton } from './FundButton';
Expand All @@ -22,7 +32,41 @@ vi.mock('../../core-react/internal/hooks/useTheme', () => ({
useTheme: vi.fn(),
}));

describe('WalletDropdownFundLink', () => {
vi.mock('../../wallet/components/ConnectWallet', () => ({
ConnectWallet: ({ className }: { className?: string }) => (
<div data-testid="ockConnectWallet_Container" className={className}>
Connect Wallet
</div>
),
}));

vi.mock('wagmi', () => ({
useAccount: vi.fn(),
useConnect: vi.fn(),
}));

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;

describe('FundButton', () => {
beforeEach(() => {
(useAccount as Mock).mockReturnValue({
address: '0x123',
});
});

afterEach(() => {
vi.clearAllMocks();
});
Expand All @@ -34,7 +78,6 @@ describe('WalletDropdownFundLink', () => {

render(<FundButton fundingUrl={fundingUrl} />);

expect(useGetFundingUrl).not.toHaveBeenCalled();
const buttonElement = screen.getByRole('button');
expect(screen.getByText('Fund')).toBeInTheDocument();

Expand Down Expand Up @@ -69,29 +112,114 @@ describe('WalletDropdownFundLink', () => {
});
});

it('renders a disabled fund button when no funding URL is provided and the default cannot be fetched', () => {
(useGetFundingUrl as Mock).mockReturnValue(undefined);
it('renders the fund button as a link when the openIn prop is set to tab', () => {
const fundingUrl = 'https://props.funding.url';
const { height, width } = { height: 200, width: 100 };
(getFundingPopupSize as Mock).mockReturnValue({ height, width });

render(<FundButton />);
render(<FundButton fundingUrl={fundingUrl} openIn="tab" />);

expect(useGetFundingUrl).toHaveBeenCalled();
const buttonElement = screen.getByRole('button');
expect(buttonElement).toHaveClass('pointer-events-none');
const linkElement = screen.getByRole('link');
expect(screen.getByText('Fund')).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', fundingUrl);
});

it('displays a spinner when in loading state', () => {
render(<FundButton state="loading" fundingUrl="https://funding.url" />);
expect(screen.getByTestId('ockSpinner')).toBeInTheDocument();
});

it('displays success text when in success state', () => {
render(<FundButton state="success" />);
expect(screen.getByTestId('ockFundButtonTextContent')).toHaveTextContent(
'Success',
);
});

it('displays error text when in error state', () => {
render(<FundButton state="error" />);
expect(screen.getByTestId('ockFundButtonTextContent')).toHaveTextContent(
'Something went wrong',
);
});

it('adds disabled class when the button is disabled', () => {
render(<FundButton disabled={true} />);
expect(screen.getByRole('button')).toHaveClass(pressable.disabled);
});

it('calls onPopupClose when the popup window is closed', () => {
vi.useFakeTimers();
const fundingUrl = 'https://props.funding.url';
const onPopupClose = vi.fn();
const { height, width } = { height: 200, width: 100 };
const mockPopupWindow = {
closed: false,
close: vi.fn(),
};
(getFundingPopupSize as Mock).mockReturnValue({ height, width });
(openPopup as Mock).mockReturnValue(mockPopupWindow);

render(<FundButton fundingUrl={fundingUrl} onPopupClose={onPopupClose} />);

const buttonElement = screen.getByRole('button');
fireEvent.click(buttonElement);
expect(openPopup as Mock).not.toHaveBeenCalled();

// Simulate closing the popup
mockPopupWindow.closed = true;
vi.runOnlyPendingTimers();

expect(onPopupClose).toHaveBeenCalled();
});

it('renders the fund button as a link when the openIn prop is set to tab', () => {
it('calls onClick when the fund button is clicked', () => {
const fundingUrl = 'https://props.funding.url';
const onClick = vi.fn();
const { height, width } = { height: 200, width: 100 };
(getFundingPopupSize as Mock).mockReturnValue({ height, width });

render(<FundButton fundingUrl={fundingUrl} openIn="tab" />);
render(<FundButton fundingUrl={fundingUrl} onClick={onClick} />);

expect(useGetFundingUrl).not.toHaveBeenCalled();
const linkElement = screen.getByRole('link');
expect(screen.getByText('Fund')).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', fundingUrl);
const buttonElement = screen.getByRole('button');
fireEvent.click(buttonElement);

expect(onClick).toHaveBeenCalled();
expect(getFundingPopupSize as Mock).toHaveBeenCalledWith('md', fundingUrl);
expect(openPopup as Mock).toHaveBeenCalledWith({
url: fundingUrl,
height,
width,
target: undefined,
});
});

it('icon is not shown when hideIcon is passed', () => {
render(<FundButton hideIcon={true} />);
expect(screen.queryByTestId('ockFundButtonIcon')).not.toBeInTheDocument();
});

it('shows ConnectWallet when no wallet is connected', () => {
(useAccount as Mock).mockReturnValue({
address: undefined,
});

render(<FundButton className="custom-class" />);

expect(
screen.queryByTestId('ockConnectWallet_Container'),
).toBeInTheDocument();
expect(screen.queryByTestId('ockFundButton')).not.toBeInTheDocument();
expect(screen.getByTestId('ockConnectWallet_Container')).toHaveClass(
'custom-class',
);
});

it('shows Fund button when wallet is connected', () => {
render(<FundButton />);

expect(screen.queryByTestId('ockFundButton')).toBeInTheDocument();
expect(
screen.queryByTestId('ockConnectWallet_Container'),
).not.toBeInTheDocument();
});
});
Loading
Loading