From 356b99036815ddaf6ae9e0bf05178e46a2b443e9 Mon Sep 17 00:00:00 2001 From: Marcio Bevervanso Date: Mon, 15 Jun 2026 22:19:31 -0300 Subject: [PATCH] Checkout transparente Pix+Stripe e funis integrados - Adiciona pagina /checkout com todos os 4 planos, seletor bento, Pix (QR + polling) e cartao (Stripe redirect) - Modo locked: plano fixo em starter quando source=landing/funil/campanha - Offer.tsx e WhatsAppFunnel.tsx redirecionam para /checkout?plan=starter - Adiciona wooviService (criar cobranca e polling status via n8n) - Rota /checkout registrada no App Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 2 + src/components/Offer.tsx | 35 +- src/pages/Checkout.tsx | 625 +++++++++++++++++++++++++++++++++++ src/pages/WhatsAppFunnel.tsx | 64 +--- src/services/wooviService.ts | 98 ++++++ 5 files changed, 731 insertions(+), 93 deletions(-) create mode 100644 src/pages/Checkout.tsx create mode 100644 src/services/wooviService.ts diff --git a/src/App.tsx b/src/App.tsx index 5ec0875..5f7ef30 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import LandingPage from './pages/LandingPage'; import SuccessPage from './pages/SuccessPage'; import WhatsAppFunnel from './pages/WhatsAppFunnel'; +import Checkout from './pages/Checkout'; export default function App() { return ( @@ -10,6 +11,7 @@ export default function App() { } /> } /> } /> + } /> ); diff --git a/src/components/Offer.tsx b/src/components/Offer.tsx index 3e2ea20..64a0cc4 100644 --- a/src/components/Offer.tsx +++ b/src/components/Offer.tsx @@ -3,12 +3,6 @@ import { motion } from 'framer-motion'; import { Check, ShieldCheck, Gift, ArrowRight, Loader2 } from 'lucide-react'; import { cn } from '../lib/utils'; -function getCookie(name: string) { - const match = document.cookie.match( - new RegExp('(^| )' + name + '=([^;]+)') - ); - return match ? match[2] : ''; -} export default function Offer() { const benefits = [ @@ -41,34 +35,7 @@ export default function Offer() { setIsLoading(true); setError(''); - const fbp = getCookie('_fbp'); - const fbc = getCookie('_fbc'); - - try { - const res = await fetch('https://n8n.seureview.com.br/webhook/festa-magica-stripe', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - source: 'landing', - userEmail: email, - fbp: fbp || '', - fbc: fbc || '' - }), - }); - - const data = await res.json(); - if (data.success && data.url) { - window.location.href = data.url; - } else { - setError('Ocorreu um erro ao gerar o checkout. Tente novamente.'); - setIsLoading(false); - } - } catch (err) { - setError('Erro de conexão. Verifique sua internet e tente novamente.'); - setIsLoading(false); - } + window.location.href = `/checkout?plan=starter&source=landing&email=${encodeURIComponent(email)}`; }; return ( diff --git a/src/pages/Checkout.tsx b/src/pages/Checkout.tsx new file mode 100644 index 0000000..fbad855 --- /dev/null +++ b/src/pages/Checkout.tsx @@ -0,0 +1,625 @@ +import { useState, useEffect, useRef, type FormEvent } from 'react'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + ShieldCheck, Lock, QrCode, CreditCard, Copy, Check, + Loader2, Gift, Sparkles, ArrowRight, Star, Wand2, Zap, +} from 'lucide-react'; +import { cn } from '../lib/utils'; +import { createPixCharge, pollPixStatus, type PixCharge } from '../services/wooviService'; + +// ─── Planos (espelha src/utils/pricing.ts do app) ────────────────────────── +const PLANS = [ + { + id: 'starter', + name: 'Starter', + credits: 10, + price: 999, + display: 'R$9,99', + perImg: 'R$1,00/img', + priceId: 'price_1TT5lpLjQo8Mvvq1HdDD7paN', + planName: 'Festa Magica IA - Kit Inicial', + }, + { + id: 'popular', + name: 'Kit Festa', + credits: 30, + price: 1999, + display: 'R$19,99', + perImg: 'R$0,67/img', + priceId: 'price_1TT5kTLjQo8Mvvq1Tus1RS5D', + planName: 'Festa Magica IA - Kit Festa', + badge: 'Mais vendido', + }, + { + id: 'pro', + name: 'Festa VIP', + credits: 80, + price: 4999, + display: 'R$49,99', + perImg: 'R$0,62/img', + priceId: 'price_1THThELjQo8Mvvq1uXkgh5jB', + planName: 'Festa Magica IA - Festa VIP', + }, + { + id: 'professional', + name: 'Profissional', + credits: 200, + price: 9999, + display: 'R$99,99', + perImg: 'R$0,50/img', + priceId: 'price_1TT5i9LjQo8Mvvq12yhPqe1G', + planName: 'Festa Magica IA - Profissional', + }, +] as const; + +type Plan = (typeof PLANS)[number]; +type Method = 'pix' | 'card'; + +const STRIPE_URL = 'https://n8n.seureview.com.br/webhook/festa-magica-stripe'; + +function getCookie(name: string) { + const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + return match ? match[2] : ''; +} + +export default function Checkout() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const source = searchParams.get('source') || 'landing'; + const userId = searchParams.get('userId') || ''; + const locked = searchParams.get('lock') === '1' || ['landing', 'funil', 'campanha'].includes(searchParams.get('source') || ''); + const [email, setEmail] = useState(searchParams.get('email') || ''); + const [method, setMethod] = useState('pix'); + const [error, setError] = useState(''); + + // Plano inicial vindo da URL (?plan=popular) ou padrão = starter + const initialPlan = PLANS.find(p => p.id === searchParams.get('plan')) ?? PLANS[0]; + const [plan, setPlan] = useState(initialPlan); + + // ── Pix ────────────────────────────────────────────────────────────────── + const [pixLoading, setPixLoading] = useState(false); + const [pix, setPix] = useState(null); + const [copied, setCopied] = useState(false); + const [confirming, setConfirming] = useState(false); + const pollRef = useRef | null>(null); + + // ── Cartão ──────────────────────────────────────────────────────────────── + const [cardLoading, setCardLoading] = useState(false); + + // ── Skeleton de entrada ─────────────────────────────────────────────────── + const [ready, setReady] = useState(false); + useEffect(() => { + const t = setTimeout(() => setReady(true), 1200); + return () => clearTimeout(t); + }, []); + + useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []); + + // Resetar Pix ao trocar de plano + useEffect(() => { + if (pollRef.current) clearInterval(pollRef.current); + setPix(null); + setConfirming(false); + setError(''); + }, [plan.id]); + + const validEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + + const trackEvent = () => { + if (typeof window !== 'undefined' && 'fbq' in window) { + (window as any).fbq('track', 'InitiateCheckout'); + } + }; + + // ── Pix ────────────────────────────────────────────────────────────────── + const handlePix = async (e?: FormEvent) => { + e?.preventDefault(); + if (!validEmail) { setError('Informe um e-mail válido para continuar.'); return; } + setError(''); + setPixLoading(true); + trackEvent(); + try { + const charge = await createPixCharge({ + userEmail: email, + source, + userId, + value: plan.price, + credits: plan.credits, + planName: plan.planName, + priceId: plan.priceId, + fbp: getCookie('_fbp'), + fbc: getCookie('_fbc'), + }); + setPix(charge); + startPolling(charge.correlationID); + } catch (err: any) { + setError(err.message || 'Não foi possível gerar o Pix. Tente novamente.'); + } finally { + setPixLoading(false); + } + }; + + const startPolling = (correlationID: string) => { + setConfirming(true); + if (pollRef.current) clearInterval(pollRef.current); + pollRef.current = setInterval(async () => { + try { + const status = await pollPixStatus(correlationID); + if (status === 'COMPLETED') { + clearInterval(pollRef.current!); + navigate('/sucesso?status=approved', { replace: true }); + } else if (status === 'EXPIRED') { + clearInterval(pollRef.current!); + setConfirming(false); + setPix(null); + setError('O Pix expirou. Gere um novo código para pagar.'); + } + } catch { /* silencia; tenta no próximo tick */ } + }, 4000); + }; + + const handleCopy = async () => { + if (!pix) return; + try { + await navigator.clipboard.writeText(pix.brCode); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { setError('Não consegui copiar. Copie o código manualmente.'); } + }; + + // ── Cartão ──────────────────────────────────────────────────────────────── + const handleCard = async (e?: FormEvent) => { + e?.preventDefault(); + if (!validEmail) { setError('Informe um e-mail válido para continuar.'); return; } + setError(''); + setCardLoading(true); + trackEvent(); + try { + const res = await fetch(STRIPE_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source, + userEmail: email, + userId, + priceId: plan.priceId, + credits: plan.credits, + planName: plan.planName, + fbp: getCookie('_fbp'), + fbc: getCookie('_fbc'), + }), + }); + const data = await res.json(); + if (data.success && data.url) { + window.location.href = data.url; + } else { + setCardLoading(false); + setError('Ocorreu um erro ao abrir o pagamento. Tente novamente.'); + } + } catch { + setCardLoading(false); + setError('Erro de conexão. Verifique sua internet e tente novamente.'); + } + }; + + const features = [ + 'Personagem 3D exclusivo criado com IA', + `${plan.credits} créditos para itens do catálogo`, + 'Arquivos em PDF prontos para impressão', + 'Acesso imediato à plataforma', + ]; + + return ( +
+ + {/* ── Topo ─────────────────────────────────────────────────────── */} +
+
+
+
+ +
+ + Festa MágicaIA + +
+
+ + Ambiente seguro +
+
+
+ +
+ + {/* ── Skeleton ─────────────────────────────────────────────────────── */} + + {!ready && ( + + {/* Esquerda */} +
+ {!locked && ( +
+
+
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+
+ )} +
+
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+
+
+
+
+
+ {/* Direita */} +
+
+
+
+
+
+
+
+
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+
+ + )} + + + {/* ── Conteúdo real ────────────────────────────────────────────────── */} + + {ready && ( + +
+ + {/* ── Coluna esquerda: seletor de plano + resumo ───────────── */} +
+ + {/* Seletor de plano — oculto no modo locked (funil/campanha) */} + {!locked && ( + +

+ Escolha o plano +

+ +
+ {PLANS.map((p) => ( + + ))} +
+
+ )} + + {/* Resumo do pedido */} + +
+
+ +
+
+

Resumo

+

{plan.name}

+
+
+ +
    + {features.map((f, i) => ( +
  • + + + + {f} +
  • + ))} +
+ + {/* Primeira imagem grátis — só pro starter */} + {plan.id === 'starter' && ( +
+ +

+ Sua primeira imagem por nossa conta — não consome nenhum crédito. +

+
+ )} + + {/* Valor final */} +
+ Você paga hoje + {plan.display} +
+

Pagamento único · sem assinatura

+ + {/* Prova social */} +
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ +10.000 mães já fizeram a festa +
+
+
+ + {/* ── Coluna direita: pagamento ─────────────────────────────── */} + +

Pagamento

+ + {/* E-mail */} +
+ + { setEmail(e.target.value); if (error) setError(''); }} + placeholder="seu@email.com" + disabled={!!pix} + className="w-full h-11 px-4 rounded-xl border-2 border-gray-100 focus:border-pink-400 outline-none text-sm font-medium text-gray-900 transition-colors placeholder:text-gray-300 disabled:bg-gray-50 disabled:text-gray-400" + /> +

+ É aqui que enviamos o acesso ao painel. +

+
+ + {/* Método */} +
+ + +
+ + {/* Erro */} + + {error && ( + + {error} + + )} + + + {/* ── Pix ── */} + + {method === 'pix' && ( + + {!pix ? ( +
+ +

+ + QR Code gerado na hora · pague pelo app do banco +

+
+ ) : ( +
+
+ QR Code Pix +
+ +

+ Escaneie o QR Code ou use o Pix Copia e Cola: +

+ +
+ + +
+ + {confirming && ( +
+ + Aguardando confirmação do pagamento... +
+ )} +

+ Assim que o Pix cair, liberamos seu acesso. Pode deixar esta tela aberta. +

+
+ )} +
+ )} + + {/* ── Cartão ── */} + {method === 'card' && ( + +
+ +

+ + Você finaliza no ambiente seguro do Stripe +

+
+
+ )} +
+ + {/* ── Selos de confiança ── */} +
+
+ Pagamento 100% seguro e criptografado +
+
+ {[ + { icon: QrCode, label: 'Pix · Banco Central' }, + { icon: CreditCard, label: 'Cartão via Stripe' }, + { icon: Lock, label: 'SSL 256-bit' }, + { icon: ShieldCheck, label: 'Garantia de 7 dias' }, + ].map(({ icon: Icon, label }) => ( +
+ + {label} +
+ ))} +
+
+
+
+ + {/* Rodapé */} +

+ Ao prosseguir você concorda com os Termos de Uso e Política de Privacidade. + Garantia de reembolso integral em até 7 dias, sem perguntas. +

+
+ )} +
+
+
+ ); +} diff --git a/src/pages/WhatsAppFunnel.tsx b/src/pages/WhatsAppFunnel.tsx index 9c4b0c8..9bae13e 100644 --- a/src/pages/WhatsAppFunnel.tsx +++ b/src/pages/WhatsAppFunnel.tsx @@ -112,13 +112,6 @@ function loadSavedFunnelState() { return null; } -function getCookie(name: string) { - const match = document.cookie.match( - new RegExp('(^| )' + name + '=([^;]+)') - ); - return match ? match[2] : ''; -} - function playMessageSound() { try { const ctx = getAudioContext(); @@ -301,68 +294,21 @@ export default function WhatsAppFunnel() { id: 'loading-checkout', sender: 'bot', type: 'text', - content: 'Processando seu e-mail e gerando o link de pagamento ⏳...', + content: 'Perfeito! Abrindo o checkout seguro pra você 🎉', } ]); - + setCurrentRequireEmail(false); setIsLoadingCheckout(true); scrollToBottom(); - // Fire Meta Pixel event if (typeof window !== 'undefined' && 'fbq' in window) { (window as any).fbq('track', 'InitiateCheckout'); } - const fbp = getCookie('_fbp'); - const fbc = getCookie('_fbc'); - - try { - const res = await fetch('https://n8n.seureview.com.br/webhook/festa-magica-stripe', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - source: 'funil', - userEmail: emailInput, - fbp: fbp || '', - fbc: fbc || '' - }), - }); - - const data = await res.json(); - if (data.success && data.url) { - // Redireciona para o checkout do stripe - window.location.href = data.url; - } else { - setIsLoadingCheckout(false); - setCurrentRequireEmail(true); - setMessages((prev) => [ - ...prev.filter(m => m.id !== 'loading-checkout'), - { - id: Math.random().toString(), - sender: 'bot', - type: 'text', - content: 'Ops, tive um probleminha aqui pra gerar o seu link 😅 Pode tentar de novo?', - }, - ]); - scrollToBottom(); - } - } catch (err) { - setIsLoadingCheckout(false); - setCurrentRequireEmail(true); - setMessages((prev) => [ - ...prev.filter(m => m.id !== 'loading-checkout'), - { - id: Math.random().toString(), - sender: 'bot', - type: 'text', - content: 'Hmm, parece que a conexão falhou. Confere sua internet e manda de novo, por favor 🙏', - }, - ]); - scrollToBottom(); - } + setTimeout(() => { + window.location.href = `/checkout?plan=starter&source=funil&email=${encodeURIComponent(emailInput)}`; + }, 800); }; const renderMessageContent = (m: RenderedMessage) => { diff --git a/src/services/wooviService.ts b/src/services/wooviService.ts new file mode 100644 index 0000000..229ae6b --- /dev/null +++ b/src/services/wooviService.ts @@ -0,0 +1,98 @@ +// ============================================================ +// Festa Mágica IA — Woovi (Pix) Service +// ============================================================ +// Fluxo Pix transparente: +// Front → n8n (cria cobrança Woovi) → QR/copia-e-cola na tela +// Front faz polling do status via n8n até COMPLETED +// Webhook Woovi → n8n credita / cria usuário / magic link / e-mail +// +// IMPORTANTE: o App ID da Woovi NUNCA passa pelo browser. +// Tanto a criação da cobrança quanto a consulta de status são +// feitas server-side pelo n8n. O front só fala com o n8n. +// ============================================================ + +const N8N_BASE = 'https://n8n.seureview.com.br/webhook'; + +const PIX_CREATE_URL = `${N8N_BASE}/festa-magica-pix`; +const PIX_STATUS_URL = `${N8N_BASE}/festa-magica-pix-status`; + +export interface CreatePixParams { + userEmail: string; + source?: string; // 'landing' | 'funil' | 'default' + userId?: string; + value?: number; // em centavos BRL + credits?: number; + planName?: string; + priceId?: string; + fbp?: string; + fbc?: string; +} + +export interface PixCharge { + correlationID: string; + brCode: string; // código copia-e-cola + qrCodeImage: string; // URL da imagem do QR (PNG) + value: number; // em centavos + expiresDate?: string; +} + +export type PixStatus = 'ACTIVE' | 'COMPLETED' | 'EXPIRED'; + +/** + * Cria uma cobrança Pix na Woovi (via n8n) e retorna os dados + * para exibir o QR Code e o copia-e-cola na própria tela. + */ +export async function createPixCharge(params: CreatePixParams): Promise { + const response = await fetch(PIX_CREATE_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source: params.source || 'landing', + userEmail: params.userEmail, + userId: params.userId || '', + value: params.value, + credits: params.credits, + planName: params.planName, + priceId: params.priceId, + fbp: params.fbp || '', + fbc: params.fbc || '', + }), + }); + + if (!response.ok) { + throw new Error(`Erro ao gerar Pix: ${response.status}`); + } + + const data = await response.json(); + + if (!data.success || !data.brCode) { + throw new Error(data.error || 'Não foi possível gerar o Pix.'); + } + + return { + correlationID: data.correlationID, + brCode: data.brCode, + qrCodeImage: data.qrCodeImage, + value: data.value, + expiresDate: data.expiresDate, + }; +} + +/** + * Consulta o status de uma cobrança Pix (via n8n). + * Usado no polling enquanto o usuário paga. + */ +export async function pollPixStatus(correlationID: string): Promise { + const response = await fetch(PIX_STATUS_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ correlationID }), + }); + + if (!response.ok) { + throw new Error(`Erro ao consultar status do Pix: ${response.status}`); + } + + const data = await response.json(); + return (data.status as PixStatus) || 'ACTIVE'; +}