mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-01 08:16:44 +00:00
Further billing
This commit is contained in:
@@ -1405,9 +1405,9 @@
|
|||||||
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
|
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
|
||||||
"billingDataUsage": "Data Usage",
|
"billingDataUsage": "Data Usage",
|
||||||
"billingSites": "Sites",
|
"billingSites": "Sites",
|
||||||
"billingUsers": "Active Users",
|
"billingUsers": "Users",
|
||||||
"billingDomains": "Active Domains",
|
"billingDomains": "Domains",
|
||||||
"billingRemoteExitNodes": "Active Self-hosted Nodes",
|
"billingRemoteExitNodes": "Remote Nodes",
|
||||||
"billingNoLimitConfigured": "No limit configured",
|
"billingNoLimitConfigured": "No limit configured",
|
||||||
"billingEstimatedPeriod": "Estimated Billing Period",
|
"billingEstimatedPeriod": "Estimated Billing Period",
|
||||||
"billingIncludedUsage": "Included Usage",
|
"billingIncludedUsage": "Included Usage",
|
||||||
@@ -1533,6 +1533,14 @@
|
|||||||
"billingManageLicenseSubscription": "Manage your subscription for paid self-hosted license keys",
|
"billingManageLicenseSubscription": "Manage your subscription for paid self-hosted license keys",
|
||||||
"billingCurrentKeys": "Current Keys",
|
"billingCurrentKeys": "Current Keys",
|
||||||
"billingModifyCurrentPlan": "Modify Current Plan",
|
"billingModifyCurrentPlan": "Modify Current Plan",
|
||||||
|
"billingConfirmUpgrade": "Confirm Upgrade",
|
||||||
|
"billingConfirmDowngrade": "Confirm Downgrade",
|
||||||
|
"billingConfirmUpgradeDescription": "You are about to upgrade your plan. Review the new limits and pricing below.",
|
||||||
|
"billingConfirmDowngradeDescription": "You are about to downgrade your plan. Review the new limits and pricing below.",
|
||||||
|
"billingPlanIncludes": "Plan Includes",
|
||||||
|
"billingProcessing": "Processing...",
|
||||||
|
"billingConfirmUpgradeButton": "Confirm Upgrade",
|
||||||
|
"billingConfirmDowngradeButton": "Confirm Downgrade",
|
||||||
"signUpTerms": {
|
"signUpTerms": {
|
||||||
"IAgreeToThe": "I agree to the",
|
"IAgreeToThe": "I agree to the",
|
||||||
"termsOfService": "terms of service",
|
"termsOfService": "terms of service",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const freeLimitSet: LimitSet = {
|
|||||||
description: "Free tier limit"
|
description: "Free tier limit"
|
||||||
}, // 25 GB
|
}, // 25 GB
|
||||||
[FeatureId.DOMAINS]: { value: 3, description: "Free tier limit" },
|
[FeatureId.DOMAINS]: { value: 3, description: "Free tier limit" },
|
||||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Free tier limit" }
|
[FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Free tier limit" }
|
||||||
};
|
};
|
||||||
|
|
||||||
export const homeLabLimitSet: LimitSet = {
|
export const homeLabLimitSet: LimitSet = {
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ interface StripeEvent {
|
|||||||
|
|
||||||
export function noop() {
|
export function noop() {
|
||||||
if (
|
if (
|
||||||
build !== "saas" ||
|
build !== "saas"
|
||||||
!process.env.S3_BUCKET ||
|
|
||||||
!process.env.LOCAL_FILE_PATH
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ import {
|
|||||||
InfoSections,
|
InfoSections,
|
||||||
InfoSectionTitle
|
InfoSectionTitle
|
||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
|
import {
|
||||||
|
Credenza,
|
||||||
|
CredenzaBody,
|
||||||
|
CredenzaClose,
|
||||||
|
CredenzaContent,
|
||||||
|
CredenzaDescription,
|
||||||
|
CredenzaFooter,
|
||||||
|
CredenzaHeader,
|
||||||
|
CredenzaTitle
|
||||||
|
} from "@app/components/Credenza";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import {
|
import {
|
||||||
CreditCard,
|
CreditCard,
|
||||||
@@ -30,7 +40,8 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
Globe,
|
Globe,
|
||||||
Server,
|
Server,
|
||||||
Layout
|
Layout,
|
||||||
|
Check
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
GetOrgSubscriptionResponse,
|
GetOrgSubscriptionResponse,
|
||||||
@@ -40,7 +51,7 @@ import { useTranslations } from "use-intl";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
// Plan tier definitions matching the mockup
|
// Plan tier definitions matching the mockup
|
||||||
type PlanId = "starter" | "homelab" | "team" | "business" | "enterprise";
|
type PlanId = "free" | "homelab" | "team" | "business" | "enterprise";
|
||||||
|
|
||||||
interface PlanOption {
|
interface PlanOption {
|
||||||
id: PlanId;
|
id: PlanId;
|
||||||
@@ -50,10 +61,39 @@ interface PlanOption {
|
|||||||
tierType: "home_lab" | "starter" | "scale" | null; // Maps to backend tier types
|
tierType: "home_lab" | "starter" | "scale" | null; // Maps to backend tier types
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tier limits for display in confirmation dialog
|
||||||
|
interface TierLimits {
|
||||||
|
sites: number;
|
||||||
|
users: number;
|
||||||
|
domains: number;
|
||||||
|
remoteNodes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tierLimits: Record<"home_lab" | "starter" | "scale", TierLimits> = {
|
||||||
|
home_lab: {
|
||||||
|
sites: 3,
|
||||||
|
users: 3,
|
||||||
|
domains: 3,
|
||||||
|
remoteNodes: 1
|
||||||
|
},
|
||||||
|
starter: {
|
||||||
|
sites: 10,
|
||||||
|
users: 150,
|
||||||
|
domains: 250,
|
||||||
|
remoteNodes: 5
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
sites: 10,
|
||||||
|
users: 150,
|
||||||
|
domains: 250,
|
||||||
|
remoteNodes: 5
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const planOptions: PlanOption[] = [
|
const planOptions: PlanOption[] = [
|
||||||
{
|
{
|
||||||
id: "starter",
|
id: "free",
|
||||||
name: "Starter",
|
name: "Free",
|
||||||
price: "Free",
|
price: "Free",
|
||||||
tierType: null
|
tierType: null
|
||||||
},
|
},
|
||||||
@@ -96,10 +136,12 @@ export default function BillingPage() {
|
|||||||
const [allSubscriptions, setAllSubscriptions] = useState<
|
const [allSubscriptions, setAllSubscriptions] = useState<
|
||||||
GetOrgSubscriptionResponse["subscriptions"]
|
GetOrgSubscriptionResponse["subscriptions"]
|
||||||
>([]);
|
>([]);
|
||||||
const [tierSubscription, setTierSubscription] =
|
const [tierSubscription, setTierSubscription] = useState<
|
||||||
useState<GetOrgSubscriptionResponse["subscriptions"][0] | null>(null);
|
GetOrgSubscriptionResponse["subscriptions"][0] | null
|
||||||
const [licenseSubscription, setLicenseSubscription] =
|
>(null);
|
||||||
useState<GetOrgSubscriptionResponse["subscriptions"][0] | null>(null);
|
const [licenseSubscription, setLicenseSubscription] = useState<
|
||||||
|
GetOrgSubscriptionResponse["subscriptions"][0] | null
|
||||||
|
>(null);
|
||||||
const [subscriptionLoading, setSubscriptionLoading] = useState(true);
|
const [subscriptionLoading, setSubscriptionLoading] = useState(true);
|
||||||
|
|
||||||
// Usage and limits data
|
// Usage and limits data
|
||||||
@@ -122,6 +164,15 @@ export default function BillingPage() {
|
|||||||
const DOMAINS = "domains";
|
const DOMAINS = "domains";
|
||||||
const REMOTE_EXIT_NODES = "remoteExitNodes";
|
const REMOTE_EXIT_NODES = "remoteExitNodes";
|
||||||
|
|
||||||
|
// Confirmation dialog state
|
||||||
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
|
const [pendingTier, setPendingTier] = useState<{
|
||||||
|
tier: "home_lab" | "starter" | "scale";
|
||||||
|
action: "upgrade" | "downgrade";
|
||||||
|
planName: string;
|
||||||
|
price: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchSubscription() {
|
async function fetchSubscription() {
|
||||||
setSubscriptionLoading(true);
|
setSubscriptionLoading(true);
|
||||||
@@ -133,10 +184,11 @@ export default function BillingPage() {
|
|||||||
setAllSubscriptions(subscriptions);
|
setAllSubscriptions(subscriptions);
|
||||||
|
|
||||||
// Find tier subscription
|
// Find tier subscription
|
||||||
const tierSub = subscriptions.find(({ subscription }) =>
|
const tierSub = subscriptions.find(
|
||||||
subscription?.type === "home_lab" ||
|
({ subscription }) =>
|
||||||
subscription?.type === "starter" ||
|
subscription?.type === "home_lab" ||
|
||||||
subscription?.type === "scale"
|
subscription?.type === "starter" ||
|
||||||
|
subscription?.type === "scale"
|
||||||
);
|
);
|
||||||
setTierSubscription(tierSub || null);
|
setTierSubscription(tierSub || null);
|
||||||
|
|
||||||
@@ -190,7 +242,9 @@ export default function BillingPage() {
|
|||||||
fetchUsage();
|
fetchUsage();
|
||||||
}, [org.org.orgId]);
|
}, [org.org.orgId]);
|
||||||
|
|
||||||
const handleStartSubscription = async (tier: "home_lab" | "starter" | "scale") => {
|
const handleStartSubscription = async (
|
||||||
|
tier: "home_lab" | "starter" | "scale"
|
||||||
|
) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await api.post<AxiosResponse<string>>(
|
const response = await api.post<AxiosResponse<string>>(
|
||||||
@@ -270,15 +324,43 @@ export default function BillingPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmTierChange = () => {
|
||||||
|
if (!pendingTier) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
pendingTier.action === "upgrade" ||
|
||||||
|
pendingTier.action === "downgrade"
|
||||||
|
) {
|
||||||
|
if (hasSubscription) {
|
||||||
|
handleChangeTier(pendingTier.tier);
|
||||||
|
} else {
|
||||||
|
handleStartSubscription(pendingTier.tier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowConfirmDialog(false);
|
||||||
|
setPendingTier(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showTierConfirmation = (
|
||||||
|
tier: "home_lab" | "starter" | "scale",
|
||||||
|
action: "upgrade" | "downgrade",
|
||||||
|
planName: string,
|
||||||
|
price: string
|
||||||
|
) => {
|
||||||
|
setPendingTier({ tier, action, planName, price });
|
||||||
|
setShowConfirmDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleContactUs = () => {
|
const handleContactUs = () => {
|
||||||
window.open("mailto:sales@pangolin.net", "_blank");
|
window.open("mailto:sales@pangolin.net", "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get current plan ID from tier
|
// Get current plan ID from tier
|
||||||
const getCurrentPlanId = (): PlanId => {
|
const getCurrentPlanId = (): PlanId => {
|
||||||
if (!hasSubscription || !currentTier) return "starter";
|
if (!hasSubscription || !currentTier) return "free";
|
||||||
const plan = planOptions.find((p) => p.tierType === currentTier);
|
const plan = planOptions.find((p) => p.tierType === currentTier);
|
||||||
return plan?.id || "starter";
|
return plan?.id || "free";
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentPlanId = getCurrentPlanId();
|
const currentPlanId = getCurrentPlanId();
|
||||||
@@ -295,8 +377,8 @@ export default function BillingPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (plan.id === currentPlanId) {
|
if (plan.id === currentPlanId) {
|
||||||
// If it's the free plan (starter with no subscription), show as current but disabled
|
// If it's the free plan (free with no subscription), show as current but disabled
|
||||||
if (plan.id === "starter" && !hasSubscription) {
|
if (plan.id === "free" && !hasSubscription) {
|
||||||
return {
|
return {
|
||||||
label: "Current Plan",
|
label: "Current Plan",
|
||||||
action: () => {},
|
action: () => {},
|
||||||
@@ -320,10 +402,18 @@ export default function BillingPage() {
|
|||||||
if (planIndex < currentIndex) {
|
if (planIndex < currentIndex) {
|
||||||
return {
|
return {
|
||||||
label: "Downgrade",
|
label: "Downgrade",
|
||||||
action: () =>
|
action: () => {
|
||||||
plan.tierType
|
if (plan.tierType) {
|
||||||
? handleChangeTier(plan.tierType)
|
showTierConfirmation(
|
||||||
: handleModifySubscription(),
|
plan.tierType,
|
||||||
|
"downgrade",
|
||||||
|
plan.name,
|
||||||
|
plan.price + (plan.priceDetail || "")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
handleModifySubscription();
|
||||||
|
}
|
||||||
|
},
|
||||||
variant: "outline" as const,
|
variant: "outline" as const,
|
||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
@@ -331,12 +421,18 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
label: "Upgrade",
|
label: "Upgrade",
|
||||||
action: () =>
|
action: () => {
|
||||||
plan.tierType
|
if (plan.tierType) {
|
||||||
? hasSubscription
|
showTierConfirmation(
|
||||||
? handleChangeTier(plan.tierType)
|
plan.tierType,
|
||||||
: handleStartSubscription(plan.tierType)
|
"upgrade",
|
||||||
: handleModifySubscription(),
|
plan.name,
|
||||||
|
plan.price + (plan.priceDetail || "")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
handleModifySubscription();
|
||||||
|
}
|
||||||
|
},
|
||||||
variant: "outline" as const,
|
variant: "outline" as const,
|
||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
@@ -407,11 +503,11 @@ export default function BillingPage() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="font-semibold text-lg">
|
<div className="text-2xl">
|
||||||
{plan.name}
|
{plan.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<span className="text-2xl font-bold">
|
<span className="text-xl">
|
||||||
{plan.price}
|
{plan.price}
|
||||||
</span>
|
</span>
|
||||||
{plan.priceDetail && (
|
{plan.priceDetail && (
|
||||||
@@ -431,7 +527,9 @@ export default function BillingPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={planAction.action}
|
onClick={planAction.action}
|
||||||
disabled={isLoading || planAction.disabled}
|
disabled={
|
||||||
|
isLoading || planAction.disabled
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{planAction.label}
|
{planAction.label}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -469,7 +567,7 @@ export default function BillingPage() {
|
|||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{/* Current Usage */}
|
{/* Current Usage */}
|
||||||
<div className="border rounded-lg p-4 bg-muted/30">
|
<div className="border rounded-lg p-4">
|
||||||
<div className="text-sm text-muted-foreground mb-2">
|
<div className="text-sm text-muted-foreground mb-2">
|
||||||
{t("billingCurrentUsage") || "Current Usage"}
|
{t("billingCurrentUsage") || "Current Usage"}
|
||||||
</div>
|
</div>
|
||||||
@@ -480,27 +578,27 @@ export default function BillingPage() {
|
|||||||
<span className="text-lg">
|
<span className="text-lg">
|
||||||
{t("billingUsers") || "Users"}
|
{t("billingUsers") || "Users"}
|
||||||
</span>
|
</span>
|
||||||
|
{hasSubscription && getPricePerUser() > 0 && (
|
||||||
|
<div className="text-sm text-muted-foreground mt-1">
|
||||||
|
x ${getPricePerUser()} / month = $
|
||||||
|
{getUserCount() * getPricePerUser()} /
|
||||||
|
month
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{hasSubscription && getPricePerUser() > 0 && (
|
|
||||||
<div className="text-sm text-muted-foreground mt-1">
|
|
||||||
x ${getPricePerUser()} / month = $
|
|
||||||
{getUserCount() * getPricePerUser()} / month
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Maximum Limits */}
|
{/* Maximum Limits */}
|
||||||
<div className="border rounded-lg p-4 bg-muted/30">
|
<div className="border rounded-lg p-4">
|
||||||
<div className="text-sm text-muted-foreground mb-3">
|
<div className="text-sm text-muted-foreground mb-3">
|
||||||
{t("billingMaximumLimits") || "Maximum Limits"}
|
{t("billingMaximumLimits") || "Maximum Limits"}
|
||||||
</div>
|
</div>
|
||||||
<InfoSections cols={4}>
|
<InfoSections cols={4}>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle className="text-xs text-muted-foreground font-normal flex items-center gap-1">
|
<InfoSectionTitle className="flex items-center gap-1 text-xs">
|
||||||
<Users className="h-3 w-3" />
|
|
||||||
{t("billingUsers") || "Users"}
|
{t("billingUsers") || "Users"}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent className="font-semibold">
|
<InfoSectionContent className="text-sm">
|
||||||
{getLimitValue(USERS) ??
|
{getLimitValue(USERS) ??
|
||||||
t("billingUnlimited") ??
|
t("billingUnlimited") ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
@@ -509,11 +607,10 @@ export default function BillingPage() {
|
|||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle className="text-xs text-muted-foreground font-normal flex items-center gap-1">
|
<InfoSectionTitle className="flex items-center gap-1 text-xs">
|
||||||
<Layout className="h-3 w-3" />
|
|
||||||
{t("billingSites") || "Sites"}
|
{t("billingSites") || "Sites"}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent className="font-semibold">
|
<InfoSectionContent className="text-sm">
|
||||||
{getLimitValue(SITES) ??
|
{getLimitValue(SITES) ??
|
||||||
t("billingUnlimited") ??
|
t("billingUnlimited") ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
@@ -522,11 +619,10 @@ export default function BillingPage() {
|
|||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle className="text-xs text-muted-foreground font-normal flex items-center gap-1">
|
<InfoSectionTitle className="flex items-center gap-1 text-xs">
|
||||||
<Globe className="h-3 w-3" />
|
|
||||||
{t("billingDomains") || "Domains"}
|
{t("billingDomains") || "Domains"}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent className="font-semibold">
|
<InfoSectionContent className="text-sm">
|
||||||
{getLimitValue(DOMAINS) ??
|
{getLimitValue(DOMAINS) ??
|
||||||
t("billingUnlimited") ??
|
t("billingUnlimited") ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
@@ -535,12 +631,11 @@ export default function BillingPage() {
|
|||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle className="text-xs text-muted-foreground font-normal flex items-center gap-1">
|
<InfoSectionTitle className="flex items-center gap-1 text-xs">
|
||||||
<Server className="h-3 w-3" />
|
|
||||||
{t("billingRemoteNodes") ||
|
{t("billingRemoteNodes") ||
|
||||||
"Remote Nodes"}
|
"Remote Nodes"}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent className="font-semibold">
|
<InfoSectionContent className="text-sm">
|
||||||
{getLimitValue(REMOTE_EXIT_NODES) ??
|
{getLimitValue(REMOTE_EXIT_NODES) ??
|
||||||
t("billingUnlimited") ??
|
t("billingUnlimited") ??
|
||||||
"∞"}{" "}
|
"∞"}{" "}
|
||||||
@@ -559,8 +654,7 @@ export default function BillingPage() {
|
|||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t("billingPaidLicenseKeys") ||
|
{t("billingPaidLicenseKeys") || "Paid License Keys"}
|
||||||
"Paid License Keys"}
|
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t("billingManageLicenseSubscription") ||
|
{t("billingManageLicenseSubscription") ||
|
||||||
@@ -597,6 +691,115 @@ export default function BillingPage() {
|
|||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Tier Change Confirmation Dialog */}
|
||||||
|
<Credenza
|
||||||
|
open={showConfirmDialog}
|
||||||
|
onOpenChange={setShowConfirmDialog}
|
||||||
|
>
|
||||||
|
<CredenzaContent>
|
||||||
|
<CredenzaHeader>
|
||||||
|
<CredenzaTitle>
|
||||||
|
{pendingTier?.action === "upgrade"
|
||||||
|
? t("billingConfirmUpgrade") ||
|
||||||
|
"Confirm Upgrade"
|
||||||
|
: t("billingConfirmDowngrade") ||
|
||||||
|
"Confirm Downgrade"}
|
||||||
|
</CredenzaTitle>
|
||||||
|
<CredenzaDescription>
|
||||||
|
{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.`}
|
||||||
|
</CredenzaDescription>
|
||||||
|
</CredenzaHeader>
|
||||||
|
<CredenzaBody>
|
||||||
|
{pendingTier && pendingTier.tier && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="border rounded-lg p-4 bg-muted/30">
|
||||||
|
<div className="font-semibold text-lg mb-2">
|
||||||
|
{pendingTier.planName}
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{pendingTier.price}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-3">
|
||||||
|
{t("billingPlanIncludes") ||
|
||||||
|
"Plan Includes:"}
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
tierLimits[pendingTier.tier]
|
||||||
|
.sites
|
||||||
|
}{" "}
|
||||||
|
{t("billingSites") || "Sites"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
tierLimits[pendingTier.tier]
|
||||||
|
.users
|
||||||
|
}{" "}
|
||||||
|
{t("billingUsers") || "Users"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
tierLimits[pendingTier.tier]
|
||||||
|
.domains
|
||||||
|
}{" "}
|
||||||
|
{t("billingDomains") ||
|
||||||
|
"Domains"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
tierLimits[pendingTier.tier]
|
||||||
|
.remoteNodes
|
||||||
|
}{" "}
|
||||||
|
{t("billingRemoteNodes") ||
|
||||||
|
"Remote Nodes"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CredenzaBody>
|
||||||
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline" disabled={isLoading}>
|
||||||
|
{t("cancel") || "Cancel"}
|
||||||
|
</Button>
|
||||||
|
</CredenzaClose>
|
||||||
|
<Button
|
||||||
|
onClick={confirmTierChange}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading
|
||||||
|
? t("billingProcessing") || "Processing..."
|
||||||
|
: pendingTier?.action === "upgrade"
|
||||||
|
? t("billingConfirmUpgradeButton") ||
|
||||||
|
"Confirm Upgrade"
|
||||||
|
: t("billingConfirmDowngradeButton") ||
|
||||||
|
"Confirm Downgrade"}
|
||||||
|
</Button>
|
||||||
|
</CredenzaFooter>
|
||||||
|
</CredenzaContent>
|
||||||
|
</Credenza>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user