feat: Fase 2 - Integração Stripe
- Biblioteca Stripe configurada - API de checkout para criar sessões de pagamento - Webhook para processar eventos (subscription created/updated/deleted) - Página de planos com os 3 níveis (Essencial R97, Completo R97, Intensivo R97) - Páginas de sucesso e cancelamento - Efeito de confete na confirmação
This commit is contained in:
53
package-lock.json
generated
53
package-lock.json
generated
@@ -10,18 +10,22 @@
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^2.11.1",
|
||||
"@prisma/client": "^6.19.2",
|
||||
"@stripe/stripe-js": "^8.7.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"framer-motion": "^12.33.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "16.1.6",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"prisma": "^6.19.2",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
"react-dom": "19.2.3",
|
||||
"stripe": "^20.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
@@ -1376,6 +1380,15 @@
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stripe/stripe-js": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.7.0.tgz",
|
||||
"integrity": "sha512-tNUerSstwNC1KuHgX4CASGO0Md3CB26IJzSXmVlSuFvhsBP4ZaEPpY4jxWOn9tfdDscuVT4Kqb8cZ2o9nLCgRQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@@ -1677,6 +1690,13 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/canvas-confetti": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz",
|
||||
"integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -1702,7 +1722,7 @@
|
||||
"version": "20.19.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz",
|
||||
"integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@@ -2748,6 +2768,16 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvas-confetti": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz",
|
||||
"integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==",
|
||||
"license": "ISC",
|
||||
"funding": {
|
||||
"type": "donate",
|
||||
"url": "https://www.paypal.me/kirilvatev"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -6585,6 +6615,23 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "20.3.1",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-20.3.1.tgz",
|
||||
"integrity": "sha512-k990yOT5G5rhX3XluRPw5Y8RLdJDW4dzQ29wWT66piHrbnM2KyamJ1dKgPsw4HzGHRWjDiSSdcI2WdxQUPV3aQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=16"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||
@@ -6922,7 +6969,7 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unrs-resolver": {
|
||||
|
||||
@@ -11,18 +11,22 @@
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^2.11.1",
|
||||
"@prisma/client": "^6.19.2",
|
||||
"@stripe/stripe-js": "^8.7.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"framer-motion": "^12.33.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "16.1.6",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"prisma": "^6.19.2",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
"react-dom": "19.2.3",
|
||||
"stripe": "^20.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
||||
103
src/app/api/checkout/route.ts
Normal file
103
src/app/api/checkout/route.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { stripe, PLANS, PlanId } from '@/lib/stripe';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Não autenticado' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { planId } = body as { planId: PlanId };
|
||||
|
||||
if (!planId || !PLANS[planId]) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Plano inválido' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const plan = PLANS[planId];
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: { subscription: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Usuário não encontrado' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Criar ou recuperar customer no Stripe
|
||||
let customerId = user.subscription?.stripeCustomerId;
|
||||
|
||||
if (!customerId) {
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
customerId = customer.id;
|
||||
|
||||
// Atualizar subscription com customerId
|
||||
await prisma.subscription.upsert({
|
||||
where: { userId: user.id },
|
||||
update: { stripeCustomerId: customerId },
|
||||
create: {
|
||||
userId: user.id,
|
||||
plan: planId,
|
||||
status: 'pending',
|
||||
stripeCustomerId: customerId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Criar sessão de checkout
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: 'brl',
|
||||
product_data: {
|
||||
name: `Íris TEA - Plano ${plan.name}`,
|
||||
description: plan.description,
|
||||
},
|
||||
unit_amount: plan.price,
|
||||
recurring: {
|
||||
interval: 'month',
|
||||
},
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: 'subscription',
|
||||
success_url: `${process.env.AUTH_URL}/checkout/sucesso?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${process.env.AUTH_URL}/checkout/cancelado`,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
planId,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ url: checkoutSession.url });
|
||||
} catch (error) {
|
||||
console.error('Erro no checkout:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erro ao criar sessão de checkout' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
124
src/app/api/webhooks/stripe/route.ts
Normal file
124
src/app/api/webhooks/stripe/route.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.text();
|
||||
const signature = request.headers.get('stripe-signature')!;
|
||||
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
body,
|
||||
signature,
|
||||
process.env.STRIPE_WEBHOOK_SECRET!
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.error('Webhook signature verification failed:', err.message);
|
||||
return NextResponse.json(
|
||||
{ error: 'Webhook signature verification failed' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed': {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
const userId = session.metadata?.userId;
|
||||
const planId = session.metadata?.planId;
|
||||
|
||||
if (userId && planId) {
|
||||
await prisma.subscription.update({
|
||||
where: { userId },
|
||||
data: {
|
||||
status: 'active',
|
||||
plan: planId,
|
||||
stripeSubId: session.subscription as string,
|
||||
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // +30 dias
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.updated': {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const customerId = subscription.customer as string;
|
||||
|
||||
const sub = await prisma.subscription.findFirst({
|
||||
where: { stripeCustomerId: customerId },
|
||||
});
|
||||
|
||||
if (sub) {
|
||||
// @ts-expect-error - Stripe types may vary
|
||||
const periodEnd = subscription.current_period_end || subscription.currentPeriodEnd;
|
||||
await prisma.subscription.update({
|
||||
where: { id: sub.id },
|
||||
data: {
|
||||
status: subscription.status === 'active' ? 'active' : 'cancelled',
|
||||
currentPeriodEnd: periodEnd ? new Date(periodEnd * 1000) : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'customer.subscription.deleted': {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const customerId = subscription.customer as string;
|
||||
|
||||
await prisma.subscription.updateMany({
|
||||
where: { stripeCustomerId: customerId },
|
||||
data: {
|
||||
status: 'cancelled',
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'invoice.payment_succeeded': {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const customerId = invoice.customer as string;
|
||||
|
||||
const sub = await prisma.subscription.findFirst({
|
||||
where: { stripeCustomerId: customerId },
|
||||
});
|
||||
|
||||
if (sub) {
|
||||
await prisma.subscription.update({
|
||||
where: { id: sub.id },
|
||||
data: {
|
||||
status: 'active',
|
||||
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'invoice.payment_failed': {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const customerId = invoice.customer as string;
|
||||
|
||||
await prisma.subscription.updateMany({
|
||||
where: { stripeCustomerId: customerId },
|
||||
data: {
|
||||
status: 'expired',
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true });
|
||||
} catch (error) {
|
||||
console.error('Webhook handler error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Webhook handler failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
64
src/app/checkout/cancelado/page.tsx
Normal file
64
src/app/checkout/cancelado/page.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Sparkles, XCircle, ArrowLeft, MessageCircle } from 'lucide-react';
|
||||
|
||||
export default function CheckoutCanceladoPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="p-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-xl gradient-rainbow flex items-center justify-center">
|
||||
<Sparkles className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gradient">Íris</span>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md text-center">
|
||||
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<XCircle className="w-12 h-12 text-gray-400" />
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-4">
|
||||
Pagamento cancelado
|
||||
</h1>
|
||||
|
||||
<p className="text-gray-600 mb-8">
|
||||
Você cancelou o processo de pagamento. Se foi por engano ou
|
||||
se precisar de ajuda, estamos aqui para você.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="block w-full bg-indigo-600 text-white py-3 rounded-xl font-medium hover:bg-indigo-700 transition"
|
||||
>
|
||||
Tentar novamente
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center justify-center gap-2 w-full text-gray-600 py-3 hover:text-gray-900 transition"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Voltar para o início
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 p-4 bg-indigo-50 rounded-xl">
|
||||
<p className="text-sm text-indigo-700 flex items-center justify-center gap-2">
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
Dúvidas? Fale conosco pelo WhatsApp
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
193
src/app/checkout/page.tsx
Normal file
193
src/app/checkout/page.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { Sparkles, Check, ArrowLeft, Loader2 } from 'lucide-react';
|
||||
|
||||
const plans = [
|
||||
{
|
||||
id: 'essencial',
|
||||
name: 'Essencial',
|
||||
price: 297,
|
||||
sessions: '2 sessões/mês',
|
||||
description: 'Para famílias iniciando a jornada',
|
||||
features: [
|
||||
'2 sessões de 50min com BCBA',
|
||||
'Assistente IA 24/7',
|
||||
'Relatórios básicos mensais',
|
||||
'Grupo de apoio online',
|
||||
],
|
||||
popular: false,
|
||||
},
|
||||
{
|
||||
id: 'completo',
|
||||
name: 'Completo',
|
||||
price: 497,
|
||||
sessions: '4 sessões/mês',
|
||||
description: 'O mais escolhido pelas famílias',
|
||||
features: [
|
||||
'4 sessões de 50min com BCBA',
|
||||
'Assistente IA 24/7 avançado',
|
||||
'Relatórios detalhados semanais',
|
||||
'Supervisão parental mensal',
|
||||
'Grupo de apoio online',
|
||||
'Materiais exclusivos',
|
||||
],
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: 'intensivo',
|
||||
name: 'Intensivo',
|
||||
price: 897,
|
||||
sessions: '8 sessões/mês',
|
||||
description: 'Para resultados acelerados',
|
||||
features: [
|
||||
'8 sessões de 50min com BCBA',
|
||||
'Assistente IA 24/7 premium',
|
||||
'Relatórios detalhados semanais',
|
||||
'Supervisão parental quinzenal',
|
||||
'Suporte prioritário WhatsApp',
|
||||
'Consultas emergenciais',
|
||||
'Materiais exclusivos',
|
||||
'Desconto em cursos',
|
||||
],
|
||||
popular: false,
|
||||
},
|
||||
];
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const router = useRouter();
|
||||
const { data: session, status } = useSession();
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
|
||||
const handleCheckout = async (planId: string) => {
|
||||
if (status === 'unauthenticated') {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(planId);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/checkout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ planId }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.url) {
|
||||
window.location.href = data.url;
|
||||
} else {
|
||||
throw new Error(data.error || 'Erro ao processar');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro no checkout:', error);
|
||||
alert('Erro ao processar pagamento. Tente novamente.');
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-indigo-50 to-white">
|
||||
{/* Header */}
|
||||
<header className="bg-white border-b border-gray-100">
|
||||
<div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-xl gradient-rainbow flex items-center justify-center">
|
||||
<Sparkles className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gradient">Íris</span>
|
||||
</Link>
|
||||
<Link href="/" className="flex items-center gap-2 text-gray-600 hover:text-gray-900">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Voltar
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-6xl mx-auto px-4 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Escolha seu plano
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
Todos os planos incluem acesso à nossa plataforma completa,
|
||||
suporte via chat e garantia de 7 dias.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={`bg-white rounded-2xl shadow-lg overflow-hidden ${
|
||||
plan.popular ? 'ring-2 ring-indigo-600 relative' : ''
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="bg-indigo-600 text-white text-center py-2 text-sm font-medium">
|
||||
Mais Popular
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900">{plan.name}</h3>
|
||||
<p className="text-gray-500 mt-1">{plan.description}</p>
|
||||
|
||||
<div className="mt-6">
|
||||
<span className="text-5xl font-bold text-gray-900">
|
||||
R$ {plan.price}
|
||||
</span>
|
||||
<span className="text-gray-500">/mês</span>
|
||||
</div>
|
||||
|
||||
<p className="text-indigo-600 font-medium mt-2">{plan.sessions}</p>
|
||||
|
||||
<button
|
||||
onClick={() => handleCheckout(plan.id)}
|
||||
disabled={loading !== null}
|
||||
className={`w-full mt-8 py-3 rounded-xl font-medium transition flex items-center justify-center gap-2 ${
|
||||
plan.popular
|
||||
? 'bg-indigo-600 text-white hover:bg-indigo-700'
|
||||
: 'bg-gray-100 text-gray-900 hover:bg-gray-200'
|
||||
} disabled:opacity-50`}
|
||||
>
|
||||
{loading === plan.id ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
'Começar agora'
|
||||
)}
|
||||
</button>
|
||||
|
||||
<ul className="mt-8 space-y-3">
|
||||
{plan.features.map((feature, i) => (
|
||||
<li key={i} className="flex items-start gap-3">
|
||||
<Check className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-gray-600">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Garantia */}
|
||||
<div className="mt-12 text-center">
|
||||
<div className="inline-flex items-center gap-2 bg-green-50 text-green-700 px-6 py-3 rounded-full">
|
||||
<Check className="w-5 h-5" />
|
||||
<span className="font-medium">Garantia de 7 dias ou seu dinheiro de volta</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
121
src/app/checkout/sucesso/page.tsx
Normal file
121
src/app/checkout/sucesso/page.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useEffect } from 'react';
|
||||
import { Sparkles, CheckCircle, ArrowRight, Calendar, MessageCircle, FileText } from 'lucide-react';
|
||||
import confetti from 'canvas-confetti';
|
||||
|
||||
export default function CheckoutSucessoPage() {
|
||||
useEffect(() => {
|
||||
// Efeito de confete ao carregar
|
||||
const duration = 3 * 1000;
|
||||
const end = Date.now() + duration;
|
||||
|
||||
const frame = () => {
|
||||
confetti({
|
||||
particleCount: 2,
|
||||
angle: 60,
|
||||
spread: 55,
|
||||
origin: { x: 0 },
|
||||
colors: ['#6366f1', '#8b5cf6', '#a855f7'],
|
||||
});
|
||||
confetti({
|
||||
particleCount: 2,
|
||||
angle: 120,
|
||||
spread: 55,
|
||||
origin: { x: 1 },
|
||||
colors: ['#6366f1', '#8b5cf6', '#a855f7'],
|
||||
});
|
||||
|
||||
if (Date.now() < end) {
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
};
|
||||
frame();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-green-50 to-white flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="p-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-xl gradient-rainbow flex items-center justify-center">
|
||||
<Sparkles className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gradient">Íris</span>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-2xl text-center">
|
||||
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<CheckCircle className="w-12 h-12 text-green-600" />
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Bem-vindo à Íris! 🎉
|
||||
</h1>
|
||||
|
||||
<p className="text-xl text-gray-600 mb-8">
|
||||
Sua assinatura foi confirmada com sucesso.
|
||||
Você já pode começar a usar todos os recursos da plataforma.
|
||||
</p>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-lg p-8 mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Próximos passos:
|
||||
</h2>
|
||||
<div className="space-y-4 text-left">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Calendar className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">Agende sua primeira sessão</h3>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Nossa equipe entrará em contato em até 24h para agendar sua primeira sessão com um BCBA.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<MessageCircle className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">Explore o chat com IA</h3>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Use nossa assistente para tirar dúvidas e receber orientações 24 horas por dia.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<FileText className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">Complete o perfil</h3>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Quanto mais informações tivermos, melhor poderemos personalizar o atendimento.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="inline-flex items-center gap-2 bg-indigo-600 text-white px-8 py-4 rounded-full text-lg font-medium hover:bg-indigo-700 transition"
|
||||
>
|
||||
Acessar meu Dashboard
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
src/lib/stripe.ts
Normal file
54
src/lib/stripe.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
// @ts-expect-error - Use latest API version
|
||||
apiVersion: '2024-12-18.acacia',
|
||||
typescript: true,
|
||||
});
|
||||
|
||||
export const PLANS = {
|
||||
essencial: {
|
||||
name: 'Essencial',
|
||||
price: 29700, // em centavos (R$ 297)
|
||||
sessions: 2,
|
||||
description: '2 sessões de teleterapia/mês + Chat IA 24/7',
|
||||
features: [
|
||||
'2 sessões de 50min com BCBA',
|
||||
'Assistente IA 24/7',
|
||||
'Relatórios básicos mensais',
|
||||
'Grupo de apoio online',
|
||||
],
|
||||
},
|
||||
completo: {
|
||||
name: 'Completo',
|
||||
price: 49700, // R$ 497
|
||||
sessions: 4,
|
||||
description: '4 sessões de teleterapia/mês + Supervisão + Chat IA 24/7',
|
||||
features: [
|
||||
'4 sessões de 50min com BCBA',
|
||||
'Assistente IA 24/7 avançado',
|
||||
'Relatórios detalhados semanais',
|
||||
'Supervisão parental mensal',
|
||||
'Grupo de apoio online',
|
||||
'Materiais exclusivos',
|
||||
],
|
||||
},
|
||||
intensivo: {
|
||||
name: 'Intensivo',
|
||||
price: 89700, // R$ 897
|
||||
sessions: 8,
|
||||
description: '8 sessões de teleterapia/mês + Supervisão + Suporte prioritário',
|
||||
features: [
|
||||
'8 sessões de 50min com BCBA',
|
||||
'Assistente IA 24/7 premium',
|
||||
'Relatórios detalhados semanais',
|
||||
'Supervisão parental quinzenal',
|
||||
'Suporte prioritário WhatsApp',
|
||||
'Consultas emergenciais',
|
||||
'Materiais exclusivos',
|
||||
'Desconto em cursos',
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type PlanId = keyof typeof PLANS;
|
||||
Reference in New Issue
Block a user