feat: enhance Stripe payment provider with customer management and error handling

- Added functionality to create or get a Stripe customer based on email, improving user experience during checkout.
- Implemented robust error handling for updating user records with customer IDs.
- Updated README to reflect new actions for creating checkout sessions and customer portal sessions.
This commit is contained in:
javayhu 2025-03-24 01:09:21 +08:00
parent c577cbc933
commit a7d2ddef1a
3 changed files with 73 additions and 2 deletions

View File

@ -42,6 +42,7 @@ export const createCheckoutAction = actionClient
// Create the checkout session with localized URLs
const baseUrlWithLocale = getBaseUrlWithLocale(locale);
const successUrl = `${baseUrlWithLocale}/payment/success?session_id={CHECKOUT_SESSION_ID}`;
// TODO: maybe add a cancel url as param, do not redirect to the cancel page
const cancelUrl = `${baseUrlWithLocale}/payment/cancel`;
const params: CreateCheckoutParams = {
planId,

View File

@ -8,7 +8,8 @@ This module provides a flexible payment integration with Stripe, supporting both
- `/payment/index.ts` - Main payment interface and global provider instance
- `/payment/provider/stripe.ts` - Stripe payment provider implementation
- `/payment/config/payment-config.ts` - Payment plans configuration
- `/actions/payment.ts` - Server actions for payment operations
- `/actions/create-checkout-session.ts` - Server actions for creating checkout session
- `/actions/create-customer-portal-session.ts` - Server actions for creating portal session
- `/app/api/webhooks/stripe/route.ts` - API route for Stripe webhook events
- `/app/[locale]/(marketing)/payment/success/page.tsx` - Success page for completed checkout
- `/app/[locale]/(marketing)/payment/cancel/page.tsx` - Cancel page for abandoned checkout

View File

@ -99,6 +99,11 @@ export class StripeProvider implements PaymentProvider {
metadata,
});
// Update user record in database with the new customer ID (non-blocking)
this.updateUserWithCustomerId(customer.id, email || '').catch(error => {
console.error('Error updating user with customer ID:', error);
});
return customer.id;
} catch (error) {
console.error('Error creating or getting customer:', error);
@ -106,6 +111,41 @@ export class StripeProvider implements PaymentProvider {
}
}
/**
* Updates a user record with a Stripe customer ID
* @param customerId Stripe customer ID
* @param email Customer email
* @returns Promise that resolves when the update is complete
*/
private async updateUserWithCustomerId(customerId: string, email: string): Promise<void> {
try {
// Dynamic import to avoid circular dependencies
// TODO: can we avoid using dynamic import?
const { default: db } = await import('@/db/index');
const { user } = await import('@/db/schema');
const { eq } = await import('drizzle-orm');
// Update user record with customer ID if email matches
const result = await db
.update(user)
.set({
customerId: customerId,
updatedAt: new Date()
})
.where(eq(user.email, email))
.returning({ id: user.id });
if (result.length > 0) {
console.log(`Updated user ${result[0].id} with customer ID ${customerId}`);
} else {
console.log(`No user found with email ${email}`);
}
} catch (error) {
console.error('update user with customer ID error:', error);
throw error; // Re-throw to be caught by the caller
}
}
/**
* Create a checkout session for a plan
* @param params Parameters for creating the checkout session
@ -151,8 +191,22 @@ export class StripeProvider implements PaymentProvider {
},
};
// If customer email is provided, add it to the checkout
// If customer email is provided, create or get a customer
if (customerEmail) {
// Get customer name from metadata if available
const customerName = metadata?.name;
// Create or get customer
const customerId = await this.createOrGetCustomer(
customerEmail,
customerName,
metadata
);
// Add customer to checkout session
checkoutParams.customer = customerId;
} else {
// If no customer email provided, add email field to collect it during checkout
checkoutParams.customer_email = customerEmail;
}
@ -397,4 +451,19 @@ export class StripeProvider implements PaymentProvider {
console.error('Error in default webhook handler:', error);
}
}
/**
* Create a Stripe customer if one doesn't exist for the email
* @param email Customer email
* @param name Optional customer name
* @param metadata Optional metadata
* @returns Stripe customer ID
*/
public async createCustomer(
email: string,
name?: string,
metadata?: Record<string, string>
): Promise<string> {
return this.createOrGetCustomer(email, name, metadata);
}
}