Skip to content

Commit 4ae8823

Browse files
initial commit
1 parent 880c6cc commit 4ae8823

File tree

12 files changed

+823
-0
lines changed

12 files changed

+823
-0
lines changed

playground/nextjs-app-router/components/Demo.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import TransactionDemo from './demo/Transaction';
1818
import TransactionDefaultDemo from './demo/TransactionDefault';
1919
import WalletDemo from './demo/Wallet';
2020
import WalletDefaultDemo from './demo/WalletDefault';
21+
import FundSwapDemo from './demo/FundSwap';
2122

2223
const activeComponentMapping: Record<OnchainKitComponent, React.FC> = {
2324
[OnchainKitComponent.Fund]: FundDemo,
25+
[OnchainKitComponent.FundSwap]: FundSwapDemo,
2426
[OnchainKitComponent.Identity]: IdentityDemo,
2527
[OnchainKitComponent.Transaction]: TransactionDemo,
2628
[OnchainKitComponent.Checkout]: CheckoutDemo,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { ENVIRONMENT, ENVIRONMENT_VARIABLES } from '@/lib/constants';
2+
import { type LifecycleStatus, FundSwap } from '@coinbase/onchainkit/swap';
3+
import type { SwapError } from '@coinbase/onchainkit/swap';
4+
import type { Token } from '@coinbase/onchainkit/token';
5+
import { useCallback, useContext } from 'react';
6+
import type { TransactionReceipt } from 'viem';
7+
import { base } from 'viem/chains';
8+
import { AppContext } from '../AppProvider';
9+
10+
const FALLBACK_DEFAULT_MAX_SLIPPAGE = 3;
11+
12+
function FundSwapComponent() {
13+
const { chainId, isSponsored, defaultMaxSlippage } = useContext(AppContext);
14+
15+
const degenToken: Token = {
16+
name: 'DEGEN',
17+
address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed',
18+
symbol: 'DEGEN',
19+
decimals: 18,
20+
image:
21+
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm',
22+
chainId: base.id,
23+
};
24+
25+
const ethToken: Token = {
26+
name: 'ETH',
27+
address: '',
28+
symbol: 'ETH',
29+
decimals: 18,
30+
image:
31+
'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
32+
chainId: base.id,
33+
};
34+
35+
const usdcToken: Token = {
36+
name: 'USDC',
37+
address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
38+
symbol: 'USDC',
39+
decimals: 6,
40+
image:
41+
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2',
42+
chainId: base.id,
43+
};
44+
45+
const wethToken: Token = {
46+
name: 'Wrapped Ether',
47+
address: '0x4200000000000000000000000000000000000006',
48+
symbol: 'WETH',
49+
decimals: 6,
50+
image:
51+
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/47/bc/47bc3593c2dec7c846b66b7ba5f6fa6bd69ec34f8ebb931f2a43072e5aaac7a8-YmUwNmRjZDUtMjczYy00NDFiLWJhZDUtMzgwNjFmYWM0Njkx',
52+
chainId: base.id,
53+
};
54+
55+
const swappableTokens = [degenToken, ethToken, usdcToken, wethToken];
56+
57+
const handleOnStatus = useCallback((lifecycleStatus: LifecycleStatus) => {
58+
console.log('Status:', lifecycleStatus);
59+
}, []);
60+
61+
const handleOnSuccess = useCallback(
62+
(transactionReceipt: TransactionReceipt) => {
63+
console.log('Success:', transactionReceipt);
64+
},
65+
[],
66+
);
67+
68+
const handleOnError = useCallback((swapError: SwapError) => {
69+
console.log('Error:', swapError);
70+
}, []);
71+
72+
return (
73+
<div className="relative flex h-full w-full flex-col items-center">
74+
{chainId !== base.id ? (
75+
<div className="absolute top-0 left-0 z-10 flex h-full w-full flex-col justify-center rounded-xl bg-[#000000] bg-opacity-50 text-center">
76+
<div className="mx-auto w-2/3 rounded-md bg-muted p-6 text-sm">
77+
Swap Demo is only available on Base.
78+
<br />
79+
You're connected to a different network. Switch to Base to continue
80+
using the app.
81+
</div>
82+
</div>
83+
) : (
84+
<></>
85+
)}
86+
87+
{ENVIRONMENT_VARIABLES[ENVIRONMENT.ENVIRONMENT] === 'production' &&
88+
chainId === base.id ? (
89+
<div className="mb-5 italic">
90+
Note: Swap is disabled on production. To test, run the app locally.
91+
</div>
92+
) : null}
93+
94+
<FundSwap
95+
className="w-full"
96+
onStatus={handleOnStatus}
97+
onSuccess={handleOnSuccess}
98+
onError={handleOnError}
99+
config={{
100+
maxSlippage: defaultMaxSlippage || FALLBACK_DEFAULT_MAX_SLIPPAGE,
101+
}}
102+
isSponsored={isSponsored}
103+
toToken={degenToken}
104+
/>
105+
</div>
106+
);
107+
}
108+
109+
export default function FundSwapDemo() {
110+
return (
111+
<div className="mx-auto">
112+
<FundSwapComponent />
113+
</div>
114+
);
115+
}

playground/nextjs-app-router/components/form/active-component.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function ActiveComponent() {
2727
</SelectTrigger>
2828
<SelectContent>
2929
<SelectItem value={OnchainKitComponent.Fund}>Fund</SelectItem>
30+
<SelectItem value={OnchainKitComponent.FundSwap}>FundSwap</SelectItem>
3031
<SelectItem value={OnchainKitComponent.Identity}>Identity</SelectItem>
3132
<SelectItem value={OnchainKitComponent.IdentityCard}>
3233
IdentityCard

playground/nextjs-app-router/types/onchainkit.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export enum OnchainKitComponent {
22
Fund = 'fund',
3+
FundSwap = 'fund-swap',
34
Identity = 'identity',
45
IdentityCard = 'identity-card',
56
Checkout = 'checkout',

src/swap/components/FundSwap.tsx

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useEffect, useRef } from 'react';
2+
import { cn } from '../../styles/theme';
3+
import { FALLBACK_DEFAULT_MAX_SLIPPAGE } from '../constants';
4+
import type { FundSwapReact } from '../types';
5+
import { FundSwapButton } from './FundSwapButton';
6+
import { FundSwapDropdown } from './FundSwapDropdown';
7+
import { FundSwapInput } from './FundSwapInput';
8+
import { FundSwapProvider, useFundSwapContext } from './FundSwapProvider';
9+
//
10+
export function FundSwapContent({ className }: { className?: string }) {
11+
const { isDropdownOpen, setIsDropdownOpen } = useFundSwapContext();
12+
const walletContainerRef = useRef<HTMLDivElement>(null);
13+
14+
// Handle clicking outside the wallet component to close the dropdown.
15+
useEffect(() => {
16+
const handleClickOutsideComponent = (event: MouseEvent) => {
17+
if (
18+
walletContainerRef.current &&
19+
!walletContainerRef.current.contains(event.target as Node) &&
20+
isDropdownOpen
21+
) {
22+
setIsDropdownOpen(false);
23+
}
24+
};
25+
26+
document.addEventListener('click', handleClickOutsideComponent);
27+
return () =>
28+
document.removeEventListener('click', handleClickOutsideComponent);
29+
}, [isDropdownOpen, setIsDropdownOpen]);
30+
31+
return (
32+
<div
33+
ref={walletContainerRef}
34+
className={cn('flex items-center gap-4 relative', className)}
35+
>
36+
<FundSwapInput />
37+
<FundSwapButton />
38+
{isDropdownOpen && <FundSwapDropdown />}
39+
</div>
40+
);
41+
}
42+
export function FundSwap({
43+
config = {
44+
maxSlippage: FALLBACK_DEFAULT_MAX_SLIPPAGE,
45+
},
46+
className,
47+
experimental = { useAggregator: false },
48+
isSponsored = false,
49+
onError,
50+
onStatus,
51+
onSuccess,
52+
toToken,
53+
}: FundSwapReact) {
54+
return (
55+
<FundSwapProvider
56+
config={config}
57+
experimental={experimental}
58+
isSponsored={isSponsored}
59+
onError={onError}
60+
onStatus={onStatus}
61+
onSuccess={onSuccess}
62+
toToken={toToken}
63+
>
64+
<FundSwapContent className={className} />
65+
</FundSwapProvider>
66+
);
67+
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useCallback } from 'react';
2+
import { Spinner } from '../../internal/components/Spinner';
3+
import {
4+
cn,
5+
text,
6+
color,
7+
background,
8+
border,
9+
pressable,
10+
} from '../../styles/theme';
11+
import { useFundSwapContext } from './FundSwapProvider';
12+
13+
export function FundSwapButton() {
14+
const { setIsDropdownOpen } = useFundSwapContext();
15+
const isLoading = false;
16+
const isDisabled = false;
17+
18+
const handleSubmit = useCallback(() => {
19+
setIsDropdownOpen(true);
20+
}, []);
21+
22+
return (
23+
<button
24+
type="button"
25+
className={cn(
26+
background.primary,
27+
border.radius,
28+
'rounded-xl',
29+
'px-4 py-3',
30+
isDisabled && pressable.disabled,
31+
text.headline,
32+
)}
33+
onClick={handleSubmit}
34+
data-testid="ockFundSwapButton_Button"
35+
>
36+
{isLoading ? (
37+
<Spinner />
38+
) : (
39+
<span className={cn(text.headline, color.inverse)}>Buy</span>
40+
)}
41+
</button>
42+
);
43+
}
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useAccount } from 'wagmi';
2+
import { TokenImage, type Token } from '../../token';
3+
import { base } from 'viem/chains';
4+
import { background, cn, color } from '../../styles/theme';
5+
6+
function TokenItem({ token }: { token: Token }) {
7+
return (
8+
<div className="flex">
9+
<TokenImage token={token} />
10+
<div className="flex flex-col">{token.name}</div>
11+
</div>
12+
);
13+
}
14+
15+
export function FundSwapDropdown() {
16+
const { chainId } = useAccount();
17+
18+
const ethToken: Token = {
19+
name: 'ETH',
20+
address: '',
21+
symbol: 'ETH',
22+
decimals: 18,
23+
image:
24+
'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
25+
chainId: chainId || base.id,
26+
};
27+
28+
const usdcToken: Token = {
29+
name: 'USDC',
30+
address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
31+
symbol: 'USDC',
32+
decimals: 6,
33+
image:
34+
'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/44/2b/442b80bd16af0c0d9b22e03a16753823fe826e5bfd457292b55fa0ba8c1ba213-ZWUzYjJmZGUtMDYxNy00NDcyLTg0NjQtMWI4OGEwYjBiODE2',
35+
chainId: chainId || base.id,
36+
};
37+
38+
return (
39+
<div
40+
className={cn(
41+
color.foreground,
42+
background.alternate,
43+
'flex flex-col absolute translate-y-[110%] right-0 bottom-0 gap-4',
44+
'rounded p-4',
45+
)}
46+
>
47+
<div>Buy with</div>
48+
<TokenItem token={ethToken} />
49+
<TokenItem token={usdcToken} />
50+
</div>
51+
);
52+
}

src/swap/components/FundSwapInput.tsx

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useCallback } from 'react';
2+
import { useFundSwapContext } from './FundSwapProvider';
3+
import { TextInput } from '../../internal/components/TextInput';
4+
import { TokenChip } from '../../token';
5+
import { formatAmount } from '../utils/formatAmount';
6+
import { isValidAmount } from '../../internal/utils/isValidAmount';
7+
import { cn, pressable } from '../../styles/theme';
8+
9+
export function FundSwapInput() {
10+
const { to, handleAmountChange } = useFundSwapContext();
11+
12+
const handleChange = useCallback(
13+
(amount: string) => {
14+
handleAmountChange('to', amount);
15+
},
16+
[handleAmountChange],
17+
);
18+
19+
if (!to?.token) {
20+
return null;
21+
}
22+
23+
return (
24+
<div className="flex items-center h-full border rounded-lg px-4">
25+
<TextInput
26+
className={cn(
27+
'mr-2 w-full border-[none] bg-transparent font-display',
28+
'leading-none outline-none',
29+
// hasInsufficientBalance && address ? color.error : color.foreground,
30+
)}
31+
placeholder="0.0"
32+
delayMs={1000}
33+
value={formatAmount(to.amount)}
34+
setValue={to.setAmount}
35+
disabled={to.loading}
36+
onChange={handleChange}
37+
inputValidator={isValidAmount}
38+
/>
39+
<TokenChip className={pressable.inverse} token={to.token} />
40+
</div>
41+
);
42+
}

0 commit comments

Comments
 (0)