// In my case, this goes into functions/src/services/stripe.ts import type Stripe from 'stripe'; import * as Sentry from '@sentry/google-cloud-serverless'; import { getFirestore } from 'firebase-admin/firestore'; import * as logger from 'firebase-functions/logger'; import type { CustomerSubscription } from '../types/subscription'; import { getSubscriptionPlans } from '../config/constants'; const db = getFirestore(); export async function createStripeCustomer(stripe: Stripe, userId: string, email: string): Promise { try { const customer = await stripe.customers.create({ email, metadata: { firebaseUID: userId, }, }); await db.collection('users').doc(userId).set( { stripeCustomerId: customer.id, }, { merge: true }, ); return customer.id; } catch (error) { logger.error('Error creating Stripe customer:', error); Sentry.captureException(error); throw error; } } export async function getOrCreateStripeCustomer(stripe: Stripe, userId: string, email: string): Promise { const userDoc = await db.collection('users').doc(userId).get(); const userData = userDoc.data(); if (userData?.stripeCustomerId) { return userData.stripeCustomerId; } return createStripeCustomer(stripe, userId, email); } export async function createCheckoutSession( stripe: Stripe, customerId: string, priceId: string, successUrl: string, cancelUrl: string, ): Promise { const session = await stripe.checkout.sessions.create({ customer: customerId, payment_method_types: ['card'], line_items: [ { price: priceId, quantity: 1, }, ], mode: 'subscription', success_url: successUrl, cancel_url: cancelUrl, }); return session.url || ''; } export async function createPortalSession(stripe: Stripe, customerId: string, returnUrl: string): Promise { const session = await stripe.billingPortal.sessions.create({ customer: customerId, return_url: returnUrl, }); return session.url; } export async function getCustomerSubscription( stripe: Stripe, customerId: string, ): Promise { try { const subscriptions = await stripe.subscriptions.list({ customer: customerId, status: 'active', expand: ['data.default_payment_method'], }); if (subscriptions.data.length === 0) { return null; } const subscription = subscriptions.data[0]; const priceId = subscription.items.data[0].price.id; // Find the plan that matches the price ID const plan = Object.values(getSubscriptionPlans).find((p) => p.id === priceId); if (!plan) { logger.error(`No matching plan found for price ID: ${priceId}`); return null; } return { id: subscription.id, status: subscription.status, plan, currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(), cancelAtPeriodEnd: subscription.cancel_at_period_end, }; } catch (error) { logger.error('Error fetching customer subscription:', error); Sentry.captureException(error); return null; } } export async function handleSubscriptionUpdated(event: Stripe.Event): Promise { const subscription = event.data.object as Stripe.Subscription; const customerId = subscription.customer as string; const status = subscription.status; const priceId = subscription.items.data[0].price.id; // Find user with this Stripe customer ID const usersSnapshot = await db.collection('users').where('stripeCustomerId', '==', customerId).limit(1).get(); if (usersSnapshot.empty) { logger.error(`No user found for Stripe customer ID: ${customerId}`); Sentry.captureException(new Error(`No user found for Stripe customer ID: ${customerId}`)); return; } const userId = usersSnapshot.docs[0].id; // Update subscription status in Firestore await db .collection('users') .doc(userId) .set( { subscription: { id: subscription.id, status, priceId, currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(), cancelAtPeriodEnd: subscription.cancel_at_period_end, }, }, { merge: true }, ); }