This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
17.

Embedding the LemonSqueezy Checkout Overlay

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

In the last chapter, we got our hands dirty and built a custom LemonSqueezy Stimulus controller. It pulls up the LemonSqueezy checkout page in an iFrame right in our site. That's pretty sweet, but what if I told you we can make it even better? We're going to lay the checkout page on top of our cart page and create a real overlay - like a modal.

But before we dive in, let's add a couple key features to the "Checkout with LemonSqueezy" button:

  • Preventing double clicks
  • And showing loading progress

If you were curious why we created a custom Stimulus controller for the checkout link, this is why!

Preventing Double Clicks and Showing Loading Progress

To kick things off, open lemon-squeezy_controller.js. We're going to create some private methods here. Start with #disableLink() and pass in a link argument. Then add #enableLink() and also pass link as the argument.

// ... lines 1 - 2
export default class extends Controller {
// ... lines 4 - 41
#disableLink(link) {
// ... lines 43 - 45
}
#enableLink(link) {
// ... lines 49 - 51
}
}

For #disableLink(), we'll write some code to add the disabled CSS class to the link, disable pointer events, and dim the link slightly. Write link.classList.add('disabled'), then link.style.pointerEvents = 'none', and finish with link.style.opacity = '0.5'.

// ... lines 1 - 41
#disableLink(link) {
link.classList.add('disabled');
link.style.pointerEvents = 'none';
link.style.opacity = '0.5';
}
// ... lines 47 - 54

In #enableLink(), do the opposite: link.classList.remove('disabled'), link.style.pointerEvents = 'auto', and link.style.opacity = '1'.

// ... lines 1 - 47
#enableLink(link) {
link.classList.remove('disabled');
link.style.pointerEvents = 'auto';
link.style.opacity = '1';
}
// ... lines 53 - 54

Okay, in the openOverlay() method, right after we creat linkEl, call this.#disableLink(linkEl). Now, in the second .then() after this window.LemonSqueezy... line, call this.#enableLink(linkEl). Do the same thing in .catch() after console.error().

// ... lines 1 - 10
openOverlay(e) {
// ... lines 12 - 14
this.#disableLink(linkEl);
// ... line 16
fetch(this.checkoutCreateUrlValue, {
// ... lines 18 - 21
})
// ... lines 23 - 29
.then(data => {
window.LemonSqueezy.Url.Open(data.targetUrl);
this.#enableLink(linkEl);
})
.catch(error => {
console.error('Fetch error:', error);
this.#enableLink(linkEl);
});
}
// ... lines 41 - 54

All right, over on our site, reload the cart page, and if we click on the "Checkout with LemonSqueezy" button a few times... we can see that it's slightly dimmed and completely ignores our double clicks. Nice!

Embedding the Checkout Page

Now, onto the fun part - embedding! Open src/Store/LemonSqueezyApi.php and, in the createCheckoutUrl() method, after setting the custom user ID, add $attributes['checkout_options']['embed'] = true.

157 lines | src/Store/LemonSqueezyApi.php
// ... lines 1 - 12
final readonly class LemonSqueezyApi
{
// ... lines 15 - 24
public function createCheckoutUrl(User $user): string
{
// ... lines 27 - 37
$attributes['checkout_options']['embed'] = true;
// ... lines 39 - 86
}
// ... lines 88 - 155
}

Go refresh the cart page, click the checkout button again, and... there it is - a shiny new LemonSqueezy overlay! We can still see our cart page in the background. When we close this, our "Checkout with LemonSqueezy" button is ready to go again.

Improving createCheckoutUrl()

At the moment, we're calling createCheckoutUrl() in a couple places - in OrderController::createCheckout() and again in checkout(). If we want to use embedding for just the JavaScript version, we can add an $embed boolean argument to LemonSqueezyApi::createCheckoutUrl() that defaults to false. Also replace the hard-coded true we used earlier with the new $embed variable.

157 lines | src/Store/LemonSqueezyApi.php
// ... lines 1 - 12
final readonly class LemonSqueezyApi
{
// ... lines 15 - 24
public function createCheckoutUrl(User $user, bool $embed = false): string
{
// ... lines 27 - 37
$attributes['checkout_options']['embed'] = $embed;
// ... lines 39 - 86
}
// ... lines 88 - 155
}

Back in OrderController, pass true to createCheckoutUrl() in the createCheckout() action.

