From c7527a07eb607e4cc57dad348af71f1b67302cc5 Mon Sep 17 00:00:00 2001 From: Adam Lessey Date: Fri, 28 Feb 2025 16:48:50 -0500 Subject: [PATCH] fix: add webpage to sign manifest --- account-manifest/.gitignore | 7 + account-manifest/README.md | 54 ++++++ account-manifest/eslint.config.js | 28 +++ account-manifest/index.html | 13 ++ account-manifest/package.json | 35 ++++ account-manifest/src/App.tsx | 26 +++ account-manifest/src/components/Page.tsx | 177 ++++++++++++++++++ account-manifest/src/components/Step.tsx | 20 ++ account-manifest/src/hooks/useGetFid.ts | 36 ++++ account-manifest/src/hooks/useSignManifest.ts | 72 +++++++ account-manifest/src/index.css | 37 ++++ account-manifest/src/main.tsx | 16 ++ account-manifest/src/utils.ts | 11 ++ account-manifest/src/vite-env.d.ts | 1 + account-manifest/tailwind.config.js | 11 ++ account-manifest/tsconfig.app.json | 26 +++ account-manifest/tsconfig.json | 7 + account-manifest/tsconfig.node.json | 24 +++ account-manifest/vite.config.ts | 7 + create-onchain/.gitignore | 2 + create-onchain/package.json | 16 +- create-onchain/src/cli.ts | 154 +++++++++++---- 22 files changed, 740 insertions(+), 40 deletions(-) create mode 100644 account-manifest/.gitignore create mode 100644 account-manifest/README.md create mode 100644 account-manifest/eslint.config.js create mode 100644 account-manifest/index.html create mode 100644 account-manifest/package.json create mode 100644 account-manifest/src/App.tsx create mode 100644 account-manifest/src/components/Page.tsx create mode 100644 account-manifest/src/components/Step.tsx create mode 100644 account-manifest/src/hooks/useGetFid.ts create mode 100644 account-manifest/src/hooks/useSignManifest.ts create mode 100644 account-manifest/src/index.css create mode 100644 account-manifest/src/main.tsx create mode 100644 account-manifest/src/utils.ts create mode 100644 account-manifest/src/vite-env.d.ts create mode 100644 account-manifest/tailwind.config.js create mode 100644 account-manifest/tsconfig.app.json create mode 100644 account-manifest/tsconfig.json create mode 100644 account-manifest/tsconfig.node.json create mode 100644 account-manifest/vite.config.ts create mode 100644 create-onchain/.gitignore diff --git a/account-manifest/.gitignore b/account-manifest/.gitignore new file mode 100644 index 0000000000..bc76babded --- /dev/null +++ b/account-manifest/.gitignore @@ -0,0 +1,7 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +dist + +# dependencies +node_modules diff --git a/account-manifest/README.md b/account-manifest/README.md new file mode 100644 index 0000000000..40ede56ea6 --- /dev/null +++ b/account-manifest/README.md @@ -0,0 +1,54 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config({ + extends: [ + // Remove ...tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config({ + plugins: { + // Add the react-x and react-dom plugins + 'react-x': reactX, + 'react-dom': reactDom, + }, + rules: { + // other rules... + // Enable its recommended typescript rules + ...reactX.configs['recommended-typescript'].rules, + ...reactDom.configs.recommended.rules, + }, +}) +``` diff --git a/account-manifest/eslint.config.js b/account-manifest/eslint.config.js new file mode 100644 index 0000000000..092408a9f0 --- /dev/null +++ b/account-manifest/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/account-manifest/index.html b/account-manifest/index.html new file mode 100644 index 0000000000..778e917ed2 --- /dev/null +++ b/account-manifest/index.html @@ -0,0 +1,13 @@ + + + + + + + Account Manifest Generator + + +
+ + + diff --git a/account-manifest/package.json b/account-manifest/package.json new file mode 100644 index 0000000000..307d6338e8 --- /dev/null +++ b/account-manifest/package.json @@ -0,0 +1,35 @@ +{ + "name": "account-manifest", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "copy": "cp -R ./dist/* ../create-onchain/src/manifest", + "build:copy": "npm run build && npm run copy", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@coinbase/onchainkit": "^0.37.5", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "wagmi": "^2.14.12" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@tailwindcss/vite": "^4.0.9", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "tailwindcss": "^4.0.9", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^6.2.0" + } +} diff --git a/account-manifest/src/App.tsx b/account-manifest/src/App.tsx new file mode 100644 index 0000000000..b4c5a30516 --- /dev/null +++ b/account-manifest/src/App.tsx @@ -0,0 +1,26 @@ +import { base } from 'wagmi/chains'; +import { OnchainKitProvider } from '@coinbase/onchainkit'; +import Page from './components/Page'; + +function App() { + return ( + + + + ) +} + +export default App diff --git a/account-manifest/src/components/Page.tsx b/account-manifest/src/components/Page.tsx new file mode 100644 index 0000000000..d76a3a1d5c --- /dev/null +++ b/account-manifest/src/components/Page.tsx @@ -0,0 +1,177 @@ +import { useEffect, useRef, useState } from 'react' +import { useAccount } from 'wagmi'; +import { + ConnectWallet, + Wallet, + WalletDropdown, + WalletDropdownDisconnect, +} from '@coinbase/onchainkit/wallet'; +import { + Address, + Avatar, + Name, + Identity, + EthBalance, +} from '@coinbase/onchainkit/identity'; +import { Step } from './Step'; +import { useGetFid } from '../hooks/useGetFid'; +import { validateUrl } from '../utils'; +import { useSignManifest } from '../hooks/useSignManifest'; + +function Page() { + const wsRef = useRef(null); + const [fid, setFid] = useState(null); + const [domain, setDomain] = useState(''); + const [domainError, setDomainError] = useState(null); + + const getFid = useGetFid(); + const { address } = useAccount(); + const { isPending, error, generateAccountAssociation } = useSignManifest({ + domain, + fid, + address, + onSigned: (accountAssociation) => { + wsRef.current?.send(JSON.stringify(accountAssociation)); + window.close(); + } + }); + + useEffect(() => { + wsRef.current = new WebSocket('ws://localhost:3333'); + + return () => { + wsRef.current?.close(); + } + }, []); + + useEffect(() => { + if (address) { + getFid(address).then(setFid); + } + }, [address, getFid]) + + useEffect(() => { + // super hacky way to remove the sign up button and 'or continue' div from the wallet modal + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: cause above + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.addedNodes.length) { + const modal = document.querySelector('[data-testid="ockModalOverlay"]'); + if (modal) { + const signUpButton = modal.querySelector('div > div.flex.w-full.flex-col.gap-3 > button:first-of-type'); + const orContinueDiv = modal.querySelector('div > div.flex.w-full.flex-col.gap-3 > div.relative'); + + if (signUpButton) { + signUpButton.style.display = 'none'; + } + if (orContinueDiv) { + orContinueDiv.style.display = 'none'; + } + } + } + }; + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + return () => observer.disconnect(); + }, []); + + const handleValidateUrl = () => { + const isValid = validateUrl(domain); + if (!isValid) { + setDomainError('Invalid URL'); + } + }; + + const handleDomainChange = (e: React.ChangeEvent) => { + setDomain(e.target.value); + setDomainError(null); + } + + return ( +
+ +

Use coinbase smart wallet if you have a passkey farcaster account through TBA

+

Use MetaMask or Phantom to set up a wallet using your warpcast recovery key

+ + } + > + + + + + + + + + +
+ + + + + + + + +

