From fa3c2cd3a49e44a0129cd24ca1c489d77736c60c Mon Sep 17 00:00:00 2001 From: Alec Chen <93971719+0xAlec@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:01:42 -0700 Subject: [PATCH 01/23] `SwapSettings` --- .changeset/clean-mirrors-sparkle.md | 5 + site/docs/pages/swap/types.mdx | 10 ++ src/swap/components/Swap.tsx | 29 ++-- src/swap/components/SwapSettings.test.tsx | 47 +++++++ src/swap/components/SwapSettings.tsx | 128 ++++++++++++++++++ .../components/SwapSettingsBottomSheet.tsx | 0 src/swap/index.ts | 1 + 7 files changed, 209 insertions(+), 11 deletions(-) create mode 100644 .changeset/clean-mirrors-sparkle.md create mode 100644 src/swap/components/SwapSettings.test.tsx create mode 100644 src/swap/components/SwapSettings.tsx create mode 100644 src/swap/components/SwapSettingsBottomSheet.tsx diff --git a/.changeset/clean-mirrors-sparkle.md b/.changeset/clean-mirrors-sparkle.md new file mode 100644 index 0000000000..81c21d35b9 --- /dev/null +++ b/.changeset/clean-mirrors-sparkle.md @@ -0,0 +1,5 @@ +--- +"@coinbase/onchainkit": patch +--- + +**feat**: Add SwapSettings sub-component. This allows the user to customize their swaps max slippage percentage. If max slippage is not specified then the component will default to a max slippage of 10%. by @0xAlec @cpcramer #1051 diff --git a/site/docs/pages/swap/types.mdx b/site/docs/pages/swap/types.mdx index 73beff276b..d5cb7333f2 100644 --- a/site/docs/pages/swap/types.mdx +++ b/site/docs/pages/swap/types.mdx @@ -180,6 +180,16 @@ type SwapReact = { }; ``` +## `SwapSettingsReact` + +```ts +type SwapSettingsReact = { + className?: string; // Optional className override for top div element. + icon?: ReactNode; // Optional icon override + text?: string; // Optional text override +}; +``` + ## `SwapToggleButtonReact` ```ts diff --git a/src/swap/components/Swap.tsx b/src/swap/components/Swap.tsx index f0914690e2..9fe8ca0745 100644 --- a/src/swap/components/Swap.tsx +++ b/src/swap/components/Swap.tsx @@ -7,6 +7,7 @@ import { SwapAmountInput } from './SwapAmountInput'; import { SwapButton } from './SwapButton'; import { SwapMessage } from './SwapMessage'; import { SwapProvider } from './SwapProvider'; +import { SwapSettings } from './SwapSettings'; import { SwapToggleButton } from './SwapToggleButton'; export function Swap({ @@ -18,15 +19,17 @@ export function Swap({ onSuccess, title = 'Swap', }: SwapReact) { - const { inputs, toggleButton, swapButton, swapMessage } = 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)), - }; - }, [children]); + const { inputs, toggleButton, swapButton, swapMessage, swapSettings } = + 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)), + }; + }, [children]); const isMounted = useIsMounted(); @@ -50,10 +53,14 @@ export function Swap({ )} data-testid="ockSwap_Container" > -
-

+
+

{title}

