Kaynağa Gözat

option for cancle sub and more info (#24)

Bernt Christian Egeland 7 ay önce
ebeveyn
işleme
c4010bdbd7

+ 56 - 0
messages/en/settings.json

@@ -719,5 +719,61 @@
     "failedSave": "Failed to save settings",
     "testSentTo": "Test SMS sent to {phone}",
     "testFailed": "SMS test failed"
+  },
+  "subscription": {
+    "title": "Subscription Plan",
+    "description": "Your organization's current Torqvoice subscription tier and available features.",
+    "currentPlan": "Current Plan",
+    "planFree": "Free",
+    "planPro": "Torq Pro",
+    "planEnterprise": "Enterprise",
+    "price": "{price}/{interval}",
+    "perYear": "yr",
+    "perMonth": "mo",
+    "statusActive": "Active",
+    "statusCanceling": "Canceling",
+    "statusPastDue": "Past Due",
+    "statusCanceled": "Canceled",
+    "billingPeriod": "Billing Period",
+    "billingPeriodDates": "{start} — {end}",
+    "nextRenewal": "Next Renewal",
+    "cancelingWarning": "Your subscription will cancel on {date}. After that, you'll be downgraded to Free.",
+    "pastDueWarning": "Your payment is past due. Please update your payment method to avoid service interruption.",
+    "featuresTitle": "Plan Features",
+    "featuresDescription": "What's included in your current plan.",
+    "featureCustomers": "Customers",
+    "featureTeamMembers": "Team Members",
+    "featureSmtp": "Custom Email (SMTP)",
+    "featureApi": "API Access",
+    "featurePayments": "Online Payments",
+    "featureCustomFields": "Custom Fields",
+    "featureSms": "SMS Messaging",
+    "featureCustomerPortal": "Customer Portal",
+    "featureCustomTemplates": "Custom Templates",
+    "featureBrandingRemoved": "Branding Removed",
+    "usageOf": "{used} / {limit}",
+    "usageUnlimited": "{used} / Unlimited",
+    "included": "Included",
+    "notIncluded": "Not Included",
+    "manageTitle": "Manage Subscription",
+    "manageBilling": "Manage Billing",
+    "manageBillingDescription": "Update payment method, view invoices, and manage billing details.",
+    "cancelSubscription": "Cancel Subscription",
+    "resumeSubscription": "Resume Subscription",
+    "cancelDialogTitle": "Cancel Subscription",
+    "cancelDialogDescription": "Your subscription will remain active until {date}. After that, you'll be downgraded to Free.",
+    "cancelDialogConfirm": "Yes, Cancel",
+    "cancelDialogCancel": "Keep Subscription",
+    "cancelSuccess": "Subscription will cancel at end of billing period",
+    "cancelError": "Failed to cancel subscription",
+    "resumeSuccess": "Subscription resumed successfully",
+    "resumeError": "Failed to resume subscription",
+    "billingPortalError": "Failed to open billing portal",
+    "upgradeTitle": "Upgrade Plan",
+    "upgradeDescription": "Unlock more features by upgrading your plan.",
+    "upgradeToProDescription": "Upgrade to unlock unlimited customers, team members, reports, SMTP, API access, and more.",
+    "upgradeToEnterpriseDescription": "Need more than 5 team members? Upgrade to Enterprise for up to 50 users.",
+    "upgradeToPro": "Upgrade to Pro — $99/yr",
+    "upgradeToEnterprise": "Enterprise — $140/yr"
   }
 }

BIN
public/logo1.png


+ 19 - 2
src/app/(authenticated)/settings/subscription/page.tsx

@@ -1,7 +1,7 @@
 import { getAuthContext } from "@/lib/get-auth-context";
 import { db } from "@/lib/db";
 import { redirect } from "next/navigation";
-import { isCloudMode } from "@/lib/features";
+import { isCloudMode, PLAN_FEATURES, type Plan } from "@/lib/features";
 import { SubscriptionSettings } from "@/features/subscription/Components/subscription-settings";
 
 export default async function SubscriptionPage() {
@@ -17,16 +17,33 @@ export default async function SubscriptionPage() {
     include: { plan: true },
   });
 
-  const plan = subscription?.status === "active"
+  const plan: Plan = subscription?.status === "active" || subscription?.status === "trialing"
     ? (subscription.plan.name.toLowerCase() === "enterprise" ? "enterprise" : "pro")
     : "free";
 
