From 2e4f0f11ef14368c9e60b660b8ba5c7e8a13bdd2 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sun, 14 Sep 2025 22:21:49 +0200 Subject: [PATCH 1/4] add privy login example --- .changeset/config.json | 9 +- CLAUDE.md | 2 + apps/privy-login-example/README.md | 50 +++ apps/privy-login-example/components.json | 20 + apps/privy-login-example/index.html | 13 + apps/privy-login-example/package.json | 54 +++ apps/privy-login-example/public/vite.svg | 1 + apps/privy-login-example/src/Boot.tsx | 41 ++ .../src/components/InboxCard.tsx | 54 +++ .../src/components/SpaceChat.tsx | 71 ++++ .../src/components/create-events.tsx | 99 +++++ .../create-properties-and-types-event.tsx | 135 ++++++ .../create-properties-and-types-todos.tsx | 150 +++++++ .../src/components/dev-tool.tsx | 64 +++ .../src/components/event.tsx | 30 ++ .../src/components/events/events.tsx | 91 ++++ .../src/components/logout.tsx | 20 + .../src/components/playground.tsx | 91 ++++ .../src/components/spinner.tsx | 42 ++ .../src/components/todo/todos-local.tsx | 52 +++ .../src/components/todo/todos-public.tsx | 62 +++ .../src/components/todos-read-only-filter.tsx | 51 +++ .../src/components/todos-read-only.tsx | 28 ++ .../src/components/todos.tsx | 99 +++++ .../src/components/todos2.tsx | 273 ++++++++++++ .../src/components/ui/avatar.tsx | 38 ++ .../src/components/ui/button.tsx | 48 +++ .../src/components/ui/card.tsx | 43 ++ .../src/components/ui/input.tsx | 23 ++ .../src/components/ui/label.tsx | 19 + .../src/components/ui/modal.tsx | 40 ++ .../src/components/user-entry.tsx | 25 ++ .../src/components/users.tsx | 37 ++ .../src/components/users/users-local.tsx | 37 ++ .../src/components/users/users-merged.tsx | 29 ++ .../src/components/users/users-public.tsx | 44 ++ apps/privy-login-example/src/index.css | 129 ++++++ .../src/lib/create-ethereum-account.ts | 11 + apps/privy-login-example/src/lib/utils.ts | 6 + apps/privy-login-example/src/main.test.ts | 5 + apps/privy-login-example/src/main.tsx | 10 + apps/privy-login-example/src/mapping.ts | 69 ++++ apps/privy-login-example/src/routeTree.gen.ts | 327 +++++++++++++++ .../privy-login-example/src/routes/__root.tsx | 63 +++ .../src/routes/account-inbox/$inboxId.tsx | 49 +++ .../src/routes/authenticate-success.tsx | 27 ++ .../src/routes/friends/$accountAddress.tsx | 36 ++ apps/privy-login-example/src/routes/index.tsx | 161 ++++++++ .../src/routes/login.lazy.tsx | 111 +++++ .../src/routes/playground.lazy.tsx | 27 ++ .../src/routes/space/$spaceId.tsx | 72 ++++ .../src/routes/space/$spaceId/chat.tsx | 22 + .../src/routes/space/$spaceId/events.tsx | 23 ++ .../src/routes/space/$spaceId/index.tsx | 57 +++ .../src/routes/space/$spaceId/playground.tsx | 24 ++ .../space/$spaceId/public-integration.tsx | 26 ++ .../src/routes/space/$spaceId/users.tsx | 27 ++ apps/privy-login-example/src/schema.ts | 43 ++ apps/privy-login-example/src/types.ts | 3 + apps/privy-login-example/src/vite-env.d.ts | 1 + apps/privy-login-example/tsconfig.app.json | 41 ++ apps/privy-login-example/tsconfig.json | 8 + apps/privy-login-example/tsconfig.node.json | 29 ++ apps/privy-login-example/vite.config.ts | 17 + apps/privy-login-example/vitest.config.ts | 13 + pnpm-lock.yaml | 389 ++++++++++++++---- tsconfig.json | 3 +- vitest.config.ts | 2 +- 68 files changed, 3730 insertions(+), 86 deletions(-) create mode 100644 apps/privy-login-example/README.md create mode 100644 apps/privy-login-example/components.json create mode 100644 apps/privy-login-example/index.html create mode 100644 apps/privy-login-example/package.json create mode 100644 apps/privy-login-example/public/vite.svg create mode 100644 apps/privy-login-example/src/Boot.tsx create mode 100644 apps/privy-login-example/src/components/InboxCard.tsx create mode 100644 apps/privy-login-example/src/components/SpaceChat.tsx create mode 100644 apps/privy-login-example/src/components/create-events.tsx create mode 100644 apps/privy-login-example/src/components/create-properties-and-types-event.tsx create mode 100644 apps/privy-login-example/src/components/create-properties-and-types-todos.tsx create mode 100644 apps/privy-login-example/src/components/dev-tool.tsx create mode 100644 apps/privy-login-example/src/components/event.tsx create mode 100644 apps/privy-login-example/src/components/events/events.tsx create mode 100644 apps/privy-login-example/src/components/logout.tsx create mode 100644 apps/privy-login-example/src/components/playground.tsx create mode 100644 apps/privy-login-example/src/components/spinner.tsx create mode 100644 apps/privy-login-example/src/components/todo/todos-local.tsx create mode 100644 apps/privy-login-example/src/components/todo/todos-public.tsx create mode 100644 apps/privy-login-example/src/components/todos-read-only-filter.tsx create mode 100644 apps/privy-login-example/src/components/todos-read-only.tsx create mode 100644 apps/privy-login-example/src/components/todos.tsx create mode 100644 apps/privy-login-example/src/components/todos2.tsx create mode 100644 apps/privy-login-example/src/components/ui/avatar.tsx create mode 100644 apps/privy-login-example/src/components/ui/button.tsx create mode 100644 apps/privy-login-example/src/components/ui/card.tsx create mode 100644 apps/privy-login-example/src/components/ui/input.tsx create mode 100644 apps/privy-login-example/src/components/ui/label.tsx create mode 100644 apps/privy-login-example/src/components/ui/modal.tsx create mode 100644 apps/privy-login-example/src/components/user-entry.tsx create mode 100644 apps/privy-login-example/src/components/users.tsx create mode 100644 apps/privy-login-example/src/components/users/users-local.tsx create mode 100644 apps/privy-login-example/src/components/users/users-merged.tsx create mode 100644 apps/privy-login-example/src/components/users/users-public.tsx create mode 100644 apps/privy-login-example/src/index.css create mode 100644 apps/privy-login-example/src/lib/create-ethereum-account.ts create mode 100644 apps/privy-login-example/src/lib/utils.ts create mode 100644 apps/privy-login-example/src/main.test.ts create mode 100644 apps/privy-login-example/src/main.tsx create mode 100644 apps/privy-login-example/src/mapping.ts create mode 100644 apps/privy-login-example/src/routeTree.gen.ts create mode 100644 apps/privy-login-example/src/routes/__root.tsx create mode 100644 apps/privy-login-example/src/routes/account-inbox/$inboxId.tsx create mode 100644 apps/privy-login-example/src/routes/authenticate-success.tsx create mode 100644 apps/privy-login-example/src/routes/friends/$accountAddress.tsx create mode 100644 apps/privy-login-example/src/routes/index.tsx create mode 100644 apps/privy-login-example/src/routes/login.lazy.tsx create mode 100644 apps/privy-login-example/src/routes/playground.lazy.tsx create mode 100644 apps/privy-login-example/src/routes/space/$spaceId.tsx create mode 100644 apps/privy-login-example/src/routes/space/$spaceId/chat.tsx create mode 100644 apps/privy-login-example/src/routes/space/$spaceId/events.tsx create mode 100644 apps/privy-login-example/src/routes/space/$spaceId/index.tsx create mode 100644 apps/privy-login-example/src/routes/space/$spaceId/playground.tsx create mode 100644 apps/privy-login-example/src/routes/space/$spaceId/public-integration.tsx create mode 100644 apps/privy-login-example/src/routes/space/$spaceId/users.tsx create mode 100644 apps/privy-login-example/src/schema.ts create mode 100644 apps/privy-login-example/src/types.ts create mode 100644 apps/privy-login-example/src/vite-env.d.ts create mode 100644 apps/privy-login-example/tsconfig.app.json create mode 100644 apps/privy-login-example/tsconfig.json create mode 100644 apps/privy-login-example/tsconfig.node.json create mode 100644 apps/privy-login-example/vite.config.ts create mode 100644 apps/privy-login-example/vitest.config.ts diff --git a/.changeset/config.json b/.changeset/config.json index 95793762..d1fba5d6 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,6 +7,13 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["events", "next-example", "docs", "hypergraph-vite-react-template", "hypergraph-template-nextjs"], + "ignore": [ + "events", + "privy-login-example", + "next-example", + "docs", + "hypergraph-vite-react-template", + "hypergraph-template-nextjs" + ], "prettier": false } diff --git a/CLAUDE.md b/CLAUDE.md index 77ceff16..3bb34563 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,7 @@ Hypergraph is a local-first framework for building web3 consumer applications th ```bash # Run specific apps cd apps/events && pnpm dev # Events demo app +cd apps/privy-login-example && pnpm dev # Privy login example app cd apps/server && pnpm dev # Backend sync server cd apps/connect && pnpm dev # Geo Connect auth app ``` @@ -52,6 +53,7 @@ pnpm clean # Clean all build artifacts - **apps/** - Complete applications - `server/` - Backend sync server (Express + Prisma + SQLite/PostgreSQL) - `events/` - Demo app showcasing the framework (Vite + React) + - `privy-login-example/` - Privy login example app (Vite + React) - `connect/` - Geo Connect authentication app - `next-example/` - Next.js integration example - **docs/** - Docusaurus documentation site diff --git a/apps/privy-login-example/README.md b/apps/privy-login-example/README.md new file mode 100644 index 00000000..74872fd4 --- /dev/null +++ b/apps/privy-login-example/README.md @@ -0,0 +1,50 @@ +# 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: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from 'eslint-plugin-react' + +export default tseslint.config({ + // Set the react version + settings: { react: { version: '18.3' } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + }, +}) +``` diff --git a/apps/privy-login-example/components.json b/apps/privy-login-example/components.json new file mode 100644 index 00000000..aa38200a --- /dev/null +++ b/apps/privy-login-example/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/apps/privy-login-example/index.html b/apps/privy-login-example/index.html new file mode 100644 index 00000000..e4b78eae --- /dev/null +++ b/apps/privy-login-example/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/apps/privy-login-example/package.json b/apps/privy-login-example/package.json new file mode 100644 index 00000000..974e42dc --- /dev/null +++ b/apps/privy-login-example/package.json @@ -0,0 +1,54 @@ +{ + "name": "privy-login-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --force", + "preview": "vite preview", + "typesync": "hypergraph typesync" + }, + "dependencies": { + "@graphprotocol/grc-20": "^0.24.1", + "@graphprotocol/hypergraph": "workspace:*", + "@graphprotocol/hypergraph-react": "workspace:*", + "@noble/hashes": "^1.8.0", + "@privy-io/react-auth": "^2.21.4", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@tanstack/react-query": "^5.85.5", + "@tanstack/react-router": "^1.131.27", + "@tanstack/react-router-devtools": "^1.131.27", + "@xstate/store": "^3.9.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "effect": "^3.17.9", + "framer-motion": "^12.23.12", + "graphql-request": "^7.2.0", + "isomorphic-ws": "^5.0.0", + "lucide-react": "^0.541.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-select": "^5.10.2", + "siwe": "^3.0.0", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7", + "uuid": "^11.1.0", + "viem": "^2.34.0", + "vite": "^7.1.3" + }, + "devDependencies": { + "@biomejs/biome": "2.2.0", + "@tailwindcss/vite": "^4.1.12", + "@tanstack/router-plugin": "^1.131.27", + "@types/node": "^24.3.0", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@types/uuid": "^10.0.0", + "@vitejs/plugin-react": "^5.0.1", + "globals": "^16.3.0", + "tailwindcss": "^4.1.12" + } +} diff --git a/apps/privy-login-example/public/vite.svg b/apps/privy-login-example/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/apps/privy-login-example/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/privy-login-example/src/Boot.tsx b/apps/privy-login-example/src/Boot.tsx new file mode 100644 index 00000000..a4eb2bba --- /dev/null +++ b/apps/privy-login-example/src/Boot.tsx @@ -0,0 +1,41 @@ +import { HypergraphAppProvider } from '@graphprotocol/hypergraph-react'; +import { PrivyProvider } from '@privy-io/react-auth'; +import { createRouter, RouterProvider } from '@tanstack/react-router'; +import { mapping } from './mapping.js'; +import { routeTree } from './routeTree.gen'; + +// Create a new router instance +const router = createRouter({ routeTree }); + +// Register the router instance for type safety +declare module '@tanstack/react-router' { + interface Register { + router: typeof router; + } +} + +export function Boot() { + return ( + + + + + + ); +} diff --git a/apps/privy-login-example/src/components/InboxCard.tsx b/apps/privy-login-example/src/components/InboxCard.tsx new file mode 100644 index 00000000..235ba542 --- /dev/null +++ b/apps/privy-login-example/src/components/InboxCard.tsx @@ -0,0 +1,54 @@ +import { useExternalAccountInbox } from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; + +interface InboxCardProps { + accountAddress: string; + inboxId: string; +} + +export function InboxCard({ accountAddress, inboxId }: InboxCardProps) { + const [message, setMessage] = useState(''); + const { loading, error, sendMessage, isPublic, authPolicy } = useExternalAccountInbox(accountAddress, inboxId); + + const handleSendMessage = async () => { + if (!message.trim()) return; + + try { + await sendMessage(message.trim()); + setMessage(''); // Clear the input after sending + } catch (err) { + console.error('Failed to send message:', err); + } + }; + + return ( +
+

Inbox: {inboxId}

