Continue to clean things up

This commit is contained in:
Owen
2026-02-10 18:30:01 -08:00
parent e2ac6e6d4d
commit 9ff863db5e
6 changed files with 158 additions and 54 deletions

View File

@@ -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:", "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", "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.", "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": { "signUpTerms": {
"IAgreeToThe": "I agree to the", "IAgreeToThe": "I agree to the",
"termsOfService": "terms of service", "termsOfService": "terms of service",
@@ -1959,6 +1960,7 @@
"orgAuthBackToSignIn": "Back to standard sign in", "orgAuthBackToSignIn": "Back to standard sign in",
"orgAuthNoAccount": "Don't have an account?", "orgAuthNoAccount": "Don't have an account?",
"subscriptionRequiredToUse": "A subscription is required to use this feature.", "subscriptionRequiredToUse": "A subscription is required to use this feature.",
"mustUpgradeToUse": "You must upgrade your subscription to use this feature.",
"idpDisabled": "Identity providers are disabled.", "idpDisabled": "Identity providers are disabled.",
"orgAuthPageDisabled": "Organization auth page is disabled.", "orgAuthPageDisabled": "Organization auth page is disabled.",
"domainRestartedDescription": "Domain verification restarted successfully", "domainRestartedDescription": "Domain verification restarted successfully",

View File

@@ -1,10 +1,6 @@
import { Request, Response, NextFunction } from "express"; 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 createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { usageService } from "@server/lib/billing/usageService"; import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build"; import { build } from "@server/build";

View File

