Using Lemon Squeezy with Svelte and SvelteKit

Published: 2023-07-16

Audjust (formerly Mofi) is a SvelteKit application and uses Lemon Squeezy to process payments for our subscriptions. We want to share a bit on how we integrate the latter to make for a more seamless developer experience since there is no native Svelte component for Lemon Squeezy.

Along with a storefront hosted by them, Lemon Squeezy provides the option of showing the checkout flow as an overlay on top of the website. This provides a more seamless checkout experience as the user does not have to leave our website. To show this checkout flow, they provide a helper script lemon.js that one can embed on their site and looks for links with the lemonsqueezy-button class. But in our case, we wanted to trigger the overlay as part of a Svelte component. How do we do that?

Loading and Initializing the lemon.js Library

In our codebase, we have a helper TypeScript file called lemonsqueezy.ts with some glue code. It looks something like this:

import { browser } from "$app/environment";

const windowWithLemonSqueezy = browser
	? (window as unknown as {
			LemonSqueezy: any;
			createLemonSqueezy: () => void;
	  })
	: undefined;

export async function openLemonSqueezyUrl(s: string) {
	const LemonSqueezy = await getLemonSqueezy();
	return LemonSqueezy.Url.Open(s);
}

async function getLemonSqueezy() {
	if (!windowWithLemonSqueezy?.LemonSqueezy) {
		await loadAndInitialize();
	}

	return windowWithLemonSqueezy?.LemonSqueezy;
}

function loadAndInitialize() {
	return new Promise<void>((resolve) => {
		const script = document.createElement("script");
		script.onload = function () {
			windowWithLemonSqueezy?.createLemonSqueezy();
			resolve();
		};
		script.src = "https://assets.lemonsqueezy.com/lemon.js";

		document.head.appendChild(script);
	});
}

We use a special windowWithLemonSqueezy variable which is cast to a type that matches the properties added by Lemon Squeezy’s script to the window object to use TypeScript’s typing. Since we want to support SvelteKit’s server-side rendering functionality, we check to make sure to only access the window object when running in the browser.

This file exports a single openLemonSqueezyUrl function that can be used to load a Lemon Squeezy URL (here is their documentation describing this functionality).

As a side effect, this exported function loads and initialized the lemon.js library if not already loaded. We push off loading this script until the last possible moment to avoid loading in most cases (page loads where the user will not trigger the overlay vastly outweight those where they do).

Example Usage

When the user chooses to purchase a plan, we need to show the overlay. Since Audjust supports light and dark themes, we want to customize the Lemon Squeezy overlay to match. We can do that by toggling the dark query parameter depending on the user’s color scheme. We also pass in some custom data like the user’s name and email to pre-populate the checkout form. Finally, we also pass in the user ID as custom data to be able to match the transaction against our user database later on.

function purchasePlan() {
	const itemId = "YOUR-ITEM-ID";
	const isDarkMode = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
	const checkoutUrl =
		`https://YOUR-STORE.lemonsqueezy.com/checkout/buy/${itemId}?` +
		new URLSearchParams({
			embed: "1",
			dark: isDarkMode ? "1" : "0",
			"checkout[email]": $user.email,
			"checkout[name]": $user.displayName,
			"checkout[custom][user_id]": $user.uid,
		}).toString();

	openLemonSqueezyUrl(checkoutUrl);
}

Finishing the Transaction

While outside of SvelteKit, we wanted to include this part for completeness. After the user completes their transaction in the checkout form, Lemon Squeezy triggers a webhook that then updates our user records with the successful transaction.

The data received by the webhook looks something like this:

{
	"data": {
		"id": "12345",
		"type": "subscriptions",
		...
	},
	"meta": {
		"event_name": "subscription_created",
		"custom_data": {
			"user_id": "USER_ID"
		}
	}
}

The value of meta.custom_data.user_id here matches the value passed in from the purchasePlan function above and can be used to look up the user.

Conclusion

In case you are integrating Lemon Squeezy in your Svelte or SvelteKit application, hopefully this overview provides you a helpful starting point for your approach!

Try Audjust →
More blog entries →