import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { createClient } from '@supabase/supabase-js';
import Stripe from 'stripe';
// This is your Stripe webhook secret for testing your endpoint locally.
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
if (!webhookSecret) {
throw new Error('STRIPE_WEBHOOK_SECRET is not set');
}
export async function POST(req: Request) {
let event: Stripe.Event;
try {
const body = await req.text();
const signature = headers().get('stripe-signature');
if (!signature) {
console.error('Missing stripe-signature header');
return NextResponse.json(
{ error: 'Missing stripe-signature header' },
{ status: 400 }
);
}
try {
event = stripe!.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return NextResponse.json(
{ error: 'Webhook signature verification failed' },
{ status: 400 }
);
}
// Create Supabase client with service role for webhooks
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
);
// Handle the event
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
if (!session.metadata?.userId) {
console.error('Missing userId in session metadata');
return NextResponse.json(
{ error: 'Missing userId in metadata' },
{ status: 400 }
);
}
// Get full subscription details from Stripe
if (session.subscription && typeof session.subscription === 'string') {
const subscription = await stripe!.subscriptions.retrieve(session.subscription) as unknown as Stripe.Subscription & {
current_period_start: number;
current_period_end: number;
};
const { error } = await supabase
.from('subscriptions')
.upsert({
user_id: session.metadata.userId,
stripe_customer_id: session.customer as string,
stripe_subscription_id: subscription.id,
status: subscription.status,
price_id: subscription.items.data[0]?.price.id,
current_period_start: subscription.current_period_start ? new Date(subscription.current_period_start * 1000).toISOString() : null,
current_period_end: subscription.current_period_end ? new Date(subscription.current_period_end * 1000).toISOString() : null,
cancel_at_period_end: subscription.cancel_at_period_end,
trial_start: subscription.trial_start ? new Date(subscription.trial_start * 1000).toISOString() : null,
trial_end: subscription.trial_end ? new Date(subscription.trial_end * 1000).toISOString() : null,
}, {
onConflict: 'user_id'
});
if (error) {
console.error('Error upserting subscription:', error);
return NextResponse.json(
{ error: 'Error updating subscription' },
{ status: 500 }
);
}
}
break;
}
case 'customer.subscription.created': {
const subscription = event.data.object as Stripe.Subscription & {
current_period_start: number;
current_period_end: number;
};
// Get the customer to find the user_id from metadata
const customer = await stripe!.customers.retrieve(subscription.customer as string) as Stripe.Customer;
if (!customer.metadata?.userId) {
console.error('Missing userId in customer metadata');
break;
}
const { error } = await supabase
.from('subscriptions')
.upsert({
user_id: customer.metadata.userId,
stripe_customer_id: subscription.customer as string,
stripe_subscription_id: subscription.id,
status: subscription.status,
price_id: subscription.items.data[0]?.price.id,
current_period_start: subscription.current_period_start ? new Date(subscription.current_period_start * 1000).toISOString() : null,
current_period_end: subscription.current_period_end ? new Date(subscription.current_period_end * 1000).toISOString() : null,
cancel_at_period_end: subscription.cancel_at_period_end,
trial_start: subscription.trial_start ? new Date(subscription.trial_start * 1000).toISOString() : null,
trial_end: subscription.trial_end ? new Date(subscription.trial_end * 1000).toISOString() : null,
}, {
onConflict: 'user_id'
});
if (error) {
console.error('Error creating subscription:', error);
return NextResponse.json(
{ error: 'Error creating subscription' },
{ status: 500 }
);
}
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription & {
current_period_start: number;
current_period_end: number;
};
const { error } = await supabase
.from('subscriptions')
.update({
status: subscription.status,
price_id: subscription.items.data[0]?.price.id,
current_period_start: subscription.current_period_start ? new Date(subscription.current_period_start * 1000).toISOString() : null,
current_period_end: subscription.current_period_end ? new Date(subscription.current_period_end * 1000).toISOString() : null,
cancel_at_period_end: subscription.cancel_at_period_end,
canceled_at: subscription.canceled_at ? new Date(subscription.canceled_at * 1000).toISOString() : null,
trial_start: subscription.trial_start ? new Date(subscription.trial_start * 1000).toISOString() : null,
trial_end: subscription.trial_end ? new Date(subscription.trial_end * 1000).toISOString() : null,
})
.eq('stripe_subscription_id', subscription.id);
if (error) {
console.error('Error updating subscription:', error);
return NextResponse.json(
{ error: 'Error updating subscription' },
{ status: 500 }
);
}
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
const { error } = await supabase
.from('subscriptions')
.update({
status: 'canceled',
canceled_at: new Date(subscription.canceled_at! * 1000).toISOString(),
})
.eq('stripe_subscription_id', subscription.id);
if (error) {
console.error('Error canceling subscription:', error);
return NextResponse.json(
{ error: 'Error canceling subscription' },
{ status: 500 }
);
}
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice & { subscription?: string | Stripe.Subscription };
if (invoice.subscription && typeof invoice.subscription === 'string') {
const { error } = await supabase
.from('subscriptions')
.update({
status: 'past_due',
})
.eq('stripe_subscription_id', invoice.subscription);
if (error) {
console.error('Error updating subscription status to past_due:', error);
}
}
break;
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object as Stripe.Invoice & { subscription?: string | Stripe.Subscription };
if (invoice.subscription && typeof invoice.subscription === 'string') {
const { error } = await supabase
.from('subscriptions')
.update({
status: 'active',
})
.eq('stripe_subscription_id', invoice.subscription);
if (error) {
console.error('Error updating subscription status to active:', error);
}
}
break;
}
// Handle other events silently (they're informational but don't require action)
case 'payment_method.attached':
case 'customer.created':
case 'customer.updated':
case 'payment_intent.succeeded':
case 'payment_intent.created':
case 'invoice.created':
case 'invoice.finalized':
case 'invoice.paid':
case 'charge.succeeded':
// These events are expected but don't require database updates
console.log(`Handled event type: ${event.type}`);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Error processing webhook:', error);
return NextResponse.json(
{ error: 'Error processing webhook' },
{ status: 500 }
);
}
}