commit 6e23e40bf38d3bec660154e3a48543616efd3a53 Author: Malin Date: Thu Feb 19 11:51:12 2026 +0100 feat: initial implementation of WooList phpList Integration plugin v1.0.0 - phpList REST API wrapper with subscriber get-or-create + list assignment - WooCommerce Settings tab (5 sections: connection, orders, signup, newsletter) - Test Connection button via admin-post action - Hooks for order completed/cancelled and user_register events - [woolist_newsletter] shortcode with jQuery AJAX, fixed & auto-generated coupons - Responsive front-end form styles and JS with loading/success/error states Co-Authored-By: Claude Sonnet 4.6 diff --git a/woolist-phplist/assets/css/woolist-public.css b/woolist-phplist/assets/css/woolist-public.css new file mode 100644 index 0000000..6632d21 --- /dev/null +++ b/woolist-phplist/assets/css/woolist-public.css @@ -0,0 +1,88 @@ +/* ========================================================================== + WooList — Newsletter Form Styles + ========================================================================== */ + +.woolist-newsletter-wrap { + max-width: 480px; + margin: 1.5em auto; + font-family: inherit; +} + +/* Form row: input + button side by side */ +.woolist-form-row { + display: flex; + gap: 0.5em; + flex-wrap: wrap; +} + +.woolist-email-input { + flex: 1 1 auto; + min-width: 0; + padding: 0.65em 0.9em; + font-size: 1em; + border: 1px solid #ccc; + border-radius: 4px; + outline: none; + transition: border-color 0.2s ease; + box-sizing: border-box; +} + +.woolist-email-input:focus { + border-color: #0071a1; + box-shadow: 0 0 0 2px rgba(0, 113, 161, 0.15); +} + +.woolist-submit-btn { + flex: 0 0 auto; + padding: 0.65em 1.4em; + font-size: 1em; + font-weight: 600; + color: #fff; + background-color: #0071a1; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease, opacity 0.2s ease; + white-space: nowrap; +} + +.woolist-submit-btn:hover { + background-color: #005f8a; +} + +.woolist-submit-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Response message area */ +.woolist-response { + margin-top: 0.85em; + padding: 0.75em 1em; + border-radius: 4px; + font-size: 0.95em; + line-height: 1.5; +} + +.woolist-response.woolist-success { + background-color: #edfaf1; + border: 1px solid #52c41a; + color: #2d6a0f; +} + +.woolist-response.woolist-error { + background-color: #fff1f0; + border: 1px solid #ff4d4f; + color: #a8071a; +} + +/* Responsive: stack on very small screens */ +@media (max-width: 360px) { + .woolist-form-row { + flex-direction: column; + } + + .woolist-submit-btn { + width: 100%; + } +} diff --git a/woolist-phplist/assets/js/woolist-public.js b/woolist-phplist/assets/js/woolist-public.js new file mode 100644 index 0000000..0275e60 --- /dev/null +++ b/woolist-phplist/assets/js/woolist-public.js @@ -0,0 +1,69 @@ +/* global woolist, jQuery */ +( function ( $ ) { + 'use strict'; + + $( document ).on( 'submit', '#woolist-newsletter-form', function ( e ) { + e.preventDefault(); + + var $form = $( this ); + var $wrap = $form.closest( '.woolist-newsletter-wrap' ); + var $btn = $form.find( '.woolist-submit-btn' ); + var $email = $form.find( '[name="woolist_email"]' ); + var $resp = $wrap.find( '.woolist-response' ); + + // Clear previous response state. + $resp.hide().removeClass( 'woolist-success woolist-error' ).html( '' ); + + var email = $email.val().trim(); + if ( ! email ) { + showResponse( $resp, woolist.i18n.error, false ); + return; + } + + // Loading state. + var originalLabel = $btn.text(); + $btn.prop( 'disabled', true ).text( woolist.i18n.subscribing ); + + $.ajax( { + url: woolist.ajaxurl, + method: 'POST', + data: { + action: 'woolist_newsletter_submit', + nonce: woolist.nonce, + woolist_email: email, + }, + } ) + .done( function ( response ) { + if ( response.success && response.data && response.data.message ) { + // Hide the form, show the success message. + $form.slideUp( 200 ); + showResponse( $resp, response.data.message, true ); + } else { + var msg = ( response.data && response.data.message ) + ? response.data.message + : woolist.i18n.error; + showResponse( $resp, msg, false ); + $btn.prop( 'disabled', false ).text( originalLabel ); + } + } ) + .fail( function () { + showResponse( $resp, woolist.i18n.error, false ); + $btn.prop( 'disabled', false ).text( originalLabel ); + } ); + } ); + + /** + * Display a response message in the response container. + * + * @param {jQuery} $el The response element. + * @param {string} message HTML or text message to display. + * @param {boolean} success Whether this is a success message. + */ + function showResponse( $el, message, success ) { + $el + .addClass( success ? 'woolist-success' : 'woolist-error' ) + .html( message ) + .slideDown( 200 ); + } + +} )( jQuery ); diff --git a/woolist-phplist/includes/class-woolist-admin.php b/woolist-phplist/includes/class-woolist-admin.php new file mode 100644 index 0000000..8cf4038 --- /dev/null +++ b/woolist-phplist/includes/class-woolist-admin.php @@ -0,0 +1,341 @@ +api = $api; + + // Register the custom 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' ] ); + + // Enqueue admin JS/CSS only on the WC settings page. + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + } + + /** + * Add "phpList" tab to WooCommerce → Settings. + * + * @param array $tabs Existing tabs. + * @return array + */ + public function add_settings_tab( array $tabs ): array { + $tabs['woolist'] = __( 'phpList', 'woolist-phplist' ); + return $tabs; + } + + /** + * Render the settings tab content. + */ + public function render_settings(): void { + woocommerce_admin_fields( $this->get_settings() ); + $this->render_test_connection_button(); + } + + /** + * Save settings when the form is submitted. + */ + public function save_settings(): void { + woocommerce_update_options( $this->get_settings() ); + } + + /** + * Output the "Test Connection" button below the settings form. + */ + private function render_test_connection_button(): void { + $action_url = wp_nonce_url( + admin_url( 'admin-post.php?action=woolist_test_connection' ), + 'woolist_test_connection' + ); + + // Display any stored transient notice. + $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' ) + . '

'; + } + + /** + * Handle the "Test Connection" form action. + */ + 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' ) ); + } + + $result = $this->api->lists_get(); + $user_id = get_current_user_id(); + + if ( is_wp_error( $result ) ) { + 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 ) : '?'; + 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; + } + + /** + * Enqueue admin-side assets on WC Settings → phpList tab. + */ + public function enqueue_admin_assets(): void { + $screen = get_current_screen(); + if ( ! $screen || strpos( $screen->id, 'woocommerce' ) === false ) { + return; + } + // No custom admin JS/CSS needed yet; WC handles the settings form. + } + + /** + * Build and return the WooCommerce settings field definitions. + * + * @return array + */ + 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', + ], + ]; + } +} diff --git a/woolist-phplist/includes/class-woolist-api.php b/woolist-phplist/includes/class-woolist-api.php new file mode 100644 index 0000000..cf42a38 --- /dev/null +++ b/woolist-phplist/includes/class-woolist-api.php @@ -0,0 +1,195 @@ +get_option( 'phplist_url' ); + $base = rtrim( $base, '/' ); + + if ( empty( $base ) ) { + return new WP_Error( 'woolist_no_url', __( 'phpList base URL is not configured.', 'woolist-phplist' ) ); + } + + $params = array_merge( + [ + 'page' => 'call', + 'pi' => 'restapi', + 'login' => $this->get_option( 'phplist_login' ), + 'password' => $this->get_option( 'phplist_password' ), + 'cmd' => $cmd, + ], + $extra + ); + + return $base . '/admin/?' . http_build_query( $params ); + } + + /** + * Execute an API call and return decoded JSON data. + * + * @param string $cmd phpList API command. + * @param array $extra Additional query parameters. + * @return array|WP_Error Decoded response data or WP_Error. + */ + public function call( string $cmd, array $extra = [] ) { + $url = $this->build_url( $cmd, $extra ); + + if ( is_wp_error( $url ) ) { + return $url; + } + + $response = wp_remote_post( $url, [ 'timeout' => 15 ] ); + + if ( is_wp_error( $response ) ) { + error_log( '[WooList] API request failed for cmd=' . $cmd . ': ' . $response->get_error_message() ); + return $response; + } + + $code = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body( $response ); + + if ( $code < 200 || $code >= 300 ) { + error_log( '[WooList] API returned HTTP ' . $code . ' for cmd=' . $cmd ); + return new WP_Error( 'woolist_http_error', 'HTTP error ' . $code ); + } + + $data = json_decode( $body, true ); + + if ( json_last_error() !== JSON_ERROR_NONE ) { + error_log( '[WooList] API returned invalid JSON for cmd=' . $cmd . ': ' . $body ); + return new WP_Error( 'woolist_json_error', 'Invalid JSON response from phpList.' ); + } + + // phpList REST API signals errors via a "status" field. + if ( isset( $data['status'] ) && strtolower( $data['status'] ) === 'error' ) { + $message = $data['errormessage'] ?? $data['message'] ?? 'Unknown API error'; + error_log( '[WooList] API error for cmd=' . $cmd . ': ' . $message ); + return new WP_Error( 'woolist_api_error', $message ); + } + + return $data; + } + + /** + * Retrieve a subscriber by email address. + * + * @param string $email Subscriber email. + * @return array|WP_Error + */ + public function subscriber_get_by_email( string $email ) { + return $this->call( 'subscriberGetByEmail', [ 'email' => rawurlencode( $email ) ] ); + } + + /** + * Add a new confirmed subscriber. + * + * @param string $email Subscriber email. + * @return array|WP_Error + */ + public function subscriber_add( string $email ) { + return $this->call( + 'subscriberAdd', + [ + 'email' => rawurlencode( $email ), + 'confirmed' => 1, + 'htmlemail' => 1, + ] + ); + } + + /** + * Add a subscriber (by ID) to a phpList list. + * + * @param int $list_id phpList list ID. + * @param int $subscriber_id phpList subscriber ID. + * @return array|WP_Error + */ + public function list_subscriber_add( int $list_id, int $subscriber_id ) { + return $this->call( + 'listSubscriberAdd', + [ + 'listid' => $list_id, + 'subscriberid' => $subscriber_id, + ] + ); + } + + /** + * High-level helper: subscribe an email to a list. + * + * Gets or creates the subscriber, then adds them to the list. + * + * @param string $email Subscriber email address. + * @param int $list_id phpList list ID. + * @return array{success: bool, subscriber_id: int|null} + */ + public function subscribe_email_to_list( string $email, int $list_id ): array { + // Try to find existing subscriber. + $subscriber_id = null; + $existing = $this->subscriber_get_by_email( $email ); + + if ( ! is_wp_error( $existing ) && ! empty( $existing['id'] ) ) { + $subscriber_id = (int) $existing['id']; + } else { + // Create new subscriber. + $added = $this->subscriber_add( $email ); + if ( is_wp_error( $added ) ) { + error_log( '[WooList] Could not add subscriber ' . $email . ': ' . $added->get_error_message() ); + return [ 'success' => false, 'subscriber_id' => null ]; + } + $subscriber_id = isset( $added['id'] ) ? (int) $added['id'] : null; + } + + if ( ! $subscriber_id ) { + error_log( '[WooList] Could not determine subscriber ID for ' . $email ); + return [ 'success' => false, 'subscriber_id' => null ]; + } + + // Add subscriber to the list. + $result = $this->list_subscriber_add( $list_id, $subscriber_id ); + + if ( is_wp_error( $result ) ) { + error_log( '[WooList] Could not add subscriber ' . $subscriber_id . ' to list ' . $list_id . ': ' . $result->get_error_message() ); + return [ 'success' => false, 'subscriber_id' => $subscriber_id ]; + } + + return [ 'success' => true, 'subscriber_id' => $subscriber_id ]; + } + + /** + * Retrieve all lists (used for connection testing). + * + * @return array|WP_Error + */ + public function lists_get() { + return $this->call( 'listsGet' ); + } +} diff --git a/woolist-phplist/includes/class-woolist-hooks.php b/woolist-phplist/includes/class-woolist-hooks.php new file mode 100644 index 0000000..b845a7f --- /dev/null +++ b/woolist-phplist/includes/class-woolist-hooks.php @@ -0,0 +1,104 @@ +api = $api; + + add_action( 'woocommerce_order_status_completed', [ $this, 'on_order_completed' ] ); + add_action( 'woocommerce_order_status_cancelled', [ $this, 'on_order_cancelled' ] ); + add_action( 'user_register', [ $this, 'on_user_register' ] ); + } + + /** + * Sync billing email to phpList when an order is marked completed. + * + * @param int $order_id WooCommerce order ID. + */ + public function on_order_completed( int $order_id ): void { + if ( get_option( 'woolist_sync_completed' ) !== 'yes' ) { + return; + } + + $list_id = (int) get_option( 'woolist_completed_list_id', 0 ); + if ( $list_id < 1 ) { + error_log( '[WooList] Completed order sync enabled but no list ID configured.' ); + return; + } + + $order = wc_get_order( $order_id ); + if ( ! $order ) { + return; + } + + $email = $order->get_billing_email(); + if ( ! is_email( $email ) ) { + return; + } + + $this->api->subscribe_email_to_list( $email, $list_id ); + } + + /** + * Sync billing email to phpList when an order is cancelled. + * + * @param int $order_id WooCommerce order ID. + */ + public function on_order_cancelled( int $order_id ): void { + if ( get_option( 'woolist_sync_cancelled' ) !== 'yes' ) { + return; + } + + $list_id = (int) get_option( 'woolist_cancelled_list_id', 0 ); + if ( $list_id < 1 ) { + error_log( '[WooList] Cancelled order sync enabled but no list ID configured.' ); + return; + } + + $order = wc_get_order( $order_id ); + if ( ! $order ) { + return; + } + + $email = $order->get_billing_email(); + if ( ! is_email( $email ) ) { + return; + } + + $this->api->subscribe_email_to_list( $email, $list_id ); + } + + /** + * Sync new user email to phpList when a WordPress account is created. + * + * @param int $user_id Newly registered user ID. + */ + public function on_user_register( int $user_id ): void { + if ( get_option( 'woolist_sync_signup' ) !== 'yes' ) { + return; + } + + $list_id = (int) get_option( 'woolist_signup_list_id', 0 ); + if ( $list_id < 1 ) { + error_log( '[WooList] Account signup sync enabled but no list ID configured.' ); + return; + } + + $user = get_userdata( $user_id ); + if ( ! $user || ! is_email( $user->user_email ) ) { + return; + } + + $this->api->subscribe_email_to_list( $user->user_email, $list_id ); + } +} diff --git a/woolist-phplist/includes/class-woolist-shortcode.php b/woolist-phplist/includes/class-woolist-shortcode.php new file mode 100644 index 0000000..bfd5817 --- /dev/null +++ b/woolist-phplist/includes/class-woolist-shortcode.php @@ -0,0 +1,201 @@ +api = $api; + + add_shortcode( 'woolist_newsletter', [ $this, 'render_shortcode' ] ); + + // Register AJAX handlers for logged-in and guest visitors. + add_action( 'wp_ajax_woolist_newsletter_submit', [ $this, 'handle_ajax' ] ); + add_action( 'wp_ajax_nopriv_woolist_newsletter_submit', [ $this, 'handle_ajax' ] ); + + // Enqueue front-end assets. + add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] ); + } + + /** + * Enqueue CSS and JS for the newsletter form. + */ + public function enqueue_assets(): void { + wp_enqueue_style( + 'woolist-public', + WOOLIST_URL . 'assets/css/woolist-public.css', + [], + WOOLIST_VERSION + ); + + wp_enqueue_script( + 'woolist-public', + WOOLIST_URL . 'assets/js/woolist-public.js', + [ 'jquery' ], + WOOLIST_VERSION, + true + ); + + wp_localize_script( + 'woolist-public', + 'woolist', + [ + 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'woolist_newsletter_nonce' ), + 'i18n' => [ + 'subscribing' => __( 'Subscribing…', 'woolist-phplist' ), + 'subscribe' => __( 'Subscribe', 'woolist-phplist' ), + 'error' => __( 'Something went wrong. Please try again.', 'woolist-phplist' ), + ], + ] + ); + } + + /** + * Render the [woolist_newsletter] shortcode. + * + * @return string HTML output. + */ + public function render_shortcode(): string { + if ( get_option( 'woolist_sync_newsletter' ) !== 'yes' ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG && current_user_can( 'manage_options' ) ) { + return ''; + } + return ''; + } + + ob_start(); + ?> +
+
+
+ + +
+
+ +
+ __( 'Security check failed. Please refresh and try again.', 'woolist-phplist' ) ], 403 ); + } + + // 2. Validate email. + $email = isset( $_POST['woolist_email'] ) ? sanitize_email( wp_unslash( $_POST['woolist_email'] ) ) : ''; + if ( ! is_email( $email ) ) { + wp_send_json_error( [ 'message' => __( 'Please enter a valid email address.', 'woolist-phplist' ) ], 400 ); + } + + // 3. Get list ID. + $list_id = (int) get_option( 'woolist_newsletter_list_id', 0 ); + if ( $list_id < 1 ) { + wp_send_json_error( [ 'message' => __( 'Newsletter is not configured. Please contact the site administrator.', 'woolist-phplist' ) ], 500 ); + } + + // 4. Subscribe email to phpList. + $result = $this->api->subscribe_email_to_list( $email, $list_id ); + if ( ! $result['success'] ) { + wp_send_json_error( [ 'message' => __( 'Could not subscribe your email. Please try again later.', 'woolist-phplist' ) ], 500 ); + } + + // 5. Handle coupon generation. + $coupon_code = ''; + $thankyou_msg = get_option( 'woolist_newsletter_thankyou', __( 'Thank you for subscribing!', 'woolist-phplist' ) ); + + if ( get_option( 'woolist_newsletter_enable_coupon' ) === 'yes' ) { + $coupon_mode = get_option( 'woolist_coupon_mode', 'fixed' ); + + if ( $coupon_mode === 'fixed' ) { + $coupon_code = sanitize_text_field( get_option( 'woolist_coupon_fixed_code', '' ) ); + } else { + $coupon_code = $this->generate_coupon( $email ); + } + } + + // Replace {coupon} placeholder in the thank-you message. + $thankyou_msg = str_replace( '{coupon}', esc_html( $coupon_code ), $thankyou_msg ); + + wp_send_json_success( + [ + 'message' => wp_kses_post( $thankyou_msg ), + 'coupon' => $coupon_code, + ] + ); + } + + /** + * Generate a unique WooCommerce percentage coupon for the subscriber. + * + * @param string $email Subscriber email. + * @return string Generated coupon code, or empty string on failure. + */ + private function generate_coupon( string $email ): string { + if ( ! class_exists( 'WC_Coupon' ) ) { + error_log( '[WooList] WC_Coupon class not available; cannot generate coupon.' ); + return ''; + } + + $discount_pct = (int) get_option( 'woolist_coupon_discount_pct', 10 ); + $expiry_days = (int) get_option( 'woolist_coupon_expiry_days', 30 ); + $coupon_code = 'WOOLIST-' . strtoupper( substr( md5( $email . time() ), 0, 8 ) ); + + $expiry_date = ''; + if ( $expiry_days > 0 ) { + $expiry_date = gmdate( 'Y-m-d', strtotime( '+' . $expiry_days . ' days' ) ); + } + + $post_id = wp_insert_post( + [ + 'post_title' => $coupon_code, + 'post_name' => $coupon_code, + 'post_status' => 'publish', + 'post_type' => 'shop_coupon', + 'post_excerpt' => 'WooList newsletter signup coupon for ' . $email, + ], + true + ); + + if ( is_wp_error( $post_id ) ) { + error_log( '[WooList] Failed to create coupon post: ' . $post_id->get_error_message() ); + return ''; + } + + // Set coupon meta via WC functions. + update_post_meta( $post_id, 'discount_type', 'percent' ); + update_post_meta( $post_id, 'coupon_amount', (string) $discount_pct ); + update_post_meta( $post_id, 'usage_limit', '1' ); + update_post_meta( $post_id, 'usage_limit_per_user', '1' ); + update_post_meta( $post_id, 'individual_use', 'yes' ); + update_post_meta( $post_id, 'customer_email', [ $email ] ); + + if ( $expiry_date ) { + update_post_meta( $post_id, 'date_expires', strtotime( $expiry_date ) ); + } + + return $coupon_code; + } +} diff --git a/woolist-phplist/woolist-phplist.php b/woolist-phplist/woolist-phplist.php new file mode 100644 index 0000000..ceaffd2 --- /dev/null +++ b/woolist-phplist/woolist-phplist.php @@ -0,0 +1,57 @@ +

' + . esc_html__( 'WooList — phpList Integration requires WooCommerce to be installed and active.', 'woolist-phplist' ) + . '

'; + } ); + return false; + } + return true; +} + +/** + * Bootstrap the plugin after all plugins are loaded. + */ +function woolist_init() { + if ( ! woolist_check_woocommerce() ) { + return; + } + + require_once WOOLIST_PATH . 'includes/class-woolist-api.php'; + require_once WOOLIST_PATH . 'includes/class-woolist-admin.php'; + require_once WOOLIST_PATH . 'includes/class-woolist-hooks.php'; + require_once WOOLIST_PATH . 'includes/class-woolist-shortcode.php'; + + $api = new WooList_API(); + new WooList_Admin( $api ); + new WooList_Hooks( $api ); + new WooList_Shortcode( $api ); +} +add_action( 'plugins_loaded', 'woolist_init' );