Skip to content

Persist sortable checkout address field order in admin UI - #9822

Open
jakejackson1 wants to merge 1 commit into
awesomemotive:mainfrom
jakejackson1:fix/checkout-address-fields-order-persistence
Open

Persist sortable checkout address field order in admin UI#9822
jakejackson1 wants to merge 1 commit into
awesomemotive:mainfrom
jakejackson1:fix/checkout-address-fields-order-persistence

Conversation

@jakejackson1

Copy link
Copy Markdown

Summary

This PR fixes a couple bugs with how the address fields are displayed/sorted in the admin area when using the old checkout shortcode. Full details are below (summarized by AI).


The "Checkout Address Fields" multicheck on Downloads → Settings → Payments → Checkout is supposed to remember the order you drag the fields into. It doesn't — the saved order is correctly written to edd_settings[checkout_address_fields_order] and honored by the front-end checkout, but the admin settings screen ignores it and re-renders the list in a hardcoded order. Re-saving the settings page without re-dragging then wipes the stored order entirely. Two compounding bugs are responsible; this PR fixes both.

Bug 1 — edd_multicheck_callback() always reads an empty order

includes/admin/settings/register-settings.php:709

'order' => edd_get_option( 'edd_settings[' . edd_sanitize_key( $args['id'] ) . '_order', array() ),

The literal key passed to edd_get_option() is edd_settings[checkout_address_fields_order — the closing ] is on the wrong side of the function call, and the edd_settings[…] wrapper shouldn't be there in the first place because edd_get_option() already reads from the edd_settings array (see lines 838 and 903 in the same file for the correct pattern, e.g. edd_get_option( 'payment_icons_order', '' )).

As a result this lookup always returns the array() default, so the hidden <input class="edd-order" value=""> rendered by EDD\HTML\Multicheck is always blank on page load. The jQuery UI sortable stop handler updates the hidden input on drag, so a drag-then-save round-trip works once — but the next page load wipes the value, and any non-drag save (e.g. toggling an unrelated checkbox) POSTs an empty string and erases the saved order.

Fix: strip the wrapper and the stray bracket so the key is just <id>_order.

Bug 2 — Gateways::get_address_options() clobbers the saved order

src/Admin/Settings/Tabs/Gateways.php:591-601

$options = \EDD\Forms\Checkout\Registry::get_fields();
$order   = edd_get_option( 'checkout_address_fields_order', array() );
if ( ! empty( $order ) ) {
    $order   = explode( ',', $order );
    $options = array_merge( array_flip( $order ), $options );
}

if ( ! \EDD\Checkout\Validator::has_block() ) {
    $original_shortcode_order = array( 'address', 'address_2', 'city', 'zip', 'country', 'state' );
    $options                  = array_merge( array_flip( $original_shortcode_order ), $options );
}

The first block correctly reorders $options to the user's saved sort. The second block then unconditionally prepends the hardcoded shortcode order. Because array_merge assigns key positions by first occurrence, this reseats every key at the hardcoded position and discards the user's order. On any site whose checkout page uses the [download_checkout] shortcode rather than the edd/checkout block, the multicheck list is forced back to address → address_2 → city → zip → country → state regardless of what's stored. This appears to have been intended as a first-render fallback for the shortcode flow but is missing the empty-order guard.

Fix: apply the hardcoded fallback only when no saved order exists (elseif).

Reproduction (before this PR)

  1. Install EDD on a site whose checkout page uses the [download_checkout] shortcode.
  2. Go to Downloads → Settings → Payments → Checkout.
  3. Drag "Country" above "Address Line 1", save.
  4. Reload the page → the list reverts to the default order.
  5. Inspect wp_options.edd_settings.checkout_address_fields_order → it contains the correct dragged order. The front-end checkout also renders fields in the correct dragged order (Registry consumes the option directly at src/Forms/Checkout/Registry.php:93). Only the admin UI is wrong.
  6. Toggle any unrelated checkbox and save → checkout_address_fields_order is now wiped to an empty string.

After

  • The hidden order input is hydrated with the stored value on every render, so non-drag saves no longer wipe the order.
  • The admin multicheck list renders in the user's saved order on both shortcode and block checkouts.
  • The hardcoded shortcode order is preserved as a first-run fallback when nothing has been saved yet.

## Summary

