` simply does not apply. The * additional-field API (`woocommerce_register_additional_checkout_field`) does * support selects, but its options are registered once on the server and cannot * depend on the chosen province; one select holding all 2,704 cities would be * unusable, and a second city field would leave the core one still there. * * So the field is **enhanced rather than replaced**: the real `city` input stays * exactly as WooCommerce made it, and a province-aware suggestion list is * attached to it. Everything downstream — the Store API, order meta, emails, * invoices, shipping plugins — sees the same plain `city` string it always did, * and a customer whose browser fails to run the script still has a working text * field. * * Province labels need nothing extra here: the block checkout reads its state * list from `WC()->countries->get_states()`, which {@see ProvinceLabels} already * filters. */ final class BlockCheckout { /** * Is the current page rendering the checkout or cart block? * * Checked before anything is enqueued so the classic and block scripts never * both load, and neither loads on a page without an address form. */ public static function isBlockCheckout(): bool { if ( ! function_exists( 'has_block' ) ) { return false; } foreach ( [ 'woocommerce/checkout', 'woocommerce/cart' ] as $block ) { if ( has_block( $block ) ) { return true; } } return false; } /** * Load the block enhancer. * * @internal Hooked callback. */ public function enqueue(): void { wp_enqueue_style( 'rahkar-iran-cities-blocks', RAHKAR_WP_URL . 'assets/dist/iran-cities-blocks.css', [], RAHKAR_WP_VERSION ); Tokens::attach( 'rahkar-iran-cities-blocks' ); wp_enqueue_script( 'rahkar-iran-cities-blocks', RAHKAR_WP_URL . 'assets/dist/iran-cities-blocks.js', [], RAHKAR_WP_VERSION, true ); wp_add_inline_script( 'rahkar-iran-cities-blocks', 'window.rahkarCities=' . wp_json_encode( [ 'baseUrl' => RAHKAR_WP_URL . 'assets/data/ir/', 'version' => LocationRepository::datasetVersion(), 'custom' => (object) LocationRepository::allCustomCities(), 'noResults' => __( 'شهری با این نام پیدا نشد', 'rahkar-toolkit' ), // No placeholder: the block checkout's floating label occupies // the same space and the two overlap. 'strict' => (bool) Settings::get( IranLocationsModule::SLUG . '.city_required', true ), ], JSON_UNESCAPED_UNICODE ) . ';', 'before' ); } /** * Reject a city that does not belong to the chosen province. * * The block checkout submits through the Store API, so the classic * `woocommerce_after_checkout_validation` hook never fires. This is the * equivalent, and it is the check that actually counts — the suggestion list * in the browser is a convenience, not a constraint. * * WooCommerce also passes the location group (`billing`, `shipping`, `other`). * It is not taken: only a location with an Iranian country is checked, and * the `other` group carries no country field, so it falls out on its own. * * @param WP_Error $errors Error collector. * @param array $fields Submitted fields for this location. * * @internal Hooked callback. */ public function validate( WP_Error $errors, array $fields ): void { $country = (string) ( $fields['country'] ?? '' ); $state = (string) ( $fields['state'] ?? '' ); $city = (string) ( $fields['city'] ?? '' ); if ( 'IR' !== $country || '' === $state ) { return; } if ( '' === trim( $city ) ) { // An empty city is WooCommerce's own required-field business; saying // it twice would show the customer two errors on one field. return; } if ( LocationRepository::isValidCity( $state, $city ) ) { return; } $errors->add( 'rahkar_city_invalid', sprintf( /* translators: 1: city name, 2: province name */ __( 'شهر «%1$s» در استان %2$s یافت نشد. لطفاً یکی از شهرهای فهرست را انتخاب کنید.', 'rahkar-toolkit' ), $city, LocationRepository::provinceName( $state ) ) ); } }