Skip to content

Commit

Permalink
fix: add webpage to sign manifest
Browse files Browse the repository at this point in the history
  • Loading branch information
alessey committed Mar 3, 2025
1 parent fe469e5 commit 6bf776f
Show file tree
Hide file tree
Showing 22 changed files with 740 additions and 40 deletions.
7 changes: 7 additions & 0 deletions account-manifest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.

# compiled output
dist

# dependencies
node_modules
54 changes: 54 additions & 0 deletions account-manifest/README.md
Original file line number Diff line number Diff line change
@@ -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,
},
})
```
28 changes: 28 additions & 0 deletions account-manifest/eslint.config.js
Original file line number Diff line number Diff line change
@@ -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 },
],
},
},
)
13 changes: 13 additions & 0 deletions account-manifest/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="http://docs.base.org/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Account Manifest Generator</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
35 changes: 35 additions & 0 deletions account-manifest/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
26 changes: 26 additions & 0 deletions account-manifest/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { base } from 'wagmi/chains';
import { OnchainKitProvider } from '@coinbase/onchainkit';
import Page from './components/Page';

function App() {
return (
<OnchainKitProvider
chain={base}
config={{
appearance: {
name: 'Account Manifest Generator',
logo: 'https://pbs.twimg.com/media/GkXUnEnaoAIkKvG?format=jpg&name=medium',
mode: 'auto',
theme: 'base',
},
wallet: {
display: 'modal',
}
}}
>
<Page />
</OnchainKitProvider>
)
}

export default App
177 changes: 177 additions & 0 deletions account-manifest/src/components/Page.tsx
Original file line number Diff line number Diff line change
@@ -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<WebSocket | null>(null);
const [fid, setFid] = useState<number | null>(null);
const [domain, setDomain] = useState<string>('');
const [domainError, setDomainError] = useState<string | null>(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<HTMLElement>('div > div.flex.w-full.flex-col.gap-3 > button:first-of-type');
const orContinueDiv = modal.querySelector<HTMLElement>('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<HTMLInputElement>) => {
setDomain(e.target.value);
setDomainError(null);
}

return (
<main className='flex min-h-screen w-[600px] flex-col gap-6 font-sans'>
<Step
label='1'
description={
<>
<p className='p-4'>Use coinbase smart wallet if you have a passkey farcaster account through TBA</p>
<p className='p-4'>Use MetaMask or Phantom to set up a wallet using your warpcast recovery key</p>
</>
}
>
<Wallet className='w-[206px]'>
<ConnectWallet className='w-full'>
<Avatar className="h-6 w-6" />
<Name />
</ConnectWallet>
<WalletDropdown>
<Identity className="px-4 pt-3 pb-2" hasCopyAddressOnClick={true}>
<Avatar />
<Name />
<Address />
<EthBalance />
</Identity>
<WalletDropdownDisconnect />
</WalletDropdown>
</Wallet>
</Step>

<Step
label='2'
disabled={!address}
description={
<>
<p className='p-4'>Enter the domain your app will be hosted on</p>
<p className='p-4'>This will be used to generate the account manifest and also added to your .env file as the `NEXT_PUBLIC_URL` variable</p>
</>
}
>
<div className='flex flex-col gap-2'>
<input type="text"
placeholder="Enter Domain"
className='rounded border border-gray-300 px-4 py-2'
value={domain}
onChange={handleDomainChange}
onBlur={handleValidateUrl}
/>
{domainError && <p className='text-red-500'>{domainError}</p>}
</div>
</Step>

<Step
label='3'
disabled={!address || !domain || fid === 0}
description={
<>
<p className='p-4'>This will generate the account manifest and sign it with your wallet</p>
<p className='p-4'>The account manifest will be saved to your .env file as `FARCASTER_HEADER`, `FARCASTER_PAYLOAD` and `FARCASTER_SIGNATURE` variables</p>
</>
}
>
<div className='flex flex-col gap-2'>
{fid === 0
? <p className='text-red-500'>There is no FID associated with this account, please connect with your TBA passkey account.</p>
: <p>Your FID is {fid}</p>}

<button
type="button"
disabled={!address || !domain || fid === 0}
onClick={generateAccountAssociation}
className={`rounded px-4 py-2 text-white ${
!address || !domain || fid === 0 ? 'bg-blue-200!' : 'bg-blue-800!'
}`}
>
{isPending ? 'Signing...' : 'Sign Account Manifest'}
</button>
{error && <p className='text-red-500'>{error.message.split('\n').map(line => <span key={line}>{line}<br /></span>)}</p>}
</div>
</Step>
</main>
)
}

export default Page;
20 changes: 20 additions & 0 deletions account-manifest/src/components/Step.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`flex w-full items-center gap-4 ${disabled ? 'opacity-50' : ''}`}>
<div className='flex h-[50px] w-[50px] shrink-0 items-center justify-center rounded-full border-2 border-gray-300 p-4'>
{label}
</div>
<div className='flex-shrink-0 flex-grow-0 basis-[206px]'>
{children}
</div>
{description && <div className='text-gray-500 text-sm'>{description}</div>}
</div>
)
}
Loading

0 comments on commit 6bf776f

Please sign in to comment.