Commit d6f98d98971 for woocommerce

commit d6f98d98971367c73544d910f3ec66e69bb157a8
Author: Alefe Souza <contact@alefesouza.com>
Date:   Thu Nov 12 10:33:48 2026 -0300

    Open a pre-filled edit form when an address copy can't be saved (#70834)

diff --git a/plugins/woocommerce/changelog/wooairr-574-copy-address-prefill b/plugins/woocommerce/changelog/wooairr-574-copy-address-prefill
new file mode 100644
index 00000000000..b42db315736
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooairr-574-copy-address-prefill
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Pre-fill copied address forms, including conditional fields; refines the unreleased copy link.
+
diff --git a/plugins/woocommerce/includes/class-wc-form-handler.php b/plugins/woocommerce/includes/class-wc-form-handler.php
index 6cf67694c4d..ef64581f62e 100644
--- a/plugins/woocommerce/includes/class-wc-form-handler.php
+++ b/plugins/woocommerce/includes/class-wc-form-handler.php
@@ -391,6 +391,78 @@ class WC_Form_Handler {
 		);
 	}

+	/**
+	 * Get the address type to fill an empty edit address form from, after a copy that couldn't be saved as is.
+	 *
+	 * Nothing is saved until the customer submits the form, which has its own nonce, and it only shows the
+	 * current customer's own addresses, so this read-only request needs no nonce.
+	 *
+	 * @since 11.4.0
+	 * @internal For use by WooCommerce only.
+	 *
+	 * @param string $address_type Address type being edited; 'billing' or 'shipping'.
+	 * @return string The other address type, or an empty string when the form keeps its saved values.
+	 */
+	public static function get_copy_address_form_source( $address_type ) {
+		$source_type = isset( $_GET['copy-address-from'] ) ? sanitize_key( wp_unslash( $_GET['copy-address-from'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, see the description above.
+
+		if ( '' === $source_type || self::get_copy_address_source_type( $address_type ) !== $source_type ) {
+			return '';
+		}
+
+		if ( ! self::can_copy_address( $address_type, $source_type, get_current_user_id() ) ) {
+			return '';
+		}
+
+		$source_country = ( new WC_Customer( get_current_user_id() ) )->{"get_{$source_type}_country"}( 'edit' );
+
+		if ( ! self::is_copy_address_country_allowed( $address_type, $source_country ) ) {
+			return '';
+		}
+
+		return $source_type;
+	}
+
+	/**
+	 * Check that a copied address's country is available for the address type it is copied into.
+	 *
+	 * @param string $target_type Address type being filled.
+	 * @param string $country     Country code of the copied address.
+	 * @return bool
+	 */
+	private static function is_copy_address_country_allowed( string $target_type, string $country ): bool {
+		$countries = 'shipping' === $target_type ? WC()->countries->get_shipping_countries() : WC()->countries->get_allowed_countries();
+
+		return array_key_exists( $country, $countries );
+	}
+
+	/**
+	 * Get the address type that a copy into the given address type reads from.
+	 *
+	 * @param mixed $target_type Address type being filled.
+	 * @return string 'shipping' for 'billing', 'billing' for 'shipping', or an empty string for any other value.
+	 */
+	private static function get_copy_address_source_type( $target_type ): string {
+		$source_types = array(
+			'billing'  => 'shipping',
+			'shipping' => 'billing',
+		);
+
+		return is_string( $target_type ) ? ( $source_types[ $target_type ] ?? '' ) : '';
+	}
+
+	/**
+	 * Check that the target address is empty and the source address is saved, so a copy can't overwrite an address.
+	 *
+	 * @param string $target_type Address type being filled.
+	 * @param string $source_type Address type being copied.
+	 * @param int    $user_id     Customer ID.
+	 * @return bool
+	 */
+	private static function can_copy_address( string $target_type, string $source_type, int $user_id ): bool {
+		return ! wc_get_account_formatted_address( $target_type, $user_id ) && wc_get_account_formatted_address( $source_type, $user_id );
+	}
+
 	/**
 	 * Copy the saved billing address into an empty shipping address, or vice versa, from the My Account addresses page.
 	 *
@@ -421,14 +493,11 @@ class WC_Form_Handler {
 		}

 		$target_type  = sanitize_key( wp_unslash( $_GET['copy-address'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- The nonce is verified below.
-		$source_types = array(
-			'billing'  => 'shipping',
-			'shipping' => 'billing',
-		);
+		$source_type  = self::get_copy_address_source_type( $target_type );
 		$redirect_url = wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) );
 		$nonce_value  = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';

-		if ( ! isset( $source_types[ $target_type ] ) ) {
+		if ( '' === $source_type ) {
 			wc_add_notice( __( 'The address could not be copied. Please try again.', 'woocommerce' ), 'error' );
 			wp_safe_redirect( $redirect_url );
 			exit;
@@ -440,18 +509,15 @@ class WC_Form_Handler {
 			exit;
 		}

-		$source_type = $source_types[ $target_type ];
-
-		if ( wc_get_account_formatted_address( $target_type, $user_id ) || ! wc_get_account_formatted_address( $source_type, $user_id ) ) {
+		if ( ! self::can_copy_address( $target_type, $source_type, $user_id ) ) {
 			wp_safe_redirect( $redirect_url );
 			exit;
 		}

-		$customer         = new WC_Customer( $user_id );
-		$source_country   = 'billing' === $source_type ? $customer->get_billing_country( 'edit' ) : $customer->get_shipping_country( 'edit' );
-		$target_countries = 'shipping' === $target_type ? WC()->countries->get_shipping_countries() : WC()->countries->get_allowed_countries();
+		$customer       = new WC_Customer( $user_id );
+		$source_country = 'billing' === $source_type ? $customer->get_billing_country( 'edit' ) : $customer->get_shipping_country( 'edit' );

-		if ( ! array_key_exists( $source_country, $target_countries ) ) {
+		if ( ! self::is_copy_address_country_allowed( $target_type, $source_country ) ) {
 			wc_add_notice( __( 'This address uses a country that is not available for the other address type. Please add the address manually.', 'woocommerce' ), 'error' );
 			wp_safe_redirect( $redirect_url );
 			exit;
@@ -466,6 +532,12 @@ class WC_Form_Handler {
 			// Read stored values, so getter filters are not saved into the other address.
 			$value = is_callable( array( $customer, $getter ) ) ? $customer->{$getter}( 'edit' ) : $customer->get_meta( $source_key, true, 'edit' );

+			// Keep what the target address already has, such as a billing email or phone, when the copied address has none.
+			if ( '' === $value ) {
+				$target_getter = "get_{$target_key}";
+				$value         = is_callable( array( $customer, $target_getter ) ) ? $customer->{$target_getter}( 'edit' ) : $customer->get_meta( $target_key, true, 'edit' );
+			}
+
 			// Shipping has no email field, so fall back to the account email like the edit address form does.
 			if ( 'billing_email' === $target_key && ! $value ) {
 				$value = $customer->get_email( 'edit' );
@@ -496,7 +568,8 @@ class WC_Form_Handler {
 		$field_values    = array();

 		foreach ( array_keys( $checkout_fields->get_fields_for_location( 'address' ) ) as $field_key ) {
-			$field_values[ $field_key ] = $checkout_fields->get_field_from_object( $field_key, $customer, $source_type, false );
+			$value                      = $checkout_fields->get_field_from_object( $field_key, $customer, $source_type, false );
+			$field_values[ $field_key ] = '' === $value ? $checkout_fields->get_field_from_object( $field_key, $customer, $target_type, false ) : $value;
 		}

 		// Validate the additional fields against the target address, like the edit address form does.
@@ -514,10 +587,10 @@ class WC_Form_Handler {
 		}

 		/**
-		 * Fires before a customer's copied address is saved, so extensions can reject it by adding an error notice.
+		 * Fires before a customer's copied address is saved, so extensions can stop the save by adding an error notice.
 		 *
-		 * Unlike woocommerce_after_save_address_validation, there's no submitted form, so read the copied values from
-		 * the customer instead of the request.
+		 * There's no submitted form, so read the copied values from the customer instead of the request. A stopped copy
+		 * opens the edit address form filled with the copied address, and saving it runs woocommerce_after_save_address_validation.
 		 *
 		 * @since 11.4.0
 		 *
@@ -528,8 +601,10 @@ class WC_Form_Handler {
 		 */
 		do_action( 'woocommerce_before_copy_address_save', $user_id, $target_type, $source_type, $customer );

+		// Open the edit form filled with the copied address instead, so the customer only fixes what the errors point to.
 		if ( 0 < wc_notice_count( 'error' ) ) {
-			wp_safe_redirect( $redirect_url );
+			wc_add_notice( __( 'Review the copied address below, then save it.', 'woocommerce' ), 'notice' );
+			wp_safe_redirect( add_query_arg( 'copy-address-from', $source_type, wc_get_endpoint_url( 'edit-address', $target_type, wc_get_page_permalink( 'myaccount' ) ) ) );
 			exit;
 		}

diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
index 1c704482f09..c7ff53046a1 100644
--- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
+++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
@@ -206,7 +206,9 @@ class WC_Shortcode_My_Account {
 	public static function edit_address( $load_address = 'billing' ) {
 		$current_user = wp_get_current_user();
 		$load_address = sanitize_key( $load_address );
-		$country      = get_user_meta( get_current_user_id(), $load_address . '_country', true );
+		$copy_source  = WC_Form_Handler::get_copy_address_form_source( $load_address );
+		$values_from  = $copy_source ? $copy_source : $load_address;
+		$country      = get_user_meta( get_current_user_id(), $values_from . '_country', true );

 		if ( ! $country ) {
 			$country = WC()->countries->get_base_country();
@@ -236,8 +238,18 @@ class WC_Shortcode_My_Account {

 		// Prepare values.
 		foreach ( $address as $key => $field ) {
+			$meta_key = $key;

-			$value = get_user_meta( get_current_user_id(), $key, true );
+			if ( $copy_source && 0 === strpos( $key, $load_address . '_' ) ) {
+				$meta_key = $copy_source . substr( $key, strlen( $load_address ) );
+			}
+
+			$value = get_user_meta( get_current_user_id(), $meta_key, true );
+
+			// Keep what the edited address already has, such as a billing email or phone, when the copied address has none.
+			if ( '' === $value && $meta_key !== $key ) {
+				$value = get_user_meta( get_current_user_id(), $key, true );
+			}

 			if ( ! $value ) {
 				switch ( $key ) {
diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsFrontend.php b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsFrontend.php
index 5179bd36c51..11a59cb59ee 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsFrontend.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsFrontend.php
@@ -186,9 +186,29 @@ class CheckoutFieldsFrontend {
 	 * @return array Updated address fields.
 	 */
 	public function edit_address_fields( $address, $address_type ) {
-		$customer = new WC_Customer( get_current_user_id() );
+		$customer      = new WC_Customer( get_current_user_id() );
+		$copy_source   = \WC_Form_Handler::get_copy_address_form_source( $address_type );
+		$copied_values = [];
+		$request_data  = [];
+
+		if ( $copy_source ) {
+			foreach ( array_keys( $this->checkout_fields_controller->get_fields_for_location( 'address' ) ) as $key ) {
+				// Defaults are computed for the address being edited, rather than the copied address.
+				$value                 = $this->checkout_fields_controller->get_field_from_object( $key, $customer, $copy_source, false );
+				$copied_values[ $key ] = '' === $value ? $this->checkout_fields_controller->get_field_from_object( $key, $customer, $address_type ) : $value;
+			}

-		$document_object = new DocumentObject();
+			$address_values = $copied_values;
+			foreach ( $address as $key => $field ) {
+				if ( 0 === strpos( $key, $address_type . '_' ) ) {
+					$address_values[ substr( $key, strlen( $address_type ) + 1 ) ] = $field['value'] ?? '';
+				}
+			}
+
+			$request_data = [ 'customer' => [ $address_type . '_address' => $address_values ] ];
+		}
+
+		$document_object = new DocumentObject( $request_data );
 		$document_object->set_customer( $customer );
 		$document_object->set_context( $address_type . '_address' );
 		$fields = $this->checkout_fields_controller->get_contextual_fields_for_location( 'address', $document_object );
@@ -196,7 +216,7 @@ class CheckoutFieldsFrontend {
 		foreach ( $fields as $key => $field ) {
 			$field_key                      = CheckoutFields::get_group_key( $address_type ) . $key;
 			$address[ $field_key ]          = $field;
-			$address[ $field_key ]['value'] = $this->checkout_fields_controller->get_field_from_object( $key, $customer, $address_type );
+			$address[ $field_key ]['value'] = $copied_values[ $key ] ?? $this->checkout_fields_controller->get_field_from_object( $key, $customer, $address_type );

 			$address[ $field_key ] = $this->checkout_fields_controller->prepare_form_field( $address[ $field_key ] );
 		}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-form-handler-test.php b/plugins/woocommerce/tests/php/includes/class-wc-form-handler-test.php
index 4cec4e95319..5c4e42c3df4 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-form-handler-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-form-handler-test.php
@@ -569,7 +569,7 @@ class WC_Form_Handler_Test extends WC_Unit_Test_Case {
 		);

 		$this->prepare_copy_address_request( 'shipping' );
-		$this->dispatch_copy_address_expecting_redirect();
+		$this->dispatch_copy_address_expecting_redirect( $this->get_copy_address_form_url( 'shipping', 'billing' ) );

 		$this->assertSame( 'Extension rejected the address.', wc_get_notices( 'error' )[0]['notice'] ?? null );
 		$this->assertEmpty( wc_get_notices( 'success' ), 'A rejected copy should not show a success notice.' );
@@ -590,6 +590,27 @@ class WC_Form_Handler_Test extends WC_Unit_Test_Case {
 		$this->assertSame( 'copy-address-customer@example.test', ( new WC_Customer( $customer->get_id() ) )->get_billing_email() );
 	}

+	/**
+	 * @testdox copy_address() keeps the billing email and phone already saved on an empty billing address when the shipping address has none.
+	 *
+	 * @covers WC_Form_Handler::copy_address()
+	 */
+	public function test_copy_address_keeps_target_email_and_phone(): void {
+		$customer = $this->create_customer_with_address( 'shipping' );
+		$customer->set_shipping_phone( '' );
+		$customer->set_billing_email( 'invoices@example.test' );
+		$customer->set_billing_phone( '4155550199' );
+		$customer->save();
+
+		$this->prepare_copy_address_request( 'billing' );
+		$this->dispatch_copy_address_expecting_redirect();
+
+		$saved = new WC_Customer( $customer->get_id() );
+		$this->assertSame( '456 Madison Avenue', $saved->get_billing_address_1(), 'The shipping address should be copied.' );
+		$this->assertSame( 'invoices@example.test', $saved->get_billing_email(), 'The saved billing email should not be replaced by the account email.' );
+		$this->assertSame( '4155550199', $saved->get_billing_phone(), 'The saved billing phone should not be cleared.' );
+	}
+
 	/**
 	 * @testdox copy_address() leaves an address that is already set up untouched.
 	 *
@@ -754,7 +775,7 @@ class WC_Form_Handler_Test extends WC_Unit_Test_Case {
 		);

 		$this->prepare_copy_address_request( 'billing' );
-		$this->dispatch_copy_address_expecting_redirect();
+		$this->dispatch_copy_address_expecting_redirect( $this->get_copy_address_form_url( 'billing', 'shipping' ) );

 		$this->assertSame( 'Phone is a required field.', wc_get_notices( 'error' )[0]['notice'] ?? null );
 		$this->assert_copy_address_rejected( $customer->get_id(), 'billing' );
@@ -771,7 +792,7 @@ class WC_Form_Handler_Test extends WC_Unit_Test_Case {
 		$customer->save();

 		$this->prepare_copy_address_request( 'shipping' );
-		$this->dispatch_copy_address_expecting_redirect();
+		$this->dispatch_copy_address_expecting_redirect( $this->get_copy_address_form_url( 'shipping', 'billing' ) );

 		$this->assertSame( 'Please enter a valid postcode / ZIP.', wc_get_notices( 'error' )[0]['notice'] ?? null );
 		$this->assert_copy_address_rejected( $customer->get_id(), 'shipping' );
@@ -802,6 +823,30 @@ class WC_Form_Handler_Test extends WC_Unit_Test_Case {
 		$this->assertSame( '1234', $checkout_fields->get_field_from_object( 'test-namespace/gate-code', new WC_Customer( $customer->get_id() ), 'shipping' ) );
 	}

+	/**
+	 * @testdox copy_address() keeps a Checkout block address field already saved on the empty target address when the copied address has no value.
+	 *
+	 * @covers WC_Form_Handler::copy_address()
+	 */
+	public function test_copy_address_keeps_target_additional_checkout_field(): void {
+		$this->register_checkout_field(
+			array(
+				'id'       => 'test-namespace/gate-code',
+				'label'    => 'Gate code',
+				'location' => 'address',
+			)
+		);
+		$customer        = $this->create_customer_with_address( 'billing' );
+		$checkout_fields = Package::container()->get( CheckoutFields::class );
+		$checkout_fields->persist_field_for_customer( 'test-namespace/gate-code', '5678', $customer, 'shipping' );
+		$customer->save();
+
+		$this->prepare_copy_address_request( 'shipping' );
+		$this->dispatch_copy_address_expecting_redirect();
+
+		$this->assertSame( '5678', $checkout_fields->get_field_from_object( 'test-namespace/gate-code', new WC_Customer( $customer->get_id() ), 'shipping' ), 'The saved shipping gate code should not be cleared.' );
+	}
+
 	/**
 	 * @testdox copy_address() refuses to copy when a required Checkout block address field is empty.
 	 *
@@ -819,12 +864,204 @@ class WC_Form_Handler_Test extends WC_Unit_Test_Case {
 		$customer = $this->create_customer_with_address( 'billing' );

 		$this->prepare_copy_address_request( 'shipping' );
-		$this->dispatch_copy_address_expecting_redirect();
+		$this->dispatch_copy_address_expecting_redirect( $this->get_copy_address_form_url( 'shipping', 'billing' ) );

 		$this->assertSame( '<strong>Gate code</strong> is required', wc_get_notices( 'error' )[0]['notice'] ?? null );
 		$this->assert_copy_address_rejected( $customer->get_id(), 'shipping' );
 	}

+	/**
+	 * @testdox The edit address form for an empty billing address is filled from the shipping address after a refused copy.
+	 *
+	 * @covers WC_Shortcode_My_Account::edit_address()
+	 * @covers WC_Form_Handler::get_copy_address_form_source()
+	 */
+	public function test_edit_address_form_is_filled_from_copy_source(): void {
+		$customer = $this->create_customer_with_address( 'shipping' );
+		$customer->set_shipping_phone( '' );
+		$customer->save();
+
+		$fields = $this->get_edit_address_form_fields( 'billing', 'shipping' );
+
+		foreach ( array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country' ) as $field ) {
+			$this->assertSame( $customer->{"get_shipping_{$field}"}(), $fields[ "billing_{$field}" ]['value'] ?? null, "billing_{$field} should be filled from the shipping address." );
+		}
+		$this->assertSame( '', $fields['billing_phone']['value'] ?? null, 'The missing phone should stay empty for the customer to fill in.' );
+		$this->assertSame( 'copy-address-customer@example.test', $fields['billing_email']['value'] ?? null, 'The email should fall back to the account email.' );
+		$this->assertSame( '', ( new WC_Customer( $customer->get_id() ) )->get_billing_address_1(), 'Filling the form should not save the billing address.' );
+	}
+
+	/**
+	 * @testdox The edit address form keeps the billing email and phone already saved on an empty billing address when the shipping address has none.
+	 *
+	 * @covers WC_Shortcode_My_Account::edit_address()
+	 */
+	public function test_edit_address_form_keeps_edited_address_email_and_phone(): void {
+		$customer = $this->create_customer_with_address( 'shipping' );
+		$customer->set_shipping_phone( '' );
+		$customer->set_billing_email( 'invoices@example.test' );
+		$customer->set_billing_phone( '4155550199' );
+		$customer->save();
+
+		$fields = $this->get_edit_address_form_fields( 'billing', 'shipping' );
+
+		$this->assertSame( '456 Madison Avenue', $fields['billing_address_1']['value'] ?? null, 'The form should be filled from the shipping address.' );
+		$this->assertSame( 'invoices@example.test', $fields['billing_email']['value'] ?? null, 'The saved billing email should be shown, not the account email.' );
+		$this->assertSame( '4155550199', $fields['billing_phone']['value'] ?? null, 'The saved billing phone should be shown.' );
+	}
+
+	/**
+	 * @testdox The edit address form keeps its saved values when $scenario.
+	 *
+	 * @testWith ["the copy-address-from arg is missing", ""]
+	 *           ["the copy-address-from arg names the edited address type", "billing"]
+	 *           ["the copy-address-from arg is unknown", "contact"]
+	 * @covers WC_Form_Handler::get_copy_address_form_source()
+	 *
+	 * @param string $scenario    Scenario description.
+	 * @param string $source_type Value of the copy-address-from query arg.
+	 */
+	public function test_edit_address_form_ignores_invalid_copy_source( string $scenario, string $source_type ): void {
+		$this->create_customer_with_address( 'shipping' );
+
+		$fields = $this->get_edit_address_form_fields( 'billing', $source_type );
+
+		$this->assertSame( '', $fields['billing_address_1']['value'] ?? null, "The billing form should not be filled when {$scenario}." );
+	}
+
+	/**
+	 * @testdox The edit address form isn't filled from the other address when the edited address is already saved.
+	 *
+	 * @covers WC_Form_Handler::get_copy_address_form_source()
+	 */
+	public function test_edit_address_form_is_not_filled_over_a_saved_address(): void {
+		$customer = $this->create_customer_with_address( 'shipping' );
+		foreach ( self::provide_address_types()['billing'][1] as $field => $value ) {
+			$customer->{"set_billing_{$field}"}( $value );
+		}
+		$customer->save();
+
+		$fields = $this->get_edit_address_form_fields( 'billing', 'shipping' );
+
+		$this->assertSame( '123 Market Street', $fields['billing_address_1']['value'] ?? null, 'A saved billing address should be shown as is.' );
+	}
+
+	/**
+	 * @testdox The edit address form isn't filled from the other address when its country isn't available for the edited address type.
+	 *
+	 * @covers WC_Form_Handler::get_copy_address_form_source()
+	 */
+	public function test_edit_address_form_is_not_filled_from_country_not_available(): void {
+		$this->create_customer_with_address( 'billing' );
+		add_filter(
+			'woocommerce_countries_shipping_countries',
+			function () {
+				return array( 'CA' => 'Canada' );
+			}
+		);
+
+		$fields = $this->get_edit_address_form_fields( 'shipping', 'billing' );
+
+		$this->assertSame( '', $fields['shipping_address_1']['value'] ?? null, 'A billing address in a country the store does not ship to should not fill the shipping form.' );
+	}
+
+	/**
+	 * @testdox The edit address form fills Checkout block address fields from the other address after a refused copy.
+	 *
+	 * @covers \Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsFrontend::edit_address_fields()
+	 */
+	public function test_edit_address_form_fills_additional_checkout_fields_from_copy_source(): void {
+		$this->register_checkout_field(
+			array(
+				'id'       => 'test-namespace/gate-code',
+				'label'    => 'Gate code',
+				'location' => 'address',
+			)
+		);
+		$customer        = $this->create_customer_with_address( 'billing' );
+		$checkout_fields = Package::container()->get( CheckoutFields::class );
+		$checkout_fields->persist_field_for_customer( 'test-namespace/gate-code', '1234', $customer, 'billing' );
+		$customer->save();
+
+		$fields = $this->get_edit_address_form_fields( 'shipping', 'billing' );
+
+		$this->assertSame( '1234', $fields['_wc_shipping/test-namespace/gate-code']['value'] ?? null, 'The additional field should be filled from the billing address.' );
+	}
+
+	/**
+	 * @testdox The copied edit form evaluates conditional fields against its displayed values in the destination context.
+	 *
+	 * @testWith ["shipping", "billing", "country", "US"]
+	 *           ["billing", "shipping", "country", "US"]
+	 *           ["shipping", "billing", "test-namespace/gate-code", "1234"]
+	 *           ["billing", "shipping", "test-namespace/gate-code", "1234"]
+	 * @covers \Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsFrontend::edit_address_fields()
+	 *
+	 * @param string $source_type Address type copied from.
+	 * @param string $target_type Address type being edited.
+	 * @param string $condition_field Field controlling visibility and whether the field is required.
+	 * @param string $condition_value Value that makes the field visible and required.
+	 */
+	public function test_edit_address_form_evaluates_conditions_against_copied_values( string $source_type, string $target_type, string $condition_field, string $condition_value ): void {
+		$this->register_checkout_field(
+			array(
+				'id'       => 'test-namespace/gate-code',
+				'label'    => 'Gate code',
+				'location' => 'address',
+			)
+		);
+		$condition = array( 'properties' => array( $condition_field => array( 'const' => $condition_value ) ) );
+		$this->register_checkout_field(
+			array(
+				'id'       => 'test-namespace/tax-id',
+				'label'    => 'Tax ID',
+				'location' => 'address',
+				'hidden'   => array( 'customer' => array( 'properties' => array( 'address' => array( 'not' => $condition ) ) ) ),
+				'required' => array( 'customer' => array( 'properties' => array( $target_type . '_address' => $condition ) ) ),
+			)
+		);
+		$customer        = $this->create_customer_with_address( $source_type );
+		$checkout_fields = Package::container()->get( CheckoutFields::class );
+		$checkout_fields->persist_field_for_customer( 'test-namespace/gate-code', '1234', $customer, $source_type );
+		$customer->save();
+
+		$fields    = $this->get_edit_address_form_fields( $target_type, $source_type );
+		$field_key = CheckoutFields::get_group_key( $target_type ) . 'test-namespace/tax-id';
+
+		$this->assertArrayHasKey( $field_key, $fields, 'A conditional field matching the copied values should be visible.' );
+		$this->assertTrue( $fields[ $field_key ]['required'], 'Required rules should use the copied values in the destination address context.' );
+		$this->assertSame( '', $fields[ $field_key ]['value'], 'The missing value should be left for the customer to fill in.' );
+		$this->assertSame( '', ( new WC_Customer( $customer->get_id() ) )->{"get_{$target_type}_country"}(), 'Rendering the copied form should not save the destination address.' );
+	}
+
+	/**
+	 * @testdox The edit address form gives a Checkout block address field the edited address's default when the copied address has no value.
+	 *
+	 * @covers \Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsFrontend::edit_address_fields()
+	 */
+	public function test_edit_address_form_uses_edited_address_default_for_empty_copied_field(): void {
+		$this->register_checkout_field(
+			array(
+				'id'       => 'test-namespace/gate-code',
+				'label'    => 'Gate code',
+				'location' => 'address',
+			)
+		);
+		add_filter(
+			'woocommerce_get_default_value_for_test-namespace/gate-code',
+			function ( $value, $group ) {
+				return "{$group} default";
+			},
+			10,
+			2
+		);
+		$this->create_customer_with_address( 'billing' );
+
+		$fields = $this->get_edit_address_form_fields( 'shipping', 'billing' );
+
+		$this->assertSame( 'shipping default', $fields['_wc_shipping/test-namespace/gate-code']['value'] ?? null, 'The default should be computed for the shipping address being edited.' );
+	}
+
 	/**
 	 * @testdox save_account_details() saves other account fields when an email-like display name is unchanged.
 	 *
@@ -1226,16 +1463,18 @@ class WC_Form_Handler_Test extends WC_Unit_Test_Case {
 	}

 	/**
-	 * Dispatches the copy address handler and expects a redirect back to Addresses.
+	 * Dispatches the copy address handler and expects a redirect, by default back to Addresses.
+	 *
+	 * @param string $expected_url Expected redirect URL; Addresses when empty.
 	 */
-	private function dispatch_copy_address_expecting_redirect(): void {
+	private function dispatch_copy_address_expecting_redirect( string $expected_url = '' ): void {
 		try {
 			WC_Form_Handler::copy_address();
 		} catch ( RuntimeException $e ) {
 			$this->assertSame(
-				wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) ),
+				esc_url_raw( $expected_url ? $expected_url : wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) ) ),
 				$e->getMessage(),
-				'Copying an address should redirect to Addresses.'
+				'Copying an address should redirect to the expected page.'
 			);
 			return;
 		}
@@ -1243,6 +1482,43 @@ class WC_Form_Handler_Test extends WC_Unit_Test_Case {
 		$this->fail( 'Expected copy_address() to redirect. Notices: ' . wp_json_encode( wc_get_notices() ) );
 	}

+	/**
+	 * Gets the edit address form URL that a refused copy redirects to.
+	 *
+	 * @param string $target_type Address type the copy targeted.
+	 * @param string $source_type Address type the copy read from.
+	 * @return string
+	 */
+	private function get_copy_address_form_url( string $target_type, string $source_type ): string {
+		return add_query_arg( 'copy-address-from', $source_type, wc_get_endpoint_url( 'edit-address', $target_type, wc_get_page_permalink( 'myaccount' ) ) );
+	}
+
+	/**
+	 * Renders the edit address form and returns the fields it was given.
+	 *
+	 * @param string $address_type Address type to edit.
+	 * @param string $source_type  Value of the copy-address-from query arg, or an empty string to leave it out.
+	 * @return array<string,array<string,mixed>>
+	 */
+	private function get_edit_address_form_fields( string $address_type, string $source_type ): array {
+		$_GET   = $source_type ? array( 'copy-address-from' => $source_type ) : array();
+		$fields = array();
+		add_filter(
+			'woocommerce_address_to_edit',
+			function ( $address ) use ( &$fields ) {
+				$fields = $address;
+				return $address;
+			},
+			PHP_INT_MAX
+		);
+
+		ob_start();
+		WC_Shortcode_My_Account::edit_address( $address_type );
+		ob_end_clean();
+
+		return $fields;
+	}
+
 	/**
 	 * Asserts that a copy was refused: no success notice and the target address is still empty.
 	 *
@@ -1250,6 +1526,7 @@ class WC_Form_Handler_Test extends WC_Unit_Test_Case {
 	 * @param string $target_type Address type the copy targeted.
 	 */
 	private function assert_copy_address_rejected( int $customer_id, string $target_type ): void {
+		$this->assertSame( 'Review the copied address below, then save it.', wc_get_notices( 'notice' )[0]['notice'] ?? null, 'A refused copy should ask the customer to review the edit form.' );
 		$this->assertEmpty( wc_get_notices( 'success' ), 'A refused copy should not show a success notice.' );
 		$this->assertSame( '', ( new WC_Customer( $customer_id ) )->{"get_{$target_type}_address_1"}(), "The {$target_type} address should not be saved." );
 	}