CheckoutFieldsSchema
11 months ago
Email
1 year ago
CheckoutFields.php
11 months ago
CheckoutFieldsAdmin.php
2 years ago
CheckoutFieldsFrontend.php
11 months ago
CheckoutLink.php
11 months ago
CreateAccount.php
1 year ago
DraftOrders.php
2 months ago
FeatureGating.php
1 year ago
GoogleAnalytics.php
2 years ago
Hydration.php
5 months ago
Notices.php
1 year ago
functions.php
2 years ago
CheckoutFieldsFrontend.php
382 lines
| 1 | <?php |
| 2 | |
| 3 | namespace Automattic\WooCommerce\Blocks\Domain\Services; |
| 4 | |
| 5 | use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema\DocumentObject; |
| 6 | use WC_Customer; |
| 7 | use WC_Order; |
| 8 | |
| 9 | /** |
| 10 | * Service class managing checkout fields and its related extensibility points on the frontend. |
| 11 | */ |
| 12 | class CheckoutFieldsFrontend { |
| 13 | |
| 14 | /** |
| 15 | * Checkout field controller. |
| 16 | * |
| 17 | * @var CheckoutFields |
| 18 | */ |
| 19 | private $checkout_fields_controller; |
| 20 | |
| 21 | /** |
| 22 | * Sets up core fields. |
| 23 | * |
| 24 | * @param CheckoutFields $checkout_fields_controller Instance of the checkout field controller. |
| 25 | */ |
| 26 | public function __construct( CheckoutFields $checkout_fields_controller ) { |
| 27 | $this->checkout_fields_controller = $checkout_fields_controller; |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * Initialize hooks. This is not run Store API requests. |
| 32 | */ |
| 33 | public function init() { |
| 34 | // Show custom checkout fields on the order details page. |
| 35 | add_action( 'woocommerce_order_details_after_customer_address', array( $this, 'render_order_address_fields' ), 10, 2 ); |
| 36 | add_action( 'woocommerce_order_details_after_customer_details', array( $this, 'render_order_other_fields' ), 10 ); |
| 37 | |
| 38 | // Show custom checkout fields on the My Account page. |
| 39 | add_action( 'woocommerce_my_account_after_my_address', array( $this, 'render_address_fields' ), 10, 1 ); |
| 40 | |
| 41 | // Edit account form under my account (for contact details). |
| 42 | add_filter( 'woocommerce_edit_account_form_fields', array( $this, 'edit_account_form_fields' ), 10, 1 ); |
| 43 | add_action( 'woocommerce_save_account_details', array( $this, 'save_account_form_fields' ), 10, 1 ); |
| 44 | |
| 45 | // Edit address form under my account. |
| 46 | add_filter( 'woocommerce_address_to_edit', array( $this, 'edit_address_fields' ), 10, 2 ); |
| 47 | add_action( 'woocommerce_customer_save_address', array( $this, 'save_address_fields' ), 10, 4 ); |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * Render custom fields. |
| 52 | * |
| 53 | * @param array $fields List of additional fields with values. |
| 54 | * @return string |
| 55 | */ |
| 56 | protected function render_additional_fields( $fields ) { |
| 57 | return ! empty( $fields ) ? '<dl class="wc-block-components-additional-fields-list">' . implode( '', array_map( array( $this, 'render_additional_field' ), $fields ) ) . '</dl>' : ''; |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * Render custom field. |
| 62 | * |
| 63 | * @param array $field An additional field and value. |
| 64 | * @return string |
| 65 | */ |
| 66 | protected function render_additional_field( $field ) { |
| 67 | return sprintf( |
| 68 | '<dt>%1$s</dt><dd>%2$s</dd>', |
| 69 | esc_html( $field['label'] ), |
| 70 | esc_html( $field['value'] ) |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * Renders address fields on the order details page. |
| 76 | * |
| 77 | * @param string $address_type Type of address (billing or shipping). |
| 78 | * @param WC_Order $order Order object. |
| 79 | */ |
| 80 | public function render_order_address_fields( $address_type, $order ) { |
| 81 | // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 82 | echo $this->render_additional_fields( $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $address_type, 'view' ) ); |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Renders additional fields on the order details page. |
| 87 | * |
| 88 | * @param WC_Order $order Order object. |
| 89 | */ |
| 90 | public function render_order_other_fields( $order ) { |
| 91 | $fields = array_merge( |
| 92 | $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', 'other', 'view' ), |
| 93 | $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'order', 'other', 'view' ), |
| 94 | ); |
| 95 | |
| 96 | $context = array( |
| 97 | 'caller' => 'CheckoutFieldsFrontend::render_order_other_fields', |
| 98 | 'order' => $order, |
| 99 | ); |
| 100 | |
| 101 | $fields = $this->checkout_fields_controller->filter_fields_for_order_confirmation( $fields, $context ); |
| 102 | |
| 103 | if ( ! $fields ) { |
| 104 | return; |
| 105 | } |
| 106 | |
| 107 | echo '<section class="wc-block-order-confirmation-additional-fields-wrapper">'; |
| 108 | echo '<h2>' . esc_html__( 'Additional information', 'woocommerce' ) . '</h2>'; |
| 109 | echo $this->render_additional_fields( $fields ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 110 | echo '</section>'; |
| 111 | } |
| 112 | |
| 113 | /** |
| 114 | * Renders address fields on the account page. |
| 115 | * |
| 116 | * @param string $address_type Type of address (billing or shipping). |
| 117 | */ |
| 118 | public function render_address_fields( $address_type ) { |
| 119 | if ( ! in_array( $address_type, array( 'billing', 'shipping' ), true ) ) { |
| 120 | return; |
| 121 | } |
| 122 | |
| 123 | $customer = new WC_Customer( get_current_user_id() ); |
| 124 | |
| 125 | $document_object = new DocumentObject(); |
| 126 | $document_object->set_customer( $customer ); |
| 127 | $document_object->set_context( $address_type . '_address' ); |
| 128 | $fields = $this->checkout_fields_controller->get_contextual_fields_for_location( 'address', $document_object ); |
| 129 | |
| 130 | if ( ! $fields || ! $customer ) { |
| 131 | return; |
| 132 | } |
| 133 | |
| 134 | foreach ( $fields as $key => $field ) { |
| 135 | $value = $this->checkout_fields_controller->format_additional_field_value( |
| 136 | $this->checkout_fields_controller->get_field_from_object( $key, $customer, $address_type ), |
| 137 | $field |
| 138 | ); |
| 139 | |
| 140 | if ( ! $value ) { |
| 141 | continue; |
| 142 | } |
| 143 | |
| 144 | printf( '<br><strong>%s</strong>: %s', wp_kses_post( $field['label'] ), wp_kses_post( $value ) ); |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | /** |
| 149 | * Adds additional contact fields to the My Account edit account form. |
| 150 | */ |
| 151 | public function edit_account_form_fields() { |
| 152 | $customer = new WC_Customer( get_current_user_id() ); |
| 153 | |
| 154 | $document_object = new DocumentObject(); |
| 155 | $document_object->set_customer( $customer ); |
| 156 | $document_object->set_context( 'contact' ); |
| 157 | $fields = $this->checkout_fields_controller->get_contextual_fields_for_location( 'contact', $document_object ); |
| 158 | |
| 159 | foreach ( $fields as $key => $field ) { |
| 160 | $field_key = CheckoutFields::get_group_key( 'other' ) . $key; |
| 161 | $form_field = $field; |
| 162 | $form_field['id'] = $field_key; |
| 163 | $form_field['value'] = $this->checkout_fields_controller->get_field_from_object( $key, $customer, 'contact' ); |
| 164 | |
| 165 | if ( 'select' === $field['type'] ) { |
| 166 | $form_field['options'] = array_column( $field['options'], 'label', 'value' ); |
| 167 | } |
| 168 | |
| 169 | if ( 'checkbox' === $field['type'] ) { |
| 170 | $form_field['checked_value'] = '1'; |
| 171 | $form_field['unchecked_value'] = '0'; |
| 172 | } |
| 173 | |
| 174 | woocommerce_form_field( $field_key, $form_field, wc_get_post_data_by_key( $key, $form_field['value'] ) ); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | /** |
| 179 | * Adds additional address fields to the My Account edit address form. |
| 180 | * |
| 181 | * @param array $address Address fields. |
| 182 | * @param string $address_type Type of address (billing or shipping). |
| 183 | * @return array Updated address fields. |
| 184 | */ |
| 185 | public function edit_address_fields( $address, $address_type ) { |
| 186 | $customer = new WC_Customer( get_current_user_id() ); |
| 187 | |
| 188 | $document_object = new DocumentObject(); |
| 189 | $document_object->set_customer( $customer ); |
| 190 | $document_object->set_context( $address_type . '_address' ); |
| 191 | $fields = $this->checkout_fields_controller->get_contextual_fields_for_location( 'address', $document_object ); |
| 192 | |
| 193 | foreach ( $fields as $key => $field ) { |
| 194 | $field_key = CheckoutFields::get_group_key( $address_type ) . $key; |
| 195 | $address[ $field_key ] = $field; |
| 196 | $address[ $field_key ]['value'] = $this->checkout_fields_controller->get_field_from_object( $key, $customer, $address_type ); |
| 197 | |
| 198 | if ( 'select' === $field['type'] ) { |
| 199 | $address[ $field_key ]['options'] = array_column( $field['options'], 'label', 'value' ); |
| 200 | |
| 201 | // If a placeholder is set, add a placeholder option if it doesn't exist already. |
| 202 | if ( |
| 203 | ! empty( $address[ $field_key ]['placeholder'] ) |
| 204 | && ! array_key_exists( '', $address[ $field_key ]['options'] ) |
| 205 | ) { |
| 206 | $address[ $field_key ]['options'] = array( '' => $address[ $field_key ]['placeholder'] ) + $address[ $field_key ]['options']; |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | if ( 'checkbox' === $field['type'] ) { |
| 211 | $address[ $field_key ]['checked_value'] = '1'; |
| 212 | $address[ $field_key ]['unchecked_value'] = '0'; |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | return $address; |
| 217 | } |
| 218 | |
| 219 | /** |
| 220 | * Validates and saves additional address fields to the customer object on the My Account page. |
| 221 | * |
| 222 | * Customer is not provided by this hook so we handle save here. |
| 223 | * |
| 224 | * @param integer $user_id User ID. |
| 225 | */ |
| 226 | public function save_account_form_fields( $user_id ) { |
| 227 | try { |
| 228 | $customer = new WC_Customer( $user_id ); |
| 229 | $result = $this->update_additional_fields_for_customer( $customer, 'contact', 'other' ); |
| 230 | |
| 231 | if ( is_wp_error( $result ) ) { |
| 232 | foreach ( $result->get_error_messages() as $error_message ) { |
| 233 | wc_add_notice( $error_message, 'error' ); |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | $customer->save(); |
| 238 | } catch ( \Exception $e ) { |
| 239 | wc_add_notice( |
| 240 | sprintf( |
| 241 | /* translators: %s: Error message. */ |
| 242 | __( 'An error occurred while saving account details: %s', 'woocommerce' ), |
| 243 | esc_html( $e->getMessage() ) |
| 244 | ), |
| 245 | 'error' |
| 246 | ); |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | /** |
| 251 | * For the My Account page, save address fields. This uses the Store API endpoint for saving addresses so |
| 252 | * extensibility hooks are consistent across the codebase. |
| 253 | * |
| 254 | * The caller saves the customer object if there are no errors. Nonces are checked before this method executes. |
| 255 | * |
| 256 | * @param integer $user_id User ID. |
| 257 | * @param string $address_type Type of address (billing or shipping). |
| 258 | * @param array $address Address fields. |
| 259 | * @param WC_Customer $customer Customer object. |
| 260 | */ |
| 261 | public function save_address_fields( $user_id, $address_type, $address = [], $customer = null ) { |
| 262 | try { |
| 263 | $customer = $customer ?? new WC_Customer( $user_id ); |
| 264 | $result = $this->update_additional_fields_for_customer( $customer, 'address', $address_type ); |
| 265 | |
| 266 | if ( is_wp_error( $result ) ) { |
| 267 | foreach ( $result->get_error_messages() as $error_message ) { |
| 268 | wc_add_notice( $error_message, 'error' ); |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | $customer->save(); |
| 273 | } catch ( \Exception $e ) { |
| 274 | wc_add_notice( |
| 275 | sprintf( |
| 276 | /* translators: %s: Error message. */ |
| 277 | __( 'An error occurred while saving address details: %s', 'woocommerce' ), |
| 278 | esc_html( $e->getMessage() ) |
| 279 | ), |
| 280 | 'error' |
| 281 | ); |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | /** |
| 286 | * Get posted additional field values. |
| 287 | * |
| 288 | * @param string $location The location to get fields for. |
| 289 | * @param string $group The group to get fields for. |
| 290 | * @param boolean $sanitize Whether to sanitize the field values. |
| 291 | * @return array The posted field values and sanitized field values. |
| 292 | */ |
| 293 | protected function get_posted_additional_field_values( $location, $group, $sanitize = true ) { |
| 294 | $additional_fields = $this->checkout_fields_controller->get_fields_for_location( $location ); |
| 295 | $field_values = []; |
| 296 | |
| 297 | // phpcs:disable WordPress.Security.NonceVerification.Missing |
| 298 | foreach ( $additional_fields as $field_key => $field_data ) { |
| 299 | $post_key = CheckoutFields::get_group_key( $group ) . $field_key; |
| 300 | $field_values[ $field_key ] = wc_clean( wp_unslash( $_POST[ $post_key ] ?? '' ) ); |
| 301 | |
| 302 | if ( $sanitize ) { |
| 303 | $field_values[ $field_key ] = $this->checkout_fields_controller->sanitize_field( $field_key, $field_values[ $field_key ] ); |
| 304 | } |
| 305 | } |
| 306 | // phpcs:enable WordPress.Security.NonceVerification.Missing |
| 307 | return $field_values; |
| 308 | } |
| 309 | |
| 310 | /** |
| 311 | * Validate and save additional fields for a given customer. |
| 312 | * |
| 313 | * @param WC_Customer $customer Customer object. |
| 314 | * @param string $location Location to save fields for. |
| 315 | * @param string $group Group to save fields for. |
| 316 | * @return true|\WP_Error True if successful, \WP_Error if there are errors. |
| 317 | */ |
| 318 | protected function update_additional_fields_for_customer( $customer, $location, $group ) { |
| 319 | // Get all values from the POST request before validating. |
| 320 | $field_values = $this->get_posted_additional_field_values( $location, $group, false ); // These values are used to see if required fields have values. |
| 321 | $sanitized_field_values = $this->get_posted_additional_field_values( $location, $group ); // These values are used to validate custom rules, generate the document object, and save fields to the account. |
| 322 | |
| 323 | $document_object = new DocumentObject( |
| 324 | [ |
| 325 | 'customer' => [ |
| 326 | ( 'address' === $location ? $group . '_address' : 'additional_fields' ) => $sanitized_field_values, |
| 327 | ], |
| 328 | ] |
| 329 | ); |
| 330 | $document_object->set_customer( $customer ); |
| 331 | $document_object->set_context( 'address' === $location ? $group . '_address' : $location ); |
| 332 | $fields = $this->checkout_fields_controller->get_contextual_fields_for_location( $location, $document_object ); |
| 333 | |
| 334 | // Holds values to be persisted to the customer object. |
| 335 | $persist_fields = []; |
| 336 | $errors = new \WP_Error(); |
| 337 | |
| 338 | // Validate individual fields agains the document object. Errors are added to the $errors object, and each field is validated regardless of other field errors. |
| 339 | foreach ( $fields as $field_key => $field ) { |
| 340 | $field_value = $field_values[ $field_key ]; |
| 341 | |
| 342 | if ( empty( $field_value ) ) { |
| 343 | if ( true === $field['required'] ) { |
| 344 | $errors->add( |
| 345 | 'required_field', |
| 346 | /* translators: %s: is the field label */ |
| 347 | sprintf( __( '%s is required', 'woocommerce' ), '<strong>' . $field['label'] . '</strong>' ) |
| 348 | ); |
| 349 | continue; |
| 350 | } |
| 351 | $persist_fields[ $field_key ] = ''; |
| 352 | continue; |
| 353 | } |
| 354 | |
| 355 | $sanitized_field_value = $sanitized_field_values[ $field_key ]; |
| 356 | $valid_check = $this->checkout_fields_controller->validate_field( $field, $sanitized_field_value ); |
| 357 | |
| 358 | if ( is_wp_error( $valid_check ) && $valid_check->has_errors() ) { |
| 359 | // Get one error message from the WP_Error object per field to avoid overlapping error messages. |
| 360 | $errors->add( $valid_check->get_error_code(), $valid_check->get_error_message() ); |
| 361 | continue; |
| 362 | } |
| 363 | |
| 364 | $persist_fields[ $field_key ] = $sanitized_field_value; |
| 365 | } |
| 366 | |
| 367 | // Validate all fields for this location (this runs custom validation callbacks). If an error is found, no values will be persisted to the customer object. |
| 368 | $location_validation = $this->checkout_fields_controller->validate_fields_for_location( $sanitized_field_values, $location, $group ); |
| 369 | |
| 370 | if ( is_wp_error( $location_validation ) && $location_validation->has_errors() ) { |
| 371 | $errors->merge_from( $location_validation ); |
| 372 | return $errors; |
| 373 | } |
| 374 | |
| 375 | foreach ( $persist_fields as $field_key => $field_value ) { |
| 376 | $this->checkout_fields_controller->persist_field_for_customer( $field_key, $field_value, $customer, $group ); |
| 377 | } |
| 378 | |
| 379 | return $errors->has_errors() ? $errors : true; |
| 380 | } |
| 381 | } |
| 382 |