From d858d2293044e844c1091350695f8e675eca41c4 Mon Sep 17 00:00:00 2001
From: Mike Jolley
Date: Mon, 5 Feb 2024 10:59:17 +0000
Subject: [PATCH] [Experimental] Surface custom fields in My Account pages
(#43823)
* Get default attributes on front end
* Remove console log
* Show notice about 0 registered fields in editor
* Do not show block in editor if no fields are registered
* Wrapper block
* Update webpack config
* Wrapper block type definition
* Separate fields from the hook block
* Hide when no custom fields exist
* Address and contact fields
* Shared form fields in settings
* Revert render_content to original
* Allow definition of unchecked value which includes a hidden field
* Move woocommerce_edit_account_form above password section, which should be last.
* My Account hooks
* Move account handling to service class
* Show fields in order details
* Remove unused prop
* remove comment
* Hook docblocks
* Add changefile(s) from automation for the following project(s): woocommerce
* Add note about nonce verification
* wp_kses_post for label
* fix nonce linting
* Add docblock to hook
* Bump template version
* Persist contact fields to customer
* Change action name
* Add changefile(s) from automation for the following project(s): woocommerce
* Changelog
* Margin fix on order confirmation for older core themes
* Bump version tags
---------
Co-authored-by: Thomas Roberts
Co-authored-by: github-actions
---
...dd-additional-information-myaccount-fields | 4 +
.../client/legacy/css/twenty-twenty-two.scss | 3 +-
.../legacy/css/woocommerce-blocktheme.scss | 5 +-
.../client/legacy/css/woocommerce.scss | 86 +++++-
.../includes/wc-template-functions.php | 22 +-
.../src/Blocks/Domain/Bootstrap.php | 10 +-
.../Blocks/Domain/Services/CheckoutFields.php | 5 +-
.../Services/CheckoutFieldsFrontend.php | 256 ++++++++++++++++++
.../templates/myaccount/form-edit-account.php | 20 +-
.../templates/myaccount/my-address.php | 10 +-
.../order/order-details-customer.php | 24 +-
11 files changed, 433 insertions(+), 12 deletions(-)
create mode 100644 plugins/woocommerce/changelog/43823-add-additional-information-myaccount-fields
create mode 100644 plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsFrontend.php
diff --git a/plugins/woocommerce/changelog/43823-add-additional-information-myaccount-fields b/plugins/woocommerce/changelog/43823-add-additional-information-myaccount-fields
new file mode 100644
index 00000000000..8bb60ff0bca
--- /dev/null
+++ b/plugins/woocommerce/changelog/43823-add-additional-information-myaccount-fields
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Introduced `woocommerce_my_account_after_my_address`, `woocommerce_order_details_after_customer_address`, and `woocommerce_edit_account_form_fields` hooks.
diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss
index eea3c30d68a..8f36b918a5d 100644
--- a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss
+++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss
@@ -1002,7 +1002,8 @@ ul.wc-tabs {
}
.woocommerce-thankyou-order-received,
- h2.woocommerce-column__title {
+ .woocommerce-column__title,
+ .woocommerce-customer-details h2 {
font-family: var(--wp--preset--font-family--source-serif-pro);
}
diff --git a/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss b/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss
index 2d62431c432..d2e3e151cc6 100644
--- a/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss
+++ b/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss
@@ -349,7 +349,8 @@ a.added_to_cart {
* Order confirmation
*/
.woocommerce-thankyou-order-received,
- h2.woocommerce-column__title {
+ .woocommerce-column__title,
+ .woocommerce-customer-details h2 {
font-size: var(--wp--preset--font-size--large);
font-weight: 300;
}
@@ -383,7 +384,7 @@ a.added_to_cart {
// Ensure customer details match order overview.
box-sizing: border-box;
width: 70%;
- padding: 2rem;
+ padding: 1rem;
border-width: 1px;
border-radius: 0;
}
diff --git a/plugins/woocommerce/client/legacy/css/woocommerce.scss b/plugins/woocommerce/client/legacy/css/woocommerce.scss
index 04c9b1813df..f771f538856 100644
--- a/plugins/woocommerce/client/legacy/css/woocommerce.scss
+++ b/plugins/woocommerce/client/legacy/css/woocommerce.scss
@@ -1410,8 +1410,86 @@ p.demo_store,
}
}
+ .woocommerce-customer-details .addresses,
+ .woocommerce-customer-details .additional-fields {
+ margin-bottom: 2em;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .addresses .wc-block-components-additional-fields-list {
+ margin: 0;
+ padding: 0;
+
+ dt {
+ margin: 0;
+ padding: 0;
+ font-style: normal;
+ font-weight: bold;
+ display: inline;
+
+ &::after {
+ content: ": ";
+ }
+
+ &::before {
+ content: "";
+ display: block;
+ }
+ }
+
+ dd {
+ margin: 0;
+ padding: 0;
+ font-style: normal;
+ display: inline;
+ }
+ }
+
+ .wc-block-order-confirmation-additional-fields-wrapper .wc-block-components-additional-fields-list {
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ padding: 0;
+ display: grid;
+ grid-template-columns: 1fr max-content;
+
+ dt {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+ font-style: normal;
+ font-weight: bold;
+ padding: 1rem;
+ box-sizing: border-box;
+ margin: 0 !important;
+
+ &::after {
+ display: none;
+ }
+
+ &:last-of-type {
+ border-bottom: 0;
+ }
+ }
+
+ dd {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+ padding: 1rem;
+ box-sizing: border-box;
+ text-align: right;
+ margin: 0 !important;
+
+ &:last-of-type {
+ border-bottom: 0;
+ }
+ }
+ }
+
.woocommerce-customer-details {
+ .woocommerce-column__title {
+ margin-top: 0;
+ }
+
address {
font-style: normal;
margin-bottom: 0;
@@ -1422,12 +1500,16 @@ p.demo_store,
width: 100%;
border-radius: 5px;
padding: 6px 12px;
+ box-sizing: border-box;
}
.woocommerce-customer-details--phone,
.woocommerce-customer-details--email {
- margin-bottom: 0;
padding-left: 1.5em;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
}
.woocommerce-customer-details--phone::before {
@@ -1634,6 +1716,7 @@ p.demo_store,
*/
.woocommerce:where(body:not(.woocommerce-block-theme-has-button-styles)),
:where(body:not(.woocommerce-block-theme-has-button-styles)) .woocommerce {
+
a.button,
button.button,
input.button,
@@ -1740,6 +1823,7 @@ p.demo_store,
}
div.product {
+
span.price,
p.price {
color: $highlight;
diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php
index dba1ec3a052..85c5433f218 100644
--- a/plugins/woocommerce/includes/wc-template-functions.php
+++ b/plugins/woocommerce/includes/wc-template-functions.php
@@ -2830,6 +2830,8 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) {
'default' => '',
'autofocus' => '',
'priority' => '',
+ 'unchecked_value' => null,
+ 'checked_value' => '1',
);
$args = wp_parse_args( $args, $defaults );
@@ -2956,8 +2958,24 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) {
break;
case 'checkbox':
- $field = '';
+ $field = '';
break;
case 'text':
diff --git a/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php b/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
index e353f0245ed..fb70ee61f86 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
@@ -16,6 +16,7 @@ use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsAdmin;
+use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsFrontend;
use Automattic\WooCommerce\Blocks\InboxNotifications;
use Automattic\WooCommerce\Blocks\Installer;
use Automattic\WooCommerce\Blocks\Migration;
@@ -142,7 +143,7 @@ class Bootstrap {
$this->container->get( Installer::class )->init();
$this->container->get( GoogleAnalytics::class )->init();
$this->container->get( CheckoutFields::class )->init();
- $this->container->get( CheckoutFieldsAdmin::class )->init();
+ $this->container->get( is_admin() ? CheckoutFieldsAdmin::class : CheckoutFieldsFrontend::class )->init();
}
// Load assets unless this is a request specifically for the store API.
@@ -362,6 +363,13 @@ class Bootstrap {
return new CheckoutFieldsAdmin( $checkout_fields_controller );
}
);
+ $this->container->register(
+ CheckoutFieldsFrontend::class,
+ function( Container $container ) {
+ $checkout_fields_controller = $container->get( CheckoutFields::class );
+ return new CheckoutFieldsFrontend( $checkout_fields_controller );
+ }
+ );
$this->container->register(
PaymentsApi::class,
function ( Container $container ) {
diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php
index 0080be51288..9036ecedcb4 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php
@@ -1001,7 +1001,10 @@ class CheckoutFields {
* @return array The filtered fields.
*/
public function filter_fields_for_customer( $fields ) {
- $customer_fields_keys = $this->get_address_fields_keys();
+ $customer_fields_keys = array_merge(
+ $this->get_address_fields_keys(),
+ $this->get_contact_fields_keys()
+ );
return array_filter(
$fields,
function( $key ) use ( $customer_fields_keys ) {
diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsFrontend.php b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsFrontend.php
new file mode 100644
index 00000000000..6b53e042b13
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsFrontend.php
@@ -0,0 +1,256 @@
+checkout_fields_controller = $checkout_fields_controller;
+ }
+
+ /**
+ * Initialize hooks. This is not run Store API requests.
+ */
+ public function init() {
+ // Show custom checkout fields on the order details page.
+ add_action( 'woocommerce_order_details_after_customer_address', array( $this, 'render_order_address_fields' ), 10, 2 );
+ add_action( 'woocommerce_order_details_after_customer_details', array( $this, 'render_order_additional_fields' ), 10 );
+
+ // Show custom checkout fields on the My Account page.
+ add_action( 'woocommerce_my_account_after_my_address', array( $this, 'render_address_fields' ), 10, 1 );
+
+ // Field editing in my account area.
+ add_filter( 'woocommerce_save_account_details_required_fields', array( $this, 'edit_account_form_required_fields' ), 10, 1 );
+ add_filter( 'woocommerce_edit_account_form_fields', array( $this, 'edit_account_form_fields' ), 10, 1 );
+ add_action( 'woocommerce_save_account_details', array( $this, 'save_account_form_fields' ), 10, 1 );
+ add_filter( 'woocommerce_address_to_edit', array( $this, 'edit_address_fields' ), 10, 2 );
+ add_action( 'woocommerce_after_save_address_validation', array( $this, 'save_address_fields' ), 10, 2 );
+ }
+
+ /**
+ * Render custom fields.
+ *
+ * @param array $fields List of additional fields with values.
+ * @return string
+ */
+ protected function render_additional_fields( $fields ) {
+ return ! empty( $fields ) ? '' . implode( '', array_map( array( $this, 'render_additional_field' ), $fields ) ) . '
' : '';
+ }
+
+ /**
+ * Render custom field.
+ *
+ * @param array $field An additional field and value.
+ * @return string
+ */
+ protected function render_additional_field( $field ) {
+ return sprintf(
+ '%1$s%2$s',
+ esc_html( $field['label'] ),
+ esc_html( $field['value'] )
+ );
+ }
+
+ /**
+ * Renders address fields on the order details page.
+ *
+ * @param string $address_type Type of address (billing or shipping).
+ * @param WC_Order $order Order object.
+ */
+ public function render_order_address_fields( $address_type, $order ) {
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ echo $this->render_additional_fields( $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $address_type, 'view' ) );
+ }
+
+ /**
+ * Renders additional fields on the order details page.
+ *
+ * @param WC_Order $order Order object.
+ */
+ public function render_order_additional_fields( $order ) {
+ $fields = array_merge(
+ $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', '', 'view' ),
+ $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'additional', '', 'view' ),
+ );
+
+ if ( ! $fields ) {
+ return;
+ }
+
+ echo '';
+ echo '' . esc_html__( 'Additional information', 'woocommerce' ) . '
';
+ echo $this->render_additional_fields( $fields ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ echo '';
+ }
+
+ /**
+ * Renders address fields on the account page.
+ *
+ * @param string $address_type Type of address (billing or shipping).
+ */
+ public function render_address_fields( $address_type ) {
+ if ( ! in_array( $address_type, array( 'billing', 'shipping' ), true ) ) {
+ return;
+ }
+
+ $customer = new WC_Customer( get_current_user_id() );
+ $fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
+
+ if ( ! $fields || ! $customer ) {
+ return;
+ }
+
+ foreach ( $fields as $key => $field ) {
+ $value = $this->checkout_fields_controller->format_additional_field_value(
+ $this->checkout_fields_controller->get_field_from_customer( $key, $customer, $address_type ),
+ $field
+ );
+
+ if ( ! $value ) {
+ continue;
+ }
+
+ printf( '
%s: %s', wp_kses_post( $field['label'] ), wp_kses_post( $value ) );
+ }
+ }
+
+ /**
+ * Register required additional contact fields.
+ *
+ * @param array $fields Required fields.
+ * @return array
+ */
+ public function edit_account_form_required_fields( $fields ) {
+ $additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
+
+ foreach ( $additional_fields as $key => $field ) {
+ if ( ! empty( $field['required'] ) ) {
+ $fields[ $key ] = $field['label'];
+ }
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Adds additional contact fields to the My Account edit account form.
+ */
+ public function edit_account_form_fields() {
+ $customer = new WC_Customer( get_current_user_id() );
+ $fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
+
+ foreach ( $fields as $key => $field ) {
+ $form_field = $field;
+ $form_field['value'] = $this->checkout_fields_controller->get_field_from_customer( $key, $customer, 'contact' );
+
+ if ( 'select' === $field['type'] ) {
+ $form_field['options'] = array_column( $field['options'], 'label', 'value' );
+ }
+
+ if ( 'checkbox' === $field['type'] ) {
+ $form_field['checked_value'] = '1';
+ $form_field['unchecked_value'] = '0';
+ }
+
+ woocommerce_form_field( $key, $form_field, wc_get_post_data_by_key( $key, $form_field['value'] ) );
+ }
+ }
+
+ /**
+ * Validates and saves additional address fields to the customer object on the My Account page.
+ *
+ * @param integer $user_id User ID.
+ */
+ public function save_account_form_fields( $user_id ) {
+ $customer = new WC_Customer( $user_id );
+ $fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
+
+ // phpcs:disable WordPress.Security.NonceVerification.Missing
+ foreach ( $fields as $field_key => $field ) {
+ if ( ! isset( $_POST[ $field_key ] ) ) {
+ continue;
+ }
+ $this->checkout_fields_controller->persist_field_for_customer( $field_key, wc_clean( wp_unslash( $_POST[ $field_key ] ) ), $customer );
+ }
+ // phpcs:enable WordPress.Security.NonceVerification.Missing
+ }
+
+ /**
+ * Adds additional address fields to the My Account edit address form.
+ *
+ * @param array $address Address fields.
+ * @param string $address_type Type of address (billing or shipping).
+ * @return array Updated address fields.
+ */
+ public function edit_address_fields( $address, $address_type ) {
+ $customer = new WC_Customer( get_current_user_id() );
+ $fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
+
+ foreach ( $fields as $key => $field ) {
+ $field_key = 'billing' === $address_type ? '/billing/' . $key : '/shipping/' . $key;
+ $address[ $field_key ] = $field;
+ $address[ $field_key ]['value'] = $this->checkout_fields_controller->get_field_from_customer( $key, $customer, $address_type );
+
+ if ( 'select' === $field['type'] ) {
+ $address[ $field_key ]['options'] = array_column( $field['options'], 'label', 'value' );
+ }
+
+ if ( 'checkbox' === $field['type'] ) {
+ $address[ $field_key ]['checked_value'] = '1';
+ $address[ $field_key ]['unchecked_value'] = '0';
+ }
+ }
+
+ return $address;
+ }
+
+ /**
+ * Validates and saves additional address fields to the customer object on the My Account page.
+ *
+ * @param integer $user_id User ID.
+ * @param string $address_type Type of address (billing or shipping).
+ */
+ public function save_address_fields( $user_id, $address_type ) {
+ $customer = new WC_Customer( $user_id );
+ $fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
+
+ // Nonces are checked before the action is fired that this function hooks into.
+ // phpcs:disable WordPress.Security.NonceVerification.Missing
+ foreach ( $fields as $key => $field ) {
+ $field_key = 'billing' === $address_type ? '/billing/' . $key : '/shipping/' . $key;
+
+ if ( ! isset( $_POST[ $field_key ] ) ) {
+ continue;
+ }
+
+ $value = wc_clean( wp_unslash( $_POST[ $field_key ] ) );
+
+ if ( ! empty( $field['required'] ) && empty( $value ) ) {
+ // translators: %s field label.
+ wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), $field['label'] ), 'error' );
+ continue;
+ }
+
+ $this->checkout_fields_controller->persist_field_for_customer( $field_key, $value, $customer );
+ }
+ // phpcs:enable WordPress.Security.NonceVerification.Missing
+ }
+}
diff --git a/plugins/woocommerce/templates/myaccount/form-edit-account.php b/plugins/woocommerce/templates/myaccount/form-edit-account.php
index fc70e4bade8..66a12f8eb5e 100644
--- a/plugins/woocommerce/templates/myaccount/form-edit-account.php
+++ b/plugins/woocommerce/templates/myaccount/form-edit-account.php
@@ -12,7 +12,7 @@
*
* @see https://woo.com/document/template-structure/
* @package WooCommerce\Templates
- * @version 7.0.1
+ * @version 8.7.0
*/
defined( 'ABSPATH' ) || exit;
@@ -44,6 +44,15 @@ do_action( 'woocommerce_before_edit_account_form' ); ?>
+
+
-
+
diff --git a/plugins/woocommerce/templates/myaccount/my-address.php b/plugins/woocommerce/templates/myaccount/my-address.php
index 15c8e40ade0..52673526637 100644
--- a/plugins/woocommerce/templates/myaccount/my-address.php
+++ b/plugins/woocommerce/templates/myaccount/my-address.php
@@ -12,7 +12,7 @@
*
* @see https://woo.com/document/template-structure/
* @package WooCommerce\Templates
- * @version 2.6.0
+ * @version 8.7.0
*/
defined( 'ABSPATH' ) || exit;
@@ -65,6 +65,14 @@ $col = 1;
diff --git a/plugins/woocommerce/templates/order/order-details-customer.php b/plugins/woocommerce/templates/order/order-details-customer.php
index 1c209ffb950..503d4fda054 100644
--- a/plugins/woocommerce/templates/order/order-details-customer.php
+++ b/plugins/woocommerce/templates/order/order-details-customer.php
@@ -12,7 +12,7 @@
*
* @see https://woo.com/document/template-structure/
* @package WooCommerce\Templates
- * @version 5.6.0
+ * @version 8.7.0
*/
defined( 'ABSPATH' ) || exit;
@@ -40,6 +40,17 @@ $show_shipping = ! wc_ship_to_billing_address_only() && $order->needs_shipping_a
get_billing_email() ) : ?>
get_billing_email() ); ?>
+
+
@@ -54,6 +65,17 @@ $show_shipping = ! wc_ship_to_billing_address_only() && $order->needs_shipping_a
get_shipping_phone() ) : ?>
get_shipping_phone() ); ?>
+
+