The quickest way to integrate PayPal checkout with Blazor SSR in .NET 8

March 6, 2024 · 6 minute read · Tags: aspnet | blazor

You have a Blazor web app and want to integrate with PayPal to accept payments.

How to make it work?

You could go down the road of using Blazor Server/Web Assembly and a little bit of JavaScript Interop to stitch it all together…

But what if you’re keen to avoid paying the interactivity tax, and want to stick with Blazor Server-side rendering?

The challenge

PayPal has a JavaScript script you can use to initiate checkout and accept payments online.

In a .NET 8 Blazor app you can reference that script in App.razor (more on that in a moment).

Then, one of the easier ways to trigger PayPal checkout is to use PayPal’s Buttons API to show checkout buttons:

<div id="paypal_container"></div>
let container = document.getElementById('paypal_container');

paypal.Buttons({
    style: {
        shape: 'rect',
        color: 'gold',
        layout: 'vertical',
        label: 'subscribe'
    },
    createSubscription: function (data, actions) {
        return actions.subscription.create({
            // replace with your own plan Id
            plan_id: 'P-61S92112RP924735VMOE434Y'
        });
    },
    onApprove: function (data, actions) {
        alert(data.subscriptionID); // optional success message for the subscriber
    }
}).render(container); // Renders the PayPal button 

Run this script and you get PayPal Buttons rendered in the #paypal_container element.

At this point users can click one of the buttons and proceed to checkout via PayPal’s hosted checkout UI.

PayPalButtons.png

But how to do this with Blazor when you’re using static server-side rendering?

Reference the PayPal script

The first step is to load the PayPal script.

We can do that directly in App.razor.

<!DOCTYPE html>
<html lang="en">

<head>
    ...
    <HeadOutlet/>   
    
    <script src="https://www.paypal.com/sdk/js?client-id=<your-client-id-here>&vault=true&intent=subscription" data-sdk-integration-source="button-factory"></script>
</head>

<body>
<Routes/>
<script src="_framework/blazor.web.js"></script>

</body>

</html>

Initialise the PayPal buttons

If you’ve used Blazor Server or Blazor WASM you’re probably familiar with IJSRuntime, which you can use to interact with JavaScript code.

But when you’re running .NET 8 in SSR mode IJSRuntime doesn’t really work, because the component is being rendered once on the server, and plain HTML returned to the browser.

A naive approach here would be to simply include the JavaScript directly in the component’s markup.

Home.razor

@page "/"

<div id="paypal_container"></div>

<script>
    let container = document.getElementById('paypal_container');
    paypal.Buttons({
        style: {
            shape: 'rect',
            color: 'gold',
            layout: 'vertical',
            label: 'subscribe'
        },
        createSubscription: function (data, actions) {
            return actions.subscription.create({
                // replace with your own plan Id
                plan_id: 'P-61S92112RP924735VMOE434Y'
            });
        },
        onApprove: function (data, actions) {
            alert(data.subscriptionID); // optional success message
        }
    }).render(container); // Renders the PayPal button     
</script>

This works, but has some flaws.

Run this and you will indeed see the PayPal checkout buttons when the home page first loads.

The flow is:

  • The Razor component is rendered on the server
  • The resulting HTML is returned to the browser
  • The browser renders the HTML, including the script tag
  • The browser executes the JavaScript in the script tag

However, things soon fall apart as users navigate between different pages in the app.

In .NET 8 Blazor uses something called enhanced navigation as you move between pages.

This uses the browsers fetch API to retrieve the new page, then patches the DOM with the parts of the page that have changed.

This avoids performing full page loads every time you navigate between pages (especially useful if only a small part of the UI has actually changed).

In this case, however, it breaks our checkout buttons.

The buttons are not (re)rendered when enhanced navigation is used.

Instead our PayPal code in the Script tag will be executed once, and once only, when the page is loaded for the first time.

The upshot is the buttons may or may not appear, depending on how the user gets to the checkout page!

So we need another option, one that ensures the JS code is invoked every time the page is visited, even when enhanced navigation occurs.

The best solution I’ve found for this is to use a handy NuGet package from Mackinnon Buck called blazor-page-script.

With this package you can create a .JS file for your component, and hook into some methods that will be invoked as enhanced navigation occurs.

In this case, let’s create a Home.razor.js file for our component, right next to the existing Home.razor file.

In Home.razor, we can load this script using Mackinnon’s library.

Home.razor

@page "/"

<PageTitle>Home</PageTitle>

<PageScript Src="./Components/Pages/Home.razor.js"/>

<div id="paypal_container"></div>

The PageScript component lives in a namespace called BlazorPageScript so make sure to add that to your _Imports.razor file.

Now we can go ahead and add our PayPal code to Home.razor.js.

function initialisePayPal() {

    if(paypal.Buttons.instances.length >= 1) return;

    let container = document.getElementById('paypal_container');
    paypal.Buttons({
        style: {
            shape: 'rect',
            color: 'gold',
            layout: 'vertical',
            label: 'subscribe'
        },
        createSubscription: function (data, actions) {
            return actions.subscription.create({
                // replace with your own plan Id
                plan_id: 'P-61S92112RP924735VMOE434Y'
            });
        },
        onApprove: function (data, actions) {
            alert(data.subscriptionID);
        }
    }).render(container);

}

export function onUpdate() {
    initialisePayPal();
}

This is almost identical to the code we had before, with one small improvement.

It checks if PayPal buttons have already been initialized. If so we return early to avoid initializing the buttons multiple times.

More interestingly, notice how we’re calling the initialization code from a method called onUpdate?

blazor-pages-script will automatically call this method after enhanced navigation has occurred.

There are a few events you can hook in to:

  • onLoad (called when the script first gets loaded on the page)
  • onUpdate (when an enhanced page update occurs, and once immediately after the initial load)
  • onDispose (called when an enhanced page update removes the script from the page)

If you’re interested to see how that’s actually implement you can check out the source code, particularly this JavaScript Module.

It employs JavaScript’s module system to load your JS script and invoke any relevant methods (see above) in response to key enhanced navigation events.

In summary

The simple, lightweight blazor-pages-script package makes it much easier to run JS when enhanced navigation events occur.

You can co-locate your JavaScript code next to your components, load that code in via the PageScript component, then declare functions for the enhanced navigation events you want to handle.

Here we were able to use it to initialise PayPal checkout buttons in a div element on a Razor page which employs server-side rendering, and ensure the checkout buttons appeared even after a user navigated between pages.

Check out the complete source code for this example here.

Join the Practical ASP.NET Newsletter

Ship better Blazor apps, faster. One practical tip every Tuesday.

I respect your email privacy. Unsubscribe with one click.

    Next up

    Finally! Improved Blazor Server reconnection UX
    .NET 9 changes how your Blazor Server app behaves when server connection is lost
    .NET 9 improves JavaScript module importing for Blazor
    .NET 9 ensures your users always get the latest version of your JS modules
    How to use .NET 9 to ensure users always get the latest version of your stylesheets
    .NET 9 changes how static files are served, and it solves a long-standing problem