diff --git a/client/src/api/admin/sites.ts b/client/src/api/admin/sites.ts index 7c59611..d8528b7 100644 --- a/client/src/api/admin/sites.ts +++ b/client/src/api/admin/sites.ts @@ -1,4 +1,3 @@ -import { useQuery } from "@tanstack/react-query"; import { BACKEND_URL } from "../../lib/const"; import { authedFetch, useGenericQuery } from "../utils"; diff --git a/client/src/api/admin/subscription.ts b/client/src/api/admin/subscription.ts index f4c642f..2d8b754 100644 --- a/client/src/api/admin/subscription.ts +++ b/client/src/api/admin/subscription.ts @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { authClient } from "../../lib/auth"; +import { BACKEND_URL } from "../../lib/const"; +import { authedFetch } from "../utils"; export type Subscription = { id: string; @@ -25,29 +26,18 @@ export type Subscription = { metadata?: Record; }; -export function useSubscription() { - return useQuery({ - queryKey: ["subscription"], +export type SubscriptionWithUsage = Subscription & { + monthlyEventCount: number; + overMonthlyLimit: boolean; + monthlyEventLimit: number; +}; + +export function useSubscriptionWithUsage() { + return useQuery({ + queryKey: ["subscriptionWithUsage"], queryFn: async () => { - try { - const { data, error } = await authClient.subscription.list(); - - if (error) { - throw new Error(error.message); - } - - // Find the active subscription - const activeSubscription = - data?.find( - (sub) => sub.status === "active" || sub.status === "trialing" - ) || null; - - // Ensure the returned data has the correct shape for our frontend - return activeSubscription as Subscription | null; - } catch (error) { - console.error("Failed to fetch subscription:", error); - throw error; - } + const res = await authedFetch(`${BACKEND_URL}/user/subscription`); + return res.json(); }, }); } diff --git a/client/src/app/settings/subscription/components/CurrentPlanCard.tsx b/client/src/app/settings/subscription/components/CurrentPlanCard.tsx index d0d7d6d..e6a2385 100644 --- a/client/src/app/settings/subscription/components/CurrentPlanCard.tsx +++ b/client/src/app/settings/subscription/components/CurrentPlanCard.tsx @@ -18,7 +18,7 @@ import { AlertCircle, ArrowRight, X } from "lucide-react"; interface CurrentPlanCardProps { activeSubscription: Subscription; currentPlan: PlanTemplate | null; - currentUsage: { events: number }; + currentUsage: number; eventLimit: number; usagePercentage: number; isProcessing: boolean; @@ -126,7 +126,7 @@ export function CurrentPlanCard({
Events - {currentUsage.events.toLocaleString()} /{" "} + {currentUsage.toLocaleString()} /{" "} {eventLimit.toLocaleString()}
diff --git a/client/src/app/settings/subscription/page.tsx b/client/src/app/settings/subscription/page.tsx index 5df7c4d..eb288ad 100644 --- a/client/src/app/settings/subscription/page.tsx +++ b/client/src/app/settings/subscription/page.tsx @@ -15,14 +15,14 @@ import { STRIPE_PRICES } from "@/lib/stripe"; import { AlertCircle, ArrowRight } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; -import { useSubscription } from "../../../api/admin/subscription"; +import { useSubscriptionWithUsage } from "../../../api/admin/subscription"; import { authClient } from "../../../lib/auth"; import { ChangePlanDialog } from "./components/ChangePlanDialog"; import { CurrentPlanCard } from "./components/CurrentPlanCard"; import { ErrorDialog } from "./components/ErrorDialog"; import { HelpSection } from "./components/HelpSection"; import { PlanFeaturesCard } from "./components/PlanFeaturesCard"; -import { DEFAULT_EVENT_LIMIT, DEFAULT_USAGE } from "./utils/constants"; +import { DEFAULT_EVENT_LIMIT } from "./utils/constants"; import { getPlanDetails } from "./utils/planUtils"; export default function SubscriptionPage() { @@ -33,7 +33,7 @@ export default function SubscriptionPage() { isLoading, error: subscriptionError, refetch, - } = useSubscription(); + } = useSubscriptionWithUsage(); // State variables const [errorType, setErrorType] = useState<"cancel" | "resume">("cancel"); @@ -43,7 +43,7 @@ export default function SubscriptionPage() { const [actionError, setActionError] = useState(null); // Current usage - in a real app, you would fetch this from your API - const currentUsage = DEFAULT_USAGE; + const currentUsage = activeSubscription?.monthlyEventCount || 0; const handleCancelSubscription = async () => { try { @@ -190,7 +190,7 @@ export default function SubscriptionPage() { }; const eventLimit = getEventLimit(); - const usagePercentage = (currentUsage.events / eventLimit) * 100; + const usagePercentage = (currentUsage / eventLimit) * 100; return (
diff --git a/client/src/app/settings/subscription/utils/constants.ts b/client/src/app/settings/subscription/utils/constants.ts index 860291a..a857818 100644 --- a/client/src/app/settings/subscription/utils/constants.ts +++ b/client/src/app/settings/subscription/utils/constants.ts @@ -1,7 +1,2 @@ -// Current usage - in a real app, you would fetch this from your API -export const DEFAULT_USAGE = { - events: 45000, // Example value -}; - // Default event limit if not specified in subscription -export const DEFAULT_EVENT_LIMIT = 100000; +export const DEFAULT_EVENT_LIMIT = 20000; diff --git a/server/src/api/getUserSubscription.ts b/server/src/api/getUserSubscription.ts new file mode 100644 index 0000000..a668222 --- /dev/null +++ b/server/src/api/getUserSubscription.ts @@ -0,0 +1,84 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { getSession } from "../lib/auth-utils.js"; +import { getUserEventLimit } from "./sites/getSites.js"; +import { db } from "../db/postgres/postgres.js"; +import { user, subscription } from "../db/postgres/schema.js"; +import { eq, and, inArray } from "drizzle-orm"; +import { STRIPE_PRICES } from "../lib/const.js"; + +// Define the plan interface +interface StripePlan { + name: string; + priceId: string; + interval: string; + limits: { + events: number; + [key: string]: any; + }; +} + +export async function getUserSubscription( + req: FastifyRequest, + reply: FastifyReply +) { + const session = await getSession(req); + if (!session) { + return reply.status(401).send({ error: "Unauthorized" }); + } + + try { + // Get user's monthly event count + const userData = await db.query.user.findFirst({ + where: eq(user.id, session.user.id), + columns: { + monthlyEventCount: true, + overMonthlyLimit: true, + }, + }); + + // Find user's active subscription + const userSubscription = await db + .select() + .from(subscription) + .where( + and( + eq(subscription.referenceId, session.user.id), + inArray(subscription.status, ["active", "trialing"]) + ) + ) + .limit(1); + + // Get plan details + let subscriptionPlanDetails = null; + const eventLimit = await getUserEventLimit(session.user.id); + + if (userSubscription.length > 0) { + const subData = userSubscription[0]; + // Find detailed plan information from STRIPE_PRICES + const planDetails = STRIPE_PRICES.find( + (plan: StripePlan) => plan.name === subData.plan + ); + + subscriptionPlanDetails = { + ...subData, + planDetails: planDetails || null, + }; + } + + // Construct the response + const response = { + ...subscriptionPlanDetails, + monthlyEventCount: userData?.monthlyEventCount || 0, + overMonthlyLimit: userData?.overMonthlyLimit || false, + monthlyEventLimit: eventLimit, + }; + + return reply.status(200).send(response); + } catch (error) { + console.error("Error fetching user subscription:", error); + return reply.status(500).send({ + error: "Failed to fetch subscription details", + message: error instanceof Error ? error.message : String(error), + }); + } +} diff --git a/server/src/api/sites/getSites.ts b/server/src/api/sites/getSites.ts index c6b4850..8447ad4 100644 --- a/server/src/api/sites/getSites.ts +++ b/server/src/api/sites/getSites.ts @@ -3,7 +3,7 @@ import { FastifyReply, FastifyRequest } from "fastify"; import { db } from "../../db/postgres/postgres.js"; import { member, user, subscription } from "../../db/postgres/schema.js"; import { getSitesUserHasAccessTo } from "../../lib/auth-utils.js"; -import { STRIPE_PLANS } from "../../lib/const.js"; +import { STRIPE_PRICES } from "../../lib/const.js"; // Default event limit for users without an active subscription const DEFAULT_EVENT_LIMIT = 20_000; @@ -11,7 +11,7 @@ const DEFAULT_EVENT_LIMIT = 20_000; /** * Get subscription event limit for a user */ -async function getUserEventLimit(userId: string): Promise { +export async function getUserEventLimit(userId: string): Promise { try { // Find active subscription const userSubscription = await db @@ -30,7 +30,7 @@ async function getUserEventLimit(userId: string): Promise { } // Find the plan in STRIPE_PLANS - const plan = STRIPE_PLANS.find((p) => p.name === userSubscription[0].plan); + const plan = STRIPE_PRICES.find((p) => p.name === userSubscription[0].plan); return plan ? plan.limits.events : DEFAULT_EVENT_LIMIT; } catch (error) { console.error(`Error getting event limit for user ${userId}:`, error); diff --git a/server/src/cron/monthly-usage-checker.ts b/server/src/cron/monthly-usage-checker.ts index abd84c1..fd80632 100644 --- a/server/src/cron/monthly-usage-checker.ts +++ b/server/src/cron/monthly-usage-checker.ts @@ -1,6 +1,6 @@ import { user, member, sites, subscription } from "../db/postgres/schema.js"; import { clickhouse } from "../db/clickhouse/clickhouse.js"; -import { STRIPE_PLANS } from "../lib/const.js"; +import { STRIPE_PRICES } from "../lib/const.js"; import { eq, inArray, and } from "drizzle-orm"; import { db } from "../db/postgres/postgres.js"; import { processResults } from "../api/utils.js"; @@ -74,7 +74,7 @@ async function getUserSubscriptionInfo( } // Find the plan in STRIPE_PLANS - const plan = STRIPE_PLANS.find((p) => p.name === userSubscription[0].plan); + const plan = STRIPE_PRICES.find((p) => p.name === userSubscription[0].plan); const eventLimit = plan ? plan.limits.events : DEFAULT_EVENT_LIMIT; // Get period start date - if not available, use first day of month diff --git a/server/src/index.ts b/server/src/index.ts index 7d86ba5..cae0b6d 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -29,6 +29,7 @@ import { trackPageView } from "./tracker/trackPageView.js"; import { listOrganizationMembers } from "./api/listOrganizationMembers.js"; import { getUserOrganizations } from "./api/getUserOrganizations.js"; import { initializeCronJobs } from "./cron/index.js"; +import { getUserSubscription } from "./api/getUserSubscription.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -139,6 +140,7 @@ server.get( listOrganizationMembers ); server.get("/user/organizations", getUserOrganizations); +server.get("/user/subscription", getUserSubscription); // Track pageview endpoint server.post("/track/pageview", trackPageView); diff --git a/server/src/lib/auth.ts b/server/src/lib/auth.ts index f42e118..122cb3a 100644 --- a/server/src/lib/auth.ts +++ b/server/src/lib/auth.ts @@ -4,7 +4,7 @@ import dotenv from "dotenv"; import pg from "pg"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "../db/postgres/postgres.js"; -import { IS_CLOUD, STRIPE_PLANS } from "./const.js"; +import { IS_CLOUD, STRIPE_PRICES } from "./const.js"; import * as schema from "../db/postgres/schema.js"; import { eq } from "drizzle-orm"; import { stripe } from "@better-auth/stripe"; @@ -31,7 +31,7 @@ const pluginList = IS_CLOUD createCustomerOnSignUp: true, subscription: { enabled: true, - plans: STRIPE_PLANS, + plans: STRIPE_PRICES, }, }), ] @@ -120,6 +120,13 @@ export function initAuth(allowedOrigins: string[]) { enabled: true, }, user: { + additionalFields: { + monthlyEventCount: { + type: "number", + defaultValue: 0, + required: false, + }, + }, deleteUser: { enabled: true, // Add a hook to run before deleting a user diff --git a/server/src/lib/const.ts b/server/src/lib/const.ts index 4d5b564..151397b 100644 --- a/server/src/lib/const.ts +++ b/server/src/lib/const.ts @@ -4,7 +4,7 @@ dotenv.config(); export const IS_CLOUD = process.env.CLOUD === "true"; -export const STRIPE_PLANS = [ +export const STRIPE_PRICES = [ { priceId: "price_1R1fIVDFVprnAny2yJtRRPBm", name: "basic100k",