Skip to content

chore: Buy component touch ups #1775

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

Merged
merged 8 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/buy/components/Buy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function Buy({
maxSlippage: FALLBACK_DEFAULT_MAX_SLIPPAGE,
},
className,
disabled = false,
experimental = { useAggregator: false },
isSponsored = false,
onError,
Expand All @@ -51,6 +52,7 @@ export function Buy({
return (
<BuyProvider
config={config}
disabled={disabled}
experimental={experimental}
isSponsored={isSponsored}
onError={onError}
Expand Down
2 changes: 1 addition & 1 deletion src/buy/components/BuyAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function BuyAmountInput() {
return (
<div
className={cn(
'flex h-full items-center border px-2 pl-4',
'flex h-12 items-center border px-2 pl-4',
background.default,
border.radius,
)}
Expand Down
3 changes: 2 additions & 1 deletion src/buy/components/BuyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useBuyContext } from './BuyProvider';
export function BuyButton() {
const {
address,
disabled,
setIsDropdownOpen,
isDropdownOpen,
from,
Expand All @@ -35,7 +36,7 @@ export function BuyButton() {
statusName === 'transactionApproved';

const isMissingRequiredField = !to?.amount || !to?.token;
const isDisabled = isLoading;
const isDisabled = isLoading || disabled;

const handleSubmit = useCallback(() => {
if (isMissingRequiredField) {
Expand Down
19 changes: 19 additions & 0 deletions src/buy/components/BuyOnrampItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,23 @@ describe('BuyOnrampItem', () => {
expect(button).toHaveClass('flex items-center gap-2 rounded-lg p-2');
expect(button).toHaveAttribute('type', 'button');
});

it('should show overlay on mouse enter', () => {
const { getByTestId, getByText, queryByText } = render(
<BuyOnrampItem
name="Apple Pay"
description="Fast and secure payments."
onClick={mockOnClick}
icon="applePay"
/>,
);

fireEvent.mouseEnter(getByTestId('ockBuyApplePayInfo'));

expect(getByText('Only on mobile and Safari')).toBeInTheDocument();

fireEvent.mouseLeave(getByTestId('ockBuyApplePayInfo'));

expect(queryByText('Only on mobile and Safari')).toBeNull();
});
});
8 changes: 7 additions & 1 deletion src/buy/components/BuyOnrampItem.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Tooltip } from '@/ui-react/internal/components/Tooltip';
import { useCallback } from 'react';
import { appleSvg } from '../../internal/svg/appleSvg';
import { cardSvg } from '../../internal/svg/cardSvg';
Expand Down Expand Up @@ -47,7 +48,12 @@ export function BuyOnrampItem({
{ONRAMP_ICON_MAP[icon]}
</div>
<div className="flex flex-col items-start">
<div>{name}</div>
<div className="relative flex items-center gap-1">
<div>{name}</div>
{name === 'Apple Pay' && (
<Tooltip content="Only on mobile and Safari" />
)}
</div>
<div className={cn('text-xs', color.foregroundMuted)}>
{description}
</div>
Expand Down
22 changes: 12 additions & 10 deletions src/buy/components/BuyProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -813,24 +813,26 @@ describe('BuyProvider', () => {
});
});

it('should setLifecycleStatus to error when projectId is not provided', async () => {
it('logs an error when projectId is not provided', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
(useOnchainKit as Mock).mockReturnValue({
projectId: undefined,
config: {
paymaster: undefined,
},
});

const { result } = renderHook(() => useBuyContext(), { wrapper });
expect(result.current.lifecycleStatus).toEqual({
statusName: 'error',
statusData: expect.objectContaining({
code: 'TmBPc04',
error:
'Project ID is required, please set the projectId in the OnchainKitProvider',
message: '',
}),
renderHook(() => useBuyContext(), { wrapper });

await waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Project ID is required for this component, please set the projectId in the OnchainKitProvider',
);
});

consoleErrorSpy.mockRestore();
});

it('should handle submit correctly', async () => {
Expand Down
16 changes: 6 additions & 10 deletions src/buy/components/BuyProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function BuyProvider({
config = {
maxSlippage: FALLBACK_DEFAULT_MAX_SLIPPAGE,
},
disabled,
experimental,
isSponsored,
onError,
Expand Down Expand Up @@ -132,17 +133,11 @@ export function BuyProvider({

useEffect(() => {
if (!projectId) {
updateLifecycleStatus({
statusName: 'error',
statusData: {
code: 'TmBPc04',
error:
'Project ID is required, please set the projectId in the OnchainKitProvider',
message: '',
},
});
console.error(
'Project ID is required for this component, please set the projectId in the OnchainKitProvider',
);
}
}, [projectId, updateLifecycleStatus]);
}, [projectId]);

useEffect(() => {
// Reset inputs after status reset. `resetInputs` is dependent
Expand Down Expand Up @@ -424,6 +419,7 @@ export function BuyProvider({
const value = useValue({
address,
config,
disabled,
from,
fromETH,
fromUSDC,
Expand Down
3 changes: 3 additions & 0 deletions src/buy/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Address, TransactionReceipt } from 'viem';
export type BuyReact = {
className?: string; // Optional className override for top div element.
config?: SwapConfig;
disabled?: boolean; // Disables Buy button
experimental?: {
useAggregator: boolean; // Whether to use a DEX aggregator. (default: true)
};
Expand All @@ -26,6 +27,7 @@ export type BuyReact = {
export type BuyContextType = {
address?: Address; // Used to check if user is connected in SwapButton
config: SwapConfig;
disabled?: boolean;
fromETH: SwapUnit;
fromUSDC: SwapUnit;
lifecycleStatus: LifecycleStatus;
Expand All @@ -48,6 +50,7 @@ export type BuyProviderReact = {
config?: {
maxSlippage: number; // Maximum acceptable slippage for a swap. (default: 10) This is as a percent, not basis points
};
disabled?: boolean;
experimental: {
useAggregator: boolean; // Whether to use a DEX aggregator. (default: true)
};
Expand Down
2 changes: 2 additions & 0 deletions src/internal/svg/appleSvg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export const appleSvg = (
preserveAspectRatio="xMidYMid meet"
id="Artwork"
data-testid="appleSvg"
width="24"
height="24"
>
<title>Apple Pay Onramp</title>
<path
Expand Down
35 changes: 35 additions & 0 deletions src/ui/react/internal/components/Tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Tooltip } from './Tooltip';

describe('Tooltip', () => {
it('renders the children', () => {
render(<Tooltip content="Test Content" />);
expect(screen.getByTestId('ockBuyApplePayInfo')).toBeInTheDocument();
});

it('shows the content on mouse enter', () => {
render(<Tooltip content="Test Content" />);
const triggerElement = screen.getByTestId('ockBuyApplePayInfo');

fireEvent.mouseEnter(triggerElement);
expect(screen.getByText('Test Content')).toBeInTheDocument();
});

it('hides the content on mouse leave', () => {
render(<Tooltip content="Test Content" />);
const triggerElement = screen.getByTestId('ockBuyApplePayInfo');

fireEvent.mouseEnter(triggerElement);
fireEvent.mouseLeave(triggerElement);

expect(screen.queryByText('Test Content')).not.toBeInTheDocument();
});

it('renders custom children if provided', () => {
const CustomChild = <div>Custom Child</div>;
render(<Tooltip content="Test Content">{CustomChild}</Tooltip>);

expect(screen.getByText('Custom Child')).toBeInTheDocument();
});
});
47 changes: 47 additions & 0 deletions src/ui/react/internal/components/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { infoSvg } from '@/internal/svg/infoSvg';
import { background, border, cn, text } from '@/styles/theme';
import { useCallback, useState } from 'react';

type TooltipReact = {
children?: React.ReactNode;
content: React.ReactNode;
};

export function Tooltip({ children = infoSvg, content }: TooltipReact) {
const [isOverlayVisible, setIsOverlayVisible] = useState(false);

const showOverlay = useCallback(() => {
setIsOverlayVisible(true);
}, []);

const hideOverlay = useCallback(() => {
setIsOverlayVisible(false);
}, []);

return (
<>
<div
data-testid="ockBuyApplePayInfo"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ockTooltip

className={cn('h-2.5 w-2.5 cursor-pointer object-cover')}
onMouseEnter={showOverlay}
onMouseLeave={hideOverlay}
>
{children}
</div>
{isOverlayVisible && (
<div
className={cn(
'absolute top-0 right-0 flex translate-x-[100%] translate-y-[-100%]',
'whitespace-nowrap p-2',
border.radius,
background.inverse,
text.legal,
border.lineDefault,
)}
>
{content}
</div>
)}
</>
);
}
Loading