saas-subscription-helper
Version:
A helper package for integrating Stripe Payment Links with Supabase in SaaS applications.
155 lines (134 loc) • 6.17 kB
JavaScript
// Processes Stripe webhooks for subscription updates.
const stripe = require('stripe');
const { updateUser } = require('../supabase');
async function handleWebhooks(config, rawBody, headers) {
if (config.debug) console.log('[DEBUG] Starting webhook processing...');
const stripeClient = stripe(config.stripeSecretKey);
// Get stripe signature from headers
const stripeSignature = headers?.['stripe-signature'];
if (config.debug) {
console.log('Implementation received:', {
rawBody,
headers,
signature: stripeSignature
});
}
if (config.debug && config.debugHeaders && headers) {
console.log('[DEBUG] Received headers:', headers);
}
if (!stripeSignature) {
if (config.debug) {
console.log('[DEBUG] Error: Missing Stripe signature');
if (headers && config.debugHeaders) {
console.log('Available headers:', Object.keys(headers));
}
}
throw new Error('No Stripe signature found in request');
}
if (!rawBody) {
if (config.debug) console.log('[DEBUG] Error: Missing raw request body');
throw new Error('No request body found');
}
if (config.debug) {
console.log('[DEBUG] Verifying webhook:');
console.log('- Signature:', stripeSignature);
console.log('- Secret starts with:', config.stripeWebhookSecret?.slice(0, 8));
console.log('- Raw body length:', rawBody.length);
try {
// Try to parse and log the body for debugging
const parsedBody = JSON.parse(rawBody);
console.log('- Event type:', parsedBody.type);
} catch (e) {
console.log('- Could not parse body for debugging');
}
}
try {
const event = stripeClient.webhooks.constructEvent(
rawBody,
stripeSignature,
config.stripeWebhookSecret
);
if (config.debug) console.log(`[DEBUG] Processing event: ${event.type}`);
// Handle new subscription creation from payment link
if (event.type === 'customer.subscription.created') {
const newSubscription = event.data.object;
const customer = await stripeClient.customers.retrieve(newSubscription.customer);
// Check for other active subscriptions that need cancellation
const existingSubscriptions = await stripeClient.subscriptions.list({
customer: newSubscription.customer,
status: 'active'
});
// Cancel any old subscriptions immediately if we find a new one
for (const sub of existingSubscriptions.data) {
if (sub.id !== newSubscription.id) {
await stripeClient.subscriptions.cancel(sub.id, {
prorate: true
});
}
}
}
if (event.type === 'customer.subscription.updated' ||
event.type === 'customer.subscription.deleted' ||
event.type === 'customer.subscription.cancelled') {
try {
const subscription = event.data.object;
const email = subscription.customer_email ||
(await stripeClient.customers.retrieve(subscription.customer)).email;
// Get the correct status - important for cancellations
const status = subscription.cancel_at_period_end ? 'canceled' : subscription.status;
const priceId = subscription.items.data[0].price.id;
if (!email) {
if (config.debug) console.log('[DEBUG] Error: No email found in webhook data');
throw new Error('No email found in webhook data');
}
let updateData;
if (event.type === 'customer.subscription.deleted') {
console.log('DEBUG: Constructing deleted subscription data:', {
subscriptionField: config.subscriptionField,
updateData: {
[config.subscriptionField]: 'inactive',
stripe_price_id: null
}
});
updateData = {
[config.subscriptionField]: 'inactive',
stripe_price_id: null
};
} else {
console.log('DEBUG: Constructing update subscription data:', {
status,
priceId,
updateData: {
[config.subscriptionField]: status,
stripe_price_id: priceId
}
});
updateData = {
[config.subscriptionField]: status,
stripe_price_id: priceId
};
}
// Add trial status if configured
if (config.syncedStripeFields?.trial) {
console.log('\n[handleWebhooks] Adding trial data:', {
trial_end: subscription.trial_end,
setting_trial_to: !!subscription.trial_end
});
updateData.trial = !!subscription.trial_end;
if (subscription.trial_end) {
updateData.trial_end = new Date(subscription.trial_end * 1000);
}
}
console.log('\n[handleWebhooks] Final updateData:', JSON.stringify(updateData, null, 2));
const result = await updateUser(config, email, updateData);
} catch (e) {
if (config.debug) console.log('[DEBUG] Error: Failed to update user subscription details');
throw e;
}
}
} catch (e) {
if (config.debug) console.log('[DEBUG] Error: Failed to process webhook');
throw e;
}
}
module.exports = handleWebhooks;