Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added
- Added WooCommerce integration with product and cart synchronization

### Changed
- Changed plugin to sync subscribers using background tasks avoiding errors if API isn't available

Expand Down
36 changes: 36 additions & 0 deletions css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,39 @@
padding: 16px 8px;
}
}

/* Sync Status Box Styling */
.mailrelay-sync-status {
margin: 20px 0;
display: flex;
flex-wrap: wrap;
}

.mailrelay-sync-box {
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
min-width: 180px;
margin-bottom: 20px;
transition: all 0.3s ease;
}

.mailrelay-sync-box:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #0073aa;
}

.mailrelay-sync-count {
font-size: 32px;
font-weight: bold;
color: #23282d;
margin: 5px 0;
}

.mailrelay-sync-label {
color: #646970;
font-size: 14px;
}
8 changes: 8 additions & 0 deletions inc/class-mailrelay-api-request-exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

if ( ! defined( 'ABSPATH' ) ) {
die( 'Access Denied.' );
}

class MailrelayApiRequestException extends Exception {
}
151 changes: 151 additions & 0 deletions inc/class-mailrelay-pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public function render_admin_page() {
$response = $this->process_save_settings();
} elseif ( 'mailrelay_manual_sync' === $_POST['action'] ) {
$response = $this->process_manual_sync();
} elseif ( 'mailrelay_save_woocommerce_sync_settings' === $_POST['action'] ) {
$response = $this->process_save_woocommerce_sync_settings();
} elseif ( 'mailrelay_manual_woocommerce_sync' === $_POST['action'] ) {
$response = $this->process_manual_woocommerce_sync();
} elseif ( 'mailrelay_connect_woocommerce_store' === $_POST['action'] ) {
$response = $this->process_connect_woocommerce_store();
}

