feat: migrate to single PRO plan and update Stripe Production Checkout

This commit is contained in:
Marcio Bevervanso 2026-04-13 18:52:37 -03:00
parent 2e28f9d27c
commit cce4743b71
5 changed files with 96 additions and 108 deletions

35
image-renderer/Dockerfile Normal file
View file

@ -0,0 +1,35 @@
# Usa a imagem oficial do Node.js baseada em Alpine (Leve mas precisa de libs do browser)
# Trocando pra Debian/Ubuntu leve por causa das dependencias complexas do Chrome
FROM node:18-slim
# Instalar dependências necessárias para rodar o Puppeteer (Chromium) no Linux
RUN apt-get update \
&& apt-get install -y wget gnupg \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Definir a variável de ambiente para forçar o Puppeteer a usar o Chrome instalado ao invés de baixar um próprio
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable
# Criar o diretório de trabalho da aplicação
WORKDIR /usr/src/app
# Copiar os arquivos de dependência
COPY package*.json ./
# Instalar as dependências do Node
RUN npm install
# Copiar o resto do código do servidor
COPY . .
# Expor a porta que a aplicação Express escuta
EXPOSE 3001
# Comando para iniciar o servidor
CMD [ "npm", "start" ]

View file

@ -0,0 +1,14 @@
# Configuração do Coolify Nixpacks
# O Coolify vai ler isso na raiz para o serviço específico "image-renderer"
preBuild:
- npm ci
start:
- node index.js
# Variáveis do Node / Chrome
envs:
- PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
- PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable
- NODE_ENV=production

View file

