From 94f03dbeede2573903b58427be65386176330127 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 26 Jan 2026 16:07:56 +0800 Subject: [PATCH 1/5] REST API: Add Store Subscriber Email as ID in Cookie Migrates the `store_subscriber_email_as_id_in_cookie` method from admin-ajax.php to the WordPress REST API --- includes/class-convertkit-ajax.php | 75 -------------------- includes/class-convertkit-output.php | 56 ++++++++++++++- includes/class-wp-convertkit.php | 1 - resources/frontend/js/convertkit.js | 19 ++--- tests/Integration/RESTAPITest.php | 100 +++++++++++++++++++++++++++ wp-convertkit.php | 1 - 6 files changed, 164 insertions(+), 88 deletions(-) delete mode 100644 includes/class-convertkit-ajax.php diff --git a/includes/class-convertkit-ajax.php b/includes/class-convertkit-ajax.php deleted file mode 100644 index 86c342b54..000000000 --- a/includes/class-convertkit-ajax.php +++ /dev/null @@ -1,75 +0,0 @@ -validate_and_store_subscriber_email( $email ); - - // Bail if an error occured i.e. API hasn't been configured, subscriber ID does not exist in ConvertKit etc. - if ( is_wp_error( $subscriber_id ) ) { - wp_send_json_error( $subscriber_id->get_error_message() ); - } - - // Return the subscriber ID. - wp_send_json_success( - array( - 'id' => $subscriber_id, - ) - ); - - } - -} diff --git a/includes/class-convertkit-output.php b/includes/class-convertkit-output.php index 8167df8ef..ed7bc03ca 100644 --- a/includes/class-convertkit-output.php +++ b/includes/class-convertkit-output.php @@ -67,6 +67,7 @@ class ConvertKit_Output { */ public function __construct() { + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); add_action( 'init', array( $this, 'get_subscriber_id_from_request' ) ); add_action( 'wp', array( $this, 'maybe_tag_subscriber' ) ); add_action( 'template_redirect', array( $this, 'output_form' ) ); @@ -80,6 +81,57 @@ public function __construct() { } + /** + * Register REST API routes. + * + * @since 3.1.7 + */ + public function register_routes() { + + // Register route to store the Kit subscriber's email's ID in a cookie. + register_rest_route( + 'kit/v1', + '/subscriber/store-email-as-id-in-cookie', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => function ( $request ) { + + // Get email address. + $email = $request->get_param( 'email' ); + + // Bail if the email address is empty. + if ( empty( $email ) ) { + return rest_ensure_response( new WP_Error( 'convertkit_subscriber_store_email_as_id_in_cookie_error', __( 'Kit: Required parameter `email` is empty.', 'convertkit' ) ) ); + } + + // Bail if the email address isn't a valid email address. + if ( ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) { + return rest_ensure_response( new WP_Error( 'convertkit_subscriber_store_email_as_id_in_cookie_error', __( 'Kit: Required parameter `email` is not an email address.', 'convertkit' ) ) ); + } + + // Get subscriber ID. + $subscriber = new ConvertKit_Subscriber(); + $subscriber_id = $subscriber->validate_and_store_subscriber_email( $email ); + + // Bail if an error occured i.e. API hasn't been configured. + if ( is_wp_error( $subscriber_id ) ) { + return rest_ensure_response( $subscriber_id ); + } + + // Return the subscriber ID. + return rest_ensure_response( + array( + 'id' => $subscriber_id, + ) + ); + + }, + 'permission_callback' => '__return_true', + ) + ); + + } + /** * Tags the subscriber, if: * - a subscriber ID exists in the cookie or URL, @@ -756,9 +808,9 @@ public function enqueue_scripts() { 'convertkit-js', 'convertkit', array( - 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'ajaxurl' => rest_url( 'kit/v1/blocks' ), 'debug' => $settings->debug_enabled(), - 'nonce' => wp_create_nonce( 'convertkit' ), + 'nonce' => wp_create_nonce( 'wp_rest' ), 'subscriber_id' => $this->subscriber_id, ) ); diff --git a/includes/class-wp-convertkit.php b/includes/class-wp-convertkit.php index 9e095b9ce..340d4ac20 100644 --- a/includes/class-wp-convertkit.php +++ b/includes/class-wp-convertkit.php @@ -178,7 +178,6 @@ private function initialize_global() { $this->classes['admin_notices'] = new ConvertKit_Admin_Notices(); $this->classes['admin_refresh_resources'] = new ConvertKit_Admin_Refresh_Resources(); - $this->classes['ajax'] = new ConvertKit_AJAX(); $this->classes['blocks_convertkit_broadcasts'] = new ConvertKit_Block_Broadcasts(); $this->classes['blocks_convertkit_content'] = new ConvertKit_Block_Content(); $this->classes['blocks_convertkit_formtrigger'] = new ConvertKit_Block_Form_Trigger(); diff --git a/resources/frontend/js/convertkit.js b/resources/frontend/js/convertkit.js index e2a4f5a9f..5353e9cc6 100644 --- a/resources/frontend/js/convertkit.js +++ b/resources/frontend/js/convertkit.js @@ -25,16 +25,15 @@ function convertStoreSubscriberEmailAsIDInCookie(emailAddress) { console.log(emailAddress); } - fetch(convertkit.ajaxurl, { - method: 'POST', + const url = new URL(convertkit.ajaxurl); + url.searchParams.append('email', emailAddress); + + fetch(url, { + method: 'GET', headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Type': 'application/json', + 'X-WP-Nonce': convertkit.nonce, }, - body: new URLSearchParams({ - action: 'convertkit_store_subscriber_email_as_id_in_cookie', - convertkit_nonce: convertkit.nonce, - email: emailAddress, - }), }) .then(function (response) { if (convertkit.debug) { @@ -48,9 +47,11 @@ function convertStoreSubscriberEmailAsIDInCookie(emailAddress) { console.log(result); } + // @TODO Handle error. + // Emit custom event with subscriber ID. convertKitEmitCustomEvent('convertkit_user_subscribed', { - id: result.data.id, + id: result.id, email: emailAddress, }); }) diff --git a/tests/Integration/RESTAPITest.php b/tests/Integration/RESTAPITest.php index 46510f122..fddbfea38 100644 --- a/tests/Integration/RESTAPITest.php +++ b/tests/Integration/RESTAPITest.php @@ -517,6 +517,106 @@ public function testRestrictContentSubscriberAuthenticationProductInvalidEmail() $this->assertArrayHasKey( 'data', $data ); } + /** + * Test that the /wp-json/kit/v1/subscriber/store-email-as-id-in-cookie REST API route stores + * the subscriber ID in a cookie when a valid email address is given. + * + * @since 3.1.7 + */ + public function testStoreEmailAsIDInCookie() + { + // Build request. + $request = new \WP_REST_Request( 'GET', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_query_params( [ 'email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] ] ); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertEquals( (int) $_ENV['CONVERTKIT_API_SUBSCRIBER_ID'], (int) $data['id'] ); + } + + /** + * Test that the /wp-json/kit/v1/subscriber/store-email-as-id-in-cookie REST API returns + * no subscriber ID when a non-subscriber email address is given. + * + * @since 3.1.7 + */ + public function testStoreEmailAsIDInCookieWithNonSubscriberEmail() + { + // Build request. + $request = new \WP_REST_Request( 'GET', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_query_params( [ 'email' => 'fail@kit.com' ] ); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 200, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertEquals( 0, (int) $data['id'] ); + } + + /** + * Test that the /wp-json/kit/v1/subscriber/store-email-as-id-in-cookie REST API returns + * an error when no email address is given. + * + * @since 3.1.7 + */ + public function testStoreEmailAsIDInCookieWithNoEmail() + { + // Build request. + $request = new \WP_REST_Request( 'GET', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_query_params( [ 'email' => '' ] ); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 500, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertEquals( 'convertkit_subscriber_store_email_as_id_in_cookie_error', $data['code'] ); + $this->assertEquals( 'Kit: Required parameter `email` is empty.', $data['message'] ); + } + + /** + * Test that the /wp-json/kit/v1/subscriber/store-email-as-id-in-cookie REST API returns + * an error when an invalid email address is given. + * + * @since 3.1.7 + */ + public function testStoreEmailAsIDInCookieWithInvalidEmail() + { + // Build request. + $request = new \WP_REST_Request( 'GET', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_query_params( [ 'email' => 'not-an-email' ] ); + + // Send request. + $response = rest_get_server()->dispatch( $request ); + + // Assert response is successful. + $this->assertSame( 500, $response->get_status() ); + + // Assert response data has the expected keys and data. + $data = $response->get_data(); + $this->assertEquals( 'convertkit_subscriber_store_email_as_id_in_cookie_error', $data['code'] ); + $this->assertEquals( 'Kit: Required parameter `email` is not an email address.', $data['message'] ); + } + /** * Act as an editor user. * diff --git a/wp-convertkit.php b/wp-convertkit.php index cd24b4865..84f67533c 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -53,7 +53,6 @@ require_once CONVERTKIT_PLUGIN_PATH . '/includes/functions.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-wp-convertkit.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-admin-notices.php'; -require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-ajax.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-broadcasts-exporter.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-broadcasts-importer.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-cache-plugins.php'; From 48098440bd0f730399113c956fc8c713321a76cf Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 26 Jan 2026 16:11:55 +0800 Subject: [PATCH 2/5] Fix REST API endpoint --- includes/class-convertkit-output.php | 2 +- resources/frontend/js/convertkit.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/class-convertkit-output.php b/includes/class-convertkit-output.php index ed7bc03ca..70a1fbacb 100644 --- a/includes/class-convertkit-output.php +++ b/includes/class-convertkit-output.php @@ -808,7 +808,7 @@ public function enqueue_scripts() { 'convertkit-js', 'convertkit', array( - 'ajaxurl' => rest_url( 'kit/v1/blocks' ), + 'ajaxurl' => rest_url( 'kit/v1/subscriber/store-email-as-id-in-cookie' ), 'debug' => $settings->debug_enabled(), 'nonce' => wp_create_nonce( 'wp_rest' ), 'subscriber_id' => $this->subscriber_id, diff --git a/resources/frontend/js/convertkit.js b/resources/frontend/js/convertkit.js index 5353e9cc6..5b37b30a8 100644 --- a/resources/frontend/js/convertkit.js +++ b/resources/frontend/js/convertkit.js @@ -47,8 +47,6 @@ function convertStoreSubscriberEmailAsIDInCookie(emailAddress) { console.log(result); } - // @TODO Handle error. - // Emit custom event with subscriber ID. convertKitEmitCustomEvent('convertkit_user_subscribed', { id: result.id, From 8cb0725cbd1538299cfe95320b5dea9fc7dc9d0c Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 26 Jan 2026 17:10:34 +0800 Subject: [PATCH 3/5] Use POST, `validate_callback` and `sanitize_callback` to separate validation from logic --- includes/class-convertkit-output.php | 27 +++++++++------- tests/Integration/RESTAPITest.php | 48 ++++++++++++++++++---------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/includes/class-convertkit-output.php b/includes/class-convertkit-output.php index 70a1fbacb..64960da5e 100644 --- a/includes/class-convertkit-output.php +++ b/includes/class-convertkit-output.php @@ -93,22 +93,25 @@ public function register_routes() { 'kit/v1', '/subscriber/store-email-as-id-in-cookie', array( - 'methods' => WP_REST_Server::READABLE, + 'methods' => WP_REST_Server::CREATABLE, + 'args' => array( + // Email: Validate email is included in the request, a valid email address + // and sanitize the email address. + 'email' => array( + 'required' => true, + 'validate_callback' => function ( $param ) { + + return is_string( $param ) && is_email( $param ); + + }, + 'sanitize_callback' => 'sanitize_email', + ), + ), 'callback' => function ( $request ) { // Get email address. $email = $request->get_param( 'email' ); - // Bail if the email address is empty. - if ( empty( $email ) ) { - return rest_ensure_response( new WP_Error( 'convertkit_subscriber_store_email_as_id_in_cookie_error', __( 'Kit: Required parameter `email` is empty.', 'convertkit' ) ) ); - } - - // Bail if the email address isn't a valid email address. - if ( ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) { - return rest_ensure_response( new WP_Error( 'convertkit_subscriber_store_email_as_id_in_cookie_error', __( 'Kit: Required parameter `email` is not an email address.', 'convertkit' ) ) ); - } - // Get subscriber ID. $subscriber = new ConvertKit_Subscriber(); $subscriber_id = $subscriber->validate_and_store_subscriber_email( $email ); @@ -126,6 +129,8 @@ public function register_routes() { ); }, + + // No authentication required, as this is on the frontend site. 'permission_callback' => '__return_true', ) ); diff --git a/tests/Integration/RESTAPITest.php b/tests/Integration/RESTAPITest.php index fddbfea38..53b49ce89 100644 --- a/tests/Integration/RESTAPITest.php +++ b/tests/Integration/RESTAPITest.php @@ -526,9 +526,13 @@ public function testRestrictContentSubscriberAuthenticationProductInvalidEmail() public function testStoreEmailAsIDInCookie() { // Build request. - $request = new \WP_REST_Request( 'GET', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); + $request = new \WP_REST_Request( 'POST', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); $request->set_header( 'Content-Type', 'application/json' ); - $request->set_query_params( [ 'email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'] ] ); + $request->set_body_params( + [ + 'email' => $_ENV['CONVERTKIT_API_SUBSCRIBER_EMAIL'], + ], + ); // Send request. $response = rest_get_server()->dispatch( $request ); @@ -551,9 +555,13 @@ public function testStoreEmailAsIDInCookie() public function testStoreEmailAsIDInCookieWithNonSubscriberEmail() { // Build request. - $request = new \WP_REST_Request( 'GET', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); + $request = new \WP_REST_Request( 'POST', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); $request->set_header( 'Content-Type', 'application/json' ); - $request->set_query_params( [ 'email' => 'fail@kit.com' ] ); + $request->set_body_params( + [ + 'email' => 'fail@kit.com', + ], + ); // Send request. $response = rest_get_server()->dispatch( $request ); @@ -576,20 +584,24 @@ public function testStoreEmailAsIDInCookieWithNonSubscriberEmail() public function testStoreEmailAsIDInCookieWithNoEmail() { // Build request. - $request = new \WP_REST_Request( 'GET', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); + $request = new \WP_REST_Request( 'POST', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); $request->set_header( 'Content-Type', 'application/json' ); - $request->set_query_params( [ 'email' => '' ] ); + $request->set_body_params( + [ + 'email' => '', + ], + ); // Send request. $response = rest_get_server()->dispatch( $request ); - // Assert response is successful. - $this->assertSame( 500, $response->get_status() ); + // Assert response failed. + $this->assertSame( 400, $response->get_status() ); // Assert response data has the expected keys and data. $data = $response->get_data(); - $this->assertEquals( 'convertkit_subscriber_store_email_as_id_in_cookie_error', $data['code'] ); - $this->assertEquals( 'Kit: Required parameter `email` is empty.', $data['message'] ); + $this->assertEquals( 'rest_invalid_param', $data['code'] ); + $this->assertEquals( 'Invalid parameter(s): email', $data['message'] ); } /** @@ -601,20 +613,24 @@ public function testStoreEmailAsIDInCookieWithNoEmail() public function testStoreEmailAsIDInCookieWithInvalidEmail() { // Build request. - $request = new \WP_REST_Request( 'GET', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); + $request = new \WP_REST_Request( 'POST', '/kit/v1/subscriber/store-email-as-id-in-cookie' ); $request->set_header( 'Content-Type', 'application/json' ); - $request->set_query_params( [ 'email' => 'not-an-email' ] ); + $request->set_body_params( + [ + 'email' => 'not-an-email', + ], + ); // Send request. $response = rest_get_server()->dispatch( $request ); - // Assert response is successful. - $this->assertSame( 500, $response->get_status() ); + // Assert response failed. + $this->assertSame( 400, $response->get_status() ); // Assert response data has the expected keys and data. $data = $response->get_data(); - $this->assertEquals( 'convertkit_subscriber_store_email_as_id_in_cookie_error', $data['code'] ); - $this->assertEquals( 'Kit: Required parameter `email` is not an email address.', $data['message'] ); + $this->assertEquals( 'rest_invalid_param', $data['code'] ); + $this->assertEquals( 'Invalid parameter(s): email', $data['message'] ); } /** From 8ba6060a3846beff035a8851e0253881816fafb3 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 26 Jan 2026 17:27:05 +0800 Subject: [PATCH 4/5] Coding standards --- includes/class-convertkit-output.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-convertkit-output.php b/includes/class-convertkit-output.php index 64960da5e..5078a58ff 100644 --- a/includes/class-convertkit-output.php +++ b/includes/class-convertkit-output.php @@ -94,7 +94,7 @@ public function register_routes() { '/subscriber/store-email-as-id-in-cookie', array( 'methods' => WP_REST_Server::CREATABLE, - 'args' => array( + 'args' => array( // Email: Validate email is included in the request, a valid email address // and sanitize the email address. 'email' => array( From 658494973f6f459180c65fb082d7396faad79201 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 26 Jan 2026 18:58:53 +0800 Subject: [PATCH 5/5] Fix JS --- resources/frontend/js/convertkit.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/resources/frontend/js/convertkit.js b/resources/frontend/js/convertkit.js index 5b37b30a8..598916fe6 100644 --- a/resources/frontend/js/convertkit.js +++ b/resources/frontend/js/convertkit.js @@ -25,15 +25,15 @@ function convertStoreSubscriberEmailAsIDInCookie(emailAddress) { console.log(emailAddress); } - const url = new URL(convertkit.ajaxurl); - url.searchParams.append('email', emailAddress); - - fetch(url, { - method: 'GET', + fetch(convertkit.ajaxurl, { + method: 'POST', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', 'X-WP-Nonce': convertkit.nonce, }, + body: new URLSearchParams({ + email: emailAddress, + }), }) .then(function (response) { if (convertkit.debug) {