From ef82d9c97a35904ddbe25460e641b24eb6b10010 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 3 Aug 2026 15:55:40 +0800 Subject: [PATCH 01/10] =?UTF-8?q?Abilities=20API:=20Check=20Creator?= =?UTF-8?q?=E2=80=99s=20Plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...class-convertkit-admin-section-general.php | 8 +- .../class-convertkit-admin-section-mcp.php | 48 ++++++ includes/class-convertkit-account.php | 145 ++++++++++++++++++ includes/class-convertkit-settings-mcp.php | 10 +- includes/mcp/class-convertkit-mcp.php | 6 + wp-convertkit.php | 1 + 6 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 includes/class-convertkit-account.php diff --git a/admin/section/class-convertkit-admin-section-general.php b/admin/section/class-convertkit-admin-section-general.php index 16323d49c..061ba7081 100644 --- a/admin/section/class-convertkit-admin-section-general.php +++ b/admin/section/class-convertkit-admin-section-general.php @@ -157,9 +157,9 @@ private function check_credentials() { 'settings' ); - // Get Account Details, which we'll use in account_name_callback(), but also lets us test - // whether the API credentials are valid. - $this->account = $this->api->get_account(); + // Refresh account details. + $account = new ConvertKit_Account(); + $this->account = $account->refresh(); // If the request succeeded, no need to perform further actions. if ( ! is_wp_error( $this->account ) ) { @@ -224,6 +224,7 @@ private function maybe_disconnect() { } // Delete cached resources. + $account = new ConvertKit_Account(); $creator_network = new ConvertKit_Resource_Creator_Network_Recommendations(); $custom_fields = new ConvertKit_Resource_Custom_Fields(); $forms = new ConvertKit_Resource_Forms(); @@ -232,6 +233,7 @@ private function maybe_disconnect() { $products = new ConvertKit_Resource_Products(); $sequences = new ConvertKit_Resource_Sequences(); $tags = new ConvertKit_Resource_Tags(); + $account->delete(); $creator_network->delete(); $custom_fields->delete(); $forms->delete(); diff --git a/admin/section/class-convertkit-admin-section-mcp.php b/admin/section/class-convertkit-admin-section-mcp.php index daf2c91a8..6233f8c20 100644 --- a/admin/section/class-convertkit-admin-section-mcp.php +++ b/admin/section/class-convertkit-admin-section-mcp.php @@ -23,6 +23,16 @@ class ConvertKit_Admin_Section_MCP extends ConvertKit_Admin_Section_Base { */ private $authorization_header = false; + /** + * The Account class instance, used to check whether the connected Kit + * account is on a paid plan (required for MCP access). + * + * @since 3.4.0 + * + * @var ConvertKit_Account + */ + private $account; + /** * Constructor. * @@ -53,6 +63,10 @@ public function __construct() { ), ); + // Refresh account details. + $account = new ConvertKit_Account(); + $this->account = $account->refresh(); + $this->maybe_generate_authentication_header(); $this->maybe_revoke_application_password(); @@ -168,6 +182,19 @@ public function enqueue_scripts( $section ) { */ public function register_fields() { + // Only show MCP settings if the connected Kit account is on a paid plan. + if ( ! $this->account->is_paid_plan() ) { + add_settings_field( + 'upgrade_required', + __( 'Paid Plan Required', 'convertkit' ), + array( $this, 'upgrade_required_callback' ), + $this->settings_key, + $this->name + ); + + return; + } + // Enable. add_settings_field( 'enabled', @@ -241,6 +268,27 @@ public function documentation_url() { } + /** + * Renders the upgrade CTA when the connected Kit account is on + * the free plan. + * + * @since 3.4.0 + */ + public function upgrade_required_callback() { + + ?> +

+ +

+

+ + + +

