diff --git a/.wp-env.json b/.wp-env.json
index c59ed228..d4de68e9 100644
--- a/.wp-env.json
+++ b/.wp-env.json
@@ -10,5 +10,9 @@
},
"lifecycleScripts": {
"afterStart": "bash ./tests/bin/initialize.sh"
+ },
+ "config": {
+ "ALTERNATE_WP_CRON": true,
+ "WP_AUTO_UPDATE_CORE": false
}
}
diff --git a/assets/css/admin.css b/assets/css/admin.css
index 5e05be8b..4f1b1840 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -19,7 +19,8 @@
table.mc-user,
.mc-list-row,
.mc-list-note,
-.mc-section {
+.mc-section,
+.mailchimp-sf-nav-tab-wrapper {
max-width: 900px;
width: 100%;
}
@@ -38,17 +39,23 @@ table.mc-user,
}
/* Sections */
-table.mc-widefat {
+table.mc-widefat, .mailchimp-sf-user-sync-page table.form-table {
background: var(--mailchimp-color-white);
border: 2px solid var(--mailchimp-color-header-bg) !important;
border-radius: 6px;
margin: 2.75rem 0 2.25rem;
}
+.mailchimp-sf-user-sync-page table.form-table {
+ display: block;
+}
+
+.mailchimp-sf-user-sync-page table.form-table tr:first-child,
table.mc-widefat tr:first-child {
background: var(--mailchimp-color-header-bg);
}
+.mailchimp-sf-user-sync-page table.form-table tr:first-child th,
table.mc-widefat tr:first-child th {
color: var(--mailchimp-color-text);
font-weight: 500;
@@ -181,6 +188,8 @@ table.mc-list-select {
}
/* Table */
+.mailchimp-sf-user-sync-page table.form-table td,
+.mailchimp-sf-user-sync-page table.form-table th,
table.mc-widefat td,
table.mc-widefat th {
padding: 18px;
@@ -188,6 +197,8 @@ table.mc-widefat th {
text-shadow: none;
}
+.mailchimp-sf-user-sync-page table.form-table .last-row td,
+.mailchimp-sf-user-sync-page table.form-table .last-row th,
table.mc-widefat .last-row td,
table.mc-widefat .last-row th {
border-bottom: none !important;
@@ -199,6 +210,11 @@ table.mc-widefat th {
width: 130px;
}
+.mailchimp-sf-user-sync-page table.form-table th {
+ color: var(--mailchimp-color-text-light);
+ font-weight: 500;
+}
+
table.mc-widefat td label {
display: block;
font-size: 0.75rem;
@@ -213,6 +229,7 @@ table.mc-widefat td {
line-height: 1.125 !important;
}
+.mailchimp-sf-user-sync-page table.form-table td input,
table.mc-widefat td input {
display: inline-block;
font-style: normal;
@@ -261,6 +278,31 @@ th.mailchimp-connect {
margin-top: 26px;
}
+/**
+ * Navigation
+ */
+.mailchimp-sf-nav-tab-wrapper {
+ margin-top: 1em;
+}
+
+.mailchimp-sf-nav-tab-wrapper a.nav-tab {
+ border: 0px;
+ background: transparent;
+ color: #000;
+ margin-left: 0;
+ font-weight: 500;
+ padding: 5px 12px;
+}
+
+.mailchimp-sf-nav-tab-wrapper a.nav-tab:hover {
+ color: var(--mailchimp-color-link);
+}
+
+.mailchimp-sf-nav-tab-wrapper a.nav-tab.nav-tab-active {
+ border-bottom: 2px solid var(--mailchimp-color-link);
+ color: var(--mailchimp-color-link);
+}
+
/**
* Mailchimp OAuth CSS
*/
@@ -390,6 +432,11 @@ body.toplevel_page_mailchimp_sf_options a {
color: var(--mailchimp-color-link);
}
+body.admin_page_mailchimp_sf_create_account a:hover,
+body.toplevel_page_mailchimp_sf_options a:hover {
+ color: #006570;
+}
+
body.admin_page_mailchimp_sf_create_account #footer-upgrade,
body.toplevel_page_mailchimp_sf_options #footer-upgrade {
display: none;
@@ -516,7 +563,8 @@ body.toplevel_page_mailchimp_sf_options #footer-upgrade {
cursor: not-allowed;
}
-.button.mailchimp-sf-button.small {
+.button.mailchimp-sf-button.small,
+.button.mailchimp-sf-button.user-sync-settings-submit {
padding: 8px 16px;
line-height: 14px;
float: right;
@@ -749,3 +797,97 @@ body.toplevel_page_mailchimp_sf_options #footer-upgrade {
column-gap: 16px;
}
}
+
+.mailchimp-sf-user-sync-page {
+ max-width: 900px;
+}
+
+.mailchimp-sf-user-sync-page .subscribe_status_label {
+ font-weight: 500;
+}
+
+.mailchimp-sf-user-sync-page p.description_small {
+ font-size: 0.9em;
+ margin-bottom: 10px;
+}
+
+.mailchimp-sf-user-sync-status {
+ margin: 15px 0;
+ padding: 10px;
+ background: #fff;
+ border: 1px solid #ccd0d4;
+ border-radius: 4px;
+}
+.mailchimp-sf-user-sync-status .mailchimp-sf-sync-progress {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+}
+.mailchimp-sf-user-sync-status .sync-status-text {
+ font-size: 14px;
+ line-height: 1.4;
+}
+
+.button.mailchimp-cancel-user-sync-button {
+ margin-left: auto;
+}
+
+@media screen and (max-width: 480px) {
+ .mailchimp-sf-user-sync-status .mailchimp-sf-sync-progress {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .button.mailchimp-cancel-user-sync-button {
+ margin-left: 0px;
+ margin-top: 10px;
+ }
+}
+
+.mailchimp-sf-user-sync-errors {
+ margin-top: 2rem;
+}
+
+.mailchimp-sf-user-sync-errors-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.mailchimp-sf-user-sync-error-action {
+ min-width: 120px;
+ text-align: right;
+}
+
+.mailchimp-sf-start-user-sync-wrapper {
+ width: 100%;
+ margin-top: 2em;
+ background-color: #ffffff;
+ border: 1px solid #ccd0d4;
+ border-radius: 6px;
+}
+
+.mailchimp-sf-start-user-sync-box {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+}
+
+.mailchimp-sf-start-user-sync-box h2 {
+ margin: 0;
+}
+
+.mailchimp-sf-start-user-sync-box p {
+ margin-top: 0.5rem;
+}
+
+.mailchimp-sf-start-user-sync-box .mailchimp-sf-button {
+ float: none;
+ margin-right: 8px;
+}
+
+.mailchimp-sf-start-user-sync-box a {
+ text-decoration: none;
+}
+
diff --git a/assets/js/admin.js b/assets/js/admin.js
index 663c6ad7..bbcba3fa 100644
--- a/assets/js/admin.js
+++ b/assets/js/admin.js
@@ -414,3 +414,143 @@
});
});
})(jQuery); // eslint-disable-line no-undef
+
+// User Sync Settings.
+(function ($) {
+ const userSyncSettingsPage = $('.mailchimp-sf-user-sync-page');
+ if (userSyncSettingsPage.length > 0) {
+ const syncExistingContactsOnly = $(
+ 'tr.mailchimp-user-sync-existing-contacts-only input[type="checkbox"]',
+ );
+ if (syncExistingContactsOnly) {
+ syncExistingContactsOnly.change(function () {
+ if (this.checked) {
+ $('tr.mailchimp-user-sync-subscriber-status').hide();
+ } else {
+ $('tr.mailchimp-user-sync-subscriber-status').show();
+ }
+ });
+
+ // Trigger change event to hide/show subscriber status.
+ syncExistingContactsOnly.trigger('change');
+ }
+ }
+})(jQuery); // eslint-disable-line no-undef
+
+// Update the user sync status.
+(function ($) {
+ const statusWrapper = $('.mailchimp-sf-user-sync-status');
+ const processRunning = statusWrapper.length;
+ if (!processRunning) {
+ return;
+ }
+
+ const params = window.mailchimp_sf_admin_params || {};
+ const ajaxUrl = params.ajax_url;
+ const ajaxNonce = params.user_sync_status_nonce;
+
+ const intervalId = setInterval(function () {
+ $.ajax({
+ url: ajaxUrl,
+ type: 'POST',
+ data: {
+ action: 'mailchimp_sf_get_user_sync_status',
+ nonce: ajaxNonce,
+ },
+ success(response) {
+ if (response.success && response.data) {
+ if (response.data.is_running && response.data.status) {
+ // Update the sync status on the page
+ statusWrapper.html(response.data.status);
+ } else {
+ // Clear interval and reload the page.
+ clearInterval(intervalId);
+ window.location.reload();
+ }
+ }
+ },
+ error(jqXHR, textStatus, errorThrown) {
+ // eslint-disable-next-line no-console
+ console.error('Error: ', textStatus, ', Details: ', errorThrown);
+ },
+ });
+ }, 30000); // 30000 milliseconds = 30 seconds
+})(jQuery); // eslint-disable-line no-undef
+
+// User Sync Error logs.
+(function ($) {
+ const userSyncErrors = $('.mailchimp-sf-user-sync-errors');
+ if (!userSyncErrors) {
+ return;
+ }
+
+ const params = window.mailchimp_sf_admin_params || {};
+ const tableSelector = 'table.mailchimp-sf-user-sync-errors-table';
+ const noErrorsFoundRow =
+ '
' + params.no_errors_found + ' |
';
+ $('#mailchimp-sf-clear-user-sync-errors').on('click', function (e) {
+ e.preventDefault();
+ $(this).prop('disabled', true);
+ $('.mailchimp-sf-user-sync-errors-header-actions .spinner').addClass('is-active');
+
+ $.ajax({
+ url: params.ajax_url,
+ type: 'POST',
+ data: {
+ action: 'mailchimp_sf_delete_user_sync_error',
+ id: 'all',
+ nonce: params.delete_user_sync_error_nonce,
+ },
+ success(response) {
+ if (response && response.success) {
+ $(tableSelector + ' tbody').html(noErrorsFoundRow);
+ $('.mailchimp-sf-user-sync-errors-header-actions .spinner').removeClass(
+ 'is-active',
+ );
+ } else {
+ window.location.reload();
+ }
+ },
+ error(jqXHR, textStatus, errorThrown) {
+ // eslint-disable-next-line no-console
+ console.error('Error: ', textStatus, ', Details: ', errorThrown);
+ window.location.reload();
+ },
+ });
+ });
+
+ $(tableSelector).on('click', '.mailchimp-sf-user-sync-error-delete', function (e) {
+ e.preventDefault();
+
+ const errorId = $(this).data('id');
+ const rowId = '#row-' + errorId;
+ $(rowId).find('.mailchimp-sf-user-sync-error-action .spinner').addClass('is-active');
+ $(this).prop('disabled', true);
+ $.ajax({
+ url: params.ajax_url,
+ type: 'POST',
+ data: {
+ action: 'mailchimp_sf_delete_user_sync_error',
+ nonce: params.delete_user_sync_error_nonce,
+ id: errorId,
+ },
+ success(response) {
+ if (response && response.success) {
+ $(rowId).remove();
+
+ if (!$(tableSelector + ' tbody tr').length) {
+ $(tableSelector + ' tbody').html(noErrorsFoundRow);
+ $('#mailchimp-sf-clear-user-sync-errors').prop('disabled', true);
+ }
+ } else {
+ window.location.reload();
+ }
+ },
+ error(jqXHR, textStatus, errorThrown) {
+ // eslint-disable-next-line no-console
+ console.error('Error: ', textStatus, ', Details: ', errorThrown);
+ window.location.reload();
+ },
+ });
+ });
+})(jQuery); // eslint-disable-line no-undef
diff --git a/composer.json b/composer.json
index 0da82f6f..ef422f81 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,8 @@
],
"prefer-stable": true,
"require": {
- "php": ">=7.0"
+ "php": ">=7.0",
+ "woocommerce/action-scheduler": "3.8.2"
},
"require-dev": {
"10up/phpcs-composer": "^3.0",
@@ -37,4 +38,4 @@
"scripts": {
"lint": "phpcs --standard=./phpcs.xml -p -s ."
}
-}
\ No newline at end of file
+}
diff --git a/composer.lock b/composer.lock
index a2b433e6..1db8650d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,52 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "b4631e7ae4a2f6a3795a92a813440087",
- "packages": [],
+ "content-hash": "5b8fa284bf852263974f1227edb89665",
+ "packages": [
+ {
+ "name": "woocommerce/action-scheduler",
+ "version": "3.8.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/woocommerce/action-scheduler.git",
+ "reference": "2bc91d88fdbc2c07ab899cbb56b983e11e62cf69"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/2bc91d88fdbc2c07ab899cbb56b983e11e62cf69",
+ "reference": "2bc91d88fdbc2c07ab899cbb56b983e11e62cf69",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.5",
+ "woocommerce/woocommerce-sniffs": "0.1.0",
+ "wp-cli/wp-cli": "~2.5.0",
+ "yoast/phpunit-polyfills": "^2.0"
+ },
+ "type": "wordpress-plugin",
+ "extra": {
+ "scripts-description": {
+ "test": "Run unit tests",
+ "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer",
+ "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-3.0-or-later"
+ ],
+ "description": "Action Scheduler for WordPress and WooCommerce",
+ "homepage": "https://actionscheduler.org/",
+ "support": {
+ "issues": "https://github.com/woocommerce/action-scheduler/issues",
+ "source": "https://github.com/woocommerce/action-scheduler/tree/3.8.2"
+ },
+ "time": "2024-09-12T23:12:58+00:00"
+ }
+ ],
"packages-dev": [
{
"name": "10up/phpcs-composer",
@@ -810,12 +854,12 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": ">=7.0"
},
- "platform-dev": [],
+ "platform-dev": {},
"plugin-api-version": "2.6.0"
}
diff --git a/includes/admin/class-mailchimp-user-sync.php b/includes/admin/class-mailchimp-user-sync.php
new file mode 100644
index 00000000..8a8300b3
--- /dev/null
+++ b/includes/admin/class-mailchimp-user-sync.php
@@ -0,0 +1,883 @@
+background_process = new Mailchimp_User_Sync_Background_Process();
+ $this->background_process->init();
+
+ // Admin notices
+ add_action( 'admin_notices', [ $this, 'render_notices' ] );
+
+ // Ajax action handler
+ add_action( 'wp_ajax_mailchimp_sf_get_user_sync_status', [ $this, 'get_user_sync_status' ] );
+ add_action( 'wp_ajax_mailchimp_sf_delete_user_sync_error', [ $this, 'delete_user_sync_error' ] );
+ // Render the user sync status and errors.
+ add_action( 'mailchimp_sf_user_sync_before_form', [ $this, 'render_user_sync_status' ] );
+ add_action( 'mailchimp_sf_user_sync_before_form', [ $this, 'render_user_sync_start_cta' ] );
+ add_action( 'mailchimp_sf_user_sync_after_form', [ $this, 'render_user_sync_errors' ] );
+
+ $settings = $this->get_user_sync_settings();
+ // If auto user sync is enabled, keep listening to user register and profile update actions.
+ if ( isset( $settings['enable_user_sync'] ) && 1 === absint( $settings['enable_user_sync'] ) ) {
+ add_action( 'user_register', [ $this, 'sync_user_to_mailchimp' ] );
+ add_action( 'profile_update', [ $this, 'sync_user_to_mailchimp' ] );
+ }
+ }
+
+ /**
+ * Register the user sync settings.
+ *
+ * @since x.x.x
+ */
+ public function register_settings() {
+ $args = array(
+ 'sanitize_callback' => array( $this, 'sanitize_user_sync_settings' ),
+ );
+
+ register_setting( $this->option_name, $this->option_name, $args );
+ }
+
+ /**
+ * Setup the fields and sections.
+ *
+ * @since x.x.x
+ */
+ public function setup_fields_sections() {
+ $section_id = $this->option_name . '_section';
+ add_settings_section(
+ $section_id,
+ '',
+ '__return_empty_string',
+ $this->option_name
+ );
+
+ add_settings_field(
+ 'user_sync_title',
+ __( 'User Sync settings', 'mailchimp' ),
+ '__return_empty_string',
+ $this->option_name,
+ $section_id
+ );
+
+ add_settings_field(
+ 'enable_user_sync',
+ __( 'Enable Auto User Sync', 'mailchimp' ),
+ array( $this, 'enable_user_sync_field' ),
+ $this->option_name,
+ $section_id,
+ [
+ 'class' => 'mailchimp-user-sync-enable-user-sync',
+ ]
+ );
+
+ add_settings_field(
+ 'existing_contacts_only',
+ __( 'Sync existing contacts only', 'mailchimp' ),
+ array( $this, 'existing_contacts_only_field' ),
+ $this->option_name,
+ $section_id,
+ [
+ 'class' => 'mailchimp-user-sync-existing-contacts-only',
+ ]
+ );
+
+ add_settings_field(
+ 'subscriber_status',
+ __( 'Subscriber Status', 'mailchimp' ),
+ array( $this, 'subscriber_status_field' ),
+ $this->option_name,
+ $section_id,
+ [
+ 'class' => 'mailchimp-user-sync-subscriber-status',
+ ]
+ );
+
+ add_settings_field(
+ 'user_roles',
+ __( 'Roles to sync', 'mailchimp' ),
+ array( $this, 'user_roles_field' ),
+ $this->option_name,
+ $section_id,
+ [
+ 'class' => 'mailchimp-user-sync-user-roles',
+ ]
+ );
+
+ add_settings_field(
+ 'sync_all_users',
+ __( 'Sync users', 'mailchimp' ),
+ array( $this, 'sync_all_users_button' ),
+ $this->option_name,
+ $section_id
+ );
+ }
+
+ /**
+ * Get the user sync settings.
+ *
+ * @since x.x.x
+ * @param string|null $key The key to get.
+ * @return array|null The user sync settings.
+ */
+ public function get_user_sync_settings( $key = null ) {
+ $default_settings = array(
+ 'enable_user_sync' => 0,
+ 'user_roles' => array(
+ 'subscriber' => 'subscriber',
+ ),
+ 'existing_contacts_only' => 0,
+ 'subscriber_status' => 'pending',
+ );
+
+ $settings = get_option( $this->option_name, array() );
+ $settings = wp_parse_args( $settings, $default_settings );
+
+ if ( $key ) {
+ return $settings[ $key ] ?? null;
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Sanitize the user sync settings.
+ *
+ * @since x.x.x
+ * @param array $new_settings The settings to sanitize.
+ * @return array The sanitized settings.
+ */
+ public function sanitize_user_sync_settings( $new_settings ) {
+ $settings = $this->get_user_sync_settings();
+ $settings['enable_user_sync'] = isset( $new_settings['enable_user_sync'] ) ? 1 : 0;
+ $settings['user_roles'] = isset( $new_settings['user_roles'] ) ? array_map( 'sanitize_text_field', $new_settings['user_roles'] ) : array();
+ $settings['existing_contacts_only'] = isset( $new_settings['existing_contacts_only'] ) ? 1 : 0;
+ $settings['subscriber_status'] = isset( $new_settings['subscriber_status'] ) ? sanitize_text_field( $new_settings['subscriber_status'] ) : 'pending';
+
+ return $settings;
+ }
+
+ /**
+ * Render the user roles field.
+ *
+ * @since x.x.x
+ */
+ public function user_roles_field() {
+ $settings = $this->get_user_sync_settings( 'user_roles' );
+ $user_roles = get_editable_roles();
+
+ foreach ( $user_roles as $role_name => $role_details ) {
+ $value = $settings[ $role_name ] ?? '';
+
+ // Render checkbox.
+ printf(
+ '
+
+
',
+ esc_attr( $this->option_name . '[user_roles][' . $role_name . ']' ),
+ esc_attr( $role_name ),
+ checked( $value, $role_name, false ),
+ esc_html( $role_details['name'] )
+ );
+ }
+ ?>
+
+
+
+ get_user_sync_settings( 'enable_user_sync' );
+ ?>
+
+ >
+
+
+
+ get_user_sync_settings( 'subscriber_status' );
+ ?>
+
+
+
+
+ get_users_count();
+ echo wp_kses(
+ sprintf(
+ /* translators: %1$s: opening anchor tag, %2$s: closing anchor tag, %3$d: number of contacts. */
+ _n(
+ 'You will need %1$sa Mailchimp plan%2$s that includes %3$d contact.',
+ 'You will need %1$sa Mailchimp plan%2$s that includes %3$d contacts.',
+ absint( $users_count )
+ ),
+ '',
+ '',
+ absint( $users_count )
+ ),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'target' => array(),
+ 'rel' => array(),
+ ),
+ )
+ )
+ ?>
+
+
+
+ get_user_sync_settings();
+ $existing_contacts_only = isset( $settings['existing_contacts_only'] ) ? $settings['existing_contacts_only'] : 0;
+ ?>
+ />
+
+
+
+
+
+
+
+
+
+
+ 'mailchimp_sf_options',
+ 'tab' => 'user_sync',
+ ),
+ admin_url( 'admin.php' )
+ );
+
+ // Check if the user is connected to Mailchimp.
+ $api = mailchimp_sf_get_api();
+ if ( ! $api ) {
+ $this->add_notice( __( 'We encountered a problem starting the user sync process due to connection issues, Please try again after reconnecting your Mailchimp account.', 'mailchimp' ), 'error' );
+ wp_safe_redirect( esc_url_raw( $return_url ) );
+ exit;
+ }
+
+ // Check if the user has selected a list.
+ $list_id = get_option( 'mc_list_id' );
+ if ( ! $list_id ) {
+ $this->add_notice( __( 'Please select a list to sync users.', 'mailchimp' ), 'error' );
+ wp_safe_redirect( esc_url_raw( $return_url ) );
+ exit;
+ }
+
+ // Check if the user sync is already running.
+ if ( $this->background_process->in_progress() ) {
+ $this->add_notice( __( 'User sync process is already running.', 'mailchimp' ), 'warning' );
+ wp_safe_redirect( esc_url_raw( $return_url ) );
+ exit;
+ }
+
+ // Job arguments.
+ $args = array(
+ array(
+ 'job_id' => str_replace( '-', '', wp_generate_uuid4() ),
+ 'list_id' => $list_id,
+ 'processed' => 0,
+ 'failed' => 0,
+ 'success' => 0,
+ 'skipped' => 0,
+ 'offset' => 0,
+ ),
+ );
+
+ // Schedule the user sync job.
+ $this->background_process->schedule( $args );
+
+ // Add notice that the user sync has started.
+ $this->add_notice( __( 'User sync process has started.', 'mailchimp' ) );
+
+ // Redirect to the user sync settings page.
+ wp_safe_redirect( esc_url_raw( $return_url ) );
+ exit;
+ }
+
+ /**
+ * Cancel the user sync.
+ *
+ * @since x.x.x
+ */
+ public function cancel_user_sync() {
+ if (
+ empty( $_GET['mailchimp_sf_cancel_user_sync_nonce'] ) ||
+ ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['mailchimp_sf_cancel_user_sync_nonce'] ) ), 'mailchimp_sf_cancel_user_sync' ) ||
+ ! current_user_can( 'manage_options' )
+ ) {
+ wp_die( esc_html__( 'You don\'t have permission to perform this operation.', 'mailchimp' ) );
+ }
+
+ $unschedule = $this->background_process->unschedule();
+ if ( $unschedule ) {
+ $this->add_notice( __( 'User sync process will be cancelled soon.', 'mailchimp' ) );
+ }
+
+ // Redirect to the user sync settings page.
+ wp_safe_redirect(
+ esc_url_raw(
+ add_query_arg(
+ array(
+ 'page' => 'mailchimp_sf_options',
+ 'tab' => 'user_sync',
+ ),
+ admin_url( 'admin.php' )
+ )
+ )
+ );
+ exit;
+ }
+
+ /**
+ * Skip the user sync cta.
+ *
+ * @since x.x.x
+ */
+ public function skip_user_sync_cta() {
+ if (
+ empty( $_GET['mailchimp_sf_skip_user_sync_cta_nonce'] ) ||
+ ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['mailchimp_sf_skip_user_sync_cta_nonce'] ) ), 'mailchimp_sf_skip_user_sync_cta' ) ||
+ ! current_user_can( 'manage_options' )
+ ) {
+ wp_die( esc_html__( 'You don\'t have permission to perform this operation.', 'mailchimp' ) );
+ }
+
+ update_option( 'mailchimp_sf_user_sync_start_cta_shown', value: 'skipped' );
+
+ // Redirect to the user sync settings page.
+ wp_safe_redirect(
+ esc_url_raw(
+ add_query_arg(
+ array(
+ 'page' => 'mailchimp_sf_options',
+ 'tab' => 'user_sync',
+ ),
+ admin_url( 'admin.php' )
+ )
+ )
+ );
+ exit;
+ }
+
+ /**
+ * Sync user to Mailchimp.
+ *
+ * @param int $user_id The user ID.
+ */
+ public function sync_user_to_mailchimp( $user_id ) {
+ $api = mailchimp_sf_get_api();
+ $list_id = get_option( 'mc_list_id' );
+
+ // Bail if the API or list ID is not set.
+ if ( ! $api || ! $list_id ) {
+ return;
+ }
+
+ $user = get_user_by( 'id', $user_id );
+ if ( ! $user ) {
+ return;
+ }
+
+ // Enqueue the user update action to be processed in the background.
+ if ( function_exists( 'as_enqueue_async_action' ) ) {
+ // Check if the action is already scheduled, if not, enqueue it.
+ if ( ! as_has_scheduled_action( 'mailchimp_sf_handle_user_update', array( $user_id ) ) ) {
+ as_enqueue_async_action( 'mailchimp_sf_handle_user_update', array( $user_id ) );
+ }
+ }
+ }
+
+ /**
+ * Add a notice to be displayed.
+ *
+ * @param string $message Message to display.
+ * @param string $type Type of notice.
+ */
+ public function add_notice( $message, $type = 'success' ) {
+ $notices = get_transient( $this->notices_transient_key );
+
+ if ( ! is_array( $notices ) ) {
+ $notices = [];
+ }
+
+ $notices[] = array(
+ 'message' => $message,
+ 'type' => $type,
+ );
+
+ set_transient( $this->notices_transient_key, $notices, 300 );
+ }
+
+ /**
+ * Render notices in the admin.
+ */
+ public function render_notices() {
+ $notices = get_transient( $this->notices_transient_key );
+
+ if ( ! empty( $notices ) ) {
+ foreach ( $notices as $notice ) {
+ ?>
+
+ notices_transient_key );
+ }
+ }
+
+ /**
+ * Get the total users.
+ *
+ * @since x.x.x
+ * @return int The total users.
+ */
+ public function get_users_count() {
+ $settings = $this->get_user_sync_settings();
+ $user_roles = $settings['user_roles'] ?? array();
+ $total_users = 0;
+ $total_counts = count_users();
+ if ( ! empty( $total_counts['avail_roles'] ) && is_array( $total_counts['avail_roles'] ) ) {
+ foreach ( $total_counts['avail_roles'] as $role_name => $role_count ) {
+ if ( in_array( $role_name, $user_roles, true ) ) {
+ $total_users += $role_count;
+ }
+ }
+ }
+
+ return $total_users;
+ }
+
+ /**
+ * Get the user sync status.
+ *
+ * @since x.x.x
+ */
+ public function render_user_sync_status() {
+ $is_syncing = $this->background_process->in_progress();
+
+ if ( ! $is_syncing ) {
+ return;
+ }
+
+ ?>
+
+ render_user_sync_progress();
+ ?>
+
+ background_process->in_progress();
+ if ( $is_syncing ) {
+ return;
+ }
+
+ // Get the start sync URL
+ $start_sync_url = wp_nonce_url(
+ add_query_arg(
+ array(
+ 'action' => 'mailchimp_sf_start_user_sync',
+ ),
+ admin_url( 'admin-post.php' )
+ ),
+ 'mailchimp_sf_start_user_sync',
+ 'mailchimp_sf_start_user_sync_nonce'
+ );
+
+ $skip_url = wp_nonce_url(
+ add_query_arg(
+ array(
+ 'action' => 'mailchimp_sf_skip_user_sync_cta',
+ ),
+ admin_url( 'admin-post.php' )
+ ),
+ 'mailchimp_sf_skip_user_sync_cta',
+ 'mailchimp_sf_skip_user_sync_cta_nonce'
+ );
+ ?>
+
+ background_process->in_progress();
+
+ if ( ! $is_syncing ) {
+ return;
+ }
+
+ // Get the current progress from the background process
+ $total_users = $this->get_users_count();
+ $progress = $this->background_process->get_args();
+ $progress = current( $progress ) ?? array();
+ $processed = $progress['processed'] ?? 0;
+ $success = $progress['success'] ?? 0;
+ $failed = $progress['failed'] ?? 0;
+ $skipped = $progress['skipped'] ?? 0;
+ $cancel_url = wp_nonce_url(
+ add_query_arg(
+ array(
+ 'action' => 'mailchimp_sf_cancel_user_sync',
+ ),
+ admin_url( 'admin-post.php' )
+ ),
+ 'mailchimp_sf_cancel_user_sync',
+ 'mailchimp_sf_cancel_user_sync_nonce'
+ );
+ ?>
+
+ false,
+ 'status' => '',
+ );
+
+ if ( $this->background_process->in_progress() ) {
+ $data['is_running'] = true;
+ ob_start();
+ $this->render_user_sync_progress();
+ $data['status'] = ob_get_clean();
+ }
+
+ wp_send_json_success( $data );
+ }
+
+ /**
+ * Get the user sync errors.
+ *
+ * @since x.x.x
+ * @return array The user sync errors.
+ */
+ public function get_user_sync_errors() {
+ return get_option( $this->errors_option_name, array() );
+ }
+
+ /**
+ * Set the user sync errors.
+ *
+ * @since x.x.x
+ * @param array $errors The user sync errors.
+ */
+ public function set_user_sync_errors( $errors ) {
+ if ( ! is_array( $errors ) || empty( $errors ) ) {
+ return;
+ }
+
+ $current_errors = $this->get_user_sync_errors();
+ $errors = array_merge( $current_errors, $errors );
+ update_option( $this->errors_option_name, $errors );
+ }
+
+ /**
+ * Delete the user sync error.
+ *
+ * @since x.x.x
+ *
+ * @param string $id The id of the user sync error.
+ */
+ public function delete_user_sync_errors( $id ) {
+ if ( 'all' === $id ) {
+ delete_option( $this->errors_option_name );
+ return;
+ }
+
+ $errors = $this->get_user_sync_errors();
+ if ( ! isset( $errors[ $id ] ) ) {
+ return;
+ }
+
+ unset( $errors[ $id ] );
+ update_option( $this->errors_option_name, $errors );
+ }
+
+ /**
+ * Render the user sync errors.
+ * Note: This is only renders last 100 records.
+ *
+ * @since x.x.x
+ */
+ public function render_user_sync_errors() {
+ $errors = $this->get_user_sync_errors();
+
+ if ( empty( $errors ) ) {
+ return;
+ }
+
+ // Get last 100 records
+ $errors = array_slice( $errors, -100 );
+
+ ?>
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+ $error ) {
+ ?>
+
+
+
+ |
+
+ |
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+ delete_user_sync_errors( $id );
+
+ // Send the success response
+ wp_send_json_success();
+ }
+}
diff --git a/includes/admin/templates/settings.php b/includes/admin/templates/settings.php
index c549e9a5..7673c663 100644
--- a/includes/admin/templates/settings.php
+++ b/includes/admin/templates/settings.php
@@ -5,8 +5,15 @@
* @package Mailchimp
*/
+use function Mailchimp\WordPress\Includes\Admin\admin_notice_error;
+
$user = get_option( 'mc_user' );
$is_logged_in = ! ( ! $user || ( ! get_option( 'mc_api_key' ) && ! mailchimp_sf_get_access_token() ) );
+
+// If we have an API Key, see if we need to change the lists and its options
+mailchimp_sf_change_list_if_necessary();
+
+$is_list_selected = false;
?>
+
+
+
+
+
+
+
+
+
+
+
+
+ __( 'Settings', 'mailchimp' ),
+ 'user_sync' => __( 'User Sync', 'mailchimp' ),
+ )
+ ?>
+
+
+
+
+
diff --git a/includes/admin/templates/setup-page.php b/includes/admin/templates/setup-page.php
new file mode 100644
index 00000000..29cbd01f
--- /dev/null
+++ b/includes/admin/templates/setup-page.php
@@ -0,0 +1,293 @@
+
+
diff --git a/includes/admin/templates/user-sync.php b/includes/admin/templates/user-sync.php
new file mode 100644
index 00000000..03b51866
--- /dev/null
+++ b/includes/admin/templates/user-sync.php
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
diff --git a/includes/class-mailchimp-admin.php b/includes/class-mailchimp-admin.php
index 53f675d9..a3803e33 100644
--- a/includes/class-mailchimp-admin.php
+++ b/includes/class-mailchimp-admin.php
@@ -44,8 +44,11 @@ public function init() {
add_action( 'wp_ajax_mailchimp_sf_check_login_session', array( $this, 'check_login_session' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_page_scripts' ) );
- add_action( 'admin_menu', array( $this, 'add_create_account_page' ) );
+ add_action( 'admin_menu', array( $this, 'add_admin_menu_pages' ) );
add_filter( 'admin_footer_text', array( $this, 'admin_footer_text' ) );
+
+ $user_sync = new Mailchimp_User_Sync();
+ $user_sync->init();
}
/**
@@ -476,16 +479,19 @@ public function enqueue_admin_page_scripts( $hook_suffix ) {
wp_enqueue_script( 'mailchimp_sf_admin', MCSF_URL . 'assets/js/admin.js', array( 'jquery', 'jquery-ui-dialog' ), MCSF_VER, true );
$data = array(
- 'ajax_url' => esc_url( admin_url( 'admin-ajax.php' ) ),
- 'oauth_url' => esc_url( $this->oauth_url ),
- 'oauth_start_nonce' => wp_create_nonce( 'mailchimp_sf_oauth_start_nonce' ),
- 'oauth_finish_nonce' => wp_create_nonce( 'mailchimp_sf_oauth_finish_nonce' ),
- 'oauth_window_name' => esc_html__( 'Mailchimp For WordPress OAuth', 'mailchimp' ),
- 'generic_error' => esc_html__( 'An error occurred. Please try again.', 'mailchimp' ),
- 'modal_title' => esc_html__( 'Login Popup is blocked!', 'mailchimp' ),
- 'modal_button_try_again' => esc_html__( 'Try again', 'mailchimp' ),
- 'modal_button_cancel' => esc_html__( 'No, cancel!', 'mailchimp' ),
- 'admin_settings_url' => esc_url( admin_url( 'admin.php?page=mailchimp_sf_options' ) ),
+ 'ajax_url' => esc_url( admin_url( 'admin-ajax.php' ) ),
+ 'oauth_url' => esc_url( $this->oauth_url ),
+ 'oauth_start_nonce' => wp_create_nonce( 'mailchimp_sf_oauth_start_nonce' ),
+ 'oauth_finish_nonce' => wp_create_nonce( 'mailchimp_sf_oauth_finish_nonce' ),
+ 'oauth_window_name' => esc_html__( 'Mailchimp For WordPress OAuth', 'mailchimp' ),
+ 'generic_error' => esc_html__( 'An error occurred. Please try again.', 'mailchimp' ),
+ 'modal_title' => esc_html__( 'Login Popup is blocked!', 'mailchimp' ),
+ 'modal_button_try_again' => esc_html__( 'Try again', 'mailchimp' ),
+ 'modal_button_cancel' => esc_html__( 'No, cancel!', 'mailchimp' ),
+ 'admin_settings_url' => esc_url( admin_url( 'admin.php?page=mailchimp_sf_options' ) ),
+ 'user_sync_status_nonce' => wp_create_nonce( 'mailchimp_sf_user_sync_status' ),
+ 'delete_user_sync_error_nonce' => wp_create_nonce( 'mailchimp_sf_delete_user_sync_error' ),
+ 'no_errors_found' => esc_html__( 'No errors found', 'mailchimp' ),
);
// Create account page specific data.
@@ -507,11 +513,21 @@ public function enqueue_admin_page_scripts( $hook_suffix ) {
}
/**
- * Add the create account page.
+ * Add the create account page and the settings page to the admin menu.
*
* @since 1.6.0
*/
- public function add_create_account_page() {
+ public function add_admin_menu_pages() {
+ // Add settings page.
+ add_menu_page(
+ esc_html__( 'Mailchimp Setup', 'mailchimp' ),
+ esc_html__( 'Mailchimp', 'mailchimp' ),
+ MCSF_CAP_THRESHOLD,
+ 'mailchimp_sf_options',
+ array( $this, 'settings_page' ),
+ 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Mi4wMyA1NSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjx0aXRsZT5Bc3NldCAxPC90aXRsZT48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTExLjY0LDI4LjU0YTQuNzUsNC43NSwwLDAsMC0xLjE3LjA4Yy0yLjc5LjU2LTQuMzYsMi45NC00LjA1LDZhNi4yNCw2LjI0LDAsMCwwLDUuNzIsNS4yMSw0LjE3LDQuMTcsMCwwLDAsLjgtLjA2YzIuODMtLjQ4LDMuNTctMy41NSwzLjEtNi41N0MxNS41MSwyOS44MywxMy4yMSwyOC42MywxMS42NCwyOC41NFptMi43Nyw4LjA3YTEuMTcsMS4xNywwLDAsMS0xLjEuNTUsMS41MywxLjUzLDAsMCwxLTEuMzctMS41OEE0LDQsMCwwLDEsMTIuMjMsMzRhMS40NCwxLjQ0LDAsMCwwLS41NS0xLjc0LDEuNDgsMS40OCwwLDAsMC0xLjEyLS4yMSwxLjQ0LDEuNDQsMCwwLDAtLjkyLjY0LDMuMzksMy4zOSwwLDAsMC0uMzQuNzlsMCwuMTFjLS4xMy4zNC0uMzMuNDUtLjQ3LjQzcy0uMTYtLjA1LS4yMS0uMjFhMywzLDAsMCwxLC43OC0yLjU1LDIuNDYsMi40NiwwLDAsMSwyLjExLS43NiwyLjUsMi41LDAsMCwxLDEuOTEsMS4zOSwzLjE5LDMuMTksMCwwLDEtLjIzLDIuODJsLS4wOS4yQTEuMTYsMS4xNiwwLDAsMCwxMywzNmEuNzQuNzQsMCwwLDAsLjYzLjMyLDEuMzgsMS4zOCwwLDAsMCwuMzQsMGMuMTUsMCwuMy0uMDcuMzksMEEuMjQuMjQsMCwwLDEsMTQuNDEsMzYuNjFaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNTEsMzMuODhhMy44NCwzLjg0LDAsMCwwLTEuMTUtMWwtLjExLS4zNy0uMTQtLjQyYTUuNTcsNS41NywwLDAsMCwuNS0zLjMyLDUuNDMsNS40MywwLDAsMC0xLjU0LTMsMTAuMDksMTAuMDksMCwwLDAtNC4yNC0yLjI2YzAtLjY3LDAtMS40My0uMDYtMS45YTEyLjgzLDEyLjgzLDAsMCwwLS40OS0zLjI1LDEwLjQ2LDEwLjQ2LDAsMCwwLTEuMy0yLjkyYzIuMTQtMi41NiwzLjI5LTUuMjEsMy4yOS03LjU3LDAtMy44My0zLTYuMy03LjU5LTYuM2ExOS4zLDE5LjMsMCwwLDAtNy4yMiwxLjZsLS4zNC4xNEwyOC43LDEuNTJBNi4zMSw2LjMxLDAsMCwwLDI0LjQzLDAsMTQuMDcsMTQuMDcsMCwwLDAsMTcuNiwyLjJhMzYuOTMsMzYuOTMsMCwwLDAtNi43OCw1LjIxYy00LjYsNC4zOC04LjMsOS42My05LjkxLDE0QTEyLjUxLDEyLjUxLDAsMCwwLDAsMjYuNTRhNi4xNiw2LjE2LDAsMCwwLDIuMTMsNC40bC43OC42NkExMC40NCwxMC40NCwwLDAsMCwyLjc0LDM1YTkuMzYsOS4zNiwwLDAsMCwzLjIxLDYsMTAsMTAsMCwwLDAsNS4xMywyLjQzLDIwLjE5LDIwLjE5LDAsMCwwLDcuMzEsOEEyMy4zMywyMy4zMywwLDAsMCwzMC4xNyw1NUgzMWEyMy4yNywyMy4yNywwLDAsMCwxMi0zLjE2LDE5LjEsMTkuMSwwLDAsMCw3LjgyLTkuMDZsMCwwQTE2Ljg5LDE2Ljg5LDAsMCwwLDUyLDM3LjIzLDUuMTcsNS4xNywwLDAsMCw1MSwzMy44OFptLTEuNzgsOC4yMWMtMyw3LjI5LTEwLjMsMTEuMzUtMTksMTEuMDktOC4wNi0uMjQtMTQuOTQtNC41LTE4LTExLjQzYTcuOTQsNy45NCwwLDAsMS01LjEyLTIuMDYsNy41Niw3LjU2LDAsMCwxLTIuNjEtNC44NUE4LjMxLDguMzEsMCwwLDEsNSwzMUwzLjMyLDI5LjU2Qy00LjQyLDIzLDE5Ljc3LTMuODYsMjcuNTEsMi44OWwyLjY0LDIuNTgsMS40NC0uNjFjNi43OS0yLjgxLDEyLjMtMS40NSwxMi4zLDMsMCwyLjMzLTEuNDgsNS4wNS0zLjg2LDcuNTJhNy41NCw3LjU0LDAsMCwxLDIsMy40OCwxMSwxMSwwLDAsMSwuNDIsMi44MmMwLDEsLjA5LDMuMTYuMDksMy4ybDEsLjI3QTguNjQsOC42NCwwLDAsMSw0Ny4yLDI3YTMuNjYsMy42NiwwLDAsMSwxLjA2LDIuMDZBNCw0LDAsMCwxLDQ3LjU1LDMyLDEwLjE1LDEwLjE1LDAsMCwxLDQ4LDMzLjA4Yy4yLjY0LjM1LDEuMTguMzcsMS4yNS43NCwwLDEuODkuODUsMS44OSwyLjg5QTE1LjI5LDE1LjI5LDAsMCwxLDQ5LjE4LDQyLjA5WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTQ4LDM2YTEuMzYsMS4zNiwwLDAsMC0uODYtLjE2LDExLjc2LDExLjc2LDAsMCwwLS44Mi0yLjc4QTE3Ljg5LDE3Ljg5LDAsMCwxLDQwLjQ1LDM2YTIzLjY0LDIzLjY0LDAsMCwxLTcuODEuODRjLTEuNjktLjE0LTIuODEtLjYzLTMuMjMuNzRhMTguMywxOC4zLDAsMCwwLDgsLjgxLjE0LjE0LDAsMCwxLC4xNi4xMy4xNS4xNSwwLDAsMS0uMDkuMTVzLTMuMTQsMS40Ni04LjE0LS4wOGEyLjU4LDIuNTgsMCwwLDAsMS44MywxLjkxLDguMjQsOC4yNCwwLDAsMCwxLjQ0LjM5YzYuMTksMS4wNiwxMi0yLjQ3LDEzLjI3LTMuMzYuMS0uMDcuMTYsMCwuMDguMTJsLS4xMy4xOGMtMS41OSwyLjA2LTUuODgsNC40NC0xMS40NSw0LjQ0LTIuNDMsMC00Ljg2LS44Ni01Ljc1LTIuMTctMS4zOC0yLS4wNy01LDIuMjQtNC43MWwxLC4xMWEyMS4xMywyMS4xMywwLDAsMCwxMC41LTEuNjhjMy4xNS0xLjQ2LDQuMzQtMy4wNyw0LjE2LTQuMzdBMS44NywxLjg3LDAsMCwwLDQ2LDI4LjM0YTYuOCw2LjgsMCwwLDAtMy0xLjQxYy0uNS0uMTQtLjg0LS4yMy0xLjItLjM1LS42NS0uMjEtMS0uMzktMS0xLjYxLDAtLjUzLS4xMi0yLjQtLjE2LTMuMTYtLjA2LTEuMzUtLjIyLTMuMTktMS4zNi00YTEuOTIsMS45MiwwLDAsMC0xLS4zMSwxLjg2LDEuODYsMCwwLDAtLjU4LjA2LDMuMDcsMy4wNywwLDAsMC0xLjUyLjg2LDUuMjQsNS4yNCwwLDAsMS00LDEuMzJjLS44LDAtMS42NS0uMTYtMi42Mi0uMjJsLS41NywwYTUuMjIsNS4yMiwwLDAsMC01LDQuNTdjLS41NiwzLjgzLDIuMjIsNS44MSwzLDdhMSwxLDAsMCwxLC4yMi41Mi44My44MywwLDAsMS0uMjguNTVoMGE5LjgsOS44LDAsMCwwLTIuMTYsOS4yLDcuNTksNy41OSwwLDAsMCwuNDEsMS4xMmMyLDQuNzMsOC4zLDYuOTMsMTQuNDMsNC45M2ExNS4wNiwxNS4wNiwwLDAsMCwyLjMzLTEsMTIuMjMsMTIuMjMsMCwwLDAsMy41Ny0yLjY3LDEwLjYxLDEwLjYxLDAsMCwwLDMtNS44MkM0OC42LDM2LjcsNDguMzMsMzYuMjMsNDgsMzZabS04LjI1LTcuODJjMCwuNS0uMzEuOTEtLjY4LjlzLS42Ni0uNDItLjY1LS45Mi4zMS0uOTEuNjgtLjlTMzkuNzIsMjcuNjgsMzkuNzEsMjguMThabS0xLjY4LTZjLjcxLS4xMiwxLjA2LjYyLDEuMzIsMS44NWEzLjY0LDMuNjQsMCwwLDEtLjA1LDIsNC4xNCw0LjE0LDAsMCwwLTEuMDYsMCw0LjEzLDQuMTMsMCwwLDEtLjY4LTEuNjRDMzcuMjksMjMuMjMsMzcuMzEsMjIuMzQsMzgsMjIuMjNabS0yLjQsNi41N2EuODIuODIsMCwwLDEsMS4xMS0uMTljLjQ1LjIyLjY5LjY3LjUzLDFhLjgyLjgyLDAsMCwxLTEuMTEuMTlDMzUuNywyOS41OCwzNS40NywyOS4xMywzNS42MywyOC44Wm0tMi44LS4zN2MtLjA3LjExLS4yMy4wOS0uNTcuMDZhNC4yNCw0LjI0LDAsMCwwLTIuMTQuMjIsMiwyLDAsMCwxLS40OS4xNC4xNi4xNiwwLDAsMS0uMTEsMCwuMTUuMTUsMCwwLDEtLjA1LS4xMi44MS44MSwwLDAsMSwuMzItLjUxLDIuNDEsMi40MSwwLDAsMSwxLjI3LS41MywxLjk0LDEuOTQsMCwwLDEsMS43NS41N0EuMTkuMTksMCwwLDEsMzIuODMsMjguNDNabS01LjExLTEuMjZjLS4xMiwwLS4xNy0uMDctLjE5LS4xNHMuMjgtLjU2LjYyLS44MWEzLjYsMy42LDAsMCwxLDMuNTEtLjQyQTMsMywwLDAsMSwzMywyNi44N2MuMTIuMi4xNS4zNS4wNy40NHMtLjQ0LDAtLjk1LS4yNGE0LjE4LDQuMTgsMCwwLDAtMi0uNDNBMjEuODUsMjEuODUsMCwwLDAsMjcuNzEsMjcuMTdaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzUuNSwxMy4yOWMuMSwwLC4xNi0uMTUuMDctLjJhMTEsMTEsMCwwLDAtNC42OS0xLjIzLjA5LjA5LDAsMCwxLS4wNy0uMTQsNC43OCw0Ljc4LDAsMCwxLC44OC0uODkuMDkuMDksMCwwLDAtLjA2LS4xNiwxMi40NiwxMi40NiwwLDAsMC01LjYxLDIsLjA5LjA5LDAsMCwxLS4xMy0uMDksNi4xNiw2LjE2LDAsMCwxLC41OS0xLjQ1LjA4LjA4LDAsMCwwLS4xMS0uMTFBMjIuNzksMjIuNzksMCwwLDAsMjAsMTYuMjRhLjA5LjA5LDAsMCwwLC4xMi4xM0ExOS41MywxOS41MywwLDAsMSwyNywxMy4zMiwxOS4xLDE5LjEsMCwwLDEsMzUuNSwxMy4yOVoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yOC4zNCw2LjQyUzI2LjIzLDQsMjUuNiwzLjhDMjEuNjksMi43NCwxMy4yNCw4LjU3LDcuODQsMTYuMjcsNS42NiwxOS4zOSwyLjUzLDI0LjksNCwyNy43NGExMS40MywxMS40MywwLDAsMCwxLjc5LDEuNzJBNi42NSw2LjY1LDAsMCwxLDEwLDI2Ljc4LDM0LjIxLDM0LjIxLDAsMCwxLDIwLjgsMTEuNjIsNTUuMDksNTUuMDksMCwwLDEsMjguMzQsNi40MloiLz48L2c+PC9nPjwvc3ZnPg=='
+ );
+
add_submenu_page(
'admin.php',
esc_html__( 'Create Mailchimp Account', 'mailchimp' ),
@@ -541,6 +557,17 @@ public function create_account_page() {
user_sync = new Mailchimp_User_Sync();
+ }
+
+ /**
+ * Initialize the class.
+ */
+ public function init() {
+ add_action( $this->job_name, [ $this, 'run' ] );
+ add_action( 'mailchimp_sf_handle_user_update', [ $this, 'handle_user_update' ] );
+ }
+
+ /**
+ * Run the user sync job.
+ *
+ * @param array $item Item details to process.
+ */
+ public function run( $item = array() ) {
+ // Check if cancel request is made.
+ if ( isset( $item['job_id'] ) && get_transient( 'mailchimp_sf_cancel_user_sync_process' ) === $item['job_id'] ) {
+ delete_transient( 'mailchimp_sf_cancel_user_sync_process' );
+ return;
+ }
+
+ $list_id = $this->get_list_id();
+ $api = $this->get_api();
+
+ if ( ! $list_id || ! $api || ! $item['list_id'] || $item['list_id'] !== $list_id ) {
+ $this->log( 'User sync process failed due to connection issues or list not selected.' );
+ $this->user_sync->add_notice( __( 'We encountered a problem starting the user sync process due to connection issues or list not selected.', 'mailchimp' ), 'error' );
+ return;
+ }
+
+ // Start the user sync job.
+ $this->log( 'Started user sync job.' );
+
+ $limit = $this->get_limit();
+ $processed = $item['processed'] ? absint( $item['processed'] ) : 0;
+ $offset = $item['offset'] ? absint( $item['offset'] ) : 0;
+ $user_sync_settings = $this->get_user_sync_settings();
+ $user_roles = $user_sync_settings['user_roles'] ?? array();
+ $errors = array();
+
+ // If no user roles to sync, add a notice and return.
+ if ( empty( $user_roles ) ) {
+ $this->log( 'No user roles to sync, please select at least one user role.' );
+ $this->user_sync->add_notice( __( 'No user roles to sync, please select at least one user role.', 'mailchimp' ), 'warning' );
+ return;
+ }
+
+ // Get users to sync.
+ $users = get_users(
+ array(
+ 'role__in' => $user_roles,
+ 'number' => $limit,
+ 'offset' => $offset,
+ 'fields' => 'ID',
+ )
+ );
+
+ // If no users to sync, add a notice and return.
+ if ( empty( $users ) ) {
+ $this->log( 'No users to sync.' );
+ if ( 0 === $processed ) {
+ $this->user_sync->add_notice( __( 'No users to sync.', 'mailchimp' ), 'warning' );
+ } else {
+ $this->user_sync->add_notice(
+ sprintf(
+ /* translators: %d: number of processed users. */
+ _n( 'User sync process completed. %d user processed.', 'User sync process completed. %d users processed.', $processed, 'mailchimp' ),
+ $processed
+ ),
+ 'success'
+ );
+ }
+ return;
+ }
+
+ // Sync users.
+ foreach ( $users as $user_id ) {
+ try {
+ $user = get_user_by( 'id', $user_id );
+ if ( ! $user ) {
+ $this->log( 'User not found' );
+ $item['skipped'] += 1;
+ continue;
+ }
+
+ $synced = $this->sync_user( $user );
+ if ( is_wp_error( $synced ) ) {
+ $item['failed'] += 1;
+ $errors[ uniqid( 'mailchimp_sf_error_' ) ] = array(
+ 'time' => time(),
+ 'user_id' => $user->ID,
+ 'email' => $user->user_email,
+ 'error' => $synced->get_error_message(),
+ );
+ } elseif ( $synced ) {
+ $item['success'] += 1;
+ } else {
+ $item['skipped'] += 1;
+ }
+ } catch ( Exception $e ) {
+ $this->log( 'Error getting user: ' . $e->getMessage() );
+ $item['failed'] += 1;
+ $errors[ uniqid( 'mailchimp_sf_error_' ) ] = array(
+ 'time' => time(),
+ 'user_id' => $user->ID,
+ 'email' => $user->user_email,
+ 'error' => $e->getMessage(),
+ );
+ continue;
+ }
+ }
+
+ // Save errors.
+ $this->user_sync->set_user_sync_errors( $errors );
+
+ // If no more users to sync, add a notice and return.
+ $found_users = count( $users );
+ if ( $found_users < $limit ) {
+ $processed += $found_users;
+ $this->log( 'No more users to sync, User sync process completed. ' . absint( $processed ) . ' users processed.' );
+ $this->user_sync->add_notice(
+ sprintf(
+ /* translators: %1$d: number of processed users, %2$d: number of synced users, %3$d: number of failed users, %4$d: number of skipped users. */
+ _n( 'User sync process completed. %1$d user processed (Synced: %2$d, Failed: %3$d, Skipped: %4$d).', 'User sync process completed. %1$d users processed (Synced: %2$d, Failed: %3$d, Skipped: %4$d).', absint( $processed ), 'mailchimp' ),
+ absint( $processed ),
+ absint( $item['success'] ),
+ absint( $item['failed'] ),
+ absint( $item['skipped'] )
+ ),
+ 'success'
+ );
+ return;
+ }
+
+ // Schedule the next job batch.
+ $item['processed'] += $found_users;
+ $item['offset'] = $offset + $limit;
+ $this->schedule( array( $item ) );
+ }
+
+ /**
+ * Handle the user update.
+ *
+ * @param int $user_id The user ID.
+ */
+ public function handle_user_update( $user_id ) {
+ $user = get_user_by( 'id', $user_id );
+ if ( ! $user ) {
+ $this->log( 'User not found' );
+ return;
+ }
+
+ $errors = array();
+ $synced = $this->sync_user( $user );
+ if ( is_wp_error( $synced ) ) {
+ $errors[ uniqid( 'mailchimp_sf_error_' ) ] = array(
+ 'time' => time(),
+ 'user_id' => $user->ID,
+ 'email' => $user->user_email,
+ 'error' => $synced->get_error_message(),
+ );
+ $this->user_sync->set_user_sync_errors( $errors );
+ }
+ }
+
+ /**
+ * Sync the user.
+ *
+ * @param WP_User $user The user.
+ * @return bool|WP_Error True if the user is synced, WP_Error if there is an error and false if the user is not found or not synced.
+ */
+ public function sync_user( $user ) {
+ $list_id = $this->get_list_id();
+ $api = $this->get_api();
+ $settings = $this->get_user_sync_settings();
+ $existing_contacts_only = (bool) ( $settings['existing_contacts_only'] ?? false );
+ $subscribe_status = $settings['subscriber_status'] ?? 'pending';
+
+ $this->log( 'Syncing user: ' . $user->user_email . ' (ID: ' . $user->ID . ')' );
+ $user_email = strtolower( trim( $user->user_email ) );
+
+ // Check if user exists on Mailchimp.
+ $current_status = $this->get_mailchimp_user_status( $user_email );
+
+ if ( $existing_contacts_only && ! $current_status ) {
+ $this->log( 'User not exists on Mailchimp, skipping' );
+ return false;
+ }
+
+ $request_body = array(
+ 'email_address' => $user_email,
+ 'status' => $this->get_subscribe_status( $subscribe_status, $current_status, $user ),
+ );
+ $merge_fields = $this->get_user_merge_fields( $user );
+ if ( ! empty( $merge_fields ) ) {
+ $request_body['merge_fields'] = $merge_fields;
+ }
+
+ $this->log( 'Request body: ' . wp_json_encode( $request_body ) );
+
+ $endpoint = 'lists/' . $list_id . '/members/' . md5( $user_email ) . '?skip_merge_validation=true';
+ $response = $api->post( $endpoint, $request_body, 'PUT', $list_id, true );
+
+ if ( is_wp_error( $response ) ) {
+ $this->log( 'Error syncing user: ' . $response->get_error_message() );
+ return $response;
+ }
+
+ $this->log( 'User synced: ' . $user_email );
+ return true;
+ }
+
+ /**
+ * Get the subscribe status.
+ *
+ * @param string $subscribe_status The subscribe status.
+ * @param string $current_status The current status.
+ * @param WP_User $user The user.
+ * @return string
+ */
+ public function get_subscribe_status( $subscribe_status, $current_status, $user ) {
+ if ( $current_status ) {
+ switch ( $current_status ) {
+ // If user is already subscribed, unsubscribed or transactional, don't change the status.
+ case 'subscribed':
+ case 'unsubscribed':
+ case 'transactional':
+ $subscribe_status = $current_status;
+ break;
+
+ // If user is cleaned, set the status as pending.
+ case 'cleaned':
+ $subscribe_status = 'pending';
+ break;
+
+ // If user is archived, pending or anything else, set the status as per the subscribe status in settings.
+ case 'archived':
+ case 'pending':
+ default:
+ break;
+ }
+ }
+
+ // If the subscribe status is not set (sync existing contacts only), set it to the current status.
+ if ( ! $subscribe_status && $current_status ) {
+ $subscribe_status = $current_status;
+ }
+
+ /**
+ * Filter the subscribe status.
+ *
+ * @param string $subscribe_status The subscribe status set in settings.
+ * @param string $current_status The current subscribe status of the user on Mailchimp.
+ * @param WP_User $user The user.
+ * @return string
+ */
+ return apply_filters( 'mailchimp_sf_user_sync_subscribe_status', $subscribe_status, $current_status, $user );
+ }
+
+ /**
+ * Get the user merge fields.
+ *
+ * @param WP_User $user The user to get the merge fields for.
+ * @return array
+ */
+ public function get_user_merge_fields( $user ) {
+ $merge_fields = array();
+
+ if ( ! empty( $user->first_name ) ) {
+ $merge_fields['FNAME'] = $user->first_name;
+ }
+
+ if ( ! empty( $user->last_name ) ) {
+ $merge_fields['LNAME'] = $user->last_name;
+ }
+
+ /**
+ * Filter the user merge fields.
+ *
+ * @param array $merge_fields The merge fields.
+ * @param WP_User $user The user.
+ * @return array
+ */
+ return apply_filters( 'mailchimp_sf_user_sync_merge_fields', $merge_fields, $user );
+ }
+
+ /**
+ * Schedule the user sync job.
+ *
+ * @param array $args Arguments to pass to the job.
+ */
+ public function schedule( array $args = [] ) {
+ if ( function_exists( 'as_enqueue_async_action' ) ) {
+ as_enqueue_async_action( $this->job_name, $args );
+ }
+ }
+
+ /**
+ * Unschedule the user sync job.
+ *
+ * @return bool
+ */
+ public function unschedule() {
+ if ( function_exists( 'as_unschedule_all_actions' ) ) {
+ as_unschedule_all_actions( $this->job_name );
+
+ if ( ! class_exists( 'ActionScheduler_Store' ) ) {
+ return false;
+ }
+
+ $store = ActionScheduler_Store::instance();
+
+ // Check if the job is still in progress.
+ $action_id = $store->find_action(
+ $this->job_name,
+ array(
+ 'status' => ActionScheduler_Store::STATUS_RUNNING,
+ )
+ );
+
+ // If no action running, return true.
+ if ( empty( $action_id ) ) {
+ return true;
+ }
+
+ $action = $store->fetch_action( $action_id );
+ $args = $action->get_args();
+ if ( ! empty( $args ) && isset( $args[0]['job_id'] ) ) {
+ set_transient( 'mailchimp_sf_cancel_user_sync_process', $args[0]['job_id'], 300 );
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if job is in progress.
+ *
+ * @return bool
+ */
+ public function in_progress(): bool {
+ if ( function_exists( 'as_has_scheduled_action' ) ) {
+ return as_has_scheduled_action( $this->job_name );
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the arguments for the current job.
+ *
+ * @return array|bool
+ */
+ public function get_args() {
+ if ( ! class_exists( 'ActionScheduler_Store' ) ) {
+ return false;
+ }
+
+ $store = ActionScheduler_Store::instance();
+
+ $running_action_id = $store->find_action(
+ $this->job_name,
+ array(
+ 'status' => ActionScheduler_Store::STATUS_RUNNING,
+ )
+ );
+
+ $pending_action_id = $store->find_action(
+ $this->job_name,
+ array(
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ )
+ );
+
+ if ( empty( $running_action_id ) && empty( $pending_action_id ) ) {
+ return false;
+ }
+
+ $action_id = ! empty( $running_action_id ) ? $running_action_id : $pending_action_id;
+ $action = $store->fetch_action( $action_id );
+ $args = $action->get_args();
+
+ return $args;
+ }
+
+ /**
+ * Get the limit of users to sync.
+ *
+ * @return int
+ */
+ public function get_limit() {
+ /**
+ * Filter the limit of users to sync.
+ *
+ * @param int $limit The limit of users to sync.
+ * @return int
+ */
+ return apply_filters( 'mailchimp_sf_user_sync_limit', $this->limit );
+ }
+
+ /**
+ * Get the user sync settings.
+ *
+ * @return array
+ */
+ public function get_user_sync_settings() {
+ $user_sync = new Mailchimp_User_Sync();
+ return $user_sync->get_user_sync_settings();
+ }
+
+ /**
+ * Get the API instance.
+ *
+ * @return object
+ */
+ public function get_api() {
+ if ( ! $this->api ) {
+ $this->api = mailchimp_sf_get_api();
+ }
+
+ return $this->api;
+ }
+
+ /**
+ * Get the list ID.
+ *
+ * @return string
+ */
+ public function get_list_id() {
+ return get_option( 'mc_list_id' );
+ }
+
+ /**
+ * Get the mailchimp user status.
+ *
+ * @param string $user_email The user email.
+ * @return string
+ */
+ public function get_mailchimp_user_status( $user_email ) {
+ $list_id = $this->get_list_id();
+ $user_email = strtolower( trim( $user_email ) );
+ $api = $this->get_api();
+
+ $endpoint = 'lists/' . $list_id . '/members/' . md5( $user_email ) . '?fields=status';
+
+ $subscriber = $api->get( $endpoint, null );
+ if ( is_wp_error( $subscriber ) ) {
+ return false;
+ }
+ return $subscriber['status'];
+ }
+
+ /**
+ * Log a message.
+ *
+ * @param string $message The message to log.
+ */
+ public function log( $message ) {
+ $should_log = apply_filters( 'mailchimp_sf_user_sync_log', false );
+ if ( $should_log ) {
+ error_log( 'Mailchimp User Sync: ' . $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ }
+ }
+}
diff --git a/lib/mailchimp/mailchimp.php b/lib/mailchimp/mailchimp.php
index 60106d0d..1372eed7 100644
--- a/lib/mailchimp/mailchimp.php
+++ b/lib/mailchimp/mailchimp.php
@@ -142,13 +142,14 @@ public function get( $endpoint, $count = 10, $fields = array() ) {
/**
* Sends request to Mailchimp endpoint.
*
- * @param string $endpoint The endpoint to send the request.
- * @param string $body The body of the request
- * @param string $method The request method.
- * @param string $list_id The list id.
+ * @param string $endpoint The endpoint to send the request.
+ * @param string $body The body of the request
+ * @param string $method The request method.
+ * @param string $list_id The list id.
+ * @param boolean $is_sync Whether the request is for user sync.
* @return mixed
*/
- public function post( $endpoint, $body, $method = 'POST', $list_id = '' ) {
+ public function post( $endpoint, $body, $method = 'POST', $list_id = '', $is_sync = false ) {
$url = $this->api_url . $endpoint;
$headers = array();
@@ -183,7 +184,17 @@ public function post( $endpoint, $body, $method = 'POST', $list_id = '' ) {
update_option( 'mailchimp_sf_auth_error', true );
}
- $body = json_decode( $request['body'], true );
+ $body = json_decode( $request['body'], true );
+
+ // If the request is for user sync, return the error message from the API.
+ if ( $is_sync ) {
+ if ( isset( $body['detail'] ) && ! empty( $body['detail'] ) ) {
+ return new WP_Error( 'mc-subscribe-error-api', $body['detail'] );
+ } else {
+ return new WP_Error( 'mc-subscribe-error-api', $request['body'] );
+ }
+ }
+
$merges = get_option( 'mc_merge_vars' );
// Get merge fields for the list if we have a list id.
if ( ! empty( $list_id ) ) {
diff --git a/mailchimp.php b/mailchimp.php
index 73f8b6c9..725a59b5 100644
--- a/mailchimp.php
+++ b/mailchimp.php
@@ -94,6 +94,8 @@ function () {
require_once 'mailchimp_upgrade.php';
// Init Admin functions.
+require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-user-sync-backgroud-process.php';
+require_once plugin_dir_path( __FILE__ ) . 'includes/admin/class-mailchimp-user-sync.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-admin.php';
$admin = new Mailchimp_Admin();
$admin->init();
@@ -218,25 +220,6 @@ function mailchimp_sf_main_css() {
require_once MCSF_DIR . '/views/css/frontend.php';
}
-
-/**
- * Add our settings page to the admin menu
- *
- * @return void
- */
-function mailchimp_sf_add_pages() {
- // Add settings page for users who can edit plugins
- add_menu_page(
- esc_html__( 'Mailchimp Setup', 'mailchimp' ),
- esc_html__( 'Mailchimp', 'mailchimp' ),
- MCSF_CAP_THRESHOLD,
- 'mailchimp_sf_options',
- 'mailchimp_sf_setup_page',
- 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Mi4wMyA1NSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjx0aXRsZT5Bc3NldCAxPC90aXRsZT48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTExLjY0LDI4LjU0YTQuNzUsNC43NSwwLDAsMC0xLjE3LjA4Yy0yLjc5LjU2LTQuMzYsMi45NC00LjA1LDZhNi4yNCw2LjI0LDAsMCwwLDUuNzIsNS4yMSw0LjE3LDQuMTcsMCwwLDAsLjgtLjA2YzIuODMtLjQ4LDMuNTctMy41NSwzLjEtNi41N0MxNS41MSwyOS44MywxMy4yMSwyOC42MywxMS42NCwyOC41NFptMi43Nyw4LjA3YTEuMTcsMS4xNywwLDAsMS0xLjEuNTUsMS41MywxLjUzLDAsMCwxLTEuMzctMS41OEE0LDQsMCwwLDEsMTIuMjMsMzRhMS40NCwxLjQ0LDAsMCwwLS41NS0xLjc0LDEuNDgsMS40OCwwLDAsMC0xLjEyLS4yMSwxLjQ0LDEuNDQsMCwwLDAtLjkyLjY0LDMuMzksMy4zOSwwLDAsMC0uMzQuNzlsMCwuMTFjLS4xMy4zNC0uMzMuNDUtLjQ3LjQzcy0uMTYtLjA1LS4yMS0uMjFhMywzLDAsMCwxLC43OC0yLjU1LDIuNDYsMi40NiwwLDAsMSwyLjExLS43NiwyLjUsMi41LDAsMCwxLDEuOTEsMS4zOSwzLjE5LDMuMTksMCwwLDEtLjIzLDIuODJsLS4wOS4yQTEuMTYsMS4xNiwwLDAsMCwxMywzNmEuNzQuNzQsMCwwLDAsLjYzLjMyLDEuMzgsMS4zOCwwLDAsMCwuMzQsMGMuMTUsMCwuMy0uMDcuMzksMEEuMjQuMjQsMCwwLDEsMTQuNDEsMzYuNjFaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNTEsMzMuODhhMy44NCwzLjg0LDAsMCwwLTEuMTUtMWwtLjExLS4zNy0uMTQtLjQyYTUuNTcsNS41NywwLDAsMCwuNS0zLjMyLDUuNDMsNS40MywwLDAsMC0xLjU0LTMsMTAuMDksMTAuMDksMCwwLDAtNC4yNC0yLjI2YzAtLjY3LDAtMS40My0uMDYtMS45YTEyLjgzLDEyLjgzLDAsMCwwLS40OS0zLjI1LDEwLjQ2LDEwLjQ2LDAsMCwwLTEuMy0yLjkyYzIuMTQtMi41NiwzLjI5LTUuMjEsMy4yOS03LjU3LDAtMy44My0zLTYuMy03LjU5LTYuM2ExOS4zLDE5LjMsMCwwLDAtNy4yMiwxLjZsLS4zNC4xNEwyOC43LDEuNTJBNi4zMSw2LjMxLDAsMCwwLDI0LjQzLDAsMTQuMDcsMTQuMDcsMCwwLDAsMTcuNiwyLjJhMzYuOTMsMzYuOTMsMCwwLDAtNi43OCw1LjIxYy00LjYsNC4zOC04LjMsOS42My05LjkxLDE0QTEyLjUxLDEyLjUxLDAsMCwwLDAsMjYuNTRhNi4xNiw2LjE2LDAsMCwwLDIuMTMsNC40bC43OC42NkExMC40NCwxMC40NCwwLDAsMCwyLjc0LDM1YTkuMzYsOS4zNiwwLDAsMCwzLjIxLDYsMTAsMTAsMCwwLDAsNS4xMywyLjQzLDIwLjE5LDIwLjE5LDAsMCwwLDcuMzEsOEEyMy4zMywyMy4zMywwLDAsMCwzMC4xNyw1NUgzMWEyMy4yNywyMy4yNywwLDAsMCwxMi0zLjE2LDE5LjEsMTkuMSwwLDAsMCw3LjgyLTkuMDZsMCwwQTE2Ljg5LDE2Ljg5LDAsMCwwLDUyLDM3LjIzLDUuMTcsNS4xNywwLDAsMCw1MSwzMy44OFptLTEuNzgsOC4yMWMtMyw3LjI5LTEwLjMsMTEuMzUtMTksMTEuMDktOC4wNi0uMjQtMTQuOTQtNC41LTE4LTExLjQzYTcuOTQsNy45NCwwLDAsMS01LjEyLTIuMDYsNy41Niw3LjU2LDAsMCwxLTIuNjEtNC44NUE4LjMxLDguMzEsMCwwLDEsNSwzMUwzLjMyLDI5LjU2Qy00LjQyLDIzLDE5Ljc3LTMuODYsMjcuNTEsMi44OWwyLjY0LDIuNTgsMS40NC0uNjFjNi43OS0yLjgxLDEyLjMtMS40NSwxMi4zLDMsMCwyLjMzLTEuNDgsNS4wNS0zLjg2LDcuNTJhNy41NCw3LjU0LDAsMCwxLDIsMy40OCwxMSwxMSwwLDAsMSwuNDIsMi44MmMwLDEsLjA5LDMuMTYuMDksMy4ybDEsLjI3QTguNjQsOC42NCwwLDAsMSw0Ny4yLDI3YTMuNjYsMy42NiwwLDAsMSwxLjA2LDIuMDZBNCw0LDAsMCwxLDQ3LjU1LDMyLDEwLjE1LDEwLjE1LDAsMCwxLDQ4LDMzLjA4Yy4yLjY0LjM1LDEuMTguMzcsMS4yNS43NCwwLDEuODkuODUsMS44OSwyLjg5QTE1LjI5LDE1LjI5LDAsMCwxLDQ5LjE4LDQyLjA5WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTQ4LDM2YTEuMzYsMS4zNiwwLDAsMC0uODYtLjE2LDExLjc2LDExLjc2LDAsMCwwLS44Mi0yLjc4QTE3Ljg5LDE3Ljg5LDAsMCwxLDQwLjQ1LDM2YTIzLjY0LDIzLjY0LDAsMCwxLTcuODEuODRjLTEuNjktLjE0LTIuODEtLjYzLTMuMjMuNzRhMTguMywxOC4zLDAsMCwwLDgsLjgxLjE0LjE0LDAsMCwxLC4xNi4xMy4xNS4xNSwwLDAsMS0uMDkuMTVzLTMuMTQsMS40Ni04LjE0LS4wOGEyLjU4LDIuNTgsMCwwLDAsMS44MywxLjkxLDguMjQsOC4yNCwwLDAsMCwxLjQ0LjM5YzYuMTksMS4wNiwxMi0yLjQ3LDEzLjI3LTMuMzYuMS0uMDcuMTYsMCwuMDguMTJsLS4xMy4xOGMtMS41OSwyLjA2LTUuODgsNC40NC0xMS40NSw0LjQ0LTIuNDMsMC00Ljg2LS44Ni01Ljc1LTIuMTctMS4zOC0yLS4wNy01LDIuMjQtNC43MWwxLC4xMWEyMS4xMywyMS4xMywwLDAsMCwxMC41LTEuNjhjMy4xNS0xLjQ2LDQuMzQtMy4wNyw0LjE2LTQuMzdBMS44NywxLjg3LDAsMCwwLDQ2LDI4LjM0YTYuOCw2LjgsMCwwLDAtMy0xLjQxYy0uNS0uMTQtLjg0LS4yMy0xLjItLjM1LS42NS0uMjEtMS0uMzktMS0xLjYxLDAtLjUzLS4xMi0yLjQtLjE2LTMuMTYtLjA2LTEuMzUtLjIyLTMuMTktMS4zNi00YTEuOTIsMS45MiwwLDAsMC0xLS4zMSwxLjg2LDEuODYsMCwwLDAtLjU4LjA2LDMuMDcsMy4wNywwLDAsMC0xLjUyLjg2LDUuMjQsNS4yNCwwLDAsMS00LDEuMzJjLS44LDAtMS42NS0uMTYtMi42Mi0uMjJsLS41NywwYTUuMjIsNS4yMiwwLDAsMC01LDQuNTdjLS41NiwzLjgzLDIuMjIsNS44MSwzLDdhMSwxLDAsMCwxLC4yMi41Mi44My44MywwLDAsMS0uMjguNTVoMGE5LjgsOS44LDAsMCwwLTIuMTYsOS4yLDcuNTksNy41OSwwLDAsMCwuNDEsMS4xMmMyLDQuNzMsOC4zLDYuOTMsMTQuNDMsNC45M2ExNS4wNiwxNS4wNiwwLDAsMCwyLjMzLTEsMTIuMjMsMTIuMjMsMCwwLDAsMy41Ny0yLjY3LDEwLjYxLDEwLjYxLDAsMCwwLDMtNS44MkM0OC42LDM2LjcsNDguMzMsMzYuMjMsNDgsMzZabS04LjI1LTcuODJjMCwuNS0uMzEuOTEtLjY4LjlzLS42Ni0uNDItLjY1LS45Mi4zMS0uOTEuNjgtLjlTMzkuNzIsMjcuNjgsMzkuNzEsMjguMThabS0xLjY4LTZjLjcxLS4xMiwxLjA2LjYyLDEuMzIsMS44NWEzLjY0LDMuNjQsMCwwLDEtLjA1LDIsNC4xNCw0LjE0LDAsMCwwLTEuMDYsMCw0LjEzLDQuMTMsMCwwLDEtLjY4LTEuNjRDMzcuMjksMjMuMjMsMzcuMzEsMjIuMzQsMzgsMjIuMjNabS0yLjQsNi41N2EuODIuODIsMCwwLDEsMS4xMS0uMTljLjQ1LjIyLjY5LjY3LjUzLDFhLjgyLjgyLDAsMCwxLTEuMTEuMTlDMzUuNywyOS41OCwzNS40NywyOS4xMywzNS42MywyOC44Wm0tMi44LS4zN2MtLjA3LjExLS4yMy4wOS0uNTcuMDZhNC4yNCw0LjI0LDAsMCwwLTIuMTQuMjIsMiwyLDAsMCwxLS40OS4xNC4xNi4xNiwwLDAsMS0uMTEsMCwuMTUuMTUsMCwwLDEtLjA1LS4xMi44MS44MSwwLDAsMSwuMzItLjUxLDIuNDEsMi40MSwwLDAsMSwxLjI3LS41MywxLjk0LDEuOTQsMCwwLDEsMS43NS41N0EuMTkuMTksMCwwLDEsMzIuODMsMjguNDNabS01LjExLTEuMjZjLS4xMiwwLS4xNy0uMDctLjE5LS4xNHMuMjgtLjU2LjYyLS44MWEzLjYsMy42LDAsMCwxLDMuNTEtLjQyQTMsMywwLDAsMSwzMywyNi44N2MuMTIuMi4xNS4zNS4wNy40NHMtLjQ0LDAtLjk1LS4yNGE0LjE4LDQuMTgsMCwwLDAtMi0uNDNBMjEuODUsMjEuODUsMCwwLDAsMjcuNzEsMjcuMTdaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzUuNSwxMy4yOWMuMSwwLC4xNi0uMTUuMDctLjJhMTEsMTEsMCwwLDAtNC42OS0xLjIzLjA5LjA5LDAsMCwxLS4wNy0uMTQsNC43OCw0Ljc4LDAsMCwxLC44OC0uODkuMDkuMDksMCwwLDAtLjA2LS4xNiwxMi40NiwxMi40NiwwLDAsMC01LjYxLDIsLjA5LjA5LDAsMCwxLS4xMy0uMDksNi4xNiw2LjE2LDAsMCwxLC41OS0xLjQ1LjA4LjA4LDAsMCwwLS4xMS0uMTFBMjIuNzksMjIuNzksMCwwLDAsMjAsMTYuMjRhLjA5LjA5LDAsMCwwLC4xMi4xM0ExOS41MywxOS41MywwLDAsMSwyNywxMy4zMiwxOS4xLDE5LjEsMCwwLDEsMzUuNSwxMy4yOVoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yOC4zNCw2LjQyUzI2LjIzLDQsMjUuNiwzLjhDMjEuNjksMi43NCwxMy4yNCw4LjU3LDcuODQsMTYuMjcsNS42NiwxOS4zOSwyLjUzLDI0LjksNCwyNy43NGExMS40MywxMS40MywwLDAsMCwxLjc5LDEuNzJBNi42NSw2LjY1LDAsMCwxLDEwLDI2Ljc4LDM0LjIxLDM0LjIxLDAsMCwxLDIwLjgsMTEuNjIsNTUuMDksNTUuMDksMCwwLDEsMjguMzQsNi40MloiLz48L2c+PC9nPjwvc3ZnPg=='
- );
-}
-add_action( 'admin_menu', 'mailchimp_sf_add_pages' );
-
/**
* Request handler
*
@@ -746,15 +729,6 @@ function mailchimp_sf_get_interest_categories( $list_id, $new_list, $update_opti
return $igs['categories'];
}
-
-/**
- * Outputs the Settings/Options page
- */
-function mailchimp_sf_setup_page() {
- $path = plugin_dir_path( __FILE__ );
- require_once $path . '/includes/admin/templates/settings.php';
-}
-
/**
* Register the widget.
*
diff --git a/tests/cypress/config.js b/tests/cypress/config.js
index 0c7ef460..ffcfddcb 100644
--- a/tests/cypress/config.js
+++ b/tests/cypress/config.js
@@ -31,6 +31,7 @@ module.exports = defineConfig({
'tests/cypress/e2e/submission/**.test.js',
'tests/cypress/e2e/validation/**.test.js',
'tests/cypress/e2e/block.test.js',
+ 'tests/cypress/e2e/user-sync.test.js',
'tests/cypress/e2e/logout.test.js',
],
supportFile: 'tests/cypress/support/index.js',
diff --git a/tests/cypress/e2e/user-sync.test.js b/tests/cypress/e2e/user-sync.test.js
new file mode 100644
index 00000000..d0edf7b6
--- /dev/null
+++ b/tests/cypress/e2e/user-sync.test.js
@@ -0,0 +1,290 @@
+const { generateRandomEmail } = require('../support/functions/utility');
+
+/* eslint-disable no-undef */
+describe('User Sync Tests', () => {
+ before(() => {
+ cy.login();
+ cy.mailchimpLoginIfNotAlreadyLoggedIn();
+ });
+
+ it('Admin can see User Sync settings page', () => {
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.get('.mailchimp-sf-user-sync-page').should('be.visible');
+ cy.get('.form-table th').first().should('contain', 'User Sync settings');
+ });
+
+ it('Admin can save User Sync settings', () => {
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+
+ // Enable auto user sync
+ cy.get('#enable_user_sync').check();
+
+ // Select subscriber role
+ cy.get('input[name="mailchimp_sf_user_sync_settings[user_roles][subscriber]"]').check();
+
+ // Select subscriber status
+ cy.get(
+ 'input[name="mailchimp_sf_user_sync_settings[subscriber_status]"][value="subscribed"]',
+ ).check();
+
+ // Save settings
+ cy.get('#mailchimp_sf_user_sync_settings_submit').click();
+
+ // Verify success message
+ cy.get('.notice-success').should('be.visible');
+ });
+
+ it('Admin can see Start user sync CTA and skip it', () => {
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+
+ // Verify CTA is visible
+ cy.get('.mailchimp-sf-start-user-sync-box').should('be.visible');
+
+ // Skip CTA
+ cy.get('a.skip-user-sync-cta').click();
+
+ // Verify CTA is hidden
+ cy.get('.mailchimp-sf-start-user-sync-box').should('not.exist');
+ });
+
+ ['subscribed', 'pending', 'transactional'].forEach((status) => {
+ it(`[${status}] Admin can start user sync and validate sync results`, () => {
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.get('#enable_user_sync').uncheck();
+ cy.get('#mailchimp_sf_user_sync_settings_submit').click();
+
+ cy.deleteWPSubscriberUser();
+
+ const email = generateRandomEmail('user-sync-test');
+ const firstName = `First${Date.now()}`;
+ const lastName = `Last${Date.now()}`;
+ cy.wpCli(
+ `wp user create ${email} ${email} --role=subscriber --first_name=${firstName} --last_name=${lastName}`,
+ );
+
+ // Select subscriber role
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.get(
+ 'input[name="mailchimp_sf_user_sync_settings[existing_contacts_only]"]',
+ ).uncheck();
+ // Select subscriber status
+ cy.get(
+ `input[name="mailchimp_sf_user_sync_settings[subscriber_status]"][value="${status}"]`,
+ ).check();
+ cy.get('.mailchimp-user-sync-user-roles input[type="checkbox"]').uncheck();
+ cy.get('input[name="mailchimp_sf_user_sync_settings[user_roles][subscriber]"]').check();
+ cy.get('#mailchimp_sf_user_sync_settings_submit').click();
+
+ // Start sync
+ cy.get('a.button.button-secondary').contains('Synchronize all users').click();
+
+ // Verify sync started
+ cy.get('.mailchimp-sf-sync-progress').should('be.visible');
+ cy.get('.sync-status-text').should('contain', 'Syncing users');
+
+ const checkSyncStatus = (attempts = 0) => {
+ if (attempts >= 9) return;
+
+ cy.wait(10000);
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.get('body').then(($body) => {
+ if ($body.find('.mailchimp-sf-sync-progress').length === 0) {
+ return;
+ }
+ checkSyncStatus(attempts + 1);
+ });
+ };
+
+ checkSyncStatus();
+
+ // Verify success message
+ cy.get('.notice-success').should('be.visible');
+ cy.get('.notice-success').should('contain', 'User sync process completed.');
+ cy.get('.notice-success').should('contain', 'Synced: 1');
+
+ // Verify user sync status
+ cy.verifyContactInMailchimp(email).then((response) => {
+ cy.wrap(response.status).should('eq', status);
+ });
+
+ cy.deleteContactFromList(email);
+ });
+ });
+
+ it('Admin can sync existing contacts only', () => {
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.get('#enable_user_sync').uncheck();
+ cy.get('#mailchimp_sf_user_sync_settings_submit').click();
+
+ cy.deleteWPSubscriberUser();
+
+ const email = generateRandomEmail('user-sync-test');
+ const firstName = `First${Date.now()}`;
+ const lastName = `Last${Date.now()}`;
+ cy.wpCli('wp user create opensource opensource@10up.com --role=subscriber');
+ cy.wpCli(
+ `wp user create ${email} ${email} --role=subscriber --first_name=${firstName} --last_name=${lastName}`,
+ );
+
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ // Enable existing contacts only
+ cy.get('input[name="mailchimp_sf_user_sync_settings[existing_contacts_only]"]').check();
+ cy.get('#mailchimp_sf_user_sync_settings_submit').click();
+ cy.get('.notice-success').should('be.visible');
+
+ // Start sync
+ cy.get('a.button.button-secondary').contains('Synchronize all users').click();
+
+ // Verify sync started
+ cy.get('.mailchimp-sf-sync-progress').should('be.visible');
+ cy.get('.sync-status-text').should('contain', 'Syncing users');
+
+ const checkSyncStatus = (attempts = 0) => {
+ if (attempts >= 9) return;
+
+ cy.wait(10000);
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.get('body').then(($body) => {
+ if ($body.find('.mailchimp-sf-sync-progress').length === 0) {
+ return;
+ }
+ checkSyncStatus(attempts + 1);
+ });
+ };
+
+ checkSyncStatus();
+
+ // Verify success message
+ cy.get('.notice-success').should('be.visible');
+ cy.get('.notice-success').should('contain', 'User sync process completed.');
+ cy.get('.notice-success').should('contain', 'Synced: 1');
+ cy.get('.notice-success').should('contain', 'Skipped: 1');
+
+ cy.deleteWPSubscriberUser();
+ });
+
+ it('Admin can see error logs of user sync and delete specific error log', () => {
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.get('#enable_user_sync').uncheck();
+ cy.get('.mailchimp-user-sync-user-roles input[type="checkbox"]').uncheck();
+ cy.get('input[name="mailchimp_sf_user_sync_settings[existing_contacts_only]"]').uncheck();
+ cy.get('input[name="mailchimp_sf_user_sync_settings[user_roles][administrator]"]').check();
+ cy.get('#mailchimp_sf_user_sync_settings_submit').click();
+
+ // Start sync
+ cy.get('a.button.button-secondary').contains('Synchronize all users').click();
+
+ // Verify sync started
+ cy.get('.mailchimp-sf-sync-progress').should('be.visible');
+ cy.get('.sync-status-text').should('contain', 'Syncing users');
+
+ const checkSyncStatus = (attempts = 0) => {
+ if (attempts >= 9) return;
+
+ cy.wait(10000);
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.get('body').then(($body) => {
+ if ($body.find('.mailchimp-sf-sync-progress').length === 0) {
+ return;
+ }
+ checkSyncStatus(attempts + 1);
+ });
+ };
+
+ checkSyncStatus();
+
+ // Verify success message
+ cy.get('.notice-success').should('be.visible');
+ cy.get('.notice-success').should('contain', 'User sync process completed.');
+ cy.get('.notice-success').should('contain', 'Failed: 1');
+
+ // Verify error logs section
+ cy.get('.mailchimp-sf-user-sync-errors').should('be.visible');
+ cy.get('.mailchimp-sf-user-sync-errors-header h2').should('contain', 'User Sync Errors');
+
+ // Verify error log
+ cy.get('.mailchimp-sf-user-sync-errors-table tbody tr').should('have.length', 1);
+ cy.get('.mailchimp-sf-user-sync-errors-table tbody tr').should(
+ 'contain',
+ 'wordpress@example.com',
+ );
+
+ // Delete specific error
+ cy.get('.mailchimp-sf-user-sync-error-delete').first().click();
+
+ // Verify errors are cleared
+ cy.get('.mailchimp-sf-user-sync-errors-table tbody tr').should(
+ 'contain',
+ 'No errors found',
+ );
+ });
+
+ it('Admin can cancel inprogress user sync', () => {
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+
+ // Start sync
+ cy.get('a.button.button-secondary').contains('Synchronize all users').click();
+
+ // Cancel sync
+ cy.get('.mailchimp-cancel-user-sync-button').click();
+
+ // Verify cancel message
+ cy.get('.notice-success').should('contain', 'User sync process will be cancelled soon.');
+ });
+
+ it('New user and user update should sync to Mailchimp', () => {
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.get('#enable_user_sync').check();
+ cy.get('.mailchimp-user-sync-user-roles input[type="checkbox"]').uncheck();
+ cy.get('input[name="mailchimp_sf_user_sync_settings[existing_contacts_only]"]').uncheck();
+ cy.get('input[name="mailchimp_sf_user_sync_settings[user_roles][subscriber]"]').check();
+ cy.get('#mailchimp_sf_user_sync_settings_submit').click();
+
+ cy.deleteWPSubscriberUser();
+ const email = generateRandomEmail('user-sync-test2');
+
+ // Create a test user first
+ cy.wpCli(`wp user create ${email} ${email} --role=subscriber`);
+
+ const checkSyncStatus = (attempts = 0) => {
+ if (attempts >= 9) return;
+
+ cy.wait(10000);
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.getContactInMailchimp(email).then((res) => {
+ if (res && res.id) {
+ return;
+ }
+ checkSyncStatus(attempts + 1);
+ });
+ };
+
+ // Wait for sync to complete, as it happens in the background
+ checkSyncStatus();
+
+ // Update user and validate sync
+ const firstName = `First${Date.now()}`;
+ const lastName = `Last${Date.now()}`;
+ cy.wpCli(`wp user update ${email} --first_name=${firstName} --last_name=${lastName}`);
+
+ const checkSyncStatus2 = (attempts = 0) => {
+ if (attempts >= 9) return;
+
+ cy.wait(10000);
+ cy.visit('/wp-admin/admin.php?page=mailchimp_sf_options&tab=user_sync');
+ cy.getContactInMailchimp(email).then((res) => {
+ if (res && res.merge_fields?.FNAME) {
+ cy.wrap(res.merge_fields?.FNAME).should('eq', firstName);
+ cy.wrap(res.merge_fields?.LNAME).should('eq', lastName);
+ }
+ checkSyncStatus(attempts + 1);
+ });
+ };
+
+ // Wait for sync to complete, as it happens in the background
+ checkSyncStatus2();
+
+ // Remove contact from Mailchimp
+ cy.deleteContactFromList(email);
+ });
+});
diff --git a/tests/cypress/support/commands/settings.js b/tests/cypress/support/commands/settings.js
index 107ae5d9..ff1e6d39 100644
--- a/tests/cypress/support/commands/settings.js
+++ b/tests/cypress/support/commands/settings.js
@@ -122,3 +122,12 @@ Cypress.Commands.add('setMergeFieldsRequired', (required, listName = '10up', fie
});
});
});
+
+Cypress.Commands.add('deleteWPSubscriberUser', () => {
+ // Set all merge fields to required in the Mailchimp test user account
+ cy.wpCli('wp user list --role=subscriber --field=ID').then((response) => {
+ if (response.stdout) {
+ cy.wpCli(`wp user delete ${response.stdout?.replace(/\n/g, ' ')} --reassign=1`);
+ }
+ });
+});
\ No newline at end of file
diff --git a/tests/cypress/support/commands/submission.js b/tests/cypress/support/commands/submission.js
index 0f792603..9880c6f1 100644
--- a/tests/cypress/support/commands/submission.js
+++ b/tests/cypress/support/commands/submission.js
@@ -39,12 +39,28 @@ Cypress.Commands.add('verifyContactInMailchimp', (email, listName = '10up', stat
});
});
+Cypress.Commands.add('getContactInMailchimp', (email, listName = '10up', status = null) => {
+ // Step 1: Get the list ID for the specified list name
+ cy.getListId(listName).then((listId) => {
+ // Step 2: Retrieve the contacts from the specified list
+ cy.getContactsFromAList(listId, status).then((contacts) => {
+ // Step 3: Verify that the contact with the provided email exists in the list
+ const contact = contacts.find((c) => c.email_address === email);
+ if (contact) {
+ cy.wrap(contact); // Wrap the contact to allow further chaining
+ } else {
+ cy.wrap(false);
+ }
+ });
+ });
+});
+
/**
* Custom command to verify that a contact's status matches the expected status.
*
* @param {Object} contact - The contact object to verify.
* @param {string} status - The expected status to compare against.
- *
+ *
* @example
* cy.verifyContactStatus(contact, 'subscribed');
*/
diff --git a/views/setup_page.php b/views/setup_page.php
deleted file mode 100644
index f6c5610a..00000000
--- a/views/setup_page.php
+++ /dev/null
@@ -1,396 +0,0 @@
-
-