From c1cacc5fcf3622a5e949932677b884168bfcadc9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 04:21:03 +0000 Subject: [PATCH 1/2] Polish RepoFuse UI: launch copy, nav, branding, and social proof - Refresh homepage post-launch banner and align hero messaging with live product - Fix footer year, wire ImpactStats and Testimonials on landing page - Shrink header logos on dashboard and pricing; use RepoFuseLogo3D in app chrome - Unify dashboard navigation via shared config with More dropdown for secondary routes - Add cyan brand accents to dashboard header; remove viewport maximumScale cap - Remove v0.app generator metadata Co-authored-by: Cole Collins --- app/layout.tsx | 2 - app/page.tsx | 80 +++++----------- app/pricing/page.tsx | 13 ++- components/dashboard-header.tsx | 164 ++++++++++++++++++++------------ components/impact-stats.tsx | 75 +++++++++++---- components/nav-dropdown.tsx | 93 +++--------------- components/testimonials.tsx | 74 +++++++++++--- lib/dashboard-nav.ts | 127 +++++++++++++++++++++++++ 8 files changed, 393 insertions(+), 235 deletions(-) create mode 100644 lib/dashboard-nav.ts diff --git a/app/layout.tsx b/app/layout.tsx index 84d8e68..b8aec5b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -11,7 +11,6 @@ const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-geist-mono' export const metadata: Metadata = { title: 'RepoFuse - Discover Apps Hidden in Your Code', description: 'AI-powered GitHub repository analyzer that discovers what apps you can build from your existing code', - generator: 'v0.app', icons: { icon: [ { @@ -34,7 +33,6 @@ export const metadata: Metadata = { export const viewport = { width: 'device-width', initialScale: 1, - maximumScale: 1, } export default function RootLayout({ diff --git a/app/page.tsx b/app/page.tsx index 7b1fd74..e63f533 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,8 +2,10 @@ import Link from 'next/link' import { Button } from '@/components/ui/button' import { RepoFuseLogo3D } from '@/components/repofuse-logo-3d' import { NavDropdown } from '@/components/nav-dropdown' -import { Github, ArrowRight, AlertCircle, Shield, Zap, GitBranch, Rocket, Code2, Sparkles } from 'lucide-react' +import { Github, ArrowRight, AlertCircle, Rocket } from 'lucide-react' import { LaunchSignupModal } from '@/components/launch-signup-modal' +import { ImpactStats } from '@/components/impact-stats' +import { Testimonials } from '@/components/testimonials' const ERROR_MESSAGES: Record = { auth_required: 'You must sign in to access the dashboard.', @@ -41,33 +43,23 @@ export default async function HomePage({ searchParams }: { searchParams: Promise backgroundSize: '256px' }} /> - {/* Launch Day Banner - Compelling Headline */} -
-
-
-
- -
-
-

- FULL LAUNCH -

-

- 5/12/2026 -

-
-
-
-

- First 1,000 Developers Get: + {/* Post-launch offer */} +

+
+ +
+

+ Now live โ€” start free today

-

- 14 Days Free OR 3 Analyses + 1 Blueprint -

-

- Lock in lifetime pricing. Link GitHub or GitLab today. +

+ 14-day Pro trial + {' ยท '} + free tier includes repo scans and blueprint previews

