refactor: replace useQuery with useAuthQuery for improved authentication handling

- Updated multiple components to utilize useAuthQuery instead of useQuery for fetching user-related data, enhancing authentication management.
- Adjusted balance and subscription queries across various components including InitUser, ManageSubscription, PricingCards, CreditDisplay, and others to ensure consistent authentication checks.
- Improved overall code maintainability by centralizing authentication logic in the new hook.
This commit is contained in:
Matthias
2026-03-27 18:35:12 +01:00
parent 2f89465e82
commit 6e38e2d270
9 changed files with 48 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useQuery } from "convex/react";
import { useAuthQuery } from "@/hooks/use-auth-query";
import { ExternalLink } from "lucide-react";
import { Badge } from "@/components/ui/badge";
@@ -19,7 +19,7 @@ const TIER_LABELS: Record<keyof typeof TIER_MONTHLY_CREDITS, string> = {
};
export function ManageSubscription() {
const subscription = useQuery(api.credits.getSubscription);
const subscription = useAuthQuery(api.credits.getSubscription);
const tier = normalizeTier(subscription?.tier);
return (

View File

@@ -1,6 +1,6 @@
"use client";
import { useQuery } from "convex/react";
import { useAuthQuery } from "@/hooks/use-auth-query";
import { Check } from "lucide-react";
import { Badge } from "@/components/ui/badge";
@@ -18,7 +18,7 @@ import { msg } from "@/lib/toast-messages";
const TIER_ORDER = ["free", "starter", "pro", "max"] as const;
export function PricingCards() {
const subscription = useQuery(api.credits.getSubscription);
const subscription = useAuthQuery(api.credits.getSubscription);
const currentTier = normalizeTier(subscription?.tier);
async function handleCheckout(polarProductId: string) {

View File

@@ -236,10 +236,11 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
const { resolvedTheme } = useTheme();
const { data: session, isPending: isSessionPending } = authClient.useSession();
const { isLoading: isAuthLoading, isAuthenticated } = useConvexAuth();
const shouldSkipCanvasQueries = isAuthLoading || !isAuthenticated;
const shouldSkipCanvasQueries =
isSessionPending || isAuthLoading || !isAuthenticated;
const convexAuthUserProbe = useQuery(
api.auth.safeGetAuthUser,
isAuthLoading ? "skip" : {},
shouldSkipCanvasQueries ? "skip" : {},
);
useEffect(() => {
@@ -376,7 +377,6 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
// ─── Convex → Lokaler State Sync ──────────────────────────────
useEffect(() => {
if (!convexNodes || isDragging.current) return;
// eslint-disable-next-line react-hooks/set-state-in-effect
setNodes((previousNodes) => {
const incomingNodes = withResolvedCompareData(convexNodes.map(convexNodeToRF), edges);
return mergeNodesPreservingLocalState(previousNodes, incomingNodes);
@@ -385,7 +385,6 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
useEffect(() => {
if (!convexEdges) return;
// eslint-disable-next-line react-hooks/set-state-in-effect
setEdges((prev) => {
const tempEdges = prev.filter((e) => e.className === "temp");
const mapped = convexEdges.map(convexEdgeToRF);
@@ -398,7 +397,6 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
useEffect(() => {
if (isDragging.current) return;
// eslint-disable-next-line react-hooks/set-state-in-effect
setNodes((nds) => withResolvedCompareData(nds, edges));
}, [edges]);

View File

@@ -1,6 +1,7 @@
"use client";
import { useMutation, useQuery } from "convex/react";
import { useMutation } from "convex/react";
import { useAuthQuery } from "@/hooks/use-auth-query";
import { api } from "@/convex/_generated/api";
import { Coins } from "lucide-react";
import { toast } from "@/lib/toast";
@@ -27,8 +28,8 @@ const showTestCreditGrant =
process.env.NEXT_PUBLIC_ALLOW_TEST_CREDIT_GRANT === "true";
export function CreditDisplay() {
const balance = useQuery(api.credits.getBalance);
const subscription = useQuery(api.credits.getSubscription);
const balance = useAuthQuery(api.credits.getBalance);
const subscription = useAuthQuery(api.credits.getSubscription);
const grantTestCredits = useMutation(api.credits.grantTestCredits);
if (balance === undefined || subscription === undefined) {

View File

@@ -9,7 +9,8 @@ import {
type NodeProps,
type Node,
} from "@xyflow/react";
import { useMutation, useAction, useQuery } from "convex/react";
import { useMutation, useAction } from "convex/react";
import { useAuthQuery } from "@/hooks/use-auth-query";
import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import BaseNodeWrapper from "./base-node-wrapper";
@@ -108,7 +109,7 @@ export default function PromptNode({
const dataRef = useRef(data);
dataRef.current = data;
const balance = useQuery(api.credits.getBalance);
const balance = useAuthQuery(api.credits.getBalance);
const creditCost = getModel(DEFAULT_MODEL_ID)?.creditCost ?? 4;
const availableCredits =

View File

@@ -1,7 +1,7 @@
"use client";
import { useEffect } from "react";
import { useQuery } from "convex/react";
import { useAuthQuery } from "@/hooks/use-auth-query";
import { CreditCard } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -43,9 +43,9 @@ const LOW_CREDITS_THRESHOLD = 20;
export function CreditOverview() {
const router = useRouter();
const balance = useQuery(api.credits.getBalance);
const subscription = useQuery(api.credits.getSubscription);
const usageStats = useQuery(api.credits.getUsageStats);
const balance = useAuthQuery(api.credits.getBalance);
const subscription = useAuthQuery(api.credits.getSubscription);
const usageStats = useAuthQuery(api.credits.getUsageStats);
useEffect(() => {
if (balance === undefined) return;

View File

@@ -1,6 +1,6 @@
"use client";
import { useQuery } from "convex/react";
import { useAuthQuery } from "@/hooks/use-auth-query";
import { Activity, Coins } from "lucide-react";
import { Badge } from "@/components/ui/badge";
@@ -45,7 +45,7 @@ function truncatedDescription(text: string, maxLen = 40) {
// ---------------------------------------------------------------------------
export function RecentTransactions() {
const transactions = useQuery(api.credits.getRecentTransactions, {
const transactions = useAuthQuery(api.credits.getRecentTransactions, {
limit: 10,
});

View File

@@ -1,7 +1,8 @@
"use client";
import { authClient } from "@/lib/auth-client";
import { useMutation, useQuery } from "convex/react";
import { useMutation } from "convex/react";
import { useAuthQuery } from "@/hooks/use-auth-query";
import { api } from "@/convex/_generated/api";
import { useEffect, useRef } from "react";
import { toast } from "@/lib/toast";
@@ -14,10 +15,8 @@ import { msg } from "@/lib/toast-messages";
*/
export function InitUser() {
const { data: session } = authClient.useSession();
const balance = useQuery(
api.credits.getBalance,
session?.user ? {} : "skip"
);
const balance = useAuthQuery(api.credits.getBalance);
const initBalance = useMutation(api.credits.initBalance);
const initStartedRef = useRef(false);

24
hooks/use-auth-query.ts Normal file
View File

@@ -0,0 +1,24 @@
"use client";
import { useConvexAuth, useQuery } from "convex/react";
import type { FunctionReference, FunctionArgs, FunctionReturnType } from "convex/server";
/**
* Wrapper um `useQuery` der automatisch `"skip"` nutzt wenn der
* Convex-Auth-Token noch nicht bereit ist. Verhindert "Unauthenticated"-Fehler
* bei Queries die `requireAuth` nutzen.
*
* Nutzt nur `isAuthenticated` (nicht `isLoading`) als Guard — wenn ein
* `initialToken` vom SSR vorhanden ist, springt `isAuthenticated` sofort
* auf `true` ohne Loading-Phase, sodass Queries ohne Verzögerung feuern.
*/
export function useAuthQuery<F extends FunctionReference<"query">>(
query: F,
...args: [] | [FunctionArgs<F> | "skip"]
): FunctionReturnType<F> | undefined {
const { isAuthenticated } = useConvexAuth();
const shouldSkip = !isAuthenticated || args[0] === "skip";
return useQuery(query, shouldSkip ? "skip" : (args[0] ?? ({} as FunctionArgs<F>)));
}