Payment Method in Checkout: Registering the Knockout.js Frontend Component
Payment Method in Checkout: Registering the Knockout.js Frontend Component
~8 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Server-side, "Pay with loyalty points" is now complete: registered (chapter 62), availability-checked (chapter 64), booking-capable (chapter 63). Without a frontend component it still won't show up in the payment step - and this component breaks one of this project's core rules.
A deliberate, tightly scoped exception to "no Knockout.js"
Achtung: CLAUDE.md is unambiguous: no Knockout.js, no jQuery, no UI components in the Hyvä storefront - Alpine.js instead. Chapter 60 already showed that this rule doesn't even apply to view/adminhtml, because the entire Magento admin has been built on Knockout.js since version 2.0, independent of the frontend's Hyvä theme swap. This chapter is different: the files below live under view/frontend/ - and deliberately break the rule there anyway. The reason: Magento's own checkout (Magento_Checkout) is itself a self-contained Knockout.js single-page application. Every step - shipping address, shipping method, payment method, order review - is rendered through UI component layout XML and knockout view models, completely independent of which theme serves the rest of the storefront. A payment method frontend component MUST plug into exactly this system, because there is no other hook point for the payment step - as long as the shop uses Magento's own core checkout (and no separate, Hyvä-specific checkout extension is installed that replaces this step entirely, which as of this project's current state it is not). This exception stays strictly confined to view/frontend/web/js/view/payment/... and its matching template - not a single other storefront template in this module (chapters 53, 56/57, 59) uses Knockout.js, and that stays true.
Layout XML: registering the renderer
Since Magento 2.3, official payment methods (e.g. Magento_OfflinePayments, check/money order) register their renderer directly through layout XML instead of the older rendererList.push() pattern with requirejs-config.js mixins. This chapter follows the more modern path:
<?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.root">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="children" xsi:type="array">
<item name="steps" xsi:type="array">
<item name="children" xsi:type="array">
<item name="billing-step" xsi:type="array">
<item name="children" xsi:type="array">
<item name="payment" xsi:type="array">
<item name="children" xsi:type="array">
<item name="renders" xsi:type="array">
<item name="children" xsi:type="array">
<item name="mironsoft_loyalty_points" xsi:type="array">
<item name="component" xsi:type="string">Mironsoft_Loyalty/js/view/payment/method-renderer/points-redemption-method</item>
<item name="methods" xsi:type="array">
<item name="mironsoft_loyalty_points" xsi:type="array">
<item name="isBillingAddressRequired" xsi:type="boolean">false</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
Tipp: The older rendererList.push() pattern (a custom view/frontend/web/js/view/payment-method-renderer-list.js plus a requirejs-config.js mixin on Magento_Checkout/js/view/payment/renderer-list-registration) still works and is common in older third-party payment methods - for new code, the layout XML variant above is preferable since it needs no dedicated RequireJS module detour.
The knockout view model component
The RequireJS module path Mironsoft_Loyalty/js/view/payment/method-renderer/points-redemption-method automatically resolves to the file view/frontend/web/js/view/payment/method-renderer/points-redemption-method.js - the same Vendor_Module/js/... convention as any other RequireJS module in Magento:
define([
'jquery',
'Magento_Checkout/js/view/payment/default',
'Magento_Checkout/js/model/quote',
'Magento_Checkout/js/model/totals',
'mage/url'
], function ($, Component, quote, totals, url) {
'use strict';
/**
* Checkout-module Knockout.js view model for the "Mit Treuepunkten bezahlt"
* payment step - NOT Hyvä/Alpine.js code, see the surrounding chapter text
* for why this file exists at all in an otherwise Alpine.js-only project.
* Everything this component needs (points balance, conversion rate) arrives
* pre-baked via window.checkoutConfig, set by PointsRedemptionConfigProvider
* (chapter 62).
*/
return Component.extend({
defaults: {
template: 'Mironsoft_Loyalty/payment/points-redemption-form'
},
/**
* Reads the payment method code this renderer is registered for.
*
* @return {String}
*/
getCode: function () {
return window.checkoutConfig.mironsoftLoyalty.methodCode;
},
/**
* Reads the customer's current points balance from the checkout config.
*
* @return {Number}
*/
getPointsBalance: function () {
return window.checkoutConfig.mironsoftLoyalty.pointsBalance;
},
/**
* Sends the requested points amount to the ApplyPoints controller
* (chapter 63) and refreshes the checkout totals afterward, so the
* grand total shown to the customer matches what chapter 39's
* server-side plugin has already applied.
*
* @param {Number} points Points the customer wants to redeem
* @return {void}
*/
applyPoints: function (points) {
$.ajax({
url: url.build('mironsoft_loyalty/ajax/applypoints'),
type: 'POST',
data: {points: points},
dataType: 'json'
}).done(function (response) {
if (response.success) {
totals.isLoading(true);
quote.totals.valueHasMutated();
totals.isLoading(false);
}
});
}
});
});
<div class="payment-method" data-bind="css: {'_active': (getCode() === isChecked())}">
<div class="payment-method-title field choice">
<input type="radio"
name="payment[method]"
class="radio"
data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()"/>
<label class="label" data-bind="attr: {'for': getCode()}">
<span data-bind="i18n: 'Mit Treuepunkten bezahlt'"></span>
</label>
</div>
<div class="payment-method-content" data-bind="visible: getCode() === isChecked()">
<p data-bind="text: getPointsBalance() + ' Punkte verfügbar'"></p>
<button type="button" class="action primary" data-bind="click: function() { applyPoints(getPointsBalance()); }">
<span data-bind="i18n: 'Alle Punkte einlösen'"></span>
</button>
</div>
</div>
Achtung: If the key in the layout XML's renders array (mironsoft_loyalty_points) doesn't exactly match the method code from PointsRedemptionConfigProvider::METHOD_CODE, no error appears in the browser console - the payment method simply, silently, is missing from the list, even though isAvailable() (chapter 64) returns true. Chapter 70 revisits this trap.
That makes the payment method fully visible and functional in checkout. Block 8 now turns to its second half: custom shipping methods, starting again right at the fundamentals.