Skip to content

Commit

Permalink
refactor: remove swap findChildren
Browse files Browse the repository at this point in the history
  • Loading branch information
alessey committed Mar 4, 2025
1 parent c2d4c67 commit c4c27e3
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 89 deletions.
94 changes: 57 additions & 37 deletions playground/nextjs-app-router/components/demo/Swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,45 +103,65 @@ function SwapComponent() {
</div>
) : null}

<Swap
className="w-full border sm:w-[500px]"
onStatus={handleOnStatus}
onSuccess={handleOnSuccess}
onError={handleOnError}
config={{
maxSlippage: defaultMaxSlippage || FALLBACK_DEFAULT_MAX_SLIPPAGE,
}}
isSponsored={isSponsored}
>
<SwapSettings>
<SwapSettingsSlippageTitle>Max. slippage</SwapSettingsSlippageTitle>
<SwapSettingsSlippageDescription>
Your swap will revert if the prices change by more than the selected
percentage.
</SwapSettingsSlippageDescription>
<SwapSettingsSlippageInput />
</SwapSettings>
<SwapAmountInput
label="Sell"
<div className="relative flex flex-col gap-6">
<h2>Swap Default</h2>
<Swap
className="w-full border sm:w-[500px]"
swappableTokens={swappableTokens}
token={ethToken}
type="from"
toToken={usdcToken}
fromToken={ethToken}
onStatus={handleOnStatus}
onSuccess={handleOnSuccess}
onError={handleOnError}
config={{
maxSlippage: defaultMaxSlippage || FALLBACK_DEFAULT_MAX_SLIPPAGE,
}}
isSponsored={isSponsored}
headerLeftContent={<div>test</div>}
/>
<SwapToggleButton />
<SwapAmountInput
label="Buy"
swappableTokens={swappableTokens}
token={usdcToken}
type="to"
/>
<SwapButton
disabled={
ENVIRONMENT_VARIABLES[ENVIRONMENT.ENVIRONMENT] === 'production'
}
/>
<SwapMessage />
<SwapToast />
</Swap>

<h2>Swap with Children</h2>
<Swap
className="w-full border sm:w-[500px]"
onStatus={handleOnStatus}
onSuccess={handleOnSuccess}
onError={handleOnError}
config={{
maxSlippage: defaultMaxSlippage || FALLBACK_DEFAULT_MAX_SLIPPAGE,
}}
isSponsored={isSponsored}
headerLeftContent={<div>test</div>}
>
<SwapSettings>
<SwapSettingsSlippageTitle>Max. slippage</SwapSettingsSlippageTitle>
<SwapSettingsSlippageDescription>
Your swap will revert if the prices change by more than the
selected percentage.
</SwapSettingsSlippageDescription>
<SwapSettingsSlippageInput />
</SwapSettings>
<SwapAmountInput
label="Sell"
swappableTokens={swappableTokens}
token={ethToken}
type="from"
/>
<SwapToggleButton />
<SwapAmountInput
label="Buy"
swappableTokens={swappableTokens}
token={usdcToken}
type="to"
/>
<SwapButton
disabled={
ENVIRONMENT_VARIABLES[ENVIRONMENT.ENVIRONMENT] === 'production'
}
/>
<SwapMessage />
<SwapToast />
</Swap>
</div>
</div>
);
}
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.37.4",
"version": "0.37.5",
"type": "module",
"repository": "https://github.com/coinbase/onchainkit.git",
"license": "MIT",
Expand Down
82 changes: 81 additions & 1 deletion src/swap/components/Swap.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,52 @@ import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Swap } from './Swap';

vi.mock('wagmi', async (importOriginal) => {
const actual = await importOriginal<typeof import('wagmi')>();
return {
...actual,
useAccount: vi.fn().mockReturnValue({
address: '0x0000000000000000000000000000000000000000' as `0x${string}`,
chainId: 1,
}),
useConfig: vi.fn(),
useSwitchChain: vi.fn(),
useWriteContract: vi.fn(),
};
});

vi.mock('@/internal/hooks/useBreakpoints', () => ({
useBreakpoints: vi.fn().mockReturnValue('md'),
}));

vi.mock('./SwapProvider', () => ({
SwapProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="mock-SwapProvider">{children}</div>
),
useSwapContext: vi.fn(),
useSwapContext: vi.fn().mockReturnValue({
address: '0x0000000000000000000000000000000000000000' as `0x${string}`,
config: {
maxSlippage: 10,
},
from: {
amount: '100',
},
to: {
amount: '100',
},
lifecycleStatus: {
statusName: 'init',
statusData: {
isMissingRequiredField: true,
maxSlippage: 10,
},
},
handleAmountChange: vi.fn(),
handleSubmit: vi.fn(),
handleToggle: vi.fn(),
updateLifecycleStatus: vi.fn(),
isToastVisible: false,
}),
}));

