A Deep Dive into Frontend Embedding by Major Payment Providers

A Deep Dive into Frontend Embedding by Major Payment Providers

Posted 2023-12-30

Everyone who uses the web is familiar with common payment flows - you often visit a site, add items to a cart, and then on a checkout page there will be some kind of embedded widget (iframe, JavaScript, HTML, or a form) from a payment provider (such as Stripe, PayPal etc).

In this deep dive, I will look into the frontend embedding techniques used by leading payment providers like PayPal, Stripe, and others.

Some definitions before I begin

Before diving deeper, I'll clarify what I mean by 'embedding' in this context. This can include:

  • iframe integration - and I'll look into how they load that iframe (with JS, plain HTML etc)
  • script embedding - a <script> embed which runs fully in the context of your application, and I'll look into what those scripts do
  • form based submissions which use standard <form> elements to submit payment details to the payment provider.
  • external links to pay on a new tab/window

By payment providers, I am referring to any commonly used payment gateway, processor, or service.

This discussion assumes familiarity with most of the providers mentioned.

Note: The code snippets here are illustrative and modified for clarity. The full code snippets can be found from the official docs for each payment provider.


PayPal

PayPal have a few frontend offerings when it comes to integrating them on your site. I am looking today at their standard checkout integration.

They use a <script> tag (pointing to https://www.paypal.com/sdk/js?client-id=test&currency=USD), and custom JS that runs functions on window. PayPal.

Show PayPal embed code snippet examples

The basic HTML to your page (including their script tag, and your JS:

<div id="paypal-button-container"></div> <script src="https://www.paypal.com/sdk/js?client-id=test&currency=USD"></script> <script src="your-app.js"></script>

And this is (a trimmed-down version) of your JS, which sets up a PayPal order button.

your-app.js
✅ copied
window.paypal .Buttons({ async createOrder() { const response = await fetch("/api/orders", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ cart: [ { id: "YOUR_PRODUCT_ID", quantity: "YOUR_PRODUCT_QUANTITY", }, ], }), }); const orderData = await response.json(); return orderData.id; }, async onApprove(data, actions) { const response = await fetch(`/api/orders/${data.orderID}/capture`, { method: "POST", headers: { "Content-Type": "application/json", }, }); const captureData = await response.json(); console.log(captureData) }, }) .render("#paypal-button-container");

The JS (with client-id=test) downloads 80KB - around 12,000 lines (after prettifying it).

It appears to include some CSS inline in the JS.

Some other scripts are loaded inline, such as https://www.sandbox.paypal.com/tagmanager/pptm.js?id=" + r + "&t=xo&v=5.0.414&source=payments_sdk.

They tend to make use of using query params in the JS they request.

The main JS script has the cache headers which tell browsers to cache it for 1 hour (3 hours in shared caches, like CDNs) - Cache-Control: public, max-age=3600, s-maxage=10800.

The JS is not versioned - presumably, it always serves up the latest version of their script.


Stripe

Stripe has a wide range of payment integrations for your website.

As Stripe has such a great reputation in the engineering world (great docs, great APIs) I will dive into them in more detail.

The easiest to set up is a payment link. This isn't really what I would normally consider a way to embed a payment flow on your website, but it is interesting that they have such a simple way to set up a link.

With this link (URL) you can share it on any website. They also offer a way to turn this into a QR code.

But the main reason I am mentioning it here is because they offer a way to easily add that link on your website via their buttons.

Their embed button is an easy way to embed a payment. There are two versions that they offer - plain HTML/JS or a React version.

Stripe embed button - plain HTML / JS

Show Stripe embed button code (plain HTML/JS version)
✅ copied
<body> <script async src="https://js.stripe.com/v3/buy-button.js"> </script> <stripe-buy-button buy-button-id='{{YOUR_BUY_BUTTON_ID}}' publishable-key="{{YOUR_KEY}}" > </stripe-buy-button> </body>

This code will download a small 11kb (400 lines after prettifying) JS script.

This adds an iframe (for processing the payment/collecting user data), and uses web components to manage the <script-buy-button> element.

This web component adds the iframe to the DOM and handles postMessage to/from the iframe. One the the things that it does is set the height of the iframe (based on a postMessage from the iframe).

This script is versioned - at v3

Stripe embed button code (React version)

Their react version is not an actual NPM package - it is some sample code to copy/paste that does the same thing as the plain HTML/JS version.

Show React version of Stripe's button embed

Put this somewhere in your app (e.g. index.html):