+
+
{isPublic ? 'Public' : 'Private'} inbox
+
Auth Policy: {authPolicy}
+
+ +
+ setMessage(e.target.value)} + placeholder="Type a message..." + className="flex-1 px-3 py-2 border rounded" + disabled={loading} + /> + +
+ + {error &&
{error.message}
} +
+ ); +} diff --git a/apps/privy-login-example/src/components/SpaceChat.tsx b/apps/privy-login-example/src/components/SpaceChat.tsx new file mode 100644 index 00000000..1d076749 --- /dev/null +++ b/apps/privy-login-example/src/components/SpaceChat.tsx @@ -0,0 +1,71 @@ +import { useOwnSpaceInbox } from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { Button } from './ui/button'; + +interface SpaceChatProps { + spaceId: string; +} + +export function SpaceChat({ spaceId }: SpaceChatProps) { + const [message, setMessage] = useState(''); + + // This will create the inbox if it doesn't exist, or use the first inbox in the space + const { messages, error, sendMessage, loading } = useOwnSpaceInbox({ + spaceId, + autoCreate: true, + }); + + if (loading) { + return
Creating space chat...
; + } + + const handleSendMessage = async () => { + if (!message.trim()) return; + + try { + await sendMessage(message.trim()); + setMessage(''); // Clear the input after sending + } catch (err) { + console.error('Failed to send message:', err); + } + }; + + return ( +
+

Space Chat

+ +
+ {/* Messages */} +
+ {messages?.map((msg) => ( +
+
+
From: {msg.authorAccountAddress?.substring(0, 6) || 'Anonymous'}
+
{new Date(msg.createdAt).toLocaleString()}
+
+
{msg.plaintext}
+
+ ))} + {messages?.length === 0 &&
No messages yet
} +
+ + {/* Input */} +
+ setMessage(e.target.value)} + placeholder="Type a message..." + className="flex-1 px-3 py-2 border rounded" + disabled={loading} + /> + +
+ + {error &&
{error.message}
} +
+
+ ); +} diff --git a/apps/privy-login-example/src/components/create-events.tsx b/apps/privy-login-example/src/components/create-events.tsx new file mode 100644 index 00000000..f3d1fb87 --- /dev/null +++ b/apps/privy-login-example/src/components/create-events.tsx @@ -0,0 +1,99 @@ +import { Graph, type Op } from '@graphprotocol/grc-20'; +import type { Connect } from '@graphprotocol/hypergraph'; +import { publishOps, useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { mapping } from '../mapping'; +import { Button } from './ui/button'; + +const createEvents = async ({ + smartSessionClient, + space, +}: { + smartSessionClient: Connect.SmartSessionClient; + space: string; +}) => { + try { + const ops: Array = []; + + const { id: jobOfferTypeId, ops: createJobOfferTypeOps } = Graph.createEntity({ + name: 'My Test Job Offer', + types: mapping.JobOffer.typeIds, + values: [ + { + property: mapping.JobOffer.properties?.salary as string, + value: '80000', + }, + ], + }); + ops.push(...createJobOfferTypeOps); + + const { id: jobOfferTypeId2, ops: createJobOfferTypeOps2 } = Graph.createEntity({ + name: 'My Test Job Offer 2', + types: mapping.JobOffer.typeIds, + values: [ + { + property: mapping.JobOffer.properties?.salary as string, + value: '90000', + }, + ], + }); + ops.push(...createJobOfferTypeOps2); + + console.log('jobOfferTypeId', jobOfferTypeId); + console.log('jobOfferTypeId2', jobOfferTypeId2); + + const { id: companyTypeId, ops: createCompanyTypeOps } = Graph.createEntity({ + name: 'My Test Company', + types: mapping.Company.typeIds, + relations: { + [mapping.Company.relations?.jobOffers as string]: [{ toEntity: jobOfferTypeId }, { toEntity: jobOfferTypeId2 }], + }, + }); + ops.push(...createCompanyTypeOps); + + console.log('companyTypeId', companyTypeId); + + const { ops: createEventTypeOps, id: eventTypeId } = Graph.createEntity({ + name: 'My Test Event', + types: mapping.Event.typeIds, + relations: { + [mapping.Event.relations?.sponsors as string]: [{ toEntity: companyTypeId }], + }, + }); + ops.push(...createEventTypeOps); + + console.log('eventTypeId', eventTypeId); + + const result = await publishOps({ + ops, + walletClient: smartSessionClient, + space, + name: 'Create Job Offers, Companies and Events', + }); + console.log('result', result, ops); + alert('Events created'); + } catch (error) { + console.error('error', error); + } +}; + +export const CreateEvents = ({ space }: { space: string }) => { + const { getSmartSessionClient } = useHypergraphApp(); + return ( +
+ +
+ ); +}; diff --git a/apps/privy-login-example/src/components/create-properties-and-types-event.tsx b/apps/privy-login-example/src/components/create-properties-and-types-event.tsx new file mode 100644 index 00000000..235ded3d --- /dev/null +++ b/apps/privy-login-example/src/components/create-properties-and-types-event.tsx @@ -0,0 +1,135 @@ +import { Graph, type Op } from '@graphprotocol/grc-20'; +import type { Connect } from '@graphprotocol/hypergraph'; +import { publishOps, useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { Button } from './ui/button'; +import { Card, CardContent } from './ui/card'; + +const createPropertiesAndTypesEvent = async ({ + smartSessionClient, + space, +}: { + smartSessionClient: Connect.SmartSessionClient; + space: string; +}) => { + const ops: Array = []; + const { id: salaryPropertyId, ops: createSalaryPropertyOps } = Graph.createProperty({ + dataType: 'NUMBER', + name: 'Salary', + }); + ops.push(...createSalaryPropertyOps); + + const { id: jobOfferTypeId, ops: createJobOfferTypeOps } = Graph.createType({ + name: 'Job Offer', + properties: [salaryPropertyId], + }); + ops.push(...createJobOfferTypeOps); + + const { id: jobOffersRelationTypeId, ops: createJobOffersRelationTypeOps } = Graph.createProperty({ + dataType: 'RELATION', + name: 'Job Offer', + relationValueTypes: [jobOfferTypeId], + }); + ops.push(...createJobOffersRelationTypeOps); + + const { id: companyTypeId, ops: createCompanyTypeOps } = Graph.createType({ + name: 'Company', + properties: [salaryPropertyId, jobOffersRelationTypeId], + }); + ops.push(...createCompanyTypeOps); + + const { id: sponsorsRelationTypeId, ops: createSponsorsRelationTypeOps } = Graph.createProperty({ + dataType: 'RELATION', + name: 'Sponsor', + relationValueTypes: [companyTypeId], + }); + ops.push(...createSponsorsRelationTypeOps); + + const { id: eventTypeId, ops: createEventTypeOps } = Graph.createType({ + name: 'Event', + properties: [sponsorsRelationTypeId], + }); + ops.push(...createEventTypeOps); + + const result = await publishOps({ + ops, + walletClient: smartSessionClient, + space, + name: 'Create properties and types', + }); + return { + result, + eventTypeId, + companyTypeId, + jobOfferTypeId, + salaryPropertyId, + jobOffersRelationTypeId, + sponsorsRelationTypeId, + }; +}; + +export const CreatePropertiesAndTypesEvent = ({ space }: { space: string }) => { + const [mapping, setMapping] = useState(''); + const { getSmartSessionClient } = useHypergraphApp(); + + return ( +
+ {mapping && ( + + +
{mapping}
+
+
+ )} + +
+ ); +}; diff --git a/apps/privy-login-example/src/components/create-properties-and-types-todos.tsx b/apps/privy-login-example/src/components/create-properties-and-types-todos.tsx new file mode 100644 index 00000000..9350afa4 --- /dev/null +++ b/apps/privy-login-example/src/components/create-properties-and-types-todos.tsx @@ -0,0 +1,150 @@ +import { Graph, type Op } from '@graphprotocol/grc-20'; +import type { Connect } from '@graphprotocol/hypergraph'; +import { publishOps, useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { Button } from './ui/button'; +import { Card, CardContent } from './ui/card'; + +const createPropertiesAndTypesTodos = async ({ + smartSessionClient, + space, +}: { + smartSessionClient: Connect.SmartSessionClient; + space: string; +}) => { + const ops: Array = []; + const { id: checkedPropertyId, ops: createCheckedPropertyOps } = Graph.createProperty({ + dataType: 'BOOLEAN', + name: 'Checked', + }); + ops.push(...createCheckedPropertyOps); + + const { id: userId, ops: createUserOps } = Graph.createType({ + name: 'User', + }); + ops.push(...createUserOps); + + const { id: assigneesRelationTypeId, ops: createAssigneesRelationTypeOps } = Graph.createProperty({ + dataType: 'RELATION', + name: 'Assignees', + relationValueTypes: [userId], + }); + ops.push(...createAssigneesRelationTypeOps); + + const { id: duePropertyId, ops: createDuePropertyOps } = Graph.createProperty({ + dataType: 'TIME', + name: 'Due', + }); + ops.push(...createDuePropertyOps); + + const { id: pointPropertyId, ops: createPointPropertyOps } = Graph.createProperty({ + dataType: 'POINT', + name: 'Point', + }); + ops.push(...createPointPropertyOps); + + const { id: amountPropertyId, ops: createAmountPropertyOps } = Graph.createProperty({ + dataType: 'NUMBER', + name: 'Amount', + }); + ops.push(...createAmountPropertyOps); + + const { id: websitePropertyId, ops: createWebsitePropertyOps } = Graph.createProperty({ + dataType: 'STRING', + name: 'Website', + }); + ops.push(...createWebsitePropertyOps); + + const { id: todoTypeId, ops: createTodoTypeOps } = Graph.createType({ + name: 'Todo', + properties: [ + checkedPropertyId, + assigneesRelationTypeId, + duePropertyId, + pointPropertyId, + websitePropertyId, + amountPropertyId, + ], + }); + ops.push(...createTodoTypeOps); + + const result = await publishOps({ + ops, + walletClient: smartSessionClient, + space, + name: 'Create properties and types', + }); + return { + result, + todoTypeId, + checkedPropertyId, + userId, + assigneesRelationTypeId, + duePropertyId, + pointPropertyId, + websitePropertyId, + amountPropertyId, + }; +}; + +export const CreatePropertiesAndTypesTodos = ({ space }: { space: string }) => { + const [mapping, setMapping] = useState(''); + const { getSmartSessionClient } = useHypergraphApp(); + return ( +
+ {mapping && ( + + +
{mapping}
+
+
+ )} + +
+ ); +}; diff --git a/apps/privy-login-example/src/components/dev-tool.tsx b/apps/privy-login-example/src/components/dev-tool.tsx new file mode 100644 index 00000000..507853ac --- /dev/null +++ b/apps/privy-login-example/src/components/dev-tool.tsx @@ -0,0 +1,64 @@ +import { store } from '@graphprotocol/hypergraph'; +import { useSelector } from '@xstate/store/react'; +import { useState } from 'react'; + +import { Button } from './ui/button'; + +export function DevTool({ spaceId }: { spaceId: string }) { + const [isOpen, setIsOpen] = useState(false); + + const spaces = useSelector(store, (state) => state.context.spaces); + const updatesInFlight = useSelector(store, (state) => state.context.updatesInFlight); + + const space = spaces.find((space) => space.id === spaceId); + + return ( + <> +
+ +
+ {isOpen && !space &&
Space not found
} + {isOpen && space && ( + <> +

Space id: {space.id}

+

Keys:

+
{JSON.stringify(space.keys)}
+
+

Updates in flight

+
    + {updatesInFlight.map((updateInFlight) => { + return ( +
  • + {updateInFlight} +
  • + ); + })} +
+
+

State

+
+
{JSON.stringify(space.state, null, 2)}
+
+
+

Events

+
    + {space.events.map((event) => { + return ( +
  • +
    {JSON.stringify(event, null, 2)}
    +
  • + ); + })} +
+ + )} + + ); +} diff --git a/apps/privy-login-example/src/components/event.tsx b/apps/privy-login-example/src/components/event.tsx new file mode 100644 index 00000000..5ba8bda4 --- /dev/null +++ b/apps/privy-login-example/src/components/event.tsx @@ -0,0 +1,30 @@ +import { useEntity } from '@graphprotocol/hypergraph-react'; +import { Event as EventType } from '../schema'; + +export const Event = ({ spaceId, entityId }: { spaceId: string; entityId: string }) => { + const { data, isPending, isError } = useEntity(EventType, { + mode: 'public', + include: { + sponsors: { + jobOffers: {}, + }, + }, + id: entityId, + space: spaceId, + }); + + console.log({ component: 'Event', isPending, isError, data }); + + return ( +
+ {isPending &&
Loading...
} + {isError &&
Error
} + {data && ( +
+

Event Details

+
{JSON.stringify(data, null, 2)}
+
+ )} +
+ ); +}; diff --git a/apps/privy-login-example/src/components/events/events.tsx b/apps/privy-login-example/src/components/events/events.tsx new file mode 100644 index 00000000..90ecc163 --- /dev/null +++ b/apps/privy-login-example/src/components/events/events.tsx @@ -0,0 +1,91 @@ +import { + preparePublish, + publishOps, + useCreateEntity, + useHypergraphApp, + useQuery, + useSpaces, +} from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { Event } from '../../schema'; +import { Button } from '../ui/button'; +import { Input } from '../ui/input'; +import { Label } from '../ui/label'; + +export const Events = () => { + const { data: eventsLocalData } = useQuery(Event, { mode: 'private' }); + const createEvent = useCreateEntity(Event); + const { getSmartSessionClient } = useHypergraphApp(); + const { data: spaces } = useSpaces({ mode: 'public' }); + const [selectedSpace, setSelectedSpace] = useState(''); + + const handlePublish = async (event: Event) => { + if (!selectedSpace) { + alert('No space selected'); + return; + } + const { ops } = await preparePublish({ entity: event, publicSpace: selectedSpace }); + const smartSessionClient = await getSmartSessionClient(); + if (!smartSessionClient) { + throw new Error('Missing smartSessionClient'); + } + const publishResult = await publishOps({ + ops, + space: selectedSpace, + name: 'Publish Event', + walletClient: smartSessionClient, + }); + console.log(publishResult, ops); + }; + + return ( + <> +

Events (Local)

+ {eventsLocalData.map((event) => ( +
+

{event.name}

+

{event.description}

+
{event.id}
+ + +
+ ))} +
{ + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const name = formData.get('name') as string; + const description = formData.get('description') as string; + if (!name) { + alert('Name is required'); + return; + } + if (description) { + createEvent({ name, description }); + } else { + createEvent({ name }); + } + }} + > + + + + + +
+ + ); +}; diff --git a/apps/privy-login-example/src/components/logout.tsx b/apps/privy-login-example/src/components/logout.tsx new file mode 100644 index 00000000..fa92a77a --- /dev/null +++ b/apps/privy-login-example/src/components/logout.tsx @@ -0,0 +1,20 @@ +import { useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { useRouter } from '@tanstack/react-router'; +import { Button } from './ui/button'; + +export function Logout() { + const { logout: graphLogout } = useHypergraphApp(); + const router = useRouter(); + const disconnectWallet = async () => { + graphLogout(); // needs to be called after privy logout since it triggers a re-render + router.navigate({ + to: '/login', + }); + }; + + return ( + + ); +} diff --git a/apps/privy-login-example/src/components/playground.tsx b/apps/privy-login-example/src/components/playground.tsx new file mode 100644 index 00000000..1ecaf4d9 --- /dev/null +++ b/apps/privy-login-example/src/components/playground.tsx @@ -0,0 +1,91 @@ +import { + _useCreateEntityPublic, + _useDeleteEntityPublic, + useHypergraphApp, + useQuery, + useSpace, +} from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { Event } from '../schema'; +import { Button } from './ui/button'; + +export const Playground = ({ spaceId }: { spaceId: string }) => { + const { data, isLoading, isError } = useQuery(Event, { + mode: 'public', + include: { + sponsors: { + jobOffers: {}, + }, + }, + // filter: { + // or: [{ name: { startsWith: 'test' } }, { name: { startsWith: 'ETH' } }], + // }, + first: 100, + space: spaceId, + }); + const [isDeleting, setIsDeleting] = useState(false); + const [isCreating, setIsCreating] = useState(false); + const { getSmartSessionClient } = useHypergraphApp(); + + const { name } = useSpace({ mode: 'public', space: spaceId }); + + const deleteEntity = _useDeleteEntityPublic(Event, { space: spaceId }); + const createEntity = _useCreateEntityPublic(Event, { space: spaceId }); + + console.log({ isLoading, isError, data }); + + return ( +
+

Space: {name}

+ {isLoading &&
Loading...
} + {isError &&
Error
} + + {data?.map((event) => ( +
+

{event.name}

+ +
{JSON.stringify(event, null, 2)}
+
+ ))} +
+ ); +}; diff --git a/apps/privy-login-example/src/components/spinner.tsx b/apps/privy-login-example/src/components/spinner.tsx new file mode 100644 index 00000000..5c3247c8 --- /dev/null +++ b/apps/privy-login-example/src/components/spinner.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; + +interface SpinnerProps { + size?: 'sm' | 'md' | 'lg' | 'xl'; + color?: 'default' | 'primary' | 'secondary' | 'accent' | 'white'; + className?: string; +} + +export function Spinner({ size = 'md', color = 'primary', className }: SpinnerProps) { + const sizeClasses = { + sm: 'h-4 w-4 border-2', + md: 'h-6 w-6 border-2', + lg: 'h-8 w-8 border-3', + xl: 'h-12 w-12 border-4', + }; + + const colorClasses = { + default: 'border-muted-foreground/30 border-t-muted-foreground', + primary: 'border-primary/30 border-t-primary', + secondary: 'border-secondary/30 border-t-secondary', + accent: 'border-accent/30 border-t-accent', + white: 'border-white/30 border-t-white', + }; + + return ( + + Loading... + + ); +} diff --git a/apps/privy-login-example/src/components/todo/todos-local.tsx b/apps/privy-login-example/src/components/todo/todos-local.tsx new file mode 100644 index 00000000..4ae800ce --- /dev/null +++ b/apps/privy-login-example/src/components/todo/todos-local.tsx @@ -0,0 +1,52 @@ +import { useHardDeleteEntity, useQuery, useUpdateEntity } from '@graphprotocol/hypergraph-react'; +import { Todo2 } from '../../schema'; +import { Button } from '../ui/button'; + +export const TodosLocal = () => { + const updateEntity = useUpdateEntity(Todo2); + const hardDeleteEntity = useHardDeleteEntity(); + const { data: todosLocalData, deleted: deletedTodosLocalData } = useQuery(Todo2, { mode: 'private' }); + + return ( + <> +

Todos (Local)

+ {todosLocalData.map((todo) => ( +
+

{todo.name}

+
{todo.id}
+
{todo.due.toLocaleDateString()}
+
{todo.amount}
+ {todo.point &&
{todo.point.join(', ')}
} + {todo.website &&
{todo.website.toString()}
} + {todo.assignees.map((assignee) => ( + + {assignee.name} + + ))} + updateEntity(todo.id, { checked: e.target.checked })} + /> + {/* @ts-expect-error */} +
{todo.__deleted ? 'deleted' : 'not deleted'}
+ {/* @ts-expect-error */} +
{todo.__version}
+ +
+ ))} +

Deleted Todos (Local)

+ {deletedTodosLocalData.map((todo) => ( +
+

{todo.name}

+
{todo.id}
+ +
+ ))} + + ); +}; diff --git a/apps/privy-login-example/src/components/todo/todos-public.tsx b/apps/privy-login-example/src/components/todo/todos-public.tsx new file mode 100644 index 00000000..42d4473d --- /dev/null +++ b/apps/privy-login-example/src/components/todo/todos-public.tsx @@ -0,0 +1,62 @@ +import { _generateDeleteOps, publishOps, useHypergraphApp, useQuery, useSpace } from '@graphprotocol/hypergraph-react'; +import { Todo2 } from '../../schema'; +import { Spinner } from '../spinner'; +import { Button } from '../ui/button'; + +export const TodosPublic = () => { + const { id: spaceId } = useSpace({ mode: 'public' }); + const { getSmartSessionClient } = useHypergraphApp(); + const { + data: dataPublic, + isLoading: isLoadingPublic, + isError: isErrorPublic, + } = useQuery(Todo2, { + mode: 'public', + include: { assignees: {} }, + }); + + return ( + <> +
+

Todos (Public Geo)

+ {isLoadingPublic && } +
+ {isErrorPublic &&
Error loading todos
} + {dataPublic.map((todo) => ( +
+

{todo.name}

+
{todo.id}
+
{todo.due.toLocaleDateString()}
+
{todo.amount}
+ {todo.point &&
{todo.point.join(', ')}
} + {todo.website &&
{todo.website.toString()}
} + + {todo.assignees.map((assignee) => ( + + {assignee.name} + + ))} + + +
+ ))} + + ); +}; diff --git a/apps/privy-login-example/src/components/todos-read-only-filter.tsx b/apps/privy-login-example/src/components/todos-read-only-filter.tsx new file mode 100644 index 00000000..7b3b31ca --- /dev/null +++ b/apps/privy-login-example/src/components/todos-read-only-filter.tsx @@ -0,0 +1,51 @@ +import { useQuery } from '@graphprotocol/hypergraph-react'; +import { Todo } from '../schema'; + +export const TodosReadOnlyFilter = () => { + const { data: todosCompleted } = useQuery(Todo, { mode: 'private', filter: { completed: { is: true } } }); + const { data: todosNotCompleted } = useQuery(Todo, { + mode: 'private', + filter: { completed: { is: false } }, + }); + + return ( + <> +

Todos Filter (read only)

+

Not completed

+ {todosNotCompleted.map((todo) => ( +
+

{todo.name}

+ {todo.assignees.length > 0 && ( + + Assigned to:{' '} + {todo.assignees.map((assignee) => ( + + {assignee.name} + + ))} + + )} + +
+ ))} + +

Completed

+ {todosCompleted.map((todo) => ( +
+

{todo.name}

+ {todo.assignees.length > 0 && ( + + Assigned to:{' '} + {todo.assignees.map((assignee) => ( + + {assignee.name} + + ))} + + )} + +
+ ))} + + ); +}; diff --git a/apps/privy-login-example/src/components/todos-read-only.tsx b/apps/privy-login-example/src/components/todos-read-only.tsx new file mode 100644 index 00000000..46eb7a1c --- /dev/null +++ b/apps/privy-login-example/src/components/todos-read-only.tsx @@ -0,0 +1,28 @@ +import { useQuery } from '@graphprotocol/hypergraph-react'; +import { Todo } from '../schema'; + +export const TodosReadOnly = () => { + const { data: todos } = useQuery(Todo, { mode: 'private' }); + + return ( + <> +

Todos (read only)

+ {todos.map((todo) => ( +
+

{todo.name}

+ {todo.assignees.length > 0 && ( + + Assigned to:{' '} + {todo.assignees.map((assignee) => ( + + {assignee.name} + + ))} + + )} + +
+ ))} + + ); +}; diff --git a/apps/privy-login-example/src/components/todos.tsx b/apps/privy-login-example/src/components/todos.tsx new file mode 100644 index 00000000..bda38ed6 --- /dev/null +++ b/apps/privy-login-example/src/components/todos.tsx @@ -0,0 +1,99 @@ +import { + useCreateEntity, + useDeleteEntity, + useQuery, + useRemoveRelation, + useSpace, + useUpdateEntity, +} from '@graphprotocol/hypergraph-react'; +import { useEffect, useState } from 'react'; +import Select from 'react-select'; +import { Todo, User } from '../schema'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; + +export const Todos = () => { + const { data: todos } = useQuery(Todo, { + mode: 'private', + include: { assignees: {} }, + // filter: { or: [{ name: { startsWith: 'aa' } }, { name: { is: 'sdasd' } }] }, + }); + const { data: users } = useQuery(User, { mode: 'private' }); + const { ready: spaceReady } = useSpace({ mode: 'private' }); + const createEntity = useCreateEntity(Todo); + const updateEntity = useUpdateEntity(Todo); + const deleteEntity = useDeleteEntity(); + const removeRelation = useRemoveRelation(); + const [newTodoName, setNewTodoName] = useState(''); + const [assignees, setAssignees] = useState<{ value: string; label: string }[]>([]); + + useEffect(() => { + setAssignees((prevFilteredAssignees) => { + // filter out assignees that are not in the users array whenever users change + return prevFilteredAssignees.filter((assignee) => users.some((user) => user.id === assignee.value)); + }); + }, [users]); + + if (!spaceReady) { + return
Loading space...
; + } + + const userOptions = users.map((user) => ({ value: user.id, label: user.name })); + return ( + <> +

Todos

+
+ setNewTodoName(e.target.value)} /> + updateEntity(todo.id, { completed: e.target.checked })} + /> + +
+ ))} + + ); +}; diff --git a/apps/privy-login-example/src/components/todos2.tsx b/apps/privy-login-example/src/components/todos2.tsx new file mode 100644 index 00000000..356530ce --- /dev/null +++ b/apps/privy-login-example/src/components/todos2.tsx @@ -0,0 +1,273 @@ +import type { PublishDiffInfo } from '@graphprotocol/hypergraph-react'; +import { + PublishDiff, + publishOps, + useCreateEntity, + useDeleteEntity, + useHypergraphApp, + useQuery, + useSpace, + useUpdateEntity, +} from '@graphprotocol/hypergraph-react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; +import Select from 'react-select'; +import { cn } from '@/lib/utils'; +import { Todo2, User } from '../schema'; +import { Spinner } from './spinner'; +import { TodosLocal } from './todo/todos-local'; +import { TodosPublic } from './todo/todos-public'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Modal } from './ui/modal'; + +export const Todos2 = () => { + const { + data: dataTodos, + isLoading: isLoadingTodos, + isError: isErrorTodos, + // preparePublish: preparePublishTodos, + } = useQuery(Todo2, { mode: 'private', include: { assignees: {} } }); + const { + data: dataUsers, + isLoading: isLoadingUsers, + isError: isErrorUsers, + // preparePublish: preparePublishUsers, + } = useQuery(User, { mode: 'private' }); + const { ready: spaceReady, id: spaceId } = useSpace({ mode: 'private' }); + const { getSmartSessionClient } = useHypergraphApp(); + const createTodo = useCreateEntity(Todo2); + const updateTodo = useUpdateEntity(Todo2); + const createUser = useCreateEntity(User); + const deleteEntity = useDeleteEntity(); + const [newTodoName, setNewTodoName] = useState(''); + const [newTodoAssignees, setNewTodoAssignees] = useState<{ value: string; label: string }[]>([]); + const [newUserName, setNewUserName] = useState(''); + const queryClient = useQueryClient(); + const [publishData, setPublishData] = useState(null); + const [isPublishDiffModalOpen, setIsPublishDiffModalOpen] = useState(false); + const [isPreparingPublish, setIsPreparingPublish] = useState(false); + const [isPublishing, setIsPublishing] = useState(false); + + useEffect(() => { + setNewTodoAssignees((prevFilteredAssignees) => { + // filter out assignees that are not in the users array whenever users change + return prevFilteredAssignees.filter((assignee) => dataUsers.some((user) => user.id === assignee.value)); + }); + }, [dataUsers]); + + const userOptions = dataUsers.map((user) => ({ value: user.id, label: user.name })); + + if (!spaceReady) { + return
Loading space...
; + } + + return ( + <> +
+

Users (Merged)

+ {isLoadingUsers && } +
+ {isErrorUsers &&
Error loading todos
} +
+ {dataUsers.map((user) => ( +
+

{user.name}

+
{user.id}
+ {/* @ts-expect-error */} +
{user.__version}
+ +
+ ))} +
+ +
+ setNewUserName(e.target.value)} /> + +
+ +
+

