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 <noreply@anthropic.com>
This commit is contained in:
88
woolist-phplist/assets/css/woolist-public.css
Normal file
88
woolist-phplist/assets/css/woolist-public.css
Normal file
@@ -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%;
|
||||
}
|
||||
}
|
||||
69
woolist-phplist/assets/js/woolist-public.js
Normal file
69
woolist-phplist/assets/js/woolist-public.js
Normal file
@@ -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 );
|
||||
341
woolist-phplist/includes/class-woolist-admin.php
Normal file
341
woolist-phplist/includes/class-woolist-admin.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Settings tab for WooList — phpList Integration.
|
||||
*
|
||||
* @package WooList
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WooList_Admin {
|
||||
|
||||
/** @var WooList_API */
|
||||
private WooList_API $api;
|
||||
|
||||
public function __construct( WooList_API $api ) {
|
||||
$this->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(
|
||||
'<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></p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
195
woolist-phplist/includes/class-woolist-api.php
Normal file
195
woolist-phplist/includes/class-woolist-api.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
/**
|
||||
* phpList REST API wrapper.
|
||||
*
|
||||
* @package WooList
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WooList_API {
|
||||
|
||||
/**
|
||||
* Retrieve a saved plugin option.
|
||||
*
|
||||
* @param string $key Option key (without prefix).
|
||||
* @param mixed $default Default value.
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_option( string $key, $default = '' ) {
|
||||
return get_option( 'woolist_' . $key, $default );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full phpList REST API URL.
|
||||
*
|
||||
* All parameters are passed as query-string arguments because the phpList
|
||||
* REST API reads them from $_GET regardless of the HTTP method used.
|
||||
*
|
||||
* @param string $cmd phpList API command.
|
||||
* @param array $extra Additional query parameters.
|
||||
* @return string|WP_Error Full URL or WP_Error when base URL is missing.
|
||||
*/
|
||||
public function build_url( string $cmd, array $extra = [] ) {
|
||||
$base = $this->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' );
|
||||
}
|
||||
}
|
||||
104
woolist-phplist/includes/class-woolist-hooks.php
Normal file
104
woolist-phplist/includes/class-woolist-hooks.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce and WordPress action hooks for WooList.
|
||||
*
|
||||
* @package WooList
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WooList_Hooks {
|
||||
|
||||
/** @var WooList_API */
|
||||
private WooList_API $api;
|
||||
|
||||
public function __construct( WooList_API $api ) {
|
||||
$this->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 );
|
||||
}
|
||||
}
|
||||
201
woolist-phplist/includes/class-woolist-shortcode.php
Normal file
201
woolist-phplist/includes/class-woolist-shortcode.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
/**
|
||||
* [woolist_newsletter] shortcode and AJAX handler.
|
||||
*
|
||||
* @package WooList
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WooList_Shortcode {
|
||||
|
||||
/** @var WooList_API */
|
||||
private WooList_API $api;
|
||||
|
||||
public function __construct( WooList_API $api ) {
|
||||
$this->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 '<!-- WooList: newsletter shortcode is disabled in settings -->';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="woolist-newsletter-wrap">
|
||||
<form id="woolist-newsletter-form" novalidate>
|
||||
<div class="woolist-form-row">
|
||||
<input
|
||||
type="email"
|
||||
name="woolist_email"
|
||||
class="woolist-email-input"
|
||||
placeholder="<?php esc_attr_e( 'Your email address', 'woolist-phplist' ); ?>"
|
||||
required
|
||||
/>
|
||||
<button type="submit" class="woolist-submit-btn">
|
||||
<?php esc_html_e( 'Subscribe', 'woolist-phplist' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="woolist-response" style="display:none;" aria-live="polite"></div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX newsletter subscription submission.
|
||||
*/
|
||||
public function handle_ajax(): void {
|
||||
// 1. Verify nonce.
|
||||
if ( ! check_ajax_referer( 'woolist_newsletter_nonce', 'nonce', false ) ) {
|
||||
wp_send_json_error( [ 'message' => __( '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;
|
||||
}
|
||||
}
|
||||
57
woolist-phplist/woolist-phplist.php
Normal file
57
woolist-phplist/woolist-phplist.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: WooList — phpList Integration
|
||||
* Plugin URI: https://github.com/woolist/woolist-phplist
|
||||
* Description: Sync WooCommerce customers and newsletter signups to phpList.
|
||||
* Version: 1.0.0
|
||||
* Author: WooList
|
||||
* Text Domain: woolist-phplist
|
||||
* Domain Path: /languages
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* Requires Plugins: woocommerce
|
||||
*
|
||||
* @package WooList
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
// Plugin constants.
|
||||
define( 'WOOLIST_VERSION', '1.0.0' );
|
||||
define( 'WOOLIST_PATH', plugin_dir_path( __FILE__ ) );
|
||||
define( 'WOOLIST_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
/**
|
||||
* Check that WooCommerce is active; show admin notice if not.
|
||||
*/
|
||||
function woolist_check_woocommerce() {
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
add_action( 'admin_notices', function () {
|
||||
echo '<div class="notice notice-error"><p>'
|
||||
. esc_html__( 'WooList — phpList Integration requires WooCommerce to be installed and active.', 'woolist-phplist' )
|
||||
. '</p></div>';
|
||||
} );
|
||||
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' );
|
||||
Reference in New Issue
Block a user