+
{swapSettings}
{inputs[0]}
{toggleButton}
diff --git a/src/swap/components/SwapSettings.test.tsx b/src/swap/components/SwapSettings.test.tsx new file mode 100644 index 0000000000..010c245d75 --- /dev/null +++ b/src/swap/components/SwapSettings.test.tsx @@ -0,0 +1,47 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { useBreakpoints } from '../../useBreakpoints'; +import { SwapSettings } from './SwapSettings'; + +vi.mock('../../useBreakpoints', () => ({ + useBreakpoints: vi.fn(), +})); + +const useBreakpointsMock = useBreakpoints as vi.Mock; + +describe('SwapSettings', () => { + it('renders with default title', () => { + useBreakpointsMock.mockReturnValue('md'); + render(); + const settingsContainer = screen.getByTestId('ockSwapSettings_Settings'); + expect(settingsContainer.textContent).toBe(''); + }); + + it('renders with custom title', () => { + useBreakpointsMock.mockReturnValue('md'); + render(); + expect(screen.getByText('Custom')).toBeInTheDocument(); + }); + + it('renders custom icon when provided', () => { + useBreakpointsMock.mockReturnValue('md'); + const CustomIcon = () => ( + + ); + render(} />); + expect(screen.getByTestId('custom-icon')).toBeInTheDocument(); + }); + + it('applies correct classes to the button', () => { + useBreakpointsMock.mockReturnValue('md'); + render(); + const button = screen.getByRole('button', { + name: /toggle swap settings/i, + }); + expect(button).toHaveClass( + 'rounded-full p-2 opacity-50 transition-opacity hover:opacity-100', + ); + }); +}); diff --git a/src/swap/components/SwapSettings.tsx b/src/swap/components/SwapSettings.tsx new file mode 100644 index 0000000000..bdff237903 --- /dev/null +++ b/src/swap/components/SwapSettings.tsx @@ -0,0 +1,128 @@ +import { useCallback, useState } from 'react'; +import { + background, + cn, + color, + pressable, + text as themeText, +} from '../../styles/theme'; +import { useBreakpoints } from '../../useBreakpoints'; +import { useIcon } from '../../wallet/hooks/useIcon'; +import type { SwapSettingsReact } from '../types'; + +export function SwapSettings({ + className, + icon = 'swapSettings', + text = '', +}: SwapSettingsReact) { + const [isOpen, setIsOpen] = useState(false); + const [slippageMode, setSlippageMode] = useState<'Auto' | 'Custom'>('Auto'); + const [customSlippage, setCustomSlippage] = useState('0.5'); + const breakpoint = useBreakpoints(); + + const handleToggle = useCallback(() => { + setIsOpen(!isOpen); + }, [isOpen]); + + const iconSvg = useIcon({ icon }); + + if (!breakpoint) { + return null; + } + + // Placeholder for SwapSettingsBottomSheet + // Implement mobile version here, similar to WalletBottomSheet + if (breakpoint === 'sm') { + return
Mobile version not implemented
; + } + + return ( +
+ {text} +
+ + {isOpen && ( +
+
+

+ Max. slippage +

+

+ Your swap will revert if the prices change by more than the + selected percentage. +

+
+
+ + +
+
+ setCustomSlippage(e.target.value)} + className={cn( + background.default, + 'w-16 rounded-l-md border-t border-b border-l px-2 py-1 text-left', + )} + disabled={slippageMode === 'Auto'} + /> + + % + +
+
+
+
+ )} +
+
+ ); +} diff --git a/src/swap/components/SwapSettingsBottomSheet.tsx b/src/swap/components/SwapSettingsBottomSheet.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/swap/index.ts b/src/swap/index.ts index 25827b1bb0..3586996a83 100644 --- a/src/swap/index.ts +++ b/src/swap/index.ts @@ -3,6 +3,7 @@ export { Swap } from './components/Swap'; export { SwapAmountInput } from './components/SwapAmountInput'; export { SwapButton } from './components/SwapButton'; export { SwapMessage } from './components/SwapMessage'; +export { SwapSettings } from './components/SwapSettings'; export { SwapSettingsSlippageInput } from './components/SwapSettingsSlippageInput'; export { SwapSettingsSlippageTitle } from './components/SwapSettingsSlippageTitle'; export { SwapSettingsSlippageDescription } from './components/SwapSettingsSlippageDescription'; From 65f74620030b5e933a29f6bba55fd2f5c4c27e3f Mon Sep 17 00:00:00 2001 From: Paul Cramer Date: Sun, 25 Aug 2024 22:05:39 -0700 Subject: [PATCH 02/23] Add button type prop --- .changeset/clean-mirrors-sparkle.md | 5 - .changeset/sixty-clocks-shop.md | 5 + site/docs/pages/swap/types.mdx | 10 - src/swap/components/Swap.tsx | 3 +- src/swap/components/SwapSettings.test.tsx | 179 +++++++++++++++--- src/swap/components/SwapSettings.tsx | 141 ++++++-------- .../components/SwapSettingsBottomSheet.tsx | 0 .../components/SwapSettingsSlippageInput.tsx | 29 ++- src/swap/index.ts | 4 +- src/swap/types.ts | 1 + 10 files changed, 243 insertions(+), 134 deletions(-) delete mode 100644 .changeset/clean-mirrors-sparkle.md create mode 100644 .changeset/sixty-clocks-shop.md delete mode 100644 src/swap/components/SwapSettingsBottomSheet.tsx diff --git a/.changeset/clean-mirrors-sparkle.md b/.changeset/clean-mirrors-sparkle.md deleted file mode 100644 index 81c21d35b9..0000000000 --- a/.changeset/clean-mirrors-sparkle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@coinbase/onchainkit": patch ---- - -**feat**: Add SwapSettings sub-component. This allows the user to customize their swaps max slippage percentage. If max slippage is not specified then the component will default to a max slippage of 10%. by @0xAlec @cpcramer #1051 diff --git a/.changeset/sixty-clocks-shop.md b/.changeset/sixty-clocks-shop.md new file mode 100644 index 0000000000..771a986107 --- /dev/null +++ b/.changeset/sixty-clocks-shop.md @@ -0,0 +1,5 @@ +--- +"@coinbase/onchainkit": patch +--- + +- **feat**: Implement slippage support in Swap component with settings UI. This combined feat incorporates slippage functionality into the Swap component, including a dedicated settings section with a title, description, and input field for configuring slippage tolerance. By @cpcramer #1176 #1187 #1189 #1191 #1192 #1195 #1196 #1206 \ No newline at end of file diff --git a/site/docs/pages/swap/types.mdx b/site/docs/pages/swap/types.mdx index d5cb7333f2..73beff276b 100644 --- a/site/docs/pages/swap/types.mdx +++ b/site/docs/pages/swap/types.mdx @@ -180,16 +180,6 @@ type SwapReact = { }; ``` -## `SwapSettingsReact` - -```ts -type SwapSettingsReact = { - className?: string; // Optional className override for top div element. - icon?: ReactNode; // Optional icon override - text?: string; // Optional text override -}; -``` - ## `SwapToggleButtonReact` ```ts diff --git a/src/swap/components/Swap.tsx b/src/swap/components/Swap.tsx index 9fe8ca0745..b9d790efb6 100644 --- a/src/swap/components/Swap.tsx +++ b/src/swap/components/Swap.tsx @@ -37,7 +37,6 @@ export function Swap({ if (!isMounted) { return null; } - return ( {title}

