mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-13 02:53:50 +00:00
feat(#32): Add separate flow for subscriptions
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
import { type RequestHandler, redirect } from '@sveltejs/kit';
|
||||||
|
import { stripe as stripeClient } from '$lib/utils/stripe';
|
||||||
|
|
||||||
|
// Create a subscription checkout session
|
||||||
|
export const POST: RequestHandler = async ({ request, cookies, locals: { safeGetSession } }) => {
|
||||||
|
const { session } = await safeGetSession();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
// If no session is found, the user isn't authenticated, so block the action
|
||||||
|
return new Response('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id;
|
||||||
|
|
||||||
|
// Get the price ID from the request body
|
||||||
|
const { priceId } = await request.json();
|
||||||
|
|
||||||
|
if (!priceId) {
|
||||||
|
return new Response('Price ID is required', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a subscription checkout session with Stripe
|
||||||
|
const checkoutSession = await stripeClient.checkout.sessions.create({
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price: priceId,
|
||||||
|
quantity: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
mode: 'subscription',
|
||||||
|
success_url: `${request.headers.get('origin')}/checkout/success`,
|
||||||
|
cancel_url: `${request.headers.get('origin')}/checkout/cancelled`,
|
||||||
|
metadata: {
|
||||||
|
userId: userId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the checkout session.id for access from the frontend
|
||||||
|
cookies.set('checkout_session_id', checkoutSession.id, { path: '/' });
|
||||||
|
return redirect(303, checkoutSession.url as string);
|
||||||
|
};
|
||||||
@@ -44,6 +44,20 @@
|
|||||||
}, 5000);
|
}, 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let item = {};
|
let item = {};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -58,7 +72,8 @@
|
|||||||
imgSrc: product.images[0],
|
imgSrc: product.images[0],
|
||||||
price: 0,
|
price: 0,
|
||||||
priceId: product.default_price,
|
priceId: product.default_price,
|
||||||
quantity: 1
|
quantity: 1,
|
||||||
|
isSubscription: product.isSubscription
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch the price of the product
|
// Fetch the price of the product
|
||||||
@@ -301,99 +316,101 @@
|
|||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 py-8">
|
{#if !item.isSubscription}
|
||||||
<div class="flex sm:items-center sm:justify-center w-full">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 py-8">
|
||||||
<button
|
<div class="flex sm:items-center sm:justify-center w-full">
|
||||||
class="group py-4 px-6 border border-gray-400 rounded-l-full bg-white transition-all duration-300 hover:bg-gray-50 hover:shadow-sm hover:shadow-gray-300"
|
<button
|
||||||
>
|
class="group py-4 px-6 border border-gray-400 rounded-l-full bg-white transition-all duration-300 hover:bg-gray-50 hover:shadow-sm hover:shadow-gray-300"
|
||||||
<svg
|
|
||||||
class="stroke-gray-900 group-hover:stroke-black"
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 22 22"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
>
|
||||||
<path d="M16.5 11H5.5" stroke="" stroke-width="1.6" stroke-linecap="round" />
|
<svg
|
||||||
<path
|
class="stroke-gray-900 group-hover:stroke-black"
|
||||||
d="M16.5 11H5.5"
|
width="22"
|
||||||
stroke=""
|
height="22"
|
||||||
stroke-opacity="0.2"
|
viewBox="0 0 22 22"
|
||||||
stroke-width="1.6"
|
fill="none"
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
/>
|
>
|
||||||
<path
|
<path d="M16.5 11H5.5" stroke="" stroke-width="1.6" stroke-linecap="round" />
|
||||||
d="M16.5 11H5.5"
|
<path
|
||||||
stroke=""
|
d="M16.5 11H5.5"
|
||||||
stroke-opacity="0.2"
|
stroke=""
|
||||||
stroke-width="1.6"
|
stroke-opacity="0.2"
|
||||||
stroke-linecap="round"
|
stroke-width="1.6"
|
||||||
/>
|
stroke-linecap="round"
|
||||||
</svg>
|
/>
|
||||||
</button>
|
<path
|
||||||
<input
|
d="M16.5 11H5.5"
|
||||||
type="text"
|
stroke=""
|
||||||
class="font-semibold text-gray-900 cursor-pointer text-lg py-[13px] px-6 w-full sm:max-w-[118px] outline-0 border-y border-gray-400 bg-transparent placeholder:text-gray-900 text-center hover:bg-gray-50"
|
stroke-opacity="0.2"
|
||||||
placeholder="1"
|
stroke-width="1.6"
|
||||||
/>
|
stroke-linecap="round"
|
||||||
<button
|
/>
|
||||||
class="group py-4 px-6 border border-gray-400 rounded-r-full bg-white transition-all duration-300 hover:bg-gray-50 hover:shadow-sm hover:shadow-gray-300"
|
</svg>
|
||||||
>
|
</button>
|
||||||
<svg
|
<input
|
||||||
class="stroke-gray-900 group-hover:stroke-black"
|
type="text"
|
||||||
width="22"
|
class="font-semibold text-gray-900 cursor-pointer text-lg py-[13px] px-6 w-full sm:max-w-[118px] outline-0 border-y border-gray-400 bg-transparent placeholder:text-gray-900 text-center hover:bg-gray-50"
|
||||||
height="22"
|
placeholder="1"
|
||||||
viewBox="0 0 22 22"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M11 5.5V16.5M16.5 11H5.5"
|
|
||||||
stroke="#9CA3AF"
|
|
||||||
stroke-width="1.6"
|
|
||||||
stroke-linecap="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M11 5.5V16.5M16.5 11H5.5"
|
|
||||||
stroke="black"
|
|
||||||
stroke-opacity="0.2"
|
|
||||||
stroke-width="1.6"
|
|
||||||
stroke-linecap="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M11 5.5V16.5M16.5 11H5.5"
|
|
||||||
stroke="black"
|
|
||||||
stroke-opacity="0.2"
|
|
||||||
stroke-width="1.6"
|
|
||||||
stroke-linecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="group py-4 px-5 rounded-full bg-indigo-50 text-indigo-600 font-semibold text-lg w-full flex items-center justify-center gap-2 transition-all duration-500 hover:bg-indigo-100"
|
|
||||||
on:click={addToBasket}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="stroke-indigo-600"
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 22 22"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10.7394 17.875C10.7394 18.6344 10.1062 19.25 9.32511 19.25C8.54402 19.25 7.91083 18.6344 7.91083 17.875M16.3965 17.875C16.3965 18.6344 15.7633 19.25 14.9823 19.25C14.2012 19.25 13.568 18.6344 13.568 17.875M4.1394 5.5L5.46568 12.5908C5.73339 14.0221 5.86724 14.7377 6.37649 15.1605C6.88573 15.5833 7.61377 15.5833 9.06984 15.5833H15.2379C16.6941 15.5833 17.4222 15.5833 17.9314 15.1605C18.4407 14.7376 18.5745 14.0219 18.8421 12.5906L19.3564 9.84059C19.7324 7.82973 19.9203 6.8243 19.3705 6.16215C18.8207 5.5 17.7979 5.5 15.7522 5.5H4.1394ZM4.1394 5.5L3.66797 2.75"
|
|
||||||
stroke=""
|
|
||||||
stroke-width="1.6"
|
|
||||||
stroke-linecap="round"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
<button
|
||||||
Add to basket</button
|
class="group py-4 px-6 border border-gray-400 rounded-r-full bg-white transition-all duration-300 hover:bg-gray-50 hover:shadow-sm hover:shadow-gray-300"
|
||||||
>
|
>
|
||||||
</div>
|
<svg
|
||||||
<div class="flex items-center gap-3">
|
class="stroke-gray-900 group-hover:stroke-black"
|
||||||
<button
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 22 22"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11 5.5V16.5M16.5 11H5.5"
|
||||||
|
stroke="#9CA3AF"
|
||||||
|
stroke-width="1.6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11 5.5V16.5M16.5 11H5.5"
|
||||||
|
stroke="black"
|
||||||
|
stroke-opacity="0.2"
|
||||||
|
stroke-width="1.6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11 5.5V16.5M16.5 11H5.5"
|
||||||
|
stroke="black"
|
||||||
|
stroke-opacity="0.2"
|
||||||
|
stroke-width="1.6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="group py-4 px-5 rounded-full bg-indigo-50 text-indigo-600 font-semibold text-lg w-full flex items-center justify-center gap-2 transition-all duration-500 hover:bg-indigo-100"
|
||||||
|
on:click={addToBasket}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="stroke-indigo-600"
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 22 22"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.7394 17.875C10.7394 18.6344 10.1062 19.25 9.32511 19.25C8.54402 19.25 7.91083 18.6344 7.91083 17.875M16.3965 17.875C16.3965 18.6344 15.7633 19.25 14.9823 19.25C14.2012 19.25 13.568 18.6344 13.568 17.875M4.1394 5.5L5.46568 12.5908C5.73339 14.0221 5.86724 14.7377 6.37649 15.1605C6.88573 15.5833 7.61377 15.5833 9.06984 15.5833H15.2379C16.6941 15.5833 17.4222 15.5833 17.9314 15.1605C18.4407 14.7376 18.5745 14.0219 18.8421 12.5906L19.3564 9.84059C19.7324 7.82973 19.9203 6.8243 19.3705 6.16215C18.8207 5.5 17.7979 5.5 15.7522 5.5H4.1394ZM4.1394 5.5L3.66797 2.75"
|
||||||
|
stroke=""
|
||||||
|
stroke-width="1.6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Add to basket</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- <button
|
||||||
class="group transition-all duration-500 p-4 rounded-full bg-indigo-50 hover:bg-indigo-100 hover:shadow-sm hover:shadow-indigo-300"
|
class="group transition-all duration-500 p-4 rounded-full bg-indigo-50 hover:bg-indigo-100 hover:shadow-sm hover:shadow-indigo-300"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -412,13 +429,15 @@
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button> -->
|
||||||
<button
|
<button
|
||||||
class="text-center w-full px-5 py-4 rounded-[100px] bg-indigo-600 flex items-center justify-center font-semibold text-lg text-white shadow-sm transition-all duration-500 hover:bg-indigo-700 hover:shadow-indigo-400"
|
class="text-center w-full px-5 py-4 rounded-[100px] bg-indigo-600 flex items-center justify-center font-semibold text-lg text-white shadow-sm transition-all duration-500 hover:bg-indigo-700 hover:shadow-indigo-400"
|
||||||
>
|
on:click={startSubscription}
|
||||||
Buy Now
|
>
|
||||||
</button>
|
Subscribe
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,5 +4,10 @@ import { stripe as stripeClient } from '$lib/utils/stripe';
|
|||||||
export const GET = async ({ params }) => {
|
export const GET = async ({ params }) => {
|
||||||
const { productId } = params;
|
const { productId } = params;
|
||||||
const product = await stripeClient.products.retrieve(productId);
|
const product = await stripeClient.products.retrieve(productId);
|
||||||
|
|
||||||
|
const price = await stripeClient.prices.retrieve(product.default_price);
|
||||||
|
|
||||||
|
const isSubscription = price.type === 'recurring';
|
||||||
|
product.isSubscription = isSubscription;
|
||||||
return json(product);
|
return json(product);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user