diff --git a/messages/en-US.json b/messages/en-US.json index f442635eb..e6510fba4 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1545,6 +1545,7 @@ "billingLimitViolationDescription": "Your current usage exceeds the limits of this plan. After downgrading, all actions will be disabled until you reduce usage within the new limits. Please review the features below that are currently over the limits. Limits in violation:", "billingFeatureLossWarning": "Feature Availability Notice", "billingFeatureLossDescription": "By downgrading, features not available in the new plan will be automatically disabled. Some settings and configurations may be lost. Please review the pricing matrix to understand which features will no longer be available.", + "billingUsageExceedsLimit": "Current usage ({current}) exceeds limit ({limit})", "signUpTerms": { "IAgreeToThe": "I agree to the", "termsOfService": "terms of service", @@ -1959,6 +1960,7 @@ "orgAuthBackToSignIn": "Back to standard sign in", "orgAuthNoAccount": "Don't have an account?", "subscriptionRequiredToUse": "A subscription is required to use this feature.", + "mustUpgradeToUse": "You must upgrade your subscription to use this feature.", "idpDisabled": "Identity providers are disabled.", "orgAuthPageDisabled": "Organization auth page is disabled.", "domainRestartedDescription": "Domain verification restarted successfully", diff --git a/server/middlewares/verifyLimits.ts b/server/middlewares/verifyLimits.ts index 49c5f38ad..667895309 100644 --- a/server/middlewares/verifyLimits.ts +++ b/server/middlewares/verifyLimits.ts @@ -1,10 +1,6 @@ import { Request, Response, NextFunction } from "express"; -import { db, orgs } from "@server/db"; -import { userOrgs } from "@server/db"; -import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; -import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { usageService } from "@server/lib/billing/usageService"; import { build } from "@server/build"; diff --git a/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts index c431f3862..0305e7f1b 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts @@ -188,7 +188,7 @@ export async function handleSubscriptionUpdated( const orgId = customer.orgId; if (!orgId) { - logger.warn( + logger.debug( `No orgId found in subscription metadata for subscription ${subscription.id}. Skipping usage reset.` ); continue; diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index ad5c3260c..f64c95578 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -40,6 +40,11 @@ import { AlertTitle, AlertDescription } from "@app/components/ui/alert"; +import { + Tooltip, + TooltipTrigger, + TooltipContent +} from "@app/components/ui/tooltip"; import { GetOrgSubscriptionResponse, GetOrgUsageResponse @@ -527,6 +532,13 @@ export default function BillingPage() { return limit?.value ?? null; }; + // Check if usage exceeds limit for a specific feature + const isOverLimit = (featureId: string): boolean => { + const usage = getUsageValue(featureId); + const limit = getLimitValue(featureId); + return limit !== null && usage > limit; + }; + // Calculate current usage cost for display const getUserCount = () => getUsageValue(USERS); const getPricePerUser = () => { @@ -746,11 +758,33 @@ export default function BillingPage() { {t("billingUsers") || "Users"} - {getLimitValue(USERS) ?? - t("billingUnlimited") ?? - "∞"}{" "} - {getLimitValue(USERS) !== null && - "users"} + {isOverLimit(USERS) ? ( + + + + + {getLimitValue(USERS) ?? + t("billingUnlimited") ?? + "∞"}{" "} + {getLimitValue(USERS) !== null && + "users"} + + + +

{t("billingUsageExceedsLimit", { current: getUsageValue(USERS), limit: getLimitValue(USERS) ?? 0 }) || `Current usage (${getUsageValue(USERS)}) exceeds limit (${getLimitValue(USERS)})`}

+
+
+ ) : ( + <> + {getLimitValue(USERS) ?? + t("billingUnlimited") ?? + "∞"}{" "} + {getLimitValue(USERS) !== null && + "users"} + + )}
@@ -758,11 +792,33 @@ export default function BillingPage() { {t("billingSites") || "Sites"} - {getLimitValue(SITES) ?? - t("billingUnlimited") ?? - "∞"}{" "} - {getLimitValue(SITES) !== null && - "sites"} + {isOverLimit(SITES) ? ( + + + + + {getLimitValue(SITES) ?? + t("billingUnlimited") ?? + "∞"}{" "} + {getLimitValue(SITES) !== null && + "sites"} + + + +

{t("billingUsageExceedsLimit", { current: getUsageValue(SITES), limit: getLimitValue(SITES) ?? 0 }) || `Current usage (${getUsageValue(SITES)}) exceeds limit (${getLimitValue(SITES)})`}

+
+
+ ) : ( + <> + {getLimitValue(SITES) ?? + t("billingUnlimited") ?? + "∞"}{" "} + {getLimitValue(SITES) !== null && + "sites"} + + )}
@@ -770,11 +826,33 @@ export default function BillingPage() { {t("billingDomains") || "Domains"} - {getLimitValue(DOMAINS) ?? - t("billingUnlimited") ?? - "∞"}{" "} - {getLimitValue(DOMAINS) !== null && - "domains"} + {isOverLimit(DOMAINS) ? ( + + + + + {getLimitValue(DOMAINS) ?? + t("billingUnlimited") ?? + "∞"}{" "} + {getLimitValue(DOMAINS) !== null && + "domains"} + + + +

{t("billingUsageExceedsLimit", { current: getUsageValue(DOMAINS), limit: getLimitValue(DOMAINS) ?? 0 }) || `Current usage (${getUsageValue(DOMAINS)}) exceeds limit (${getLimitValue(DOMAINS)})`}

+
+
+ ) : ( + <> + {getLimitValue(DOMAINS) ?? + t("billingUnlimited") ?? + "∞"}{" "} + {getLimitValue(DOMAINS) !== null && + "domains"} + + )}
@@ -783,11 +861,33 @@ export default function BillingPage() { "Remote Nodes"} - {getLimitValue(REMOTE_EXIT_NODES) ?? - t("billingUnlimited") ?? - "∞"}{" "} - {getLimitValue(REMOTE_EXIT_NODES) !== - null && "remote nodes"} + {isOverLimit(REMOTE_EXIT_NODES) ? ( + + + + + {getLimitValue(REMOTE_EXIT_NODES) ?? + t("billingUnlimited") ?? + "∞"}{" "} + {getLimitValue(REMOTE_EXIT_NODES) !== + null && "remote nodes"} + + + +

{t("billingUsageExceedsLimit", { current: getUsageValue(REMOTE_EXIT_NODES), limit: getLimitValue(REMOTE_EXIT_NODES) ?? 0 }) || `Current usage (${getUsageValue(REMOTE_EXIT_NODES)}) exceeds limit (${getLimitValue(REMOTE_EXIT_NODES)})`}

+
+
+ ) : ( + <> + {getLimitValue(REMOTE_EXIT_NODES) ?? + t("billingUnlimited") ?? + "∞"}{" "} + {getLimitValue(REMOTE_EXIT_NODES) !== + null && "remote nodes"} + + )}
@@ -809,31 +909,34 @@ export default function BillingPage() { -
-
-
- {t("billingCurrentKeys") || "Current Keys"} -
-
- - {getLicenseKeyCount()} - - - {getLicenseKeyCount() === 1 - ? "key" - : "keys"} - +
+
+
+
+ {t("billingCurrentKeys") || "Current Keys"} +
+
+ + {getLicenseKeyCount()} + + + {getLicenseKeyCount() === 1 + ? "key" + : "keys"} + +
+
-
@@ -864,12 +967,14 @@ export default function BillingPage() { {pendingTier && pendingTier.tier && (
-
-
+
+
{pendingTier.planName}
-
- {pendingTier.price} +
+ + {pendingTier.price} +
diff --git a/src/components/PaidFeaturesAlert.tsx b/src/components/PaidFeaturesAlert.tsx index 19b35ee0c..c94043b69 100644 --- a/src/components/PaidFeaturesAlert.tsx +++ b/src/components/PaidFeaturesAlert.tsx @@ -21,7 +21,7 @@ type Props = { export function PaidFeaturesAlert({ tiers }: Props) { const t = useTranslations(); - const { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus(); + const { hasSaasSubscription, hasEnterpriseLicense, isActive } = usePaidStatus(); const { env } = useEnvContext(); if (env.flags.disableEnterpriseFeatures) { @@ -35,7 +35,7 @@ export function PaidFeaturesAlert({ tiers }: Props) {
- {t("subscriptionRequiredToUse")} + {isActive ? t("mustUpgradeToUse") : t("subscriptionRequiredToUse")}
diff --git a/src/hooks/usePaidStatus.ts b/src/hooks/usePaidStatus.ts index 5d5fec2e3..54f6257fb 100644 --- a/src/hooks/usePaidStatus.ts +++ b/src/hooks/usePaidStatus.ts @@ -33,6 +33,7 @@ export function usePaidStatus() { hasEnterpriseLicense, hasSaasSubscription, isPaidUser, + isActive: tierData?.active, subscriptionTier: tierData?.tier }; }