Fixes: ✅ Issue #2: Mobile drawer now uses accordion (no nested modals) ✅ Issue #3: Duplicate "Local pickup" - now shows as: - Local pickup - Local pickup (local_pickup_plus) Changes: - Mobile drawer matches desktop accordion pattern - Smaller text/spacing for mobile - Deduplication logic in backend API - Adds method ID suffix for duplicate titles Result: ✅ No modal-over-modal on any device ✅ Consistent UX desktop/mobile ✅ Clear distinction between similar methods
686 lines
18 KiB
PHP
686 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Shipping REST API Controller
|
|
*
|
|
* Provides REST endpoints for shipping zones and methods management.
|
|
*
|
|
* @package WooNooW
|
|
*/
|
|
|
|
namespace WooNooW\Api;
|
|
|
|
use WP_REST_Controller;
|
|
use WP_REST_Request;
|
|
use WP_REST_Response;
|
|
use WC_Shipping_Zones;
|
|
|
|
class ShippingController extends WP_REST_Controller {
|
|
|
|
protected $namespace = 'woonoow/v1';
|
|
protected $rest_base = 'settings/shipping';
|
|
|
|
public function register_routes() {
|
|
// Get shipping zones
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base . '/zones',
|
|
array(
|
|
array(
|
|
'methods' => \WP_REST_Server::READABLE,
|
|
'callback' => array( $this, 'get_zones' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
),
|
|
)
|
|
);
|
|
|
|
// Toggle shipping method
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base . '/zones/(?P<zone_id>\d+)/methods/(?P<instance_id>\d+)/toggle',
|
|
array(
|
|
array(
|
|
'methods' => \WP_REST_Server::CREATABLE,
|
|
'callback' => array( $this, 'toggle_method' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
'args' => array(
|
|
'zone_id' => array(
|
|
'required' => true,
|
|
'type' => 'integer',
|
|
),
|
|
'instance_id' => array(
|
|
'required' => true,
|
|
'type' => 'integer',
|
|
),
|
|
'enabled' => array(
|
|
'required' => true,
|
|
'type' => 'boolean',
|
|
),
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
// Get available shipping methods
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base . '/methods/available',
|
|
array(
|
|
array(
|
|
'methods' => \WP_REST_Server::READABLE,
|
|
'callback' => array( $this, 'get_available_methods' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
),
|
|
)
|
|
);
|
|
|
|
// Add shipping method to zone
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base . '/zones/(?P<zone_id>\d+)/methods',
|
|
array(
|
|
array(
|
|
'methods' => \WP_REST_Server::CREATABLE,
|
|
'callback' => array( $this, 'add_method' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
'args' => array(
|
|
'zone_id' => array(
|
|
'required' => true,
|
|
'type' => 'integer',
|
|
),
|
|
'method_id' => array(
|
|
'required' => true,
|
|
'type' => 'string',
|
|
),
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
// Delete shipping method from zone
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base . '/zones/(?P<zone_id>\d+)/methods/(?P<instance_id>\d+)',
|
|
array(
|
|
array(
|
|
'methods' => \WP_REST_Server::DELETABLE,
|
|
'callback' => array( $this, 'delete_method' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
'args' => array(
|
|
'zone_id' => array(
|
|
'required' => true,
|
|
'type' => 'integer',
|
|
),
|
|
'instance_id' => array(
|
|
'required' => true,
|
|
'type' => 'integer',
|
|
),
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
// Get method settings
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base . '/zones/(?P<zone_id>\d+)/methods/(?P<instance_id>\d+)/settings',
|
|
array(
|
|
array(
|
|
'methods' => \WP_REST_Server::READABLE,
|
|
'callback' => array( $this, 'get_method_settings' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
),
|
|
)
|
|
);
|
|
|
|
// Update method settings
|
|
register_rest_route(
|
|
$this->namespace,
|
|
'/' . $this->rest_base . '/zones/(?P<zone_id>\d+)/methods/(?P<instance_id>\d+)/settings',
|
|
array(
|
|
array(
|
|
'methods' => \WP_REST_Server::EDITABLE,
|
|
'callback' => array( $this, 'update_method_settings' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get all shipping zones with their methods
|
|
*/
|
|
public function get_zones( WP_REST_Request $request ) {
|
|
try {
|
|
$zones_data = array();
|
|
|
|
// Get all shipping zones
|
|
$zones = WC_Shipping_Zones::get_zones();
|
|
|
|
foreach ( $zones as $zone_data ) {
|
|
$zone = WC_Shipping_Zones::get_zone( $zone_data['id'] );
|
|
|
|
// Get zone locations (regions)
|
|
$locations = $zone->get_zone_locations();
|
|
$regions = array();
|
|
|
|
foreach ( $locations as $location ) {
|
|
if ( $location->type === 'country' ) {
|
|
$countries = WC()->countries->get_countries();
|
|
$regions[] = $countries[ $location->code ] ?? $location->code;
|
|
} elseif ( $location->type === 'state' ) {
|
|
$states = WC()->countries->get_states( substr( $location->code, 0, 2 ) );
|
|
$regions[] = $states[ substr( $location->code, 3 ) ] ?? $location->code;
|
|
} elseif ( $location->type === 'continent' ) {
|
|
$continents = WC()->countries->get_continents();
|
|
$regions[] = $continents[ $location->code ]['name'] ?? $location->code;
|
|
} elseif ( $location->type === 'postcode' ) {
|
|
$regions[] = 'Postcode: ' . $location->code;
|
|
}
|
|
}
|
|
|
|
// Get shipping methods for this zone (false = get all, not just enabled)
|
|
$shipping_methods = $zone->get_shipping_methods( false );
|
|
$rates = array();
|
|
|
|
foreach ( $shipping_methods as $method ) {
|
|
// Get fresh settings from database
|
|
$settings = get_option( $method->get_instance_option_key(), array() );
|
|
$is_enabled = isset( $settings['enabled'] ) && $settings['enabled'] === 'yes';
|
|
|
|
$rate = array(
|
|
'id' => $method->id . ':' . $method->instance_id,
|
|
'instance_id' => $method->instance_id,
|
|
'method_id' => $method->id,
|
|
'name' => $method->get_title(),
|
|
'enabled' => $is_enabled,
|
|
);
|
|
|
|
// Get cost if available
|
|
if ( isset( $method->cost ) && $method->cost !== '' ) {
|
|
$rate['price'] = wc_price( $method->cost );
|
|
} elseif ( $method->id === 'free_shipping' ) {
|
|
$rate['price'] = __( 'Free', 'woonoow' );
|
|
} else {
|
|
$rate['price'] = __( 'Calculated', 'woonoow' );
|
|
}
|
|
|
|
// Get method description if available
|
|
if ( method_exists( $method, 'get_method_description' ) ) {
|
|
$rate['description'] = $method->get_method_description();
|
|
}
|
|
|
|
$rates[] = $rate;
|
|
}
|
|
|
|
$zones_data[] = array(
|
|
'id' => $zone_data['id'],
|
|
'name' => $zone->get_zone_name(),
|
|
'order' => $zone->get_zone_order(),
|
|
'regions' => ! empty( $regions ) ? implode( ', ', $regions ) : __( 'No regions', 'woonoow' ),
|
|
'rates' => $rates,
|
|
);
|
|
}
|
|
|
|
// Add "Rest of the World" zone (zone 0)
|
|
$zone_0 = new \WC_Shipping_Zone( 0 );
|
|
$shipping_methods = $zone_0->get_shipping_methods( false );
|
|
$rates = array();
|
|
|
|
foreach ( $shipping_methods as $method ) {
|
|
// Get fresh settings from database
|
|
$settings = get_option( $method->get_instance_option_key(), array() );
|
|
$is_enabled = isset( $settings['enabled'] ) && $settings['enabled'] === 'yes';
|
|
|
|
$rate = array(
|
|
'id' => $method->id . ':' . $method->instance_id,
|
|
'instance_id' => $method->instance_id,
|
|
'method_id' => $method->id,
|
|
'name' => $method->get_title(),
|
|
'enabled' => $is_enabled,
|
|
);
|
|
|
|
if ( isset( $method->cost ) && $method->cost !== '' ) {
|
|
$rate['price'] = wc_price( $method->cost );
|
|
} elseif ( $method->id === 'free_shipping' ) {
|
|
$rate['price'] = __( 'Free', 'woonoow' );
|
|
} else {
|
|
$rate['price'] = __( 'Calculated', 'woonoow' );
|
|
}
|
|
|
|
$rates[] = $rate;
|
|
}
|
|
|
|
if ( ! empty( $rates ) ) {
|
|
$zones_data[] = array(
|
|
'id' => 0,
|
|
'name' => __( 'Rest of the World', 'woonoow' ),
|
|
'order' => 9999,
|
|
'regions' => __( 'Locations not covered by other zones', 'woonoow' ),
|
|
'rates' => $rates,
|
|
);
|
|
}
|
|
|
|
return new WP_REST_Response( $zones_data, 200 );
|
|
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'fetch_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle shipping method enabled/disabled
|
|
*/
|
|
public function toggle_method( WP_REST_Request $request ) {
|
|
try {
|
|
$zone_id = $request->get_param( 'zone_id' );
|
|
$instance_id = $request->get_param( 'instance_id' );
|
|
$enabled = $request->get_param( 'enabled' );
|
|
|
|
// Convert to boolean (handles both bool and string "true"/"false")
|
|
$enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
|
|
|
|
if ( $enabled === null ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'invalid_enabled_value',
|
|
'message' => __( 'The "enabled" parameter must be a boolean', 'woonoow' ),
|
|
),
|
|
400
|
|
);
|
|
}
|
|
|
|
// Get the zone
|
|
$zone = \WC_Shipping_Zones::get_zone( $zone_id );
|
|
if ( ! $zone ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'zone_not_found',
|
|
'message' => __( 'Shipping zone not found', 'woonoow' ),
|
|
),
|
|
404
|
|
);
|
|
}
|
|
|
|
// CRITICAL: Get ALL shipping methods (both enabled and disabled)
|
|
$shipping_methods = $zone->get_shipping_methods( false );
|
|
$method_found = false;
|
|
|
|
foreach ( $shipping_methods as $method ) {
|
|
if ( $method->instance_id == $instance_id ) {
|
|
$method_found = true;
|
|
|
|
// Get current settings from database
|
|
$option_key = $method->get_instance_option_key();
|
|
$current_settings = get_option( $option_key, array() );
|
|
|
|
// Update the enabled value
|
|
$new_enabled_value = $enabled ? 'yes' : 'no';
|
|
$current_settings['enabled'] = $new_enabled_value;
|
|
|
|
// Save to wp_options table
|
|
update_option( $option_key, $current_settings, 'yes' );
|
|
|
|
// Also update the zone_methods table (where WooCommerce admin reads from)
|
|
global $wpdb;
|
|
$table_name = $wpdb->prefix . 'woocommerce_shipping_zone_methods';
|
|
$wpdb->update(
|
|
$table_name,
|
|
array( 'is_enabled' => $enabled ? 1 : 0 ),
|
|
array( 'instance_id' => $instance_id ),
|
|
array( '%d' ),
|
|
array( '%d' )
|
|
);
|
|
|
|
// Clear the zone's method cache so it reloads from DB
|
|
delete_transient( 'wc_shipping_method_count' );
|
|
wp_cache_delete( 'shipping_zones', 'woocommerce' );
|
|
wp_cache_delete( $zone_id, 'shipping_zones' );
|
|
\WC_Cache_Helper::get_transient_version( 'shipping', true );
|
|
|
|
// Also update the in-memory property
|
|
$method->enabled = $new_enabled_value;
|
|
|
|
// Fire action hook for other plugins/code
|
|
do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method->id, $zone_id, $enabled );
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( ! $method_found ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'method_not_found',
|
|
'message' => __( 'Shipping method not found', 'woonoow' ),
|
|
),
|
|
404
|
|
);
|
|
}
|
|
|
|
// Clear shipping cache
|
|
\WC_Cache_Helper::get_transient_version( 'shipping', true );
|
|
wp_cache_flush();
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'success' => true,
|
|
'message' => $enabled
|
|
? __( 'Shipping method enabled', 'woonoow' )
|
|
: __( 'Shipping method disabled', 'woonoow' ),
|
|
),
|
|
200
|
|
);
|
|
|
|
} catch ( \Exception $e ) {
|
|
error_log( sprintf( '[WooNooW] Toggle exception: %s', $e->getMessage() ) );
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'toggle_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get available shipping methods
|
|
*/
|
|
public function get_available_methods( WP_REST_Request $request ) {
|
|
try {
|
|
$shipping = \WC()->shipping();
|
|
$methods = $shipping->get_shipping_methods();
|
|
|
|
$available = array();
|
|
$seen_titles = array();
|
|
|
|
foreach ( $methods as $method ) {
|
|
$title = $method->get_method_title();
|
|
$id = $method->id;
|
|
|
|
// Deduplicate by adding ID suffix if title already exists
|
|
if ( isset( $seen_titles[ $title ] ) ) {
|
|
$title .= ' (' . $id . ')';
|
|
}
|
|
$seen_titles[ $title ] = true;
|
|
|
|
$available[] = array(
|
|
'id' => $id,
|
|
'title' => $title,
|
|
'description' => $method->get_method_description(),
|
|
);
|
|
}
|
|
|
|
return new WP_REST_Response( $available, 200 );
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'fetch_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add shipping method to zone
|
|
*/
|
|
public function add_method( WP_REST_Request $request ) {
|
|
try {
|
|
$zone_id = $request->get_param( 'zone_id' );
|
|
$method_id = $request->get_param( 'method_id' );
|
|
|
|
$zone = \WC_Shipping_Zones::get_zone( $zone_id );
|
|
if ( ! $zone ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'zone_not_found',
|
|
'message' => __( 'Shipping zone not found', 'woonoow' ),
|
|
),
|
|
404
|
|
);
|
|
}
|
|
|
|
// Add method to zone
|
|
$instance_id = $zone->add_shipping_method( $method_id );
|
|
|
|
if ( ! $instance_id ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'add_failed',
|
|
'message' => __( 'Failed to add shipping method', 'woonoow' ),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
|
|
// Clear caches
|
|
\WC_Cache_Helper::get_transient_version( 'shipping', true );
|
|
wp_cache_flush();
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'success' => true,
|
|
'instance_id' => $instance_id,
|
|
'message' => __( 'Shipping method added successfully', 'woonoow' ),
|
|
),
|
|
200
|
|
);
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'add_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete shipping method from zone
|
|
*/
|
|
public function delete_method( WP_REST_Request $request ) {
|
|
try {
|
|
$zone_id = $request->get_param( 'zone_id' );
|
|
$instance_id = $request->get_param( 'instance_id' );
|
|
|
|
$zone = \WC_Shipping_Zones::get_zone( $zone_id );
|
|
if ( ! $zone ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'zone_not_found',
|
|
'message' => __( 'Shipping zone not found', 'woonoow' ),
|
|
),
|
|
404
|
|
);
|
|
}
|
|
|
|
// Delete method from zone
|
|
$zone->delete_shipping_method( $instance_id );
|
|
|
|
// Clear caches
|
|
\WC_Cache_Helper::get_transient_version( 'shipping', true );
|
|
wp_cache_flush();
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'success' => true,
|
|
'message' => __( 'Shipping method deleted successfully', 'woonoow' ),
|
|
),
|
|
200
|
|
);
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'delete_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get method settings (form fields)
|
|
*/
|
|
public function get_method_settings( WP_REST_Request $request ) {
|
|
try {
|
|
$zone_id = $request->get_param( 'zone_id' );
|
|
$instance_id = $request->get_param( 'instance_id' );
|
|
|
|
$zone = \WC_Shipping_Zones::get_zone( $zone_id );
|
|
if ( ! $zone ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'zone_not_found',
|
|
'message' => __( 'Shipping zone not found', 'woonoow' ),
|
|
),
|
|
404
|
|
);
|
|
}
|
|
|
|
$shipping_methods = $zone->get_shipping_methods( false );
|
|
foreach ( $shipping_methods as $method ) {
|
|
if ( $method->instance_id == $instance_id ) {
|
|
// Get form fields
|
|
$form_fields = $method->get_instance_form_fields();
|
|
|
|
// Get current settings
|
|
$settings = get_option( $method->get_instance_option_key(), array() );
|
|
|
|
// Format fields with current values
|
|
$formatted_fields = array();
|
|
foreach ( $form_fields as $key => $field ) {
|
|
$formatted_fields[ $key ] = array(
|
|
'id' => $key,
|
|
'type' => $field['type'] ?? 'text',
|
|
'title' => $field['title'] ?? '',
|
|
'description' => $field['description'] ?? '',
|
|
'default' => $field['default'] ?? '',
|
|
'value' => $settings[ $key ] ?? ( $field['default'] ?? '' ),
|
|
'placeholder' => $field['placeholder'] ?? '',
|
|
'options' => $field['options'] ?? array(),
|
|
);
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'method_id' => $method->id,
|
|
'method_title' => $method->get_method_title(),
|
|
'instance_id' => $instance_id,
|
|
'settings' => $formatted_fields,
|
|
),
|
|
200
|
|
);
|
|
}
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'method_not_found',
|
|
'message' => __( 'Shipping method not found', 'woonoow' ),
|
|
),
|
|
404
|
|
);
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'fetch_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update method settings
|
|
*/
|
|
public function update_method_settings( WP_REST_Request $request ) {
|
|
try {
|
|
$zone_id = $request->get_param( 'zone_id' );
|
|
$instance_id = $request->get_param( 'instance_id' );
|
|
$settings = $request->get_param( 'settings' );
|
|
|
|
if ( empty( $settings ) || ! is_array( $settings ) ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'invalid_settings',
|
|
'message' => __( 'Invalid settings data', 'woonoow' ),
|
|
),
|
|
400
|
|
);
|
|
}
|
|
|
|
$zone = \WC_Shipping_Zones::get_zone( $zone_id );
|
|
if ( ! $zone ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'zone_not_found',
|
|
'message' => __( 'Shipping zone not found', 'woonoow' ),
|
|
),
|
|
404
|
|
);
|
|
}
|
|
|
|
$shipping_methods = $zone->get_shipping_methods( false );
|
|
foreach ( $shipping_methods as $method ) {
|
|
if ( $method->instance_id == $instance_id ) {
|
|
// Get current settings
|
|
$option_key = $method->get_instance_option_key();
|
|
$current_settings = get_option( $option_key, array() );
|
|
|
|
// Merge with new settings
|
|
$new_settings = array_merge( $current_settings, $settings );
|
|
|
|
// Save to database
|
|
update_option( $option_key, $new_settings, 'yes' );
|
|
|
|
// Clear caches
|
|
\WC_Cache_Helper::get_transient_version( 'shipping', true );
|
|
wp_cache_flush();
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'success' => true,
|
|
'message' => __( 'Shipping method settings updated successfully', 'woonoow' ),
|
|
),
|
|
200
|
|
);
|
|
}
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'method_not_found',
|
|
'message' => __( 'Shipping method not found', 'woonoow' ),
|
|
),
|
|
404
|
|
);
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'update_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user has permission to manage shipping
|
|
*/
|
|
public function check_permission() {
|
|
return current_user_can( 'manage_woocommerce' );
|
|
}
|
|
}
|