simpler / magento-checkout-hyva
Hyvä theme compatibility layer for the Simpler Checkout button. Replaces the RequireJS/Knockout integration of simpler/magento-checkout with vanilla JS hooks into Hyvä's customer-data lifecycle.
Package info
gitlab.com/saysimpler/magento-modules/module-checkout-hyva
Type:magento2-module
pkg:composer/simpler/magento-checkout-hyva
Requires
- php: >=7.1
- hyva-themes/magento2-compat-module-fallback: *
- simpler/magento-checkout: >=1.5
Suggests
- hyva-themes/magento2-default-theme: Install this module only on storefronts running a Hyvä theme.
This package is not auto-updated.
Last update: 2026-05-22 13:23:40 UTC
README
Hyvä theme compatibility layer for Simpler Checkout. Replaces the RequireJS / Knockout integration of simpler/magento-checkout with vanilla JS hooks into Hyvä's customer-data lifecycle.
You can find extensive documentation on installing & configuring Simpler for Magento 2 here.
Installation
composer require simpler/magento-checkout-hyva
bin/magento module:enable Simpler_CheckoutHyva
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy
Install this only on storefronts running a Hyvä theme. The module activates automatically on Hyvä storefronts via the hyva-themes/magento2-compat-module-fallback mechanism — no theme switch is required. On Luma it stays out of the way.
What it does
Zero-config. Mirrors the base module's Luma behaviour on Hyvä, driven by a mix of layout XML (server-rendered surfaces Hyvä preserves) and a small JS runtime (PDP + session bridge):
| Surface | How it renders |
|---|---|
| Product page (simple, virtual, configurable, bundle) | JS fetches /simpler/config/product?productId=… on page load, builds a <simpler-checkout fullWidth="true">, inserts it as a full-width row before the .flex.mt-4.justify-end shortcut row inside the product-info .card. Falls back to appending to .card if the shortcut row is absent. |
| Product page — configurable swatch change | The auto-injected element subscribes to Hyvä's configurable-selection-changed window event and re-fetches with the selected variant's id. payload updates live to reflect the chosen variant (Luma is static). |
| Cart drawer (minicart) | Server-rendered via the compat module's layout/default.xml into Hyvä's extra_actions slot with fullWidth="true". JS keeps payload in sync on private-content-loaded (so the button reflects the live cart total inside the drawer). |
| Cart page | Server-rendered by the base module's checkout_cart_index.xml into checkout.cart.totals.container, which Hyvä preserves. The compat module does nothing extra here — JS only refreshes payload on cart change. |
| Checkout / success pages | Server-rendered by the base module. No compat-module involvement. |
Magento config flags (simpler_checkout/settings/enable, show_in_product_view, show_in_cart_view) gate every surface — when false, the backend returns shouldRender: false (PDP) or the layout block is suppressed (cart/minicart) and nothing is mounted.
Architecture
Three pieces, all lightweight:
Controller/Config/Product.php(route/simpler/config/product) — JSON endpoint. Looks the product up viaMagento\Catalog\Api\ProductRepositoryInterface, dispatches on$product->getTypeId()to the right base-module ViewModel (SimpleProductButton,ConfigurableProductButton,BundleProductButton), and returns{shouldRender, position, attrs: {appId, locale, payload, withAcceptedCards}}— identical shape to the existing/simpler/config/checkout. Payload computation is reused from the base module verbatim; no parallel logic.view/frontend/templates/button.phtml— Hyvä-only override of the base module's button template (scoped viaCompatModuleRegistry+ViewFileOverride). Early-returns for anysimpler.product.page*position (those are JS-injected), and emitsfullWidth="true"for thesimpler.minicartposition. All other positions (cart page, checkout, success, minicart) render through this template.view/frontend/web/js/simpler-hyva.js— small ES2020 IIFE (no build step, no RequireJS). Loaded byhead.phtmlafter the Simpler SDK. Responsibilities:- Inject
sessionIdfromwindow.hyva.getCustomerData('customer').simpler_hashonto every<simpler-checkout>element. AMutationObserverrewatches the body so elements Alpine clones into the cart drawer later also get their sessionId. - Fetch
/simpler/config/checkouton load and on everyprivate-content-loaded, then apply{appId, locale, payload, withAcceptedCards}to allsimpler-checkout[position="simpler.minicart"]andsimpler-checkout[position="simpler.cart.page"]elements in the DOM. This keeps the server-rendered minicart and cart-page buttons reflecting the live cart. - Fetch
/simpler/config/product?productId=…on PDPs, build a<simpler-checkout fullWidth="true">, anchor it in the product card (see selector contract below), and subscribe toconfigurable-selection-changedto refreshpayloadandpositionon variant change.
- Inject
Loader URLs flow into JS via data-checkout-url / data-product-url attributes on the <script id="simpler-hyva-loader"> tag emitted by head.phtml. No hard-coded paths; getUrl() handles base-URL and rewrites per store.
Selector contract (PDP auto-injection)
The PDP auto-injection in simpler-hyva.js::initPdp() reads exactly three DOM landmarks from the default Hyvä product-info.phtml. If your child theme renames or removes any of them, behaviour degrades as listed:
| Selector | Used as | If missing |
|---|---|---|
#product_addtocart_form input[name="product"] | Source of the productId to send to /simpler/config/product. | initPdp() returns early — no button on PDP. |
#product-addtocart-button | Anchor whose .closest('.card') ancestor scopes all subsequent queries. Hyvä's product-info form contains only hidden inputs, so the ATC button itself lives outside <form> (associated via form="…"). The .card ancestor is the only stable scope. | initPdp() returns early. |
.flex.mt-4.justify-end (inside the card) | Sibling anchor — the wrapper is inserted before it as a full-width row. | Falls back to card.appendChild(wrapper) — element appears at the bottom of the card instead of directly under the ATC button. |
The wrapper itself is <div class="mt-4 w-full"> and the <simpler-checkout> gets inline display: block; width: 100% plus the fullWidth="true" attribute (which toggles the SDK's full-width style on the rendered button).
If you've rebuilt product-info.phtml and want the auto-injection to keep working, the minimum contract is: keep an element with id="product-addtocart-button" somewhere inside a .card wrapper. The shortcut-row anchor is optional (graceful fallback).
Customising for a child theme
Three escape hatches, ordered from least to most invasive.
1. Hide a surface entirely
Magento admin → Stores → Configuration → Simpler Checkout:
show_in_product_view→ No → PDP endpoint returnsshouldRender: false, JS skips injection.show_in_cart_view→ No → both the minicart-drawer block (compat module) and the cart-page block (base module) are suppressed server-side.simpler_checkout/settings/enable→ No → kills everything.
2. Re-anchor the PDP button yourself
When you want Simpler on PDP but in a different spot than the default JS placement:
<!-- in your child theme's product-info.phtml or a fragment of your choice -->
<simpler-checkout
position="simpler.product.page"
appId="<?= $block->escapeHtmlAttr($simplerAppId) ?>"
locale="<?= $block->escapeHtmlAttr($locale) ?>"
withAcceptedCards="true"
fullWidth="true"
payload="<?= $block->escapeHtmlAttr($payload) ?>">
</simpler-checkout>
Compute appId / locale / payload from one of the base-module ViewModels (Simpler\Checkout\ViewModel\SimpleProductButton, ConfigurableProductButton, BundleProductButton). The compat module's MutationObserver will pick up your element and inject sessionId automatically — no custom JS needed for the session bridge.
Two caveats:
- The default JS auto-injection will also run if you leave
show_in_product_view = Yes. Either flip the config to No (and accept that no surface gets auto-wiring) or hide the auto-injected element with a CSS rule scoped to its specific wrapper. - The
configurable-selection-changedlistener is attached only to the auto-injected element. A hand-rolled embed on a configurable PDP will have a staticpayload(whichever variant was selected at page load). If you need live variant updates, add an Alpine listener that re-fetches/simpler/config/product?productId=<variantId>and writes the newpayloadattribute onto your element.
3. Render in a non-PDP / non-cart surface
For sticky add-to-cart bars, CMS blocks, custom Hyvä pages, etc. — drop the same <simpler-checkout> snippet in. The JS doesn't care where the element is; the MutationObserver covers it for sessionId, and if the position is simpler.minicart or simpler.cart.page the cart-config refresh applies attrs automatically.
You can also call the JSON endpoints directly from your own Alpine component:
const productData = await fetch('/simpler/config/product?productId=' + productId)
.then(r => r.json());
// productData.attrs.{appId, locale, payload, withAcceptedCards}, productData.position
const cartData = await fetch('/simpler/config/checkout')
.then(r => r.json());
// cartData.attrs.{appId, locale, payload, withAcceptedCards}
How the Hyvä bridge wires up
- Frontend-area
etc/frontend/di.xmlregisters the pairSimpler_CheckoutHyva→Simpler_CheckoutwithHyva\CompatModuleFallback\Model\CompatModuleRegistry. The compat-fallback'sViewFileOverrideplugin makes ourview/frontend/templates/head.phtmlandtemplates/button.phtmlwin over the base module's equivalents — but only whenHyva\Theme\Service\CurrentTheme::isHyva()returns true. On Luma, the base module's templates stay. etc/frontend/routes.xmladds the compat module under the existingsimplerfrontname so/simpler/config/productresolves toSimpler_CheckoutHyva\Controller\Config\Product.- The minicart drawer block in
view/frontend/layout/default.xmltargets Hyvä'sextra_actionscontainer. That container doesn't exist on Luma, so the layout instruction is a silent no-op there. - No
events.xml, nocrontab.xml, no observers — the module is a pure rendering bridge.