Todos (Merged)

+ {isLoadingTodos && } +
+ {isErrorTodos &&
Error loading todos
} +
+ {dataTodos.map((todo) => ( +
+

{todo.name}

+
{todo.id}
+ updateTodo(todo.id, { checked: e.target.checked })} + /> +
{todo.due.toLocaleDateString()}
+
{todo.amount}
+ {todo.point &&
{todo.point.join(', ')}
} + {todo.website &&
{todo.website.toString()}
} + {todo.assignees.length > 0 && ( + + Assigned to:{' '} + {todo.assignees.map((assignee) => ( + + {assignee.name} + + + ))} + + )} + {/* @ts-expect-error */} +
{todo.__version}
+ +
+ ))} +
+ +
+ setNewTodoName(e.target.value)} /> + + ); +}); +Input.displayName = 'Input'; + +export { Input }; diff --git a/apps/privy-login-example/src/components/ui/label.tsx b/apps/privy-login-example/src/components/ui/label.tsx new file mode 100644 index 00000000..713b8db6 --- /dev/null +++ b/apps/privy-login-example/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +import * as LabelPrimitive from '@radix-ui/react-label'; +import type * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Label({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/apps/privy-login-example/src/components/ui/modal.tsx b/apps/privy-login-example/src/components/ui/modal.tsx new file mode 100644 index 00000000..c3d3f37b --- /dev/null +++ b/apps/privy-login-example/src/components/ui/modal.tsx @@ -0,0 +1,40 @@ +import { useEffect } from 'react'; + +type ModalProps = { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + children: React.ReactNode; +}; + +export function Modal({ isOpen, onOpenChange, children }: ModalProps) { + useEffect(() => { + const handleEsc = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onOpenChange(false); + } + }; + + if (isOpen) { + document.addEventListener('keydown', handleEsc); + document.body.style.overflow = 'hidden'; + } + + return () => { + document.removeEventListener('keydown', handleEsc); + document.body.style.overflow = 'unset'; + }; + }, [isOpen, onOpenChange]); + + if (!isOpen) return null; + + return ( +
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: Modal has keyboard support via Escape key */} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: Modal has keyboard support via Escape key */} +
onOpenChange(false)} /> +
+
{children}
+
+
+ ); +} diff --git a/apps/privy-login-example/src/components/user-entry.tsx b/apps/privy-login-example/src/components/user-entry.tsx new file mode 100644 index 00000000..d631ed5b --- /dev/null +++ b/apps/privy-login-example/src/components/user-entry.tsx @@ -0,0 +1,25 @@ +import { useDeleteEntity, useUpdateEntity } from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { User } from '../schema.js'; +import { Button } from './ui/button'; +import { Input } from './ui/input.js'; + +export const UserEntry = (user: User & { id: string }) => { + const deleteEntity = useDeleteEntity(); + const updateEntity = useUpdateEntity(User); + const [editMode, setEditMode] = useState(false); + + return ( +
+

+ {user.name} ({user.id}) +

+ + + + {editMode && ( + updateEntity(user.id, { name: e.target.value })} /> + )} +
+ ); +}; diff --git a/apps/privy-login-example/src/components/users.tsx b/apps/privy-login-example/src/components/users.tsx new file mode 100644 index 00000000..056109f6 --- /dev/null +++ b/apps/privy-login-example/src/components/users.tsx @@ -0,0 +1,37 @@ +import { useCreateEntity, useQuery, useSpace } from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { User } from '../schema.js'; +import { Button } from './ui/button.js'; +import { Input } from './ui/input.js'; +import { UserEntry } from './user-entry.js'; + +export const Users = () => { + const { data: users } = useQuery(User, { mode: 'private' }); + const { ready: spaceReady } = useSpace({ mode: 'private' }); + const createEntity = useCreateEntity(User); + const [newUserName, setNewUserName] = useState(''); + + if (!spaceReady) { + return
Loading space...
; + } + + return ( + <> +

Users

+
+ setNewUserName(e.target.value)} /> + +
+ {users.map((user) => ( + + ))} + + ); +}; diff --git a/apps/privy-login-example/src/components/users/users-local.tsx b/apps/privy-login-example/src/components/users/users-local.tsx new file mode 100644 index 00000000..b05f53b9 --- /dev/null +++ b/apps/privy-login-example/src/components/users/users-local.tsx @@ -0,0 +1,37 @@ +import { useHardDeleteEntity, useQuery } from '@graphprotocol/hypergraph-react'; +import { User } from '../../schema'; +import { Button } from '../ui/button'; + +export const UsersLocal = () => { + const hardDeleteEntity = useHardDeleteEntity(); + const { data: usersLocalData, deleted: deletedUsersLocalData } = useQuery(User, { mode: 'private' }); + + return ( + <> +

Users (Local)

+ {usersLocalData.map((user) => ( +
+

{user.name}

+
{user.id}
+ {/* @ts-expect-error */} +
{user.__deleted ? 'deleted' : 'not deleted'}
+ {/* @ts-expect-error */} +
{user.__version}
+ +
+ ))} +

Deleted Users (Local)

+ {deletedUsersLocalData.map((user) => ( +
+

{user.name}

+
{user.id}
+ +
+ ))} + + ); +}; diff --git a/apps/privy-login-example/src/components/users/users-merged.tsx b/apps/privy-login-example/src/components/users/users-merged.tsx new file mode 100644 index 00000000..e5ec5642 --- /dev/null +++ b/apps/privy-login-example/src/components/users/users-merged.tsx @@ -0,0 +1,29 @@ +import { useDeleteEntity, useQuery } from '@graphprotocol/hypergraph-react'; +import { User } from '../../schema'; +import { Spinner } from '../spinner'; +import { Button } from '../ui/button'; + +export const UsersMerged = () => { + const { data, isLoading, isError } = useQuery(User, { mode: 'private' }); + const deleteEntity = useDeleteEntity(); + + return ( + <> +
+

Users (Merged)

+ {isLoading && } +
+ {isError &&
Error loading users
} + {data.map((user) => ( +
+

{user.name}

+
{user.id}
+ + +
+ ))} + + ); +}; diff --git a/apps/privy-login-example/src/components/users/users-public.tsx b/apps/privy-login-example/src/components/users/users-public.tsx new file mode 100644 index 00000000..7bfd9aad --- /dev/null +++ b/apps/privy-login-example/src/components/users/users-public.tsx @@ -0,0 +1,44 @@ +import { _generateDeleteOps, publishOps, useHypergraphApp, useQuery, useSpace } from '@graphprotocol/hypergraph-react'; +import { User } from '../../schema'; +import { Spinner } from '../spinner'; +import { Button } from '../ui/button'; + +export const UsersPublic = () => { + const { id: spaceId } = useSpace({ mode: 'public' }); + const { data: dataPublic, isLoading: isLoadingPublic, isError: isErrorPublic } = useQuery(User, { mode: 'public' }); + const { getSmartSessionClient } = useHypergraphApp(); + return ( + <> +
+

Users (Public Geo)

+ {isLoadingPublic && } +
+ {isErrorPublic &&
Error loading users
} + {dataPublic.map((user) => ( +
+

{user.name}

+
{user.id}
+ + +
+ ))} + + ); +}; diff --git a/apps/privy-login-example/src/index.css b/apps/privy-login-example/src/index.css new file mode 100644 index 00000000..a5b733f7 --- /dev/null +++ b/apps/privy-login-example/src/index.css @@ -0,0 +1,129 @@ +@import "tailwindcss"; + +@plugin 'tailwindcss-animate'; + +@source '../../../packages/hypergraph-react/src/**/*.{js,ts,jsx,tsx}'; + +@custom-variant dark (&:is(.dark *)); + +@theme { + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); + + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + + --color-chart-1: hsl(var(--chart-1)); + --color-chart-2: hsl(var(--chart-2)); + --color-chart-3: hsl(var(--chart-3)); + --color-chart-4: hsl(var(--chart-4)); + --color-chart-5: hsl(var(--chart-5)); +} + +/* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/privy-login-example/src/lib/create-ethereum-account.ts b/apps/privy-login-example/src/lib/create-ethereum-account.ts new file mode 100644 index 00000000..12c01ad6 --- /dev/null +++ b/apps/privy-login-example/src/lib/create-ethereum-account.ts @@ -0,0 +1,11 @@ +import { bytesToHex, randomBytes } from '@noble/hashes/utils'; +import { privateKeyToAccount } from 'viem/accounts'; + +export const createEthereumAccount = () => { + const privateKey = `0x${bytesToHex(randomBytes(32))}` as `0x${string}`; + const account = privateKeyToAccount(privateKey); + return { + privateKey, + address: account.address, + }; +}; diff --git a/apps/privy-login-example/src/lib/utils.ts b/apps/privy-login-example/src/lib/utils.ts new file mode 100644 index 00000000..9ad0df42 --- /dev/null +++ b/apps/privy-login-example/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/apps/privy-login-example/src/main.test.ts b/apps/privy-login-example/src/main.test.ts new file mode 100644 index 00000000..07b9190a --- /dev/null +++ b/apps/privy-login-example/src/main.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest'; + +test('hello', () => { + expect('Hello from hypergraph').toBe('Hello from hypergraph'); +}); diff --git a/apps/privy-login-example/src/main.tsx b/apps/privy-login-example/src/main.tsx new file mode 100644 index 00000000..c0cc1602 --- /dev/null +++ b/apps/privy-login-example/src/main.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client'; +import { Boot } from './Boot.js'; +import './index.css'; + +// biome-ignore lint/style/noNonNullAssertion: root element is always there +createRoot(document.getElementById('root')!).render( + // + , + // +); diff --git a/apps/privy-login-example/src/mapping.ts b/apps/privy-login-example/src/mapping.ts new file mode 100644 index 00000000..849a04a8 --- /dev/null +++ b/apps/privy-login-example/src/mapping.ts @@ -0,0 +1,69 @@ +import type { Mapping } from '@graphprotocol/hypergraph'; +import { Id } from '@graphprotocol/hypergraph'; + +export const mapping: Mapping.Mapping = { + User: { + typeIds: [Id('bffa181e-a333-495b-949c-57f2831d7eca')], + properties: { + name: Id('c9c79675-850a-42c5-bbbd-9e5c55d3f4e7'), + }, + }, + Todo: { + typeIds: [Id('44fe82a9-e4c2-4330-a395-ce85ed78e421')], + properties: { + name: Id('c668aa67-bbca-4b2c-908c-9c5599035eab'), + completed: Id('71e7654f-2623-4794-88fb-841c8f3dd9b4'), + }, + relations: { + assignees: Id('5b80d3ee-2463-4246-b628-44ba808ab3e1'), + }, + }, + Todo2: { + typeIds: [Id('210f4e94-234c-49d7-af0f-f3b74fb07650')], + properties: { + name: Id('e291f4da-632d-4b70-aca8-5c6c01dbf1ca'), + checked: Id('d1cc82ef-8bde-45f4-b31c-56b6d59279ec'), + due: Id('6a28f275-b31c-47bc-83cd-ad416aaa7073'), + amount: Id('0c8219be-e284-4738-bd95-91a1c113c78e'), + point: Id('7f032477-c60e-4c85-a161-019b70db05ca'), + website: Id('75b6a647-5c2b-41e7-92c0-b0a0c9b28b02'), + }, + relations: { + assignees: Id('1115e9f8-db2e-41df-8969-c5d34c367c10'), + }, + }, + JobOffer: { + typeIds: [Id('f60585af-71b6-4674-9a26-b74ca6c1cceb')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + salary: Id('baa36ac9-78ac-4cf7-8394-6b2d3006bebe'), + }, + }, + Company: { + typeIds: [Id('6c504df5-1a8f-43d1-bf2d-1ef9fa5b08b5')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + }, + relations: { + jobOffers: Id('1203064e-9741-4235-89d4-97f4b22eddfb'), + }, + }, + Event: { + typeIds: [Id('7f9562d4-034d-4385-bf5c-f02cdebba47a')], + properties: { + name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'), + description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'), + }, + relations: { + sponsors: Id('6860bfac-f703-4289-b789-972d0aaf3abe'), + }, + }, + Todo3: { + typeIds: [Id('4f7bba76-7855-4d63-b59d-1d9f2be866df')], + properties: { + name: Id('47006386-d351-411c-8287-1dae1c1aa8c1'), + completed: Id('9f9f00eb-4f32-4f71-92ba-b266566d0013'), + description: Id('89cac80a-1dbd-4bca-97b2-45e1556d9122'), + }, + }, +}; diff --git a/apps/privy-login-example/src/routeTree.gen.ts b/apps/privy-login-example/src/routeTree.gen.ts new file mode 100644 index 00000000..de3d360a --- /dev/null +++ b/apps/privy-login-example/src/routeTree.gen.ts @@ -0,0 +1,327 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { createFileRoute } from '@tanstack/react-router' + +import { Route as rootRouteImport } from './routes/__root' +import { Route as AuthenticateSuccessRouteImport } from './routes/authenticate-success' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SpaceSpaceIdRouteImport } from './routes/space/$spaceId' +import { Route as FriendsAccountAddressRouteImport } from './routes/friends/$accountAddress' +import { Route as AccountInboxInboxIdRouteImport } from './routes/account-inbox/$inboxId' +import { Route as SpaceSpaceIdIndexRouteImport } from './routes/space/$spaceId/index' +import { Route as SpaceSpaceIdUsersRouteImport } from './routes/space/$spaceId/users' +import { Route as SpaceSpaceIdPublicIntegrationRouteImport } from './routes/space/$spaceId/public-integration' +import { Route as SpaceSpaceIdPlaygroundRouteImport } from './routes/space/$spaceId/playground' +import { Route as SpaceSpaceIdEventsRouteImport } from './routes/space/$spaceId/events' +import { Route as SpaceSpaceIdChatRouteImport } from './routes/space/$spaceId/chat' + +const PlaygroundLazyRouteImport = createFileRoute('/playground')() +const LoginLazyRouteImport = createFileRoute('/login')() + +const PlaygroundLazyRoute = PlaygroundLazyRouteImport.update({ + id: '/playground', + path: '/playground', + getParentRoute: () => rootRouteImport, +} as any).lazy(() => import('./routes/playground.lazy').then((d) => d.Route)) +const LoginLazyRoute = LoginLazyRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any).lazy(() => import('./routes/login.lazy').then((d) => d.Route)) +const AuthenticateSuccessRoute = AuthenticateSuccessRouteImport.update({ + id: '/authenticate-success', + path: '/authenticate-success', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SpaceSpaceIdRoute = SpaceSpaceIdRouteImport.update({ + id: '/space/$spaceId', + path: '/space/$spaceId', + getParentRoute: () => rootRouteImport, +} as any) +const FriendsAccountAddressRoute = FriendsAccountAddressRouteImport.update({ + id: '/friends/$accountAddress', + path: '/friends/$accountAddress', + getParentRoute: () => rootRouteImport, +} as any) +const AccountInboxInboxIdRoute = AccountInboxInboxIdRouteImport.update({ + id: '/account-inbox/$inboxId', + path: '/account-inbox/$inboxId', + getParentRoute: () => rootRouteImport, +} as any) +const SpaceSpaceIdIndexRoute = SpaceSpaceIdIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => SpaceSpaceIdRoute, +} as any) +const SpaceSpaceIdUsersRoute = SpaceSpaceIdUsersRouteImport.update({ + id: '/users', + path: '/users', + getParentRoute: () => SpaceSpaceIdRoute, +} as any) +const SpaceSpaceIdPublicIntegrationRoute = + SpaceSpaceIdPublicIntegrationRouteImport.update({ + id: '/public-integration', + path: '/public-integration', + getParentRoute: () => SpaceSpaceIdRoute, + } as any) +const SpaceSpaceIdPlaygroundRoute = SpaceSpaceIdPlaygroundRouteImport.update({ + id: '/playground', + path: '/playground', + getParentRoute: () => SpaceSpaceIdRoute, +} as any) +const SpaceSpaceIdEventsRoute = SpaceSpaceIdEventsRouteImport.update({ + id: '/events', + path: '/events', + getParentRoute: () => SpaceSpaceIdRoute, +} as any) +const SpaceSpaceIdChatRoute = SpaceSpaceIdChatRouteImport.update({ + id: '/chat', + path: '/chat', + getParentRoute: () => SpaceSpaceIdRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/authenticate-success': typeof AuthenticateSuccessRoute + '/login': typeof LoginLazyRoute + '/playground': typeof PlaygroundLazyRoute + '/account-inbox/$inboxId': typeof AccountInboxInboxIdRoute + '/friends/$accountAddress': typeof FriendsAccountAddressRoute + '/space/$spaceId': typeof SpaceSpaceIdRouteWithChildren + '/space/$spaceId/chat': typeof SpaceSpaceIdChatRoute + '/space/$spaceId/events': typeof SpaceSpaceIdEventsRoute + '/space/$spaceId/playground': typeof SpaceSpaceIdPlaygroundRoute + '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute + '/space/$spaceId/users': typeof SpaceSpaceIdUsersRoute + '/space/$spaceId/': typeof SpaceSpaceIdIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/authenticate-success': typeof AuthenticateSuccessRoute + '/login': typeof LoginLazyRoute + '/playground': typeof PlaygroundLazyRoute + '/account-inbox/$inboxId': typeof AccountInboxInboxIdRoute + '/friends/$accountAddress': typeof FriendsAccountAddressRoute + '/space/$spaceId/chat': typeof SpaceSpaceIdChatRoute + '/space/$spaceId/events': typeof SpaceSpaceIdEventsRoute + '/space/$spaceId/playground': typeof SpaceSpaceIdPlaygroundRoute + '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute + '/space/$spaceId/users': typeof SpaceSpaceIdUsersRoute + '/space/$spaceId': typeof SpaceSpaceIdIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/authenticate-success': typeof AuthenticateSuccessRoute + '/login': typeof LoginLazyRoute + '/playground': typeof PlaygroundLazyRoute + '/account-inbox/$inboxId': typeof AccountInboxInboxIdRoute + '/friends/$accountAddress': typeof FriendsAccountAddressRoute + '/space/$spaceId': typeof SpaceSpaceIdRouteWithChildren + '/space/$spaceId/chat': typeof SpaceSpaceIdChatRoute + '/space/$spaceId/events': typeof SpaceSpaceIdEventsRoute + '/space/$spaceId/playground': typeof SpaceSpaceIdPlaygroundRoute + '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute + '/space/$spaceId/users': typeof SpaceSpaceIdUsersRoute + '/space/$spaceId/': typeof SpaceSpaceIdIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/authenticate-success' + | '/login' + | '/playground' + | '/account-inbox/$inboxId' + | '/friends/$accountAddress' + | '/space/$spaceId' + | '/space/$spaceId/chat' + | '/space/$spaceId/events' + | '/space/$spaceId/playground' + | '/space/$spaceId/public-integration' + | '/space/$spaceId/users' + | '/space/$spaceId/' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/authenticate-success' + | '/login' + | '/playground' + | '/account-inbox/$inboxId' + | '/friends/$accountAddress' + | '/space/$spaceId/chat' + | '/space/$spaceId/events' + | '/space/$spaceId/playground' + | '/space/$spaceId/public-integration' + | '/space/$spaceId/users' + | '/space/$spaceId' + id: + | '__root__' + | '/' + | '/authenticate-success' + | '/login' + | '/playground' + | '/account-inbox/$inboxId' + | '/friends/$accountAddress' + | '/space/$spaceId' + | '/space/$spaceId/chat' + | '/space/$spaceId/events' + | '/space/$spaceId/playground' + | '/space/$spaceId/public-integration' + | '/space/$spaceId/users' + | '/space/$spaceId/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AuthenticateSuccessRoute: typeof AuthenticateSuccessRoute + LoginLazyRoute: typeof LoginLazyRoute + PlaygroundLazyRoute: typeof PlaygroundLazyRoute + AccountInboxInboxIdRoute: typeof AccountInboxInboxIdRoute + FriendsAccountAddressRoute: typeof FriendsAccountAddressRoute + SpaceSpaceIdRoute: typeof SpaceSpaceIdRouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/playground': { + id: '/playground' + path: '/playground' + fullPath: '/playground' + preLoaderRoute: typeof PlaygroundLazyRouteImport + parentRoute: typeof rootRouteImport + } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginLazyRouteImport + parentRoute: typeof rootRouteImport + } + '/authenticate-success': { + id: '/authenticate-success' + path: '/authenticate-success' + fullPath: '/authenticate-success' + preLoaderRoute: typeof AuthenticateSuccessRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/space/$spaceId': { + id: '/space/$spaceId' + path: '/space/$spaceId' + fullPath: '/space/$spaceId' + preLoaderRoute: typeof SpaceSpaceIdRouteImport + parentRoute: typeof rootRouteImport + } + '/friends/$accountAddress': { + id: '/friends/$accountAddress' + path: '/friends/$accountAddress' + fullPath: '/friends/$accountAddress' + preLoaderRoute: typeof FriendsAccountAddressRouteImport + parentRoute: typeof rootRouteImport + } + '/account-inbox/$inboxId': { + id: '/account-inbox/$inboxId' + path: '/account-inbox/$inboxId' + fullPath: '/account-inbox/$inboxId' + preLoaderRoute: typeof AccountInboxInboxIdRouteImport + parentRoute: typeof rootRouteImport + } + '/space/$spaceId/': { + id: '/space/$spaceId/' + path: '/' + fullPath: '/space/$spaceId/' + preLoaderRoute: typeof SpaceSpaceIdIndexRouteImport + parentRoute: typeof SpaceSpaceIdRoute + } + '/space/$spaceId/users': { + id: '/space/$spaceId/users' + path: '/users' + fullPath: '/space/$spaceId/users' + preLoaderRoute: typeof SpaceSpaceIdUsersRouteImport + parentRoute: typeof SpaceSpaceIdRoute + } + '/space/$spaceId/public-integration': { + id: '/space/$spaceId/public-integration' + path: '/public-integration' + fullPath: '/space/$spaceId/public-integration' + preLoaderRoute: typeof SpaceSpaceIdPublicIntegrationRouteImport + parentRoute: typeof SpaceSpaceIdRoute + } + '/space/$spaceId/playground': { + id: '/space/$spaceId/playground' + path: '/playground' + fullPath: '/space/$spaceId/playground' + preLoaderRoute: typeof SpaceSpaceIdPlaygroundRouteImport + parentRoute: typeof SpaceSpaceIdRoute + } + '/space/$spaceId/events': { + id: '/space/$spaceId/events' + path: '/events' + fullPath: '/space/$spaceId/events' + preLoaderRoute: typeof SpaceSpaceIdEventsRouteImport + parentRoute: typeof SpaceSpaceIdRoute + } + '/space/$spaceId/chat': { + id: '/space/$spaceId/chat' + path: '/chat' + fullPath: '/space/$spaceId/chat' + preLoaderRoute: typeof SpaceSpaceIdChatRouteImport + parentRoute: typeof SpaceSpaceIdRoute + } + } +} + +interface SpaceSpaceIdRouteChildren { + SpaceSpaceIdChatRoute: typeof SpaceSpaceIdChatRoute + SpaceSpaceIdEventsRoute: typeof SpaceSpaceIdEventsRoute + SpaceSpaceIdPlaygroundRoute: typeof SpaceSpaceIdPlaygroundRoute + SpaceSpaceIdPublicIntegrationRoute: typeof SpaceSpaceIdPublicIntegrationRoute + SpaceSpaceIdUsersRoute: typeof SpaceSpaceIdUsersRoute + SpaceSpaceIdIndexRoute: typeof SpaceSpaceIdIndexRoute +} + +const SpaceSpaceIdRouteChildren: SpaceSpaceIdRouteChildren = { + SpaceSpaceIdChatRoute: SpaceSpaceIdChatRoute, + SpaceSpaceIdEventsRoute: SpaceSpaceIdEventsRoute, + SpaceSpaceIdPlaygroundRoute: SpaceSpaceIdPlaygroundRoute, + SpaceSpaceIdPublicIntegrationRoute: SpaceSpaceIdPublicIntegrationRoute, + SpaceSpaceIdUsersRoute: SpaceSpaceIdUsersRoute, + SpaceSpaceIdIndexRoute: SpaceSpaceIdIndexRoute, +} + +const SpaceSpaceIdRouteWithChildren = SpaceSpaceIdRoute._addFileChildren( + SpaceSpaceIdRouteChildren, +) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AuthenticateSuccessRoute: AuthenticateSuccessRoute, + LoginLazyRoute: LoginLazyRoute, + PlaygroundLazyRoute: PlaygroundLazyRoute, + AccountInboxInboxIdRoute: AccountInboxInboxIdRoute, + FriendsAccountAddressRoute: FriendsAccountAddressRoute, + SpaceSpaceIdRoute: SpaceSpaceIdRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/apps/privy-login-example/src/routes/__root.tsx b/apps/privy-login-example/src/routes/__root.tsx new file mode 100644 index 00000000..858a421e --- /dev/null +++ b/apps/privy-login-example/src/routes/__root.tsx @@ -0,0 +1,63 @@ +import { Logout } from '@/components/logout'; +import { usePrivy } from '@privy-io/react-auth'; +import { createRootRoute, Link, Outlet, useLayoutEffect, useRouter } from '@tanstack/react-router'; +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; + +export const Route = createRootRoute({ + component: () => { + const { authenticated, ready } = usePrivy(); + const router = useRouter(); + + useLayoutEffect(() => { + if (router.state.location.href.startsWith('/login')) { + return; + } + + if (ready && !authenticated) { + router.navigate({ + to: '/login', + }); + } + }, [authenticated, ready]); + + return ( +
+
+ + Home + + +
+
+ + + + + +
+

© 2025 Acme Events. All rights reserved.

+ +
+
+ ); + }, +}); diff --git a/apps/privy-login-example/src/routes/account-inbox/$inboxId.tsx b/apps/privy-login-example/src/routes/account-inbox/$inboxId.tsx new file mode 100644 index 00000000..ae2720e3 --- /dev/null +++ b/apps/privy-login-example/src/routes/account-inbox/$inboxId.tsx @@ -0,0 +1,49 @@ +import { useHypergraphAuth, useOwnAccountInbox } from '@graphprotocol/hypergraph-react'; +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/account-inbox/$inboxId')({ + component: RouteComponent, +}); + +function RouteComponent() { + const { inboxId } = Route.useParams(); + const { identity } = useHypergraphAuth(); + const { messages, loading, error } = useOwnAccountInbox(inboxId); + + // Ensure we have an authenticated user + if (!identity?.accountAddress) { + return
Please login to view your inbox
; + } + + if (loading) { + return
Loading inbox messages...
; + } + + if (error) { + return
Error: {error.message}
; + } + + if (!messages) { + return
Inbox not found
; + } + + return ( +
+

Inbox Messages

+
+ {messages.map((message) => ( +
+
{message.plaintext}
+
+
{new Date(message.createdAt).toLocaleString()}
+ {message.authorAccountAddress &&
From: {message.authorAccountAddress}
} +
+
+ ))} + {messages.length === 0 && ( +
No messages in this inbox
+ )} +
+
+ ); +} diff --git a/apps/privy-login-example/src/routes/authenticate-success.tsx b/apps/privy-login-example/src/routes/authenticate-success.tsx new file mode 100644 index 00000000..b546efec --- /dev/null +++ b/apps/privy-login-example/src/routes/authenticate-success.tsx @@ -0,0 +1,27 @@ +import { useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import { useEffect } from 'react'; + +export const Route = createFileRoute('/authenticate-success')({ + component: RouteComponent, + validateSearch: (search: Record): { ciphertext: string; nonce: string } => { + return { + ciphertext: search.ciphertext as string, + nonce: search.nonce as string, + }; + }, +}); + +function RouteComponent() { + const { ciphertext, nonce } = Route.useSearch(); + const { processConnectAuthSuccess } = useHypergraphApp(); + const navigate = useNavigate(); + + useEffect(() => { + processConnectAuthSuccess({ storage: localStorage, ciphertext, nonce }); + console.log('redirecting to /'); + navigate({ to: '/', replace: true }); + }, [ciphertext, nonce, processConnectAuthSuccess, navigate]); + + return
Authenticating …
; +} diff --git a/apps/privy-login-example/src/routes/friends/$accountAddress.tsx b/apps/privy-login-example/src/routes/friends/$accountAddress.tsx new file mode 100644 index 00000000..92dcf2dc --- /dev/null +++ b/apps/privy-login-example/src/routes/friends/$accountAddress.tsx @@ -0,0 +1,36 @@ +import { usePublicAccountInboxes } from '@graphprotocol/hypergraph-react'; +import { createFileRoute } from '@tanstack/react-router'; +import { InboxCard } from '../../components/InboxCard'; + +export const Route = createFileRoute('/friends/$accountAddress')({ + component: RouteComponent, +}); + +function RouteComponent() { + const { accountAddress } = Route.useParams(); + const { publicInboxes, loading, error } = usePublicAccountInboxes(accountAddress); + + if (loading) { + return
Loading...
; + } + + if (error) { + return
Error: {error.message}
; + } + + if (publicInboxes.length === 0) { + return
No public inboxes found
; + } + + return ( +
+

Friend's Public Inboxes: {accountAddress}

+ +
+ {publicInboxes.map((inbox: { inboxId: string }) => ( + + ))} +
+
+ ); +} diff --git a/apps/privy-login-example/src/routes/index.tsx b/apps/privy-login-example/src/routes/index.tsx new file mode 100644 index 00000000..c504c645 --- /dev/null +++ b/apps/privy-login-example/src/routes/index.tsx @@ -0,0 +1,161 @@ +import { store } from '@graphprotocol/hypergraph'; +import { useHypergraphApp, useSpaces } from '@graphprotocol/hypergraph-react'; +import { createFileRoute, Link } from '@tanstack/react-router'; +import { useSelector } from '@xstate/store/react'; +import { useEffect, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; + +export const Route = createFileRoute('/')({ + component: Index, +}); + +// @ts-expect-error +window.HYPERGRAPH_STORE = store; + +function Index() { + const { data: publicSpaces } = useSpaces({ mode: 'public' }); + const { data: privateSpaces } = useSpaces({ mode: 'private' }); + const [spaceName, setSpaceName] = useState(''); + + const accountInboxes = useSelector(store, (state) => state.context.accountInboxes); + const { + createSpace, + listInvitations, + invitations, + acceptInvitation, + createAccountInbox, + getOwnAccountInboxes, + isConnecting, + } = useHypergraphApp(); + + useEffect(() => { + if (!isConnecting) { + listInvitations(); + getOwnAccountInboxes(); + } + }, [isConnecting, listInvitations, getOwnAccountInboxes]); + + if (isConnecting) { + return
Loading …
; + } + + return ( +
+

Invitations

+ {invitations.length === 0 &&
No invitations
} +
    + {invitations.map((invitation) => { + return ( +
  • + + + Space: {invitation.spaceId} + + + + + +
  • + ); + })} +
+ +
+

Spaces

+
+
+ setSpaceName(e.target.value)} /> + +
+ +

Private Spaces

+
    + {privateSpaces && privateSpaces.length === 0 &&
    No spaces
    } + {privateSpaces?.map((space) => { + return ( +
  • + + + + {space.name} + + + +
  • + ); + })} +
+ +

Public Spaces

+
    + {publicSpaces?.map((space) => { + return ( +
  • + + + {space.name} + {space.id} + + +
  • + ); + })} +
+ +
+

Account Inboxes

+ +
    + {accountInboxes.length === 0 &&
    No account inboxes
    } + {accountInboxes.map((inbox) => { + return ( +
  • + + + + {inbox.inboxId} + + + +
  • + ); + })} +
+
+
+ ); +} diff --git a/apps/privy-login-example/src/routes/login.lazy.tsx b/apps/privy-login-example/src/routes/login.lazy.tsx new file mode 100644 index 00000000..8de4d5ce --- /dev/null +++ b/apps/privy-login-example/src/routes/login.lazy.tsx @@ -0,0 +1,111 @@ +import { Button } from '@/components/ui/button'; +import { Connect, type Identity } from '@graphprotocol/hypergraph'; +import { ConnectedWallet, useIdentityToken, usePrivy, useWallets } from '@privy-io/react-auth'; +import { createLazyFileRoute, useRouter } from '@tanstack/react-router'; +import { useCallback, useEffect, useState } from 'react'; +import { createWalletClient, custom, type WalletClient } from 'viem'; + +const CHAIN = import.meta.env.VITE_HYPERGRAPH_CHAIN === 'geogenesis' ? Connect.GEOGENESIS : Connect.GEO_TESTNET; +const syncServerUri = import.meta.env.VITE_HYPERGRAPH_SYNC_SERVER_ORIGIN; +const addressStorage = localStorage; +const keysStorage = sessionStorage; + +export const Route = createLazyFileRoute('/login')({ + component: () => , +}); + +function Login() { + const { ready: privyReady, login: privyLogin, signMessage, authenticated: privyAuthenticated } = usePrivy(); + const { ready: walletsReady, wallets } = useWallets(); + const { navigate } = useRouter(); + const [hypergraphLoginStarted, setHypergraphLoginStarted] = useState(false); + const { identityToken } = useIdentityToken(); + + const hypergraphLogin = useCallback( + async (walletClient: WalletClient, embeddedWallet: ConnectedWallet) => { + console.log('hypergraphLogin'); + if (!identityToken) { + return; + } + const signer: Identity.Signer = { + getAddress: async () => { + const [address] = await walletClient.getAddresses(); + return address; + }, + signMessage: async (message: string) => { + if (embeddedWallet.walletClientType === 'privy') { + const { signature } = await signMessage({ message }, { uiOptions: { showWalletUIs: false } }); + return signature; + } + const [address] = await walletClient.getAddresses(); + return await walletClient.signMessage({ account: address, message }); + }, + }; + + const address = await signer.getAddress(); + if (!address) { + return; + } + + const rpcUrl = import.meta.env.VITE_HYPERGRAPH_RPC_URL; + + console.log(walletClient); + console.log(rpcUrl); + console.log(CHAIN); + await Connect.login({ + walletClient, + signer, + syncServerUri, + addressStorage, + keysStorage, + identityToken, + rpcUrl, + chain: CHAIN, + }); + }, + [identityToken, signMessage], + ); + + useEffect(() => { + console.log('useEffect in login.lazy.tsx'); + if ( + !hypergraphLoginStarted && // avoid re-running the effect too often + privyAuthenticated && // privy must be authenticated to run it + walletsReady && // wallets must be ready to run it + wallets.length > 0 // wallets must have at least one wallet to run it + ) { + console.log('running login effect'); + setHypergraphLoginStarted(true); + (async () => { + try { + const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy') || wallets[0]; + const privyProvider = await embeddedWallet.getEthereumProvider(); + const walletClient = createWalletClient({ + account: embeddedWallet.address as `0x${string}`, + chain: CHAIN, + transport: custom(privyProvider), + }); + + await hypergraphLogin(walletClient, embeddedWallet); + + navigate({ to: '/', replace: true }); + } catch (error) { + alert('Failed to login'); + console.error(error); + } + })(); + } + }, [privyAuthenticated, walletsReady, wallets, hypergraphLogin, navigate, hypergraphLoginStarted]); + + return ( +
+ +
+ ); +} diff --git a/apps/privy-login-example/src/routes/playground.lazy.tsx b/apps/privy-login-example/src/routes/playground.lazy.tsx new file mode 100644 index 00000000..a628da74 --- /dev/null +++ b/apps/privy-login-example/src/routes/playground.lazy.tsx @@ -0,0 +1,27 @@ +import { HypergraphSpaceProvider } from '@graphprotocol/hypergraph-react'; +import { createLazyFileRoute } from '@tanstack/react-router'; +import { CreateEvents } from '@/components/create-events'; +import { CreatePropertiesAndTypesEvent } from '@/components/create-properties-and-types-event'; +import { Event } from '@/components/event'; +import { Playground } from '@/components/playground'; + +export const Route = createLazyFileRoute('/playground')({ + component: RouteComponent, +}); + +function RouteComponent() { + const space = 'a393e509-ae56-4d99-987c-bed71d9db631'; + return ( + <> + + + +
+

Playground

+ + +
+
+ + ); +} diff --git a/apps/privy-login-example/src/routes/space/$spaceId.tsx b/apps/privy-login-example/src/routes/space/$spaceId.tsx new file mode 100644 index 00000000..2234257f --- /dev/null +++ b/apps/privy-login-example/src/routes/space/$spaceId.tsx @@ -0,0 +1,72 @@ +import { createFileRoute, Link, Outlet } from '@tanstack/react-router'; + +export const Route = createFileRoute('/space/$spaceId')({ + component: RouteComponent, +}); + +function RouteComponent() { + const { spaceId } = Route.useParams(); + + return ( +
+
+ +
+ +
+ +
+
+ ); +} diff --git a/apps/privy-login-example/src/routes/space/$spaceId/chat.tsx b/apps/privy-login-example/src/routes/space/$spaceId/chat.tsx new file mode 100644 index 00000000..023c1309 --- /dev/null +++ b/apps/privy-login-example/src/routes/space/$spaceId/chat.tsx @@ -0,0 +1,22 @@ +import { useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { createFileRoute } from '@tanstack/react-router'; +import { SpaceChat } from '@/components/SpaceChat'; + +export const Route = createFileRoute('/space/$spaceId/chat')({ + component: RouteComponent, +}); + +function RouteComponent() { + const { spaceId } = Route.useParams(); + const { isConnecting } = useHypergraphApp(); + + if (isConnecting) { + return
Loading …
; + } + + return ( +
+ +
+ ); +} diff --git a/apps/privy-login-example/src/routes/space/$spaceId/events.tsx b/apps/privy-login-example/src/routes/space/$spaceId/events.tsx new file mode 100644 index 00000000..f7f01b5b --- /dev/null +++ b/apps/privy-login-example/src/routes/space/$spaceId/events.tsx @@ -0,0 +1,23 @@ +import { HypergraphSpaceProvider, useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { createFileRoute } from '@tanstack/react-router'; +import { Events } from '@/components/events/events'; +export const Route = createFileRoute('/space/$spaceId/events')({ + component: RouteComponent, +}); + +function RouteComponent() { + const { spaceId } = Route.useParams(); + const { isConnecting, isLoadingSpaces } = useHypergraphApp(); + + if (isConnecting || isLoadingSpaces[spaceId]) { + return
Loading …
; + } + + return ( +
+ + + +
+ ); +} diff --git a/apps/privy-login-example/src/routes/space/$spaceId/index.tsx b/apps/privy-login-example/src/routes/space/$spaceId/index.tsx new file mode 100644 index 00000000..dd0b1443 --- /dev/null +++ b/apps/privy-login-example/src/routes/space/$spaceId/index.tsx @@ -0,0 +1,57 @@ +import { HypergraphSpaceProvider, useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { createFileRoute } from '@tanstack/react-router'; +import { useState } from 'react'; +import { DevTool } from '@/components/dev-tool'; +import { Todos } from '@/components/todos'; +import { TodosReadOnly } from '@/components/todos-read-only'; +import { TodosReadOnlyFilter } from '@/components/todos-read-only-filter'; +import { Button } from '@/components/ui/button'; +import { Users } from '@/components/users'; + +export const Route = createFileRoute('/space/$spaceId/')({ + component: Space, +}); + +function Space() { + const { spaceId } = Route.useParams(); + const { isConnecting } = useHypergraphApp(); + const [show2ndTodos, setShow2ndTodos] = useState(false); + + if (isConnecting) { + return
Loading …
; + } + + return ( +
+ + + + + + {show2ndTodos && } + {/*

Invite people

+
+ {availableAccounts.map((invitee) => { + return ( + + ); + })} +
*/} +
+ + +
+
+
+ ); +} diff --git a/apps/privy-login-example/src/routes/space/$spaceId/playground.tsx b/apps/privy-login-example/src/routes/space/$spaceId/playground.tsx new file mode 100644 index 00000000..72956372 --- /dev/null +++ b/apps/privy-login-example/src/routes/space/$spaceId/playground.tsx @@ -0,0 +1,24 @@ +import { HypergraphSpaceProvider, useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { createFileRoute } from '@tanstack/react-router'; +import { TodosPublic } from '@/components/todo/todos-public'; + +export const Route = createFileRoute('/space/$spaceId/playground')({ + component: PlaygroundRouteComponent, +}); + +function PlaygroundRouteComponent() { + const { spaceId } = Route.useParams(); + const { isConnecting, isLoadingSpaces } = useHypergraphApp(); + + if (isConnecting || isLoadingSpaces[spaceId]) { + return
Loading …
; + } + + return ( +
+ + + +
+ ); +} diff --git a/apps/privy-login-example/src/routes/space/$spaceId/public-integration.tsx b/apps/privy-login-example/src/routes/space/$spaceId/public-integration.tsx new file mode 100644 index 00000000..d0fa9997 --- /dev/null +++ b/apps/privy-login-example/src/routes/space/$spaceId/public-integration.tsx @@ -0,0 +1,26 @@ +import { HypergraphSpaceProvider, useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { createFileRoute } from '@tanstack/react-router'; +import { CreatePropertiesAndTypesTodos } from '@/components/create-properties-and-types-todos'; +import { Todos2 } from '@/components/todos2'; + +export const Route = createFileRoute('/space/$spaceId/public-integration')({ + component: PublicIntegration, +}); + +function PublicIntegration() { + const { spaceId } = Route.useParams(); + const { isConnecting, isLoadingSpaces } = useHypergraphApp(); + + if (isConnecting || isLoadingSpaces[spaceId]) { + return
Loading …
; + } + + return ( +
+ + + + +
+ ); +} diff --git a/apps/privy-login-example/src/routes/space/$spaceId/users.tsx b/apps/privy-login-example/src/routes/space/$spaceId/users.tsx new file mode 100644 index 00000000..e8b80efe --- /dev/null +++ b/apps/privy-login-example/src/routes/space/$spaceId/users.tsx @@ -0,0 +1,27 @@ +import { HypergraphSpaceProvider, useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { createFileRoute } from '@tanstack/react-router'; +import { UsersMerged } from '@/components/users/users-merged'; +import { UsersPublic } from '@/components/users/users-public'; +import { UsersLocal } from '../../../components/users/users-local'; +export const Route = createFileRoute('/space/$spaceId/users')({ + component: UsersRouteComponent, +}); + +function UsersRouteComponent() { + const { spaceId } = Route.useParams(); + const { isConnecting, isLoadingSpaces } = useHypergraphApp(); + + if (isConnecting || isLoadingSpaces[spaceId]) { + return
Loading …
; + } + + return ( +
+ + + + + +
+ ); +} diff --git a/apps/privy-login-example/src/schema.ts b/apps/privy-login-example/src/schema.ts new file mode 100644 index 00000000..ff8bf043 --- /dev/null +++ b/apps/privy-login-example/src/schema.ts @@ -0,0 +1,43 @@ +import { Entity, Type } from '@graphprotocol/hypergraph'; + +export class User extends Entity.Class('User')({ + name: Type.String, +}) {} + +export class Todo extends Entity.Class('Todo')({ + name: Type.String, + completed: Type.Boolean, + assignees: Type.Relation(User), +}) {} + +export class Todo2 extends Entity.Class('Todo2')({ + name: Type.String, + checked: Type.Boolean, + assignees: Type.Relation(User), + due: Type.Date, + amount: Type.Number, + point: Type.Point, + website: Type.String, +}) {} + +export class JobOffer extends Entity.Class('JobOffer')({ + name: Type.String, + salary: Type.Number, +}) {} + +export class Company extends Entity.Class('Company')({ + name: Type.String, + jobOffers: Type.Relation(JobOffer), +}) {} + +export class Event extends Entity.Class('Event')({ + name: Type.String, + description: Type.optional(Type.String), + sponsors: Type.Relation(Company), +}) {} + +export class Todo3 extends Entity.Class('Todo3')({ + name: Type.String, + completed: Type.Boolean, + description: Type.String, +}) {} diff --git a/apps/privy-login-example/src/types.ts b/apps/privy-login-example/src/types.ts new file mode 100644 index 00000000..b4984090 --- /dev/null +++ b/apps/privy-login-example/src/types.ts @@ -0,0 +1,3 @@ +export interface Doc { + count: number; +} diff --git a/apps/privy-login-example/src/vite-env.d.ts b/apps/privy-login-example/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/apps/privy-login-example/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/privy-login-example/tsconfig.app.json b/apps/privy-login-example/tsconfig.app.json new file mode 100644 index 00000000..0ce5612c --- /dev/null +++ b/apps/privy-login-example/tsconfig.app.json @@ -0,0 +1,41 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "target": "ES2021", + "useDefineForClassFields": true, + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "composite": false, + "incremental": false, + "declaration": false, + "declarationMap": false, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "exactOptionalPropertyTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Shadcn */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@graphprotocol/hypergraph": ["../../packages/hypergraph/src/index.js"], + "@graphprotocol/hypergraph/*": ["../../packages/hypergraph/src/*.js"], + "@graphprotocol/hypergraph-react": ["../../packages/hypergraph-react/src/index.js"], + "@graphprotocol/hypergraph-react/*": ["../../packages/hypergraph-react/src/*.js"] + } + } +} diff --git a/apps/privy-login-example/tsconfig.json b/apps/privy-login-example/tsconfig.json new file mode 100644 index 00000000..1bfa43d4 --- /dev/null +++ b/apps/privy-login-example/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/privy-login-example/tsconfig.node.json b/apps/privy-login-example/tsconfig.node.json new file mode 100644 index 00000000..132ac4b6 --- /dev/null +++ b/apps/privy-login-example/tsconfig.node.json @@ -0,0 +1,29 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["vite.config.ts"], + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "composite": false, + "incremental": false, + "declaration": false, + "declarationMap": false, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "exactOptionalPropertyTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + } +} diff --git a/apps/privy-login-example/vite.config.ts b/apps/privy-login-example/vite.config.ts new file mode 100644 index 00000000..befb4a38 --- /dev/null +++ b/apps/privy-login-example/vite.config.ts @@ -0,0 +1,17 @@ +import path from 'node:path'; +import tailwindcss from '@tailwindcss/vite'; +import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [TanStackRouterVite(), react(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@graphprotocol/hypergraph': path.resolve(__dirname, '../../packages/hypergraph/src'), + '@graphprotocol/hypergraph-react': path.resolve(__dirname, '../../packages/hypergraph-react/src'), + }, + }, +}); diff --git a/apps/privy-login-example/vitest.config.ts b/apps/privy-login-example/vitest.config.ts new file mode 100644 index 00000000..60f4dc1f --- /dev/null +++ b/apps/privy-login-example/vitest.config.ts @@ -0,0 +1,13 @@ +import react from '@vitejs/plugin-react'; +import { mergeConfig } from 'vitest/config'; + +import shared from '../../vitest.shared.js'; + +const config = { + plugins: [react()], + test: { + environment: 'jsdom', + }, +}; + +export default mergeConfig(shared, config); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1879506d..1a610031 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -291,6 +291,127 @@ importers: specifier: ^5.9.2 version: 5.9.2 + apps/privy-login-example: + dependencies: + '@graphprotocol/grc-20': + specifier: ^0.24.1 + version: 0.24.1(bufferutil@4.0.9)(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@graphprotocol/hypergraph': + specifier: workspace:* + version: link:../../packages/hypergraph/publish + '@graphprotocol/hypergraph-react': + specifier: workspace:* + version: link:../../packages/hypergraph-react/publish + '@noble/hashes': + specifier: ^1.8.0 + version: 1.8.0 + '@privy-io/react-auth': + specifier: ^2.21.4 + version: 2.21.4(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(@types/react@19.1.10)(bs58@6.0.0)(bufferutil@4.0.9)(immer@9.0.21)(permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@radix-ui/react-avatar': + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-icons': + specifier: ^1.3.2 + version: 1.3.2(react@19.1.1) + '@radix-ui/react-label': + specifier: ^2.1.7 + version: 2.1.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@19.1.10)(react@19.1.1) + '@tanstack/react-query': + specifier: ^5.85.5 + version: 5.85.5(react@19.1.1) + '@tanstack/react-router': + specifier: ^1.131.27 + version: 1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@tanstack/react-router-devtools': + specifier: ^1.131.27 + version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.131.27)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.5)(tiny-invariant@1.3.3) + '@xstate/store': + specifier: ^3.9.2 + version: 3.9.2(react@19.1.1)(solid-js@1.9.5) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + effect: + specifier: ^3.17.9 + version: 3.17.9 + framer-motion: + specifier: ^12.23.12 + version: 12.23.12(@emotion/is-prop-valid@1.2.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + graphql-request: + specifier: ^7.2.0 + version: 7.2.0(graphql@16.11.0) + isomorphic-ws: + specifier: ^5.0.0 + version: 5.0.0(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + lucide-react: + specifier: ^0.541.0 + version: 0.541.0(react@19.1.1) + react: + specifier: ^19.1.1 + version: 19.1.1 + react-dom: + specifier: ^19.1.1 + version: 19.1.1(react@19.1.1) + react-select: + specifier: ^5.10.2 + version: 5.10.2(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + siwe: + specifier: ^3.0.0 + version: 3.0.0(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@4.1.12) + uuid: + specifier: ^11.1.0 + version: 11.1.0 + viem: + specifier: ^2.34.0 + version: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + vite: + specifier: ^7.1.3 + version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + devDependencies: + '@biomejs/biome': + specifier: 2.2.0 + version: 2.2.0 + '@tailwindcss/vite': + specifier: ^4.1.12 + version: 4.1.12(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) + '@tanstack/router-plugin': + specifier: ^1.131.27 + version: 1.131.27(@tanstack/react-router@1.131.27(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(webpack@5.101.0) + '@types/node': + specifier: ^24.3.0 + version: 24.3.0 + '@types/react': + specifier: ^19.1.10 + version: 19.1.10 + '@types/react-dom': + specifier: ^19.1.7 + version: 19.1.7(@types/react@19.1.10) + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@vitejs/plugin-react': + specifier: ^5.0.1 + version: 5.0.1(vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) + globals: + specifier: ^16.3.0 + version: 16.3.0 + tailwindcss: + specifier: ^4.1.12 + version: 4.1.12 + apps/server: dependencies: '@effect/opentelemetry': @@ -2888,18 +3009,9 @@ packages: '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - '@floating-ui/dom@1.7.3': - resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==} - '@floating-ui/dom@1.7.4': resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} - '@floating-ui/react-dom@2.1.5': - resolution: {integrity: sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - '@floating-ui/react-dom@2.1.6': resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} peerDependencies: @@ -8770,9 +8882,6 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.12.13: - resolution: {integrity: sha512-QZXnR/OGiDcBjF4hGk0wwVrPcZvbSSyzlvkjXv5LFfktj7O2VZDrt4Xs8SgR/vOFco+qk1i8J43ikMXZoTrtPw==} - libphonenumber-js@1.12.14: resolution: {integrity: sha512-HBAMAV7f3yGYy7ZZN5FxQ1tXJTwC77G5/96Yn/SH/HPyKX2EMLGFuCIYUmdLU7CxxJlQcvJymP/PGLzyapurhQ==} @@ -11858,10 +11967,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ua-parser-js@1.0.40: - resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} - hasBin: true - ua-parser-js@1.0.41: resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true @@ -13790,7 +13895,7 @@ snapshots: idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) preact: 10.24.2 - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@19.1.10)(immer@9.0.21)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) transitivePeerDependencies: - '@types/react' @@ -15714,22 +15819,11 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.7.3': - dependencies: - '@floating-ui/core': 1.7.3 - '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.7.4': dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': - dependencies: - '@floating-ui/dom': 1.7.3 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - '@floating-ui/react-dom@2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@floating-ui/dom': 1.7.4 @@ -15738,7 +15832,7 @@ snapshots: '@floating-ui/react@0.26.28(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: - '@floating-ui/react-dom': 2.1.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@floating-ui/react-dom': 2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@floating-ui/utils': 0.2.10 react: 19.1.1 react-dom: 19.1.1(react@19.1.1) @@ -15795,7 +15889,7 @@ snapshots: fflate: 0.8.2 fractional-indexing-jittered: 1.0.0 image-size: 2.0.2 - permissionless: 0.2.52(ox@0.6.9(typescript@5.9.2)(zod@4.0.17))(viem@2.33.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + permissionless: 0.2.52(ox@0.6.9(typescript@5.9.2)(zod@4.0.17))(viem@2.33.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.0.17)) uuid: 11.1.0 viem: 2.33.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.0.17) transitivePeerDependencies: @@ -16900,11 +16994,11 @@ snapshots: '@privy-io/chains@0.0.2': {} - '@privy-io/ethereum@0.0.2(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@privy-io/ethereum@0.0.2(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@privy-io/js-sdk-core@0.53.4(bufferutil@4.0.9)(permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@privy-io/js-sdk-core@0.53.4(bufferutil@4.0.9)(permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@ethersproject/abstract-signer': 5.8.0 '@ethersproject/bignumber': 5.8.0 @@ -16920,12 +17014,39 @@ snapshots: fetch-retry: 6.0.0 jose: 4.15.9 js-cookie: 3.0.5 - libphonenumber-js: 1.12.13 + libphonenumber-js: 1.12.14 set-cookie-parser: 2.7.1 uuid: 9.0.1 optionalDependencies: permissionless: 0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@privy-io/js-sdk-core@0.53.4(bufferutil@4.0.9)(permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + '@ethersproject/abstract-signer': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/contracts': 5.8.0 + '@ethersproject/providers': 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@ethersproject/transactions': 5.8.0 + '@ethersproject/units': 5.8.0 + '@privy-io/api-base': 1.6.0 + '@privy-io/chains': 0.0.2 + '@privy-io/public-api': 2.43.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + canonicalize: 2.1.0 + eventemitter3: 5.0.1 + fetch-retry: 6.0.0 + jose: 4.15.9 + js-cookie: 3.0.5 + libphonenumber-js: 1.12.14 + set-cookie-parser: 2.7.1 + uuid: 9.0.1 + optionalDependencies: + permissionless: 0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -16935,8 +17056,8 @@ snapshots: dependencies: '@privy-io/api-base': 1.6.0 bs58: 5.0.0 - libphonenumber-js: 1.12.13 - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + libphonenumber-js: 1.12.14 + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -16966,8 +17087,8 @@ snapshots: '@metamask/eth-sig-util': 6.0.2 '@privy-io/api-base': 1.6.0 '@privy-io/chains': 0.0.2 - '@privy-io/ethereum': 0.0.2(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@privy-io/js-sdk-core': 0.53.4(bufferutil@4.0.9)(permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@privy-io/ethereum': 0.0.2(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@privy-io/js-sdk-core': 0.53.4(bufferutil@4.0.9)(permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) '@privy-io/public-api': 2.43.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@reown/appkit': 1.7.20(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@scure/base': 1.2.6 @@ -16999,7 +17120,7 @@ snapshots: stylis: 4.3.6 tinycolor2: 1.6.0 uuid: 9.0.1 - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.8(@types/react@19.1.10)(immer@9.0.21)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) optionalDependencies: '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) @@ -17033,6 +17154,84 @@ snapshots: - utf-8-validate - zod + '@privy-io/react-auth@2.21.4(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(@types/react@19.1.10)(bs58@6.0.0)(bufferutil@4.0.9)(immer@9.0.21)(permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@base-org/account': 1.1.1(@types/react@19.1.10)(bufferutil@4.0.9)(immer@9.0.21)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.2 + '@floating-ui/react': 0.26.28(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@headlessui/react': 2.2.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@heroicons/react': 2.2.0(react@19.1.1) + '@marsidev/react-turnstile': 0.4.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@metamask/eth-sig-util': 6.0.2 + '@privy-io/api-base': 1.6.0 + '@privy-io/chains': 0.0.2 + '@privy-io/ethereum': 0.0.2(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@privy-io/js-sdk-core': 0.53.4(bufferutil@4.0.9)(permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@privy-io/public-api': 2.43.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@reown/appkit': 1.7.20(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@scure/base': 1.2.6 + '@simplewebauthn/browser': 9.0.1 + '@solana/wallet-adapter-base': 0.9.23(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)) + '@solana/wallet-standard-wallet-adapter-base': 1.1.4(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@6.0.0) + '@solana/wallet-standard-wallet-adapter-react': 1.1.4(@solana/wallet-adapter-base@0.9.23(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)))(@solana/web3.js@1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10))(bs58@6.0.0)(react@19.1.1) + '@tanstack/react-virtual': 3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@wallet-standard/app': 1.1.0 + '@walletconnect/ethereum-provider': 2.21.7(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + base64-js: 1.5.1 + dotenv: 16.6.1 + encoding: 0.1.13 + eventemitter3: 5.0.1 + fast-password-entropy: 1.1.1 + jose: 4.15.9 + js-cookie: 3.0.5 + lokijs: 1.5.12 + md5: 2.3.0 + mipd: 0.0.7(typescript@5.9.2) + ofetch: 1.4.1 + pino-pretty: 10.3.1 + qrcode: 1.5.4 + react: 19.1.1 + react-device-detect: 2.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react-dom: 19.1.1(react@19.1.1) + secure-password-utilities: 0.2.1 + styled-components: 6.1.19(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + stylis: 4.3.6 + tinycolor2: 1.6.0 + uuid: 9.0.1 + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.8(@types/react@19.1.10)(immer@9.0.21)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + optionalDependencies: + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) + permissionless: 0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bs58 + - bufferutil + - db0 + - immer + - ioredis + - supports-color + - typescript + - uploadthing + - use-sync-external-store + - utf-8-validate + - zod + '@privy-io/server-auth@1.32.0(bufferutil@4.0.9)(encoding@0.1.13)(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@hpke/chacha20poly1305': 1.7.1 @@ -17371,7 +17570,7 @@ snapshots: dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript @@ -17382,7 +17581,7 @@ snapshots: dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -17393,7 +17592,7 @@ snapshots: dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript @@ -17404,7 +17603,7 @@ snapshots: dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -17417,7 +17616,7 @@ snapshots: '@reown/appkit-wallet': 1.7.20(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.21.7(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 2.1.5(@types/react@19.1.10)(react@19.1.1) - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -17451,7 +17650,7 @@ snapshots: '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@19.1.10)(react@19.1.1) - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -17707,7 +17906,7 @@ snapshots: '@walletconnect/logger': 2.1.2 '@walletconnect/universal-provider': 2.21.7(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 2.1.5(@types/react@19.1.10)(react@19.1.1) - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -17744,7 +17943,7 @@ snapshots: '@walletconnect/logger': 2.1.2 '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@19.1.10)(react@19.1.1) - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -17808,7 +18007,7 @@ snapshots: bs58: 6.0.0 semver: 7.7.2 valtio: 2.1.5(@types/react@19.1.10)(react@19.1.1) - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: '@lit/react': 1.0.8(@types/react@19.1.10) transitivePeerDependencies: @@ -17852,7 +18051,7 @@ snapshots: '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 valtio: 1.13.2(@types/react@19.1.10)(react@19.1.1) - viem: 2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -18094,7 +18293,7 @@ snapshots: '@scure/bip32@1.7.0': dependencies: - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 @@ -19836,6 +20035,11 @@ snapshots: typescript: 5.9.2 zod: 4.0.17 + abitype@1.0.9(typescript@5.9.2)(zod@3.22.4): + optionalDependencies: + typescript: 5.9.2 + zod: 3.22.4 + abitype@1.0.9(typescript@5.9.2)(zod@3.25.76): optionalDependencies: typescript: 5.9.2 @@ -19845,7 +20049,6 @@ snapshots: optionalDependencies: typescript: 5.9.2 zod: 4.0.17 - optional: true abort-controller@3.0.0: dependencies: @@ -20343,7 +20546,7 @@ snapshots: browserify-rsa@4.1.1: dependencies: - bn.js: 5.2.1 + bn.js: 5.2.2 randombytes: 2.1.0 safe-buffer: 5.2.1 @@ -22084,7 +22287,7 @@ snapshots: object-assign: 4.1.1 promise: 7.3.1 setimmediate: 1.0.5 - ua-parser-js: 1.0.40 + ua-parser-js: 1.0.41 transitivePeerDependencies: - encoding @@ -23337,8 +23540,6 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.12.13: {} - libphonenumber-js@1.12.14: {} lightningcss-darwin-arm64@1.30.1: @@ -24069,7 +24270,7 @@ snapshots: miller-rabin@4.0.1: dependencies: - bn.js: 4.12.1 + bn.js: 4.12.2 brorand: 1.1.0 mime-db@1.33.0: {} @@ -24475,11 +24676,11 @@ snapshots: ox@0.6.7(typescript@5.9.2)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.0 - '@noble/curves': 1.8.1 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 - abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.0.9(typescript@5.9.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.2 @@ -24519,11 +24720,11 @@ snapshots: dependencies: '@adraffy/ens-normalize': 1.11.0 '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + abitype: 1.0.9(typescript@5.9.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.2 @@ -24534,11 +24735,11 @@ snapshots: dependencies: '@adraffy/ens-normalize': 1.11.0 '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + abitype: 1.0.9(typescript@5.9.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.2 @@ -24549,11 +24750,11 @@ snapshots: dependencies: '@adraffy/ens-normalize': 1.11.0 '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.2)(zod@4.0.17) + abitype: 1.0.9(typescript@5.9.2)(zod@4.0.17) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.2 @@ -24564,11 +24765,11 @@ snapshots: dependencies: '@adraffy/ens-normalize': 1.11.0 '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.6 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.2)(zod@3.22.4) + abitype: 1.0.9(typescript@5.9.2)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.2 @@ -24579,11 +24780,11 @@ snapshots: dependencies: '@adraffy/ens-normalize': 1.11.0 '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.6 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + abitype: 1.0.9(typescript@5.9.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.2 @@ -24807,9 +25008,16 @@ snapshots: ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) optional: true - permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@4.0.17))(viem@2.33.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)): + permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@3.25.76))(viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)): dependencies: - viem: 2.33.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) + optional: true + + permissionless@0.2.52(ox@0.6.9(typescript@5.9.2)(zod@4.0.17))(viem@2.33.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.0.17)): + dependencies: + viem: 2.33.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.0.17) optionalDependencies: ox: 0.6.9(typescript@5.9.2)(zod@4.0.17) @@ -27059,8 +27267,6 @@ snapshots: typescript@5.9.2: {} - ua-parser-js@1.0.40: {} - ua-parser-js@1.0.41: {} uc.micro@2.1.0: {} @@ -27441,15 +27647,15 @@ snapshots: - utf-8-validate - zod - viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4): + viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.9.6 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.2)(zod@3.22.4) + abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ox: 0.8.7(typescript@5.9.2)(zod@3.22.4) + ox: 0.8.7(typescript@5.9.2)(zod@3.25.76) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.2 @@ -27458,15 +27664,15 @@ snapshots: - utf-8-validate - zod - viem@2.34.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): + viem@2.35.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4): dependencies: '@noble/curves': 1.9.6 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + abitype: 1.0.8(typescript@5.9.2)(zod@3.22.4) isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ox: 0.8.7(typescript@5.9.2)(zod@3.25.76) + ox: 0.8.7(typescript@5.9.2)(zod@3.22.4) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.2 @@ -27498,7 +27704,7 @@ snapshots: debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -27519,7 +27725,7 @@ snapshots: debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -27587,6 +27793,23 @@ snapshots: tsx: 4.20.5 yaml: 2.8.1 + vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1): + dependencies: + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.47.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 24.3.0 + fsevents: 2.3.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + terser: 5.43.1 + tsx: 4.20.4 + yaml: 2.8.1 + vite@7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 diff --git a/tsconfig.json b/tsconfig.json index cbe80ab1..bcec9b17 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,8 +9,9 @@ { "path": "apps/server" }, { "path": "apps/connect" }, { "path": "apps/events" }, + { "path": "apps/privy-login-example" }, { "path": "apps/template-nextjs" }, { "path": "apps/template-vite-react" }, - { "path": "apps/next-example" }, + { "path": "apps/next-example" } ] } diff --git a/vitest.config.ts b/vitest.config.ts index a75cf478..138fcbef 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - projects: ['./packages/*', './apps/events', './apps/connect', './apps/server'], + projects: ['./packages/*', './apps/events', './apps/connect', './apps/server', './apps/privy-login-example'], }, }); From 3285392ffea5af51ea46191a2ab90f3f5cdcfa58 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 16 Sep 2025 15:37:45 +0200 Subject: [PATCH 2/4] extend with privy based app --- apps/connect/src/routes/authenticate.tsx | 7 - .../src/components/logout.tsx | 3 + .../privy-login-example/src/routes/__root.tsx | 2 +- .../src/routes/account-inbox/$inboxId.tsx | 4 +- .../src/routes/login.lazy.tsx | 16 +- apps/server/src/services/connections.ts | 6 +- apps/server/src/services/invitations.ts | 6 +- apps/server/src/services/spaces.ts | 68 ++++-- apps/server/src/websocket.ts | 39 +++- .../src/HypergraphAppContext.tsx | 98 +++++--- .../hypergraph-react/src/hooks/use-spaces.ts | 6 +- .../src/hooks/useExternalAccountInbox.ts | 10 +- .../src/hooks/useExternalSpaceInbox.ts | 8 +- .../src/hooks/useOwnAccountInbox.ts | 10 +- .../src/hooks/useOwnSpaceInbox.ts | 8 +- packages/hypergraph/src/connect/types.ts | 4 + .../hypergraph/src/identity/auth-storage.ts | 44 +++- packages/hypergraph/src/identity/logout.ts | 3 +- packages/hypergraph/src/index.ts | 1 + .../hypergraph/src/privy-auth/privy-auth.ts | 213 ++++++++++++++++++ packages/hypergraph/src/store.ts | 18 +- 21 files changed, 475 insertions(+), 99 deletions(-) create mode 100644 packages/hypergraph/src/privy-auth/privy-auth.ts diff --git a/apps/connect/src/routes/authenticate.tsx b/apps/connect/src/routes/authenticate.tsx index c98d166c..0f76a4d4 100644 --- a/apps/connect/src/routes/authenticate.tsx +++ b/apps/connect/src/routes/authenticate.tsx @@ -336,8 +336,6 @@ function AuthenticateComponent() { const newAppIdentity = Connect.createAppIdentity(); - console.log('creating smart session'); - console.log('public spaces data', publicSpacesData); const spaces = publicSpacesData // .filter((space) => selectedPublicSpaces.has(space.id)) @@ -348,10 +346,8 @@ function AuthenticateComponent() { : (space.mainVotingAddress as `0x${string}`), type: space.type as 'personal' | 'public', })) ?? []; - console.log('spaces', spaces); const localAccount = privateKeyToAccount(keys.signaturePrivateKey as `0x${string}`); - console.log('local account', localAccount.address); // TODO: add additional actions (must be passed from the app) const permissionId = await Connect.createSmartSession( localAccount, @@ -365,7 +361,6 @@ function AuthenticateComponent() { additionalActions: [], }, ); - console.log('smart session created'); const smartAccountClient = await Connect.getSmartAccountWalletClient({ owner: localAccount, address: accountAddress, @@ -373,9 +368,7 @@ function AuthenticateComponent() { rpcUrl: import.meta.env.VITE_HYPERGRAPH_RPC_URL, }); - console.log('encrypting app identity'); const { ciphertext } = await Connect.encryptAppIdentity({ ...newAppIdentity, permissionId }, keys); - console.log('proving ownership'); const { accountProof, keyProof } = await Identity.proveIdentityOwnership( smartAccountClient, accountAddress, diff --git a/apps/privy-login-example/src/components/logout.tsx b/apps/privy-login-example/src/components/logout.tsx index fa92a77a..ca0620a6 100644 --- a/apps/privy-login-example/src/components/logout.tsx +++ b/apps/privy-login-example/src/components/logout.tsx @@ -1,11 +1,14 @@ import { useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { usePrivy } from '@privy-io/react-auth'; import { useRouter } from '@tanstack/react-router'; import { Button } from './ui/button'; export function Logout() { const { logout: graphLogout } = useHypergraphApp(); + const { logout: privyLogout } = usePrivy(); const router = useRouter(); const disconnectWallet = async () => { + privyLogout(); graphLogout(); // needs to be called after privy logout since it triggers a re-render router.navigate({ to: '/login', diff --git a/apps/privy-login-example/src/routes/__root.tsx b/apps/privy-login-example/src/routes/__root.tsx index 858a421e..b4ba2ae8 100644 --- a/apps/privy-login-example/src/routes/__root.tsx +++ b/apps/privy-login-example/src/routes/__root.tsx @@ -1,7 +1,7 @@ -import { Logout } from '@/components/logout'; import { usePrivy } from '@privy-io/react-auth'; import { createRootRoute, Link, Outlet, useLayoutEffect, useRouter } from '@tanstack/react-router'; import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; +import { Logout } from '@/components/logout'; export const Route = createRootRoute({ component: () => { diff --git a/apps/privy-login-example/src/routes/account-inbox/$inboxId.tsx b/apps/privy-login-example/src/routes/account-inbox/$inboxId.tsx index ae2720e3..79fff65f 100644 --- a/apps/privy-login-example/src/routes/account-inbox/$inboxId.tsx +++ b/apps/privy-login-example/src/routes/account-inbox/$inboxId.tsx @@ -7,11 +7,11 @@ export const Route = createFileRoute('/account-inbox/$inboxId')({ function RouteComponent() { const { inboxId } = Route.useParams(); - const { identity } = useHypergraphAuth(); + const { privyIdentity } = useHypergraphAuth(); const { messages, loading, error } = useOwnAccountInbox(inboxId); // Ensure we have an authenticated user - if (!identity?.accountAddress) { + if (!privyIdentity?.accountAddress) { return
Please login to view your inbox
; } diff --git a/apps/privy-login-example/src/routes/login.lazy.tsx b/apps/privy-login-example/src/routes/login.lazy.tsx index 8de4d5ce..37aec02e 100644 --- a/apps/privy-login-example/src/routes/login.lazy.tsx +++ b/apps/privy-login-example/src/routes/login.lazy.tsx @@ -1,9 +1,9 @@ -import { Button } from '@/components/ui/button'; -import { Connect, type Identity } from '@graphprotocol/hypergraph'; -import { ConnectedWallet, useIdentityToken, usePrivy, useWallets } from '@privy-io/react-auth'; +import { Connect, type Identity, PrivyAuth } from '@graphprotocol/hypergraph'; +import { type ConnectedWallet, useIdentityToken, usePrivy, useWallets } from '@privy-io/react-auth'; import { createLazyFileRoute, useRouter } from '@tanstack/react-router'; import { useCallback, useEffect, useState } from 'react'; import { createWalletClient, custom, type WalletClient } from 'viem'; +import { Button } from '@/components/ui/button'; const CHAIN = import.meta.env.VITE_HYPERGRAPH_CHAIN === 'geogenesis' ? Connect.GEOGENESIS : Connect.GEO_TESTNET; const syncServerUri = import.meta.env.VITE_HYPERGRAPH_SYNC_SERVER_ORIGIN; @@ -23,7 +23,6 @@ function Login() { const hypergraphLogin = useCallback( async (walletClient: WalletClient, embeddedWallet: ConnectedWallet) => { - console.log('hypergraphLogin'); if (!identityToken) { return; } @@ -49,10 +48,7 @@ function Login() { const rpcUrl = import.meta.env.VITE_HYPERGRAPH_RPC_URL; - console.log(walletClient); - console.log(rpcUrl); - console.log(CHAIN); - await Connect.login({ + await PrivyAuth.login({ walletClient, signer, syncServerUri, @@ -67,14 +63,12 @@ function Login() { ); useEffect(() => { - console.log('useEffect in login.lazy.tsx'); if ( !hypergraphLoginStarted && // avoid re-running the effect too often privyAuthenticated && // privy must be authenticated to run it walletsReady && // wallets must be ready to run it wallets.length > 0 // wallets must have at least one wallet to run it ) { - console.log('running login effect'); setHypergraphLoginStarted(true); (async () => { try { @@ -100,9 +94,11 @@ function Login() { return (
diff --git a/apps/server/src/services/connections.ts b/apps/server/src/services/connections.ts index 7f73855a..7bc6104f 100644 --- a/apps/server/src/services/connections.ts +++ b/apps/server/src/services/connections.ts @@ -4,7 +4,7 @@ import type * as Mailbox from 'effect/Mailbox'; type Connection = { accountAddress: string; - appIdentityAddress: string; + appIdentityAddress: string | undefined; mailbox: Mailbox.Mailbox; subscribedSpaces: Set; }; @@ -14,7 +14,7 @@ export class ConnectionsService extends Context.Tag('ConnectionsService')< { readonly registerConnection: (params: { accountAddress: string; - appIdentityAddress: string; + appIdentityAddress: string | undefined; mailbox: Mailbox.Mailbox; }) => Effect.Effect; readonly removeConnection: (connectionId: string) => Effect.Effect; @@ -45,7 +45,7 @@ export const layer = Effect.gen(function* () { mailbox, }: { accountAddress: string; - appIdentityAddress: string; + appIdentityAddress: string | undefined; mailbox: Mailbox.Mailbox; }) { const nextId = yield* Ref.updateAndGet(connectionCounter, (n) => n + 1); diff --git a/apps/server/src/services/invitations.ts b/apps/server/src/services/invitations.ts index 3d15c6cc..22de5766 100644 --- a/apps/server/src/services/invitations.ts +++ b/apps/server/src/services/invitations.ts @@ -7,7 +7,7 @@ import * as IdentityService from './identity.js'; export class InvitationsService extends Context.Tag('InvitationsService')< InvitationsService, { - readonly listByAppIdentity: ( + readonly listByAccountAddress: ( appIdentityAddress: string, ) => Effect.Effect; } @@ -18,7 +18,7 @@ const decodeSpaceState = Schema.decodeUnknownEither(SpaceEvents.SpaceState); export const layer = Effect.gen(function* () { const { use } = yield* DatabaseService.DatabaseService; - const listByAppIdentity = Effect.fn('listByAppIdentity')(function* (accountAddress: string) { + const listByAccountAddress = Effect.fn('listByAccountAddress')(function* (accountAddress: string) { const invitations = yield* use((client) => client.invitation.findMany({ where: { @@ -57,6 +57,6 @@ export const layer = Effect.gen(function* () { }); return { - listByAppIdentity, + listByAccountAddress, } as const; }).pipe(Layer.effect(InvitationsService), Layer.provide(DatabaseService.layer), Layer.provide(IdentityService.layer)); diff --git a/apps/server/src/services/spaces.ts b/apps/server/src/services/spaces.ts index b5301a9b..2768bc53 100644 --- a/apps/server/src/services/spaces.ts +++ b/apps/server/src/services/spaces.ts @@ -77,7 +77,7 @@ export interface CreateSpaceParams { export interface GetSpaceParams { spaceId: string; accountAddress: string; - appIdentityAddress: string; + appIdentityAddress?: string | undefined; } export interface AddAppIdentityToSpacesParams { @@ -103,9 +103,13 @@ export class SpacesService extends Context.Tag('SpacesService')< readonly addAppIdentityToSpaces: ( params: AddAppIdentityToSpacesParams, ) => Effect.Effect; - readonly listByAppIdentity: ( - appIdentityAddress: string, - ) => Effect.Effect; + readonly listByAppIdentityOrAccount: ({ + appIdentityAddress, + accountAddress, + }: { + appIdentityAddress?: string | undefined; + accountAddress: string; + }) => Effect.Effect; readonly getSpace: ( params: GetSpaceParams, ) => Effect.Effect; @@ -141,6 +145,7 @@ export const layer = Effect.gen(function* () { keyBoxes: { where: { accountAddress, + appIdentityAddress: null, }, }, }, @@ -171,16 +176,30 @@ export const layer = Effect.gen(function* () { })); }); - const listByAppIdentity = Effect.fn('listByAppIdentity')(function* (appIdentityAddress: string) { + const listByAppIdentityOrAccount = Effect.fn('listByAppIdentityOrAccount')(function* ({ + appIdentityAddress, + accountAddress, + }: { + appIdentityAddress?: string | undefined; + accountAddress: string; + }) { return yield* use((client) => client.space.findMany({ - where: { - appIdentities: { - some: { - address: appIdentityAddress, + where: appIdentityAddress + ? { + appIdentities: { + some: { + address: appIdentityAddress, + }, + }, + } + : { + members: { + some: { + address: accountAddress, + }, + }, }, - }, - }, include: { appIdentities: { select: { @@ -191,9 +210,15 @@ export const layer = Effect.gen(function* () { keys: { include: { keyBoxes: { - where: { - appIdentityAddress, - }, + where: appIdentityAddress + ? { + accountAddress, + appIdentityAddress, + } + : { + accountAddress, + appIdentityAddress: null, + }, }, }, }, @@ -224,10 +249,15 @@ export const layer = Effect.gen(function* () { keys: { include: { keyBoxes: { - where: { - accountAddress, - appIdentityAddress, - }, + where: appIdentityAddress + ? { + accountAddress, + appIdentityAddress, + } + : { + accountAddress, + appIdentityAddress: null, + }, select: { nonce: true, ciphertext: true, @@ -534,7 +564,7 @@ export const layer = Effect.gen(function* () { return { listByAccount, - listByAppIdentity, + listByAppIdentityOrAccount, getSpace, createSpace, addAppIdentityToSpaces, diff --git a/apps/server/src/websocket.ts b/apps/server/src/websocket.ts index 7b50552c..e3abac73 100644 --- a/apps/server/src/websocket.ts +++ b/apps/server/src/websocket.ts @@ -13,6 +13,7 @@ import * as AppIdentityService from './services/app-identity.ts'; import * as ConnectionsService from './services/connections.ts'; import * as IdentityService from './services/identity.ts'; import * as InvitationsService from './services/invitations.ts'; +import * as PrivyAuthService from './services/privy-auth.ts'; import * as SpaceInboxService from './services/space-inbox.ts'; import * as SpacesService from './services/spaces.ts'; import * as UpdatesService from './services/updates.ts'; @@ -30,18 +31,34 @@ export const WebSocketLayer = HttpLayerRouter.add( const connectionsService = yield* ConnectionsService.ConnectionsService; const accountInboxService = yield* AccountInboxService.AccountInboxService; const spaceInboxService = yield* SpaceInboxService.SpaceInboxService; + const appIdentityService = yield* AppIdentityService.AppIdentityService; + const identityService = yield* IdentityService.IdentityService; + const privyAuthService = yield* PrivyAuthService.PrivyAuthService; const responseMailbox = yield* Mailbox.make(); const searchParams = HttpServerRequest.searchParamsFromURL(new URL(request.url, 'http://localhost')); const token = isArray(searchParams.token) ? searchParams.token[0] : searchParams.token; - - if (!token) { + const privyIdentityToken = isArray(searchParams['privy-identity-token']) + ? searchParams['privy-identity-token'][0] + : searchParams['privy-identity-token']; + const privyAccountAddress = isArray(searchParams['account-address']) + ? searchParams['account-address'][0] + : searchParams['account-address']; + + if (!token && (!privyIdentityToken || !privyAccountAddress)) { return yield* HttpServerResponse.empty({ status: 400 }); } - const appIdentityService = yield* AppIdentityService.AppIdentityService; - const identityService = yield* IdentityService.IdentityService; - const { accountAddress, address } = yield* appIdentityService.getBySessionToken(token).pipe(Effect.orDie); + let accountAddress: string; + let address: string | undefined; + if (privyIdentityToken && privyAccountAddress) { + yield* privyAuthService.authenticateRequest(privyIdentityToken, privyAccountAddress).pipe(Effect.orDie); + accountAddress = privyAccountAddress; + } else { + const result = yield* appIdentityService.getBySessionToken(token).pipe(Effect.orDie); + accountAddress = result.accountAddress; + address = result.address; + } // Register this connection const connectionId = yield* connectionsService.registerConnection({ @@ -60,14 +77,17 @@ export const WebSocketLayer = HttpLayerRouter.add( const request = yield* decodeRequestMessage(json); switch (request.type) { case 'list-spaces': { - const spaces = yield* spacesService.listByAppIdentity(address); + const spaces = yield* spacesService.listByAppIdentityOrAccount({ + appIdentityAddress: address, + accountAddress, + }); const outgoingMessage: Messages.ResponseListSpaces = { type: 'list-spaces', spaces: spaces }; // TODO: fix Messages.serialize yield* responseMailbox.offer(Messages.serializeV2(outgoingMessage)); break; } case 'list-invitations': { - const invitations = yield* invitationsService.listByAppIdentity(accountAddress); + const invitations = yield* invitationsService.listByAccountAddress(accountAddress); const outgoingMessage: Messages.ResponseListInvitations = { type: 'list-invitations', invitations, @@ -215,7 +235,7 @@ export const WebSocketLayer = HttpLayerRouter.add( const inviteeAccountAddress = request.event.transaction.inviteeAccountAddress; // Get the updated invitation list for the invitee - const invitations = yield* invitationsService.listByAppIdentity(inviteeAccountAddress); + const invitations = yield* invitationsService.listByAccountAddress(inviteeAccountAddress); const invitationMessage: Messages.ResponseListInvitations = { type: 'list-invitations', invitations, @@ -432,5 +452,6 @@ export const WebSocketLayer = HttpLayerRouter.add( .pipe(Effect.provide(IdentityService.layer)) .pipe(Effect.provide(UpdatesService.layer)) .pipe(Effect.provide(AccountInboxService.layer)) - .pipe(Effect.provide(SpaceInboxService.layer)), + .pipe(Effect.provide(SpaceInboxService.layer)) + .pipe(Effect.provide(PrivyAuthService.layer)), ); diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index 8b5cbd14..c6f03f6f 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -209,8 +209,9 @@ export function useHypergraphApp() { export function useHypergraphAuth() { const authenticated = useSelectorStore(store, (state) => state.context.authenticated); const identity = useSelectorStore(store, (state) => state.context.identity); + const privyIdentity = useSelectorStore(store, (state) => state.context.privyIdentity); - return { authenticated, identity }; + return { authenticated, identity, privyIdentity }; } export type HypergraphAppProviderProps = Readonly<{ @@ -230,11 +231,6 @@ const mockStorage = { removeItem(_key: string) {}, }; -// 1) a) Get session token from local storage, or -// b) Auth with the sync server -// 2) a)Try to get identity from the sync server, or -// b) If identity is not found, create a new identity -// (and store it in the sync server) export function HypergraphAppProvider({ storage = typeof window !== 'undefined' ? localStorage : mockStorage, syncServerUri = 'https://sync.geobrowser.io', @@ -250,6 +246,10 @@ export function HypergraphAppProvider({ const invitations = useSelectorStore(store, (state) => state.context.invitations); const repo = useSelectorStore(store, (state) => state.context.repo); const identity = useSelectorStore(store, (state) => state.context.identity); + const privyIdentity = useSelectorStore(store, (state) => state.context.privyIdentity); + + console.log('identity', identity); + console.log('privyIdentity', privyIdentity); const logout = useCallback(() => { websocketConnection?.close(); @@ -265,7 +265,6 @@ export function HypergraphAppProvider({ type: 'setAuth', identity, }); - console.log('Identity set'); }, [storage], ); @@ -287,19 +286,39 @@ export function HypergraphAppProvider({ identity, }); } - // set render auth check to true so next potential rerender doesn't proc this + const privyIdentity = Identity.loadPrivyIdentity(storage); + if (privyIdentity) { + store.send({ + type: 'setPrivyAuth', + identity: privyIdentity, + }); + } + + // set render auth check to true so next potential rerender doesn't try this again initialRenderAuthCheckRef.current = true; } }, [storage, mapping]); useEffect(() => { - if (!identity) { + if (identity === null && privyIdentity === null) { setIsConnecting(true); return; } const syncServerUrl = new URL(syncServerUri); - const syncServerWsUrl = new URL(`/?token=${identity.sessionToken}`, syncServerUrl.toString()); + let syncServerWsUrl: URL; + + if (identity) { + syncServerWsUrl = new URL(`/?token=${identity.sessionToken}`, syncServerUrl.toString()); + } else if (privyIdentity) { + syncServerWsUrl = new URL( + `/?privy-identity-token=${privyIdentity.privyIdentityToken}&account-address=${privyIdentity.accountAddress}`, + syncServerUrl.toString(), + ); + } else { + // This should never happen due to the early returns above, but TypeScript needs this + return; + } // Use 'wss:' by default, only use 'ws:' for local development const isLocalDev = syncServerUrl.hostname === 'localhost' || syncServerUrl.hostname === '127.0.0.1'; @@ -335,30 +354,40 @@ export function HypergraphAppProvider({ websocketConnection.removeEventListener('close', onClose); websocketConnection.close(); }; - }, [identity, syncServerUri]); + }, [identity, privyIdentity, syncServerUri]); // Handle WebSocket messages in a separate effect useEffect(() => { if (!websocketConnection) return; - if (!identity) { + if (identity === null && privyIdentity === null) { console.error('No identity found'); return; } - const encryptionPrivateKey = identity.encryptionPrivateKey; + const encryptionPrivateKey = identity?.encryptionPrivateKey || privyIdentity?.encryptionPrivateKey; if (!encryptionPrivateKey) { console.error('No encryption private key found'); return; } - const encryptionPublicKey = identity.encryptionPublicKey; + const encryptionPublicKey = identity?.encryptionPublicKey || privyIdentity?.encryptionPublicKey; if (!encryptionPublicKey) { console.error('No encryption public key found'); return; } - const signaturePrivateKey = identity.signaturePrivateKey; + const signaturePrivateKey = identity?.signaturePrivateKey || privyIdentity?.signaturePrivateKey; if (!signaturePrivateKey) { console.error('No signature private key found.'); return; } + const signaturePublicKey = identity?.signaturePublicKey || privyIdentity?.signaturePublicKey; + if (!signaturePublicKey) { + console.error('No signature public key found'); + return; + } + const accountAddress = identity?.accountAddress || privyIdentity?.accountAddress; + if (!accountAddress) { + console.error('No account address found'); + return; + } const applyUpdates = async ( spaceId: string, @@ -523,7 +552,7 @@ export function HypergraphAppProvider({ const updateId = uuid(); const messageToSend = Messages.signedUpdateMessage({ - accountAddress: identity.accountAddress, + accountAddress, updateId, spaceId: space.id, message: lastLocalChange, @@ -630,13 +659,13 @@ export function HypergraphAppProvider({ } case 'account-inbox': { // Validate the signature of the inbox corresponds to the current account's identity - if (!identity.signaturePrivateKey) { + if (signaturePrivateKey) { console.error('No signature private key found to process account inbox'); return; } const inboxCreator = Inboxes.recoverAccountInboxCreatorKey(response.inbox); - if (inboxCreator !== identity.signaturePublicKey) { - console.error('Invalid inbox creator', response.inbox, inboxCreator, identity.signaturePublicKey); + if (inboxCreator !== signaturePublicKey) { + console.error('Invalid inbox creator', response.inbox, inboxCreator, signaturePublicKey); return; } @@ -713,7 +742,7 @@ export function HypergraphAppProvider({ const isValid = await Inboxes.validateAccountInboxMessage( response.message, inbox, - identity.accountAddress, + accountAddress, syncServerUri, CHAIN, RPC_URL, @@ -767,7 +796,7 @@ export function HypergraphAppProvider({ } case 'account-inbox-messages': { // Validate the signature of the inbox corresponds to the current account's identity - if (!identity.signaturePrivateKey) { + if (!signaturePrivateKey) { console.error('No signature private key found to process account inbox'); return; } @@ -783,7 +812,7 @@ export function HypergraphAppProvider({ return Inboxes.validateAccountInboxMessage( message, inbox, - identity.accountAddress, + accountAddress, syncServerUri, CHAIN, RPC_URL, @@ -903,7 +932,7 @@ export function HypergraphAppProvider({ return () => { websocketConnection.removeEventListener('message', onMessage); }; - }, [websocketConnection, spaces, identity, syncServerUri]); + }, [websocketConnection, spaces, identity, privyIdentity, syncServerUri]); const createSpaceForContext = useCallback< ({ name, smartSessionClient }: { name: string; smartSessionClient?: Connect.SmartSessionClient }) => Promise @@ -1285,14 +1314,21 @@ export function HypergraphAppProvider({ accountAddress: string; }; }>) => { - if (!identity) { + if (!identity && !privyIdentity) { throw new Error('No identity found'); } - const encryptionPrivateKey = identity.encryptionPrivateKey; - const encryptionPublicKey = identity.encryptionPublicKey; - const signaturePrivateKey = identity.signaturePrivateKey; - const signaturePublicKey = identity.signaturePublicKey; - if (!encryptionPrivateKey || !encryptionPublicKey || !signaturePrivateKey || !signaturePublicKey) { + const encryptionPrivateKey = identity?.encryptionPrivateKey || privyIdentity?.encryptionPrivateKey; + const encryptionPublicKey = identity?.encryptionPublicKey || privyIdentity?.encryptionPublicKey; + const signaturePrivateKey = identity?.signaturePrivateKey || privyIdentity?.signaturePrivateKey; + const signaturePublicKey = identity?.signaturePublicKey || privyIdentity?.signaturePublicKey; + const accountAddress = identity?.accountAddress || privyIdentity?.accountAddress; + if ( + !encryptionPrivateKey || + !encryptionPublicKey || + !signaturePrivateKey || + !signaturePublicKey || + !accountAddress + ) { throw new Error('Missing keys'); } if (!space.state) { @@ -1310,7 +1346,7 @@ export function HypergraphAppProvider({ const spaceEvent = await Effect.runPromiseExit( SpaceEvents.createInvitation({ author: { - accountAddress: identity.accountAddress, + accountAddress, signaturePublicKey, encryptionPublicKey, signaturePrivateKey, @@ -1347,7 +1383,7 @@ export function HypergraphAppProvider({ }; websocketConnection?.send(Messages.serialize(message)); }, - [identity, websocketConnection, syncServerUri, appId], + [identity, privyIdentity, websocketConnection, syncServerUri, appId], ); const getVerifiedIdentityForContext = useCallback( diff --git a/packages/hypergraph-react/src/hooks/use-spaces.ts b/packages/hypergraph-react/src/hooks/use-spaces.ts index 21eccad6..df41c576 100644 --- a/packages/hypergraph-react/src/hooks/use-spaces.ts +++ b/packages/hypergraph-react/src/hooks/use-spaces.ts @@ -29,7 +29,11 @@ type PublicSpacesQueryResult = { }; export const useSpaces = (params: { mode: 'public' | 'private' }) => { - const accountAddress = useSelector(store, (state) => state.context.identity?.accountAddress); + const identityAccountAddress = useSelector(store, (state) => state.context.identity?.accountAddress); + const privyIdentityAccountAddress = useSelector(store, (state) => state.context.privyIdentity?.accountAddress); + + const accountAddress = identityAccountAddress ? identityAccountAddress : privyIdentityAccountAddress; + const publicResult = useQuery({ queryKey: ['hypergraph-spaces', params.mode], queryFn: async () => { diff --git a/packages/hypergraph-react/src/hooks/useExternalAccountInbox.ts b/packages/hypergraph-react/src/hooks/useExternalAccountInbox.ts index 55d96226..c50f53be 100644 --- a/packages/hypergraph-react/src/hooks/useExternalAccountInbox.ts +++ b/packages/hypergraph-react/src/hooks/useExternalAccountInbox.ts @@ -1,4 +1,4 @@ -import type { Messages } from '@graphprotocol/hypergraph'; +import type { Connect, Messages } from '@graphprotocol/hypergraph'; import { useCallback, useEffect, useState } from 'react'; import { useHypergraphApp, useHypergraphAuth } from '../HypergraphAppContext.js'; @@ -8,7 +8,11 @@ import { useHypergraphApp, useHypergraphAuth } from '../HypergraphAppContext.js' */ export function useExternalAccountInbox(accountAddress: string, inboxId: string) { const { sendAccountInboxMessage, getAccountInbox } = useHypergraphApp(); - const { identity } = useHypergraphAuth(); + const result = useHypergraphAuth(); + let identity: Connect.PrivatePrivyAppIdentity | Connect.PrivateAppIdentity | null = result.identity; + if (!identity && result.privyIdentity) { + identity = result.privyIdentity; + } // Use local state for external inbox const [inbox, setInbox] = useState(null); @@ -45,7 +49,7 @@ export function useExternalAccountInbox(accountAddress: string, inboxId: string) let authorAccountAddress: string | null = null; let signaturePrivateKey: string | null = null; - if (identity?.address && inbox.authPolicy !== 'anonymous') { + if (identity?.accountAddress && inbox.authPolicy !== 'anonymous') { authorAccountAddress = identity.accountAddress; signaturePrivateKey = identity.signaturePrivateKey; } diff --git a/packages/hypergraph-react/src/hooks/useExternalSpaceInbox.ts b/packages/hypergraph-react/src/hooks/useExternalSpaceInbox.ts index f5a4959d..bc36ba6c 100644 --- a/packages/hypergraph-react/src/hooks/useExternalSpaceInbox.ts +++ b/packages/hypergraph-react/src/hooks/useExternalSpaceInbox.ts @@ -1,4 +1,4 @@ -import type { Messages } from '@graphprotocol/hypergraph'; +import type { Connect, Messages } from '@graphprotocol/hypergraph'; import { useCallback, useEffect, useState } from 'react'; import { useHypergraphApp, useHypergraphAuth } from '../HypergraphAppContext.js'; @@ -8,7 +8,11 @@ import { useHypergraphApp, useHypergraphAuth } from '../HypergraphAppContext.js' */ export function useExternalSpaceInbox({ spaceId, inboxId }: { spaceId: string; inboxId: string }) { const { sendSpaceInboxMessage, getSpaceInbox } = useHypergraphApp(); - const { identity } = useHypergraphAuth(); + const result = useHypergraphAuth(); + let identity: Connect.PrivatePrivyAppIdentity | Connect.PrivateAppIdentity | null = result.identity; + if (!identity && result.privyIdentity) { + identity = result.privyIdentity; + } // Use local state for external inbox const [inbox, setInbox] = useState(null); diff --git a/packages/hypergraph-react/src/hooks/useOwnAccountInbox.ts b/packages/hypergraph-react/src/hooks/useOwnAccountInbox.ts index 96874bc9..c9bf30a4 100644 --- a/packages/hypergraph-react/src/hooks/useOwnAccountInbox.ts +++ b/packages/hypergraph-react/src/hooks/useOwnAccountInbox.ts @@ -1,4 +1,4 @@ -import { store } from '@graphprotocol/hypergraph'; +import { type Connect, store } from '@graphprotocol/hypergraph'; import { useSelector as useSelectorStore } from '@xstate/store/react'; import { useCallback, useEffect, useState } from 'react'; import { useHypergraphApp, useHypergraphAuth } from '../HypergraphAppContext.js'; @@ -9,8 +9,12 @@ import { useHypergraphApp, useHypergraphAuth } from '../HypergraphAppContext.js' */ export function useOwnAccountInbox(inboxId: string) { const { getLatestAccountInboxMessages, sendAccountInboxMessage, getOwnAccountInboxes } = useHypergraphApp(); - const { identity } = useHypergraphAuth(); - const accountAddress = identity?.address; + const result = useHypergraphAuth(); + let identity: Connect.PrivatePrivyAppIdentity | Connect.PrivateAppIdentity | null = result.identity; + if (!identity && result.privyIdentity) { + identity = result.privyIdentity; + } + const accountAddress = identity?.accountAddress; // Get own inbox from store const inbox = useSelectorStore(store, (state) => state.context.accountInboxes.find((i) => i.inboxId === inboxId)); diff --git a/packages/hypergraph-react/src/hooks/useOwnSpaceInbox.ts b/packages/hypergraph-react/src/hooks/useOwnSpaceInbox.ts index 98c213a3..ca5c9293 100644 --- a/packages/hypergraph-react/src/hooks/useOwnSpaceInbox.ts +++ b/packages/hypergraph-react/src/hooks/useOwnSpaceInbox.ts @@ -1,4 +1,4 @@ -import { type Inboxes, store } from '@graphprotocol/hypergraph'; +import { type Connect, type Inboxes, store } from '@graphprotocol/hypergraph'; import { useSelector as useSelectorStore } from '@xstate/store/react'; import { useCallback, useEffect, useState } from 'react'; import { useHypergraphApp, useHypergraphAuth } from '../HypergraphAppContext.js'; @@ -21,7 +21,11 @@ export function useOwnSpaceInbox({ authPolicy?: Inboxes.InboxSenderAuthPolicy; }) { const { getLatestSpaceInboxMessages, sendSpaceInboxMessage, ensureSpaceInbox } = useHypergraphApp(); - const { identity } = useHypergraphAuth(); + const result = useHypergraphAuth(); + let identity: Connect.PrivatePrivyAppIdentity | Connect.PrivateAppIdentity | null = result.identity; + if (!identity && result.privyIdentity) { + identity = result.privyIdentity; + } // Get own space inbox from store const space = useSelectorStore(store, (state) => state.context.spaces.find((s) => s.id === spaceId)); diff --git a/packages/hypergraph/src/connect/types.ts b/packages/hypergraph/src/connect/types.ts index 9375fce2..e178cc5e 100644 --- a/packages/hypergraph/src/connect/types.ts +++ b/packages/hypergraph/src/connect/types.ts @@ -63,6 +63,10 @@ export type PrivateAppIdentity = IdentityKeys & { accountAddress: string; }; +export type PrivatePrivyAppIdentity = IdentityKeys & { + accountAddress: string; + privyIdentityToken: string; +}; export class InvalidIdentityError { readonly _tag = 'InvalidIdentityError'; } diff --git a/packages/hypergraph/src/identity/auth-storage.ts b/packages/hypergraph/src/identity/auth-storage.ts index ffdc7e55..1c98e3bb 100644 --- a/packages/hypergraph/src/identity/auth-storage.ts +++ b/packages/hypergraph/src/identity/auth-storage.ts @@ -1,4 +1,4 @@ -import type { PrivateAppIdentity } from '../connect/types.js'; +import type { PrivateAppIdentity, PrivatePrivyAppIdentity } from '../connect/types.js'; import type { Storage } from './types.js'; export const storeIdentity = (storage: Storage, identity: PrivateAppIdentity) => { @@ -14,6 +14,39 @@ export const storeIdentity = (storage: Storage, identity: PrivateAppIdentity) => storage.setItem('hypergraph:permission-id', identity.permissionId); }; +export const storePrivyIdentity = (storage: Storage, identity: PrivatePrivyAppIdentity) => { + storage.setItem('hypergraph:privy-auth-identity-token', identity.privyIdentityToken); + storage.setItem('hypergraph:privy-auth-signature-public-key', identity.signaturePublicKey); + storage.setItem('hypergraph:privy-auth-signature-private-key', identity.signaturePrivateKey); + storage.setItem('hypergraph:privy-auth-encryption-public-key', identity.encryptionPublicKey); + storage.setItem('hypergraph:privy-auth-encryption-private-key', identity.encryptionPrivateKey); + storage.setItem('hypergraph:privy-auth-account-address', identity.accountAddress); +}; + +export const loadPrivyIdentity = (storage: Storage): PrivatePrivyAppIdentity | null => { + const privyIdentityToken = storage.getItem('hypergraph:privy-auth-identity-token'); + const signaturePublicKey = storage.getItem('hypergraph:privy-auth-signature-public-key'); + const signaturePrivateKey = storage.getItem('hypergraph:privy-auth-signature-private-key'); + const encryptionPublicKey = storage.getItem('hypergraph:privy-auth-encryption-public-key'); + const encryptionPrivateKey = storage.getItem('hypergraph:privy-auth-encryption-private-key'); + const accountAddress = storage.getItem('hypergraph:privy-auth-account-address'); + return privyIdentityToken && + signaturePublicKey && + signaturePrivateKey && + encryptionPublicKey && + encryptionPrivateKey && + accountAddress + ? { + privyIdentityToken, + signaturePublicKey, + signaturePrivateKey, + encryptionPublicKey, + encryptionPrivateKey, + accountAddress, + } + : null; +}; + export const loadIdentity = (storage: Storage): PrivateAppIdentity | null => { const address = storage.getItem('hypergraph:app-identity-address'); const addressPrivateKey = storage.getItem('hypergraph:app-identity-address-private-key'); @@ -65,3 +98,12 @@ export const wipeIdentity = (storage: Storage) => { storage.removeItem('hypergraph:session-token-expires'); storage.removeItem('hypergraph:permission-id'); }; + +export const wipePrivyIdentity = (storage: Storage) => { + storage.removeItem('hypergraph:privy-auth-identity-token'); + storage.removeItem('hypergraph:privy-auth-signature-public-key'); + storage.removeItem('hypergraph:privy-auth-signature-private-key'); + storage.removeItem('hypergraph:privy-auth-encryption-public-key'); + storage.removeItem('hypergraph:privy-auth-encryption-private-key'); + storage.removeItem('hypergraph:privy-auth-account-address'); +}; diff --git a/packages/hypergraph/src/identity/logout.ts b/packages/hypergraph/src/identity/logout.ts index 1c999f71..4334921f 100644 --- a/packages/hypergraph/src/identity/logout.ts +++ b/packages/hypergraph/src/identity/logout.ts @@ -1,8 +1,9 @@ import { store } from './../store.js'; -import { wipeIdentity } from './auth-storage.js'; +import { wipeIdentity, wipePrivyIdentity } from './auth-storage.js'; import type { Storage } from './types.js'; export function logout(storage: Storage) { wipeIdentity(storage); + wipePrivyIdentity(storage); store.send({ type: 'resetAuth' }); } diff --git a/packages/hypergraph/src/index.ts b/packages/hypergraph/src/index.ts index 1658592c..f8cff785 100644 --- a/packages/hypergraph/src/index.ts +++ b/packages/hypergraph/src/index.ts @@ -7,6 +7,7 @@ export * as Inboxes from './inboxes/index.js'; export * as Key from './key/index.js'; export * as Mapping from './mapping/index.js'; export * as Messages from './messages/index.js'; +export * as PrivyAuth from './privy-auth/privy-auth.js'; export * as SpaceEvents from './space-events/index.js'; export * as SpaceInfo from './space-info/index.js'; export * from './store.js'; diff --git a/packages/hypergraph/src/privy-auth/privy-auth.ts b/packages/hypergraph/src/privy-auth/privy-auth.ts new file mode 100644 index 00000000..3f3ac1e8 --- /dev/null +++ b/packages/hypergraph/src/privy-auth/privy-auth.ts @@ -0,0 +1,213 @@ +import * as Schema from 'effect/Schema'; +import type { SmartAccountClient } from 'permissionless'; +import type { Address, Chain, Hex, WalletClient } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { loadAccountAddress, storeAccountAddress, storeKeys } from '../connect/auth-storage.js'; +import { createIdentityKeys } from '../connect/create-identity-keys.js'; +import { decryptIdentity, encryptIdentity } from '../connect/identity-encryption.js'; +import { + addSmartAccountOwner, + getSmartAccountWalletClient, + isSmartAccountDeployed, + type SmartAccountParams, + smartAccountNeedsUpdate, + updateLegacySmartAccount, +} from '../connect/smart-account.js'; +import type { IdentityKeys, Signer, Storage } from '../connect/types.js'; +import { storePrivyIdentity } from '../identity/auth-storage.js'; +import { proveIdentityOwnership } from '../identity/prove-ownership.js'; +import * as Messages from '../messages/index.js'; +import { store } from '../store.js'; + +export async function identityExists(accountAddress: string, syncServerUri: string) { + const res = await fetch(new URL(`/connect/identity?accountAddress=${accountAddress}`, syncServerUri), { + method: 'GET', + }); + return res.status === 200; +} + +export async function signup( + signer: Signer, + _walletClient: WalletClient, + smartAccountClient: SmartAccountClient, + accountAddress: Address, + syncServerUri: string, + addressStorage: Storage, + keysStorage: Storage, + identityToken: string, + chain: Chain, + rpcUrl: string, +) { + const keys = createIdentityKeys(); + const { ciphertext, nonce } = await encryptIdentity(signer, keys); + + const localAccount = privateKeyToAccount(keys.signaturePrivateKey as `0x${string}`); + // This will deploy the smart account if it's not deployed + await addSmartAccountOwner(smartAccountClient, localAccount.address, chain, rpcUrl); + const localSmartAccountClient = await getSmartAccountWalletClient({ + owner: localAccount, + address: accountAddress, + rpcUrl, + chain, + }); + + const { accountProof, keyProof } = await proveIdentityOwnership(localSmartAccountClient, accountAddress, keys); + + const req: Messages.RequestConnectCreateIdentity = { + keyBox: { signer: await signer.getAddress(), accountAddress, ciphertext, nonce }, + accountProof, + keyProof, + signaturePublicKey: keys.signaturePublicKey, + encryptionPublicKey: keys.encryptionPublicKey, + }; + const res = await fetch(new URL('/connect/identity', syncServerUri), { + method: 'POST', + headers: { + 'privy-id-token': identityToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(req), + }); + if (res.status !== 200) { + // TODO: handle this better? + throw new Error(`Error creating identity: ${res.status}`); + } + const decoded = Schema.decodeUnknownSync(Messages.ResponseConnectCreateIdentity)(await res.json()); + if (!decoded.success) { + throw new Error('Error creating identity'); + } + storeKeys(keysStorage, accountAddress, keys); + storeAccountAddress(addressStorage, accountAddress); + return { + accountAddress, + keys, + }; +} + +export async function restoreKeys( + signer: Signer, + accountAddress: Address, + syncServerUri: string, + addressStorage: Storage, + keysStorage: Storage, + identityToken: string, +) { + const res = await fetch(new URL('/connect/identity/encrypted', syncServerUri), { + method: 'GET', + headers: { + 'privy-id-token': identityToken, + 'account-address': accountAddress, + 'Content-Type': 'application/json', + }, + }); + + if (res.status === 200) { + const decoded = Schema.decodeUnknownSync(Messages.ResponseIdentityEncrypted)(await res.json()); + const { keyBox } = decoded; + const { ciphertext, nonce } = keyBox; + const keys = await decryptIdentity(signer, ciphertext, nonce); + storeKeys(keysStorage, accountAddress, keys); + storeAccountAddress(addressStorage, accountAddress); + return { + accountAddress, + keys, + }; + } + throw new Error(`Error fetching identity ${res.status}`); +} + +const getAndUpdateSmartAccount = async ( + walletClient: WalletClient, + rpcUrl: string, + chain: Chain, + addressStorage: Storage, +) => { + const accountAddressFromStorage = loadAccountAddress(addressStorage) as Hex; + const smartAccountParams: SmartAccountParams = { + owner: walletClient, + rpcUrl, + chain, + }; + if (accountAddressFromStorage) { + smartAccountParams.address = accountAddressFromStorage; + } + const smartAccountWalletClient = await getSmartAccountWalletClient(smartAccountParams); + if (!smartAccountWalletClient.account) { + throw new Error('Smart account wallet client not found'); + } + console.log('smartAccountWalletClient', smartAccountWalletClient); + console.log('address', smartAccountWalletClient.account.address); + console.log('is deployed', await isSmartAccountDeployed(smartAccountWalletClient)); + // This will prompt the user to sign a user operation to update the smart account + if (await smartAccountNeedsUpdate(smartAccountWalletClient, chain, rpcUrl)) { + console.log('updating smart account'); + await updateLegacySmartAccount(smartAccountWalletClient, chain, rpcUrl); + smartAccountParams.address = smartAccountWalletClient.account.address; + // Create the client again to ensure we have the 7579 config now + return getSmartAccountWalletClient(smartAccountParams); + } + console.log('leaving getAndUpdateSmartAccount'); + return smartAccountWalletClient; +}; + +export async function login({ + walletClient, + signer, + syncServerUri, + addressStorage, + keysStorage, + identityToken, + rpcUrl, + chain, +}: { + walletClient: WalletClient; + signer: Signer; + syncServerUri: string; + addressStorage: Storage; + keysStorage: Storage; + identityToken: string; + rpcUrl: string; + chain: Chain; +}) { + const smartAccountWalletClient = await getAndUpdateSmartAccount(walletClient, rpcUrl, chain, addressStorage); + if (!smartAccountWalletClient.account) { + throw new Error('Smart account wallet client account not found'); + } + const accountAddress = smartAccountWalletClient.account.address; + + let authData: { + accountAddress: Address; + keys: IdentityKeys; + }; + const exists = await identityExists(accountAddress, syncServerUri); + if (!exists) { + authData = await signup( + signer, + walletClient, + smartAccountWalletClient, + accountAddress, + syncServerUri, + addressStorage, + keysStorage, + identityToken, + chain, + rpcUrl, + ); + } else { + authData = await restoreKeys(signer, accountAddress, syncServerUri, addressStorage, keysStorage, identityToken); + } + store.send({ type: 'reset' }); + store.send({ + type: 'setPrivyAuth', + identity: { + ...authData.keys, + accountAddress: authData.accountAddress, + privyIdentityToken: identityToken, + }, + }); + storePrivyIdentity(addressStorage, { + ...authData.keys, + accountAddress: authData.accountAddress, + privyIdentityToken: identityToken, + }); +} diff --git a/packages/hypergraph/src/store.ts b/packages/hypergraph/src/store.ts index 78566fc6..6f24a500 100644 --- a/packages/hypergraph/src/store.ts +++ b/packages/hypergraph/src/store.ts @@ -1,6 +1,6 @@ import type { AnyDocumentId, DocHandle, Repo } from '@automerge/automerge-repo'; import { createStore, type Store } from '@xstate/store'; -import type { PrivateAppIdentity } from './connect/types.js'; +import type { PrivateAppIdentity, PrivatePrivyAppIdentity } from './connect/types.js'; import type { DocumentContent } from './entity/types.js'; import { mergeMessages } from './inboxes/merge-messages.js'; import type { InboxSenderAuthPolicy } from './inboxes/types.js'; @@ -69,6 +69,7 @@ interface StoreContext { }; authenticated: boolean; identity: PrivateAppIdentity | null; + privyIdentity: PrivatePrivyAppIdentity | null; lastUpdateClock: { [spaceId: string]: number }; accountInboxes: AccountInboxStorageEntry[]; mapping: Mapping; @@ -83,6 +84,7 @@ const initialStoreContext: StoreContext = { identities: {}, authenticated: false, identity: null, + privyIdentity: null, lastUpdateClock: {}, accountInboxes: [], mapping: {}, @@ -146,6 +148,10 @@ type StoreEvent = type: 'setAuth'; identity: PrivateAppIdentity; } + | { + type: 'setPrivyAuth'; + identity: PrivatePrivyAppIdentity; + } | { type: 'resetAuth'; } @@ -534,12 +540,22 @@ export const store: Store = create authenticated: true, // TODO: remove hard-coded account address and use the one from the identity identity: { ...event.identity }, + privyIdentity: null, + }; + }, + setPrivyAuth: (context, event: { identity: PrivatePrivyAppIdentity }) => { + return { + ...context, + authenticated: true, + privyIdentity: { ...event.identity }, + identity: null, }; }, resetAuth: (context) => { return { ...context, identity: null, + privyIdentity: null, authenticated: false, }; }, From 4accb51656d3702eb274519aafc12d359d7b1264 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 16 Sep 2025 21:29:05 +0200 Subject: [PATCH 3/4] lint fix --- apps/privy-login-example/src/routes/login.lazy.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/privy-login-example/src/routes/login.lazy.tsx b/apps/privy-login-example/src/routes/login.lazy.tsx index 37aec02e..32d4da5c 100644 --- a/apps/privy-login-example/src/routes/login.lazy.tsx +++ b/apps/privy-login-example/src/routes/login.lazy.tsx @@ -49,6 +49,7 @@ function Login() { const rpcUrl = import.meta.env.VITE_HYPERGRAPH_RPC_URL; await PrivyAuth.login({ + // @ts-expect-error incompatible viem types walletClient, signer, syncServerUri, From c483a7b0ec8329e5d18b3e995d60d884510b40a4 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Tue, 16 Sep 2025 21:31:25 +0200 Subject: [PATCH 4/4] add changeset --- .changeset/silver-bikes-lie.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/silver-bikes-lie.md diff --git a/.changeset/silver-bikes-lie.md b/.changeset/silver-bikes-lie.md new file mode 100644 index 00000000..ef784e58 --- /dev/null +++ b/.changeset/silver-bikes-lie.md @@ -0,0 +1,9 @@ +--- +"@graphprotocol/hypergraph-react": patch +"@graphprotocol/hypergraph": patch +"connect": patch +"server": patch +--- + +add privy authentication functionality for internal apps + \ No newline at end of file