Skip to content

JS-driven Payment Method

We've significantly improved the way JS-driven payment methods are built since version 1.3.5. When developing a payment method, you'll need to consider using this enhanced version of the API, bearing in mind that this will require developers to use at least 1.3.5 or higher, which means they'll need to upgrade their entire checkout. Please take this into consideration when you start development. We always encourage our customers to upgrade to the latest version to benefit from ongoing improvements in stability, performance, and security. However, this isn't always feasible, and you can't assume everyone will have upgraded.

For comprehensive details about our Payment API, please refer to our documentation page.

This example assumes you've already created a payment method named hyva using existing tutorials or the official Magento developer documentation.

Basics

Let’s start with a simple example, which we can later expand into more advanced topics.

Register Method Renderer

We begin by defining a payment method render template, which is optional and should only be set when required.

The crucial element in this example is the as="{method_code}" alias, which maps your payment method to this block, making it the designated renderer.

Example_Module::view/frontend/layout/hyva_checkout_components.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"
>
    <body>
        <referenceBlock name="checkout.payment.methods">
            <block name="checkout.payment.method.hyva"
                   as="hyva"
                   template="Example_Module::hyva-checkout/payment/method/hyva.phtml"
            >
                <arguments>
                    <!-- Optional: to make your renderer a Magewire component. -->
                    <argument name="magewire" xsi:type="object">
                        Example\Module\Magewire\Payment\Method\Hyva
                    </argument>
                </arguments>
            </block>
        </referenceBlock>
    </body>
</page>

The template could look like this. Please bear in mind that it's best practice to keep your payment method renderer template as minimal as possible and, as the example demonstrates, separate the required JavaScript.

Example_Module::hyva-checkout/payment/method/hyva.phtml
<div>
    Hyvä Payment Method.
</div>

Writing Belonging JavaScript

To register the payment method on the frontend, we need to write some JavaScript by adding a custom block with an accompanying template.

Example_Module::view/frontend/layout/hyva_checkout_index_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"
>
    <body>
        <referenceContainer name="hyva.checkout.api-v1.payment-methods">
            <block name="hyva.checkout.alpinejs.payment-method-hyva"
                   template="Example_Module::hyva/checkout/page/js/api/v1/alpinejs/payment/method/hyva.phtml"
            />
        </referenceContainer>
    </body>
</page>

The following code uses our hyvaCheckout.api.after method, allowing you to register a callback that executes once the Hyvä Checkout frontend API is fully initialised. This is considered best practice, not only for payment methods, but for all custom code that relies on hyvaCheckout. subsections.

Example_Module::hyva/checkout/page/js/api/v1/alpinejs/payment/method/hyva.phtml
<script>
    (() => {
        hyvaCheckout.api.after(() => {
            hyvaCheckout.payment.registerMethod({
                code: 'hyva',

                method: {
                    initialize: async function () {
                        console.log('Hyvä Payment: method initialized.');
                    },
                    uninitialize: async function () {
                        console.log('Hyvä Payment: method un-initialized.');
                    },
                    placeOrder: async function({ fallback }) {
                        fallback();
                    },
                    validate: async function() {
                        console.log('Hyvä Payment: validation simulation started.');

                        return new Promise(resolve => setTimeout(() => {
                            console.log('Hyvä Payment: validation simulation stopped.');

                            resolve(true)
                        }, 2000));
                    }
                }
            });
        });
    })();
</script>
<?php $hyvaCsp->registerInlineScript() ?>

The above JavaScript requires Hyvä Checkout version 1.3.5 or higher. Either make this a dependency of your module or use the activate method as an alternative for older versions.

Summary

In essence, you only need a single JavaScript file that registers your JS-driven payment method.

If it requires user interface elements (such as a form), this can be optionally implemented using a block aliased with the payment method code and placed in the checkout.payment.methods container.

Advanced

The advanced topics below either extend or can be applied to the basic example shown above.

Payment Method Validation

In this example, we create a fictive frontend driven payment method validation method that first asks for confirmation to fill in a password, then prompts a dialog to fill in a password which in this example is 123. When one of these step are incorrect, a error message dialog is shown telling the customer what went wrong.

<script>
    (() => {
        hyvaCheckout.api.after(() => {
            hyvaCheckout.payment.registerMethod({
                code: 'hyva',

                method: {
                    validate: async function() {
                        console.log('Hyvä Payment: validation simulation started.');

                        return new Promise(resolve => {
                            const confirmed = confirm('Do you want to proceed with this payment? Click OK to continue or Cancel to abort.');

                            if (! confirmed) {
                                hyvaCheckout.message.dialog('Please confirm to proceed.');

                                resolve(false);
                                return;
                            }

                            const password = prompt('Please enter your password to confirm the payment:');

                            if (password === null) {
                                hyvaCheckout.message.dialog('Please fill in the password.');
                                resolve(false);
                                return;
                            }

                            if (password.trim() === '') {
                                hyvaCheckout.message.dialog('Password can not be empty.');
                                resolve(false);
                                return;
                            }

                            if (password === '123') {
                                console.log('Hyvä Payment: validation simulation stopped - success.');
                                resolve(true);
                            } else {
                                hyvaCheckout.message.dialog('Wrong password, please use "123".');
                                console.log('Hyvä Payment: validation simulation stopped - failed.');
                                resolve(false);
                            }
                        });
                    }
                }
            });
        });
    })();
