@coinbase/create-cdp-app 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template-react-components/index.html +1 -1
- package/template-react-components/package.json +5 -4
- package/template-react-components/public/eth.svg +25 -0
- package/template-react-components/src/App.tsx +8 -9
- package/template-react-components/src/Header.tsx +62 -0
- package/template-react-components/src/Icons.tsx +66 -0
- package/template-react-components/src/Loading.tsx +15 -0
- package/template-react-components/src/SignInScreen.tsx +17 -0
- package/template-react-components/src/SignedInScreen.tsx +64 -0
- package/template-react-components/src/Transaction.tsx +86 -39
- package/template-react-components/src/UserBalance.tsx +43 -0
- package/template-react-components/src/index.css +264 -67
- package/template-react-components/src/main.tsx +2 -1
- package/template-react-components/src/theme.ts +32 -0
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>
|
|
7
|
+
<title>CDP React StarterKit</title>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="root"></div>
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
"preview": "vite preview"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
+
"@coinbase/cdp-core": "latest",
|
|
14
|
+
"@coinbase/cdp-hooks": "latest",
|
|
15
|
+
"@coinbase/cdp-react": "latest",
|
|
13
16
|
"react": "^19.1.0",
|
|
14
17
|
"react-dom": "^19.1.0",
|
|
15
|
-
"
|
|
16
|
-
"@coinbase/cdp-hooks": "latest",
|
|
17
|
-
"@coinbase/cdp-core": "latest"
|
|
18
|
+
"viem": "^2.33.0"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@eslint/js": "^9.30.1",
|
|
@@ -29,4 +30,4 @@
|
|
|
29
30
|
"typescript-eslint": "^8.35.1",
|
|
30
31
|
"vite": "^7.0.4"
|
|
31
32
|
}
|
|
32
|
-
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="32"
|
|
4
|
+
height="32"
|
|
5
|
+
viewBox="0 0 32 32"
|
|
6
|
+
aria-hidden="true"
|
|
7
|
+
>
|
|
8
|
+
<g fill="none" fillRule="evenodd">
|
|
9
|
+
<circle cx="16" cy="16" r="16" fill="#627EEA" />
|
|
10
|
+
<g fill="#FFF" fillRule="nonzero">
|
|
11
|
+
<path fillOpacity=".602" d="M16.498 4v8.87l7.497 3.35z" />
|
|
12
|
+
<path d="M16.498 4L9 16.22l7.498-3.35z" />
|
|
13
|
+
<path
|
|
14
|
+
fillOpacity=".602"
|
|
15
|
+
d="M16.498 21.968v6.027L24 17.616z"
|
|
16
|
+
/>
|
|
17
|
+
<path d="M16.498 27.995v-6.028L9 17.616z" />
|
|
18
|
+
<path
|
|
19
|
+
fillOpacity=".2"
|
|
20
|
+
d="M16.498 20.573l7.497-4.353-7.497-3.348z"
|
|
21
|
+
/>
|
|
22
|
+
<path fillOpacity=".602" d="M9 16.22l7.498 4.353v-7.701z" />
|
|
23
|
+
</g>
|
|
24
|
+
</g>
|
|
25
|
+
</svg>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useIsInitialized, useIsSignedIn
|
|
2
|
-
import { AuthButton } from "@coinbase/cdp-react";
|
|
1
|
+
import { useIsInitialized, useIsSignedIn } from "@coinbase/cdp-hooks";
|
|
3
2
|
|
|
4
|
-
import
|
|
3
|
+
import Loading from "./Loading";
|
|
4
|
+
import SignedInScreen from "./SignedInScreen";
|
|
5
|
+
import SignInScreen from "./SignInScreen";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* This component how to use the useIsIntialized, useEvmAddress, and useIsSignedIn hooks.
|
|
@@ -9,17 +10,15 @@ import Transaction from "./Transaction";
|
|
|
9
10
|
*/
|
|
10
11
|
function App() {
|
|
11
12
|
const isInitialized = useIsInitialized();
|
|
12
|
-
const evmAddress = useEvmAddress();
|
|
13
13
|
const isSignedIn = useIsSignedIn();
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
|
-
<div className="app">
|
|
17
|
-
<
|
|
18
|
-
{!isInitialized && <div>Loading...</div>}
|
|
16
|
+
<div className="app flex-col-container flex-grow">
|
|
17
|
+
{!isInitialized && <Loading />}
|
|
19
18
|
{isInitialized && (
|
|
20
19
|
<>
|
|
21
|
-
<
|
|
22
|
-
{isSignedIn &&
|
|
20
|
+
{!isSignedIn && <SignInScreen />}
|
|
21
|
+
{isSignedIn && <SignedInScreen />}
|
|
23
22
|
</>
|
|
24
23
|
)}
|
|
25
24
|
</div>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEvmAddress } from "@coinbase/cdp-hooks";
|
|
2
|
+
import { AuthButton } from "@coinbase/cdp-react/components/AuthButton";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { IconCheck, IconCopy, IconUser } from "./Icons";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Header component
|
|
9
|
+
*/
|
|
10
|
+
function Header() {
|
|
11
|
+
const evmAddress = useEvmAddress();
|
|
12
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
13
|
+
|
|
14
|
+
const copyAddress = async () => {
|
|
15
|
+
if (!evmAddress) return;
|
|
16
|
+
try {
|
|
17
|
+
await navigator.clipboard.writeText(evmAddress);
|
|
18
|
+
setIsCopied(true);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(error);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!isCopied) return;
|
|
26
|
+
const timeout = setTimeout(() => {
|
|
27
|
+
setIsCopied(false);
|
|
28
|
+
}, 2000);
|
|
29
|
+
return () => clearTimeout(timeout);
|
|
30
|
+
}, [isCopied]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<header>
|
|
34
|
+
<div className="header-inner">
|
|
35
|
+
<h1 className="site-title">CDP React StarterKit</h1>
|
|
36
|
+
<div className="user-info flex-row-container">
|
|
37
|
+
{evmAddress && (
|
|
38
|
+
<button
|
|
39
|
+
aria-label="copy wallet address"
|
|
40
|
+
className="flex-row-container copy-address-button"
|
|
41
|
+
onClick={copyAddress}
|
|
42
|
+
>
|
|
43
|
+
{!isCopied && (
|
|
44
|
+
<>
|
|
45
|
+
<IconUser className="user-icon user-icon--user" />
|
|
46
|
+
<IconCopy className="user-icon user-icon--copy" />
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
{isCopied && <IconCheck className="user-icon user-icon--check" />}
|
|
50
|
+
<span className="wallet-address">
|
|
51
|
+
{evmAddress.slice(0, 6)}...{evmAddress.slice(-4)}
|
|
52
|
+
</span>
|
|
53
|
+
</button>
|
|
54
|
+
)}
|
|
55
|
+
<AuthButton />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</header>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default Header;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type ReactNode, type SVGProps } from "react";
|
|
2
|
+
|
|
3
|
+
const SvgIcon = ({ children, ...props }: SVGProps<SVGSVGElement> & { children: ReactNode }) => {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
fill="currentColor"
|
|
8
|
+
role="img"
|
|
9
|
+
aria-hidden={props["aria-label"] ? undefined : true}
|
|
10
|
+
{...props}
|
|
11
|
+
>
|
|
12
|
+
{children}
|
|
13
|
+
</svg>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check icon
|
|
19
|
+
*
|
|
20
|
+
* @param props - SVG props
|
|
21
|
+
* @returns SVG element
|
|
22
|
+
*/
|
|
23
|
+
export const IconCheck = (props: Omit<SVGProps<SVGSVGElement>, "viewBox">) => {
|
|
24
|
+
return (
|
|
25
|
+
<SvgIcon width="24" height="24" viewBox="0 0 24 24" {...props}>
|
|
26
|
+
<path
|
|
27
|
+
fillRule="evenodd"
|
|
28
|
+
d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z"
|
|
29
|
+
clipRule="evenodd"
|
|
30
|
+
/>
|
|
31
|
+
</SvgIcon>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Copy icon
|
|
37
|
+
*
|
|
38
|
+
* @param props - SVG props
|
|
39
|
+
* @returns SVG element
|
|
40
|
+
*/
|
|
41
|
+
export const IconCopy = (props: Omit<SVGProps<SVGSVGElement>, "viewBox">) => {
|
|
42
|
+
return (
|
|
43
|
+
<SvgIcon width="24" height="24" viewBox="0 0 24 24" {...props}>
|
|
44
|
+
<path d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z" />
|
|
45
|
+
<path d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z" />
|
|
46
|
+
</SvgIcon>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* User icon
|
|
52
|
+
*
|
|
53
|
+
* @param props - SVG props
|
|
54
|
+
* @returns SVG element
|
|
55
|
+
*/
|
|
56
|
+
export const IconUser = (props: Omit<SVGProps<SVGSVGElement>, "viewBox">) => {
|
|
57
|
+
return (
|
|
58
|
+
<SvgIcon width="24" height="24" viewBox="0 0 24 24" {...props}>
|
|
59
|
+
<path
|
|
60
|
+
fillRule="evenodd"
|
|
61
|
+
d="M18.685 19.097A9.723 9.723 0 0 0 21.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 0 0 3.065 7.097A9.716 9.716 0 0 0 12 21.75a9.716 9.716 0 0 0 6.685-2.653Zm-12.54-1.285A7.486 7.486 0 0 1 12 15a7.486 7.486 0 0 1 5.855 2.812A8.224 8.224 0 0 1 12 20.25a8.224 8.224 0 0 1-5.855-2.438ZM15.75 9a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
|
|
62
|
+
clipRule="evenodd"
|
|
63
|
+
/>
|
|
64
|
+
</SvgIcon>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LoadingSpinner } from "@coinbase/cdp-react/components/LoadingSpinner";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* App loading screen
|
|
5
|
+
*/
|
|
6
|
+
function Loading() {
|
|
7
|
+
return (
|
|
8
|
+
<main>
|
|
9
|
+
<h1 className="sr-only">Loading</h1>
|
|
10
|
+
<LoadingSpinner />
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default Loading;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AuthButton } from "@coinbase/cdp-react/components/AuthButton";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Sign In screen
|
|
5
|
+
*/
|
|
6
|
+
function SignInScreen() {
|
|
7
|
+
return (
|
|
8
|
+
<main className="card card--login">
|
|
9
|
+
<h1 className="sr-only">Sign in</h1>
|
|
10
|
+
<p className="card-title">Welcome!</p>
|
|
11
|
+
<p>Please sign in to continue.</p>
|
|
12
|
+
<AuthButton />
|
|
13
|
+
</main>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default SignInScreen;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEvmAddress, useIsSignedIn } from "@coinbase/cdp-hooks";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { createPublicClient, http, formatEther } from "viem";
|
|
4
|
+
import { baseSepolia } from "viem/chains";
|
|
5
|
+
|
|
6
|
+
import Header from "./Header";
|
|
7
|
+
import Transaction from "./Transaction";
|
|
8
|
+
import UserBalance from "./UserBalance";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a viem client to access user's balance on the Base Sepolia network
|
|
12
|
+
*/
|
|
13
|
+
const client = createPublicClient({
|
|
14
|
+
chain: baseSepolia,
|
|
15
|
+
transport: http(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The Signed In screen
|
|
20
|
+
*/
|
|
21
|
+
function SignedInScreen() {
|
|
22
|
+
const isSignedIn = useIsSignedIn();
|
|
23
|
+
const evmAddress = useEvmAddress();
|
|
24
|
+
const [balance, setBalance] = useState<bigint | undefined>(undefined);
|
|
25
|
+
|
|
26
|
+
const formattedBalance = useMemo(() => {
|
|
27
|
+
if (balance === undefined) return undefined;
|
|
28
|
+
return formatEther(balance);
|
|
29
|
+
}, [balance]);
|
|
30
|
+
|
|
31
|
+
const getBalance = useCallback(async () => {
|
|
32
|
+
if (!evmAddress) return;
|
|
33
|
+
const balance = await client.getBalance({
|
|
34
|
+
address: evmAddress,
|
|
35
|
+
});
|
|
36
|
+
setBalance(balance);
|
|
37
|
+
}, [evmAddress]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
getBalance();
|
|
41
|
+
const interval = setInterval(getBalance, 500);
|
|
42
|
+
return () => clearInterval(interval);
|
|
43
|
+
}, [getBalance]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<Header />
|
|
48
|
+
<main className="main flex-col-container flex-grow">
|
|
49
|
+
<div className="main-inner flex-col-container">
|
|
50
|
+
<div className="card card--user-balance">
|
|
51
|
+
<UserBalance balance={formattedBalance} />
|
|
52
|
+
</div>
|
|
53
|
+
<div className="card card--transaction">
|
|
54
|
+
{isSignedIn && evmAddress && (
|
|
55
|
+
<Transaction balance={formattedBalance} onSuccess={getBalance} />
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</main>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default SignedInScreen;
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import { useSendEvmTransaction, useEvmAddress } from "@coinbase/cdp-hooks";
|
|
2
|
-
import {
|
|
2
|
+
import { Button } from "@coinbase/cdp-react/components/Button";
|
|
3
|
+
import { LoadingSkeleton } from "@coinbase/cdp-react/components/LoadingSkeleton";
|
|
4
|
+
import { type MouseEvent, useCallback, useMemo, useState } from "react";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
balance?: string;
|
|
8
|
+
onSuccess?: () => void;
|
|
9
|
+
}
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* This component demonstrates how to send an EVM transaction using the CDP hooks.
|
|
13
|
+
*
|
|
14
|
+
* @param {Props} props - The props for the Transaction component.
|
|
15
|
+
* @param {string} [props.balance] - The user's balance.
|
|
16
|
+
* @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
|
|
17
|
+
* @returns A component that displays a transaction form and a transaction hash.
|
|
6
18
|
*/
|
|
7
|
-
function Transaction() {
|
|
19
|
+
function Transaction(props: Props) {
|
|
20
|
+
const { balance, onSuccess } = props;
|
|
8
21
|
const sendEvmTransaction = useSendEvmTransaction();
|
|
9
22
|
const evmAddress = useEvmAddress();
|
|
10
23
|
|
|
24
|
+
const [isPending, setIsPending] = useState(false);
|
|
11
25
|
const [transactionHash, setTransactionHash] = useState<string | null>(null);
|
|
26
|
+
const hasBalance = useMemo(() => {
|
|
27
|
+
return balance && balance !== "0";
|
|
28
|
+
}, [balance]);
|
|
12
29
|
|
|
13
30
|
const handleSendTransaction = useCallback(
|
|
14
31
|
async (e: MouseEvent<HTMLButtonElement>) => {
|
|
@@ -17,6 +34,7 @@ function Transaction() {
|
|
|
17
34
|
}
|
|
18
35
|
|
|
19
36
|
e.preventDefault();
|
|
37
|
+
setIsPending(true);
|
|
20
38
|
|
|
21
39
|
const { transactionHash } = await sendEvmTransaction({
|
|
22
40
|
transaction: {
|
|
@@ -31,51 +49,80 @@ function Transaction() {
|
|
|
31
49
|
});
|
|
32
50
|
|
|
33
51
|
setTransactionHash(transactionHash);
|
|
52
|
+
setIsPending(false);
|
|
53
|
+
onSuccess?.();
|
|
34
54
|
},
|
|
35
|
-
[evmAddress, sendEvmTransaction],
|
|
55
|
+
[evmAddress, sendEvmTransaction, onSuccess],
|
|
36
56
|
);
|
|
37
57
|
|
|
38
58
|
return (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<span className="wallet-address">{evmAddress}</span>
|
|
42
|
-
<p>
|
|
43
|
-
Get testnet ETH from{" "}
|
|
44
|
-
<a
|
|
45
|
-
href="https://portal.cdp.coinbase.com/products/faucet"
|
|
46
|
-
target="_blank"
|
|
47
|
-
rel="noopener noreferrer"
|
|
48
|
-
>
|
|
49
|
-
Base Sepolia Faucet
|
|
50
|
-
</a>
|
|
51
|
-
</p>
|
|
52
|
-
{!transactionHash && (
|
|
59
|
+
<>
|
|
60
|
+
{balance === undefined && (
|
|
53
61
|
<>
|
|
54
|
-
<h2>Send
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
</button>
|
|
62
|
+
<h2 className="card-title">Send a transaction</h2>
|
|
63
|
+
<LoadingSkeleton className="loading--text" />
|
|
64
|
+
<LoadingSkeleton className="loading--btn" />
|
|
58
65
|
</>
|
|
59
66
|
)}
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
{balance !== undefined && (
|
|
68
|
+
<>
|
|
69
|
+
{!transactionHash && (
|
|
70
|
+
<>
|
|
71
|
+
<h2 className="card-title">Send a transaction</h2>
|
|
72
|
+
{hasBalance && (
|
|
73
|
+
<>
|
|
74
|
+
<p>Send 0.000001 ETH to yourself on Base Sepolia</p>
|
|
75
|
+
<Button
|
|
76
|
+
className="tx-button"
|
|
77
|
+
onClick={handleSendTransaction}
|
|
78
|
+
isPending={isPending}
|
|
79
|
+
>
|
|
80
|
+
Send Transaction
|
|
81
|
+
</Button>
|
|
82
|
+
</>
|
|
83
|
+
)}
|
|
84
|
+
{!hasBalance && (
|
|
85
|
+
<>
|
|
86
|
+
<p>You need ETH to send a transaction, but you have none.</p>
|
|
87
|
+
<p>
|
|
88
|
+
Get some from{" "}
|
|
89
|
+
<a
|
|
90
|
+
href="https://portal.cdp.coinbase.com/products/faucet"
|
|
91
|
+
target="_blank"
|
|
92
|
+
rel="noopener noreferrer"
|
|
93
|
+
>
|
|
94
|
+
Base Sepolia Faucet
|
|
95
|
+
</a>
|
|
96
|
+
</p>
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
{transactionHash && (
|
|
102
|
+
<>
|
|
103
|
+
<h2 className="card-title">Transaction sent</h2>
|
|
104
|
+
<p>
|
|
105
|
+
Transaction hash:{" "}
|
|
106
|
+
<a
|
|
107
|
+
href={`https://sepolia.basescan.org/tx/${transactionHash}`}
|
|
108
|
+
target="_blank"
|
|
109
|
+
rel="noopener noreferrer"
|
|
110
|
+
>
|
|
111
|
+
{transactionHash.slice(0, 6)}...{transactionHash.slice(-4)}
|
|
112
|
+
</a>
|
|
113
|
+
</p>
|
|
114
|
+
<Button
|
|
115
|
+
variant="secondary"
|
|
116
|
+
className="tx-button"
|
|
117
|
+
onClick={() => setTransactionHash(null)}
|
|
118
|
+
>
|
|
119
|
+
Send another transaction
|
|
120
|
+
</Button>
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
</>
|
|
77
124
|
)}
|
|
78
|
-
|
|
125
|
+
</>
|
|
79
126
|
);
|
|
80
127
|
}
|
|
81
128
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { LoadingSkeleton } from "@coinbase/cdp-react/components/LoadingSkeleton";
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
balance?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A component that displays the user's balance.
|
|
9
|
+
*
|
|
10
|
+
* @param {Props} props - The props for the UserBalance component.
|
|
11
|
+
* @param {string} [props.balance] - The user's balance.
|
|
12
|
+
* @returns A component that displays the user's balance.
|
|
13
|
+
*/
|
|
14
|
+
function UserBalance(props: Props) {
|
|
15
|
+
const { balance } = props;
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<h2 className="card-title">Available balance</h2>
|
|
19
|
+
<p className="user-balance flex-col-container flex-grow">
|
|
20
|
+
{balance === undefined && <LoadingSkeleton as="span" className="loading--balance" />}
|
|
21
|
+
{balance !== undefined && (
|
|
22
|
+
<span className="flex-row-container">
|
|
23
|
+
<img src="/eth.svg" alt="" className="balance-icon" />
|
|
24
|
+
<span>{balance}</span>
|
|
25
|
+
<span className="sr-only">Ethereum</span>
|
|
26
|
+
</span>
|
|
27
|
+
)}
|
|
28
|
+
</p>
|
|
29
|
+
<p>
|
|
30
|
+
Get testnet ETH from{" "}
|
|
31
|
+
<a
|
|
32
|
+
href="https://portal.cdp.coinbase.com/products/faucet"
|
|
33
|
+
target="_blank"
|
|
34
|
+
rel="noopener noreferrer"
|
|
35
|
+
>
|
|
36
|
+
Base Sepolia Faucet
|
|
37
|
+
</a>
|
|
38
|
+
</p>
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default UserBalance;
|
|
@@ -1,119 +1,316 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
--cdp-example-page-bg-color: #eaeaea;
|
|
3
|
+
--cdp-example-bg-overlay-color: rgba(0, 0, 0, 0.25);
|
|
4
|
+
--cdp-example-bg-skeleton-color: rgba(0, 0, 0, 0.1);
|
|
5
|
+
--cdp-example-text-color: #111111;
|
|
6
|
+
--cdp-example-text-secondary-color: #757575;
|
|
7
|
+
--cdp-example-accent-color: #0052ff;
|
|
8
|
+
--cdp-example-accent-hover-color: #0044d6;
|
|
9
|
+
--cdp-example-accent-foreground-color: #ffffff;
|
|
10
|
+
--cdp-example-bg-low-contrast-color: #eeeeee;
|
|
11
|
+
--cdp-example-card-bg-color: #ffffff;
|
|
12
|
+
--cdp-example-card-border-color: #dcdcdc;
|
|
13
|
+
--cdp-example-card-max-width: 30rem;
|
|
14
|
+
--cdp-example-base-font-size: 16px;
|
|
15
|
+
--cdp-example-font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
5
16
|
|
|
17
|
+
background-color: var(--cdp-example-page-bg-color);
|
|
18
|
+
color: var(--cdp-example-card-border-color);
|
|
6
19
|
color-scheme: light dark;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
20
|
+
font-family: var(--cdp-example-font-family);
|
|
21
|
+
font-size: var(--cdp-example-base-font-size);
|
|
10
22
|
font-synthesis: none;
|
|
23
|
+
font-weight: 400;
|
|
24
|
+
line-height: 1.5;
|
|
11
25
|
text-rendering: optimizeLegibility;
|
|
12
26
|
-webkit-font-smoothing: antialiased;
|
|
13
27
|
-moz-osx-font-smoothing: grayscale;
|
|
14
28
|
}
|
|
15
29
|
|
|
30
|
+
@media (prefers-color-scheme: dark) {
|
|
31
|
+
:root {
|
|
32
|
+
--cdp-example-page-bg-color: #0a0b0d;
|
|
33
|
+
--cdp-example-bg-overlay-color: rgba(0, 0, 0, 0.25);
|
|
34
|
+
--cdp-example-bg-skeleton-color: rgba(255, 255, 255, 0.1);
|
|
35
|
+
--cdp-example-text-color: #fafafa;
|
|
36
|
+
--cdp-example-text-secondary-color: #8a919e;
|
|
37
|
+
--cdp-example-accent-color: #578bfa;
|
|
38
|
+
--cdp-example-accent-hover-color: #3e79f9;
|
|
39
|
+
--cdp-example-accent-foreground-color: #0a0b0d;
|
|
40
|
+
--cdp-example-bg-low-contrast-color: #32353d;
|
|
41
|
+
--cdp-example-card-bg-color: #141519;
|
|
42
|
+
--cdp-example-card-border-color: #24262a;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
::selection {
|
|
47
|
+
background-color: color(from var(--cdp-example-accent-color) srgb r g b / 0.3);
|
|
48
|
+
}
|
|
49
|
+
|
|
16
50
|
html,
|
|
17
51
|
html * {
|
|
18
52
|
box-sizing: border-box;
|
|
19
53
|
}
|
|
20
54
|
|
|
21
|
-
|
|
55
|
+
body {
|
|
22
56
|
margin: 0;
|
|
23
|
-
|
|
24
|
-
width:
|
|
25
|
-
height: 100vh;
|
|
57
|
+
min-height: 100vh;
|
|
58
|
+
min-width: 320px;
|
|
26
59
|
}
|
|
27
60
|
|
|
28
|
-
|
|
61
|
+
body,
|
|
62
|
+
#root,
|
|
63
|
+
#root > div,
|
|
64
|
+
.flex-col-container {
|
|
29
65
|
display: flex;
|
|
30
66
|
flex-direction: column;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
place-items: center;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.flex-row-container {
|
|
31
72
|
align-items: center;
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: row;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.flex-grow {
|
|
78
|
+
flex-grow: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.sr-only {
|
|
82
|
+
border-width: 0;
|
|
83
|
+
clip: rect(0, 0, 0, 0);
|
|
84
|
+
height: 1px;
|
|
85
|
+
margin: -1px;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
padding: 0;
|
|
88
|
+
position: absolute;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
width: 1px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#root {
|
|
94
|
+
flex-grow: 1;
|
|
95
|
+
margin: 0;
|
|
96
|
+
padding: 0rem;
|
|
32
97
|
width: 100%;
|
|
33
|
-
height: 100%;
|
|
34
|
-
color: var(--cdp-web-colors-text);
|
|
35
|
-
margin: 0 auto;
|
|
36
98
|
}
|
|
37
99
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
--cdp-web-button-ring-inset-color: transparent;
|
|
42
|
-
--cdp-web-button-ring-inset-width: 2px;
|
|
100
|
+
#root > div {
|
|
101
|
+
flex-grow: 1;
|
|
102
|
+
}
|
|
43
103
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
104
|
+
p {
|
|
105
|
+
margin: 0;
|
|
106
|
+
}
|
|
47
107
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
justify-content: center;
|
|
51
|
-
gap: 0.5em;
|
|
52
|
-
padding: 1em;
|
|
53
|
-
border-radius: 9999em;
|
|
54
|
-
border: 0;
|
|
55
|
-
background-color: var(--cdp-web-colors-primary);
|
|
56
|
-
color: var(--cdp-web-colors-primaryText);
|
|
57
|
-
font-size: 1em;
|
|
58
|
-
font-weight: 500;
|
|
59
|
-
line-height: 1.5;
|
|
60
|
-
text-decoration: none;
|
|
61
|
-
cursor: pointer;
|
|
62
|
-
transition: all 0.15s ease-in-out;
|
|
63
|
-
user-select: none;
|
|
108
|
+
a {
|
|
109
|
+
color: var(--cdp-example-accent-color);
|
|
64
110
|
}
|
|
65
111
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
color: var(--cdp-web-colors-primaryHoverText);
|
|
112
|
+
a:hover {
|
|
113
|
+
color: var(--cdp-example-accent-hover-color);
|
|
69
114
|
}
|
|
70
115
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
116
|
+
hr {
|
|
117
|
+
border: 0 solid var(--cdp-example-card-border-color);
|
|
118
|
+
border-bottom-width: 1px;
|
|
119
|
+
margin: 1rem 0;
|
|
75
120
|
}
|
|
76
121
|
|
|
77
|
-
.
|
|
122
|
+
.app {
|
|
123
|
+
height: 100%;
|
|
124
|
+
color: var(--cdp-web-colors-text);
|
|
125
|
+
margin: 0 auto;
|
|
78
126
|
width: 100%;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
header {
|
|
130
|
+
background-color: var(--cdp-example-card-bg-color);
|
|
131
|
+
border-bottom: 1px solid var(--cdp-web-colors-border);
|
|
132
|
+
padding: 0.5rem 1rem;
|
|
133
|
+
width: 100%;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
header .wallet-address {
|
|
137
|
+
margin-right: 1rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.header-inner {
|
|
141
|
+
align-items: center;
|
|
79
142
|
display: flex;
|
|
80
143
|
flex-direction: column;
|
|
81
|
-
|
|
144
|
+
justify-content: space-between;
|
|
145
|
+
margin: 0 auto;
|
|
146
|
+
max-width: 75rem;
|
|
147
|
+
text-align: center;
|
|
82
148
|
}
|
|
83
149
|
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
150
|
+
.user-info {
|
|
151
|
+
justify-content: space-between;
|
|
152
|
+
width: 100%;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.user-icon {
|
|
156
|
+
flex-grow: 0;
|
|
157
|
+
flex-shrink: 0;
|
|
158
|
+
height: 1.25rem;
|
|
159
|
+
margin-right: 0.25rem;
|
|
160
|
+
width: auto;
|
|
87
161
|
}
|
|
88
162
|
|
|
89
163
|
.wallet-address {
|
|
90
164
|
font-family: monospace;
|
|
91
|
-
|
|
92
|
-
padding: 0.5rem;
|
|
93
|
-
border-radius: 4px;
|
|
94
|
-
font-size: 0.9rem;
|
|
165
|
+
font-size: 0.875rem;
|
|
95
166
|
word-break: break-all;
|
|
96
167
|
}
|
|
97
168
|
|
|
98
|
-
.
|
|
99
|
-
|
|
169
|
+
.main {
|
|
170
|
+
padding: 0.5rem;
|
|
171
|
+
width: 100%;
|
|
100
172
|
}
|
|
101
173
|
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
174
|
+
.main-inner {
|
|
175
|
+
gap: 1rem;
|
|
176
|
+
width: 100%;
|
|
105
177
|
}
|
|
106
178
|
|
|
107
|
-
.
|
|
108
|
-
|
|
179
|
+
.main-inner > .card {
|
|
180
|
+
width: 100%;
|
|
109
181
|
}
|
|
110
182
|
|
|
111
|
-
|
|
112
|
-
|
|
183
|
+
.site-title {
|
|
184
|
+
font-size: 1.2rem;
|
|
185
|
+
font-weight: 400;
|
|
186
|
+
line-height: 1.2;
|
|
113
187
|
margin: 0;
|
|
188
|
+
margin-bottom: 0.5rem;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.site-title br {
|
|
192
|
+
display: none;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.card {
|
|
196
|
+
align-items: center;
|
|
197
|
+
background-color: var(--cdp-example-card-bg-color);
|
|
198
|
+
border: 1px solid var(--cdp-web-colors-border);
|
|
199
|
+
border-radius: 1rem;
|
|
114
200
|
display: flex;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
201
|
+
flex-direction: column;
|
|
202
|
+
gap: 1rem;
|
|
203
|
+
justify-content: space-between;
|
|
204
|
+
max-width: var(--cdp-example-card-max-width);
|
|
205
|
+
padding: 2rem 1rem;
|
|
206
|
+
text-align: center;
|
|
207
|
+
width: 100%;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.card-title {
|
|
211
|
+
font-size: 1.25rem;
|
|
212
|
+
font-weight: 500;
|
|
213
|
+
line-height: 1.2;
|
|
214
|
+
margin: 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.loading--balance {
|
|
218
|
+
border-radius: 9999em;
|
|
219
|
+
display: inline-block;
|
|
220
|
+
height: 2.25rem;
|
|
221
|
+
width: 7rem;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.loading--text {
|
|
225
|
+
height: 1rem;
|
|
226
|
+
border-radius: 9999em;
|
|
227
|
+
margin: 0.25rem 0;
|
|
228
|
+
width: 20rem;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.loading--btn {
|
|
232
|
+
border-radius: 9999em;
|
|
233
|
+
height: 3.5rem;
|
|
234
|
+
width: 9.375rem;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.user-balance {
|
|
238
|
+
font-size: 1.5rem;
|
|
239
|
+
font-weight: 400;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.user-balance .flex-row-container {
|
|
243
|
+
justify-content: center;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.balance-icon {
|
|
247
|
+
flex-grow: 0;
|
|
248
|
+
flex-shrink: 0;
|
|
249
|
+
height: 1.5rem;
|
|
250
|
+
margin-right: 0.5rem;
|
|
251
|
+
width: auto;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.copy-address-button {
|
|
255
|
+
background-color: transparent;
|
|
256
|
+
border: 0;
|
|
257
|
+
color: var(--cdp-example-text-color);
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
padding: 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.copy-address-button:hover .user-icon--user,
|
|
263
|
+
.copy-address-button .user-icon--copy {
|
|
264
|
+
display: none;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.copy-address-button .user-icon--user,
|
|
268
|
+
.copy-address-button:hover .user-icon--copy {
|
|
269
|
+
display: inline;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.tx-button {
|
|
273
|
+
padding-left: 2rem;
|
|
274
|
+
padding-right: 2rem;
|
|
275
|
+
min-width: 11.75rem;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@media (min-width: 540px) {
|
|
279
|
+
.header-inner {
|
|
280
|
+
flex-direction: row;
|
|
281
|
+
text-align: left;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.user-info {
|
|
285
|
+
width: auto;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.site-title {
|
|
289
|
+
margin-bottom: 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.site-title br {
|
|
293
|
+
display: inline;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.main {
|
|
297
|
+
padding: 1rem;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@media (min-width: 860px) {
|
|
302
|
+
:root {
|
|
303
|
+
--cdp-example-card-max-width: 35rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.main-inner {
|
|
307
|
+
align-items: stretch;
|
|
308
|
+
flex-direction: row;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@media (min-width: 1920px) {
|
|
313
|
+
:root {
|
|
314
|
+
--cdp-base-font-size: 20px;
|
|
315
|
+
}
|
|
119
316
|
}
|
|
@@ -4,11 +4,12 @@ import { createRoot } from "react-dom/client";
|
|
|
4
4
|
|
|
5
5
|
import App from "./App.tsx";
|
|
6
6
|
import { CDP_CONFIG } from "./config.ts";
|
|
7
|
+
import { theme } from "./theme.ts";
|
|
7
8
|
import "./index.css";
|
|
8
9
|
|
|
9
10
|
createRoot(document.getElementById("root")!).render(
|
|
10
11
|
<StrictMode>
|
|
11
|
-
<CDPReactProvider config={CDP_CONFIG}>
|
|
12
|
+
<CDPReactProvider config={CDP_CONFIG} theme={theme}>
|
|
12
13
|
<App />
|
|
13
14
|
</CDPReactProvider>
|
|
14
15
|
</StrictMode>,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Theme } from "@coinbase/cdp-react/theme";
|
|
2
|
+
|
|
3
|
+
export const theme: Partial<Theme> = {
|
|
4
|
+
"colors-background": "var(--cdp-example-card-bg-color)",
|
|
5
|
+
"colors-backgroundOverlay": "var(--cdp-example-bg-overlay-color)",
|
|
6
|
+
"colors-backgroundSkeleton": "var(--cdp-example-bg-skeleton-color)",
|
|
7
|
+
"colors-text": "var(--cdp-example-text-color)",
|
|
8
|
+
"colors-textSecondary": "var(--cdp-example-text-secondary-color)",
|
|
9
|
+
"colors-border": "var(--cdp-example-card-border-color)",
|
|
10
|
+
"colors-primary": "var(--cdp-example-accent-color)",
|
|
11
|
+
"colors-primaryText": "var(--cdp-example-accent-foreground-color)",
|
|
12
|
+
"colors-primaryHoverBackground": "var(--cdp-example-accent-hover-color)",
|
|
13
|
+
"colors-primaryHoverText": "var(--cdp-example-accent-foreground-color)",
|
|
14
|
+
"colors-primaryFocusRing": "var(--cdp-example-accent-color)",
|
|
15
|
+
"colors-secondary": "var(--cdp-example-bg-low-contrast-color)",
|
|
16
|
+
"colors-secondaryText": "var(--cdp-example-text-color)",
|
|
17
|
+
"colors-secondaryHoverBackground": "var(--cdp-example-bg-low-contrast-color)",
|
|
18
|
+
"colors-secondaryHoverText": "var(--cdp-example-text-color)",
|
|
19
|
+
"colors-secondaryFocusRing": "var(--cdp-example-accent-color)",
|
|
20
|
+
"colors-inputBackground": "var(--cdp-example-card-bg-color)",
|
|
21
|
+
"colors-inputBorder": "var(--cdp-example-text-secondary-color)",
|
|
22
|
+
"colors-inputText": "var(--cdp-example-text-color)",
|
|
23
|
+
"colors-inputLabel": "var(--cdp-example-text-color)",
|
|
24
|
+
"colors-inputPlaceholder": "var(--cdp-example-text-secondary-color)",
|
|
25
|
+
"colors-inputFocusBorder": "var(--cdp-example-accent-color)",
|
|
26
|
+
"colors-linkText": "var(--cdp-example-accent-color)",
|
|
27
|
+
"colors-linkHover": "var(--cdp-example-accent-hover-color)",
|
|
28
|
+
"colors-linkSecondaryText": "var(--cdp-example-text-color)",
|
|
29
|
+
"colors-linkSecondaryHover": "var(--cdp-example-text-color)",
|
|
30
|
+
"fontFamily-sans": "var(--cdp-example-font-family)",
|
|
31
|
+
fontSizeBase: "var(--cdp-example-base-font-size)",
|
|
32
|
+
};
|