The "Checkout Address Fields" multicheck on `Downloads → Settings → Payments → Checkout` is supposed to remember the order you drag the fields into. It doesn't — the saved order is correctly written to `edd_settings[checkout_address_fields_order]` and honored by the front-end checkout, but the admin settings screen ignores it and re-renders the list in a hardcoded order. Re-saving the settings page without re-dragging then wipes the stored order entirely. Two compounding bugs are responsible; this PR fixes both.

## Bug 1 — `edd_multicheck_callback()` always reads an empty order

`includes/admin/settings/register-settings.php:709`

```php
'order' => edd_get_option( 'edd_settings[' . edd_sanitize_key( $args['id'] ) . '_order', array() ),
```

The literal key passed to `edd_get_option()` is `edd_settings[checkout_address_fields_order` — the closing `]` is on the wrong side of the function call, and the `edd_settings[…]` wrapper shouldn't be there in the first place because `edd_get_option()` already reads from the `edd_settings` array (see lines 838 and 903 in the same file for the correct pattern, e.g. `edd_get_option( 'payment_icons_order', '' )`).

As a result this lookup always returns the `array()` default, so the hidden `<input class="edd-order" value="">` rendered by `EDD\HTML\Multicheck` is **always blank on page load**. The jQuery UI `sortable` `stop` handler updates the hidden input on drag, so a drag-then-save round-trip works once — but the next page load wipes the value, and any non-drag save (e.g. toggling an unrelated checkbox) POSTs an empty string and erases the saved order.

**Fix:** strip the wrapper and the stray bracket so the key is just `<id>_order`.

## Bug 2 — `Gateways::get_address_options()` clobbers the saved order

`src/Admin/Settings/Tabs/Gateways.php:591-601`

```php
$options = \EDD\Forms\Checkout\Registry::get_fields();
$order   = edd_get_option( 'checkout_address_fields_order', array() );
if ( ! empty( $order ) ) {
    $order   = explode( ',', $order );
    $options = array_merge( array_flip( $order ), $options );
}

if ( ! \EDD\Checkout\Validator::has_block() ) {
    $original_shortcode_order = array( 'address', 'address_2', 'city', 'zip', 'country', 'state' );
    $options                  = array_merge( array_flip( $original_shortcode_order ), $options );
}
```

The first block correctly reorders `$options` to the user's saved sort. The second block then unconditionally prepends the hardcoded shortcode order. Because `array_merge` assigns key positions by *first occurrence*, this reseats every key at the hardcoded position and discards the user's order. On any site whose checkout page uses the `[download_checkout]` shortcode rather than the `edd/checkout` block, the multicheck list is forced back to `address → address_2 → city → zip → country → state` regardless of what's stored. This appears to have been intended as a first-render fallback for the shortcode flow but is missing the empty-order guard.

**Fix:** apply the hardcoded fallback only when no saved order exists (`elseif`).

## Reproduction (before this PR)

1. Install EDD on a site whose checkout page uses the `[download_checkout]` shortcode.
2. Go to `Downloads → Settings → Payments → Checkout`.
3. Drag "Country" above "Address Line 1", save.
4. Reload the page → the list reverts to the default order.
5. Inspect `wp_options.edd_settings.checkout_address_fields_order` → it contains the correct dragged order. The front-end checkout also renders fields in the correct dragged order (Registry consumes the option directly at `src/Forms/Checkout/Registry.php:93`). Only the admin UI is wrong.
6. Toggle any unrelated checkbox and save → `checkout_address_fields_order` is now wiped to an empty string.

## After

- The hidden order input is hydrated with the stored value on every render, so non-drag saves no longer wipe the order.
- The admin multicheck list renders in the user's saved order on both shortcode and block checkouts.
- The hardcoded shortcode order is preserved as a first-run fallback when nothing has been saved yet.

## Test plan

- [ ] Shortcode checkout site, no saved order → list renders as `address, address_2, city, zip, country, state` (unchanged default).
- [ ] Shortcode checkout site, drag to a custom order, save, reload → custom order persists in the admin UI and on the front-end.
- [ ] Same site, save the settings page without dragging → `checkout_address_fields_order` retains its prior value.
- [ ] Block checkout site → behavior unchanged from current main (saved order honored, no shortcode fallback applied).
- [ ] Other sortable multichecks routed through `edd_multicheck_callback` (e.g. anything else passing `'sortable' => true`) → hidden input is now populated from the matching `<id>_order` option on render.
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant