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(
+ ,
);
- 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
)}
>