vi.mock('@/internal/svg/closeSvg', () => ({
Expand Down Expand Up @@ -46,4 +87,43 @@ describe('Swap Component', () => {
const container = screen.getByTestId('ockSwap_Container');
expect(container).toHaveClass('custom-class');
});

it('should render children', () => {
render(
<Swap>
<div>Test Child</div>
</Swap>,
);

expect(screen.getByText('Test Child')).toBeInTheDocument();
});

it('should render children when swappableTokens, toToken, and fromToken are provided', () => {
const toToken = {
address: '0x0000000000000000000000000000000000000000' as `0x${string}`,
chainId: 1,
decimals: 18,
image: 'https://example.com/image.png',
name: 'toToken',
symbol: 'toToken',
};
const fromToken = {
address: '0x0000000000000000000000000000000000000001' as `0x${string}`,
chainId: 1,
decimals: 18,
image: 'https://example.com/image.png',
name: 'fromToken',
symbol: 'fromToken',
};
render(
<Swap
swappableTokens={[toToken, fromToken]}
toToken={toToken}
fromToken={fromToken}
/>,
);

expect(screen.getByText('Buy')).toBeInTheDocument();
expect(screen.getByText('Sell')).toBeInTheDocument();
});
});
77 changes: 45 additions & 32 deletions src/swap/components/Swap.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
'use client';
import { Children, useMemo } from 'react';
import { useIsMounted } from '../../internal/hooks/useIsMounted';
import { useTheme } from '../../internal/hooks/useTheme';
import { findComponent } from '../../internal/utils/findComponent';
import { background, border, cn, color, text } from '../../styles/theme';
import { FALLBACK_DEFAULT_MAX_SLIPPAGE } from '../constants';
import type { SwapReact } from '../types';
Expand All @@ -14,12 +12,43 @@ import { SwapSettings } from './SwapSettings';
import { SwapToast } from './SwapToast';
import { SwapToggleButton } from './SwapToggleButton';

function SwapDefaultContent({
swappableTokens,
toToken,
fromToken,
}: Pick<SwapReact, 'swappableTokens' | 'toToken' | 'fromToken'>) {
return (
<>
<SwapSettings />
<SwapAmountInput
label="Sell"
swappableTokens={swappableTokens}
token={fromToken}
type="from"
/>
<SwapToggleButton />
<SwapAmountInput
label="Buy"
swappableTokens={swappableTokens}
token={toToken}
type="to"
/>
<SwapButton />
<SwapMessage />
<SwapToast />
</>
);
}

export function Swap({
children,
config = {
maxSlippage: FALLBACK_DEFAULT_MAX_SLIPPAGE,
},
className,
swappableTokens,
toToken,
fromToken,
experimental = { useAggregator: false },
isSponsored = false,
onError,
Expand All @@ -30,32 +59,13 @@ export function Swap({
}: SwapReact) {
const componentTheme = useTheme();

const {
inputs,
toggleButton,
swapButton,
swapMessage,
swapSettings,
swapToast,
} = useMemo(() => {
const childrenArray = Children.toArray(children);

return {
inputs: childrenArray.filter(findComponent(SwapAmountInput)),
toggleButton: childrenArray.find(findComponent(SwapToggleButton)),
swapButton: childrenArray.find(findComponent(SwapButton)),
swapMessage: childrenArray.find(findComponent(SwapMessage)),
swapSettings: childrenArray.find(findComponent(SwapSettings)),
swapToast: childrenArray.find(findComponent(SwapToast)),
};
}, [children]);

const isMounted = useIsMounted();

// prevents SSR hydration issue
if (!isMounted) {
return null;
}

return (
<SwapProvider
config={config}
Expand All @@ -71,24 +81,27 @@ export function Swap({
background.default,
border.radius,
color.foreground,
'flex w-[500px] flex-col px-6 pt-6 pb-4',
'relative flex w-full max-w-[500px] flex-col px-6 pt-6 pb-4',
className,
)}
data-testid="ockSwap_Container"
>
<div className="mb-4 flex items-center justify-between">
<div className="absolute flex w-1/2 items-center justify-between">
{headerLeftContent}
<h3 className={cn(text.title3)} data-testid="ockSwap_Title">
<h3
className={cn(text.title3, 'text-center')}
data-testid="ockSwap_Title"
>
{title}
</h3>
{swapSettings}
</div>
{inputs[0]}
<div className="relative h-1">{toggleButton}</div>
{inputs[1]}
{swapButton}
{swapToast}
<div className="flex">{swapMessage}</div>
{children ?? (
<SwapDefaultContent
swappableTokens={swappableTokens}
toToken={toToken}
fromToken={fromToken}
/>
)}
</div>
</SwapProvider>
);
Expand Down
25 changes: 17 additions & 8 deletions src/swap/components/SwapAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,19 @@ export function SwapAmountInput({
className={cn(
background.secondary,
border.radius,
'box-border flex h-[148px] w-full flex-col items-start p-4',
'my-0.5 box-border flex h-[148px] w-full flex-col items-start p-4',
className,
)}
data-testid="ockSwapAmountInput_Container"
>
<div className="flex w-full items-center justify-between">
<span className={cn(text.label2, color.foregroundMuted)}>{label}</span>
<div
className={cn(
text.label2,
color.foregroundMuted,
'flex w-full items-center justify-between',
)}
>
{label}
</div>
<div className="flex w-full items-center justify-between">
<TextInput
Expand Down Expand Up @@ -140,12 +146,11 @@ export function SwapAmountInput({
)}
</div>
<div className="mt-4 flex w-full justify-between">
<div className="flex items-center">
<span className={cn(text.label2, color.foregroundMuted)}>
<div className={cn('mt-4 flex w-full items-center justify-between')}>
<span className={cn(text.label2, color.foregroundMuted, 'grow')}>
{formatUSD(source.amountUSD)}
</span>
</div>
<span className={cn(text.label2, color.foregroundMuted)}>{''}</span>
<div className="flex items-center">
{source.balance && (
<span
Expand All @@ -155,11 +160,15 @@ export function SwapAmountInput({
{type === 'from' && address && (
<button
type="button"
className="flex cursor-pointer items-center justify-center px-2 py-1"
className={cn(
text.label1,
color.primary,
'flex cursor-pointer items-center justify-center px-2 py-1',
)}
data-testid="ockSwapAmountInput_MaxButton"
onClick={handleMaxButtonClick}
>
<span className={cn(text.label1, color.primary)}>Max</span>
Max
</button>
)}
</div>
Expand Down
Loading

0 comments on commit c4c27e3

Please sign in to comment.