After upgrading Magento version the custom checkout component created to handle customer informations does not work anymore, with the previous version everything was fine but now I am getting those errors in browser console:
[ERROR] Failed to load the "ui/form/field" template requested by "checkout.billing.address.lastname".
[ERROR] Failed to load the "ui/group/group" template requested by "checkout.billing.address.street".
The code is very simple there is a layout processor that adds the information about the fields reading the attribute data and then join them using Magento merger (Magento\Checkout\Block\Checkout\AttributeMerger):
<?php
declare(strict_types=1);
namespace CustomModules\Checkout\Block\Checkout;
use Magento\Eav\Api\Data\AttributeInterface;
use Magento\Checkout\Block\Checkout\AttributeMerger;
use Magento\Ui\Component\Form\AttributeMapper;
use Magento\Checkout\Block\Checkout\LayoutProcessorInterface;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Checkout\Model\Session as CheckoutSession;
use Magento\Customer\Model\AttributeMetadataDataProvider;
use Magento\Customer\Model\Customer;
use Magento\Quote\Model\Quote;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
class LayoutProcessor implements LayoutProcessorInterface
{
/**
* @var AttributeMetadataDataProvider
*/
private $attributeMetadataDataProvider;
/**
* @var AttributeMapper
*/
private $attributeMapper;
/**
* @var AttributeMerger
*/
private $merger;
/**
* @var CustomerSession
*/
private $customerSession;
/**
* @var CheckoutSession
*/
private $checkoutSession;
/**
* @var array
*/
private $previousLayout;
/**
* @var array
*/
private $jsLayout;
/**
* @param CustomerSession $customerSession
* @param CheckoutSession $checkoutSession
* @param AttributeMerger $merger
* @param AttributeMapper $attributeMapper
* @param AttributeMetadataDataProvider $attributeMetadataDataProvider
* @param array $jsLayout
*/
public function __construct(
CustomerSession $customerSession,
CheckoutSession $checkoutSession,
AttributeMerger $merger,
AttributeMapper $attributeMapper,
AttributeMetadataDataProvider $attributeMetadataDataProvider,
array $jsLayout = []
) {
$this->attributeMetadataDataProvider = $attributeMetadataDataProvider;
$this->attributeMapper = $attributeMapper;
$this->merger = $merger;
$this->customerSession = $customerSession;
$this->checkoutSession = $checkoutSession;
$this->jsLayout = $jsLayout;
}
/**
* @param array $previosLayout
* @return array
* @throws LocalizedException
*/
public function process($previousLayout): array
{
$this->previousLayout= $previousLayout;
$this->processAddress();
$this->movePayment();
$this->prefillBillingAddressForm();
return $this->jsLayout;
}
/**
* @return void
* @throws LocalizedException
*/
private function processAddress(): void
{
$fields = $this->jsLayout['components']['checkout']['children']['billing']['children']['address']['children'];
$elements = $this->getAddressAttributes();
$fields = $this->merger->merge(
$elements,
'checkoutProvider',
'billingAddress',
$fields
);
$this->jsLayout['components']['checkout']['children']['billing']['children']['address']['children'] = $fields;
}
/**
* @return array
* @throws LocalizedException
*/
private function getAddressAttributes(): array
{
$attributes = $this->attributeMetadataDataProvider->loadAttributesCollection(
'customer_address',
'customer_register_address'
);
$elements = [];
foreach ($attributes as $attribute) {
if (!$attribute->getIsUserDefined()) {
$code = $attribute->getAttributeCode();
$elements[$code] = $this->attributeMapper->map($attribute);
if (isset($elements[$code]['label'])) {
$elements[$code]['label'] = \__($label);
}
}
}
return $elements;
}
/**
* @return void
*/
private function movePayment(): void
{
$paymentComponents = $this->previousLayout['components']['checkout']['children']['steps']['children']
['billing-step']['children']['payment'];
$this->jsLayout['components']['checkout']['children']['payment'] = \array_replace_recursive(
$paymentComponents,
$this->jsLayout['components']['checkout']['children']['payment']
);
$this->removeUnusedPaymentForms();
}
/**
* @return void
*/
private function removeUnusedPaymentForms(): void
{
$unusedPaymentForms = array_keys($this->jsLayout['components']['checkout']
['children']['payment']['children']['payments-list']['children']);
foreach ($unusedPaymentForms as $paymentFormName) {
unset($this->jsLayout['components']['checkout']['children']['payment']
['children']['payments-list']['children'][$paymentFormName]);
}
}
/**
* @return void
* @throws LocalizedException
* @throws NoSuchEntityException
*/
private function prefillBillingAddressForm(): void
{
$customer = $this->customerSession->getCustomer();
$quote = $this->checkoutSession->getQuote();
if (!$customer || !$quote) {
return;
}
$billingAddress = $this->jsLayout['components']['checkout']['children']
['billing']['children']['address']['children'];
if (!$this->customerSession->isLoggedIn()) {
$this->prefillBillingAddressForGuest($billingAddress, $quote);
} else {
$this->prefillBillingAddressForCustomer($billingAddress, $customer);
}
$this->jsLayout['components']['checkout']['children']
['billing']['children']['address']['children'] = $billingAddress;
}
/**
* @param array $billingAddress
* @param Customer $customer
* @return void
*/
private function prefillBillingAddressForCustomer(array &$billingAddress, Customer $customer): void
{
$billingAddress['email']['value'] = $customer->getEmail();
$billingAddress['telephone']['value'] = $customer->getPhonePrefix() . $customer->getPhone();
$customerAddress = $customer->getDefaultBillingAddress();
$billingAddress['street']['children'][0]['value'] = '';
$billingAddress['street']['children'][1]['value'] = '';
$billingAddress['city']['value'] = '';
$billingAddress['country_id']['value'] = '';
$billingAddress['region_id']['value'] = '';
$billingAddress['postcode']['value'] = '';
if ($customerAddress) {
$billingAddress['street']['children'][0]['value'] = $customerAddress->getStreet()[0] ?? '';
$billingAddress['street']['children'][1]['value'] = $customerAddress->getStreet()[1] ?? '';
$billingAddress['postcode']['value'] = $customerAddress->getPostcode();
$billingAddress['city']['value'] = $customerAddress->getCity();
$billingAddress['country_id']['value'] = $customerAddress->getCountryId();
$billingAddress['region_id']['value'] = $customerAddress->getRegionId();
}
}
/**
* @param array $billingAddress
* @param Quote $quote
* @return void
*/
private function prefillBillingAddressForGuest(array &$billingAddress, Quote $quote): void
{
$billingAddress['firstname']['value'] = $quote->getCustomerFirstname();
$billingAddress['lastname']['value'] = $quote->getCustomerLastname();
$billingAddress['email']['value'] = $quote->getCustomerEmail();
$billingAddressFromQuote = $quote->getBillingAddress();
$billingAddress['telephone']['value'] = $billingAddressFromQuote->getTelephone();
}
}
All the informations about the components are injected in the constructor parameter jsLayout from a di.xml file:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Magento\Checkout\Block\Onepage"
type="CustomModules\Checkout\Block\Checkout\Onepage"/>
<type name="CustomModules\Checkout\Block\Checkout\Onepage">
<arguments>
<argument name="layoutProcessors" xsi:type="array">
<item name="CustomModules_Checkout::CheckoutLayoutProcessor" xsi:type="object">
CustomModules\Checkout\Block\Checkout\LayoutProcessor
</item>
<item name="directoryData" xsi:type="object">
Magento\Checkout\Block\Checkout\DirectoryDataProcessor
</item>
</argument>
</arguments>
</type>
<type name="CustomModules\Checkout\Block\Checkout\LayoutProcessor">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
<item name="config" xsi:type="array">
<item name="template" xsi:type="string">CustomModules_Checkout/onepage</item>
</item>
<item name="children" xsi:type="array">
<item name="billing" xsi:type="array">
<item name="component" xsi:type="string">
CustomModules_Checkout/js/view/billing
</item>
<item name="config" xsi:type="array">
<item name="template" xsi:type="string">CustomModules_Checkout/onepage/billing</item>
</item>
<item name="provider" xsi:type="string">checkoutProvider</item>
<item name="deps" xsi:type="array">
<item name="0" xsi:type="string">checkoutProvider</item>
</item>
<item name="dataScopePrefix" xsi:type="string">billingAddress</item>
<item name="children" xsi:type="array">
<item name="address" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
<item name="displayArea" xsi:type="string">billingAddress</item>
<item name="dataScopePrefix" xsi:type="string">billingAddress</item>
<item name="children" xsi:type="array">
<item name="firstname" xsi:type="array">
<item name="config" xsi:type="array">
<item name="disabled" xsi:type="boolean">true</item>
</item>
</item>
<item name="lastname" xsi:type="array">
<item name="config" xsi:type="array">
<item name="disabled" xsi:type="boolean">true</item>
</item>
</item>
<item name="email" xsi:type="array">
<item name="config" xsi:type="array">
<item name="disabled" xsi:type="boolean">true</item>
<item name="template" xsi:type="string">
ui/form/field
</item>
<item name="elementTmpl" xsi:type="string">
ui/form/element/input
</item>
</item>
<item name="label" xsi:type="string" translate="true">
Email
</item>
</item>
<item name="telephone" xsi:type="array">
<item name="config" xsi:type="array">
<item name="disabled" xsi:type="boolean">true</item>
</item>
<item name="validation" xsi:type="array">
<item name="required-entry" xsi:type="boolean">
false
</item>
</item>
</item>
<item name="region" xsi:type="array">
<item name="visible" xsi:type="boolean">false</item>
</item>
<item name="region_id" xsi:type="array">
<item name="component" xsi:type="string">
Magento_Ui/js/form/element/region
</item>
<item name="visible" xsi:type="boolean">false</item>
<item name="config" xsi:type="array">
<item name="elementTmpl" xsi:type="string">
ui/form/element/select
</item>
<item name="customEntry" xsi:type="string">
billingAddress.region
</item>
<item name="additionalClasses" xsi:type="string">
select-option
</item>
</item>
<item name="validation" xsi:type="array">
<item name="required-entry" xsi:type="boolean">
true
</item>
</item>
<item name="filterBy" xsi:type="array">
<item name="target" xsi:type="string">
${ $.provider }:${ $.parentScope }.country_id
</item>
<item name="field" xsi:type="string">
country_id
</item>
</item>
</item>
<item name="country_id" xsi:type="array">
<item name="config" xsi:type="array">
<item name="disabled" xsi:type="boolean">true</item>
</item>
</item>
<item name="postcode" xsi:type="array">
<item name="validation" xsi:type="array">
<item name="required-entry" xsi:type="boolean">
true
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
<item name="checkoutProvider" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
</item>
</item>
</argument>
</arguments>
</type>
</config>