- 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,
+ },
});