if ( isset( $response ) ) {
Expand All @@ -79,6 +85,8 @@ public function render_admin_page() {
$authenticated = false;
}

$display_woocommerce_tab = ! $disconnected && mailrelay_woo_commerce_installed();

$tab = isset( $_GET['tab'] ) ? wp_unslash( $_GET['tab'] ) : $default_tab; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
?>

Expand All @@ -102,6 +110,9 @@ public function render_admin_page() {
<a href="?page=mailrelay&tab=Authentication" class="nav-tab <?php echo ( $authenticated && 'Authentication' !== $tab ) ? 'hidden' : ''; ?> <?php echo ( 'Authentication' === $tab ? 'nav-tab-active' : '' ); ?>"><?php esc_html_e( 'Authentication', 'mailrelay' ); ?></a>
<a href="?page=mailrelay&tab=Settings" class="nav-tab <?php echo ( $disconnected ? 'hidden' : '' ); ?> <?php echo ( 'Settings' === $tab ? 'nav-tab-active' : '' ); ?>"><?php esc_html_e( 'Settings', 'mailrelay' ); ?></a>
<a href="?page=mailrelay&tab=Manual" class="nav-tab <?php echo ( $disconnected ? 'hidden' : '' ); ?> <?php echo ( 'Manual' === $tab ? 'nav-tab-active' : '' ); ?>"><?php esc_html_e( 'Manual Sync', 'mailrelay' ); ?></a>
<?php if ( $display_woocommerce_tab ) : ?>
<a href="?page=mailrelay&tab=WooCommerce" class="nav-tab <?php echo ( 'WooCommerce' === $tab ? 'nav-tab-active' : '' ); ?>"><?php esc_html_e( 'WooCommerce Sync', 'mailrelay' ); ?></a>
<?php endif; ?>
</nav>

<div class="tab-content">
Expand All @@ -122,6 +133,11 @@ public function render_admin_page() {
$link = admin_url( '/admin.php?page=mailrelay&tab=Authentication' );
include_once __DIR__ . '/partials/tab-settings.php';
break;

case 'WooCommerce':
$this->setup_woocommerce_page_fields();
include_once __DIR__ . '/partials/tab-woocommerce.php';
break;
endswitch;
?>
</div>
Expand Down Expand Up @@ -490,4 +506,139 @@ public function process_manual_sync() {
'message' => $message,
);
}

public function setup_woocommerce_page_fields() {
add_settings_section(
'mailrelay_woocommerce_sync_section', // id
'', // title
function () { }, // callback
'mailrelay-woocommerce-sync-page' // page
);

$store_id = MailrelayWoocommerce::instance()->get_store_id();

if ( $store_id ) {
add_settings_field(
'woocommerce_auto_sync', // id
__( 'Automatically sync WooCommerce with Mailrelay', 'mailrelay' ), // title
array( $this, 'woocommerce_auto_sync_callback' ), // callback
'mailrelay-woocommerce-sync-page', // page
'mailrelay_woocommerce_sync_section'// section
);

add_settings_field(
'action', // id
'', // title
array( $this, 'action_woocommerce_settings_callback' ), // callback
'mailrelay-woocommerce-sync-page', // page
'mailrelay_woocommerce_sync_section', // section
array(
'class' => 'hidden',
)
);
}
}

public function action_woocommerce_settings_callback() {
printf(
'<input type="hidden" name="action" value="mailrelay_save_woocommerce_sync_settings" />'
);
}

public function woocommerce_auto_sync_callback() {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: Nonce verification happens at render_admin_page.
$value = isset( $_POST['mailrelay_woocommerce_auto_sync'] ) ? filter_var( wp_unslash( $_POST['mailrelay_woocommerce_auto_sync'] ), FILTER_SANITIZE_NUMBER_INT ) : get_option( 'mailrelay_woocommerce_auto_sync' );
?>
<input type="checkbox" name="mailrelay_woocommerce_auto_sync" id="mailrelay_woocommerce_auto_sync" value="1" <?php checked( 1, $value ); ?>/>
<p class="description"><?php esc_html_e( 'When enabled, products, orders and carts will be automatically synced to Mailrelay.', 'mailrelay' ); ?></p>
<?php
}

public function process_save_woocommerce_sync_settings() {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: Nonce verification happens at render_admin_page.
$auto_sync = isset( $_POST['mailrelay_woocommerce_auto_sync'] ) ? filter_var( wp_unslash( $_POST['mailrelay_woocommerce_auto_sync'] ), FILTER_SANITIZE_NUMBER_INT ) : 0;
update_option( 'mailrelay_woocommerce_auto_sync', $auto_sync );

if ( $auto_sync ) {
MailrelayWoocommerce::instance()->enqueue_sync_all_products();
}

return array(
'valid' => true,
'message' => __( 'WooCommerce Sync settings saved successfully.', 'mailrelay' ),
);
}

public function process_connect_woocommerce_store() {
// First, try to find a store with the same URL as the current site
$site_url = home_url();

$query_parameters = array(
'q' => array(
'url_eq' => $site_url,
),
);

$response = mailrelay_api_request( 'GET', 'ecommerce/stores?' . http_build_query( $query_parameters ) );

if ( $response['wp_error'] || 200 !== $response['code'] ) {
return array(
'valid' => false,
'message' => __( 'Failed to connect to Mailrelay API. Please try again.', 'mailrelay' ),
);
}

$existing_store = $response['body'][0];

if ( $existing_store ) {
// Store found, save the ID
update_option( 'mailrelay_woocommerce_store_id', $existing_store['id'] );
return array(
'valid' => true,
'message' => __( 'Successfully connected to existing store in Mailrelay.', 'mailrelay' ),
);
} else {
// Create a new store
$store_data = array(
'name' => get_bloginfo( 'name' ) . ' - WooCommerce',
'url' => $site_url,
'currency' => get_woocommerce_currency(),
);

$response = mailrelay_api_request(
'POST',
'ecommerce/stores',
array(
'body' => wp_json_encode( $store_data ),
'headers' => array( 'content-type' => 'application/json' ),
)
);

if ( $response['wp_error'] || ( 200 !== $response['code'] && 201 !== $response['code'] ) ) {
return array(
'valid' => false,
'message' => __( 'Failed to create store in Mailrelay. Please try again.', 'mailrelay' ),
);
}

update_option( 'mailrelay_woocommerce_store_id', $response['body']['id'] );
update_option( 'mailrelay_woocommerce_auto_sync', 1 );

MailrelayWoocommerce::instance()->enqueue_sync_all_products();

return array(
'valid' => true,
'message' => __( 'Successfully created and connected to a new store in Mailrelay.', 'mailrelay' ),
);
}
}

public function process_manual_woocommerce_sync() {
MailrelayWoocommerce::instance()->enqueue_sync_all_products();

return array(
'valid' => true,
'message' => __( 'Products are being synced to Mailrelay. It may take a few minutes to complete.', 'mailrelay' ),
);
}
}
52 changes: 52 additions & 0 deletions inc/class-mailrelay-woocommerce-all-products.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

if ( ! defined( 'ABSPATH' ) ) {
die( 'Access Denied.' );
}

class MailrelayWoocommerceAllProducts {
/**
* Batch size for processing products
*/
const BATCH_SIZE = 100;

/**
* Sync products in batches to avoid memory issues
*
* @param int $page Current page number.
*/
public function sync( $page = 1 ) {
$args = array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => self::BATCH_SIZE,
'paged' => $page,
'fields' => 'ids',
'no_found_rows' => false, // We need total count for pagination
);

$query = new WP_Query( $args );
$products = $query->posts;

// Queue individual product sync jobs
foreach ( $products as $product_id ) {
as_enqueue_async_action(
'mailrelay_sync_product_background',
array( $product_id ),
'mailrelay'
);
}

// If there are more pages, schedule the next batch
if ( $page < $query->max_num_pages ) {
as_enqueue_async_action(
'mailrelay_sync_all_products_background',
array( $page + 1 ),
'mailrelay'
);
}

// Clean up memory
wp_reset_postdata();
}
}
85 changes: 85 additions & 0 deletions inc/class-mailrelay-woocommerce-cart.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