@@ -188,7 +188,7 @@ export async function handleSubscriptionUpdated(
const orgId = customer.orgId; const orgId = customer.orgId;
if (!orgId) { if (!orgId) {
logger.warn( logger.debug(
`No orgId found in subscription metadata for subscription ${subscription.id}. Skipping usage reset.` `No orgId found in subscription metadata for subscription ${subscription.id}. Skipping usage reset.`
); );
continue; continue;

View File

@@ -40,6 +40,11 @@ import {
AlertTitle, AlertTitle,
AlertDescription AlertDescription
} from "@app/components/ui/alert"; } from "@app/components/ui/alert";
import {
Tooltip,
TooltipTrigger,
TooltipContent
} from "@app/components/ui/tooltip";
import { import {
GetOrgSubscriptionResponse, GetOrgSubscriptionResponse,
GetOrgUsageResponse GetOrgUsageResponse
@@ -527,6 +532,13 @@ export default function BillingPage() {
return limit?.value ?? null; 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 // Calculate current usage cost for display
const getUserCount = () => getUsageValue(USERS); const getUserCount = () => getUsageValue(USERS);
const getPricePerUser = () => { const getPricePerUser = () => {
@@ -746,11 +758,33 @@ export default function BillingPage() {
{t("billingUsers") || "Users"} {t("billingUsers") || "Users"}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent className="text-sm"> <InfoSectionContent className="text-sm">
{getLimitValue(USERS) ?? {isOverLimit(USERS) ? (
t("billingUnlimited") ?? <Tooltip>
"∞"}{" "} <TooltipTrigger className="flex items-center gap-1">
{getLimitValue(USERS) !== null && <AlertTriangle className="h-3 w-3 text-orange-400" />
"users"} <span className={cn(
"text-orange-600 dark:text-orange-400 font-medium"
)}>
{getLimitValue(USERS) ??
t("billingUnlimited") ??
"∞"}{" "}
{getLimitValue(USERS) !== null &&
"users"}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{t("billingUsageExceedsLimit", { current: getUsageValue(USERS), limit: getLimitValue(USERS) ?? 0 }) || `Current usage (${getUsageValue(USERS)}) exceeds limit (${getLimitValue(USERS)})`}</p>
</TooltipContent>
</Tooltip>
) : (
<>
{getLimitValue(USERS) ??
t("billingUnlimited") ??
"∞"}{" "}
{getLimitValue(USERS) !== null &&
"users"}
</>
)}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
@@ -758,11 +792,33 @@ export default function BillingPage() {
{t("billingSites") || "Sites"} {t("billingSites") || "Sites"}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent className="text-sm"> <InfoSectionContent className="text-sm">
{getLimitValue(SITES) ?? {isOverLimit(SITES) ? (
t("billingUnlimited") ?? <Tooltip>
"∞"}{" "} <TooltipTrigger className="flex items-center gap-1">
{getLimitValue(SITES) !== null && <AlertTriangle className="h-3 w-3 text-orange-400" />
"sites"} <span className={cn(
"text-orange-600 dark:text-orange-400 font-medium"
)}>
{getLimitValue(SITES) ??
t("billingUnlimited") ??
"∞"}{" "}
{getLimitValue(SITES) !== null &&
"sites"}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{t("billingUsageExceedsLimit", { current: getUsageValue(SITES), limit: getLimitValue(SITES) ?? 0 }) || `Current usage (${getUsageValue(SITES)}) exceeds limit (${getLimitValue(SITES)})`}</p>
</TooltipContent>
</Tooltip>
) : (
<>
{getLimitValue(SITES) ??
t("billingUnlimited") ??
"∞"}{" "}
{getLimitValue(SITES) !== null &&
"sites"}
</>
)}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
@@ -770,11 +826,33 @@ export default function BillingPage() {
{t("billingDomains") || "Domains"} {t("billingDomains") || "Domains"}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent className="text-sm"> <InfoSectionContent className="text-sm">
{getLimitValue(DOMAINS) ?? {isOverLimit(DOMAINS) ? (
t("billingUnlimited") ?? <Tooltip>
"∞"}{" "} <TooltipTrigger className="flex items-center gap-1">
{getLimitValue(DOMAINS) !== null && <AlertTriangle className="h-3 w-3 text-orange-400" />
"domains"} <span className={cn(
"text-orange-600 dark:text-orange-400 font-medium"
)}>
{getLimitValue(DOMAINS) ??
t("billingUnlimited") ??
"∞"}{" "}
{getLimitValue(DOMAINS) !== null &&
"domains"}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{t("billingUsageExceedsLimit", { current: getUsageValue(DOMAINS), limit: getLimitValue(DOMAINS) ?? 0 }) || `Current usage (${getUsageValue(DOMAINS)}) exceeds limit (${getLimitValue(DOMAINS)})`}</p>
</TooltipContent>
</Tooltip>
) : (
<>
{getLimitValue(DOMAINS) ??
t("billingUnlimited") ??
"∞"}{" "}
{getLimitValue(DOMAINS) !== null &&
"domains"}
</>
)}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
@@ -783,11 +861,33 @@ export default function BillingPage() {
"Remote Nodes"} "Remote Nodes"}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent className="text-sm"> <InfoSectionContent className="text-sm">
{getLimitValue(REMOTE_EXIT_NODES) ?? {isOverLimit(REMOTE_EXIT_NODES) ? (
t("billingUnlimited") ?? <Tooltip>
"∞"}{" "} <TooltipTrigger className="flex items-center gap-1">
{getLimitValue(REMOTE_EXIT_NODES) !== <AlertTriangle className="h-3 w-3 text-orange-400" />
null && "remote nodes"} <span className={cn(
"text-orange-600 dark:text-orange-400 font-medium"
)}>
{getLimitValue(REMOTE_EXIT_NODES) ??
t("billingUnlimited") ??
"∞"}{" "}
{getLimitValue(REMOTE_EXIT_NODES) !==
null && "remote nodes"}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{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)})`}</p>
</TooltipContent>
</Tooltip>
) : (
<>
{getLimitValue(REMOTE_EXIT_NODES) ??
t("billingUnlimited") ??
"∞"}{" "}
{getLimitValue(REMOTE_EXIT_NODES) !==
null && "remote nodes"}
</>
)}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
</InfoSections> </InfoSections>
@@ -809,31 +909,34 @@ export default function BillingPage() {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border rounded-lg p-4 bg-muted/30"> <div className="w-full md:w-1/2">
<div> <div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border rounded-lg p-4">
<div className="text-sm text-muted-foreground mb-1"> <div>
{t("billingCurrentKeys") || "Current Keys"} <div className="text-sm text-muted-foreground mb-1">
</div> {t("billingCurrentKeys") || "Current Keys"}
<div className="flex items-baseline gap-2"> </div>
<span className="text-3xl font-bold"> <div className="flex items-baseline gap-2">
{getLicenseKeyCount()} <span className="text-3xl font-bold">
</span> {getLicenseKeyCount()}
<span className="text-lg"> </span>
{getLicenseKeyCount() === 1 <span className="text-lg">
? "key" {getLicenseKeyCount() === 1
: "keys"} ? "key"
</span> : "keys"}
</span>
</div>
</div> </div>
<Button
variant="outline"
onClick={handleModifySubscription}
disabled={isLoading}
loading={isLoading}
>
<CreditCard className="mr-2 h-4 w-4" />
{t("billingModifyCurrentPlan") ||
"Modify Current Plan"}
</Button>
</div> </div>
<Button
variant="outline"
onClick={handleModifySubscription}
disabled={isLoading}
>
<CreditCard className="mr-2 h-4 w-4" />
{t("billingModifyCurrentPlan") ||
"Modify Current Plan"}
</Button>
</div> </div>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
@@ -864,12 +967,14 @@ export default function BillingPage() {
<CredenzaBody> <CredenzaBody>
{pendingTier && pendingTier.tier && ( {pendingTier && pendingTier.tier && (
<div className="space-y-4"> <div className="space-y-4">
<div className="border rounded-lg p-4 bg-muted/30"> <div className="border rounded-lg p-4">
<div className="font-semibold text-lg mb-2"> <div className="text-2xl">
{pendingTier.planName} {pendingTier.planName}
</div> </div>
<div className="text-2xl font-bold"> <div className="mt-1">
{pendingTier.price} <span className="text-xl">
{pendingTier.price}
</span>
</div> </div>
</div> </div>

View File

@@ -21,7 +21,7 @@ type Props = {
export function PaidFeaturesAlert({ tiers }: Props) { export function PaidFeaturesAlert({ tiers }: Props) {
const t = useTranslations(); const t = useTranslations();
const { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus(); const { hasSaasSubscription, hasEnterpriseLicense, isActive } = usePaidStatus();
const { env } = useEnvContext(); const { env } = useEnvContext();
if (env.flags.disableEnterpriseFeatures) { if (env.flags.disableEnterpriseFeatures) {
@@ -35,7 +35,7 @@ export function PaidFeaturesAlert({ tiers }: Props) {
<CardContent className={bannerContentClassName}> <CardContent className={bannerContentClassName}>
<div className={bannerRowClassName}> <div className={bannerRowClassName}>
<KeyRound className="size-4 shrink-0 text-primary" /> <KeyRound className="size-4 shrink-0 text-primary" />
<span>{t("subscriptionRequiredToUse")}</span> <span>{isActive ? t("mustUpgradeToUse") : t("subscriptionRequiredToUse")}</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -33,6 +33,7 @@ export function usePaidStatus() {
hasEnterpriseLicense, hasEnterpriseLicense,
hasSaasSubscription, hasSaasSubscription,
isPaidUser, isPaidUser,
isActive: tierData?.active,
subscriptionTier: tierData?.tier subscriptionTier: tierData?.tier
}; };
} }