@ -41,64 +41,34 @@ const Pricing: React.FC<PricingProps> = ({ onRegister }) => {
</div>
</div>
<div className="grid lg:grid-cols-3 gap-8 lg:gap-14 max-w-7xl mx-auto items-center relative z-10">
{/* Plan: Monthly (Was Starter in structure, now Monthly) */}
<div className="bg-white rounded-3xl border border-gray-200 p-8 flex flex-col hover:border-gray-300 transition-colors lg:mt-12 hover:shadow-lg">
<h3 className="text-lg font-bold text-gray-900">{t.pricing.plans.monthly.title}</h3>
<div className="mt-4 mb-2 flex items-baseline gap-1">
<span className="text-4xl font-extrabold text-gray-900">{t.pricing.plans.monthly.price}</span>
<span className="text-gray-500 text-sm">{t.pricing.plans.monthly.period}</span>
</div>
<p className="text-gray-500 mb-6 text-sm">{t.pricing.plans.monthly.description}</p>
<p className="text-xs font-semibold text-gray-400 mb-6 uppercase tracking-wide">{t.pricing.plans.monthly.billingInfo}</p>
<ul className="space-y-4 mb-8 flex-grow">
{t.pricing.plans.monthly.features.map((item, i) => (
<li key={i} className="flex items-start gap-3 text-gray-600 text-sm">
<Check className="w-5 h-5 text-gray-400 shrink-0" strokeWidth={2} />
<span>{item}</span>
</li>
))}
</ul>
<button
onClick={() => onRegister('monthly')}
className="block w-full text-center bg-white border border-gray-200 hover:bg-gray-50 hover:border-gray-300 text-gray-900 font-semibold py-3.5 rounded-xl transition-all text-sm shadow-sm"
>
{t.pricing.plans.monthly.btnText}
</button>
</div>
{/* Plan: Annual (Highlighted) */}
<div className="relative z-20 transform lg:scale-105">
<div className="bg-brand-950 rounded-3xl border border-brand-800 p-8 flex flex-col relative shadow-2xl shadow-brand-900/20 ring-1 ring-brand-700/50 h-full">
<div className="max-w-md mx-auto relative z-10">
<div className="bg-brand-950 rounded-3xl border border-brand-800 p-8 flex flex-col relative shadow-2xl shadow-brand-900/20 ring-1 ring-brand-700/50">
<div className="flex justify-between items-start mt-2">
<div>
<h3 className="text-lg font-bold text-white flex items-center gap-2">
{t.pricing.plans.annual.title}
<span className="bg-white/10 text-white text-[10px] px-2 py-0.5 rounded-full border border-white/10">{t.pricing.plans.annual.savings}</span>
<h3 className="flex items-center gap-2 text-xl font-bold text-white">
{t.pricing.plans.monthly.title}
<span className="bg-brand-500 rounded-full px-2 py-0.5 pb-[3px] text-[10px] font-bold uppercase tracking-wider text-white">PRO</span>
</h3>
<p className="text-brand-200 text-sm mt-1 opacity-90">{t.pricing.plans.annual.description}</p>
<p className="text-brand-200 mt-1 text-sm opacity-90">{t.pricing.plans.monthly.description}</p>
</div>
<div className="p-2 bg-white/5 rounded-lg border border-white/10">
<Sparkles className="text-accent-400" size={20} />
<div className="bg-white/5 border-white/10 rounded-lg border p-2">
<Sparkles className="text-accent-400" size={24} />
</div>
</div>
<div className="mt-6 mb-2 flex items-baseline gap-1">
<span className="text-5xl font-extrabold text-white tracking-tight">{t.pricing.plans.annual.price}</span>
<span className="text-brand-200 text-sm font-medium">{t.pricing.plans.annual.period}</span>
<div className="mb-2 mt-6 flex items-baseline gap-1">
<span className="text-5xl font-extrabold tracking-tight text-white">{t.pricing.plans.monthly.price}</span>
<span className="text-brand-200 text-sm font-medium">{t.pricing.plans.monthly.period}</span>
</div>
<p className="text-xs font-medium text-brand-300 mb-8 uppercase tracking-wide">{t.pricing.plans.annual.billingInfo}</p>
<p className="text-brand-300 mb-8 text-xs font-medium uppercase tracking-wide">{t.pricing.plans.monthly.billingInfo}</p>
<div className="h-px bg-gradient-to-r from-transparent via-brand-800 to-transparent mb-8"></div>
<div className="from-transparent via-brand-800 to-transparent mb-8 h-px bg-gradient-to-r"></div>
<ul className="space-y-4 mb-8 flex-grow">
{t.pricing.plans.annual.features.map((item, i) => (
<li key={i} className="flex items-start gap-3 text-gray-100 text-sm">
<div className="bg-brand-600 rounded-full p-0.5 text-white shadow-sm mt-0.5">
<Check className="w-3 h-3" strokeWidth={3} />
<ul className="mb-8 flex-grow space-y-4">
{t.pricing.plans.monthly.features.map((item, i) => (
<li key={i} className="flex items-start gap-3 text-sm text-gray-100">
<div className="bg-brand-600 mt-0.5 rounded-full p-0.5 text-white shadow-sm">
<Check className="h-3 w-3" strokeWidth={3} />
</div>
<span className="leading-snug">{item}</span>
</li>
@ -106,45 +76,14 @@ const Pricing: React.FC<PricingProps> = ({ onRegister }) => {
</ul>
<button
onClick={() => onRegister('annual')}
className="block w-full text-center bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-400 hover:to-brand-500 text-white font-bold py-4 rounded-xl transition-all shadow-lg shadow-brand-500/20 text-sm hover:-translate-y-0.5 relative overflow-hidden group"
onClick={() => onRegister('monthly')}
className="group from-brand-500 to-brand-600 hover:from-brand-400 hover:to-brand-500 shadow-brand-500/20 hover:-translate-y-0.5 relative block w-full overflow-hidden rounded-xl bg-gradient-to-r py-4 text-center text-sm font-bold text-white shadow-lg transition-all"
>
<div className="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
<span className="relative">{t.pricing.plans.annual.btnText}</span>
<div className="absolute inset-0 translate-y-full bg-white/20 transition-transform duration-300 group-hover:translate-y-0"></div>
<span className="relative">{t.pricing.plans.monthly.btnText}</span>
</button>
</div>
</div>
{/* Plan: Quarterly */}
<div className="bg-white rounded-3xl border border-gray-200 p-8 flex flex-col hover:border-brand-300 hover:shadow-lg transition-all lg:mt-12 group">
<h3 className="text-lg font-bold text-gray-900 group-hover:text-brand-600 transition-colors">{t.pricing.plans.quarterly.title}</h3>
<div className="mt-4 mb-2 flex items-baseline gap-1">
<span className="text-4xl font-extrabold text-gray-900">{t.pricing.plans.quarterly.price}</span>
<span className="text-gray-500 text-sm">{t.pricing.plans.quarterly.period}</span>
</div>
<p className="text-gray-500 mb-6 text-sm">{t.pricing.plans.quarterly.description}</p>
<p className="text-xs font-semibold text-gray-400 mb-6 uppercase tracking-wide">{t.pricing.plans.quarterly.billingInfo}</p>
<ul className="space-y-4 mb-8 flex-grow">
{t.pricing.plans.quarterly.features.map((item, i) => (
<li key={i} className="flex items-start gap-3 text-gray-600 text-sm">
<Check className="w-5 h-5 text-brand-500 shrink-0" strokeWidth={2} />
<span>{item}</span>
</li>
))}
</ul>
<button
onClick={() => onRegister('quarterly')}
className="block w-full text-center bg-white border-2 border-gray-100 hover:border-brand-500 hover:text-brand-600 text-gray-700 font-bold py-3.5 rounded-xl transition-all text-sm"
>
{t.pricing.plans.quarterly.btnText}
</button>
</div>
</div>
{/* --- PROFESSIONAL PLAN SECTION --- */}
</div>\n {/* --- PROFESSIONAL PLAN SECTION --- */}
<div className="max-w-2xl mx-auto mt-16 animate-in fade-in slide-in-from-bottom-6 duration-700">
<div className="relative bg-gray-900 rounded-2xl p-6 md:p-8 overflow-hidden shadow-xl border border-gray-800">
{/* Abstract Shapes */}

View file

@ -371,12 +371,12 @@ const dictionary: Record<Language, Translations> = {
secure: 'Compra segura. Satisfação garantida ou seu dinheiro de volta em 7 dias.',
plans: {
monthly: {
title: 'Mensal',
price: 'R$ 49,90',
title: 'Plano Único PRO',
price: 'R$ 14,99',
period: '/mês',
billingInfo: 'Sem fidelidade',
description: 'Para quem quer flexibilidade.',
btnText: 'Começar Mensal',
billingInfo: 'Cancele quando quiser',
description: 'Acesso completo e ilimitado ao FoodSnap.',
btnText: 'Assinar Agora',
features: [
'Fotos Ilimitadas',
'Feedback Imediato',
@ -721,12 +721,12 @@ const dictionary: Record<Language, Translations> = {
secure: 'Secure payment via Stripe. Cancel anytime.',
plans: {
monthly: {
title: 'Monthly',
price: '$9.99',
title: 'PRO Subscription',
price: '$14.99',
period: '/mo',
billingInfo: 'Billed monthly',
description: 'Total flexibility.',
btnText: 'Subscribe Monthly',
billingInfo: 'Cancel anytime',
description: 'Full unlimited access to FoodSnap.',
btnText: 'Subscribe Now',
features: [
'Unlimited Queries',
'AI Nutritionist Chat',
@ -1071,12 +1071,12 @@ const dictionary: Record<Language, Translations> = {
secure: 'Pago seguro vía Stripe. Cancela cuando quieras.',
plans: {
monthly: {
title: 'Mensual',
price: '€ 14,90',
title: 'Plan Único PRO',
price: '€ 14,99',
period: '/mes',
billingInfo: 'Cobrado mensualmente',
description: 'Flexibilidad total.',
btnText: 'Suscribir Mensual',
billingInfo: 'Cancela cuando quieras',
description: 'Acceso completo e ilimitado.',
btnText: 'Suscribir Ahora',
features: [
'Consultas Ilimitadas',
'Chat con Nutricionista IA',

View file

@ -8,7 +8,7 @@ const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY")!;
const SITE_URL = Deno.env.get("SITE_URL")!;
// ✅ seus PRICE IDs (recorrentes)
const PRICE_MENSAL = "price_1SeOVpPHwVDouhbBWZj9beS3";
const PRICE_MENSAL = "price_1TLsAFA5oAF7o14GoHRM3LZ8";
const PRICE_TRIMESTRAL = "price_1SeOeXPHwVDouhbBcaiUy3vu";
const PRICE_ANUAL = "price_1SeOg4PHwVDouhbBTEiUPhMl";