-
Notifications
You must be signed in to change notification settings - Fork 192
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
feat: WalletDropdownFundLink
#1021
Merged
Merged
Changes from all commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
c62ca07
`WalletDropdownFundLink` component
0xAlec 1f8aa0e
types
0xAlec ae3e8dd
window mode
0xAlec 89112aa
docs
0xAlec ec45503
lint
0xAlec 9212b30
fix imports
0xAlec 5bb09e7
organize export
0xAlec 6468a62
changeset
0xAlec 7a98d5a
fix build
0xAlec 7d8fd70
alphabetical export
0xAlec c80f4b4
format
0xAlec bff46b6
cleanup
0xAlec 7495d92
update prop names
0xAlec a8dc224
add test for `windowSize`
0xAlec 48a926f
don't use prop destructuring
0xAlec dd771bf
lint and format
0xAlec 3cbe66f
organize and add comments
0xAlec 3b5a079
Update witty-crabs-change.md
0xAlec 83b0cc3
remove component docs
0xAlec 06ada06
add `onchainkit=version` to params
0xAlec 7e0f393
`Fund Wallet` default copy
0xAlec d726dff
format
0xAlec ad0127b
add newline
0xAlec 4252671
fix tests
0xAlec 96597ca
sentence case
0xAlec 48a25bc
fix
0xAlec c74b3a1
add `fundingUrl`
0xAlec 99816a0
format
0xAlec 5325599
fix test
0xAlec 7387e2d
final
0xAlec 2efd1d7
add windowsizes
0xAlec 71ac9f5
use viewports
0xAlec 320004a
`getWindowDimensions`
0xAlec c2d8dfe
remove comment
0xAlec a0f5829
`overrideClassName`
0xAlec c1d3365
update funding url
0xAlec a9c6a57
`windowSize` -> `popupSize`
0xAlec f162d4b
`openIn=window` -> `popup`
0xAlec 0ba3f3c
fix tests
0xAlec 07d4bf3
fix dimensions tests
0xAlec 5cda967
change query params
0xAlec 0b536ac
tweak size of `sm`
0xAlec 6874ac2
add `popupFeatures` as an override
0xAlec e262968
fixes
0xAlec 3f15e98
add comments
0xAlec 09e82f7
wrap using `useMemo` and `useCallback`
0xAlec 56a243a
`useIcon` hook
0xAlec a4ba654
`useEffect` for `fundingUrl`
0xAlec 91ee34e
Update witty-crabs-change.md
0xAlec df97e48
alphabetical order
0xAlec f040a52
alphabetical
0xAlec File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@coinbase/onchainkit": patch | ||
--- | ||
|
||
**feat**: `WalletDropdownFundLink` - add a wallet dropdown link for the keys.coinbase.com funding flow by @0xAlec #1021 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { fill } from '../../styles/theme'; | ||
|
||
export const fundWalletSvg = ( | ||
<svg | ||
role="img" | ||
aria-label="fund-wallet-svg" | ||
width="100%" | ||
height="100%" | ||
viewBox="0 0 20 20" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<path | ||
d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM1.63962 4.33685C5.1477 6.36224 9.63276 5.16047 11.6582 1.65239C9.63276 5.16047 10.8345 9.64553 14.3413 11.6702C10.8345 9.64553 6.35021 10.846 4.32482 14.3541C6.35021 10.846 5.1477 6.36224 1.63962 4.33685Z" | ||
fill="#0A0B0D" | ||
0xAlec marked this conversation as resolved.
Show resolved
Hide resolved
|
||
className={fill.defaultReverse} | ||
/> | ||
</svg> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import { expect, it, vi } from 'vitest'; | ||
import { version } from '../../version'; | ||
import type { WindowSizes } from '../types'; | ||
import { WalletDropdownFundLink } from './WalletDropdownFundLink'; | ||
|
||
const FUNDING_URL = `http://keys.coinbase.com/fund?dappName=&dappUrl=http%3A%2F%2Flocalhost%3A3000%2F&version=${version}&source=onchainkit`; | ||
|
||
describe('WalletDropdownFundLink', () => { | ||
0xAlec marked this conversation as resolved.
Show resolved
Hide resolved
|
||
it('renders correctly with default props', () => { | ||
render(<WalletDropdownFundLink />); | ||
|
||
const linkElement = screen.getByRole('link'); | ||
expect(linkElement).toBeInTheDocument(); | ||
expect(linkElement).toHaveAttribute('href', FUNDING_URL); | ||
expect(screen.getByText('Fund wallet')).toBeInTheDocument(); | ||
0xAlec marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
it('renders correctly with custom icon element', () => { | ||
const customIcon = <svg aria-label="custom-icon" />; | ||
render(<WalletDropdownFundLink icon={customIcon} />); | ||
|
||
const linkElement = screen.getByRole('link'); | ||
expect(linkElement).toBeInTheDocument(); | ||
expect(linkElement).toHaveAttribute('href', FUNDING_URL); | ||
expect(screen.getByText('Fund wallet')).toBeInTheDocument(); | ||
expect(screen.getByLabelText('custom-icon')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders correctly with custom text', () => { | ||
render(<WalletDropdownFundLink text="test" />); | ||
|
||
const linkElement = screen.getByRole('link'); | ||
expect(linkElement).toBeInTheDocument(); | ||
expect(linkElement).toHaveAttribute('href', FUNDING_URL); | ||
expect(screen.getByText('test')).toBeInTheDocument(); | ||
}); | ||
|
||
it('opens a new window when clicked with type="window" (default size medium)', () => { | ||
// Mock window.open | ||
const mockOpen = vi.fn(); | ||
vi.stubGlobal('open', mockOpen); | ||
|
||
// Mock window.screen | ||
vi.stubGlobal('screen', { width: 1024, height: 768 }); | ||
|
||
render(<WalletDropdownFundLink openIn="popup" />); | ||
|
||
const linkElement = screen.getByText('Fund wallet'); | ||
fireEvent.click(linkElement); | ||
|
||
// Check if window.open was called with the correct arguments | ||
expect(mockOpen).toHaveBeenCalledWith( | ||
expect.stringContaining('http://keys.coinbase.com/fund'), | ||
undefined, | ||
expect.stringContaining( | ||
'width=297,height=371,resizable,scrollbars=yes,status=1,left=364,top=199', | ||
), | ||
); | ||
|
||
// Clean up | ||
vi.unstubAllGlobals(); | ||
}); | ||
|
||
const testCases: WindowSizes = { | ||
sm: { width: '23vw', height: '28.75vw' }, | ||
md: { width: '29vw', height: '36.25vw' }, | ||
lg: { width: '35vw', height: '43.75vw' }, | ||
}; | ||
|
||
const minWidth = 280; | ||
const minHeight = 350; | ||
|
||
for (const [size, { width, height }] of Object.entries(testCases)) { | ||
it(`opens a new window when clicked with type="window" and popupSize="${size}"`, () => { | ||
const mockOpen = vi.fn(); | ||
const screenWidth = 1024; | ||
const screenHeight = 768; | ||
const innerWidth = 1024; | ||
const innerHeight = 768; | ||
|
||
vi.stubGlobal('open', mockOpen); | ||
vi.stubGlobal('screen', { width: screenWidth, height: screenHeight }); | ||
|
||
render( | ||
<WalletDropdownFundLink | ||
openIn="popup" | ||
popupSize={size as keyof typeof testCases} | ||
/>, | ||
); | ||
|
||
const linkElement = screen.getByText('Fund wallet'); | ||
fireEvent.click(linkElement); | ||
|
||
const vwToPx = (vw: string) => | ||
Math.round((Number.parseFloat(vw) / 100) * innerWidth); | ||
|
||
const expectedWidth = Math.max(minWidth, vwToPx(width)); | ||
const expectedHeight = Math.max(minHeight, vwToPx(height)); | ||
const adjustedHeight = Math.min( | ||
expectedHeight, | ||
Math.round(innerHeight * 0.9), | ||
); | ||
const expectedLeft = Math.round((screenWidth - expectedWidth) / 2); | ||
const expectedTop = Math.round((screenHeight - adjustedHeight) / 2); | ||
expect(mockOpen).toHaveBeenCalledWith( | ||
expect.stringContaining('http://keys.coinbase.com/fund'), | ||
undefined, | ||
expect.stringContaining( | ||
`width=${expectedWidth},height=${adjustedHeight},resizable,scrollbars=yes,status=1,left=${expectedLeft},top=${expectedTop}`, | ||
), | ||
); | ||
|
||
vi.unstubAllGlobals(); | ||
vi.clearAllMocks(); | ||
}); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { useCallback, useMemo } from 'react'; | ||
import { useEffect, useState } from 'react'; | ||
import { cn, pressable, text as themeText } from '../../styles/theme'; | ||
import { version } from '../../version'; | ||
import { useIcon } from '../hooks/useIcon'; | ||
import type { WalletDropdownFundLinkReact } from '../types'; | ||
import { getWindowDimensions } from '../utils/getWindowDimensions'; | ||
|
||
export function WalletDropdownFundLink({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. alphabetical order There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed in new diff, updated in |
||
className, | ||
icon = 'fundWallet', | ||
openIn = 'tab', | ||
popupFeatures, | ||
popupSize = 'md', | ||
rel, | ||
target, | ||
text = 'Fund wallet', | ||
0xAlec marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}: WalletDropdownFundLinkReact) { | ||
const [fundingUrl, setFundingUrl] = useState(''); | ||
|
||
const iconSvg = useIcon({ icon }); | ||
|
||
useEffect(() => { | ||
const currentURL = window.location.href; | ||
const tabName = document.title; | ||
const url = `http://keys.coinbase.com/fund?dappName=${encodeURIComponent( | ||
tabName, | ||
)}&dappUrl=${encodeURIComponent(currentURL)}&version=${encodeURIComponent( | ||
version, | ||
)}&source=onchainkit`; | ||
setFundingUrl(url); | ||
}, []); | ||
|
||
const handleClick = useCallback( | ||
(e: React.MouseEvent) => { | ||
e.preventDefault(); | ||
const { width, height } = getWindowDimensions(popupSize); | ||
|
||
const left = Math.round((window.screen.width - width) / 2); | ||
const top = Math.round((window.screen.height - height) / 2); | ||
|
||
const windowFeatures = | ||
popupFeatures || | ||
`width=${width},height=${height},resizable,scrollbars=yes,status=1,left=${left},top=${top}`; | ||
window.open(fundingUrl, target, windowFeatures); | ||
}, | ||
[fundingUrl, popupFeatures, popupSize, target], | ||
); | ||
|
||
const overrideClassName = cn( | ||
pressable.default, | ||
'relative flex items-center px-4 py-3', | ||
className, | ||
); | ||
|
||
const linkContent = useMemo( | ||
() => ( | ||
<> | ||
<div className="-translate-y-1/2 absolute top-1/2 left-4 flex h-4 w-4 items-center justify-center"> | ||
{iconSvg} | ||
</div> | ||
<span className={cn(themeText.body, 'pl-6')}>{text}</span> | ||
</> | ||
), | ||
[iconSvg, text], | ||
); | ||
|
||
if (openIn === 'tab') { | ||
return ( | ||
<a | ||
className={overrideClassName} | ||
href={fundingUrl} | ||
target={target} | ||
rel={rel} | ||
> | ||
{linkContent} | ||
</a> | ||
); | ||
} | ||
return ( | ||
<button type="button" className={overrideClassName} onClick={handleClick}> | ||
{linkContent} | ||
</button> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { renderHook } from '@testing-library/react'; | ||
import { describe, expect, it } from 'vitest'; | ||
import { fundWalletSvg } from '../../internal/svg/fundWallet'; | ||
import { walletSvg } from '../../internal/svg/walletSvg'; | ||
import { useIcon } from './useIcon'; | ||
|
||
describe('useIcon', () => { | ||
it('should return null when icon is undefined', () => { | ||
const { result } = renderHook(() => useIcon({ icon: undefined })); | ||
expect(result.current).toBeNull(); | ||
}); | ||
|
||
it('should return walletSvg when icon is "wallet"', () => { | ||
const { result } = renderHook(() => useIcon({ icon: 'wallet' })); | ||
expect(result.current).toBe(walletSvg); | ||
}); | ||
|
||
it('should return fundWalletSvg when icon is "fundWallet"', () => { | ||
const { result } = renderHook(() => useIcon({ icon: 'fundWallet' })); | ||
expect(result.current).toBe(fundWalletSvg); | ||
}); | ||
|
||
it('should memoize the result for undefined', () => { | ||
const { result, rerender } = renderHook(() => useIcon({}), { | ||
initialProps: {}, | ||
}); | ||
|
||
const initialResult = result.current; | ||
rerender({}); | ||
expect(result.current).toBe(initialResult); | ||
}); | ||
|
||
it('should memoize the result for wallet', () => { | ||
const { result, rerender } = renderHook(({ icon }) => useIcon({ icon }), { | ||
initialProps: { icon: 'wallet' }, | ||
}); | ||
|
||
const initialResult = result.current; | ||
rerender({ icon: 'wallet' }); | ||
expect(result.current).toBe(initialResult); | ||
}); | ||
|
||
it('should memoize the result for fundWallet', () => { | ||
const { result, rerender } = renderHook(({ icon }) => useIcon({ icon }), { | ||
initialProps: { icon: 'fundWallet' }, | ||
}); | ||
|
||
const initialResult = result.current; | ||
rerender({ icon: 'fundWallet' }); | ||
expect(result.current).toBe(initialResult); | ||
}); | ||
|
||
it('should memoize the result for custom icon', () => { | ||
const customIcon = <svg aria-label="custom-icon" />; | ||
const { result, rerender } = renderHook(({ icon }) => useIcon({ icon }), { | ||
initialProps: { icon: customIcon }, | ||
}); | ||
|
||
const initialResult = result.current; | ||
rerender({ icon: customIcon }); | ||
expect(result.current).toBe(initialResult); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { isValidElement, useMemo } from 'react'; | ||
import { fundWalletSvg } from '../../internal/svg/fundWallet'; | ||
import { walletSvg } from '../../internal/svg/walletSvg'; | ||
|
||
export const useIcon = ({ icon }: { icon?: React.ReactNode }) => { | ||
return useMemo(() => { | ||
if (icon === undefined) { | ||
return null; | ||
} | ||
switch (icon) { | ||
case 'wallet': | ||
return walletSvg; | ||
case 'fundWallet': | ||
return fundWalletSvg; | ||
} | ||
if (isValidElement(icon)) { | ||
return icon; | ||
} | ||
}, [icon]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://www.w3schools.com/tags/att_a_target.asp
target
is a different attribute thanhref
-target
will control wherehref
opens (i.e. new tab or replace current tab)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
href
is hardcoded here to be keys.coinbase.com/fundThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks I was confusing the two
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think overall there is an interesting question to ask. What are properties we want to be able to customize and what we don't want to have customized.
Otherwise it feels a weird overalap with the Link component.