</script>

Remember that we're using simple confirmation and prompt dialogs, which are native browser functionalities. However, you can implement any solution you prefer, provided it returns a Promise that resolves to either true or false.

This sits within the validate method, which runs before the system even attempts to resolve the correct payment method and handle order placement. Using the validate method allows you to implement different error handling approaches, such as the dialog demonstrated in the example above.

Alternatively, you can implement this in your payment method's placeOrder method. Instead of using a dialog, you can throw a new Error(), which is automatically caught and passed to the method's handleException function.

If you've also implemented that for your method, you can create a consistent approach for error display across your payment implementation.

As you can see, we provide numerous options to meet your specific requirements, recognizing that every payment method has unique needs.

Example_Module::hyva-checkout/page/js/api/v1/alpinejs/payment/method/hyva.phtml
<script>
    (() => {
        hyvaCheckout.api.after(() => {
            hyvaCheckout.payment.registerMethod({
                code: 'hyva',

                method: {
                    placeOrder: async function({ fallback }) {
                        const password = prompt('Please enter your password to confirm the payment:');

                        if (password !== '123') {
                            throw new Error('Wrong password, please use "123".');
                        }

                        await fallback();
                    },
                    handleException: async function({ exception, fallback }) {
                        hyvaCheckout.message.dialog(exception);

                        await fallback({ exception: exception });
                    }
                }
            });
        });
    })();
</script>

Multiple Payment Methods using a single Handler

In some scenarios, you have multiple payment methods that perform essentially the same function, with only slight variations. It would be poor practice to duplicate code repeatedly, leading to unnecessary redundancy and maintenance overhead.

We've developed a solution using the canHandle() method within your payment method object, which informs the checkout system whether it can or should handle order placement based on the given method code.

Example_Module::hyva-checkout/page/js/api/v1/alpinejs/payment/method/hyva.phtml
<script>
    (() => {
        ['hyva', 'checkmo'].forEach(code => {
            hyvaCheckout.payment.registerMethod({
                code: code,

                method: {
                    initialize: function() {
                        console.log(`Method "${this.code}" initialized`);
                    },
                    uninitialize: function() {
                        console.log(`Method "${this.code}" uninitialized`);
                    }
                }
            });
        });
    })();
</script>

Redirect to a PSP after Order Placement

It is a common practice that once an order is placed, the customer should be redirected away from the checkout — either to complete payment on an external site or to an order success page displaying the order ID.

For this purpose, the canRedirect method works closely with the getRedirectUrl method. canRedirect informs the order processing system that a custom redirect URL should be used, after which the value returned by getRedirectUrl is applied automatically. This value can be a fully qualified URL or a relative URI such as onepage/success.

Example_Module::hyva-checkout/page/js/api/v1/alpinejs/payment/method/hyva.phtml
<script>
    (() => {
        hyvaCheckout.payment.registerMethod({
            code: 'hyva',

            method: {
                canRedirect: function() {
                    return true;
                },
                getRedirectUrl: async function({ fallback }) {
                    const response = await fetch('https://api.mollie.com/v1/token/generate');

                    if (response.ok) {
                        const result = await response.json();
                        const url = new URL('https://api.mollie.com/v1/order/pay/redirect');

                        url.searchParams.set('token', result.token);
                        return url.toString();
                    }

                    throw new Error('Something went wrong while trying to handle your order.');
                }
            }
        });
    })();
</script>

Exception Handling

It is always very important to provide customers with feedback on what is actually going wrong when something fails.

This can occur at various stages within the process. Due to our flexible checkout, we do not provide out-of-the-box exception handling.

The reason for this is that we cannot know what should actually happen when something goes wrong, and we want to give developers the freedom to go above and beyond rather than having to adhere to the rules we've applied.

Yes, we do provide fallback options if you accept them. Otherwise, you have complete freedom to create the best possible user experience for when something does go wrong.

In this example, we simulate an asynchronous method execution waiting for an API fetch call to an endpoint.

Example_Module::hyva-checkout/page/js/api/v1/alpinejs/payment/method/hyva.phtml
<script>
    (() => {
        const handlePlaceOrderException = function({ exception }) {
            hyvaCheckout.message.dialog(exception.message, 'Something went wrong while placing the order', 'error', () => {
                console.log('A callback executed when the customer pressed the OK button.')
            }, {
                // Make sure the customer presses the confirm button (can always refresh).
                cancelable: false
            });
        };

        hyvaCheckout.payment.registerMethod({
            code: 'hyva',

            method: {
                placeOrder: async function() {
                    await new Promise((resolve, reject) => setTimeout(() => {
                        const error = new Error('Could not reach the custom payment place order endpoint.');
                        error.code = 'PLACE_ORDER';

                        reject(error);
                    }, 1000));
                },
                handleException: async function({ exception, fallback }) {
                    if (exception.code === 'PLACE_ORDER') {
                        handlePlaceOrderException({ exception });
                        return;
                    }

                    // Always provide the exception when executing the fallback.
                    fallback({ exception });
                }
            }
        });
    })();
</script>