mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-13 11:03:49 +00:00
feat(#1): use stripe session to populate checkout success
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { formatCurrency } from '$lib/utils/currency';
|
||||||
export let imgAlt = '';
|
export let imgAlt = '';
|
||||||
export let imgSrc = '';
|
export let imgSrc = '';
|
||||||
export let itemCategoryDescription = '';
|
// export let itemCategoryDescription = '';
|
||||||
export let itemName = '';
|
export let itemName = '';
|
||||||
export let price = 0;
|
export let price = 0;
|
||||||
export let quantity = 0;
|
export let quantity = 0;
|
||||||
|
export let currency = 'gbp';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 min-[550px]:gap-6 border-b border-gray-200 py-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 min-[550px]:gap-6 border-b border-gray-200 py-6">
|
||||||
@@ -18,17 +20,17 @@
|
|||||||
<h5 class="font-semibold text-xl leading-8 text-black max-[550px]:text-center">
|
<h5 class="font-semibold text-xl leading-8 text-black max-[550px]:text-center">
|
||||||
{itemName}
|
{itemName}
|
||||||
</h5>
|
</h5>
|
||||||
<p
|
<!-- <p
|
||||||
class="font-normal text-lg leading-8 text-gray-500 my-2 min-[550px]:my-3 max-[550px]:text-center"
|
class="font-normal text-lg leading-8 text-gray-500 my-2 min-[550px]:my-3 max-[550px]:text-center"
|
||||||
>
|
>
|
||||||
{itemCategoryDescription}
|
{itemCategoryDescription}
|
||||||
</p>
|
</p> -->
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<h6
|
<h6
|
||||||
class="font-medium text-lg leading-8 text-indigo-600 max-[550px]:text-center pr-4 mr-4 border-r border-gray-200"
|
class="font-medium text-lg leading-8 text-indigo-600 max-[550px]:text-center pr-4 mr-4 border-r border-gray-200"
|
||||||
>
|
>
|
||||||
£ {price.toFixed(2)}
|
{ formatCurrency(price, currency) }
|
||||||
</h6>
|
</h6>
|
||||||
<p class="font-medium text-base leading-7 text-black">
|
<p class="font-medium text-base leading-7 text-black">
|
||||||
Qty: <span class="text-gray-500">{quantity}</span>
|
Qty: <span class="text-gray-500">{quantity}</span>
|
||||||
@@ -43,7 +45,7 @@
|
|||||||
<h6
|
<h6
|
||||||
class="text-indigo-600 font-manrope font-bold text-2xl leading-9 w-full max-w-[176px] text-center"
|
class="text-indigo-600 font-manrope font-bold text-2xl leading-9 w-full max-w-[176px] text-center"
|
||||||
>
|
>
|
||||||
£{(price * quantity).toFixed(2)}
|
{ formatCurrency(price, currency) }
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export function formatCurrency(amount: number, currency: string): string {
|
||||||
|
return new Intl.NumberFormat('en-GB', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency,
|
||||||
|
}).format(amount);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import stripe from 'stripe';
|
|||||||
const stripeClient = new stripe(PUBLIC_STRIPE_SECRET_KEY);
|
const stripeClient = new stripe(PUBLIC_STRIPE_SECRET_KEY);
|
||||||
|
|
||||||
// Create a checkout session
|
// Create a checkout session
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||||
const formData = new URLSearchParams(await request.text());
|
const formData = new URLSearchParams(await request.text());
|
||||||
const items: { priceId: string; quantity: number }[] = [];
|
const items: { priceId: string; quantity: number }[] = [];
|
||||||
|
|
||||||
@@ -26,8 +26,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||||||
cancel_url: `${request.headers.get('origin')}/checkout/cancelled`
|
cancel_url: `${request.headers.get('origin')}/checkout/cancelled`
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO - Store the checkout session.id for access from the frontend
|
// Store the checkout session.id for access from the frontend
|
||||||
// https://docs.stripe.com/api/checkout/sessions/retrieve
|
cookies.set('checkout_session_id', session.id, { path: '/' });
|
||||||
|
|
||||||
return redirect(303, session.url as string);
|
return redirect(303, session.url as string);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { PUBLIC_STRIPE_SECRET_KEY } from '$env/static/public';
|
||||||
|
import stripe from 'stripe';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
const stripeClient = new stripe(PUBLIC_STRIPE_SECRET_KEY);
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ cookies }) => {
|
||||||
|
const sessionId = cookies.get('checkout_session_id');
|
||||||
|
if (!sessionId) {
|
||||||
|
return json({ error: 'No checkout session found' }, { status: 400 });
|
||||||
|
}
|
||||||
|
const checkoutSession = await stripeClient.checkout.sessions.retrieve(sessionId, { expand: ['line_items'] });
|
||||||
|
|
||||||
|
const statusResponse = {
|
||||||
|
checkoutSession: checkoutSession,
|
||||||
|
paymentStatus: checkoutSession.payment_status,
|
||||||
|
lineItems: checkoutSession.line_items
|
||||||
|
};
|
||||||
|
|
||||||
|
return json(statusResponse);
|
||||||
|
};
|
||||||
@@ -2,19 +2,27 @@
|
|||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { basket, type Basket, type Item } from '$lib/stores/basket.js';
|
import { basket, type Basket, type Item } from '$lib/stores/basket.js';
|
||||||
import OrderConfirmationItem from '$lib/components/checkout/OrderConfirmationItem.svelte';
|
import OrderConfirmationItem from '$lib/components/checkout/OrderConfirmationItem.svelte';
|
||||||
|
import { formatCurrency } from '$lib/utils/currency';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
let orderConfirmationBasket: Basket;
|
export let data: PageData;
|
||||||
let localBasket: Basket;
|
let localBasket: Basket;
|
||||||
const unsubscribe = basket.subscribe((value) => {
|
const unsubscribe = basket.subscribe((value) => {
|
||||||
localBasket = value;
|
localBasket = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let {
|
||||||
|
checkoutSession,
|
||||||
|
lineItems,
|
||||||
|
paymentStatus
|
||||||
|
} = data;
|
||||||
|
|
||||||
onDestroy(unsubscribe);
|
onDestroy(unsubscribe);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
calculateSubtotal();
|
calculateSubtotal();
|
||||||
|
|
||||||
// Create a deep copy of the basket to store the order confirmation then clear the basket
|
// Clear the basket
|
||||||
orderConfirmationBasket = JSON.parse(JSON.stringify(localBasket));
|
|
||||||
localBasket = { items: [], subtotal: 0 };
|
localBasket = { items: [], subtotal: 0 };
|
||||||
basket.set(localBasket);
|
basket.set(localBasket);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
@@ -60,19 +68,17 @@
|
|||||||
>
|
>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="w-full px-3 min-[400px]:px-6">
|
<div class="w-full px-3 min-[400px]:px-6">
|
||||||
{#if orderConfirmationBasket == undefined}
|
{#if lineItems.length == 0}
|
||||||
<p class="font-semibold text-lg leading-7 text-black text-center">
|
<p class="font-semibold text-lg leading-7 text-black text-center">
|
||||||
No items in the basket
|
No items in the basket
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each orderConfirmationBasket.items as item (item.id)}
|
{#each lineItems as item (item.id)}
|
||||||
<OrderConfirmationItem
|
<OrderConfirmationItem
|
||||||
itemCategoryDescription={item.categoryDescription}
|
itemName={item.description}
|
||||||
itemName={item.name}
|
price={item.amount_total / 100}
|
||||||
imgAlt={item.imgAlt}
|
|
||||||
imgSrc={item.imgSrc}
|
|
||||||
price={item.price}
|
|
||||||
quantity={item.quantity}
|
quantity={item.quantity}
|
||||||
|
currency={item.currency}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -82,7 +88,9 @@
|
|||||||
>
|
>
|
||||||
<div class="flex flex-col sm:flex-row items-center max-lg:border-b border-gray-200"></div>
|
<div class="flex flex-col sm:flex-row items-center max-lg:border-b border-gray-200"></div>
|
||||||
<p class="font-semibold text-lg text-black py-6">
|
<p class="font-semibold text-lg text-black py-6">
|
||||||
Total Price: <span class="text-indigo-600"> $xx.xx</span>
|
{#if checkoutSession?.amount_total != null}
|
||||||
|
Total Price: <span class="text-indigo-600"> {formatCurrency(checkoutSession?.amount_total / 100, checkoutSession?.currency ?? 'gbp')}</span>
|
||||||
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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');
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to fetch checkout status');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = await response.json();
|
||||||
|
const checkoutSession: Stripe.Checkout.Session = responseJson.checkoutSession;
|
||||||
|
const lineItems: Stripe.LineItem[] = responseJson.lineItems.data;
|
||||||
|
const paymentStatus: Stripe.Checkout.Session.PaymentStatus = responseJson.paymentStatus;
|
||||||
|
|
||||||
|
if (paymentStatus == 'unpaid') {
|
||||||
|
throw redirect(303, '/checkout/cancelled');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
checkoutSession,
|
||||||
|
lineItems,
|
||||||
|
paymentStatus
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user