"use client"; import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; import { useState, useEffect } from "react"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { formatAxiosError } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { SettingsContainer, SettingsFormCell, SettingsFormGrid, SettingsSection, SettingsSectionHeader, SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, SettingsSectionFooter, SettingsSectionForm } from "@app/components/Settings"; import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { cn } from "@app/lib/cn"; import { CreditCard, ExternalLink, Check, AlertTriangle } from "lucide-react"; import { Badge } from "@app/components/ui/badge"; import { Alert, AlertTitle, AlertDescription } from "@app/components/ui/alert"; import { Tooltip, TooltipTrigger, TooltipContent } from "@app/components/ui/tooltip"; import { GetOrgSubscriptionResponse, GetOrgUsageResponse } from "@server/routers/billing/types"; import { useTranslations } from "use-intl"; import Link from "next/link"; import { Tier } from "@server/types/Tiers"; import { freeLimitSet, tier1LimitSet, tier2LimitSet, tier3LimitSet } from "@server/lib/billing/limitSet"; import { LimitId } from "@server/lib/billing/features"; import TrialBillingBanner from "@app/components/TrialBillingBanner"; // Plan tier definitions matching the mockup type PlanId = "basic" | "home" | "team" | "business" | "enterprise"; type PlanOption = { id: PlanId; name: string; price: string; priceDetail?: string; tierType: Tier | null; features: string[]; }; const planOptions: PlanOption[] = [ { id: "basic", name: "Basic", price: "Free", tierType: null, features: [ "Basic Pangolin features", "Free provided domains", "Web-based proxy resources", "Private resources and clients", "Peer-to-peer connections" ] }, { id: "home", name: "Home", price: "$12.50", priceDetail: "/ month", tierType: "tier1", features: [ "Everything in Basic", "OAuth2/OIDC, Google, & Azure SSO", "Bring your own identity provider", "Pangolin SSH", "Custom branding", "Device admin approvals" ] }, { id: "team", name: "Team", price: "$4", priceDetail: "per user / month", tierType: "tier2", features: [ "Everything in Basic", "Custom domains", "OAuth2/OIDC, Google, & Azure SSO", "Access and action audit logs", "Device posture information" ] }, { id: "business", name: "Business", price: "$9", priceDetail: "per user / month", tierType: "tier3", features: [ "Everything in Team", "Multiple organizations (multi-tenancy)", "Auto-provisioning via IdP", "Pangolin SSH", "Device approvals", "Custom branding", "Business support" ] }, { id: "enterprise", name: "Enterprise", price: "Custom", tierType: null, features: [ "Everything in Business", "Custom limits", "Priority support and SLA", "Log push and export", "Private and Gov-Cloud deployment options", "Dedicated, premium relay/exit nodes", "Pay by invoice " ] } ]; // Tier limits mapping derived from limit sets const tierLimits: Record< Tier | "basic", { users: number; sites: number; domains: number; remoteNodes: number; organizations: number; publicResources: number; privateResources: number; machineClients: number; } > = { basic: { users: freeLimitSet[LimitId.USERS]?.value ?? 0, sites: freeLimitSet[LimitId.SITES]?.value ?? 0, domains: freeLimitSet[LimitId.DOMAINS]?.value ?? 0, remoteNodes: freeLimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0, organizations: freeLimitSet[LimitId.ORGANIZATIONS]?.value ?? 0, publicResources: freeLimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0, privateResources: freeLimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0, machineClients: freeLimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0 }, tier1: { users: tier1LimitSet[LimitId.USERS]?.value ?? 0, sites: tier1LimitSet[LimitId.SITES]?.value ?? 0, domains: tier1LimitSet[LimitId.DOMAINS]?.value ?? 0, remoteNodes: tier1LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0, organizations: tier1LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0, publicResources: tier1LimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0, privateResources: tier1LimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0, machineClients: tier1LimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0 }, tier2: { users: tier2LimitSet[LimitId.USERS]?.value ?? 0, sites: tier2LimitSet[LimitId.SITES]?.value ?? 0, domains: tier2LimitSet[LimitId.DOMAINS]?.value ?? 0, remoteNodes: tier2LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0, organizations: tier2LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0, publicResources: tier2LimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0, privateResources: tier2LimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0, machineClients: tier2LimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0 }, tier3: { users: tier3LimitSet[LimitId.USERS]?.value ?? 0, sites: tier3LimitSet[LimitId.SITES]?.value ?? 0, domains: tier3LimitSet[LimitId.DOMAINS]?.value ?? 0, remoteNodes: tier3LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0, organizations: tier3LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0, publicResources: tier3LimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0, privateResources: tier3LimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0, machineClients: tier3LimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0 }, enterprise: { users: 0, // Custom for enterprise sites: 0, // Custom for enterprise domains: 0, // Custom for enterprise remoteNodes: 0, // Custom for enterprise organizations: 0, // Custom for enterprise publicResources: 0, // Custom for enterprise privateResources: 0, // Custom for enterprise machineClients: 0 // Custom for enterprise } }; export default function BillingPage() { const { org } = useOrgContext(); const envContext = useEnvContext(); const api = createApiClient(envContext); const t = useTranslations(); // Subscription state const [allSubscriptions, setAllSubscriptions] = useState< GetOrgSubscriptionResponse["subscriptions"] >([]); const [tierSubscription, setTierSubscription] = useState< GetOrgSubscriptionResponse["subscriptions"][0] | null >(null); const [licenseSubscription, setLicenseSubscription] = useState< GetOrgSubscriptionResponse["subscriptions"][0] | null >(null); const [subscriptionLoading, setSubscriptionLoading] = useState(true); // Usage and limits data const [usageData, setUsageData] = useState( [] ); const [limitsData, setLimitsData] = useState( [] ); const [hasSubscription, setHasSubscription] = useState(false); const [isTrial, setIsTrial] = useState(false); const [isLoading, setIsLoading] = useState(false); const [currentTier, setCurrentTier] = useState(null); // Usage IDs const USERS = "users"; const SITES = "sites"; const DOMAINS = "domains"; const REMOTE_EXIT_NODES = "remoteExitNodes"; const ORGINIZATIONS = "organizations"; const PUBLIC_RESOURCES = "publicResources"; const PRIVATE_RESOURCES = "privateResources"; const MACHINE_CLIENTS = "machineClients"; // Confirmation dialog state const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [pendingTier, setPendingTier] = useState<{ tier: Tier | "basic"; action: "upgrade" | "downgrade"; planName: string; price: string; } | null>(null); useEffect(() => { async function fetchSubscription() { setSubscriptionLoading(true); try { const res = await api.get< AxiosResponse >(`/org/${org.org.orgId}/billing/subscriptions`); const { subscriptions } = res.data.data; setAllSubscriptions(subscriptions); // Find tier subscription const tierSub = subscriptions.find( ({ subscription }) => subscription?.type === "tier1" || subscription?.type === "tier2" || subscription?.type === "tier3" || subscription?.type === "enterprise" ); setTierSubscription(tierSub || null); if (tierSub?.subscription) { setCurrentTier(tierSub.subscription.type as Tier); setHasSubscription( tierSub.subscription.status === "active" ); setIsTrial(tierSub.subscription.expiresAt != null); } // Find license subscription const licenseSub = subscriptions.find( ({ subscription }) => subscription?.type === "license" ); setLicenseSubscription(licenseSub || null); } catch (error) { toast({ title: t("billingFailedToLoadSubscription"), description: formatAxiosError(error), variant: "destructive" }); } finally { setSubscriptionLoading(false); } } fetchSubscription(); }, [org.org.orgId]); useEffect(() => { async function fetchUsage() { try { const res = await api.get>( `/org/${org.org.orgId}/billing/usage` ); const { usage, limits } = res.data.data; setUsageData(usage); setLimitsData(limits); } catch (error) { toast({ title: t("billingFailedToLoadUsage"), description: formatAxiosError(error), variant: "destructive" }); } } fetchUsage(); }, [org.org.orgId]); const handleStartSubscription = async (tier: Tier) => { setIsLoading(true); try { const response = await api.post>( `/org/${org.org.orgId}/billing/create-checkout-session`, { tier } ); const checkoutUrl = response.data.data; if (checkoutUrl) { window.location.href = checkoutUrl; } else { toast({ title: t("billingFailedToGetCheckoutUrl"), description: t("billingPleaseTryAgainLater"), variant: "destructive" }); setIsLoading(false); } } catch (error) { toast({ title: t("billingCheckoutError"), description: formatAxiosError(error), variant: "destructive" }); setIsLoading(false); } }; const handleModifySubscription = async () => { setIsLoading(true); try { const response = await api.post>( `/org/${org.org.orgId}/billing/create-portal-session`, {} ); const portalUrl = response.data.data; if (portalUrl) { window.location.href = portalUrl; } else { toast({ title: t("billingFailedToGetPortalUrl"), description: t("billingPleaseTryAgainLater"), variant: "destructive" }); setIsLoading(false); } } catch (error) { toast({ title: t("billingPortalError"), description: formatAxiosError(error), variant: "destructive" }); setIsLoading(false); } }; const handleChangeTier = async (tier: Tier) => { if (!hasSubscription) { // If no subscription, start a new one handleStartSubscription(tier); return; } setIsLoading(true); try { await api.post(`/org/${org.org.orgId}/billing/change-tier`, { tier }); // Poll the API to check if the tier change has been reflected const pollForTierChange = async (targetTier: Tier) => { const maxAttempts = 30; // 30 seconds with 1 second interval let attempts = 0; const poll = async (): Promise => { try { const res = await api.get< AxiosResponse >(`/org/${org.org.orgId}/billing/subscriptions`); const { subscriptions } = res.data.data; // Find tier subscription const tierSub = subscriptions.find( ({ subscription }) => subscription?.type === "tier1" || subscription?.type === "tier2" || subscription?.type === "tier3" ); // Check if the tier has changed to the target tier if (tierSub?.subscription?.type === targetTier) { return true; } return false; } catch (error) { console.error("Error polling subscription:", error); return false; } }; while (attempts < maxAttempts) { const success = await poll(); if (success) { // Tier change reflected, refresh the page window.location.reload(); return; } attempts++; if (attempts < maxAttempts) { // Wait 1 second before next poll await new Promise((resolve) => setTimeout(resolve, 1000) ); } } // If we've exhausted all attempts, show an error toast({ title: "Tier change processing", description: "Your tier change is taking longer than expected. Please refresh the page in a moment to see the changes.", variant: "destructive" }); setIsLoading(false); }; // Start polling for the tier change pollForTierChange(tier); } catch (error) { toast({ title: "Failed to change tier", description: formatAxiosError(error), variant: "destructive" }); setIsLoading(false); } }; const confirmTierChange = () => { if (!pendingTier) return; if ( pendingTier.action === "upgrade" || pendingTier.action === "downgrade" ) { // If downgrading to basic (free tier), go to Stripe portal if (pendingTier.tier === "basic") { handleModifySubscription(); } else if (hasSubscription) { handleChangeTier(pendingTier.tier); } else { handleStartSubscription(pendingTier.tier); } } // setShowConfirmDialog(false); // setPendingTier(null); }; const showTierConfirmation = ( tier: Tier | "basic", action: "upgrade" | "downgrade", planName: string, price: string ) => { setPendingTier({ tier, action, planName, price }); setShowConfirmDialog(true); }; const handleContactUs = () => { window.open("https://pangolin.net/contact", "_blank"); }; // Get current plan ID from tier const getCurrentPlanId = (): PlanId => { if (!hasSubscription || !currentTier) return "basic"; // Handle enterprise subscription type directly if (currentTier === "enterprise") return "enterprise"; const plan = planOptions.find((p) => p.tierType === currentTier); return plan?.id || "basic"; }; const currentPlanId = getCurrentPlanId(); const visiblePlanOptions = planOptions.filter( (plan) => plan.id !== "home" || currentPlanId === "home" ); // Check if subscription is in a problematic state that requires attention const hasProblematicSubscription = (): boolean => { if (!tierSubscription?.subscription) return false; const status = tierSubscription.subscription.status; return ( status === "past_due" || status === "unpaid" || status === "incomplete" || status === "incomplete_expired" ); }; const isProblematicState = hasProblematicSubscription(); // Get user-friendly subscription status message const getSubscriptionStatusMessage = (): { title: string; description: string; } | null => { if (!tierSubscription?.subscription || !isProblematicState) return null; const status = tierSubscription.subscription.status; switch (status) { case "past_due": return { title: t("billingPastDueTitle") || "Payment Past Due", description: t("billingPastDueDescription") || "Your payment is past due. Please update your payment method to continue using your current plan features. If not resolved, your subscription will be canceled and you'll be reverted to the free tier." }; case "unpaid": return { title: t("billingUnpaidTitle") || "Subscription Unpaid", description: t("billingUnpaidDescription") || "Your subscription is unpaid and you have been reverted to the free tier. Please update your payment method to restore your subscription." }; case "incomplete": return { title: t("billingIncompleteTitle") || "Payment Incomplete", description: t("billingIncompleteDescription") || "Your payment is incomplete. Please complete the payment process to activate your subscription." }; case "incomplete_expired": return { title: t("billingIncompleteExpiredTitle") || "Payment Expired", description: t("billingIncompleteExpiredDescription") || "Your payment was never completed and has expired. You have been reverted to the free tier. Please subscribe again to restore access to paid features." }; default: return null; } }; const statusMessage = getSubscriptionStatusMessage(); // Get button label and action for each plan const getPlanAction = (plan: PlanOption) => { if (plan.id === "enterprise") { if (plan.id === currentPlanId && !isTrial) { return { label: "Manage Current Plan", action: handleModifySubscription, variant: "default" as const, disabled: false }; } return { label: "Contact Us", action: handleContactUs, variant: "outline" as const, disabled: false }; } if (plan.id === currentPlanId) { // If it's the basic plan (basic with no subscription), show as current but disabled if ( plan.id === "basic" && !hasSubscription && !isProblematicState ) { return { label: "Current Plan", action: () => {}, variant: "default" as const, disabled: true }; } // If on free tier but has a problematic subscription, allow them to manage it if (plan.id === "basic" && isProblematicState) { return { label: "Manage Subscription", action: handleModifySubscription, variant: "default" as const, disabled: false }; } // If this is a trial subscription, show an upgrade button that starts a real checkout if (isTrial) { return { label: "Upgrade", action: () => { if (plan.tierType) { handleStartSubscription(plan.tierType); } }, variant: "default" as const, disabled: isProblematicState }; } return { label: "Manage Current Plan", action: handleModifySubscription, variant: "default" as const, disabled: false }; } const currentIndex = planOptions.findIndex( (p) => p.id === currentPlanId ); const planIndex = planOptions.findIndex((p) => p.id === plan.id); // During a trial, never show a downgrade option — all non-current plans are upgrades if (!isTrial && planIndex < currentIndex) { return { label: "Downgrade", action: () => { if (plan.tierType) { showTierConfirmation( plan.tierType, "downgrade", plan.name, plan.price + (" " + plan.priceDetail || "") ); } else if (plan.id === "basic") { // Show confirmation for downgrading to basic (free tier) showTierConfirmation( "basic", "downgrade", plan.name, plan.price ); } else { handleModifySubscription(); } }, variant: "outline" as const, disabled: isProblematicState }; } return { label: "Upgrade", action: () => { if (plan.tierType) { // During a trial, go straight to checkout instead of the tier-change flow if (isTrial) { handleStartSubscription(plan.tierType); } else { showTierConfirmation( plan.tierType, "upgrade", plan.name, plan.price + (" " + plan.priceDetail || "") ); } } else { handleModifySubscription(); } }, variant: "outline" as const, disabled: isProblematicState || (isTrial && plan.id == "basic") }; }; // Get usage value by feature ID const getUsageValue = (featureId: string): number => { const usage = usageData.find((u) => u.featureId === featureId); return usage?.instantaneousValue || usage?.latestValue || 0; }; // Get limit value by feature ID const getLimitValue = (featureId: string): number | null => { const limit = limitsData.find((l) => l.featureId === featureId); 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 = () => { if (!tierSubscription?.items) return 0; // Find the subscription item for USERS feature const usersItem = tierSubscription.items.find( (item) => item.featureId === USERS ); console.log("Users subscription item:", usersItem); // unitAmount is in cents, convert to dollars if (usersItem?.unitAmount) { return usersItem.unitAmount / 100; } return 0; }; // Get license key count const getLicenseKeyCount = (): number => { if (!licenseSubscription?.items) return 0; return licenseSubscription.items.length; }; // Check if downgrading to a tier would violate current usage limits const checkLimitViolations = ( targetTier: Tier | "basic" ): Array<{ feature: string; currentUsage: number; newLimit: number; }> => { const violations: Array<{ feature: string; currentUsage: number; newLimit: number; }> = []; const limits = tierLimits[targetTier]; // Check users const usersUsage = getUsageValue(USERS); if (limits.users > 0 && usersUsage > limits.users) { violations.push({ feature: "Users", currentUsage: usersUsage, newLimit: limits.users }); } // Check sites const sitesUsage = getUsageValue(SITES); if (limits.sites > 0 && sitesUsage > limits.sites) { violations.push({ feature: "Sites", currentUsage: sitesUsage, newLimit: limits.sites }); } // Check domains const domainsUsage = getUsageValue(DOMAINS); if (limits.domains > 0 && domainsUsage > limits.domains) { violations.push({ feature: "Domains", currentUsage: domainsUsage, newLimit: limits.domains }); } // Check remote nodes const remoteNodesUsage = getUsageValue(REMOTE_EXIT_NODES); if (limits.remoteNodes > 0 && remoteNodesUsage > limits.remoteNodes) { violations.push({ feature: "Remote Exit Nodes", currentUsage: remoteNodesUsage, newLimit: limits.remoteNodes }); } // Check organizations const organizationsUsage = getUsageValue(ORGINIZATIONS); if ( limits.organizations > 0 && organizationsUsage > limits.organizations ) { violations.push({ feature: "Organizations", currentUsage: organizationsUsage, newLimit: limits.organizations }); } // Check public resources const publicResourcesUsage = getUsageValue(PUBLIC_RESOURCES); if ( limits.publicResources > 0 && publicResourcesUsage > limits.publicResources ) { violations.push({ feature: "Public Resources", currentUsage: publicResourcesUsage, newLimit: limits.publicResources }); } // Check private resources const privateResourcesUsage = getUsageValue(PRIVATE_RESOURCES); if ( limits.privateResources > 0 && privateResourcesUsage > limits.privateResources ) { violations.push({ feature: "Private Resources", currentUsage: privateResourcesUsage, newLimit: limits.privateResources }); } // Check machine clients const machineClientsUsage = getUsageValue(MACHINE_CLIENTS); if ( limits.machineClients > 0 && machineClientsUsage > limits.machineClients ) { violations.push({ feature: "Machine Clients", currentUsage: machineClientsUsage, newLimit: limits.machineClients }); } return violations; }; if (subscriptionLoading) { return (
{t("billingLoadingSubscription")}
); } return ( {/* Trial Banner */} {isTrial && ( { const currentPlan = planOptions.find( (p) => p.id === currentPlanId ); if (currentPlan?.tierType) { handleStartSubscription(currentPlan.tierType); } }} /> )} {/* Subscription Status Alert */} {isProblematicState && statusMessage && ( {statusMessage.title} {statusMessage.description}{" "} )} {/* Your Plan Section */} {t("billingYourPlan") || "Your Plan"} {t("billingViewOrModifyPlan") || "View or modify your current plan"} {/* Plan Cards Grid */}
{visiblePlanOptions.map((plan) => { const isCurrentPlan = plan.id === currentPlanId; const planAction = getPlanAction(plan); return (
{plan.name} {isCurrentPlan && isTrial && ( {t("billingTrialBadge") || "Free Trial"} )}
{plan.price} {plan.priceDetail && ( {plan.priceDetail} )}
{isProblematicState && planAction.disabled && !isCurrentPlan && plan.id !== "enterprise" ? (

{t( "billingResolvePaymentIssue" ) || "Please resolve your payment issue before upgrading or downgrading"}

) : ( )}
); })}
{/* Usage and Limits Section */} {t("billingUsageAndLimits") || "Usage and Limits"} {t("billingViewUsageAndLimits") || "View your plan's limits and current usage"}
{/* Current Usage */}
{t("billingCurrentUsage") || "Current Usage"}
{getUserCount()} {t("billingUsers") || "Users"} {hasSubscription && getPricePerUser() > 0 && (
x ${getPricePerUser()} / month = $ {getUserCount() * getPricePerUser()} / month
)}
{/* Maximum Limits */}
{t("billingMaximumLimits") || "Maximum Limits"}
{t("billingUsers") || "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"} )}
{t("billingSites") || "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"} )}
{t("billingDomains") || "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"} )}
{t("billingOrganizations") || "Organizations"} {isOverLimit(ORGINIZATIONS) ? ( {getLimitValue( ORGINIZATIONS ) ?? t( "billingUnlimited" ) ?? "∞"}{" "} {getLimitValue( ORGINIZATIONS ) !== null && "orgs"}

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

) : ( <> {getLimitValue(ORGINIZATIONS) ?? t("billingUnlimited") ?? "∞"}{" "} {getLimitValue( ORGINIZATIONS ) !== null && "orgs"} )}
{t("billingRemoteNodes") || "Remote Nodes"} {isOverLimit(REMOTE_EXIT_NODES) ? ( {getLimitValue( REMOTE_EXIT_NODES ) ?? t( "billingUnlimited" ) ?? "∞"}{" "} {getLimitValue( REMOTE_EXIT_NODES ) !== null && "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 && "nodes"} )}
{t("billingPublicResources") || "Public Resources"} {isOverLimit(PUBLIC_RESOURCES) ? ( {getLimitValue( PUBLIC_RESOURCES ) ?? t( "billingUnlimited" ) ?? "∞"}

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

) : ( <> {getLimitValue( PUBLIC_RESOURCES ) ?? t("billingUnlimited") ?? "∞"} )}
{t("billingPrivateResources") || "Private Resources"} {isOverLimit(PRIVATE_RESOURCES) ? ( {getLimitValue( PRIVATE_RESOURCES ) ?? t( "billingUnlimited" ) ?? "∞"}

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

) : ( <> {getLimitValue( PRIVATE_RESOURCES ) ?? t("billingUnlimited") ?? "∞"} )}
{t("billingMachineClients") || "Machine Clients"} {isOverLimit(MACHINE_CLIENTS) ? ( {getLimitValue( MACHINE_CLIENTS ) ?? t( "billingUnlimited" ) ?? "∞"}

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

) : ( <> {getLimitValue( MACHINE_CLIENTS ) ?? t("billingUnlimited") ?? "∞"} )}
{/* Paid License Keys Section */} {(licenseSubscription || getLicenseKeyCount() > 0) && ( {t("billingPaidLicenseKeys") || "Paid License Keys"} {t("billingManageLicenseSubscription") || "Manage your subscription for paid self-hosted license keys"}
{t("billingCurrentKeys") || "Current Keys"}
{getLicenseKeyCount()} {getLicenseKeyCount() === 1 ? "key" : "keys"}

{t( "billingManageLicenseSubscriptionDescription" ) || "Manage your subscription for paid self-hosted license keys and download invoices."}

)} {/* Tier Change Confirmation Dialog */} {pendingTier?.action === "upgrade" ? t("billingConfirmUpgrade") || "Confirm Upgrade" : t("billingConfirmDowngrade") || "Confirm Downgrade"} {pendingTier?.action === "upgrade" ? t("billingConfirmUpgradeDescription") || `You are about to upgrade to the ${pendingTier?.planName} plan.` : t("billingConfirmDowngradeDescription") || `You are about to downgrade to the ${pendingTier?.planName} plan.`} {pendingTier && pendingTier.tier && (
{pendingTier.planName}
{pendingTier.price}
{/* Features with check marks */} {(() => { const plan = planOptions.find( (p) => p.tierType === pendingTier.tier || (pendingTier.tier === "basic" && p.id === "basic") ); return plan?.features?.length ? (

{"What's included:"}

{plan.features.map( (feature, i) => (
{feature}
) )}
) : null; })()} {/* Limits without check marks */} {tierLimits[pendingTier.tier] && (

{"Up to:"}

{ tierLimits[ pendingTier.tier ].users }{" "} {t("billingUsers") || "Users"}
{ tierLimits[ pendingTier.tier ].sites }{" "} {t("billingSites") || "Sites"}
{ tierLimits[ pendingTier.tier ].domains }{" "} {t("billingDomains") || "Domains"}
{ tierLimits[ pendingTier.tier ].organizations }{" "} {t( "billingOrganizations" ) || "Organizations"}
{ tierLimits[ pendingTier.tier ].remoteNodes }{" "} {t("billingRemoteNodes") || "Remote Nodes"}
{ tierLimits[ pendingTier.tier ].publicResources }{" "} {t( "billingPublicResources" ) || "Public Resources"}
{ tierLimits[ pendingTier.tier ].privateResources }{" "} {t( "billingPrivateResources" ) || "Private Resources"}
{ tierLimits[ pendingTier.tier ].machineClients }{" "} {t( "billingMachineClients" ) || "Machine Clients"}
)} {/* Warning for limit violations when downgrading */} {pendingTier.action === "downgrade" && (() => { const violations = checkLimitViolations( pendingTier.tier ); if (violations.length > 0) { return ( {t( "billingLimitViolationWarning" ) || "Usage Exceeds New Plan Limits"}

{t( "billingLimitViolationDescription" ) || "Your current usage exceeds the limits of this plan. The following features will be disabled until you reduce usage:"}

    {violations.map( ( violation, index ) => (
  • { violation.feature } : Currently using{" "} { violation.currentUsage } , new limit is{" "} { violation.newLimit }
  • ) )}
); } return null; })()} {/* Warning for feature loss when downgrading */} {pendingTier.action === "downgrade" && ( {t("billingFeatureLossWarning") || "Feature Availability Notice"} {t( "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."} )}
)}
); }