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
<?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.
<?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-htmldirective. With Map x-html-safe to x-html enabled, the HTML goes throughx-html-safe, which removes<script>tags, Alpine attributes such asx-dataand@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 strictscript-srcpolicy 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:initlistener in the block is never called, soAlpine.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.