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:
parent
c577cbc933
commit
a7d2ddef1a
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user