From 3d4efa9281c320c3632fe5cfe1f2ac5705e9bb3c Mon Sep 17 00:00:00 2001 From: Marcio Bevervanso Date: Tue, 17 Feb 2026 18:32:47 -0300 Subject: [PATCH] fix: implement stripe portal and upgrade button logic --- .../dashboard/DashboardSubscription.tsx | 25 ++++++- src/pages/Dashboard.tsx | 2 +- supabase/functions/stripe-portal/index.ts | 72 +++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 supabase/functions/stripe-portal/index.ts diff --git a/src/components/dashboard/DashboardSubscription.tsx b/src/components/dashboard/DashboardSubscription.tsx index 4b7bac0..1c8e584 100644 --- a/src/components/dashboard/DashboardSubscription.tsx +++ b/src/components/dashboard/DashboardSubscription.tsx @@ -166,7 +166,30 @@ const DashboardSubscription: React.FC = ({ user, pla Obtenha análises ilimitadas, histórico completo e acesso às funcionalidades profissionais.

- diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index f82c54c..bb5f6c1 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -95,7 +95,7 @@ const Dashboard: React.FC = ({ user, onLogout, onOpenAdmin, onOp return; } - const response = await fetch(`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/stripe-checkout`, { + const response = await fetch(`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/stripe-portal`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/supabase/functions/stripe-portal/index.ts b/supabase/functions/stripe-portal/index.ts new file mode 100644 index 0000000..c30b5a3 --- /dev/null +++ b/supabase/functions/stripe-portal/index.ts @@ -0,0 +1,72 @@ + +/// +import Stripe from "npm:stripe@16.12.0"; + +const STRIPE_SECRET_KEY = Deno.env.get("STRIPE_SECRET_KEY") ?? ""; +const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? ""; +const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY") ?? ""; +const SITE_URL = Deno.env.get("SITE_URL") ?? "http://localhost:3000"; + +const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: "2025-11-17.clover" }); + +const corsHeaders = { + "access-control-allow-origin": "*", + "access-control-allow-headers": "authorization, x-client-info, apikey, content-type", + "access-control-allow-methods": "POST, OPTIONS", +}; + +function json(data: unknown, status = 200) { + return new Response(JSON.stringify(data), { + status, + headers: { "content-type": "application/json", ...corsHeaders }, + }); +} + +async function getUserFromJwt(jwt: string) { + const res = await fetch(`${SUPABASE_URL}/auth/v1/user`, { + headers: { + apikey: SUPABASE_ANON_KEY, + authorization: `Bearer ${jwt}`, + }, + }); + if (!res.ok) return null; + return await res.json(); +} + +async function getStripeCustomerId(userId: string) { + const res = await fetch(`${SUPABASE_URL}/rest/v1/stripe_customers?user_id=eq.${userId}&select=stripe_customer_id`, { + headers: { + apikey: SUPABASE_ANON_KEY, + authorization: `Bearer ${SUPABASE_ANON_KEY}`, + }, + }); + if (!res.ok) return null; + const rows = await res.json(); + return rows?.[0]?.stripe_customer_id; +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders }); + if (req.method !== "POST") return json({ ok: false, error: "Method not allowed" }, 405); + + try { + const auth = req.headers.get("authorization") || ""; + const jwt = auth.startsWith("Bearer ") ? auth.slice(7) : ""; + if (!jwt) return json({ ok: false, error: "Missing Authorization" }, 401); + + const user = await getUserFromJwt(jwt); + if (!user?.id) return json({ ok: false, error: "Invalid token" }, 401); + + const customerId = await getStripeCustomerId(user.id); + if (!customerId) return json({ ok: false, error: "Customer not found" }, 404); + + const session = await stripe.billingPortal.sessions.create({ + customer: customerId, + return_url: `${SITE_URL}/dashboard`, + }); + + return json({ ok: true, url: session.url }); + } catch (err) { + return json({ ok: false, error: String((err as any)?.message ?? err) }, 500); + } +});