Files
WooList/woolist-phplist/includes/class-woolist-admin.php
Malin 7f1f351ff1 fix: meta box visibility + add WooCommerce Order Actions dropdown entries
Meta box:
- Replace generic add_meta_boxes hook with screen-specific
  add_meta_boxes_shop_order and add_meta_boxes_woocommerce_page_wc-orders
  so the box reliably appears on both classic and HPOS order edit pages

Order Actions dropdown:
- Hook woocommerce_order_actions filter to inject "phpList: Add to X list"
  entries for every configured list (only lists with a saved ID appear)
- Register individual woocommerce_order_action_woolist_sync_{list_id}
  handlers via closures at init time so WooCommerce can process them
- Shared do_sync() used by both the dropdown action and the meta box AJAX;
  appends an order note (visible in order timeline) on success or failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 08:06:05 +01:00

605 lines
21 KiB
PHP

<?php
/**
* WooCommerce Settings tab and order-page meta box for WooList.
*
* @package WooList
*/
defined( 'ABSPATH' ) || exit;
class WooList_Admin {
/** @var WooList_API */
private WooList_API $api;
public function __construct( WooList_API $api ) {
$this->api = $api;
// WooCommerce settings tab.
add_filter( 'woocommerce_settings_tabs_array', [ $this, 'add_settings_tab' ], 50 );
add_action( 'woocommerce_settings_tabs_woolist', [ $this, 'render_settings' ] );
add_action( 'woocommerce_update_options_woolist', [ $this, 'save_settings' ] );
// Test connection admin-post handler.
add_action( 'admin_post_woolist_test_connection', [ $this, 'handle_test_connection' ] );
// Order page meta box — use screen-specific hooks so both classic and
// HPOS order edit pages are reliably targeted.
add_action( 'add_meta_boxes_shop_order', [ $this, 'register_meta_box' ] );
add_action( 'add_meta_boxes_woocommerce_page_wc-orders', [ $this, 'register_meta_box' ] );
// WooCommerce Order Actions dropdown (works with classic + HPOS).
add_filter( 'woocommerce_order_actions', [ $this, 'add_order_actions' ] );
// Register one action handler per configured list (list IDs from options).
$this->register_order_action_handlers();
// AJAX handler for the manual-subscribe meta box button.
add_action( 'wp_ajax_woolist_manual_subscribe', [ $this, 'handle_manual_subscribe' ] );
// Admin assets.
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
}
// ── Settings tab ─────────────────────────────────────────────────────────
public function add_settings_tab( array $tabs ): array {
$tabs['woolist'] = __( 'phpList', 'woolist-phplist' );
return $tabs;
}
public function render_settings(): void {
woocommerce_admin_fields( $this->get_settings() );
$this->render_test_connection_button();
}
public function save_settings(): void {
woocommerce_update_options( $this->get_settings() );
}
private function render_test_connection_button(): void {
$action_url = wp_nonce_url(
admin_url( 'admin-post.php?action=woolist_test_connection' ),
'woolist_test_connection'
);
$notice = get_transient( 'woolist_test_connection_notice_' . get_current_user_id() );
if ( $notice ) {
delete_transient( 'woolist_test_connection_notice_' . get_current_user_id() );
$type = $notice['success'] ? 'updated' : 'error';
printf(
'<div class="notice notice-%s inline"><p>%s</p></div>',
esc_attr( $type ),
esc_html( $notice['message'] )
);
}
echo '<p><a href="' . esc_url( $action_url ) . '" class="button button-secondary">'
. esc_html__( 'Test Connection', 'woolist-phplist' )
. '</a>';
echo ' <span style="color:#646970;font-size:13px;margin-left:8px;">'
. esc_html__( 'Check WooCommerce → Status → Logs (source: woolist-phplist) to see results.', 'woolist-phplist' )
. '</span></p>';
}
public function handle_test_connection(): void {
check_admin_referer( 'woolist_test_connection' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'woolist-phplist' ) );
}
WooList_Logger::info( 'Test Connection triggered by user ID ' . get_current_user_id() );
$result = $this->api->lists_get();
$user_id = get_current_user_id();
if ( is_wp_error( $result ) ) {
WooList_Logger::error( 'Test Connection failed: ' . $result->get_error_message() );
set_transient(
'woolist_test_connection_notice_' . $user_id,
[
'success' => false,
'message' => __( 'Connection failed: ', 'woolist-phplist' ) . $result->get_error_message(),
],
60
);
} else {
$count = is_array( $result ) ? count( $result ) : '?';
WooList_Logger::info( 'Test Connection succeeded. Found ' . $count . ' list(s).' );
set_transient(
'woolist_test_connection_notice_' . $user_id,
[
'success' => true,
'message' => sprintf(
/* translators: %d: number of lists found */
__( 'Connection successful! Found %d list(s) in phpList.', 'woolist-phplist' ),
$count
),
],
60
);
}
wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=woolist' ) );
exit;
}
// ── Order page meta box ───────────────────────────────────────────────────
/**
* Called by add_meta_boxes_shop_order and add_meta_boxes_woocommerce_page_wc-orders.
* The $post_or_order argument is passed by WC but not needed here.
*/
public function register_meta_box( $post_or_order = null ): void {
// Determine the correct screen ID from the current hook name.
$screen = did_action( 'add_meta_boxes_shop_order' )
? 'shop_order'
: 'woocommerce_page_wc-orders';
add_meta_box(
'woolist_order_metabox',
__( 'phpList Sync', 'woolist-phplist' ),
[ $this, 'render_order_meta_box' ],
$screen,
'side',
'default'
);
}
// ── WooCommerce Order Actions dropdown ────────────────────────────────────
/**
* Inject a "phpList: …" entry for each configured list into the
* WooCommerce Order Actions dropdown (the select in the order sidebar).
*
* @param array $actions Existing order actions.
* @return array
*/
public function add_order_actions( array $actions ): array {
foreach ( $this->get_configured_lists() as $list_id => $label ) {
$actions[ 'woolist_sync_' . $list_id ] = sprintf(
/* translators: %s: list label e.g. "Completed Orders" */
__( 'phpList: Add to %s list', 'woolist-phplist' ),
$label
);
}
return $actions;
}
/**
* Register a woocommerce_order_action_{slug} handler for every
* configured list at init time so WooCommerce can call them.
*/
private function register_order_action_handlers(): void {
foreach ( $this->get_configured_lists() as $list_id => $label ) {
$list_id_captured = $list_id; // explicit capture for the closure
add_action(
'woocommerce_order_action_woolist_sync_' . $list_id,
function ( $order ) use ( $list_id_captured ) {
$this->do_sync( $order, $list_id_captured );
}
);
}
}
/**
* Shared subscribe logic used by both the Order Action and the meta box AJAX.
*
* @param WC_Abstract_Order $order The order.
* @param int $list_id Target phpList list ID.
* @return bool True on success.
*/
private function do_sync( WC_Abstract_Order $order, int $list_id ): bool {
$email = $order->get_billing_email();
$order_id = $order->get_id();
if ( ! is_email( $email ) ) {
WooList_Logger::error( 'do_sync: order #' . $order_id . ' has no valid billing email.' );
return false;
}
WooList_Logger::info( 'do_sync: order #' . $order_id . ' email=' . $email . ' list_id=' . $list_id );
$result = $this->api->subscribe_email_to_list( $email, $list_id );
if ( $result['success'] ) {
// Add a visible note on the order so it's traceable in the order timeline.
$order->add_order_note( sprintf(
/* translators: 1: email, 2: list ID */
__( 'WooList: %1$s added to phpList list %2$d.', 'woolist-phplist' ),
$email,
$list_id
) );
return true;
}
$order->add_order_note( sprintf(
/* translators: 1: email, 2: list ID */
__( 'WooList: failed to add %1$s to phpList list %2$d. Check WooCommerce → Status → Logs.', 'woolist-phplist' ),
$email,
$list_id
) );
return false;
}
/**
* Render the "Add to phpList" meta box on the order edit page.
*
* @param WP_Post|WC_Abstract_Order $post_or_order Classic WP_Post or HPOS order object.
*/
public function render_order_meta_box( $post_or_order ): void {
// Normalise to a WC order object regardless of storage mode.
if ( $post_or_order instanceof WC_Abstract_Order ) {
$order = $post_or_order;
} else {
$order = wc_get_order( $post_or_order->ID );
}
if ( ! $order ) {
echo '<p>' . esc_html__( 'Could not load order.', 'woolist-phplist' ) . '</p>';
return;
}
$order_id = $order->get_id();
$email = $order->get_billing_email();
// Build list of configured lists so the admin can pick one.
$lists = $this->get_configured_lists();
if ( empty( $lists ) ) {
echo '<p style="color:#d63638;">'
. esc_html__( 'No phpList lists are configured yet. Add list IDs under WooCommerce → Settings → phpList.', 'woolist-phplist' )
. '</p>';
return;
}
$nonce = wp_create_nonce( 'woolist_manual_subscribe_' . $order_id );
?>
<div class="woolist-metabox-wrap">
<p style="margin:0 0 8px;">
<strong><?php esc_html_e( 'Email:', 'woolist-phplist' ); ?></strong>
<?php echo esc_html( $email ?: __( '(none)', 'woolist-phplist' ) ); ?>
</p>
<p style="margin:0 0 6px;">
<label for="woolist-list-select-<?php echo esc_attr( $order_id ); ?>" style="display:block;margin-bottom:4px;font-weight:600;">
<?php esc_html_e( 'Target list:', 'woolist-phplist' ); ?>
</label>
<select id="woolist-list-select-<?php echo esc_attr( $order_id ); ?>"
class="woolist-list-select"
style="width:100%;">
<?php foreach ( $lists as $list_id => $label ) : ?>
<option value="<?php echo esc_attr( $list_id ); ?>">
<?php echo esc_html( $label . ' (ID: ' . $list_id . ')' ); ?>
</option>
<?php endforeach; ?>
</select>
</p>
<button type="button"
class="button woolist-manual-subscribe-btn"
style="width:100%;margin-top:4px;"
data-order-id="<?php echo esc_attr( $order_id ); ?>"
data-nonce="<?php echo esc_attr( $nonce ); ?>">
<?php esc_html_e( 'Add to phpList', 'woolist-phplist' ); ?>
</button>
<div class="woolist-metabox-response" style="display:none;margin-top:8px;padding:6px 8px;border-radius:3px;font-size:13px;"></div>
</div>
<?php
}
/**
* Build an associative array of [ list_id => human label ] for all lists
* that have an ID configured in settings, regardless of whether sync is enabled.
*
* @return array<int, string>
*/
private function get_configured_lists(): array {
$map = [
(int) get_option( 'woolist_completed_list_id', 0 ) => __( 'Completed Orders', 'woolist-phplist' ),
(int) get_option( 'woolist_cancelled_list_id', 0 ) => __( 'Cancelled Orders', 'woolist-phplist' ),
(int) get_option( 'woolist_signup_list_id', 0 ) => __( 'Account Signup', 'woolist-phplist' ),
(int) get_option( 'woolist_newsletter_list_id', 0 ) => __( 'Newsletter', 'woolist-phplist' ),
];
// Remove entries where no list ID has been set.
unset( $map[0] );
return $map;
}
/**
* AJAX handler for the "Add to phpList" order meta box button.
*/
public function handle_manual_subscribe(): void {
$order_id = (int) ( $_POST['order_id'] ?? 0 );
if ( ! check_ajax_referer( 'woolist_manual_subscribe_' . $order_id, 'nonce', false ) ) {
wp_send_json_error( [ 'message' => __( 'Security check failed.', 'woolist-phplist' ) ], 403 );
}
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( [ 'message' => __( 'Permission denied.', 'woolist-phplist' ) ], 403 );
}
$list_id = (int) ( $_POST['list_id'] ?? 0 );
if ( $list_id < 1 ) {
wp_send_json_error( [ 'message' => __( 'No list selected.', 'woolist-phplist' ) ] );
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
wp_send_json_error( [ 'message' => __( 'Order not found.', 'woolist-phplist' ) ] );
}
WooList_Logger::info( 'Meta box manual subscribe: order #' . $order_id . ' list_id=' . $list_id . ' user#' . get_current_user_id() );
$ok = $this->do_sync( $order, $list_id );
if ( ! $ok ) {
wp_send_json_error( [
'message' => __( 'Subscription failed. Check WooCommerce → Status → Logs for details.', 'woolist-phplist' ),
] );
}
wp_send_json_success( [
'message' => sprintf(
/* translators: 1: email address, 2: list ID */
__( '%1$s added to list %2$d.', 'woolist-phplist' ),
esc_html( $order->get_billing_email() ),
$list_id
),
] );
}
// ── Admin assets ─────────────────────────────────────────────────────────
public function enqueue_admin_assets(): void {
$screen = get_current_screen();
if ( ! $screen ) {
return;
}
// Enqueue button JS on both classic and HPOS order edit screens.
if ( in_array( $screen->id, [ 'shop_order', 'woocommerce_page_wc-orders' ], true ) ) {
wp_enqueue_script(
'woolist-admin',
WOOLIST_URL . 'assets/js/woolist-admin.js',
[ 'jquery' ],
WOOLIST_VERSION,
true
);
// Inline styles for the meta box response area.
wp_add_inline_style(
'woocommerce_admin_styles',
'.woolist-mb-success{background:#edfaf1;color:#2d6a0f;border:1px solid #52c41a;}'
. '.woolist-mb-error{background:#fff1f0;color:#a8071a;border:1px solid #ff4d4f;}'
);
}
}
// ── Settings definitions ─────────────────────────────────────────────────
private function get_settings(): array {
return [
// ── Section 1: Connection ────────────────────────────────────────
[
'title' => __( 'phpList Connection', 'woolist-phplist' ),
'type' => 'title',
'id' => 'woolist_section_connection',
],
[
'title' => __( 'phpList Base URL', 'woolist-phplist' ),
'desc' => __( 'e.g. https://newsletter.example.com', 'woolist-phplist' ),
'id' => 'woolist_phplist_url',
'type' => 'text',
'default' => '',
'desc_tip' => true,
],
[
'title' => __( 'Login', 'woolist-phplist' ),
'desc' => __( 'phpList admin username.', 'woolist-phplist' ),
'id' => 'woolist_phplist_login',
'type' => 'text',
'default' => '',
'desc_tip' => true,
],
[
'title' => __( 'Password', 'woolist-phplist' ),
'desc' => __( 'phpList admin password.', 'woolist-phplist' ),
'id' => 'woolist_phplist_password',
'type' => 'password',
'default' => '',
'desc_tip' => true,
],
[
'type' => 'sectionend',
'id' => 'woolist_section_connection',
],
// ── Section 2: Completed Orders ──────────────────────────────────
[
'title' => __( 'Completed Orders', 'woolist-phplist' ),
'type' => 'title',
'id' => 'woolist_section_completed',
],
[
'title' => __( 'Enable sync', 'woolist-phplist' ),
'desc' => __( 'Subscribe customers to phpList when an order is completed.', 'woolist-phplist' ),
'id' => 'woolist_sync_completed',
'type' => 'checkbox',
'default' => 'no',
],
[
'title' => __( 'List ID', 'woolist-phplist' ),
'desc' => __( 'phpList list ID to subscribe completed-order customers to.', 'woolist-phplist' ),
'id' => 'woolist_completed_list_id',
'type' => 'number',
'default' => '',
'custom_attributes' => [ 'min' => '1' ],
'desc_tip' => true,
],
[
'type' => 'sectionend',
'id' => 'woolist_section_completed',
],
// ── Section 3: Cancelled Orders ──────────────────────────────────
[
'title' => __( 'Cancelled Orders', 'woolist-phplist' ),
'type' => 'title',
'id' => 'woolist_section_cancelled',
],
[
'title' => __( 'Enable sync', 'woolist-phplist' ),
'desc' => __( 'Subscribe customers to phpList when an order is cancelled.', 'woolist-phplist' ),
'id' => 'woolist_sync_cancelled',
'type' => 'checkbox',
'default' => 'no',
],
[
'title' => __( 'List ID', 'woolist-phplist' ),
'desc' => __( 'phpList list ID to subscribe cancelled-order customers to.', 'woolist-phplist' ),
'id' => 'woolist_cancelled_list_id',
'type' => 'number',
'default' => '',
'custom_attributes' => [ 'min' => '1' ],
'desc_tip' => true,
],
[
'type' => 'sectionend',
'id' => 'woolist_section_cancelled',
],
// ── Section 4: Account Signup ────────────────────────────────────
[
'title' => __( 'Account Signup', 'woolist-phplist' ),
'type' => 'title',
'id' => 'woolist_section_signup',
],
[
'title' => __( 'Enable sync', 'woolist-phplist' ),
'desc' => __( 'Subscribe new WordPress users to phpList when they register.', 'woolist-phplist' ),
'id' => 'woolist_sync_signup',
'type' => 'checkbox',
'default' => 'no',
],
[
'title' => __( 'List ID', 'woolist-phplist' ),
'desc' => __( 'phpList list ID for new account signups.', 'woolist-phplist' ),
'id' => 'woolist_signup_list_id',
'type' => 'number',
'default' => '',
'custom_attributes' => [ 'min' => '1' ],
'desc_tip' => true,
],
[
'type' => 'sectionend',
'id' => 'woolist_section_signup',
],
// ── Section 5: Newsletter Shortcode ──────────────────────────────
[
'title' => __( 'Newsletter Shortcode', 'woolist-phplist' ),
'type' => 'title',
'desc' => __( 'Settings for the [woolist_newsletter] shortcode.', 'woolist-phplist' ),
'id' => 'woolist_section_newsletter',
],
[
'title' => __( 'Enable sync', 'woolist-phplist' ),
'desc' => __( 'Enable the [woolist_newsletter] shortcode.', 'woolist-phplist' ),
'id' => 'woolist_sync_newsletter',
'type' => 'checkbox',
'default' => 'no',
],
[
'title' => __( 'List ID', 'woolist-phplist' ),
'desc' => __( 'phpList list ID for newsletter shortcode subscribers.', 'woolist-phplist' ),
'id' => 'woolist_newsletter_list_id',
'type' => 'number',
'default' => '',
'custom_attributes' => [ 'min' => '1' ],
'desc_tip' => true,
],
[
'title' => __( 'Enable incentive coupon', 'woolist-phplist' ),
'desc' => __( 'Send a WooCommerce coupon after newsletter signup.', 'woolist-phplist' ),
'id' => 'woolist_newsletter_enable_coupon',
'type' => 'checkbox',
'default' => 'no',
],
[
'title' => __( 'Coupon mode', 'woolist-phplist' ),
'desc' => __( 'Choose how the coupon is provided.', 'woolist-phplist' ),
'id' => 'woolist_coupon_mode',
'type' => 'select',
'default' => 'fixed',
'options' => [
'fixed' => __( 'Fixed code', 'woolist-phplist' ),
'autogenerate' => __( 'Auto-generate unique code', 'woolist-phplist' ),
],
'desc_tip' => true,
],
[
'title' => __( 'Fixed coupon code', 'woolist-phplist' ),
'desc' => __( 'The coupon code to show when mode is set to "Fixed code".', 'woolist-phplist' ),
'id' => 'woolist_coupon_fixed_code',
'type' => 'text',
'default' => '',
'desc_tip' => true,
],
[
'title' => __( 'Discount (%)', 'woolist-phplist' ),
'desc' => __( 'Percentage discount for auto-generated coupons.', 'woolist-phplist' ),
'id' => 'woolist_coupon_discount_pct',
'type' => 'number',
'default' => '10',
'custom_attributes' => [ 'min' => '1', 'max' => '100' ],
'desc_tip' => true,
],
[
'title' => __( 'Expiry (days)', 'woolist-phplist' ),
'desc' => __( 'Days until auto-generated coupon expires. Use 0 for no expiry.', 'woolist-phplist' ),
'id' => 'woolist_coupon_expiry_days',
'type' => 'number',
'default' => '30',
'custom_attributes' => [ 'min' => '0' ],
'desc_tip' => true,
],
[
'title' => __( 'Thank-you message', 'woolist-phplist' ),
'desc' => __( 'Message shown after signup. Use {coupon} as a placeholder for the coupon code.', 'woolist-phplist' ),
'id' => 'woolist_newsletter_thankyou',
'type' => 'textarea',
'default' => __( 'Thank you for subscribing! Use coupon {coupon} for 10% off your first order.', 'woolist-phplist' ),
'css' => 'width:100%;height:80px;',
'desc_tip' => true,
],
[
'type' => 'sectionend',
'id' => 'woolist_section_newsletter',
],
// ── Section 6: Logging ───────────────────────────────────────────
[
'title' => __( 'Logging', 'woolist-phplist' ),
'type' => 'title',
'desc' => __( 'Logs are written to WooCommerce → Status → Logs (source: woolist-phplist).', 'woolist-phplist' ),
'id' => 'woolist_section_logging',
],
[
'title' => __( 'Enable debug logging', 'woolist-phplist' ),
'desc' => __( 'Log full API request URLs (password redacted) and raw responses. Useful for diagnosing API issues; disable on production once confirmed working.', 'woolist-phplist' ),
'id' => 'woolist_enable_debug_log',
'type' => 'checkbox',
'default' => 'no',
],
[
'type' => 'sectionend',
'id' => 'woolist_section_logging',
],
];
}
}