-
{swapSettings}
+ {swapSettings}
{inputs[0]}
{toggleButton}
diff --git a/src/swap/components/SwapSettings.test.tsx b/src/swap/components/SwapSettings.test.tsx index 010c245d75..1406d1d6cf 100644 --- a/src/swap/components/SwapSettings.test.tsx +++ b/src/swap/components/SwapSettings.test.tsx @@ -1,7 +1,40 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +import '@testing-library/jest-dom'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, vi } from 'vitest'; import { useBreakpoints } from '../../useBreakpoints'; +import { useIcon } from '../../wallet/hooks/useIcon'; import { SwapSettings } from './SwapSettings'; +import { SwapSettingsSlippageDescription } from './SwapSettingsSlippageDescription'; +import { SwapSettingsSlippageInput } from './SwapSettingsSlippageInput'; +import { SwapSettingsSlippageTitle } from './SwapSettingsSlippageTitle'; + +vi.mock('../../wallet/hooks/useIcon', () => ({ + useIcon: vi.fn(() => ), +})); + +vi.mock('./SwapSettingsSlippageLayout', () => ({ + SwapSettingsSlippageLayout: vi.fn(({ children }) => ( +
{children}
+ )), +})); + +vi.mock('./SwapSettingsSlippageLayoutBottomSheet', () => ({ + SwapSettingsSlippageLayoutBottomSheet: vi.fn(({ children }) => ( +
{children}
+ )), +})); + +vi.mock('./SwapSettingsSlippageTitle', () => ({ + SwapSettingsSlippageTitle: vi.fn(() =>
Title
), +})); + +vi.mock('./SwapSettingsSlippageDescription', () => ({ + SwapSettingsSlippageDescription: vi.fn(() =>
Description
), +})); + +vi.mock('./SwapSettingsSlippageInput', () => ({ + SwapSettingsSlippageInput: vi.fn(() =>
Input
), +})); vi.mock('../../useBreakpoints', () => ({ useBreakpoints: vi.fn(), @@ -9,39 +42,139 @@ vi.mock('../../useBreakpoints', () => ({ const useBreakpointsMock = useBreakpoints as vi.Mock; +const renderComponent = (props = {}) => { + return render( + + + + + , + ); +}; + describe('SwapSettings', () => { - it('renders with default title', () => { + beforeEach(() => { + vi.clearAllMocks(); useBreakpointsMock.mockReturnValue('md'); - render(); - const settingsContainer = screen.getByTestId('ockSwapSettings_Settings'); - expect(settingsContainer.textContent).toBe(''); }); - it('renders with custom title', () => { - useBreakpointsMock.mockReturnValue('md'); - render(); - expect(screen.getByText('Custom')).toBeInTheDocument(); + it('should render with default props and handle dropdown toggle for desktop', async () => { + renderComponent(); + expect(screen.getByTestId('ockSwapSettings_Settings')).toBeInTheDocument(); + expect(screen.getByTestId('mock-icon')).toBeInTheDocument(); + const button = screen.getByRole('button', { + name: /toggle swap settings/i, + }); + fireEvent.click(button); + await waitFor(() => { + expect(screen.getByTestId('ockSwapSettingsDropdown')).toBeInTheDocument(); + expect(screen.getByTestId('mock-layout')).toBeInTheDocument(); + expect(screen.getByText('Title')).toBeInTheDocument(); + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Input')).toBeInTheDocument(); + }); + fireEvent.click(button); + await waitFor(() => { + expect( + screen.queryByTestId('ockSwapSettingsDropdown'), + ).not.toBeInTheDocument(); + }); }); - it('renders custom icon when provided', () => { - useBreakpointsMock.mockReturnValue('md'); - const CustomIcon = () => ( - + it('should render with custom props and handle outside click', async () => { + render( +
+ +
Outside
+
, ); - render(} />); - expect(screen.getByTestId('custom-icon')).toBeInTheDocument(); + expect(screen.getByText('Custom Text')).toBeInTheDocument(); + expect(screen.getByTestId('ockSwapSettings_Settings')).toHaveClass( + 'custom-class', + ); + expect(useIcon).toHaveBeenCalledWith({ icon: 'custom-icon' }); + const button = screen.getByRole('button', { + name: /toggle swap settings/i, + }); + fireEvent.click(button); + await waitFor(() => { + expect(screen.getByTestId('ockSwapSettingsDropdown')).toBeInTheDocument(); + }); + fireEvent.mouseDown(screen.getByTestId('outside')); + await waitFor(() => { + expect( + screen.queryByTestId('ockSwapSettingsDropdown'), + ).not.toBeInTheDocument(); + }); }); - it('applies correct classes to the button', () => { - useBreakpointsMock.mockReturnValue('md'); - render(); + it('should handle non-valid React elements as children', async () => { + render( + + + Plain text child + + , + ); const button = screen.getByRole('button', { name: /toggle swap settings/i, }); - expect(button).toHaveClass( - 'rounded-full p-2 opacity-50 transition-opacity hover:opacity-100', + fireEvent.click(button); + await waitFor(() => { + expect(screen.getByTestId('ockSwapSettingsDropdown')).toBeInTheDocument(); + expect(screen.getByText('Title')).toBeInTheDocument(); + expect(screen.getByText('Plain text child')).toBeInTheDocument(); + expect(screen.getByText('Input')).toBeInTheDocument(); + }); + }); + + it('renders SwapSettingsSlippageLayoutBottomSheet when breakpoint is "sm"', async () => { + useBreakpointsMock.mockReturnValue('sm'); + renderComponent(); + const button = screen.getByRole('button', { + name: /toggle swap settings/i, + }); + const initialBottomSheet = screen.getByTestId( + 'ockSwapSettingsSlippageLayoutBottomSheet_container', + ); + expect(initialBottomSheet).toHaveClass('ease-in-out'); + expect(initialBottomSheet).toHaveClass('group-[]:bottom-0'); + fireEvent.click(button); + await waitFor(() => { + const parentDiv = screen + .getByTestId('ockSwapSettings_Settings') + .querySelector('div'); + expect(parentDiv).toHaveClass('group'); + const openBottomSheet = screen.getByTestId( + 'ockSwapSettingsSlippageLayoutBottomSheet_container', + ); + expect(openBottomSheet).toHaveClass('group-[]:bottom-0'); + }); + fireEvent.click(button); + await waitFor(() => { + const parentDiv = screen + .getByTestId('ockSwapSettings_Settings') + .querySelector('div'); + expect(parentDiv).not.toHaveClass('group'); + const closedBottomSheet = screen.getByTestId( + 'ockSwapSettingsSlippageLayoutBottomSheet_container', + ); + expect(closedBottomSheet).toHaveClass('ease-in-out'); + }); + }); + + it('removes event listener on unmount', () => { + const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener'); + const { unmount } = renderComponent(); + unmount(); + expect(removeEventListenerSpy).toHaveBeenCalledWith( + 'mousedown', + expect.any(Function), ); + removeEventListenerSpy.mockRestore(); }); }); diff --git a/src/swap/components/SwapSettings.tsx b/src/swap/components/SwapSettings.tsx index bdff237903..657820303f 100644 --- a/src/swap/components/SwapSettings.tsx +++ b/src/swap/components/SwapSettings.tsx @@ -1,126 +1,95 @@ -import { useCallback, useState } from 'react'; -import { - background, - cn, - color, - pressable, - text as themeText, -} from '../../styles/theme'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { background, border, cn, pressable, text } from '../../styles/theme'; import { useBreakpoints } from '../../useBreakpoints'; import { useIcon } from '../../wallet/hooks/useIcon'; import type { SwapSettingsReact } from '../types'; +import { SwapSettingsSlippageLayout } from './SwapSettingsSlippageLayout'; +import { SwapSettingsSlippageLayoutBottomSheet } from './SwapSettingsSlippageLayoutBottomSheet'; export function SwapSettings({ + children, className, icon = 'swapSettings', - text = '', + text: buttonText = '', }: SwapSettingsReact) { - const [isOpen, setIsOpen] = useState(false); - const [slippageMode, setSlippageMode] = useState<'Auto' | 'Custom'>('Auto'); - const [customSlippage, setCustomSlippage] = useState('0.5'); const breakpoint = useBreakpoints(); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); const handleToggle = useCallback(() => { setIsOpen(!isOpen); }, [isOpen]); - const iconSvg = useIcon({ icon }); + const handleClickOutsideComponent = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; - if (!breakpoint) { - return null; - } + useEffect(() => { + document.addEventListener('mousedown', handleClickOutsideComponent); + return () => { + document.removeEventListener('mousedown', handleClickOutsideComponent); + }; + }, [handleClickOutsideComponent]); - // Placeholder for SwapSettingsBottomSheet - // Implement mobile version here, similar to WalletBottomSheet - if (breakpoint === 'sm') { - return
Mobile version not implemented
; - } + const iconSvg = useIcon({ icon }); return (
- {text} -
+ {buttonText && {buttonText}} +
- {isOpen && ( + {breakpoint === 'sm' ? (
-
-

- Max. slippage -

-

- Your swap will revert if the prices change by more than the - selected percentage. -

-
-
- - -
-
- setCustomSlippage(e.target.value)} - className={cn( - background.default, - 'w-16 rounded-l-md border-t border-b border-l px-2 py-1 text-left', - )} - disabled={slippageMode === 'Auto'} - /> - - % - -
-
-
+ + {children} +
+ ) : ( + isOpen && ( +
+ + {children} + +
+ ) )}
diff --git a/src/swap/components/SwapSettingsBottomSheet.tsx b/src/swap/components/SwapSettingsBottomSheet.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/swap/components/SwapSettingsSlippageInput.tsx b/src/swap/components/SwapSettingsSlippageInput.tsx index 3c1b12fdc7..ec17e59efe 100644 --- a/src/swap/components/SwapSettingsSlippageInput.tsx +++ b/src/swap/components/SwapSettingsSlippageInput.tsx @@ -12,10 +12,23 @@ export function SwapSettingsSlippageInput({ className, defaultSlippage = 3, }: SwapSettingsSlippageInputReact) { - const { setLifeCycleStatus } = useSwapContext(); - const [slippage, setSlippage] = useState(defaultSlippage); + const { setLifeCycleStatus, lifeCycleStatus } = useSwapContext(); + const getMaxSlippage = () => { + if ( + !!lifeCycleStatus.statusData && + 'maxSlippage' in lifeCycleStatus.statusData + ) { + return lifeCycleStatus.statusData.maxSlippage; + } + return defaultSlippage; + }; + + // TODO: Add comment about how we do these checks so the dropdown opens to the correct slippage value and button that it left off on + const [slippage, setSlippage] = useState(getMaxSlippage()); const [slippageSetting, setSlippageSetting] = useState( - SLIPPAGE_SETTINGS.AUTO, + getMaxSlippage() === defaultSlippage + ? SLIPPAGE_SETTINGS.AUTO + : SLIPPAGE_SETTINGS.CUSTOM, ); const updateSlippage = useCallback( @@ -33,9 +46,13 @@ export function SwapSettingsSlippageInput({ // Parses the input and updates slippage if valid const handleSlippageChange = useCallback( (newSlippage: string) => { - const newSlippageNumber = Number.parseFloat(newSlippage); - if (!Number.isNaN(newSlippageNumber)) { - updateSlippage(newSlippageNumber); + if (newSlippage === '') { + setSlippage(0); + } else { + const newSlippageNumber = Number.parseFloat(newSlippage); + if (!Number.isNaN(newSlippageNumber)) { + updateSlippage(newSlippageNumber); + } } }, [updateSlippage], diff --git a/src/swap/index.ts b/src/swap/index.ts index 3586996a83..33682b1513 100644 --- a/src/swap/index.ts +++ b/src/swap/index.ts @@ -4,9 +4,9 @@ export { SwapAmountInput } from './components/SwapAmountInput'; export { SwapButton } from './components/SwapButton'; export { SwapMessage } from './components/SwapMessage'; export { SwapSettings } from './components/SwapSettings'; +export { SwapSettingsSlippageDescription } from './components/SwapSettingsSlippageDescription'; export { SwapSettingsSlippageInput } from './components/SwapSettingsSlippageInput'; export { SwapSettingsSlippageTitle } from './components/SwapSettingsSlippageTitle'; -export { SwapSettingsSlippageDescription } from './components/SwapSettingsSlippageDescription'; export { SwapToggleButton } from './components/SwapToggleButton'; export type { BuildSwapTransaction, @@ -21,9 +21,9 @@ export type { SwapQuote, SwapReact, SwapSettingsReact, + SwapSettingsSlippageDescriptionReact, SwapSettingsSlippageInputReact, SwapSettingsSlippageTitleReact, - SwapSettingsSlippageDescriptionReact, SwapToggleButtonReact, Transaction, } from './types'; diff --git a/src/swap/types.ts b/src/swap/types.ts index f60fd4569f..db28e58384 100644 --- a/src/swap/types.ts +++ b/src/swap/types.ts @@ -237,6 +237,7 @@ export type SwapReact = { * Note: exported as public Type */ export type SwapSettingsReact = { + children: ReactNode; className?: string; // Optional className override for top div element. icon?: ReactNode; // Optional icon override text?: string; // Optional text override From 39bebadba6c4087fd07fcbee231626841f887fdb Mon Sep 17 00:00:00 2001 From: Paul Cramer Date: Wed, 4 Sep 2024 10:22:59 -0700 Subject: [PATCH 03/23] changeset --- .changeset/clever-lies-care.md | 5 +++++ .changeset/sixty-clocks-shop.md | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 .changeset/clever-lies-care.md delete mode 100644 .changeset/sixty-clocks-shop.md diff --git a/.changeset/clever-lies-care.md b/.changeset/clever-lies-care.md new file mode 100644 index 0000000000..7f2edf3ce6 --- /dev/null +++ b/.changeset/clever-lies-care.md @@ -0,0 +1,5 @@ +--- +'@coinbase/onchainkit': patch +--- + +-**feat**: added custom slippage support settings sub-component in the `Swap` component. By @cpcramer #1210 diff --git a/.changeset/sixty-clocks-shop.md b/.changeset/sixty-clocks-shop.md deleted file mode 100644 index 771a986107..0000000000 --- a/.changeset/sixty-clocks-shop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@coinbase/onchainkit": patch ---- - -- **feat**: Implement slippage support in Swap component with settings UI. This combined feat incorporates slippage functionality into the Swap component, including a dedicated settings section with a title, description, and input field for configuring slippage tolerance. By @cpcramer #1176 #1187 #1189 #1191 #1192 #1195 #1196 #1206 \ No newline at end of file From 818f332bf22ec5838976f1c82f7d5d59eb3f063f Mon Sep 17 00:00:00 2001 From: Paul Cramer Date: Wed, 4 Sep 2024 10:41:17 -0700 Subject: [PATCH 04/23] tests --- src/swap/components/SwapSettingsSlippageInput.test.tsx | 5 +++++ src/swap/components/SwapSettingsSlippageInput.tsx | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/swap/components/SwapSettingsSlippageInput.test.tsx b/src/swap/components/SwapSettingsSlippageInput.test.tsx index 695c3ffb46..341be15646 100644 --- a/src/swap/components/SwapSettingsSlippageInput.test.tsx +++ b/src/swap/components/SwapSettingsSlippageInput.test.tsx @@ -3,10 +3,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SwapSettingsSlippageInput } from './SwapSettingsSlippageInput'; const mockSetLifeCycleStatus = vi.fn(); +let mockLifeCycleStatus = { statusData: { maxSlippage: 3 } }; vi.mock('./SwapProvider', () => ({ useSwapContext: () => ({ setLifeCycleStatus: mockSetLifeCycleStatus, + lifeCycleStatus: mockLifeCycleStatus, }), })); @@ -17,6 +19,7 @@ vi.mock('../styles/theme', () => ({ describe('SwapSettingsSlippageInput', () => { beforeEach(() => { mockSetLifeCycleStatus.mockClear(); + mockLifeCycleStatus = { statusData: { maxSlippage: 3 } }; }); it('renders with default props', () => { @@ -35,6 +38,7 @@ describe('SwapSettingsSlippageInput', () => { }); it('uses provided defaultSlippage', () => { + mockLifeCycleStatus = { statusData: {} }; render(); expect(screen.getByRole('textbox')).toHaveValue('1.5'); }); @@ -56,6 +60,7 @@ describe('SwapSettingsSlippageInput', () => { }); it('switches between Auto and Custom modes', () => { + mockLifeCycleStatus = { statusData: {} }; render(); const input = screen.getByRole('textbox'); expect(input).toBeDisabled(); diff --git a/src/swap/components/SwapSettingsSlippageInput.tsx b/src/swap/components/SwapSettingsSlippageInput.tsx index ec17e59efe..7db673d9a3 100644 --- a/src/swap/components/SwapSettingsSlippageInput.tsx +++ b/src/swap/components/SwapSettingsSlippageInput.tsx @@ -23,7 +23,8 @@ export function SwapSettingsSlippageInput({ return defaultSlippage; }; - // TODO: Add comment about how we do these checks so the dropdown opens to the correct slippage value and button that it left off on + // Set initial slippage values to match previous selection or default, + // ensuring consistency when dropdown is reopened const [slippage, setSlippage] = useState(getMaxSlippage()); const [slippageSetting, setSlippageSetting] = useState( getMaxSlippage() === defaultSlippage From 7735d76bfd4d87befd69473b19fde9cfb3a07291 Mon Sep 17 00:00:00 2001 From: Paul Cramer Date: Wed, 4 Sep 2024 10:47:34 -0700 Subject: [PATCH 05/23] cleaning --- src/swap/components/Swap.tsx | 1 - .../components/SwapSettingsSlippageInput.tsx | 32 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/swap/components/Swap.tsx b/src/swap/components/Swap.tsx index b9d790efb6..bd15a75bc0 100644 --- a/src/swap/components/Swap.tsx +++ b/src/swap/components/Swap.tsx @@ -59,7 +59,6 @@ export function Swap({ > {title} - {swapSettings}
{inputs[0]}
{toggleButton}
diff --git a/src/swap/components/SwapSettingsSlippageInput.tsx b/src/swap/components/SwapSettingsSlippageInput.tsx index 7db673d9a3..8f1e7729ac 100644 --- a/src/swap/components/SwapSettingsSlippageInput.tsx +++ b/src/swap/components/SwapSettingsSlippageInput.tsx @@ -29,7 +29,7 @@ export function SwapSettingsSlippageInput({ const [slippageSetting, setSlippageSetting] = useState( getMaxSlippage() === defaultSlippage ? SLIPPAGE_SETTINGS.AUTO - : SLIPPAGE_SETTINGS.CUSTOM, + : SLIPPAGE_SETTINGS.CUSTOM ); const updateSlippage = useCallback( @@ -40,23 +40,25 @@ export function SwapSettingsSlippageInput({ statusData: { maxSlippage: newSlippage }, }); }, - [setLifeCycleStatus], + [setLifeCycleStatus] ); // Handles user input for custom slippage // Parses the input and updates slippage if valid const handleSlippageChange = useCallback( (newSlippage: string) => { + // Empty '' when the input field is cleared. if (newSlippage === '') { setSlippage(0); - } else { - const newSlippageNumber = Number.parseFloat(newSlippage); - if (!Number.isNaN(newSlippageNumber)) { - updateSlippage(newSlippageNumber); - } + return; + } + + const newSlippageNumber = Number.parseFloat(newSlippage); + if (!Number.isNaN(newSlippageNumber)) { + updateSlippage(newSlippageNumber); } }, - [updateSlippage], + [updateSlippage] ); // Toggles between auto and custom slippage settings @@ -68,7 +70,7 @@ export function SwapSettingsSlippageInput({ updateSlippage(defaultSlippage); } }, - [updateSlippage, defaultSlippage], + [updateSlippage, defaultSlippage] ); return ( @@ -77,14 +79,14 @@ export function SwapSettingsSlippageInput({ background.default, border.defaultActive, 'flex items-center gap-2', - className, + className )} >
Slippage Setting @@ -99,7 +101,7 @@ export function SwapSettingsSlippageInput({ // Highlight the button if it is selected slippageSetting === setting ? cn(background.inverse, color.primary, pressable.shadow) - : color.foregroundMuted, + : color.foregroundMuted )} onClick={() => handleSlippageSettingChange(setting)} > @@ -112,7 +114,7 @@ export function SwapSettingsSlippageInput({ background.default, border.defaultActive, 'flex h-9 w-24 items-center justify-between rounded-lg border px-2 py-1', - slippageSetting === SLIPPAGE_SETTINGS.AUTO && 'opacity-50', + slippageSetting === SLIPPAGE_SETTINGS.AUTO && 'opacity-50' )} >