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; ?>
+
+
+ + + + + + +

:

+
+
+ + + +
+
+ + +
+

+
+

+

+ +
+ get( 'lists', 100, array( 'fields' => 'lists.id,lists.name,lists.email_type_option' ) ); + if ( is_wp_error( $lists ) ) { + $msg = sprintf( + /* translators: %s: error message */ + esc_html__( 'Uh-oh, we couldn\'t get your lists from Mailchimp! Error: %s', 'mailchimp' ), + esc_html( $lists->get_error_message() ) + ); + admin_notice_error( $msg ); + } elseif ( isset( $lists['lists'] ) && count( $lists['lists'] ) === 0 ) { + $msg = sprintf( + /* translators: %s: link to Mailchimp */ + esc_html__( 'Uh-oh, you don\'t have any lists defined! Please visit %s, login, and setup a list before using this tool!', 'mailchimp' ), + "Mailchimp" + ); + admin_notice_error( $msg ); + } else { + $lists = $lists['lists']; + $option = get_option( 'mc_list_id' ); + $list_ids = array_map( + function ( $ele ) { + return $ele['id']; + }, + $lists + ); + $is_list_selected = in_array( $option, $list_ids, true ); + ?> + + + + + +
+ + + + + +
+ +
+
+ +
+ + __( '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 @@ + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+ + +
+
+ +
+ + + +
+
+ + + + + +
onclick="showMe('mc-custom-styling')"/>
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + /> + +
+ + + + px +
+ + + # + + # +
+ + + # + + # +
+ + + # + + # +
+
+ + + + + + + + + + + + + + + + + + + + + +
id="mc_double_optin" class="code" /> + +
id="mc_update_existing" class="code" /> + +
id="mc_use_unsub_link" class="code" /> + +
+ Note: If you haven\'t already, please add your website URL to your Mailchimp Audience account settings so users can properly return to your site after subscribing.', 'mailchimp' ), + 'https://mailchimp.com/help/change-or-update-the-return-to-our-website-button/' + ), + [ + 'a' => [ + 'href' => [], + 'target' => [], + 'rel' => [], + ], + 'strong' => [], + ] + ) + ?> +
+
+
+ + +
+ + + +
+
+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + /> + +  —  + +
+
+
+ + +
+

+
+ + + + + + + + + + + + + + + + + + + +
+ + + /> +
+
    + +
  • + +
+
+ +
+
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 @@ - -
-
- - - - - -

:

-
-
- - - -
-
- -

- -
-

-

- -
- get( 'lists', 100, array( 'fields' => 'lists.id,lists.name,lists.email_type_option' ) ); - if ( is_wp_error( $lists ) ) { - $msg = sprintf( - /* translators: %s: error message */ - esc_html__( 'Uh-oh, we couldn\'t get your lists from Mailchimp! Error: %s', 'mailchimp' ), - esc_html( $lists->get_error_message() ) - ); - admin_notice_error( $msg ); - } elseif ( isset( $lists['lists'] ) && count( $lists['lists'] ) === 0 ) { - $msg = sprintf( - /* translators: %s: link to Mailchimp */ - esc_html__( 'Uh-oh, you don\'t have any lists defined! Please visit %s, login, and setup a list before using this tool!', 'mailchimp' ), - "Mailchimp" - ); - admin_notice_error( $msg ); - } else { - $lists = $lists['lists']; - $option = get_option( 'mc_list_id' ); - $list_ids = array_map( - function ( $ele ) { - return $ele['id']; - }, - $lists - ); - $is_list_selected = in_array( $option, $list_ids, true ); - ?> - - - - - -
- - - - - -
- -
-
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - - -
- - -
- -
- - -
-
- -
- - - -
-
- - - - - -
onclick="showMe('mc-custom-styling')"/>
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - /> - -
- - - - px -
- - - # - - # -
- - - # - - # -
- - - # - - # -
-
- - - - - - - - - - - - - - - - - - - - - -
id="mc_double_optin" class="code" /> - -
id="mc_update_existing" class="code" /> - -
id="mc_use_unsub_link" class="code" /> - -
- Note: If you haven\'t already, please add your website URL to your Mailchimp Audience account settings so users can properly return to your site after subscribing.', 'mailchimp' ), - 'https://mailchimp.com/help/change-or-update-the-return-to-our-website-button/' - ), - [ - 'a' => [ - 'href' => [], - 'target' => [], - 'rel' => [], - ], - 'strong' => [], - ] - ) - ?> -
-
-
- - -
- - - -
-
- -
- - - - - - - - - - - - - - - - - - -
- -
- - - /> - -  —  - -
-
-
- - -
-

-
- - - - - - - - - - - - - - - - - - - -
- - - /> -
-
    - -
  • - -
-
- -
-
-