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 a99dff6
Show file tree
Hide file tree
Showing 24 changed files with 777 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 reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import globals from 'globals';
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 { OnchainKitProvider } from '@coinbase/onchainkit';
import { base } from 'wagmi/chains';
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;
214 changes: 214 additions & 0 deletions account-manifest/src/components/Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import {
Address,
Avatar,
EthBalance,
Identity,
Name,
} from '@coinbase/onchainkit/identity';
import {
ConnectWallet,
Wallet,
WalletDropdown,
WalletDropdownDisconnect,
} from '@coinbase/onchainkit/wallet';
import { useEffect, useRef, useState } from 'react';
import { useAccount } from 'wagmi';
import { useGetFid } from '../hooks/useGetFid';
import { useSignManifest } from '../hooks/useSignManifest';
import { validateUrl } from '../utils';
import { Step } from './Step';

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;
Loading

0 comments on commit a99dff6

Please sign in to comment.