+  const features = PLAN_FEATURES[plan];
+
+  const [customerCount, memberCount] = await Promise.all([
+    db.customer.count({
+      where: { organizationId: authContext.organizationId },
+    }),
+    db.organizationMember.count({
+      where: { organizationId: authContext.organizationId },
+    }),
+  ]);
+
   return (
     <SubscriptionSettings
       plan={plan}
       status={subscription?.status ?? null}
       cancelAtPeriodEnd={subscription?.cancelAtPeriodEnd ?? false}
       currentPeriodEnd={subscription?.currentPeriodEnd?.toISOString() ?? null}
+      currentPeriodStart={subscription?.currentPeriodStart?.toISOString() ?? null}
+      planPrice={subscription?.plan.price ?? 0}
+      planInterval={subscription?.plan.interval ?? "year"}
+      hasStripeCustomer={!!subscription?.stripeCustomerId}
+      usage={{ customers: customerCount, members: memberCount }}
+      features={features}
     />
   );
 }

+ 64 - 0
src/app/api/protected/subscription/billing-portal/route.ts

@@ -0,0 +1,64 @@
+import { NextResponse } from "next/server";
+import Stripe from "stripe";
+import { auth } from "@/lib/auth";
+import { headers } from "next/headers";
+import { db } from "@/lib/db";
+
+let stripeClient: Stripe | null = null;
+
+function getStripe(): Stripe {
+  if (!stripeClient) {
+    const key = process.env.STRIPE_SECRET_KEY;
+    if (!key) throw new Error("STRIPE_SECRET_KEY is not configured");
+    stripeClient = new Stripe(key);
+  }
+  return stripeClient;
+}
+
+export async function POST() {
+  try {
+    const session = await auth.api.getSession({ headers: await headers() });
+    if (!session?.user?.id) {
+      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+    }
+
+    const membership = await db.organizationMember.findFirst({
+      where: { userId: session.user.id },
+      select: { organizationId: true },
+    });
+
+    if (!membership?.organizationId) {
+      return NextResponse.json(
+        { error: "No organization found" },
+        { status: 400 },
+      );
+    }
+
+    const subscription = await db.subscription.findUnique({
+      where: { organizationId: membership.organizationId },
+      select: { stripeCustomerId: true },
+    });
+
+    if (!subscription?.stripeCustomerId) {
+      return NextResponse.json(
+        { error: "No billing account found" },
+        { status: 400 },
+      );
+    }
+
+    const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
+    const stripe = getStripe();
+
+    const portalSession = await stripe.billingPortal.sessions.create({
+      customer: subscription.stripeCustomerId,
+      return_url: `${appUrl}/settings/subscription`,
+    });
+
+    return NextResponse.json({ url: portalSession.url });
+  } catch (error) {
+    console.error("[Billing Portal] Error:", error);
+    const message =
+      error instanceof Error ? error.message : "Failed to create billing portal session";
+    return NextResponse.json({ error: message }, { status: 500 });
+  }
+}

+ 66 - 0
src/features/subscription/Actions/subscriptionActions.ts

@@ -0,0 +1,66 @@
+"use server";
+
+import Stripe from "stripe";
+import { withAuth } from "@/lib/with-auth";
+import { db } from "@/lib/db";
+
+let stripeClient: Stripe | null = null;
+
+function getStripe(): Stripe {
+  if (!stripeClient) {
+    const key = process.env.STRIPE_SECRET_KEY;
+    if (!key) throw new Error("STRIPE_SECRET_KEY is not configured");
+    stripeClient = new Stripe(key);
+  }
+  return stripeClient;
+}
+
+export async function cancelSubscription() {
+  return withAuth(async ({ organizationId }) => {
+    const subscription = await db.subscription.findUnique({
+      where: { organizationId },
+    });
+
+    if (!subscription?.stripeSubscriptionId) {
+      throw new Error("No active subscription found");
+    }
+
+    const stripe = getStripe();
+
+    await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
+      cancel_at_period_end: true,
+    });
+
+    await db.subscription.update({
+      where: { organizationId },
+      data: { cancelAtPeriodEnd: true },
+    });
+
+    return { cancelAtPeriodEnd: true };
+  });
+}
+
+export async function resumeSubscription() {
+  return withAuth(async ({ organizationId }) => {
+    const subscription = await db.subscription.findUnique({
+      where: { organizationId },
+    });
+
+    if (!subscription?.stripeSubscriptionId) {
+      throw new Error("No active subscription found");
+    }
+
+    const stripe = getStripe();
+
+    await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
+      cancel_at_period_end: false,
+    });
+
+    await db.subscription.update({
+      where: { organizationId },
+      data: { cancelAtPeriodEnd: false },
+    });
+
+    return { cancelAtPeriodEnd: false };
+  });
+}

