From 03157a6d2fc5184b79518481639e557203f6be99 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 20 Oct 2024 17:21:43 +0100 Subject: [PATCH 1/7] fix(#45): Enable customer creation when creating a stripe checkout session --- src/routes/api/checkout/+server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/api/checkout/+server.ts b/src/routes/api/checkout/+server.ts index 3753ec7..35794c4 100644 --- a/src/routes/api/checkout/+server.ts +++ b/src/routes/api/checkout/+server.ts @@ -27,6 +27,7 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet })); const checkoutSession = await stripeClient.checkout.sessions.create({ + customer_creation: 'always', customer_email: userEmail, line_items: lineItems, mode: 'payment', From 7d994d1c09a94a6178c52ee6aaf96e8e83726b80 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 20 Oct 2024 18:37:40 +0100 Subject: [PATCH 2/7] feat(#31): Add ability to record the stripe customer id in the customers table when a purchase is made --- src/lib/utils/supabase/admin.ts | 29 ++++++++++++++++++- src/routes/api/checkout/+server.ts | 28 +++++++++++++++--- .../api/checkout/create-customer/+server.ts | 16 ++++++++++ src/routes/checkout/success/+page.ts | 15 ++++++++-- 4 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 src/routes/api/checkout/create-customer/+server.ts diff --git a/src/lib/utils/supabase/admin.ts b/src/lib/utils/supabase/admin.ts index 63f5c0b..31a1bf0 100644 --- a/src/lib/utils/supabase/admin.ts +++ b/src/lib/utils/supabase/admin.ts @@ -110,6 +110,31 @@ const createCustomerInStripe = async (uuid: string, email: string) => { return newCustomer.id; }; +const getStripeCustomerId = async (email: string, uuid: string) => { + const { data: existingSupabaseCustomer, error: queryError } = await supabaseAdmin + .from('customers') + .select('*') + .eq('id', uuid) + .maybeSingle(); + + if (queryError) { + throw new Error(`Supabase customer lookup failed: ${queryError.message}`); + } + + let stripeCustomerId: string | undefined; + if (existingSupabaseCustomer?.stripe_customer_id) { + const existingStripeCustomer = await stripeClient.customers.retrieve( + existingSupabaseCustomer.stripe_customer_id + ); + stripeCustomerId = existingStripeCustomer.id; + } else { + const stripeCustomers = await stripeClient.customers.list({ email: email }); + stripeCustomerId = stripeCustomers.data.length > 0 ? stripeCustomers.data[0].id : undefined; + } + + return stripeCustomerId; +}; + const createOrRetrieveCustomer = async ({ email, uuid }: { email: string; uuid: string }) => { // Check if the customer already exists in Supabase const { data: existingSupabaseCustomer, error: queryError } = await supabaseAdmin @@ -453,5 +478,7 @@ export { addCredits, deductCredits, getUserCredits, - getActiveProductsWithPrices + getActiveProductsWithPrices, + upsertCustomerToSupabase, + getStripeCustomerId }; diff --git a/src/routes/api/checkout/+server.ts b/src/routes/api/checkout/+server.ts index 35794c4..05d3272 100644 --- a/src/routes/api/checkout/+server.ts +++ b/src/routes/api/checkout/+server.ts @@ -1,5 +1,7 @@ import { type RequestHandler, redirect } from '@sveltejs/kit'; import { stripe as stripeClient } from '$lib/utils/stripe'; +import { getStripeCustomerId } from '$lib/utils/supabase/admin'; +import type Stripe from 'stripe'; // Create a checkout session export const POST: RequestHandler = async ({ request, cookies, locals: { safeGetSession } }) => { @@ -26,9 +28,16 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet quantity: item.quantity })); - const checkoutSession = await stripeClient.checkout.sessions.create({ - customer_creation: 'always', - customer_email: userEmail, + let stripeCustomerId = null; + + // Get the existing Stripe customer ID from Supabase, if it exists + try { + stripeCustomerId = await getStripeCustomerId(userEmail!, userId); + } catch (error) { + console.error('Error fetching Stripe customer ID:', error); + } + + const stripeCheckoutSessionObject = { line_items: lineItems, mode: 'payment', success_url: `${request.headers.get('origin')}/checkout/success`, @@ -36,7 +45,18 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet metadata: { userId: userId } - }); + } as Stripe.Checkout.SessionCreateParams; + + if (stripeCustomerId) { + // If the user has a Stripe customer ID, attach it to the checkout session + stripeCheckoutSessionObject.customer = stripeCustomerId; + } else { + // If the user doesn't have a Stripe customer ID, create a new customer + stripeCheckoutSessionObject.customer_creation = 'always'; + stripeCheckoutSessionObject.customer_email = userEmail; + } + + const checkoutSession = await stripeClient.checkout.sessions.create(stripeCheckoutSessionObject); // Store the checkout session.id for access from the frontend cookies.set('checkout_session_id', checkoutSession.id, { path: '/' }); diff --git a/src/routes/api/checkout/create-customer/+server.ts b/src/routes/api/checkout/create-customer/+server.ts new file mode 100644 index 0000000..e8926f4 --- /dev/null +++ b/src/routes/api/checkout/create-customer/+server.ts @@ -0,0 +1,16 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { upsertCustomerToSupabase } from '$lib/utils/supabase/admin'; + +export const GET: RequestHandler = async ({ url, locals: { safeGetSession } }) => { + const stripeCustomerId = url.searchParams.get('stripeCustomerId'); + const { session } = await safeGetSession(); + + if (!session || !stripeCustomerId) { + return json({ error: 'Unauthorized' }, { status: 401 }); + } + + await upsertCustomerToSupabase(session?.user.id, stripeCustomerId); + + return json({ message: 'Customer created' }); +}; diff --git a/src/routes/checkout/success/+page.ts b/src/routes/checkout/success/+page.ts index 82c4ce4..63fc4ce 100644 --- a/src/routes/checkout/success/+page.ts +++ b/src/routes/checkout/success/+page.ts @@ -2,8 +2,8 @@ import type { PageLoad } from './$types'; import { redirect } from '@sveltejs/kit'; import Stripe from 'stripe'; -export const load: PageLoad = async ({fetch}) => { - const response = await fetch('/api/checkout/status'); +export const load: PageLoad = async ({ fetch }) => { + const response = await fetch('/api/checkout/status'); if (!response.ok) { console.error('Failed to fetch checkout status'); return; @@ -18,10 +18,19 @@ export const load: PageLoad = async ({fetch}) => { throw redirect(303, '/checkout/cancelled'); } + // Save the stripe customer id in the customers table + const stripeCustomerId = checkoutSession.customer as string; + console.log('stripeCustomerId', stripeCustomerId); + const createCustomerResponse = await fetch( + '/api/checkout/create-customer?stripeCustomerId=' + stripeCustomerId + ); + if (!createCustomerResponse.ok) { + console.error('Failed to create customer'); + } + return { checkoutSession, lineItems, paymentStatus }; }; - From 4c44f772e51fcc816a1da40cccb3f2cf6232ac99 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:50:07 +0100 Subject: [PATCH 3/7] feat(#31): Add support for subscriptions to create stripe customers --- src/routes/api/subscribe/+server.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/routes/api/subscribe/+server.ts b/src/routes/api/subscribe/+server.ts index ca7dafc..dff594b 100644 --- a/src/routes/api/subscribe/+server.ts +++ b/src/routes/api/subscribe/+server.ts @@ -1,5 +1,7 @@ import { type RequestHandler, redirect } from '@sveltejs/kit'; import { stripe as stripeClient } from '$lib/utils/stripe'; +import { getStripeCustomerId } from '$lib/utils/supabase/admin'; +import type Stripe from 'stripe'; // Create a subscription checkout session export const POST: RequestHandler = async ({ request, cookies, locals: { safeGetSession } }) => { @@ -11,6 +13,7 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet } const userId = session.user.id; + const userEmail = session.user.email; // Get the price ID from the request body const { priceId } = await request.json(); @@ -19,8 +22,16 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet return new Response('Price ID is required', { status: 400 }); } - // Create a subscription checkout session with Stripe - const checkoutSession = await stripeClient.checkout.sessions.create({ + let stripeCustomerId = null; + + // Get the existing Stripe customer ID from Supabase, if it exists + try { + stripeCustomerId = await getStripeCustomerId(userEmail!, userId); + } catch (error) { + console.error('Error fetching Stripe customer ID:', error); + } + + const stripeCheckoutSessionObject = { line_items: [ { price: priceId, @@ -33,7 +44,19 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet metadata: { userId: userId } - }); + } as Stripe.Checkout.SessionCreateParams; + + if (stripeCustomerId) { + // If the user has a Stripe customer ID, attach it to the checkout session + stripeCheckoutSessionObject.customer = stripeCustomerId; + } else { + // If the user doesn't have a Stripe customer ID, create a new customer + stripeCheckoutSessionObject.customer_creation = 'always'; + stripeCheckoutSessionObject.customer_email = userEmail; + } + + // Create a subscription checkout session with Stripe + const checkoutSession = await stripeClient.checkout.sessions.create(stripeCheckoutSessionObject); // Store the checkout session.id for access from the frontend cookies.set('checkout_session_id', checkoutSession.id, { path: '/' }); From f3e6c236d50d712c558e7eb3709412b770ef5ce3 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:15:56 +0100 Subject: [PATCH 4/7] fix(*): Prevent CORS errors with subscriptions --- src/routes/api/subscribe/+server.ts | 5 +-- src/routes/products/[productId]/+page.svelte | 32 +++++++------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/routes/api/subscribe/+server.ts b/src/routes/api/subscribe/+server.ts index dff594b..f03b008 100644 --- a/src/routes/api/subscribe/+server.ts +++ b/src/routes/api/subscribe/+server.ts @@ -15,8 +15,9 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet const userId = session.user.id; const userEmail = session.user.email; - // Get the price ID from the request body - const { priceId } = await request.json(); + // Get the price ID from the request form data + const formData = await request.formData(); + const priceId = formData.get('priceId'); if (!priceId) { return new Response('Price ID is required', { status: 400 }); diff --git a/src/routes/products/[productId]/+page.svelte b/src/routes/products/[productId]/+page.svelte index a481043..54c6e1d 100644 --- a/src/routes/products/[productId]/+page.svelte +++ b/src/routes/products/[productId]/+page.svelte @@ -50,20 +50,6 @@ }); }, 5000); } - - function startSubscription() { - fetch('/api/subscribe', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ priceId: item.priceId }) - }) - .then((response) => response.json()) - .then((data) => { - console.log(data); - }); - }
@@ -85,7 +71,8 @@
- £{item.price} {item.isSubscription ? `/ ${item.interval}` : ''} + £{item.price} + {item.isSubscription ? `/ ${item.interval}` : ''}
@@ -373,12 +360,15 @@ {/if} {#if item.isSubscription}
- +
+ + +
{/if} {/if} From 570b11ef2a191563f52a69df708e0e9564e2cb96 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 22 Oct 2024 19:14:16 +0100 Subject: [PATCH 5/7] fix(#45): Remove customer creation from subscription checkout mode --- src/routes/api/subscribe/+server.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/routes/api/subscribe/+server.ts b/src/routes/api/subscribe/+server.ts index f03b008..403f5e4 100644 --- a/src/routes/api/subscribe/+server.ts +++ b/src/routes/api/subscribe/+server.ts @@ -50,10 +50,6 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet if (stripeCustomerId) { // If the user has a Stripe customer ID, attach it to the checkout session stripeCheckoutSessionObject.customer = stripeCustomerId; - } else { - // If the user doesn't have a Stripe customer ID, create a new customer - stripeCheckoutSessionObject.customer_creation = 'always'; - stripeCheckoutSessionObject.customer_email = userEmail; } // Create a subscription checkout session with Stripe From a33779ce53ab9a2881a419d6077629309a41775c Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 22 Oct 2024 19:16:09 +0100 Subject: [PATCH 6/7] fix(#45): Re-add email --- src/routes/api/subscribe/+server.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/routes/api/subscribe/+server.ts b/src/routes/api/subscribe/+server.ts index 403f5e4..bcd7168 100644 --- a/src/routes/api/subscribe/+server.ts +++ b/src/routes/api/subscribe/+server.ts @@ -50,6 +50,9 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet if (stripeCustomerId) { // If the user has a Stripe customer ID, attach it to the checkout session stripeCheckoutSessionObject.customer = stripeCustomerId; + } else { + // If the user doesn't have a Stripe customer ID, create a new customer with their email + stripeCheckoutSessionObject.customer_email = userEmail; } // Create a subscription checkout session with Stripe From e47fa54ea8d8a52b380b76399b9ede358fb2fd64 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 22 Oct 2024 19:21:38 +0100 Subject: [PATCH 7/7] chore(*): Remove console log --- src/routes/checkout/success/+page.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/checkout/success/+page.ts b/src/routes/checkout/success/+page.ts index 63fc4ce..19e3544 100644 --- a/src/routes/checkout/success/+page.ts +++ b/src/routes/checkout/success/+page.ts @@ -20,7 +20,6 @@ export const load: PageLoad = async ({ fetch }) => { // Save the stripe customer id in the customers table const stripeCustomerId = checkoutSession.customer as string; - console.log('stripeCustomerId', stripeCustomerId); const createCustomerResponse = await fetch( '/api/checkout/create-customer?stripeCustomerId=' + stripeCustomerId );