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' ); ?>

+ +
@@ -62,7 +71,14 @@ 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() ) : ?> + + @@ -54,6 +65,17 @@ $show_shipping = ! wc_ship_to_billing_address_only() && $order->needs_shipping_a get_shipping_phone() ) : ?>

get_shipping_phone() ); ?>

+ +