Merge branch '29-refactor-stripe-and-supabase-implementation' of https://github.com/jcreek/SvelteKitSaasBoilerplate into 29-refactor-stripe-and-supabase-implementation

This commit is contained in:
OllyNicholass
2024-09-17 19:13:58 +01:00
4 changed files with 46 additions and 39 deletions
+16 -12
View File
@@ -31,8 +31,11 @@ const upsertProductRecord = async (product: stripe.Product) => {
active: product.active, active: product.active,
name: product.name, name: product.name,
description: product.description ?? null, description: product.description ?? null,
image: product.images?.[0] ?? null, features: product.marketing_features.map(({ name }) => name || '').filter((name) => !!name),
metadata: product.metadata 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]); 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 : '', product_id: typeof price.product === 'string' ? price.product : '',
active: price.active, active: price.active,
currency: price.currency, currency: price.currency,
description: price.nickname ?? null,
metadata: price.metadata,
type: price.type, type: price.type,
unit_amount: price.unit_amount ?? null, unit_amount: price.unit_amount ?? null,
interval: price.recurring?.interval ?? null, interval: price.recurring?.interval ?? null,
@@ -208,9 +213,7 @@ const manageSubscriptionStatusChange = async (
metadata: subscription.metadata, metadata: subscription.metadata,
status: subscription.status, status: subscription.status,
price_id: subscription.items.data[0].price.id, price_id: subscription.items.data[0].price.id,
//TODO check quantity on subscription quantity: 1, //subscription.quantity,
quantity: subscription.quantity,
cancel_at_period_end: subscription.cancel_at_period_end, cancel_at_period_end: subscription.cancel_at_period_end,
cancel_at: subscription.cancel_at ? toDateTime(subscription.cancel_at).toISOString() : null, cancel_at: subscription.cancel_at ? toDateTime(subscription.cancel_at).toISOString() : null,
canceled_at: subscription.canceled_at canceled_at: subscription.canceled_at
@@ -232,13 +235,14 @@ const manageSubscriptionStatusChange = async (
if (upsertError) throw new Error(`Subscription insert/update failed: ${upsertError.message}`); if (upsertError) throw new Error(`Subscription insert/update failed: ${upsertError.message}`);
console.log(`Inserted/updated subscription [${subscription.id}] for user [${uuid}]`); console.log(`Inserted/updated subscription [${subscription.id}] for user [${uuid}]`);
// For a new subscription copy the billing details to the customer object. // // 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. // // NOTE: This is a costly operation and should happen at the very end.
if (createAction && subscription.default_payment_method && uuid) // if (createAction && subscription.default_payment_method && uuid) {
await copyBillingDetailsToCustomer( // await copyBillingDetailsToCustomer(
uuid, // uuid,
subscription.default_payment_method as stripe.PaymentMethod // subscription.default_payment_method as stripe.PaymentMethod
); // );
// }
}; };
export { export {
+16 -8
View File
@@ -6,16 +6,24 @@
$: ({ supabase, session } = data); $: ({ supabase, session } = data);
</script> </script>
<div class="hero bg-base-100 my-36"> {#if session !== null}
<div class="hero-content flex-col lg:flex-row-reverse"> <div class="hero bg-base-100 my-36">
<div class="text-center lg:text-left"> Welcome back {session.user.email}
<h1 class="text-5xl font-bold">Sign up for this amazing SaaS right now - don't miss out!</h1> </div>
</div> {:else}
<div id="sign-up" class="card shrink-0 w-full max-w-sm shadow-2xl"> <div class="hero bg-base-100 my-36">
<SignUp {supabase} /> <div class="hero-content flex-col lg:flex-row-reverse">
<div class="text-center lg:text-left">
<h1 class="text-5xl font-bold">
Sign up for this amazing SaaS right now - don't miss out!
</h1>
</div>
<div id="sign-up" class="card shrink-0 w-full max-w-sm shadow-2xl">
<SignUp {supabase} />
</div>
</div> </div>
</div> </div>
</div> {/if}
<div <div
class="md:container md:mx-auto justify-self-center flex justify-center items-center h-full flex-col mb-10" class="md:container md:mx-auto justify-self-center flex justify-center items-center h-full flex-col mb-10"
+13 -18
View File
@@ -1,7 +1,7 @@
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import stripe from 'stripe'; import stripe from 'stripe';
import { PUBLIC_STRIPE_SECRET_KEY } from '$env/static/public'; import { STRIPE_SECRET_KEY } from '$env/static/private';
import { PUBLIC_STRIPE_ENDPOINT_SECRET } from '$env/static/public'; import { STRIPE_ENDPOINT_SECRET } from '$env/static/private';
import { import {
deletePriceRecord, deletePriceRecord,
deleteProductRecord, deleteProductRecord,
@@ -10,7 +10,7 @@ import {
upsertProductRecord upsertProductRecord
} from '$lib/utils/supabase/admin'; } 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 }) => { export const POST: RequestHandler = async ({ request }) => {
const body = await request.text(); 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. // Only verify the event if you have an endpoint secret defined.
// Otherwise use the basic event deserialized with JSON.parse // Otherwise use the basic event deserialized with JSON.parse
if (PUBLIC_STRIPE_ENDPOINT_SECRET && signature) { if (STRIPE_ENDPOINT_SECRET && signature) {
try { try {
event = stripeClient.webhooks.constructEvent( event = stripeClient.webhooks.constructEvent(body, signature, STRIPE_ENDPOINT_SECRET);
body,
signature,
PUBLIC_STRIPE_ENDPOINT_SECRET
);
} catch (err) { } catch (err) {
console.error(`⚠️ Webhook signature verification failed.`, err.message); console.error(`⚠️ Webhook signature verification failed.`, err.message);
return { return {
@@ -44,6 +40,9 @@ export const POST: RequestHandler = async ({ request }) => {
case 'product.updated': case 'product.updated':
await upsertProductRecord(event.data.object as stripe.Product); await upsertProductRecord(event.data.object as stripe.Product);
break; break;
case 'product.deleted':
await deleteProductRecord(event.data.object as stripe.Product);
break;
case 'price.created': case 'price.created':
case 'price.updated': case 'price.updated':
await upsertPriceRecord(event.data.object as stripe.Price); await upsertPriceRecord(event.data.object as stripe.Price);
@@ -51,12 +50,11 @@ export const POST: RequestHandler = async ({ request }) => {
case 'price.deleted': case 'price.deleted':
await deletePriceRecord(event.data.object as stripe.Price); await deletePriceRecord(event.data.object as stripe.Price);
break; break;
case 'product.deleted':
await deleteProductRecord(event.data.object as stripe.Product);
break;
case 'customer.subscription.created': case 'customer.subscription.created':
case 'customer.subscription.updated': 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; const subscription = event.data.object as stripe.Subscription;
await manageSubscriptionStatusChange( await manageSubscriptionStatusChange(
subscription.id, subscription.id,
@@ -86,10 +84,7 @@ export const POST: RequestHandler = async ({ request }) => {
status: 200 status: 200
}); });
} catch (err) { } catch (err) {
console.log(`Webhook Error: ${err.message}`); const message = err instanceof Error ? err.message : 'An unknown error has occurred';
return { return new Response(`Webhook Error: ${message}`, { status: 500 });
status: 400,
body: `Webhook Error: ${err.message}`
};
} }
}; };
+1 -1
View File
@@ -12,7 +12,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"module": "es2015" "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 // 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 // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes