Skip to content

Commit a0daa2a

Browse files
committed
add signup and managing spaces in connect app
1 parent 8cb57dc commit a0daa2a

File tree

127 files changed

+4166
-1235
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

127 files changed

+4166
-1235
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
pnpm install
99
cd apps/server
1010
cp .env.example .env
11+
# add the PRIVY_APP_SECRET & PRIVY_APP_ID to the apps/server/.env file
1112
pnpm prisma migrate dev
1213
```
1314

apps/connect/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "events",
2+
"name": "connect",
33
"private": true,
44
"version": "0.0.0",
55
"type": "module",
@@ -9,12 +9,15 @@
99
},
1010
"dependencies": {
1111
"@graphprotocol/grc-20": "^0.11.5",
12+
"@graphprotocol/hypergraph": "workspace:*",
13+
"@graphprotocol/hypergraph-react": "workspace:*",
1214
"@privy-io/react-auth": "^2.13.0",
1315
"@radix-ui/react-avatar": "^1.1.9",
1416
"@radix-ui/react-icons": "^1.3.2",
1517
"@radix-ui/react-slot": "^1.2.2",
1618
"@tanstack/react-query": "^5.75.5",
1719
"@tanstack/react-router": "^1.120.2",
20+
"@xstate/store": "^3.5.1",
1821
"class-variance-authority": "^0.7.1",
1922
"clsx": "^2.1.1",
2023
"effect": "^3.16.3",
@@ -24,6 +27,7 @@
2427
"react-dom": "^19.1.0",
2528
"tailwind-merge": "^3.2.0",
2629
"tailwindcss-animate": "^1.0.7",
30+
"viem": "^2.30.6",
2731
"vite": "^6.3.5"
2832
},
2933
"devDependencies": {

apps/connect/src/Boot.tsx

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import { Identity, store } from '@graphprotocol/hypergraph';
12
import { PrivyProvider } from '@privy-io/react-auth';
3+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
24
import { RouterProvider, createRouter } from '@tanstack/react-router';
5+
import { useLayoutEffect, useRef } from 'react';
6+
import { getAddress } from 'viem';
37
import { routeTree } from './routeTree.gen';
48

59
// Create a new router instance
@@ -12,22 +16,51 @@ declare module '@tanstack/react-router' {
1216
}
1317
}
1418

19+
const queryClient = new QueryClient();
20+
21+
const storage = localStorage;
22+
1523
export function Boot() {
24+
// check if the user is already authenticated on initial render
25+
const initialRenderAuthCheckRef = useRef(false);
26+
// using a layout effect to avoid a re-render
27+
useLayoutEffect(() => {
28+
if (!initialRenderAuthCheckRef.current) {
29+
const accountAddress = Identity.loadAccountAddress(storage);
30+
if (accountAddress) {
31+
const keys = Identity.loadKeys(storage, accountAddress);
32+
if (keys) {
33+
// user is already authenticated, set state
34+
store.send({
35+
type: 'setAuth',
36+
accountAddress: getAddress(accountAddress),
37+
sessionToken: 'dummy',
38+
keys,
39+
});
40+
}
41+
}
42+
// set render auth check to true so next potential rerender doesn't proc this
43+
initialRenderAuthCheckRef.current = true;
44+
}
45+
}, []);
46+
1647
return (
17-
<PrivyProvider
18-
appId="cm4wx6ziv00ngrmfjf9ik36iu"
19-
config={{
20-
loginMethods: ['email', 'wallet', 'google', 'twitter', 'github'],
21-
appearance: {
22-
theme: 'light',
23-
accentColor: '#676FFF',
24-
},
25-
embeddedWallets: {
26-
createOnLogin: 'users-without-wallets',
27-
},
28-
}}
29-
>
30-
<RouterProvider router={router} />
31-
</PrivyProvider>
48+
<QueryClientProvider client={queryClient}>
49+
<PrivyProvider
50+
appId="cmbhnmo1x000bla0mxudtd8z9"
51+
config={{
52+
loginMethods: ['email', 'google'],
53+
appearance: {
54+
theme: 'light',
55+
accentColor: '#676FFF',
56+
},
57+
embeddedWallets: {
58+
createOnLogin: 'users-without-wallets',
59+
},
60+
}}
61+
>
62+
<RouterProvider router={router} />
63+
</PrivyProvider>
64+
</QueryClientProvider>
3265
);
3366
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Key, type Messages, SpaceEvents, SpaceInfo, Utils, store } from '@graphprotocol/hypergraph';
2+
import { useIdentityToken } from '@privy-io/react-auth';
3+
import { useQueryClient } from '@tanstack/react-query';
4+
import { useSelector } from '@xstate/store/react';
5+
import { Effect } from 'effect';
6+
import { useState } from 'react';
7+
import { Spinner } from './spinner';
8+
import { Button } from './ui/button';
9+
import { Input } from './ui/input';
10+
11+
export function CreateSpace() {
12+
const [isLoading, setIsLoading] = useState(false);
13+
const [spaceName, setSpaceName] = useState('');
14+
const { identityToken } = useIdentityToken();
15+
const accountAddress = useSelector(store, (state) => state.context.accountAddress);
16+
const keys = useSelector(store, (state) => state.context.keys);
17+
const queryClient = useQueryClient();
18+
19+
const createSpace = async () => {
20+
setIsLoading(true);
21+
if (!accountAddress || !keys || !identityToken) {
22+
console.error('Missing required fields', { accountAddress, keys, identityToken });
23+
return;
24+
}
25+
26+
try {
27+
const { encryptionPrivateKey, encryptionPublicKey, signaturePrivateKey, signaturePublicKey } = keys;
28+
const spaceId = Utils.generateId();
29+
30+
const spaceEvent = await Effect.runPromise(
31+
SpaceEvents.createSpace({
32+
author: {
33+
accountAddress,
34+
encryptionPublicKey,
35+
signaturePrivateKey,
36+
signaturePublicKey,
37+
},
38+
spaceId,
39+
}),
40+
);
41+
const result = Key.createKey({
42+
privateKey: Utils.hexToBytes(encryptionPrivateKey),
43+
publicKey: Utils.hexToBytes(encryptionPublicKey),
44+
});
45+
46+
const { infoContent, signature } = SpaceInfo.encryptAndSignSpaceInfo({
47+
accountAddress,
48+
name: spaceName,
49+
secretKey: Utils.bytesToHex(result.key),
50+
signaturePrivateKey,
51+
spaceId,
52+
});
53+
54+
const message: Messages.RequestConnectCreateSpaceEvent = {
55+
type: 'connect-create-space-event',
56+
event: spaceEvent,
57+
spaceId: spaceEvent.transaction.id,
58+
keyBox: {
59+
accountAddress,
60+
ciphertext: Utils.bytesToHex(result.keyBoxCiphertext),
61+
nonce: Utils.bytesToHex(result.keyBoxNonce),
62+
authorPublicKey: encryptionPublicKey,
63+
id: Utils.generateId(),
64+
},
65+
infoContent: Utils.bytesToHex(infoContent),
66+
infoSignature: signature,
67+
name: spaceName,
68+
};
69+
70+
const response = await fetch('http://localhost:3030/connect/spaces', {
71+
headers: {
72+
'privy-id-token': identityToken,
73+
'Content-Type': 'application/json',
74+
},
75+
method: 'POST',
76+
body: JSON.stringify(message),
77+
});
78+
const data = await response.json();
79+
if (data.space) {
80+
queryClient.invalidateQueries({ queryKey: ['spaces'] });
81+
setSpaceName('');
82+
} else {
83+
throw new Error('Failed to create space');
84+
}
85+
} catch (error) {
86+
alert('Failed to create space');
87+
console.error(error);
88+
} finally {
89+
setIsLoading(false);
90+
}
91+
};
92+
93+
return (
94+
<>
95+
<Input value={spaceName} onChange={(e) => setSpaceName(e.target.value)} />
96+
<Button className="home-button" onClick={createSpace} disabled={isLoading}>
97+
Create Space {isLoading ? <Spinner /> : null}
98+
</Button>
99+
</>
100+
);
101+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { useIdentityToken } from '@privy-io/react-auth';
2+
import { useQuery } from '@tanstack/react-query';
3+
4+
export function Spaces() {
5+
const { identityToken } = useIdentityToken();
6+
7+
const { isPending, error, data } = useQuery({
8+
queryKey: ['spaces'],
9+
queryFn: async () => {
10+
if (!identityToken) return;
11+
const response = await fetch('http://localhost:3030/connect/spaces', {
12+
headers: { 'privy-id-token': identityToken },
13+
});
14+
return await response.json();
15+
},
16+
});
17+
18+
if (isPending) return 'Loading spaces …';
19+
20+
if (error) return `An error has occurred: ${error.message}`;
21+
22+
return (
23+
<>
24+
{data.spaces.map((space: { id: string; name: string }) => (
25+
<div key={space.id}>
26+
<h2>
27+
{space.name} ({space.id})
28+
</h2>
29+
</div>
30+
))}
31+
</>
32+
);
33+
}

apps/connect/src/routeTree.gen.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { createFileRoute } from '@tanstack/react-router'
1313
// Import Routes
1414

1515
import { Route as rootRoute } from './routes/__root'
16+
import { Route as AuthenticateImport } from './routes/authenticate'
1617
import { Route as IndexImport } from './routes/index'
1718

1819
// Create Virtual Routes
@@ -27,6 +28,12 @@ const LoginLazyRoute = LoginLazyImport.update({
2728
getParentRoute: () => rootRoute,
2829
} as any).lazy(() => import('./routes/login.lazy').then((d) => d.Route))
2930

31+
const AuthenticateRoute = AuthenticateImport.update({
32+
id: '/authenticate',
33+
path: '/authenticate',
34+
getParentRoute: () => rootRoute,
35+
} as any)
36+
3037
const IndexRoute = IndexImport.update({
3138
id: '/',
3239
path: '/',
@@ -44,6 +51,13 @@ declare module '@tanstack/react-router' {
4451
preLoaderRoute: typeof IndexImport
4552
parentRoute: typeof rootRoute
4653
}
54+
'/authenticate': {
55+
id: '/authenticate'
56+
path: '/authenticate'
57+
fullPath: '/authenticate'
58+
preLoaderRoute: typeof AuthenticateImport
59+
parentRoute: typeof rootRoute
60+
}
4761
'/login': {
4862
id: '/login'
4963
path: '/login'
@@ -58,36 +72,41 @@ declare module '@tanstack/react-router' {
5872

5973
export interface FileRoutesByFullPath {
6074
'/': typeof IndexRoute
75+
'/authenticate': typeof AuthenticateRoute
6176
'/login': typeof LoginLazyRoute
6277
}
6378

6479
export interface FileRoutesByTo {
6580
'/': typeof IndexRoute
81+
'/authenticate': typeof AuthenticateRoute
6682
'/login': typeof LoginLazyRoute
6783
}
6884

6985
export interface FileRoutesById {
7086
__root__: typeof rootRoute
7187
'/': typeof IndexRoute
88+
'/authenticate': typeof AuthenticateRoute
7289
'/login': typeof LoginLazyRoute
7390
}
7491

7592
export interface FileRouteTypes {
7693
fileRoutesByFullPath: FileRoutesByFullPath
77-
fullPaths: '/' | '/login'
94+
fullPaths: '/' | '/authenticate' | '/login'
7895
fileRoutesByTo: FileRoutesByTo
79-
to: '/' | '/login'
80-
id: '__root__' | '/' | '/login'
96+
to: '/' | '/authenticate' | '/login'
97+
id: '__root__' | '/' | '/authenticate' | '/login'
8198
fileRoutesById: FileRoutesById
8299
}
83100

84101
export interface RootRouteChildren {
85102
IndexRoute: typeof IndexRoute
103+
AuthenticateRoute: typeof AuthenticateRoute
86104
LoginLazyRoute: typeof LoginLazyRoute
87105
}
88106

89107
const rootRouteChildren: RootRouteChildren = {
90108
IndexRoute: IndexRoute,
109+
AuthenticateRoute: AuthenticateRoute,
91110
LoginLazyRoute: LoginLazyRoute,
92111
}
93112

@@ -102,12 +121,16 @@ export const routeTree = rootRoute
102121
"filePath": "__root.tsx",
103122
"children": [
104123
"/",
124+
"/authenticate",
105125
"/login"
106126
]
107127
},
108128
"/": {
109129
"filePath": "index.tsx"
110130
},
131+
"/authenticate": {
132+
"filePath": "authenticate.tsx"
133+
},
111134
"/login": {
112135
"filePath": "login.lazy.tsx"
113136
}

0 commit comments

Comments
 (0)