+ 355 - 74
src/features/subscription/Components/subscription-settings.tsx

@@ -2,6 +2,8 @@
 
 import { useState } from "react";
 import { toast } from "sonner";
+import { useTranslations } from "next-intl";
+import { useRouter } from "next/navigation";
 import { Button } from "@/components/ui/button";
 import { Badge } from "@/components/ui/badge";
 import { Label } from "@/components/ui/label";
@@ -12,20 +14,65 @@ import {
   CardHeader,
   CardTitle,
 } from "@/components/ui/card";
-import { Crown, Loader2, Shield, Zap } from "lucide-react";
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+  AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import {
+  Check,
+  Crown,
+  CreditCard,
+  Loader2,
+  Shield,
+  X,
+  Zap,
+  AlertTriangle,
+} from "lucide-react";
+import { cancelSubscription, resumeSubscription } from "@/features/subscription/Actions/subscriptionActions";
+import type { PlanFeatures } from "@/lib/features";
+
+type Props = {
+  plan: string;
+  status: string | null;
+  cancelAtPeriodEnd: boolean;
+  currentPeriodEnd: string | null;
+  currentPeriodStart: string | null;
+  planPrice: number;
+  planInterval: string;
+  hasStripeCustomer: boolean;
+  usage: { customers: number; members: number };
+  features: PlanFeatures;
+};
 
 export function SubscriptionSettings({
   plan,
   status,
   cancelAtPeriodEnd,
   currentPeriodEnd,
-}: {
-  plan: string;
-  status: string | null;
-  cancelAtPeriodEnd: boolean;
-  currentPeriodEnd: string | null;
-}) {
+  currentPeriodStart,
+  planPrice,
+  planInterval,
+  hasStripeCustomer,
+  usage,
+  features,
+}: Props) {
+  const t = useTranslations("settings");
+  const router = useRouter();
   const [checkoutLoading, setCheckoutLoading] = useState<"pro" | "enterprise" | null>(null);
+  const [cancelLoading, setCancelLoading] = useState(false);
+  const [resumeLoading, setResumeLoading] = useState(false);
+  const [billingLoading, setBillingLoading] = useState(false);
+
+  const isPaid = plan === "pro" || plan === "enterprise";
+  const isCanceling = cancelAtPeriodEnd && status === "active";
+  const isPastDue = status === "past_due";
 
   const handleCheckout = async (selectedPlan: "pro" | "enterprise") => {
     setCheckoutLoading(selectedPlan);
@@ -48,89 +95,299 @@ export function SubscriptionSettings({
     }
   };
 
+  const handleCancel = async () => {
+    setCancelLoading(true);
+    const result = await cancelSubscription();
+    if (result.success) {
+      toast.success(t("subscription.cancelSuccess"));
+      router.refresh();
+    } else {
+      toast.error(result.error ?? t("subscription.cancelError"));
+    }
+    setCancelLoading(false);
+  };
+
+  const handleResume = async () => {
+    setResumeLoading(true);
+    const result = await resumeSubscription();
+    if (result.success) {
+      toast.success(t("subscription.resumeSuccess"));
+      router.refresh();
+    } else {
+      toast.error(result.error ?? t("subscription.resumeError"));
+    }
+    setResumeLoading(false);
+  };
+
+  const handleBillingPortal = async () => {
+    setBillingLoading(true);
+    try {
+      const res = await fetch("/api/protected/subscription/billing-portal", {
+        method: "POST",
+      });
+      const data = await res.json();
+      if (data.url) {
+        window.location.href = data.url;
+      } else {
+        toast.error(data.error ?? t("subscription.billingPortalError"));
+        setBillingLoading(false);
+      }
+    } catch {
+      toast.error(t("subscription.billingPortalError"));
+      setBillingLoading(false);
+    }
+  };
+
+  const formatDate = (iso: string | null) => {
+    if (!iso) return "—";
+    return new Date(iso).toLocaleDateString(undefined, {
+      year: "numeric",
+      month: "long",
+      day: "numeric",
+    });
+  };
+
+  const intervalLabel = planInterval === "month" ? t("subscription.perMonth") : t("subscription.perYear");
+
+  const planIcon = plan === "enterprise" ? (
+    <Crown className="h-5 w-5" />
+  ) : plan === "pro" ? (
+    <Zap className="h-5 w-5" />
+  ) : (
+    <Shield className="h-5 w-5" />
+  );
+
+  const planBadge = plan === "enterprise" ? (
+    <Badge className="bg-purple-600 hover:bg-purple-700">{t("subscription.planEnterprise")}</Badge>
+  ) : plan === "pro" ? (
+    <Badge className="bg-blue-600 hover:bg-blue-700">{t("subscription.planPro")}</Badge>
+  ) : (
+    <Badge variant="secondary">{t("subscription.planFree")}</Badge>
+  );
+
+  const statusBadge = isPastDue ? (
+    <Badge variant="destructive">{t("subscription.statusPastDue")}</Badge>
+  ) : isCanceling ? (
+    <Badge variant="outline" className="border-yellow-500 text-yellow-600">
+      {t("subscription.statusCanceling")}
+    </Badge>
+  ) : status === "active" ? (
+    <Badge variant="outline" className="border-green-500 text-green-600">
+      {t("subscription.statusActive")}
+    </Badge>
+  ) : null;
+
+  const featureRows: { label: string; type: "usage" | "boolean"; value: boolean | string }[] = [
+    {
+      label: t("subscription.featureCustomers"),
+      type: "usage",
+      value: features.maxCustomers >= 999999
+        ? t("subscription.usageUnlimited", { used: String(usage.customers) })
+        : t("subscription.usageOf", { used: String(usage.customers), limit: String(features.maxCustomers) }),
+    },
+    {
+      label: t("subscription.featureTeamMembers"),
+      type: "usage",
+      value: features.maxUsers >= 999999
+        ? t("subscription.usageUnlimited", { used: String(usage.members) })
+        : t("subscription.usageOf", { used: String(usage.members), limit: String(features.maxUsers) }),
+    },
+    { label: t("subscription.featureSmtp"), type: "boolean", value: features.smtp },
+    { label: t("subscription.featureApi"), type: "boolean", value: features.api },
+    { label: t("subscription.featurePayments"), type: "boolean", value: features.payments },
+    { label: t("subscription.featureCustomFields"), type: "boolean", value: features.customFields },
+    { label: t("subscription.featureSms"), type: "boolean", value: features.sms },
+    { label: t("subscription.featureCustomerPortal"), type: "boolean", value: features.customerPortal },
+    { label: t("subscription.featureCustomTemplates"), type: "boolean", value: features.customTemplates },
+    { label: t("subscription.featureBrandingRemoved"), type: "boolean", value: features.brandingRemoved },
+  ];
+
   return (
     <div className="space-y-6">
+      {/* Card 1: Current Plan */}
       <Card>
         <CardHeader>
           <CardTitle className="flex items-center gap-2">
-            {plan === "enterprise" ? (
-              <Crown className="h-5 w-5" />
-            ) : plan === "pro" ? (
-              <Zap className="h-5 w-5" />
-            ) : (
-              <Shield className="h-5 w-5" />
-            )}
-            Subscription Plan
+            {planIcon}
+            {t("subscription.title")}
           </CardTitle>
-          <CardDescription>
-            Your organization&apos;s current Torqvoice subscription tier and available features.
-          </CardDescription>
+          <CardDescription>{t("subscription.description")}</CardDescription>
         </CardHeader>
         <CardContent className="space-y-4">
           <div className="flex items-center gap-3">
-            <Label>Current Plan:</Label>
-            {plan === "enterprise" ? (
-              <Badge className="bg-purple-600 hover:bg-purple-700">Enterprise</Badge>
-            ) : plan === "pro" ? (
-              <Badge className="bg-blue-600 hover:bg-blue-700">Torq Pro</Badge>
-            ) : (
-              <Badge variant="secondary">Free</Badge>
-            )}
+            <Label>{t("subscription.currentPlan")}:</Label>
+            {planBadge}
+            {statusBadge}
           </div>
 
-          {status === "past_due" && (
-            <p className="text-sm text-destructive">
-              Your payment is past due. Please update your payment method to avoid service interruption.
-            </p>
+          {isPaid && planPrice > 0 && (
+            <div className="text-sm text-muted-foreground">
+              ${planPrice}/{intervalLabel}
+            </div>
           )}
 
-          {cancelAtPeriodEnd && currentPeriodEnd && (
-            <p className="text-sm text-muted-foreground">
-              Your subscription will cancel at the end of the current period ({new Date(currentPeriodEnd).toLocaleDateString()}).
-            </p>
+          {isPaid && currentPeriodStart && currentPeriodEnd && (
+            <div className="flex flex-col gap-1 text-sm">
+              <div className="flex items-center gap-2">
+                <span className="text-muted-foreground">{t("subscription.billingPeriod")}:</span>
+                <span>
+                  {t("subscription.billingPeriodDates", {
+                    start: formatDate(currentPeriodStart),
+                    end: formatDate(currentPeriodEnd),
+                  })}
+                </span>
+              </div>
+              {!isCanceling && (
+                <div className="flex items-center gap-2">
+                  <span className="text-muted-foreground">{t("subscription.nextRenewal")}:</span>
+                  <span>{formatDate(currentPeriodEnd)}</span>
+                </div>
+              )}
+            </div>
           )}
 
-          {plan === "free" && (
-            <div className="rounded-md border p-3 space-y-2">
-              <p className="text-sm text-muted-foreground">
-                Upgrade to unlock unlimited customers, team members, reports, SMTP, API access, and more.
+          {isPastDue && (
+            <div className="flex items-center gap-2 rounded-md border border-destructive/50 bg-destructive/10 p-3">
+              <AlertTriangle className="h-4 w-4 text-destructive shrink-0" />
+              <p className="text-sm text-destructive">
+                {t("subscription.pastDueWarning")}
               </p>
-              <div className="flex gap-2">
-                <Button
-                  variant="default"
-                  size="sm"
-                  onClick={() => handleCheckout("pro")}
-                  disabled={checkoutLoading !== null}
-                >
-                  {checkoutLoading === "pro" ? (
-                    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-                  ) : (
-                    <Zap className="mr-2 h-4 w-4" />
-                  )}
-                  Upgrade to Pro — $99/yr
-                </Button>
-                <Button
-                  variant="outline"
-                  size="sm"
-                  onClick={() => handleCheckout("enterprise")}
-                  disabled={checkoutLoading !== null}
-                >
-                  {checkoutLoading === "enterprise" ? (
-                    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-                  ) : (
-                    <Crown className="mr-2 h-4 w-4" />
-                  )}
-                  Enterprise — $140/yr
-                </Button>
-              </div>
             </div>
           )}
 
-          {plan === "pro" && (
-            <div className="rounded-md border p-3 space-y-2">
-              <p className="text-sm text-muted-foreground">
-                Need more than 5 team members? Upgrade to Enterprise for up to 50 users.
+          {isCanceling && currentPeriodEnd && (
+            <div className="flex items-center gap-2 rounded-md border border-yellow-500/50 bg-yellow-500/10 p-3">
+              <AlertTriangle className="h-4 w-4 text-yellow-600 shrink-0" />
+              <p className="text-sm text-yellow-600">
+                {t("subscription.cancelingWarning", { date: formatDate(currentPeriodEnd) })}
               </p>
+            </div>
+          )}
+        </CardContent>
+      </Card>
+
+      {/* Card 2: Plan Features */}
+      <Card>
+        <CardHeader>
+          <CardTitle>{t("subscription.featuresTitle")}</CardTitle>
+          <CardDescription>{t("subscription.featuresDescription")}</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
+            {featureRows.map((row) => (
+              <div
+                key={row.label}
+                className="flex items-center justify-between rounded-md border px-3 py-2"
+              >
+                <span className="text-sm">{row.label}</span>
+                {row.type === "usage" ? (
+                  <span className="text-sm text-muted-foreground">{row.value as string}</span>
+                ) : row.value ? (
+                  <div className="flex items-center gap-1 text-green-600">
+                    <Check className="h-4 w-4" />
+                    <span className="text-xs">{t("subscription.included")}</span>
+                  </div>
+                ) : (
+                  <div className="flex items-center gap-1 text-muted-foreground">
+                    <X className="h-4 w-4" />
+                    <span className="text-xs">{t("subscription.notIncluded")}</span>
+                  </div>
+                )}
+              </div>
+            ))}
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* Card 3: Manage Subscription */}
+      {isPaid && (
+        <Card>
+          <CardHeader>
+            <CardTitle>{t("subscription.manageTitle")}</CardTitle>
+          </CardHeader>
+          <CardContent className="flex flex-wrap gap-3">
+            {hasStripeCustomer && (
+              <Button
+                variant="outline"
+                onClick={handleBillingPortal}
+                disabled={billingLoading}
+              >
+                {billingLoading ? (
+                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                ) : (
+                  <CreditCard className="mr-2 h-4 w-4" />
+                )}
+                {t("subscription.manageBilling")}
+              </Button>
+            )}
+
+            {isCanceling ? (
+              <Button
+                variant="default"
+                onClick={handleResume}
+                disabled={resumeLoading}
+              >
+                {resumeLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+                {t("subscription.resumeSubscription")}
+              </Button>
+            ) : (
+              status === "active" && !cancelAtPeriodEnd && (
+                <AlertDialog>
+                  <AlertDialogTrigger asChild>
+                    <Button variant="destructive">
+                      {t("subscription.cancelSubscription")}
+                    </Button>
+                  </AlertDialogTrigger>
+                  <AlertDialogContent>
+                    <AlertDialogHeader>
+                      <AlertDialogTitle>{t("subscription.cancelDialogTitle")}</AlertDialogTitle>
+                      <AlertDialogDescription>
+                        {t("subscription.cancelDialogDescription", {
+                          date: formatDate(currentPeriodEnd),
+                        })}
+                      </AlertDialogDescription>
+                    </AlertDialogHeader>
+                    <AlertDialogFooter>
+                      <AlertDialogCancel>{t("subscription.cancelDialogCancel")}</AlertDialogCancel>
+                      <AlertDialogAction
+                        variant="destructive"
+                        onClick={handleCancel}
+                        disabled={cancelLoading}
+                      >
+                        {cancelLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+                        {t("subscription.cancelDialogConfirm")}
+                      </AlertDialogAction>
+                    </AlertDialogFooter>
+                  </AlertDialogContent>
+                </AlertDialog>
+              )
+            )}
+          </CardContent>
+        </Card>
+      )}
+
+      {/* Upgrade Section */}
+      {plan === "free" && (
+        <Card>
+          <CardHeader>
+            <CardTitle>{t("subscription.upgradeTitle")}</CardTitle>
+            <CardDescription>{t("subscription.upgradeToProDescription")}</CardDescription>
+          </CardHeader>
+          <CardContent>
+            <div className="flex gap-2">
+              <Button
+                variant="default"
+                size="sm"
+                onClick={() => handleCheckout("pro")}
+                disabled={checkoutLoading !== null}
+              >
+                {checkoutLoading === "pro" ? (
+                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                ) : (
+                  <Zap className="mr-2 h-4 w-4" />
+                )}
+                {t("subscription.upgradeToPro")}
+              </Button>
               <Button
                 variant="outline"
                 size="sm"
@@ -142,12 +399,36 @@ export function SubscriptionSettings({
                 ) : (
                   <Crown className="mr-2 h-4 w-4" />
                 )}
-                Upgrade to Enterprise — $140/yr
+                {t("subscription.upgradeToEnterprise")}
               </Button>
             </div>
-          )}
-        </CardContent>
-      </Card>
+          </CardContent>
+        </Card>
+      )}
+
+      {plan === "pro" && (
+        <Card>
+          <CardHeader>
+            <CardTitle>{t("subscription.upgradeTitle")}</CardTitle>
+            <CardDescription>{t("subscription.upgradeToEnterpriseDescription")}</CardDescription>
+          </CardHeader>
+          <CardContent>
+            <Button
+              variant="outline"
+              size="sm"
+              onClick={() => handleCheckout("enterprise")}
+              disabled={checkoutLoading !== null}
+            >
+              {checkoutLoading === "enterprise" ? (
+                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+              ) : (
+                <Crown className="mr-2 h-4 w-4" />
+              )}
+              {t("subscription.upgradeToEnterprise")}
+            </Button>
+          </CardContent>
+        </Card>
+      )}
     </div>
   );
 }