From 0c22041623f732d4f89c509dd807e3e884c46350 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 17 Mar 2026 23:11:50 -0600 Subject: [PATCH] refactor: update billing component to manage server quantities for hobby and startup tiers - Replaced single server quantity state with separate states for hobby and startup server quantities. - Adjusted calculations and UI elements to reflect the new state management for each tier. - Ensured proper handling of server quantity in pricing calculations and button states. --- .../settings/billing/show-billing.tsx | 63 ++++++++++--------- apps/dokploy/server/api/routers/stripe.ts | 25 +++++--- packages/server/src/lib/auth.ts | 2 +- 3 files changed, 52 insertions(+), 38 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx index 6e56bdd70..fc8333430 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx @@ -91,7 +91,8 @@ export const ShowBilling = () => { api.stripe.upgradeSubscription.useMutation(); const utils = api.useUtils(); - const [serverQuantity, setServerQuantity] = useState(3); + const [hobbyServerQuantity, setHobbyServerQuantity] = useState(1); + const [startupServerQuantity, setStartupServerQuantity] = useState(STARTUP_SERVERS_INCLUDED); const [isAnnual, setIsAnnual] = useState(false); const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>( null, @@ -111,6 +112,12 @@ export const ShowBilling = () => { productId: string, ) => { const stripe = await stripePromise; + const serverQuantity = + tier === "startup" + ? startupServerQuantity + : tier === "hobby" + ? hobbyServerQuantity + : hobbyServerQuantity; if (data && data.subscriptions.length === 0) { createCheckoutSession({ tier, @@ -679,7 +686,7 @@ export const ShowBilling = () => {

$ {calculatePriceHobby( - serverQuantity, + hobbyServerQuantity, isAnnual, ).toFixed(2)} /{isAnnual ? "yr" : "mo"} @@ -692,7 +699,7 @@ export const ShowBilling = () => {

$ {( - calculatePriceHobby(serverQuantity, true) / 12 + calculatePriceHobby(hobbyServerQuantity, true) / 12 ).toFixed(2)} /mo

@@ -724,19 +731,19 @@ export const ShowBilling = () => { Servers: - setServerQuantity( + setHobbyServerQuantity( Math.max( 1, Number( @@ -750,7 +757,7 @@ export const ShowBilling = () => { @@ -775,7 +782,7 @@ export const ShowBilling = () => { onClick={() => handleCheckout("hobby", data!.hobbyProductId!) } - disabled={serverQuantity < 1} + disabled={hobbyServerQuantity < 1} > Get Started @@ -806,7 +813,7 @@ export const ShowBilling = () => {

$ {calculatePriceStartup( - serverQuantity, + startupServerQuantity, isAnnual, ).toFixed(2)} /{isAnnual ? "yr" : "mo"} @@ -819,7 +826,7 @@ export const ShowBilling = () => {

$ {( - calculatePriceStartup(serverQuantity, true) / 12 + calculatePriceStartup(startupServerQuantity, true) / 12 ).toFixed(2)} /mo

@@ -856,13 +863,13 @@ export const ShowBilling = () => {
- setServerQuantity( + setStartupServerQuantity( Math.max( STARTUP_SERVERS_INCLUDED, Number( @@ -887,7 +894,7 @@ export const ShowBilling = () => { variant="outline" size="icon" className="h-8 w-8" - onClick={() => setServerQuantity((q) => q + 1)} + onClick={() => setStartupServerQuantity((q) => q + 1)} > @@ -917,7 +924,7 @@ export const ShowBilling = () => { ) } disabled={ - serverQuantity < STARTUP_SERVERS_INCLUDED + startupServerQuantity < STARTUP_SERVERS_INCLUDED } > Get Started @@ -1009,7 +1016,7 @@ export const ShowBilling = () => {

${" "} {calculatePrice( - serverQuantity, + hobbyServerQuantity, isAnnual, ).toFixed(2)}{" "} USD @@ -1018,7 +1025,7 @@ export const ShowBilling = () => {

${" "} {( - calculatePrice(serverQuantity, isAnnual) / 12 + calculatePrice(hobbyServerQuantity, isAnnual) / 12 ).toFixed(2)}{" "} / Month USD

@@ -1026,7 +1033,7 @@ export const ShowBilling = () => { ) : (

${" "} - {calculatePrice(serverQuantity, isAnnual).toFixed( + {calculatePrice(hobbyServerQuantity, isAnnual).toFixed( 2, )}{" "} USD @@ -1071,26 +1078,26 @@ export const ShowBilling = () => {

- {serverQuantity} Servers + {hobbyServerQuantity} Servers
{ - setServerQuantity( + setHobbyServerQuantity( e.target.value as unknown as number, ); }} @@ -1099,7 +1106,7 @@ export const ShowBilling = () => { diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts index 109f2aac3..2a6292dfc 100644 --- a/apps/dokploy/server/api/routers/stripe.ts +++ b/apps/dokploy/server/api/routers/stripe.ts @@ -21,7 +21,12 @@ import { STARTUP_PRODUCT_ID, WEBSITE_URL, } from "@/server/utils/stripe"; -import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; +import { + adminProcedure, + createTRPCRouter, + protectedProcedure, + withPermission, +} from "../trpc"; export const stripeRouter = createTRPCRouter({ /** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */ @@ -314,16 +319,18 @@ export const stripeRouter = createTRPCRouter({ return { ok: true }; }), - canCreateMoreServers: adminProcedure.query(async ({ ctx }) => { - const user = await findUserById(ctx.user.ownerId); - const servers = await findServersByUserId(user.id); + canCreateMoreServers: withPermission("server", "create").query( + async ({ ctx }) => { + const user = await findUserById(ctx.user.ownerId); + const servers = await findServersByUserId(user.id); - if (!IS_CLOUD) { - return true; - } + if (!IS_CLOUD) { + return true; + } - return servers.length < user.serversQuantity; - }), + return servers.length < user.serversQuantity; + }, + ), getInvoices: adminProcedure.query(async ({ ctx }) => { const user = await findUserById(ctx.user.ownerId); diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 0721bbf85..0b425ee89 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -116,7 +116,7 @@ const { handler, api } = betterAuth({ emailAndPassword: { enabled: true, autoSignIn: !IS_CLOUD, - requireEmailVerification: IS_CLOUD, + requireEmailVerification: IS_CLOUD && process.env.NODE_ENV === "production", password: { async hash(password) { return bcrypt.hashSync(password, 10);