102 lines | src/Controller/OrderController.php
// ... lines 1 - 70
public function createCheckout(
// ... lines 72 - 73
): Response {
return $this->json([
'targetUrl' => $lsApi->createCheckoutUrl($user, true),
]);
}
// ... lines 79 - 100
}

Automating lemon.js Inclusion

To ensure our LemonSqueezy Stimulus controller works, we need to include lemon.js on every page we use it. If that sounds tedious, it is, so let's automate it.

In the connect() method, create a script variable equal to window.document.querySelector('script[src=""]', copy the lemon.js URL from our template and paste here. Check the script doesn't already exist with if (!script). Inside, write script = window.document.createElement('script'). Now, set script.src to the full lemon.js URL. Add script.defer = true and finally, add it to the DOM with window.document.head.appendChild(script).

// ... lines 1 - 7
connect() {
// Load the Lemon Squeezy script dynamically avoiding double loading
let script = window.document.querySelector('script[src="https://app.lemonsqueezy.com/js/lemon.js"]');
if (!script) {
script = window.document.createElement('script');
script.src = 'https://app.lemonsqueezy.com/js/lemon.js';
script.defer = true;
window.document.head.appendChild(script);
}
}
// ... lines 18 - 62

Now we can celebrate by removing the javascripts block from the template!

Reload the cart page, click the checkout button, we can see that it's loading, and... yes! We can still see the overlay!

Debugging for Non-authenticated Users

But there's a hiccup for non-authenticated users. If we log out, add a product to the cart, and try to checkout again... nothing happens. If you open the Dev Tools, you can see that the request is redirected to a login page first, but our JavaScript logic doesn't follow that redirect. Let's fix that!

In our code, add a console.log(response) before the response.ok check.

// ... lines 1 - 18
openOverlay(e) {
// ... lines 20 - 24
fetch(this.checkoutCreateUrlValue, {
// ... lines 26 - 29
})
.then(response => {
console.log(response);
if (!response.ok) {
throw new Error("Network response was not ok " + response.statusText);
}
// ... lines 36 - 37
})
// ... lines 39 - 48
}
// ... lines 50 - 63

Back on our site, in the "Console" tab, we can see that response.redirected is set to true for that request. Let's add another check - if (response.redirected) - send the user to the login page with window.location.href = response.url. Below, stop further execution of this chain with return Promise.reject('User is not authenticated!'). I'll add a comment above to explain this.

// ... lines 1 - 18
openOverlay(e) {
// ... lines 20 - 24
fetch(this.checkoutCreateUrlValue, {
// ... lines 26 - 29
})
.then(response => {
// ... lines 32 - 35
if (response.redirected) {
window.location.href = response.url+'?_target_path='+window.location.pathname;
// Stop further execution
return Promise.reject("User is not authenticated!");
}
// ... lines 42 - 43
})
// ... lines 45 - 54
}
// ... lines 56 - 69

Redirecting Users Back to the Cart Page

Okay, now if we click the checkout button when we're not logged in... we're redirected to the login page! Nice! And if we enter our credentials and log in... we're... redirected to the homepage instead of back to the cart page.

Let's fix that too! In lemon-squeezy_controller.js, after response.url, concatenate ?_target_path=, and window.location.pathname. This _target_path query parameter is a Symfony convention to tell us where to redirect after login.

We have a custom authenticator for our login form, so to make this actually work, we need to make some adjustments. Open src/Security/LoginFormAuthenticator. At the start of the onAuthenticationSuccess() method, add if ($targetPath = $request->query->get('_target_path')). Inside, return new RedirectResponse($targetPath).

62 lines | src/Security/LoginFormAuthenticator.php
// ... lines 1 - 18
final class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
// ... lines 21 - 44
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $request->query->get('_target_path')) {
return new RedirectResponse($targetPath);
}
// ... lines 50 - 54
}
// ... lines 56 - 60
}

This time, if we logout and try to checkout again... we're redirected to the login page. If we sign in again... boom! We're back on the cart page! Click the checkout button and... it loads our awesome checkout overlay! I'll fill in some information so we can complete the checkout... click the "Pay" button, and... tada! Here's our success message!

Next: Let's learn how to listen to LemonSqueezy JavaScript events and use those to sync the customer ID with the current user as an alternative to the webhook we set up earlier.