Skip to content

Adding Express Checkout Buttons

Payment modules often add express checkout buttons, also called shortcut buttons, such as Apple Pay, Google Pay or PayPal. This page shows where these buttons belong in a Hyvä theme, and how to build them so they work with the full page cache and with Alpine CSP.

Location Where to add the button
Cart drawer The cart-drawer.shortcut-buttons container (since Hyvä 1.5.4)
Cart page The checkout.cart.methods container, below the Proceed to Checkout button
Product page A child block of product.info.addtocart

The cart page and the product page are rendered on the server like any other block, so a regular block with its own Alpine component works there. The cart drawer needs more care, because its content is rendered by Alpine from the customer section data.

Cart drawer buttons

Since Hyvä 1.5.4

The cart-drawer.shortcut-buttons container is part of the Default Theme 1.5.4. The $store.privateContent Alpine store is part of the Hyva_Theme module 1.5.4.

The cart drawer is rendered on every page and stored in the full page cache. A button in the drawer can't use cart data rendered on the server, so it reads the current cart from the privateContent Alpine store.

The cart-drawer.shortcut-buttons container is rendered once, outside the x-if templates of the drawer. The scripts of your block run once on page load, before Alpine starts, and your component keeps its state when the cart changes. Your component decides when the button is visible, for example only when the cart has items.

1. Add the block to the container

view/frontend/layout/default.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="cart-drawer.shortcut-buttons">
            <block name="example-pay.cart-drawer.button"
                   template="Vendor_ExamplePay::cart-drawer/button.phtml"
                   ifconfig="payment/example_pay/cart_drawer_button"/>
        </referenceContainer>
    </body>
</page>

Don't add cacheable="false", because that disables the full page cache for every page.

2. Read the cart from the store

The component reads the cart from the store in a method, so it always uses the latest section data. It works with the standard and the CSP Alpine build, because the directives only use property and method names.

view/frontend/templates/cart-drawer/button.phtml
<?php
declare(strict_types=1);

use Hyva\Theme\ViewModel\HyvaCsp;
use Magento\Framework\Escaper;
use Magento\Framework\View\Element\Template;

/** @var Template $block */
/** @var Escaper $escaper */
/** @var HyvaCsp $hyvaCsp */
?>
<div x-data="examplePayCartDrawerButton" x-show="isVisible">
    <button type="button" class="btn w-full" @click="pay">
        <?= $escaper->escapeHtml(__('Pay with Example Pay')) ?>
    </button>
</div>
<script>
    function examplePayCartDrawerButton() {
        return {
            cart() {
                return this.$store.privateContent.cart || {};
            },
            isVisible() {
                return Number(this.cart().summary_count || 0) > 0;
            },
            pay() {
                const amount = this.cart().subtotalAmount;
                // Start the payment with the current cart total
            }
        };
    }

    window.addEventListener('alpine:init', () => {
        Alpine.data('examplePayCartDrawerButton', examplePayCartDrawerButton);
    }, {once: true});
</script>
<?php $hyvaCsp->registerInlineScript() ?>

If a wallet API decides whether the button can be shown, such as ApplePaySession.canMakePayments(), add that check to isVisible() after the cart check. Alpine only updates x-show for the store data that was read in the last evaluation, so a check that returns early before the cart is read stops the button from following the cart.

Any data your button needs for one specific customer, such as a cart or quote ID, has to come from the section data as well. Add it to the cart section on the server, and read it from $store.privateContent.cart.

Why not the extra actions

Luma adds mini cart shortcut buttons through the shortcut_buttons_container event. Magento renders the result as HTML into the extra_actions key of the cart section data. Hyvä supports two variants of extra actions in the cart drawer, and neither is a good fit for new buttons.

extra_actions in the section data

Deprecated since Hyvä 1.5.4

The section data extra actions are rendered with x-html, which isn't available in the Alpine CSP build.

The cart drawer renders cart.extra_actions with x-html, and runs the scripts in that HTML every time the drawer renders. This keeps existing buttons working on stores without CSP, but it has several problems:

  • No CSP support: The Alpine CSP build has no x-html directive. With Map x-html-safe to x-html enabled, the HTML goes through x-html-safe, which removes <script> tags, Alpine attributes such as x-data and @click, and event handlers, so the buttons don't work.
  • Scripts run again: The inline scripts run every time the drawer content renders, not once per page.
  • Blocked by strict CSP: The scripts are copied into the page at runtime without a nonce, and aren't registered with $hyvaCsp->registerInlineScript(), so a strict script-src policy blocks them.

If your module already adds its button through the shortcut_buttons_container event, stop adding it to the mini cart once you use the new container, otherwise the cart drawer shows the button twice. The event uses Magento\Catalog\Block\ShortcutButtons for the mini cart, and Magento\Checkout\Block\QuoteShortcutButtons for the cart page, which can keep the button.

The extra_actions layout container

The cart drawer also has an extra_actions layout container. A block in this container is rendered on the server, but it is placed inside the x-if template that shows the cart items. Alpine only adds the template content to the page after it has started, and adds it again each time the cart changes from empty to filled. As a result:

  • an alpine:init listener in the block is never called, so Alpine.data() registered that way is missing;
  • the scripts of the block run again on each render;
  • the component state is lost when the cart is emptied.

Use the cart-drawer.shortcut-buttons container instead.

CSP in-context payment buttons

On stores that only use strict CSP on the checkout, the In-Context Payment Buttons module renders the buttons in an iframe with strict CSP instead.