From 65fdc1834aebaf397f3f6997531988086a5e9fcf Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 10 Sep 2024 20:44:19 +0100 Subject: [PATCH 1/5] chore(*): Ensure env vars get strongly typed --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index c545d33..611f6e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "moduleResolution": "bundler", "module": "es2015" }, - "include": ["src/**/*", "node_modules/**/*.d.ts"] + "include": ["src/**/*", "node_modules/**/*.d.ts", ".svelte-kit/ambient.d.ts"] // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes From 31a81f6312087ae9f1ab525d7ca91229b39e89bd Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 10 Sep 2024 20:45:43 +0100 Subject: [PATCH 2/5] feat(#29): Add conditional switch for sign in/up or welcome message --- src/routes/+page.svelte | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 136c686..f0db6cd 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -6,16 +6,24 @@ $: ({ supabase, session } = data); -
-
-
-

Sign up for this amazing SaaS right now - don't miss out!

-
-
- +{#if session !== null} +
+ Welcome back {session.user.email} +
+{:else} +
+
+
+

+ Sign up for this amazing SaaS right now - don't miss out! +

+
+
+ +
-
+{/if}
Date: Tue, 10 Sep 2024 20:54:59 +0100 Subject: [PATCH 3/5] feat(#29): Add additional stripe webhook event handlers --- src/routes/api/webhook/stripe/+server.ts | 31 ++++++++++-------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/routes/api/webhook/stripe/+server.ts b/src/routes/api/webhook/stripe/+server.ts index 7370213..eb79fd8 100644 --- a/src/routes/api/webhook/stripe/+server.ts +++ b/src/routes/api/webhook/stripe/+server.ts @@ -1,7 +1,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import stripe from 'stripe'; -import { PUBLIC_STRIPE_SECRET_KEY } from '$env/static/public'; -import { PUBLIC_STRIPE_ENDPOINT_SECRET } from '$env/static/public'; +import { STRIPE_SECRET_KEY } from '$env/static/private'; +import { STRIPE_ENDPOINT_SECRET } from '$env/static/private'; import { deletePriceRecord, deleteProductRecord, @@ -10,7 +10,7 @@ import { upsertProductRecord } from '$lib/utils/supabase/admin'; -const stripeClient = new stripe(PUBLIC_STRIPE_SECRET_KEY); +const stripeClient = new stripe(STRIPE_SECRET_KEY); export const POST: RequestHandler = async ({ request }) => { const body = await request.text(); @@ -23,13 +23,9 @@ export const POST: RequestHandler = async ({ request }) => { // Only verify the event if you have an endpoint secret defined. // Otherwise use the basic event deserialized with JSON.parse - if (PUBLIC_STRIPE_ENDPOINT_SECRET && signature) { + if (STRIPE_ENDPOINT_SECRET && signature) { try { - event = stripeClient.webhooks.constructEvent( - body, - signature, - PUBLIC_STRIPE_ENDPOINT_SECRET - ); + event = stripeClient.webhooks.constructEvent(body, signature, STRIPE_ENDPOINT_SECRET); } catch (err) { console.error(`⚠️ Webhook signature verification failed.`, err.message); return { @@ -44,6 +40,9 @@ export const POST: RequestHandler = async ({ request }) => { case 'product.updated': await upsertProductRecord(event.data.object as stripe.Product); break; + case 'product.deleted': + await deleteProductRecord(event.data.object as stripe.Product); + break; case 'price.created': case 'price.updated': await upsertPriceRecord(event.data.object as stripe.Price); @@ -51,12 +50,11 @@ export const POST: RequestHandler = async ({ request }) => { case 'price.deleted': await deletePriceRecord(event.data.object as stripe.Price); break; - case 'product.deleted': - await deleteProductRecord(event.data.object as stripe.Product); - break; case 'customer.subscription.created': case 'customer.subscription.updated': - case 'customer.subscription.deleted': { + case 'customer.subscription.deleted': + case 'customer.subscription.paused': + case 'customer.subscription.resumed': { const subscription = event.data.object as stripe.Subscription; await manageSubscriptionStatusChange( subscription.id, @@ -86,10 +84,7 @@ export const POST: RequestHandler = async ({ request }) => { status: 200 }); } catch (err) { - console.log(`Webhook Error: ${err.message}`); - return { - status: 400, - body: `Webhook Error: ${err.message}` - }; + const message = err instanceof Error ? err.message : 'An unknown error has occurred'; + return new Response(`Webhook Error: ${message}`, { status: 500 }); } }; From b365d77c16ae3459e9c6018e787a66997184d728 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:25:44 +0100 Subject: [PATCH 4/5] feat(#29): Handle upserting new fields for products and prices --- src/lib/utils/supabase/admin.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/utils/supabase/admin.ts b/src/lib/utils/supabase/admin.ts index a5a53eb..874255b 100644 --- a/src/lib/utils/supabase/admin.ts +++ b/src/lib/utils/supabase/admin.ts @@ -31,8 +31,11 @@ const upsertProductRecord = async (product: stripe.Product) => { active: product.active, name: product.name, description: product.description ?? null, - image: product.images?.[0] ?? null, - metadata: product.metadata + features: product.marketing_features.map(({ name }) => name || '').filter((name) => !!name), + images: product.images ?? null, + metadata: product.metadata, + created: new Date(product.created * 1000).toISOString(), + updated: new Date(product.updated * 1000).toISOString() }; const { error: upsertError } = await supabaseAdmin.from('products').upsert([productData]); @@ -46,6 +49,8 @@ const upsertPriceRecord = async (price: stripe.Price, retryCount = 0, maxRetries product_id: typeof price.product === 'string' ? price.product : '', active: price.active, currency: price.currency, + description: price.nickname ?? null, + metadata: price.metadata, type: price.type, unit_amount: price.unit_amount ?? null, interval: price.recurring?.interval ?? null, From 7e0c06bab3f5b4e1ee975151f09372cada151c23 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:47:46 +0100 Subject: [PATCH 5/5] fix(#29): Ensure updating subscription works properly --- src/lib/utils/supabase/admin.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/lib/utils/supabase/admin.ts b/src/lib/utils/supabase/admin.ts index 874255b..6a774d6 100644 --- a/src/lib/utils/supabase/admin.ts +++ b/src/lib/utils/supabase/admin.ts @@ -213,9 +213,7 @@ const manageSubscriptionStatusChange = async ( metadata: subscription.metadata, status: subscription.status, price_id: subscription.items.data[0].price.id, - //TODO check quantity on subscription - - quantity: subscription.quantity, + quantity: 1, //subscription.quantity, cancel_at_period_end: subscription.cancel_at_period_end, cancel_at: subscription.cancel_at ? toDateTime(subscription.cancel_at).toISOString() : null, canceled_at: subscription.canceled_at @@ -237,13 +235,14 @@ const manageSubscriptionStatusChange = async ( if (upsertError) throw new Error(`Subscription insert/update failed: ${upsertError.message}`); console.log(`Inserted/updated subscription [${subscription.id}] for user [${uuid}]`); - // For a new subscription copy the billing details to the customer object. - // NOTE: This is a costly operation and should happen at the very end. - if (createAction && subscription.default_payment_method && uuid) - await copyBillingDetailsToCustomer( - uuid, - subscription.default_payment_method as stripe.PaymentMethod - ); + // // For a new subscription copy the billing details to the customer object. + // // NOTE: This is a costly operation and should happen at the very end. + // if (createAction && subscription.default_payment_method && uuid) { + // await copyBillingDetailsToCustomer( + // uuid, + // subscription.default_payment_method as stripe.PaymentMethod + // ); + // } }; export {