Enter the domain your app will be hosted on

+

This will be used to generate the account manifest and also added to your .env file as the `NEXT_PUBLIC_URL` variable

+ + } + > +
+ + {domainError &&

{domainError}

} +
+
+ + +

This will generate the account manifest and sign it with your wallet

+

The account manifest will be saved to your .env file as `FARCASTER_HEADER`, `FARCASTER_PAYLOAD` and `FARCASTER_SIGNATURE` variables

+ + } + > +
+ {fid === 0 + ?

There is no FID associated with this account, please connect with your TBA passkey account.

+ :

Your FID is {fid}

} + + + {error &&

{error.message.split('\n').map(line => {line}
)}

} +
+
+
+ ) +} + +export default Page; diff --git a/account-manifest/src/components/Step.tsx b/account-manifest/src/components/Step.tsx new file mode 100644 index 0000000000..aa30825dc6 --- /dev/null +++ b/account-manifest/src/components/Step.tsx @@ -0,0 +1,20 @@ +type StepProps = { + disabled?: boolean; + label: string; + children: React.ReactNode; + description?: React.ReactNode; +} + +export function Step({disabled, label, description, children}: StepProps) { + return ( +
+
+ {label} +
+
+ {children} +
+ {description &&
{description}
} +
+ ) +} \ No newline at end of file diff --git a/account-manifest/src/hooks/useGetFid.ts b/account-manifest/src/hooks/useGetFid.ts new file mode 100644 index 0000000000..6dc905d1b3 --- /dev/null +++ b/account-manifest/src/hooks/useGetFid.ts @@ -0,0 +1,36 @@ +import { http } from "viem"; +import { createPublicClient } from "viem"; +import type { Address } from "viem"; +import { optimism } from "viem/chains"; + +// ID Registry Contract +const ID_REGISTRY_ADDRESS = '0x00000000Fc6c5F01Fc30151999387Bb99A9f489b'; +const ID_REGISTRY_ABI = [ + { + "inputs": [{"internalType": "address", "name": "owner", "type": "address"}], + "name": "idOf", + "outputs": [{"internalType": "uint256", "name": "fid", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + } +]; + +export function useGetFid() { + return async function getFid(address: Address) { + const client = createPublicClient({ + chain: optimism, + transport: http(), + }); + + // query the ID Registry contract for the fids custody address + const resolvedFid = await client.readContract({ + address: ID_REGISTRY_ADDRESS, + abi: ID_REGISTRY_ABI, + functionName: 'idOf', + args: [address], + }); + + return Number(resolvedFid); + } +} + diff --git a/account-manifest/src/hooks/useSignManifest.ts b/account-manifest/src/hooks/useSignManifest.ts new file mode 100644 index 0000000000..0366800ebb --- /dev/null +++ b/account-manifest/src/hooks/useSignManifest.ts @@ -0,0 +1,72 @@ +// hooks/useAccountManifest.ts +import { useRef, useEffect } from 'react'; +import { useSignMessage } from 'wagmi'; +import { toBase64Url } from '../utils'; + +export type AccountAssociation = { + header: string; + payload: string; + signature: string; + domain: string; +} + +type SignManifestProps ={ + domain: string; + fid: number | null; + address: string | undefined; + onSigned: (accountAssociation: AccountAssociation) => void; +} + +export function useSignManifest({ domain, fid, address, onSigned }: SignManifestProps) { + const encodedHeader = useRef(''); + const encodedPayload = useRef(''); + const { data: signMessageData, isPending, error, signMessage } = useSignMessage(); + + useEffect(() => { + async function signManifest() { + if (signMessageData) { + const encodedSignature = toBase64Url(signMessageData); + const accountAssociation = { + header: encodedHeader.current, + payload: encodedPayload.current, + signature: encodedSignature, + domain: domain, + }; + + console.log('Account association generated:', accountAssociation); + onSigned(accountAssociation); + } + } + + signManifest(); + }, [signMessageData, domain, onSigned]); + + const generateAccountAssociation = async () => { + if (!domain || !fid || !address) { + console.error('Domain, FID and wallet connection are required'); + return; + } + + const header = { + fid, + type: "custody", + key: address, + }; + + const payload = { + domain: domain.replace(/^(http|https):\/\//, ''), + }; + + encodedHeader.current = toBase64Url(JSON.stringify(header)); + encodedPayload.current = toBase64Url(JSON.stringify(payload)); + const messageToSign = `${encodedHeader.current}.${encodedPayload.current}`; + + signMessage({message: messageToSign}); + }; + + return { + isPending, + error, + generateAccountAssociation + }; +} \ No newline at end of file diff --git a/account-manifest/src/index.css b/account-manifest/src/index.css new file mode 100644 index 0000000000..78db37f7cb --- /dev/null +++ b/account-manifest/src/index.css @@ -0,0 +1,37 @@ +@import "tailwindcss"; + +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +header, main { + max-width: 600px; + margin: 0 auto; +} \ No newline at end of file diff --git a/account-manifest/src/main.tsx b/account-manifest/src/main.tsx new file mode 100644 index 0000000000..fde6998ed1 --- /dev/null +++ b/account-manifest/src/main.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import '@coinbase/onchainkit/styles.css'; +import App from './App.tsx' + +const root = document.getElementById('root'); +if (!root) { + throw new Error('Root element not found'); +} + +createRoot(root).render( + + + , +) diff --git a/account-manifest/src/utils.ts b/account-manifest/src/utils.ts new file mode 100644 index 0000000000..5424a0897f --- /dev/null +++ b/account-manifest/src/utils.ts @@ -0,0 +1,11 @@ +export const toBase64Url = (str: string) => { + return btoa(str) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +}; + +export const validateUrl = (domain: string) => { + const urlRegex = /^https?:\/\/([\da-z.-]+)\.([a-z.]{2,6})(\/[\w.-]*(?: [\w.-]*)*)*\/?$/ + return domain.length === 0 || (domain.length > 0 && urlRegex.test(domain)); +}; \ No newline at end of file diff --git a/account-manifest/src/vite-env.d.ts b/account-manifest/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/account-manifest/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/account-manifest/tailwind.config.js b/account-manifest/tailwind.config.js new file mode 100644 index 0000000000..27a2e56a9a --- /dev/null +++ b/account-manifest/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ + +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/account-manifest/tsconfig.app.json b/account-manifest/tsconfig.app.json new file mode 100644 index 0000000000..358ca9ba93 --- /dev/null +++ b/account-manifest/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/account-manifest/tsconfig.json b/account-manifest/tsconfig.json new file mode 100644 index 0000000000..1ffef600d9 --- /dev/null +++ b/account-manifest/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/account-manifest/tsconfig.node.json b/account-manifest/tsconfig.node.json new file mode 100644 index 0000000000..db0becc8b0 --- /dev/null +++ b/account-manifest/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/account-manifest/vite.config.ts b/account-manifest/vite.config.ts new file mode 100644 index 0000000000..0616e59599 --- /dev/null +++ b/account-manifest/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [react(), tailwindcss()], +}) diff --git a/create-onchain/.gitignore b/create-onchain/.gitignore new file mode 100644 index 0000000000..324d32c940 --- /dev/null +++ b/create-onchain/.gitignore @@ -0,0 +1,2 @@ +src/manifest/* +!src/manifest/.gitkeep \ No newline at end of file diff --git a/create-onchain/package.json b/create-onchain/package.json index 9933d9c9b9..8d844de3e3 100644 --- a/create-onchain/package.json +++ b/create-onchain/package.json @@ -4,7 +4,8 @@ "version": "0.0.16", "license": "MIT", "scripts": { - "build": "bun run clean && bun run build:esm+types", + "build": "bun run clean && bun run build:esm+types && bun run build:manifest", + "build:manifest": "cd ../account-manifest && bun install && bun run build && cd ../create-onchain && cp -R ../account-manifest/dist/* ./src/manifest", "build:esm+types": "tsc --project tsconfig.build.json --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types", "check:types": "tsc --noEmit", "clean": "rm -rf dist tsconfig.tsbuildinfo", @@ -28,14 +29,17 @@ }, "dependencies": { "cac": "^6.7.14", - "cross-spawn": "^7.0.3", + "express": "^4.21.2", + "open": "^10.1.0", "ora": "^8.1.0", "picocolors": "^1.1.0", - "prompts": "^2.4.2" + "prompts": "^2.4.2", + "ws": "^8.18.1" }, "devDependencies": { - "@types/cross-spawn": "^6.0.6", + "@types/express": "^5.0.0", "@types/node": "^20.12.10", - "@types/prompts": "^2.4.9" + "@types/prompts": "^2.4.9", + "@types/ws": "^8.5.14" } -} \ No newline at end of file +} diff --git a/create-onchain/src/cli.ts b/create-onchain/src/cli.ts index 1d723c704b..9e8c0274e1 100644 --- a/create-onchain/src/cli.ts +++ b/create-onchain/src/cli.ts @@ -11,7 +11,10 @@ import { toValidPackageName, optimizedCopy, } from './utils.js'; -import { spawn } from 'child_process'; +import open from 'open'; +import express from 'express'; +import { createServer } from 'http'; +import { WebSocketServer, WebSocket } from 'ws'; const renameFiles: Record = { _gitignore: '.gitignore', @@ -41,6 +44,98 @@ async function copyDir(src: string, dest: string) { } } +type WebpageData = { header: string, payload: string, signature: string, domain: string }; + +async function getWebpageData(browser: 'safari' | 'google chrome' | 'none'): Promise { + const app = express(); + const server = createServer(app); + const wss = new WebSocketServer({ server }); + + app.use(express.static(path.resolve( + fileURLToPath(import.meta.url), + '../../../src/manifest' + ))); + + return new Promise((resolve, reject) => { + wss.on('connection', (ws: WebSocket) => { + ws.on('message', (data: Buffer) => { + const parsedData = JSON.parse(data.toString()); + server.close(); + resolve(parsedData); + }); + ws.on('close', () => { + server.close(); + reject(new Error('WebSocket connection closed')); + }); + }); + + server.listen(3333, () => { + open('http://localhost:3333', browser === 'none' ? undefined : { + app: { + name: browser + } + }); + }); + }); +} + +async function createMiniKitAccountAssociation(envPath?: string) { + if (!envPath) { + envPath = path.join(process.cwd(), '.env'); + } + + const existingEnv = await fs.promises.readFile(envPath, 'utf-8').catch(() => null); + if (!existingEnv) { + console.log(pc.red('\n* Failed to read .env file. Please ensure you are in your project directory.')); + return false; + } + + let browserResult: prompts.Answers<'browser'>; + try { + browserResult = await prompts( + [ + { + type: 'select', + name: 'browser', + message: pc.reset('If you want to sign your account manifest with your TBA account, please select the OS of your device:'), + choices: [ + { title: 'iOS', value: 'safari' }, + { title: 'Android', value: 'google chrome' }, + { title: 'Not using a passkey account', value: 'none' }, + ], + } + ], + { + onCancel: () => { + throw new Error('Browser selection cancelled.'); + }, + } + ); + } catch (cancelled: any) { + console.log(pc.red(`\n${cancelled.message}`)); + return false; + } + + const { browser } = browserResult; + try { + const webpageData = await getWebpageData(browser); + const envContent = `FARCASTER_HEADER=${webpageData.header}\nFARCASTER_PAYLOAD=${webpageData.payload}\nFARCASTER_SIGNATURE=${webpageData.signature}\nNEXT_PUBLIC_URL=${webpageData.domain}`; + const updatedEnv = existingEnv + .split('\n') + .filter(line => !line.startsWith('FARCASTER_') && !line.startsWith('NEXT_PUBLIC_URL')) + .concat(envContent) + .join('\n'); + await fs.promises.writeFile(envPath, updatedEnv); + + console.log(pc.blue('\n* Account association generated successfully and added to your .env file!')); + } catch (error) { + console.log(pc.red('\n* Failed to generate account association. Please try again.')); + return false; + } + + return true; +} + async function createMiniKitTemplate() { console.log( `${pc.greenBright(` @@ -153,10 +248,10 @@ REDIS_TOKEN=` spinner.succeed(); - console.log(`\n${pc.magenta(`Created new MiniKit project in ${root}`)}`); + console.log(`\n${pc.magenta(`Created new MiniKit project in ${root}`)}\n`); - console.log('\nWould you like to set up your Frames Account Association now?'); - console.log(pc.blue('* You can set this up later by running `npm run generate-account-association` or updating your `.env` file manually.\n')); + console.log(`\n${pc.reset('Do you want to set up your Frames Account Manifest now?')}`); + console.log(pc.blue('* You can run this later by running `npm create-onchain --generate` in your project directory.')); let setUpFrameResult: prompts.Answers<'setUpFrame'>; try { @@ -165,49 +260,30 @@ REDIS_TOKEN=` { type: 'toggle', name: 'setUpFrame', - message: pc.reset('Set up Frame integration now?'), + message: pc.reset('Set up now?'), initial: true, active: 'yes', inactive: 'no', - } + }, ], { onCancel: () => { console.log('\nSetup frame cancelled.'); - process.exit(1); + return false; }, } ); } catch (cancelled: any) { console.log(cancelled.message); - process.exit(1); + return false; } const { setUpFrame } = setUpFrameResult; if (setUpFrame) { - const scriptPath = path.resolve( - fileURLToPath(import.meta.url), - '../../../templates/minikit/scripts/generateAccountAssociation.mjs' - ); - - // spawn the generate-account-association command - const generateAccountAssociation = spawn('node', [scriptPath, root], { - stdio: 'inherit', - cwd: process.cwd(), - shell: true - }); - - generateAccountAssociation.on('close', (code: number) => { - if (code === 0) { - logMiniKitSetupSummary(projectName, root, clientKey); - } else { - console.error('Failed to generate account association'); - logMiniKitSetupSummary(projectName, root, clientKey); - } - }); - } else { - logMiniKitSetupSummary(projectName, root, clientKey); + await createMiniKitAccountAssociation(envPath); } + + logMiniKitSetupSummary(projectName, root, clientKey); } function logMiniKitSetupSummary(projectName: string, root: string, clientKey: string) { @@ -392,9 +468,6 @@ async function createOnchainKitTemplate() { async function init() { const isHelp = process.argv.some(arg => ['--help', '-h'].includes(arg)); - const isVersion = process.argv.some(arg => ['--version', '-v'].includes(arg)); - const isMinikit = process.argv.some(arg => ['--mini', '-m'].includes(arg)); - if (isHelp) { console.log( `${pc.greenBright(` @@ -406,19 +479,32 @@ Creates an OnchainKit project based on nextJs. Options: --version, -v: Show version --mini, -m: Create a MiniKit project +--generate, -g: Generate your Frames account association --help, -h: Show help `)}` ); process.exit(0); } + const isVersion = process.argv.some(arg => ['--version', '-v'].includes(arg)); if (isVersion) { - const packageJsonContent = fs.readFileSync('./package.json', 'utf8'); + const pkgPath = path.resolve( + fileURLToPath(import.meta.url), + '../../../package.json' + ); + const packageJsonContent = fs.readFileSync(pkgPath, 'utf8'); const packageJson = JSON.parse(packageJsonContent); console.log(`${pc.greenBright(`v${packageJson.version}`)}`); process.exit(0); } + const isGenerate = process.argv.some(arg => ['--generate', '-g'].includes(arg)); + if (isGenerate) { + await createMiniKitAccountAssociation(); + process.exit(0); + } + + const isMinikit = process.argv.some(arg => ['--mini', '-m'].includes(arg)); if (isMinikit) { await createMiniKitTemplate(); } else {