show usage in subsription

This commit is contained in:
Bill Yang 2025-03-13 23:26:56 -07:00
parent 3c046f1ec5
commit 69d06c6607
11 changed files with 122 additions and 45 deletions

View file

@ -1,4 +1,3 @@
import { useQuery } from "@tanstack/react-query";
import { BACKEND_URL } from "../../lib/const";
import { authedFetch, useGenericQuery } from "../utils";

View file

@ -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<string, any>;
};
export function useSubscription() {
return useQuery({
queryKey: ["subscription"],
export type SubscriptionWithUsage = Subscription & {
monthlyEventCount: number;
overMonthlyLimit: boolean;
monthlyEventLimit: number;
};
export function useSubscriptionWithUsage() {
return useQuery<SubscriptionWithUsage>({
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();
},
});
}

View file

@ -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({
<div className="flex justify-between mb-1">
<span className="text-sm">Events</span>
<span className="text-sm">
{currentUsage.events.toLocaleString()} /{" "}
{currentUsage.toLocaleString()} /{" "}
{eventLimit.toLocaleString()}
</span>
</div>

View file

@ -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<string | null>(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 (
<div className="container py-10 max-w-5xl mx-auto">

View file

@ -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;

View file

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

View file

@ -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<number> {
export async function getUserEventLimit(userId: string): Promise<number> {
try {
// Find active subscription
const userSubscription = await db
@ -30,7 +30,7 @@ async function getUserEventLimit(userId: string): Promise<number> {
}
// 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);

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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",