Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/plain-eggs-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphprotocol/hypergraph-react": patch
---

add privy auth based createSpace hooks

42 changes: 26 additions & 16 deletions apps/privy-login-example/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { store } from '@graphprotocol/hypergraph';
import { useHypergraphApp, useSpaces } from '@graphprotocol/hypergraph-react';
import {
useHypergraphApp,
_usePrivyAuthCreatePrivateSpace as usePrivyAuthCreatePrivateSpace,
_usePrivyAuthCreatePublicSpace as usePrivyAuthCreatePublicSpace,
useSpaces,
} from '@graphprotocol/hypergraph-react';
import { createFileRoute, Link } from '@tanstack/react-router';
import { useSelector } from '@xstate/store/react';
import { useEffect, useState } from 'react';
Expand All @@ -18,24 +23,21 @@ function Index() {
const { data: publicSpaces } = useSpaces({ mode: 'public' });
const { data: privateSpaces } = useSpaces({ mode: 'private' });
const [spaceName, setSpaceName] = useState('');
const { createPrivateSpace, isLoading: isCreatingPrivateSpace } = usePrivyAuthCreatePrivateSpace();
const { createPublicSpace, isLoading: isCreatingPublicSpace } = usePrivyAuthCreatePublicSpace();
console.log({ isCreatingPrivateSpace, isCreatingPublicSpace });
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statement should be removed before merging to production.

Suggested change
console.log({ isCreatingPrivateSpace, isCreatingPublicSpace });

Copilot uses AI. Check for mistakes.


const accountInboxes = useSelector(store, (state) => state.context.accountInboxes);
const {
createSpace,
listInvitations,
invitations,
acceptInvitation,
createAccountInbox,
getOwnAccountInboxes,
isConnecting,
} = useHypergraphApp();
const { listInvitations, invitations, acceptInvitation, createAccountInbox, getOwnAccountInboxes, isConnecting } =
useHypergraphApp();

useEffect(() => {
if (!isConnecting) {
listInvitations();
getOwnAccountInboxes();
}
}, [isConnecting, listInvitations, getOwnAccountInboxes]);
const [spaceType, setSpaceType] = useState<'private' | 'public'>('private');

if (isConnecting) {
return <div className="flex justify-center items-center h-screen">Loading …</div>;
Expand Down Expand Up @@ -76,15 +78,23 @@ function Index() {
</div>
<div className="flex flex-row gap-2 justify-between items-center">
<Input value={spaceName} onChange={(e) => setSpaceName(e.target.value)} />
<select
className="c-input shrink-0"
value={spaceType}
onChange={(e) => setSpaceType(e.target.value as 'private' | 'public')}
>
<option value="private">Private</option>
<option value="public">Public</option>
</select>
<Button
disabled={true} // disabled until we have delegation for creating a space
disabled={isCreatingPrivateSpace || isCreatingPublicSpace}
onClick={async (event) => {
event.preventDefault();
// const smartSessionClient = await getSmartSessionClient();
// if (!smartSessionClient) {
// throw new Error('Missing smartSessionClient');
// }
createSpace({ name: spaceName });
if (spaceType === 'private') {
await createPrivateSpace({ name: spaceName });
} else {
await createPublicSpace({ name: spaceName });
}
setSpaceName('');
}}
>
Expand Down
3 changes: 3 additions & 0 deletions packages/hypergraph-react/src/HypergraphAppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export type HypergraphAppCtx = {
redirectFn: (url: URL) => void;
}): void;
processConnectAuthSuccess(params: { storage: Identity.Storage; ciphertext: string; nonce: string }): void;
syncServerUri: string;
};

export const HypergraphAppContext = createContext<HypergraphAppCtx>({
Expand Down Expand Up @@ -200,6 +201,7 @@ export const HypergraphAppContext = createContext<HypergraphAppCtx>({
processConnectAuthSuccess() {
throw new Error('processConnectAuthSuccess is missing');
},
syncServerUri: '',
});

export function useHypergraphApp() {
Expand Down Expand Up @@ -1553,6 +1555,7 @@ export function HypergraphAppProvider({
ensureSpaceInbox: ensureSpaceInboxForContext,
redirectToConnect: redirectToConnectForContext,
processConnectAuthSuccess: processConnectAuthSuccessForContext,
syncServerUri,
}}
>
<QueryClientProvider client={queryClient}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Key, type Messages, SpaceEvents, SpaceInfo, Utils } from '@graphprotocol/hypergraph';
import * as Effect from 'effect/Effect';
import { useState } from 'react';
import { useHypergraphApp, useHypergraphAuth } from '../HypergraphAppContext.js';

type CreatePrivateSpaceParams = {
name: string;
};

export const usePrivyAuthCreatePrivateSpace = () => {
const [isLoading, setIsLoading] = useState(false);
const { privyIdentity } = useHypergraphAuth();
const { syncServerUri, listSpaces } = useHypergraphApp();

const createPrivateSpace = async ({ name }: CreatePrivateSpaceParams) => {
const accountAddress = privyIdentity?.accountAddress;
if (!accountAddress) {
setIsLoading(false);
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting isLoading to false before throwing an error is incorrect. The loading state should remain true since the operation hasn't completed yet, and the error will be thrown to the caller.

Copilot uses AI. Check for mistakes.

throw new Error('No account address found');
}
const encryptionPrivateKey = privyIdentity?.encryptionPrivateKey;
const encryptionPublicKey = privyIdentity?.encryptionPublicKey;
const signaturePrivateKey = privyIdentity?.signaturePrivateKey;
const signaturePublicKey = privyIdentity?.signaturePublicKey;
if (!encryptionPrivateKey || !encryptionPublicKey || !signaturePrivateKey || !signaturePublicKey) {
setIsLoading(false);
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting isLoading to false before throwing an error is incorrect. The loading state should remain true since the operation hasn't completed yet, and the error will be thrown to the caller.

Copilot uses AI. Check for mistakes.

throw new Error('No keys found');
}
const privyIdentityToken = privyIdentity?.privyIdentityToken;
if (!privyIdentityToken) {
setIsLoading(false);
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting isLoading to false before throwing an error is incorrect. The loading state should remain true since the operation hasn't completed yet, and the error will be thrown to the caller.

Copilot uses AI. Check for mistakes.

throw new Error('No Privy identity token found');
}
setIsLoading(true);

try {
const spaceId = Utils.generateId();

const spaceEvent = await Effect.runPromise(
SpaceEvents.createSpace({
author: {
accountAddress,
encryptionPublicKey,
signaturePrivateKey,
signaturePublicKey,
},
spaceId,
}),
);
const result = Key.createKey({
privateKey: Utils.hexToBytes(encryptionPrivateKey),
publicKey: Utils.hexToBytes(encryptionPublicKey),
});

const { infoContent, signature } = SpaceInfo.encryptAndSignSpaceInfo({
accountAddress,
name,
secretKey: Utils.bytesToHex(result.key),
signaturePrivateKey,
spaceId,
});

const message: Messages.RequestConnectCreateSpaceEvent = {
type: 'connect-create-space-event',
accountAddress,
event: spaceEvent,
spaceId: spaceEvent.transaction.id,
keyBox: {
accountAddress,
ciphertext: Utils.bytesToHex(result.keyBoxCiphertext),
nonce: Utils.bytesToHex(result.keyBoxNonce),
authorPublicKey: encryptionPublicKey,
id: Utils.generateId(),
},
infoContent: Utils.bytesToHex(infoContent),
infoSignature: signature,
name,
};

const response = await fetch(`${syncServerUri}/connect/spaces`, {
headers: {
'privy-id-token': privyIdentityToken,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(message),
});
const data = await response.json();
if (data.space) {
listSpaces(); // list spaces to update the list of private spaces
} else {
throw new Error('Failed to create space');
}
return data.space;
} finally {
setIsLoading(false);
}
};

return { createPrivateSpace, isLoading };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Graph } from '@graphprotocol/grc-20';
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useHypergraphAuth } from '../HypergraphAppContext.js';

type CreatePublicSpaceParams = {
name: string;
};

export const usePrivyAuthCreatePublicSpace = () => {
const [isLoading, setIsLoading] = useState(false);
const queryClient = useQueryClient();
const { privyIdentity } = useHypergraphAuth();

const createPublicSpace = async ({ name }: CreatePublicSpaceParams) => {
const accountAddress = privyIdentity?.accountAddress;
if (!accountAddress) {
throw new Error('No account address found');
}
try {
setIsLoading(true);
const { id } = await Graph.createSpace({
editorAddress: accountAddress,
name,
network: 'TESTNET',
});
queryClient.invalidateQueries({ queryKey: ['hypergraph-public-spaces'] });
setIsLoading(false);
return id;
} catch (error) {
setIsLoading(false);
throw error;
}
};
return { createPublicSpace, isLoading };
};
2 changes: 1 addition & 1 deletion packages/hypergraph-react/src/hooks/use-spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const useSpaces = (params: { mode: 'public' | 'private' }) => {
const accountAddress = identityAccountAddress ? identityAccountAddress : privyIdentityAccountAddress;

const publicResult = useQuery({
queryKey: ['hypergraph-spaces', params.mode],
queryKey: ['hypergraph-public-spaces', params.mode],
queryFn: async () => {
const result = await request<PublicSpacesQueryResult>(
`${Graph.TESTNET_API_ORIGIN}/graphql`,
Expand Down
2 changes: 2 additions & 0 deletions packages/hypergraph-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export { useCreateEntity } from './hooks/use-create-entity.js';
export { useDeleteEntity } from './hooks/use-delete-entity.js';
export { useEntity } from './hooks/use-entity.js';
export { useHardDeleteEntity } from './hooks/use-hard-delete-entity.js';
export { usePrivyAuthCreatePrivateSpace as _usePrivyAuthCreatePrivateSpace } from './hooks/use-privy-auth-create-private-space.js';
export { usePrivyAuthCreatePublicSpace as _usePrivyAuthCreatePublicSpace } from './hooks/use-privy-auth-create-public-space.js';
export { useQuery } from './hooks/use-query.js';
export { useRemoveRelation } from './hooks/use-remove-relation.js';
export { useSpace } from './hooks/use-space.js';
Expand Down
Loading