diff --git a/admin/class-convertkit-admin-legacy-resource-notice.php b/admin/class-convertkit-admin-legacy-resource-notice.php
new file mode 100644
index 000000000..259555f0a
--- /dev/null
+++ b/admin/class-convertkit-admin-legacy-resource-notice.php
@@ -0,0 +1,265 @@
+get_current_post_id();
+ if ( ! $post_id ) {
+ return;
+ }
+
+ // Get warnings.
+ $warnings = $this->get_legacy_warnings_for_post_settings( $post_id );
+
+ // Bail if no warnings are found.
+ if ( empty( $warnings ) ) {
+ return;
+ }
+
+ // Output warnings.
+ ?>
+
+
+ :
+
+
+
+ ' . esc_html( $warning ) . '';
+ }
+ ?>
+
+
+ get_current_post_id();
+ if ( ! $post_id ) {
+ return;
+ }
+
+ // Get warnings.
+ $warnings = $this->get_legacy_warnings_for_post_settings( $post_id );
+
+ // Bail if no warnings are found.
+ if ( empty( $warnings ) ) {
+ return;
+ }
+
+ // Enqueue script.
+ wp_enqueue_script(
+ 'convertkit-admin-legacy-resource-notice',
+ CONVERTKIT_PLUGIN_URL . 'resources/backend/js/legacy-resource-notice.js',
+ array( 'wp-data', 'wp-notices' ),
+ CONVERTKIT_PLUGIN_VERSION,
+ true
+ );
+
+ // Localize script.
+ wp_localize_script(
+ 'convertkit-admin-legacy-resource-notice',
+ 'convertkit_legacy_resource_notice',
+ array(
+ 'id' => self::GUTENBERG_NOTICE_ID,
+ 'intro' => __( 'Kit: This page uses one or more legacy Kit resources that are no longer offered for new selections. The following continue to work, but we recommend migrating:', 'convertkit' ),
+ 'warnings' => $warnings,
+ )
+ );
+
+ }
+
+ /**
+ * Returns an array of warning strings for the given Post, if the Post settings
+ * reference a Legacy Form or Legacy Landing Page.
+ *
+ * @since 3.3.7
+ *
+ * @param int $post_id Post ID.
+ * @return array
+ */
+ private function get_legacy_warnings_for_post_settings( $post_id ) {
+
+ // Get Post settings.
+ $convertkit_post = new ConvertKit_Post( $post_id );
+ $settings = $convertkit_post->get();
+
+ // Get resources.
+ $forms = new ConvertKit_Resource_Forms();
+ $landing_pages = new ConvertKit_Resource_Landing_Pages();
+
+ // Initialize warnings array.
+ $warnings = array();
+
+ // Form.
+ if ( $forms->is_legacy( $convertkit_post->get_form() ) ) {
+ $warnings[] = sprintf(
+ /* translators: %s: Form name */
+ __( 'Form: %s', 'convertkit' ),
+ $this->get_form_display_name( $convertkit_post->get_form(), $forms )
+ );
+ }
+
+ // Landing Page.
+ if ( $landing_pages->is_legacy( $convertkit_post->get_landing_page() ) ) {
+ $warnings[] = sprintf(
+ /* translators: %s: Landing page name */
+ __( 'Landing Page: %s', 'convertkit' ),
+ $this->get_landing_page_display_name( $convertkit_post->get_landing_page(), $landing_pages )
+ );
+ }
+
+ // Restrict Content.
+ if ( $convertkit_post->get_restrict_content_type() === 'form' && $forms->is_legacy( $convertkit_post->get_restrict_content_id() ) ) {
+ $warnings[] = sprintf(
+ /* translators: %s: Form name */
+ __( 'Member Content: %s', 'convertkit' ),
+ $this->get_form_display_name( $convertkit_post->get_restrict_content_id(), $forms )
+ );
+ }
+
+ return $warnings;
+
+ }
+
+ /**
+ * Resolves the form ID to a human-readable "Form Name" string, falling
+ * back to "a Legacy Form" when the resource isn't cached.
+ *
+ * @since 3.3.7
+ *
+ * @param int $form_id Form ID.
+ * @param ConvertKit_Resource_Forms $forms Forms resource class.
+ * @return string
+ */
+ private function get_form_display_name( $form_id, $forms ) {
+
+ $form = $forms->get_by_id( $form_id );
+ if ( $form && ! empty( $form['name'] ) ) {
+ return sanitize_text_field( $form['name'] );
+ }
+
+ return __( 'a Legacy Form', 'convertkit' );
+
+ }
+
+ /**
+ * Resolves the landing page identifier (numeric ID or URL string) to a
+ * human-readable "Landing Page Name" string, falling back to
+ * "a Legacy Landing Page" when the resource isn't cached.
+ *
+ * @since 3.3.7
+ *
+ * @param int|string $id_or_url Landing Page ID or URL.
+ * @param ConvertKit_Resource_Landing_Pages $landing_pages Landing Pages resource class.
+ * @return string
+ */
+ private function get_landing_page_display_name( $id_or_url, $landing_pages ) {
+
+ // URL-string legacy assignments (pre-1.9.6) don't have a cached
+ // resource entry we can look up by ID, so fall back straight away.
+ if ( is_string( $id_or_url ) && strstr( $id_or_url, 'http' ) ) {
+ return __( 'a Legacy Landing Page', 'convertkit' );
+ }
+
+ $landing_page = $landing_pages->get_by_id( (int) $id_or_url );
+ if ( $landing_page && ! empty( $landing_page['name'] ) ) {
+ return sanitize_text_field( $landing_page['name'] );
+ }
+
+ return __( 'a Legacy Landing Page', 'convertkit' );
+
+ }
+
+ /**
+ * Returns the current post ID being edited, or 0 if not on a post edit
+ * screen. Reads $_GET['post'] because $post isn't always set in the
+ * admin_notices / enqueue_block_editor_assets hook contexts.
+ *
+ * @since 3.3.7
+ *
+ * @return int
+ */
+ private function get_current_post_id() {
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( isset( $_GET['post'] ) ) {
+ return absint( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ }
+
+ return 0;
+
+ }
+
+}
diff --git a/includes/class-wp-convertkit.php b/includes/class-wp-convertkit.php
index 371ab59d0..922e466b8 100644
--- a/includes/class-wp-convertkit.php
+++ b/includes/class-wp-convertkit.php
@@ -83,6 +83,7 @@ private function initialize_admin() {
$this->classes['admin_cache_plugins'] = new ConvertKit_Admin_Cache_Plugins();
$this->classes['admin_category'] = new ConvertKit_Admin_Category();
$this->classes['admin_landing_page'] = new ConvertKit_Admin_Landing_Page();
+ $this->classes['admin_legacy_resource_notice'] = new ConvertKit_Admin_Legacy_Resource_Notice();
$this->classes['admin_importer_activecampaign'] = new ConvertKit_Admin_Importer_ActiveCampaign();
$this->classes['admin_importer_aweber'] = new ConvertKit_Admin_Importer_AWeber();
$this->classes['admin_importer_campaignmonitor'] = new ConvertKit_Admin_Importer_CampaignMonitor();
diff --git a/resources/backend/js/legacy-resource-notice.js b/resources/backend/js/legacy-resource-notice.js
new file mode 100644
index 000000000..cf605d543
--- /dev/null
+++ b/resources/backend/js/legacy-resource-notice.js
@@ -0,0 +1,49 @@
+/**
+ * Renders a non-dismissible warning notice inside Gutenberg
+ * when a post/page references a legacy Kit resource.
+ *
+ * @author ConvertKit
+ * @since 3.3.7
+ */
+
+document.addEventListener('DOMContentLoaded', function () {
+ // Bail if the notice data is not available.
+ if (typeof convertkit_legacy_resource_notice === 'undefined') {
+ return;
+ }
+
+ // Bail if no warnings are found.
+ if (
+ typeof convertkit_legacy_resource_notice.warnings === 'undefined' ||
+ convertkit_legacy_resource_notice.warnings.length === 0
+ ) {
+ return;
+ }
+
+ // Bail if Gutenberg is not available.
+ if (
+ typeof wp === 'undefined' ||
+ typeof wp.data === 'undefined' ||
+ typeof wp.data.dispatch !== 'function'
+ ) {
+ return;
+ }
+
+ // Build the notice.
+ const message =
+ convertkit_legacy_resource_notice.intro +
+ '\n' +
+ convertkit_legacy_resource_notice.warnings
+ .map(function (warning) {
+ return '- ' + warning;
+ })
+ .join('\n');
+
+ // Create the notice.
+ wp.data
+ .dispatch('core/notices')
+ .createWarningNotice(message, {
+ id: convertkit_legacy_resource_notice.id,
+ isDismissible: false,
+ });
+});
diff --git a/tests/EndToEnd/general/other/BlockEditorLegacyNoticeCest.php b/tests/EndToEnd/general/other/BlockEditorLegacyNoticeCest.php
new file mode 100644
index 000000000..c6d71c988
--- /dev/null
+++ b/tests/EndToEnd/general/other/BlockEditorLegacyNoticeCest.php
@@ -0,0 +1,237 @@
+activateKitPlugin($I);
+ $I->setupKitPlugin($I);
+ $I->setupKitPluginResources($I);
+ }
+
+ /**
+ * Test that a page with a legacy Form assignment displays the warning
+ * notice inside Gutenberg, listing the legacy form by name.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testNoticeDisplaysForLegacyForm(EndToEndTester $I)
+ {
+ // Create a page with a Legacy Form assignment.
+ $pageID = $I->havePageInDatabase(
+ [
+ 'post_status' => 'publish',
+ 'post_title' => 'Kit: Legacy Notice: Gutenberg: Form',
+ 'meta_input' => [
+ '_wp_convertkit_post_meta' => [
+ 'form' => (string) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ 'landing_page' => '',
+ 'tag' => '',
+ ],
+ ],
+ ]
+ );
+
+ // Navigate to the page and wait for the notice to appear.
+ $I->amOnAdminPage('post.php?post=' . $pageID . '&action=edit');
+ $I->waitForElementVisible($this->noticeSelector);
+
+ // Assert the notice displays the legacy form by name.
+ $I->see('Form: ' . $_ENV['CONVERTKIT_API_LEGACY_FORM_NAME'], $this->noticeSelector);
+
+ // Assert the notice does not have a dismiss button.
+ $I->dontSeeElementInDOM($this->noticeSelector . ' button.components-notice__dismiss');
+ }
+
+ /**
+ * Test that a page with a legacy Landing Page assignment displays the
+ * warning notice inside Gutenberg, listing the legacy landing page by
+ * name.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testNoticeDisplaysForLegacyLandingPage(EndToEndTester $I)
+ {
+ // Create a page with a Legacy Landing Page assignment.
+ $pageID = $I->havePageInDatabase(
+ [
+ 'post_status' => 'publish',
+ 'post_title' => 'Kit: Legacy Notice: Gutenberg: Landing Page',
+ 'meta_input' => [
+ '_wp_convertkit_post_meta' => [
+ 'form' => '0',
+ 'landing_page' => (string) $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_ID'],
+ 'tag' => '',
+ ],
+ ],
+ ]
+ );
+
+ // Navigate to the page and wait for the notice to appear.
+ $I->amOnAdminPage('post.php?post=' . $pageID . '&action=edit');
+ $I->waitForElementVisible($this->noticeSelector);
+
+ // Assert the notice displays the legacy landing page by name.
+ $I->see('Landing Page: ' . $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_NAME'], $this->noticeSelector);
+
+ // Assert the notice does not have a dismiss button.
+ $I->dontSeeElementInDOM($this->noticeSelector . ' button.components-notice__dismiss');
+ }
+
+ /**
+ * Test that a page with a Restrict Content assignment referencing a
+ * legacy Kit form displays the warning notice inside Gutenberg, scoped
+ * to Member Content.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testNoticeDisplaysForLegacyRestrictContentForm(EndToEndTester $I)
+ {
+ // Create a page with a Restrict Content assignment referencing a
+ // legacy Kit form.
+ $pageID = $I->havePageInDatabase(
+ [
+ 'post_status' => 'publish',
+ 'post_title' => 'Kit: Legacy Notice: Gutenberg: Restrict Content',
+ 'meta_input' => [
+ '_wp_convertkit_post_meta' => [
+ 'form' => '0',
+ 'landing_page' => '',
+ 'tag' => '',
+ 'restrict_content' => 'form_' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ ],
+ ],
+ ]
+ );
+
+ // Navigate to the page and wait for the notice to appear.
+ $I->amOnAdminPage('post.php?post=' . $pageID . '&action=edit');
+ $I->waitForElementVisible($this->noticeSelector);
+
+ // Assert the notice displays the legacy form by name.
+ $I->see('Member Content: ' . $_ENV['CONVERTKIT_API_LEGACY_FORM_NAME'], $this->noticeSelector);
+
+ // Assert the notice does not have a dismiss button.
+ $I->dontSeeElementInDOM($this->noticeSelector . ' button.components-notice__dismiss');
+ }
+
+ /**
+ * Test that a page assigning only v4 Kit resources does not display
+ * the legacy warning notice inside Gutenberg.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testNoticeDoesNotDisplayForV4Resources(EndToEndTester $I)
+ {
+ // Create a page assigning only v4 Kit resources.
+ $pageID = $I->havePageInDatabase(
+ [
+ 'post_status' => 'publish',
+ 'post_title' => 'Kit: Legacy Notice: Gutenberg: V4 Only',
+ 'meta_input' => [
+ '_wp_convertkit_post_meta' => [
+ 'form' => (string) $_ENV['CONVERTKIT_API_FORM_ID'],
+ 'landing_page' => (string) $_ENV['CONVERTKIT_API_LANDING_PAGE_ID'],
+ 'tag' => '',
+ ],
+ ],
+ ]
+ );
+
+ // Navigate to the page and wait for Gutenberg to settle.
+ $I->amOnAdminPage('post.php?post=' . $pageID . '&action=edit');
+
+ // Assert the notice is not present.
+ $I->waitForElementVisible('.block-editor');
+ $I->dontSeeElementInDOM($this->noticeSelector);
+ }
+
+ /**
+ * Test that a page with legacy form + legacy landing page + legacy
+ * Member Content assignments displays a single consolidated warning
+ * notice inside Gutenberg with all three items listed.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testConsolidatedNoticeWhenMultipleLegacy(EndToEndTester $I)
+ {
+ // Create a page with legacy form + legacy landing page + legacy
+ // Member Content assignments.
+ $pageID = $I->havePageInDatabase(
+ [
+ 'post_status' => 'publish',
+ 'post_title' => 'Kit: Legacy Notice: Gutenberg: Consolidated',
+ 'meta_input' => [
+ '_wp_convertkit_post_meta' => [
+ 'form' => (string) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ 'landing_page' => (string) $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_ID'],
+ 'tag' => '',
+ 'restrict_content' => 'form_' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ ],
+ ],
+ ]
+ );
+
+ // Navigate to the page and wait for the notice to appear.
+ $I->amOnAdminPage('post.php?post=' . $pageID . '&action=edit');
+ $I->waitForElementVisible($this->noticeSelector);
+
+ // Assert the notice displays all three legacy resources.
+ $I->seeNumberOfElementsInDOM($this->noticeSelector, 1);
+ $I->see('Form: ' . $_ENV['CONVERTKIT_API_LEGACY_FORM_NAME'], $this->noticeSelector);
+ $I->see('Landing Page: ' . $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_NAME'], $this->noticeSelector);
+ $I->see('Member Content: ' . $_ENV['CONVERTKIT_API_LEGACY_FORM_NAME'], $this->noticeSelector);
+ }
+
+ /**
+ * 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
+ * deactivation and not the true test error.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function _passed(EndToEndTester $I)
+ {
+ $I->deactivateKitPlugin($I);
+ $I->resetKitPlugin($I);
+ }
+}
diff --git a/tests/EndToEnd/general/other/ClassicEditorLegacyNoticeCest.php b/tests/EndToEnd/general/other/ClassicEditorLegacyNoticeCest.php
new file mode 100644
index 000000000..91040f2bf
--- /dev/null
+++ b/tests/EndToEnd/general/other/ClassicEditorLegacyNoticeCest.php
@@ -0,0 +1,239 @@
+activateKitPlugin($I);
+ $I->activateThirdPartyPlugin($I, 'classic-editor');
+ $I->setupKitPlugin($I);
+ $I->setupKitPluginResources($I);
+ }
+
+ /**
+ * Test that a page with a legacy Form assignment displays the warning
+ * notice on the classic editor, listing the legacy form by name.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testNoticeDisplaysForLegacyForm(EndToEndTester $I)
+ {
+ // Create a page with a Legacy Form assignment.
+ $pageID = $I->havePageInDatabase(
+ [
+ 'post_status' => 'publish',
+ 'post_title' => 'Kit: Legacy Notice: Form',
+ 'meta_input' => [
+ '_wp_convertkit_post_meta' => [
+ 'form' => (string) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ 'landing_page' => '',
+ 'tag' => '',
+ ],
+ ],
+ ]
+ );
+
+ // Navigate to the page and wait for the notice to appear.
+ $I->amOnAdminPage('post.php?post=' . $pageID . '&action=edit');
+ $I->seeElementInDOM($this->noticeSelector);
+
+ // Assert the notice displays the legacy form by name.
+ $I->see('Form: ' . $_ENV['CONVERTKIT_API_LEGACY_FORM_NAME'], $this->noticeSelector);
+
+ // Assert the notice does not have a dismiss button.
+ $I->dontSeeElementInDOM($this->noticeSelector . ' button.notice-dismiss');
+ }
+
+ /**
+ * Test that a page with a legacy Landing Page assignment displays the
+ * warning notice on the classic editor, listing the legacy landing
+ * page by name.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testNoticeDisplaysForLegacyLandingPage(EndToEndTester $I)
+ {
+ // Create a page with a Legacy Landing Page assignment.
+ $pageID = $I->havePageInDatabase(
+ [
+ 'post_status' => 'publish',
+ 'post_title' => 'Kit: Legacy Notice: Landing Page',
+ 'meta_input' => [
+ '_wp_convertkit_post_meta' => [
+ 'form' => '0',
+ 'landing_page' => (string) $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_ID'],
+ 'tag' => '',
+ ],
+ ],
+ ]
+ );
+
+ // Navigate to the page and wait for the notice to appear.
+ $I->amOnAdminPage('post.php?post=' . $pageID . '&action=edit');
+ $I->seeElementInDOM($this->noticeSelector);
+
+ // Assert the notice displays the legacy landing page by name.
+ $I->see('Landing Page: ' . $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_NAME'], $this->noticeSelector);
+
+ // Assert the notice does not have a dismiss button.
+ $I->dontSeeElementInDOM($this->noticeSelector . ' button.notice-dismiss');
+ }
+
+ /**
+ * Test that a page with a Restrict Content assignment referencing a
+ * legacy Kit form displays the warning notice on the classic editor,
+ * scoped to Member Content.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testNoticeDisplaysForLegacyRestrictContentForm(EndToEndTester $I)
+ {
+ // Create a page with a Restrict Content assignment referencing a
+ // legacy Kit form.
+ $pageID = $I->havePageInDatabase(
+ [
+ 'post_status' => 'publish',
+ 'post_title' => 'Kit: Legacy Notice: Restrict Content',
+ 'meta_input' => [
+ '_wp_convertkit_post_meta' => [
+ 'form' => '0',
+ 'landing_page' => '',
+ 'tag' => '',
+ 'restrict_content' => 'form_' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ ],
+ ],
+ ]
+ );
+
+ // Navigate to the page and wait for the notice to appear.
+ $I->amOnAdminPage('post.php?post=' . $pageID . '&action=edit');
+ $I->seeElementInDOM($this->noticeSelector);
+
+ // Assert the notice displays the legacy form by name.
+ $I->see('Member Content: ' . $_ENV['CONVERTKIT_API_LEGACY_FORM_NAME'], $this->noticeSelector);
+
+ // Assert the notice does not have a dismiss button.
+ $I->dontSeeElementInDOM($this->noticeSelector . ' button.notice-dismiss');
+ }
+
+ /**
+ * Test that a page assigning only v4 Kit resources does not display
+ * the legacy warning notice.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testNoticeDoesNotDisplayForV4Resources(EndToEndTester $I)
+ {
+ // Create a page assigning only v4 Kit resources.
+ $pageID = $I->havePageInDatabase(
+ [
+ 'post_status' => 'publish',
+ 'post_title' => 'Kit: Legacy Notice: V4 Only',
+ 'meta_input' => [
+ '_wp_convertkit_post_meta' => [
+ 'form' => (string) $_ENV['CONVERTKIT_API_FORM_ID'],
+ 'landing_page' => (string) $_ENV['CONVERTKIT_API_LANDING_PAGE_ID'],
+ 'tag' => '',
+ ],
+ ],
+ ]
+ );
+
+ // Navigate to the page and wait for the notice to appear.
+ $I->amOnAdminPage('post.php?post=' . $pageID . '&action=edit');
+
+ // Assert the notice is not present.
+ $I->dontSee('Kit: This page uses one or more legacy Kit resources');
+ }
+
+ /**
+ * Test that a page with legacy form + legacy landing page + legacy
+ * Member Content assignments displays a single consolidated warning
+ * notice with all three items listed.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function testConsolidatedNoticeWhenMultipleLegacyResources(EndToEndTester $I)
+ {
+ // Create a page with legacy form + legacy landing page + legacy
+ // Member Content assignments.
+ $pageID = $I->havePageInDatabase(
+ [
+ 'post_status' => 'publish',
+ 'post_title' => 'Kit: Legacy Notice: Consolidated',
+ 'meta_input' => [
+ '_wp_convertkit_post_meta' => [
+ 'form' => (string) $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ 'landing_page' => (string) $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_ID'],
+ 'tag' => '',
+ 'restrict_content' => 'form_' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ ],
+ ],
+ ]
+ );
+
+ // Navigate to the page and wait for the notice to appear.
+ $I->amOnAdminPage('post.php?post=' . $pageID . '&action=edit');
+
+ // Assert the notice displays all three legacy resources.
+ $I->seeNumberOfElementsInDOM($this->noticeSelector, 1);
+ $I->see('Form: ' . $_ENV['CONVERTKIT_API_LEGACY_FORM_NAME'], $this->noticeSelector);
+
+ // Assert the notice displays the legacy landing page by name.
+ $I->see('Landing Page: ' . $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_NAME'], $this->noticeSelector);
+ $I->see('Member Content: ' . $_ENV['CONVERTKIT_API_LEGACY_FORM_NAME'], $this->noticeSelector);
+ }
+
+ /**
+ * 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
+ * deactivation and not the true test error.
+ *
+ * @since 3.3.7
+ *
+ * @param EndToEndTester $I Tester.
+ */
+ public function _passed(EndToEndTester $I)
+ {
+ $I->deactivateThirdPartyPlugin($I, 'classic-editor');
+ $I->deactivateKitPlugin($I);
+ $I->resetKitPlugin($I);
+ }
+}
diff --git a/tests/Integration/AdminLegacyResourceNoticeTest.php b/tests/Integration/AdminLegacyResourceNoticeTest.php
new file mode 100644
index 000000000..26ac6bbe8
--- /dev/null
+++ b/tests/Integration/AdminLegacyResourceNoticeTest.php
@@ -0,0 +1,228 @@
+ $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
+ 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'],
+ ]
+ );
+
+ // Refresh Forms and Landing Pages resources so is_legacy() calls resolve
+ // correctly against the API-backed cache.
+ $forms = new \ConvertKit_Resource_Forms();
+ $forms->refresh();
+
+ $landing_pages = new \ConvertKit_Resource_Landing_Pages();
+ $landing_pages->refresh();
+
+ $this->notice = new \ConvertKit_Admin_Legacy_Resource_Notice();
+ }
+
+ /**
+ * Test that a settings array with only v4 resource assignments returns
+ * no warnings.
+ *
+ * @since 3.3.7
+ */
+ public function testNoWarningsForV4Resources()
+ {
+ $warnings = $this->notice->get_legacy_warnings_for_post_settings(
+ [
+ 'form' => $_ENV['CONVERTKIT_API_FORM_ID'],
+ 'landing_page' => $_ENV['CONVERTKIT_API_LANDING_PAGE_ID'],
+ 'restrict_content' => 'form_' . $_ENV['CONVERTKIT_API_FORM_ID'],
+ ]
+ );
+
+ $this->assertSame([], $warnings);
+ }
+
+ /**
+ * Test that a legacy form assignment produces a form-scoped warning
+ * containing the form's name.
+ *
+ * @since 3.3.7
+ */
+ public function testWarningForLegacyForm()
+ {
+ $warnings = $this->notice->get_legacy_warnings_for_post_settings(
+ [
+ 'form' => $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ ]
+ );
+
+ $this->assertCount(1, $warnings);
+ $this->assertStringContainsString('Form:', $warnings[0]);
+ $this->assertStringContainsString($_ENV['CONVERTKIT_API_LEGACY_FORM_NAME'], $warnings[0]);
+ }
+
+ /**
+ * Test that a legacy landing page assignment (by numeric ID) produces
+ * a landing-page-scoped warning containing the landing page's name.
+ *
+ * @since 3.3.7
+ */
+ public function testWarningForLegacyLandingPageByID()
+ {
+ $warnings = $this->notice->get_legacy_warnings_for_post_settings(
+ [
+ 'landing_page' => $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_ID'],
+ ]
+ );
+
+ $this->assertCount(1, $warnings);
+ $this->assertStringContainsString('Landing Page:', $warnings[0]);
+ $this->assertStringContainsString($_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_NAME'], $warnings[0]);
+ }
+
+ /**
+ * Test that a legacy landing page assignment stored as a URL string
+ * (pre-1.9.6 storage) produces a landing-page-scoped warning using the
+ * fallback label since we can't resolve a name from the URL alone.
+ *
+ * @since 3.3.7
+ */
+ public function testWarningForLegacyLandingPageByURL()
+ {
+ $warnings = $this->notice->get_legacy_warnings_for_post_settings(
+ [
+ 'landing_page' => $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_URL'],
+ ]
+ );
+
+ $this->assertCount(1, $warnings);
+ $this->assertStringContainsString('Landing Page:', $warnings[0]);
+ $this->assertStringContainsString('a Legacy Landing Page', $warnings[0]);
+ }
+
+ /**
+ * Test that a restrict_content value of `form_` produces a
+ * Member Content warning.
+ *
+ * @since 3.3.7
+ */
+ public function testWarningForLegacyRestrictContentForm()
+ {
+ $warnings = $this->notice->get_legacy_warnings_for_post_settings(
+ [
+ 'restrict_content' => 'form_' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ ]
+ );
+
+ $this->assertCount(1, $warnings);
+ $this->assertStringContainsString('Member Content:', $warnings[0]);
+ $this->assertStringContainsString($_ENV['CONVERTKIT_API_LEGACY_FORM_NAME'], $warnings[0]);
+ }
+
+ /**
+ * Test that restrict_content values of `tag_` and `product_`
+ * never trigger a warning, because tags and products have no legacy
+ * variant.
+ *
+ * @since 3.3.7
+ */
+ public function testNoWarningForLegacyRestrictContentTagOrProduct()
+ {
+ $tag = $this->notice->get_legacy_warnings_for_post_settings(
+ [
+ 'restrict_content' => 'tag_' . $_ENV['CONVERTKIT_API_TAG_ID'],
+ ]
+ );
+ $product = $this->notice->get_legacy_warnings_for_post_settings(
+ [
+ 'restrict_content' => 'product_1',
+ ]
+ );
+
+ $this->assertSame([], $tag);
+ $this->assertSame([], $product);
+ }
+
+ /**
+ * Test that a settings array with legacy form + legacy landing page +
+ * legacy restrict_content form produces three warnings in a single
+ * returned array.
+ *
+ * @since 3.3.7
+ */
+ public function testWarningsForMultipleLegacyResources()
+ {
+ $warnings = $this->notice->get_legacy_warnings_for_post_settings(
+ [
+ 'form' => $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ 'landing_page' => $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_ID'],
+ 'restrict_content' => 'form_' . $_ENV['CONVERTKIT_API_LEGACY_FORM_ID'],
+ ]
+ );
+
+ $this->assertCount(3, $warnings);
+ }
+
+ /**
+ * Test that when a legacy form ID doesn't resolve to any cached resource
+ * (e.g. the form was deleted from the Kit account), the warning falls
+ * back to "a Legacy Form" rather than omitting the entry.
+ *
+ * @since 3.3.7
+ */
+ public function testWarningWithMissingResourceFallsBackToLabel()
+ {
+ // Wipe the Forms cache so is_legacy() can no longer resolve any
+ // numeric ID to a real resource. is_legacy() will therefore return
+ // false for numeric IDs — but a URL-based landing page assignment
+ // still resolves as legacy by the URL-string check, exercising the
+ // name-fallback path.
+ delete_option('convertkit_forms');
+ delete_option('convertkit_landing_pages');
+
+ $warnings = $this->notice->get_legacy_warnings_for_post_settings(
+ [
+ 'landing_page' => $_ENV['CONVERTKIT_API_LEGACY_LANDING_PAGE_URL'],
+ ]
+ );
+
+ $this->assertCount(1, $warnings);
+ $this->assertStringContainsString('a Legacy Landing Page', $warnings[0]);
+ }
+}
diff --git a/wp-convertkit.php b/wp-convertkit.php
index caead13e1..a400ce5fd 100644
--- a/wp-convertkit.php
+++ b/wp-convertkit.php
@@ -111,6 +111,7 @@
require_once CONVERTKIT_PLUGIN_PATH . '/admin/class-convertkit-admin-cache-plugins.php';
require_once CONVERTKIT_PLUGIN_PATH . '/admin/class-convertkit-admin-category.php';
require_once CONVERTKIT_PLUGIN_PATH . '/admin/class-convertkit-admin-landing-page.php';
+require_once CONVERTKIT_PLUGIN_PATH . '/admin/class-convertkit-admin-legacy-resource-notice.php';
require_once CONVERTKIT_PLUGIN_PATH . '/admin/class-convertkit-admin-post.php';
require_once CONVERTKIT_PLUGIN_PATH . '/admin/class-convertkit-admin-refresh-resources.php';
require_once CONVERTKIT_PLUGIN_PATH . '/admin/class-convertkit-admin-restrict-content.php';