mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-07 11:16:37 +00:00
Adjust billing upgrade language
This commit is contained in:
@@ -35,11 +35,7 @@ import {
|
|||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { CreditCard, ExternalLink, Check, AlertTriangle } from "lucide-react";
|
import { CreditCard, ExternalLink, Check, AlertTriangle } from "lucide-react";
|
||||||
import {
|
import { Alert, AlertTitle, AlertDescription } from "@app/components/ui/alert";
|
||||||
Alert,
|
|
||||||
AlertTitle,
|
|
||||||
AlertDescription
|
|
||||||
} from "@app/components/ui/alert";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
@@ -69,6 +65,7 @@ type PlanOption = {
|
|||||||
price: string;
|
price: string;
|
||||||
priceDetail?: string;
|
priceDetail?: string;
|
||||||
tierType: Tier | null;
|
tierType: Tier | null;
|
||||||
|
features: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const planOptions: PlanOption[] = [
|
const planOptions: PlanOption[] = [
|
||||||
@@ -76,41 +73,87 @@ const planOptions: PlanOption[] = [
|
|||||||
id: "basic",
|
id: "basic",
|
||||||
name: "Basic",
|
name: "Basic",
|
||||||
price: "Free",
|
price: "Free",
|
||||||
tierType: null
|
tierType: null,
|
||||||
|
features: [
|
||||||
|
"Basic Pangolin features",
|
||||||
|
"Free provided domains",
|
||||||
|
"Web-based proxy resources",
|
||||||
|
"Private resources and clients",
|
||||||
|
"Peer-to-peer connections"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "home",
|
id: "home",
|
||||||
name: "Home",
|
name: "Home",
|
||||||
price: "$12.50",
|
price: "$12.50",
|
||||||
priceDetail: "/ month",
|
priceDetail: "/ month",
|
||||||
tierType: "tier1"
|
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",
|
id: "team",
|
||||||
name: "Team",
|
name: "Team",
|
||||||
price: "$4",
|
price: "$4",
|
||||||
priceDetail: "per user / month",
|
priceDetail: "per user / month",
|
||||||
tierType: "tier2"
|
tierType: "tier2",
|
||||||
|
features: [
|
||||||
|
"Everything in Basic",
|
||||||
|
"Custom domains",
|
||||||
|
"OAuth2/OIDC, Google, & Azure SSO",
|
||||||
|
"Access and action audit logs",
|
||||||
|
"Device posture information"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "business",
|
id: "business",
|
||||||
name: "Business",
|
name: "Business",
|
||||||
price: "$9",
|
price: "$9",
|
||||||
priceDetail: "per user / month",
|
priceDetail: "per user / month",
|
||||||
tierType: "tier3"
|
tierType: "tier3",
|
||||||
|
features: [
|
||||||
|
"Everything in Team",
|
||||||
|
"Multiple organizations (multi-tenancy)",
|
||||||
|
"Auto-provisioning via IdP",
|
||||||
|
"Pangolin SSH",
|
||||||
|
"Device approvals",
|
||||||
|
"Custom branding",
|
||||||
|
"Business support"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "enterprise",
|
id: "enterprise",
|
||||||
name: "Enterprise",
|
name: "Enterprise",
|
||||||
price: "Custom",
|
price: "Custom",
|
||||||
tierType: null
|
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
|
// Tier limits mapping derived from limit sets
|
||||||
const tierLimits: Record<
|
const tierLimits: Record<
|
||||||
Tier | "basic",
|
Tier | "basic",
|
||||||
{ users: number; sites: number; domains: number; remoteNodes: number; organizations: number }
|
{
|
||||||
|
users: number;
|
||||||
|
sites: number;
|
||||||
|
domains: number;
|
||||||
|
remoteNodes: number;
|
||||||
|
organizations: number;
|
||||||
|
}
|
||||||
> = {
|
> = {
|
||||||
basic: {
|
basic: {
|
||||||
users: freeLimitSet[FeatureId.USERS]?.value ?? 0,
|
users: freeLimitSet[FeatureId.USERS]?.value ?? 0,
|
||||||
@@ -463,31 +506,43 @@ export default function BillingPage() {
|
|||||||
const isProblematicState = hasProblematicSubscription();
|
const isProblematicState = hasProblematicSubscription();
|
||||||
|
|
||||||
// Get user-friendly subscription status message
|
// Get user-friendly subscription status message
|
||||||
const getSubscriptionStatusMessage = (): { title: string; description: string } | null => {
|
const getSubscriptionStatusMessage = (): {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
} | null => {
|
||||||
if (!tierSubscription?.subscription || !isProblematicState) return null;
|
if (!tierSubscription?.subscription || !isProblematicState) return null;
|
||||||
|
|
||||||
const status = tierSubscription.subscription.status;
|
const status = tierSubscription.subscription.status;
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "past_due":
|
case "past_due":
|
||||||
return {
|
return {
|
||||||
title: t("billingPastDueTitle") || "Payment Past Due",
|
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."
|
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":
|
case "unpaid":
|
||||||
return {
|
return {
|
||||||
title: t("billingUnpaidTitle") || "Subscription Unpaid",
|
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."
|
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":
|
case "incomplete":
|
||||||
return {
|
return {
|
||||||
title: t("billingIncompleteTitle") || "Payment Incomplete",
|
title: t("billingIncompleteTitle") || "Payment Incomplete",
|
||||||
description: t("billingIncompleteDescription") || "Your payment is incomplete. Please complete the payment process to activate your subscription."
|
description:
|
||||||
|
t("billingIncompleteDescription") ||
|
||||||
|
"Your payment is incomplete. Please complete the payment process to activate your subscription."
|
||||||
};
|
};
|
||||||
case "incomplete_expired":
|
case "incomplete_expired":
|
||||||
return {
|
return {
|
||||||
title: t("billingIncompleteExpiredTitle") || "Payment Expired",
|
title:
|
||||||
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."
|
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:
|
default:
|
||||||
return null;
|
return null;
|
||||||
@@ -509,7 +564,11 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
if (plan.id === currentPlanId) {
|
if (plan.id === currentPlanId) {
|
||||||
// If it's the basic plan (basic with no subscription), show as current but disabled
|
// If it's the basic plan (basic with no subscription), show as current but disabled
|
||||||
if (plan.id === "basic" && !hasSubscription && !isProblematicState) {
|
if (
|
||||||
|
plan.id === "basic" &&
|
||||||
|
!hasSubscription &&
|
||||||
|
!isProblematicState
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
label: "Current Plan",
|
label: "Current Plan",
|
||||||
action: () => {},
|
action: () => {},
|
||||||
@@ -632,7 +691,9 @@ export default function BillingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if downgrading to a tier would violate current usage limits
|
// Check if downgrading to a tier would violate current usage limits
|
||||||
const checkLimitViolations = (targetTier: Tier | "basic"): Array<{
|
const checkLimitViolations = (
|
||||||
|
targetTier: Tier | "basic"
|
||||||
|
): Array<{
|
||||||
feature: string;
|
feature: string;
|
||||||
currentUsage: number;
|
currentUsage: number;
|
||||||
newLimit: number;
|
newLimit: number;
|
||||||
@@ -687,7 +748,10 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
// Check organizations
|
// Check organizations
|
||||||
const organizationsUsage = getUsageValue(ORGINIZATIONS);
|
const organizationsUsage = getUsageValue(ORGINIZATIONS);
|
||||||
if (limits.organizations > 0 && organizationsUsage > limits.organizations) {
|
if (
|
||||||
|
limits.organizations > 0 &&
|
||||||
|
organizationsUsage > limits.organizations
|
||||||
|
) {
|
||||||
violations.push({
|
violations.push({
|
||||||
feature: "Organizations",
|
feature: "Organizations",
|
||||||
currentUsage: organizationsUsage,
|
currentUsage: organizationsUsage,
|
||||||
@@ -712,17 +776,15 @@ export default function BillingPage() {
|
|||||||
{isProblematicState && statusMessage && (
|
{isProblematicState && statusMessage && (
|
||||||
<Alert variant="destructive" className="mb-6">
|
<Alert variant="destructive" className="mb-6">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<AlertTitle>
|
<AlertTitle>{statusMessage.title}</AlertTitle>
|
||||||
{statusMessage.title}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{statusMessage.description}
|
{statusMessage.description}{" "}
|
||||||
{" "}
|
|
||||||
<button
|
<button
|
||||||
onClick={handleModifySubscription}
|
onClick={handleModifySubscription}
|
||||||
className="underline font-semibold hover:no-underline"
|
className="underline font-semibold hover:no-underline"
|
||||||
>
|
>
|
||||||
{t("billingManageSubscription") || "Manage your subscription"}
|
{t("billingManageSubscription") ||
|
||||||
|
"Manage your subscription"}
|
||||||
</button>
|
</button>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -772,7 +834,10 @@ export default function BillingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{isProblematicState && planAction.disabled && !isCurrentPlan && plan.id !== "enterprise" ? (
|
{isProblematicState &&
|
||||||
|
planAction.disabled &&
|
||||||
|
!isCurrentPlan &&
|
||||||
|
plan.id !== "enterprise" ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div>
|
<div>
|
||||||
@@ -784,18 +849,29 @@ export default function BillingPage() {
|
|||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={planAction.action}
|
onClick={
|
||||||
disabled={
|
planAction.action
|
||||||
isLoading || planAction.disabled
|
}
|
||||||
|
disabled={
|
||||||
|
isLoading ||
|
||||||
|
planAction.disabled
|
||||||
|
}
|
||||||
|
loading={
|
||||||
|
isLoading &&
|
||||||
|
isCurrentPlan
|
||||||
}
|
}
|
||||||
loading={isLoading && isCurrentPlan}
|
|
||||||
>
|
>
|
||||||
{planAction.label}
|
{planAction.label}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{t("billingResolvePaymentIssue") || "Please resolve your payment issue before upgrading or downgrading"}</p>
|
<p>
|
||||||
|
{t(
|
||||||
|
"billingResolvePaymentIssue"
|
||||||
|
) ||
|
||||||
|
"Please resolve your payment issue before upgrading or downgrading"}
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
@@ -809,9 +885,12 @@ export default function BillingPage() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={planAction.action}
|
onClick={planAction.action}
|
||||||
disabled={
|
disabled={
|
||||||
isLoading || planAction.disabled
|
isLoading ||
|
||||||
|
planAction.disabled
|
||||||
|
}
|
||||||
|
loading={
|
||||||
|
isLoading && isCurrentPlan
|
||||||
}
|
}
|
||||||
loading={isLoading && isCurrentPlan}
|
|
||||||
>
|
>
|
||||||
{planAction.label}
|
{planAction.label}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -886,18 +965,38 @@ export default function BillingPage() {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="flex items-center gap-1">
|
<TooltipTrigger className="flex items-center gap-1">
|
||||||
<AlertTriangle className="h-3 w-3 text-orange-400" />
|
<AlertTriangle className="h-3 w-3 text-orange-400" />
|
||||||
<span className={cn(
|
<span
|
||||||
"text-orange-600 dark:text-orange-400 font-medium"
|
className={cn(
|
||||||
)}>
|
"text-orange-600 dark:text-orange-400 font-medium"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{getLimitValue(USERS) ??
|
{getLimitValue(USERS) ??
|
||||||
t("billingUnlimited") ??
|
t(
|
||||||
|
"billingUnlimited"
|
||||||
|
) ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
{getLimitValue(USERS) !== null &&
|
{getLimitValue(
|
||||||
"users"}
|
USERS
|
||||||
|
) !== null && "users"}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{t("billingUsageExceedsLimit", { current: getUsageValue(USERS), limit: getLimitValue(USERS) ?? 0 }) || `Current usage (${getUsageValue(USERS)}) exceeds limit (${getLimitValue(USERS)})`}</p>
|
<p>
|
||||||
|
{t(
|
||||||
|
"billingUsageExceedsLimit",
|
||||||
|
{
|
||||||
|
current:
|
||||||
|
getUsageValue(
|
||||||
|
USERS
|
||||||
|
),
|
||||||
|
limit:
|
||||||
|
getLimitValue(
|
||||||
|
USERS
|
||||||
|
) ?? 0
|
||||||
|
}
|
||||||
|
) ||
|
||||||
|
`Current usage (${getUsageValue(USERS)}) exceeds limit (${getLimitValue(USERS)})`}
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
@@ -905,8 +1004,8 @@ export default function BillingPage() {
|
|||||||
{getLimitValue(USERS) ??
|
{getLimitValue(USERS) ??
|
||||||
t("billingUnlimited") ??
|
t("billingUnlimited") ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
{getLimitValue(USERS) !== null &&
|
{getLimitValue(USERS) !==
|
||||||
"users"}
|
null && "users"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
@@ -920,18 +1019,38 @@ export default function BillingPage() {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="flex items-center gap-1">
|
<TooltipTrigger className="flex items-center gap-1">
|
||||||
<AlertTriangle className="h-3 w-3 text-orange-400" />
|
<AlertTriangle className="h-3 w-3 text-orange-400" />
|
||||||
<span className={cn(
|
<span
|
||||||
"text-orange-600 dark:text-orange-400 font-medium"
|
className={cn(
|
||||||
)}>
|
"text-orange-600 dark:text-orange-400 font-medium"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{getLimitValue(SITES) ??
|
{getLimitValue(SITES) ??
|
||||||
t("billingUnlimited") ??
|
t(
|
||||||
|
"billingUnlimited"
|
||||||
|
) ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
{getLimitValue(SITES) !== null &&
|
{getLimitValue(
|
||||||
"sites"}
|
SITES
|
||||||
|
) !== null && "sites"}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{t("billingUsageExceedsLimit", { current: getUsageValue(SITES), limit: getLimitValue(SITES) ?? 0 }) || `Current usage (${getUsageValue(SITES)}) exceeds limit (${getLimitValue(SITES)})`}</p>
|
<p>
|
||||||
|
{t(
|
||||||
|
"billingUsageExceedsLimit",
|
||||||
|
{
|
||||||
|
current:
|
||||||
|
getUsageValue(
|
||||||
|
SITES
|
||||||
|
),
|
||||||
|
limit:
|
||||||
|
getLimitValue(
|
||||||
|
SITES
|
||||||
|
) ?? 0
|
||||||
|
}
|
||||||
|
) ||
|
||||||
|
`Current usage (${getUsageValue(SITES)}) exceeds limit (${getLimitValue(SITES)})`}
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
@@ -939,8 +1058,8 @@ export default function BillingPage() {
|
|||||||
{getLimitValue(SITES) ??
|
{getLimitValue(SITES) ??
|
||||||
t("billingUnlimited") ??
|
t("billingUnlimited") ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
{getLimitValue(SITES) !== null &&
|
{getLimitValue(SITES) !==
|
||||||
"sites"}
|
null && "sites"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
@@ -954,18 +1073,40 @@ export default function BillingPage() {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="flex items-center gap-1">
|
<TooltipTrigger className="flex items-center gap-1">
|
||||||
<AlertTriangle className="h-3 w-3 text-orange-400" />
|
<AlertTriangle className="h-3 w-3 text-orange-400" />
|
||||||
<span className={cn(
|
<span
|
||||||
"text-orange-600 dark:text-orange-400 font-medium"
|
className={cn(
|
||||||
)}>
|
"text-orange-600 dark:text-orange-400 font-medium"
|
||||||
{getLimitValue(DOMAINS) ??
|
)}
|
||||||
t("billingUnlimited") ??
|
>
|
||||||
|
{getLimitValue(
|
||||||
|
DOMAINS
|
||||||
|
) ??
|
||||||
|
t(
|
||||||
|
"billingUnlimited"
|
||||||
|
) ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
{getLimitValue(DOMAINS) !== null &&
|
{getLimitValue(
|
||||||
"domains"}
|
DOMAINS
|
||||||
|
) !== null && "domains"}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{t("billingUsageExceedsLimit", { current: getUsageValue(DOMAINS), limit: getLimitValue(DOMAINS) ?? 0 }) || `Current usage (${getUsageValue(DOMAINS)}) exceeds limit (${getLimitValue(DOMAINS)})`}</p>
|
<p>
|
||||||
|
{t(
|
||||||
|
"billingUsageExceedsLimit",
|
||||||
|
{
|
||||||
|
current:
|
||||||
|
getUsageValue(
|
||||||
|
DOMAINS
|
||||||
|
),
|
||||||
|
limit:
|
||||||
|
getLimitValue(
|
||||||
|
DOMAINS
|
||||||
|
) ?? 0
|
||||||
|
}
|
||||||
|
) ||
|
||||||
|
`Current usage (${getUsageValue(DOMAINS)}) exceeds limit (${getLimitValue(DOMAINS)})`}
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
@@ -973,8 +1114,8 @@ export default function BillingPage() {
|
|||||||
{getLimitValue(DOMAINS) ??
|
{getLimitValue(DOMAINS) ??
|
||||||
t("billingUnlimited") ??
|
t("billingUnlimited") ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
{getLimitValue(DOMAINS) !== null &&
|
{getLimitValue(DOMAINS) !==
|
||||||
"domains"}
|
null && "domains"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
@@ -989,18 +1130,40 @@ export default function BillingPage() {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="flex items-center gap-1">
|
<TooltipTrigger className="flex items-center gap-1">
|
||||||
<AlertTriangle className="h-3 w-3 text-orange-400" />
|
<AlertTriangle className="h-3 w-3 text-orange-400" />
|
||||||
<span className={cn(
|
<span
|
||||||
"text-orange-600 dark:text-orange-400 font-medium"
|
className={cn(
|
||||||
)}>
|
"text-orange-600 dark:text-orange-400 font-medium"
|
||||||
{getLimitValue(ORGINIZATIONS) ??
|
)}
|
||||||
t("billingUnlimited") ??
|
>
|
||||||
|
{getLimitValue(
|
||||||
|
ORGINIZATIONS
|
||||||
|
) ??
|
||||||
|
t(
|
||||||
|
"billingUnlimited"
|
||||||
|
) ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
{getLimitValue(ORGINIZATIONS) !==
|
{getLimitValue(
|
||||||
null && "orgs"}
|
ORGINIZATIONS
|
||||||
|
) !== null && "orgs"}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{t("billingUsageExceedsLimit", { current: getUsageValue(ORGINIZATIONS), limit: getLimitValue(ORGINIZATIONS) ?? 0 }) || `Current usage (${getUsageValue(ORGINIZATIONS)}) exceeds limit (${getLimitValue(ORGINIZATIONS)})`}</p>
|
<p>
|
||||||
|
{t(
|
||||||
|
"billingUsageExceedsLimit",
|
||||||
|
{
|
||||||
|
current:
|
||||||
|
getUsageValue(
|
||||||
|
ORGINIZATIONS
|
||||||
|
),
|
||||||
|
limit:
|
||||||
|
getLimitValue(
|
||||||
|
ORGINIZATIONS
|
||||||
|
) ?? 0
|
||||||
|
}
|
||||||
|
) ||
|
||||||
|
`Current usage (${getUsageValue(ORGINIZATIONS)}) exceeds limit (${getLimitValue(ORGINIZATIONS)})`}
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
@@ -1008,8 +1171,9 @@ export default function BillingPage() {
|
|||||||
{getLimitValue(ORGINIZATIONS) ??
|
{getLimitValue(ORGINIZATIONS) ??
|
||||||
t("billingUnlimited") ??
|
t("billingUnlimited") ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
{getLimitValue(ORGINIZATIONS) !==
|
{getLimitValue(
|
||||||
null && "orgs"}
|
ORGINIZATIONS
|
||||||
|
) !== null && "orgs"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
@@ -1024,27 +1188,52 @@ export default function BillingPage() {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="flex items-center gap-1">
|
<TooltipTrigger className="flex items-center gap-1">
|
||||||
<AlertTriangle className="h-3 w-3 text-orange-400" />
|
<AlertTriangle className="h-3 w-3 text-orange-400" />
|
||||||
<span className={cn(
|
<span
|
||||||
"text-orange-600 dark:text-orange-400 font-medium"
|
className={cn(
|
||||||
)}>
|
"text-orange-600 dark:text-orange-400 font-medium"
|
||||||
{getLimitValue(REMOTE_EXIT_NODES) ??
|
)}
|
||||||
t("billingUnlimited") ??
|
>
|
||||||
|
{getLimitValue(
|
||||||
|
REMOTE_EXIT_NODES
|
||||||
|
) ??
|
||||||
|
t(
|
||||||
|
"billingUnlimited"
|
||||||
|
) ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
{getLimitValue(REMOTE_EXIT_NODES) !==
|
{getLimitValue(
|
||||||
null && "nodes"}
|
REMOTE_EXIT_NODES
|
||||||
|
) !== null && "nodes"}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<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>
|
<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>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{getLimitValue(REMOTE_EXIT_NODES) ??
|
{getLimitValue(
|
||||||
|
REMOTE_EXIT_NODES
|
||||||
|
) ??
|
||||||
t("billingUnlimited") ??
|
t("billingUnlimited") ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
{getLimitValue(REMOTE_EXIT_NODES) !==
|
{getLimitValue(
|
||||||
null && "nodes"}
|
REMOTE_EXIT_NODES
|
||||||
|
) !== null && "nodes"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
@@ -1072,7 +1261,8 @@ export default function BillingPage() {
|
|||||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border rounded-lg p-4">
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border rounded-lg p-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-muted-foreground mb-1">
|
<div className="text-sm text-muted-foreground mb-1">
|
||||||
{t("billingCurrentKeys") || "Current Keys"}
|
{t("billingCurrentKeys") ||
|
||||||
|
"Current Keys"}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<span className="text-3xl font-bold">
|
<span className="text-3xl font-bold">
|
||||||
@@ -1137,61 +1327,101 @@ export default function BillingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Features with check marks */}
|
||||||
|
{(() => {
|
||||||
|
const plan = planOptions.find(
|
||||||
|
(p) =>
|
||||||
|
p.tierType === pendingTier.tier ||
|
||||||
|
(pendingTier.tier === "basic" &&
|
||||||
|
p.id === "basic")
|
||||||
|
);
|
||||||
|
return plan?.features?.length ? (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-3">
|
||||||
|
{"What's included:"}
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{plan.features.map(
|
||||||
|
(feature, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 text-green-600 shrink-0" />
|
||||||
|
<span>
|
||||||
|
{feature}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Limits without check marks */}
|
||||||
{tierLimits[pendingTier.tier] && (
|
{tierLimits[pendingTier.tier] && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold mb-3">
|
<h4 className="font-semibold mb-3">
|
||||||
{t("billingPlanIncludes") ||
|
{"Up to:"}
|
||||||
"Plan Includes:"}
|
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 text-sm text-muted-foreground">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Check className="h-4 w-4 text-green-600" />
|
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground/50 shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{
|
{
|
||||||
tierLimits[pendingTier.tier]
|
tierLimits[
|
||||||
.users
|
pendingTier.tier
|
||||||
|
].users
|
||||||
}{" "}
|
}{" "}
|
||||||
{t("billingUsers") || "Users"}
|
{t("billingUsers") ||
|
||||||
|
"Users"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Check className="h-4 w-4 text-green-600" />
|
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground/50 shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{
|
{
|
||||||
tierLimits[pendingTier.tier]
|
tierLimits[
|
||||||
.sites
|
pendingTier.tier
|
||||||
|
].sites
|
||||||
}{" "}
|
}{" "}
|
||||||
{t("billingSites") || "Sites"}
|
{t("billingSites") ||
|
||||||
|
"Sites"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Check className="h-4 w-4 text-green-600" />
|
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground/50 shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{
|
{
|
||||||
tierLimits[pendingTier.tier]
|
tierLimits[
|
||||||
.domains
|
pendingTier.tier
|
||||||
|
].domains
|
||||||
}{" "}
|
}{" "}
|
||||||
{t("billingDomains") ||
|
{t("billingDomains") ||
|
||||||
"Domains"}
|
"Domains"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Check className="h-4 w-4 text-green-600" />
|
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground/50 shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{
|
{
|
||||||
tierLimits[pendingTier.tier]
|
tierLimits[
|
||||||
.organizations
|
pendingTier.tier
|
||||||
|
].organizations
|
||||||
}{" "}
|
}{" "}
|
||||||
{t("billingOrganizations") ||
|
{t(
|
||||||
"Organizations"}
|
"billingOrganizations"
|
||||||
|
) || "Organizations"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Check className="h-4 w-4 text-green-600" />
|
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground/50 shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{
|
{
|
||||||
tierLimits[pendingTier.tier]
|
tierLimits[
|
||||||
.remoteNodes
|
pendingTier.tier
|
||||||
|
].remoteNodes
|
||||||
}{" "}
|
}{" "}
|
||||||
{t("billingRemoteNodes") ||
|
{t("billingRemoteNodes") ||
|
||||||
"Remote Nodes"}
|
"Remote Nodes"}
|
||||||
@@ -1202,43 +1432,84 @@ export default function BillingPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Warning for limit violations when downgrading */}
|
{/* Warning for limit violations when downgrading */}
|
||||||
{pendingTier.action === "downgrade" && (() => {
|
{pendingTier.action === "downgrade" &&
|
||||||
const violations = checkLimitViolations(pendingTier.tier);
|
(() => {
|
||||||
if (violations.length > 0) {
|
const violations = checkLimitViolations(
|
||||||
return (
|
pendingTier.tier
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertTriangle className="h-4 w-4" />
|
|
||||||
<AlertTitle>
|
|
||||||
{t("billingLimitViolationWarning") || "Usage Exceeds New Plan Limits"}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
<p className="mb-3">
|
|
||||||
{t("billingLimitViolationDescription") || "Your current usage exceeds the limits of this plan. The following features will be disabled until you reduce usage:"}
|
|
||||||
</p>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{violations.map((violation, index) => (
|
|
||||||
<li key={index} className="flex items-center gap-2">
|
|
||||||
<span className="font-medium">{violation.feature}:</span>
|
|
||||||
<span>Currently using {violation.currentUsage}, new limit is {violation.newLimit}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
);
|
);
|
||||||
}
|
if (violations.length > 0) {
|
||||||
return null;
|
return (
|
||||||
})()}
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertTitle>
|
||||||
|
{t(
|
||||||
|
"billingLimitViolationWarning"
|
||||||
|
) ||
|
||||||
|
"Usage Exceeds New Plan Limits"}
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<p className="mb-3">
|
||||||
|
{t(
|
||||||
|
"billingLimitViolationDescription"
|
||||||
|
) ||
|
||||||
|
"Your current usage exceeds the limits of this plan. The following features will be disabled until you reduce usage:"}
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{violations.map(
|
||||||
|
(
|
||||||
|
violation,
|
||||||
|
index
|
||||||
|
) => (
|
||||||
|
<li
|
||||||
|
key={
|
||||||
|
index
|
||||||
|
}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="font-medium">
|
||||||
|
{
|
||||||
|
violation.feature
|
||||||
|
}
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Currently
|
||||||
|
using{" "}
|
||||||
|
{
|
||||||
|
violation.currentUsage
|
||||||
|
}
|
||||||
|
,
|
||||||
|
new
|
||||||
|
limit
|
||||||
|
is{" "}
|
||||||
|
{
|
||||||
|
violation.newLimit
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Warning for feature loss when downgrading */}
|
{/* Warning for feature loss when downgrading */}
|
||||||
{pendingTier.action === "downgrade" && (
|
{pendingTier.action === "downgrade" && (
|
||||||
<Alert variant="warning">
|
<Alert variant="warning">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
{t("billingFeatureLossWarning") || "Feature Availability Notice"}
|
{t("billingFeatureLossWarning") ||
|
||||||
|
"Feature Availability Notice"}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{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."}
|
{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."}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user