Skip to content

Commit 7eb42f9

Browse files
committed
select private and public spaces
1 parent 91fa874 commit 7eb42f9

File tree

10 files changed

+212
-75
lines changed

10 files changed

+212
-75
lines changed

apps/connect/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"clsx": "^2.1.1",
2222
"effect": "^3.16.3",
2323
"framer-motion": "^12.10.1",
24+
"graphql-request": "^7.2.0",
2425
"lucide-react": "^0.508.0",
2526
"react": "^19.1.0",
2627
"react-dom": "^19.1.0",

apps/connect/src/components/spaces.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useSpaces } from '@/hooks/use-spaces';
1+
import { usePrivateSpaces } from '@/hooks/use-private-spaces';
22

33
export function Spaces() {
4-
const { isPending, error, data } = useSpaces();
4+
const { isPending, error, data } = usePrivateSpaces();
55

66
return (
77
<div>

apps/connect/src/hooks/use-spaces.ts renamed to apps/connect/src/hooks/use-private-spaces.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getAppInfoByIds } from '@/lib/get-app-info-by-ids';
22
import { Connect } from '@graphprotocol/hypergraph';
33
import { useIdentityToken } from '@privy-io/react-auth';
4-
import { useQuery } from '@tanstack/react-query';
4+
import { type UseQueryResult, useQuery } from '@tanstack/react-query';
55

66
type SpaceData = {
77
id: string;
@@ -16,11 +16,11 @@ type SpaceData = {
1616
}[];
1717
};
1818

19-
export const useSpaces = () => {
19+
export const usePrivateSpaces = (): UseQueryResult<SpaceData[], Error> => {
2020
const { identityToken } = useIdentityToken();
2121

2222
return useQuery<SpaceData[]>({
23-
queryKey: ['spaces'],
23+
queryKey: ['private-spaces'],
2424
queryFn: async () => {
2525
if (!identityToken) return [];
2626
const accountAddress = Connect.loadAccountAddress(localStorage);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Connect } from '@graphprotocol/hypergraph';
2+
import { type UseQueryResult, useQuery } from '@tanstack/react-query';
3+
import { gql, request } from 'graphql-request';
4+
5+
const publicSpacesQueryDocument = gql`
6+
query Spaces($accountAddress: String!) {
7+
spaces(filter: {
8+
member: { is: $accountAddress }
9+
}) {
10+
id
11+
type
12+
mainVotingAddress
13+
personalAddress
14+
entity {
15+
name
16+
}
17+
}
18+
}
19+
`;
20+
21+
type SpaceQueryResult = {
22+
id: string;
23+
type: string;
24+
mainVotingAddress: string;
25+
personalAddress: string;
26+
entity: {
27+
name: string;
28+
};
29+
};
30+
31+
type PublicSpacesQueryResult = {
32+
spaces: SpaceQueryResult[];
33+
};
34+
35+
export type PublicSpaceData = {
36+
id: string;
37+
type: string;
38+
mainVotingAddress: string;
39+
personalAddress: string;
40+
name: string;
41+
};
42+
43+
export const usePublicSpaces = (url: string): UseQueryResult<PublicSpaceData[], Error> => {
44+
return useQuery<PublicSpaceData[]>({
45+
queryKey: ['public-spaces'],
46+
queryFn: async () => {
47+
const accountAddress = Connect.loadAccountAddress(localStorage);
48+
if (!accountAddress) return [];
49+
const result = await request<PublicSpacesQueryResult>(url, publicSpacesQueryDocument, {
50+
accountAddress,
51+
});
52+
return result?.spaces
53+
? result.spaces.map((space: SpaceQueryResult) => ({
54+
id: space.id,
55+
name: space.entity.name,
56+
type: space.type,
57+
mainVotingAddress: space.mainVotingAddress,
58+
personalAddress: space.personalAddress,
59+
}))
60+
: [];
61+
},
62+
});
63+
};

apps/connect/src/routes/authenticate.tsx

Lines changed: 128 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CreateSpace } from '@/components/create-space';
22
import { Button } from '@/components/ui/button';
3-
import { useSpaces } from '@/hooks/use-spaces';
3+
import { usePrivateSpaces } from '@/hooks/use-private-spaces';
4+
import { type PublicSpaceData, usePublicSpaces } from '@/hooks/use-public-spaces';
45
import { Connect, type Identity, Key, type Messages, StoreConnect, Utils } from '@graphprotocol/hypergraph';
56
import { GEOGENESIS, GEO_TESTNET } from '@graphprotocol/hypergraph/connect/smart-account';
67
import { useIdentityToken, usePrivy, useWallets } from '@privy-io/react-auth';
@@ -137,7 +138,31 @@ function AuthenticateComponent() {
137138

138139
const state = useSelector(componentStore, (state) => state.context);
139140

140-
const { isPending, error: spacesError, data: spacesData } = useSpaces();
141+
const { isPending: privateSpacesPending, error: privateSpacesError, data: privateSpacesData } = usePrivateSpaces();
142+
const {
143+
isPending: publicSpacesPending,
144+
error: publicSpacesError,
145+
data: publicSpacesData,
146+
} = usePublicSpaces(import.meta.env.VITE_HYPERGRAPH_API_URL);
147+
148+
const selectedPrivateSpaces = new Set<string>();
149+
const selectedPublicSpaces = new Set<string>();
150+
151+
const handlePrivateSpaceToggle = (spaceId: string, checked: boolean) => {
152+
if (checked) {
153+
selectedPrivateSpaces.add(spaceId);
154+
} else {
155+
selectedPrivateSpaces.delete(spaceId);
156+
}
157+
};
158+
159+
const handlePublicSpaceToggle = (spaceId: string, checked: boolean) => {
160+
if (checked) {
161+
selectedPublicSpaces.add(spaceId);
162+
} else {
163+
selectedPublicSpaces.delete(spaceId);
164+
}
165+
};
141166

142167
useEffect(() => {
143168
const run = async () => {
@@ -217,55 +242,57 @@ function AuthenticateComponent() {
217242
return;
218243
}
219244

220-
const spacesInput = spacesData
221-
? spacesData.map((space) => {
222-
// TODO: currently without checking we assume all keyboxes exists and we don't create any - we should check if the keyboxes exist and create them if they don't
223-
if (space.appIdentities.some((spaceAppIdentity) => spaceAppIdentity.address === appIdentity.address))
224-
return {
225-
id: space.id,
226-
keyBoxes: [],
227-
};
228-
229-
const spaceKeys = space.keyBoxes.map((keyboxData) => {
230-
const key = Key.decryptKey({
231-
privateKey: Utils.hexToBytes(keys.encryptionPrivateKey),
232-
publicKey: Utils.hexToBytes(keyboxData.authorPublicKey),
233-
keyBoxCiphertext: Utils.hexToBytes(keyboxData.ciphertext),
234-
keyBoxNonce: Utils.hexToBytes(keyboxData.nonce),
245+
const privateSpacesInput = privateSpacesData
246+
? privateSpacesData
247+
.filter((space) => selectedPrivateSpaces.has(space.id))
248+
.map((space) => {
249+
// TODO: currently without checking we assume all keyboxes exists and we don't create any - we should check if the keyboxes exist and create them if they don't
250+
if (space.appIdentities.some((spaceAppIdentity) => spaceAppIdentity.address === appIdentity.address))
251+
return {
252+
id: space.id,
253+
keyBoxes: [],
254+
};
255+
256+
const spaceKeys = space.keyBoxes.map((keyboxData) => {
257+
const key = Key.decryptKey({
258+
privateKey: Utils.hexToBytes(keys.encryptionPrivateKey),
259+
publicKey: Utils.hexToBytes(keyboxData.authorPublicKey),
260+
keyBoxCiphertext: Utils.hexToBytes(keyboxData.ciphertext),
261+
keyBoxNonce: Utils.hexToBytes(keyboxData.nonce),
262+
});
263+
return {
264+
id: keyboxData.id,
265+
key: key,
266+
};
235267
});
236-
return {
237-
id: keyboxData.id,
238-
key: key,
239-
};
240-
});
241268

242-
const keyBoxes = spaceKeys.map((keyData) => {
243-
const keyBox = Key.encryptKey({
244-
privateKey: Utils.hexToBytes(keys.encryptionPrivateKey),
245-
publicKey: Utils.hexToBytes(appIdentity.encryptionPublicKey),
246-
key: keyData.key,
269+
const keyBoxes = spaceKeys.map((keyData) => {
270+
const keyBox = Key.encryptKey({
271+
privateKey: Utils.hexToBytes(keys.encryptionPrivateKey),
272+
publicKey: Utils.hexToBytes(appIdentity.encryptionPublicKey),
273+
key: keyData.key,
274+
});
275+
return {
276+
id: keyData.id,
277+
ciphertext: Utils.bytesToHex(keyBox.keyBoxCiphertext),
278+
nonce: Utils.bytesToHex(keyBox.keyBoxNonce),
279+
authorPublicKey: appIdentity.encryptionPublicKey,
280+
accountAddress: accountAddress,
281+
};
247282
});
283+
248284
return {
249-
id: keyData.id,
250-
ciphertext: Utils.bytesToHex(keyBox.keyBoxCiphertext),
251-
nonce: Utils.bytesToHex(keyBox.keyBoxNonce),
252-
authorPublicKey: appIdentity.encryptionPublicKey,
253-
accountAddress: accountAddress,
285+
id: space.id,
286+
keyBoxes,
254287
};
255-
});
256-
257-
return {
258-
id: space.id,
259-
keyBoxes,
260-
};
261-
})
288+
})
262289
: [];
263290

264291
const message: Messages.RequestConnectAddAppIdentityToSpaces = {
265292
type: 'connect-add-app-identity-to-spaces',
266293
appIdentityAddress: appIdentity.address,
267294
accountAddress,
268-
spacesInput,
295+
spacesInput: privateSpacesInput,
269296
};
270297

271298
// TODO add loading indicator by updating the state
@@ -293,7 +320,8 @@ function AuthenticateComponent() {
293320
signaturePrivateKey: appIdentity.signaturePrivateKey,
294321
signaturePublicKey: appIdentity.signaturePublicKey,
295322
encryptionPublicKey: appIdentity.encryptionPublicKey,
296-
spaces: spacesData?.map((space) => ({ id: space.id })) ?? [],
323+
privateSpaces: privateSpacesInput?.map((space) => ({ id: space.id })) ?? [],
324+
publicSpaces: publicSpacesData?.map((space) => ({ id: space.id })) ?? [],
297325
expiry: appInfo.expiry,
298326
sessionToken: appIdentity.sessionToken,
299327
sessionTokenExpires: appIdentity.sessionTokenExpires.getTime(),
@@ -336,7 +364,8 @@ function AuthenticateComponent() {
336364
};
337365

338366
const newAppIdentity = Connect.createAppIdentity();
339-
// TODO: add spaces and additional actions
367+
368+
// TODO: add additional actions (must be passed from the app)
340369
const permissionId = await Connect.createSmartSession(
341370
walletClient,
342371
accountAddress,
@@ -345,7 +374,16 @@ function AuthenticateComponent() {
345374
import.meta.env.VITE_HYPERGRAPH_RPC_URL,
346375
{
347376
allowCreateSpace: true,
348-
spaces: [],
377+
spaces:
378+
publicSpacesData
379+
?.filter((space) => selectedPublicSpaces.has(space.id))
380+
.map((space) => ({
381+
address:
382+
space.type === 'personal'
383+
? (space.personalAddress as `0x${string}`)
384+
: (space.mainVotingAddress as `0x${string}`),
385+
type: space.type as 'personal' | 'public',
386+
})) ?? [],
349387
additionalActions: [],
350388
},
351389
);
@@ -472,20 +510,53 @@ function AuthenticateComponent() {
472510
</div>
473511
<h2 className="font-bold mb-2 mt-2">Spaces</h2>
474512
<ul className="space-y-4">
475-
{isPending && <p>Loading spaces …</p>}
476-
{spacesError && <p>An error has occurred loading spaces: {spacesError.message}</p>}
477-
{!isPending && !spacesError && spacesData?.length === 0 && <p>No spaces found</p>}
478-
{spacesData?.map((space) => (
479-
<li key={space.id}>
480-
<p>{space.name}</p>
481-
<p className="text-xs text-gray-500 mt-2 mb-1">Apps with access to this space</p>
482-
<ul>
483-
{space.apps.map((app) => (
484-
<li key={app.id} className="text-sm">
485-
{app.name}
486-
</li>
487-
))}
488-
</ul>
513+
{privateSpacesPending && <p>Loading private spaces …</p>}
514+
{privateSpacesError && <p>An error has occurred loading private spaces: {privateSpacesError.message}</p>}
515+
{!privateSpacesPending && !privateSpacesError && privateSpacesData?.length === 0 && (
516+
<p>No private spaces found</p>
517+
)}
518+
{privateSpacesData?.map((space) => (
519+
<li key={space.id} className="flex items-center gap-3 p-3 border rounded-lg">
520+
<input
521+
type="checkbox"
522+
id={`private-${space.id}`}
523+
checked={selectedPrivateSpaces.has(space.id)}
524+
onChange={(e) => handlePrivateSpaceToggle(space.id, e.target.checked)}
525+
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2"
526+
/>
527+
<label htmlFor={`private-${space.id}`} className="flex-1 cursor-pointer">
528+
<p className="font-medium">{space.name}</p>
529+
<p className="text-xs text-gray-500 mt-2 mb-1">Apps with access to this space</p>
530+
<ul>
531+
{space.apps.map((app) => (
532+
<li key={app.id} className="text-sm">
533+
{app.name}
534+
</li>
535+
))}
536+
</ul>
537+
</label>
538+
</li>
539+
))}
540+
</ul>
541+
<h2 className="font-bold mb-2 mt-2">Public Spaces</h2>
542+
<ul className="space-y-4">
543+
{publicSpacesPending && <p>Loading public spaces …</p>}
544+
{publicSpacesError && <p>An error has occurred loading public spaces: {publicSpacesError.message}</p>}
545+
{!publicSpacesPending && !publicSpacesError && publicSpacesData?.length === 0 && (
546+
<p>No public spaces found</p>
547+
)}
548+
{publicSpacesData?.map((space: PublicSpaceData) => (
549+
<li key={space.id} className="flex items-center gap-3 p-3 border rounded-lg">
550+
<input
551+
type="checkbox"
552+
id={`public-${space.id}`}
553+
checked={selectedPublicSpaces.has(space.id)}
554+
onChange={(e) => handlePublicSpaceToggle(space.id, e.target.checked)}
555+
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2"
556+
/>
557+
<label htmlFor={`public-${space.id}`} className="flex-1 cursor-pointer">
558+
<p className="font-medium">{space.name}</p>
559+
</label>
489560
</li>
490561
))}
491562
</ul>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export const GEO_API_MAINNET_ENDPOINT = 'https://hypergraph-v2.up.railway.app//graphql';
1+
export const GEO_API_MAINNET_ENDPOINT = 'https://hypergraph-v2.up.railway.app/graphql';
22
export const GEO_API_TESTNET_ENDPOINT = 'https://hypergraph-v2-testnet.up.railway.app/graphql';

packages/hypergraph/src/connect/create-callback-params.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ type CreateAuthUrlParams = {
1616
encryptionPrivateKey: string;
1717
sessionToken: string;
1818
sessionTokenExpires: number;
19-
spaces: { id: string }[];
19+
privateSpaces: { id: string }[];
20+
publicSpaces: { id: string }[];
2021
};
2122

2223
export const createCallbackParams = ({ nonce, ephemeralPublicKey, ...rest }: CreateAuthUrlParams) => {

packages/hypergraph/src/connect/parse-callback-params.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export const parseCallbackParams = ({
6060
encryptionPrivateKey: data.encryptionPrivateKey,
6161
sessionToken: data.sessionToken,
6262
sessionTokenExpires: new Date(data.sessionTokenExpires),
63-
spaces: data.spaces,
63+
privateSpaces: data.privateSpaces,
64+
publicSpaces: data.publicSpaces,
6465
});
6566
} catch (error) {
6667
console.error(error);

packages/hypergraph/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export const ConnectCallbackResult = Schema.Struct({
2828
encryptionPrivateKey: Schema.String,
2929
sessionToken: Schema.String,
3030
sessionTokenExpires: Schema.Date,
31-
spaces: Schema.Array(Schema.Struct({ id: Schema.String })),
31+
privateSpaces: Schema.Array(Schema.Struct({ id: Schema.String })),
32+
publicSpaces: Schema.Array(Schema.Struct({ id: Schema.String })),
3233
});
3334

3435
export type ConnectCallbackResult = Schema.Schema.Type<typeof ConnectCallbackResult>;

0 commit comments

Comments
 (0)