From 098e4fcfb8946ecb117cd948ec3584f9166e33f0 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Tue, 27 Jan 2026 12:05:50 +0800 Subject: [PATCH 1/7] REST API: Refresh Resources --- .../class-ckwc-refresh-resources.php | 19 ++---- includes/class-ckwc-rest-api.php | 21 ++++++ includes/class-wp-ckwc.php | 28 ++++---- resources/backend/js/refresh-resources.js | 34 ++++------ tests/Integration/RESTAPITest.php | 65 +++++++++++++++++++ woocommerce-convertkit.php | 2 +- 6 files changed, 120 insertions(+), 49 deletions(-) rename admin/class-ckwc-admin-refresh-resources.php => includes/class-ckwc-refresh-resources.php (80%) diff --git a/admin/class-ckwc-admin-refresh-resources.php b/includes/class-ckwc-refresh-resources.php similarity index 80% rename from admin/class-ckwc-admin-refresh-resources.php rename to includes/class-ckwc-refresh-resources.php index 20ab3d56..922daf06 100644 --- a/admin/class-ckwc-admin-refresh-resources.php +++ b/includes/class-ckwc-refresh-resources.php @@ -13,7 +13,7 @@ * @package CKWC * @author ConvertKit */ -class CKWC_Admin_Refresh_Resources { +class CKWC_Refresh_Resources { /** * Registers action and filter hooks. @@ -22,7 +22,6 @@ class CKWC_Admin_Refresh_Resources { */ public function __construct() { - add_action( 'wp_ajax_ckwc_admin_refresh_resources', array( $this, 'refresh_resources' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); } @@ -34,9 +33,6 @@ public function __construct() { */ public function refresh_resources() { - // Check nonce. - check_ajax_referer( 'ckwc_admin_refresh_resources', 'nonce' ); - // Define an array to store resources in. $resources = array(); @@ -46,7 +42,7 @@ public function refresh_resources() { // Bail if an error occured. if ( is_wp_error( $resources['forms'] ) ) { - wp_send_json_error( $resources['forms']->get_error_message() ); + return $resources['forms']; } // Fetch sequences. @@ -55,7 +51,7 @@ public function refresh_resources() { // Bail if an error occured. if ( is_wp_error( $resources['sequences'] ) ) { - wp_send_json_error( $resources['sequences']->get_error_message() ); + return $resources['sequences']; } // Fetch tags. @@ -64,7 +60,7 @@ public function refresh_resources() { // Bail if an error occured. if ( is_wp_error( $resources['tags'] ) ) { - wp_send_json_error( $resources['tags']->get_error_message() ); + return $resources['tags']; } // Return resources as a zero based sequential array, so that JS retains the order of resources. @@ -72,7 +68,7 @@ public function refresh_resources() { $resources['sequences'] = array_values( $resources['sequences'] ); $resources['tags'] = array_values( $resources['tags'] ); - wp_send_json_success( $resources ); + return $resources; } @@ -104,10 +100,9 @@ public function enqueue_scripts( $hook ) { 'ckwc-admin-refresh-resources', 'ckwc_admin_refresh_resources', array( - 'action' => 'ckwc_admin_refresh_resources', - 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'ajaxurl' => rest_url( 'kit/v1/woocommerce/resources/refresh' ), 'debug' => $integration->get_option_bool( 'debug' ), - 'nonce' => wp_create_nonce( 'ckwc_admin_refresh_resources' ), + 'nonce' => wp_create_nonce( 'wp_rest' ), ) ); diff --git a/includes/class-ckwc-rest-api.php b/includes/class-ckwc-rest-api.php index 3ebd3d93..a11f8335 100644 --- a/includes/class-ckwc-rest-api.php +++ b/includes/class-ckwc-rest-api.php @@ -42,6 +42,27 @@ public function __construct() { */ public function register_routes() { + // Register route to refresh resources. + register_rest_route( + 'kit/v1', + '/woocommerce/resources/refresh', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => function () { + + return rest_ensure_response( WP_CKWC()->get_class( 'refresh_resources' )->refresh_resources() ); + + }, + + // Only refresh resources for users who can edit posts. + 'permission_callback' => function () { + + return current_user_can( 'edit_posts' ); + + }, + ) + ); + // Register route to send a WooCommerce Order to Kit. register_rest_route( 'kit/v1', diff --git a/includes/class-wp-ckwc.php b/includes/class-wp-ckwc.php index 338a503c..77802c1e 100644 --- a/includes/class-wp-ckwc.php +++ b/includes/class-wp-ckwc.php @@ -155,12 +155,11 @@ private function initialize_admin() { return; } - $this->classes['admin_bulk_edit'] = new CKWC_Admin_Bulk_Edit(); - $this->classes['admin_coupon'] = new CKWC_Admin_Coupon(); - $this->classes['admin_plugin'] = new CKWC_Admin_Plugin(); - $this->classes['admin_product'] = new CKWC_Admin_Product(); - $this->classes['admin_quick_edit'] = new CKWC_Admin_Quick_Edit(); - $this->classes['admin_refresh_resources'] = new CKWC_Admin_Refresh_Resources(); + $this->classes['admin_bulk_edit'] = new CKWC_Admin_Bulk_Edit(); + $this->classes['admin_coupon'] = new CKWC_Admin_Coupon(); + $this->classes['admin_plugin'] = new CKWC_Admin_Plugin(); + $this->classes['admin_product'] = new CKWC_Admin_Product(); + $this->classes['admin_quick_edit'] = new CKWC_Admin_Quick_Edit(); /** * Initialize integration classes for the WordPress Administration interface. @@ -247,14 +246,15 @@ private function initialize_frontend() { */ private function initialize_global() { - $this->classes['abandoned_cart'] = new CKWC_Abandoned_Cart(); - $this->classes['admin_notices'] = new CKWC_Admin_Notices(); - $this->classes['checkout'] = new CKWC_Checkout(); - $this->classes['order'] = new CKWC_Order(); - $this->classes['rest_api'] = new CKWC_REST_API(); - $this->classes['review_request'] = new ConvertKit_Review_Request( 'Kit for WooCommerce', 'convertkit-for-woocommerce', CKWC_PLUGIN_PATH ); - $this->classes['setup'] = new CKWC_Setup(); - $this->classes['wc_subscriptions'] = new CKWC_WC_Subscriptions(); + $this->classes['abandoned_cart'] = new CKWC_Abandoned_Cart(); + $this->classes['admin_notices'] = new CKWC_Admin_Notices(); + $this->classes['checkout'] = new CKWC_Checkout(); + $this->classes['order'] = new CKWC_Order(); + $this->classes['rest_api'] = new CKWC_REST_API(); + $this->classes['refresh_resources'] = new CKWC_Refresh_Resources(); + $this->classes['review_request'] = new ConvertKit_Review_Request( 'Kit for WooCommerce', 'convertkit-for-woocommerce', CKWC_PLUGIN_PATH ); + $this->classes['setup'] = new CKWC_Setup(); + $this->classes['wc_subscriptions'] = new CKWC_WC_Subscriptions(); /** * Initialize integration classes for the frontend web site. diff --git a/resources/backend/js/refresh-resources.js b/resources/backend/js/refresh-resources.js index 74fdfd60..61ba8e47 100644 --- a/resources/backend/js/refresh-resources.js +++ b/resources/backend/js/refresh-resources.js @@ -23,11 +23,13 @@ jQuery(document).ready(function ($) { // Perform AJAX request to refresh resource. $.ajax({ type: 'POST', - data: { - action: 'ckwc_admin_refresh_resources', - nonce: ckwc_admin_refresh_resources.nonce, - }, url: ckwc_admin_refresh_resources.ajaxurl, + beforeSend(xhr) { + xhr.setRequestHeader( + 'X-WP-Nonce', + ckwc_admin_refresh_resources.nonce + ); + }, success(response) { if (ckwc_admin_refresh_resources.debug) { console.log(response); @@ -36,19 +38,6 @@ jQuery(document).ready(function ($) { // Remove any existing error notices that might be displayed. ckwcRefreshResourcesRemoveNotices(); - // Show an error if the request wasn't successful. - if (!response.success) { - // Show error notice. - ckwcRefreshResourcesOutputErrorNotice(response.data); - - // Enable button. - $(button) - .prop('disabled', false) - .removeClass('is-refreshing'); - - return; - } - // Get currently selected option. const selectedOption = $(field).val(); @@ -69,9 +58,7 @@ jQuery(document).ready(function ($) { // Populate each resource type with select options from response data into // the application option group. - for (const [resource, resources] of Object.entries( - response.data - )) { + for (const [resource, resources] of Object.entries(response)) { // resource = forms, sequences, tags. // resoruces = array of resources. resources.forEach(function (item) { @@ -108,8 +95,11 @@ jQuery(document).ready(function ($) { ckwcRefreshResourcesOutputErrorNotice( 'Kit for WooCommerce: ' + response.status + - ' ' + - response.statusText + ': ' + + (typeof response.responseJSON !== 'undefined' && + response.responseJSON.message + ? response.responseJSON.message + : response.statusText) ); // Enable button. diff --git a/tests/Integration/RESTAPITest.php b/tests/Integration/RESTAPITest.php index f01b8130..39c0227c 100644 --- a/tests/Integration/RESTAPITest.php +++ b/tests/Integration/RESTAPITest.php @@ -132,6 +132,44 @@ public function testSyncPastOrder() $this->assertStringContainsString( 'WooCommerce Order ID #' . $order->get_id() . ' added to Kit Purchase Data successfully. Kit Purchase ID: #' . get_post_meta( $order->get_id(), 'ckwc_purchase_data_id', true ), $data['data'] ); } + /** + * Test that the /wp-json/kit/v1/woocommerce/resources/refresh REST API route returns a 401 when the user is not authorized. + * + * @since 2.0.6 + */ + public function testRefreshResourcesWhenUnauthorized() + { + // Make request. + $request = new \WP_REST_Request( 'POST', '/kit/v1/woocommerce/resources/refresh' ); + $response = rest_get_server()->dispatch( $request ); + + // Assert response is unsuccessful. + $this->assertSame( 401, $response->get_status() ); + } + + /** + * Test that the /wp-json/kit/v1/woocommerce/resources/refresh REST API route refreshes and returns resources when the user is authorized. + * + * @since 2.0.6 + */ + public function testRefreshResources() + { + // Create and become editor. + $this->actAsEditor(); + + // Send request. + $request = new \WP_REST_Request( 'POST', '/kit/v1/woocommerce/resources/refresh' ); + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertArrayHasKeys( $data, [ 'forms', 'sequences', 'tags' ] ); + } + /** * Act as an administrator. * @@ -142,4 +180,31 @@ private function actAsAdministrator() $administrator_id = static::factory()->user->create( [ 'role' => 'administrator' ] ); wp_set_current_user( $administrator_id ); } + + /** + * Act as an editor. + * + * @since 2.0.6 + */ + private function actAsEditor() + { + $editor_id = static::factory()->user->create( [ 'role' => 'editor' ] ); + wp_set_current_user( $editor_id ); + } + + /** + * Assert that an array has the expected keys. + * + * @since 2.0.6 + * + * @param array $arr The array to assert. + * @param array $keys The keys to assert. + * @return void + */ + private function assertArrayHasKeys( $arr, $keys ) + { + foreach ( $keys as $key ) { + $this->assertArrayHasKey( $key, $arr ); + } + } } diff --git a/woocommerce-convertkit.php b/woocommerce-convertkit.php index b86a3a53..31232d54 100755 --- a/woocommerce-convertkit.php +++ b/woocommerce-convertkit.php @@ -59,6 +59,7 @@ require_once CKWC_PLUGIN_PATH . '/includes/class-ckwc-cli-sync-past-orders.php'; require_once CKWC_PLUGIN_PATH . '/includes/class-ckwc-order.php'; require_once CKWC_PLUGIN_PATH . '/includes/class-ckwc-rest-api.php'; +require_once CKWC_PLUGIN_PATH . '/includes/class-ckwc-refresh-resources.php'; require_once CKWC_PLUGIN_PATH . '/includes/class-ckwc-resource.php'; require_once CKWC_PLUGIN_PATH . '/includes/class-ckwc-resource-custom-fields.php'; require_once CKWC_PLUGIN_PATH . '/includes/class-ckwc-resource-forms.php'; @@ -74,7 +75,6 @@ require_once CKWC_PLUGIN_PATH . '/admin/class-ckwc-admin-plugin.php'; require_once CKWC_PLUGIN_PATH . '/admin/class-ckwc-admin-product.php'; require_once CKWC_PLUGIN_PATH . '/admin/class-ckwc-admin-quick-edit.php'; -require_once CKWC_PLUGIN_PATH . '/admin/class-ckwc-admin-refresh-resources.php'; // Register Plugin activation and deactivation functions. register_activation_hook( __FILE__, 'ckwc_plugin_activate' ); From d255195172ff199e0c06c1ddd61cc32a3ff9bb78 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 5 Feb 2026 12:52:55 +0800 Subject: [PATCH 2/7] Fix `is_empty` null error --- includes/class-ckwc-abandoned-cart.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/includes/class-ckwc-abandoned-cart.php b/includes/class-ckwc-abandoned-cart.php index 45a723f4..37e8c5c4 100644 --- a/includes/class-ckwc-abandoned-cart.php +++ b/includes/class-ckwc-abandoned-cart.php @@ -76,6 +76,11 @@ public function track_abandoned_cart() { // Get cart. $cart = WC()->cart; + // Bail if the cart is not initialized. + if ( ! $cart ) { + return; + } + // If the cart is empty, remove the abandoned cart flag. if ( $cart->is_empty() ) { WC()->session->__unset( 'ckwc_abandoned_cart_timestamp' ); From 046dc3b247832b883d0891ae7d1deb9e4725c047 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 5 Feb 2026 13:02:28 +0800 Subject: [PATCH 3/7] PHPStan compat. --- includes/class-ckwc-abandoned-cart.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/includes/class-ckwc-abandoned-cart.php b/includes/class-ckwc-abandoned-cart.php index 37e8c5c4..d637f565 100644 --- a/includes/class-ckwc-abandoned-cart.php +++ b/includes/class-ckwc-abandoned-cart.php @@ -43,6 +43,11 @@ public function __construct() { return; } + // If we're in the WordPress Admin, don't load any other actions or filters. + if ( is_admin() ) { + return; + } + // Track the abandoned cart when products are added, edited or removed from the cart. add_action( 'woocommerce_add_to_cart', array( $this, 'track_abandoned_cart' ) ); add_action( 'woocommerce_after_cart_item_quantity_update', array( $this, 'track_abandoned_cart' ) ); @@ -77,7 +82,7 @@ public function track_abandoned_cart() { $cart = WC()->cart; // Bail if the cart is not initialized. - if ( ! $cart ) { + if ( ! $cart ) { // @phpstan-ignore-line return; } From ec1339315aa60cfcfa5a5ebc096b786a7a6c1611 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 5 Feb 2026 18:33:49 +0800 Subject: [PATCH 4/7] Fix: Tests: Plugin Activation/Deactivation Test --- .github/workflows/test.yml | 14 ++------------ .../general/ActivateDeactivatePluginCest.php | 4 ++++ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc7521d7..4a7b5826 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,21 +50,11 @@ jobs: fail-fast: false matrix: wp-versions: [ 'latest' ] #[ '6.1.1', 'latest' ] - php-versions: [ '8.1', '8.2', '8.3', '8.4' ] #[ '7.4', '8.0', '8.1' ] + php-versions: [ '8.1' ] #[ '7.4', '8.0', '8.1' ] # Folder names within the 'tests' folder to run tests in parallel. test-groups: [ - 'EndToEnd/abandoned-cart', - 'EndToEnd/general', - 'EndToEnd/integrations', - 'EndToEnd/purchase-data', - 'EndToEnd/settings', - 'EndToEnd/subscribe/checkout-block', - 'EndToEnd/subscribe/order-completed', - 'EndToEnd/subscribe/order-pending-payment', - 'EndToEnd/subscribe/order-processing', - 'EndToEnd/sync-past-orders', - 'Integration' + 'EndToEnd/general/ActivateDeactivatePluginCest' ] # Steps to install, configure and run tests diff --git a/tests/EndToEnd/general/ActivateDeactivatePluginCest.php b/tests/EndToEnd/general/ActivateDeactivatePluginCest.php index d66a1dd6..16126dcf 100644 --- a/tests/EndToEnd/general/ActivateDeactivatePluginCest.php +++ b/tests/EndToEnd/general/ActivateDeactivatePluginCest.php @@ -32,6 +32,10 @@ public function testPluginActivationAndDeactivation(EndToEndTester $I) // Deactivate the Plugin. $I->deactivateConvertKitPlugin($I); + // Confirm the Plugin is deactivated. + $I->amOnPluginsPage(); + $I->seePluginDeactivated('convertkit-for-woocommerce'); + // Confirm the Action Scheduler action is unscheduled. $I->amOnAdminPage('admin.php?page=wc-status&status=pending&tab=action-scheduler&s=ckwc_abandoned_cart'); $I->waitForElementVisible('tbody[data-wp-lists="list:action-scheduler"] tr.no-items'); From 55e58bbe679204f651dea1473ddcb0dadc468539 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 5 Feb 2026 18:38:56 +0800 Subject: [PATCH 5/7] Reinstate all tests --- .github/workflows/test.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a7b5826..dc7521d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,11 +50,21 @@ jobs: fail-fast: false matrix: wp-versions: [ 'latest' ] #[ '6.1.1', 'latest' ] - php-versions: [ '8.1' ] #[ '7.4', '8.0', '8.1' ] + php-versions: [ '8.1', '8.2', '8.3', '8.4' ] #[ '7.4', '8.0', '8.1' ] # Folder names within the 'tests' folder to run tests in parallel. test-groups: [ - 'EndToEnd/general/ActivateDeactivatePluginCest' + 'EndToEnd/abandoned-cart', + 'EndToEnd/general', + 'EndToEnd/integrations', + 'EndToEnd/purchase-data', + 'EndToEnd/settings', + 'EndToEnd/subscribe/checkout-block', + 'EndToEnd/subscribe/order-completed', + 'EndToEnd/subscribe/order-pending-payment', + 'EndToEnd/subscribe/order-processing', + 'EndToEnd/sync-past-orders', + 'Integration' ] # Steps to install, configure and run tests From 1c023bf269928fffa19475c4ede09c0341bc0a3f Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 5 Feb 2026 18:57:05 +0800 Subject: [PATCH 6/7] Tests: Set CC expiry to later date --- tests/Support/Helper/WooCommerce.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Support/Helper/WooCommerce.php b/tests/Support/Helper/WooCommerce.php index 3879f86c..4f154fda 100644 --- a/tests/Support/Helper/WooCommerce.php +++ b/tests/Support/Helper/WooCommerce.php @@ -823,7 +823,7 @@ public function wooCommerceCheckoutWithProduct($I, $productID, $productName, $em // Complete Credit Card Details. $I->switchToIFrame('iframe[title="Secure payment input frame"]'); // Switch to CC Stripe iFrame. $I->fillField('number', '4242424242424242'); - $I->fillfield('expiry', '01/26'); + $I->fillfield('expiry', '01/28'); $I->fillField('cvc', '123'); $I->switchToIFrame(); // Switch back to main window. break; From d44a023969b60133c8ea5948e27b5171cd9c0661 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 5 Feb 2026 21:15:08 +0800 Subject: [PATCH 7/7] Remove is_admin check --- includes/class-ckwc-abandoned-cart.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/includes/class-ckwc-abandoned-cart.php b/includes/class-ckwc-abandoned-cart.php index d637f565..ab09ce8c 100644 --- a/includes/class-ckwc-abandoned-cart.php +++ b/includes/class-ckwc-abandoned-cart.php @@ -43,11 +43,6 @@ public function __construct() { return; } - // If we're in the WordPress Admin, don't load any other actions or filters. - if ( is_admin() ) { - return; - } - // Track the abandoned cart when products are added, edited or removed from the cart. add_action( 'woocommerce_add_to_cart', array( $this, 'track_abandoned_cart' ) ); add_action( 'woocommerce_after_cart_item_quantity_update', array( $this, 'track_abandoned_cart' ) );