feat(billing): integrate Stripe for subscription management and billing access

- Added billing-related fields to the User model in the database.
- Implemented functions for managing billing access, including trial periods and subscription statuses.
- Created new billing utility functions for Stripe integration.
- Updated onboarding page to include billing overview and workspace creation eligibility.
- Enhanced route access checks to require billing access for certain actions.
- Implemented cleanup scripts for expired billing workspaces and associated media.
- Updated header component to conditionally show app navigation based on billing access.
- Added new migrations for billing-related database changes.
This commit is contained in:
Yusuf İpek
2026-04-08 17:50:40 +03:00
parent 8db7a031e6
commit 6f22b0bf8b
45 changed files with 1859 additions and 218 deletions
+88
View File
@@ -0,0 +1,88 @@
import { NextRequest } from 'next/server';
import type Stripe from 'stripe';
import {
markSubscriptionCanceledByCustomerId,
syncStripeSubscriptionToUser,
} from '@/lib/billing';
import { getStripe, getStripeWebhookSecret } from '@/lib/stripe';
export const runtime = 'nodejs';
async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
const customerId =
typeof subscription.customer === 'string'
? subscription.customer
: subscription.customer.id;
const currentPeriodEnd =
'current_period_end' in subscription && typeof subscription.current_period_end === 'number'
? new Date(subscription.current_period_end * 1000)
: null;
const endedAt =
'ended_at' in subscription && typeof subscription.ended_at === 'number'
? new Date(subscription.ended_at * 1000)
: currentPeriodEnd;
await markSubscriptionCanceledByCustomerId(customerId, {
currentPeriodEnd,
endedAt,
});
}
export async function POST(request: NextRequest) {
const signature = request.headers.get('stripe-signature');
if (!signature) {
return new Response('Missing Stripe signature', { status: 400 });
}
let event: Stripe.Event;
try {
const stripe = getStripe();
const body = await request.text();
event = stripe.webhooks.constructEvent(body, signature, getStripeWebhookSecret());
} catch (error) {
console.error('Failed to verify Stripe webhook:', error);
return new Response('Invalid webhook signature', { status: 400 });
}
try {
const stripe = getStripe();
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
if (session.mode === 'subscription' && session.subscription) {
const subscriptionId =
typeof session.subscription === 'string'
? session.subscription
: session.subscription.id;
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
await syncStripeSubscriptionToUser(subscription);
}
break;
}
case 'customer.subscription.created':
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
await syncStripeSubscriptionToUser(subscription);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionDeleted(subscription);
break;
}
default:
break;
}
return new Response(JSON.stringify({ received: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Failed to process Stripe webhook:', error);
return new Response('Webhook processing failed', { status: 500 });
}
}