CSP Checkout without CSP Theme
Hyvä Checkout can be configured for strict Content Security Policy (CSP) compliance without requiring a full migration of the entire theme to be CSP-compatible. This approach allows you to enable CSP protection for the checkout route while maintaining your existing theme implementation.
However, any components shared between the theme and Hyvä Checkout must be made CSP-compatible to ensure proper functionality. These shared components include authentication drawers, cookie notices, footer elements, and messaging systems.
Known Inline Scripts
In the Hyvä default theme (hyva-themes/magento2-default-theme), several script templates are shared between the theme and Hyvä Checkout. These inline scripts require CSP authorization to function in a strict CSP environment.
Read more about this on the How does Hyvä work without unsafe-inline? page.
Registering Inline Scripts for CSP
Scripts may be used depending on enabled features. To authorize inline scripts for CSP, add the $hyvaCsp->registerInlineScript() call immediately after the closing </script> tag. This pattern applies to all inline script blocks:
Templates Requiring Script Registration
The following template files contain inline scripts that need CSP registration:
Configurable Products and Swatches Options
Magento_ConfigurableProduct::product/view/type/options/js/configurable-options.phtmlMagento_Swatches::product/js/swatch-options.phtml
Analytics and ReCaptcha
Magento_GoogleAnalytics::ga.phtmlMagento_GoogleGtag::ads.phtmlMagento_GoogleGtag::ga.phtmlMagento_ReCaptchaFrontendUi::js/script_loader.phtmlMagento_ReCaptchaFrontendUi::js/script_token.phtmlMagento_ReCaptchaFrontendUi::js/script_token_invisible.phtmlMagento_ReCaptchaFrontendUi::js/script_token_recaptcha.phtmlMagento_ReCaptchaFrontendUi::recaptcha_checkbox.phtmlMagento_ReCaptchaFrontendUi::recaptcha_invisible.phtml
Theme
Magento_Theme::html/mobile-safari-bug-workaround.phtml
Alpine Components Requiring CSP Migration
The Hyvä default theme (hyva-themes/magento2-default-theme) shares several Alpine.js components with Hyvä Checkout. These components must be migrated to use Alpine's CSP-compatible mode, which separates inline script execution from component markup.
Shared Components List
Cookie Notice Component
Magento_Cookie::notices.phtml
The Authentication Drawer
Magento_Customer::account/authentication-popup.phtml
Footer Components (Currency, Store, Language Selector, and Newsletter Subscription)
Magento_Directory::currency.phtmlMagento_Newsletter::subscribe.phtmlMagento_Store::switch/languages.phtmlMagento_Store::switch/stores.phtml
Header Components (Login as Customer Notice and Logout Link)
Magento_LoginAsCustomerFrontendUi::html/notices.phtmlMagento_LoginAsCustomerFrontendUi::html/notices/logout-link.phtml
Messaging Component
Magento_Theme::messages.phtml
There may be more shared components depending on checkout customizations and installed extensions. All components shared between the theme and Hyvä Checkout must be made CSP-compatible.
No Alpine v2 CSP
Hyvä Themes using Alpine v2 must be upgraded to use Alpine v3, as there is no alpine-csp build of version 2.
To check the Alpine version a theme is using, visit a page in a desktop browser, open the developer console, and type Alpine.version. If the reported version string starts with a 2, the theme must be updated before CSP compatibility can be achieved.
Please refer to the Hyvä Theme 1.2.0 upgrade notes for more information.
Migrating to the Hyvä Checkout CSP Edition Without Updating the Full Theme
Install the Required Packages
First update the hyva-themes/magento2-theme-module package to the newest version. Then install the CSP version of hyva-themes/magento2-hyva-checkout as specified in the installation instructions. This will also upgrade magewirephp/magewire to at least 1.12.0.
Make Shared Components in Your Theme CSP-Compatible
In the default checkout, the critical shared components are the messages component, the authentication drawer, and the newsletter subscription form (see above). There may be more shared components due to extensions and checkout customizations also needing to be made CSP-compatible.
Alpine Component CSP Migration Patterns
The following diffs demonstrate the Alpine CSP migration pattern for shared components. These are taken from the hyva-themes/magento2-default-theme-csp package and show the key changes needed to make Alpine components CSP-compatible.
Key Migration Pattern
All Alpine component migrations follow this pattern:
- Add
HyvaCspview model to template dependencies - Register Alpine component data function with
Alpine.data()instead of inline initialization - Change component method calls from inline expressions to method references (e.g.,
@click="open = false"becomes@click="close") - Register the script with
<?php $hyvaCsp->registerInlineScript() ?> - Initialize components with
x-data="componentName"instead ofx-data="componentName()"
Magento_Cookie::notices.phtml
This diff shows how to migrate the cookie notice banner component to be CSP-compatible. The key changes are registering the Alpine component with Alpine.data(), adding method references instead of inline expressions, and registering the inline script with the HyvaCsp view model.
--- a/Magento_Cookie/templates/notices.phtml
+++ b/Magento_Cookie/templates/notices.phtml
@@ -10,14 +10,16 @@ declare(strict_types=1);
use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\HeroiconsOutline;
+use Hyva\Theme\ViewModel\HyvaCsp;
use Hyva\Theme\ViewModel\Store as StoreViewModel;
use Magento\Cookie\Block\Html\Notices;
use Magento\Framework\Escaper;
use Magento\Cookie\Helper\Cookie;
/** @var Notices $block */
-/** @var Escaper $escaper */
/** @var Cookie $cookieHelper */
+/** @var Escaper $escaper */
+/** @var HyvaCsp $hyvaCsp */
/** @var ViewModelRegistry $viewModels */
/** @var HeroiconsOutline $heroicons */
@@ -56,6 +58,9 @@ if ($cookieHelper->isCookieRestrictionModeEnabled()): ?>
checkAcceptCookies() {
this.showCookieBanner = ! isAllowedSaveCookie();
},
+ hideCookieBanner() {
+ this.showCookieBanner = false;
+ },
setAcceptCookies() {
const cookieExpires = this.cookieLifetime / 60 / 60 / 24;
hyva.setCookie(this.cookieName, this.cookieValue, cookieExpires);
@@ -64,15 +69,17 @@ if ($cookieHelper->isCookieRestrictionModeEnabled()): ?>
} else {
window.dispatchEvent(new CustomEvent('user-allowed-save-cookie'));
}
+ this.showCookieBanner = false;
}
}
}
+ window.addEventListener('alpine:init', () => Alpine.data('initCookieBanner', initCookieBanner), {once: true})
</script>
-
+ <?php $hyvaCsp->registerInlineScript() ?>
<section id="notice-cookie-block"
aria-label="<?= $escaper->escapeHtmlAttr(__('We use cookies to make your experience better.')) ?>"
- x-data="initCookieBanner()"
- x-init="checkAcceptCookies()"
+ x-data="initCookieBanner"
+ x-init="checkAcceptCookies"
x-defer="idle"
>
<template x-if="showCookieBanner">
@@ -82,7 +89,7 @@ if ($cookieHelper->isCookieRestrictionModeEnabled()): ?>
border-t-2 border-container-darker"
>
<button
- @click="showCookieBanner = false;"
+ @click="hideCookieBanner"
aria-label="<?= $escaper->escapeHtmlAttr(__('Close panel')) ?>"
title="<?= $escaper->escapeHtmlAttr(__('Close panel')) ?>"
class="absolute right-0 top-0 p-4"
@@ -108,7 +115,7 @@ if ($cookieHelper->isCookieRestrictionModeEnabled()): ?>
</a>
</p>
<div class="my-2">
- <button @click="setAcceptCookies(); showCookieBanner = false"
+ <button @click="setAcceptCookies"
id="btn-cookie-allow"
class="btn btn-primary"
>
Magento_Customer::account/authentication-popup.phtml
This diff shows the authentication drawer component migration to CSP compatibility. Note the conversion of inline method calls to method references and the use of this.$event to access event data within methods.
--- a/Magento_Customer/templates/account/authentication-popup.phtml
+++ b/Magento_Customer/templates/account/authentication-popup.phtml
@@ -11,6 +11,7 @@ declare(strict_types=1);
use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\Customer\LoginButton;
use Hyva\Theme\ViewModel\HeroiconsOutline;
+use Hyva\Theme\ViewModel\HyvaCsp;
use Hyva\Theme\ViewModel\ReCaptcha;
use Hyva\Theme\ViewModel\StoreConfig;
use Magento\Framework\Escaper;
@@ -18,6 +19,7 @@ use Magento\Customer\Block\Account\Customer;
/** @var Escaper $escaper */
/** @var Customer $block */
+/** @var HyvaCsp $hyvaCsp */
/** @var ViewModelRegistry $viewModels */
/** @var ReCaptcha $recaptcha */
/** @var HeroiconsOutline $heroicons */
@@ -37,17 +39,20 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
function initAuthentication() {
return {
open: false,
+ close() {
+ this.open = false;
+ },
forceAuthentication: false,
checkoutUrl: '<?= $escaper->escapeUrl($block->getUrl('checkout/index')) ?>',
errors: 0,
hasCaptchaToken: 0,
displayErrorMessage: false,
errorMessages: [],
- setErrorMessages: function setErrorMessages(messages) {
- this.errorMessages = [messages];
- this.displayErrorMessage = this.errorMessages.length;
+ setErrorMessages(message) {
+ this.errorMessages = [message];
+ this.displayErrorMessage = true;
},
- submitForm: function () {
+ submitForm() {
// Do not rename $form, the variable is expected to be declared in the recaptcha output
const $form = document.querySelector('#login-form');
<?= $recaptcha ? $recaptcha->getValidationJsHtml('customer_login', 'auth-popup') : '' ?>
@@ -56,13 +61,17 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
this.dispatchLoginRequest($form);
}
},
- onPrivateContentLoaded: function (data) {
+ onPrivateContentLoaded() {
+ const data = this.$event.detail.data;
const isLoggedIn = data.customer && data.customer.firstname;
if (data.cart && !isLoggedIn) {
this.forceAuthentication = !data.cart.isGuestCheckoutAllowed;
}
},
- redirectIfAuthenticated: function (event) {
+ redirectIfAuthenticated() {
+ const event = this.$event;
+ this.open = this.forceAuthentication;
+
if (event.detail && event.detail.url) {
this.checkoutUrl = event.detail.url;
}
@@ -70,7 +79,10 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
window.location.href = this.checkoutUrl;
}
},
- dispatchLoginRequest: function(form) {
+ resetErrors() {
+ this.errors = 0;
+ },
+ dispatchLoginRequest(form) {
this.isLoading = true;
const username = this.$refs['customer-email'].value;
const password = this.$refs['customer-password'].value;
@@ -99,7 +111,7 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
).then(response => {
return response.json()
}
- ).then(data=> {
+ ).then(data => {
this.isLoading = false;
if (data.errors) {
this.setErrorMessages(data.message);
@@ -112,12 +124,15 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
}
}
}
+
+ window.addEventListener('alpine:init', () => Alpine.data('initAuthentication', initAuthentication), {once: true})
</script>
+<?php $hyvaCsp->registerInlineScript() ?>
<section id="authentication-popup"
- x-data="initAuthentication()"
- @private-content-loaded.window="onPrivateContentLoaded($event.detail.data)"
- @toggle-authentication.window="open = forceAuthentication; redirectIfAuthenticated(event)"
- @keydown.window.escape="open = false"
+ x-data="initAuthentication"
+ @private-content-loaded.window="onPrivateContentLoaded"
+ @toggle-authentication.window="redirectIfAuthenticated"
+ @keydown.window.escape="close"
>
<div
class="backdrop"
@@ -130,18 +145,18 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
x-transition:leave="ease-in-out duration-500"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
- @click="open = false"
+ @click="close"
></div>
<div role="dialog"
aria-modal="true"
- @click.outside="open = false"
+ @click.outside="close"
class="inset-y-0 right-0 z-30 flex max-w-full fixed"
x-cloak
x-show="open"
>
<div class="relative w-screen max-w-md pt-16 bg-container-lighter"
x-show="open"
- x-cloak=""
+ x-cloak
x-transition:enter="transform transition ease-in-out duration-500 sm:duration-700"
x-transition:enter-start="translate-x-full"
x-transition:enter-end="translate-x-0"
@@ -151,7 +166,7 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
>
<div
x-show="open"
- x-cloak=""
+ x-cloak
x-transition:enter="ease-in-out duration-500"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@@ -160,7 +175,7 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
x-transition:leave-end="opacity-0" class="absolute top-0 right-2 flex p-2 mt-2">
<button
type="button"
- @click="open = false;"
+ @click="close"
aria-label="<?= $escaper->escapeHtmlAttr(__('Close panel')) ?>"
class="p-2 text-gray-300 transition duration-150 ease-in-out hover:text-black"
>
@@ -187,7 +202,7 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
<form class="form form-login"
method="post"
- @submit.prevent="submitForm();"
+ @submit.prevent="submitForm"
id="login-form"
>
<?= $recaptcha ? $recaptcha->getInputHtml('customer_login', 'auth-popup') : '' ?>
@@ -200,7 +215,7 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
<input name="username"
id="form-login-username"
x-ref="customer-email"
- @change="errors = 0"
+ @change="resetErrors"
type="email"
required
autocomplete="<?= $isAutocompleteEnabled ? 'email' : 'off' ?>"
@@ -220,7 +235,7 @@ $isAutocompleteEnabled = $storeConfig->getStoreConfig('customer/password/autocom
required
x-ref="customer-password"
autocomplete="<?= $isAutocompleteEnabled ? 'current-password' : 'off' ?>"
- @change="errors = 0"
+ @change="resetErrors"
>
</div>
</div>
Magento_Directory::currency.phtml
This diff shows the currency switcher component migration. Note the use of hyva.createBooleanObject() helper for managing open/close state and data attributes for passing currency data to click handlers.
--- a/Magento_Directory/templates/currency.phtml
+++ b/Magento_Directory/templates/currency.phtml
@@ -11,6 +11,7 @@ declare(strict_types=1);
use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\Currency;
use Hyva\Theme\ViewModel\HeroiconsSolid;
+use Hyva\Theme\ViewModel\HyvaCsp;
use Magento\Framework\Escaper;
use Magento\Framework\View\Element\Template;
@@ -18,6 +19,7 @@ use Magento\Framework\View\Element\Template;
/** @var Template $block */
/** @var Escaper $escaper */
+/** @var HyvaCsp $hyvaCsp */
/** @var ViewModelRegistry $viewModels */
/** @var HeroiconsSolid $heroiconsSolid */
@@ -29,7 +31,7 @@ $currencyViewModel = $viewModels->require(Currency::class);
<?php if ($currencyViewModel->getCurrencyCount() > 1): ?>
<?php $currencies = $currencyViewModel->getCurrencies(); ?>
<?php $currentCurrencyCode = $currencyViewModel->getCurrentCurrencyCode(); ?>
- <div x-data="{ open: false }"
+ <div x-data="initCurrencySwitcher"
class="w-full sm:w-1/2 md:w-full pr-4"
>
<h2
@@ -40,13 +42,13 @@ $currencyViewModel = $viewModels->require(Currency::class);
</h2>
<div class="relative inline-block text-left">
<div>
- <button @click.prevent="open = !open"
- @click.outside="open = false"
- @keydown.window.escape="open=false"
+ <button @click.prevent="toggleOpen"
+ @click.outside="setOpenFalse"
+ @keydown.window.escape="setOpenFalse"
type="button"
class="inline-flex justify-center w-full form-select px-4 py-2 bg-white focus:outline-none"
aria-haspopup="true"
- :aria-expanded="open"
+ :aria-expanded="ariaExpanded"
>
<?= $escaper->escapeHtml($currentCurrencyCode) ?>
<?php if ($currencies[$currentCurrencyCode]): ?>
@@ -55,7 +57,7 @@ $currencyViewModel = $viewModels->require(Currency::class);
<?= $heroiconsSolid->chevronDownHtml("flex self-center h-5 w-5 -mr-1 ml-2", 25, 25) ?>
</button>
</div>
- <nav x-cloak=""
+ <nav x-cloak
x-show="open"
class="absolute right-0 top-full z-20 w-full lg:w-56 py-2 mt-1 overflow-auto origin-top-left rounded-sm shadow-lg sm:w-48 lg:mt-3 bg-container-lighter"
aria-labelledby="currency-heading"
@@ -67,7 +69,8 @@ $currencyViewModel = $viewModels->require(Currency::class);
role="link"
class="block px-4 py-2 lg:px-5 lg:py-2 hover:bg-gray-100"
aria-describedby="currency-heading"
- @click.prevent='hyva.postForm(<?= /* @noEscape */ $currencyViewModel->getSwitchCurrencyPostData($code) ?>)'
+ @click.prevent="switchCurrency"
+ data-currency-data="<?= $escaper->escapeHtmlAttr($currencyViewModel->getSwitchCurrencyPostData($code)) ?>"
>
<?= $escaper->escapeHtml($code) ?> - <?= $escaper->escapeHtml($name) ?>
</button>
@@ -77,4 +80,18 @@ $currencyViewModel = $viewModels->require(Currency::class);
</nav>
</div>
</div>
+ <script>
+ function initCurrencySwitcher() {
+ return hyva.createBooleanObject('open', false, {
+ ariaExpanded() {
+ return this.open() ? 'true' : 'false';
+ },
+ switchCurrency() {
+ hyva.postForm(this.$el.dataset.currencyData)
+ }
+ });
+ }
+ window.addEventListener('alpine:init', () => Alpine.data('initCurrencySwitcher', initCurrencySwitcher), {once: true})
+ </script>
+ <?php $hyvaCsp->registerInlineScript() ?>
<?php endif; ?>
```
### `Magento_Newsletter::subscribe.phtml`
This diff shows the newsletter subscription form component migration. The pattern is consistent with other components: register with `Alpine.data()`, use method references instead of inline calls, and register the script with HyvaCsp.
```diff
--- a/Magento_Newsletter/templates/subscribe.phtml
+++ b/Magento_Newsletter/templates/subscribe.phtml
@@ -8,12 +8,14 @@
use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\HeroiconsOutline;
+use Hyva\Theme\ViewModel\HyvaCsp;
use Hyva\Theme\ViewModel\ReCaptcha;
use Magento\Framework\Escaper;
use Magento\Newsletter\Block\Subscribe;
/** @var Subscribe $block */
/** @var Escaper $escaper */
+/** @var HyvaCsp $hyvaCsp */
/** @var ViewModelRegistry $viewModels */
/** @var ReCaptcha $recaptcha */
/** @var HeroiconsOutline $heroicons */
@@ -29,8 +31,8 @@ $recaptcha = $block->getData('viewModelRecaptcha');
class="form subscribe"
action="<?= $escaper->escapeUrl($block->getFormActionUrl()) ?>"
method="post"
- x-data="initNewsletterForm()"
- @submit.prevent="submitForm()"
+ x-data="initNewsletterForm"
+ @submit.prevent="submitForm"
id="newsletter-validate-detail"
aria-label="<?= $escaper->escapeHtmlAttr(__('Subscribe to Newsletter')) ?>"
>
@@ -98,5 +100,8 @@ $recaptcha = $block->getData('viewModelRecaptcha');
}
}
}
+
+ window.addEventListener('alpine:init', () => Alpine.data('initNewsletterForm', initNewsletterForm), {once: true})
</script>
+ <?php $hyvaCsp->registerInlineScript() ?>
</div>
Magento_Store::switch/languages.phtml
This diff shows the language switcher component migration. The component uses hyva.createBooleanObject() helper for simple open/close state management.
--- a/Magento_Store/templates/switch/languages.phtml
+++ b/Magento_Store/templates/switch/languages.phtml
@@ -9,6 +9,7 @@
declare(strict_types=1);
use Hyva\Theme\Model\ViewModelRegistry;
+use Hyva\Theme\ViewModel\HyvaCsp;
use Hyva\Theme\ViewModel\Store;
use Hyva\Theme\ViewModel\StoreSwitcher;
use Magento\Framework\Escaper;
@@ -19,6 +20,7 @@ use Magento\Store\ViewModel\SwitcherUrlProvider;
/** @var Template $block */
/** @var Escaper $escaper */
+/** @var HyvaCsp $hyvaCsp */
/** @var ViewModelRegistry $viewModels */
/** @var SwitcherUrlProvider $switcherUrlProvider */
@@ -33,7 +35,7 @@ $storeSwitcherViewModel = $viewModels->require(StoreSwitcher::class);
$currentStore = $storeSwitcherViewModel->getStore();
?>
<?php if (count($storeSwitcherViewModel->getStores()) > 1): ?>
- <div x-data="{ open: false }"
+ <div x-data="initLanguageSwitcher"
class="w-full sm:w-1/2 md:w-full"
>
<div class="title-font font-medium text-gray-900 tracking-widest text-sm mb-3 uppercase">
@@ -41,9 +43,9 @@ $currentStore = $storeSwitcherViewModel->getStore();
</div>
<div class="relative inline-block text-left">
<div>
- <button @click.prevent="open = !open"
- @click.outside="open = false"
- @keydown.window.escape="open=false"
+ <button @click.prevent="toggleOpen"
+ @click.outside="setOpenFalse"
+ @keydown.window.escape="setOpenFalse"
type="button"
class="form-select w-full pl-4"
aria-haspopup="true"
@@ -52,21 +54,28 @@ $currentStore = $storeSwitcherViewModel->getStore();
<?= $escaper->escapeHtml($currentStore->getName()) ?>
</button>
</div>
- <nav x-cloak=""
+ <nav x-cloak
x-show="open"
class="absolute right-0 top-full z-20 w-56 py-2 mt-1 overflow-auto origin-top-left rounded-sm shadow-lg sm:w-48 lg:mt-3 bg-container-lighter">
<div class="my-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
- <?php foreach ($storeSwitcherViewModel->getStores() as $lang): ?>
- <?php if ($lang->getId() != $storeViewModel->getStoreId()): ?>
- <a href="<?= $escaper->escapeUrl($switcherUrlProvider->getTargetStoreRedirectUrl($lang)) ?>"
- class="block px-4 py-2 lg:px-5 lg:py-2 hover:bg-gray-100"
- >
- <?= $escaper->escapeHtml($lang->getName()) ?>
- </a>
- <?php endif; ?>
- <?php endforeach; ?>
+ <?php foreach ($storeSwitcherViewModel->getStores() as $lang): ?>
+ <?php if ($lang->getId() != $storeViewModel->getStoreId()): ?>
+ <a href="<?= $escaper->escapeUrl($switcherUrlProvider->getTargetStoreRedirectUrl($lang)) ?>"
+ class="block px-4 py-2 lg:px-5 lg:py-2 hover:bg-gray-100"
+ >
+ <?= $escaper->escapeHtml($lang->getName()) ?>
+ </a>
+ <?php endif; ?>
+ <?php endforeach; ?>
</div>
</nav>
</div>
</div>
+ <script>
+ function initLanguageSwitcher() {
+ return hyva.createBooleanObject('open')
+ }
+ window.addEventListener('alpine:init', () => Alpine.data('initLanguageSwitcher', initLanguageSwitcher), {once: true})
+ </script>
+ <?php $hyvaCsp->registerInlineScript() ?>
<?php endif; ?>
Magento_Store::switch/stores.phtml
This diff shows the store switcher component migration, following the same pattern as the language switcher.
--- a/Magento_Store/templates/switch/stores.phtml
+++ b/Magento_Store/templates/switch/stores.phtml
@@ -9,6 +9,7 @@
declare(strict_types=1);
use Hyva\Theme\Model\ViewModelRegistry;
+use Hyva\Theme\ViewModel\HyvaCsp;
use Hyva\Theme\ViewModel\Store;
use Hyva\Theme\ViewModel\StoreSwitcher;
use Magento\Framework\Escaper;
@@ -19,6 +20,7 @@ use Magento\Store\ViewModel\SwitcherUrlProvider;
/** @var Template $block */
/** @var Escaper $escaper */
+/** @var HyvaCsp $hyvaCsp */
/** @var ViewModelRegistry $viewModels */
/** @var SwitcherUrlProvider $switcherUrlProvider */
@@ -33,7 +35,7 @@ $storeSwitcherViewModel = $viewModels->require(StoreSwitcher::class);
$currentStore = $storeSwitcherViewModel->getStore();
?>
<?php if (count($storeSwitcherViewModel->getGroups()) > 1): ?>
- <div x-data="{ open: false }"
+ <div x-data="initStoreSwitcher"
class="w-full sm:w-1/2 md:w-full"
>
<div class="title-font font-medium text-gray-900 tracking-widest text-sm mb-3 uppercase">
@@ -43,9 +45,9 @@ $currentStore = $storeSwitcherViewModel->getStore();
<div>
<?php foreach ($storeSwitcherViewModel->getGroups() as $group): ?>
<?php if ($group->getId() == $storeSwitcherViewModel->getCurrentGroupId()): ?>
- <button @click.prevent="open = !open"
- @click.outside="open = false"
- @keydown.window.escape="open=false"
+ <button @click.prevent="toggleOpen"
+ @click.outside="setOpenFalse"
+ @keydown.window.escape="setOpenFalse"
type="button"
class="form-select w-full pl-4"
aria-haspopup="true"
@@ -56,7 +58,7 @@ $currentStore = $storeSwitcherViewModel->getStore();
<?php endif; ?>
<?php endforeach; ?>
</div>
- <nav x-cloak=""
+ <nav x-cloak
x-show="open"
class="absolute right-0 top-full z-20 w-56 py-2 mt-1 overflow-auto origin-top-left rounded-sm shadow-lg sm:w-48 lg:mt-3 bg-container-lighter">
<div class="my-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
@@ -73,4 +75,11 @@ $currentStore = $storeSwitcherViewModel->getStore();
</nav>
</div>
</div>
+ <script>
+ function initStoreSwitcher() {
+ return hyva.createBooleanObject('open')
+ }
+ window.addEventListener('alpine:init', () => Alpine.data('initStoreSwitcher', initStoreSwitcher), {once: true})
+ </script>
+ <?php $hyvaCsp->registerInlineScript() ?>
<?php endif; ?>
Magento_LoginAsCustomerFrontendUi::html/notices.phtml
This diff shows the Login as Customer notice component migration. The pattern is identical to the cookie notice banner.
--- a/Magento_Cookie/templates/notices.phtml
+++ b/Magento_Cookie/templates/notices.phtml
@@ -10,14 +10,16 @@ declare(strict_types=1);
use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\HeroiconsOutline;
+use Hyva\Theme\ViewModel\HyvaCsp;
use Hyva\Theme\ViewModel\Store as StoreViewModel;
use Magento\Cookie\Block\Html\Notices;
use Magento\Framework\Escaper;
use Magento\Cookie\Helper\Cookie;
/** @var Notices $block */
-/** @var Escaper $escaper */
/** @var Cookie $cookieHelper */
+/** @var Escaper $escaper */
+/** @var HyvaCsp $hyvaCsp */
/** @var ViewModelRegistry $viewModels */
/** @var HeroiconsOutline $heroicons */
@@ -56,6 +58,9 @@ if ($cookieHelper->isCookieRestrictionModeEnabled()): ?>
checkAcceptCookies() {
this.showCookieBanner = ! isAllowedSaveCookie();
},
+ hideCookieBanner() {
+ this.showCookieBanner = false;
+ },
setAcceptCookies() {
const cookieExpires = this.cookieLifetime / 60 / 60 / 24;
hyva.setCookie(this.cookieName, this.cookieValue, cookieExpires);
@@ -64,15 +69,17 @@ if ($cookieHelper->isCookieRestrictionModeEnabled()): ?>
} else {
window.dispatchEvent(new CustomEvent('user-allowed-save-cookie'));
}
+ this.showCookieBanner = false;
}
}
}
+ window.addEventListener('alpine:init', () => Alpine.data('initCookieBanner', initCookieBanner), {once: true})
</script>
-
+ <?php $hyvaCsp->registerInlineScript() ?>
<section id="notice-cookie-block"
aria-label="<?= $escaper->escapeHtmlAttr(__('We use cookies to make your experience better.')) ?>"
- x-data="initCookieBanner()"
- x-init="checkAcceptCookies()"
+ x-data="initCookieBanner"
+ x-init="checkAcceptCookies"
x-defer="idle"
>
<template x-if="showCookieBanner">
@@ -82,7 +89,7 @@ if ($cookieHelper->isCookieRestrictionModeEnabled()): ?>
border-t-2 border-container-darker"
>
<button
- @click="showCookieBanner = false;"
+ @click="hideCookieBanner"
aria-label="<?= $escaper->escapeHtmlAttr(__('Close panel')) ?>"
title="<?= $escaper->escapeHtmlAttr(__('Close panel')) ?>"
class="absolute right-0 top-0 p-4"
@@ -108,7 +115,7 @@ if ($cookieHelper->isCookieRestrictionModeEnabled()): ?>
</a>
</p>
<div class="my-2">
- <button @click="setAcceptCookies(); showCookieBanner = false"
+ <button @click="setAcceptCookies"
id="btn-cookie-allow"
class="btn btn-primary"
>
Magento_LoginAsCustomerFrontendUi::html/notices/logout-link.phtml
This diff shows the Login as Customer logout link component migration. Note the use of data attributes and method references for handling the logout form submission.
--- a/Magento_LoginAsCustomerFrontendUi/templates/html/notices/logout-link.phtml
+++ b/Magento_LoginAsCustomerFrontendUi/templates/html/notices/logout-link.phtml
@@ -8,23 +8,35 @@
use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\HeroiconsOutline;
+use Hyva\Theme\ViewModel\HyvaCsp;
use Magento\Customer\Block\Account\AuthorizationLink;
use Magento\Framework\Escaper;
/** @var AuthorizationLink $block */
/** @var Escaper $escaper */
/** @var ViewModelRegistry $viewModels */
+/** @var HyvaCsp $hyvaCsp */
/** @var HeroiconsOutline $heroicons */
$heroicons = $viewModels->require(HeroiconsOutline::class);
$dataPostParam = '';
if ($block->isLoggedIn()) {
- $dataPostParam = sprintf(" @click.prevent='hyva.postForm(%s)'", $block->getPostParams());
+ $dataPostParam = sprintf(' @click.prevent="closeSession"');
}
?>
-
-<a
+<script>
+ function initCloseLoginAsCustomerSession() {
+ return {
+ closeSession() {
+ hyva.postForm(<?= /** @noEscape */ $block->getPostParams() ?>);
+ }
+ }
+ }
+ window.addEventListener('alpine:init', () => Alpine.data('initCloseLoginAsCustomerSession', initCloseLoginAsCustomerSession), {once: true})
+</script>
+<?php $hyvaCsp->registerInlineScript(); ?>
+<a x-data="initCloseLoginAsCustomerSession"
class="-mr-1 flex p-2 rounded-md text-white"
<?= /* @noEscape */ $block->getLinkAttributes() ?>
<?= /* @noEscape */ $dataPostParam ?>
Magento_Theme::messages.phtml
This diff shows the messages component migration. The component handles displaying and dismissing system messages with proper CSP compatibility.
--- a/Magento_Theme/templates/messages.phtml
+++ b/Magento_Theme/templates/messages.phtml
@@ -10,10 +10,12 @@ declare(strict_types=1);
use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\HeroiconsOutline;
+use Hyva\Theme\ViewModel\HyvaCsp;
use Hyva\Theme\ViewModel\StoreConfig;
use Magento\Framework\Escaper;
/** @var Escaper $escaper */
+/** @var HyvaCsp $hyvaCsp */
/** @var ViewModelRegistry $viewModels */
/** @var HeroiconsOutline $heroicons */
/** @var StoreConfig $storeConfig */
@@ -38,8 +40,14 @@ $defaultSuccessMessageTimeout = $storeConfig->getStoreConfig('hyva_theme_general
}, true
)
},
- removeMessage(messageIndex) {
- this.messages[messageIndex] = undefined;
+ hasMessages() {
+ return !this.isEmpty();
+ },
+ hasMessage() {
+ return !!this.message;
+ },
+ removeMessage() {
+ this.messages[this.index] = undefined;
},
addMessages(messages, hideAfter) {
messages.map((message) => {
@@ -74,31 +82,38 @@ $defaultSuccessMessageTimeout = $storeConfig->getStoreConfig('hyva_theme_general
['@clear-messages.window']() {
this.messages = [];
}
+ },
+ getMessageUiId() {
+ return 'message-' + this.message.type;
}
}
}
+
+ window.addEventListener('alpine:init', () => Alpine.data('initMessages', initMessages), {once: true})
</script>
+<?php $hyvaCsp->registerInlineScript() ?>
<section id="messages"
- x-data="initMessages()"
+ x-data="initMessages"
x-bind="eventListeners"
aria-live="assertive"
role="alert"
>
- <template x-if="!isEmpty()">
+ <template x-if="hasMessages">
<div class="w-full">
<div class="messages container mx-auto py-3">
<template x-for="(message, index) in messages" :key="index">
<div>
- <template x-if="message">
- <div class="message" :class="message.type"
- :ui-id="'message-' + message.type"
+ <template x-if="hasMessage">
+ <div class="message"
+ :class="message.type"
+ :ui-id="getMessageUiId"
>
<span x-html="message.text"></span>
<button
type="button"
class="text-gray-600 hover:text-black"
aria-label="<?= $escaper->escapeHtml(__('Close message')) ?>"
- @click.prevent="removeMessage(index)"
+ @click.prevent="removeMessage"
>
<?= $heroicons->xHtml('stroke-current', 18, 18, ['aria-hidden' => 'true']); ?>
</button>