plugin updates
This commit is contained in:
@@ -7,6 +7,7 @@ use Automattic\WooCommerce\Blocks\AssetsController;
|
||||
use Automattic\WooCommerce\Blocks\BlockPatterns;
|
||||
use Automattic\WooCommerce\Blocks\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Blocks\BlockTypesController;
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CreateAccount;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\Notices;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders;
|
||||
@@ -14,6 +15,7 @@ use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating;
|
||||
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\InboxNotifications;
|
||||
use Automattic\WooCommerce\Blocks\Installer;
|
||||
use Automattic\WooCommerce\Blocks\Migration;
|
||||
@@ -140,6 +142,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();
|
||||
}
|
||||
|
||||
// Load assets unless this is a request specifically for the store API.
|
||||
@@ -160,6 +163,8 @@ class Bootstrap {
|
||||
$this->container->get( SingleProductTemplateCompatibility::class )->init();
|
||||
$this->container->get( Notices::class )->init();
|
||||
}
|
||||
|
||||
$this->container->get( QueryFilters::class )->init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,6 +355,13 @@ class Bootstrap {
|
||||
return new CheckoutFields( $container->get( AssetDataRegistry::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
CheckoutFieldsAdmin::class,
|
||||
function( Container $container ) {
|
||||
$checkout_fields_controller = $container->get( CheckoutFields::class );
|
||||
return new CheckoutFieldsAdmin( $checkout_fields_controller );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
PaymentsApi::class,
|
||||
function ( Container $container ) {
|
||||
@@ -413,6 +425,12 @@ class Bootstrap {
|
||||
return new TasksController();
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
QueryFilters::class,
|
||||
function() {
|
||||
return new QueryFilters();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -242,153 +242,262 @@ class CheckoutFields {
|
||||
* @return \WP_Error|void True if the field was registered, a WP_Error otherwise.
|
||||
*/
|
||||
public function register_checkout_field( $options ) {
|
||||
if ( empty( $options['id'] ) ) {
|
||||
wc_get_logger()->warning( 'A checkout field cannot be registered without an id.' );
|
||||
|
||||
// Check the options and show warnings if they're not supplied. Return early if an error that would prevent registration is encountered.
|
||||
$result = $this->validate_options( $options );
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The above validate_options function ensures these options are valid. Type might not be supplied but then it defaults to text.
|
||||
$id = $options['id'];
|
||||
$location = $options['location'];
|
||||
$type = $options['type'] ?? 'text';
|
||||
|
||||
$field_data = array(
|
||||
'label' => $options['label'],
|
||||
'hidden' => false,
|
||||
'type' => $type,
|
||||
'optionalLabel' => empty( $options['optionalLabel'] ) ? sprintf(
|
||||
/* translators: %s Field label. */
|
||||
__( '%s (optional)', 'woocommerce' ),
|
||||
$options['label']
|
||||
) : $options['optionalLabel'],
|
||||
'required' => empty( $options['required'] ) ? false : $options['required'],
|
||||
);
|
||||
|
||||
$field_data['attributes'] = $this->register_field_attributes( $id, $options['attributes'] ?? [] );
|
||||
|
||||
if ( 'checkbox' === $type ) {
|
||||
$result = $this->process_checkbox_field( $options, $field_data );
|
||||
|
||||
// $result will be false if an error that will prevent the field being registered is encountered.
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
$field_data = $result;
|
||||
}
|
||||
|
||||
if ( 'select' === $type ) {
|
||||
$result = $this->process_select_field( $options, $field_data );
|
||||
|
||||
// $result will be false if an error that will prevent the field being registered is encountered.
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
$field_data = $result;
|
||||
}
|
||||
|
||||
// Insert new field into the correct location array.
|
||||
$this->additional_fields[ $id ] = $field_data;
|
||||
$this->fields_locations[ $location ][] = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the "base" options (id, label, location) and shows warnings if they're not supplied.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @return bool false if an error was encountered, true otherwise.
|
||||
*/
|
||||
private function validate_options( $options ) {
|
||||
if ( empty( $options['id'] ) ) {
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', 'A checkout field cannot be registered without an id.', '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Having fewer than 2 after exploding around a / means there is no namespace.
|
||||
if ( count( explode( '/', $options['id'] ) ) < 2 ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'A checkout field id must consist of namespace/name.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'A checkout field id must consist of namespace/name.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( empty( $options['label'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'The field label is required.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field label is required.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( empty( $options['location'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'The field location is required.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field location is required.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! in_array( $options['location'], array_keys( $this->fields_locations ), true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'The field location is invalid.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field location is invalid.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = 'text';
|
||||
if ( ! empty( $options['type'] ) ) {
|
||||
if ( ! in_array( $options['type'], $this->supported_field_types, true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf(
|
||||
'Unable to register field with id: "%s". Registering a field with type "%s" is not supported. The supported types are: %s.',
|
||||
esc_html( $options['id'] ),
|
||||
esc_html( $options['type'] ),
|
||||
implode( ', ', $this->supported_field_types )
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
$type = $options['type'];
|
||||
}
|
||||
|
||||
// At this point, the essentials fields and its location should be set.
|
||||
// At this point, the essentials fields and its location should be set and valid.
|
||||
$location = $options['location'];
|
||||
$id = $options['id'];
|
||||
|
||||
// Check to see if field is already in the array.
|
||||
if ( ! empty( $this->additional_fields[ $id ] ) || in_array( $id, $this->fields_locations[ $location ], true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $id ), 'The field is already registered.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The field is already registered.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hidden fields are not supported right now. They will be registered with hidden => false.
|
||||
if ( ! empty( $options['hidden'] ) && true === $options['hidden'] ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Registering a field with hidden set to true is not supported. The field "%s" will be registered as visible.', esc_html( $id ) )
|
||||
$message = sprintf( 'Registering a field with hidden set to true is not supported. The field "%s" will be registered as visible.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
// Don't return here unlike the other fields because this is not an issue that will prevent registration.
|
||||
}
|
||||
|
||||
if ( ! empty( $options['type'] ) ) {
|
||||
if ( ! in_array( $options['type'], $this->supported_field_types, true ) ) {
|
||||
$message = sprintf(
|
||||
'Unable to register field with id: "%s". Registering a field with type "%s" is not supported. The supported types are: %s.',
|
||||
$id,
|
||||
$options['type'],
|
||||
implode( ', ', $this->supported_field_types )
|
||||
);
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the options for a select field and returns the new field_options array.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @param array $field_data The field data array to be updated.
|
||||
*
|
||||
* @return array|false The updated $field_data array or false if an error was encountered.
|
||||
*/
|
||||
private function process_select_field( $options, $field_data ) {
|
||||
$id = $options['id'];
|
||||
|
||||
if ( empty( $options['options'] ) || ! is_array( $options['options'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'Fields of type "select" must have an array of "options".' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Select fields are always required. Log a warning if it's set explicitly as false.
|
||||
$field_data['required'] = true;
|
||||
if ( isset( $options['required'] ) && false === $options['required'] ) {
|
||||
$message = sprintf( 'Registering select fields as optional is not supported. "%s" will be registered as required.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
$cleaned_options = array();
|
||||
$added_values = array();
|
||||
|
||||
// Check all entries in $options['options'] has a key and value member.
|
||||
foreach ( $options['options'] as $option ) {
|
||||
if ( ! isset( $option['value'] ) || ! isset( $option['label'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'Fields of type "select" must have an array of "options" and each option must contain a "value" and "label" member.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$sanitized_value = sanitize_text_field( $option['value'] );
|
||||
$sanitized_label = sanitize_text_field( $option['label'] );
|
||||
|
||||
if ( in_array( $sanitized_value, $added_values, true ) ) {
|
||||
$message = sprintf( 'Duplicate key found when registering field with id: "%s". The value in each option of "select" fields must be unique. Duplicate value "%s" found. The duplicate key will be removed.', $id, $sanitized_value );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
continue;
|
||||
}
|
||||
|
||||
$added_values[] = $sanitized_value;
|
||||
|
||||
$cleaned_options[] = array(
|
||||
'value' => $sanitized_value,
|
||||
'label' => $sanitized_label,
|
||||
);
|
||||
}
|
||||
|
||||
$field_data = array(
|
||||
'label' => $options['label'],
|
||||
'hidden' => false,
|
||||
'type' => $type,
|
||||
'optionalLabel' => empty( $options['optionalLabel'] ) ? '' : $options['optionalLabel'],
|
||||
'required' => empty( $options['required'] ) ? false : $options['required'],
|
||||
'autocomplete' => empty( $options['autocomplete'] ) ? '' : $options['autocomplete'],
|
||||
'autocapitalize' => empty( $options['autocapitalize'] ) ? '' : $options['autocapitalize'],
|
||||
$field_data['options'] = $cleaned_options;
|
||||
return $field_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the options for a checkbox field and returns the new field_options array.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @param array $field_data The field data array to be updated.
|
||||
*
|
||||
* @return array|false The updated $field_data array or false if an error was encountered.
|
||||
*/
|
||||
private function process_checkbox_field( $options, $field_data ) {
|
||||
$id = $options['id'];
|
||||
|
||||
// Checkbox fields are always optional. Log a warning if it's set explicitly as true.
|
||||
$field_data['required'] = false;
|
||||
|
||||
if ( isset( $options['required'] ) && true === $options['required'] ) {
|
||||
$message = sprintf( 'Registering checkbox fields as required is not supported. "%s" will be registered as optional.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
return $field_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the attributes supplied during field registration.
|
||||
*
|
||||
* @param array $id The field ID.
|
||||
* @param array $attributes The attributes supplied during field registration.
|
||||
*
|
||||
* @return array The processed attributes.
|
||||
*/
|
||||
private function register_field_attributes( $id, $attributes ) {
|
||||
|
||||
// We check if attributes are valid. This is done to prevent too much nesting and also to allow field registration
|
||||
// even if the attributes property is invalid. We can just skip it and register the field without attributes.
|
||||
$has_attributes = false;
|
||||
|
||||
if ( empty( $attributes ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( ! is_array( $attributes ) || 0 === count( $attributes ) ) {
|
||||
$message = sprintf( 'An invalid attributes value was supplied when registering field with id: "%s". %s', $id, 'Attributes must be a non-empty array.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return [];
|
||||
}
|
||||
|
||||
// These are formatted in camelCase because React components expect them that way.
|
||||
$allowed_attributes = array(
|
||||
'maxLength',
|
||||
'readOnly',
|
||||
'pattern',
|
||||
'autocomplete',
|
||||
'autocapitalize',
|
||||
'title',
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle Checkbox fields.
|
||||
*/
|
||||
if ( 'checkbox' === $type ) {
|
||||
// Checkbox fields are always optional. Log a warning if it's set explicitly as true.
|
||||
$field_data['required'] = false;
|
||||
if ( isset( $options['required'] ) && true === $options['required'] ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Registering checkbox fields as required is not supported. "%s" will be registered as optional.', esc_html( $id ) )
|
||||
);
|
||||
}
|
||||
$valid_attributes = array_filter(
|
||||
$attributes,
|
||||
function( $_, $key ) use ( $allowed_attributes ) {
|
||||
return in_array( $key, $allowed_attributes, true ) || strpos( $key, 'aria-' ) === 0 || strpos( $key, 'data-' ) === 0;
|
||||
},
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
|
||||
// Any invalid attributes should show a doing_it_wrong warning. It shouldn't stop field registration, though.
|
||||
if ( count( $attributes ) !== count( $valid_attributes ) ) {
|
||||
$invalid_attributes = array_keys( array_diff_key( $attributes, $valid_attributes ) );
|
||||
$message = sprintf( 'Invalid attribute found when registering field with id: "%s". Attributes: %s are not allowed.', $id, implode( ', ', $invalid_attributes ) );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Select fields.
|
||||
*/
|
||||
if ( 'select' === $type ) {
|
||||
if ( empty( $options['options'] ) || ! is_array( $options['options'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $id ), 'Fields of type "select" must have an array of "options".' )
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Select fields are always required. Log a warning if it's set explicitly as false.
|
||||
$field_data['required'] = true;
|
||||
if ( isset( $options['required'] ) && false === $options['required'] ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Registering select fields as optional is not supported. "%s" will be registered as required.', esc_html( $id ) )
|
||||
);
|
||||
}
|
||||
|
||||
$cleaned_options = array();
|
||||
$added_values = array();
|
||||
|
||||
// Check all entries in $options['options'] has a key and value member.
|
||||
foreach ( $options['options'] as $option ) {
|
||||
if ( ! isset( $option['value'] ) || ! isset( $option['label'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $id ), 'Fields of type "select" must have an array of "options" and each option must contain a "value" and "label" member.' )
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$sanitized_value = sanitize_text_field( $option['value'] );
|
||||
$sanitized_label = sanitize_text_field( $option['label'] );
|
||||
|
||||
if ( in_array( $sanitized_value, $added_values, true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Duplicate key found when registering field with id: "%s". The value in each option of "select" fields must be unique. Duplicate value "%s" found. The duplicate key will be removed.', esc_html( $id ), esc_html( $sanitized_value ) )
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$added_values[] = $sanitized_value;
|
||||
|
||||
$cleaned_options[] = array(
|
||||
'value' => $sanitized_value,
|
||||
'label' => $sanitized_label,
|
||||
);
|
||||
}
|
||||
|
||||
$field_data['options'] = $cleaned_options;
|
||||
}
|
||||
|
||||
// Insert new field into the correct location array.
|
||||
$this->additional_fields[ $id ] = $field_data;
|
||||
|
||||
$this->fields_locations[ $location ][] = $id;
|
||||
// Escape attributes to remove any malicious code and return them.
|
||||
return array_map(
|
||||
function( $value ) {
|
||||
return esc_attr( $value );
|
||||
},
|
||||
$valid_attributes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -409,6 +518,91 @@ class CheckoutFields {
|
||||
return $this->additional_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the location of a field.
|
||||
*
|
||||
* @param string $field_key The key of the field to get the location for.
|
||||
* @return string The location of the field.
|
||||
*/
|
||||
public function get_field_location( $field_key ) {
|
||||
foreach ( $this->fields_locations as $location => $fields ) {
|
||||
if ( in_array( $field_key, $fields, true ) ) {
|
||||
return $location;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an additional field against any custom validation rules. The result should be a WP_Error or true.
|
||||
*
|
||||
* @param string $key The key of the field.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @param \WP_REST_Request $request The current API Request.
|
||||
* @param string|null $address_type The type of address (billing, shipping, or null if the field is a contact/additional field).
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
public function validate_field( $key, $field_value, $request, $address_type = null ) {
|
||||
|
||||
$error = new \WP_Error();
|
||||
try {
|
||||
/**
|
||||
* Filter the result of validating an additional field.
|
||||
*
|
||||
* @param \WP_Error $error A WP_Error that extensions may add errors to.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @param \WP_REST_Request $request The current API Request.
|
||||
* @param string|null $address_type The type of address (billing, shipping, or null if the field is a contact/additional field).
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
$filtered_result = apply_filters( 'woocommerce_blocks_validate_additional_field_' . $key, $error, $field_value, $request, $address_type );
|
||||
|
||||
if ( $error !== $filtered_result ) {
|
||||
|
||||
// Different WP_Error was returned. This would remove errors from other filters. Skip filtering and allow the order to place without validating this field.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s encountered an error. One of the filters returned a new WP_Error. Filters should use the same WP_Error passed to the filter and use the WP_Error->add function to add errors. The field will not have any custom validation applied to it.',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key ),
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
|
||||
// One of the filters errored so skip them and validate the field. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s encountered an error. The field will not have any custom validation applied to it. %s',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key ),
|
||||
esc_html( $e->getMessage() )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
|
||||
return new \WP_Error();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $filtered_result ) ) {
|
||||
return $filtered_result;
|
||||
}
|
||||
|
||||
// If the filters didn't return a valid value, ignore them and return an empty WP_Error. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s did not return a valid value. The field will not have any custom validation applied to it.',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
return new \WP_Error();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the default locale with additional fields without country limitations.
|
||||
*
|
||||
@@ -451,17 +645,43 @@ class CheckoutFields {
|
||||
return $this->fields_locations['additional'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of fields definitions only meant for order.
|
||||
*
|
||||
* @return array An array of fields definitions.
|
||||
*/
|
||||
public function get_order_only_fields() {
|
||||
// For now, all contact fields are order only fields, along with additional fields.
|
||||
$order_fields_keys = array_merge( $this->get_contact_fields_keys(), $this->get_additional_fields_keys() );
|
||||
|
||||
return array_filter(
|
||||
$this->get_additional_fields(),
|
||||
function( $key ) use ( $order_fields_keys ) {
|
||||
return in_array( $key, $order_fields_keys, true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of fields for a given group.
|
||||
*
|
||||
* @param string $location The location to get fields for (address|contact|additional).
|
||||
*
|
||||
* @return array An array of fields.
|
||||
* @return array An array of fields definitions.
|
||||
*/
|
||||
public function get_fields_for_location( $location ) {
|
||||
if ( in_array( $location, array_keys( $this->fields_locations ), true ) ) {
|
||||
return $this->fields_locations[ $location ];
|
||||
$order_fields_keys = $this->fields_locations[ $location ];
|
||||
|
||||
return array_filter(
|
||||
$this->get_additional_fields(),
|
||||
function( $key ) use ( $order_fields_keys ) {
|
||||
return in_array( $key, $order_fields_keys, true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -469,7 +689,7 @@ class CheckoutFields {
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param string $location The gslocation to validate the field for (address|contact|additional).
|
||||
* @param string $location The location to validate the field for (address|contact|additional).
|
||||
*
|
||||
* @return true|\WP_Error True if the field is valid, a WP_Error otherwise.
|
||||
*/
|
||||
@@ -791,4 +1011,54 @@ class CheckoutFields {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional fields for an order.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param string $location The location to get fields for (address|contact|additional).
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
* @param string $context The context to get the field value for (edit|view).
|
||||
* @return array An array of fields definitions as well as their values formatted for display.
|
||||
*/
|
||||
public function get_order_additional_fields_with_values( $order, $location, $group = '', $context = 'edit' ) {
|
||||
$fields = $this->get_fields_for_location( $location );
|
||||
$fields_with_values = array();
|
||||
|
||||
foreach ( $fields as $field_key => $field ) {
|
||||
$value = $this->get_field_from_order( $field_key, $order, $group );
|
||||
|
||||
if ( '' === $value || null === $value ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'view' === $context ) {
|
||||
$value = $this->format_additional_field_value( $value, $field );
|
||||
}
|
||||
|
||||
$field['value'] = $value;
|
||||
$fields_with_values[ $field_key ] = $field;
|
||||
}
|
||||
|
||||
return $fields_with_values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a raw field value for display based on its type definition.
|
||||
*
|
||||
* @param string $value Value to format.
|
||||
* @param array $field Additional field definition.
|
||||
* @return string
|
||||
*/
|
||||
public function format_additional_field_value( $value, $field ) {
|
||||
if ( 'checkbox' === $field['type'] ) {
|
||||
$value = $value ? __( 'Yes', 'woocommerce' ) : __( 'No', 'woocommerce' );
|
||||
}
|
||||
|
||||
if ( 'select' === $field['type'] ) {
|
||||
$options = array_column( $field['options'], 'label', 'value' );
|
||||
$value = isset( $options[ $value ] ) ? $options[ $value ] : $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Domain\Services;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* Service class managing checkout fields and its related extensibility points in the admin area.
|
||||
*/
|
||||
class CheckoutFieldsAdmin {
|
||||
|
||||
/**
|
||||
* Checkout field controller.
|
||||
*
|
||||
* @var CheckoutFields
|
||||
*/
|
||||
private $checkout_fields_controller;
|
||||
|
||||
/**
|
||||
* Sets up core fields.
|
||||
*
|
||||
* @param CheckoutFields $checkout_fields_controller Instance of the checkout field controller.
|
||||
*/
|
||||
public function __construct( CheckoutFields $checkout_fields_controller ) {
|
||||
$this->checkout_fields_controller = $checkout_fields_controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks. This is not run Store API requests.
|
||||
*/
|
||||
public function init() {
|
||||
add_filter( 'woocommerce_admin_billing_fields', array( $this, 'admin_address_fields' ), 10, 3 );
|
||||
add_filter( 'woocommerce_admin_billing_fields', array( $this, 'admin_contact_fields' ), 10, 3 );
|
||||
add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_address_fields' ), 10, 3 );
|
||||
add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_additional_fields' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the shape of a checkout field to match whats needed in the WooCommerce meta boxes.
|
||||
*
|
||||
* @param array $field The field to format.
|
||||
* @param string $key The field key. This will be used for the ID of the field when passed to the meta box.
|
||||
* @return array Formatted field.
|
||||
*/
|
||||
protected function format_field_for_meta_box( $field, $key ) {
|
||||
$formatted_field = array(
|
||||
'id' => $key,
|
||||
'label' => $field['label'],
|
||||
'value' => $field['value'],
|
||||
'type' => $field['type'],
|
||||
'update_callback' => array( $this, 'update_callback' ),
|
||||
'show' => true,
|
||||
'wrapper_class' => 'form-field-wide',
|
||||
);
|
||||
|
||||
if ( 'select' === $field['type'] ) {
|
||||
$formatted_field['options'] = array_column( $field['options'], 'label', 'value' );
|
||||
}
|
||||
|
||||
if ( 'checkbox' === $field['type'] ) {
|
||||
$formatted_field['checked_value'] = '1';
|
||||
$formatted_field['unchecked_value'] = '0';
|
||||
}
|
||||
|
||||
return $formatted_field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a field value for an order.
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param \WC_Order $order The order to update the field for.
|
||||
*/
|
||||
public function update_callback( $key, $value, $order ) {
|
||||
$this->checkout_fields_controller->persist_field_for_order( $key, $value, $order, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects address fields in WC admin orders screen.
|
||||
*
|
||||
* @param array $fields The fields to show.
|
||||
* @param \WC_Order|boolean $order The order to show the fields for.
|
||||
* @param string $context The context to show the fields for.
|
||||
* @return array
|
||||
*/
|
||||
public function admin_address_fields( $fields, $order = null, $context = 'edit' ) {
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$group = doing_action( 'woocommerce_admin_billing_fields' ) ? 'billing' : 'shipping';
|
||||
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $group, $context );
|
||||
foreach ( $additional_fields as $key => $field ) {
|
||||
$group_key = '/' . $group . '/' . $key;
|
||||
$additional_fields[ $key ] = $this->format_field_for_meta_box( $field, $group_key );
|
||||
}
|
||||
|
||||
array_splice(
|
||||
$fields,
|
||||
array_search(
|
||||
'state',
|
||||
array_keys( $fields ),
|
||||
true
|
||||
) + 1,
|
||||
0,
|
||||
$additional_fields
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects contact fields in WC admin orders screen.
|
||||
*
|
||||
* @param array $fields The fields to show.
|
||||
* @param \WC_Order|boolean $order The order to show the fields for.
|
||||
* @param string $context The context to show the fields for.
|
||||
* @return array
|
||||
*/
|
||||
public function admin_contact_fields( $fields, $order = null, $context = 'edit' ) {
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', '', $context );
|
||||
|
||||
return array_merge(
|
||||
$fields,
|
||||
array_map(
|
||||
array( $this, 'format_field_for_meta_box' ),
|
||||
$additional_fields,
|
||||
array_keys( $additional_fields )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects additional fields in WC admin orders screen.
|
||||
*
|
||||
* @param array $fields The fields to show.
|
||||
* @param \WC_Order|boolean $order The order to show the fields for.
|
||||
* @param string $context The context to show the fields for.
|
||||
* @return array
|
||||
*/
|
||||
public function admin_additional_fields( $fields, $order = null, $context = 'edit' ) {
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'additional', '', $context );
|
||||
|
||||
return array_merge(
|
||||
$fields,
|
||||
array_map(
|
||||
array( $this, 'format_field_for_meta_box' ),
|
||||
$additional_fields,
|
||||
array_keys( $additional_fields )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -69,8 +69,19 @@ class GoogleAnalytics {
|
||||
}
|
||||
|
||||
if ( ! wp_script_is( 'google-tag-manager', 'registered' ) ) {
|
||||
// Using an array with strategies as the final argument to wp_register_script was introduced in WP 6.3.
|
||||
// WC requires at least 6.3 at the point of adding this, so it's safe to leave in without version checks.
|
||||
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
||||
wp_register_script( 'google-tag-manager', 'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'], [], null, false );
|
||||
wp_register_script(
|
||||
'google-tag-manager',
|
||||
'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'],
|
||||
[],
|
||||
null,
|
||||
[
|
||||
'in_footer' => false,
|
||||
'strategy' => 'async',
|
||||
]
|
||||
);
|
||||
wp_add_inline_script(
|
||||
'google-tag-manager',
|
||||
"
|
||||
|
||||
@@ -41,8 +41,12 @@ class Notices {
|
||||
* Initialize notice hooks.
|
||||
*/
|
||||
public function init() {
|
||||
add_filter( 'woocommerce_kses_notice_allowed_tags', [ $this, 'add_kses_notice_allowed_tags' ] );
|
||||
add_action( 'wp_head', [ $this, 'enqueue_notice_styles' ] );
|
||||
if ( wp_is_block_theme() ) {
|
||||
add_filter( 'woocommerce_kses_notice_allowed_tags', [ $this, 'add_kses_notice_allowed_tags' ] );
|
||||
add_filter( 'wc_get_template', [ $this, 'get_notices_template' ], 10, 5 );
|
||||
add_action( 'wp_head', [ $this, 'enqueue_notice_styles' ] );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,6 +72,40 @@ class Notices {
|
||||
return array_merge( $allowed_tags, $svg_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces core notice templates with those from blocks.
|
||||
*
|
||||
* The new notice templates match block components with matching icons and styling. The differences are:
|
||||
* 1. Core has notices for info, success, and error notices, blocks has notices for info, success, error,
|
||||
* warning, and a default notice type.
|
||||
* 2. The block notices use different CSS classes to the core notices. Core uses `woocommerce-message`, `is-info`
|
||||
* and `is-error` classes, blocks uses `wc-block-components-notice-banner is-error`,
|
||||
* `wc-block-components-notice-banner is-info`, and `wc-block-components-notice-banner is-success`.
|
||||
* 3. The markup of the notices is different, with the block notices using SVG icons and a slightly different
|
||||
* structure to accommodate this.
|
||||
*
|
||||
* @param string $template Located template path.
|
||||
* @param string $template_name Template name.
|
||||
* @param array $args Template arguments.
|
||||
* @param string $template_path Template path.
|
||||
* @param string $default_path Default path.
|
||||
* @return string
|
||||
*/
|
||||
public function get_notices_template( $template, $template_name, $args, $template_path, $default_path ) {
|
||||
$directory = get_stylesheet_directory();
|
||||
$file = $directory . '/woocommerce/' . $template_name;
|
||||
if ( file_exists( $file ) ) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
if ( in_array( $template_name, $this->notice_templates, true ) ) {
|
||||
$template = $this->package->get_path( 'templates/block-' . $template_name );
|
||||
wp_enqueue_style( 'wc-blocks-style' );
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all notices with the new block based notices.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user