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).
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:
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_xxxCambiar 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: truemuestra el badge "MAS POPULAR" con estilo primarioisFeatured: truemuestra 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>