+ get(); + + if ( ! $account || ! isset( $account['account']['plan_type'] ) ) { + return null; + } + + return (string) $account['account']['plan_type']; + + } + + /** + * Returns whether the cached plan is a paid Kit plan. + * + * @since 3.4.0 + * + * @return bool + */ + public function is_paid_plan() { + + $plan_type = $this->get_plan_type(); + + if ( $plan_type === null ) { + return false; + } + + return $plan_type !== 'free'; + + } + + /** + * Fetches the account from Kit and caches the response. + * + * @since 3.4.0 + * + * @return WP_Error|array + */ + public function refresh() { + + // Initialize the settings object. + $settings = new ConvertKit_Settings(); + + // Bail if we don't have access and refresh tokens. + if ( ! $settings->has_access_and_refresh_token() ) { + return new WP_Error( + 'convertkit_account_no_credentials', + __( 'Cannot refresh account: no OAuth credentials configured.', 'convertkit' ) + ); + } + + // Initialize the API client. + $api = new ConvertKit_API_V4( + CONVERTKIT_OAUTH_CLIENT_ID, + CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, + $settings->get_access_token(), + $settings->get_refresh_token(), + $settings->debug_enabled(), + 'account' + ); + + // Fetch the account. + $account = $api->get_account(); + + // Bail if there was an error. + if ( is_wp_error( $account ) ) { + return $account; + } + + // Update the cached account. + update_option( self::OPTION_NAME, $account ); + + return $account; + + } + + /** + * Deletes the cached account. + * + * @since 3.4.0 + * + * @return bool + */ + public function delete() { + + return delete_option( self::OPTION_NAME ); + + } + +} diff --git a/includes/class-convertkit-settings-mcp.php b/includes/class-convertkit-settings-mcp.php index 9e58b50f1..7ba995798 100644 --- a/includes/class-convertkit-settings-mcp.php +++ b/includes/class-convertkit-settings-mcp.php @@ -65,7 +65,8 @@ public function get() { } /** - * Returns whether the MCP server is enabled. + * Returns whether the user has access to MCP via a paid plan, + * and if so whether the MCP server is enabled in the Plugin's settings. * * @since 3.4.0 * @@ -73,6 +74,13 @@ public function get() { */ public function enabled() { + // Bail if the connected Kit account isn't on a paid plan. + // This queries the cached account details, so no live API call is made. + $account = new ConvertKit_Account(); + if ( ! $account->is_paid_plan() ) { + return false; + } + return ( $this->settings['enabled'] === 'on' ? true : false ); } diff --git a/includes/mcp/class-convertkit-mcp.php b/includes/mcp/class-convertkit-mcp.php index 7be6e074a..7b2e9013d 100644 --- a/includes/mcp/class-convertkit-mcp.php +++ b/includes/mcp/class-convertkit-mcp.php @@ -77,6 +77,12 @@ public static function get_server_url() { */ public function __construct() { + // Bail if the MCP server isn't enabled. + $settings = new ConvertKit_Settings_MCP(); + if ( ! $settings->enabled() ) { + return; + } + // Register the ability category. add_action( 'wp_abilities_api_categories_init', array( $this, 'register_abilities_category' ) ); diff --git a/wp-convertkit.php b/wp-convertkit.php index 9b49df2f6..9d2823044 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -53,6 +53,7 @@ require_once CONVERTKIT_PLUGIN_PATH . '/includes/cron-functions.php'; 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-account.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-admin-notices.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-broadcasts-exporter.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-broadcasts-importer.php'; From 9bedc27e2f46c4341ba57958ab62478614f03669 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 3 Aug 2026 16:02:32 +0800 Subject: [PATCH 02/10] Added tests --- .../plugin-screens/PluginSettingsMCPCest.php | 79 ++++++ tests/Integration/AccountTest.php | 255 ++++++++++++++++++ tests/Integration/SettingsMCPTest.php | 132 +++++++++ tests/Support/Helper/KitPlugin.php | 3 + 4 files changed, 469 insertions(+) create mode 100644 tests/Integration/AccountTest.php create mode 100644 tests/Integration/SettingsMCPTest.php diff --git a/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php b/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php index cf0a22683..a8ee143c3 100644 --- a/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php +++ b/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php @@ -158,6 +158,85 @@ public function testGenerateAndRevokeApplicationPassword(EndToEndTester $I) $I->waitForElementNotVisible('#convertkit-settings-mcp-revoke-application-password'); } + /** + * Tests that a free-plan Kit account sees the upgrade CTA on the MCP tab + * instead of the enable / connect UI, and that the MCP REST route is not + * registered even when the enabled setting is on. + * + * @since 3.4.0 + * + * @param EndToEndTester $I Tester. + */ + public function testFreePlanShowsUpgradeCTA(EndToEndTester $I) + { + // Simulate a Kit account that is on the free plan. + $I->setupKitPluginFakeAPIKey($I); + $I->haveOptionInDatabase( + 'convertkit_account', + [ + 'account' => [ + 'plan_type' => 'free', + ], + ] + ); + + // Enable MCP server. + $I->haveOptionInDatabase( + '_wp_convertkit_settings_mcp', + [ + 'enabled' => 'on', + ] + ); + + // Load the MCP settings tab. + $I->loadKitSettingsMCPScreen($I); + + // Assert that the upgrade CTA is shown. + $I->see('Paid Plan Required'); + $I->see('MCP is available on paid Kit plans.'); + $I->seeLink('Upgrade Kit Account'); + + // Assert no option to enable/disable the MCP server are shown. + $I->dontSeeElement('#enabled'); + $I->dontSee('Create Application Password'); + + // Assert that the MCP server is not registered. + $I->doesNotHaveRoute($I, '/kit/mcp'); + $I->doesNotHaveRoute($I, '/kit/mcp/v1'); + } + + /** + * Tests that a paid-plan Kit account sees the enable UI on the MCP tab + * (i.e. the upgrade CTA is not shown). + * + * @since 3.4.0 + * + * @param EndToEndTester $I Tester. + */ + public function testPaidPlanShowsEnableUI(EndToEndTester $I) + { + // Simulate a Kit account that is on a paid plan. + $I->setupKitPluginFakeAPIKey($I); + $I->haveOptionInDatabase( + 'convertkit_account', + [ + 'account' => [ + 'plan_type' => 'creator_pro', + ], + ] + ); + + // Load the MCP settings tab. + $I->loadKitSettingsMCPScreen($I); + + // The upgrade CTA should not be shown. + $I->dontSee('Paid Plan Required'); + $I->dontSee('Upgrade Kit Account'); + + // The Enable checkbox should be visible. + $I->seeElement('#enabled'); + } + /** * Deactivate and reset Plugin(s) after each test, if the test passes. * We don't use _after, as this would provide a screenshot of the Plugin diff --git a/tests/Integration/AccountTest.php b/tests/Integration/AccountTest.php new file mode 100644 index 000000000..7244108ad --- /dev/null +++ b/tests/Integration/AccountTest.php @@ -0,0 +1,255 @@ +settings = new \ConvertKit_Settings(); + update_option( + $this->settings::SETTINGS_NAME, + [ + 'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], + 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'], + ] + ); + + $this->account = new \ConvertKit_Account(); + } + + /** + * Performs actions after each test. + * + * @since 3.4.0 + */ + public function tearDown(): void + { + delete_option($this->settings::SETTINGS_NAME); + delete_option(\ConvertKit_Account::OPTION_NAME); + + unset($this->account); + + deactivate_plugins('convertkit/wp-convertkit.php'); + + parent::tearDown(); + } + + /** + * Test that get() returns false when no cache has been written yet. + * + * @since 3.4.0 + */ + public function testGetReturnsFalseWhenNoCache() + { + $this->assertSame(false, $this->account->get()); + } + + /** + * Test that get_plan_type() returns null when no cache has been written yet. + * + * @since 3.4.0 + */ + public function testGetPlanTypeReturnsNullWhenNoCache() + { + $this->assertSame(null, $this->account->get_plan_type()); + } + + /** + * Test that is_paid_plan() fails closed when no cache has been written yet. + * + * @since 3.4.0 + */ + public function testIsPaidPlanFailsClosedWhenNoCache() + { + $this->assertSame(false, $this->account->is_paid_plan()); + } + + /** + * Test that refresh() fetches the account from Kit and caches it. + * + * @since 3.4.0 + */ + public function testRefreshPopulatesCache() + { + $result = $this->account->refresh(); + $this->assertNotInstanceOf(\WP_Error::class, $result); + $this->assertIsArray($result); + $this->assertArrayHasKey('account', $result); + $this->assertArrayHasKey('plan_type', $result['account']); + + // Confirm the value was persisted. + $cached = $this->account->get(); + $this->assertIsArray($cached); + $this->assertSame($result['account']['plan_type'], $cached['account']['plan_type']); + } + + /** + * Test that is_paid_plan() returns true when the cached plan_type is not free. + * + * @since 3.4.0 + */ + public function testIsPaidPlanTrueForPaidPlan() + { + update_option( + \ConvertKit_Account::OPTION_NAME, + [ + 'account' => [ + 'plan_type' => 'creator_pro', + ], + ] + ); + + $this->assertSame('creator_pro', $this->account->get_plan_type()); + $this->assertSame(true, $this->account->is_paid_plan()); + } + + /** + * Test that is_paid_plan() returns false when the cached plan_type is free. + * + * @since 3.4.0 + */ + public function testIsPaidPlanFalseForFreePlan() + { + update_option( + \ConvertKit_Account::OPTION_NAME, + [ + 'account' => [ + 'plan_type' => 'free', + ], + ] + ); + + $this->assertSame('free', $this->account->get_plan_type()); + $this->assertSame(false, $this->account->is_paid_plan()); + } + + /** + * Test that is_paid_plan() treats an unrecognised plan_type as paid, so + * newly-introduced paid Kit plans don't lock creators out until we ship + * an update. + * + * @since 3.4.0 + */ + public function testIsPaidPlanTrueForUnknownPlan() + { + update_option( + \ConvertKit_Account::OPTION_NAME, + [ + 'account' => [ + 'plan_type' => 'some_new_paid_plan', + ], + ] + ); + + $this->assertSame(true, $this->account->is_paid_plan()); + } + + /** + * Test that delete() clears the cache. + * + * @since 3.4.0 + */ + public function testDeleteClearsCache() + { + update_option( + \ConvertKit_Account::OPTION_NAME, + [ + 'account' => [ + 'plan_type' => 'creator', + ], + ] + ); + + $this->assertSame(true, $this->account->is_paid_plan()); + + $this->account->delete(); + + $this->assertSame(false, $this->account->get()); + $this->assertSame(false, $this->account->is_paid_plan()); + } + + /** + * Test that refresh() returns a WP_Error when no credentials are set. + * + * @since 3.4.0 + */ + public function testRefreshReturnsErrorWithoutCredentials() + { + delete_option($this->settings::SETTINGS_NAME); + + $result = $this->account->refresh(); + $this->assertInstanceOf(\WP_Error::class, $result); + } + + /** + * Test that a failed refresh() does not clobber an existing cache. + * + * @since 3.4.0 + */ + public function testRefreshFailureLeavesCacheIntact() + { + update_option( + \ConvertKit_Account::OPTION_NAME, + [ + 'account' => [ + 'plan_type' => 'creator_pro', + ], + ] + ); + + // Wipe credentials so refresh fails. + delete_option($this->settings::SETTINGS_NAME); + + $result = $this->account->refresh(); + $this->assertInstanceOf(\WP_Error::class, $result); + + // Cache should still be intact. + $cached = $this->account->get(); + $this->assertIsArray($cached); + $this->assertSame('creator_pro', $cached['account']['plan_type']); + } +} diff --git a/tests/Integration/SettingsMCPTest.php b/tests/Integration/SettingsMCPTest.php new file mode 100644 index 000000000..b7644dbc0 --- /dev/null +++ b/tests/Integration/SettingsMCPTest.php @@ -0,0 +1,132 @@ + '' ]); + update_option( + \ConvertKit_Account::OPTION_NAME, + [ 'account' => [ 'plan_type' => 'creator_pro' ] ] + ); + + $settings = new \ConvertKit_Settings_MCP(); + $this->assertSame(false, $settings->enabled()); + } + + /** + * Test that enabled() returns false when the toggle is on but no account is + * cached (fail closed). + * + * @since 3.4.0 + */ + public function testEnabledFalseWhenToggleOnAndNoAccountCache() + { + update_option(\ConvertKit_Settings_MCP::SETTINGS_NAME, [ 'enabled' => 'on' ]); + delete_option(\ConvertKit_Account::OPTION_NAME); + + $settings = new \ConvertKit_Settings_MCP(); + $this->assertSame(false, $settings->enabled()); + } + + /** + * Test that enabled() returns false when the toggle is on but the cached + * plan is free. + * + * @since 3.4.0 + */ + public function testEnabledFalseWhenToggleOnAndFreePlan() + { + update_option(\ConvertKit_Settings_MCP::SETTINGS_NAME, [ 'enabled' => 'on' ]); + update_option( + \ConvertKit_Account::OPTION_NAME, + [ 'account' => [ 'plan_type' => 'free' ] ] + ); + + $settings = new \ConvertKit_Settings_MCP(); + $this->assertSame(false, $settings->enabled()); + } + + /** + * Test that enabled() returns true when the toggle is on and the cached + * plan is a paid plan. + * + * @since 3.4.0 + */ + public function testEnabledTrueWhenToggleOnAndPaidPlan() + { + update_option(\ConvertKit_Settings_MCP::SETTINGS_NAME, [ 'enabled' => 'on' ]); + update_option( + \ConvertKit_Account::OPTION_NAME, + [ 'account' => [ 'plan_type' => 'creator' ] ] + ); + + $settings = new \ConvertKit_Settings_MCP(); + $this->assertSame(true, $settings->enabled()); + } + + /** + * Test that enabled() returns true for creator_pro plans. + * + * @since 3.4.0 + */ + public function testEnabledTrueForCreatorProPlan() + { + update_option(\ConvertKit_Settings_MCP::SETTINGS_NAME, [ 'enabled' => 'on' ]); + update_option( + \ConvertKit_Account::OPTION_NAME, + [ 'account' => [ 'plan_type' => 'creator_pro' ] ] + ); + + $settings = new \ConvertKit_Settings_MCP(); + $this->assertSame(true, $settings->enabled()); + } +} diff --git a/tests/Support/Helper/KitPlugin.php b/tests/Support/Helper/KitPlugin.php index 31384c1f5..8e722fb65 100644 --- a/tests/Support/Helper/KitPlugin.php +++ b/tests/Support/Helper/KitPlugin.php @@ -584,6 +584,9 @@ public function resetKitPlugin($I) $I->dontHaveOptionInDatabase('_wp_convertkit_settings_mcp'); $I->dontHaveOptionInDatabase('convertkit_version'); + // Account. + $I->dontHaveOptionInDatabase('convertkit_account'); + // Resources. $I->dontHaveOptionInDatabase('convertkit_broadcasts'); $I->dontHaveOptionInDatabase('convertkit_broadcasts_last_queried'); From 9d3f0dfa183b6ca2df3c62da667e62ccdcfd3e0a Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 6 Aug 2026 14:19:04 +0800 Subject: [PATCH 03/10] Fixing tests --- .../class-convertkit-admin-section-mcp.php | 32 +++++------ includes/class-convertkit-account.php | 12 +++- includes/class-convertkit-settings-mcp.php | 3 + includes/mcp/class-convertkit-mcp.php | 5 ++ .../plugin-screens/PluginSettingsMCPCest.php | 39 ++++++++++--- tests/Integration/AccountTest.php | 4 +- tests/Support/Helper/KitPlugin.php | 57 +++++++++++++++++++ tests/Support/Helper/WPRestAPI.php | 2 + 8 files changed, 122 insertions(+), 32 deletions(-) diff --git a/admin/section/class-convertkit-admin-section-mcp.php b/admin/section/class-convertkit-admin-section-mcp.php index 6233f8c20..b4f587249 100644 --- a/admin/section/class-convertkit-admin-section-mcp.php +++ b/admin/section/class-convertkit-admin-section-mcp.php @@ -63,9 +63,8 @@ public function __construct() { ), ); - // Refresh account details. - $account = new ConvertKit_Account(); - $this->account = $account->refresh(); + // Register the account class. + $this->account = new ConvertKit_Account(); $this->maybe_generate_authentication_header(); $this->maybe_revoke_application_password(); @@ -182,19 +181,6 @@ public function enqueue_scripts( $section ) { */ public function register_fields() { - // Only show MCP settings if the connected Kit account is on a paid plan. - if ( ! $this->account->is_paid_plan() ) { - add_settings_field( - 'upgrade_required', - __( 'Paid Plan Required', 'convertkit' ), - array( $this, 'upgrade_required_callback' ), - $this->settings_key, - $this->name - ); - - return; - } - // Enable. add_settings_field( 'enabled', @@ -274,11 +260,11 @@ public function documentation_url() { * * @since 3.4.0 */ - public function upgrade_required_callback() { + public function output_upgrade_required_message() { ?>

- +

@@ -298,6 +284,16 @@ public function upgrade_required_callback() { */ public function enabled_callback( $args ) { + // If the user doesn't have a paid plan, show the upgrade required message. + $this->account = new ConvertKit_Account(); + if ( ! $this->account->is_paid_plan() ) { + // Disable saving settings. + $this->save_disabled = true; + + $this->output_upgrade_required_message(); + return; + } + // Output field. $this->output_checkbox_field( $args['name'], diff --git a/includes/class-convertkit-account.php b/includes/class-convertkit-account.php index 8c41a71e5..59df87829 100644 --- a/includes/class-convertkit-account.php +++ b/includes/class-convertkit-account.php @@ -51,16 +51,19 @@ public function get() { * * @since 3.4.0 * - * @return string|null + * @return bool|string */ public function get_plan_type() { + // Get account details from cache. $account = $this->get(); + // If no account details are found, or the plan type is not set, return false. if ( ! $account || ! isset( $account['account']['plan_type'] ) ) { - return null; + return false; } + // Return the plan type. return (string) $account['account']['plan_type']; } @@ -74,12 +77,15 @@ public function get_plan_type() { */ public function is_paid_plan() { + // Get the plan type from the account details. $plan_type = $this->get_plan_type(); - if ( $plan_type === null ) { + // If no plan type is found, return false. + if ( ! $plan_type ) { return false; } + // Return true if the plan type is not free, false otherwise. return $plan_type !== 'free'; } diff --git a/includes/class-convertkit-settings-mcp.php b/includes/class-convertkit-settings-mcp.php index 7ba995798..288bdb957 100644 --- a/includes/class-convertkit-settings-mcp.php +++ b/includes/class-convertkit-settings-mcp.php @@ -74,12 +74,15 @@ public function get() { */ public function enabled() { + /* // Bail if the connected Kit account isn't on a paid plan. // This queries the cached account details, so no live API call is made. $account = new ConvertKit_Account(); if ( ! $account->is_paid_plan() ) { + error_log( 'not paid plan' ); return false; } + */ return ( $this->settings['enabled'] === 'on' ? true : false ); diff --git a/includes/mcp/class-convertkit-mcp.php b/includes/mcp/class-convertkit-mcp.php index 7b2e9013d..2d3a656ba 100644 --- a/includes/mcp/class-convertkit-mcp.php +++ b/includes/mcp/class-convertkit-mcp.php @@ -78,10 +78,13 @@ public static function get_server_url() { public function __construct() { // Bail if the MCP server isn't enabled. + /* $settings = new ConvertKit_Settings_MCP(); if ( ! $settings->enabled() ) { + error_log( 'mcp not enabled' ); return; } + */ // Register the ability category. add_action( 'wp_abilities_api_categories_init', array( $this, 'register_abilities_category' ) ); @@ -265,6 +268,8 @@ public function register_abilities() { */ public function register_mcp_server( $adapter ) { + error_log( 'register_mcp_server' ); + // Get abilities. $abilities = convertkit_get_abilities(); diff --git a/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php b/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php index a8ee143c3..d74639698 100644 --- a/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php +++ b/tests/EndToEnd/general/plugin-screens/PluginSettingsMCPCest.php @@ -22,9 +22,6 @@ public function _before(EndToEndTester $I) { // Activate Kit Plugin. $I->activateKitPlugin($I); - - // Setup Plugin. - $I->setupKitPlugin($I); } /** @@ -36,8 +33,20 @@ public function _before(EndToEndTester $I) */ public function testEnableAndDisableMCPServerSetting(EndToEndTester $I) { + // Simulate a Kit account that is on a paid plan. + $I->setupKitPlugin($I); + $I->haveOptionInDatabase( + 'convertkit_account', + [ + 'account' => [ + 'plan_type' => 'creator_pro', + ], + ] + ); + // Check that the MCP server is not registered. - $I->doesNotHaveRoute($I, '/kit-mcp'); + $I->doesNotHaveRoute($I, '/kit/mcp'); + $I->doesNotHaveRoute($I, '/kit/mcp/v1'); // Go to the Plugin's MCP Screen. $I->loadKitSettingsMCPScreen($I); @@ -88,6 +97,17 @@ public function testEnableAndDisableMCPServerSetting(EndToEndTester $I) */ public function testGenerateAndRevokeApplicationPassword(EndToEndTester $I) { + // Simulate a Kit account that is on a paid plan. + $I->setupKitPlugin($I); + $I->haveOptionInDatabase( + 'convertkit_account', + [ + 'account' => [ + 'plan_type' => 'creator_pro', + ], + ] + ); + // Go to the Plugin's MCP Screen. $I->loadKitSettingsMCPScreen($I); @@ -171,6 +191,7 @@ public function testFreePlanShowsUpgradeCTA(EndToEndTester $I) { // Simulate a Kit account that is on the free plan. $I->setupKitPluginFakeAPIKey($I); + $I->setupKitPluginResources($I); $I->haveOptionInDatabase( 'convertkit_account', [ @@ -192,8 +213,7 @@ public function testFreePlanShowsUpgradeCTA(EndToEndTester $I) $I->loadKitSettingsMCPScreen($I); // Assert that the upgrade CTA is shown. - $I->see('Paid Plan Required'); - $I->see('MCP is available on paid Kit plans.'); + $I->see('The Kit WordPress MCP is available on paid Kit plans. Upgrade your Kit account to connect AI clients to your WordPress site.'); $I->seeLink('Upgrade Kit Account'); // Assert no option to enable/disable the MCP server are shown. @@ -216,7 +236,8 @@ public function testFreePlanShowsUpgradeCTA(EndToEndTester $I) public function testPaidPlanShowsEnableUI(EndToEndTester $I) { // Simulate a Kit account that is on a paid plan. - $I->setupKitPluginFakeAPIKey($I); + $I->setupKitPlugin($I); + $I->setupKitPluginResources($I); $I->haveOptionInDatabase( 'convertkit_account', [ @@ -230,8 +251,8 @@ public function testPaidPlanShowsEnableUI(EndToEndTester $I) $I->loadKitSettingsMCPScreen($I); // The upgrade CTA should not be shown. - $I->dontSee('Paid Plan Required'); - $I->dontSee('Upgrade Kit Account'); + $I->dontSee('The Kit WordPress MCP is available on paid Kit plans. Upgrade your Kit account to connect AI clients to your WordPress site.'); + $I->dontSeeLink('Upgrade Kit Account'); // The Enable checkbox should be visible. $I->seeElement('#enabled'); diff --git a/tests/Integration/AccountTest.php b/tests/Integration/AccountTest.php index 7244108ad..ccf8e8eef 100644 --- a/tests/Integration/AccountTest.php +++ b/tests/Integration/AccountTest.php @@ -93,9 +93,9 @@ public function testGetReturnsFalseWhenNoCache() * * @since 3.4.0 */ - public function testGetPlanTypeReturnsNullWhenNoCache() + public function testGetPlanTypeReturnsFalseWhenNoCache() { - $this->assertSame(null, $this->account->get_plan_type()); + $this->assertSame(false, $this->account->get_plan_type()); } /** diff --git a/tests/Support/Helper/KitPlugin.php b/tests/Support/Helper/KitPlugin.php index affb8af26..d538c354b 100644 --- a/tests/Support/Helper/KitPlugin.php +++ b/tests/Support/Helper/KitPlugin.php @@ -511,12 +511,62 @@ public function setupKitPluginResources($I) ] ); + // Define Custom Fields. + $I->haveOptionInDatabase( + 'convertkit_custom_fields', + [ + 1075083 => [ + 'id' => 1075083, + 'name' => 'ck_field_1075083_url', + 'key' => 'url', + 'label' => 'URL', + ], + 276295 => [ + 'id' => 276295, + 'name' => 'Payment Method', + 'key' => 'ck_field_276295_payment_method', + 'label' => 'Payment Method', + ], + 276273 => [ + 'id' => 276273, + 'name' => 'Billing Address', + 'key' => 'ck_field_276273_billing_address', + 'label' => 'Billing Address', + ], + 276272 => [ + 'id' => 276272, + 'name' => 'Shipping Address', + 'key' => 'ck_field_276272_shipping_address', + 'label' => 'Shipping Address', + ], + 276271 => [ + 'id' => 276271, + 'name' => 'Phone Number', + 'key' => 'ck_field_276271_phone_number', + 'label' => 'Phone Number', + ], + 264073 => [ + 'id' => 264073, + 'name' => 'Last Name', + 'key' => 'ck_field_264073_last_name', + 'label' => 'Last Name', + ], + 258240 => [ + 'id' => 258240, + 'name' => 'ck_field_258240_notes', + 'key' => 'notes', + 'label' => 'Notes', + ], + ] + ); + // Define last queried to now for all resources, so they're not automatically immediately refreshed by the Plugin's logic. $I->haveOptionInDatabase( 'convertkit_forms_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_landing_pages_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_posts_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_products_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_tags_last_queried', strtotime( 'now' ) ); + $I->haveOptionInDatabase( 'convertkit_custom_fields_last_queried', strtotime( 'now' ) ); } /** @@ -566,6 +616,11 @@ public function setupKitPluginResourcesNoData($I) $I->haveOptionInDatabase( 'convertkit_posts_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_products_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_tags_last_queried', strtotime( 'now' ) ); + // Custom fields is instantiated by the Form Builder block's Custom Field + // block on every admin request. Without a fresh last-queried timestamp + // its constructor triggers refresh() → 401 against fake tokens → + // convertkit_maybe_delete_credentials wipes the fake tokens mid-request. + $I->haveOptionInDatabase( 'convertkit_custom_fields_last_queried', strtotime( 'now' ) ); } /** @@ -600,6 +655,8 @@ public function resetKitPlugin($I) $I->dontHaveOptionInDatabase('convertkit_products_last_queried'); $I->dontHaveOptionInDatabase('convertkit_tags'); $I->dontHaveOptionInDatabase('convertkit_tags_last_queried'); + $I->dontHaveOptionInDatabase('convertkit_custom_fields'); + $I->dontHaveOptionInDatabase('convertkit_custom_fields_last_queried'); // Persistent notices. $I->dontHaveOptionInDatabase('convertkit-admin-notices'); diff --git a/tests/Support/Helper/WPRestAPI.php b/tests/Support/Helper/WPRestAPI.php index 479a50a35..3d66fd400 100644 --- a/tests/Support/Helper/WPRestAPI.php +++ b/tests/Support/Helper/WPRestAPI.php @@ -19,6 +19,8 @@ class WPRestAPI extends \Codeception\Module */ public function hasRoute($I, $route) { + var_dump( $this->getRoutes() ); + die(); $I->assertTrue( in_array( $route, $this->getRoutes(), true ) ); } From b9631f022781bd502840a21b1907a5cb22cfdcc3 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 6 Aug 2026 14:34:01 +0800 Subject: [PATCH 04/10] Fix tests --- composer.json | 2 + includes/class-convertkit-settings-mcp.php | 3 - includes/mcp/class-convertkit-mcp.php | 5 -- tests/Support/Helper/KitPlugin.php | 68 +++++++++++----------- tests/Support/Helper/WPRestAPI.php | 2 - 5 files changed, 36 insertions(+), 44 deletions(-) diff --git a/composer.json b/composer.json index 851ce53ed..319b18988 100644 --- a/composer.json +++ b/composer.json @@ -55,10 +55,12 @@ "fix-js-coding-standards": "npm run fix:js", "php-static-analysis": "vendor/bin/phpstan analyse --memory-limit=1250M", "test": [ + "@composer update --no-interaction", "vendor/bin/codecept build @no_additional_args", "vendor/bin/codecept run EndToEnd @additional_args --fail-fast" ], "test-integration": [ + "@composer update --no-interaction", "vendor/bin/codecept build @no_additional_args", "vendor/bin/codecept run Integration @additional_args --fail-fast" ] diff --git a/includes/class-convertkit-settings-mcp.php b/includes/class-convertkit-settings-mcp.php index 288bdb957..7ba995798 100644 --- a/includes/class-convertkit-settings-mcp.php +++ b/includes/class-convertkit-settings-mcp.php @@ -74,15 +74,12 @@ public function get() { */ public function enabled() { - /* // Bail if the connected Kit account isn't on a paid plan. // This queries the cached account details, so no live API call is made. $account = new ConvertKit_Account(); if ( ! $account->is_paid_plan() ) { - error_log( 'not paid plan' ); return false; } - */ return ( $this->settings['enabled'] === 'on' ? true : false ); diff --git a/includes/mcp/class-convertkit-mcp.php b/includes/mcp/class-convertkit-mcp.php index 2d3a656ba..7b2e9013d 100644 --- a/includes/mcp/class-convertkit-mcp.php +++ b/includes/mcp/class-convertkit-mcp.php @@ -78,13 +78,10 @@ public static function get_server_url() { public function __construct() { // Bail if the MCP server isn't enabled. - /* $settings = new ConvertKit_Settings_MCP(); if ( ! $settings->enabled() ) { - error_log( 'mcp not enabled' ); return; } - */ // Register the ability category. add_action( 'wp_abilities_api_categories_init', array( $this, 'register_abilities_category' ) ); @@ -268,8 +265,6 @@ public function register_abilities() { */ public function register_mcp_server( $adapter ) { - error_log( 'register_mcp_server' ); - // Get abilities. $abilities = convertkit_get_abilities(); diff --git a/tests/Support/Helper/KitPlugin.php b/tests/Support/Helper/KitPlugin.php index d538c354b..2365967c9 100644 --- a/tests/Support/Helper/KitPlugin.php +++ b/tests/Support/Helper/KitPlugin.php @@ -516,46 +516,46 @@ public function setupKitPluginResources($I) 'convertkit_custom_fields', [ 1075083 => [ - 'id' => 1075083, - 'name' => 'ck_field_1075083_url', - 'key' => 'url', - 'label' => 'URL', + 'id' => 1075083, + 'name' => 'ck_field_1075083_url', + 'key' => 'url', + 'label' => 'URL', ], - 276295 => [ - 'id' => 276295, - 'name' => 'Payment Method', - 'key' => 'ck_field_276295_payment_method', - 'label' => 'Payment Method', + 276295 => [ + 'id' => 276295, + 'name' => 'Payment Method', + 'key' => 'ck_field_276295_payment_method', + 'label' => 'Payment Method', ], - 276273 => [ - 'id' => 276273, - 'name' => 'Billing Address', - 'key' => 'ck_field_276273_billing_address', - 'label' => 'Billing Address', + 276273 => [ + 'id' => 276273, + 'name' => 'Billing Address', + 'key' => 'ck_field_276273_billing_address', + 'label' => 'Billing Address', ], - 276272 => [ - 'id' => 276272, - 'name' => 'Shipping Address', - 'key' => 'ck_field_276272_shipping_address', - 'label' => 'Shipping Address', + 276272 => [ + 'id' => 276272, + 'name' => 'Shipping Address', + 'key' => 'ck_field_276272_shipping_address', + 'label' => 'Shipping Address', ], - 276271 => [ - 'id' => 276271, - 'name' => 'Phone Number', - 'key' => 'ck_field_276271_phone_number', - 'label' => 'Phone Number', + 276271 => [ + 'id' => 276271, + 'name' => 'Phone Number', + 'key' => 'ck_field_276271_phone_number', + 'label' => 'Phone Number', ], - 264073 => [ - 'id' => 264073, - 'name' => 'Last Name', - 'key' => 'ck_field_264073_last_name', - 'label' => 'Last Name', + 264073 => [ + 'id' => 264073, + 'name' => 'Last Name', + 'key' => 'ck_field_264073_last_name', + 'label' => 'Last Name', ], - 258240 => [ - 'id' => 258240, - 'name' => 'ck_field_258240_notes', - 'key' => 'notes', - 'label' => 'Notes', + 258240 => [ + 'id' => 258240, + 'name' => 'ck_field_258240_notes', + 'key' => 'notes', + 'label' => 'Notes', ], ] ); diff --git a/tests/Support/Helper/WPRestAPI.php b/tests/Support/Helper/WPRestAPI.php index 3d66fd400..479a50a35 100644 --- a/tests/Support/Helper/WPRestAPI.php +++ b/tests/Support/Helper/WPRestAPI.php @@ -19,8 +19,6 @@ class WPRestAPI extends \Codeception\Module */ public function hasRoute($I, $route) { - var_dump( $this->getRoutes() ); - die(); $I->assertTrue( in_array( $route, $this->getRoutes(), true ) ); } From 2a6d41f61712fd37c6bb6ee956c89d4e908c59de Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 6 Aug 2026 14:44:15 +0800 Subject: [PATCH 05/10] PHPStan compat. --- ...class-convertkit-admin-section-general.php | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/admin/section/class-convertkit-admin-section-general.php b/admin/section/class-convertkit-admin-section-general.php index 061ba7081..42e3f5a5b 100644 --- a/admin/section/class-convertkit-admin-section-general.php +++ b/admin/section/class-convertkit-admin-section-general.php @@ -15,16 +15,7 @@ class ConvertKit_Admin_Section_General extends ConvertKit_Admin_Section_Base { /** - * Holds the API instance. - * - * @since 1.9.6 - * - * @var ConvertKit_API_V4 - */ - private $api; - - /** - * Holds the ConvertKit Account Name. + * Holds the ConvertKit Account data. * * @since 1.9.6 * @@ -147,16 +138,6 @@ private function check_credentials() { exit(); } - // Initialize the API. - $this->api = new ConvertKit_API_V4( - CONVERTKIT_OAUTH_CLIENT_ID, - CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, - $this->settings->get_access_token(), - $this->settings->get_refresh_token(), - $this->settings->debug_enabled(), - 'settings' - ); - // Refresh account details. $account = new ConvertKit_Account(); $this->account = $account->refresh(); From 0df071e5f159c59c73b8d0dc03f19365ac8ce4a5 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 6 Aug 2026 16:32:33 +0800 Subject: [PATCH 06/10] Remove duplicate code --- composer.json | 2 - includes/class-convertkit-account.php | 151 ------------ includes/class-convertkit-settings-mcp.php | 2 +- tests/Integration/AccountTest.php | 255 --------------------- tests/Support/Helper/KitPlugin.php | 55 ----- wp-convertkit.php | 1 - 6 files changed, 1 insertion(+), 465 deletions(-) delete mode 100644 includes/class-convertkit-account.php delete mode 100644 tests/Integration/AccountTest.php diff --git a/composer.json b/composer.json index 319b18988..851ce53ed 100644 --- a/composer.json +++ b/composer.json @@ -55,12 +55,10 @@ "fix-js-coding-standards": "npm run fix:js", "php-static-analysis": "vendor/bin/phpstan analyse --memory-limit=1250M", "test": [ - "@composer update --no-interaction", "vendor/bin/codecept build @no_additional_args", "vendor/bin/codecept run EndToEnd @additional_args --fail-fast" ], "test-integration": [ - "@composer update --no-interaction", "vendor/bin/codecept build @no_additional_args", "vendor/bin/codecept run Integration @additional_args --fail-fast" ] diff --git a/includes/class-convertkit-account.php b/includes/class-convertkit-account.php deleted file mode 100644 index 59df87829..000000000 --- a/includes/class-convertkit-account.php +++ /dev/null @@ -1,151 +0,0 @@ -get(); - - // If no account details are found, or the plan type is not set, return false. - if ( ! $account || ! isset( $account['account']['plan_type'] ) ) { - return false; - } - - // Return the plan type. - return (string) $account['account']['plan_type']; - - } - - /** - * Returns whether the cached plan is a paid Kit plan. - * - * @since 3.4.0 - * - * @return bool - */ - public function is_paid_plan() { - - // Get the plan type from the account details. - $plan_type = $this->get_plan_type(); - - // If no plan type is found, return false. - if ( ! $plan_type ) { - return false; - } - - // Return true if the plan type is not free, false otherwise. - return $plan_type !== 'free'; - - } - - /** - * Fetches the account from Kit and caches the response. - * - * @since 3.4.0 - * - * @return WP_Error|array - */ - public function refresh() { - - // Initialize the settings object. - $settings = new ConvertKit_Settings(); - - // Bail if we don't have access and refresh tokens. - if ( ! $settings->has_access_and_refresh_token() ) { - return new WP_Error( - 'convertkit_account_no_credentials', - __( 'Cannot refresh account: no OAuth credentials configured.', 'convertkit' ) - ); - } - - // Initialize the API client. - $api = new ConvertKit_API_V4( - CONVERTKIT_OAUTH_CLIENT_ID, - CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, - $settings->get_access_token(), - $settings->get_refresh_token(), - $settings->debug_enabled(), - 'account' - ); - - // Fetch the account. - $account = $api->get_account(); - - // Bail if there was an error. - if ( is_wp_error( $account ) ) { - return $account; - } - - // Update the cached account. - update_option( self::OPTION_NAME, $account ); - - return $account; - - } - - /** - * Deletes the cached account. - * - * @since 3.4.0 - * - * @return bool - */ - public function delete() { - - return delete_option( self::OPTION_NAME ); - - } - -} diff --git a/includes/class-convertkit-settings-mcp.php b/includes/class-convertkit-settings-mcp.php index 7ba995798..8e62099b2 100644 --- a/includes/class-convertkit-settings-mcp.php +++ b/includes/class-convertkit-settings-mcp.php @@ -76,7 +76,7 @@ public function enabled() { // Bail if the connected Kit account isn't on a paid plan. // This queries the cached account details, so no live API call is made. - $account = new ConvertKit_Account(); + $account = new ConvertKit_Resource_Account(); if ( ! $account->is_paid_plan() ) { return false; } diff --git a/tests/Integration/AccountTest.php b/tests/Integration/AccountTest.php deleted file mode 100644 index ccf8e8eef..000000000 --- a/tests/Integration/AccountTest.php +++ /dev/null @@ -1,255 +0,0 @@ -settings = new \ConvertKit_Settings(); - update_option( - $this->settings::SETTINGS_NAME, - [ - 'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], - 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'], - ] - ); - - $this->account = new \ConvertKit_Account(); - } - - /** - * Performs actions after each test. - * - * @since 3.4.0 - */ - public function tearDown(): void - { - delete_option($this->settings::SETTINGS_NAME); - delete_option(\ConvertKit_Account::OPTION_NAME); - - unset($this->account); - - deactivate_plugins('convertkit/wp-convertkit.php'); - - parent::tearDown(); - } - - /** - * Test that get() returns false when no cache has been written yet. - * - * @since 3.4.0 - */ - public function testGetReturnsFalseWhenNoCache() - { - $this->assertSame(false, $this->account->get()); - } - - /** - * Test that get_plan_type() returns null when no cache has been written yet. - * - * @since 3.4.0 - */ - public function testGetPlanTypeReturnsFalseWhenNoCache() - { - $this->assertSame(false, $this->account->get_plan_type()); - } - - /** - * Test that is_paid_plan() fails closed when no cache has been written yet. - * - * @since 3.4.0 - */ - public function testIsPaidPlanFailsClosedWhenNoCache() - { - $this->assertSame(false, $this->account->is_paid_plan()); - } - - /** - * Test that refresh() fetches the account from Kit and caches it. - * - * @since 3.4.0 - */ - public function testRefreshPopulatesCache() - { - $result = $this->account->refresh(); - $this->assertNotInstanceOf(\WP_Error::class, $result); - $this->assertIsArray($result); - $this->assertArrayHasKey('account', $result); - $this->assertArrayHasKey('plan_type', $result['account']); - - // Confirm the value was persisted. - $cached = $this->account->get(); - $this->assertIsArray($cached); - $this->assertSame($result['account']['plan_type'], $cached['account']['plan_type']); - } - - /** - * Test that is_paid_plan() returns true when the cached plan_type is not free. - * - * @since 3.4.0 - */ - public function testIsPaidPlanTrueForPaidPlan() - { - update_option( - \ConvertKit_Account::OPTION_NAME, - [ - 'account' => [ - 'plan_type' => 'creator_pro', - ], - ] - ); - - $this->assertSame('creator_pro', $this->account->get_plan_type()); - $this->assertSame(true, $this->account->is_paid_plan()); - } - - /** - * Test that is_paid_plan() returns false when the cached plan_type is free. - * - * @since 3.4.0 - */ - public function testIsPaidPlanFalseForFreePlan() - { - update_option( - \ConvertKit_Account::OPTION_NAME, - [ - 'account' => [ - 'plan_type' => 'free', - ], - ] - ); - - $this->assertSame('free', $this->account->get_plan_type()); - $this->assertSame(false, $this->account->is_paid_plan()); - } - - /** - * Test that is_paid_plan() treats an unrecognised plan_type as paid, so - * newly-introduced paid Kit plans don't lock creators out until we ship - * an update. - * - * @since 3.4.0 - */ - public function testIsPaidPlanTrueForUnknownPlan() - { - update_option( - \ConvertKit_Account::OPTION_NAME, - [ - 'account' => [ - 'plan_type' => 'some_new_paid_plan', - ], - ] - ); - - $this->assertSame(true, $this->account->is_paid_plan()); - } - - /** - * Test that delete() clears the cache. - * - * @since 3.4.0 - */ - public function testDeleteClearsCache() - { - update_option( - \ConvertKit_Account::OPTION_NAME, - [ - 'account' => [ - 'plan_type' => 'creator', - ], - ] - ); - - $this->assertSame(true, $this->account->is_paid_plan()); - - $this->account->delete(); - - $this->assertSame(false, $this->account->get()); - $this->assertSame(false, $this->account->is_paid_plan()); - } - - /** - * Test that refresh() returns a WP_Error when no credentials are set. - * - * @since 3.4.0 - */ - public function testRefreshReturnsErrorWithoutCredentials() - { - delete_option($this->settings::SETTINGS_NAME); - - $result = $this->account->refresh(); - $this->assertInstanceOf(\WP_Error::class, $result); - } - - /** - * Test that a failed refresh() does not clobber an existing cache. - * - * @since 3.4.0 - */ - public function testRefreshFailureLeavesCacheIntact() - { - update_option( - \ConvertKit_Account::OPTION_NAME, - [ - 'account' => [ - 'plan_type' => 'creator_pro', - ], - ] - ); - - // Wipe credentials so refresh fails. - delete_option($this->settings::SETTINGS_NAME); - - $result = $this->account->refresh(); - $this->assertInstanceOf(\WP_Error::class, $result); - - // Cache should still be intact. - $cached = $this->account->get(); - $this->assertIsArray($cached); - $this->assertSame('creator_pro', $cached['account']['plan_type']); - } -} diff --git a/tests/Support/Helper/KitPlugin.php b/tests/Support/Helper/KitPlugin.php index 2365967c9..b27efb6b6 100644 --- a/tests/Support/Helper/KitPlugin.php +++ b/tests/Support/Helper/KitPlugin.php @@ -511,62 +511,12 @@ public function setupKitPluginResources($I) ] ); - // Define Custom Fields. - $I->haveOptionInDatabase( - 'convertkit_custom_fields', - [ - 1075083 => [ - 'id' => 1075083, - 'name' => 'ck_field_1075083_url', - 'key' => 'url', - 'label' => 'URL', - ], - 276295 => [ - 'id' => 276295, - 'name' => 'Payment Method', - 'key' => 'ck_field_276295_payment_method', - 'label' => 'Payment Method', - ], - 276273 => [ - 'id' => 276273, - 'name' => 'Billing Address', - 'key' => 'ck_field_276273_billing_address', - 'label' => 'Billing Address', - ], - 276272 => [ - 'id' => 276272, - 'name' => 'Shipping Address', - 'key' => 'ck_field_276272_shipping_address', - 'label' => 'Shipping Address', - ], - 276271 => [ - 'id' => 276271, - 'name' => 'Phone Number', - 'key' => 'ck_field_276271_phone_number', - 'label' => 'Phone Number', - ], - 264073 => [ - 'id' => 264073, - 'name' => 'Last Name', - 'key' => 'ck_field_264073_last_name', - 'label' => 'Last Name', - ], - 258240 => [ - 'id' => 258240, - 'name' => 'ck_field_258240_notes', - 'key' => 'notes', - 'label' => 'Notes', - ], - ] - ); - // Define last queried to now for all resources, so they're not automatically immediately refreshed by the Plugin's logic. $I->haveOptionInDatabase( 'convertkit_forms_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_landing_pages_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_posts_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_products_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_tags_last_queried', strtotime( 'now' ) ); - $I->haveOptionInDatabase( 'convertkit_custom_fields_last_queried', strtotime( 'now' ) ); } /** @@ -639,9 +589,6 @@ public function resetKitPlugin($I) $I->dontHaveOptionInDatabase('_wp_convertkit_settings_mcp'); $I->dontHaveOptionInDatabase('convertkit_version'); - // Account. - $I->dontHaveOptionInDatabase('convertkit_account'); - // Resources. $I->dontHaveOptionInDatabase('convertkit_broadcasts'); $I->dontHaveOptionInDatabase('convertkit_broadcasts_last_queried'); @@ -655,8 +602,6 @@ public function resetKitPlugin($I) $I->dontHaveOptionInDatabase('convertkit_products_last_queried'); $I->dontHaveOptionInDatabase('convertkit_tags'); $I->dontHaveOptionInDatabase('convertkit_tags_last_queried'); - $I->dontHaveOptionInDatabase('convertkit_custom_fields'); - $I->dontHaveOptionInDatabase('convertkit_custom_fields_last_queried'); // Persistent notices. $I->dontHaveOptionInDatabase('convertkit-admin-notices'); diff --git a/wp-convertkit.php b/wp-convertkit.php index 707b78217..a2e99e7e3 100644 --- a/wp-convertkit.php +++ b/wp-convertkit.php @@ -53,7 +53,6 @@ require_once CONVERTKIT_PLUGIN_PATH . '/includes/cron-functions.php'; 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-account.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-admin-notices.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-broadcasts-exporter.php'; require_once CONVERTKIT_PLUGIN_PATH . '/includes/class-convertkit-broadcasts-importer.php'; From e25477e9ad917f03d03d62a47b2b90823f7913ba Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 6 Aug 2026 16:35:05 +0800 Subject: [PATCH 07/10] Remove account class --- .../class-convertkit-admin-section-mcp.php | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/admin/section/class-convertkit-admin-section-mcp.php b/admin/section/class-convertkit-admin-section-mcp.php index b4f587249..4d04a0722 100644 --- a/admin/section/class-convertkit-admin-section-mcp.php +++ b/admin/section/class-convertkit-admin-section-mcp.php @@ -23,16 +23,6 @@ class ConvertKit_Admin_Section_MCP extends ConvertKit_Admin_Section_Base { */ private $authorization_header = false; - /** - * The Account class instance, used to check whether the connected Kit - * account is on a paid plan (required for MCP access). - * - * @since 3.4.0 - * - * @var ConvertKit_Account - */ - private $account; - /** * Constructor. * @@ -63,9 +53,6 @@ public function __construct() { ), ); - // Register the account class. - $this->account = new ConvertKit_Account(); - $this->maybe_generate_authentication_header(); $this->maybe_revoke_application_password(); @@ -285,8 +272,8 @@ public function output_upgrade_required_message() { public function enabled_callback( $args ) { // If the user doesn't have a paid plan, show the upgrade required message. - $this->account = new ConvertKit_Account(); - if ( ! $this->account->is_paid_plan() ) { + $account = new ConvertKit_Resource_Account(); + if ( ! $account->is_paid_plan() ) { // Disable saving settings. $this->save_disabled = true; From 5813f52654faf1da31ef66578eba8920bb73a21f Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Thu, 6 Aug 2026 16:36:07 +0800 Subject: [PATCH 08/10] Remove custom field code --- tests/Support/Helper/KitPlugin.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Support/Helper/KitPlugin.php b/tests/Support/Helper/KitPlugin.php index b27efb6b6..9f0079f56 100644 --- a/tests/Support/Helper/KitPlugin.php +++ b/tests/Support/Helper/KitPlugin.php @@ -566,11 +566,6 @@ public function setupKitPluginResourcesNoData($I) $I->haveOptionInDatabase( 'convertkit_posts_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_products_last_queried', strtotime( 'now' ) ); $I->haveOptionInDatabase( 'convertkit_tags_last_queried', strtotime( 'now' ) ); - // Custom fields is instantiated by the Form Builder block's Custom Field - // block on every admin request. Without a fresh last-queried timestamp - // its constructor triggers refresh() → 401 against fake tokens → - // convertkit_maybe_delete_credentials wipes the fake tokens mid-request. - $I->haveOptionInDatabase( 'convertkit_custom_fields_last_queried', strtotime( 'now' ) ); } /** From f784fec5ca2bf422f6fcf18bcf16a7cf6e69124a Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 7 Aug 2026 19:35:50 +0800 Subject: [PATCH 09/10] Fix tests --- tests/Integration/SettingsMCPTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Integration/SettingsMCPTest.php b/tests/Integration/SettingsMCPTest.php index b7644dbc0..edb393b29 100644 --- a/tests/Integration/SettingsMCPTest.php +++ b/tests/Integration/SettingsMCPTest.php @@ -39,7 +39,7 @@ public function setUp(): void public function tearDown(): void { delete_option(\ConvertKit_Settings_MCP::SETTINGS_NAME); - delete_option(\ConvertKit_Account::OPTION_NAME); + delete_option('convertkit_account'); deactivate_plugins('convertkit/wp-convertkit.php'); parent::tearDown(); } @@ -54,7 +54,7 @@ public function testEnabledFalseWhenToggleOff() { update_option(\ConvertKit_Settings_MCP::SETTINGS_NAME, [ 'enabled' => '' ]); update_option( - \ConvertKit_Account::OPTION_NAME, + 'convertkit_account', [ 'account' => [ 'plan_type' => 'creator_pro' ] ] ); @@ -71,7 +71,7 @@ public function testEnabledFalseWhenToggleOff() public function testEnabledFalseWhenToggleOnAndNoAccountCache() { update_option(\ConvertKit_Settings_MCP::SETTINGS_NAME, [ 'enabled' => 'on' ]); - delete_option(\ConvertKit_Account::OPTION_NAME); + delete_option('convertkit_account'); $settings = new \ConvertKit_Settings_MCP(); $this->assertSame(false, $settings->enabled()); @@ -87,7 +87,7 @@ public function testEnabledFalseWhenToggleOnAndFreePlan() { update_option(\ConvertKit_Settings_MCP::SETTINGS_NAME, [ 'enabled' => 'on' ]); update_option( - \ConvertKit_Account::OPTION_NAME, + 'convertkit_account', [ 'account' => [ 'plan_type' => 'free' ] ] ); @@ -105,7 +105,7 @@ public function testEnabledTrueWhenToggleOnAndPaidPlan() { update_option(\ConvertKit_Settings_MCP::SETTINGS_NAME, [ 'enabled' => 'on' ]); update_option( - \ConvertKit_Account::OPTION_NAME, + 'convertkit_account', [ 'account' => [ 'plan_type' => 'creator' ] ] ); @@ -122,7 +122,7 @@ public function testEnabledTrueForCreatorProPlan() { update_option(\ConvertKit_Settings_MCP::SETTINGS_NAME, [ 'enabled' => 'on' ]); update_option( - \ConvertKit_Account::OPTION_NAME, + 'convertkit_account', [ 'account' => [ 'plan_type' => 'creator_pro' ] ] ); From e740f48b4c8f48ab8de81e764423a88584d2da0c Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 7 Aug 2026 20:53:55 +0800 Subject: [PATCH 10/10] Move enable/register MCP server gate --- includes/mcp/class-convertkit-mcp.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/mcp/class-convertkit-mcp.php b/includes/mcp/class-convertkit-mcp.php index 7b2e9013d..cbc0b8559 100644 --- a/includes/mcp/class-convertkit-mcp.php +++ b/includes/mcp/class-convertkit-mcp.php @@ -77,12 +77,6 @@ public static function get_server_url() { */ public function __construct() { - // Bail if the MCP server isn't enabled. - $settings = new ConvertKit_Settings_MCP(); - if ( ! $settings->enabled() ) { - return; - } - // Register the ability category. add_action( 'wp_abilities_api_categories_init', array( $this, 'register_abilities_category' ) ); @@ -265,6 +259,12 @@ public function register_abilities() { */ public function register_mcp_server( $adapter ) { + // Bail if the MCP server isn't enabled. + $settings = new ConvertKit_Settings_MCP(); + if ( ! $settings->enabled() ) { + return; + } + // Get abilities. $abilities = convertkit_get_abilities();