mirror of
https://github.com/rybbit-io/rybbit.git
synced 2025-05-11 12:25:36 +02:00
show usage in subsription
This commit is contained in:
parent
3c046f1ec5
commit
69d06c6607
11 changed files with 122 additions and 45 deletions
|
@ -1,4 +1,3 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { BACKEND_URL } from "../../lib/const";
|
||||
import { authedFetch, useGenericQuery } from "../utils";
|
||||
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
84
server/src/api/getUserSubscription.ts
Normal file
84
server/src/api/getUserSubscription.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue