EmpiezaTuSaaS

Pricing

Componente de planes de precios con Stripe

Pricing

El boilerplate incluye un componente de precios completo que lee los planes de config/stripe.ts y usa CheckoutButton para pagos reales con Stripe.

Componente Pricing de la landing

El componente Pricing en components/landing/pricing.tsx muestra los 3 planes configurados en config/stripe.ts. Los planes son de pago unico (no suscripcion).

components/landing/pricing.tsx
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
import { pricingPlans } from "@/config/stripe";
import { formatPrice } from "@/lib/utils";
import { CheckoutButton } from "@/components/checkout-button";

export function Pricing() {
  return (
    <section id="pricing" className="py-20">
      <div className="container mx-auto max-w-6xl px-4">
        <div className="text-center mb-16">
          <p className="text-sm font-medium text-muted-foreground uppercase tracking-widest mb-4">
            Precios
          </p>
          <h2 className="text-3xl md:text-4xl font-bold mb-4">
            Invierte una vez, lanza para siempre
          </h2>
          <p className="text-muted-foreground">
            Sin suscripciones. Pago único. Acceso inmediato al código.
          </p>
        </div>

        <div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
          {pricingPlans.map((plan) => (
            <Card
              key={plan.id}
              className={`relative flex flex-col ${
                plan.isPopular
                  ? "border-primary shadow-lg shadow-primary/10"
                  : ""
              }`}
            >
              {plan.isPopular && (
                <div className="absolute -top-3 left-1/2 -translate-x-1/2">
                  <Badge className="px-4 py-1 text-xs font-semibold">
                    MÁS POPULAR
                  </Badge>
                </div>
              )}
              {plan.isFeatured && (
                <div className="absolute -top-3 left-1/2 -translate-x-1/2">
                  <Badge variant="secondary" className="px-4 py-1 text-xs font-semibold">
                    MEJOR VALOR
                  </Badge>
                </div>
              )}

              <CardHeader className="pb-0">
                <h3 className="text-lg font-semibold">{plan.name}</h3>
                <p className="text-sm text-muted-foreground mt-1">{plan.description}</p>
                <div className="flex items-baseline gap-1 mt-4">
                  <span className="text-4xl font-bold">
                    {formatPrice(plan.price, plan.currency)}
                  </span>
                  <span className="text-muted-foreground text-sm">
                    {plan.interval === "one_time" ? "pago único" : `/${plan.interval}`}
                  </span>
                </div>
              </CardHeader>

              <CardContent className="flex-1 pt-6">
                <ul className="space-y-3">
                  {plan.features.map((feature, i) => (
                    <li key={i} className="flex items-start gap-2.5 text-sm">
                      <Check className="h-4 w-4 text-green-500 mt-0.5 shrink-0" />
                      <span>{feature}</span>
                    </li>
                  ))}
                </ul>
              </CardContent>

              <CardFooter>
                <CheckoutButton
                  priceId={plan.priceId}
                  planName={plan.name}
                  variant={plan.isPopular ? "default" : "outline"}
                >
                  {plan.cta}
                </CheckoutButton>
              </CardFooter>
            </Card>
          ))}
        </div>

        <p className="text-center text-sm text-muted-foreground mt-8">
          Pago seguro con Stripe. Garantía de devolución de 7 días.
        </p>
      </div>
    </section>
  );
}

Configuración de planes

Los planes se definen en config/stripe.ts:

config/stripe.ts
export interface PricingPlan {
  id: string;
  name: string;
  description: string;
  price: number;
  currency: string;
  interval: "month" | "year" | "one_time";
  priceId: string;
  features: string[];
  isPopular?: boolean;
  isFeatured?: boolean;
  cta: string;
  mode: "subscription" | "payment";
}

export const pricingPlans: PricingPlan[] = [
  {
    id: "starter",
    name: "Starter",
    description: "La base esencial para lanzar y personalizar tu SaaS",
    price: 49,
    currency: "EUR",
    interval: "one_time",
    priceId: process.env.STRIPE_PRICE_ID_STARTER || "",
    mode: "payment",
    cta: "Empezar ahora",
    features: [
      "Next.js 16 + React 19",
      "Autenticación completa (Better Auth)",
      "Base de datos PostgreSQL + Prisma",
      "Pagos con Stripe integrados",
      "Emails transaccionales con Resend",
      "Landing page con shadcn/ui",
      "Blog MDX con RSS, Atom y JSON Feed",
      "SEO técnico: sitemap, robots y JSON-LD",
    ],
  },
  {
    id: "pro",
    name: "Pro",
    description: "Para lanzar con panel interno, contenido y operaciones listas",
    price: 99,
    currency: "EUR",
    interval: "one_time",
    priceId: process.env.STRIPE_PRICE_ID_PRO || "",
    mode: "payment",
    isPopular: true,
    cta: "Obtener Pro",
    features: [
      "Todo lo de Starter",
      "Panel de administración",
      "Gestión de usuarios y sesiónes",
      "Página de preventa y waitlist",
      "Portal de cliente de Stripe",
      "Plantillas de email editables",
      "Documentación de arranque en español",
      "Base lista para personalizar y desplegar",
    ],
  },
  {
    id: "lifetime",
    name: "Lifetime",
    description: "El paquete completo del boilerplate para usarlo sin límites",
    price: 199,
    currency: "EUR",
    interval: "one_time",
    priceId: process.env.STRIPE_PRICE_ID_LIFETIME || "",
    mode: "payment",
    isFeatured: true,
    cta: "Acceso de por vida",
    features: [
      "Todo lo de Pro",
      "Acceso completo al código fuente",
      "Activos de branding y SEO incluidos",
      "Checklist de lanzamiento",
      "Seed opcional para demos locales",
      "Workflow CI básico incluido",
      "Preparado para extenderse sin lock-in",
    ],
  },
];

Personalizar planes

Cambiar precios y features

Edita config/stripe.ts. Asegúrate de crear los Price IDs correspondientes en tu Dashboard de Stripe y configurar las variables de entorno:

STRIPE_PRICE_ID_STARTER=price_xxx
STRIPE_PRICE_ID_PRO=price_xxx
STRIPE_PRICE_ID_LIFETIME=price_xxx

Cambiar a suscripciones

Si quieres usar suscripciones en vez de pagos únicos, cambia interval y mode:

{
  id: "pro",
  name: "Pro",
  price: 29,
  interval: "month",       // "month" o "year"
  mode: "subscription",    // en vez de "payment"
  // ...
}

Cambiar badges

  • isPopular: true muestra el badge "MAS POPULAR" con estilo primario
  • isFeatured: true muestra el badge "MEJOR VALOR" con estilo secondary

Formato de precio

El componente usa formatPrice() de lib/utils.ts que formatea el precio con la moneda adecuada:

import { formatPrice } from "@/lib/utils";

formatPrice(99, "EUR");  // → "99 €"
formatPrice(49);         // → "49 €" (EUR por defecto)

CheckoutButton

El boton de checkout (components/checkout-button.tsx) hace un POST a /api/stripe/checkout con el priceId y planName, y redirige al usuario a la sesión de checkout de Stripe. Muestra estado de carga con spinner y maneja errores con toast de Sonner.

<CheckoutButton
  priceId="price_xxx"
  planName="Pro"
  variant="default"      // variante del boton
  className="w-full"     // clases adicionales
>
  Obtener Pro
</CheckoutButton>

On this page