+
@@ -136,7 +128,7 @@ export default async function HomePage({ searchParams }: { searchParams: Promise {/* Badge */}
- Now in Public Beta + Now live
{/* Heading */} @@ -164,7 +156,7 @@ export default async function HomePage({ searchParams }: { searchParams: Promise @@ -178,7 +170,7 @@ export default async function HomePage({ searchParams }: { searchParams: Promise {/* Social proof */}
- 2,400+ developers already on the waitlist + Join developers turning repos into shippable products
@@ -225,29 +217,7 @@ export default async function HomePage({ searchParams }: { searchParams: Promise
- {/* Metrics Strip */} -
-
-
-
-

12k+

-

Repos Scanned

-
-
-

4.1k

-

Ideas Found

-
-
-

89%

-

Code Reuse

-
-
-

<30s

-

Analysis Time

-
-
-
-
+ {/* How It Works */}
@@ -266,7 +236,7 @@ export default async function HomePage({ searchParams }: { searchParams: Promise
{/* Connecting line */} -
+
{[ { num: '01', title: 'Connect', icon: '๐Ÿ”—', desc: 'OAuth in one click. Read-only access to your repos.' }, @@ -330,6 +300,8 @@ export default async function HomePage({ searchParams }: { searchParams: Promise
+ + {/* CTA Section */}
@@ -340,7 +312,7 @@ export default async function HomePage({ searchParams }: { searchParams: Promise

- Join 2,400+ developers who've stopped guessing and started shipping. + Connect your repos and get your first blueprint in minutes.

+ + + + Workspace + + + {SECONDARY_DASHBOARD_NAV.map((item) => ( + + + + {item.label} + {item.isPro && } + + + ))} + +
-
+
{user ? (
{user.github_avatar_url && ( {user.github_username} )}

@{user.github_username}

Sign out @@ -80,17 +139,17 @@ export function DashboardHeader({ user }: DashboardHeaderProps) {
Connect GitHub - - + + Connect GitLab @@ -100,28 +159,11 @@ export function DashboardHeader({ user }: DashboardHeaderProps) {
-
))} - + - +
- diff --git a/components/testimonials.tsx b/components/testimonials.tsx index 6a07da5..de832cf 100644 --- a/components/testimonials.tsx +++ b/components/testimonials.tsx @@ -1,44 +1,76 @@ 'use client' import { Card } from '@/components/ui/card' +import { cn } from '@/lib/utils' const testimonials = [ { - quote: 'RepoFuse discovered 3 hidden apps in our codebase. We shipped one in 2 weeks and it generated $50k in revenue.', + quote: + 'RepoFuse discovered 3 hidden apps in our codebase. We shipped one in 2 weeks and it generated $50k in revenue.', author: 'Alex Chen', title: 'Founder & CTO', company: 'TechStartup Inc', }, { - quote: 'Instead of starting from scratch, RepoFuse helped us assemble products from existing code. Saved us 6 months of development time.', + quote: + 'Instead of starting from scratch, RepoFuse helped us assemble products from existing code. Saved us 6 months of development time.', author: 'Sarah Johnson', title: 'Product Lead', company: 'Global Tech', }, { - quote: 'The Reddit demand insights showed us exactly what users were asking for. We built exactly what the market needed.', + quote: + 'The demand insights showed us exactly what users were asking for. We built exactly what the market needed.', author: 'Marcus Rodriguez', title: 'Indie Developer', company: 'Solo Founder', }, { - quote: 'Reduced our AI costs by 60% using the BYOK plan. Quality stayed the same, profit went way up.', + quote: + 'Reduced our AI costs by 60% using the BYOK plan. Quality stayed the same, profit went way up.', author: 'Emily Watson', title: 'Engineering Manager', company: 'Scale.io', }, ] -export function Testimonials() { +type TestimonialsProps = { + variant?: 'default' | 'marketing' +} + +export function Testimonials({ variant = 'default' }: TestimonialsProps) { + const isMarketing = variant === 'marketing' + return ( -
+
-

+ {isMarketing && ( +
+ + TESTIMONIALS +
+ )} +

Developers ship faster with RepoFuse

-

- Join thousands of builders discovering, assembling, and shipping apps from their existing code. +

+ Join builders discovering, assembling, and shipping apps from their existing code.

@@ -46,19 +78,33 @@ export function Testimonials() { {testimonials.map((testimonial, idx) => (
{[...Array(5)].map((_, i) => ( - + โ˜… ))}
-

"{testimonial.quote}"

+

+ “{testimonial.quote}” +

-

{testimonial.author}

-

+

+ {testimonial.author} +

+

{testimonial.title} at {testimonial.company}

diff --git a/lib/dashboard-nav.ts b/lib/dashboard-nav.ts new file mode 100644 index 0000000..e3e2416 --- /dev/null +++ b/lib/dashboard-nav.ts @@ -0,0 +1,127 @@ +import type { LucideIcon } from 'lucide-react' +import { + BarChart3, + FolderGit2, + LineChart, + AppWindow, + Star, + Layout, + FileCode, + CheckCircle2, + AlertTriangle, + LayoutGrid, + Cpu, + CreditCard, +} from 'lucide-react' + +export interface DashboardNavItem { + label: string + href: string + icon: LucideIcon + description: string + isPro: boolean + /** Show in the main horizontal nav (desktop) */ + primary?: boolean +} + +/** Shared routes for marketing dropdown and dashboard navigation */ +export const DASHBOARD_NAV_ITEMS: DashboardNavItem[] = [ + { + label: 'Overview', + href: '/dashboard', + icon: BarChart3, + description: 'Code intelligence overview', + isPro: false, + primary: true, + }, + { + label: 'Repositories', + href: '/dashboard/repositories', + icon: FolderGit2, + description: 'Connected GitHub & GitLab repos', + isPro: false, + primary: true, + }, + { + label: 'Analyses', + href: '/dashboard/analyses', + icon: LineChart, + description: 'AI-powered repo scans', + isPro: false, + primary: true, + }, + { + label: 'Built Apps', + href: '/dashboard/built-apps', + icon: AppWindow, + description: 'Existing apps detected in your repos', + isPro: false, + }, + { + label: 'Most Desired', + href: '/dashboard/most-desired', + icon: Star, + description: 'Your saved and prioritized ideas', + isPro: true, + }, + { + label: 'Templates', + href: '/dashboard/templates/browse', + icon: Layout, + description: 'Browse AI-generated templates', + isPro: false, + primary: true, + }, + { + label: 'Blueprints', + href: '/dashboard/blueprints', + icon: FileCode, + description: 'Full project blueprints', + isPro: true, + primary: true, + }, + { + label: 'Idea Board', + href: '/dashboard/idea-board', + icon: LayoutGrid, + description: 'Organize and prioritize ideas', + isPro: false, + }, + { + label: 'Pattern Analyzer', + href: '/dashboard/pattern-analyzer', + icon: Cpu, + description: 'Discover new project patterns', + isPro: false, + }, + { + label: 'Completed', + href: '/dashboard/completed', + icon: CheckCircle2, + description: 'Projects you marked as shipped', + isPro: false, + }, + { + label: 'Missing Code', + href: '/dashboard/gaps', + icon: AlertTriangle, + description: 'Gaps to fill before shipping', + isPro: true, + }, + { + label: 'Billing', + href: '/dashboard/billing', + icon: CreditCard, + description: 'Plans and credits', + isPro: false, + primary: true, + }, +] + +export const PRIMARY_DASHBOARD_NAV = DASHBOARD_NAV_ITEMS.filter((item) => item.primary) +export const SECONDARY_DASHBOARD_NAV = DASHBOARD_NAV_ITEMS.filter((item) => !item.primary) + +/** Marketing site dropdown (excludes overview & billing) */ +export const MARKETING_DASHBOARD_NAV = DASHBOARD_NAV_ITEMS.filter( + (item) => item.href !== '/dashboard' && item.href !== '/dashboard/billing', +) From afa258328546f78065cea75613254a50b1496020 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 23:50:50 +0000 Subject: [PATCH 2/2] fix: restore Neon webhook after merge (main had Supabase stub) Co-authored-by: Cole Collins --- app/api/stripe/webhook/route.ts | 380 +++++++++++++------------------- 1 file changed, 156 insertions(+), 224 deletions(-) diff --git a/app/api/stripe/webhook/route.ts b/app/api/stripe/webhook/route.ts index eddca3e..9f8e865 100644 --- a/app/api/stripe/webhook/route.ts +++ b/app/api/stripe/webhook/route.ts @@ -1,252 +1,184 @@ -import { NextRequest, NextResponse } from "next/server"; -import Stripe from "stripe"; -import { createClient } from "@supabase/supabase-js"; - -// โ”€โ”€โ”€ Clients โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: "2024-11-20.acacia", -}); - -// Use the service role key so we can write to Supabase from the server -const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.SUPABASE_SERVICE_ROLE_KEY! -); - -// โ”€โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -function getPlanFromPriceId(priceId: string): string { - const priceMap: Record = { - [process.env.STRIPE_PRICE_PRO_MONTHLY || ""]: "pro", - [process.env.STRIPE_PRICE_PRO_YEARLY || ""]: "pro", - [process.env.STRIPE_PRICE_STARTER_MONTHLY || ""]: "starter", - }; - return priceMap[priceId] || "free"; -} - -// โ”€โ”€โ”€ Webhook Handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -export async function POST(req: NextRequest) { - // 1. Get the raw body โ€” required for Stripe signature verification - const body = await req.text(); - const sig = req.headers.get("stripe-signature"); +import { NextRequest, NextResponse } from 'next/server' +import { getStripe } from '@/lib/stripe' +import { upsertSubscription, getSubscriptionByStripeCustomerId, getUserByGithubId } from '@/lib/queries' +import { grantCredits, CREDITS } from '@/lib/credits' +import type Stripe from 'stripe' + +/** Stripe SDK requires Node.js (not Edge). */ +export const runtime = 'nodejs' + +const HANDLED_EVENTS = new Set([ + 'checkout.session.completed', + 'customer.subscription.updated', + 'customer.subscription.deleted', + 'invoice.payment_failed', + 'invoice.payment_succeeded', +]) + +export async function POST(request: NextRequest) { + const signature = request.headers.get('stripe-signature') + + if (!process.env.STRIPE_WEBHOOK_SECRET || !process.env.STRIPE_SECRET_KEY) { + console.error('[stripe/webhook] Missing STRIPE_WEBHOOK_SECRET or STRIPE_SECRET_KEY in environment') + return NextResponse.json({ error: 'Webhook not configured' }, { status: 503 }) + } - if (!sig) { - console.error("โš ๏ธ Missing stripe-signature header"); - return NextResponse.json({ error: "Missing signature" }, { status: 400 }); + if (!signature) { + return NextResponse.json({ error: 'Missing stripe-signature header' }, { status: 400 }) } - if (!process.env.STRIPE_WEBHOOK_SECRET) { - console.error("โš ๏ธ STRIPE_WEBHOOK_SECRET is not set in environment variables"); - return NextResponse.json({ error: "Webhook secret not configured" }, { status: 500 }); + let body: string + try { + body = await request.text() + } catch (err) { + console.error('[stripe/webhook] Failed to read request body:', err) + return NextResponse.json({ error: 'Invalid body' }, { status: 400 }) } - // 2. Verify the event actually came from Stripe - let event: Stripe.Event; + let stripe: ReturnType try { - event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : "Unknown error"; - console.error(`โš ๏ธ Webhook signature verification failed: ${message}`); - return NextResponse.json({ error: `Webhook Error: ${message}` }, { status: 400 }); + stripe = getStripe() + } catch (err) { + console.error('[stripe/webhook] Stripe client init failed:', err) + return NextResponse.json({ error: 'Stripe not configured' }, { status: 503 }) } - console.log(`โœ… Stripe webhook received: ${event.type}`); + let event: Stripe.Event - // 3. Handle each event type try { - switch (event.type) { + event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET) + } catch (err) { + console.error('[stripe/webhook] Signature verification failed:', err) + return NextResponse.json({ error: 'Invalid signature' }, { status: 400 }) + } - // โ”€โ”€ Checkout completed (first subscription purchase) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - case "checkout.session.completed": { - const session = event.data.object as Stripe.Checkout.Session; - const customerId = session.customer as string; - const subscriptionId = session.subscription as string; - const customerEmail = session.customer_email || session.customer_details?.email; - - if (!customerEmail) break; - - await supabase - .from("subscriptions") - .upsert({ - stripe_customer_id: customerId, - stripe_subscription_id: subscriptionId, - email: customerEmail, - status: "active", - updated_at: new Date().toISOString(), - }, { onConflict: "email" }); - - // Also update the user's plan in the profiles table - await supabase - .from("profiles") - .update({ - stripe_customer_id: customerId, - subscription_status: "active", - }) - .eq("email", customerEmail); + console.log('[stripe/webhook] Received event', { id: event.id, type: event.type }) - console.log(`โœ… Checkout completed for ${customerEmail}`); - break; - } + if (!HANDLED_EVENTS.has(event.type)) { + return NextResponse.json({ received: true, ignored: event.type }) + } - // โ”€โ”€ Subscription created โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - case "customer.subscription.created": { - const subscription = event.data.object as Stripe.Subscription; - const customerId = subscription.customer as string; - const priceId = subscription.items.data[0]?.price.id; - const plan = getPlanFromPriceId(priceId); - - // Get customer email from Stripe - const customer = await stripe.customers.retrieve(customerId) as Stripe.Customer; - const email = customer.email; - - if (!email) break; - - await supabase - .from("subscriptions") - .upsert({ - stripe_customer_id: customerId, - stripe_subscription_id: subscription.id, - email, - status: subscription.status, - plan, - current_period_end: new Date(subscription.current_period_end * 1000).toISOString(), - updated_at: new Date().toISOString(), - }, { onConflict: "stripe_subscription_id" }); - - await supabase - .from("profiles") - .update({ plan, subscription_status: subscription.status }) - .eq("email", email); - - console.log(`โœ… Subscription created: ${email} โ†’ ${plan}`); - break; + try { + switch (event.type) { + case 'checkout.session.completed': { + const session = event.data.object as Stripe.Checkout.Session + if (session.mode === 'subscription' && session.customer && session.subscription) { + const sub = await stripe.subscriptions.retrieve(session.subscription as string) + const githubId = Number(sub.metadata.github_id || session.metadata?.github_id) + const periodEnd = (sub as unknown as { current_period_end?: number }).current_period_end + if (githubId) { + await upsertSubscription({ + github_id: githubId, + stripe_customer_id: session.customer as string, + stripe_subscription_id: sub.id, + plan: 'pro', + status: 'active', + current_period_end: periodEnd ? new Date(periodEnd * 1000).toISOString() : null, + }) + + try { + const user = await getUserByGithubId(githubId) + if (user) { + await grantCredits( + user.id, + CREDITS.INITIAL_GRANT, + 'Pro plan signup bonus', + { stripe_customer_id: session.customer as string }, + ) + console.log(`[stripe/webhook] Granted ${CREDITS.INITIAL_GRANT} signup credits to ${user.id}`) + } + } catch (err) { + console.error('[stripe/webhook] Failed to grant signup credits (subscription saved):', err) + } + } else { + console.warn('[stripe/webhook] checkout.session.completed missing github_id metadata', { + sessionId: session.id, + }) + } + } + break } - // โ”€โ”€ Subscription updated (plan change, renewal, etc.) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - case "customer.subscription.updated": { - const subscription = event.data.object as Stripe.Subscription; - const customerId = subscription.customer as string; - const priceId = subscription.items.data[0]?.price.id; - const plan = getPlanFromPriceId(priceId); - - const customer = await stripe.customers.retrieve(customerId) as Stripe.Customer; - const email = customer.email; - - if (!email) break; - - await supabase - .from("subscriptions") - .update({ - status: subscription.status, - plan, - current_period_end: new Date(subscription.current_period_end * 1000).toISOString(), - updated_at: new Date().toISOString(), + case 'customer.subscription.updated': { + const sub = event.data.object as Stripe.Subscription + const existing = await getSubscriptionByStripeCustomerId(sub.customer as string) + const periodEnd = (sub as unknown as { current_period_end?: number }).current_period_end + if (existing) { + const isPro = sub.status === 'active' || sub.status === 'trialing' + await upsertSubscription({ + github_id: existing.github_id, + plan: isPro ? 'pro' : 'free', + status: + sub.status === 'active' + ? 'active' + : sub.status === 'past_due' + ? 'past_due' + : sub.status === 'trialing' + ? 'trialing' + : 'canceled', + current_period_end: periodEnd ? new Date(periodEnd * 1000).toISOString() : null, }) - .eq("stripe_subscription_id", subscription.id); - - await supabase - .from("profiles") - .update({ plan, subscription_status: subscription.status }) - .eq("email", email); - - console.log(`โœ… Subscription updated: ${email} โ†’ ${plan} (${subscription.status})`); - break; + } + break } - // โ”€โ”€ Subscription cancelled/deleted โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - case "customer.subscription.deleted": { - const subscription = event.data.object as Stripe.Subscription; - const customerId = subscription.customer as string; - - const customer = await stripe.customers.retrieve(customerId) as Stripe.Customer; - const email = customer.email; - - if (!email) break; - - await supabase - .from("subscriptions") - .update({ - status: "canceled", - plan: "free", - updated_at: new Date().toISOString(), + case 'customer.subscription.deleted': { + const sub = event.data.object as Stripe.Subscription + const existing = await getSubscriptionByStripeCustomerId(sub.customer as string) + if (existing) { + await upsertSubscription({ + github_id: existing.github_id, + plan: 'free', + status: 'canceled', }) - .eq("stripe_subscription_id", subscription.id); - - await supabase - .from("profiles") - .update({ plan: "free", subscription_status: "canceled" }) - .eq("email", email); - - console.log(`โœ… Subscription cancelled: ${email}`); - break; + } + break } - // โ”€โ”€ Invoice paid (recurring renewal) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - case "invoice.payment_succeeded": { - const invoice = event.data.object as Stripe.Invoice; - const subscriptionId = invoice.subscription as string; - - if (!subscriptionId) break; - - // Update period end on renewal - const subscription = await stripe.subscriptions.retrieve(subscriptionId); - await supabase - .from("subscriptions") - .update({ - status: "active", - current_period_end: new Date(subscription.current_period_end * 1000).toISOString(), - updated_at: new Date().toISOString(), - }) - .eq("stripe_subscription_id", subscriptionId); - - console.log(`โœ… Invoice paid for subscription: ${subscriptionId}`); - break; + case 'invoice.payment_failed': { + const invoice = event.data.object as Stripe.Invoice + if (invoice.customer) { + const existing = await getSubscriptionByStripeCustomerId(invoice.customer as string) + if (existing) { + await upsertSubscription({ + github_id: existing.github_id, + status: 'past_due', + }) + } + } + break } - // โ”€โ”€ Invoice payment failed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - case "invoice.payment_failed": { - const invoice = event.data.object as Stripe.Invoice; - const customerId = invoice.customer as string; - - const customer = await stripe.customers.retrieve(customerId) as Stripe.Customer; - const email = customer.email; - - if (!email) break; - - await supabase - .from("subscriptions") - .update({ - status: "past_due", - updated_at: new Date().toISOString(), - }) - .eq("stripe_customer_id", customerId); - - await supabase - .from("profiles") - .update({ subscription_status: "past_due" }) - .eq("email", email); - - console.log(`โš ๏ธ Payment failed for: ${email}`); - break; + case 'invoice.payment_succeeded': { + const invoice = event.data.object as Stripe.Invoice + if (invoice.customer) { + const existing = await getSubscriptionByStripeCustomerId(invoice.customer as string) + if (existing) { + try { + if (invoice.number && invoice.number !== '0001') { + const user = await getUserByGithubId(existing.github_id) + if (user) { + await grantCredits( + user.id, + CREDITS.MONTHLY_GRANT, + 'Monthly subscription renewal', + { invoice_id: invoice.id, stripe_customer_id: invoice.customer as string }, + ) + console.log(`[stripe/webhook] Granted ${CREDITS.MONTHLY_GRANT} renewal credits to ${user.id}`) + } + } + } catch (err) { + console.error('[stripe/webhook] Failed to grant renewal credits:', err) + } + } + } + break } - - default: - console.log(`โ„น๏ธ Unhandled event type: ${event.type}`); } - } catch (error) { - console.error("โŒ Error processing webhook event:", error); - // Still return 200 so Stripe doesn't keep retrying for logic errors - return NextResponse.json({ received: true, warning: "Processing error" }, { status: 200 }); + } catch (err) { + console.error('[stripe/webhook] Handler error:', { eventId: event.id, type: event.type, err }) + return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 }) } - // 4. Always return 200 so Stripe knows we received the event - return NextResponse.json({ received: true }, { status: 200 }); + return NextResponse.json({ received: true }) } - -// โ”€โ”€โ”€ Block all other HTTP methods โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -// This prevents the 405 error Stripe was getting before -export async function GET() { - return NextResponse.json({ error: "Method not allowed" }, { status: 405 }); -} \ No newline at end of file