From 9c34168fc5669df93ae2487a205c7594ff9c79a9 Mon Sep 17 00:00:00 2001
From: rimar1337 <132627503+rimar1337@users.noreply.github.com>
Date: Mon, 13 Oct 2025 08:35:37 +0700
Subject: [PATCH] oauth!!! (UnifiedAuthProvider)
---
.gitignore | 1 +
README.md | 20 +-
index.html | 1 -
oauthdev.mts | 48 +++
package-lock.json | 205 +++++++++++-
package.json | 5 +-
src/components/InfiniteCustomFeed.tsx | 5 +-
src/components/Login.tsx | 398 ++++++++++++-----------
src/components/UniversalPostRenderer.tsx | 2 +-
src/providers/UnifiedAuthProvider.tsx | 205 ++++++++++++
src/routeTree.gen.ts | 21 ++
src/routes/__root.tsx | 12 +-
src/routes/callback/index.tsx | 13 +
src/routes/index.tsx | 16 +-
src/utils/atoms.ts | 4 +-
src/utils/oauthClient.ts | 16 +
src/utils/useQuery.ts | 12 +-
vite.config.ts | 18 +
18 files changed, 765 insertions(+), 237 deletions(-)
create mode 100644 oauthdev.mts
create mode 100644 src/providers/UnifiedAuthProvider.tsx
create mode 100644 src/routes/callback/index.tsx
create mode 100644 src/utils/oauthClient.ts
diff --git a/.gitignore b/.gitignore
index 693a705..51e048c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ count.txt
.env
.nitro
.tanstack
+public/client-metadata.json
\ No newline at end of file
diff --git a/README.md b/README.md
index 86f662f..8e59dcc 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,16 @@ Red Dwarf is a Bluesky client that does not use any AppView servers, instead it
huge thanks to [Microcosm](https://microcosm.blue/) for making this possible
+## running dev and build
+in the `vite.config.ts` file you should change these values
+```ts
+const PROD_URL = "https://reddwarf.whey.party"
+const DEV_URL = "https://local3768forumtest.whey.party"
+```
+the PROD_URL is what will compile your oauth client metadata so it is very important to change that. same for DEV_URL if you are using a tunnel for dev work
+
+run dev with `npm run dev` (port 3768) and build with `npm run build` (the output is the `dist` folder)
+
## useQuery
Red Dwarf has been upgraded from its original bespoke caching system to Tanstack Query (react query). this migration was done to achieve a more robust and maintainable approach to data fetching and caching and state synchronization. ive seen serious performance gains from this switch!
@@ -22,8 +32,14 @@ the beating heart of Red Dwarf, the backlink index that provides contextual info
### Slingshot
though Red Dwarf was made before Microcosm [Slingshot](https://slingshot.microcosm.blue) existed, it now uses Slingshot to reduce load from each respective PDS server. Slignshot
-## PassAuthProvider
-a really bad app-password auth provider, inherited from TestFront and used in all my projects from TestFront to ForumTest (im very good at naming things). in ForumTest, its been superseded by the [OAuthProvider](https://tangled.sh/@whey.party/forumtest/blob/main/src/providers/OAuthProvider.tsx). i havent backported it here and maybe soon, although oauth makes it slightly more annoying to do development because it requires a tunnel so maybe someday if i managed to merge the password and oauth logins to provide both options
+## UnifiedAuthProvider
+a merged auth provider with oauth and password based login. oauth makes it slightly more annoying to do development because it requires a tunnel, so so the password auth option is still here if you do prefer password login for whatever reason.
+
+### Pass Auth
+a really bad app-password auth provider, inherited from TestFront and used in all my projects from TestFront to ForumTest (im very good at naming things).
+
+### OAuth
+taken from ForumTest [OAuthProvider](https://tangled.sh/@whey.party/forumtest/blob/main/src/providers/OAuthProvider.tsx)
## Custom Feeds
they work, but i havent implemented a simple way of viewing arbitraty feeds. currently it either loads discover (logged out) or your saved feeds (logged in) and its not a technical limitation i just havent implemented it yet
diff --git a/index.html b/index.html
index 91c6184..2efb19f 100644
--- a/index.html
+++ b/index.html
@@ -11,7 +11,6 @@
/>
-
Red Dwarf
diff --git a/oauthdev.mts b/oauthdev.mts
new file mode 100644
index 0000000..73382f5
--- /dev/null
+++ b/oauthdev.mts
@@ -0,0 +1,48 @@
+import fs from 'fs';
+import path from 'path';
+//import { generateClientMetadata } from './src/helpers/oauthClient'
+export const generateClientMetadata = (appOrigin: string) => {
+ const callbackPath = '/callback';
+
+ return {
+ "client_id": `${appOrigin}/client-metadata.json`,
+ "client_name": "ForumTest",
+ "client_uri": appOrigin,
+ "logo_uri": `${appOrigin}/logo192.png`,
+ "tos_uri": `${appOrigin}/terms-of-service`,
+ "policy_uri": `${appOrigin}/privacy-policy`,
+ "redirect_uris": [`${appOrigin}${callbackPath}`] as [string, ...string[]],
+ "scope": "atproto transition:generic",
+ "grant_types": ["authorization_code", "refresh_token"] as ["authorization_code", "refresh_token"],
+ "response_types": ["code"] as ["code"],
+ "token_endpoint_auth_method": "none" as "none",
+ "application_type": "web" as "web",
+ "dpop_bound_access_tokens": true
+ };
+}
+
+
+export function generateMetadataPlugin({prod, dev}:{prod: string, dev: string}) {
+ return {
+ name: 'vite-plugin-generate-metadata',
+ config(_config: any, { mode }: any) {
+ let appOrigin;
+ if (mode === 'production') {
+ appOrigin = prod
+ if (!appOrigin || !appOrigin.startsWith('https://')) {
+ throw new Error('VITE_APP_ORIGIN environment variable must be set to a valid HTTPS URL for production build.');
+ }
+ } else {
+ appOrigin = dev;
+ }
+
+
+ const metadata = generateClientMetadata(appOrigin);
+ const outputPath = path.resolve(process.cwd(), 'public', 'client-metadata.json');
+
+ fs.writeFileSync(outputPath, JSON.stringify(metadata, null, 2));
+
+ console.log(`✅ Generated client-metadata.json for ${appOrigin}`);
+ },
+ };
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index b0ea903..3579f31 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,7 @@
"name": "red-dwarf-tanstack",
"dependencies": {
"@atproto/api": "^0.16.6",
+ "@atproto/oauth-client-browser": "^0.3.33",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/query-sync-storage-persister": "^5.85.6",
"@tanstack/react-devtools": "^0.2.2",
@@ -71,6 +72,79 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/@atproto-labs/did-resolver": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.2.tgz",
+ "integrity": "sha512-ca2B7xR43tVoQ8XxBvha58DXwIH8cIyKQl6lpOKGkPUrJuFoO4iCLlDiSDi2Ueh+yE1rMDPP/qveHdajgDX3WQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/fetch": "0.2.3",
+ "@atproto-labs/pipe": "0.1.1",
+ "@atproto-labs/simple-store": "0.3.0",
+ "@atproto-labs/simple-store-memory": "0.1.4",
+ "@atproto/did": "0.2.1",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto-labs/fetch": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz",
+ "integrity": "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/pipe": "0.1.1"
+ }
+ },
+ "node_modules/@atproto-labs/handle-resolver": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.3.2.tgz",
+ "integrity": "sha512-KIerCzh3qb+zZoqWbIvTlvBY0XPq0r56kwViaJY/LTe/3oPO2JaqlYKS/F4dByWBhHK6YoUOJ0sWrh6PMJl40A==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/simple-store": "0.3.0",
+ "@atproto-labs/simple-store-memory": "0.1.4",
+ "@atproto/did": "0.2.1",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto-labs/identity-resolver": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.3.2.tgz",
+ "integrity": "sha512-MYxO9pe0WsFyi5HFdKAwqIqHfiF2kBPoVhAIuH/4PYHzGr799ED47xLhNMxR3ZUYrJm5+TQzWXypGZ0Btw1Ffw==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/did-resolver": "0.2.2",
+ "@atproto-labs/handle-resolver": "0.3.2"
+ }
+ },
+ "node_modules/@atproto-labs/pipe": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/pipe/-/pipe-0.1.1.tgz",
+ "integrity": "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==",
+ "license": "MIT"
+ },
+ "node_modules/@atproto-labs/simple-store": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.3.0.tgz",
+ "integrity": "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==",
+ "license": "MIT"
+ },
+ "node_modules/@atproto-labs/simple-store-memory": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.4.tgz",
+ "integrity": "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/simple-store": "0.3.0",
+ "lru-cache": "^10.2.0"
+ }
+ },
+ "node_modules/@atproto-labs/simple-store-memory/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
"node_modules/@atproto/api": {
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.16.6.tgz",
@@ -88,9 +162,9 @@
}
},
"node_modules/@atproto/common-web": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.2.tgz",
- "integrity": "sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==",
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz",
+ "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==",
"license": "MIT",
"dependencies": {
"graphemer": "^1.4.0",
@@ -99,19 +173,108 @@
"zod": "^3.23.8"
}
},
+ "node_modules/@atproto/did": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.2.1.tgz",
+ "integrity": "sha512-1i5BTU2GnBaaeYWhxUOnuEKFVq9euT5+dQPFabHpa927BlJ54PmLGyBBaOI7/NbLmN5HWwBa18SBkMpg3jGZRA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/jwk": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.6.0.tgz",
+ "integrity": "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw==",
+ "license": "MIT",
+ "dependencies": {
+ "multiformats": "^9.9.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/jwk-jose": {
+ "version": "0.1.11",
+ "resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.11.tgz",
+ "integrity": "sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/jwk": "0.6.0",
+ "jose": "^5.2.0"
+ }
+ },
+ "node_modules/@atproto/jwk-webcrypto": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.2.0.tgz",
+ "integrity": "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/jwk": "0.6.0",
+ "@atproto/jwk-jose": "0.1.11",
+ "zod": "^3.23.8"
+ }
+ },
"node_modules/@atproto/lexicon": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.0.tgz",
- "integrity": "sha512-3aAzEAy9EAPs3CxznzMhEcqDd7m3vz1eze/ya9/ThbB7yleqJIhz5GY2q76tCCwHPhn5qDDMhlA9kKV6fG23gA==",
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz",
+ "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==",
"license": "MIT",
"dependencies": {
- "@atproto/common-web": "^0.4.2",
+ "@atproto/common-web": "^0.4.3",
"@atproto/syntax": "^0.4.1",
"iso-datestring-validator": "^2.2.2",
"multiformats": "^9.9.0",
"zod": "^3.23.8"
}
},
+ "node_modules/@atproto/oauth-client": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.5.7.tgz",
+ "integrity": "sha512-pDvbvy9DCxrAJv7bAbBUzWrHZKhFy091HvEMZhr+EyZA6gSCGYmmQJG/coDj0oICSVQeafAZd+IxR0YUCWwmEg==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/did-resolver": "0.2.2",
+ "@atproto-labs/fetch": "0.2.3",
+ "@atproto-labs/handle-resolver": "0.3.2",
+ "@atproto-labs/identity-resolver": "0.3.2",
+ "@atproto-labs/simple-store": "0.3.0",
+ "@atproto-labs/simple-store-memory": "0.1.4",
+ "@atproto/did": "0.2.1",
+ "@atproto/jwk": "0.6.0",
+ "@atproto/oauth-types": "0.4.2",
+ "@atproto/xrpc": "0.7.5",
+ "core-js": "^3",
+ "multiformats": "^9.9.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@atproto/oauth-client-browser": {
+ "version": "0.3.33",
+ "resolved": "https://registry.npmjs.org/@atproto/oauth-client-browser/-/oauth-client-browser-0.3.33.tgz",
+ "integrity": "sha512-IvHn/5W3e9GXFUGXQ4MV19E4HXY4zJFgu+eZRWexIXnZl4GwgTH7op8J1SosczdOK1Ngu+LnHE6npcNhUGGd6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto-labs/did-resolver": "0.2.2",
+ "@atproto-labs/handle-resolver": "0.3.2",
+ "@atproto-labs/simple-store": "0.3.0",
+ "@atproto/did": "0.2.1",
+ "@atproto/jwk": "0.6.0",
+ "@atproto/jwk-webcrypto": "0.2.0",
+ "@atproto/oauth-client": "0.5.7",
+ "@atproto/oauth-types": "0.4.2",
+ "core-js": "^3"
+ }
+ },
+ "node_modules/@atproto/oauth-types": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.4.2.tgz",
+ "integrity": "sha512-gcfNTyFsPJcYDf79M0iKHykWqzxloscioKoerdIN3MTS3htiNOSgZjm2p8ho7pdrElLzea3qktuhTQI39j1XFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@atproto/did": "0.2.1",
+ "@atproto/jwk": "0.6.0",
+ "zod": "^3.23.8"
+ }
+ },
"node_modules/@atproto/syntax": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz",
@@ -119,12 +282,12 @@
"license": "MIT"
},
"node_modules/@atproto/xrpc": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.4.tgz",
- "integrity": "sha512-sDi68+QE1XHegTaNAndlX41Gp827pouSzSs8CyAwhrqZdsJUxE3P7TMtrA0z+zAjvxVyvzscRc0TsN/fGUGrhw==",
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz",
+ "integrity": "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==",
"license": "MIT",
"dependencies": {
- "@atproto/lexicon": "^0.5.0",
+ "@atproto/lexicon": "^0.5.1",
"zod": "^3.23.8"
}
},
@@ -2869,6 +3032,17 @@
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
"license": "MIT"
},
+ "node_modules/core-js": {
+ "version": "3.46.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
+ "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
@@ -3429,6 +3603,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jose": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
+ "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/jotai": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.13.1.tgz",
diff --git a/package.json b/package.json
index 5f79a12..3746350 100644
--- a/package.json
+++ b/package.json
@@ -3,14 +3,15 @@
"private": true,
"type": "module",
"scripts": {
- "dev": "vite --port 3000",
- "start": "vite --port 3000",
+ "dev": "vite --port 3768",
+ "start": "vite --port 3768",
"build": "vite build && tsc",
"serve": "vite preview",
"test": "vitest run"
},
"dependencies": {
"@atproto/api": "^0.16.6",
+ "@atproto/oauth-client-browser": "^0.3.33",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/query-sync-storage-persister": "^5.85.6",
"@tanstack/react-devtools": "^0.2.2",
diff --git a/src/components/InfiniteCustomFeed.tsx b/src/components/InfiniteCustomFeed.tsx
index 92367c0..6795741 100644
--- a/src/components/InfiniteCustomFeed.tsx
+++ b/src/components/InfiniteCustomFeed.tsx
@@ -1,7 +1,7 @@
import * as React from "react";
//import { useInView } from "react-intersection-observer";
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
-import { useAuth } from "~/providers/PassAuthProvider";
+import { useAuth } from "~/providers/UnifiedAuthProvider";
import {
useQueryArbitrary,
useQueryIdentity,
@@ -19,7 +19,8 @@ export function InfiniteCustomFeed({
pdsUrl,
feedServiceDid,
}: InfiniteCustomFeedProps) {
- const { agent, authed } = useAuth();
+ const { agent } = useAuth();
+ const authed = !!agent?.did;
// const identityresultmaybe = useQueryIdentity(agent?.did);
// const identity = identityresultmaybe?.data;
diff --git a/src/components/Login.tsx b/src/components/Login.tsx
index 642773d..238ead4 100644
--- a/src/components/Login.tsx
+++ b/src/components/Login.tsx
@@ -1,229 +1,233 @@
+// src/components/Login.tsx
import React, { useEffect, useState, useRef } from "react";
-import { useAuth } from "~/providers/PassAuthProvider";
+import { useAuth } from "~/providers/UnifiedAuthProvider";
+import { Agent } from "@atproto/api";
-interface LoginProps {
- compact?: boolean;
-}
-
-export default function Login({ compact = false }: LoginProps) {
- const { loginStatus, login, logout, loading, authed, agent } = useAuth();
- const [user, setUser] = useState("");
- const [password, setPassword] = useState("");
- const [serviceURL, setServiceURL] = useState("bsky.social");
- const [showLoginForm, setShowLoginForm] = useState(false);
- const formRef = useRef(null);
-
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (formRef.current && !formRef.current.contains(event.target as Node)) {
- setShowLoginForm(false);
- }
- }
+// --- 1. The Main Component (Orchestrator with `compact` prop) ---
+export default function Login({ compact = false }: { compact?: boolean }) {
+ const { status, agent, logout } = useAuth();
- if (showLoginForm) {
- document.addEventListener("mousedown", handleClickOutside);
- }
-
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- };
- }, [showLoginForm]);
-
- if (loading) {
+ // Loading state can be styled differently based on the prop
+ if (status === "loading") {
return (
-
- Loading...
+
+
);
}
- if (compact) {
- if (authed) {
+ // --- LOGGED IN STATE ---
+ if (status === "signedIn") {
+ // Large view
+ if (!compact) {
return (
+
+
+
+ You are logged in!
+
+
+
+ Log out
+
+
+
+ );
+ }
+ // Compact view
+ return (
+
+
Log out
- );
- } else {
- return (
-
-
setShowLoginForm(!showLoginForm)}
- className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors"
- >
- Log in
-
- {showLoginForm && (
-
- )}
-
- );
- }
+
+ );
+ }
+
+ // --- LOGGED OUT STATE ---
+ if (!compact) {
+ // Large view renders the form directly in the card
+ return (
+
+
+
+ );
}
+ // Compact view renders a button that toggles the form in a dropdown
+ return
;
+}
+
+// --- 2. The Reusable, Self-Contained Login Form Component ---
+export function UnifiedLoginForm() {
+ const [mode, setMode] = useState<"oauth" | "password">("oauth");
+
return (
-
- {authed ? (
-
-
- You are logged in!
-
-
-
- Log out
-
-
- ) : (
-
- )}
+
+
+ setMode("oauth")}
+ />
+ setMode("password")}
+ />
+
+ {mode === "oauth" ?
:
}
);
}
-export const ProfileThing = () => {
- const { agent, loading, loginStatus, authed } = useAuth();
- const [response, setResponse] = useState
(null);
+// --- 3. Helper components for layouts, forms, and UI ---
+
+// A new component to contain the logic for the compact dropdown
+const CompactLoginButton = () => {
+ const [showForm, setShowForm] = useState(false);
+ const formRef = useRef(null);
useEffect(() => {
- if (loginStatus && agent && !loading && authed) {
- fetchUser();
+ function handleClickOutside(event: MouseEvent) {
+ if (formRef.current && !formRef.current.contains(event.target as Node)) {
+ setShowForm(false);
+ }
}
- // eslint-disable-next-line
- }, [loginStatus, agent, loading, authed]);
-
- const fetchUser = async () => {
- if (!agent) {
- console.error("Agent is null or undefined");
- return;
+ if (showForm) {
+ document.addEventListener("mousedown", handleClickOutside);
}
- const res = await agent.app.bsky.actor.getProfile({
- actor: agent.assertDid,
- });
- setResponse(res.data);
- };
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [showForm]);
- if (!authed) {
- return
- return (
-
-
- Login
-
-
- );
- }
+ return (
+
+
setShowForm(!showForm)}
+ className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors"
+ >
+ Log in
+
+ {showForm && (
+
+
+
+ )}
+
+ );
+};
- if (!response) {
- return (
-
-
- Loading...
-
- );
- }
+const TabButton = ({ label, active, onClick }: { label: string; active: boolean; onClick: () => void; }) => (
+
+ {label}
+
+);
+const OAuthForm = () => {
+ const { loginWithOAuth } = useAuth();
+ const [handle, setHandle] = useState("");
+ const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (handle.trim()) loginWithOAuth(handle); };
return (
-
-
-
-
{response?.displayName}
-
@{response?.handle}
-
-
+
+ );
+};
+
+const PasswordForm = () => {
+ const { loginWithPassword } = useAuth();
+ const [user, setUser] = useState("");
+ const [password, setPassword] = useState("");
+ const [serviceURL, setServiceURL] = useState("bsky.social");
+ const [error, setError] = useState(null);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError(null);
+ try {
+ await loginWithPassword(user, password, `https://${serviceURL}`);
+ } catch (err) {
+ setError("Login failed. Check your handle and App Password.");
+ }
+ };
+
+ return (
+
);
};
+
+// --- Profile Component (now supports a `large` prop for styling) ---
+export const ProfileThing = ({ agent, large = false }: { agent: Agent | null; large?: boolean }) => {
+ const [profile, setProfile] = useState(null);
+
+ useEffect(() => {
+ const fetchUser = async () => {
+ const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid;
+ if (!did) return;
+ try {
+ const res = await agent!.getProfile({ actor: did });
+ setProfile(res.data);
+ } catch (e) { console.error("Failed to fetch profile", e); }
+ };
+ if (agent) fetchUser();
+ }, [agent]);
+
+ if (!profile) {
+ return ( // Skeleton loader
+
+ );
+ }
+
+ return (
+
+
+
+
{profile?.displayName}
+
@{profile?.handle}
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/UniversalPostRenderer.tsx b/src/components/UniversalPostRenderer.tsx
index 533a590..2767c79 100644
--- a/src/components/UniversalPostRenderer.tsx
+++ b/src/components/UniversalPostRenderer.tsx
@@ -954,7 +954,7 @@ import type {
} from "@atproto/api/dist/client/types/app/bsky/feed/defs";
import { useEffect, useRef, useState } from "react";
import ReactPlayer from "react-player";
-import { useAuth } from "~/providers/PassAuthProvider";
+import { useAuth } from "~/providers/UnifiedAuthProvider";
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
// import type {
// ViewRecord,
diff --git a/src/providers/UnifiedAuthProvider.tsx b/src/providers/UnifiedAuthProvider.tsx
new file mode 100644
index 0000000..094c335
--- /dev/null
+++ b/src/providers/UnifiedAuthProvider.tsx
@@ -0,0 +1,205 @@
+// src/providers/UnifiedAuthProvider.tsx
+import React, {
+ createContext,
+ useState,
+ useEffect,
+ useContext,
+ useCallback,
+} from "react";
+// Import both Agent and the (soon to be deprecated) AtpAgent
+import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api";
+import { oauthClient } from "../utils/oauthClient"; // Adjust path if needed
+import {
+ type OAuthSession,
+ TokenInvalidError,
+ TokenRefreshError,
+ TokenRevokedError,
+} from "@atproto/oauth-client-browser";
+
+// Define the unified status and authentication method
+type AuthStatus = "loading" | "signedIn" | "signedOut";
+type AuthMethod = "password" | "oauth" | null;
+
+interface AuthContextValue {
+ agent: Agent | null; // The agent is typed as the base class `Agent`
+ status: AuthStatus;
+ authMethod: AuthMethod;
+ loginWithPassword: (
+ user: string,
+ password: string,
+ service?: string,
+ ) => Promise;
+ loginWithOAuth: (handleOrPdsUrl: string) => Promise;
+ logout: () => Promise;
+}
+
+const AuthContext = createContext({} as AuthContextValue);
+
+export const UnifiedAuthProvider = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ // The state is typed as the base class `Agent`, which accepts both `Agent` and `AtpAgent` instances.
+ const [agent, setAgent] = useState(null);
+ const [status, setStatus] = useState("loading");
+ const [authMethod, setAuthMethod] = useState(null);
+ const [oauthSession, setOauthSession] = useState(null);
+
+ // Unified Initialization Logic
+ const initialize = useCallback(async () => {
+ // --- 1. Try OAuth initialization first ---
+ try {
+ const oauthResult = await oauthClient.init();
+ if (oauthResult) {
+ console.log("OAuth session restored.");
+ const apiAgent = new Agent(oauthResult.session); // Standard Agent
+ setAgent(apiAgent);
+ setOauthSession(oauthResult.session);
+ setAuthMethod("oauth");
+ setStatus("signedIn");
+ return; // Success
+ }
+ } catch (e) {
+ console.error("OAuth init failed, checking password session.", e);
+ }
+
+ // --- 2. If no OAuth, try password-based session using AtpAgent ---
+ try {
+ const service = localStorage.getItem("service");
+ const sessionString = localStorage.getItem("sess");
+
+ if (service && sessionString) {
+ console.log("Resuming password-based session using AtpAgent...");
+ // Use the original, working AtpAgent logic
+ const apiAgent = new AtpAgent({ service });
+ const session: AtpSessionData = JSON.parse(sessionString);
+ await apiAgent.resumeSession(session);
+
+ console.log("Password-based session resumed successfully.");
+ setAgent(apiAgent); // This works because AtpAgent is a subclass of Agent
+ setAuthMethod("password");
+ setStatus("signedIn");
+ return; // Success
+ }
+ } catch (e) {
+ console.error("Failed to resume password-based session.", e);
+ localStorage.removeItem("sess");
+ localStorage.removeItem("service");
+ }
+
+ // --- 3. If neither worked, user is signed out ---
+ console.log("No active session found.");
+ setStatus("signedOut");
+ setAgent(null);
+ setAuthMethod(null);
+ }, []);
+
+ useEffect(() => {
+ const handleOAuthSessionDeleted = (
+ event: CustomEvent<{ sub: string; cause: TokenRefreshError | TokenRevokedError | TokenInvalidError }>,
+ ) => {
+ console.error(`OAuth Session for ${event.detail.sub} was deleted.`, event.detail.cause);
+ setAgent(null);
+ setOauthSession(null);
+ setAuthMethod(null);
+ setStatus("signedOut");
+ };
+
+ oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener);
+ initialize();
+
+ return () => {
+ oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener);
+ };
+ }, [initialize]);
+
+ // --- Login Methods ---
+ const loginWithPassword = async (
+ user: string,
+ password: string,
+ service: string = "https://bsky.social",
+ ) => {
+ if (status !== "signedOut") return;
+ setStatus("loading");
+ try {
+ let sessionData: AtpSessionData | undefined;
+ // Use the AtpAgent for its simple login and session persistence
+ const apiAgent = new AtpAgent({
+ service,
+ persistSession: (_evt, sess) => {
+ sessionData = sess;
+ },
+ });
+ await apiAgent.login({ identifier: user, password });
+
+ if (sessionData) {
+ localStorage.setItem("service", service);
+ localStorage.setItem("sess", JSON.stringify(sessionData));
+ setAgent(apiAgent); // Store the AtpAgent instance in our state
+ setAuthMethod("password");
+ setStatus("signedIn");
+ console.log("Successfully logged in with password.");
+ } else {
+ throw new Error("Session data not persisted after login.");
+ }
+ } catch (e) {
+ console.error("Password login failed:", e);
+ setStatus("signedOut");
+ throw e;
+ }
+ };
+
+ const loginWithOAuth = useCallback(async (handleOrPdsUrl: string) => {
+ if (status !== "signedOut") return;
+ try {
+ sessionStorage.setItem("postLoginRedirect", window.location.pathname + window.location.search);
+ await oauthClient.signIn(handleOrPdsUrl);
+ } catch (err) {
+ console.error("OAuth sign-in aborted or failed:", err);
+ }
+ }, [status]);
+
+ // --- Unified Logout ---
+ const logout = useCallback(async () => {
+ if (status !== "signedIn" || !agent) return;
+ setStatus("loading");
+
+ try {
+ if (authMethod === "oauth" && oauthSession) {
+ await oauthClient.revoke(oauthSession.sub);
+ console.log("OAuth session revoked.");
+ } else if (authMethod === "password") {
+ localStorage.removeItem("service");
+ localStorage.removeItem("sess");
+ // AtpAgent has its own logout methods
+ await (agent as AtpAgent).com.atproto.server.deleteSession();
+ console.log("Password-based session deleted.");
+ }
+ } catch (e) {
+ console.error("Logout failed:", e);
+ } finally {
+ setAgent(null);
+ setAuthMethod(null);
+ setOauthSession(null);
+ setStatus("signedOut");
+ }
+ }, [status, authMethod, agent, oauthSession]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => useContext(AuthContext);
\ No newline at end of file
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 6cd7388..74bddf7 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -15,6 +15,7 @@ import { Route as NotificationsRouteImport } from './routes/notifications'
import { Route as FeedsRouteImport } from './routes/feeds'
import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as CallbackIndexRouteImport } from './routes/callback/index'
import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
import { Route as ProfileDidIndexRouteImport } from './routes/profile.$did/index'
import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
@@ -50,6 +51,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const CallbackIndexRoute = CallbackIndexRouteImport.update({
+ id: '/callback/',
+ path: '/callback/',
+ getParentRoute: () => rootRouteImport,
+} as any)
const PathlessLayoutNestedLayoutRoute =
PathlessLayoutNestedLayoutRouteImport.update({
id: '/_nested-layout',
@@ -84,6 +90,7 @@ export interface FileRoutesByFullPath {
'/notifications': typeof NotificationsRoute
'/search': typeof SearchRoute
'/settings': typeof SettingsRoute
+ '/callback': typeof CallbackIndexRoute
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
'/profile/$did': typeof ProfileDidIndexRoute
@@ -95,6 +102,7 @@ export interface FileRoutesByTo {
'/notifications': typeof NotificationsRoute
'/search': typeof SearchRoute
'/settings': typeof SettingsRoute
+ '/callback': typeof CallbackIndexRoute
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
'/profile/$did': typeof ProfileDidIndexRoute
@@ -109,6 +117,7 @@ export interface FileRoutesById {
'/search': typeof SearchRoute
'/settings': typeof SettingsRoute
'/_pathlessLayout/_nested-layout': typeof PathlessLayoutNestedLayoutRouteWithChildren
+ '/callback/': typeof CallbackIndexRoute
'/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
'/profile/$did/': typeof ProfileDidIndexRoute
@@ -122,6 +131,7 @@ export interface FileRouteTypes {
| '/notifications'
| '/search'
| '/settings'
+ | '/callback'
| '/route-a'
| '/route-b'
| '/profile/$did'
@@ -133,6 +143,7 @@ export interface FileRouteTypes {
| '/notifications'
| '/search'
| '/settings'
+ | '/callback'
| '/route-a'
| '/route-b'
| '/profile/$did'
@@ -146,6 +157,7 @@ export interface FileRouteTypes {
| '/search'
| '/settings'
| '/_pathlessLayout/_nested-layout'
+ | '/callback/'
| '/_pathlessLayout/_nested-layout/route-a'
| '/_pathlessLayout/_nested-layout/route-b'
| '/profile/$did/'
@@ -159,6 +171,7 @@ export interface RootRouteChildren {
NotificationsRoute: typeof NotificationsRoute
SearchRoute: typeof SearchRoute
SettingsRoute: typeof SettingsRoute
+ CallbackIndexRoute: typeof CallbackIndexRoute
ProfileDidIndexRoute: typeof ProfileDidIndexRoute
ProfileDidPostRkeyRoute: typeof ProfileDidPostRkeyRoute
}
@@ -207,6 +220,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/callback/': {
+ id: '/callback/'
+ path: '/callback'
+ fullPath: '/callback'
+ preLoaderRoute: typeof CallbackIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/_pathlessLayout/_nested-layout': {
id: '/_pathlessLayout/_nested-layout'
path: ''
@@ -282,6 +302,7 @@ const rootRouteChildren: RootRouteChildren = {
NotificationsRoute: NotificationsRoute,
SearchRoute: SearchRoute,
SettingsRoute: SettingsRoute,
+ CallbackIndexRoute: CallbackIndexRoute,
ProfileDidIndexRoute: ProfileDidIndexRoute,
ProfileDidPostRkeyRoute: ProfileDidPostRkeyRoute,
}
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 13983da..407c8f0 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -21,9 +21,9 @@ import Login from "~/components/Login";
import { NotFound } from "~/components/NotFound";
import appCss from "~/styles/app.css?url";
import { seo } from "~/utils/seo";
-import { AuthProvider, useAuth } from "~/providers/PassAuthProvider";
+import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider";
import { PersistentStoreProvider } from "~/providers/PersistentStoreProvider";
-import type AtpAgent from "@atproto/api";
+import type Agent from "@atproto/api";
import type { QueryClient } from "@tanstack/react-query";
export const Route = createRootRouteWithContext<{
@@ -44,7 +44,6 @@ export const Route = createRootRouteWithContext<{
}),
],
links: [
- { rel: "stylesheet", href: appCss },
{
rel: "apple-touch-icon",
sizes: "180x180",
@@ -79,20 +78,21 @@ export const Route = createRootRouteWithContext<{
function RootComponent() {
return (
-
+
-
+
);
}
function RootDocument({ children }: { children: React.ReactNode }) {
const location = useLocation();
const navigate = useNavigate();
- const { agent, authed } = useAuth();
+ const { agent } = useAuth();
+ const authed = !!agent?.did;
const isHome = location.pathname === "/";
const isNotifications = location.pathname.startsWith("/notifications");
const isProfile = agent && ((location.pathname === (`/profile/${agent?.did}`)) || (location.pathname === (`/profile/${encodeURIComponent(agent?.did??"")}`)));
diff --git a/src/routes/callback/index.tsx b/src/routes/callback/index.tsx
new file mode 100644
index 0000000..1743457
--- /dev/null
+++ b/src/routes/callback/index.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute, useNavigate } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/callback/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const navigate = useNavigate()
+ const redirectPath = sessionStorage.getItem('postLoginRedirect') || '/';
+ navigate({to:redirectPath})
+ sessionStorage.removeItem('postLoginRedirect');
+ return Hello "/callback/"!
+}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 3f9c6ab..225019a 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -6,7 +6,7 @@ import {
UniversalPostRendererATURILoader,
} from "~/components/UniversalPostRenderer";
import * as React from "react";
-import { useAuth } from "~/providers/PassAuthProvider";
+import { useAuth } from "~/providers/UnifiedAuthProvider";
//import { usePersistentStore } from "~/providers/PersistentStoreProvider";
import {
useQueryIdentity,
@@ -99,12 +99,13 @@ function PendingHome() {
function Home() {
const {
agent,
- loginStatus,
- login,
+ status,
+ authMethod,
+ loginWithPassword,
+ loginWithOAuth,
logout,
- loading: loadering,
- authed,
} = useAuth();
+ const authed = !!agent?.did;
useEffect(() => {
if (agent?.did) {
@@ -112,14 +113,15 @@ function Home() {
} else {
store.set(authedAtom, false);
}
- }, [loginStatus, agent, authed]);
+ }, [status, agent, authed]);
useEffect(() => {
if (agent) {
+ // is it just me or is the type really weird here it should be Agent not AtpAgent
store.set(agentAtom, agent);
} else {
store.set(agentAtom, null);
}
- }, [loginStatus, agent, authed]);
+ }, [status, agent, authed]);
//const { get, set } = usePersistentStore();
// const [feed, setFeed] = React.useState([]);
diff --git a/src/utils/atoms.ts b/src/utils/atoms.ts
index 5d86cbf..5f3a203 100644
--- a/src/utils/atoms.ts
+++ b/src/utils/atoms.ts
@@ -1,4 +1,4 @@
-import type AtpAgent from "@atproto/api";
+import type Agent from "@atproto/api";
import { atom, createStore } from "jotai";
import { atomWithStorage } from 'jotai/utils';
@@ -21,5 +21,5 @@ export const likedPostsAtom = atomWithStorage>(
{}
);
-export const agentAtom = atom(null);
+export const agentAtom = atom(null);
export const authedAtom = atom(false);
diff --git a/src/utils/oauthClient.ts b/src/utils/oauthClient.ts
new file mode 100644
index 0000000..494e1ed
--- /dev/null
+++ b/src/utils/oauthClient.ts
@@ -0,0 +1,16 @@
+// src/helpers/oauthClient.ts
+import { BrowserOAuthClient, type ClientMetadata } from '@atproto/oauth-client-browser';
+
+// This is your app's PDS for resolving handles if not provided.
+// You might need to host your own or use a public one.
+const handleResolverPDS = 'https://bsky.social';
+
+// This assumes your client-metadata.json is in the /public folder
+// and will be served at the root of your domain.
+import clientMetadata from '../../public/client-metadata.json' assert { type: 'json' };
+
+export const oauthClient = new BrowserOAuthClient({
+ // The type assertion is needed because the static import isn't strictly typed
+ clientMetadata: clientMetadata as ClientMetadata,
+ handleResolver: handleResolverPDS,
+});
\ No newline at end of file
diff --git a/src/utils/useQuery.ts b/src/utils/useQuery.ts
index 5f12ccb..9400d67 100644
--- a/src/utils/useQuery.ts
+++ b/src/utils/useQuery.ts
@@ -332,7 +332,7 @@ type linksAllResponse = {
export function constructFeedSkeletonQuery(options?: {
feedUri: string;
- agent?: ATPAPI.AtpAgent;
+ agent?: ATPAPI.Agent;
isAuthed: boolean;
pdsUrl?: string;
feedServiceDid?: string;
@@ -372,7 +372,7 @@ export function constructFeedSkeletonQuery(options?: {
export function useQueryFeedSkeleton(options?: {
feedUri: string;
- agent?: ATPAPI.AtpAgent;
+ agent?: ATPAPI.Agent;
isAuthed: boolean;
pdsUrl?: string;
feedServiceDid?: string;
@@ -380,7 +380,7 @@ export function useQueryFeedSkeleton(options?: {
return useQuery(constructFeedSkeletonQuery(options));
}
-export function constructPreferencesQuery(agent?: ATPAPI.AtpAgent | undefined, pdsUrl?: string | undefined) {
+export function constructPreferencesQuery(agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined) {
return queryOptions({
queryKey: ['preferences', agent?.did],
queryFn: async () => {
@@ -393,7 +393,7 @@ export function constructPreferencesQuery(agent?: ATPAPI.AtpAgent | undefined, p
});
}
export function useQueryPreferences(options: {
- agent?: ATPAPI.AtpAgent | undefined, pdsUrl?: string | undefined
+ agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined
}) {
return useQuery(constructPreferencesQuery(options.agent, options.pdsUrl));
}
@@ -498,7 +498,7 @@ type FeedSkeletonPage = ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema;
export function constructInfiniteFeedSkeletonQuery(options: {
feedUri: string;
- agent?: ATPAPI.AtpAgent;
+ agent?: ATPAPI.Agent;
isAuthed: boolean;
pdsUrl?: string;
feedServiceDid?: string;
@@ -537,7 +537,7 @@ export function constructInfiniteFeedSkeletonQuery(options: {
export function useInfiniteQueryFeedSkeleton(options: {
feedUri: string;
- agent?: ATPAPI.AtpAgent;
+ agent?: ATPAPI.Agent;
isAuthed: boolean;
pdsUrl?: string;
feedServiceDid?: string;
diff --git a/vite.config.ts b/vite.config.ts
index e7e9ab6..4becafd 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,6 +1,14 @@
import { defineConfig } from "vite";
import viteReact from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
+import { generateMetadataPlugin } from "./oauthdev.mts";
+
+const PROD_URL = "https://reddwarf.whey.party"
+const DEV_URL = "https://local3768forumtest.whey.party"
+
+function shp(url: string): string {
+ return url.replace(/^https?:\/\//, '');
+}
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import { resolve } from "node:path";
@@ -8,6 +16,10 @@ import { resolve } from "node:path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
+ generateMetadataPlugin({
+ prod: PROD_URL,
+ dev: DEV_URL,
+ }),
TanStackRouterVite({ autoCodeSplitting: true }),
viteReact(),
tailwindcss(),
@@ -22,4 +34,10 @@ export default defineConfig({
"~": resolve(__dirname, "./src"),
},
},
+ server: {
+ allowedHosts: [shp(PROD_URL),shp(DEV_URL)],
+ },
+ css: {
+ devSourcemap: true,
+ },
});
--
2.51.2