if ( ! defined( 'ABSPATH' ) ) {
die( 'Access Denied.' );
}

require_once __DIR__ . '/class-mailrelay-woocommerce-helper.php';
require_once __DIR__ . '/class-mailrelay-api-request-exception.php';

class MailrelayWoocommerceCart {
public function sync( array $cart_data ) {
if ( empty( $cart_data['email'] ) ) {
return;
}

$data = $this->transform( $cart_data );

$store_id = MailrelayWoocommerce::instance()->get_store_id();
$endpoint = "ecommerce/stores/{$store_id}/carts/sync";

$response = mailrelay_api_request(
'POST',
$endpoint,
array(
'body' => wp_json_encode( $data ),
'headers' => array( 'content-type' => 'application/json' ),
)
);

if ( $response['wp_error'] || ( isset( $response['code'] ) && $response['code'] >= 500 ) ) {
throw new MailrelayApiRequestException( 'Mailrelay API request failed, will retry.' );
}

return $response;
}

public function set_completed( $cart_id, $email ) {
$store_id = MailrelayWoocommerce::instance()->get_store_id();
$endpoint = "ecommerce/stores/{$store_id}/carts/{$cart_id}";

$data = array(
'email' => $email,
'status' => 'completed',
);

$response = mailrelay_api_request(
'PATCH',
$endpoint,
array(
'body' => wp_json_encode( $data ),
'headers' => array( 'content-type' => 'application/json' ),
)
);

if ( $response['wp_error'] || ( isset( $response['code'] ) && $response['code'] >= 500 ) ) {
throw new MailrelayApiRequestException( 'Mailrelay API request failed, will retry.' );
}

return $response;
}

public function transform( array $cart_data ) {
$cart_items_attributes = array();
if ( ! empty( $cart_data['cart_items'] ) ) {
foreach ( $cart_data['cart_items'] as $cart_item_key => $cart_item ) {
$cart_items_attributes[] = array(
'cart_item_id' => $cart_item_key,
'product_variant_id' => (string) ( ! empty( $cart_item['variation_id'] ) ? $cart_item['variation_id'] : $cart_item['product_id'] ),
'quantity' => $cart_item['quantity'],
'price' => MailrelayWoocommerceHelper::transform_price( $cart_item['product_price'] ),
);
}
}

return array(
'cart_id' => $cart_data['cart_id'],
'status' => 'pending',
'checkout_url' => $cart_data['checkout_url'],
'total' => MailrelayWoocommerceHelper::transform_price( $cart_data['cart_total'] ),
'tax_total' => MailrelayWoocommerceHelper::transform_price( $cart_data['cart_tax_total'] ),
'email' => $cart_data['email'],
'cart_items_attributes' => $cart_items_attributes,
);
}
}
10 changes: 10 additions & 0 deletions inc/class-mailrelay-woocommerce-helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
die( 'Access Denied.' );
}

class MailrelayWoocommerceHelper {
public static function transform_price( $price ) {
return (int) round( floatval( $price ) * 100 );
}
}
Loading
Loading