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( '

%s

', esc_attr( $type ), esc_html( $notice['message'] ) ); } echo '

' . esc_html__( 'Test Connection', 'woolist-phplist' ) . ''; echo ' ' . esc_html__( 'Check WooCommerce → Status → Logs (source: woolist-phplist) to see results.', 'woolist-phplist' ) . '

'; } 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 '

' . esc_html__( 'Could not load order.', 'woolist-phplist' ) . '

'; 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 '

' . esc_html__( 'No phpList lists are configured yet. Add list IDs under WooCommerce → Settings → phpList.', 'woolist-phplist' ) . '

'; return; } $nonce = wp_create_nonce( 'woolist_manual_subscribe_' . $order_id ); ?>

human label ] for all lists * that have an ID configured in settings, regardless of whether sync is enabled. * * @return array */ 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', ], ]; } }