From feee18ad98c556ef2121cafe6ec9ebbbf8526cd9 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 2 Feb 2026 17:59:44 +0800 Subject: [PATCH 1/3] Setup Wizard: Form Importers --- .../class-convertkit-admin-importer.php | 5 + ...s-convertkit-admin-setup-wizard-plugin.php | 177 ++++++++++++------ .../content-form-importer.php | 71 +++++++ 3 files changed, 195 insertions(+), 58 deletions(-) create mode 100644 views/backend/setup-wizard/convertkit-setup/content-form-importer.php diff --git a/admin/importers/class-convertkit-admin-importer.php b/admin/importers/class-convertkit-admin-importer.php index 94877e72e..6d218bc41 100644 --- a/admin/importers/class-convertkit-admin-importer.php +++ b/admin/importers/class-convertkit-admin-importer.php @@ -119,6 +119,11 @@ public function import( $mappings ) { // Iterate through the mappings, replacing the third party form shortcodes and blocks with the Kit form shortcodes and blocks. foreach ( $mappings as $third_party_form_id => $kit_form_id ) { + // Skip empty Kit Form IDs i.e. no mapping was provided for this third party form. + if ( empty( $kit_form_id ) ) { + continue; + } + $this->replace_blocks_in_posts( (int) $third_party_form_id, (int) $kit_form_id ); $this->replace_shortcodes_in_posts( (int) $third_party_form_id, (int) $kit_form_id ); } diff --git a/admin/setup-wizard/class-convertkit-admin-setup-wizard-plugin.php b/admin/setup-wizard/class-convertkit-admin-setup-wizard-plugin.php index 154f0dd9c..24f517177 100644 --- a/admin/setup-wizard/class-convertkit-admin-setup-wizard-plugin.php +++ b/admin/setup-wizard/class-convertkit-admin-setup-wizard-plugin.php @@ -92,6 +92,24 @@ class ConvertKit_Admin_Setup_Wizard_Plugin extends ConvertKit_Admin_Setup_Wizard */ public $exit_url = 'options-general.php?page=_wp_convertkit_settings'; + /** + * If the Form Importer step will be displayed. + * + * @since 3.1.7 + * + * @var bool + */ + public $show_form_importer_step = false; + + /** + * Holds the form importers. + * + * @since 3.1.7 + * + * @var array + */ + public $form_importers = array(); + /** * Registers action and filter hooks. * @@ -103,6 +121,8 @@ public function __construct() { $this->api = new ConvertKit_API_V4( CONVERTKIT_OAUTH_CLIENT_ID, CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI, false, false, false, 'setup_wizard' ); $this->settings = new ConvertKit_Settings(); + $this->show_form_importer_step = count( convertkit_get_form_importers() ) > 0 ? true : false; + // Define details for each step in the setup process. $this->steps = array( 'start' => array( @@ -115,12 +135,24 @@ public function __construct() { 'configuration' => array( 'name' => __( 'Configuration', 'convertkit' ), 'next_button' => array( - 'label' => __( 'Finish Setup', 'convertkit' ), + 'label' => $this->show_form_importer_step ? __( 'Next', 'convertkit' ) : __( 'Finish Setup', 'convertkit' ), ), ), - 'finish' => array( - 'name' => __( 'Done', 'convertkit' ), - ), + ); + + // If the Form Importer step will be displayed, add it to the steps. + if ( $this->show_form_importer_step ) { + $this->steps['form-importer'] = array( + 'name' => __( 'Form Importer', 'convertkit' ), + 'next_button' => array( + 'label' => __( 'Finish Setup', 'convertkit' ), + ), + ); + } + + // Add the finish step. + $this->steps['finish'] = array( + 'name' => __( 'Done', 'convertkit' ), ); // Register link to Setup Wizard below Plugin Name at Plugins > Installed Plugins. @@ -209,69 +241,92 @@ public function maybe_redirect_to_setup_screen() { public function process_form( $step ) { // Depending on the step, process the form data. - switch ( $step ) { - case 'configuration': - // If an error occured from OAuth i.e. the user did not authorize, show it now. - if ( array_key_exists( 'error', $_REQUEST ) && array_key_exists( 'error_description', $_REQUEST ) ) { - // Decrement the step. - $this->step = 'start'; - $this->error = sanitize_text_field( wp_unslash( $_REQUEST['error_description'] ) ); - return; - } + if ( array_key_exists( 'code', $_REQUEST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - no nonce is sent back from OAuth. + $this->save_oauth( map_deep( wp_unslash( $_REQUEST ), 'sanitize_text_field' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - no nonce is sent back from OAuth. + return; + } - // Bail if no authorization code is included in the request. - if ( ! array_key_exists( 'code', $_REQUEST ) ) { - return; - } + // Run security checks. + if ( ! isset( $_REQUEST['_wpnonce'] ) ) { + return; + } + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), $this->page_name ) ) { + // Decrement the step. + $this->step = 'configuration'; + $this->error = __( 'Invalid nonce specified.', 'convertkit' ); + return; + } - // Sanitize token. - $authorization_code = sanitize_text_field( wp_unslash( $_REQUEST['code'] ) ); + // Configuration. + if ( array_key_exists( 'post_form', $_REQUEST ) || array_key_exists( 'page_form', $_REQUEST ) || array_key_exists( 'usage_tracking', $_REQUEST ) ) { + $settings = new ConvertKit_Settings(); + $settings->save( + array( + 'post_form' => isset( $_REQUEST['post_form'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['post_form'] ) ) : '0', + 'page_form' => isset( $_REQUEST['page_form'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page_form'] ) ) : '0', + 'usage_tracking' => isset( $_REQUEST['usage_tracking'] ) ? 'on' : '', + ) + ); + return; + } - // Exchange the authorization code and verifier for an access token. - $result = $this->api->get_access_token( $authorization_code ); + // Form Importer. + if ( array_key_exists( 'form_importer', $_REQUEST ) ) { + // Replace third party form shortcodes and blocks with Kit form shortcodes and blocks. + foreach ( map_deep( wp_unslash( $_REQUEST['form_importer'] ), 'sanitize_text_field' ) as $form_importer_name => $mappings ) { + // Sanitize mappings. + $mappings = array_map( 'sanitize_text_field', wp_unslash( $mappings ) ); - // Show an error message if we could not fetch the access token. - if ( is_wp_error( $result ) ) { - // Decrement the step. - $this->step = 'start'; - $this->error = $result->get_error_message(); - return; - } + // Replace third party form shortcodes and blocks with Kit form shortcodes and blocks. + WP_ConvertKit()->get_class( 'admin_importer_' . $form_importer_name )->import( $mappings ); + } - // Store Access Token, Refresh Token and expiry. - $this->settings->save( - array( - 'access_token' => $result['access_token'], - 'refresh_token' => $result['refresh_token'], - 'token_expires' => ( time() + $result['expires_in'] ), - ) - ); - break; + return; + } - case 'finish': - // Run security checks. - if ( ! isset( $_REQUEST['_wpnonce'] ) ) { - return; - } - if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), $this->page_name ) ) { - // Decrement the step. - $this->step = 'configuration'; - $this->error = __( 'Invalid nonce specified.', 'convertkit' ); - return; - } + } - // Save Default Page and Post Form settings. - $settings = new ConvertKit_Settings(); - $settings->save( - array( - 'post_form' => ( isset( $_POST['post_form'] ) ? sanitize_text_field( wp_unslash( $_POST['post_form'] ) ) : '0' ), - 'page_form' => ( isset( $_POST['page_form'] ) ? sanitize_text_field( wp_unslash( $_POST['page_form'] ) ) : '0' ), - 'usage_tracking' => ( isset( $_POST['usage_tracking'] ) ? 'on' : '' ), - ) - ); - break; + /** + * Save the OAuth credentials. + * + * @since 3.1.7 + * + * @param array $request Request. + * @return void + */ + private function save_oauth( $request ) { + + // If an error occured from OAuth i.e. the user did not authorize, show it now. + if ( array_key_exists( 'error', $request ) && array_key_exists( 'error_description', $request ) ) { + // Decrement the step. + $this->step = 'start'; + $this->error = sanitize_text_field( wp_unslash( $request['error_description'] ) ); + return; + } + + // Sanitize token. + $authorization_code = sanitize_text_field( wp_unslash( $request['code'] ) ); + + // Exchange the authorization code and verifier for an access token. + $result = $this->api->get_access_token( $authorization_code ); + + // Show an error message if we could not fetch the access token. + if ( is_wp_error( $result ) ) { + // Decrement the step. + $this->step = 'start'; + $this->error = $result->get_error_message(); + return; } + // Store Access Token, Refresh Token and expiry. + $this->settings->save( + array( + 'access_token' => $result['access_token'], + 'refresh_token' => $result['refresh_token'], + 'token_expires' => ( time() + $result['expires_in'] ), + ) + ); + } /** @@ -356,6 +411,12 @@ function ( $hosts ) { $this->preview_post_url = WP_ConvertKit()->get_class( 'preview_output' )->get_preview_form_url( 'post' ); $this->preview_page_url = WP_ConvertKit()->get_class( 'preview_output' )->get_preview_form_url( 'page' ); break; + + case 'form-importer': + // Fetch form importers and Kit Forms. + $this->form_importers = convertkit_get_form_importers(); + $this->forms = new ConvertKit_Resource_Forms( 'setup_wizard' ); + break; } } diff --git a/views/backend/setup-wizard/convertkit-setup/content-form-importer.php b/views/backend/setup-wizard/convertkit-setup/content-form-importer.php new file mode 100644 index 000000000..3b5a6d662 --- /dev/null +++ b/views/backend/setup-wizard/convertkit-setup/content-form-importer.php @@ -0,0 +1,71 @@ + Form Importer step. + * + * @package ConvertKit + * @author ConvertKit + */ + +foreach ( $this->form_importers as $form_importer ) { + ?> +
+

+

+ +

+ + + + + + + + + + $form_importer_form_title ) { + ?> + + + + + + +
+ + + +
+ forms->output_select_field_all( + 'form_importer[' . $form_importer['name'] . '][' . $form_importer_form_id . ']', + 'form-importer-' . $form_importer['name'] . '-' . $form_importer_form_id, + array( + 'convertkit-select2', + 'widefat', + ), + '', + array( + '0' => esc_html__( 'Don\'t replace.', 'convertkit' ), + ) + ); + ?> +
+
+ Date: Mon, 2 Feb 2026 18:23:03 +0800 Subject: [PATCH 2/3] Added Tests --- .../plugin-screens/PluginSetupWizardCest.php | 114 +++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/tests/EndToEnd/general/plugin-screens/PluginSetupWizardCest.php b/tests/EndToEnd/general/plugin-screens/PluginSetupWizardCest.php index ecbd7cfab..33ed640af 100644 --- a/tests/EndToEnd/general/plugin-screens/PluginSetupWizardCest.php +++ b/tests/EndToEnd/general/plugin-screens/PluginSetupWizardCest.php @@ -535,6 +535,115 @@ public function testSetupWizardFormConfigurationScreenWhenNoPostsOrPagesExist(En $I->dontSeeElementInDOM('a#convertkit-preview-form-page'); } + /** + * Test that the Setup Wizard > Form Importer screen works as expected + * when Active Campaign Forms exist. + * + * @since 3.1.7 + * + * @param EndToEndTester $I Tester. + */ + public function testSetupWizardFormImporterScreen(EndToEndTester $I) + { + // Activate Plugin. + $this->_activatePlugin($I); + + // Wait for the Plugin Setup Wizard screen to load. + $I->waitForElementVisible('body.convertkit'); + + // Define Plugin settings and resources. + $I->setupKitPluginResources($I); + $I->setupKitPluginNoDefaultForms($I); + + // Create Active Campaign Forms. + $I->haveOptionInDatabase( + 'settings_activecampaign', + [ + 'forms' => [ + 1 => [ + 'id' => '1', + 'name' => 'ActiveCampaign Form #1', + ], + 2 => [ + 'id' => '2', + 'name' => 'ActiveCampaign Form #2', + ], + ], + ] + ); + + // Create Pages with ActiveCampaign Form Shortcode and Block. + $pageIDs = [ + $I->havePostInDatabase( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Page with ActiveCampaign Form', + 'post_content' => '[activecampaign form="1"]', + 'meta_input' => [ + '_wp_convertkit_post_meta' => [ + 'form' => '0', + 'landing_page' => '', + 'tag' => '', + ], + ], + ] + ), + $I->havePostInDatabase( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => 'Page with ActiveCampaign Block', + 'post_content' => '
Some content with characters !@£$%^&*()_+~!@£$%^&*()_+\\\
', + + // Configure Kit Plugin to not display a default Form, so we test against the Kit Form in the content. + 'meta_input' => [ + '_wp_convertkit_post_meta' => [ + 'form' => '0', + 'landing_page' => '', + 'tag' => '', + ], + ], + ] + ), + ]; + + // Load Form Importer screen of Setup Wizard. + $I->amOnAdminPage('options.php?page=convertkit-setup&step=form-importer'); + + // Select the Kit Forms to replace the ActiveCampaign Forms. + $I->fillSelect2Field( + $I, + container: '#select2-form-importer-activecampaign-1-container', + value: $_ENV['CONVERTKIT_API_FORM_NAME'] + ); + $I->fillSelect2Field( + $I, + container: '#select2-form-importer-activecampaign-2-container', + value: $_ENV['CONVERTKIT_API_FORM_NAME'] + ); + + // Click the Finish Setup button. + $I->click('Finish Setup'); + + // Confirm expected setup wizard screen is displayed. + $this->_seeExpectedSetupWizardScreen( + $I, + step: 'finish', + stepCount: 4, + totalSteps: 4, + title: 'Setup complete' + ); + + // Test each Page. + foreach ($pageIDs as $pageID) { + $I->amOnPage('?p=' . $pageID); + + // Check Kit Forms are displayed. + $I->seeElementInDOM('form[data-sv-form]'); + } + } + /** * Tests that a link to the Setup Wizard exists on the Plugins screen, and works when clicked. * @@ -606,8 +715,9 @@ private function _activatePlugin(EndToEndTester $I) * @param int $stepCount Current step count. * @param string $title Expected title. * @param bool $nextButtonIsLink Check that next button is a link (false = must be a