@coinbase/create-cdp-app 0.0.6 → 0.0.8
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/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/template-react-components/index.html +1 -1
- package/template-react-components/public/eth.svg +25 -0
- package/template-react-components/src/App.tsx +9 -8
- 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 +87 -43
- 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/template-react-components/pnpm-lock.yaml +0 -3116
package/dist/index.js
CHANGED
|
@@ -170,8 +170,9 @@ function printNextSteps(projectRoot) {
|
|
|
170
170
|
if (projectRoot !== process.cwd()) {
|
|
171
171
|
console.log(`cd ${path.relative(process.cwd(), projectRoot)}`);
|
|
172
172
|
}
|
|
173
|
+
const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
|
|
173
174
|
console.log(`${packageManager} install`);
|
|
174
|
-
console.log(
|
|
175
|
+
console.log(devCommand);
|
|
175
176
|
}
|
|
176
177
|
function copyTemplateFiles(templateDir, root, projectName, projectId) {
|
|
177
178
|
const writeFileToTarget = (file, content) => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Prepare the project directory\n *\n * @param targetDir - The target directory for the project\n * @param shouldOverwrite - Whether to overwrite the existing directory\n * @returns The path to the prepared project directory\n */\nexport function prepareProjectDirectory(targetDir: string, shouldOverwrite: boolean): string {\n const root = path.join(process.cwd(), targetDir);\n\n if (shouldOverwrite) {\n emptyDir(root);\n } else if (!fs.existsSync(root)) {\n fs.mkdirSync(root, { recursive: true });\n }\n\n return root;\n}\n\n/**\n * Customize package.json for the new project\n *\n * @param templateDir - The directory containing the template files\n * @param projectName - The name of the project\n * @returns The customized package.json content\n */\nexport function customizePackageJson(templateDir: string, projectName: string): string {\n const packageJsonPath = path.join(templateDir, \"package.json\");\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, \"utf-8\"));\n packageJson.name = projectName;\n return JSON.stringify(packageJson, null, 2) + \"\\n\";\n}\n\n/**\n * Set up the .env file for the new project\n *\n * @param templateDir - The directory containing the template files\n * @param projectId - The project ID\n * @returns The customized .env content\n */\nexport function customizeEnv(templateDir: string, projectId: string): string {\n const exampleEnvPath = path.join(templateDir, \"env.example\");\n const exampleEnv = fs.readFileSync(exampleEnvPath, \"utf-8\");\n const envContent = exampleEnv.replace(\n /VITE_CDP_PROJECT_ID=.*(\\r?\\n|$)/,\n `VITE_CDP_PROJECT_ID=${projectId}\\n`,\n );\n return envContent;\n}\n\n/**\n * Copy a file or directory recursively\n *\n * @param filePath - The source path\n * @param destPath - The destination path\n */\nexport function copyFile(filePath: string, destPath: string): void {\n const stat = fs.statSync(filePath);\n if (stat.isDirectory()) {\n copyDir(filePath, destPath);\n } else {\n fs.copyFileSync(filePath, destPath);\n }\n}\n\n/**\n * Copy a directory recursively\n *\n * @param srcDir - The source directory path\n * @param destDir - The destination directory path\n */\nfunction copyDir(srcDir: string, destDir: string): void {\n fs.mkdirSync(destDir, { recursive: true });\n for (const file of fs.readdirSync(srcDir)) {\n const srcFile = path.resolve(srcDir, file);\n const destFile = path.resolve(destDir, file);\n copyFile(srcFile, destFile);\n }\n}\n\n/**\n * Check if a directory is empty\n *\n * @param dirPath - The path to the directory\n * @returns True if the directory is empty, false otherwise\n */\nexport function isDirEmpty(dirPath: string): boolean {\n const files = fs.readdirSync(dirPath);\n return files.length === 0 || (files.length === 1 && files[0] === \".git\");\n}\n\n/**\n * Empty a directory while preserving .git\n *\n * @param dirPath - The path to the directory\n */\nfunction emptyDir(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n return;\n }\n for (const file of fs.readdirSync(dirPath)) {\n if (file === \".git\") {\n continue;\n }\n fs.rmSync(path.resolve(dirPath, file), { recursive: true, force: true });\n }\n}\n\n/**\n * Detect which package manager invoked the create command\n *\n * @returns The detected package manager or 'pnpm' as default\n */\nexport function detectPackageManager(): \"npm\" | \"pnpm\" | \"yarn\" {\n const userAgent = process.env.npm_config_user_agent;\n\n if (userAgent) {\n if (userAgent.startsWith(\"yarn\")) return \"yarn\";\n if (userAgent.startsWith(\"pnpm\")) return \"pnpm\";\n if (userAgent.startsWith(\"npm\")) return \"npm\";\n }\n\n return \"npm\"; // Default to npm if we can't detect\n}\n","#!/usr/bin/env node\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { blue, red, green, reset } from \"kolorist\";\nimport prompts from \"prompts\";\n\nimport {\n prepareProjectDirectory,\n isDirEmpty,\n customizePackageJson,\n copyFile,\n customizeEnv,\n detectPackageManager,\n} from \"./utils.js\";\n\n// Available templates for project creation\nconst TEMPLATES = [\n {\n name: \"react-components\",\n display: \"React Components\",\n color: blue,\n },\n];\n\nconst defaultTargetDir = \"cdp-app\";\n\nconst fileRenames: Record<string, string | undefined> = {\n _gitignore: \".gitignore\",\n};\n\ninterface ProjectOptions {\n projectName: string;\n template: string;\n targetDirectory: string;\n projectId: string;\n}\n\nconst uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Initialize a new CDP app project\n */\nasync function init(): Promise<void> {\n const { projectName, template, targetDirectory, projectId } = await getProjectDetails();\n\n console.log(`\\nScaffolding project in ${targetDirectory}...`);\n\n const root = prepareProjectDirectory(targetDirectory, false);\n const templateDir = path.resolve(fileURLToPath(import.meta.url), \"../..\", `template-${template}`);\n\n copyTemplateFiles(templateDir, root, projectName, projectId);\n printNextSteps(root);\n}\n\n/**\n * Get project details from command line arguments or prompt the user\n *\n * @returns The project details\n */\nasync function getProjectDetails(): Promise<ProjectOptions> {\n // Get target directory from command line args (first non-option argument)\n let targetDir = process.argv[2];\n const defaultProjectName = targetDir ?? defaultTargetDir;\n\n try {\n const result = await prompts(\n [\n {\n type: targetDir ? null : \"text\",\n name: \"projectName\",\n message: reset(\"Project name:\"),\n initial: defaultProjectName,\n onState: state => {\n targetDir = String(state.value).trim() || defaultProjectName;\n },\n },\n {\n type: \"select\",\n name: \"template\",\n message: reset(\"Select a template:\"),\n initial: 0,\n choices: TEMPLATES.map(template => ({\n title: template.color(template.display || template.name),\n value: template.name,\n })),\n },\n {\n type: \"text\",\n name: \"projectId\",\n message: reset(\n \"CDP Project ID (Find your project ID at https://portal.cdp.coinbase.com/projects/overview):\",\n ),\n validate: value => {\n if (!value || !uuidRegex.test(value)) {\n return \"Project ID is required\";\n }\n return true;\n },\n initial: \"\",\n },\n {\n type: \"text\",\n name: \"corsConfirmation\",\n message: reset(\n \"Confirm you have whitelisted 'http://localhost:3000' at https://portal.cdp.coinbase.com/products/embedded-wallets/cors by typing 'y'\",\n ),\n validate: value => {\n if (value !== \"y\") {\n return \"You must whitelist your app domain for your app to be functional.\";\n }\n return true;\n },\n initial: \"\",\n },\n {\n type: () => (!fs.existsSync(targetDir) || isDirEmpty(targetDir) ? null : \"confirm\"),\n name: \"overwrite\",\n message: () =>\n (targetDir === \".\" ? \"Current directory\" : `Target directory \"${targetDir}\"`) +\n \" is not empty. Remove existing files and continue?\",\n },\n {\n type: (_, { overwrite }: { overwrite?: boolean }) => {\n if (overwrite === false) {\n throw new Error(red(\"✖\") + \" Operation cancelled\");\n }\n return null;\n },\n name: \"overwriteChecker\",\n },\n ],\n {\n onCancel: () => {\n throw new Error(red(\"✖\") + \" Operation cancelled\");\n },\n },\n );\n\n return {\n projectName: result.projectName,\n template: result.template,\n targetDirectory: targetDir,\n projectId: result.projectId,\n };\n } catch (cancelled: unknown) {\n if (cancelled instanceof Error) {\n console.log(cancelled.message);\n }\n process.exit(0);\n }\n}\n\n/**\n * Print next steps for the user\n *\n * @param projectRoot - The root directory of the project\n */\nfunction printNextSteps(projectRoot: string): void {\n const packageManager = detectPackageManager();\n\n console.log(green(\"\\nDone. Now run your app:\\n\"));\n if (projectRoot !== process.cwd()) {\n console.log(`cd ${path.relative(process.cwd(), projectRoot)}`);\n }\n console.log(`${packageManager} install`);\n console.log(`${packageManager} dev`);\n}\n\n/**\n * Copy template files to the project directory\n *\n * @param templateDir - The directory containing the template files\n * @param root - The root directory of the project\n * @param projectName - The name of the project\n * @param projectId - The project ID\n */\nfunction copyTemplateFiles(\n templateDir: string,\n root: string,\n projectName: string,\n projectId?: string,\n): void {\n const writeFileToTarget = (file: string, content?: string) => {\n const targetPath = path.join(root, fileRenames[file] ?? file);\n if (content) {\n fs.writeFileSync(targetPath, content);\n } else {\n copyFile(path.join(templateDir, file), targetPath);\n }\n };\n\n const files = fs.readdirSync(templateDir);\n for (const file of files) {\n if (file === \"package.json\") {\n const customizedPackageJson = customizePackageJson(templateDir, projectName);\n writeFileToTarget(file, customizedPackageJson);\n } else if (file === \"env.example\" && projectId) {\n const customizedEnv = customizeEnv(templateDir, projectId);\n writeFileToTarget(file);\n console.log(\"Copying project id to .env\");\n writeFileToTarget(\".env\", customizedEnv);\n } else {\n writeFileToTarget(file);\n }\n }\n}\n\ninit().catch(e => {\n console.error(e);\n process.exit(1);\n});\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Prepare the project directory\n *\n * @param targetDir - The target directory for the project\n * @param shouldOverwrite - Whether to overwrite the existing directory\n * @returns The path to the prepared project directory\n */\nexport function prepareProjectDirectory(targetDir: string, shouldOverwrite: boolean): string {\n const root = path.join(process.cwd(), targetDir);\n\n if (shouldOverwrite) {\n emptyDir(root);\n } else if (!fs.existsSync(root)) {\n fs.mkdirSync(root, { recursive: true });\n }\n\n return root;\n}\n\n/**\n * Customize package.json for the new project\n *\n * @param templateDir - The directory containing the template files\n * @param projectName - The name of the project\n * @returns The customized package.json content\n */\nexport function customizePackageJson(templateDir: string, projectName: string): string {\n const packageJsonPath = path.join(templateDir, \"package.json\");\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, \"utf-8\"));\n packageJson.name = projectName;\n return JSON.stringify(packageJson, null, 2) + \"\\n\";\n}\n\n/**\n * Set up the .env file for the new project\n *\n * @param templateDir - The directory containing the template files\n * @param projectId - The project ID\n * @returns The customized .env content\n */\nexport function customizeEnv(templateDir: string, projectId: string): string {\n const exampleEnvPath = path.join(templateDir, \"env.example\");\n const exampleEnv = fs.readFileSync(exampleEnvPath, \"utf-8\");\n const envContent = exampleEnv.replace(\n /VITE_CDP_PROJECT_ID=.*(\\r?\\n|$)/,\n `VITE_CDP_PROJECT_ID=${projectId}\\n`,\n );\n return envContent;\n}\n\n/**\n * Copy a file or directory recursively\n *\n * @param filePath - The source path\n * @param destPath - The destination path\n */\nexport function copyFile(filePath: string, destPath: string): void {\n const stat = fs.statSync(filePath);\n if (stat.isDirectory()) {\n copyDir(filePath, destPath);\n } else {\n fs.copyFileSync(filePath, destPath);\n }\n}\n\n/**\n * Copy a directory recursively\n *\n * @param srcDir - The source directory path\n * @param destDir - The destination directory path\n */\nfunction copyDir(srcDir: string, destDir: string): void {\n fs.mkdirSync(destDir, { recursive: true });\n for (const file of fs.readdirSync(srcDir)) {\n const srcFile = path.resolve(srcDir, file);\n const destFile = path.resolve(destDir, file);\n copyFile(srcFile, destFile);\n }\n}\n\n/**\n * Check if a directory is empty\n *\n * @param dirPath - The path to the directory\n * @returns True if the directory is empty, false otherwise\n */\nexport function isDirEmpty(dirPath: string): boolean {\n const files = fs.readdirSync(dirPath);\n return files.length === 0 || (files.length === 1 && files[0] === \".git\");\n}\n\n/**\n * Empty a directory while preserving .git\n *\n * @param dirPath - The path to the directory\n */\nfunction emptyDir(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n return;\n }\n for (const file of fs.readdirSync(dirPath)) {\n if (file === \".git\") {\n continue;\n }\n fs.rmSync(path.resolve(dirPath, file), { recursive: true, force: true });\n }\n}\n\n/**\n * Detect which package manager invoked the create command\n *\n * @returns The detected package manager or 'pnpm' as default\n */\nexport function detectPackageManager(): \"npm\" | \"pnpm\" | \"yarn\" {\n const userAgent = process.env.npm_config_user_agent;\n\n if (userAgent) {\n if (userAgent.startsWith(\"yarn\")) return \"yarn\";\n if (userAgent.startsWith(\"pnpm\")) return \"pnpm\";\n if (userAgent.startsWith(\"npm\")) return \"npm\";\n }\n\n return \"npm\"; // Default to npm if we can't detect\n}\n","#!/usr/bin/env node\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { blue, red, green, reset } from \"kolorist\";\nimport prompts from \"prompts\";\n\nimport {\n prepareProjectDirectory,\n isDirEmpty,\n customizePackageJson,\n copyFile,\n customizeEnv,\n detectPackageManager,\n} from \"./utils.js\";\n\n// Available templates for project creation\nconst TEMPLATES = [\n {\n name: \"react-components\",\n display: \"React Components\",\n color: blue,\n },\n];\n\nconst defaultTargetDir = \"cdp-app\";\n\nconst fileRenames: Record<string, string | undefined> = {\n _gitignore: \".gitignore\",\n};\n\ninterface ProjectOptions {\n projectName: string;\n template: string;\n targetDirectory: string;\n projectId: string;\n}\n\nconst uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Initialize a new CDP app project\n */\nasync function init(): Promise<void> {\n const { projectName, template, targetDirectory, projectId } = await getProjectDetails();\n\n console.log(`\\nScaffolding project in ${targetDirectory}...`);\n\n const root = prepareProjectDirectory(targetDirectory, false);\n const templateDir = path.resolve(fileURLToPath(import.meta.url), \"../..\", `template-${template}`);\n\n copyTemplateFiles(templateDir, root, projectName, projectId);\n printNextSteps(root);\n}\n\n/**\n * Get project details from command line arguments or prompt the user\n *\n * @returns The project details\n */\nasync function getProjectDetails(): Promise<ProjectOptions> {\n // Get target directory from command line args (first non-option argument)\n let targetDir = process.argv[2];\n const defaultProjectName = targetDir ?? defaultTargetDir;\n\n try {\n const result = await prompts(\n [\n {\n type: targetDir ? null : \"text\",\n name: \"projectName\",\n message: reset(\"Project name:\"),\n initial: defaultProjectName,\n onState: state => {\n targetDir = String(state.value).trim() || defaultProjectName;\n },\n },\n {\n type: \"select\",\n name: \"template\",\n message: reset(\"Select a template:\"),\n initial: 0,\n choices: TEMPLATES.map(template => ({\n title: template.color(template.display || template.name),\n value: template.name,\n })),\n },\n {\n type: \"text\",\n name: \"projectId\",\n message: reset(\n \"CDP Project ID (Find your project ID at https://portal.cdp.coinbase.com/projects/overview):\",\n ),\n validate: value => {\n if (!value || !uuidRegex.test(value)) {\n return \"Project ID is required\";\n }\n return true;\n },\n initial: \"\",\n },\n {\n type: \"text\",\n name: \"corsConfirmation\",\n message: reset(\n \"Confirm you have whitelisted 'http://localhost:3000' at https://portal.cdp.coinbase.com/products/embedded-wallets/cors by typing 'y'\",\n ),\n validate: value => {\n if (value !== \"y\") {\n return \"You must whitelist your app domain for your app to be functional.\";\n }\n return true;\n },\n initial: \"\",\n },\n {\n type: () => (!fs.existsSync(targetDir) || isDirEmpty(targetDir) ? null : \"confirm\"),\n name: \"overwrite\",\n message: () =>\n (targetDir === \".\" ? \"Current directory\" : `Target directory \"${targetDir}\"`) +\n \" is not empty. Remove existing files and continue?\",\n },\n {\n type: (_, { overwrite }: { overwrite?: boolean }) => {\n if (overwrite === false) {\n throw new Error(red(\"✖\") + \" Operation cancelled\");\n }\n return null;\n },\n name: \"overwriteChecker\",\n },\n ],\n {\n onCancel: () => {\n throw new Error(red(\"✖\") + \" Operation cancelled\");\n },\n },\n );\n\n return {\n projectName: result.projectName,\n template: result.template,\n targetDirectory: targetDir,\n projectId: result.projectId,\n };\n } catch (cancelled: unknown) {\n if (cancelled instanceof Error) {\n console.log(cancelled.message);\n }\n process.exit(0);\n }\n}\n\n/**\n * Print next steps for the user\n *\n * @param projectRoot - The root directory of the project\n */\nfunction printNextSteps(projectRoot: string): void {\n const packageManager = detectPackageManager();\n\n console.log(green(\"\\nDone. Now run your app:\\n\"));\n if (projectRoot !== process.cwd()) {\n console.log(`cd ${path.relative(process.cwd(), projectRoot)}`);\n }\n const devCommand = packageManager === \"npm\" ? \"npm run dev\" : `${packageManager} dev`;\n console.log(`${packageManager} install`);\n console.log(devCommand);\n}\n\n/**\n * Copy template files to the project directory\n *\n * @param templateDir - The directory containing the template files\n * @param root - The root directory of the project\n * @param projectName - The name of the project\n * @param projectId - The project ID\n */\nfunction copyTemplateFiles(\n templateDir: string,\n root: string,\n projectName: string,\n projectId?: string,\n): void {\n const writeFileToTarget = (file: string, content?: string) => {\n const targetPath = path.join(root, fileRenames[file] ?? file);\n if (content) {\n fs.writeFileSync(targetPath, content);\n } else {\n copyFile(path.join(templateDir, file), targetPath);\n }\n };\n\n const files = fs.readdirSync(templateDir);\n for (const file of files) {\n if (file === \"package.json\") {\n const customizedPackageJson = customizePackageJson(templateDir, projectName);\n writeFileToTarget(file, customizedPackageJson);\n } else if (file === \"env.example\" && projectId) {\n const customizedEnv = customizeEnv(templateDir, projectId);\n writeFileToTarget(file);\n console.log(\"Copying project id to .env\");\n writeFileToTarget(\".env\", customizedEnv);\n } else {\n writeFileToTarget(file);\n }\n }\n}\n\ninit().catch(e => {\n console.error(e);\n process.exit(1);\n});\n"],"names":[],"mappings":"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"}
|
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>
|
|
@@ -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,23 +1,24 @@
|
|
|
1
1
|
import { useIsInitialized, useIsSignedIn } from "@coinbase/cdp-hooks";
|
|
2
|
-
import { AuthButton } from "@coinbase/cdp-react";
|
|
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
|
+
* It also demonstrates how to use the AuthButton component to sign in and out of the app.
|
|
8
10
|
*/
|
|
9
11
|
function App() {
|
|
10
12
|
const isInitialized = useIsInitialized();
|
|
11
13
|
const isSignedIn = useIsSignedIn();
|
|
12
14
|
|
|
13
15
|
return (
|
|
14
|
-
<div className="app">
|
|
15
|
-
<
|
|
16
|
-
{!isInitialized && <div>Loading...</div>}
|
|
16
|
+
<div className="app flex-col-container flex-grow">
|
|
17
|
+
{!isInitialized && <Loading />}
|
|
17
18
|
{isInitialized && (
|
|
18
19
|
<>
|
|
19
|
-
<
|
|
20
|
-
{isSignedIn && <
|
|
20
|
+
{!isSignedIn && <SignInScreen />}
|
|
21
|
+
{isSignedIn && <SignedInScreen />}
|
|
21
22
|
</>
|
|
22
23
|
)}
|
|
23
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
|
/**
|
|
12
|
+
* This component demonstrates how to send an EVM transaction using the CDP hooks.
|
|
5
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,15 +34,13 @@ function Transaction() {
|
|
|
17
34
|
}
|
|
18
35
|
|
|
19
36
|
e.preventDefault();
|
|
37
|
+
setIsPending(true);
|
|
20
38
|
|
|
21
39
|
const { transactionHash } = await sendEvmTransaction({
|
|
22
40
|
transaction: {
|
|
23
41
|
to: evmAddress, // Send to yourself for testing
|
|
24
|
-
value:
|
|
25
|
-
nonce: 0,
|
|
42
|
+
value: 1000000000000n, // 0.000001 ETH in wei
|
|
26
43
|
gas: 21000n,
|
|
27
|
-
maxFeePerGas: 30000000000n,
|
|
28
|
-
maxPriorityFeePerGas: 1000000000n,
|
|
29
44
|
chainId: 84532, // Base Sepolia
|
|
30
45
|
type: "eip1559",
|
|
31
46
|
},
|
|
@@ -34,51 +49,80 @@ function Transaction() {
|
|
|
34
49
|
});
|
|
35
50
|
|
|
36
51
|
setTransactionHash(transactionHash);
|
|
52
|
+
setIsPending(false);
|
|
53
|
+
onSuccess?.();
|
|
37
54
|
},
|
|
38
|
-
[evmAddress, sendEvmTransaction],
|
|
55
|
+
[evmAddress, sendEvmTransaction, onSuccess],
|
|
39
56
|
);
|
|
40
57
|
|
|
41
58
|
return (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<span className="wallet-address">{evmAddress}</span>
|
|
45
|
-
<p>
|
|
46
|
-
Get testnet ETH from{" "}
|
|
47
|
-
<a
|
|
48
|
-
href="https://portal.cdp.coinbase.com/products/faucet"
|
|
49
|
-
target="_blank"
|
|
50
|
-
rel="noopener noreferrer"
|
|
51
|
-
>
|
|
52
|
-
Base Sepolia Faucet
|
|
53
|
-
</a>
|
|
54
|
-
</p>
|
|
55
|
-
{!transactionHash && (
|
|
59
|
+
<>
|
|
60
|
+
{balance === undefined && (
|
|
56
61
|
<>
|
|
57
|
-
<h2>Send
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
</button>
|
|
62
|
+
<h2 className="card-title">Send a transaction</h2>
|
|
63
|
+
<LoadingSkeleton className="loading--text" />
|
|
64
|
+
<LoadingSkeleton className="loading--btn" />
|
|
61
65
|
</>
|
|
62
66
|
)}
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
</>
|
|
80
124
|
)}
|
|
81
|
-
|
|
125
|
+
</>
|
|
82
126
|
);
|
|
83
127
|
}
|
|
84
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;
|