✅ copied
<head> <script async src="https://js.stripe.com/v3/buy-button.js"> </script> </head>

Then use the web component in your React components...

✅ copied
export function BuyButtonComponent() { return ( <stripe-buy-button buy-button-id={BUY_BUTTON_ID} publishable-key={publishableKey} /> ); }

Note that stripe-buy-button here is the same web component as in the HTML version - this is not a React-specific implementation.

This is just one Stripe embed option. They have many more, including dedicated React components. Let's look into those now.

React Stripe.js

They describe React Stripe.js as:

React Stripe.js is a thin wrapper around Stripe Elements. It allows you to add Elements to any React app.

Show example React code using their React Stripe.js wrapper

This is how to use it in React:

✅ copied
import {Elements} from '@stripe/react-stripe-js'; import {loadStripe} from '@stripe/stripe-js'; const stripePromise = loadStripe(YOUR_STRIPE_KEY); export default function App() { const options = { clientSecret: '{{CLIENT_SECRET}}', }; return ( <Elements stripe={stripePromise} options={options}> <CheckoutForm /> </Elements> ); };

They load stripe with const stripePromise = loadStripe(YOUR_STRIPE_KEY), and then you pass that Promise object as a prop to <Elements stripe={stripePromise}>.

It is interesting that their example does not try to keep the object stable (e.g. with const options = useMemo(...)).

Their documentation on github.com/stripe/stripe-js says:

Note: To be PCI compliant, you must load Stripe.js directly from https://js.stripe.com. You cannot include it in a bundle or host it yourself. This package wraps the global Stripe function provided by the Stripe.js script as an ES module.

Calling loadStripe always loads the latest version of Stripe.js, regardless of which version of @stripe/stripe-js you use. Updates for this package only impact tooling around the loadStripe helper itself and the TypeScript type definitions provided for Stripe.js. Updates do not affect runtime availability of features of Stripe.js.

Show their implementation of loadStripe() & my thoughts on it

On GitHub you can see how they load stripe see here.

It is interesting how they resolve a promise first (with a comment explaining why they do it).

✅ copied
import {loadScript, initStripe, LoadStripe} from './shared'; // Execute our own script injection after a tick to give users time to do their // own script injection. const stripePromise = Promise.resolve().then(() => loadScript(null)); let loadCalled = false; stripePromise.catch((err: Error) => { if (!loadCalled) { console.warn(err); } }); export const loadStripe: LoadStripe = (...args) => { loadCalled = true; const startTime = Date.now(); return stripePromise.then((maybeStripe) => initStripe(maybeStripe, args, startTime) ); };

The actual loading of the script can be found in https://github.com/stripe/stripe-js/blob/src/shared.ts.

This is where the more interesting code lives - I describe it's main features below.

This loadStripe() function checks if the stripe JS script already exists, and if not it will add it to the page and set up event handlers for when it has finished loading.

Show exactly what loadStripe does...

sets a constant for the script JS URL

They set a constant for the actual url, plus a regex to check all <script> tags against.

const V3_URL = 'https://js.stripe.com/v3' const V3_URL_REGEX = /^https:\/\/js\.stripe\.com\/v3\/?(\?.*)?$/;

The URL regex will match scripts starting with V3_URL. At first, I wondered why they didn't just use script.src.startsWith(V3_URL), but their regex is stricter (will only match if a valid query string immediately follows).

Checks window.Stripe

If window.Stripe already exists, then it resolves the promise with this value.

Otherwise, it continues with more checks:

Checks if the page already has the script

It has a function to check if the script exists, which looks something like this:

findScript() snippet
✅ copied
const scripts = document.querySelectorAll( `script[src^="${V3_URL}"]` ); // then for each <script> checks against V£_URL_REGEX V3_URL_REGEX.test(script.src);

This is used in the following code:

loadScript() snippet
✅ copied
let script = findScript(); if (script && params) { console.warn(EXISTING_SCRIPT_MESSAGE); } else if (!script) { script = injectScript(params); } else if ( script && onLoadListener !== null && onErrorListener !== null ) { script.removeEventListener('load', onLoadListener); script.removeEventListener('error', onErrorListener); // if script exists, but we are reloading due to an error, // reload script to trigger 'load' event script.parentNode?.removeChild(script); script = injectScript(params); } script.addEventListener('load', onLoad(resolve, reject)); script.addEventListener('error', onError(reject));

Note the 'reload' script logic in the final else if block.

Injecting the <script> tag

As you can see above, it will try to add a <script> tag. This is how it does it:

injectScript Stripe snippet
✅ copied
// note: I've edited this to simplify it, just to show the basic idea of what it is doing const injectScript = () => { const script = document.createElement('script'); const queryString = '' // note: not shown in this snippet script.src = `${V3_URL}${queryString}`; const headOrBody = document.head || document.body; headOrBody.appendChild(script); return script; };

Once loadStripe() has run - it will resolve a promise pointing to window.Stripe.

The loadStripe() function will load https://js.stripe.com/v3 (this is different than their buy-button script on the same domain).

This is a 592kb, or around 24,000 lines after prettifying it.

This file is huge, and I won't go into analysing it. But it is safe to say that this contains the bulk of the Stripe payment flow.


WePay

WePay uses a mix of JS and HTML to set up their embed script, which loads an iframe.

See their docs here.

Show full HTML implementation for WebPay

This is a simplified version of their code snippet. See their docs for the full version.

✅ copied
<script src="https://static.wepay.com/min/js/tokenization.4.latest.js"></script> <div id="credit-card-iframe"></div> <button id="submit-credit-card-button">submit</button> <div id="token"></div> <script> // stage or production WePay.set_endpoint("stage"); var iframe_container_id = "credit-card-iframe"; var creditCard = WePay.createCreditCardIframe('credit-card-iframe'); document.getElementById('submit-credit-card-button') .addEventListener('click', function (event) { creditCard.tokenize({ "client_id": "31506", "user_name": "SAMPLE USERNAME", "email": "test@test.com", }, function (response) { console.log(response); } ); }); </script>
  • You add a <script src="https://static.wepay.com/min/js/tokenization.4.latest.js"> tag to inject in their JS
  • Then call WePay.createCreditCardIframe(document.getElementById('your-credit-card-iframe-container')) to add an iFrame to your page
  • And add an event listener for your submit button to call creditCard.tokenize(formDetails, responseFunction), and handle the response (in responseFunction).

https://static.wepay.com/min/js/tokenization.4.latest.js is 50kb in size - just under 2,000 lines when prettified.

Show interesting snippets from their code

They define a lot of constants for domains - prod/staging/vm (which includes localhost URLs).

At a guess, I would say vm = virtual-machine. Maybe they run local dev under a vm, so call it that.

snippet constants
✅ copied
WePay.PROD_IFRAME = "https://iframe.wepay.com"; WePay.PROD_ENDPOINT = "https://www.wepayapi.com"; WePay.PROD_DOMAIN = "https://www.wepay.com"; WePay.PROD_STATIC = "https://static.wepay.com"; WePay.STAGE_IFRAME = "https://stage-iframe.wepay.com"; WePay.STAGE_ENDPOINT = "https://stage.wepayapi.com"; WePay.STAGE_DOMAIN = "https://stage.wepay.com"; WePay.STAGE_STATIC = "https://stage.wepay.com"; WePay.VM_IFRAME = "http://localhost:8000"; WePay.VM_ENDPOINT = "http://vm.wepay.com"; WePay.VM_DOMAIN = "http://vm.wepay.com"; WePay.VM_STATIC = "http://vm.wepay.com";

They then load more script tags. They also set the background on a div to something that looks like it tracks.

snippet loading more scripts
✅ copied
var div = document.createElement("div"); var div_div = document.createElement("div"); var div_img = document.createElement("img"); div.id = "WePay-tags"; div.style.position = "absolute"; div.style.left = "-1000px"; div_div.style.background = "url('https://t.wepay.com/fp/clear.png?org_id=ncwzrc4k&session_id=" + session_id + "&m=1')"; div_img.src = "https://t.wepay.com/fp/clear.png?org_id=ncwzrc4k&session_id=" + session_id + "&m=2"; div_img.alt = ""; var div_script = document.createElement("script"); div_script.src = "https://t.wepay.com/fp/check.js?org_id=ncwzrc4k&session_id=" + session_id; div_script.type = "text/javascript"; div_script.async = "true";

But it looks like the bulk of the action happens within a hidden (0px) iframe:

✅ copied
if (!WePay.messenger && WePay.endpoint) { WePay.messenger = document.createElement('iframe'); WePay.messenger.loaded = false; WePay.messenger.src = WePay.endpoint + "/api/messenger"; WePay.messenger.setAttribute("style", "display:none; width:1px; height:1px;"); WePay.messenger.setAttribute("width", "0"); WePay.messenger.setAttribute("height", "0"); document.body.appendChild(WePay.messenger); if (!document.getElementById(WePay.messenger.id)) { WePay.messenger = null; } setTimeout(function() { if (!WePay.tags.device_token) { device_id = WePay.tags.insert(); WePay.tags.enable_device.bind(WePay.tags, device_id)(); } }, 5000); }

And elsewhere in the app, it sends messages to this iframe with code like this:

WePay.messenger.contentWindow.postMessage(WePay.JSON.stringify(data), "*");

(And there are a few places which set up window.addEventListener("message", ...) to receive messages, handled by functions like this:

WePay.receiveMessage = WePay.receiveMessage || function(e) {
    try {
        var data = WePay.JSON.parse(e.data);
    } catch (e) {}
    if (data) {
        WePay.trigger(data.wepay_message_type, data);
    }
};

Note the use of WePay.receiveMessage = WePay.receiveMessage || /* the implementation */


WorldPay

I won't go into too much detail on WorldPay, as their embeds are just forms that submit to a WorldPay endpoint. See the example snippet.

Show WorldPay form embed

It is quite simple to use WorldPay - you just set up a <form> pointing to their URL, and put some hidden fields with the WorldPay installation ID and product Ids.

index.html
✅ copied
<form action="https://secure.worldpay.com/wcc/purchase" method="POST"> <input type="hidden" name="instId" value="Your installation ID "> <input type="hidden" name="cartId" value="Your ID for the product "> <input type="hidden" name="amount" value="The cost of the product "> <input type="hidden" name="currency" value="currency code e.g. GBP, USD "> <input type="submit" value=" Buy This "> </form>

It feels a bit ancient compared to other approaches - but WorldPay serve a huge amount of transactions a day, and despite it simplicity it is reliable.tag

(It would not be an approach I would advocate for though in 2023).

They also have an option to link to a page to process the payment, the example url given on their docs is https://secure-test.worldpay.com/wcc/purchase?instId=123456&cartId=WorldPay+Test&amount=40.00&currency=GBP&desc=WorldPay+Test&testMode=100


Adyen

Adyen have various embeddable integrations, with some nice integration examples.

The front end side of their embed uses the @adyen/adyen-web npm package.

Show Adyen code snippets (frontend embed)

Import the AdyenCheckout function (and their CSS) for the npm package:

import AdyenCheckout from '@adyen/adyen-web'; import '@adyen/adyen-web/dist/adyen.css';

Note: this import (AdyenCheckout) is not a React component, but a function used to configure Adyen.

app.jsx
✅ copied
const configuration = { clientKey: 'test_870be2...', session: { id: 'CSD9CAC3...', sessionData: 'Ab02b4c...' }, onPaymentCompleted: (result, component) => { console.info(result, component); }, onError: (error, component) => { console.error(error.name, error.message, error.stack, component); }, paymentMethodsConfiguration: { card: { hasHolderName: true, holderNameRequired: true, billingAddressRequired: true } } };

Here is a larger example showing how it can be used in React:

(This is a shortened snippet, see here for full code)

app2.js
✅ copied
import React, { useEffect, useRef } from "react"; import AdyenCheckout from "@adyen/adyen-web"; import "@adyen/adyen-web/dist/adyen.css"; import { getRedirectUrl } from "../../util/redirect"; const Checkout = (props) => { const payment = props.payment const paymentContainer = useRef(null); useEffect(() => { const { config, session } = payment; if (!session || !paymentContainer.current) { // initiateCheckout is not finished yet. return; } const createCheckout = async () => { const checkout = await AdyenCheckout({ ...config, session, onPaymentCompleted: (response, _component) => navigate(getRedirectUrl(response.resultCode), { replace: true }), onError: (error, _component) => { console.error(error); navigate(`/status/error?reason=${error.message}`, { replace: true }); }, }); // The 'ignore' flag is used to avoid double re-rendering caused by React 18 StrictMode // More about it here: https://beta.reactjs.org/learn/synchronizing-with-effects#fetching-data if (paymentContainer.current && !ignore) { checkout.create(type).mount(paymentContainer.current); } } createCheckout(); }, [payment, type, navigate]) return ( <div ref={paymentContainer} /> ); }

They also have an alternative way to load their library - see here


Checkout.com embed

Checkout.com have a way to embed their payment flow on your site, using a mix of <form> and JS.

Checkout.com example snippet code
index.html
✅ copied
<form id="payment-form" method="POST" action="https://merchant.com/charge-card" > <div class="one-liner"> <div class="card-frame"></div> <button id="pay-button" disabled>PAY GBP 24.99</button> </div> <p class="error-message"></p> <p class="success-payment-message"></p> </form> <script src="https://cdn.checkout.com/js/framesv2.min.js"></script> <script src="your-app.js"></script>

Then in your JS files, you can run code to set it all up (and access window.Frames to set it up)

your-app.js
✅ copied
var payButton = document.getElementById("pay-button"); var form = document.getElementById("payment-form"); var errorStack = []; Frames.init("YOUR_KEY"); Frames.addEventHandler( Frames.Events.FRAME_VALIDATION_CHANGED, onValidationChanged ); function onValidationChanged(event) { console.log("FRAME_VALIDATION_CHANGED: %o", event); var errorMessageElement = document.querySelector(".error-message"); var hasError = !event.isValid && !event.isEmpty; if(hasError) { console.error('Error with validation'); } } function getErrorMessage(element) { var errors = { "card-number": "Please enter a valid card number", "expiry-date": "Please enter a valid expiry date", cvv: "Please enter a valid cvv code", }; return errors[element]; } Frames.addEventHandler( Frames.Events.CARD_TOKENIZATION_FAILED, onCardTokenizationFailed ); function onCardTokenizationFailed(error) { console.log("CARD_TOKENIZATION_FAILED: %o", error); Frames.enableSubmitForm(); } Frames.addEventHandler(Frames.Events.CARD_TOKENIZED, onCardTokenized); function onCardTokenized(event) { var el = document.querySelector(".success-payment-message"); el.innerHTML = "Card tokenization completed<br>" + 'Your card token is: <span class="token">' + event.token + "</span>"; } form.addEventListener("submit", function (event) { event.preventDefault(); Frames.submitCard(); });

The https://cdn.checkout.com/js/framesv2.min.js file that it loads in the <script> tag is 86kb - around 4,000 lines after pretifying.

Their code will add an iframe, which it controls in the JS.


Square

Square offers paylink links which look quite easy to set up and share.

They then offer ways to embed this as a button, even including tips on how to embed the payment on sites like Wix.

Embedding their pay button in React is documented here.

Show code for embedding Square payments in React

This code looks pretty old (it is class-based, not hook-based...) but the idea is pretty simple. (taken from here).

app.js
✅ copied
export class App extends Component { constructor(props){ super(props) this.state = { loaded: false } } componentWillMount(){ const that = this; let sqPaymentScript = document.createElement('script'); sqPaymentScript.src = "https://js.squareup.com/v2/paymentform"; sqPaymentScript.type = "text/javascript" sqPaymentScript.async = false; sqPaymentScript.onload = ()=>{that.setState({ loaded: true })}; document.getElementsByTagName("head")[0].appendChild(sqPaymentScript); } render() { return ( this.state.loaded && <PaymentForm paymentForm={ window.SqPaymentForm } /> ); } }

They load a script from https://js.squareup.com/v2/paymentform. But right now for me, that won't load (and this is coming from their official docs).


Fastspring

Fastspring allows you to generate a checkout ready to be embedded in their admin panel. Then you add a small snippet on your site to embed it.

Show Fastspring embedded checkout snippet

Taken from their docs

index.html
✅ copied
<script id="fsc-api" src="https://d1f8f9xcsvx3ha.cloudfront.net/sbl/0.8.3/fastspring-builder.min.js" type="text/javascript" data-storefront="yourexamplestore.test.onfastspring.com/embedded">; </script>

Note that this is one of the few examples that I found that use a very specific version for their script. All the others just have a generic url (or maybe versioned like /v3. But not as specific as Semver (0.8.3).

This JS file is 41kb - around 1,000 lines after prettifying.

It looks like they load some scripts, including https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/3.0.1/handlebars.min.js. There is also code to add an iframe. I didn't load up their widget in a browser, but it looks like they inject in an iframe and handle the checkout flow within their iframe

One interesting thing I noticed...
fastspring-builder.min.js
✅ copied
Object.defineProperty(window, "_f", { get: function() { return console.log("Warning: '_f' is deprecated, please use 'fastspring.builder' instead"), i } })

Chargebee

ChargeBee have documentation here about integrating their checkout flow on your website.

Show ChargeBee embed snippets
<script src="https://js.chargebee.com/v2/chargebee.js"></script>
var cbInstance = Chargebee.init({ site: "site-name", // your test site domain: "https://mybilling.acme.com" // this is an optional parameter. publishableKey: "test__" })

Then they give examples in jQuery, Vue, Angular, and React.

I'll show the jQuery one (for nostalgic reasons)

jquery.js
✅ copied
cbInstance.load("components").then(() => { var cardComponent = cbInstance.createComponent("card"); cardComponent.createField("number").at("#card-number"); cardComponent.createField("expiry").at("#card-expiry"); cardComponent.createField("cvv").at("#card-cvc"); cardComponent.mount(); $("#payment").on("submit", function(event) { event.preventDefault(); cardComponent.tokenize().then(data => { var token = data.token; // Send ajax call to create a subscription or to create a card payment source }) }); });

Their script URL https://js.chargebee.com/v2/chargebee.js is versioned (although it looks like v2 has been out for a while) so I suspect that this is always the most up-to-date version of their code.

It comes in at 260kb.

There are various calls to create iframe or script tags.

APP_DOMAIN = "https://${site}.chargebee.com"; JS_DOMAIN = "https://js.chargebee.com"; STATIC_DOMAIN = "https://${site}.chargebeestaticv2.com"; ASSET_PATH = "https://js.chargebee.com/assets/cbjs-2023.12.13-07.12/v2"

It looks like they post (and receive) messages to/from the iframe to process the payments.


Paddle

Paddle uses data attributes on HTML elements along with their JS script to embed their payment flow on your site.

Paddle HTML embed snippet

This is how easy it is to set up when there is just one or two parameters to configure. Note the data-product id in the <a> tag.

index.html
✅ copied
<a href="#!" class="paddle_button" data-product="12345">Buy Now!</a> <script src="https://cdn.paddle.com/paddle/paddle.js"></script> <script type="text/javascript"> Paddle.Environment.set('sandbox'); Paddle.Setup({ vendor: 111111 }); </script>

The unversioned https://cdn.paddle.com/paddle/paddle.js javascript is 230kb.

A lot of configuration is done via data attributes.

Show some of the data attributes used for configuration

Sometimes it is interesting to see what kind of configuration they use. I am not personally a fan of data attributes for settings, as they are often harder to manage - and with this amount of configuration, it could be a huge amount of properties to set on an element.

paddle.js
✅ copied
data-init data-download data-download-url data-download-prompt data-download-heading data-download-subheading data-download-cta data-vendor-name data-allow-quantity data-product data-quantity data-theme data-upsell-button data-upsell-coupon data-upsell-action data-upsell-title data-upsell-text data-upsell data-method data-disable-logout data-title data-referrer data-message data-locale data-coupon data-upsell-passthrough data-passthrough data-postcode data-country data-email data-marketing-consent data-display-mode-theme data-checkout-version data-trial-days-auth data-auth data-trial-days data-price data-success data-close-callback data-load-callback data-success-callback data-override data-type data-custom-data data-hide-tax-lin

(This is not even a complete list)


Summary

There is definitely a trend where your website ends up running an externally loaded JS script file. Even the React versions of some of the payment providers are just wrappers which get the latest version from their CDN delivered JS.

Stripe is notable in that it actually calls out why it does it -

Note: To be PCI compliant, you must load Stripe.js directly from https://js.stripe.com. You cannot include it in a bundle or host it yourself. This package wraps the global Stripe function provided by the Stripe.js script as an ES module.

Calling loadStripe always loads the latest version of Stripe.js, regardless of which version of @stripe/stripe-js you use. Updates for this package only impact tooling around the loadStripe helper itself and the TypeScript type definitions provided for Stripe.js. Updates do not affect runtime availability of features of Stripe.js.

There are also quite a few implementations which use their JS to load an iframe, then put all of their checkout flow within that iframe. (And other JS manages messages between the parent website which embeds it, and the child iframe (especially when the iframe needs to resize it's height).

Subscribe to my
Full Stack Typescript Developer
newsletter

If you enjoyed my content and want more Full Stack Typescript content, then enter your email below.

I send a few links every couple of weeks, that I personally found useful from around the web.

Focusing on Frontend, Typescript, NextJS, and also
engineering soft skills posts.

Welcome to my site and blog - Code Driven Development.

This is where I dive topics relating to modern software engineering - with a focus on Typescript, React, web accessibility and Agile practices

I pick topics that I think are interesting, that I think others might find interesting, or write up guides that I wish I had read before learning a topic.