From 1106c44d1e984db7f463cc044318b0eae66cd6de Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Fri, 27 Dec 2024 14:12:56 -0500
Subject: [PATCH 01/49] Initial shot at a refactor starting point, got some
 very basics working, but still some unused code areas and bugs, only 15% of
 the way there.

---
 ..._subscriptions.field_page_notify_email.yml |  18 -
 ...ubscriptions.field_page_notify_node_id.yml |  18 -
 ..._subscriptions.field_page_notify_token.yml |  18 -
 ...ptions.field_page_notify_token_user_id.yml |  18 -
 ...d.storage.node.field_page_notify_email.yml |  23 -
 ...storage.node.field_page_notify_node_id.yml |  23 -
 ...d.storage.node.field_page_notify_token.yml |  23 -
 ...e.node.field_page_notify_token_user_id.yml |  23 -
 .../node.type.page_notify_subscriptions.yml   |  13 -
 .../install/page_notifications.settings.yml   |  36 ++
 config/schema/page_notifications.schema.yml   |  16 +
 js/page_notifications.js                      |  28 -
 page_notifications.info.yml                   |   6 +-
 page_notifications.install                    | 393 +-------------
 page_notifications.libraries.yml              |   7 -
 page_notifications.links.menu.yml             |  91 +---
 page_notifications.links.task.yml             |  28 -
 page_notifications.module                     | 354 +------------
 page_notifications.permissions.yml            |  26 +-
 page_notifications.routing.yml                | 130 +----
 page_notifications.services.yml               |  34 +-
 src/Access/RoleAccessCheck.php                |  24 -
 src/Controller/AdminSubscriptionsListPage.php | 112 ----
 .../PageNotificationsController.php           | 256 ---------
 src/Controller/SubscriberPage.php             | 194 -------
 .../SubscriptionsAutoCompleteController.php   |  88 ----
 src/Entity/Subscription.php                   | 243 +++++++++
 .../SubscriptionAccessControlHandler.php      |  50 ++
 src/Entity/SubscriptionInterface.php          | 147 ++++++
 src/Entity/SubscriptionListBuilder.php        | 151 ++++++
 src/EventSubscriber/NodeUpdateSubscriber.php  |  14 +
 src/Form/AccessVerificationStep.php           | 214 --------
 src/Form/ContentTypeMigrationForm.php         | 274 ----------
 src/Form/EmailConfirmationPage.php            | 239 ---------
 src/Form/EmailUnsubscribePage.php             | 268 ----------
 src/Form/GeneralSettingsForm.php              | 151 ------
 src/Form/MessagesForm.php                     | 322 ------------
 src/Form/MigrationForm.php                    | 275 ----------
 src/Form/PageNotificationsBlockForm.php       | 496 ------------------
 src/Form/SettingsForm.php                     | 192 +++++++
 src/Form/SubscriptionDeleteForm.php           |   8 +
 src/Form/SubscriptionForm.php                 | 100 ++++
 src/Form/UserSubscriptionsPage.php            | 350 ------------
 src/LoadDataBaseInfo.php                      | 321 ------------
 src/Mail/PageNotificationsMailHandler.php     | 127 +++++
 src/Plugin/Block/PageNotificationsBlock.php   | 130 -----
 src/Plugin/Block/SubscriptionBlock.php        | 134 +++++
 src/Plugin/QueueWorker/NotificationQueue.php  |  18 +
 .../PageNotificationsDynamicRoutes.php        |  55 --
 src/Routing/RouteSubscriber.php               |  29 -
 src/Service/NotificationManager.php           | 329 ++++++++++++
 src/Service/NotificationManagerInterface.php  |  53 ++
 src/Token/SubscriptionToken.php               |  71 +++
 src/src/Controller/VerificationController.php |  62 +++
 templates/description.html.twig               |  34 --
 55 files changed, 1881 insertions(+), 4976 deletions(-)
 delete mode 100644 config/install/field.field.node.page_notify_subscriptions.field_page_notify_email.yml
 delete mode 100644 config/install/field.field.node.page_notify_subscriptions.field_page_notify_node_id.yml
 delete mode 100644 config/install/field.field.node.page_notify_subscriptions.field_page_notify_token.yml
 delete mode 100644 config/install/field.field.node.page_notify_subscriptions.field_page_notify_token_user_id.yml
 delete mode 100644 config/install/field.storage.node.field_page_notify_email.yml
 delete mode 100644 config/install/field.storage.node.field_page_notify_node_id.yml
 delete mode 100644 config/install/field.storage.node.field_page_notify_token.yml
 delete mode 100644 config/install/field.storage.node.field_page_notify_token_user_id.yml
 delete mode 100644 config/install/node.type.page_notify_subscriptions.yml
 create mode 100644 config/install/page_notifications.settings.yml
 create mode 100644 config/schema/page_notifications.schema.yml
 delete mode 100644 js/page_notifications.js
 delete mode 100644 page_notifications.libraries.yml
 delete mode 100644 page_notifications.links.task.yml
 delete mode 100644 src/Access/RoleAccessCheck.php
 delete mode 100644 src/Controller/AdminSubscriptionsListPage.php
 delete mode 100644 src/Controller/PageNotificationsController.php
 delete mode 100644 src/Controller/SubscriberPage.php
 delete mode 100644 src/Controller/SubscriptionsAutoCompleteController.php
 create mode 100644 src/Entity/Subscription.php
 create mode 100644 src/Entity/SubscriptionAccessControlHandler.php
 create mode 100644 src/Entity/SubscriptionInterface.php
 create mode 100644 src/Entity/SubscriptionListBuilder.php
 create mode 100644 src/EventSubscriber/NodeUpdateSubscriber.php
 delete mode 100644 src/Form/AccessVerificationStep.php
 delete mode 100644 src/Form/ContentTypeMigrationForm.php
 delete mode 100644 src/Form/EmailConfirmationPage.php
 delete mode 100644 src/Form/EmailUnsubscribePage.php
 delete mode 100644 src/Form/GeneralSettingsForm.php
 delete mode 100644 src/Form/MessagesForm.php
 delete mode 100644 src/Form/MigrationForm.php
 delete mode 100644 src/Form/PageNotificationsBlockForm.php
 create mode 100644 src/Form/SettingsForm.php
 create mode 100644 src/Form/SubscriptionDeleteForm.php
 create mode 100644 src/Form/SubscriptionForm.php
 delete mode 100644 src/Form/UserSubscriptionsPage.php
 delete mode 100644 src/LoadDataBaseInfo.php
 create mode 100644 src/Mail/PageNotificationsMailHandler.php
 delete mode 100644 src/Plugin/Block/PageNotificationsBlock.php
 create mode 100644 src/Plugin/Block/SubscriptionBlock.php
 create mode 100644 src/Plugin/QueueWorker/NotificationQueue.php
 delete mode 100644 src/Routing/PageNotificationsDynamicRoutes.php
 delete mode 100644 src/Routing/RouteSubscriber.php
 create mode 100644 src/Service/NotificationManager.php
 create mode 100644 src/Service/NotificationManagerInterface.php
 create mode 100644 src/Token/SubscriptionToken.php
 create mode 100644 src/src/Controller/VerificationController.php
 delete mode 100644 templates/description.html.twig

diff --git a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_email.yml b/config/install/field.field.node.page_notify_subscriptions.field_page_notify_email.yml
deleted file mode 100644
index 41639d3..0000000
--- a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_email.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  config:
-    - field.storage.node.field_page_notify_email
-    - node.type.page_notify_subscriptions
-id: node.page_notify_subscriptions.field_page_notify_email
-field_name: field_page_notify_email
-entity_type: node
-bundle: page_notify_subscriptions
-label: 'Subscriber E-mail'
-description: ''
-required: false
-translatable: false
-default_value: {  }
-default_value_callback: ''
-settings: {  }
-field_type: string
diff --git a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_node_id.yml b/config/install/field.field.node.page_notify_subscriptions.field_page_notify_node_id.yml
deleted file mode 100644
index 37c75b4..0000000
--- a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_node_id.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  config:
-    - field.storage.node.field_page_notify_node_id
-    - node.type.page_notify_subscriptions
-id: node.page_notify_subscriptions.field_page_notify_node_id
-field_name: field_page_notify_node_id
-entity_type: node
-bundle: page_notify_subscriptions
-label: 'Subscribed node'
-description: ''
-required: false
-translatable: false
-default_value: {  }
-default_value_callback: ''
-settings: {  }
-field_type: string
diff --git a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token.yml b/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token.yml
deleted file mode 100644
index 634b692..0000000
--- a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  config:
-    - field.storage.node.field_page_notify_token
-    - node.type.page_notify_subscriptions
-id: node.page_notify_subscriptions.field_page_notify_token
-field_name: field_page_notify_token
-entity_type: node
-bundle: page_notify_subscriptions
-label: 'Token'
-description: ''
-required: false
-translatable: false
-default_value: {  }
-default_value_callback: ''
-settings: {  }
-field_type: string
diff --git a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token_user_id.yml b/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token_user_id.yml
deleted file mode 100644
index ac4729b..0000000
--- a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token_user_id.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  config:
-    - field.storage.node.field_page_notify_token_user_id
-    - node.type.page_notify_subscriptions
-id: node.page_notify_subscriptions.field_page_notify_token_user_id
-field_name: field_page_notify_token_user_id
-entity_type: node
-bundle: page_notify_subscriptions
-label: 'User Token'
-description: ''
-required: false
-translatable: false
-default_value: {  }
-default_value_callback: ''
-settings: {  }
-field_type: string
diff --git a/config/install/field.storage.node.field_page_notify_email.yml b/config/install/field.storage.node.field_page_notify_email.yml
deleted file mode 100644
index c55519b..0000000
--- a/config/install/field.storage.node.field_page_notify_email.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  module:
-    - node
-  enforced:
-    module:
-      - page_notifications
-id: node.field_page_notify_email
-field_name: field_page_notify_email
-entity_type: node
-type: string
-settings:
-  max_length: 255
-  is_ascii: false
-  case_sensitive: false
-module: core
-locked: false
-cardinality: 1
-translatable: true
-indexes: {  }
-persist_with_no_fields: false
-custom_storage: false
diff --git a/config/install/field.storage.node.field_page_notify_node_id.yml b/config/install/field.storage.node.field_page_notify_node_id.yml
deleted file mode 100644
index da8209f..0000000
--- a/config/install/field.storage.node.field_page_notify_node_id.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  module:
-    - node
-  enforced:
-    module:
-      - page_notifications
-id: node.field_page_notify_node_id
-field_name: field_page_notify_node_id
-entity_type: node
-type: string
-settings:
-  max_length: 255
-  is_ascii: false
-  case_sensitive: false
-module: core
-locked: false
-cardinality: 1
-translatable: true
-indexes: {  }
-persist_with_no_fields: false
-custom_storage: false
diff --git a/config/install/field.storage.node.field_page_notify_token.yml b/config/install/field.storage.node.field_page_notify_token.yml
deleted file mode 100644
index f6f40c5..0000000
--- a/config/install/field.storage.node.field_page_notify_token.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  module:
-    - node
-  enforced:
-    module:
-      - page_notifications
-id: node.field_page_notify_token
-field_name: field_page_notify_token
-entity_type: node
-type: string
-settings:
-  max_length: 255
-  is_ascii: false
-  case_sensitive: false
-module: core
-locked: false
-cardinality: 1
-translatable: true
-indexes: {  }
-persist_with_no_fields: false
-custom_storage: false
diff --git a/config/install/field.storage.node.field_page_notify_token_user_id.yml b/config/install/field.storage.node.field_page_notify_token_user_id.yml
deleted file mode 100644
index 492502f..0000000
--- a/config/install/field.storage.node.field_page_notify_token_user_id.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  module:
-    - node
-  enforced:
-    module:
-      - page_notifications
-id: node.field_page_notify_token_user_id
-field_name: field_page_notify_token_user_id
-entity_type: node
-type: string
-settings:
-  max_length: 255
-  is_ascii: false
-  case_sensitive: false
-module: core
-locked: false
-cardinality: 1
-translatable: true
-indexes: {  }
-persist_with_no_fields: false
-custom_storage: false
diff --git a/config/install/node.type.page_notify_subscriptions.yml b/config/install/node.type.page_notify_subscriptions.yml
deleted file mode 100644
index d9dc52d..0000000
--- a/config/install/node.type.page_notify_subscriptions.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-langcode: en
-status: true
-dependencies:
-  enforced:
-    module:
-      - page_notifications
-name: 'Page Notifications - Subscriptions'
-type: page_notify_subscriptions
-description: 'Subscriptions to pages created by users who would like to be notified.'
-help: 'Do not create nodes for this Content type. They are getting created automaticly by Page Notifications module.'
-new_revision: false
-preview_mode: 1
-display_submitted: true
diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml
new file mode 100644
index 0000000..c5ef6fa
--- /dev/null
+++ b/config/install/page_notifications.settings.yml
@@ -0,0 +1,36 @@
+notification_settings:
+  from_email: ''
+  token_expiration: 48
+
+email_templates:
+  verification_subject: 'Verify your subscription to [node:title]'
+  verification_body: |
+    Hello,
+
+    Please verify your email subscription to the page "[node:title]".
+    Click the following link to confirm your subscription:
+    [subscription:verify-url]
+
+    This verification link will expire soon.
+    Please verify your subscription promptly.
+
+    If you did not request this subscription, please ignore this email.
+
+  notification_subject: '[node:title] has been updated'
+  notification_body: |
+    Dear subscriber,
+
+    The page "[node:title]" that you are subscribed to has been updated.
+
+    You can view the updated page here:
+    [node:url]
+
+    To unsubscribe from these notifications, click here:
+    [subscription:unsubscribe-url]
+
+    Regards,
+    [site:name] team
+
+security:
+  require_verification: true
+  cleanup_unverified: true
\ No newline at end of file
diff --git a/config/schema/page_notifications.schema.yml b/config/schema/page_notifications.schema.yml
new file mode 100644
index 0000000..22153d6
--- /dev/null
+++ b/config/schema/page_notifications.schema.yml
@@ -0,0 +1,16 @@
+page_notifications.settings:
+  type: config_object
+  label: 'Page Notifications settings'
+  mapping:
+    notification_settings:
+      type: mapping
+      mapping:
+        from_email:
+          type: string
+          label: 'From email address'
+        email_template:
+          type: text
+          label: 'Email template'
+        token_expiration:
+          type: integer
+          label: 'Token expiration time in hours'
\ No newline at end of file
diff --git a/js/page_notifications.js b/js/page_notifications.js
deleted file mode 100644
index a044f7a..0000000
--- a/js/page_notifications.js
+++ /dev/null
@@ -1,28 +0,0 @@
-(function($){
-
-  var checkbox = document.getElementById("edit-pagenotifycheckall");
-  var checkboxs = document.getElementsByClassName("form-checkbox");
-
-  if (checkbox) {
-    checkbox.addEventListener("click", checkUncheck);
-  }
-
-  function checkUncheck() {
-    if (checkbox.checked == false){
-      for (i = 0; i < checkboxs.length; i++) {
-        var target = document.getElementById('edit-page-notifications-list-'+i+'-field-page-notify-node-id');
-        if (target !== null){
-          target.checked = false;
-        }
-      }
-    } else {
-      for (i = 0; i < checkboxs.length; i++) {
-        var target = document.getElementById('edit-page-notifications-list-'+i+'-field-page-notify-node-id');
-        if (target !== null){
-          target.checked = true;
-        }
-      }
-    }
-  }
-
-})(jQuery);
diff --git a/page_notifications.info.yml b/page_notifications.info.yml
index b113093..9397d71 100644
--- a/page_notifications.info.yml
+++ b/page_notifications.info.yml
@@ -1,8 +1,8 @@
 name: Page Notifications
 type: module
-description: 'Anonymous users can subscribe to a page to receive notifications about updates about page/node.'
-core_version_requirement: ^8 || ^9 || ^10
-configure: page_notifications.tabs
+description: 'Anonymous users can subscribe to pages to receive notifications about updates.'
+core_version_requirement: ^10
+configure: page_notifications.settings
 dependencies:
   - drupal:node
 
diff --git a/page_notifications.install b/page_notifications.install
index 4e3e355..237398b 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -1,393 +1,38 @@
 <?php
 
-use \Drupal\field\Entity\FieldStorageConfig;
-use \Drupal\field\Entity\FieldConfig;
-use Drupal\Core\Database\Database;
-
-
 /**
  * @file
- * Install, update and uninstall functions for the Page Notifications module.
+ * Install, update and uninstall functions for the page_notifications module.
  */
 
 /**
- *  hook_install()
+ * Implements hook_install().
  */
-function page_notifications_install()
-{
-
-  $moduleHandler = \Drupal::service('module_handler');
-  $page_notify_recaptcha = ($moduleHandler->moduleExists('recaptcha')) ? 1 : 0;
-
-  $settings_query = \Drupal::database()->insert('page_notify_settings')
-    ->fields([
-      'page_notify_settings_group_name' => 'page_notify_general_settings',
-      'page_notify_recaptcha' => $page_notify_recaptcha,
-      'page_notify_captcha' => 0,
-      'page_notify_subscribers_count' => 1,
-      'enable_message_subscription_not_available' => 1,
-      'page_notify_settings_enable_content_type' => 0,
-      'page_notify_settings_enable_view' => 0,
-    ]);
-  $settings_query->execute();
-
-  $request_time = Drupal::time()->getRequestTime();
-  $template_query = \Drupal::database()->insert('page_notify_email_template')
-    ->fields([
-      'body' => '<p>Subscribe to: [notify_node_title]</p>',
-      'from_email' => '',
-      'checkbox_field' => '',
-      'notes_field' => '',
-      'node_timestamp' => '',
-      'created' => $request_time,
-      'verification_email_subject' => 'Subscription Confirmation – [notify_node_title]',
-      'verification_email_text' => '<p>Hello [notify_user_email],</p>
-          <p>Please <a href="[notify_verify_url]">confirm your subscription</a>.</p>
-          <p>Once complete, you will receive a “Now Subscribed” email notification.</p>
-          <p>Thank you!</p>',
-      'confirmation_email_subject' => 'You are now subscribed to - [notify_node_title]',
-      'confirmation_email_text' => '<p>Hello [notify_user_email],</p>
-          <p>You are now subscribed to <a href="[notify_node_url]">[notify_node_title]</a>.<br />
-          <a href="[notify_unsubscribe_url]">Unsubscribe</a> or visit <a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p>
-          <p>Thank you!</p>',
-      'sent_verify_web_page_message' => '<p>Hey [notify_user_email],</p>
-           <p>Please check your email to finalize your subscription!</p>
-           <p style="font-size:9px">*If you didn’t get an e-mail, please check the spam folder</p>',
-      'record_exist_verify_web_page_message' => '<p>Hey [notify_user_email],</p>
-          <p>You already subscribed to this page!</p>
-          <p><a href="[notify_unsubscribe_url]">Unsubscribe from this page</a></p>',
-      'error_web_page_message' => '<p>Hey [notify_user_email],</p><p>There was an error on this page!</p>',
-      'subscription_not_available_web_page_message' => '<p>Subscription is not available for this page.</p>',
-      'confirmation_web_page_message' => '<p>Hey [notify_user_email],</p>
-          <p>You are all set!</p>
-          <p>Thank you for subscribing!</p>
-          <p><a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p>',
-      'general_email_template_subject' => '[notify_node_title] – Notification of New Update',
-      'general_email_template' => '<p>Hello [notify_user_email],</p>
-          <p>The "<a href="[notify_node_url]">[notify_node_title]</a>." has been updated.<br />
-          If you would like to unsubscribe to this page please go <a href="[notify_user_email]">here</a> or visit <a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p>
-          <p>[notify_notes]</p>
-          <p>Thank you!</p>',
-    ]);
-  $template_query->execute();
-
+function page_notifications_install() {
+  // Create default configuration.
+  $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
+  if ($config->isNew()) {
+    $config
+      ->set('notification_settings.from_email', '')
+      ->set('notification_settings.email_template', '')
+      ->set('notification_settings.token_expiration', 48)
+      ->save();
+  }
 }
 
 /**
  * Implements hook_schema().
  */
-function page_notifications_schema()
-{
+function page_notifications_schema() {
   $schema = [];
-  $schema['page_notify_email_template'] = [
-    'description' => 'Table of accounts subscribed for notifications.',
-    'fields' => [
-      'template_id' => [
-        'description' => 'The primary identifier for submition.',
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ],
-      'body' => [
-        'description' => 'Header of Block Notify.',
-        'type' => 'text',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'from_email' => [
-        'description' => 'From email',
-        'type' => 'varchar',
-        'length' => 50,
-        'not null' => FALSE,
-      ],
-      'checkbox_field' => [
-        'description' => 'Checkbox field of the node',
-        'type' => 'varchar',
-        'length' => 50,
-        'not null' => FALSE,
-      ],
-      'notes_field' => [
-        'description' => 'Notes field of the node',
-        'type' => 'varchar',
-        'length' => 50,
-        'not null' => FALSE,
-      ],
-      'node_timestamp' => [
-        'description' => 'Timestamp field of the node',
-        'type' => 'varchar',
-        'length' => 50,
-        'not null' => FALSE,
-      ],
-      'created' => [
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => FALSE,
-        'default' => 0,
-      ],
-      'verification_email_subject' => [
-        'description' => 'Verification Email Subject.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'verification_email_text' => [
-        'description' => 'Verification email',
-        'type' => 'text',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'confirmation_email_subject' => [
-        'description' => 'Confirmation Email Subject.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'confirmation_email_text' => [
-        'description' => 'Confirmation email',
-        'type' => 'text',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'sent_verify_web_page_message' => [
-        'description' => 'Web message that verification email sent.',
-        'type' => 'text',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'record_exist_verify_web_page_message' => [
-        'description' => 'Web message that record exist.',
-        'type' => 'text',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'error_web_page_message' => [
-        'description' => 'Web error message.',
-        'type' => 'text',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'subscription_not_available_web_page_message' => [
-        'description' => 'Web message when subscription is not available.',
-        'type' => 'text',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'confirmation_web_page_message' => [
-        'description' => 'Confirmation web message',
-        'type' => 'text',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'general_email_template_subject' => [
-        'description' => 'Subject of the email.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-      'general_email_template' => [
-        'description' => 'Confirmation email',
-        'type' => 'text',
-        'length' => 255,
-        'not null' => FALSE,
-      ],
-    ],
-    'primary key' => ['template_id'],
-  ];
-
-  $schema['page_notify_settings'] = [
-    'description' => 'Page Notifications settings.',
-    'fields' => [
-      'page_notify_id' => [
-        'description' => 'The primary identifier settings.',
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ],
-      'page_notify_settings_group_name' => [
-        'description' => 'Machine name of the settings group',
-        'type' => 'varchar',
-        'length' => 50,
-        'not null' => FALSE,
-      ],
-      'page_notify_recaptcha' => [
-        'description' => 'Enable/Disable recaptcha',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ],
-      'page_notify_captcha' => [
-        'description' => 'Enable/Disable Captcha',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ],
-      'page_notify_subscribers_count' => [
-        'description' => 'Show number of subscribers on node edit',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ],
-      'enable_message_subscription_not_available' => [
-        'description' => 'Display message when subscriptions are not avaliable.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 1,
-      ],
-      'page_notify_settings_enable_content_type' => [
-        'description' => 'Enable/Disable content type functionality.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ],
-      'page_notify_settings_enable_view' => [
-        'description' => 'Enable/Disable views functionality.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ],
-
-    ],
-    'primary key' => ['page_notify_id'],
-  ];
-
+  // Add any additional database tables needed beyond entities
   return $schema;
 }
 
 /**
- * Uninstall Field UI.
- */
-function page_notifications_update_8001(&$sandbox)
-{
-
-  \Drupal::service('module_installer')->uninstall(['page_notifications']);
-  $result = \Drupal::entityQuery("node")
-    ->condition("type", "subscriptions")
-    ->accessCheck(FALSE)
-    ->execute();
-  $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-  $entities = $storage_handler->loadMultiple($result);
-  $storage_handler->delete($entities);
-}
-
-/**
- * Summary of page_notifications_update_9001
- * @param array $sandbox
- * @return void
+ * Implements hook_uninstall().
  */
-function page_notifications_update_9001(array &$sandbox)
-{
-  $spec = [
-    'description' => 'Page Notifications settings',
-    'fields' => [
-      'page_notify_id' => [
-        'description' => 'The primary identifier for settings.',
-        'type' => 'serial',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-      ],
-      'page_notify_settings_group_name' => [
-        'description' => 'Machine name of the settings group',
-        'type' => 'varchar',
-        'length' => 50,
-        'not null' => FALSE,
-      ],
-      'page_notify_recaptcha' => [
-        'description' => 'Enable/Disable recaptcha',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ],
-      'page_notify_captcha' => [
-        'description' => 'Enable/Disable Captcha',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ],
-      'page_notify_subscribers_count' => [
-        'description' => 'Show number of subscribers on node edit',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ],
-      'enable_message_subscription_not_available' => [
-        'description' => 'Display message when subscriptions are not avaliable.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 1,
-      ],
-      'page_notify_settings_enable_content_type' => [
-        'description' => 'Enable/Disable content type functionality.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 1,
-      ],
-      'page_notify_settings_enable_view' => [
-        'description' => 'Enable/Disable views functionality.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 1,
-      ],
-    ],
-    'primary key' => ['page_notify_id'],
-  ];
-  $schema = Database::getConnection()->schema();
-  $schema->createTable('page_notify_settings', $spec);
-
-  $moduleHandler = \Drupal::service('module_handler');
-  $page_notify_recaptcha = ($moduleHandler->moduleExists('recaptcha')) ? 1 : 0;
-
-  $query = \Drupal::database()->insert('page_notify_settings')
-    ->fields([
-      'page_notify_settings_group_name' => 'page_notify_general_settings',
-      'page_notify_recaptcha' => $page_notify_recaptcha,
-      'page_notify_captcha' => 0,
-      'page_notify_subscribers_count' => 1,
-      'enable_message_subscription_not_available' => 1,
-      'page_notify_settings_enable_content_type' => 0,
-      'page_notify_settings_enable_view' => 0,
-    ]);
-  $query->execute();
-}
-
-/**
- * Summary of page_notifications_update_9002
- * @param array $sandbox
- * @return void
- */
-function page_notifications_update_9002(array &$sandbox)
-{
-  $spec = [
-    'description' => 'Enable/Disable captcha',
-    'type' => 'int',
-    'unsigned' => TRUE,
-    'not null' => TRUE,
-    'default' => 0,
-  ];
-  $schema = Database::getConnection()->schema();
-  $schema->addField('page_notify_settings', 'page_notify_captcha', $spec);
-
-}
-
-/*
- * Implementation of hook_uninstall()
- */
-
-function page_notifications_uninstall()
-{
-  $db_connection = \Drupal::database();
-  $db_connection->schema()->dropTable('page_notify_settings');
-  $db_connection->schema()->dropTable('page_notify_email_template');
-
-}
+function page_notifications_uninstall() {
+  // Remove configuration.
+  \Drupal::configFactory()->getEditable('page_notifications.settings')->delete();
+}
\ No newline at end of file
diff --git a/page_notifications.libraries.yml b/page_notifications.libraries.yml
deleted file mode 100644
index 81dc854..0000000
--- a/page_notifications.libraries.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-page_notifications:
-  js:
-    js/page_notifications.js: {}
-recaptcha:
-  version: 1.x
-  js:
-    https://www.google.com/recaptcha/api.js: { type: external }
diff --git a/page_notifications.links.menu.yml b/page_notifications.links.menu.yml
index d1dd26a..d4a54d7 100644
--- a/page_notifications.links.menu.yml
+++ b/page_notifications.links.menu.yml
@@ -1,78 +1,13 @@
-#page_notifications:
-#  title: 'Page Notifications'
-#  description: 'Simplest possible menu type, and the parent menu entry for others'
-#  expanded: 1
-#  route_name: page_notifications
-
-#page_notifications.alternate_menu:
-#  title: 'Page Notifications: Menu in alternate menu'
-#   #If menu_name is omitted, the "Tools" menu will be used.
-#  menu_name: 'main'
-#  route_name: page_notifications.alternate_menu
-
-#page_notifications.permissioned:
-#  title: 'Permissioned Notifications'
-#  parent: page_notifications
-#  expanded: 1
-#  route_name: page_notifications.permissioned
-#  weight: 10
-
-#page_notifications.permissioned_controlled:
-#  title: 'Permissioned Menu Item'
-#  parent: page_notifications.permissioned
-#  route_name: page_notifications.permissioned_controlled
-#  weight: 10
-
-#page_notifications.custom_access:
-#  title: 'Custom Access Notifications'
-#  parent: page_notifications
-#  expanded: 1
-#  route_name: page_notifications.custom_access
-#  weight: -5
-
-#page_notifications.custom_access_page:
-#  title: 'Custom Access Menu Item'
-#  parent: page_notifications.custom_access
-#  route_name: page_notifications.custom_access_page
-
-#page_notifications.route_only:
-#  title: 'Route only notifications'
-#  parent: page_notifications
-#  route_name: page_notifications.route_only
-#  weight: 20
-
-page_notifications.tabs:
-  title: 'Tabs'
-  description: 'Shows how to create primary and secondary tabs'
-#  parent: page_notifications
-  parent: system.admin_structure
-  route_name: page_notifications.tabs
-  weight: 30
-
-#page_notifications.use_url_arguments:
-#  title: 'URL Arguments'
-#  description: 'The page callback can use the arguments provided after the path used as key'
-#  parent: page_notifications
-#  route_name: page_notifications.use_url_arguments
-#  weight: 40
-
-#page_notifications.title_callbacks:
-#  title: 'Dynamic title'
-#  description: 'The title of this menu item is dynamically generated'
-#  parent: page_notifications
-#  route_name: page_notifications.title_callbacks
-#  weight: 50
-
-#page_notifications.placeholder_argument:
-#  title: Placeholder Arguments
-#  description: ''
-#  parent: 'page_notifications'
-#  route_name: page_notifications.placeholder_argument
-#  weight: 60
-
-#page_notifications.path_override:
-#  title: Path Override
-#  description: ''
-#  parent: 'page_notifications'
-#  route_name: page_notifications.path_override
-#  weight: 70
+page_notifications.settings:
+  title: 'Page Notifications'
+  description: 'Configure Page Notifications settings'
+  parent: system.admin_config_system
+  route_name: page_notifications.settings
+  weight: 0
+
+page_notifications.subscription_list:
+  title: 'Subscriptions'
+  description: 'Manage Page Notification subscriptions'
+  parent: system.admin_content
+  route_name: page_notifications.subscription_list
+  weight: 0
\ No newline at end of file
diff --git a/page_notifications.links.task.yml b/page_notifications.links.task.yml
deleted file mode 100644
index 3f0e880..0000000
--- a/page_notifications.links.task.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-page_notifications.tabs:
-  route_name: page_notifications.tabs
-  title: General configuration
-  base_route: page_notifications.tabs
-
-page_notifications.tabs_second:
-  route_name: page_notifications.tabs_second
-  title: Migrate Subscribtions
-  base_route: page_notifications.tabs
-  weight: 2
-
-page_notifications.tabs_third:
-  route_name: page_notifications.tabs_third
-  title: Migrate Subscribtions Content Type
-  base_route: page_notifications.tabs
-  weight: 3
-
-page_notifications.tabs.secondary:
-  route_name: page_notifications.tabs
-  title: General settings
-  parent_id: page_notifications.tabs
-  weight: 1
-
-page_notifications.tabs_default_second:
-  route_name: page_notifications.tabs_default_second
-  title: Messages configuration
-  parent_id: page_notifications.tabs
-  weight: 2
diff --git a/page_notifications.module b/page_notifications.module
index f629a97..eb3db4e 100644
--- a/page_notifications.module
+++ b/page_notifications.module
@@ -1,340 +1,42 @@
 <?php
 
-use Drupal\Core\Mail\MailManagerInterface;
-use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Component\Utility\Html;
-use Drupal\Core\Mail\MailFormatHelper;
-use Drupal\page_notifications\Form;
-use Drupal\page_notifications\LoadDataBaseInfo;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Render\Markup;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\NodeInterface;
-use Drupal\taxonomy\Entity\Term;
-use \Drupal\Core\Url;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Entity\EntityBase;
-
 /**
- * Implements hook_theme().
+ * @file
+ * Primary module hooks for Page Notifications module.
  */
-function page_notifications_theme ($existing, $type, $theme, $path) {
-  return [
-    'SubscriberPage' => [
-      'variables' => [
-        'page_notifications_var' => 'page_notifications_subscriber_page'
-      ],
-    ],
-  ];
-}
 
-function page_notifications_page_attachments(array &$attachments) {
-  $attachments['#attached']['library'][] = 'page_notifications/page_notifications';
-}
-
- /**
- * Implements hook_form_alter().
+/**
+ * Implements hook_mail().
  */
-function page_notifications_form_alter(&$form, FormStateInterface $form_state, $form_id) {
-  $formObject = $form_state->getFormObject();
-  if ($form_state->getFormObject() instanceof \Drupal\Core\Entity\EntityFormInterface) {
-  $entity = $form_state->getFormObject()->getEntity();
-  $entity_type = $entity->getEntityTypeId();
-  if ($entity_type) {
-    if ($entity_type == "taxonomy_term") {
-      $nid = $form_state->getformObject()->getEntity()->id();
-      $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings();
-      $all_node_subscriptions = \Drupal::service('load.databaseinnfo.service')->getAllNodeSubscription($nid);
-      $node_subscriptions_count  = 0;
-      if ($notify_settings['page_notify_subscribers_count']) {
-        if ($all_node_subscriptions) {
-          $node_subscriptions_count = count($all_node_subscriptions);
-        }
-        $morethen = ($node_subscriptions_count != 0) ? $node_subscriptions_count . ' Subscriptions' : $node_subscriptions_count . ' Subscriptions';
-        $form['options']['page_notifications_count'] = array(
-          '#type' => 'details',
-          '#access' => TRUE,
-          '#title' => 'Page Notifications',
-          '#collapsible' => TRUE,
-          '#collapsed' => TRUE,
-          '#group' => 'advanced',
-          '#weight' => 100,
-          'page_notifications_count_active' => array(
-            '#markup' =>  t('There are: <a href="/admin/page-notifications/all-subscriptions/'.$nid.'-term" target="_blank">'. $morethen . '</a> for this page.'),
-           ),
-        );
-      }
-    } elseif ($entity_type  == "node") {
-      if ($form_state->getFormObject()) {
-        $formObject = $form_state->getFormObject();
-        if (!($formObject instanceof \Drupal\Core\Entity\EntityFormInterface)) {
-          return;
-        }
-        if ($form_state->getformObject()->getEntity()->id()) {
-          $nid = $form_state->getformObject()->getEntity()->id();
-          $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings();
-          $all_node_subscriptions = \Drupal::service('load.databaseinnfo.service')->getAllNodeSubscription($nid);
-          $node_subscriptions_count  = 0;
-          if ($notify_settings['page_notify_subscribers_count']) {
-            if ($all_node_subscriptions) {
-              $node_subscriptions_count = count($all_node_subscriptions);
-            }
-            $morethen = ($node_subscriptions_count != 0) ? $node_subscriptions_count . ' Subscriptions' : $node_subscriptions_count . ' Subscriptions';
-            $form['options']['page_notifications_count'] = array(
-              '#type' => 'details',
-              '#access' => TRUE,
-              '#title' => 'Page Notifications',
-              '#collapsible' => TRUE,
-              '#collapsed' => TRUE,
-              '#group' => 'advanced',
-              '#weight' => 100,
-              'page_notifications_count_active' => array(
-                '#markup' =>  t('There are: <a href="/admin/page-notifications/all-subscriptions/'.$nid.'" target="_blank">'. $morethen . '</a> for this page.'),
-               ),
-            );
-          }
-         }
-        }
-    }
-   }
-  }
-}
-
-
-function page_notifications_entity_presave(Drupal\Core\Entity\EntityInterface $entity) {
-  $template_info = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template();
-  $checkbox_field = $template_info['checkbox_field'];
-  $notes_field = $template_info['notes_field'];
-  if (method_exists($entity, 'hasField')) {
-   if ($entity->hasField($checkbox_field) && !is_null($checkbox_field)) {
-    if ($entity->get($checkbox_field)->value == 1) {
-      $records = page_notifications_get_subscribes_list($entity->id());
-      $count_records = count($records);
-
-      if ($records && !is_null($records) && $count_records != 0) {
-        $node_title = $entity->getName();
-        $absolute_node_path = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical',['taxonomy_term' => $entity->tid->value],['absolute' => TRUE])->toString();
-
-        if ($template_info['from_email'] && !is_null($template_info['from_email'])) {
-          $from = $template_info['from_email'];
-        } else {
-          $from = \Drupal::config('system.site')->get('mail');
-        }
-
-        if ($template_info['general_email_template_subject'] && !is_null($template_info['general_email_template_subject'])) {
-          $subject = $template_info['general_email_template_subject'];
-        } else {
-          $subject = 'New update on - "' . $node_title . '" page';
-        }
-
-        if ($entity->hasField($notes_field) && !is_null($notes_field)) {
-          $email_update_notes = $entity->get($notes_field)->value;
-        }
-
-        foreach ($records as $record) {
-          $user_token = $record->get("field_page_notify_token_user_id")->getValue();
-          $subscriber_email = $record->get("field_page_notify_email")->getValue();
-          $subscriber_token_notify = $record->get("field_page_notify_token")->getValue();
-          $host = \Drupal::request()->getSchemeAndHttpHost();
-          $unsubscribe_link = $host . "/page-notifications/unsubscribe/" . $entity->id() . "-" . $subscriber_token_notify[0]['value'];
-          $unsubscribe_text = 'To unsubscribe, please go to this <a href=' . $unsubscribe_link .'>link</a>';
-          $subscriptions_url = $host . '/page-notifications/verify-list/' . $entity->id() . "-" . $subscriber_token_notify[0]['value'];
-
-          $subject_replacements = array(
-            '[notify_user_name]' => '',
-            '[notify_user_email]' => $subscriber_email[0]['value'],
-            '[notify_verify_url]' => '',
-            '[notify_subscribe_url]' => '',
-            '[notify_unsubscribe_url]' => '',
-            '[notify_user_subscribtions]' => '',
-            '[notify_node_title]' => $node_title,
-            '[notify_node_url]' => '',
-            '[notify_notes]' => '',
-          );
-          $body_replacements = array(
-            '[notify_user_name]' => '',
-            '[notify_user_email]' => $subscriber_email[0]['value'],
-            '[notify_verify_url]' => '',
-            '[notify_subscribe_url]' => '',
-            '[notify_unsubscribe_url]' => $unsubscribe_link,
-            '[notify_user_subscribtions]' => $subscriptions_url,
-            '[notify_node_title]' => $node_title,
-            '[notify_node_url]' => $absolute_node_path,
-            '[notify_notes]' => '<div class="notify-notes">' . $email_update_notes . '</div>',
-          );
-          $tokanized_subject = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['general_email_template_subject'], $subject_replacements);
-          $tokanized_body = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['general_email_template'], $body_replacements);
-          strval($tokanized_subject);
-
-          $message['to'] = $subscriber_email[0]['value'];
-          $message['subject'] = $tokanized_subject;
-          $message['body'] = $tokanized_body;
-          $result = \Drupal::service('plugin.manager.mail')->mail(
-             'page_notifications',
-             'notifications_email',
-             $subscriber_email[0]['value'],
-             \Drupal::languageManager()->getDefaultLanguage()->getId(),
-             $message
-           );
-        }
-        \Drupal::messenger()->addStatus(t('Number of email(s) sent: ' . $count_records . '.'));
-      } else {
-        \Drupal::messenger()->addStatus(t('Sorry, there  subscribers for this page.'));
-      }
-      if ($template_info['node_timestamp'] && !is_null($template_info['node_timestamp'])) {
-        $notify_node_timestamp = $template_info['node_timestamp'];
-        if($entity->hasField($notify_node_timestamp)) {
-          $request_time = Drupal::time()->getRequestTime();
-          $entity->set($notify_node_timestamp, $request_time);
-        }
-      }
-      $entity->set($checkbox_field, 0);
-    }
-  }
- }
+function page_notifications_mail($key, &$message, $params) {
+  \Drupal::service('page_notifications.mail_handler')->mail($key, $message, $params);
 }
 
-function page_notifications_node_presave(Drupal\node\NodeInterface $node) {
-  $moduleHandler = \Drupal::service('module_handler');
-  if ($moduleHandler->moduleExists('page_notifications')) {
-    $template_info = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template();
-    //TODO we can replace those two variables
-    $checkbox_field = $template_info['checkbox_field'];
-    $notes_field = $template_info['notes_field'];
-    //TODO Do we want to have people to create checkbox and notes fields on node or just require the checkbox field?
-    if ($node->hasField($checkbox_field) && !is_null($checkbox_field)) {
-      //$node->hasField($notes_field)
-      if ($node->get($checkbox_field)->value == 1) {
-        $records = page_notifications_get_subscribes_list($node->id());
-        $count_records = count($records);
-
-        if ($records && !is_null($records) && $count_records != 0) {
-          $node_title = $node->getTitle();
-          $absolute_node_path = $node->toUrl()->setAbsolute()->toString();
-          if ($template_info['from_email'] && !is_null($template_info['from_email'])) {
-            $from = $template_info['from_email'];
-          }
-          else {
-            $from = \Drupal::config('system.site')->get('mail');
-          }
-
-          if ($template_info['general_email_template_subject'] && !is_null($template_info['general_email_template_subject'])) {
-            $subject = $template_info['general_email_template_subject'];
-          }
-          else {
-            $subject = 'New update on - "' . $node_title . '" page';
-          }
-
-          if ($node->hasField($notes_field) && !is_null($notes_field)) {
-            $email_update_notes = $node->get($notes_field)->value;
-          }
-
-          foreach ($records as $record) {
-            $user_token = $record->get("field_page_notify_token_user_id")->getValue();
-            $subscriber_email = $record->get("field_page_notify_email")->getValue();
-            $subscriber_token_notify = $record->get("field_page_notify_token")->getValue();
-
-            $host = \Drupal::request()->getSchemeAndHttpHost();
-            $unsubscribe_link = $host . "/page-notifications/unsubscribe/" . $node->id() . "-" . $subscriber_token_notify[0]['value'];
-            $unsubscribe_text = 'To unsubscribe, please go to this <a href=' . $unsubscribe_link .'>link</a>';
-            $subscriptions_url = $host . '/page-notifications/verify-list/' . $node->id() . "-" . $subscriber_token_notify[0]['value'];
-
-            $subject_replacements = array(
-              '[notify_user_name]' => '',
-              '[notify_user_email]' => $subscriber_email[0]['value'],
-              '[notify_verify_url]' => '',
-              '[notify_subscribe_url]' => '',
-              '[notify_unsubscribe_url]' => '',
-              '[notify_user_subscribtions]' => '',
-              '[notify_node_title]' => $node_title,
-              '[notify_node_url]' => '',
-              '[notify_notes]' => '',
-            );
-            $body_replacements = array(
-              '[notify_user_name]' => '',
-              '[notify_user_email]' => $subscriber_email[0]['value'],
-              '[notify_verify_url]' => '',
-              '[notify_subscribe_url]' => '',
-              '[notify_unsubscribe_url]' => $unsubscribe_link,
-              '[notify_user_subscribtions]' => $subscriptions_url,
-              '[notify_node_title]' => $node_title,
-              '[notify_node_url]' => $absolute_node_path,
-              '[notify_notes]' => '<div class="notify-notes">' . $email_update_notes . '</div>',
-            );
-            $tokanized_subject = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['general_email_template_subject'], $subject_replacements);
-            $tokanized_body = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['general_email_template'], $body_replacements);
-            strval($tokanized_subject);
-
-            $message['to'] = $subscriber_email[0]['value'];
-            $message['subject'] = $tokanized_subject;
-            $message['body'] = $tokanized_body;
-            $result = \Drupal::service('plugin.manager.mail')->mail(
-               'page_notifications',
-               'notifications_email',
-               $subscriber_email[0]['value'],
-               \Drupal::languageManager()->getDefaultLanguage()->getId(),
-               $message
-             );
-          }
-          \Drupal::messenger()->addStatus(t('Number of email(s) sent: ' . $count_records . '.'));
-        } else {
-          \Drupal::messenger()->addStatus(t('Sorry, there no subscribers for this page.'));
-        }
-        if ($template_info['node_timestamp'] && !is_null($template_info['node_timestamp'])) {
-          $notify_node_timestamp = $template_info['node_timestamp'];
-          if($node->hasField($notify_node_timestamp)) {
-            $request_time = Drupal::time()->getRequestTime();
-            $node->set($notify_node_timestamp, $request_time);
-          }
-        }
-        $node->set($checkbox_field, 0);
-      }
+/**
+ * Implements hook_mail_alter().
+ */
+function page_notifications_mail_alter(&$message) {
+  if (strpos($message['id'], 'page_notifications_') === 0) {
+    // Set format to HTML for our emails.
+    $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed; delsp=yes';
+
+    // Ensure body is processed as HTML.
+    if (is_array($message['body'])) {
+      $message['body'] = [implode("\n\n", $message['body'])];
     }
   }
 }
 
-
-function page_notifications_node_delete($node) {
-  $nid = $node->id();
-  $page_notify_subscription_nids = \Drupal::entityQuery("node")
-   ->accessCheck(FALSE)
-   ->condition('type', 'page_notify_subscriptions')
-   ->condition('field_page_notify_node_id', $nid)
-   ->execute();
-   $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-   $subscribers = $storage_handler->loadMultiple($page_notify_subscription_nids);
-   $storage_handler->delete($subscribers);
+/**
+ * Implements hook_token_info().
+ */
+function page_notifications_token_info() {
+  return \Drupal::service('page_notifications.subscription_token')->hookTokenInfo();
 }
- /**
-  * Implements hook_mail().
-  */
-  function page_notifications_mail($key, &$message) {
-    $message['headers']['Content-Type'] = 'text/html; charset=UTF-8;';
-    $text[] = $message['params']['body'];
-    $message['subject'] = t($message['params']['subject']);
-    $message['body'] = array_map(function ($text) {
-      return Markup::create($text);
-    }, $text);
-  }
 
- function page_notifications_view_alter(array &$build, BlockPluginInterface $block) {
-   // We'll search for the string 'uppercase'.
-   $definition = $block->getPluginDefinition();
-   if ((!empty($build['#configuration']['label']) && mb_strpos($build['#configuration']['label'], 'uppercase')) || (!empty($definition['subject']) && mb_strpos($definition['subject'], 'uppercase'))) {
-     // This will uppercase the block title.
-     $build['#configuration']['label'] = mb_strtoupper($build['#configuration']['label']);
-   }
- }
-
- function page_notifications_get_subscribes_list($nid) {
-   $nids = \Drupal::entityQuery("node")
-    ->accessCheck(FALSE)
-    ->condition('type', 'page_notify_subscriptions')
-    ->condition('field_page_notify_node_id', $nid)
-    ->condition('status', TRUE)
-    ->execute();
-    $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-    $subscribers = $storage_handler->loadMultiple($nids);
-    return $subscribers;
-}
+/**
+ * Implements hook_tokens().
+ */
+function page_notifications_tokens($type, $tokens, array $data, array $options, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) {
+  return \Drupal::service('page_notifications.subscription_token')->hookTokens($type, $tokens, $data, $options, $bubbleable_metadata);
+}
\ No newline at end of file
diff --git a/page_notifications.permissions.yml b/page_notifications.permissions.yml
index 0977ed4..fee0522 100644
--- a/page_notifications.permissions.yml
+++ b/page_notifications.permissions.yml
@@ -1,8 +1,20 @@
-access protected page notifications:
-  title: 'Access protected page notifications'
-  description: 'Bypass access control when accessing page notifications.'
-  restrict access: TRUE
-view page notifications reports:
-  title: 'View page notifications reports'
-  description: 'View lists and statistics of page notifications.'
+administer page notification subscriptions:
+  title: 'Administer page notification subscriptions'
+  description: 'Full administrative access to page notification subscriptions.'
   restrict access: TRUE
+
+view page notification subscriptions:
+  title: 'View page notification subscriptions'
+  description: 'View existing page notification subscriptions.'
+
+create page notification subscriptions:
+  title: 'Create page notification subscriptions'
+  description: 'Create new page notification subscriptions.'
+
+edit page notification subscriptions:
+  title: 'Edit page notification subscriptions'
+  description: 'Edit existing page notification subscriptions.'
+
+delete page notification subscriptions:
+  title: 'Delete page notification subscriptions'
+  description: 'Delete existing page notification subscriptions.'
\ No newline at end of file
diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index 19abc11..a0530a0 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -1,115 +1,33 @@
-page_notifications.tabs:
-  path: '/admin/page-notifications/tabs'
+page_notifications.settings:
+  path: '/admin/config/system/page-notifications'
   defaults:
-    _form: '\Drupal\page_notifications\Form\GeneralSettingsForm'
-    _title: 'General configuration'
+    _form: '\Drupal\page_notifications\Form\SettingsForm'
+    _title: 'Page Notifications Settings'
   requirements:
-    _permission: 'access protected page notifications'
-page_notifications.tabs_second:
-  path: '/admin/page-notifications/tabs/second'
+    _permission: 'administer page notification subscriptions'
+
+page_notifications.subscription_list:
+  path: '/admin/content/subscriptions'
   defaults:
-    _form: '\Drupal\page_notifications\Form\MigrationForm'
-    _title: 'Migration of Subscriptions'
+    _entity_list: 'page_notification_subscription'
+    _title: 'Page Notification Subscriptions'
   requirements:
-    _permission: 'access protected page notifications'
-page_notifications.tabs_third:
-  path: '/admin/page-notifications/tabs/third'
+    _permission: 'administer page notification subscriptions'
+
+page_notifications.subscription.verify:
+  path: '/page-notifications/verify/{token}'
   defaults:
-    _form: '\Drupal\page_notifications\Form\ContentTypeMigrationForm'
-    _title: 'Migration of nodes to new Content type'
+    _controller: 'page_notifications.notification_manager:verifySubscriptionRoute'
+    _title: 'Verify Subscription'
   requirements:
-    _permission: 'access protected page notifications'
-page_notifications.node_subscriptions:
-  path: '/admin/page-notifications/all-subscriptions/{node_id}'
-  defaults:
-    _title: 'Page Notifications - Node Subscriptions List'
-    _controller: '\Drupal\page_notifications\Controller\AdminSubscriptionsListPage::getNodeSubscribersList'
-    arg1: ''
-  requirements:
-    _permission: 'access protected page notifications'
+    _access: 'TRUE'
   options:
-    no_cache: 'TRUE'
-page_notifications.all:
-  path: '/admin/page-notifications/all-subscriptions'
-  defaults:
-    _title: 'Page Notifications - All Subscriptions'
-    _controller: '\Drupal\page_notifications\Controller\AdminSubscriptionsListPage::getNodeSubscribersList'
-  requirements:
-    _permission: 'access protected page notifications'
-  options:
-    no_cache: 'TRUE'
-page_notifications.tabs_default_second:
-  path: '/admin/page-notifications/tabs/default/second'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\MessagesForm'
-    _title: 'Messages configuration'
-  requirements:
-    _permission: 'access protected page notifications'
-page_notifications.path_override:
-  path: '/admin/page-notifications/menu-original-path'
-  defaults:
-    _title: 'Menu path that will be altered'
-    _controller: '\Drupal\page_notifications\Controller\PageNotificationsController::pathOverride'
-  requirements:
-    _permission: 'access content'
-route_callbacks:
-  - '\Drupal\page_notifications\Routing\PageNotificationsDynamicRoutes::routes'
-page_notifications.page_notifications_form_confirm:
-  path: '/page-notifications/confirmation/{email}/{subscription_token}'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\EmailConfirmationPage'
-    _title: 'Confirmation Page'
-  requirements:
-    _permission: 'access content'
-page_notifications.page_notifications_form_verification:
-  path: '/page-notifications/verify-list/{subscription_token}'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\AccessVerificationStep'
-    _title: 'My Subscribtions'
-  requirements:
-    _permission: 'access content'
-page_notifications.user_subscriptions_page:
-  path: '/page-notifications/my-subscriptions/{subscription_token}'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\UserSubscriptionsPage'
-    _title: 'Manage Your Page Watching Subscriptions'
-  requirements:
-    _permission: 'access content'
-  options:
-    no_cache: 'TRUE'
-page_notifications.subscriberpage:
-  path: '/page-notifications/my-list/{user_token}'
-  defaults:
-    _controller: '\Drupal\page_notifications\Controller\SubscriberPage::subscriberpage'
-    _title: 'Manage Your Page Watching Subscriptions'
-  requirements:
-    _permission: 'access content'
-  options:
-    no_cache: 'TRUE'
-page_notifications.cancel_subscription_ajax:
-  path: '/ajax/cancel_subscription/{token}'
-  defaults:
-    _controller: '\Drupal\page_notifications\Controller\SubscriberPage::cancel_subscription'
-    _title: 'Manage Your Page Watching Subscriptions'
-  requirements:
-    _permission: 'access content'
-page_notifications.cancel_all_ajax:
-  path: '/ajax/cancel_all/{user_token}'
-  defaults:
-    _controller: '\Drupal\page_notifications\Controller\SubscriberPage::cancel_all'
-    _title: 'Manage Your Page Watching Subscriptions'
-  requirements:
-    _permission: 'access content'
-page_notifications.page_notify_unsubscribe:
-  path: '/page-notifications/unsubscribe/{subscription_token}'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\EmailUnsubscribePage'
-  requirements:
-    _permission: 'access content'
-page_notifications.autocomplete.subscriptions:
-  path: '/admin/page-notifications/autocomplete/subscriptions'
+    no_cache: TRUE
+
+entity.subscription.delete_form:
+  path: '/admin/structure/page_notifications_subscription/{subscription}/delete'
   defaults:
-    _controller: '\Drupal\page_notifications\Controller\SubscriptionsAutoCompleteController::handleAutocomplete'
-    _format: json
+    _entity_form: 'page_notification_subscription.delete'
+    _title: 'Delete Subscription'
   requirements:
-    _permission: 'access protected page notifications'
+    _entity_access: 'page_notification_subscription.delete'
\ No newline at end of file
diff --git a/page_notifications.services.yml b/page_notifications.services.yml
index 0b80dfd..a2e28a4 100644
--- a/page_notifications.services.yml
+++ b/page_notifications.services.yml
@@ -1,12 +1,26 @@
 services:
-  load.databaseinnfo.service:
-    class: Drupal\page_notifications\LoadDataBaseInfo
-  page_notifications.access_check.role:
-    class: Drupal\page_notifications\Access\RoleAccessCheck
-    arguments: ['@current_user']
+  page_notifications.notification_manager:
+    class: Drupal\page_notifications\Service\NotificationManager
+    arguments:
+      - '@config.factory'
+      - '@plugin.manager.mail'
+      - '@entity_type.manager'
+      - '@queue'
+      - '@logger.factory'
+      - '@event_dispatcher'
+      - '@datetime.time'
+      - '@string_translation'
+      - '@messenger'
+    calls:
+      - [setStringTranslation, ['@string_translation']]
+  page_notifications.mail_handler:
+    class: Drupal\page_notifications\Mail\PageNotificationsMailHandler
+    arguments:
+      - '@config.factory'
+      - '@renderer'
+      - '@token'
+      - '@string_translation'
+  page_notifications.subscription_token:
+    class: Drupal\page_notifications\Token\SubscriptionToken
     tags:
-      - { name: access_check, applies_to: _page_notifications_role }
-  page_notifications.route_subscriber:
-    class: Drupal\page_notifications\Routing\RouteSubscriber
-    tags:
-      - { name: event_subscriber }
+      - { name: token.provider }
\ No newline at end of file
diff --git a/src/Access/RoleAccessCheck.php b/src/Access/RoleAccessCheck.php
deleted file mode 100644
index efac4b1..0000000
--- a/src/Access/RoleAccessCheck.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Access;
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Routing\Access\AccessInterface;
-use Drupal\Core\Session\AccountInterface;
-
-class RoleAccessCheck implements AccessInterface {
-
-  /**
-   * Checks access.
-   *
-   * @param \Drupal\Core\Session\AccountInterface $account
-   *   The currently logged in account.
-   *
-   * @return string
-   *   A \Drupal\Core\Access\AccessInterface constant value.
-   */
-  public function access(AccountInterface $account) {
-    return AccessResult::allowedIf($account->isAuthenticated());
-  }
-
-}
diff --git a/src/Controller/AdminSubscriptionsListPage.php b/src/Controller/AdminSubscriptionsListPage.php
deleted file mode 100644
index efa8908..0000000
--- a/src/Controller/AdminSubscriptionsListPage.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Drupal\Component\Render\FormattableMarkup;
-use Drupal\page_notifications\Form\AccessVerificationStep;
-use Drupal\taxonomy\Entity\Term;
-
-class AdminSubscriptionsListPage extends ControllerBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getModuleName() {
-    return 'admin-node-subscribers-list';
-  }
-
-  public function getNodeSubscribersList($node_id = NULL) {
-  $node_id_parts = explode("-", $node_id);
-  if (count($node_id_parts) == 2) {
-    $node_id_row = $node_id_parts[0];
-    $node_id_entity_type = $node_id_parts[1];
-  } else {
-    $node_id_row = $node_id;
-    $node_id_entity_type = 'node';
-  }
-
-  $header = array(
-    array('data' => $this->t('Node Title'), 'field' => 'title', 'sort' => 'asc'),
-    array('data' => $this->t('E-Mail'), 'field' => 'e-mail'),
-    array('data' => $this->t('Operations')),
-  );
-
-  if ($node_id_entity_type == 'node') {
-    $query = \Drupal::entityQuery('node')
-      ->accessCheck(FALSE)
-      ->condition('type', 'page_notify_subscriptions')
-      ->condition('field_page_notify_node_id', $node_id_row, '=')
-      ->condition('status', 1)
-      ->sort('created', 'DESC')
-      ->pager(25);
-    $records = $query->execute();
-  } elseif ($node_id_entity_type == 'term') {
-    $query = \Drupal::entityQuery('node')
-      ->accessCheck(FALSE)
-      ->condition('type', 'page_notify_subscriptions')
-      ->condition('field_page_notify_node_id', $node_id_row, '=')
-      ->condition('field_page_notify_token', 'term', 'CONTAINS')
-      ->condition('status', 1)
-      ->sort('created', 'DESC')
-      ->pager(25);
-      $records = $query->execute();
-  } else {
-    $query = \Drupal::entityQuery('node')
-      ->accessCheck(FALSE)
-      ->condition('type', 'page_notify_subscriptions')
-      ->condition('status', 1)
-      ->sort('created', 'DESC')
-      ->pager(25);
-    $records = $query->execute();
-  }
-      $rows = array();
-      foreach ($records as $record) {
-        if ($record) {
-          $node = \Drupal\node\Entity\Node::load($record);
-          $page_title = $node->getTitle();
-          $subscribed_node_url = $node->toUrl()->setAbsolute()->toString();
-          $field_page_notify_email = $node->get("field_page_notify_email")->getValue();
-        }
-
-          $rows[] = array('data' => array(
-            'title' => new FormattableMarkup('<a href="@page_url">@page_title</a>',
-              [
-                '@page_title' => $page_title,
-                '@page_url' => $subscribed_node_url,
-              ]),
-            'e-mail' => new FormattableMarkup('@user_email',
-                [
-                  '@user_email' => $field_page_notify_email[0]['value'],
-                ]),
-            'cancel_one' => new FormattableMarkup('<a href="@record_link">@name</a>',
-                ['@name' => 'View', '@record_link' => $subscribed_node_url]
-              ),
-          ));
-      }
-
-
-      $page_name = '<h1>Subscriptions List</h1>';
-      $build['page_name'] = [
-        '#markup' => $page_name,
-        '#attributes' => [
-          'class' => ['page-notifications-user-list-page-name'],
-        ],
-      ];
-      $build['config_table'] = array(
-        '#theme' => 'table',
-        '#header' => $header,
-        '#rows' => $rows,
-        '#empty' => t('No records found'),
-        '#attributes' => [
-          'class' => ['page-notifications-block-subscriberpage'],
-          'id' => 'page-notifications-block-subscriberpage',
-          'no_striping' => TRUE,
-        ],
-      );
-      $build['pager'] = array(
-        '#type' => 'pager'
-      );
-      return $build;
-  }
-}
diff --git a/src/Controller/PageNotificationsController.php b/src/Controller/PageNotificationsController.php
deleted file mode 100644
index 449bec9..0000000
--- a/src/Controller/PageNotificationsController.php
+++ /dev/null
@@ -1,256 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-//use Drupal\examples\Utility\DescriptionTemplateTrait;
-
-/**
- * Controller routines for menu example routes.
- */
-class PageNotificationsController extends ControllerBase {
-
-  //use DescriptionTemplateTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getModuleName() {
-    return 'page_notifications';
-  }
-
-  /**
-   * Page callback for the simplest introduction menu entry.
-   *
-   * The controller callback defined page_notifications.routing.yml file,
-   * maps the path 'admin/page-notifications' to this method.
-   *
-   * @throws \InvalidArgumentException
-   */
-  public function basicInstructions() {
-    return [
-      $this->description(),
-    ];
-  }
-
-  /**
-   * Show a menu link in a menu other than the default "Navigation" menu.
-   */
-  public function alternateMenu() {
-    return [
-      '#markup' => $this->t('This will be in the Main menu instead of the default Tools menu'),
-    ];
-
-  }
-
-  /**
-   * A menu entry with simple permissions using 'access protected menu example'.
-   *
-   * @throws \InvalidArgumentException
-   */
-  public function permissioned() {
-    $url = Url::fromRoute('page_notifications.permissioned_controlled');
-    return [
-      '#markup' => $this->t('A menu item that requires the "access protected menu example" permission is at @link', [
-        '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName())->toString(),
-      ]),
-    ];
-  }
-
-  /**
-   * Only accessible when the user will be granted with required permission.
-   *
-   * The permission is defined in file page_notifications.permissions.yml.
-   */
-  public function permissionedControlled() {
-    return [
-      '#markup' => $this->t('This menu entry will not show and the page will not be accessible without the "access protected menu example" permission to current user.'),
-    ];
-  }
-
-  /**
-   * Demonstrates the use of custom access check in routes.
-   *
-   * @throws \InvalidArgumentException
-   *
-   * @see \Drupal\page_notifications\Controller\PageNotificationsController::customAccessPage()
-   */
-  public function customAccess() {
-    $url = Url::fromRoute('page_notifications.custom_access_page');
-    return [
-      '#markup' => $this->t('A menu item that requires the user to posess a role of "authenticated" is at @link', [
-        '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName())->toString(),
-      ]),
-    ];
-  }
-
-  /**
-   * Content will be displayed only if access check is satisfied.
-   *
-   * @see \Drupal\page_notifications\Controller\PageNotificationsController::customAccess()
-   */
-  public function customAccessPage() {
-    return [
-      '#markup' => $this->t('This menu entry will not be visible and access will result
-        in a 403 error unless the user has the "authenticated" role. This is
-        accomplished with a custom access check plugin.'),
-    ];
-  }
-
-  /**
-   * Give the user a link to the route-only page.
-   *
-   * @throws \InvalidArgumentException
-   */
-  public function routeOnly() {
-    $url = Url::fromRoute('page_notifications.route_only.callback');
-    return [
-      '#markup' => $this->t('A menu entry with no menu link is at @link', [
-        '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName())->toString(),
-      ]),
-    ];
-  }
-
-  /**
-   * Such callbacks can be user for creating web services in Drupal 8.
-   */
-  public function routeOnlyCallback() {
-    return [
-      '#markup' => $this->t('The route entry has no corresponding menu links entry, so it provides a route without a menu link, but it is the same in every other way to the simplest example.'),
-    ];
-  }
-
-  /**
-   * Uses the path and title to determine the page content.
-   *
-   * This controller is mapped dynamically based on the 'route_callbacks:' key
-   * in the routing YAML file.
-   *
-   * @param string $path
-   *   Path/URL of menu item.
-   * @param string $title
-   *   Title of menu item.
-   *
-   * @return array
-   *   Controller response.
-   *
-   * @see Drupal\page_notifications\Routing\PageNotificationsDynamicRoutes
-   */
-  public function tabsPage($path, $title) {
-    $secondary = substr_count($path, '/') > 2 ? 'secondary ' : '';
-    return [
-      '#markup' => $this->t('This is the @secondary tab "@tabname" in the "basic tabs" example.', ['@secondary' => $secondary, '@tabname' => $title]),
-    ];
-  }
-
-  /**
-   * Demonstrates use of optional URL arguments in for menu item.
-   *
-   * @param string $arg1
-   *   First argument of URL.
-   * @param string $arg2
-   *   Second argument of URL.
-   *
-   * @return array
-   *   Controller response.
-   *
-   * @see https://www.drupal.org/docs/8/api/routing-system/parameters-in-routes
-   */
-  public function urlArgument($arg1, $arg2) {
-    // Perpare URL for single arguments.
-    $url_single = Url::fromRoute('page_notifications.use_url_arguments', ['arg1' => 'one']);
-
-    // Prepare URL for multiple arguments.
-    $url_double = Url::fromRoute('page_notifications.use_url_arguments', ['arg1' => 'one', 'arg2' => 'two']);
-
-    // Add these argument links to the page content.
-    $markup = $this->t('This page demonstrates using arguments in the url. For example, access it with @link_single for single argument or @link_double for two arguments in URL', [
-      '@link_single' => Link::createFromRoute($url_single->getInternalPath(), $url_single->getRouteName(), $url_single->getRouteParameters())->toString(),
-      '@link_double' => Link::createFromRoute($url_double->getInternalPath(), $url_double->getRouteName(), $url_double->getRouteParameters())->toString(),
-    ]);
-
-    // Process the arguments if they're provided.
-    if (!empty($arg1)) {
-      $markup .= '<div>' . $this->t('Argument 1 = @arg', ['@arg' => $arg1]) . '</div>';
-    }
-    if (!empty($arg2)) {
-      $markup .= '<div>' . $this->t('Argument 2 = @arg', ['@arg' => $arg2]) . '</div>';
-    }
-
-    // Finally return the markup.
-    return [
-      '#markup' => $markup,
-    ];
-  }
-
-  /**
-   * Demonstrate generation of dynamic creation of page title.
-   *
-   * @see \Drupal\page_notifications\Controller\PageNotificationsController::backTitle()
-   */
-  public function titleCallbackContent() {
-    return [
-      '#markup' => $this->t('The title of this page is dynamically changed by the title callback for this route defined in menu_example.routing.yml.'),
-    ];
-  }
-
-  /**
-   * Generates title dynamically.
-   *
-   * @see \Drupal\page_notifications\Controller\PageNotificationsController::titleCallback()
-   */
-  public function titleCallback() {
-    return [
-      '#markup' => $this->t('The new title is your username: @name', [
-        '@name' => $this->currentUser()->getDisplayName(),
-      ]),
-    ];
-  }
-
-  /**
-   * Demonstrates how you can provide a placeholder url arguments.
-   *
-   * @throws \InvalidArgumentException
-   *
-   * @see \Drupal\page_notifications\Controller\PageNotificationsController::placeholderArgsDisplay()
-   * @see https://www.drupal.org/docs/8/api/routing-system/using-parameters-in-routes
-   */
-  public function placeholderArgs() {
-    $url = Url::fromRoute('page_notifications.placeholder_argument.display', ['arg' => 3343]);
-    return [
-      '#markup' => $this->t('Demonstrate placeholders by visiting @link', [
-        '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName(), $url->getRouteParameters())->toString(),
-      ]),
-    ];
-  }
-
-  /**
-   * Displays placeholder argument supplied in URL.
-   *
-   * @param int $arg
-   *   URL argument.
-   *
-   * @return array
-   *   URL argument.
-   *
-   * @see \Drupal\page_notifications\Controller\PageNotificationsController::placeholderArgs()
-   */
-  public function placeholderArgsDisplay($arg) {
-    return [
-      '#markup' => $arg,
-    ];
-
-  }
-
-  /**
-   * Demonstrate how one can alter the existing routes.
-   */
-  public function pathOverride() {
-    return [
-      '#markup' => $this->t('This menu item was created strictly to allow the RouteSubscriber class to have something to operate on. page_notifications.routing.yml defined the path as admin/menu-example/menu-original-path. The alterRoutes() changes it to /admin/menu-example/menu-altered-path. You can try navigating to both paths and see what happens!'),
-    ];
-  }
-
-}
diff --git a/src/Controller/SubscriberPage.php b/src/Controller/SubscriberPage.php
deleted file mode 100644
index 94563b7..0000000
--- a/src/Controller/SubscriberPage.php
+++ /dev/null
@@ -1,194 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
-use Drupal\examples\Utility\DescriptionTemplateTrait;
-use Drupal\Core\Ajax\HtmlCommand;
-use Drupal\Component\Render\FormattableMarkup;
-use Drupal\page_notifications\Form\AccessVerificationStep;
-use Drupal\Core\Url;
-use Drupal\Core\Link;
-use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Ajax\ReplaceCommand;
-use Drupal\taxonomy\Entity\Term;
-
-/**
- * Controller routines for page example routes.
- */
-class SubscriberPage extends ControllerBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getModuleName() {
-    return 'subscriberpage';
-  }
-
-  public function subscriberpage($user_token = NULL) {
-    if ($user_token && !is_null($user_token)) {
-      $header = array(
-        // We make it sortable by name.
-        array('data' => $this->t('Watching pages list'), 'field' => 'title', 'sort' => 'asc'),
-        array('data' => $this->t('')),
-      );
-
-      $query = \Drupal::entityQuery('node')
-        ->accessCheck(FALSE)
-        ->condition('type', 'page_notify_subscriptions')
-        ->condition('field_page_notify_token_user_id', $user_token, '=')
-        ->condition('status', 1)
-        ->sort('created', 'DESC')
-        ->pager(10);
-      $records = $query->execute();
-
-      // Populate the rows.
-      $rows = array();
-      foreach ($records as $record) {
-        $subscriprion_node = \Drupal\node\Entity\Node::load($record);
-        $record_field_page_notify_node_id = $subscriprion_node->get("field_page_notify_node_id")->getValue();
-        $record_field_token_notify = $subscriprion_node->get("field_page_notify_token")->getValue();
-        $record_field_token_notify_pieces = explode("-", $record_field_token_notify[0]['value']);
-
-        if (count($record_field_token_notify_pieces) == 2) {
-          $record_field_entity_type = $record_field_token_notify_pieces[1];
-        } else {
-          $record_field_entity_type = 'node';
-        }
-
-        if ($record_field_entity_type == 'node') {
-          $node = \Drupal\node\Entity\Node::load($record_field_page_notify_node_id[0]['value']);
-          $page_title = $node->getTitle();
-          $subscribed_node_url = $node->toUrl()->setAbsolute()->toString();
-        } elseif ($record_field_entity_type == 'term') {
-          $node = Term::load($record_field_page_notify_node_id[0]['value']);
-          $page_title = $node->getName();
-          $subscribed_node_url = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical',['taxonomy_term' => $node->tid->value],['absolute' => TRUE])->toString();
-        } else {
-          $node = NULL;
-        }
-
-        $rows[] = array('data' => array(
-          'title' => new FormattableMarkup('<a href="@page_url">@page_title</a>',
-            [
-              '@page_title' => $page_title,
-              '@page_url' => $subscribed_node_url,
-            ]),
-          'cancel_one' => new FormattableMarkup('<a id="notify-cancel-@token" href="/nojs/cancel_subscription/@token" class="use-ajax btn btn-default notify-cancel-subscription">@name</a>',
-              ['@name' => 'Stop Watching', '@token' => $record_field_token_notify[0]['value']]
-            ),
-        ));
-
-        if ($rows) {
-          $cancelall =  '<a id="notify-cancel-all" href="/nojs/cancel_all/' . $user_token .'" class="use-ajax btn btn-default notify-cancel-all-subscription">Unsubscribe from all</a>';
-          $header = array(
-            // We make it sortable by name.
-            array('data' => $this->t('Page Name'), 'field' => 'title', 'sort' => 'asc'),
-            array('data' => $this->t($cancelall)),
-          );
-        }
-        else {
-          $build = array();
-        }
-      }
-
-      $page_name = '<h1>Manage Your Page Watching Subscriptions</h1>';
-      $build['page_name'] = [
-        '#markup' => $page_name,
-        '#attributes' => [
-          'class' => ['page-notifications-user-list-page-name'],
-        ],
-      ];
-      $build['config_table'] = array(
-        '#theme' => 'table',
-        '#header' => $header,
-        '#rows' => $rows,
-        '#empty' => t('No records found'),
-        '#attributes' => [
-          'class' => ['page-notifications-block-subscriberpage'],
-          'id' => 'page-notifications-block-subscriberpage',
-          'no_striping' => TRUE,
-        ],
-      );
-      $build['pager'] = array(
-        '#type' => 'pager'
-      );
-      return $build;
-    }
-  }
-
-  public function cancel_subscription($token) {
-      $response = new AjaxResponse();
-      page_notifications_delete_record($token);
-      $response->addCommand(new ReplaceCommand('#notify-cancel-' . $token, '<span class="notify-cancel-cancelled">Cancelled</span>'));
-      return $response;
-  }
-
-  public function cancel_all($user_token) {
-      $response = new AjaxResponse();
-      page_notifications_delete_all_records($user_token);
-      $response->addCommand(new ReplaceCommand('#notify-cancel-all', '<span class="notify-cancel-all-cancelled">All cancelled</span>'));
-      $response->addCommand(new ReplaceCommand('#notify-cancel-', "Cancelled"));
-      return $response;
-  }
-}
-
-
-
-function checkForRecord($subscription_token_url, $email) {
-  $record = current(\Drupal::entityTypeManager()->getStorage('node')
-    ->loadByProperties([
-      'field_page_notify_token' => $subscription_token_url,
-      'field_page_notify_email' => $email
-    ])
-  );
-  if ($record) {
-    return $record;
-  }
-  else {
-    return FALSE;
-  }
-}
-
-function getAllRecords($email) {
-  $query = \Drupal::entityQuery('node')
-    ->accessCheck(FALSE)
-    ->condition('status', 1)
-    ->condition('field_page_notify_email', $email, '=');
-  $records = $query->execute();
-  foreach ($records as $key => $record) {
-    $node = \Drupal\node\Entity\Node::load($record);
-    $nodes[] = $node;
-  }
-  if ($nodes) {
-    return $nodes;
-  }
-  else {
-    return FALSE;
-  }
-}
-
-function page_notifications_delete_record($token) {
-  $num_deleted = \Drupal::entityQuery("node")
-    ->accessCheck(FALSE)
-    ->condition("type", "page_notify_subscriptions")
-    ->condition("field_page_notify_token", $token)
-    ->accessCheck(FALSE)
-    ->execute();
-  $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-  $entities = $storage_handler->loadMultiple($num_deleted);
-  $storage_handler->delete($entities);
-}
-
-function page_notifications_delete_all_records($user_token) {
-  $num_deleted = \Drupal::entityQuery("node")
-    ->accessCheck(FALSE)
-    ->condition("type", "page_notify_subscriptions")
-    ->condition("field_page_notify_token_user_id", $user_token)
-    ->accessCheck(FALSE)
-    ->execute();
-  $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-  $entities = $storage_handler->loadMultiple($num_deleted);
-  $storage_handler->delete($entities);
-}
diff --git a/src/Controller/SubscriptionsAutoCompleteController.php b/src/Controller/SubscriptionsAutoCompleteController.php
deleted file mode 100644
index dea67c6..0000000
--- a/src/Controller/SubscriptionsAutoCompleteController.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Symfony\Component\HttpFoundation\JsonResponse;
-use Symfony\Component\HttpFoundation\Request;
-use Drupal\Component\Utility\Xss;
-use Drupal\Core\Entity\Element\EntityAutocomplete;
-
-/**
- * Defines a route controller for watches autocomplete form elements.
- */
-class SubscriptionsAutoCompleteController extends ControllerBase {
-
-  /**
-   * The node storage.
-   *
-   * @var \Drupal\node\NodeStorage
-   */
-  protected $nodeStorage;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
-    $this->nodeStroage = $entity_type_manager->getStorage('node');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('entity_type.manager')
-    );
-  }
-
-  /**
-   * Handler for autocomplete request.
-   */
-  public function handleAutocomplete(Request $request) {
-    $results = [];
-    $input = $request->query->get('q');
-
-    if (!$input) {
-      return new JsonResponse($results);
-    }
-
-    $input = Xss::filter($input);
-
-    $query = $this->nodeStroage->getQuery()
-      ->condition('title', $input, 'CONTAINS')
-      ->groupBy('nid')
-      ->sort('created', 'DESC')
-      ->range(0, 10);
-
-    $ids = $query->execute();
-    $nodes = $ids ? $this->nodeStroage->loadMultiple($ids) : [];
-
-    foreach ($nodes as $node) {
-      switch ($node->isPublished()) {
-        case TRUE:
-          $availability = '✅';
-          break;
-
-        case FALSE:
-        default:
-          $availability = '🚫';
-          break;
-      }
-
-      $label = [
-        $node->getTitle(),
-        '<small>(' . $node->id() . ')</small>',
-        $availability,
-      ];
-
-      $results[] = [
-        'value' => EntityAutocomplete::getEntityLabels([$node]),
-        'label' => implode(' ', $label),
-      ];
-    }
-    return new JsonResponse($results);
-  }
-}
diff --git a/src/Entity/Subscription.php b/src/Entity/Subscription.php
new file mode 100644
index 0000000..c8fe448
--- /dev/null
+++ b/src/Entity/Subscription.php
@@ -0,0 +1,243 @@
+<?php
+
+namespace Drupal\page_notifications\Entity;
+
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\user\EntityOwnerTrait;
+
+/**
+ * Defines the Subscription entity.
+ *
+ * @ContentEntityType(
+ *   id = "page_notification_subscription",
+ *   label = @Translation("Page Notification Subscription"),
+ *   handlers = {
+ *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
+ *     "list_builder" = "Drupal\page_notifications\Entity\SubscriptionListBuilder",
+ *     "form" = {
+ *       "default" = "Drupal\page_notifications\Form\SubscriptionForm",
+ *       "delete" = "Drupal\page_notifications\Form\SubscriptionDeleteForm"
+ *     },
+ *     "access" = "Drupal\page_notifications\Entity\SubscriptionAccessControlHandler",
+ *   },
+ *   base_table = "page_notification_subscription",
+ *   admin_permission = "administer page notification subscriptions",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "owner" = "uid",
+ *   },
+ *   links = {
+ *     "canonical" = "/admin/structure/page-notification-subscription/{page_notification_subscription}",
+ *     "delete-form" = "/admin/structure/page-notification-subscription/{page_notification_subscription}/delete",
+ *     "collection" = "/admin/structure/page-notification-subscription"
+ *   }
+ * )
+ */
+class Subscription extends ContentEntityBase implements SubscriptionInterface {
+
+  use EntityChangedTrait;
+  use EntityOwnerTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEmail() {
+    return $this->get('email')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setEmail($email) {
+    $this->set('email', $email);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSubscribedEntityId() {
+    return $this->get('subscribed_entity_id')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSubscribedEntityId($id) {
+    $this->set('subscribed_entity_id', $id);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSubscribedEntityType() {
+    return $this->get('subscribed_entity_type')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSubscribedEntityType($entity_type) {
+    $this->set('subscribed_entity_type', $entity_type);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getToken() {
+    return $this->get('token')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setToken($token) {
+    $this->set('token', $token);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isActive() {
+    return (bool) $this->get('status')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setActive($status) {
+    $this->set('status', $status ? 1 : 0);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCreatedTime() {
+    return $this->get('created')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCreatedTime($timestamp) {
+    $this->set('created', $timestamp);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLanguageCode() {
+    return $this->get('langcode')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLanguageCode($langcode) {
+    $this->set('langcode', $langcode);
+    return $this;
+  }
+
+/**
+   * Gets the default langcode.
+   *
+   * @return string
+   *   The site's default language code.
+   */
+  public static function getDefaultLangcode() {
+    return \Drupal::config('system.site')->get('default_langcode') ?: 'en';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+    $fields += static::ownerBaseFieldDefinitions($entity_type);
+
+    $fields['email'] = BaseFieldDefinition::create('email')
+      ->setLabel(t('Email'))
+      ->setDescription(t('The email address of the subscriber.'))
+      ->setRequired(TRUE)
+      ->setTranslatable(TRUE)
+      ->setSettings([
+        'max_length' => 255,
+      ])
+      ->setDisplayOptions('view', [
+        'label' => 'above',
+        'type' => 'string',
+        'weight' => -5,
+      ])
+      ->setDisplayOptions('form', [
+        'type' => 'email_default',
+        'weight' => -5,
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['subscribed_entity_id'] = BaseFieldDefinition::create('integer')
+      ->setLabel(t('Subscribed Entity ID'))
+      ->setDescription(t('The ID of the entity being subscribed to.'))
+      ->setRequired(TRUE)
+      ->setTranslatable(FALSE);
+
+    $fields['subscribed_entity_type'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Subscribed Entity Type'))
+      ->setDescription(t('The type of the entity being subscribed to.'))
+      ->setRequired(TRUE)
+      ->setTranslatable(FALSE)
+      ->setSettings([
+        'max_length' => 32,
+      ]);
+
+    $fields['token'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Token'))
+      ->setDescription(t('The subscription verification token.'))
+      ->setRequired(TRUE)
+      ->setTranslatable(FALSE)
+      ->setSettings([
+        'max_length' => 64,
+      ]);
+
+    $fields['status'] = BaseFieldDefinition::create('boolean')
+      ->setLabel(t('Status'))
+      ->setDescription(t('A boolean indicating whether the subscription is active.'))
+      ->setDefaultValue(TRUE)
+      ->setTranslatable(FALSE)
+      ->setDisplayOptions('form', [
+        'type' => 'boolean_checkbox',
+        'weight' => 0,
+      ]);
+
+    $fields['created'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Created'))
+      ->setDescription(t('The time that the subscription was created.'))
+      ->setTranslatable(FALSE);
+
+    $fields['changed'] = BaseFieldDefinition::create('changed')
+      ->setLabel(t('Changed'))
+      ->setDescription(t('The time that the subscription was last edited.'))
+      ->setTranslatable(FALSE);
+
+    $fields['langcode'] = BaseFieldDefinition::create('language')
+    ->setLabel(t('Language'))
+    ->setDescription(t('The subscription language code.'))
+    ->setDefaultValueCallback(static::class . '::getDefaultLangcode')
+    ->setDisplayOptions('form', [
+      'type' => 'language_select',
+      'weight' => 2,
+    ]);
+
+    return $fields;
+  }
+
+}
\ No newline at end of file
diff --git a/src/Entity/SubscriptionAccessControlHandler.php b/src/Entity/SubscriptionAccessControlHandler.php
new file mode 100644
index 0000000..9a6931d
--- /dev/null
+++ b/src/Entity/SubscriptionAccessControlHandler.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\page_notifications\Entity;
+
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Access\AccessResult;
+
+/**
+ * Access controller for page notification subscription entities.
+ */
+class SubscriptionAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $entity */
+    switch ($operation) {
+      case 'view':
+        return AccessResult::allowedIfHasPermission($account, 'view page notification subscriptions');
+
+      case 'update':
+        return AccessResult::allowedIfHasPermission($account, 'edit page notification subscriptions');
+
+      case 'delete':
+        // Allow deletion if user has permission or is the owner of the subscription
+        return AccessResult::allowedIfHasPermissions($account, [
+          'delete page notification subscriptions',
+          'administer page notification subscriptions',
+        ], 'OR')
+          ->orIf(AccessResult::allowedIf($account->isAuthenticated() && $account->id() === $entity->getOwnerId())
+            ->addCacheableDependency($entity));
+    }
+
+    return AccessResult::neutral();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    return AccessResult::allowedIfHasPermissions($account, [
+      'create page notification subscriptions',
+      'administer page notification subscriptions',
+    ], 'OR');
+  }
+
+}
\ No newline at end of file
diff --git a/src/Entity/SubscriptionInterface.php b/src/Entity/SubscriptionInterface.php
new file mode 100644
index 0000000..ef8d414
--- /dev/null
+++ b/src/Entity/SubscriptionInterface.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Drupal\page_notifications\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\user\EntityOwnerInterface;
+
+/**
+ * Interface for Page Notification Subscription entities.
+ */
+interface SubscriptionInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface {
+
+  /**
+   * Gets the subscription email.
+   *
+   * @return string
+   *   The subscription email address.
+   */
+  public function getEmail();
+
+  /**
+   * Sets the subscription email.
+   *
+   * @param string $email
+   *   The subscription email address.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setEmail($email);
+
+  /**
+   * Gets the subscribed entity ID.
+   *
+   * @return int
+   *   The entity ID.
+   */
+  public function getSubscribedEntityId();
+
+  /**
+   * Sets the subscribed entity ID.
+   *
+   * @param int $id
+   *   The entity ID.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setSubscribedEntityId($id);
+
+  /**
+   * Gets the subscribed entity type.
+   *
+   * @return string
+   *   The entity type (e.g., 'node', 'taxonomy_term').
+   */
+  public function getSubscribedEntityType();
+
+  /**
+   * Sets the subscribed entity type.
+   *
+   * @param string $entity_type
+   *   The entity type.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setSubscribedEntityType($entity_type);
+
+  /**
+   * Gets the subscription token.
+   *
+   * @return string
+   *   The subscription token.
+   */
+  public function getToken();
+
+  /**
+   * Sets the subscription token.
+   *
+   * @param string $token
+   *   The subscription token.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setToken($token);
+
+  /**
+   * Gets the subscription status.
+   *
+   * @return bool
+   *   TRUE if the subscription is active, FALSE otherwise.
+   */
+  public function isActive();
+
+  /**
+   * Sets the subscription status.
+   *
+   * @param bool $status
+   *   The subscription status.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setActive($status);
+
+  /**
+   * Gets the subscription creation timestamp.
+   *
+   * @return int
+   *   Creation timestamp of the subscription.
+   */
+  public function getCreatedTime();
+
+  /**
+   * Sets the subscription creation timestamp.
+   *
+   * @param int $timestamp
+   *   The subscription creation timestamp.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setCreatedTime($timestamp);
+
+  /**
+   * Gets the subscription language code.
+   *
+   * @return string
+   *   The language code of the subscription.
+   */
+  public function getLanguageCode();
+
+  /**
+   * Sets the subscription language code.
+   *
+   * @param string $langcode
+   *   The language code.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setLanguageCode($langcode);
+
+}
\ No newline at end of file
diff --git a/src/Entity/SubscriptionListBuilder.php b/src/Entity/SubscriptionListBuilder.php
new file mode 100644
index 0000000..a8951e5
--- /dev/null
+++ b/src/Entity/SubscriptionListBuilder.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\page_notifications\Entity;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityListBuilder;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+
+/**
+ * Provides a list builder for page notification subscriptions.
+ */
+class SubscriptionListBuilder extends EntityListBuilder {
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * Constructs a new SubscriptionListBuilder object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The entity storage class.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   */
+  public function __construct(
+    EntityTypeInterface $entity_type,
+    EntityStorageInterface $storage,
+    DateFormatterInterface $date_formatter
+  ) {
+    parent::__construct($entity_type, $storage);
+    $this->dateFormatter = $date_formatter;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity_type.manager')->getStorage($entity_type->id()),
+      $container->get('date.formatter')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header = [];
+    $header['id'] = $this->t('ID');
+    $header['email'] = $this->t('Email');
+    $header['subscribed_entity'] = $this->t('Subscribed To');
+    $header['status'] = $this->t('Status');
+    $header['created'] = $this->t('Created');
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $entity */
+    $row = [];
+    $row['id'] = $entity->id();
+    $row['email'] = $entity->getEmail();
+
+    // Get the subscribed entity and create a link if possible.
+    $entity_type = $entity->getSubscribedEntityType();
+    $entity_id = $entity->getSubscribedEntityId();
+    try {
+      $subscribed_entity = \Drupal::entityTypeManager()
+        ->getStorage($entity_type)
+        ->load($entity_id);
+      if ($subscribed_entity) {
+        $row['subscribed_entity'] = Link::createFromRoute(
+          $subscribed_entity->label(),
+          'entity.' . $entity_type . '.canonical',
+          [$entity_type => $entity_id]
+        );
+      }
+      else {
+        $row['subscribed_entity'] = $this->t('Entity not found (@type: @id)', [
+          '@type' => $entity_type,
+          '@id' => $entity_id,
+        ]);
+      }
+    }
+    catch (\Exception $e) {
+      $row['subscribed_entity'] = $this->t('Invalid entity reference');
+    }
+
+    $row['status'] = $entity->isActive() ? $this->t('Active') : $this->t('Inactive');
+    $row['created'] = $this->dateFormatter->format($entity->getCreatedTime(), 'short');
+
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+protected function getDefaultOperations(EntityInterface $entity) {
+    $operations = parent::getDefaultOperations($entity);
+
+    // Add a verify link if the subscription is not active.
+    if (!$entity->isActive()) {
+      // Generate verification token
+      $token = \Drupal::service('csrf_token')->get('subscription-verify-' . $entity->id());
+      $verify_operation = [
+        'verify' => [
+          'title' => $this->t('Verify'),
+          'url' => Url::fromRoute('page_notifications.subscription.verify', [
+            'token' => $token,
+            'subscription' => $entity->id(),
+          ]),
+        ],
+      ];
+      $operations = $verify_operation + $operations;
+    }
+
+    // Add delete operation
+    $operations['delete'] = [
+      'title' => $this->t('Delete'),
+      'url' => Url::fromRoute('entity.subscription.delete_form', [
+        'subscription' => $entity->id(),
+      ]),
+    ];
+
+    return $operations;
+}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $build = parent::render();
+    $build['table']['#empty'] = $this->t('No subscriptions found.');
+    return $build;
+  }
+
+}
\ No newline at end of file
diff --git a/src/EventSubscriber/NodeUpdateSubscriber.php b/src/EventSubscriber/NodeUpdateSubscriber.php
new file mode 100644
index 0000000..7278ed7
--- /dev/null
+++ b/src/EventSubscriber/NodeUpdateSubscriber.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Drupal\page_notifications\EventSubscriber;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\page_notifications\Service\NotificationManagerInterface;
+
+/**
+ * Node update subscriber for sending notifications.
+ */
+class NodeUpdateSubscriber implements EventSubscriberInterface {
+  // TODO: Implement event subscriber for node updates
+}
\ No newline at end of file
diff --git a/src/Form/AccessVerificationStep.php b/src/Form/AccessVerificationStep.php
deleted file mode 100644
index 0b87a05..0000000
--- a/src/Form/AccessVerificationStep.php
+++ /dev/null
@@ -1,214 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Mail\MailManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Language\LanguageManagerInterface;
-use Drupal\Component\Utility\EmailValidator;
-use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Core\Url;
-use Drupal\node\Entity\Node;
-use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Ajax\CloseModalDialogCommand;
-use Drupal\Core\Ajax\HtmlCommand;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-
-/**
- * Implements the build demo form controller.
- *
- * This example uses the Messenger service to demonstrate the order of
- * controller method invocations by the form api.
- *
- * @see \Drupal\Core\Form\FormBase
- * @see \Drupal\Core\Form\ConfigFormBase
- */
-class AccessVerificationStep extends FormBase {
-
-  /**
-   * The mail manager.
-   *
-   * @var \Drupal\Core\Mail\MailManagerInterface
-   */
-  protected $mailManager;
-
-  /**
-   * The email validator.
-   *
-   * @var \Drupal\Component\Utility\EmailValidator
-   */
-  protected $emailValidator;
-
-  /**
-   * The language manager.
-   *
-   * @var \Drupal\Core\Language\LanguageManagerInterface
-   */
-  protected $languageManager;
-
-  /**
-   * Counter keeping track of the sequence of method invocation.
-   *
-   * @var int
-   */
-  protected static $sequenceCounter = 0;
-
-  /**
-   * Constructs a new EmailUnsubscribePage.
-   *
-   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
-   *   The mail manager.
-   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
-   *   The language manager.
-   * @param \Drupal\Component\Utility\EmailValidator $email_validator
-   *   The email validator.
-   */
-  public function __construct(MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EmailValidator $email_validator) {
-    $this->mailManager = $mail_manager;
-    $this->languageManager = $language_manager;
-    $this->emailValidator = $email_validator;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    $form = new static(
-      $container->get('plugin.manager.mail'),
-      $container->get('language_manager'),
-      $container->get('email.validator')
-    );
-    $form->setStringTranslation($container->get('string_translation'));
-    return $form;
-  }
-
-  /**
-   * Update form processing information.
-   *
-   * Display the method being called and it's sequence in the form
-   * processing.
-   *
-   * @param string $method_name
-   *   The method being invoked.
-   */
-  private function displayMethodInvocation($method_name) {
-    self::$sequenceCounter++;
-    $this->messenger()->addMessage(self::$sequenceCounter . ". $method_name");
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state, $subscription_token = NULL) {
-
-    $host = \Drupal::request()->getSchemeAndHttpHost();
-    if ($subscription_token && !is_null($subscription_token)) {
-      $form['#id'] = 'page-notifications-block-verify';
-      $form['subscription_token'] = [
-        '#type' => 'hidden',
-        '#value' => $subscription_token,
-      ];
-      $form['email_verify'] = [
-        '#type' => 'textfield',
-        '#title' => $this->t('Enter your E-mail Address:'),
-        '#description' => $this->t('Please enter your email for verification.'),
-        '#required' => TRUE,
-      ];
-      $form['actions'] = [
-        '#type' => 'actions',
-      ];
-      $form['actions']['submit'] = [
-        '#type' => 'submit',
-        '#value' => 'Find my subscribtions',
-      ];
-      return $form;
-    }
-    else {
-      $form['intro'] = [
-        '#markup' => $this->t('<p>You link might be broken or incomplete.</p>'),
-      ];
-      return $form;
-    }
-
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page-notifications-block-verify';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    $email = $form_state->getValue('email_verify');
-    $user_token = $form_state->getValue('subscription_token');
-    if (!$this->emailValidator->isValid($form_state->getValue('email_verify')) && !is_null($form_state->getValue('email_verify'))) {
-      $form_state->setErrorByName('email', $this->t('That e-mail address is not valid.'));
-    }
-    else {
-      $record = page_notifications_verify_if_record_exist($email, $user_token);
-      //$record = \Drupal::service('load.databaseinnfo.service')->verifyByNodeAndEmail($email, $subscription_token);
-      if ($record == true) {
-        $email_verify = $form_state->getValue('email_verify');
-      }
-      else {
-        $form_state->setErrorByName('email', $this->t('We don\'t have any subscription for this email.'));
-      }
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-
-  /**
-   * Implements ajax submit callback.
-   *
-   * @param array $form
-   *   Form render array.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   Current state of the form.
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $page_notifications_user_token = \Drupal::service('load.databaseinnfo.service')->pageNotifyGetUserToken($form_state->getValue('email_verify'));
-    $user_token =  $page_notifications_user_token['field_page_notify_token_user_id'];
-    $response = $this->redirect(
-        'page_notifications.subscriberpage',
-        array('user_token' => $user_token),
-    );
-    $response->send();
-  }
-
-  /**
-   * Implements submit callback for Rebuild button.
-   *
-   * @param array $form
-   *   Form render array.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   Current state of the form.
-   */
-  public function rebuildFormSubmit(array &$form, FormStateInterface $form_state) {
-    $this->displayMethodInvocation('rebuildFormSubmit');
-    $form_state->setRebuild(TRUE);
-  }
-}
-
-function page_notifications_verify_if_record_exist($email, $user_token) {
-  $record = current(\Drupal::entityTypeManager()->getStorage('node')
-    ->loadByProperties([
-      'field_page_notify_token_user_id' => $user_token,
-      'field_page_notify_email' => $email,
-    ])
-  );
-  if ($record && !is_null($record)) {
-    return TRUE;
-  }
-  else {
-    return FALSE;
-  }
-}
diff --git a/src/Form/ContentTypeMigrationForm.php b/src/Form/ContentTypeMigrationForm.php
deleted file mode 100644
index 9460671..0000000
--- a/src/Form/ContentTypeMigrationForm.php
+++ /dev/null
@@ -1,274 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Entity\Element\EntityAutocomplete;
-use Drupal\node\Entity\Node;
-
-/**
- * @see \Drupal\Core\Form\FormBase
- */
-class ContentTypeMigrationForm extends FormBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_content_type_migration_form';
-  }
-
-  /**
-   *
-   * @var \Drupal\node\NodeStorage
-   */
-  protected $nodeStorage;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
-    $this->nodeStorage = $entity_type_manager->getStorage('node');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('entity_type.manager')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-
-    if ($form_state->has('page_num') && $form_state->get('page_num') == 2) {
-      return self::pageNotificationsPageTwo($form, $form_state);
-    }
-
-    $form_state->set('page_num', 1);
-    $form['intro'] = [
-      '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>Step 1 - Content Type selection.</h2>
-      <h3>Instructions:</h3>
-      <ol>
-       <li>Create new content type</li>
-       <li>Add custom Text (plain) fields to new content type</li>
-       <li>After that come back here and enter content type machine name in fields below</li>
-      </ol>
-      <p>You can migrate subscriptions from one content type to another but the module will work just with page_notify_subscriptions content type.</p>
-      "),
-    ];
-    $form['content_type_export'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('From Content Type'),
-      '#description' => $this->t('Content Type machine name from where subscriptions needs to be moved'),
-      '#default_value' => $form_state->getValue('content_type_export', ''),
-      '#required' => TRUE,
-      '#maxlength' => 1024,
-    ];
-    $form['content_type_import'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('To Content Type'),
-      '#description' => $this->t('Content Type machine name to where subscriptions needs to be moved'),
-      '#default_value' => $form_state->getValue('content_type_import', ''),
-      '#required' => TRUE,
-      '#maxlength' => 1024,
-    ];
-    $form['actions'] = [
-      '#type' => 'actions',
-    ];
-    $form['actions']['next'] = [
-      '#type' => 'submit',
-      '#button_type' => 'primary',
-      '#value' => $this->t('Next'),
-      '#submit' => ['::pageContentSubscriptionsMigrationForm'],
-      '#validate' => ['::pageNotificationsMultistepFormNextValidate'],
-    ];
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-
-    $page_values = $form_state->get('page_values');
-    $count = 0;
-    $clean_values = $form_state->cleanValues()->getValues();
-    $result = \Drupal::entityQuery("node")
-      ->condition("type", $page_values["content_type_export"])
-      ->accessCheck(FALSE)
-      ->execute();
-    $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-    $entities = $storage_handler->loadMultiple($result);
-
-    foreach ($entities as $entity_key => $entity) {
-      $new_values = array();
-      foreach ($clean_values as $key => $clean_value) {
-        if ($clean_value !== 'none'){
-          $new_field = array();
-          $node_filed_value = $entity->get($clean_value)->getValue();
-          if($node_filed_value[0]["value"] && $node_filed_value !== "" || $node_filed_value !== null || !is_null($node_filed_value)){
-            $new_field[$key] = $node_filed_value[0]["value"];
-          } else {
-            $new_field[$key] = $node_filed_value;
-          }
-          $new_values[$key] = $new_field[$key];
-        }
-      }
-
-      $new_node = Node::create(['type' => $page_values["content_type_import"]]);
-      $new_node->set('title', $entity->getTitle());
-      foreach ($new_values as $key => $new_value) {
-        $new_node->set($key, $new_value);
-      }
-      $new_node->enforceIsNew();
-      $new_node->save();
-      $count++;
-    }
-
-    $this->messenger()->addMessage($this->t('Migrated total of: @count', ['@count' => $count]));
-  }
-
-  /**
-   * Provides custom validation handler for page 1.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.a
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   */
-  public function pageNotificationsMultistepFormNextValidate(array &$form, FormStateInterface $form_state) {
-    if (is_null($form_state->getValue('content_type_export'))) {
-      $form_state->setErrorByName('content_type_export', $this->t('Please enter a valid value'));
-    } elseif (is_null($form_state->getValue('content_type_import'))) {
-      $form_state->setErrorByName('content_type_import', $this->t('Please enter a valid value'));
-    }
-  }
-
-  /**
-   * Provides custom submission handler for page 1.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   */
-  public function pageContentSubscriptionsMigrationForm(array &$form, FormStateInterface $form_state) {
-    $form_values = $form_state->getValue(array());
-
-    $form_state
-      ->set('page_values', [
-        'content_type_export' => $form_values['content_type_export'],
-        'content_type_import' => $form_values['content_type_import'],
-      ])
-      ->set('page_num', 2)
-      ->setRebuild(TRUE);
-  }
-
-  /**
-   * Builds the second step form (page 2).
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   *
-   * @return array
-   *   The render array defining the elements of the form.
-   */
-  public function pageNotificationsPageTwo(array &$form, FormStateInterface $form_state) {
-    $vals = $form_state->getStorage();
-    $content_type_export = $vals['page_values']['content_type_export'];
-    $content_type_import = $vals['page_values']['content_type_import'];
-
-    $content_type_export_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', $content_type_export);
-    $content_type_import_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', $content_type_import);
-
-    $export_count = count($content_type_export_fields);
-    $import_count = count($content_type_import_fields);
-
-    $result = \Drupal::entityQuery("node")
-      ->condition("type", $content_type_export)
-      ->accessCheck(FALSE)
-      ->execute();
-    $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-    $entities = $storage_handler->loadMultiple($result);
-    $entities_count = count($entities);
-
-
-    if($import_count == 0){
-      $form['intro'] = [
-        '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>Step 2 - Review. No Content type fields found.</h2>"),
-      ];
-    } else {
-      $form['intro'] = [
-        '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>Step 2 - Review. There are ".$entities_count." node(s) to transfer.</h2>
-        <p>Below is the list of content type fileds. Please map each field to corresponding page notifications content type field:</p>
-        "),
-      ];
-
-      $options = array('none' => 'None');
-      foreach($content_type_export_fields as $content_type_export_field) {
-        if (str_starts_with($content_type_export_field->getName(), 'field_')) {
-          $option = $content_type_export_field->getLabel() . ' (' . $content_type_export_field->getName() . ')';
-          $options[$content_type_export_field->getName()] = $content_type_export_field->getLabel();
-        }
-      }
-
-      $i=0;
-      foreach($content_type_import_fields as $content_type_import_field) {
-        $field_name = $content_type_import_field->getName();
-        $field_type = $content_type_import_field->getType();
-        $field_label = $content_type_import_field->getLabel();
-        if (str_starts_with($field_name, 'field_')) {
-          $form['fields-list'][$i][$field_name]= [
-            '#type' => 'select',
-            '#title' => $this->t($field_label),
-            '#description' => $this->t('Machine Name: ' . $field_name),
-            '#options' => $options,
-          ];
-          $i++;
-        }
-      }
-
-      $build['pager'] = array(
-        '#markup' => 'pager',
-      );
-      $form['back'] = [
-        '#type' => 'submit',
-        '#value' => $this->t('Back'),
-        '#submit' => ['::pageNotificationsPageTwoBack'],
-        '#limit_validation_errors' => [],
-      ];
-      $form['submit'] = [
-        '#type' => 'submit',
-        '#button_type' => 'primary',
-        '#value' => $this->t('Submit'),
-      ];
-    }
-    return $form;
-  }
-
-  /**
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   */
-  public function pageNotificationsPageTwoBack(array &$form, FormStateInterface $form_state) {
-    $form_state
-      ->setValues($form_state->get('page_values'))
-      ->set('page_num', 1)
-      ->setRebuild(TRUE);
-  }
-
-
-
-}
diff --git a/src/Form/EmailConfirmationPage.php b/src/Form/EmailConfirmationPage.php
deleted file mode 100644
index 5213878..0000000
--- a/src/Form/EmailConfirmationPage.php
+++ /dev/null
@@ -1,239 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Mail\MailManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Language\LanguageManagerInterface;
-use Drupal\Component\Utility\EmailValidator;
-use Drupal\Core\Url;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-use Drupal\Component\Render\FormattableMarkup;
-use Drupal\node\Entity\Node;
-use Drupal\taxonomy\Entity\Term;
-
-/**
- * @ingroup page_notifications
- */
-class EmailConfirmationPage extends FormBase {
-
-  /**
-   * The mail manager.
-   *
-   * @var \Drupal\Core\Mail\MailManagerInterface
-   */
-  protected $mailManager;
-
-  /**
-   * The email validator.
-   *
-   * @var \Drupal\Component\Utility\EmailValidator
-   */
-  protected $emailValidator;
-
-  /**
-   * The language manager.
-   *
-   * @var \Drupal\Core\Language\LanguageManagerInterface
-   */
-  protected $languageManager;
-
-  /**
-   * Constructs a new EmailUnsubscribePage.
-   *
-   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
-   *   The mail manager.
-   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
-   *   The language manager.
-   * @param \Drupal\Component\Utility\EmailValidator $email_validator
-   *   The email validator.
-   */
-  public function __construct(MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EmailValidator $email_validator) {
-    $this->mailManager = $mail_manager;
-    $this->languageManager = $language_manager;
-    $this->emailValidator = $email_validator;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    $form = new static(
-      $container->get('plugin.manager.mail'),
-      $container->get('language_manager'),
-      $container->get('email.validator')
-    );
-    $form->setMessenger($container->get('messenger'));
-    $form->setStringTranslation($container->get('string_translation'));
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_confirmation';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state, $email = NULL, $subscription_token = NULL) {
-    if($subscription_token != strip_tags($subscription_token) && $email != strip_tags($email)) {
-      \Drupal::messenger()->addError(t('Sorry, no data on this.'));
-    } else {
-      if($email && !is_null($email) && $subscription_token && !is_null($subscription_token)) {
-        $subscription_token_pieces = explode("-", $subscription_token);
-        $node_id = $subscription_token_pieces[0];
-        $subscription_entity_type = $subscription_token_pieces[1];
-        $subscription_token_raw = $subscription_token_pieces[2];
-        $host = \Drupal::request()->getSchemeAndHttpHost();
-        $inrecords = secondCheckIfRecordExistNode($email, $node_id, $subscription_entity_type, $subscription_token_raw);
-
-        if ($inrecords && $inrecords->isPublished() == true) {
-          $user_token_exist = \Drupal::service('load.databaseinnfo.service')->pageNotifyGetUserToken($email);
-          if ($user_token_exist && $user_token_exist != false) {
-            $user_token = $user_token_exist['field_page_notify_token_user_id'];
-          }
-          $unsubscribe_link = $host . "/page-notifications/verify-list/" . $user_token;
-          $form['intro'] = [
-            '#markup' => $this->t('<div clas="page-notifications-find-my-subscriptions"> Find your subscriptions <a href='. $unsubscribe_link .'>here</a>.</div>'),
-          ];
-          return $form;
-        }
-        else {
-          $user_token_exist = \Drupal::service('load.databaseinnfo.service')->pageNotifyGetUserToken($email);
-          if ($user_token_exist && $user_token_exist != false) {
-            $user_token = $user_token_exist['field_page_notify_token_user_id'];
-          } else {
-            $user_token = \Drupal::service('load.databaseinnfo.service')->page_notifications_generateRandom_user_token();
-          }
-
-          if ($subscription_entity_type == 'node') {
-            $node = \Drupal\node\Entity\Node::load($node_id);
-            $page_title = $node->getTitle();
-            $subscribed_node_url = $node->toUrl()->setAbsolute()->toString();
-          } elseif ($subscription_entity_type == 'term') {
-            $node = Term::load($node_id);
-            $page_title = $node->getName();
-            $subscribed_node_url = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical',['taxonomy_term' => $node->tid->value],['absolute' => TRUE])->toString();
-          } else {
-            $node = \Drupal\node\Entity\Node::load($node_id);
-            $subscribed_node_url = $node->toUrl()->setAbsolute()->toString();
-          }
-
-          $new_submition = Node::create([
-            'type' => 'page_notify_subscriptions',
-            'title' => 'Subscription to - ' . $node_id . ' - ' . $subscription_entity_type . ' - ' . $subscription_token_raw,
-            'field_page_notify_node_id' => $node_id,
-            'field_page_notify_email' => $email,
-            'field_page_notify_token' => $subscription_token_raw . '-' . $subscription_entity_type,
-            'field_page_notify_token_user_id' => $user_token,
-          ]);
-          $new_submition->save();
-
-          $all_subscriptions_url = $host . '/page-notifications/verify-list/' . $user_token;
-          $unsubscribe_link = $host . "/page-notifications/unsubscribe/" . $subscription_token_raw . '-' . $subscription_entity_type;
-
-          $template_info = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template();
-          if ($template_info['from_email']) {
-            $from = $template_info['from_email'];
-          }
-          else {
-            $from = \Drupal::config('system.site')->get('mail');
-          }
-
-          $subject_replacements = array(
-            '[notify_user_name]' => '',
-            '[notify_user_email]' => $email,
-            '[notify_verify_url]' => '',
-            '[notify_subscribe_url]' => '',
-            '[notify_unsubscribe_url]' => '',
-            '[notify_user_subscribtions]' => '',
-            '[notify_node_title]' => $page_title,
-            '[notify_node_url]' => '',
-            '[notify_notes]' => '',
-          );
-          $body_replacements = array(
-            '[notify_user_name]' => '',
-            '[notify_user_email]' => $email,
-            '[notify_verify_url]' => '',
-            '[notify_subscribe_url]' => '',
-            '[notify_unsubscribe_url]' => $unsubscribe_link,
-            '[notify_user_subscribtions]' => $all_subscriptions_url,
-            '[notify_node_title]' => $page_title,
-            '[notify_node_url]' => $subscribed_node_url,
-            '[notify_notes]' => '',
-          );
-
-          $tokanized_subject = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['confirmation_email_subject'], $subject_replacements);
-          $tokanized_body = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['confirmation_email_text'], $body_replacements);
-          strval($tokanized_subject);
-          $message['to'] = $email;
-          $message['subject'] = $tokanized_subject;
-          $message['body'] = $tokanized_body;
-          $result = \Drupal::service('plugin.manager.mail')->mail(
-             'page_notifications',
-             'configuration_email',
-             $email,
-             \Drupal::languageManager()->getDefaultLanguage()->getId(),
-             $message
-           );
-
-          $tokanized_notify_confirmation_web_page_message = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['confirmation_web_page_message'], $body_replacements);
-          if ($template_info['confirmation_web_page_message']) {
-            $form['intro'] = [
-              '#type' => 'processed_text',
-              '#text' => $this->t($tokanized_notify_confirmation_web_page_message),
-              '#format' => 'full_html',
-            ];
-          }
-          else {
-            $form['intro'] = [
-              '#markup' => new FormattableMarkup('<br /><p>You are now subscribed to</p> <h2><a href="@link">@title</a></h2><br />
-                <p><a href="@all_subscriptions_url">Manage Your Page Watching Subscriptions</a>.</p>',
-                ['@title' => $page_title, '@link' => $subscribed_node_url, '@all_subscriptions_url' => $all_subscriptions_url]
-              ),
-            ];
-          }
-          return $form;
-        }
-      }
-      else {
-        \Drupal::messenger()->addError(t('Sorry, no data on this.'));
-      }
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    $form_values = $form_state->getValues();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $form_values = $form_state->getValues();
-  }
-}
-
-function secondCheckIfRecordExistNode($email, $node_id, $subscription_entity_type = NULL, $subscription_token_raw = NULL) {
-  $record = current(\Drupal::entityTypeManager()->getStorage('node')
-    ->loadByProperties([
-      'field_page_notify_email' => $email,
-      'field_page_notify_node_id' => $node_id,
-      'field_page_notify_token' => $subscription_token_raw . '-' . $subscription_entity_type
-    ])
-  );
-  if ($record) {
-    return $record;
-  }
-  else {
-    return FALSE;
-  }
-}
diff --git a/src/Form/EmailUnsubscribePage.php b/src/Form/EmailUnsubscribePage.php
deleted file mode 100644
index 45b0859..0000000
--- a/src/Form/EmailUnsubscribePage.php
+++ /dev/null
@@ -1,268 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Mail\MailManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Language\LanguageManagerInterface;
-use Drupal\Component\Utility\EmailValidator;
-use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Core\Url;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-use Drupal\node\Entity\Node;
-use Drupal\taxonomy\Entity\Term;
-
-
-/**
- * @ingroup page_notifications
- */
-class EmailUnsubscribePage extends FormBase {
-
-  /**
-   * The mail manager.
-   *
-   * @var \Drupal\Core\Mail\MailManagerInterface
-   */
-  protected $mailManager;
-
-  /**
-   * The email validator.
-   *
-   * @var \Drupal\Component\Utility\EmailValidator
-   */
-  protected $emailValidator;
-
-  /**
-   * The language manager.
-   *
-   * @var \Drupal\Core\Language\LanguageManagerInterface
-   */
-  protected $languageManager;
-
-  /**
-   * Constructs a new EmailUnsubscribePage.
-   *
-   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
-   *   The mail manager.
-   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
-   *   The language manager.
-   * @param \Drupal\Component\Utility\EmailValidator $email_validator
-   *   The email validator.
-   */
-  public function __construct(MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EmailValidator $email_validator) {
-    $this->mailManager = $mail_manager;
-    $this->languageManager = $language_manager;
-    $this->emailValidator = $email_validator;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    $form = new static(
-      $container->get('plugin.manager.mail'),
-      $container->get('language_manager'),
-      $container->get('email.validator')
-    );
-    $form->setMessenger($container->get('messenger'));
-    $form->setStringTranslation($container->get('string_translation'));
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_unsubscribe';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state, $subscription_token = NULL) {
-    if($subscription_token != strip_tags($subscription_token) || is_null($subscription_token)) {
-      \Drupal::messenger()->addError(t('You link might be broken or incomplete. Please make sure you dont have extra space in your address link.'));
-    } else {
-      $host = \Drupal::request()->getSchemeAndHttpHost();
-      if (is_string($subscription_token) && !is_null($subscription_token)) {
-        $part_subscription_token = explode("-", $subscription_token);
-        $inrecord_by_token = checkIfRecordExistUnsubscribebyTokens($subscription_token);
-
-        if ($inrecord_by_token) {
-          $field_page_notify_token_user_id = $inrecord_by_token->get('field_page_notify_token_user_id')->getValue();
-          $subscriptions_url = $host . '/page-notifications/verify-list/' . $field_page_notify_token_user_id[0]['value'];
-          $node_id = $inrecord_by_token->get('field_page_notify_node_id')->getValue();
-          if (count($part_subscription_token) == 1 && strlen($part_subscription_token[0]) == 10) {
-            $subscription_entity_type = 'node';
-          } elseif (count($part_subscription_token) == 2) {
-            $subscription_entity_type = $part_subscription_token[1];
-          }
-
-          if ($subscription_entity_type == 'node') {
-            $node = \Drupal\node\Entity\Node::load($node_id[0]['value']);
-            $page_title = $node->getTitle();
-            $subscribed_node_url = $node->toUrl()->setAbsolute()->toString();
-          } elseif ($subscription_entity_type == 'term') {
-            $node = Term::load($node_id[0]['value']);
-            $page_title = $node->getName();
-            $subscribed_node_url = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical',['taxonomy_term' => $node->tid->value],['absolute' => TRUE])->toString();
-          } else {
-            $node = \Drupal\node\Entity\Node::load($node_id[0]['value']);
-            $subscribed_node_url = $node->toUrl()->setAbsolute()->toString();
-          }
-        }
-
-        if ($subscription_token) {
-          $form['intro'] = [
-            '#markup' => $this->t('<h1>Unsubscribe from "' . $page_title . '" page.</h1>'),
-          ];
-          $form['subscription_token'] = [
-            '#type' => 'hidden',
-            '#value' => $subscription_token,
-          ];
-          $form['email_unsubscribe'] = [
-            '#type' => 'textfield',
-            '#title' => $this->t('Enter your E-mail Address:'),
-            '#description' => $this->t('Please enter your email for confirmation.'),
-            '#required' => TRUE,
-          ];
-          $form['unsubscribe_all'] = [
-            '#type' => 'checkbox',
-            '#title' => $this->t('Unsubscribe me from all my subscriptions'),
-            '#required' => FALSE,
-          ];
-          $form['intro2'] = [
-            '#markup' => new FormattableMarkup('<div>or visit <a target="_blank" href="@url">@name</a></div><br />',
-            [
-              '@name' => ' Manage Your Page Watching Subscriptions.',
-              '@url' => $subscriptions_url
-            ]),
-          ];
-          $form['submit'] = [
-            '#type' => 'submit',
-            '#value' => $this->t('Submit'),
-          ];
-          return $form;
-        } else {
-          $form['intro'] = [
-            '#markup' => new FormattableMarkup('<div>The page you been subscribed is no longer available or moved to different location. <br />You can find out by going to: <a target="_blank" href="@url">@name</a></div><br />',
-            [
-              '@name' => ' Manage Your Page Watching Subscriptions.',
-              '@url' => $subscriptions_url
-            ]),
-          ];
-        }
-
-      } else {
-        $form['intro'] = [
-          '#markup' => $this->t('<p>You link might be broken or incomplete.</p>'),
-        ];
-      }
-
-      return $form;
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    if (!$this->emailValidator->isValid($form_state->getValue('email_unsubscribe')) || is_null($form_state->getValue('email_unsubscribe'))) {
-      $form_state->setErrorByName('email_unsubscribe', $this->t('That e-mail address is not valid.'));
-    } else {
-      $email_unsubscribe = strip_tags($form_state->getValue('email_unsubscribe'));
-      $subscription_token = strip_tags($form_state->getValue('subscription_token'));
-      if(is_string($email_unsubscribe) && is_string($subscription_token)) {
-        $record = \Drupal::service('load.databaseinnfo.service')->verifyByTokenAndEmail($form_state->getValue('email_unsubscribe'), $form_state->getValue('subscription_token'));
-        if ($record == true) {
-          $email_verify = $email_unsubscribe;
-        } else {
-          $form_state->setValue('email_unsubscribe', '');
-          \Drupal::messenger()->addError(t("We don't have subscription for this email."));
-        }
-      }
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $form_values = $form_state->getValues();
-    $unsubscribe_all = $form_state->getValue('unsubscribe_all');
-    $email_unsubscribe = $form_state->getValue('email_unsubscribe');
-    $subscription_token = $form_state->getValue('subscription_token');
-
-    if ($subscription_token == strip_tags($subscription_token) && $email_unsubscribe == strip_tags($email_unsubscribe)) {
-      $page_notify_user_token = \Drupal::service('load.databaseinnfo.service')->pageNotifyGetUserToken($email_unsubscribe);
-      //$user_token =  $page_notify_user_token['field_page_notify_token_user_id'];
-
-      if ($unsubscribe_all && !is_null($unsubscribe_all) && $page_notify_user_token != false) {
-        $result = \Drupal::entityQuery("node")
-          ->condition("type", "page_notify_subscriptions")
-          ->condition("field_page_notify_email", $email_unsubscribe)
-          ->accessCheck(FALSE)
-          ->execute();
-        $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-        $entities = $storage_handler->loadMultiple($result);
-        $storage_handler->delete($entities);
-        \Drupal::messenger()->addStatus(t('You have successfully unsubscribed from all pages.'));
-      } else {
-        $inrecords = checkIfRecordExistUnsubscribe($email_unsubscribe, $subscription_token);
-
-        if ($inrecords && !is_null($inrecords)) {
-          $result = \Drupal::entityQuery("node")
-            ->condition("type", "page_notify_subscriptions")
-            ->condition("field_page_notify_token", $form_state->getValue('subscription_token'))
-            ->accessCheck(FALSE)
-            ->execute();
-          $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-          $entities = $storage_handler->loadMultiple($result);
-          $storage_handler->delete($entities);
-          if($result){
-            \Drupal::messenger()->addStatus(t('You have successfully unsubscribed.'));
-          }
-        } else {
-          \Drupal::messenger()->addError(t('This subscription no longer exist.'));
-        }
-      }
-    } else {
-      \Drupal::messenger()->addError(t('You link might be broken or incomplete.'));
-    }
-
-    $url = \Drupal\Core\Url::fromRoute('<front>')->toString();
-    $response = new RedirectResponse($url);
-    $response->send();
-  }
-}
-
-function checkIfRecordExistUnsubscribebyTokens($subscription_token) {
-  $record = current(\Drupal::entityTypeManager()->getStorage('node')
-    ->loadByProperties([
-      'field_page_notify_token' => $subscription_token
-    ])
-  );
-  if ($record) {
-    return $record;
-  }
-  else {
-    return FALSE;
-  }
-}
-
-function checkIfRecordExistUnsubscribe($email, $subscription_token) {
-  $record = current(\Drupal::entityTypeManager()->getStorage('node')
-    ->loadByProperties([
-      'field_page_notify_email' => $email,
-      'field_page_notify_token' => $subscription_token
-    ])
-  );
-  if ($record) {
-    return $record;
-  }
-  else {
-    return FALSE;
-  }
-}
diff --git a/src/Form/GeneralSettingsForm.php b/src/Form/GeneralSettingsForm.php
deleted file mode 100644
index 049ae93..0000000
--- a/src/Form/GeneralSettingsForm.php
+++ /dev/null
@@ -1,151 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * @see \Drupal\Core\Form\FormBase
- * @see \Drupal\Core\Form\ConfigFormBase
- */
-class GeneralSettingsForm extends FormBase
-{
-
-  /**
-   * {@inheritdoc}
-   */
-  public function defaultConfiguration()
-  {
-    return [
-      'markup' => [
-        'format' => 'full_html',
-        'value' => '',
-      ],
-    ] + parent::defaultConfiguration();
-  }
-
-  /**
-   * @var int
-   */
-  protected static $sequenceCounter = 0;
-
-  /**
-   * {@inheritdoc}
-   */
-
-  /**
-   * Update form processing information.
-   *
-   * Display the method being called and it's sequence in the form
-   * processing.
-   *
-   * @param string $method_name
-   *   The method being invoked.
-   */
-  private function displayMethodInvocation($method_name)
-  {
-
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state)
-  {
-    $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings();
-
-    $moduleHandler = \Drupal::service('module_handler');
-    if ($moduleHandler->moduleExists('recaptcha')) {
-      $form['page_notify_recaptcha'] = [
-        '#type' => 'checkbox',
-        '#title' => t('Enable/Disable reCaptcha'),
-        '#description' => $this->t('Strongly recommened to use reCaptcha or Captcha for this module.'),
-        '#default_value' => $notify_settings['page_notify_recaptcha'] ? $notify_settings['page_notify_recaptcha'] : 0,
-        '#weight' => 0,
-      ];
-    }
-    if ($moduleHandler->moduleExists('captcha')) {
-      $form['page_notify_captcha'] = [
-        '#type' => 'checkbox',
-        '#title' => t('Enable/Disable Captcha'),
-        '#description' => $this->t('Strongly recommened to use reCaptcha or Captcha for this module.'),
-        '#default_value' => $notify_settings['page_notify_captcha'] ? $notify_settings['page_notify_captcha'] : 0,
-        '#weight' => 0,
-      ];
-    }
-    $form['page_notify_subscribers_count'] = [
-      '#type' => 'checkbox',
-      '#title' => t('Show number of subscribers on node edit.'),
-      '#description' => $this->t('It will output number of subscribers on the node when editing that node.'),
-      '#default_value' => $notify_settings['page_notify_subscribers_count'] ? $notify_settings['page_notify_subscribers_count'] : 0,
-      '#weight' => 1,
-    ];
-    $form['enable_message_subscription_not_available'] = [
-      '#type' => 'checkbox',
-      '#title' => t('Display message when functionality is not available.'),
-      '#description' => $this->t('The message displayed in "Subscription is not available web message" field in Messages configuration settings.'),
-      '#default_value' => $notify_settings['enable_message_subscription_not_available'] ? $notify_settings['enable_message_subscription_not_available'] : 0,
-      '#weight' => 1,
-    ];
-    $form['actions'] = [
-      '#type' => 'actions',
-    ];
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => 'Save Confirmation',
-    ];
-    $buildInfo = $form_state->getBuildInfo();
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId()
-  {
-    $this->displayMethodInvocation('getFormId');
-    return 'notify_general_congig_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state)
-  {
-    $this->displayMethodInvocation('validateForm');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state)
-  {
-    $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings();
-
-    if ($notify_settings) {
-      $query = \Drupal::database()->update('page_notify_settings')
-        ->fields([
-          'page_notify_settings_group_name' => 'page_notify_general_settings',
-          'page_notify_recaptcha' => $form_state->getValue('page_notify_recaptcha'),
-          'page_notify_captcha' => $form_state->getValue('page_notify_captcha'),
-          'page_notify_subscribers_count' => $form_state->getValue('page_notify_subscribers_count'),
-          'enable_message_subscription_not_available' => $form_state->getValue('enable_message_subscription_not_available'),
-        ])
-        ->condition('page_notify_settings_group_name', 'page_notify_general_settings')
-        ->execute();
-      \Drupal::messenger()->addStatus(t('The configuration options have been saved.'));
-    } else {
-      $query = \Drupal::database()->insert('page_notify_settings')
-        ->fields([
-          'page_notify_settings_group_name' => 'page_notify_general_settings',
-          'page_notify_recaptcha' => $form_state->getValue('page_notify_recaptcha'),
-          'page_notify_captcha' => $form_state->getValue('page_notify_captcha'),
-          'page_notify_subscribers_count' => $form_state->getValue('page_notify_subscribers_count'),
-          'enable_message_subscription_not_available' => $form_state->getValue('enable_message_subscription_not_available'),
-        ]);
-      $query->execute();
-    }
-    \Drupal::messenger()->addStatus(t('The configuration options have been saved.'));
-  }
-}
diff --git a/src/Form/MessagesForm.php b/src/Form/MessagesForm.php
deleted file mode 100644
index 8bd937f..0000000
--- a/src/Form/MessagesForm.php
+++ /dev/null
@@ -1,322 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * @see \Drupal\Core\Form\FormBase
- * @see \Drupal\Core\Form\ConfigFormBase
- */
-class MessagesForm extends FormBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function defaultConfiguration() {
-    return [
-      'markup' => [
-        'format' => 'full_html',
-        'value' => '',
-      ],
-    ] + parent::defaultConfiguration();
-  }
-
-  /**
-   * Counter keeping track of the sequence of method invocation.
-   *
-   * @var int
-   */
-  protected static $sequenceCounter = 0;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct() {
-
-  }
-
-  /**
-   * Update form processing information.
-   *
-   * Display the method being called and it's sequence in the form
-   * processing.
-   *
-   * @param string $method_name
-   *   The method being invoked.
-   */
-  private function displayMethodInvocation($method_name) {
-
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $template = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template();
-    $form['intro'] = [
-      '#markup' => $this->t("<p>Here you can configure, customize your Email Confirmation message and others like status messages that are displayed on pages.</p>")
-    ];
-    $form['from_email'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('The "from" email'),
-      '#description' => $this->t('Leave empty to use site email.'),
-      '#size' => 60,
-      '#maxlength' => 128,
-      '#default_value' => isset($template['from_email']) ? $template['from_email'] : '',
-      ];
-    $form['checkbox_field'] = [
-      '#title'  => 'Checkbox field',
-      '#description' => $this->t('Enter machine name only! Field that enables email notifications to be sent.'),
-      '#type' => 'textfield',
-      '#size' => 60,
-      '#maxlength' => 128,
-      '#default_value' => isset($template['checkbox_field']) ? $template['checkbox_field'] : '',
-    ];
-    $form['notes_field'] = [
-      '#title'  => 'Notes field',
-      '#description' => $this->t('Enter machine name only! Field that contains notes or message for subscribers.'),
-      '#type' => 'textfield',
-      '#size' => 60,
-      '#maxlength' => 128,
-      '#default_value' => isset($template['notes_field']) ? $template['notes_field'] : '',
-    ];
-    $form['node_timestamp'] = [
-      '#title'  => 'Timestamp',
-      '#description' => $this->t('Enter machine name only! Enter field name that will store timestamp for last sent email of the node.'),
-      '#type' => 'textfield',
-      '#size' => 60,
-      '#maxlength' => 128,
-      '#default_value' => isset($template['node_timestamp']) ? $template['node_timestamp'] : '',
-    ];
-    $form['body'] = [
-      '#type' => 'text_format',
-      '#title' => 'Page Notifications header text',
-      '#format' => 'full_html',
-      '#default_value' => isset($template['body']) ? $template['body'] : 'Subscribe to [notify_node_title]',
-      '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_node_url]'),
-    ];
-    $form['verification_email_subject'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Subject of Verification email'),
-      '#size' => 60,
-      '#maxlength' => 128,
-      '#default_value' => isset($template['verification_email_subject']) ? $template['verification_email_subject'] : 'Subscription Confirmation – [notify_node_title]',
-      '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_user_email]'),
-    ];
-    $form['verification_email_text'] = [
-      '#type' => 'text_format',
-      '#title' => 'Body of verification email',
-      '#format' => 'full_html',
-      '#default_value' => isset($template['verification_email_text']) ? $template['verification_email_text'] : '
-      <p>Hello [notify_user_email],</p>
-      <p>Please confirm your subscription&nbsp;<a href="[notify_verify_url]">here</a>.</p>
-      <p>Once complete, you will receive a "Now Subscribed" email notification.</p>
-      <p>Thank you!</p>',
-      '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_node_url]; [notify_user_email]; [notify_verify_url]'),
-    ];
-    $form['confirmation_email_subject'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Subject of Confirmation email'),
-      '#size' => 60,
-      '#maxlength' => 128,
-      '#default_value' => isset($template['confirmation_email_subject']) ? $template['confirmation_email_subject'] : 'You are now subscribed to - [notify_node_title]',
-      '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_user_email]'),
-    ];
-    $form['confirmation_email_text'] = [
-      '#type' => 'text_format',
-      '#title' => 'Confirmation email',
-      '#format' => 'full_html',
-      '#default_value' => isset($template['confirmation_email_text']) ? $template['confirmation_email_text'] : '
-      <p>Hello [notify_user_email],</p>
-      <p>You are now subscribed to <a href="[notify_node_url]">[notify_node_title]</a>.<br />
-      <a href="[notify_unsubscribe_url]">Unsubscribe</a> or visit <a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p>
-      <p>Thank you!</p>',
-      '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_node_url]; [notify_user_email]; [notify_unsubscribe_url]; [notify_user_subscribtions]'),
-    ];
-    $form['sent_verify_web_page_message'] = [
-      '#type' => 'text_format',
-      '#title' => 'Web message that verification email was sent.',
-      '#format' => 'full_html',
-      '#default_value' => isset($template['sent_verify_web_page_message']) ? $template['sent_verify_web_page_message'] : '
-      <p>Hey [notify_user_email],</p>
-      <p>Please check your email to finalize your subscription!</p>
-      <p style="font-size:9px">*If you didn’t get an e-mail, please check the spam folder</p>',
-      '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_user_email];'),
-    ];
-    $form['record_exist_verify_web_page_message'] = [
-      '#type' => 'text_format',
-      '#title' => 'Web message that subscription exist for that email',
-      '#format' => 'full_html',
-      '#default_value' => isset($template['record_exist_verify_web_page_message']) ? $template['record_exist_verify_web_page_message'] : '
-      <p>Hey [notify_user_email],</p>
-      <p>You already subscribed to this page!</p>
-      <p><a href="[notify_unsubscribe_url]">Unsubscribe from this page</a></p>',
-      '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_node_url]; [notify_user_email]; [notify_unsubscribe_url].'),
-    ];
-    $form['error_web_page_message'] = [
-      '#type' => 'text_format',
-      '#title' => 'Web message for error',
-      '#format' => 'full_html',
-      '#default_value' => isset($template['error_web_page_message']) ? $template['error_web_page_message'] : '
-      <p>Hey [notify_user_email],</p>
-      <p>There was an error on this page!</p>',
-      '#description' => $this->t('This message will show when recaptcha validation is incorrect.'),
-    ];
-    $form['subscription_not_available_web_page_message'] = [
-      '#type' => 'text_format',
-      '#title' => '"Subscription is not available" web message',
-      '#format' => 'full_html',
-      '#default_value' => isset($template['subscription_not_available_web_page_message']) ? $template['subscription_not_available_web_page_message'] : '<p>Subscription is not available for this page.</p>',
-      '#description' => $this->t('This message will be visible on pages that are not nodes and if block is not hidden.'),
-    ];
-
-    $form['confirmation_web_page_message'] = [
-      '#type' => 'text_format',
-      '#title' => 'Confirmation of subscription - web message',
-      '#format' => 'full_html',
-      '#default_value' => isset($template['confirmation_web_page_message']) ? $template['confirmation_web_page_message'] : '
-      <p>Hey [notify_user_email],</p>
-      <p>You are all set!</p>
-      <p>Thank you for subscribing!</p>
-      <p><a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p>',
-      '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_node_url]; [notify_user_email]; [notify_unsubscribe_url]; [notify_user_subscribtions]'),
-    ];
-    $form['general_email_template_subject'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Subject of E-mail'),
-      '#size' => 60,
-      '#maxlength' => 128,
-      '#default_value' => isset($template['general_email_template_subject']) ? $template['general_email_template_subject'] : '[notify_node_title] – Notification of New Update',
-      '#description' => $this->t('This is the Subject of Verification email that is sent to subscribers. Avaliable tokens: [notify_node_title]; [notify_user_email]'),
-    ];
-    $form['general_email_template'] = [
-      '#type' => 'text_format',
-      '#title' => 'Email body template for E-mail.',
-      '#format' => 'full_html',
-      '#default_value' => isset($template['general_email_template']) ? $template['general_email_template'] : '
-      <p>Hello [notify_user_email],</p>
-      <p>The "<a href="[notify_node_url]">[notify_node_title]</a>." has been updated.<br />
-      If you would like to unsubscribe to this page please go <a href="[notify_user_email]">here</a> or visit <a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p>
-      <p>[notify_notes]</p>
-      <p>Thank you!</p>',
-      '#description' => $this->t('This is the body of the Notify general email that is sent to subscribers. Avaliable tokens: [notify_node_title]; [notify_node_url]; [notify_user_email]; [notify_unsubscribe_url]; [notify_user_subscribtions]; [notify_notes]'),
-    ];
-    $form['#cache']['max-age'] = 0;
-    $form['actions'] = [
-      '#type' => 'actions',
-    ];
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => 'Submit',
-    ];
-    $buildInfo = $form_state->getBuildInfo();
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    $this->displayMethodInvocation('getFormId');
-    return 'form_api_example_build_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    $this->displayMethodInvocation('validateForm');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $this->configuration['body'] = $form_state->getValue('body');
-    $this->configuration['verification_email_subject'] = $form_state->getValue('verification_email_subject');
-    $this->configuration['verification_email_text'] = $form_state->getValue('verification_email_text');
-    $this->configuration['confirmation_email_subject'] = $form_state->getValue('confirmation_email_subject');
-    $this->configuration['confirmation_email_text'] = $form_state->getValue('confirmation_email_text');
-    $this->configuration['sent_verify_web_page_message'] = $form_state->getValue('sent_verify_web_page_message');
-    $this->configuration['record_exist_verify_web_page_message'] = $form_state->getValue('record_exist_verify_web_page_message');
-    $this->configuration['error_web_page_message'] = $form_state->getValue('error_web_page_message');
-    $this->configuration['subscription_not_available_web_page_message'] = $form_state->getValue('subscription_not_available_web_page_message');
-    $this->configuration['confirmation_web_page_message'] = $form_state->getValue('confirmation_web_page_message');
-    $this->configuration['general_email_template_subject'] = $form_state->getValue('general_email_template_subject');
-    $this->configuration['general_email_template'] = $form_state->getValue('general_email_template');
-
-    $notify_body_info = $form_state->getValue('body');
-    $notify_verification_email_subject = $form_state->getValue('verification_email_subject');
-    $notify_verification_email_text = $form_state->getValue('verification_email_text');
-    $notify_confirmation_email_subject = $form_state->getValue('confirmation_email_subject');
-    $notify_confirmation_email_text = $form_state->getValue('confirmation_email_text');
-    $notify_sent_verify_web_page_message = $form_state->getValue('sent_verify_web_page_message');
-    $notify_record_exist_verify_web_page_message = $form_state->getValue('record_exist_verify_web_page_message');
-    $notify_error_web_page_message = $form_state->getValue('error_web_page_message');
-    $notify_subscription_not_available_web_page_message = $form_state->getValue('subscription_not_available_web_page_message');
-    $notify_confirmation_web_page_message = $form_state->getValue('confirmation_web_page_message');
-    $notify_general_email_template_subject = $form_state->getValue('general_email_template_subject');
-    $notify_general_email_template = $form_state->getValue('general_email_template');
-
-    $template_exist = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template();
-    if ($template_exist) {
-      $request_time = \Drupal::time()->getRequestTime();
-      $query = \Drupal::database()->delete('page_notify_email_template');
-      $query->condition('template_id', $template_exist['template_id']);
-      $query->execute();
-      $query_insert = \Drupal::database()->insert('page_notify_email_template')
-        ->fields([
-          'from_email' => $form_state->getValue('from_email'),
-          'checkbox_field' => $form_state->getValue('checkbox_field'),
-          'notes_field' => $form_state->getValue('notes_field'),
-          'node_timestamp' => $form_state->getValue('node_timestamp'),
-          'created' => $request_time,
-          'body' => $notify_body_info['value'],
-          'verification_email_subject' => $form_state->getValue('verification_email_subject'),
-          'verification_email_text' => $notify_verification_email_text['value'],
-          'confirmation_email_subject' => $form_state->getValue('confirmation_email_subject'),
-          'confirmation_email_text' => $notify_confirmation_email_text['value'],
-          'sent_verify_web_page_message' => $notify_sent_verify_web_page_message['value'],
-          'record_exist_verify_web_page_message' => $notify_record_exist_verify_web_page_message['value'],
-          'error_web_page_message' => $notify_error_web_page_message['value'],
-          'subscription_not_available_web_page_message' => $notify_subscription_not_available_web_page_message['value'],
-          'confirmation_web_page_message' => $notify_confirmation_web_page_message['value'],
-          'general_email_template_subject' => $form_state->getValue('general_email_template_subject'),
-          'general_email_template' => $notify_general_email_template['value'],
-        ]);
-      $query_insert->execute();
-      if($query_insert){
-        \Drupal::messenger()->addStatus(t('The configuration options have been saved.'));
-      }
-    }
-    else {
-      $request_time = Drupal::time()->getRequestTime();
-      $query = \Drupal::database()->insert('page_notify_email_template')
-        ->fields([
-          'from_email' => $form_state->getValue('from_email'),
-          'checkbox_field' => $form_state->getValue('checkbox_field'),
-          'notes_field' => $form_state->getValue('notes_field'),
-          'node_timestamp' => $form_state->getValue('node_timestamp'),
-          'created' => $request_time,
-          'body' => $notify_body_info['value'],
-          'verification_email_subject' => $form_state->getValue('verification_email_subject'),
-          'verification_email_text' => $notify_verification_email_text['value'],
-          'confirmation_email_subject' => $form_state->getValue('confirmation_email_subject'),
-          'confirmation_email_text' => $notify_confirmation_email_text['value'],
-          'sent_verify_web_page_message' => $notify_sent_verify_web_page_message['value'],
-          'record_exist_verify_web_page_message' => $notify_record_exist_verify_web_page_message['value'],
-          'error_web_page_message' => $notify_error_web_page_message['value'],
-          'subscription_not_available_web_page_message' => $notify_subscription_not_available_web_page_message['value'],
-          'confirmation_web_page_message' => $notify_confirmation_web_page_message['value'],
-          'general_email_template_subject' => $form_state->getValue('general_email_template_subject'),
-          'general_email_template' => $notify_general_email_template['value'],
-        ]);
-      $query->execute();
-      if($query){
-        \Drupal::messenger()->addStatus(t('The configuration options have been saved.'));
-      }
-    }
-  }
-}
diff --git a/src/Form/MigrationForm.php b/src/Form/MigrationForm.php
deleted file mode 100644
index 2efc996..0000000
--- a/src/Form/MigrationForm.php
+++ /dev/null
@@ -1,275 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Entity\Element\EntityAutocomplete;
-use Drupal\node\Entity\Node;
-
-/**
- * @see \Drupal\Core\Form\FormBase
- */
-class MigrationForm extends FormBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_migration_form';
-  }
-
-  /**
-   * The node storage.
-   *
-   * @var \Drupal\node\NodeStorage
-   */
-  protected $nodeStorage;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
-    $this->nodeStorage = $entity_type_manager->getStorage('node');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('entity_type.manager')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-
-    if ($form_state->has('page_num') && $form_state->get('page_num') == 2) {
-      return self::fapiExamplePageTwo($form, $form_state);
-    }
-
-    $form_state->set('page_num', 1);
-    $form['general_settings_header'] = [
-      '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>Step 1 - Node selection.</h2>
-      <p>This migration allows to move subscribers from one node to another.</p>
-      "),
-      '#weight' => -1,
-    ];
-    $form['subscription_export'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('From Node'),
-      '#description' => $this->t('Node title from where subscriptions needs to be moved'),
-      '#default_value' => $form_state->getValue('subscription_export', ''),
-      '#required' => TRUE,
-      '#autocomplete_route_name' => 'page_notifications.autocomplete.subscriptions',
-      '#maxlength' => 1024,
-    ];
-    $form['subscription_import'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('To Node'),
-      '#description' => $this->t('Node title to where subscriptions needs to be moved'),
-      '#default_value' => $form_state->getValue('subscription_import', ''),
-      '#required' => TRUE,
-      '#autocomplete_route_name' => 'page_notifications.autocomplete.subscriptions',
-      '#maxlength' => 1024,
-    ];
-    $form['actions'] = [
-      '#type' => 'actions',
-    ];
-    $form['actions']['next'] = [
-      '#type' => 'submit',
-      '#button_type' => 'primary',
-      '#value' => $this->t('Next'),
-      '#submit' => ['::pageNotificationsSubscriptionsMigrationForm'],
-      //'#validate' => ['::fapiExampleMultistepFormNextValidate'],
-    ];
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $page_values = $form_state->get('page_values');
-    $node_subscriptions_list = $form_state->getValue('page-notifications-list');
-    $count = 0;
-    foreach ($node_subscriptions_list as $key => $value) {
-      if ($value['field_page_notify_node_id'] != 0) {
-        $node = \Drupal\node\Entity\Node::load($value['field_page_notify_node_id']);
-        $new_title = 'Subscription to - ' . $page_values['subscription_import'] . ' - ' . $node->field_page_notify_token->getString();
-        $node->set('title', $new_title);
-        $node->set('field_page_notify_node_id',  $page_values['subscription_import']);
-        $node->save();
-        $count++;
-      }
-    }
-    $morethen = ($count > 2) ? '1 Subscription' : ' '.$count.' Subscriptions';
-    $this->messenger()->addMessage($this->t('Updated total of: @count', ['@count' => $morethen]));
-  }
-
-  /**
-   * Provides custom validation handler for page 1.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.a
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   */
-  public function fapiExampleMultistepFormNextValidate(array &$form, FormStateInterface $form_state) {
-    /*if ($birth_year != '' && ($birth_year < 1900 || $birth_year > 2000)) {
-      // Set an error for the form element with a key of "birth_year".
-      $form_state->setErrorByName('birth_year', $this->t('Enter a year between 1900 and 2000.'));
-    }*/
-  }
-
-  /**
-   * Provides custom submission handler for page 1.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   */
-  public function pageNotificationsSubscriptionsMigrationForm(array &$form, FormStateInterface $form_state) {
-
-    $subscription_export_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($form_state->getValue('subscription_export'));
-    $subscription_import_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($form_state->getValue('subscription_import'));
-    $form_state
-      ->set('page_values', [
-        'subscription_export' => $subscription_export_id,
-        'subscription_import' => $subscription_import_id,
-      ])
-      ->set('page_num', 2)
-      ->setRebuild(TRUE);
-  }
-
-  /**
-   * Builds the second step form (page 2).
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   *
-   * @return array
-   *   The render array defining the elements of the form.
-   */
-  public function fapiExamplePageTwo(array &$form, FormStateInterface $form_state) {
-    $vals = $form_state->getStorage();
-    $subscription_export_node = $vals['page_values']['subscription_export'];
-
-    $nids = \Drupal::entityQuery('node')
-      ->accessCheck(FALSE)
-      ->condition('type', 'page_notify_subscriptions')
-      ->condition('status', 1)
-      ->condition('field_page_notify_node_id', $subscription_export_node , '=')
-      ->sort('created', 'DESC')
-      ->pager(10)
-      ->execute();
-    $nodes = \Drupal\node\Entity\Node::loadMultiple($nids);
-    $nids = array_keys($nodes);
-    $export_records_count = count($nids);
-    if($export_records_count == 0){
-      $form['general_settings_header'] = [
-        '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>No subscriptions found for this node.</h2>"),
-        '#weight' => -1,
-      ];
-      $form['back'] = [
-        '#type' => 'submit',
-        '#value' => $this->t('Back'),
-        '#submit' => ['::fapiExamplePageTwoBack'],
-        '#limit_validation_errors' => [],
-      ];
-    } else {
-      $morethen = ($export_records_count > 2) ? 'Subscription' : 'Subscriptions';
-      $form['general_settings_header'] = [
-        '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>Step 2 - Review. Total of ".$export_records_count . " " . $morethen . " to transfer:</h2>"),
-        '#weight' => -1,
-      ];
-      $form['pagenotifycheckall'] = array(
-        '#type' => 'checkbox',
-        '#title' => t('Select / Unselect all'),
-        '#default_value' => 1,
-        '#weight' => 0,
-      );
-      $form['page-notifications-list'] = array(
-        '#type' => 'table',
-        '#title' => 'List of Nodes',
-        '#header' => ["Checkbox", "Title", "Email", "Title (Node ID)"],
-    		'#multiple' => TRUE,
-      );
-
-      $i=0;
-      foreach($nids as $nid) {
-        $node = \Drupal\node\Entity\Node::load($nid);
-        $contenttitle=$node->title->value;
-        $receivername = $node->getOwner()->getDisplayName();
-        $field_page_notify_email = $node->get('field_page_notify_email')->getValue();
-        $field_page_notify_node_id = $node->get('field_page_notify_node_id')->getValue();
-        $field_page_notify_token = $node->get('field_page_notify_token')->getValue();
-        \Drupal::entityTypeManager()->getStorage('node')->resetCache(array($nid));
-
-        $subscribed_node = \Drupal\node\Entity\Node::load($field_page_notify_node_id[0]['value']);
-        $subscribed_node_url_str = $subscribed_node->toUrl()->toString();
-        $subscribed_node_title = $subscribed_node->getTitle();
-        $truncated_subscribed_node_title = (strlen($subscribed_node_title) > 20) ? substr($subscribed_node_title, 0, 20) . '...' : $subscribed_node_title;
-        $subscribed_node_link = '<a href="'.$subscribed_node_url_str.'">'.$truncated_subscribed_node_title.'</a>';
-
-        $form['page-notifications-list'][$i]['field_page_notify_node_id'] = array(
-          '#type' => 'checkbox',
-          '#return_value' => $nid,
-          '#default_value' => 1,
-          '#attributes' => array('checked' => 'checked')
-          );
-        $form['page-notifications-list'][$i]['Title'] = array(
-          '#type' => 'label',
-          '#title' => t($contenttitle),
-        );
-        $form['page-notifications-list'][$i]['Email'] = array(
-          '#type' => 'label',
-          '#title' => t($field_page_notify_email[0]['value']),
-        );
-        $form['page-notifications-list'][$i]['wiew_node'] = array(
-          '#markup' => t($subscribed_node_link . ' (' . $field_page_notify_node_id[0]['value'] . ')'),
-        );
-        $i++;
-      }
-      $build['pager'] = array(
-        '#markup' => 'pager',
-      );
-
-      $form['back'] = [
-        '#type' => 'submit',
-        '#value' => $this->t('Back'),
-        '#submit' => ['::fapiExamplePageTwoBack'],
-        '#limit_validation_errors' => [],
-      ];
-      $form['submit'] = [
-        '#type' => 'submit',
-        '#button_type' => 'primary',
-        '#value' => $this->t('Submit'),
-      ];
-    }
-    return $form;
-  }
-
-  /**
-   * Provides custom submission handler for 'Back' button (page 2).
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   */
-  public function fapiExamplePageTwoBack(array &$form, FormStateInterface $form_state) {
-    $form_state
-      ->setValues($form_state->get('page_values'))
-      ->set('page_num', 1)
-      ->setRebuild(TRUE);
-  }
-}
diff --git a/src/Form/PageNotificationsBlockForm.php b/src/Form/PageNotificationsBlockForm.php
deleted file mode 100644
index fc8faba..0000000
--- a/src/Form/PageNotificationsBlockForm.php
+++ /dev/null
@@ -1,496 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Mail\MailManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Language\LanguageManagerInterface;
-use Drupal\Component\Utility\EmailValidator;
-use Drupal\Core\Block\BlockPluginInterface;
-use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Core\Render\Markup;
-use Drupal\Core\Url;
-use \Drupal\node\Entity\Node;
-use Drupal\taxonomy\Entity\Term;
-use \Drupal\Component\Utility\UrlHelper;
-use Drupal\filter\Element\ProcessedText;
-use Drupal\Core\Render\BubbleableMetadata;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Component\Utility\Html;
-use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Ajax\CssCommand;
-use Drupal\Core\Ajax\HtmlCommand;
-use Drupal\Core\Ajax\InvokeCommand;
-
-/**
- * Submit a form without a page reload.
- */
-class PageNotificationsBlockForm extends FormBase
-{
-
-  /**
-   * The mail manager.
-   *
-   * @var \Drupal\Core\Mail\MailManagerInterface
-   */
-  protected $mailManager;
-
-  /**
-   * The email validator.
-   *
-   * @var \Drupal\Component\Utility\EmailValidator
-   */
-  protected $emailValidator;
-
-  /**
-   * The language manager.
-   *
-   * @var \Drupal\Core\Language\LanguageManagerInterface
-   */
-  protected $languageManager;
-
-  /**
-   * Constructs a new EmailExampleGetFormPage.
-   *
-   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
-   *   The mail manager.
-   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
-   *   The language manager.
-   * @param \Drupal\Component\Utility\EmailValidator $email_validator
-   *   The email validator.
-   */
-  public function __construct(MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EmailValidator $email_validator)
-  {
-    $this->mailManager = $mail_manager;
-    $this->languageManager = $language_manager;
-    $this->emailValidator = $email_validator;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container)
-  {
-    $form = new static(
-      $container->get('plugin.manager.mail'),
-      $container->get('language_manager'),
-      $container->get('email.validator')
-    );
-    $form->setMessenger($container->get('messenger'));
-    $form->setStringTranslation($container->get('string_translation'));
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId()
-  {
-    return 'page_notifications_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state)
-  {
-    $preload_template = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template();
-    $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings();
-    $host = \Drupal::request()->getSchemeAndHttpHost();
-    $current_uri_row = \Drupal::request()->getRequestUri();
-    $current_uri_pieces = explode("?", $current_uri_row);
-    $current_uri = $current_uri_pieces[0];
-    $reload_page_link = $host . $current_uri;
-    $page_url = '';
-    $page_id = '';
-    
-    $form['#prefix'] = '<div id="page-notifications-block-container">';
-    $form['#suffix'] = '</div>';
-    $route_match = \Drupal::routeMatch();
-
-    if ($route_match->getRouteName() == 'entity.node.canonical') {
-      $node = \Drupal::routeMatch()->getParameter('node');
-      if ($node instanceof \Drupal\node\NodeInterface) {
-        $node = \Drupal\node\Entity\Node::load($node->id());
-        $page_title = $node->getTitle();
-        $page_id = $node->id() . '-' . 'node';
-        $page_entity_type = 'node';
-        $node_node_url = $node->toUrl()->setAbsolute()->toString();
-      }
-    } elseif ($route_match->getRouteName() == 'entity.taxonomy_term.canonical') {
-      $term_id = $route_match->getRawParameter('taxonomy_term');
-      $term = Term::load($term_id);
-      $page_id = $term_id . '-' . 'term';
-      $page_entity_type = 'term';
-      $page_title = $term->getName();
-      $page_url = $term->toUrl()->setAbsolute()->toString();
-    } else {
-      $page_title = '';
-      $page_url = '';
-      $page_id = '';
-    }
-
-    if ($page_url == '' && $page_id == '') {
-      $form['container'] = [
-        '#type' => 'container',
-        '#attributes' => ['id' => 'page-notifications-block-container'],
-      ];
-      $form['block_header'] = [
-        '#type' => 'processed_text',
-        '#text' => $this->t($preload_template['subscription_not_available_web_page_message']),
-        "#processed" => true,
-        '#format' => 'full_html',
-      ];
-    } else {
-      $block_header_replacements = [
-        '[notify_user_name]' => '',
-        '[notify_user_email]' => '',
-        '[notify_verify_url]' => '',
-        '[notify_subscribe_url]' => '',
-        '[notify_unsubscribe_url]' => '',
-        '[notify_user_subscribtions]' => '',
-        '[notify_node_title]' => $page_title,
-        '[notify_node_url]' => $page_url,
-        '[notify_notes]' => '',
-      ];
-      $tokanized_notify_body = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($preload_template['body'], $block_header_replacements);
-      $form['container'] = [
-        '#type' => 'container',
-        '#attributes' => ['id' => 'page-notifications-block-container'],
-      ];
-      $form['container']['box'] = [
-        '#type' => 'processed_text',
-        '#text' => $this->t($tokanized_notify_body),
-        "#processed" => true,
-        '#format' => 'full_html',
-      ];
-      $form['current_node'] = [
-        '#type' => 'hidden',
-        '#value' => $page_id,
-      ];
-      $form['current_path'] = [
-        '#type' => 'hidden',
-        '#value' => $page_url,
-      ];
-      $form['email_notify'] = [
-        '#type' => 'email',
-        '#title' => $this->t('Enter your E-mail Address:'),
-        '#required' => true,
-      ];
-      $moduleHandler = \Drupal::service('module_handler');
-      if ($moduleHandler->moduleExists('recaptcha') && $notify_settings['page_notify_recaptcha'] == "1") {
-        $form['#attached']['library'][] = 'page_notifications/recaptcha';
-        $form['recaptcha'] = [
-          '#markup' => pageNotificationsRecaptchaHtml(),
-        ];
-      }
-      if ($moduleHandler->moduleExists('captcha') && $notify_settings['page_notify_captcha'] == "1") {
-        $captcha_type = \Drupal::config('captcha.settings')->get('default_challenge');
-        $form['my_captcha_element'] = [
-          '#type' => 'captcha',
-          '#captcha_type' => $captcha_type,
-        ];
-      }
-    }
-
-    $form['submit'] = [
-      '#type' => 'submit',
-      '#ajax' => [
-        'callback' => '::promptCallback',
-        'wrapper' => 'page-notifications-block-container',
-      ],
-      '#value' => $this->t('Submit'),
-    ];
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state)
-  {
-    if (!$this->emailValidator->isValid($form_state->getValue('email_notify')) || $form_state->getValue('email_notify') != strip_tags($form_state->getValue('email_notify'))) {
-      $form_state->setErrorByName('email', $this->t('That e-mail address is not valid.'));
-    }
-
-  }
-
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state)
-  {
-  }
-
-  /**
-   * Callback for submit_driven example.
-   *
-   * Select the 'box' element, change the markup in it, and return it as a
-   * renderable array.
-   *
-   * @return array
-   *   Renderable array (the box element)
-   */
-  public function promptCallback(array &$form, FormStateInterface $form_state)
-  {
-    $response = new AjaxResponse();
-    $element = $form['container'];
-    $host = \Drupal::request()->getSchemeAndHttpHost();
-    $template_info = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template();
-    $input = $form_state->getUserInput();
-    if (!is_null($input['g-recaptcha-response'])) {
-      if ($input['g-recaptcha-response'] == "") {
-        $form_state->setErrorByName('g-recaptcha-response', $this->t('Please enter recaptcha.'));
-      }
-    }
-    $errors = $form_state->getErrors();
-
-    if (!$errors) {
-      // if email and node not correct
-      if ($form_state->getValue('email_notify') != strip_tags($form_state->getValue('email_notify')) || $form_state->getValue('current_node') != strip_tags($form_state->getValue('current_node'))) {
-        $email_notify_strip_tags = strip_tags($form_state->getValue('email_notify'));
-        $replacements = [
-          '[notify_user_name]' => '',
-          '[notify_user_email]' => $email_notify_strip_tags,
-          '[notify_verify_url]' => '',
-          '[notify_subscribe_url]' => '',
-          '[notify_unsubscribe_url]' => '',
-          '[notify_user_subscribtions]' => '',
-          '[notify_node_title]' => '',
-          '[notify_node_url]' => '',
-          '[notify_notes]' => '',
-        ];
-        $tokanized_error_web_page_message = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['error_web_page_message'], $replacements);
-        $element['box'] = [
-          '#type' => 'processed_text',
-          '#text' => $this->t($tokanized_error_web_page_message),
-          '#format' => 'full_html',
-        ];
-      } else { // if email and node correct
-        if ($form_state->getValue('email_notify') && $form_state->getValue('current_node')) {
-          $email_notify = $form_state->getValue('email_notify');
-          $current_node = $form_state->getValue('current_node');
-          $record_exist = \Drupal::service('load.databaseinnfo.service')->checkIfRecordExistNode($email_notify, $current_node);
-
-          if ($record_exist) {
-            $token_pieces = explode("-", $record_exist['field_page_notify_token']);
-            if (count($token_pieces) == 2) {
-              $record_exist_node_id = $record_exist['field_page_notify_node_id'];
-              $record_exist_token = $token_pieces[0];
-              $record_exist_entity_type = $token_pieces[1];
-            } elseif (count($token_pieces) == 1) {
-              $record_exist_node_id = $record_exist['field_page_notify_node_id'];
-              $record_exist_token = $token_pieces[0];
-              $record_exist_entity_type = 'node';
-            } else {
-              $record_exist_node_id = $record_exist['field_page_notify_node_id'];
-              $record_exist_entity_type = 'node';
-            }
-
-            $unsubscribe_link = $host . "/page-notifications/unsubscribe/" . $record_exist['field_page_notify_token'];
-            if ($record_exist_entity_type == 'node') {
-              $node = \Drupal\node\Entity\Node::load($record_exist_node_id);
-              $page_title = $node->getTitle();
-              $subscribed_node_url = $node->toUrl()->setAbsolute()->toString();
-            } elseif ($record_exist_entity_type == 'term') {
-              $node = Term::load($record_exist_node_id);
-              $page_title = $node->getName();
-              $subscribed_node_url = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $node->tid->value], ['absolute' => TRUE])->toString();
-            } else {
-              $node = NULL;
-            }
-
-            $replacements = [
-              '[notify_user_name]' => '',
-              '[notify_user_email]' => $email_notify,
-              '[notify_verify_url]' => '',
-              '[notify_subscribe_url]' => '',
-              '[notify_unsubscribe_url]' => $unsubscribe_link ? $unsubscribe_link : '',
-              '[notify_user_subscribtions]' => '',
-              '[notify_node_title]' => $page_title ? $page_title : '',
-              '[notify_node_url]' => $subscribed_node_url ? $subscribed_node_url : '',
-              '[notify_notes]' => '',
-            ];
-            $tokanized_web_message = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['record_exist_verify_web_page_message'], $replacements);
-            $element['box'] = [
-              '#type' => 'processed_text',
-              '#text' => $this->t($tokanized_web_message),
-              '#format' => 'full_html',
-            ];
-          } else { // this is when record doesn't exist
-            $current_page_node_pieces = explode("-", $current_node);
-            if (count($current_page_node_pieces) == 2) {
-              $current_node_id = $current_page_node_pieces[0];
-              $current_entity_type = $current_page_node_pieces[1];
-            } else {
-              $current_node_id = $current_node;
-              $current_entity_type = 'node';
-            }
-
-            if ($current_entity_type == 'node') {
-              $node = \Drupal\node\Entity\Node::load($current_node_id);
-              $current_page_title = $node->getTitle();
-              $current_subscribed_node_url = $node->toUrl()->setAbsolute()->toString();
-            } elseif ($current_entity_type == 'term') {
-              $node = Term::load($current_node_id);
-              $current_page_title = $node->getName();
-              $current_subscribed_node_url = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $node->tid->value], ['absolute' => TRUE])->toString();
-            } else {
-              $node = NULL;
-            }
-            $unsubscribe_link = '';
-            if ($template_info['from_email']) {
-              $from = $template_info['from_email'];
-            } else {
-              $from = \Drupal::config('system.site')->get('mail');
-            }
-            $current_uri_row = \Drupal::request()->getRequestUri();
-            $current_uri_pieces = explode("?", $current_uri_row);
-            $current_uri = $current_uri_pieces[0];
-            //$reload_page_link = $host . $current_uri;
-            $subscription_token = page_notifications_generateRandomString();
-            $confrm_url = $host . "/page-notifications/confirmation/" . $email_notify . "/" . $current_node . "-" . $subscription_token;
-
-            $subject_replacements = [
-              '[notify_user_name]' => '',
-              '[notify_user_email]' => $email_notify,
-              '[notify_verify_url]' => '',
-              '[notify_subscribe_url]' => '',
-              '[notify_unsubscribe_url]' => '',
-              '[notify_user_subscribtions]' => '',
-              '[notify_node_title]' => $current_page_title,
-              '[notify_node_url]' => '',
-              '[notify_notes]' => '',
-            ];
-            $body_replacements = [
-              '[notify_user_name]' => '',
-              '[notify_user_email]' => $email_notify,
-              '[notify_verify_url]' => $confrm_url,
-              '[notify_subscribe_url]' => '',
-              '[notify_unsubscribe_url]' => '',
-              '[notify_user_subscribtions]' => '',
-              '[notify_node_title]' => $current_page_title,
-              '[notify_node_url]' => $current_subscribed_node_url,
-              '[notify_notes]' => '',
-            ];
-            $tokanized_subject = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['verification_email_subject'], $subject_replacements);
-            $tokanized_body = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['verification_email_text'], $body_replacements);
-            $tokanized_web_page_message = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['sent_verify_web_page_message'], $body_replacements);
-            // send an email
-            $message['to'] = $email_notify;
-            $message['subject'] = $tokanized_subject;
-            $message['body'] = $tokanized_body;
-            $result = \Drupal::service('plugin.manager.mail')->mail(
-              'page_notifications',
-              'verification_email',
-              $email_notify,
-              \Drupal::languageManager()->getDefaultLanguage()->getId(),
-              $message
-            );
-
-            if ($result['result'] !== true) {
-              $element['box'] = [
-                '#type' => 'processed_text',
-                '#text' => $this->t('There was an error sending you an email verification. Please contact ' . $from . ' for assistance.'),
-                '#format' => 'full_html',
-              ];
-            } else {
-              $element['box'] = [
-                '#type' => 'processed_text',
-                '#text' => $this->t($tokanized_web_page_message),
-                '#format' => 'full_html',
-              ];
-            }
-          } //end of when record doesn't exist and email needs to be sent
-        }
-      }
-    } else {
-      $replacements = [
-        '[notify_user_name]' => '',
-        '[notify_user_email]' => '',
-        '[notify_verify_url]' => '',
-        '[notify_subscribe_url]' => '',
-        '[notify_unsubscribe_url]' => '',
-        '[notify_user_subscribtions]' => '',
-        '[notify_node_title]' => '',
-        '[notify_node_url]' => '',
-        '[notify_notes]' => '',
-      ];
-      $tokanized_error_web_page_message = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['error_web_page_message'], $replacements);
-      $element['box'] = [
-        '#type' => 'processed_text',
-        '#text' => $this->t($tokanized_error_web_page_message),
-        '#format' => 'full_html',
-      ];
-    }
-    return $element;
-  } //promptCallback
-} //class
-
-/**
- * Summary of Drupal\page_notifications\Form\pageNotificationsRecaptchaHtml
- * @return string
- */
-function pageNotificationsRecaptchaHtml()
-{
-  $site_key = \Drupal::config('recaptcha.settings')->get('site_key');
-  if (!is_null($site_key)) {
-    define('RECAPTCHA_SITEKEY', $site_key);
-    return '<div class="g-recaptcha mb-3" data-sitekey="' . RECAPTCHA_SITEKEY . '"></div>';
-  }
-}
-
-/**
- * Summary of Drupal\page_notifications\Form\pageNotificationsPost
- * @param string $url
- * @param array $postdata
- * @return bool|string
- */
-function pageNotificationsPost(string $url, array $postdata)
-{
-  $content = http_build_query($postdata);
-  $opts = [
-    'http' =>
-      [
-        'method' => 'POST',
-        'header' => 'Content-Type: application/x-www-form-urlencoded',
-        'content' => $content
-      ]
-  ];
-  $context = stream_context_create($opts);
-  $result = file_get_contents($url, false, $context);
-  return $result;
-}
-
-function pageNotificationsRecaptchaVerify()
-{
-  if (!isset($_POST['g-recaptcha-response'])) {
-    return false;
-  }
-  $response = filter_input(INPUT_POST, 'g-recaptcha-response', FILTER_SANITIZE_STRING);
-  $secret_key = \Drupal::config('recaptcha.settings')->get('secret_key');
-  define('RECAPTCHA_SECRETKEY', $secret_key);
-  define('RECAPTCHA_URL', 'https://www.google.com/recaptcha/api/siteverify');
-  $json = pageNotificationsPost(RECAPTCHA_URL, ['secret' => RECAPTCHA_SECRETKEY, 'response' => $response]);
-  $data = json_decode($json, true);
-  if ($data['success'] == 1) {
-    return true;
-  }
-  return false;
-}
-
-function page_notifications_generateRandomString($length = 10)
-{
-  $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
-  $charactersLength = strlen($characters);
-  $randomString = '';
-  for ($i = 0; $i < $length; $i++) {
-    $randomString .= $characters[rand(0, $charactersLength - 1)];
-  }
-  return $randomString;
-}
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
new file mode 100644
index 0000000..b9c4e0e
--- /dev/null
+++ b/src/Form/SettingsForm.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Mail\MailManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+
+/**
+ * Configures Page Notifications settings.
+ */
+class SettingsForm extends ConfigFormBase {
+
+  /**
+   * The mail manager.
+   *
+   * @var \Drupal\Core\Mail\MailManagerInterface
+   */
+  protected $mailManager;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a SettingsForm object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
+   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
+   *   The mail manager service.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    MailManagerInterface $mail_manager,
+    EntityTypeManagerInterface $entity_type_manager
+  ) {
+    parent::__construct($config_factory);
+    $this->mailManager = $mail_manager;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('plugin.manager.mail'),
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_settings';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return ['page_notifications.settings'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $config = $this->config('page_notifications.settings');
+
+    $form['email_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Email Settings'),
+      '#open' => TRUE,
+    ];
+
+    $form['email_settings']['from_email'] = [
+      '#type' => 'email',
+      '#title' => $this->t('From Email Address'),
+      '#description' => $this->t('The email address that notifications will be sent from. If left empty, the site default will be used.'),
+      '#default_value' => $config->get('notification_settings.from_email'),
+    ];
+
+    $form['email_settings']['token_expiration'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Token Expiration'),
+      '#description' => $this->t('Number of hours before verification tokens expire.'),
+      '#default_value' => $config->get('notification_settings.token_expiration') ?? 48,
+      '#min' => 1,
+      '#required' => TRUE,
+    ];
+
+    $form['email_templates'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Email Templates'),
+      '#open' => TRUE,
+    ];
+
+    $form['email_templates']['verification_subject'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Verification Email Subject'),
+      '#default_value' => $config->get('email_templates.verification_subject') ?? 'Verify your subscription to [node:title]',
+      '#required' => TRUE,
+    ];
+
+    $form['email_templates']['verification_body'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Verification Email Body'),
+      '#default_value' => $config->get('email_templates.verification_body'),
+      '#description' => $this->t('Available tokens: [subscription:verify-url], [subscription:email], [node:title], [node:url]'),
+      '#required' => TRUE,
+      '#rows' => 10,
+    ];
+
+    $form['email_templates']['notification_subject'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Update Notification Subject'),
+      '#default_value' => $config->get('email_templates.notification_subject') ?? '[node:title] has been updated',
+      '#required' => TRUE,
+    ];
+
+    $form['email_templates']['notification_body'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Update Notification Body'),
+      '#default_value' => $config->get('email_templates.notification_body'),
+      '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [node:changed], [subscription:unsubscribe-url]'),
+      '#required' => TRUE,
+      '#rows' => 10,
+    ];
+
+    $form['security'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Security Settings'),
+      '#open' => TRUE,
+    ];
+
+    $form['security']['require_verification'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Require Email Verification'),
+      '#description' => $this->t('If checked, users must verify their email address before the subscription becomes active.'),
+      '#default_value' => $config->get('security.require_verification') ?? TRUE,
+    ];
+
+    $form['security']['cleanup_unverified'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Clean Up Unverified Subscriptions'),
+      '#description' => $this->t('Automatically delete unverified subscriptions after they expire.'),
+      '#default_value' => $config->get('security.cleanup_unverified') ?? TRUE,
+    ];
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    if ($form_state->getValue('from_email') && !$this->mailManager->validateAddress($form_state->getValue('from_email'))) {
+      $form_state->setErrorByName('from_email', $this->t('The email address is not valid.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->config('page_notifications.settings')
+      ->set('notification_settings.from_email', $form_state->getValue('from_email'))
+      ->set('notification_settings.token_expiration', $form_state->getValue('token_expiration'))
+      ->set('email_templates.verification_subject', $form_state->getValue('verification_subject'))
+      ->set('email_templates.verification_body', $form_state->getValue('verification_body'))
+      ->set('email_templates.notification_subject', $form_state->getValue('notification_subject'))
+      ->set('email_templates.notification_body', $form_state->getValue('notification_body'))
+      ->set('security.require_verification', $form_state->getValue('require_verification'))
+      ->set('security.cleanup_unverified', $form_state->getValue('cleanup_unverified'))
+      ->save();
+
+    parent::submitForm($form, $form_state);
+  }
+
+}
\ No newline at end of file
diff --git a/src/Form/SubscriptionDeleteForm.php b/src/Form/SubscriptionDeleteForm.php
new file mode 100644
index 0000000..8a2a8dd
--- /dev/null
+++ b/src/Form/SubscriptionDeleteForm.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Entity\ContentEntityDeleteForm;
+
+class SubscriptionDeleteForm extends ContentEntityDeleteForm {
+}
\ No newline at end of file
diff --git a/src/Form/SubscriptionForm.php b/src/Form/SubscriptionForm.php
new file mode 100644
index 0000000..5f6f288
--- /dev/null
+++ b/src/Form/SubscriptionForm.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\page_notifications\Service\NotificationManagerInterface;
+
+/**
+ * Provides a subscription form.
+ */
+class SubscriptionForm extends FormBase {
+
+  /**
+   * The notification manager service.
+   *
+   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
+   */
+  protected $notificationManager;
+
+  /**
+   * Constructs a new SubscriptionForm.
+   *
+   * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager
+   *   The notification manager service.
+   */
+  public function __construct(NotificationManagerInterface $notification_manager) {
+    $this->notificationManager = $notification_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('page_notifications.notification_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_subscription_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL) {
+    // Store the entity for use in the submit handler.
+    $form_state->set('entity', $entity);
+
+    $form['email'] = [
+      '#type' => 'email',
+      '#title' => $this->t('Email address'),
+      '#required' => TRUE,
+      '#description' => $this->t('Enter your email address to receive notifications when this content is updated.'),
+    ];
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Subscribe'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    if (!filter_var($form_state->getValue('email'), FILTER_VALIDATE_EMAIL)) {
+      $form_state->setErrorByName('email', $this->t('Please enter a valid email address.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $entity = $form_state->get('entity');
+    $email = $form_state->getValue('email');
+
+    try {
+      $subscription = $this->notificationManager->createSubscription($email, $entity);
+      $this->messenger()->addStatus($this->t('Thank you for subscribing. Please check your email to confirm your subscription.'));
+    }
+    catch (\Exception $e) {
+      $this->messenger()->addError($this->t('There was a problem creating your subscription. Please try again later.'));
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/src/Form/UserSubscriptionsPage.php b/src/Form/UserSubscriptionsPage.php
deleted file mode 100644
index 2d14f5a..0000000
--- a/src/Form/UserSubscriptionsPage.php
+++ /dev/null
@@ -1,350 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Mail\MailManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Language\LanguageManagerInterface;
-use Drupal\Component\Utility\EmailValidator;
-use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Core\Url;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-use Drupal\node\Entity\Node;
-
-/**
- * Provides a form with two steps.
- *
- * This example demonstrates a multistep form with text input elements. We
- * extend FormBase which is the simplest form base class used in Drupal.
- *
- * @see \Drupal\Core\Form\FormBase
- */
-class UserSubscriptionsPage extends FormBase {
-  /**
-   * The mail manager.
-   *
-   * @var \Drupal\Core\Mail\MailManagerInterface
-   */
-  protected $mailManager;
-
-  /**
-   * The email validator.
-   *
-   * @var \Drupal\Component\Utility\EmailValidator
-   */
-  protected $emailValidator;
-
-  /**
-   * The language manager.
-   *
-   * @var \Drupal\Core\Language\LanguageManagerInterface
-   */
-  protected $languageManager;
-
-  /**
-   * Constructs a new EmailUnsubscribePage.
-   *
-   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
-   *   The mail manager.
-   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
-   *   The language manager.
-   * @param \Drupal\Component\Utility\EmailValidator $email_validator
-   *   The email validator.
-   */
-  public function __construct(MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EmailValidator $email_validator) {
-    $this->mailManager = $mail_manager;
-    $this->languageManager = $language_manager;
-    $this->emailValidator = $email_validator;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    $form = new static(
-      $container->get('plugin.manager.mail'),
-      $container->get('language_manager'),
-      $container->get('email.validator')
-    );
-    $form->setMessenger($container->get('messenger'));
-    $form->setStringTranslation($container->get('string_translation'));
-    return $form;
-  }
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page-notifications-user-subscriptions-list';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state,  $subscription_token = NULL) {
-
-    if($subscription_token != strip_tags($subscription_token) || is_null($subscription_token)) {
-      \Drupal::messenger()->addError(t('You link might be broken or incomplete. Please make sure you dont have extra space in your address link.'));
-    } else {
-      $host = \Drupal::request()->getSchemeAndHttpHost();
-      if (is_string($subscription_token) && !is_null($subscription_token)) {
-        $part_subscription_token = explode("-", $subscription_token);
-        if (strlen($part_subscription_token[1]) == 10) {
-          if ($form_state->has('page_num') && $form_state->get('page_num') == 2) {
-            return self::userSubscriptionsPageTwo($form, $form_state);
-          }
-          $form_state->set('page_num', 1);
-          $form['subscription_token'] = [
-            '#type' => 'hidden',
-            '#value' => $subscription_token,
-          ];
-          $form['email_check'] = [
-            '#type' => 'textfield',
-            '#title' => $this->t('Enter your E-mail Address:'),
-            '#description' => $this->t('Please enter your email for varification.'),
-            '#default_value' => $form_state->getValue('email_check', ''),
-            '#required' => TRUE,
-          ];
-          $form['actions'] = [
-            '#type' => 'actions',
-          ];
-          $form['actions']['next'] = [
-            '#type' => 'submit',
-            '#button_type' => 'primary',
-            '#value' => $this->t('Next'),
-            '#submit' => ['::userSubscriptionsPageNextSubmit'],
-            '#validate' => ['::userSubscriptionsPageNextValidate'],
-          ];
-        }
-
-      } else {
-        $form['intro'] = [
-          '#markup' => $this->t('<p>You link might be broken or incomplete.</p>'),
-        ];
-      }
-    }
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-
-  }
-
-  /**
-   * Provides custom validation handler for page 1.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   */
-  public function userSubscriptionsPageNextValidate(array &$form, FormStateInterface $form_state) {
-    if (!$this->emailValidator->isValid($form_state->getValue('email_check')) || is_null($form_state->getValue('email_check'))) {
-      $form_state->setErrorByName('email_check', $this->t('That e-mail address is not valid.'));
-    } else {
-      $email_check = strip_tags($form_state->getValue('email_check'));
-      $subscription_token = strip_tags($form_state->getValue('subscription_token'));
-      if(is_string($email_check) && is_string($subscription_token)) {
-        $record = \Drupal::service('load.databaseinnfo.service')->verifyByNodeAndEmail($form_state->getValue('email_check'), $form_state->getValue('subscription_token'));
-        if ($record == true) {
-          $email_verify = $email_check;
-        } else {
-          $form_state->setErrorByName('email_check', $this->t('Incorect E-Mail.'));
-          \Drupal::messenger()->addError(t("We don't have subscription for this email."));
-        }
-      }
-    }
-  }
-
-  /**
-   * Provides custom submission handler for page 1.
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   */
-  public function userSubscriptionsPageNextSubmit(array &$form, FormStateInterface $form_state) {
-    $email_check = $form_state->getValue('email_check');
-    $form_state
-      ->set('page_values', [
-        'email_check' => $form_state->getValue('email_check'),
-        'subscription_token' => $form_state->getValue('subscription_token'),
-      ])
-      ->set('page_num', 2)
-      ->setRebuild(TRUE);
-  }
-
-  /**
-   * Builds the second step form (page 2).
-   *
-   * @param array $form
-   *   An associative array containing the structure of the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   *
-   * @return array
-   *   The render array defining the elements of the form.
-   */
-  public function userSubscriptionsPageTwo(array &$form, FormStateInterface $form_state) {
-    $part_subscription_token = explode("-", $form_state->getValue('subscription_token'));
-    $node_id_unsubscribe = $part_subscription_token[0];
-    $subscription_token = $part_subscription_token[1];
-    $page_notify_user_token = \Drupal::service('load.databaseinnfo.service')->pageNotifyGetUserToken($form_state->getValue('email_check'));
-    $user_token = $page_notify_user_token['field_page_notify_token_user_id'];
-
-    if ($user_token && !is_null($user_token)) {
-      $header = array(
-        array('data' => $this->t('Watching pages list'), 'field' => 'title', 'sort' => 'asc'),
-        array('data' => $this->t('')),
-      );
-      $query = \Drupal::entityQuery('node')
-        ->accessCheck(FALSE)
-        ->condition('type', 'page_notify_subscriptions')
-        ->condition('field_page_notify_token_user_id', $user_token, '=')
-        ->condition('status', 1)
-        ->sort('created', 'DESC')
-        ->pager(10);
-      $records = $query->execute();
-      $rows = array();
-      foreach ($records as $record) {
-        $node_record = \Drupal\node\Entity\Node::load($record);
-        $field_token_notify = $node_record->get("field_page_notify_token")->getValue();
-        $field_node_id_notify = $node_record->get("field_page_notify_node_id")->getValue();
-
-        $subscriptions_record = \Drupal\node\Entity\Node::load($field_node_id_notify[0]['value']);
-          $rows[] = array('data' => array(
-            'title' => new FormattableMarkup('<a href="@page_url">@page_title</a>',
-              [
-                '@page_title' => $subscriptions_record->getTitle(),
-                '@page_url' => $subscriptions_record->toUrl()->toString(),
-              ]),
-            'cancel_one' => new FormattableMarkup('<a id="notify-cancel-@token" href="/nojs/cancel_subscription/@token" class="use-ajax btn btn-default notify-cancel-subscription">@name</a>',
-                ['@name' => 'Stop Watching', '@token' => $field_token_notify[0]['value']]
-              ),
-          ));
-
-        if ($rows) {
-          $cancelall =  '<a id="notify-cancel-all" href="/nojs/cancel_all/' . $user_token .'" class="use-ajax btn btn-default notify-cancel-all-subscription">Unsubscribe from all</a>';
-          $header = array(
-            array('data' => $this->t('Page Name'), 'field' => 'title', 'sort' => 'asc'),
-            array('data' => $this->t($cancelall)),
-          );
-        } else {
-          $build = array();
-        }
-      }
-
-      /*$page_name = '<h1>Manage Your Page Watching Subscriptions</h1>';
-      $build['page_name'] = [
-        '#markup' => $page_name,
-        '#attributes' => [
-          'class' => ['page-notifications-user-list-page-name'],
-        ],
-      ];*/
-      $build['config_table'] = array(
-        '#theme' => 'table',
-        '#header' => $header,
-        '#rows' => $rows,
-        '#empty' => t('No records found'),
-        '#attributes' => [
-          'class' => ['page-notifications-block-subscriberpage'],
-          'id' => 'page-notifications-block-subscriberpage',
-          'no_striping' => TRUE,
-        ],
-      );
-      $build['pager'] = array(
-        '#type' => 'pager'
-      );
-      return $build;
-
-    } else {
-      $url = \Drupal\Core\Url::fromRoute('<front>')->toString();
-      $response = new RedirectResponse($url);
-      $response->send();
-      \Drupal::messenger()->addError(t('You link might be broken or incomplete.'));
-    }
-    //return $form;
-  }
-
-  public function rebuildFormSubmit(array &$form, FormStateInterface $form_state) {
-    $this->displayMethodInvocation('rebuildFormSubmit');
-    $form_state->setRebuild(TRUE);
-  }
-
-  public function cancel_subscription($token) {
-      $response = new AjaxResponse();
-      page_notifications_user_delete_record($token);
-      $response->addCommand(new ReplaceCommand('#notify-cancel-' . $token, '<span class="notify-cancel-cancelled">Cancelled</span>'));
-      return $response;
-  }
-
-  public function cancel_all($user_token) {
-      $response = new AjaxResponse();
-      page_notifications_user_delete_all_records($user_token);
-      $response->addCommand(new ReplaceCommand('#notify-cancel-all', '<span class="notify-cancel-all-cancelled">All cancelled</span>'));
-      $response->addCommand(new ReplaceCommand('#notify-cancel-', "Cancelled"));
-      return $response;
-  }
-}
-
-function checkForRecord($subscription_token_url, $email) {
-  $record = current(\Drupal::entityTypeManager()->getStorage('node')
-    ->loadByProperties([
-      'field_page_notify_token' => $subscription_token_url,
-      'field_page_notify_email' => $email
-    ])
-  );
-  if ($record) {
-    return $record;
-  }
-  else {
-    return FALSE;
-  }
-}
-
-function getAllRecords($email) {
-  $query = \Drupal::entityQuery('node')
-    ->accessCheck(FALSE)
-    ->condition('status', 1)
-    ->condition('field_page_notify_email', $email, '=');
-  $records = $query->execute();
-  foreach ($records as $key => $record) {
-    $node = \Drupal\node\Entity\Node::load($record);
-    $nodes[] = $node;
-  }
-  if ($nodes) {
-    return $nodes;
-  }
-  else {
-    return FALSE;
-  }
-}
-
-function page_notifications_user_delete_record($token) {
-  $num_deleted = \Drupal::entityQuery("node")
-    ->accessCheck(FALSE)
-    ->condition("type", "page_notify_subscriptions")
-    ->condition("field_page_notify_token", $token)
-    ->accessCheck(FALSE)
-    ->execute();
-  $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-  $entities = $storage_handler->loadMultiple($num_deleted);
-  $storage_handler->delete($entities);
-}
-
-function page_notifications_user_delete_all_records($user_token) {
-  $num_deleted = \Drupal::entityQuery("node")
-    ->accessCheck(FALSE)
-    ->condition("type", "page_notify_subscriptions")
-    ->condition("field_page_notify_token_user_id", $user_token)
-    ->accessCheck(FALSE)
-    ->execute();
-  $storage_handler = \Drupal::entityTypeManager()->getStorage("node");
-  $entities = $storage_handler->loadMultiple($num_deleted);
-  $storage_handler->delete($entities);
-}
diff --git a/src/LoadDataBaseInfo.php b/src/LoadDataBaseInfo.php
deleted file mode 100644
index a4f009d..0000000
--- a/src/LoadDataBaseInfo.php
+++ /dev/null
@@ -1,321 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications;
-
-/**
- * Class LoadDataBaseInfo.
- *
- * @package Drupal\page_notifications\src
- */
-
-class LoadDataBaseInfo
-{
-
-  public function get_notify_email_template()
-  {
-    $sql = "SELECT * FROM page_notify_email_template ORDER BY template_id DESC LIMIT 1";
-    $result = \Drupal::database()->query($sql);
-    $template = [];
-    if ($result) {
-      while ($row = $result->fetchAssoc()) {
-        $template = [
-          'template_id' => $row['template_id'],
-          'body' => $row['body'],
-          'from_email' => $row['from_email'],
-          'checkbox_field' => $row['checkbox_field'],
-          'notes_field' => $row['notes_field'],
-          'node_timestamp' => $row['node_timestamp'],
-          'verification_email_subject' => $row['verification_email_subject'],
-          'verification_email_text' => $row['verification_email_text'],
-          'confirmation_email_subject' => $row['confirmation_email_subject'],
-          'confirmation_email_text' => $row['confirmation_email_text'],
-          'sent_verify_web_page_message' => $row['sent_verify_web_page_message'],
-          'record_exist_verify_web_page_message' => $row['record_exist_verify_web_page_message'],
-          'error_web_page_message' => $row['error_web_page_message'],
-          'subscription_not_available_web_page_message' => $row['subscription_not_available_web_page_message'],
-          'confirmation_web_page_message' => $row['confirmation_web_page_message'],
-          'general_email_template_subject' => $row['general_email_template_subject'],
-          'general_email_template' => $row['general_email_template'],
-        ];
-      }
-      return $template;
-    } else {
-      return FALSE;
-    }
-  }
-
-  public function get_notify_settings()
-  {
-    $sql = "SELECT * FROM page_notify_settings ORDER BY page_notify_id DESC LIMIT 1";
-    $result = \Drupal::database()->query($sql);
-    $settings = [];
-    if ($result) {
-      while ($row = $result->fetchAssoc()) {
-        $settings = [
-          'page_notify_id' => $row['page_notify_id'],
-          'page_notify_recaptcha' => $row['page_notify_recaptcha'],
-          'page_notify_captcha' => $row['page_notify_captcha'],
-          'page_notify_subscribers_count' => $row['page_notify_subscribers_count'],
-          'enable_message_subscription_not_available' => $row['enable_message_subscription_not_available'],
-          'page_notify_settings_enable_content_type' => $row['page_notify_settings_enable_content_type'],
-          'page_notify_settings_enable_view' => $row['page_notify_settings_enable_view'],
-        ];
-      }
-      return $settings;
-    } else {
-      return FALSE;
-    }
-  }
-
-  public function page_notifications_process_tokens($text, $replacements = null)
-  {
-    $find = array(
-      '[notify_user_name]',
-      '[notify_user_email]',
-      '[notify_verify_url]',
-      '[notify_subscribe_url]',
-      '[notify_unsubscribe_url]',
-      '[notify_user_subscribtions]',
-      '[notify_node_title]',
-      '[notify_node_url]',
-      '[notify_notes]',
-    );
-    if (!is_null($replacements)) {
-      $replace = $replacements;
-    } else {
-      $replace = array(
-        '',
-        '',
-        '',
-        '',
-        '',
-        '',
-        '',
-        '',
-        ''
-      );
-    }
-
-    $new_text = str_replace($find, $replace, $text);
-    return $new_text;
-  }
-
-  public function pageNotifyGetUserToken($email_notify)
-  {
-    $record = current(
-      \Drupal::entityTypeManager()->getStorage('node')
-        ->loadByProperties([
-          'field_page_notify_email' => $email_notify,
-        ])
-    );
-    if ($record && !is_null($record)) {
-      $field_token_notify_user_id = $record->get("field_page_notify_token_user_id")->getValue();
-      $record = [
-        'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'],
-      ];
-      return $record;
-    } else {
-      return FALSE;
-    }
-  }
-
-  public function checkIfUserHasSubscription($email_notify)
-  {
-    $record = current(
-      \Drupal::entityTypeManager()->getStorage('node')
-        ->loadByProperties([
-          'field_page_notify_email' => $email_notify,
-        ])
-    );
-    if ($record && !is_null($record)) {
-      $field_token_notify_user_id = $record->get("field_page_notify_token_user_id")->getValue();
-      $user_record = [
-        'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'],
-      ];
-      return TRUE;
-    } else {
-      return FALSE;
-    }
-  }
-
-  public function verifyByTokenAndEmail($email, $subscription_token)
-  {
-    $record = current(
-      \Drupal::entityTypeManager()->getStorage('node')
-        ->loadByProperties([
-          'field_page_notify_email' => $email,
-          'field_page_notify_token' => $subscription_token,
-        ])
-    );
-    if ($record && $record->isPublished() == true) {
-      return true;
-    } else {
-      return FALSE;
-    }
-  }
-
-  public function checkIfRecordExistNode($email, $node)
-  {
-    $node_pieces = explode("-", $node);
-    $node_id = $node_pieces[0];
-    $node_entity_type = $node_pieces[1];
-
-    $query = current(
-      \Drupal::entityTypeManager()->getStorage('node')
-        ->loadByProperties([
-          'field_page_notify_email' => $email,
-          'field_page_notify_node_id' => $node_id
-        ])
-    );
-
-
-    if ($query && $query->isPublished() == true) {
-      $field_token_notify_user_id = $query->get("field_page_notify_token_user_id")->getValue();
-      $field_email_notify = $query->get("field_page_notify_email")->getValue();
-      $field_token_notify = $query->get("field_page_notify_token")->getValue();
-      $field_node_id_notify = $query->get("field_page_notify_node_id")->getValue();
-      $records = [
-        'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'],
-        'field_page_notify_email' => $field_email_notify[0]['value'],
-        'field_page_notify_token' => $field_token_notify[0]['value'],
-        'field_page_notify_node_id' => $field_node_id_notify[0]['value'],
-      ];
-
-      $field_page_notify_token_pieces = explode("-", $records['field_page_notify_token']);
-      $field_page_notify_node_id_pieces = explode("-", $records['field_page_notify_node_id']);
-
-      if ($field_page_notify_token_pieces[1] && $field_page_notify_token_pieces[1] == $node_entity_type && $field_page_notify_node_id_pieces[0] == $node_id) {
-        return $records;
-      } elseif (!$field_page_notify_token_pieces[1] && $field_page_notify_node_id_pieces[0] == $node_id) {
-        return $records;
-      } else {
-        return FALSE;
-      }
-    } else {
-      return FALSE;
-    }
-  }
-  public function checkIfRecordExistByToken($email, $token)
-  {
-    $record = current(
-      \Drupal::entityTypeManager()->getStorage('node')
-        ->loadByProperties([
-          'field_page_notify_email' => $email,
-          'field_page_notify_token' => $token
-        ])
-    );
-    if ($record && $record->isPublished() == true) {
-      $field_token_notify_user_id = $record->get("field_page_notify_token_user_id")->getValue();
-      $field_email_notify = $record->get("field_page_notify_email")->getValue();
-      $field_token_notify = $record->get("field_page_notify_token")->getValue();
-      $field_node_id_notify = $record->get("field_page_notify_node_id")->getValue();
-      $user_record = [
-        'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'],
-        'field_page_notify_email' => $field_email_notify[0]['value'],
-        'field_page_notify_token' => $field_token_notify[0]['value'],
-        'field_page_notify_node_id' => $field_node_id_notify[0]['value'],
-      ];
-      return $user_record;
-    } else {
-      return FALSE;
-    }
-  }
-
-  public function getAllUserRecords($user_token, $email)
-  {
-    $query = \Drupal::entityQuery('node')
-      ->accessCheck(FALSE)
-      ->condition('status', 1)
-      ->condition('field_page_notify_email', $email, '=')
-      ->condition('field_page_notify_token_user_id', $user_token, '=');
-    $records = $query->execute();
-    if ($records && !is_null($records)) {
-      $user_records = [];
-      foreach ($records as $record) {
-        $node_record = \Drupal\node\Entity\Node::load($record);
-        if ($node_record && $node_record->isPublished() == true) {
-          $field_token_notify_user_id = $node_record->get("field_page_notify_token_user_id")->getValue();
-          $field_email_notify = $node_record->get("field_page_notify_email")->getValue();
-          $field_token_notify = $node_record->get("field_page_notify_token")->getValue();
-          $field_node_id_notify = $node_record->get("field_page_notify_node_id")->getValue();
-          $user_record = [
-            'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'],
-            'field_page_notify_email' => $field_email_notify[0]['value'],
-            'field_page_notify_tokeny' => $field_token_notify[0]['value'],
-            'field_page_notify_node_id' => $field_node_id_notify[0]['value'],
-          ];
-          array_push($user_records, $user_record);
-        }
-      }
-      return $user_records;
-    } else {
-      return FALSE;
-    }
-  }
-
-  public function getAllNodeSubscription($node)
-  {
-    $query = \Drupal::entityQuery('node')
-      ->accessCheck(FALSE)
-      ->condition('type', 'page_notify_subscriptions')
-      ->condition('status', 1)
-      ->condition('field_page_notify_node_id', $node, '=');
-    $records = $query->execute();
-
-    if ($records && !is_null($records)) {
-      $node_subscriptions = [];
-      foreach ($records as $record) {
-        $node_record = \Drupal\node\Entity\Node::load($record);
-
-        if ($node_record) {
-          $field_token_notify_user_id = $node_record->get("field_page_notify_token_user_id")->getValue();
-          $field_email_notify = $node_record->get("field_page_notify_email")->getValue();
-          $field_token_notify = $node_record->get("field_page_notify_token")->getValue();
-          $field_node_id_notify = $node_record->get("field_page_notify_node_id")->getValue();
-          $node_record = [
-            'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'],
-            'field_page_notify_email' => $field_email_notify[0]['value'],
-            'field_page_notify_token' => $field_token_notify[0]['value'],
-            'field_page_notify_node_id' => $field_node_id_notify[0]['value'],
-            'subscription_node_id' => $node_record->id(),
-          ];
-          array_push($node_subscriptions, $node_record);
-        }
-      }
-
-      return $node_subscriptions;
-    } else {
-      return FALSE;
-    }
-  }
-
-  public function getCurrentPageInfo()
-  {
-    $node = \Drupal::routeMatch()->getParameter('node');
-    if ($node instanceof \Drupal\node\NodeInterface) {
-      $nid = $node->id();
-      $pageinfo = [
-        'current_node' => $nid,
-        'current_path' => '',
-      ];
-    } else {
-      $curr_path = \Drupal::service('path.current')->getPath();
-      $pageinfo = [
-        'current_node' => '',
-        'current_path' => $curr_path,
-      ];
-    }
-    return $pageinfo;
-  }
-
-  public function page_notifications_generateRandom_user_token($length = 6)
-  {
-    $numbers = '0123456789';
-    $numbersLength = strlen($numbers);
-    $randomNumbers = '';
-    for ($i = 0; $i < $length; $i++) {
-      $randomNumbers .= $numbers[rand(0, $numbersLength - 1)];
-    }
-    return $randomNumbers;
-  }
-}
diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php
new file mode 100644
index 0000000..9f26710
--- /dev/null
+++ b/src/Mail/PageNotificationsMailHandler.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Drupal\page_notifications\Mail;
+
+use Drupal\Core\Mail\MailManagerInterface;
+use Drupal\Core\Mail\MailFormatHelper;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\token\TokenInterface;
+/**
+ * Handles mail formatting for page notifications.
+ */
+class PageNotificationsMailHandler {
+
+  use StringTranslationTrait;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The token service.
+   *
+   * @var \Drupal\token\TokenServiceInterface
+   */
+  protected $tokenService;
+
+  /**
+   * Constructs a new PageNotificationsMailHandler.
+   */
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    RendererInterface $renderer,
+    TokenInterface $tokenService,
+    TranslationInterface $translation
+  ) {
+    $this->configFactory = $config_factory;
+    $this->renderer = $renderer;
+    $this->token = $tokenService;
+    $this->setStringTranslation($translation);
+  }
+
+  /**
+   * Implements callback_mail().
+   */
+  public function mail($key, &$message, $params) {
+    $config = $this->configFactory->get('page_notifications.settings');
+
+    switch ($key) {
+      case 'verification':
+        $this->buildVerificationEmail($message, $params);
+        break;
+
+      case 'notification':
+        $this->buildNotificationEmail($message, $params);
+        break;
+    }
+
+    // Ensure proper line endings for emails.
+    $message['body'] = array_map(function ($line) {
+      return MailFormatHelper::wrapMail($line);
+    }, $message['body']);
+  }
+
+  /**
+   * Builds a verification email.
+   *
+   * @param array $message
+   *   The message array.
+   * @param array $params
+   *   The message parameters.
+   */
+  protected function buildVerificationEmail(array &$message, array $params) {
+    $config = $this->configFactory->get('page_notifications.settings');
+    $subscription = $params['subscription'];
+    $entity = $params['entity'];
+
+    $token_data = [
+      'subscription' => $subscription,
+      'node' => $entity,
+    ];
+
+    $subject = $config->get('email_templates.verification_subject');
+    $body = $config->get('email_templates.verification_body');
+
+    $message['subject'] = $this->token->replace($subject, $token_data);
+    $message['body'][] = $this->token->replace($body, $token_data);
+  }
+
+  /**
+   * Builds a notification email.
+   *
+   * @param array $message
+   *   The message array.
+   * @param array $params
+   *   The message parameters.
+   */
+  protected function buildNotificationEmail(array &$message, array $params) {
+    $config = $this->configFactory->get('page_notifications.settings');
+    $subscription = $params['subscription'];
+    $entity = $params['entity'];
+
+    $token_data = [
+      'subscription' => $subscription,
+      'node' => $entity,
+    ];
+
+    $subject = $config->get('email_templates.notification_subject');
+    $body = $config->get('email_templates.notification_body');
+
+    $message['subject'] = $this->token->replace($subject, $token_data);
+    $message['body'][] = $this->token->replace($body, $token_data);
+  }
+
+}
\ No newline at end of file
diff --git a/src/Plugin/Block/PageNotificationsBlock.php b/src/Plugin/Block/PageNotificationsBlock.php
deleted file mode 100644
index cbb10dd..0000000
--- a/src/Plugin/Block/PageNotificationsBlock.php
+++ /dev/null
@@ -1,130 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Plugin\Block;
-
-use Drupal\Core\Block\BlockBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\Core\Form\FormBuilderInterface;
-use Drupal\page_notifications\Form\PageNotificationsBlockForm;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Block\BlockPluginInterface;
-use Drupal\filter\Element\ProcessedText;
-use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Core\Render\Markup;
-
-/**
- *
- * Provides a 'Page Notifications' block.
- *
- * @Block(
- *   id = "page_notifications",
- *   admin_label = @Translation("Page Notifications"),
- *   category = @Translation("Page Notifications")
- * )
- */
-
-class PageNotificationsBlock extends BlockBase implements ContainerFactoryPluginInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function defaultConfiguration() {
-    return [
-      'markup' => [
-        'format' => 'full_html',
-        'value' => '',
-      ],
-    ] + parent::defaultConfiguration();
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * This method defines form elements for custom block configuration. Standard
-   * block configuration fields are added by BlockBase::buildConfigurationForm()
-   * (block title and title visibility) and BlockFormController::form() (block
-   * visibility settings).
-   *
-   * @see \Drupal\block\BlockBase::buildConfigurationForm()
-   * @see \Drupal\block\BlockFormController::form()
-   */
-  public function blockForm($form, FormStateInterface $form_state) {
-    $blockManager = \Drupal::service('plugin.manager.block');
-    $contextRepository = \Drupal::service('context.repository');
-    $definitions = $blockManager->getDefinitionsForContexts(
-        $contextRepository->getAvailableContexts()
-    );
-    $buildInfo = $form_state->getBuildInfo();
-    return $form;
-  }
-
-
-  /**
-   * {@inheritdoc}
-   *
-   * This method processes the blockForm() form fields when the block
-   * configuration form is submitted.
-   *
-   * The blockValidate() method can be used to validate the form submission.
-   */
-  public function blockSubmit($form, FormStateInterface $form_state) {
-
-  }
-
-
-  /**
-   * Form builder service.
-   *
-   * @var \Drupal\Core\Form\FormBuilderInterface
-   */
-  protected $formBuilder;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
-    $this->formBuilder = $form_builder;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      $container->get('form_builder')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function build() {
-    /*$config = $this->getConfiguration();
-    $preload_template = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template();
-    $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings();
-    $route_match = \Drupal::routeMatch();
-
-    $output['body'] = [
-      '#type' => 'processed_text',
-      '#text' => $this->t($preload_template['subscription_not_available_web_page_message']),
-      "#processed" => true,
-      '#format' => $preload_template['subscription_not_available_web_page_message'],
-    ];*/
-
-    $output['form'] = $this->formBuilder->getForm(PageNotificationsBlockForm::class);
-    return $output;
-  }
-
-  /**
-     * {@inheritdoc}
-     */
-    public function getCacheMaxAge() {
-        return 0;
-    }
-
-}
diff --git a/src/Plugin/Block/SubscriptionBlock.php b/src/Plugin/Block/SubscriptionBlock.php
new file mode 100644
index 0000000..3613d97
--- /dev/null
+++ b/src/Plugin/Block/SubscriptionBlock.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace Drupal\page_notifications\Plugin\Block;
+
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+
+/**
+ * Provides a subscription block.
+ *
+ * @Block(
+ *   id = "page_notifications_subscription",
+ *   admin_label = @Translation("Page Notifications Subscription"),
+ *   category = @Translation("Page Notifications")
+ * )
+ */
+class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The form builder.
+   *
+   * @var \Drupal\Core\Form\FormBuilderInterface
+   */
+  protected $formBuilder;
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * Constructs a new SubscriptionBlock instance.
+   *
+   * @param array $configuration
+   *   The plugin configuration.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
+   *   The form builder.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger factory.
+   */
+  public function __construct(
+    array $configuration,
+    $plugin_id,
+    $plugin_definition,
+    EntityTypeManagerInterface $entity_type_manager,
+    FormBuilderInterface $form_builder,
+    LoggerChannelFactoryInterface $logger_factory
+  ) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->formBuilder = $form_builder;
+    $this->loggerFactory = $logger_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('form_builder'),
+      $container->get('logger.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    try {
+      $node = \Drupal::routeMatch()->getParameter('node');
+      if (!$node) {
+        return [];
+      }
+
+      return $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node);
+    }
+    catch (\Exception $e) {
+      $this->loggerFactory->get('page_notifications')->error(
+        'Error building subscription block: @message',
+        ['@message' => $e->getMessage()]
+      );
+      return [
+        '#markup' => $this->t('The subscription form is temporarily unavailable.'),
+      ];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function blockAccess(AccountInterface $account) {
+    try {
+      $node = \Drupal::routeMatch()->getParameter('node');
+      if (!$node) {
+        return AccessResult::forbidden();
+      }
+
+      return AccessResult::allowedIfHasPermission($account, 'access content');
+    }
+    catch (\Exception $e) {
+      $this->loggerFactory->get('page_notifications')->error(
+        'Error checking block access: @message',
+        ['@message' => $e->getMessage()]
+      );
+      return AccessResult::forbidden();
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/src/Plugin/QueueWorker/NotificationQueue.php b/src/Plugin/QueueWorker/NotificationQueue.php
new file mode 100644
index 0000000..bd8d099
--- /dev/null
+++ b/src/Plugin/QueueWorker/NotificationQueue.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\page_notifications\Plugin\QueueWorker;
+
+use Drupal\Core\Queue\QueueWorkerBase;
+
+/**
+ * Process notification queue.
+ *
+ * @QueueWorker(
+ *   id = "page_notifications_queue",
+ *   title = @Translation("Page Notifications Queue"),
+ *   cron = {"time" = 60}
+ * )
+ */
+class NotificationQueue extends QueueWorkerBase {
+  // TODO: Implement queue worker for processing notifications
+}
\ No newline at end of file
diff --git a/src/Routing/PageNotificationsDynamicRoutes.php b/src/Routing/PageNotificationsDynamicRoutes.php
deleted file mode 100644
index df9a170..0000000
--- a/src/Routing/PageNotificationsDynamicRoutes.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Routing;
-
-use Symfony\Component\Routing\Route;
-
-/**
- * Defines dynamic routes for our tab menu items.
- *
- * These routes support the links created in page_notifications.links.task.yml.
- *
- * @see page_notifications.links.task.yml
- * @see https://www.drupal.org/docs/8/api/routing-system/providing-dynamic-routes
- */
-class PageNotificationsDynamicRoutes {
-
-  /**
-   * Returns an array of route objects.
-   *
-   * @return \Symfony\Component\Routing\Route[]
-   *   An array of route objects.
-   */
-  public function routes() {
-    $routes = [];
-
-    $tabs = [
-      'tabs' => 'General configuration',
-      'tabs/second' => 'Migrate Subscribtions',
-      'tabs/third' => 'Migrate Subscribtions Content Type',
-      //'tabs/fourth' => 'Page Notifications - Node Subscribtions List',
-      'tabs/default/second' => 'Messages configuration',
-      //'tabs/default/third' => 'Third',
-    ];
-
-    foreach ($tabs as $path => $title) {
-      $machine_name = 'page_notifications.' . str_replace('/', '_', $path);
-      $routes[$machine_name] = new Route(
-
-        '/admin/page-notifications/' . $path,
-        [
-          '_controller' => '\Drupal\page_notifications\Controller\PageNotificationsController::tabsPage',
-          '_title' => $title,
-          'path' => $path,
-          'title' => $title,
-        ],
-        [
-          '_access' => 'TRUE',
-        ]
-      );
-    }
-
-    return $routes;
-  }
-
-}
diff --git a/src/Routing/RouteSubscriber.php b/src/Routing/RouteSubscriber.php
deleted file mode 100644
index 38a9497..0000000
--- a/src/Routing/RouteSubscriber.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Routing;
-
-use Drupal\Core\Routing\RouteSubscriberBase;
-use Symfony\Component\Routing\RouteCollection;
-
-/**
- * Listens to the dynamic route events.
- *
- * The \Drupal\Core\Routing\RouteSubscriberBase class contains an event
- * listener that listens to this event. We alter existing routes by
- * implementing the alterRoutes(RouteCollection $collection) method of
- * this class.
- *
- * @see https://www.drupal.org/docs/8/api/routing-system/altering-existing-routes-and-adding-new-routes-based-on-dynamic-ones
- */
-class RouteSubscriber extends RouteSubscriberBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function alterRoutes(RouteCollection $collection) {
-    $route = $collection->get('page_notifications.path_override');
-    $route->setPath('/admin/page-notifications/menu-altered-path');
-    $route->setDefault('_title', 'Menu item altered by RouteSubscriber::alterRoutes');
-  }
-
-}
diff --git a/src/Service/NotificationManager.php b/src/Service/NotificationManager.php
new file mode 100644
index 0000000..71c2e21
--- /dev/null
+++ b/src/Service/NotificationManager.php
@@ -0,0 +1,329 @@
+<?php
+
+namespace Drupal\page_notifications\Service;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Mail\MailManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\Core\Queue\QueueFactory;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Messenger\MessengerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+
+/**
+ * Service for handling page notification operations.
+ */
+class NotificationManager implements NotificationManagerInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The mail manager.
+   *
+   * @var \Drupal\Core\Mail\MailManagerInterface
+   */
+  protected $mailManager;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The queue factory.
+   *
+   * @var \Drupal\Core\Queue\QueueFactory
+   */
+  protected $queue;
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * The messenger service.
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+/**
+   * Constructs a new NotificationManager.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
+   *   The mail manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Queue\QueueFactory $queue_factory
+   *   The queue factory.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger factory.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The time service.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
+   *   The string translation service.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *  The messenger service.
+   */
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    MailManagerInterface $mail_manager,
+    EntityTypeManagerInterface $entity_type_manager,
+    QueueFactory $queue_factory,
+    LoggerChannelFactoryInterface $logger_factory,
+    EventDispatcherInterface $event_dispatcher,
+    TimeInterface $time,
+    TranslationInterface $translation,
+    MessengerInterface $messenger
+  ) {
+    $this->configFactory = $config_factory;
+    $this->mailManager = $mail_manager;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->queueFactory = $queue_factory;
+    $this->loggerFactory = $logger_factory;
+    $this->eventDispatcher = $event_dispatcher;
+    $this->time = $time;
+    $this->setStringTranslation($translation);
+    $this->messenger = $messenger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createSubscription(string $email, EntityInterface $entity, ?string $langcode = null) {
+    try {
+      $token = $this->generateToken();
+
+      /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
+      $subscription = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->create([
+          'email' => $email,
+          'subscribed_entity_id' => $entity->id(),
+          'subscribed_entity_type' => $entity->getEntityTypeId(),
+          'token' => $token,
+          'status' => !$this->requiresVerification(),
+        ]);
+
+      $subscription->save();
+
+      if ($this->requiresVerification()) {
+        $this->sendVerificationEmail($subscription);
+      }
+
+      return $subscription;
+    }
+    catch (\Exception $e) {
+      $this->loggerFactory->get('page_notifications')
+        ->error('Failed to create subscription: @message', ['@message' => $e->getMessage()]);
+      throw $e;
+    }
+  }
+
+  /**
+   * Route controller for subscription verification.
+   */
+  public function verifySubscriptionRoute($token) {
+    if ($this->verifySubscription($token)) {
+      $this->messenger->addStatus(t('Subscription verified successfully.'));
+    }
+    else {
+      $this->messenger->addError(t('Invalid verification token.'));
+    }
+    return new RedirectResponse('/');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function verifySubscription(string $token) {
+    try {
+      $subscriptions = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->loadByProperties(['token' => $token]);
+
+      if (!$subscriptions) {
+        return FALSE;
+      }
+
+      /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
+      $subscription = reset($subscriptions);
+
+      if ($this->isTokenExpired($subscription)) {
+        return FALSE;
+      }
+
+      $subscription->setActive(TRUE);
+      $subscription->save();
+
+      return TRUE;
+    }
+    catch (\Exception $e) {
+      $this->loggerFactory->get('page_notifications')
+        ->error('Failed to verify subscription: @message', ['@message' => $e->getMessage()]);
+      return FALSE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function notifySubscribers(EntityInterface $entity) {
+    try {
+      $subscriptions = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->loadByProperties([
+          'subscribed_entity_id' => $entity->id(),
+          'subscribed_entity_type' => $entity->getEntityTypeId(),
+          'status' => TRUE,
+        ]);
+
+      foreach ($subscriptions as $subscription) {
+        $this->queueNotification($subscription, $entity);
+      }
+    }
+    catch (\Exception $e) {
+      $this->loggerFactory->get('page_notifications')
+        ->error('Failed to notify subscribers: @message', ['@message' => $e->getMessage()]);
+      throw $e;
+    }
+  }
+
+  /**
+   * Retrieves the queue for processing notifications.
+   *
+   * @return \Drupal\Core\Queue\QueueInterface
+   *   The queue.
+   */
+  protected function getQueue() {
+    return $this->queueFactory->get('page_notifications_queue');
+  }
+
+  /**
+   * Queues a notification for processing.
+   *
+   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
+   *   The subscription entity.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity that was updated.
+   */
+  protected function queueNotification($subscription, EntityInterface $entity) {
+    $queue = $this->getQueue();
+    $queue->createItem([
+      'subscription_id' => $subscription->id(),
+      'entity_id' => $entity->id(),
+      'entity_type' => $entity->getEntityTypeId(),
+    ]);
+  }
+
+  /**
+   * Sends a verification email to the subscriber.
+   *
+   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
+   *   The subscription entity.
+   */
+  protected function sendVerificationEmail($subscription) {
+    $config = $this->configFactory->get('page_notifications.settings');
+    $entity = $this->entityTypeManager
+      ->getStorage($subscription->getSubscribedEntityType())
+      ->load($subscription->getSubscribedEntityId());
+
+    $params = [
+      'subscription' => $subscription,
+      'entity' => $entity,
+      'verify_url' => Url::fromRoute('page_notifications.subscription.verify', [
+        'token' => $subscription->getToken(),
+      ])->setAbsolute(TRUE)
+        ->toString(),
+    ];
+
+    $this->mailManager->mail(
+      'page_notifications',
+      'verification',
+      $subscription->getEmail(),
+      $subscription->getLanguageCode(),
+      $params,
+      $config->get('notification_settings.from_email')
+    );
+  }
+
+  /**
+   * Generates a unique token for subscription verification.
+   *
+   * @return string
+   *   The generated token.
+   */
+  protected function generateToken() {
+    return bin2hex(random_bytes(32));
+  }
+
+  /**
+   * Checks if verification is required based on configuration.
+   *
+   * @return bool
+   *   TRUE if verification is required, FALSE otherwise.
+   */
+  protected function requiresVerification() {
+    return $this->configFactory
+      ->get('page_notifications.settings')
+      ->get('security.require_verification') ?? TRUE;
+  }
+
+  /**
+   * Checks if a subscription token has expired.
+   *
+   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
+   *   The subscription entity.
+   *
+   * @return bool
+   *   TRUE if the token has expired, FALSE otherwise.
+   */
+  protected function isTokenExpired($subscription) {
+    if ($subscription->isActive()) {
+      return FALSE;
+    }
+
+    $config = $this->configFactory->get('page_notifications.settings');
+    $expiration_hours = $config->get('notification_settings.token_expiration') ?? 48;
+    $expiration_timestamp = $subscription->getCreatedTime() + ($expiration_hours * 3600);
+
+    return $this->time->getRequestTime() > $expiration_timestamp;
+  }
+
+}
\ No newline at end of file
diff --git a/src/Service/NotificationManagerInterface.php b/src/Service/NotificationManagerInterface.php
new file mode 100644
index 0000000..e5bf5a5
--- /dev/null
+++ b/src/Service/NotificationManagerInterface.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\page_notifications\Service;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Language\LanguageInterface;
+
+/**
+ * Interface for notification management service.
+ */
+interface NotificationManagerInterface {
+
+  /**
+   * Creates a new subscription.
+   *
+   * @param string $email
+   *   The subscriber's email address.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being subscribed to.
+   * @param string|null $langcode
+   *   The language code for the subscription. Defaults to site's default language.
+   *
+   * @return \Drupal\page_notifications\Entity\SubscriptionInterface
+   *   The created subscription entity.
+   *
+   * @throws \Exception
+   *   If the subscription cannot be created.
+   */
+  public function createSubscription(string $email, EntityInterface $entity, string $langcode = LanguageInterface::LANGCODE_DEFAULT);
+
+  /**
+   * Verifies a subscription using a token.
+   *
+   * @param string $token
+   *   The verification token.
+   *
+   * @return bool
+   *   TRUE if verification was successful, FALSE otherwise.
+   */
+  public function verifySubscription(string $token);
+
+  /**
+   * Notifies subscribers about updates to an entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity that was updated.
+   *
+   * @throws \Exception
+   *   If notifications cannot be sent.
+   */
+  public function notifySubscribers(EntityInterface $entity);
+
+}
\ No newline at end of file
diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php
new file mode 100644
index 0000000..3ea9222
--- /dev/null
+++ b/src/Token/SubscriptionToken.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\page_notifications\Token;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Url;
+
+/**
+ * Implements hook_token_info() and hook_tokens().
+ */
+class SubscriptionToken {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_token_info().
+   */
+  public function hookTokenInfo() {
+    $types['subscription'] = [
+      'name' => $this->t('Subscription'),
+      'description' => $this->t('Tokens related to page notification subscriptions'),
+      'needs-data' => 'subscription',
+    ];
+
+    $tokens['subscription']['verify-url'] = [
+      'name' => $this->t('Verification URL'),
+      'description' => $this->t('The URL to verify the subscription'),
+    ];
+
+    $tokens['subscription']['unsubscribe-url'] = [
+      'name' => $this->t('Unsubscribe URL'),
+      'description' => $this->t('The URL to unsubscribe from notifications'),
+    ];
+
+    return [
+      'types' => $types,
+      'tokens' => $tokens,
+    ];
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  public function hookTokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+    $replacements = [];
+
+    if ($type == 'subscription' && !empty($data['subscription'])) {
+      $subscription = $data['subscription'];
+
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'verify-url':
+            $replacements[$original] = Url::fromRoute('page_notifications.subscription.verify',
+              ['token' => $subscription->getToken()],
+              ['absolute' => TRUE]
+            )->toString();
+            break;
+
+          case 'unsubscribe-url':
+            $replacements[$original] = Url::fromRoute('entity.subscription.delete_form',
+              ['token' => $subscription->getToken()],
+              ['absolute' => TRUE]
+            )->toString();
+            break;
+        }
+      }
+    }
+
+    return $replacements;
+  }
+}
\ No newline at end of file
diff --git a/src/src/Controller/VerificationController.php b/src/src/Controller/VerificationController.php
new file mode 100644
index 0000000..8708d5b
--- /dev/null
+++ b/src/src/Controller/VerificationController.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\page_notifications\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\page_notifications\Service\NotificationManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Controller for handling subscription verification.
+ */
+class VerificationController extends ControllerBase {
+
+  /**
+   * The notification manager service.
+   *
+   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
+   */
+  protected $notificationManager;
+
+  /**
+   * Constructs a VerificationController object.
+   *
+   * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager
+   *   The notification manager service.
+   */
+  public function __construct(NotificationManagerInterface $notification_manager) {
+    $this->notificationManager = $notification_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('page_notifications.notification_manager')
+    );
+  }
+
+  /**
+   * Verifies a subscription token.
+   *
+   * @param string $token
+   *   The verification token.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   Redirects to the front page with a status message.
+   */
+  public function verify($token) {
+    if ($this->notificationManager->verifySubscription($token)) {
+      $this->messenger()->addStatus($this->t('Thank you! Your subscription has been verified.'));
+    }
+    else {
+      $this->messenger()->addError($this->t('Sorry, this verification link is invalid or has expired.'));
+    }
+
+    return new RedirectResponse('/');
+  }
+
+}
\ No newline at end of file
diff --git a/templates/description.html.twig b/templates/description.html.twig
deleted file mode 100644
index 54192ba..0000000
--- a/templates/description.html.twig
+++ /dev/null
@@ -1,34 +0,0 @@
-{#
-
-Description text for the Page Notifications.
-
-#}
-
-{% set custom_access = path('page_notifications.custom_access') %}
-{% set permissioned = path('page_notifications.permissioned') %}
-{% set route_only = path('page_notifications.route_only') %}
-{% set tabs = path('page_notifications.tabs') %}
-{% set use_url_arguments = path('page_notifications.use_url_arguments') %}
-{% set title_callbacks = path('page_notifications.title_callbacks') %}
-{% set placeholder_argument = path('page_notifications.placeholder_argument') %}
-{% set path_override = path('page_notifications.path_override') %}
-
-{% trans %}
-
-<p>This page is displayed by the simplest (and base) menu example. Note that
-the title of the page is the same as the link title. There are a number of
-examples here, from the most basic (like this one) to extravagant mappings of
-loaded placeholder arguments. Enjoy!</p>
-
-<ul>
-    <li><a href={{ custom_access }}>Custom Access Notifications</a></li>
-    <li><a href={{ permissioned }}>Permissioned Notifications</a></li>
-    <li><a href={{ route_only }}>Route only Notifications</a></li>
-    <li><a href={{ tabs }}>Tabs</a></li>
-    <li><a href={{ use_url_arguments }}>URL Arguments</a></li>
-    <li><a href={{ title_callbacks }}>Dynamic title</a></li>
-    <li><a href={{ placeholder_argument }}>Placeholder Arguments</a></li>
-    <li><a href={{ path_override }}>Path Override</a></li>
-</ul>
-
-{% endtrans %}
-- 
GitLab


From c4e574271a62c64ab7b4eea70222f6a46fa708f6 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Mon, 30 Dec 2024 12:41:38 -0500
Subject: [PATCH 02/49] Prevent duplicate subscriptions

---
 src/Service/NotificationManager.php | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/Service/NotificationManager.php b/src/Service/NotificationManager.php
index 71c2e21..e1f795d 100644
--- a/src/Service/NotificationManager.php
+++ b/src/Service/NotificationManager.php
@@ -128,6 +128,26 @@ class NotificationManager implements NotificationManagerInterface {
    */
   public function createSubscription(string $email, EntityInterface $entity, ?string $langcode = null) {
     try {
+
+      // Check for existing subscription.
+      $existing_subscriptions = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->loadByProperties([
+          'email' => $email,
+          'subscribed_entity_id' => $entity->id(),
+          'subscribed_entity_type' => $entity->getEntityTypeId(),
+        ]);
+
+      // If subscription already exists, return it.
+      if (!empty($existing_subscriptions)) {
+        $subscription = reset($existing_subscriptions);
+        // If subscription exists but isn't verified, resend verification email
+        if (!$subscription->isActive() && $this->requiresVerification()) {
+          $this->sendVerificationEmail($subscription);
+        }
+        return $subscription;
+      }
+
       $token = $this->generateToken();
 
       /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
-- 
GitLab


From 97e0e8cddb2b9fd76e36898a48b70f5a13348279 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Mon, 30 Dec 2024 14:28:47 -0500
Subject: [PATCH 03/49] Adding a view so people can customize the subscriptions
 list

---
 ...s.view.page_notification_subscriptions.yml | 459 ++++++++++++++++++
 page_notifications.info.yml                   |  21 +-
 page_notifications.permissions.yml            |   6 +-
 page_notifications.routing.yml                |   8 -
 src/Entity/Subscription.php                   |   3 +-
 src/Entity/SubscriptionViewsData.php          | 110 +++++
 6 files changed, 577 insertions(+), 30 deletions(-)
 create mode 100644 config/install/views.view.page_notification_subscriptions.yml
 create mode 100644 src/Entity/SubscriptionViewsData.php

diff --git a/config/install/views.view.page_notification_subscriptions.yml b/config/install/views.view.page_notification_subscriptions.yml
new file mode 100644
index 0000000..4de5cc2
--- /dev/null
+++ b/config/install/views.view.page_notification_subscriptions.yml
@@ -0,0 +1,459 @@
+uuid: 80c6ba5b-2a8e-4fe3-b834-2e7abe4607c9
+langcode: en
+status: true
+dependencies:
+  module:
+    - node
+    - page_notifications
+id: page_notification_subscriptions
+label: 'Page Notification Subscriptions'
+module: views
+description: 'Lists all page notification subscriptions'
+tag: ''
+base_table: page_notification_subscription
+base_field: id
+display:
+  default:
+    id: default
+    display_title: Default
+    display_plugin: default
+    position: 0
+    display_options:
+      title: 'Page Notification Subscriptions'
+      fields:
+        operations:
+          id: operations
+          table: page_notification_subscription
+          field: operations
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: null
+          entity_field: null
+          plugin_id: entity_operations
+          label: Operations
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          destination: false
+        email:
+          id: email
+          table: page_notification_subscription
+          field: email
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: email
+          plugin_id: standard
+          label: Email
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+        id:
+          id: id
+          table: page_notification_subscription
+          field: id
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: id
+          plugin_id: field
+          label: ID
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings:
+            thousand_separator: ''
+            prefix_suffix: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        status:
+          id: status
+          table: page_notification_subscription
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: status
+          plugin_id: boolean
+          label: Status
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          type: yes-no
+          type_custom_true: ''
+          type_custom_false: ''
+          not: false
+        subscribed_entity_id:
+          id: subscribed_entity_id
+          table: page_notification_subscription
+          field: subscribed_entity_id
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          plugin_id: numeric
+          label: 'Subscribed Entity ID'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          set_precision: false
+          precision: 0
+          decimal: .
+          separator: ''
+          format_plural: false
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        title:
+          id: title
+          table: node_field_data
+          field: title
+          relationship: subscribed_entity
+          group_type: group
+          admin_label: ''
+          entity_type: node
+          entity_field: title
+          plugin_id: field
+          label: Title
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+      pager:
+        type: mini
+        options:
+          offset: 0
+          pagination_heading_level: h4
+          items_per_page: 10
+          total_pages: null
+          id: 0
+          tags:
+            next: ››
+            previous: ‹‹
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      access:
+        type: none
+        options: {  }
+      cache:
+        type: tag
+        options: {  }
+      empty: {  }
+      sorts: {  }
+      arguments: {  }
+      filters: {  }
+      style:
+        type: table
+      row:
+        type: fields
+      query:
+        type: views_query
+        options:
+          query_comment: ''
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_tags: {  }
+      relationships:
+        subscribed_entity:
+          id: subscribed_entity
+          table: page_notification_subscription
+          field: subscribed_entity
+          relationship: none
+          group_type: group
+          admin_label: 'Subscribed Node'
+          entity_type: page_notification_subscription
+          plugin_id: standard
+          required: false
+      header: {  }
+      footer: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+      tags: {  }
+  page_1:
+    id: page_1
+    display_title: Page
+    display_plugin: page
+    position: 1
+    display_options:
+      display_extenders:
+        simple_sitemap_display_extender:
+          variants: {  }
+      path: admin/content/subscriptions
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+      tags: {  }
diff --git a/page_notifications.info.yml b/page_notifications.info.yml
index 9397d71..a2a6271 100644
--- a/page_notifications.info.yml
+++ b/page_notifications.info.yml
@@ -5,23 +5,4 @@ core_version_requirement: ^10
 configure: page_notifications.settings
 dependencies:
   - drupal:node
-
-# Information added by Drupal.org packaging script on 2020-11-18
-# version: '3.0.2'
-# project: 'page_notifications'
-# datestamp: 1605715872
-
-# Information added by Drupal.org packaging script on 2021-10-26
-# version: '2.0.2'
-# project: 'page_notifications'
-# datestamp: 1635249489
-
-# Information added by Drupal.org packaging script on 2023-08-07
-# version: '2.1.1'
-# project: 'page_notifications'
-# datestamp: 1691431686
-
-# Information added by Drupal.org packaging script on 2023-09-25
-version: '2.1.2'
-project: 'page_notifications'
-datestamp: 1695669145
+  - drupal:views
diff --git a/page_notifications.permissions.yml b/page_notifications.permissions.yml
index fee0522..8216ba5 100644
--- a/page_notifications.permissions.yml
+++ b/page_notifications.permissions.yml
@@ -17,4 +17,8 @@ edit page notification subscriptions:
 
 delete page notification subscriptions:
   title: 'Delete page notification subscriptions'
-  description: 'Delete existing page notification subscriptions.'
\ No newline at end of file
+  description: 'Delete existing page notification subscriptions.'
+
+view subscription list:
+  title: 'View subscription list'
+  description: 'Access the subscription list view'
\ No newline at end of file
diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index a0530a0..7e36ef5 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -6,14 +6,6 @@ page_notifications.settings:
   requirements:
     _permission: 'administer page notification subscriptions'
 
-page_notifications.subscription_list:
-  path: '/admin/content/subscriptions'
-  defaults:
-    _entity_list: 'page_notification_subscription'
-    _title: 'Page Notification Subscriptions'
-  requirements:
-    _permission: 'administer page notification subscriptions'
-
 page_notifications.subscription.verify:
   path: '/page-notifications/verify/{token}'
   defaults:
diff --git a/src/Entity/Subscription.php b/src/Entity/Subscription.php
index c8fe448..c369622 100644
--- a/src/Entity/Subscription.php
+++ b/src/Entity/Subscription.php
@@ -21,7 +21,8 @@ use Drupal\user\EntityOwnerTrait;
  *       "default" = "Drupal\page_notifications\Form\SubscriptionForm",
  *       "delete" = "Drupal\page_notifications\Form\SubscriptionDeleteForm"
  *     },
- *     "access" = "Drupal\page_notifications\Entity\SubscriptionAccessControlHandler",
+ *    "access" = "Drupal\page_notifications\Entity\SubscriptionAccessControlHandler",
+ *    "views_data" = "Drupal\page_notifications\Entity\SubscriptionViewsData",
  *   },
  *   base_table = "page_notification_subscription",
  *   admin_permission = "administer page notification subscriptions",
diff --git a/src/Entity/SubscriptionViewsData.php b/src/Entity/SubscriptionViewsData.php
new file mode 100644
index 0000000..8257cc9
--- /dev/null
+++ b/src/Entity/SubscriptionViewsData.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Drupal\page_notifications\Entity;
+
+use Drupal\views\EntityViewsData;
+
+/**
+ * Provides Views data for Page Notification Subscription entities.
+ */
+class SubscriptionViewsData extends EntityViewsData {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewsData() {
+    $data = parent::getViewsData();
+
+    // Base table definition
+    $data['page_notification_subscription']['table']['base'] = [
+      'field' => 'id',
+      'title' => $this->t('Page Notification Subscription'),
+      'help' => $this->t('Contains subscription information for page notifications.'),
+      'weight' => -10,
+    ];
+
+    // Define the relationship to nodes
+    $data['page_notification_subscription']['subscribed_entity'] = [
+      'title' => $this->t('Subscribed Node'),
+      'help' => $this->t('The node this subscription is associated with.'),
+      'relationship' => [
+        'base' => 'node_field_data',
+        'base field' => 'nid',
+        'field' => 'subscribed_entity_id',
+        'id' => 'standard',
+        'label' => $this->t('Subscribed Node'),
+      ],
+    ];
+
+    // Add specific field handlers
+    $data['page_notification_subscription']['subscribed_entity_id'] = [
+      'title' => $this->t('Subscribed Entity ID'),
+      'help' => $this->t('The ID of the entity being subscribed to.'),
+      'field' => [
+        'id' => 'numeric',
+      ],
+      'filter' => [
+        'id' => 'numeric',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+      'argument' => [
+        'id' => 'numeric',
+      ],
+    ];
+
+    // Status field
+    $data['page_notification_subscription']['status']['field'] = [
+      'title' => $this->t('Status'),
+      'help' => $this->t('The status of the subscription.'),
+      'id' => 'boolean',
+    ];
+    $data['page_notification_subscription']['status']['filter'] = [
+      'id' => 'boolean',
+      'label' => $this->t('Status'),
+      'type' => 'yes-no',
+    ];
+    $data['page_notification_subscription']['status']['sort'] = [
+      'id' => 'standard',
+    ];
+
+    // Email field
+    $data['page_notification_subscription']['email']['field'] = [
+      'title' => $this->t('Email'),
+      'help' => $this->t('The email address of the subscriber.'),
+      'id' => 'standard',
+    ];
+    $data['page_notification_subscription']['email']['filter'] = [
+      'id' => 'string',
+    ];
+    $data['page_notification_subscription']['email']['sort'] = [
+      'id' => 'standard',
+    ];
+
+    // Created timestamp
+    $data['page_notification_subscription']['created']['field'] = [
+      'title' => $this->t('Created'),
+      'help' => $this->t('When the subscription was created.'),
+      'id' => 'date',
+    ];
+    $data['page_notification_subscription']['created']['filter'] = [
+      'id' => 'date',
+    ];
+    $data['page_notification_subscription']['created']['sort'] = [
+      'id' => 'date',
+    ];
+
+    // Operations
+    $data['page_notification_subscription']['operations'] = [
+      'field' => [
+        'title' => $this->t('Operations'),
+        'help' => $this->t('Provides links to perform subscription operations.'),
+        'id' => 'entity_operations',
+      ],
+    ];
+
+    return $data;
+  }
+
+}
\ No newline at end of file
-- 
GitLab


From 15631bf0c4e39b024172c290ac37ca116dc777a6 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Tue, 7 Jan 2025 09:48:39 -0500
Subject: [PATCH 04/49] Add Queue worker to send emails, and cron to clean up
 expired tokens, still need to work on email tokens a bit like unsubscribe URL
 and note

---
 ...s.view.page_notification_subscriptions.yml |  54 ++++++
 page_notifications.module                     |  62 +++++++
 page_notifications.routing.yml                |   4 +-
 page_notifications.services.yml               |  20 ++-
 src/Entity/SubscriptionListBuilder.php        |   2 +-
 src/Mail/PageNotificationsMailHandler.php     |  18 +-
 src/Plugin/QueueWorker/NotificationQueue.php  | 155 +++++++++++++++++-
 src/Service/CronManager.php                   | 142 ++++++++++++++++
 src/Token/SubscriptionToken.php               |   4 +-
 9 files changed, 446 insertions(+), 15 deletions(-)
 create mode 100644 src/Service/CronManager.php

diff --git a/config/install/views.view.page_notification_subscriptions.yml b/config/install/views.view.page_notification_subscriptions.yml
index 4de5cc2..1ef06fb 100644
--- a/config/install/views.view.page_notification_subscriptions.yml
+++ b/config/install/views.view.page_notification_subscriptions.yml
@@ -368,6 +368,60 @@ display:
           multi_type: separator
           separator: ', '
           field_api_classes: false
+        created:
+          id: created
+          table: page_notification_subscription
+          field: created
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: created
+          plugin_id: date
+          label: Created
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          date_format: 'raw time ago'
+          custom_date_format: ''
+          timezone: ''
       pager:
         type: mini
         options:
diff --git a/page_notifications.module b/page_notifications.module
index eb3db4e..0bebf6a 100644
--- a/page_notifications.module
+++ b/page_notifications.module
@@ -1,5 +1,7 @@
 <?php
 
+use Drupal\Core\Form\FormStateInterface;
+
 /**
  * @file
  * Primary module hooks for Page Notifications module.
@@ -39,4 +41,64 @@ function page_notifications_token_info() {
  */
 function page_notifications_tokens($type, $tokens, array $data, array $options, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) {
   return \Drupal::service('page_notifications.subscription_token')->hookTokens($type, $tokens, $data, $options, $bubbleable_metadata);
+}
+
+/**
+ * Implements hook_form_BASE_FORM_ID_alter().
+ */
+function page_notifications_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  /** @var \Drupal\node\NodeInterface $node */
+  $node = $form_state->getFormObject()->getEntity();
+
+  // Get subscriber count
+  $subscriber_count = \Drupal::entityTypeManager()
+    ->getStorage('page_notification_subscription')
+    ->getQuery()
+    ->condition('subscribed_entity_id', $node->id())
+    ->condition('subscribed_entity_type', 'node')
+    ->condition('status', TRUE)
+    ->count()
+    ->accessCheck(FALSE)
+    ->execute();
+
+  // Add notification checkbox to the meta header region
+  $form['meta']['send_notification'] = [
+    '#type' => 'checkbox',
+    '#title' => t('Send notification to subscribers (@count active subscribers)', [
+      '@count' => $subscriber_count,
+    ]),
+    '#description' => $subscriber_count > 0 ?
+      t('If checked, subscribers will receive an email about this update. The revision log message will be included in the notification.') :
+      t('This content has no active subscribers.'),
+    '#default_value' => FALSE,
+    '#disabled' => $subscriber_count === 0,
+    '#group' => 'meta',
+    '#weight' => 30,
+    '#access' => \Drupal::currentUser()->hasPermission('send page notifications'),
+  ];
+
+  // Add custom submit handler
+  $form['actions']['submit']['#submit'][] = 'page_notifications_node_form_submit';
+}
+
+/**
+ * Submit handler for node form.
+ */
+function page_notifications_node_form_submit($form, FormStateInterface $form_state) {
+  $meta = $form_state->getValue('meta');
+  if (!empty($meta['send_notification'])) {
+    $node = $form_state->getFormObject()->getEntity();
+    /** @var \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager */
+    $notification_manager = \Drupal::service('page_notifications.notification_manager');
+    $notification_manager->notifySubscribers($node);
+
+    \Drupal::messenger()->addMessage(t('Notification queued for sending to subscribers.'));
+  }
+}
+
+/**
+ * Implements hook_cron().
+ */
+function page_notifications_cron() {
+  \Drupal::service('page_notifications.cron_manager')->processCron();
 }
\ No newline at end of file
diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index 7e36ef5..dd7e9a7 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -16,8 +16,8 @@ page_notifications.subscription.verify:
   options:
     no_cache: TRUE
 
-entity.subscription.delete_form:
-  path: '/admin/structure/page_notifications_subscription/{subscription}/delete'
+page_notifications.subscription.delete:
+  path: '/page_notifications_subscription/{subscription}/delete'
   defaults:
     _entity_form: 'page_notification_subscription.delete'
     _title: 'Delete Subscription'
diff --git a/page_notifications.services.yml b/page_notifications.services.yml
index a2e28a4..163c389 100644
--- a/page_notifications.services.yml
+++ b/page_notifications.services.yml
@@ -23,4 +23,22 @@ services:
   page_notifications.subscription_token:
     class: Drupal\page_notifications\Token\SubscriptionToken
     tags:
-      - { name: token.provider }
\ No newline at end of file
+      - { name: token.provider }
+  page_notifications.cron_manager:
+    class: Drupal\page_notifications\Service\CronManager
+    arguments:
+      - '@entity_type.manager'
+      - '@config.factory'
+      - '@queue'
+      - '@plugin.manager.queue_worker'
+      - '@logger.factory'
+      - '@datetime.time'
+  page_notifications.queue_worker:
+    class: Drupal\page_notifications\Plugin\QueueWorker\NotificationQueue
+    arguments:
+      - '@entity_type.manager'
+      - '@plugin.manager.mail'
+      - '@config.factory'
+      - '@logger.factory'
+    tags:
+      - { name: queue_worker, id: page_notifications_queue }
\ No newline at end of file
diff --git a/src/Entity/SubscriptionListBuilder.php b/src/Entity/SubscriptionListBuilder.php
index a8951e5..a59d962 100644
--- a/src/Entity/SubscriptionListBuilder.php
+++ b/src/Entity/SubscriptionListBuilder.php
@@ -131,7 +131,7 @@ protected function getDefaultOperations(EntityInterface $entity) {
     // Add delete operation
     $operations['delete'] = [
       'title' => $this->t('Delete'),
-      'url' => Url::fromRoute('entity.subscription.delete_form', [
+      'url' => Url::fromRoute('page_notifications.subscription.delete', [
         'subscription' => $entity->id(),
       ]),
     ];
diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php
index 9f26710..a73cc6e 100644
--- a/src/Mail/PageNotificationsMailHandler.php
+++ b/src/Mail/PageNotificationsMailHandler.php
@@ -99,7 +99,7 @@ class PageNotificationsMailHandler {
     $message['body'][] = $this->token->replace($body, $token_data);
   }
 
-  /**
+ /**
    * Builds a notification email.
    *
    * @param array $message
@@ -109,19 +109,23 @@ class PageNotificationsMailHandler {
    */
   protected function buildNotificationEmail(array &$message, array $params) {
     $config = $this->configFactory->get('page_notifications.settings');
-    $subscription = $params['subscription'];
-    $entity = $params['entity'];
+
+    if (!isset($params['subscription']) || !isset($params['entity'])) {
+      return;
+    }
 
     $token_data = [
-      'subscription' => $subscription,
-      'node' => $entity,
+      'subscription' => $params['subscription'],
+      'node' => $params['entity'],
     ];
 
     $subject = $config->get('email_templates.notification_subject');
     $body = $config->get('email_templates.notification_body');
 
-    $message['subject'] = $this->token->replace($subject, $token_data);
-    $message['body'][] = $this->token->replace($body, $token_data);
+    if ($subject && $body) {
+      $message['subject'] = $this->token->replace($subject, $token_data);
+      $message['body'][] = $this->token->replace($body, $token_data);
+    }
   }
 
 }
\ No newline at end of file
diff --git a/src/Plugin/QueueWorker/NotificationQueue.php b/src/Plugin/QueueWorker/NotificationQueue.php
index bd8d099..e8e0636 100644
--- a/src/Plugin/QueueWorker/NotificationQueue.php
+++ b/src/Plugin/QueueWorker/NotificationQueue.php
@@ -3,6 +3,14 @@
 namespace Drupal\page_notifications\Plugin\QueueWorker;
 
 use Drupal\Core\Queue\QueueWorkerBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Mail\MailManagerInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Queue\QueueInterface;
 
 /**
  * Process notification queue.
@@ -13,6 +21,149 @@ use Drupal\Core\Queue\QueueWorkerBase;
  *   cron = {"time" = 60}
  * )
  */
-class NotificationQueue extends QueueWorkerBase {
-  // TODO: Implement queue worker for processing notifications
+class NotificationQueue extends QueueWorkerBase implements ContainerFactoryPluginInterface {
+  use StringTranslationTrait;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The mail manager.
+   *
+   * @var \Drupal\Core\Mail\MailManagerInterface
+   */
+  protected $mailManager;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * Maximum number of retry attempts for failed notifications.
+   *
+   * @var int
+   */
+  protected const MAX_RETRIES = 3;
+
+  /**
+   * Constructs a new NotificationQueue worker.
+   */
+  public function __construct(
+    array $configuration,
+    $plugin_id,
+    array $plugin_definition,
+    EntityTypeManagerInterface $entity_type_manager,
+    MailManagerInterface $mail_manager,
+    ConfigFactoryInterface $config_factory,
+    LoggerChannelFactoryInterface $logger_factory
+  ) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->mailManager = $mail_manager;
+    $this->configFactory = $config_factory;
+    $this->loggerFactory = $logger_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('plugin.manager.mail'),
+      $container->get('config.factory'),
+      $container->get('logger.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processItem($data) {
+    try {
+      $subscription = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->load($data['subscription_id']);
+
+      if (!$subscription || !$subscription->isActive()) {
+        $this->loggerFactory->get('page_notifications')
+          ->notice('Skipping notification for inactive or deleted subscription: @id',
+            ['@id' => $data['subscription_id']]);
+        return;
+      }
+
+      $entity = $this->entityTypeManager
+        ->getStorage($data['entity_type'])
+        ->load($data['entity_id']);
+
+      if (!$entity) {
+        $this->loggerFactory->get('page_notifications')
+          ->error('Cannot send notification: Entity not found (@type: @id)',
+            ['@type' => $data['entity_type'], '@id' => $data['entity_id']]);
+        return;
+      }
+
+      $retries = $data['retries'] ?? 0;
+      if ($retries >= self::MAX_RETRIES) {
+        $this->loggerFactory->get('page_notifications')
+          ->error('Maximum retry attempts reached for notification to @email',
+            ['@email' => $subscription->getEmail()]);
+        return;
+      }
+
+      $config = $this->configFactory->get('page_notifications.settings');
+      $params = [
+        'subscription' => $subscription,
+        'entity' => $entity,
+        'token' => $subscription->getToken(),
+      ];
+
+      $result = $this->mailManager->mail(
+        'page_notifications',
+        'notification',
+        $subscription->getEmail(),
+        $subscription->getLanguageCode(),
+        $params,
+        $config->get('notification_settings.from_email')
+      );
+
+      if (!$result['result']) {
+        throw new \Exception('Failed to send notification email to ' . $subscription->getEmail());
+      }
+
+      $this->loggerFactory->get('page_notifications')
+        ->info('Successfully sent notification to @email for @type @id', [
+          '@email' => $subscription->getEmail(),
+          '@type' => $entity->getEntityTypeId(),
+          '@id' => $entity->id(),
+        ]);
+    }
+    catch (\Exception $e) {
+      $this->loggerFactory->get('page_notifications')
+        ->error('Error processing notification: @message', ['@message' => $e->getMessage()]);
+
+      $retries = ($data['retries'] ?? 0) + 1;
+      if ($retries < self::MAX_RETRIES) {
+        $data['retries'] = $retries;
+        throw new \Exception('Notification failed, will retry. Error: ' . $e->getMessage());
+      }
+    }
+  }
 }
\ No newline at end of file
diff --git a/src/Service/CronManager.php b/src/Service/CronManager.php
new file mode 100644
index 0000000..df6f561
--- /dev/null
+++ b/src/Service/CronManager.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Drupal\page_notifications\Service;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Queue\QueueWorkerManagerInterface;
+use Drupal\Core\Queue\QueueFactory;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Component\Datetime\TimeInterface;
+
+/**
+ * Service for handling page notifications cron operations.
+ */
+class CronManager {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The queue factory.
+   *
+   * @var \Drupal\Core\Queue\QueueFactory
+   */
+  protected $queueFactory;
+
+  /**
+   * The queue worker manager.
+   *
+   * @var \Drupal\Core\Queue\QueueWorkerManagerInterface
+   */
+  protected $queueWorkerManager;
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * Constructs a new CronManager.
+   */
+  public function __construct(
+    EntityTypeManagerInterface $entity_type_manager,
+    ConfigFactoryInterface $config_factory,
+    QueueFactory $queue_factory,
+    QueueWorkerManagerInterface $queue_worker_manager,
+    LoggerChannelFactoryInterface $logger_factory,
+    TimeInterface $time
+  ) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->configFactory = $config_factory;
+    $this->queueFactory = $queue_factory;
+    $this->queueWorkerManager = $queue_worker_manager;
+    $this->loggerFactory = $logger_factory;
+    $this->time = $time;
+  }
+
+  /**
+   * Processes cron tasks.
+   */
+  public function processCron() {
+    $this->processQueue();
+    $this->cleanupExpiredSubscriptions();
+  }
+
+  /**
+   * Process the notification queue.
+   */
+  protected function processQueue() {
+    $queue = $this->queueFactory->get('page_notifications_queue');
+    $queue_worker = $this->queueWorkerManager->createInstance('page_notifications_queue');
+
+    $time_limit = 30;
+    $end = $this->time->getRequestTime() + $time_limit;
+    $items_processed = 0;
+
+    while ($this->time->getRequestTime() < $end && ($item = $queue->claimItem())) {
+      try {
+        $queue_worker->processItem($item->data);
+        $queue->deleteItem($item);
+        $items_processed++;
+
+        if ($items_processed >= 50) {
+          break;
+        }
+      }
+      catch (\Exception $e) {
+        $queue->releaseItem($item);
+        $this->loggerFactory->get('page_notifications')->error(
+          'Error processing notification: @message',
+          ['@message' => $e->getMessage()]
+        );
+      }
+    }
+  }
+
+  /**
+   * Clean up expired unverified subscriptions.
+   */
+  protected function cleanupExpiredSubscriptions() {
+    if ($this->configFactory->get('page_notifications.settings')->get('security.cleanup_unverified')) {
+      $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
+      $config = $this->configFactory->get('page_notifications.settings');
+      $expiration_hours = $config->get('notification_settings.token_expiration') ?? 48;
+
+      $expired_ids = $storage->getQuery()
+        ->condition('status', FALSE)
+        ->condition('created', $this->time->getRequestTime() - ($expiration_hours * 3600), '<')
+        ->accessCheck(FALSE)
+        ->execute();
+
+      if (!empty($expired_ids)) {
+        $storage->delete($storage->loadMultiple($expired_ids));
+        $this->loggerFactory->get('page_notifications')->notice(
+          'Cleaned up @count expired unverified subscriptions',
+          ['@count' => count($expired_ids)]
+        );
+      }
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php
index 3ea9222..0e3e8a1 100644
--- a/src/Token/SubscriptionToken.php
+++ b/src/Token/SubscriptionToken.php
@@ -57,8 +57,8 @@ class SubscriptionToken {
             break;
 
           case 'unsubscribe-url':
-            $replacements[$original] = Url::fromRoute('entity.subscription.delete_form',
-              ['token' => $subscription->getToken()],
+            $replacements[$original] = Url::fromRoute('page_notifications.subscription.delete',
+              ['subscription' => $subscription->id()],
               ['absolute' => TRUE]
             )->toString();
             break;
-- 
GitLab


From 1f0a3eacf412924ffdc8b5f8988022b1ec2d5be5 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Tue, 7 Jan 2025 13:48:07 -0500
Subject: [PATCH 05/49] Adding a manual send admin form and adding notes token
 along with tabs to connect admin forms.

---
 .../install/page_notifications.settings.yml   |   2 +
 page_notifications.links.task.yml             |  17 ++
 page_notifications.routing.yml                |  18 ++-
 src/Controller/SubscriptionListController.php |  26 +++
 .../Controller/VerificationController.php     |   0
 src/Form/ManualNotificationForm.php           | 148 ++++++++++++++++++
 src/Mail/PageNotificationsMailHandler.php     |  21 +--
 src/Plugin/QueueWorker/NotificationQueue.php  |  57 +++----
 src/Token/SubscriptionToken.php               |  37 +++++
 9 files changed, 276 insertions(+), 50 deletions(-)
 create mode 100644 page_notifications.links.task.yml
 create mode 100644 src/Controller/SubscriptionListController.php
 rename src/{src => }/Controller/VerificationController.php (100%)
 create mode 100644 src/Form/ManualNotificationForm.php

diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml
index c5ef6fa..c0c5118 100644
--- a/config/install/page_notifications.settings.yml
+++ b/config/install/page_notifications.settings.yml
@@ -22,6 +22,8 @@ email_templates:
 
     The page "[node:title]" that you are subscribed to has been updated.
 
+    [notification:notes]
+
     You can view the updated page here:
     [node:url]
 
diff --git a/page_notifications.links.task.yml b/page_notifications.links.task.yml
new file mode 100644
index 0000000..7be32d6
--- /dev/null
+++ b/page_notifications.links.task.yml
@@ -0,0 +1,17 @@
+page_notifications.settings:
+  route_name: page_notifications.settings
+  title: 'Settings'
+  base_route: page_notifications.admin_settings
+  weight: 0
+
+page_notifications.send_manual:
+  route_name: page_notifications.send_manual
+  title: 'Send Notification'
+  base_route: page_notifications.admin_settings
+  weight: 1
+
+page_notifications.subscription_list:
+  route_name: page_notifications.subscription_list
+  title: 'Subscriptions'
+  base_route: page_notifications.admin_settings
+  weight: 2
\ No newline at end of file
diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index dd7e9a7..546da4c 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -22,4 +22,20 @@ page_notifications.subscription.delete:
     _entity_form: 'page_notification_subscription.delete'
     _title: 'Delete Subscription'
   requirements:
-    _entity_access: 'page_notification_subscription.delete'
\ No newline at end of file
+    _entity_access: 'page_notification_subscription.delete'
+
+page_notifications.send_manual:
+  path: '/admin/config/system/page-notifications/send'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\ManualNotificationForm'
+    _title: 'Send Manual Notification'
+  requirements:
+    _permission: 'administer page notification subscriptions'
+
+page_notifications.subscription_list:
+  path: '/admin/config/system/page-notifications/subscriptions'
+  defaults:
+    _controller: '\Drupal\page_notifications\Controller\SubscriptionListController::content'
+    _title: 'Subscriptions'
+  requirements:
+    _permission: 'view subscription list'
\ No newline at end of file
diff --git a/src/Controller/SubscriptionListController.php b/src/Controller/SubscriptionListController.php
new file mode 100644
index 0000000..fa6946d
--- /dev/null
+++ b/src/Controller/SubscriptionListController.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\page_notifications\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Controller for the subscription list page.
+ */
+class SubscriptionListController extends ControllerBase {
+
+  /**
+   * Displays the subscription list view.
+   *
+   * @return array
+   *   A render array for the view.
+   */
+  public function content() {
+    $view = views_embed_view('page_notification_subscriptions', 'default');
+    return [
+      '#type' => 'container',
+      'view' => $view,
+    ];
+  }
+
+}
\ No newline at end of file
diff --git a/src/src/Controller/VerificationController.php b/src/Controller/VerificationController.php
similarity index 100%
rename from src/src/Controller/VerificationController.php
rename to src/Controller/VerificationController.php
diff --git a/src/Form/ManualNotificationForm.php b/src/Form/ManualNotificationForm.php
new file mode 100644
index 0000000..fe38bb2
--- /dev/null
+++ b/src/Form/ManualNotificationForm.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\page_notifications\Service\NotificationManagerInterface;
+
+/**
+ * Form for manually sending notifications to subscribers.
+ */
+class ManualNotificationForm extends FormBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The notification manager service.
+   *
+   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
+   */
+  protected $notificationManager;
+
+  /**
+   * Constructs a new ManualNotificationForm.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager
+   *   The notification manager service.
+   */
+  public function __construct(
+    EntityTypeManagerInterface $entity_type_manager,
+    NotificationManagerInterface $notification_manager
+  ) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->notificationManager = $notification_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('page_notifications.notification_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_manual_notification_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Get content with active subscribers
+    $subscription_storage = $this->entityTypeManager->getStorage('page_notification_subscription');
+    $node_storage = $this->entityTypeManager->getStorage('node');
+
+    // Get unique node IDs that have active subscribers
+    $query = $subscription_storage->getQuery()
+      ->condition('status', TRUE)
+      ->condition('subscribed_entity_type', 'node')
+      ->accessCheck(FALSE);
+    $result = $query->execute();
+
+    if (empty($result)) {
+      $form['message'] = [
+        '#markup' => $this->t('There are no pages with active subscribers.'),
+      ];
+      return $form;
+    }
+
+    $subscriptions = $subscription_storage->loadMultiple($result);
+    $node_ids = [];
+    foreach ($subscriptions as $subscription) {
+      $node_ids[$subscription->getSubscribedEntityId()] = $subscription->getSubscribedEntityId();
+    }
+
+    // Load nodes and prepare options
+    $nodes = $node_storage->loadMultiple($node_ids);
+    $options = [];
+    foreach ($nodes as $node) {
+      $options[$node->id()] = $node->label();
+    }
+
+    $form['node'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Select Content'),
+      '#options' => $options,
+      '#required' => TRUE,
+      '#description' => $this->t('Select the content to send notifications about.'),
+    ];
+
+    $form['notes'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Notification Notes'),
+      '#description' => $this->t('Enter any additional notes to include in the notification email.'),
+      '#rows' => 4,
+    ];
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Send Notification'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $node_id = $form_state->getValue('node');
+    $notes = $form_state->getValue('notes');
+
+    try {
+      $node = $this->entityTypeManager->getStorage('node')->load($node_id);
+      if ($node) {
+        // Store notes in tempstore or pass through event system
+        \Drupal::state()->set('page_notifications_manual_notes_' . $node->id(), $notes);
+
+        $this->notificationManager->notifySubscribers($node);
+        $this->messenger()->addStatus($this->t('Notifications have been queued for sending.'));
+      }
+    }
+    catch (\Exception $e) {
+      $this->messenger()->addError($this->t('There was a problem sending the notifications.'));
+      \Drupal::logger('page_notifications')->error($e->getMessage());
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php
index a73cc6e..3540a8c 100644
--- a/src/Mail/PageNotificationsMailHandler.php
+++ b/src/Mail/PageNotificationsMailHandler.php
@@ -9,6 +9,7 @@ use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\token\TokenInterface;
+
 /**
  * Handles mail formatting for page notifications.
  */
@@ -35,7 +36,7 @@ class PageNotificationsMailHandler {
    *
    * @var \Drupal\token\TokenServiceInterface
    */
-  protected $tokenService;
+  protected $token;
 
   /**
    * Constructs a new PageNotificationsMailHandler.
@@ -43,12 +44,12 @@ class PageNotificationsMailHandler {
   public function __construct(
     ConfigFactoryInterface $config_factory,
     RendererInterface $renderer,
-    TokenInterface $tokenService,
+    TokenInterface $token,
     TranslationInterface $translation
   ) {
     $this->configFactory = $config_factory;
     $this->renderer = $renderer;
-    $this->token = $tokenService;
+    $this->token = $token;
     $this->setStringTranslation($translation);
   }
 
@@ -76,11 +77,6 @@ class PageNotificationsMailHandler {
 
   /**
    * Builds a verification email.
-   *
-   * @param array $message
-   *   The message array.
-   * @param array $params
-   *   The message parameters.
    */
   protected function buildVerificationEmail(array &$message, array $params) {
     $config = $this->configFactory->get('page_notifications.settings');
@@ -99,13 +95,8 @@ class PageNotificationsMailHandler {
     $message['body'][] = $this->token->replace($body, $token_data);
   }
 
- /**
+  /**
    * Builds a notification email.
-   *
-   * @param array $message
-   *   The message array.
-   * @param array $params
-   *   The message parameters.
    */
   protected function buildNotificationEmail(array &$message, array $params) {
     $config = $this->configFactory->get('page_notifications.settings');
@@ -117,6 +108,8 @@ class PageNotificationsMailHandler {
     $token_data = [
       'subscription' => $params['subscription'],
       'node' => $params['entity'],
+      'entity' => $params['entity'], // Add this for the notification token type
+      'notification' => [], // Add empty array to trigger notification token type
     ];
 
     $subject = $config->get('email_templates.notification_subject');
diff --git a/src/Plugin/QueueWorker/NotificationQueue.php b/src/Plugin/QueueWorker/NotificationQueue.php
index e8e0636..dd65981 100644
--- a/src/Plugin/QueueWorker/NotificationQueue.php
+++ b/src/Plugin/QueueWorker/NotificationQueue.php
@@ -10,7 +10,7 @@ use Drupal\Core\Mail\MailManagerInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\Queue\QueueInterface;
+use Drupal\Core\Language\LanguageInterface;
 
 /**
  * Process notification queue.
@@ -52,13 +52,6 @@ class NotificationQueue extends QueueWorkerBase implements ContainerFactoryPlugi
    */
   protected $loggerFactory;
 
-  /**
-   * Maximum number of retry attempts for failed notifications.
-   *
-   * @var int
-   */
-  protected const MAX_RETRIES = 3;
-
   /**
    * Constructs a new NotificationQueue worker.
    */
@@ -98,6 +91,7 @@ class NotificationQueue extends QueueWorkerBase implements ContainerFactoryPlugi
    */
   public function processItem($data) {
     try {
+      // Load the subscription
       $subscription = $this->entityTypeManager
         ->getStorage('page_notification_subscription')
         ->load($data['subscription_id']);
@@ -109,6 +103,7 @@ class NotificationQueue extends QueueWorkerBase implements ContainerFactoryPlugi
         return;
       }
 
+      // Load the entity
       $entity = $this->entityTypeManager
         ->getStorage($data['entity_type'])
         ->load($data['entity_id']);
@@ -120,50 +115,42 @@ class NotificationQueue extends QueueWorkerBase implements ContainerFactoryPlugi
         return;
       }
 
-      $retries = $data['retries'] ?? 0;
-      if ($retries >= self::MAX_RETRIES) {
-        $this->loggerFactory->get('page_notifications')
-          ->error('Maximum retry attempts reached for notification to @email',
-            ['@email' => $subscription->getEmail()]);
-        return;
-      }
-
       $config = $this->configFactory->get('page_notifications.settings');
+
+      // Prepare mail parameters
       $params = [
         'subscription' => $subscription,
         'entity' => $entity,
         'token' => $subscription->getToken(),
       ];
 
-      $result = $this->mailManager->mail(
-        'page_notifications',
-        'notification',
-        $subscription->getEmail(),
-        $subscription->getLanguageCode(),
-        $params,
-        $config->get('notification_settings.from_email')
+      $langcode = $subscription->getLanguageCode() ?? LanguageInterface::LANGCODE_DEFAULT;
+      $from_email = $config->get('notification_settings.from_email');
+
+      // Send the email
+      $this->mailManager->mail(
+        'page_notifications',           // module
+        'notification',                 // key
+        $subscription->getEmail(),      // to
+        $langcode,                     // language
+        $params,                       // params
+        $from_email ?: NULL            // from
       );
 
-      if (!$result['result']) {
-        throw new \Exception('Failed to send notification email to ' . $subscription->getEmail());
-      }
-
+      // Log the attempt regardless of the result
       $this->loggerFactory->get('page_notifications')
-        ->info('Successfully sent notification to @email for @type @id', [
+        ->info('Processed notification for @email regarding @type @id', [
           '@email' => $subscription->getEmail(),
           '@type' => $entity->getEntityTypeId(),
           '@id' => $entity->id(),
         ]);
+
     }
     catch (\Exception $e) {
+      // Log any unexpected errors
       $this->loggerFactory->get('page_notifications')
-        ->error('Error processing notification: @message', ['@message' => $e->getMessage()]);
-
-      $retries = ($data['retries'] ?? 0) + 1;
-      if ($retries < self::MAX_RETRIES) {
-        $data['retries'] = $retries;
-        throw new \Exception('Notification failed, will retry. Error: ' . $e->getMessage());
-      }
+        ->error('Error processing notification: @message',
+          ['@message' => $e->getMessage()]);
     }
   }
 }
\ No newline at end of file
diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php
index 0e3e8a1..6865fb9 100644
--- a/src/Token/SubscriptionToken.php
+++ b/src/Token/SubscriptionToken.php
@@ -5,6 +5,7 @@ namespace Drupal\page_notifications\Token;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Url;
+use Drupal\node\NodeInterface;
 
 /**
  * Implements hook_token_info() and hook_tokens().
@@ -22,6 +23,12 @@ class SubscriptionToken {
       'needs-data' => 'subscription',
     ];
 
+    $types['notification'] = [
+      'name' => $this->t('Notification'),
+      'description' => $this->t('Tokens related to page notifications'),
+      'needs-data' => 'entity',
+    ];
+
     $tokens['subscription']['verify-url'] = [
       'name' => $this->t('Verification URL'),
       'description' => $this->t('The URL to verify the subscription'),
@@ -32,6 +39,11 @@ class SubscriptionToken {
       'description' => $this->t('The URL to unsubscribe from notifications'),
     ];
 
+    $tokens['notification']['notes'] = [
+      'name' => $this->t('Notification Notes'),
+      'description' => $this->t('Additional notes from manual notifications or revision log message'),
+    ];
+
     return [
       'types' => $types,
       'tokens' => $tokens,
@@ -66,6 +78,31 @@ class SubscriptionToken {
       }
     }
 
+    if ($type == 'notification' && !empty($data['entity'])) {
+      $entity = $data['entity'];
+
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'notes':
+            // First check for manual notification notes
+            $notes = \Drupal::state()->get('page_notifications_manual_notes_' . $entity->id(), '');
+
+            // If no manual notes and entity is a node, try to get revision log
+            if (empty($notes) && $entity instanceof NodeInterface) {
+              $notes = $entity->getRevisionLogMessage();
+            }
+
+            $replacements[$original] = $notes;
+
+            // Clean up stored manual notes if they exist
+            if (\Drupal::state()->get('page_notifications_manual_notes_' . $entity->id())) {
+              \Drupal::state()->delete('page_notifications_manual_notes_' . $entity->id());
+            }
+            break;
+        }
+      }
+    }
+
     return $replacements;
   }
 }
\ No newline at end of file
-- 
GitLab


From fcc8735ac05915d003a2990e49a4fc9336934b7f Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Tue, 7 Jan 2025 15:14:25 -0500
Subject: [PATCH 06/49] Cleaned up admins can delete manually, and unsubscribe
 links are more secure and they redirect to the page related to the
 subscriptions when you verify/unsubscribe

---
 page_notifications.routing.yml            |  15 +--
 src/Controller/UnsubscribeController.php  | 118 ++++++++++++++++++++++
 src/Controller/VerificationController.php |  62 ------------
 src/Entity/Subscription.php               |  41 ++++++--
 src/Entity/SubscriptionListBuilder.php    |  34 ++-----
 src/Service/NotificationManager.php       |  77 +++++++-------
 src/Token/SubscriptionToken.php           |  15 +--
 7 files changed, 223 insertions(+), 139 deletions(-)
 create mode 100644 src/Controller/UnsubscribeController.php
 delete mode 100644 src/Controller/VerificationController.php

diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index 546da4c..12895aa 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -9,20 +9,23 @@ page_notifications.settings:
 page_notifications.subscription.verify:
   path: '/page-notifications/verify/{token}'
   defaults:
-    _controller: 'page_notifications.notification_manager:verifySubscriptionRoute'
+    _controller: 'page_notifications.notification_manager:verifySubscription'
     _title: 'Verify Subscription'
   requirements:
     _access: 'TRUE'
   options:
     no_cache: TRUE
 
-page_notifications.subscription.delete:
-  path: '/page_notifications_subscription/{subscription}/delete'
+# Secure unsubscribe for anonymous users
+page_notifications.subscription.unsubscribe:
+  path: '/page-notifications/unsubscribe/{subscription}/{token}'
   defaults:
-    _entity_form: 'page_notification_subscription.delete'
-    _title: 'Delete Subscription'
+    _controller: '\Drupal\page_notifications\Controller\UnsubscribeController::unsubscribe'
+    _title: 'Unsubscribe from Notifications'
   requirements:
-    _entity_access: 'page_notification_subscription.delete'
+    _custom_access: '\Drupal\page_notifications\Controller\UnsubscribeController::checkAccess'
+  options:
+    no_cache: TRUE
 
 page_notifications.send_manual:
   path: '/admin/config/system/page-notifications/send'
diff --git a/src/Controller/UnsubscribeController.php b/src/Controller/UnsubscribeController.php
new file mode 100644
index 0000000..7399351
--- /dev/null
+++ b/src/Controller/UnsubscribeController.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Drupal\page_notifications\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Access\AccessResult;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Url;
+
+/**
+ * Controller for handling unsubscribe requests.
+ */
+class UnsubscribeController extends ControllerBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new UnsubscribeController.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * Custom access check for unsubscribe URLs.
+   */
+  public function checkAccess($subscription, $token) {
+    if (is_numeric($subscription)) {
+      try {
+        $subscription = $this->entityTypeManager
+          ->getStorage('page_notification_subscription')
+          ->load($subscription);
+      }
+      catch (\Exception $e) {
+        return AccessResult::forbidden();
+      }
+    }
+
+    if (!$subscription) {
+      return AccessResult::forbidden();
+    }
+
+    if ($subscription->getUnsubscribeToken() !== $token) {
+      return AccessResult::forbidden();
+    }
+
+    return AccessResult::allowed();
+  }
+
+  /**
+   * Handles the unsubscribe request.
+   */
+  public function unsubscribe($subscription, $token) {
+    if (is_numeric($subscription)) {
+      try {
+        $subscription = $this->entityTypeManager
+          ->getStorage('page_notification_subscription')
+          ->load($subscription);
+      }
+      catch (\Exception $e) {
+        $this->messenger()->addError($this->t('An error occurred while processing your request.'));
+        return new RedirectResponse('/');
+      }
+    }
+
+    try {
+      if ($subscription && $subscription->getUnsubscribeToken() === $token) {
+        // Get the node ID and entity type before deleting the subscription
+        $entity_id = $subscription->getSubscribedEntityId();
+        $entity_type = $subscription->getSubscribedEntityType();
+
+        $subscription->delete();
+        $this->messenger()->addStatus($this->t('You have been successfully unsubscribed.'));
+
+        // Load the entity and get its URL
+        try {
+          $entity = $this->entityTypeManager
+            ->getStorage($entity_type)
+            ->load($entity_id);
+
+          if ($entity && $entity->hasLinkTemplate('canonical')) {
+            return new RedirectResponse($entity->toUrl()->toString());
+          }
+        }
+        catch (\Exception $e) {
+          \Drupal::logger('page_notifications')->error('Redirect error: @message', ['@message' => $e->getMessage()]);
+        }
+      }
+      else {
+        $this->messenger()->addError($this->t('Invalid unsubscribe link.'));
+      }
+    }
+    catch (\Exception $e) {
+      $this->messenger()->addError($this->t('An error occurred while processing your request.'));
+      \Drupal::logger('page_notifications')->error('Unsubscribe error: @message', ['@message' => $e->getMessage()]);
+    }
+
+    // Fallback to homepage if anything goes wrong
+    return new RedirectResponse('/');
+  }
+}
\ No newline at end of file
diff --git a/src/Controller/VerificationController.php b/src/Controller/VerificationController.php
deleted file mode 100644
index 8708d5b..0000000
--- a/src/Controller/VerificationController.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-
-namespace Drupal\page_notifications\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Drupal\page_notifications\Service\NotificationManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Messenger\MessengerInterface;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-
-/**
- * Controller for handling subscription verification.
- */
-class VerificationController extends ControllerBase {
-
-  /**
-   * The notification manager service.
-   *
-   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
-   */
-  protected $notificationManager;
-
-  /**
-   * Constructs a VerificationController object.
-   *
-   * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager
-   *   The notification manager service.
-   */
-  public function __construct(NotificationManagerInterface $notification_manager) {
-    $this->notificationManager = $notification_manager;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('page_notifications.notification_manager')
-    );
-  }
-
-  /**
-   * Verifies a subscription token.
-   *
-   * @param string $token
-   *   The verification token.
-   *
-   * @return \Symfony\Component\HttpFoundation\RedirectResponse
-   *   Redirects to the front page with a status message.
-   */
-  public function verify($token) {
-    if ($this->notificationManager->verifySubscription($token)) {
-      $this->messenger()->addStatus($this->t('Thank you! Your subscription has been verified.'));
-    }
-    else {
-      $this->messenger()->addError($this->t('Sorry, this verification link is invalid or has expired.'));
-    }
-
-    return new RedirectResponse('/');
-  }
-
-}
\ No newline at end of file
diff --git a/src/Entity/Subscription.php b/src/Entity/Subscription.php
index c369622..24c675b 100644
--- a/src/Entity/Subscription.php
+++ b/src/Entity/Subscription.php
@@ -17,24 +17,29 @@ use Drupal\user\EntityOwnerTrait;
  *   handlers = {
  *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
  *     "list_builder" = "Drupal\page_notifications\Entity\SubscriptionListBuilder",
+ *     "views_data" = "Drupal\page_notifications\Entity\SubscriptionViewsData",
  *     "form" = {
  *       "default" = "Drupal\page_notifications\Form\SubscriptionForm",
- *       "delete" = "Drupal\page_notifications\Form\SubscriptionDeleteForm"
+ *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm"
  *     },
- *    "access" = "Drupal\page_notifications\Entity\SubscriptionAccessControlHandler",
- *    "views_data" = "Drupal\page_notifications\Entity\SubscriptionViewsData",
+ *     "access" = "Drupal\page_notifications\Entity\SubscriptionAccessControlHandler",
+ *     "route_provider" = {
+ *       "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider"
+ *     }
  *   },
  *   base_table = "page_notification_subscription",
+ *   data_table = "page_notification_subscription_field_data",
  *   admin_permission = "administer page notification subscriptions",
  *   entity_keys = {
  *     "id" = "id",
  *     "uuid" = "uuid",
  *     "owner" = "uid",
+ *     "langcode" = "langcode"
  *   },
  *   links = {
- *     "canonical" = "/admin/structure/page-notification-subscription/{page_notification_subscription}",
- *     "delete-form" = "/admin/structure/page-notification-subscription/{page_notification_subscription}/delete",
- *     "collection" = "/admin/structure/page-notification-subscription"
+ *     "canonical" = "/admin/content/subscriptions/{page_notification_subscription}",
+ *     "delete-form" = "/admin/content/subscriptions/{page_notification_subscription}/delete",
+ *     "collection" = "/admin/content/subscriptions"
  *   }
  * )
  */
@@ -158,6 +163,21 @@ class Subscription extends ContentEntityBase implements SubscriptionInterface {
     return \Drupal::config('system.site')->get('default_langcode') ?: 'en';
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getUnsubscribeToken() {
+    return $this->get('unsubscribe_token')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUnsubscribeToken($token) {
+    $this->set('unsubscribe_token', $token);
+    return $this;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -238,6 +258,15 @@ class Subscription extends ContentEntityBase implements SubscriptionInterface {
       'weight' => 2,
     ]);
 
+    $fields['unsubscribe_token'] = BaseFieldDefinition::create('string')
+  ->setLabel(t('Unsubscribe Token'))
+  ->setDescription(t('The token required to unsubscribe from notifications.'))
+  ->setRequired(TRUE)
+  ->setTranslatable(FALSE)
+  ->setSettings([
+    'max_length' => 64,
+  ]);
+
     return $fields;
   }
 
diff --git a/src/Entity/SubscriptionListBuilder.php b/src/Entity/SubscriptionListBuilder.php
index a59d962..7f96449 100644
--- a/src/Entity/SubscriptionListBuilder.php
+++ b/src/Entity/SubscriptionListBuilder.php
@@ -110,33 +110,19 @@ class SubscriptionListBuilder extends EntityListBuilder {
    * {@inheritdoc}
    */
 protected function getDefaultOperations(EntityInterface $entity) {
-    $operations = parent::getDefaultOperations($entity);
-
-    // Add a verify link if the subscription is not active.
-    if (!$entity->isActive()) {
-      // Generate verification token
-      $token = \Drupal::service('csrf_token')->get('subscription-verify-' . $entity->id());
-      $verify_operation = [
-        'verify' => [
-          'title' => $this->t('Verify'),
-          'url' => Url::fromRoute('page_notifications.subscription.verify', [
-            'token' => $token,
-            'subscription' => $entity->id(),
-          ]),
-        ],
-      ];
-      $operations = $verify_operation + $operations;
-    }
-
-    // Add delete operation
-    $operations['delete'] = [
-      'title' => $this->t('Delete'),
-      'url' => Url::fromRoute('page_notifications.subscription.delete', [
-        'subscription' => $entity->id(),
+  $operations = parent::getDefaultOperations($entity);
+
+  // Add verify link if not active
+  if (!$entity->isActive()) {
+    $operations['verify'] = [
+      'title' => $this->t('Verify'),
+      'url' => Url::fromRoute('page_notifications.subscription.verify', [
+        'token' => $entity->getToken(),
       ]),
     ];
+  }
 
-    return $operations;
+  return $operations;
 }
 
   /**
diff --git a/src/Service/NotificationManager.php b/src/Service/NotificationManager.php
index e1f795d..7c37480 100644
--- a/src/Service/NotificationManager.php
+++ b/src/Service/NotificationManager.php
@@ -152,14 +152,15 @@ class NotificationManager implements NotificationManagerInterface {
 
       /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
       $subscription = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->create([
-          'email' => $email,
-          'subscribed_entity_id' => $entity->id(),
-          'subscribed_entity_type' => $entity->getEntityTypeId(),
-          'token' => $token,
-          'status' => !$this->requiresVerification(),
-        ]);
+      ->getStorage('page_notification_subscription')
+      ->create([
+        'email' => $email,
+        'subscribed_entity_id' => $entity->id(),
+        'subscribed_entity_type' => $entity->getEntityTypeId(),
+        'token' => $this->generateToken(),
+        'unsubscribe_token' => $this->generateToken(),
+        'status' => !$this->requiresVerification(),
+      ]);
 
       $subscription->save();
 
@@ -176,49 +177,55 @@ class NotificationManager implements NotificationManagerInterface {
     }
   }
 
-  /**
-   * Route controller for subscription verification.
-   */
-  public function verifySubscriptionRoute($token) {
-    if ($this->verifySubscription($token)) {
-      $this->messenger->addStatus(t('Subscription verified successfully.'));
-    }
-    else {
-      $this->messenger->addError(t('Invalid verification token.'));
-    }
-    return new RedirectResponse('/');
-  }
-
   /**
    * {@inheritdoc}
    */
   public function verifySubscription(string $token) {
     try {
+      // Find subscription by token
       $subscriptions = $this->entityTypeManager
         ->getStorage('page_notification_subscription')
         ->loadByProperties(['token' => $token]);
 
-      if (!$subscriptions) {
-        return FALSE;
-      }
+      if (!empty($subscriptions)) {
+        /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
+        $subscription = reset($subscriptions);
 
-      /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
-      $subscription = reset($subscriptions);
+        // Get entity details before setting active
+        $entity_id = $subscription->getSubscribedEntityId();
+        $entity_type = $subscription->getSubscribedEntityType();
 
-      if ($this->isTokenExpired($subscription)) {
-        return FALSE;
-      }
+        // Activate the subscription
+        $subscription->setActive(TRUE);
+        $subscription->save();
 
-      $subscription->setActive(TRUE);
-      $subscription->save();
+        $this->messenger->addStatus($this->t('Thank you! Your subscription has been verified.'));
 
-      return TRUE;
+        // Load the entity and get its URL
+        try {
+          $entity = $this->entityTypeManager
+            ->getStorage($entity_type)
+            ->load($entity_id);
+
+          if ($entity && $entity->hasLinkTemplate('canonical')) {
+            return new RedirectResponse($entity->toUrl()->toString());
+          }
+        }
+        catch (\Exception $e) {
+          \Drupal::logger('page_notifications')->error('Verification redirect error: @message', ['@message' => $e->getMessage()]);
+        }
+      }
+      else {
+        $this->messenger->addError($this->t('Sorry, this verification link is invalid or has expired.'));
+      }
     }
     catch (\Exception $e) {
-      $this->loggerFactory->get('page_notifications')
-        ->error('Failed to verify subscription: @message', ['@message' => $e->getMessage()]);
-      return FALSE;
+      $this->messenger->addError($this->t('An error occurred while verifying your subscription.'));
+      \Drupal::logger('page_notifications')->error('Verification error: @message', ['@message' => $e->getMessage()]);
     }
+
+    // Fallback to homepage if anything goes wrong
+    return new RedirectResponse('/');
   }
 
   /**
diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php
index 6865fb9..097c532 100644
--- a/src/Token/SubscriptionToken.php
+++ b/src/Token/SubscriptionToken.php
@@ -68,12 +68,15 @@ class SubscriptionToken {
             )->toString();
             break;
 
-          case 'unsubscribe-url':
-            $replacements[$original] = Url::fromRoute('page_notifications.subscription.delete',
-              ['subscription' => $subscription->id()],
-              ['absolute' => TRUE]
-            )->toString();
-            break;
+            case 'unsubscribe-url':
+              $replacements[$original] = Url::fromRoute('page_notifications.subscription.unsubscribe',
+                [
+                  'subscription' => $subscription->id(),
+                  'token' => $subscription->getUnsubscribeToken(),
+                ],
+                ['absolute' => TRUE]
+              )->toString();
+              break;
         }
       }
     }
-- 
GitLab


From 867dd84f3da562819849426b9c054e57a30f6d1a Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Tue, 7 Jan 2025 21:20:24 -0500
Subject: [PATCH 07/49] Add a migrate form to be able to transfer subscriptions
 for admins

---
 page_notifications.links.task.yml             |   8 +-
 page_notifications.routing.yml                |  10 +-
 src/Form/SubscriptionMigrateForm.php          | 220 ++++++++++++++++++
 .../NodeEnhancedSelection.php                 |  57 +++++
 .../NodeWithSubscriptionsSelection.php        | 110 +++++++++
 5 files changed, 403 insertions(+), 2 deletions(-)
 create mode 100644 src/Form/SubscriptionMigrateForm.php
 create mode 100644 src/Plugin/EntityReferenceSelection/NodeEnhancedSelection.php
 create mode 100644 src/Plugin/EntityReferenceSelection/NodeWithSubscriptionsSelection.php

diff --git a/page_notifications.links.task.yml b/page_notifications.links.task.yml
index 7be32d6..bf3e39e 100644
--- a/page_notifications.links.task.yml
+++ b/page_notifications.links.task.yml
@@ -14,4 +14,10 @@ page_notifications.subscription_list:
   route_name: page_notifications.subscription_list
   title: 'Subscriptions'
   base_route: page_notifications.admin_settings
-  weight: 2
\ No newline at end of file
+  weight: 2
+
+page_notifications.subscription_migrate:
+  route_name: page_notifications.subscription_migrate
+  title: 'Migrate Subscriptions'
+  base_route: page_notifications.admin_settings
+  weight: 3
\ No newline at end of file
diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index 12895aa..5ce0a71 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -41,4 +41,12 @@ page_notifications.subscription_list:
     _controller: '\Drupal\page_notifications\Controller\SubscriptionListController::content'
     _title: 'Subscriptions'
   requirements:
-    _permission: 'view subscription list'
\ No newline at end of file
+    _permission: 'view subscription list'
+
+page_notifications.subscription_migrate:
+  path: '/admin/config/system/page-notifications/migrate'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\SubscriptionMigrateForm'
+    _title: 'Migrate Subscriptions'
+  requirements:
+    _permission: 'administer page notification subscriptions'
\ No newline at end of file
diff --git a/src/Form/SubscriptionMigrateForm.php b/src/Form/SubscriptionMigrateForm.php
new file mode 100644
index 0000000..416c9c0
--- /dev/null
+++ b/src/Form/SubscriptionMigrateForm.php
@@ -0,0 +1,220 @@
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Form for migrating subscriptions between nodes.
+ */
+class SubscriptionMigrateForm extends FormBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new SubscriptionMigrateForm.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_subscription_migrate_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Check if there are any nodes with subscriptions
+    $subscription_count = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('subscribed_entity_type', 'node')
+      ->condition('status', TRUE)
+      ->count()
+      ->accessCheck(FALSE)
+      ->execute();
+
+    if ($subscription_count === 0) {
+      $form['message'] = [
+        '#markup' => $this->t('There are no nodes with active subscriptions.'),
+      ];
+      return $form;
+    }
+
+    $form['description'] = [
+      '#markup' => $this->t('This form will migrate all active subscriptions from one node to another.'),
+    ];
+
+    $form['source_node'] = [
+      '#type' => 'entity_autocomplete',
+      '#title' => $this->t('FROM: Source Node'),
+      '#description' => $this->t('Select the node from which to migrate subscriptions.'),
+      '#target_type' => 'node',
+      '#required' => TRUE,
+      '#selection_handler' => 'default:node_with_subscriptions',
+    ];
+
+    $form['target_node'] = [
+      '#type' => 'entity_autocomplete',
+      '#title' => $this->t('TO: Target Node'),
+      '#description' => $this->t('Select the node to which subscriptions will be migrated.'),
+      '#target_type' => 'node',
+      '#required' => TRUE,
+      '#selection_handler' => 'default:node_enhanced',
+      '#selection_settings' => [
+        'target_bundles' => NULL,
+      ],
+    ];
+
+    $form['actions']['#type'] = 'actions';
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Migrate Subscriptions'),
+      '#button_type' => 'primary',
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $source_nid = $form_state->getValue('source_node');
+    $target_nid = $form_state->getValue('target_node');
+
+    if ($source_nid === $target_nid) {
+      $form_state->setError($form['target_node'], $this->t('Source and target nodes must be different.'));
+      return;
+    }
+
+    // Verify the source node still has subscriptions (in case they were deleted)
+    $subscription_count = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('subscribed_entity_id', $source_nid)
+      ->condition('subscribed_entity_type', 'node')
+      ->condition('status', TRUE)
+      ->count()
+      ->accessCheck(FALSE)
+      ->execute();
+
+    if ($subscription_count === 0) {
+      $form_state->setError($form['source_node'], $this->t('The source node has no active subscriptions to migrate.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $source_nid = $form_state->getValue('source_node');
+    $target_nid = $form_state->getValue('target_node');
+
+    try {
+      $batch = [
+        'title' => $this->t('Migrating subscriptions'),
+        'operations' => [
+          [
+            [$this, 'processMigration'],
+            [$source_nid, $target_nid],
+          ],
+        ],
+        'finished' => [$this, 'migrationFinished'],
+      ];
+
+      batch_set($batch);
+    }
+    catch (\Exception $e) {
+      $this->messenger()->addError($this->t('An error occurred while preparing the migration: @error', [
+        '@error' => $e->getMessage(),
+      ]));
+    }
+  }
+
+  /**
+   * Batch operation callback for migrating subscriptions.
+   */
+  public function processMigration($source_nid, $target_nid, &$context) {
+    if (!isset($context['sandbox']['progress'])) {
+      $context['sandbox']['progress'] = 0;
+      $context['sandbox']['current_id'] = 0;
+      $context['sandbox']['max'] = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->getQuery()
+        ->condition('subscribed_entity_id', $source_nid)
+        ->condition('subscribed_entity_type', 'node')
+        ->count()
+        ->accessCheck(FALSE)
+        ->execute();
+    }
+
+    // Process subscriptions in chunks of 50
+    $subscription_ids = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('subscribed_entity_id', $source_nid)
+      ->condition('subscribed_entity_type', 'node')
+      ->condition('id', $context['sandbox']['current_id'], '>')
+      ->sort('id')
+      ->range(0, 50)
+      ->accessCheck(FALSE)
+      ->execute();
+
+    foreach ($subscription_ids as $id) {
+      /** @var \Drupal\page_notifications\Entity\Subscription $subscription */
+      $subscription = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->load($id);
+
+      // Simply update the entity ID
+      $subscription->setSubscribedEntityId($target_nid);
+      $subscription->save();
+
+      $context['sandbox']['progress']++;
+      $context['sandbox']['current_id'] = $id;
+    }
+
+    if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
+      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+    }
+  }
+
+  /**
+   * Batch finished callback.
+   */
+  public function migrationFinished($success, $results, $operations) {
+    if ($success) {
+      $this->messenger()->addStatus($this->t('Successfully migrated all subscriptions to the new node.'));
+    }
+    else {
+      $this->messenger()->addError($this->t('An error occurred while migrating subscriptions.'));
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/src/Plugin/EntityReferenceSelection/NodeEnhancedSelection.php b/src/Plugin/EntityReferenceSelection/NodeEnhancedSelection.php
new file mode 100644
index 0000000..0ec26ca
--- /dev/null
+++ b/src/Plugin/EntityReferenceSelection/NodeEnhancedSelection.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\page_notifications\Plugin\EntityReferenceSelection;
+
+use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection;
+
+/**
+ * Provides enhanced node selection with additional display information.
+ *
+ * @EntityReferenceSelection(
+ *   id = "default:node_enhanced",
+ *   label = @Translation("Node enhanced selection"),
+ *   entity_types = {"node"},
+ *   group = "default",
+ *   weight = 1
+ * )
+ */
+class NodeEnhancedSelection extends NodeSelection {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
+    $target_type = $this->configuration['target_type'];
+
+    $query = $this->buildEntityQuery($match, $match_operator);
+    if ($limit > 0) {
+      $query->range(0, $limit);
+    }
+
+    $result = $query->execute();
+
+    if (empty($result)) {
+      return [];
+    }
+
+    $options = [];
+    $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result);
+
+    foreach ($entities as $entity_id => $entity) {
+      $bundle = $entity->bundle();
+      $type_label = $entity->type->entity->label();
+
+      $label = sprintf(
+        '%s (ID: %d, Type: %s)',
+        $entity->label(),
+        $entity_id,
+        $type_label
+      );
+
+      $options[$bundle][$entity_id] = $label;
+    }
+
+    return $options;
+  }
+
+}
\ No newline at end of file
diff --git a/src/Plugin/EntityReferenceSelection/NodeWithSubscriptionsSelection.php b/src/Plugin/EntityReferenceSelection/NodeWithSubscriptionsSelection.php
new file mode 100644
index 0000000..61c3107
--- /dev/null
+++ b/src/Plugin/EntityReferenceSelection/NodeWithSubscriptionsSelection.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Drupal\page_notifications\Plugin\EntityReferenceSelection;
+
+use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection;
+use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
+use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides specific access control for node entities with subscriptions.
+ *
+ * @EntityReferenceSelection(
+ *   id = "default:node_with_subscriptions",
+ *   label = @Translation("Node with subscriptions selection"),
+ *   entity_types = {"node"},
+ *   group = "default",
+ *   weight = 1
+ * )
+ */
+class NodeWithSubscriptionsSelection extends NodeSelection {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
+    $query = parent::buildEntityQuery($match, $match_operator);
+
+    // Get nodes that have active subscriptions
+    $subscription_query = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('subscribed_entity_type', 'node')
+      ->condition('status', TRUE)
+      ->accessCheck(FALSE);
+
+    $subscriptions = $subscription_query->execute();
+
+    if (!empty($subscriptions)) {
+      // Get unique node IDs from subscriptions
+      $node_ids = [];
+      $subscriptions = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->loadMultiple($subscriptions);
+
+      foreach ($subscriptions as $subscription) {
+        $node_ids[] = $subscription->getSubscribedEntityId();
+      }
+
+      // Filter query to only include nodes with subscriptions
+      $query->condition('nid', $node_ids, 'IN');
+    }
+    else {
+      // If no subscriptions exist, return no results
+      $query->condition('nid', 0);
+    }
+
+    return $query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
+    $target_type = $this->configuration['target_type'];
+
+    $query = $this->buildEntityQuery($match, $match_operator);
+    if ($limit > 0) {
+      $query->range(0, $limit);
+    }
+
+    $result = $query->execute();
+
+    if (empty($result)) {
+      return [];
+    }
+
+    $options = [];
+    $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result);
+
+    foreach ($entities as $entity_id => $entity) {
+      $bundle = $entity->bundle();
+      $type_label = $entity->type->entity->label();
+
+      // Get subscription count for this node
+      $subscription_count = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->getQuery()
+        ->condition('subscribed_entity_id', $entity_id)
+        ->condition('subscribed_entity_type', 'node')
+        ->condition('status', TRUE)
+        ->count()
+        ->accessCheck(FALSE)
+        ->execute();
+
+      $label = sprintf(
+        '%s (ID: %d, Type: %s, Subscriptions: %d)',
+        $entity->label(),
+        $entity_id,
+        $type_label,
+        $subscription_count
+      );
+
+      $options[$bundle][$entity_id] = $label;
+    }
+
+    return $options;
+  }
+
+}
\ No newline at end of file
-- 
GitLab


From 7913d275ff5d66feeed0c3c2552113930a1b5d3b Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Wed, 8 Jan 2025 11:36:57 -0500
Subject: [PATCH 08/49] Implement a simple match captcha and optional recaptcha
 to prevent spam. The recaptcha parts is currently untested

---
 .../install/page_notifications.settings.yml   |  10 +-
 config/schema/page_notifications.schema.yml   |  15 ++-
 page_notifications.info.yml                   |   2 +
 page_notifications.install                    |  12 ++
 page_notifications.services.yml               |   9 +-
 src/Form/SettingsForm.php                     |  76 ++++++++++-
 src/Form/SubscriptionForm.php                 | 101 ++++++++++++++-
 src/Service/SpamPrevention.php                | 121 ++++++++++++++++++
 8 files changed, 334 insertions(+), 12 deletions(-)
 create mode 100644 src/Service/SpamPrevention.php

diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml
index c0c5118..2018f37 100644
--- a/config/install/page_notifications.settings.yml
+++ b/config/install/page_notifications.settings.yml
@@ -34,5 +34,11 @@ email_templates:
     [site:name] team
 
 security:
-  require_verification: true
-  cleanup_unverified: true
\ No newline at end of file
+  require_verification: 1
+  cleanup_unverified: 1
+
+spam_protection:
+  enable_modal: 0
+  enable_math_captcha: 1
+  math_captcha_operator: +
+  captcha_point: null
\ No newline at end of file
diff --git a/config/schema/page_notifications.schema.yml b/config/schema/page_notifications.schema.yml
index 22153d6..e054974 100644
--- a/config/schema/page_notifications.schema.yml
+++ b/config/schema/page_notifications.schema.yml
@@ -13,4 +13,17 @@ page_notifications.settings:
           label: 'Email template'
         token_expiration:
           type: integer
-          label: 'Token expiration time in hours'
\ No newline at end of file
+          label: 'Token expiration time in hours'
+    spam_prevention:
+      type: mapping
+      label: 'Spam Prevention Settings'
+      mapping:
+        captcha_type:
+          type: string
+          label: 'Captcha Type'
+        math_operator:
+          type: string
+          label: 'Math Challenge Operator'
+        use_recaptcha:
+          type: boolean
+          label: 'Use reCAPTCHA if available'
\ No newline at end of file
diff --git a/page_notifications.info.yml b/page_notifications.info.yml
index a2a6271..2505484 100644
--- a/page_notifications.info.yml
+++ b/page_notifications.info.yml
@@ -6,3 +6,5 @@ configure: page_notifications.settings
 dependencies:
   - drupal:node
   - drupal:views
+suggestions:
+  - captcha:captcha
\ No newline at end of file
diff --git a/page_notifications.install b/page_notifications.install
index 237398b..158e38e 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -16,6 +16,9 @@ function page_notifications_install() {
       ->set('notification_settings.from_email', '')
       ->set('notification_settings.email_template', '')
       ->set('notification_settings.token_expiration', 48)
+      ->set('spam_prevention.captcha_type', 'none')
+      ->set('spam_prevention.math_operator', '+')
+      ->set('spam_prevention.use_recaptcha', FALSE)
       ->save();
   }
 }
@@ -29,6 +32,15 @@ function page_notifications_schema() {
   return $schema;
 }
 
+/**
+ * Implements hook_schema().
+ */
+function page_notifications_schema() {
+  $schema = [];
+  // Add any additional database tables needed beyond entities
+  return $schema;
+}
+
 /**
  * Implements hook_uninstall().
  */
diff --git a/page_notifications.services.yml b/page_notifications.services.yml
index 163c389..75b4271 100644
--- a/page_notifications.services.yml
+++ b/page_notifications.services.yml
@@ -41,4 +41,11 @@ services:
       - '@config.factory'
       - '@logger.factory'
     tags:
-      - { name: queue_worker, id: page_notifications_queue }
\ No newline at end of file
+      - { name: queue_worker, id: page_notifications_queue }
+  page_notifications.spam_prevention:
+    class: Drupal\page_notifications\Service\SpamPrevention
+    arguments:
+      - '@config.factory'
+      - '@module_handler'
+      - '@session_manager'
+      - '@string_translation'
\ No newline at end of file
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index b9c4e0e..778183e 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -8,6 +8,7 @@ use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Mail\MailManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 
 /**
  * Configures Page Notifications settings.
@@ -28,6 +29,13 @@ class SettingsForm extends ConfigFormBase {
    */
   protected $entityTypeManager;
 
+  /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
   /**
    * Constructs a SettingsForm object.
    *
@@ -37,15 +45,19 @@ class SettingsForm extends ConfigFormBase {
    *   The mail manager service.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *  The module handler service.
    */
   public function __construct(
     ConfigFactoryInterface $config_factory,
     MailManagerInterface $mail_manager,
-    EntityTypeManagerInterface $entity_type_manager
+    EntityTypeManagerInterface $entity_type_manager,
+    ModuleHandlerInterface $module_handler
   ) {
     parent::__construct($config_factory);
     $this->mailManager = $mail_manager;
     $this->entityTypeManager = $entity_type_manager;
+    $this->moduleHandler = $module_handler;
   }
 
   /**
@@ -55,7 +67,8 @@ class SettingsForm extends ConfigFormBase {
     return new static(
       $container->get('config.factory'),
       $container->get('plugin.manager.mail'),
-      $container->get('entity_type.manager')
+      $container->get('entity_type.manager'),
+      $container->get('module_handler')
     );
   }
 
@@ -77,6 +90,7 @@ class SettingsForm extends ConfigFormBase {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildForm($form, $form_state);
     $config = $this->config('page_notifications.settings');
 
     $form['email_settings'] = [
@@ -159,6 +173,61 @@ class SettingsForm extends ConfigFormBase {
       '#default_value' => $config->get('security.cleanup_unverified') ?? TRUE,
     ];
 
+    // Add spam prevention section
+    $form['spam_prevention'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Spam Prevention'),
+      '#open' => TRUE,
+    ];
+
+    $captcha_options = [
+      'none' => $this->t('None'),
+      'math' => $this->t('Simple Math Challenge'),
+    ];
+
+    // Add reCAPTCHA option if the captcha module is installed
+    if ($this->moduleHandler->moduleExists('captcha')) {
+      $captcha_options['recaptcha'] = $this->t('reCAPTCHA');
+    }
+
+    $form['spam_prevention']['captcha_type'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Captcha Type'),
+      '#options' => $captcha_options,
+      '#default_value' => $config->get('spam_prevention.captcha_type') ?? 'none',
+      '#description' => $this->t('Select the type of spam prevention to use on the subscription form.'),
+    ];
+
+    $form['spam_prevention']['math_operator'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Math Challenge Operator'),
+      '#options' => [
+        '+' => $this->t('Addition (+)'),
+        '*' => $this->t('Multiplication (*)'),
+      ],
+      '#default_value' => $config->get('spam_prevention.math_operator') ?? '+',
+      '#description' => $this->t('Select the operator to use for the math challenge.'),
+      '#states' => [
+        'visible' => [
+          ':input[name="captcha_type"]' => ['value' => 'math'],
+        ],
+      ],
+    ];
+
+    if ($this->moduleHandler->moduleExists('captcha')) {
+      $form['spam_prevention']['use_recaptcha'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Use reCAPTCHA if available'),
+        '#default_value' => $config->get('spam_prevention.use_recaptcha') ?? FALSE,
+        '#description' => $this->t('Enable this to use reCAPTCHA if the captcha module is configured to use it.'),
+        '#states' => [
+          'visible' => [
+            ':input[name="captcha_type"]' => ['value' => 'recaptcha'],
+          ],
+        ],
+      ];
+    }
+
     return parent::buildForm($form, $form_state);
   }
 
@@ -184,6 +253,9 @@ class SettingsForm extends ConfigFormBase {
       ->set('email_templates.notification_body', $form_state->getValue('notification_body'))
       ->set('security.require_verification', $form_state->getValue('require_verification'))
       ->set('security.cleanup_unverified', $form_state->getValue('cleanup_unverified'))
+      ->set('spam_prevention.captcha_type', $form_state->getValue('captcha_type'))
+      ->set('spam_prevention.math_operator', $form_state->getValue('math_operator'))
+      ->set('spam_prevention.use_recaptcha', $form_state->getValue('use_recaptcha'))
       ->save();
 
     parent::submitForm($form, $form_state);
diff --git a/src/Form/SubscriptionForm.php b/src/Form/SubscriptionForm.php
index 5f6f288..e5a7297 100644
--- a/src/Form/SubscriptionForm.php
+++ b/src/Form/SubscriptionForm.php
@@ -7,6 +7,9 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\page_notifications\Service\NotificationManagerInterface;
+use Drupal\page_notifications\Service\SpamPrevention;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Psr\Log\LoggerInterface;
 
 /**
  * Provides a subscription form.
@@ -21,13 +24,39 @@ class SubscriptionForm extends FormBase {
   protected $notificationManager;
 
   /**
-   * Constructs a new SubscriptionForm.
+   * The spam prevention service.
+   *
+   * @var \Drupal\page_notifications\Service\SpamPrevention
+   */
+  protected $spamPrevention;
+
+  /**
+   * The config factory.
    *
-   * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager
-   *   The notification manager service.
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The logger instance.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new SubscriptionForm.
    */
-  public function __construct(NotificationManagerInterface $notification_manager) {
+  public function __construct(
+    NotificationManagerInterface $notification_manager,
+    SpamPrevention $spam_prevention,
+    ConfigFactoryInterface $config_factory,
+    LoggerInterface $logger
+  ) {
     $this->notificationManager = $notification_manager;
+    $this->spamPrevention = $spam_prevention;
+    $this->configFactory = $config_factory;
+    $this->logger = $logger;
   }
 
   /**
@@ -35,7 +64,10 @@ class SubscriptionForm extends FormBase {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('page_notifications.notification_manager')
+      $container->get('page_notifications.notification_manager'),
+      $container->get('page_notifications.spam_prevention'),
+      $container->get('config.factory'),
+      $container->get('logger.factory')->get('page_notifications')
     );
   }
 
@@ -50,7 +82,6 @@ class SubscriptionForm extends FormBase {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL) {
-    // Store the entity for use in the submit handler.
     $form_state->set('entity', $entity);
 
     $form['email'] = [
@@ -60,6 +91,47 @@ class SubscriptionForm extends FormBase {
       '#description' => $this->t('Enter your email address to receive notifications when this content is updated.'),
     ];
 
+    // Add spam prevention based on configuration
+    $config = $this->configFactory->get('page_notifications.settings');
+    $captcha_type = $config->get('spam_prevention.captcha_type');
+
+    if ($captcha_type === 'math') {
+      // Store challenge data in a hidden field to persist through form submissions
+      if (!$form_state->getUserInput()) {
+        // Only generate new challenge if this is the initial form build
+        $challenge = $this->spamPrevention->generateMathChallenge();
+
+        $form['math_challenge_data'] = [
+          '#type' => 'hidden',
+          '#value' => json_encode($challenge),
+        ];
+      }
+      else {
+        // Use existing challenge data from form input
+        $challenge = json_decode($form_state->getUserInput()['math_challenge_data'] ?? '{}', TRUE);
+      }
+
+      if (!empty($challenge)) {
+        $form['math_challenge_data'] = [
+          '#type' => 'hidden',
+          '#value' => json_encode($challenge),
+        ];
+
+        $form['math_challenge'] = [
+          '#type' => 'number',
+          '#title' => $challenge['question'],
+          '#required' => TRUE,
+          '#description' => $this->t('Please solve this simple math problem to prevent spam.'),
+        ];
+      }
+    }
+    elseif ($captcha_type === 'recaptcha' && $this->spamPrevention->isRecaptchaAvailable()) {
+      $form['captcha'] = [
+        '#type' => 'captcha',
+        '#captcha_type' => 'recaptcha/reCAPTCHA',
+      ];
+    }
+
     $form['actions'] = [
       '#type' => 'actions',
     ];
@@ -79,6 +151,22 @@ class SubscriptionForm extends FormBase {
     if (!filter_var($form_state->getValue('email'), FILTER_VALIDATE_EMAIL)) {
       $form_state->setErrorByName('email', $this->t('Please enter a valid email address.'));
     }
+
+    // Validate math challenge if enabled
+    $config = $this->configFactory->get('page_notifications.settings');
+    $captcha_type = $config->get('spam_prevention.captcha_type');
+
+    if ($captcha_type === 'math') {
+      $challenge_data = $form_state->getValue('math_challenge_data');
+      if ($challenge_data) {
+        $challenge = json_decode($challenge_data, TRUE);
+        $response = $form_state->getValue('math_challenge');
+
+        if (!$this->spamPrevention->validateMathResponse($response, $challenge)) {
+          $form_state->setErrorByName('math_challenge', $this->t('The answer to the math challenge is incorrect.'));
+        }
+      }
+    }
   }
 
   /**
@@ -94,6 +182,7 @@ class SubscriptionForm extends FormBase {
     }
     catch (\Exception $e) {
       $this->messenger()->addError($this->t('There was a problem creating your subscription. Please try again later.'));
+      $this->logger->error('Subscription creation failed: @message', ['@message' => $e->getMessage()]);
     }
   }
 
diff --git a/src/Service/SpamPrevention.php b/src/Service/SpamPrevention.php
new file mode 100644
index 0000000..c063d3b
--- /dev/null
+++ b/src/Service/SpamPrevention.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\page_notifications\Service;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Session\SessionManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+
+/**
+ * Service for handling spam prevention in Page Notifications.
+ */
+class SpamPrevention {
+  use StringTranslationTrait;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The session manager.
+   *
+   * @var \Drupal\Core\Session\SessionManagerInterface
+   */
+  protected $sessionManager;
+
+  /**
+   * Constructs a new SpamPrevention object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Session\SessionManagerInterface $session_manager
+   *   The session manager.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   */
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    ModuleHandlerInterface $module_handler,
+    SessionManagerInterface $session_manager,
+    TranslationInterface $string_translation
+  ) {
+    $this->configFactory = $config_factory;
+    $this->moduleHandler = $module_handler;
+    $this->sessionManager = $session_manager;
+    $this->setStringTranslation($string_translation);
+  }
+
+  /**
+   * Generates a math challenge.
+   *
+   * @return array
+   *   An array containing the challenge question, numbers, operator and answer.
+   */
+  public function generateMathChallenge() {
+    $config = $this->configFactory->get('page_notifications.settings');
+    $operator = $config->get('spam_prevention.math_operator') ?? '+';
+
+    // Generate two random numbers between 1 and 10
+    $num1 = rand(1, 10);
+    $num2 = rand(1, 10);
+
+    // Calculate the answer
+    $answer = $operator === '+' ? $num1 + $num2 : $num1 * $num2;
+
+    return [
+      'num1' => $num1,
+      'num2' => $num2,
+      'operator' => $operator,
+      'question' => $this->t('What is @num1 @operator @num2?', [
+        '@num1' => $num1,
+        '@operator' => $operator,
+        '@num2' => $num2,
+      ]),
+      'answer' => $answer,
+    ];
+  }
+
+  /**
+   * Validates a math challenge response.
+   *
+   * @param int $response
+   *   The user's response to the challenge.
+   * @param array $challenge
+   *   The original challenge array containing num1, num2, and operator.
+   *
+   * @return bool
+   *   TRUE if the response is correct, FALSE otherwise.
+   */
+  public function validateMathResponse($response, array $challenge) {
+    $answer = $challenge['operator'] === '+'
+      ? $challenge['num1'] + $challenge['num2']
+      : $challenge['num1'] * $challenge['num2'];
+
+    return (int) $response === (int) $answer;
+  }
+  /**
+   * Checks if reCAPTCHA is available and configured.
+   *
+   * @return bool
+   *   TRUE if reCAPTCHA is available and configured, FALSE otherwise.
+   */
+  public function isRecaptchaAvailable() {
+    return $this->moduleHandler->moduleExists('captcha') &&
+           $this->moduleHandler->moduleExists('recaptcha') &&
+           $this->configFactory->get('recaptcha.settings')->get('site_key');
+  }
+}
\ No newline at end of file
-- 
GitLab


From 7aa325dadcb90d88c21a36f444d1c94bb9bb3c02 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Wed, 8 Jan 2025 12:14:01 -0500
Subject: [PATCH 09/49] Add the ability for site owners to customize the block
 and twig template for more advanced devs

---
 page_notifications.module                     |  12 ++
 src/Plugin/Block/SubscriptionBlock.php        | 111 +++++++++++++++++-
 ...-page-notifications-subscription.html.twig |  38 ++++++
 3 files changed, 160 insertions(+), 1 deletion(-)
 create mode 100644 templates/block--page-notifications-subscription.html.twig

diff --git a/page_notifications.module b/page_notifications.module
index 0bebf6a..8176a38 100644
--- a/page_notifications.module
+++ b/page_notifications.module
@@ -101,4 +101,16 @@ function page_notifications_node_form_submit($form, FormStateInterface $form_sta
  */
 function page_notifications_cron() {
   \Drupal::service('page_notifications.cron_manager')->processCron();
+}
+
+/**
+ * Implements hook_theme().
+ */
+function page_notifications_theme() {
+  return [
+    'block__page_notifications_subscription' => [
+      'template' => 'block--page-notifications-subscription',
+      'base hook' => 'block',
+    ],
+  ];
 }
\ No newline at end of file
diff --git a/src/Plugin/Block/SubscriptionBlock.php b/src/Plugin/Block/SubscriptionBlock.php
index 3613d97..8b2df1a 100644
--- a/src/Plugin/Block/SubscriptionBlock.php
+++ b/src/Plugin/Block/SubscriptionBlock.php
@@ -10,6 +10,7 @@ use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Form\FormStateInterface;
 
 /**
  * Provides a subscription block.
@@ -87,6 +88,91 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'block_description' => $this->t('Subscribe to receive notifications when this page is updated.'),
+      'button_text' => $this->t('Subscribe'),
+      'button_classes' => 'button button--primary',
+      'form_classes' => 'subscription-form',
+      'show_description' => TRUE,
+    ] + parent::defaultConfiguration();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function blockForm($form, FormStateInterface $form_state) {
+    $form = parent::blockForm($form, $form_state);
+    $config = $this->getConfiguration();
+
+    $form['appearance'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Appearance Settings'),
+      '#open' => TRUE,
+    ];
+
+    $form['appearance']['show_description'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Show block description'),
+      '#default_value' => $config['show_description'],
+    ];
+
+    $form['appearance']['block_description'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Block Description'),
+      '#description' => $this->t('The text shown above the subscription form.'),
+      '#default_value' => $config['block_description'],
+      '#states' => [
+        'visible' => [
+          ':input[name="settings[appearance][show_description]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+
+    $form['appearance']['button_text'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Button Text'),
+      '#description' => $this->t('The text shown on the subscribe button.'),
+      '#default_value' => $config['button_text'],
+    ];
+
+    $form['styling'] = [
+      '#type' => 'details',
+      '#title' => $this->t('CSS Classes'),
+      '#open' => TRUE,
+    ];
+
+    $form['styling']['button_classes'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Button Classes'),
+      '#description' => $this->t('CSS classes to add to the subscribe button (space-separated).'),
+      '#default_value' => $config['button_classes'],
+    ];
+
+    $form['styling']['form_classes'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Form Classes'),
+      '#description' => $this->t('CSS classes to add to the subscription form wrapper (space-separated).'),
+      '#default_value' => $config['form_classes'],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function blockSubmit($form, FormStateInterface $form_state) {
+    $this->configuration['block_description'] = $form_state->getValue(['appearance', 'block_description']);
+    $this->configuration['button_text'] = $form_state->getValue(['appearance', 'button_text']);
+    $this->configuration['button_classes'] = $form_state->getValue(['styling', 'button_classes']);
+    $this->configuration['form_classes'] = $form_state->getValue(['styling', 'form_classes']);
+    $this->configuration['show_description'] = $form_state->getValue(['appearance', 'show_description']);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -97,7 +183,30 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
         return [];
       }
 
-      return $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node);
+      $form = $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node);
+
+      // Add our custom configuration to the form
+      $form['#attributes']['class'][] = $this->configuration['form_classes'];
+      $form['actions']['submit']['#value'] = $this->configuration['button_text'];
+      $form['actions']['submit']['#attributes']['class'] = explode(' ', $this->configuration['button_classes']);
+
+      $build = [];
+
+      if ($this->configuration['show_description']) {
+        $build['description'] = [
+          '#type' => 'html_tag',
+          '#tag' => 'p',
+          '#value' => $this->configuration['block_description'],
+        ];
+      }
+
+      $build['form'] = $form;
+
+      // Add cache contexts and tags
+      $build['#cache']['contexts'][] = 'url.path';
+      $build['#cache']['tags'][] = 'node:' . $node->id();
+
+      return $build;
     }
     catch (\Exception $e) {
       $this->loggerFactory->get('page_notifications')->error(
diff --git a/templates/block--page-notifications-subscription.html.twig b/templates/block--page-notifications-subscription.html.twig
new file mode 100644
index 0000000..6d02b7f
--- /dev/null
+++ b/templates/block--page-notifications-subscription.html.twig
@@ -0,0 +1,38 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a Page Notifications subscription block.
+ *
+ * Available variables:
+ * - plugin_id: The ID of the block implementation.
+ * - label: The configured label of the block if visible.
+ * - configuration: A list of the block's configuration values.
+ *   - block_description: The configured description text.
+ *   - button_text: The configured button text.
+ *   - button_classes: CSS classes for the button.
+ *   - form_classes: CSS classes for the form wrapper.
+ * - content: The content of the block.
+ * - attributes: HTML attributes for the block wrapper.
+ *
+ * @ingroup themeable
+ */
+#}
+{% set classes = [
+  'block',
+  'block-' ~ configuration.provider|clean_class,
+  'block-' ~ plugin_id|clean_class,
+]%}
+
+<div{{ attributes.addClass(classes) }}>
+  {{ title_prefix }}
+  {% if label %}
+    <h2{{ title_attributes }}>{{ label }}</h2>
+  {% endif %}
+  {{ title_suffix }}
+
+  {% block content %}
+    <div{{ content_attributes.addClass('content') }}>
+      {{ content }}
+    </div>
+  {% endblock %}
+</div>
\ No newline at end of file
-- 
GitLab


From 686cf56cb8b619c48193f208b14081d75242c8be Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Wed, 8 Jan 2025 13:31:40 -0500
Subject: [PATCH 10/49] Remove bad duplicate causing install problem

---
 page_notifications.install | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/page_notifications.install b/page_notifications.install
index 158e38e..0b5d7b2 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -32,15 +32,6 @@ function page_notifications_schema() {
   return $schema;
 }
 
-/**
- * Implements hook_schema().
- */
-function page_notifications_schema() {
-  $schema = [];
-  // Add any additional database tables needed beyond entities
-  return $schema;
-}
-
 /**
  * Implements hook_uninstall().
  */
-- 
GitLab


From 330002dbf4b854a2830a92493e41804170f80cea Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Wed, 8 Jan 2025 14:09:57 -0500
Subject: [PATCH 11/49] Simply settings around auto cleanup and token
 expiration. Add a settings form to manually add subscriptions if people have
 lists of interested parties

---
 .../install/page_notifications.settings.yml   |   1 -
 config/schema/page_notifications.schema.yml   |   2 +-
 page_notifications.routing.yml                |   8 +
 src/Controller/SubscriptionListController.php |  27 ++-
 src/Form/ManualSubscriptionAddForm.php        | 190 ++++++++++++++++++
 src/Form/SettingsForm.php                     |  10 +-
 src/Service/CronManager.php                   |   8 +-
 7 files changed, 228 insertions(+), 18 deletions(-)
 create mode 100644 src/Form/ManualSubscriptionAddForm.php

diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml
index 2018f37..65db4b3 100644
--- a/config/install/page_notifications.settings.yml
+++ b/config/install/page_notifications.settings.yml
@@ -35,7 +35,6 @@ email_templates:
 
 security:
   require_verification: 1
-  cleanup_unverified: 1
 
 spam_protection:
   enable_modal: 0
diff --git a/config/schema/page_notifications.schema.yml b/config/schema/page_notifications.schema.yml
index e054974..71f230b 100644
--- a/config/schema/page_notifications.schema.yml
+++ b/config/schema/page_notifications.schema.yml
@@ -13,7 +13,7 @@ page_notifications.settings:
           label: 'Email template'
         token_expiration:
           type: integer
-          label: 'Token expiration time in hours'
+          label: 'Token expiration time in hours (0 = never expire)'
     spam_prevention:
       type: mapping
       label: 'Spam Prevention Settings'
diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index 5ce0a71..1852d5b 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -48,5 +48,13 @@ page_notifications.subscription_migrate:
   defaults:
     _form: '\Drupal\page_notifications\Form\SubscriptionMigrateForm'
     _title: 'Migrate Subscriptions'
+  requirements:
+    _permission: 'administer page notification subscriptions'
+
+page_notifications.subscription_add:
+  path: '/admin/config/system/page-notifications/subscriptions/add'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\ManualSubscriptionAddForm'
+    _title: 'Add Subscriptions'
   requirements:
     _permission: 'administer page notification subscriptions'
\ No newline at end of file
diff --git a/src/Controller/SubscriptionListController.php b/src/Controller/SubscriptionListController.php
index fa6946d..ab1bd0b 100644
--- a/src/Controller/SubscriptionListController.php
+++ b/src/Controller/SubscriptionListController.php
@@ -3,6 +3,7 @@
 namespace Drupal\page_notifications\Controller;
 
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Url;
 
 /**
  * Controller for the subscription list page.
@@ -16,11 +17,29 @@ class SubscriptionListController extends ControllerBase {
    *   A render array for the view.
    */
   public function content() {
-    $view = views_embed_view('page_notification_subscriptions', 'default');
-    return [
-      '#type' => 'container',
-      'view' => $view,
+    // Add the "Add Subscription" button
+    $build['add_form'] = [
+      '#type' => 'link',
+      '#title' => $this->t('Add Subscription'),
+      '#url' => Url::fromRoute('page_notifications.subscription_add'),
+      '#attributes' => [
+        'class' => ['button', 'button--action', 'button--primary'],
+      ],
     ];
+
+    // Add some spacing after the button
+    $build['spacing'] = [
+      '#type' => 'html_tag',
+      '#tag' => 'div',
+      '#attributes' => [
+        'style' => 'margin: 1em 0;',
+      ],
+    ];
+
+    // Add the view
+    $build['view'] = views_embed_view('page_notification_subscriptions', 'default');
+
+    return $build;
   }
 
 }
\ No newline at end of file
diff --git a/src/Form/ManualSubscriptionAddForm.php b/src/Form/ManualSubscriptionAddForm.php
new file mode 100644
index 0000000..f5386b6
--- /dev/null
+++ b/src/Form/ManualSubscriptionAddForm.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Component\Utility\EmailValidatorInterface;
+
+/**
+ * Form for manually adding subscriptions.
+ */
+class ManualSubscriptionAddForm extends FormBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The messenger service.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * The email validator.
+   *
+   * @var \Drupal\Component\Utility\EmailValidatorInterface
+   */
+  protected $emailValidator;
+
+  /**
+   * Constructs a new ManualSubscriptionAddForm.
+   */
+  public function __construct(
+    EntityTypeManagerInterface $entity_type_manager,
+    MessengerInterface $messenger,
+    EmailValidatorInterface $email_validator
+  ) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->messenger = $messenger;
+    $this->emailValidator = $email_validator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('messenger'),
+      $container->get('email.validator')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_manual_subscription_add';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['node'] = [
+      '#type' => 'entity_autocomplete',
+      '#title' => $this->t('Content'),
+      '#description' => $this->t('Select the content to subscribe to.'),
+      '#target_type' => 'node',
+      '#required' => TRUE,
+      '#selection_handler' => 'default:node_enhanced',
+      '#selection_settings' => [
+        'target_bundles' => NULL,
+      ],
+    ];
+
+    $form['emails'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Email Addresses'),
+      '#description' => $this->t('Enter email addresses, one per line. These subscribers will be automatically verified.'),
+      '#required' => TRUE,
+      '#rows' => 10,
+    ];
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Add Subscriptions'),
+      '#button_type' => 'primary',
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $emails = explode("\n", $form_state->getValue('emails'));
+    $invalid_emails = [];
+
+    foreach ($emails as $email) {
+      $email = trim($email);
+      if (!empty($email) && !$this->emailValidator->isValid($email)) {
+        $invalid_emails[] = $email;
+      }
+    }
+
+    if (!empty($invalid_emails)) {
+      $form_state->setErrorByName('emails', $this->t('The following email addresses are invalid: @emails', [
+        '@emails' => implode(', ', $invalid_emails),
+      ]));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $node_id = $form_state->getValue('node');
+    $emails = array_filter(array_map('trim', explode("\n", $form_state->getValue('emails'))));
+    $added = 0;
+    $skipped = 0;
+
+    try {
+      foreach ($emails as $email) {
+        if (empty($email)) {
+          continue;
+        }
+
+        // Check for existing subscription
+        $existing = $this->entityTypeManager
+          ->getStorage('page_notification_subscription')
+          ->loadByProperties([
+            'email' => $email,
+            'subscribed_entity_id' => $node_id,
+            'subscribed_entity_type' => 'node',
+          ]);
+
+        if (!empty($existing)) {
+          $skipped++;
+          continue;
+        }
+
+        // Create new subscription
+        $subscription = $this->entityTypeManager
+          ->getStorage('page_notification_subscription')
+          ->create([
+            'email' => $email,
+            'subscribed_entity_id' => $node_id,
+            'subscribed_entity_type' => 'node',
+            'token' => bin2hex(random_bytes(32)),
+            'unsubscribe_token' => bin2hex(random_bytes(32)),
+            'status' => TRUE, // Automatically verified
+          ]);
+
+        $subscription->save();
+        $added++;
+      }
+
+      if ($added > 0) {
+        $this->messenger->addStatus($this->t('Successfully added @count subscription(s).', [
+          '@count' => $added,
+        ]));
+      }
+
+      if ($skipped > 0) {
+        $this->messenger->addWarning($this->t('Skipped @count existing subscription(s).', [
+          '@count' => $skipped,
+        ]));
+      }
+    }
+    catch (\Exception $e) {
+      $this->messenger->addError($this->t('An error occurred while adding subscriptions.'));
+      $this->getLogger('page_notifications')->error($e->getMessage());
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index 778183e..a65c39d 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -109,7 +109,7 @@ class SettingsForm extends ConfigFormBase {
     $form['email_settings']['token_expiration'] = [
       '#type' => 'number',
       '#title' => $this->t('Token Expiration'),
-      '#description' => $this->t('Number of hours before verification tokens expire.'),
+      '#description' => $this->t('Number of hours before verification tokens expire. Enter 0 to never expire unverified subscriptions.'),
       '#default_value' => $config->get('notification_settings.token_expiration') ?? 48,
       '#min' => 1,
       '#required' => TRUE,
@@ -166,13 +166,6 @@ class SettingsForm extends ConfigFormBase {
       '#default_value' => $config->get('security.require_verification') ?? TRUE,
     ];
 
-    $form['security']['cleanup_unverified'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Clean Up Unverified Subscriptions'),
-      '#description' => $this->t('Automatically delete unverified subscriptions after they expire.'),
-      '#default_value' => $config->get('security.cleanup_unverified') ?? TRUE,
-    ];
-
     // Add spam prevention section
     $form['spam_prevention'] = [
       '#type' => 'details',
@@ -252,7 +245,6 @@ class SettingsForm extends ConfigFormBase {
       ->set('email_templates.notification_subject', $form_state->getValue('notification_subject'))
       ->set('email_templates.notification_body', $form_state->getValue('notification_body'))
       ->set('security.require_verification', $form_state->getValue('require_verification'))
-      ->set('security.cleanup_unverified', $form_state->getValue('cleanup_unverified'))
       ->set('spam_prevention.captcha_type', $form_state->getValue('captcha_type'))
       ->set('spam_prevention.math_operator', $form_state->getValue('math_operator'))
       ->set('spam_prevention.use_recaptcha', $form_state->getValue('use_recaptcha'))
diff --git a/src/Service/CronManager.php b/src/Service/CronManager.php
index df6f561..af92968 100644
--- a/src/Service/CronManager.php
+++ b/src/Service/CronManager.php
@@ -118,10 +118,12 @@ class CronManager {
    * Clean up expired unverified subscriptions.
    */
   protected function cleanupExpiredSubscriptions() {
-    if ($this->configFactory->get('page_notifications.settings')->get('security.cleanup_unverified')) {
+    $config = $this->configFactory->get('page_notifications.settings');
+    $expiration_hours = $config->get('notification_settings.token_expiration');
+
+    // Only cleanup if expiration is set (greater than 0)
+    if ($expiration_hours > 0) {
       $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
-      $config = $this->configFactory->get('page_notifications.settings');
-      $expiration_hours = $config->get('notification_settings.token_expiration') ?? 48;
 
       $expired_ids = $storage->getQuery()
         ->condition('status', FALSE)
-- 
GitLab


From bb23bde681020ab1982ed517e58997dddfec8479 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Thu, 9 Jan 2025 13:40:53 -0500
Subject: [PATCH 12/49] Add a view to see top subscribed content

---
 ...s.view.page_notification_subscriptions.yml |   1 -
 .../views.view.top_subscribed_content.yml     | 326 ++++++++++++++++++
 page_notifications.links.task.yml             |   6 +
 page_notifications.permissions.yml            |   6 +-
 page_notifications.routing.yml                |   8 +
 src/Controller/TopSubscribedController.php    |  26 ++
 src/Entity/SubscriptionViewsData.php          | 117 ++++---
 7 files changed, 433 insertions(+), 57 deletions(-)
 create mode 100644 config/install/views.view.top_subscribed_content.yml
 create mode 100644 src/Controller/TopSubscribedController.php

diff --git a/config/install/views.view.page_notification_subscriptions.yml b/config/install/views.view.page_notification_subscriptions.yml
index 1ef06fb..34b1d81 100644
--- a/config/install/views.view.page_notification_subscriptions.yml
+++ b/config/install/views.view.page_notification_subscriptions.yml
@@ -1,4 +1,3 @@
-uuid: 80c6ba5b-2a8e-4fe3-b834-2e7abe4607c9
 langcode: en
 status: true
 dependencies:
diff --git a/config/install/views.view.top_subscribed_content.yml b/config/install/views.view.top_subscribed_content.yml
new file mode 100644
index 0000000..c362597
--- /dev/null
+++ b/config/install/views.view.top_subscribed_content.yml
@@ -0,0 +1,326 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - node
+    - page_notifications
+id: top_subscribed_content
+label: 'Top Subscribed Content'
+module: views
+description: ''
+tag: ''
+base_table: page_notification_subscription
+base_field: id
+display:
+  default:
+    id: default
+    display_title: Default
+    display_plugin: default
+    position: 0
+    display_options:
+      title: 'Top Subscribed Content'
+      fields:
+        title:
+          id: title
+          table: node_field_data
+          field: title
+          relationship: subscribed_entity
+          group_type: group
+          admin_label: ''
+          entity_type: node
+          entity_field: title
+          plugin_id: field
+          label: Title
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        id:
+          id: id
+          table: page_notification_subscription
+          field: id
+          relationship: none
+          group_type: count
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: id
+          plugin_id: field
+          label: 'Subscription Count'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+      pager:
+        type: mini
+        options:
+          offset: 0
+          pagination_heading_level: h4
+          items_per_page: 10
+          total_pages: null
+          id: 0
+          tags:
+            next: ››
+            previous: ‹‹
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      access:
+        type: none
+        options: {  }
+      cache:
+        type: tag
+        options: {  }
+      empty: {  }
+      sorts: {  }
+      arguments: {  }
+      filters:
+        status:
+          id: status
+          table: page_notification_subscription
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: status
+          plugin_id: boolean
+          operator: '='
+          value: '1'
+          group: 1
+          exposed: false
+          expose:
+            operator_id: ''
+            label: ''
+            description: ''
+            use_operator: false
+            operator: ''
+            operator_limit_selection: false
+            operator_list: {  }
+            identifier: ''
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          columns:
+            title: title
+            id: id
+            title_1: title_1
+          default: id
+          info:
+            title:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            id:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            title_1:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          override: true
+          sticky: false
+          summary: ''
+          empty_table: false
+          caption: ''
+          description: ''
+      row:
+        type: fields
+      query:
+        type: views_query
+        options:
+          query_comment: ''
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_tags: {  }
+      relationships:
+        subscribed_entity:
+          id: subscribed_entity
+          table: page_notification_subscription
+          field: subscribed_entity
+          relationship: none
+          group_type: group
+          admin_label: 'Subscribed Node'
+          entity_type: page_notification_subscription
+          plugin_id: standard
+          required: true
+      group_by: true
+      header: {  }
+      footer: {  }
+      display_extenders:
+        simple_sitemap_display_extender: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+      tags: {  }
+  page_1:
+    id: page_1
+    display_title: Page
+    display_plugin: page
+    position: 1
+    display_options:
+      display_extenders:
+        simple_sitemap_display_extender:
+          variants: {  }
+      path: admin/content/top-subscribed
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+      tags: {  }
diff --git a/page_notifications.links.task.yml b/page_notifications.links.task.yml
index bf3e39e..820c815 100644
--- a/page_notifications.links.task.yml
+++ b/page_notifications.links.task.yml
@@ -20,4 +20,10 @@ page_notifications.subscription_migrate:
   route_name: page_notifications.subscription_migrate
   title: 'Migrate Subscriptions'
   base_route: page_notifications.admin_settings
+  weight: 4
+
+page_notifications.top_subscribed:
+  route_name: page_notifications.top_subscribed
+  title: 'Top Subscribed Content'
+  base_route: page_notifications.admin_settings
   weight: 3
\ No newline at end of file
diff --git a/page_notifications.permissions.yml b/page_notifications.permissions.yml
index 8216ba5..ff8c147 100644
--- a/page_notifications.permissions.yml
+++ b/page_notifications.permissions.yml
@@ -21,4 +21,8 @@ delete page notification subscriptions:
 
 view subscription list:
   title: 'View subscription list'
-  description: 'Access the subscription list view'
\ No newline at end of file
+  description: 'Access the subscription list view'
+
+view top subscribed content:
+  title: 'View top subscribed content'
+  description: 'Access the top subscribed content list'
\ No newline at end of file
diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index 1852d5b..017d6ae 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -43,6 +43,14 @@ page_notifications.subscription_list:
   requirements:
     _permission: 'view subscription list'
 
+page_notifications.top_subscribed:
+  path: '/admin/content/top-subscribed'
+  defaults:
+    _controller: '\Drupal\page_notifications\Controller\TopSubscribedController::content'
+    _title: 'Top Subscribed Content'
+  requirements:
+    _permission: 'view subscription list'
+
 page_notifications.subscription_migrate:
   path: '/admin/config/system/page-notifications/migrate'
   defaults:
diff --git a/src/Controller/TopSubscribedController.php b/src/Controller/TopSubscribedController.php
new file mode 100644
index 0000000..a97bbcb
--- /dev/null
+++ b/src/Controller/TopSubscribedController.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\page_notifications\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Controller for the top subscribed content page.
+ */
+class TopSubscribedController extends ControllerBase {
+
+  /**
+   * Displays the top subscribed content view.
+   *
+   * @return array
+   *   A render array for the view.
+   */
+  public function content() {
+    $view = views_embed_view('top_subscribed_content', 'default');
+    return [
+      '#type' => 'container',
+      'view' => $view,
+    ];
+  }
+
+}
\ No newline at end of file
diff --git a/src/Entity/SubscriptionViewsData.php b/src/Entity/SubscriptionViewsData.php
index 8257cc9..8f51508 100644
--- a/src/Entity/SubscriptionViewsData.php
+++ b/src/Entity/SubscriptionViewsData.php
@@ -23,6 +23,7 @@ class SubscriptionViewsData extends EntityViewsData {
       'weight' => -10,
     ];
 
+
     // Define the relationship to nodes
     $data['page_notification_subscription']['subscribed_entity'] = [
       'title' => $this->t('Subscribed Node'),
@@ -36,64 +37,70 @@ class SubscriptionViewsData extends EntityViewsData {
       ],
     ];
 
-    // Add specific field handlers
-    $data['page_notification_subscription']['subscribed_entity_id'] = [
-      'title' => $this->t('Subscribed Entity ID'),
-      'help' => $this->t('The ID of the entity being subscribed to.'),
-      'field' => [
-        'id' => 'numeric',
-      ],
-      'filter' => [
-        'id' => 'numeric',
-      ],
-      'sort' => [
-        'id' => 'standard',
-      ],
-      'argument' => [
-        'id' => 'numeric',
-      ],
-    ];
+      // ID field
+      $data['page_notification_subscription']['subscribed_entity_id'] = [
+        'title' => $this->t('Subscribed Entity ID'),
+        'help' => $this->t('The ID of the entity being subscribed to.'),
+        'field' => [
+          'id' => 'numeric',
+        ],
+        'filter' => [
+          'id' => 'numeric',
+        ],
+        'sort' => [
+          'id' => 'standard',
+        ],
+        'argument' => [
+          'id' => 'numeric',
+        ],
+      ];
 
-    // Status field
-    $data['page_notification_subscription']['status']['field'] = [
-      'title' => $this->t('Status'),
-      'help' => $this->t('The status of the subscription.'),
-      'id' => 'boolean',
-    ];
-    $data['page_notification_subscription']['status']['filter'] = [
-      'id' => 'boolean',
-      'label' => $this->t('Status'),
-      'type' => 'yes-no',
-    ];
-    $data['page_notification_subscription']['status']['sort'] = [
-      'id' => 'standard',
-    ];
+      // Status field
+      $data['page_notification_subscription']['status'] = [
+        'title' => $this->t('Status'),
+        'help' => $this->t('The status of the subscription.'),
+        'field' => [
+          'id' => 'boolean',
+        ],
+        'filter' => [
+          'id' => 'boolean',
+          'label' => $this->t('Status'),
+          'type' => 'yes-no',
+        ],
+        'sort' => [
+          'id' => 'standard',
+        ],
+      ];
 
-    // Email field
-    $data['page_notification_subscription']['email']['field'] = [
-      'title' => $this->t('Email'),
-      'help' => $this->t('The email address of the subscriber.'),
-      'id' => 'standard',
-    ];
-    $data['page_notification_subscription']['email']['filter'] = [
-      'id' => 'string',
-    ];
-    $data['page_notification_subscription']['email']['sort'] = [
-      'id' => 'standard',
-    ];
+      // Email field
+      $data['page_notification_subscription']['email'] = [
+        'title' => $this->t('Email'),
+        'help' => $this->t('The email address of the subscriber.'),
+        'field' => [
+          'id' => 'standard',
+        ],
+        'filter' => [
+          'id' => 'string',
+        ],
+        'sort' => [
+          'id' => 'standard',
+        ],
+      ];
 
-    // Created timestamp
-    $data['page_notification_subscription']['created']['field'] = [
-      'title' => $this->t('Created'),
-      'help' => $this->t('When the subscription was created.'),
-      'id' => 'date',
-    ];
-    $data['page_notification_subscription']['created']['filter'] = [
-      'id' => 'date',
-    ];
-    $data['page_notification_subscription']['created']['sort'] = [
-      'id' => 'date',
-    ];
+      // Created field
+      $data['page_notification_subscription']['created'] = [
+        'title' => $this->t('Created'),
+        'help' => $this->t('When the subscription was created.'),
+        'field' => [
+          'id' => 'date',
+        ],
+        'filter' => [
+          'id' => 'date',
+        ],
+        'sort' => [
+          'id' => 'date',
+        ],
+      ];
 
     // Operations
     $data['page_notification_subscription']['operations'] = [
-- 
GitLab


From 7f4e8004ae3e75b9bfd5a72cde5fc1a57741334e Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Thu, 9 Jan 2025 15:28:45 -0500
Subject: [PATCH 13/49] A migration to transfer subs from v3 to v4

---
 page_notifications.install       |  79 ++++++++++++++
 page_notifications.services.yml  |  11 +-
 src/Service/MigrationService.php | 182 +++++++++++++++++++++++++++++++
 upgrade-docs.md                  |  31 ++++++
 4 files changed, 302 insertions(+), 1 deletion(-)
 create mode 100644 src/Service/MigrationService.php
 create mode 100644 upgrade-docs.md

diff --git a/page_notifications.install b/page_notifications.install
index 0b5d7b2..3333b3b 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -38,4 +38,83 @@ function page_notifications_schema() {
 function page_notifications_uninstall() {
   // Remove configuration.
   \Drupal::configFactory()->getEditable('page_notifications.settings')->delete();
+   // Clean up state
+   \Drupal::state()->delete('page_notifications_v3_backup');
+}
+
+/**
+ * Install new schema, views, default configuration, and migrate subscriptions from v3 to v4.
+ */
+function page_notifications_update_10001(&$sandbox) {
+  // First ensure the new schema is installed
+  $entity_type = \Drupal::entityTypeManager()->getDefinition('page_notification_subscription');
+  \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type);
+
+  // Install required views and configuration
+  $module_path = \Drupal::service('extension.list.module')->getPath('page_notifications');
+  $source = new \Drupal\Core\Config\FileStorage($module_path . '/config/install');
+
+  $config_storage = \Drupal::service('config.storage');
+
+  // List of all configurations to install
+  $configs = [
+    // Views
+    'views.view.page_notification_subscriptions',
+    'views.view.top_subscribed_content',
+    // Settings and templates
+    'page_notifications.settings',
+  ];
+
+  foreach ($configs as $config_name) {
+    $config_record = $source->read($config_name);
+    if (is_array($config_record)) {
+      $config_storage->write($config_name, $config_record);
+      \Drupal::logger('page_notifications')->notice('Installed configuration: @config', ['@config' => $config_name]);
+    }
+    else {
+      \Drupal::logger('page_notifications')->error('Failed to read configuration: @config', ['@config' => $config_name]);
+    }
+  }
+
+  // Set default email templates and settings
+  $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
+  $config
+    ->set('email_templates.verification_subject', 'Verify your subscription to [node:title]')
+    ->set('email_templates.verification_body', 'Hello,
+
+Please verify your email subscription to the page "[node:title]".
+Click the following link to confirm your subscription:
+[subscription:verify-url]
+
+This verification link will expire soon.
+Please verify your subscription promptly.
+
+If you did not request this subscription, please ignore this email.')
+    ->set('email_templates.notification_subject', '[node:title] has been updated')
+    ->set('email_templates.notification_body', 'Dear subscriber,
+
+The page "[node:title]" that you are subscribed to has been updated.
+
+[notification:notes]
+
+You can view the updated page here:
+[node:url]
+
+To unsubscribe from these notifications, click here:
+[subscription:unsubscribe-url]
+
+Regards,
+[site:name] team')
+    ->set('security.require_verification', 1)
+    ->set('spam_protection.enable_math_captcha', 1)
+    ->set('spam_protection.math_captcha_operator', '+')
+    ->save();
+
+  // Migrate subscriptions
+  $batch = \Drupal\page_notifications\Service\MigrationService::createMigrationBatch();
+  if ($batch) {
+    batch_set($batch);
+  }
+
+  return t('Subscription data has been migrated, views and default configuration have been installed. Please review your Page Notifications settings at /admin/config/system/page-notifications');
 }
\ No newline at end of file
diff --git a/page_notifications.services.yml b/page_notifications.services.yml
index 75b4271..7cb7d3f 100644
--- a/page_notifications.services.yml
+++ b/page_notifications.services.yml
@@ -48,4 +48,13 @@ services:
       - '@config.factory'
       - '@module_handler'
       - '@session_manager'
-      - '@string_translation'
\ No newline at end of file
+      - '@string_translation'
+  page_notifications.migration:
+    class: Drupal\page_notifications\Service\MigrationService
+    arguments:
+      - '@database'
+      - '@entity_type.manager'
+      - '@config.factory'
+      - '@state'
+      - '@logger.factory'
+      - '@datetime.time'
\ No newline at end of file
diff --git a/src/Service/MigrationService.php b/src/Service/MigrationService.php
new file mode 100644
index 0000000..6500a3f
--- /dev/null
+++ b/src/Service/MigrationService.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace Drupal\page_notifications\Service;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+
+/**
+ * Service for migrating subscriptions from Page Notifications v3 to v4.
+ */
+class MigrationService {
+  use StringTranslationTrait;
+  use DependencySerializationTrait;
+
+  /**
+   * Creates a batch for migrating subscriptions.
+   *
+   * @return array
+   *   The batch definition.
+   */
+  public static function createMigrationBatch() {
+    // Get total count of subscriptions to migrate
+    $count = \Drupal::database()->select('node', 'n')
+      ->condition('n.type', 'page_notify_subscriptions')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+
+    if (!$count) {
+      \Drupal::logger('page_notifications')->notice('No v3 subscriptions found to migrate.');
+      return NULL;
+    }
+
+    \Drupal::logger('page_notifications')->notice('Found @count v3 subscriptions to migrate.', ['@count' => $count]);
+
+    $batch = [
+      'title' => t('Migrating Page Notifications subscriptions'),
+      'init_message' => t('Starting subscription migration...'),
+      'progress_message' => t('Processed @current out of @total subscriptions.'),
+      'error_message' => t('Error occurred during migration.'),
+      'operations' => [],
+      'finished' => [static::class, 'migrationFinished'],
+    ];
+
+    // Process subscriptions in batches of 25
+    for ($i = 0; $i < $count; $i += 25) {
+      $batch['operations'][] = [
+        [static::class, 'migrateSubscriptionsBatch'],
+        [$i, min(25, $count - $i)]
+      ];
+    }
+
+    return $batch;
+  }
+
+  /**
+   * Migrates a batch of subscriptions.
+   */
+  public static function migrateSubscriptionsBatch($start, $limit, &$context) {
+    try {
+      $database = \Drupal::database();
+
+      // Query v3 subscriptions
+      $query = $database->select('node', 'n');
+      $query->join('node_field_data', 'nfd', 'n.nid = nfd.nid');
+      $query->fields('n', ['nid'])
+        ->fields('nfd', ['created'])
+        ->condition('n.type', 'page_notify_subscriptions')
+        ->range($start, $limit);
+
+      // Join with field tables
+      $query->join('node__field_page_notify_email', 'e', 'n.nid = e.entity_id');
+      $query->join('node__field_page_notify_node_id', 'nid', 'n.nid = nid.entity_id');
+
+      $query->fields('e', ['field_page_notify_email_value']);
+      $query->fields('nid', ['field_page_notify_node_id_value']);
+
+      $results = $query->execute();
+
+      $subscription_storage = \Drupal::entityTypeManager()->getStorage('page_notification_subscription');
+      $time = \Drupal::time()->getRequestTime();
+
+      foreach ($results as $row) {
+        // Generate new tokens
+        $verify_token = bin2hex(random_bytes(16));
+        $unsubscribe_token = bin2hex(random_bytes(32));
+
+        \Drupal::logger('page_notifications')->debug('Migrating subscription for email: @email, node: @nid', [
+          '@email' => $row->field_page_notify_email_value,
+          '@nid' => $row->field_page_notify_node_id_value,
+        ]);
+
+        // Create new v4 subscription entity
+        $subscription = $subscription_storage->create([
+          'email' => $row->field_page_notify_email_value,
+          'subscribed_entity_id' => $row->field_page_notify_node_id_value,
+          'subscribed_entity_type' => 'node',
+          'token' => $verify_token,
+          'unsubscribe_token' => $unsubscribe_token,
+          'status' => TRUE,
+          'created' => $row->created ?? $time,
+          'changed' => $time,
+          'langcode' => \Drupal::languageManager()->getDefaultLanguage()->getId(),
+        ]);
+
+        try {
+          $subscription->save();
+
+          // Update progress
+          if (!isset($context['results']['subscriptions'])) {
+            $context['results']['subscriptions'] = 0;
+          }
+          $context['results']['subscriptions']++;
+
+          \Drupal::logger('page_notifications')->debug('Successfully migrated subscription @id', [
+            '@id' => $subscription->id(),
+          ]);
+        }
+        catch (\Exception $e) {
+          \Drupal::logger('page_notifications')->error('Failed to save subscription: @error', [
+            '@error' => $e->getMessage(),
+          ]);
+        }
+      }
+
+      $context['message'] = t('Migrated @count subscriptions', [
+        '@count' => $limit,
+      ]);
+    }
+    catch (\Exception $e) {
+      \Drupal::logger('page_notifications')->error(
+        'Failed to migrate subscriptions batch: @message',
+        ['@message' => $e->getMessage()]
+      );
+      throw $e;
+    }
+  }
+
+  /**
+   * Batch finished callback.
+   */
+  public static function migrationFinished($success, $results, $operations) {
+    if ($success) {
+      // Verify migration
+      $old_count = \Drupal::database()->select('node', 'n')
+        ->condition('n.type', 'page_notify_subscriptions')
+        ->countQuery()
+        ->execute()
+        ->fetchField();
+
+      $new_count = \Drupal::entityTypeManager()
+        ->getStorage('page_notification_subscription')
+        ->getQuery()
+        ->accessCheck(FALSE)
+        ->count()
+        ->execute();
+
+      $message = t('Migration completed. Migrated @migrated subscriptions (@old v3 subscriptions, @new v4 subscriptions).', [
+        '@migrated' => $results['subscriptions'] ?? 0,
+        '@old' => $old_count,
+        '@new' => $new_count,
+      ]);
+
+      \Drupal::logger('page_notifications')->notice($message);
+      \Drupal::messenger()->addStatus($message);
+
+      if ($old_count != $new_count) {
+        $warning = t('Warning: Number of migrated subscriptions (@new) does not match original count (@old).', [
+          '@new' => $new_count,
+          '@old' => $old_count,
+        ]);
+        \Drupal::logger('page_notifications')->warning($warning);
+        \Drupal::messenger()->addWarning($warning);
+      }
+    }
+    else {
+      $message = t('Migration failed. Please check the logs for details.');
+      \Drupal::logger('page_notifications')->error($message);
+      \Drupal::messenger()->addError($message);
+    }
+  }
+}
\ No newline at end of file
diff --git a/upgrade-docs.md b/upgrade-docs.md
new file mode 100644
index 0000000..85d0107
--- /dev/null
+++ b/upgrade-docs.md
@@ -0,0 +1,31 @@
+# Upgrading from Page Notifications 3.x to 4.x
+
+## Breaking Changes
+- Complete rewrite of the module's architecture
+- New configuration system
+- New subscription storage using custom entities
+
+## Migration Process
+1. Back up your database before upgrading
+2. Install the new version over the old one
+3. Run database updates (`drush updb` or visit /update.php)
+
+## Post-Migration Steps
+After upgrading, you will need to reconfigure your Page Notifications settings:
+
+1. Visit `/admin/config/system/page-notifications`
+2. Configure the following settings:
+   - Email settings
+   - Notification templates
+   - Spam protection settings
+   - Security settings
+
+## Previous Settings
+Your previous settings from v3 will not be automatically migrated. Make note of your current settings before upgrading:
+1. Email templates
+2. From email address
+3. CAPTCHA configuration
+4. Other customizations
+
+## Subscription Data
+All subscription data (email addresses, subscribed content, tokens) will be automatically migrated to the new system.
-- 
GitLab


From 12bf3801e067acbeb61bed6bafdd587d9a01803f Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Thu, 9 Jan 2025 18:55:52 -0500
Subject: [PATCH 14/49] Update readme

---
 README.md  | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 README.txt |  90 -------------------------------------
 2 files changed, 129 insertions(+), 90 deletions(-)
 create mode 100644 README.md
 delete mode 100644 README.txt

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e56dcfb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,129 @@
+# Page Notifications
+
+A comprehensive Drupal module for managing page-level email notifications and subscriptions.
+
+## Introduction
+
+Page Notifications is a powerful module that allows anonymous and authenticated users to subscribe to specific pages and receive email notifications when content is updated. The module provides a flexible subscription system with email verification, spam protection, and comprehensive subscription management.
+
+## Features
+
+- Anonymous user subscriptions with email verification
+- Customizable email templates for verification, confirmation and notifications
+- Spam protection via reCAPTCHA or math captcha
+- Subscription management interface for users
+- Admin interface for managing subscriptions
+- Views integration for listing subscriptions and popular content
+- Token support for email templates
+- Batch subscription migration tools
+- Support for both node and taxonomy term subscriptions 
+- HTML email support
+- Subscription block with Ajax form
+- Comprehensive subscription tracking
+
+## Requirements
+
+- Drupal 10.x
+- Required modules:
+  - Node
+  - Views
+- Optional but recommended modules:
+  - reCAPTCHA 
+  - CAPTCHA
+
+## Installation
+
+1. Download and extract the Page Notifications module to your /modules directory
+2. Enable the module via the admin interface or drush:
+```bash
+drush en page_notifications
+```
+3. Configure permissions at Admin » People » Permissions
+4. Configure module settings at Admin » Structure » Page Notifications
+
+## Configuration
+
+### Basic Setup
+
+1. Go to Admin » Structure » Page Notifications » Settings
+2. Configure general settings:
+   - Enable/disable spam protection
+   - Set email verification requirements
+   - Configure notification display options
+3. Configure email templates:
+   - Verification emails
+   - Confirmation emails  
+   - Notification emails
+   - Web messages and alerts
+4. Place the subscription block in your desired region
+
+### Email Templates
+
+The module provides several customizable email templates:
+
+- Verification email - Sent when users first subscribe
+- Confirmation email - Sent after subscription is verified
+- Notification email - Sent when content is updated
+
+Templates support tokens for dynamic content:
+
+- `[notify_node_title]` - Title of subscribed content
+- `[notify_node_url]` - URL of subscribed content  
+- `[notify_user_email]` - Subscriber's email
+- `[notify_verify_url]` - Verification URL
+- `[notify_unsubscribe_url]` - Unsubscribe URL
+- `[notify_notes]` - Update notes
+
+### Spam Protection 
+
+The module provides two spam protection options:
+
+1. reCAPTCHA integration (recommended)
+   - Requires the reCAPTCHA module
+   - Configure site and secret keys
+   
+2. Math captcha
+   - Simple math problem verification
+   - Configurable operators
+
+### Subscription Management
+
+Administrators can:
+
+- View all subscriptions
+- Manage individual subscriptions
+- Migrate subscriptions between nodes
+- View subscription statistics
+- Send manual notifications
+- Export subscription data
+
+Users can:
+
+- Verify subscriptions via email
+- Manage their subscriptions
+- Unsubscribe from individual or all notifications
+- View their subscription history
+
+## API
+
+The module provides several services for programmatic integration:
+
+```php
+// Notification manager service
+\Drupal::service('page_notifications.notification_manager')
+
+// Mail handler service  
+\Drupal::service('page_notifications.mail_handler')
+
+// Spam prevention service
+\Drupal::service('page_notifications.spam_prevention')
+```
+
+## Maintainers
+
+Current maintainers:
+- Lidiya Grushetska (<lidia_ua>)
+
+## License
+
+This project is licensed under the GNU General Public License v2.0.
\ No newline at end of file
diff --git a/README.txt b/README.txt
deleted file mode 100644
index 8fe3102..0000000
--- a/README.txt
+++ /dev/null
@@ -1,90 +0,0 @@
-Page Notifications README.txt
-=================
-
-
-CONTENTS OF THIS FILE
----------------------
-
-* Introduction
-* Requirements
-* Installation
-* Configuration
-* Related projects & alternatives
-* Maintainers
-
-
-INTRODUCTION
-------------
-
-Page Notifications is a simple, lightweight module for sending e-mail
-notifications to subscribers about changes on node on a Drupal web
-site.
-
-* For a full description of the module, visit the project page:
-  https://drupal.org/project/page_notifications
-
-* For more documentation about its use, visit the documentation page:
-  https://www.drupal.org/documentation/modules/page_notifications
-
-* To submit bug reports and feature suggestions, or to track changes:
-  https://drupal.org/project/issues/page_notifications
-
-
-REQUIREMENTS
-------------
-
-This module requires a supported version of Drupal 8 or 9 to be running.
-It is highly recommended to use recaptcha module https://www.drupal.org/project/recaptcha
-For correct display of emails allow your mail settings to send HTML format email.
-
-
-INSTALLATION
-------------
-
-1. Extract the Page Notifications module directory, including all its
-   subdirectories, into directory where you keep contributed modules
-   (e.g. /modules/).
-
-2. Enable the Page Notifications module on the Modules list page. The database tables and default data
-    will be created automatically for you at this point.
-
-3. Create three fields on content type that will have notification functionality:
-    3.1 Boolean - field that will enable function to send email to subscribers
-    3.2 Text (plain, long) - field that will contain notes or short message about changes
-    3.3 Timestamp - field will record when last notification emails were sent
-
-4. Go to /admin/page-notifications/tabs » tab "Messages configuration" and enter those three machine name fields into corresponded
-    configuration fields » Save configuration.
-
-4. Place Page Notifications block into the region you would like block to be displayed » Configure » Save.
-
-
-CONFIGURATION
--------------
-
-All configuration for the module is located under /admin/page-notifications/tabs: Admin menu » Extend »
- Page Notifications » Configure.
-
-Fill Out configuration fields under "Messages configuration" tab:
-Page Notifications header text - will be displayed above block form as header of the block;
-The "from" email - enter email if you don't want to use main site emails;
-Checkbox field - the boolean type filed machine name of the field that created on one of the content types
-to enable functionality;
-Notes field - the Text (plain, long) type filed machine name of the field that created on one of the content types
-that contain notes or short message about changes;
-Timestamp - the timestamp type filed machine name of the field that created on one of the content types
-that indicates when last notification emails were sent;
-
-Note: all configuration fields for emails and web messages must be in full HTML format.
-It will automagically save in full HTML format if chosen different format before saving configuration.
-
-RELATED PROJECTS & ALTERNATIVES
--------------------------------
-
-Notify: https://www.drupal.org/project/notify
-
-
-MAINTAINERS
------------
-
-Lidiya Grushetska <grushetskl@chop.edu> is the original author https://www.drupal.org/u/lidia_ua.
-- 
GitLab


From 797464d93ce26385a82778490255e17b45dddf1d Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Thu, 9 Jan 2025 18:59:05 -0500
Subject: [PATCH 15/49] Better version

---
 README.md | 144 ++++++++++++++++++++++++------------------------------
 1 file changed, 64 insertions(+), 80 deletions(-)

diff --git a/README.md b/README.md
index e56dcfb..1bacef1 100644
--- a/README.md
+++ b/README.md
@@ -1,127 +1,111 @@
 # Page Notifications
 
-A comprehensive Drupal module for managing page-level email notifications and subscriptions.
+A module for sending e-mail notifications to subscribers about updates to Drupal content.
 
 ## Introduction
 
-Page Notifications is a powerful module that allows anonymous and authenticated users to subscribe to specific pages and receive email notifications when content is updated. The module provides a flexible subscription system with email verification, spam protection, and comprehensive subscription management.
+Page Notifications is a simple, lightweight module that enables anonymous users to subscribe to content updates via email notifications. When subscribed content is updated, subscribers receive configurable email notifications about the changes.
 
 ## Features
 
 - Anonymous user subscriptions with email verification
-- Customizable email templates for verification, confirmation and notifications
-- Spam protection via reCAPTCHA or math captcha
-- Subscription management interface for users
-- Admin interface for managing subscriptions
-- Views integration for listing subscriptions and popular content
-- Token support for email templates
-- Batch subscription migration tools
-- Support for both node and taxonomy term subscriptions 
+- Separate unsubscribe tokens for security
+- Configurable email templates for:
+  - Verification emails
+  - Confirmation emails
+  - Update notifications
+- Built-in spam protection:
+  - Math CAPTCHA option
+  - reCAPTCHA integration (when module is present)
+- View of all subscriptions and top subscribed content
+- Block with AJAX subscription form
+- Manual subscription migration tools
+- Support for both nodes and taxonomy terms
+- Token system for email customization
 - HTML email support
-- Subscription block with Ajax form
-- Comprehensive subscription tracking
+- Queue-based notification processing
 
 ## Requirements
 
-- Drupal 10.x
+This module requires:
+
+- Drupal 10.x core
 - Required modules:
   - Node
   - Views
-- Optional but recommended modules:
-  - reCAPTCHA 
-  - CAPTCHA
+- Recommended modules:
+  - reCAPTCHA (for enhanced spam protection)
+  - CAPTCHA (for basic spam protection)
 
 ## Installation
 
-1. Download and extract the Page Notifications module to your /modules directory
-2. Enable the module via the admin interface or drush:
+1. Install the Page Notifications module in your modules directory
+2. Enable the module via Extend or using drush:
 ```bash
 drush en page_notifications
 ```
-3. Configure permissions at Admin » People » Permissions
-4. Configure module settings at Admin » Structure » Page Notifications
+3. Create three fields on content types that will use notifications:
+   - Boolean field - enables sending email notifications
+   - Text (plain, long) field - for update notes/messages
+   - Timestamp field - records when notifications were last sent
+4. Configure the module at Admin » Configuration » System » Page Notifications
+5. Place the Page Notifications block in your desired region
 
 ## Configuration
 
-### Basic Setup
-
-1. Go to Admin » Structure » Page Notifications » Settings
-2. Configure general settings:
-   - Enable/disable spam protection
-   - Set email verification requirements
-   - Configure notification display options
-3. Configure email templates:
-   - Verification emails
-   - Confirmation emails  
-   - Notification emails
-   - Web messages and alerts
-4. Place the subscription block in your desired region
-
-### Email Templates
+### Email Settings
 
-The module provides several customizable email templates:
+Configure at Admin » Configuration » System » Page Notifications:
 
-- Verification email - Sent when users first subscribe
-- Confirmation email - Sent after subscription is verified
-- Notification email - Sent when content is updated
+- From email address
+- Token expiration time
+- Email template customization
+- Verification requirements
 
-Templates support tokens for dynamic content:
+### Security Settings
 
-- `[notify_node_title]` - Title of subscribed content
-- `[notify_node_url]` - URL of subscribed content  
-- `[notify_user_email]` - Subscriber's email
-- `[notify_verify_url]` - Verification URL
-- `[notify_unsubscribe_url]` - Unsubscribe URL
-- `[notify_notes]` - Update notes
+- Optional email verification requirement
+- Math CAPTCHA or reCAPTCHA integration
+- Separate tokens for verification and unsubscribe
 
-### Spam Protection 
-
-The module provides two spam protection options:
+### Email Templates
 
-1. reCAPTCHA integration (recommended)
-   - Requires the reCAPTCHA module
-   - Configure site and secret keys
-   
-2. Math captcha
-   - Simple math problem verification
-   - Configurable operators
+Customizable templates with token support:
 
-### Subscription Management
+- `[node:title]` - Content title
+- `[node:url]` - Content URL
+- `[subscription:verify-url]` - Verification URL
+- `[subscription:unsubscribe-url]` - Unsubscribe URL
+- `[notification:notes]` - Update notes
 
-Administrators can:
+### Views
 
-- View all subscriptions
-- Manage individual subscriptions
-- Migrate subscriptions between nodes
-- View subscription statistics
-- Send manual notifications
-- Export subscription data
+The module provides two built-in views that can be customized:
 
-Users can:
+- Page Notification Subscriptions - Lists all subscriptions
+- Top Subscribed Content - Shows most subscribed content
 
-- Verify subscriptions via email
-- Manage their subscriptions
-- Unsubscribe from individual or all notifications
-- View their subscription history
+### Block
 
-## API
+A subscription block is provided that contains:
 
-The module provides several services for programmatic integration:
+- Email input field
+- Customizable text for signup button and form
+- Optional CAPTCHA
+- Success/error messaging
 
-```php
-// Notification manager service
-\Drupal::service('page_notifications.notification_manager')
+## Services
 
-// Mail handler service  
-\Drupal::service('page_notifications.mail_handler')
+The module provides these key services:
 
-// Spam prevention service
-\Drupal::service('page_notifications.spam_prevention')
-```
+- NotificationManager - Core notification handling
+- CronManager - Queued notification processing
+- MailHandler - Email template processing
+- SpamPrevention - CAPTCHA handling
 
 ## Maintainers
 
-Current maintainers:
+Current maintainer:
 - Lidiya Grushetska (<lidia_ua>)
 
 ## License
-- 
GitLab


From 69c223b409f536858c21d48a5a75a441b8c39f7c Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Thu, 9 Jan 2025 19:05:56 -0500
Subject: [PATCH 16/49] Better readme version

---
 README.md | 234 +++++++++++++++++++++++++++++-------------------------
 1 file changed, 127 insertions(+), 107 deletions(-)

diff --git a/README.md b/README.md
index 1bacef1..1c33729 100644
--- a/README.md
+++ b/README.md
@@ -1,113 +1,133 @@
 # Page Notifications
 
-A module for sending e-mail notifications to subscribers about updates to Drupal content.
-
-## Introduction
-
-Page Notifications is a simple, lightweight module that enables anonymous users to subscribe to content updates via email notifications. When subscribed content is updated, subscribers receive configurable email notifications about the changes.
-
-## Features
-
-- Anonymous user subscriptions with email verification
-- Separate unsubscribe tokens for security
-- Configurable email templates for:
-  - Verification emails
-  - Confirmation emails
-  - Update notifications
-- Built-in spam protection:
-  - Math CAPTCHA option
-  - reCAPTCHA integration (when module is present)
-- View of all subscriptions and top subscribed content
-- Block with AJAX subscription form
-- Manual subscription migration tools
-- Support for both nodes and taxonomy terms
-- Token system for email customization
-- HTML email support
-- Queue-based notification processing
-
-## Requirements
-
-This module requires:
-
-- Drupal 10.x core
-- Required modules:
-  - Node
-  - Views
-- Recommended modules:
-  - reCAPTCHA (for enhanced spam protection)
-  - CAPTCHA (for basic spam protection)
-
-## Installation
-
-1. Install the Page Notifications module in your modules directory
-2. Enable the module via Extend or using drush:
-```bash
-drush en page_notifications
-```
-3. Create three fields on content types that will use notifications:
-   - Boolean field - enables sending email notifications
-   - Text (plain, long) field - for update notes/messages
-   - Timestamp field - records when notifications were last sent
-4. Configure the module at Admin » Configuration » System » Page Notifications
-5. Place the Page Notifications block in your desired region
-
-## Configuration
+A Drupal module that enables anonymous and authenticated users to subscribe to content updates and receive email notifications when changes occur.
+
+## CONTENTS OF THIS FILE
+* Introduction
+* Features
+* Requirements
+* Installation
+* Configuration
+* Usage
+* Security
+* API
+* Maintainers
+
+## INTRODUCTION
+
+Page Notifications provides a flexible system for users to subscribe to content changes on your Drupal site. When subscribed content is updated, subscribers receive customizable email notifications about the changes.
+
+## FEATURES
+
+* Email subscription system for any content entity (nodes by default)
+* Configurable email templates with token support
+* Anti-spam protection with multiple options:
+  - Simple math CAPTCHA
+  - reCAPTCHA integration (requires reCAPTCHA module)
+* Subscription management:
+  - Email verification system
+  - Secure unsubscribe links
+  - Automatic cleanup of unverified subscriptions
+  - Subscription migration tools
+* Administration:
+  - Settings interface
+  - Subscription overview and management
+  - Manual notification sending capability
+* Queue-based notification processing
+* Token support for email templates
+* Block-based subscription forms
+* Drupal Views integration
+
+## REQUIREMENTS
+
+This module requires the following:
+* Drupal 10.x
+* Node module (enabled by default)
+* Views module (enabled by default)
+
+Optional but recommended:
+* reCAPTCHA module for enhanced spam protection
+
+## INSTALLATION
+
+1. Install the module via Composer:
+   ```bash
+   composer require drupal/page_notifications
+   ```
+   Or download and extract to your modules directory.
+
+2. Enable the module at `/admin/modules` or via Drush:
+   ```bash
+   drush en page_notifications
+   ```
+
+3. Place the subscription block in your desired region at `/admin/structure/block`. Use block configuration to set visibility as desired.
+
+## CONFIGURATION
+
+All module settings can be configured at `/admin/config/system/page-notifications`:
 
 ### Email Settings
-
-Configure at Admin » Configuration » System » Page Notifications:
-
-- From email address
-- Token expiration time
-- Email template customization
-- Verification requirements
+* Configure "From" email address
+* Set verification token expiration time
+* Customize email templates for:
+  - Subscription verification
+  - Update notifications
 
 ### Security Settings
-
-- Optional email verification requirement
-- Math CAPTCHA or reCAPTCHA integration
-- Separate tokens for verification and unsubscribe
-
-### Email Templates
-
-Customizable templates with token support:
-
-- `[node:title]` - Content title
-- `[node:url]` - Content URL
-- `[subscription:verify-url]` - Verification URL
-- `[subscription:unsubscribe-url]` - Unsubscribe URL
-- `[notification:notes]` - Update notes
-
-### Views
-
-The module provides two built-in views that can be customized:
-
-- Page Notification Subscriptions - Lists all subscriptions
-- Top Subscribed Content - Shows most subscribed content
-
-### Block
-
-A subscription block is provided that contains:
-
-- Email input field
-- Customizable text for signup button and form
-- Optional CAPTCHA
-- Success/error messaging
-
-## Services
-
-The module provides these key services:
-
-- NotificationManager - Core notification handling
-- CronManager - Queued notification processing
-- MailHandler - Email template processing
-- SpamPrevention - CAPTCHA handling
-
-## Maintainers
-
-Current maintainer:
-- Lidiya Grushetska (<lidia_ua>)
-
-## License
-
-This project is licensed under the GNU General Public License v2.0.
\ No newline at end of file
+* Toggle email verification requirement
+* Configure unverified subscription cleanup
+* Set spam prevention method:
+  - None
+  - Math CAPTCHA
+  - reCAPTCHA (if module installed)
+
+### Subscription Management
+* View and manage subscriptions
+* Migrate subscriptions between content
+* Send manual notifications
+
+## USAGE
+
+### For Site Builders
+1. Place the subscription block on content types where you want to enable notifications
+2. Configure email templates and security settings
+3. Manage subscriptions through the administrative interface
+
+### For Content Editors
+1. Update content normally
+2. Option to send notifications appears in content edit form next to the revision log
+3. Can include custom notes with notifications
+
+### For Users
+1. Subscribe to content via the subscription block
+2. Receive verification email (if enabled)
+3. Get notifications when content is updated
+4. Unsubscribe via secure links in notification emails
+
+## SECURITY
+
+The module implements several security measures:
+* Email verification system
+* CAPTCHA/reCAPTCHA spam prevention
+* Secure unsubscribe tokens
+* Automatic cleanup of unverified subscriptions
+* Permission-based access control
+
+## API
+
+The module provides services and interfaces for developers to:
+* Create and manage subscriptions programmatically
+* Customize notification handling
+* Extend spam prevention mechanisms
+* Integrate with other modules
+
+Key services:
+* `page_notifications.notification_manager`
+* `page_notifications.mail_handler`
+* `page_notifications.spam_prevention`
+
+## MAINTAINERS
+
+Current maintainers:
+* Lidiya Grushetska <grushetskl@chop.edu> is the original author https://www.drupal.org/u/lidia_ua.
\ No newline at end of file
-- 
GitLab


From df0bf3deee2af0773c7d9c28b1e33982f6e0dcdb Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Thu, 9 Jan 2025 21:04:00 -0500
Subject: [PATCH 17/49] provide composer.json

---
 composer.json | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)
 create mode 100644 composer.json

diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..bcac47e
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,37 @@
+{
+  "name": "drupal/page_notifications",
+  "description": "Enables anonymous and authenticated users to subscribe to content updates and receive email notifications when changes occur.",
+  "type": "drupal-module",
+  "license": "GPL-2.0-or-later",
+  "homepage": "https://drupal.org/project/page_notifications",
+  "authors": [
+    {
+      "name": "Lidiya Grushetska",
+      "homepage": "https://www.drupal.org/u/lidia_ua",
+      "role": "Maintainer"
+    }
+  ],
+  "support": {
+    "issues": "https://drupal.org/project/issues/page_notifications",
+    "source": "https://git.drupalcode.org/project/page_notifications"
+  },
+  "require": {
+    "php": ">=8.1",
+    "drupal/core": "^10"
+  },
+  "suggest": {
+    "drupal/captcha": "Provides additional spam protection options including reCAPTCHA integration"
+  },
+  "extra": {
+    "drush": {
+      "services": {
+        "drush.services.yml": "^10"
+      }
+    }
+  },
+  "minimum-stability": "dev",
+  "prefer-stable": true,
+  "config": {
+    "sort-packages": true
+  }
+}
\ No newline at end of file
-- 
GitLab


From 2f8da551ee8e38efbba8a7f0957c0d18ec44140e Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Fri, 10 Jan 2025 16:21:01 -0500
Subject: [PATCH 18/49] Add configurable flood setting to prevent signup abuse.

---
 .../install/page_notifications.settings.yml   |   5 +
 config/schema/page_notifications.schema.yml   |  61 +++++++-
 src/Form/SettingsForm.php                     |  50 +++++++
 src/Form/SubscriptionForm.php                 |  36 ++++-
 src/Traits/FloodControlTrait.php              | 135 ++++++++++++++++++
 5 files changed, 279 insertions(+), 8 deletions(-)
 create mode 100644 src/Traits/FloodControlTrait.php

diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml
index 65db4b3..abcc808 100644
--- a/config/install/page_notifications.settings.yml
+++ b/config/install/page_notifications.settings.yml
@@ -35,6 +35,11 @@ email_templates:
 
 security:
   require_verification: 1
+  flood_control:
+    ip_limit: 5
+    ip_window: 24
+    identifier_limit: 5
+    identifier_window: 24
 
 spam_protection:
   enable_modal: 0
diff --git a/config/schema/page_notifications.schema.yml b/config/schema/page_notifications.schema.yml
index 71f230b..73ec57a 100644
--- a/config/schema/page_notifications.schema.yml
+++ b/config/schema/page_notifications.schema.yml
@@ -14,16 +14,69 @@ page_notifications.settings:
         token_expiration:
           type: integer
           label: 'Token expiration time in hours (0 = never expire)'
-    spam_prevention:
+    email_templates:
       type: mapping
-      label: 'Spam Prevention Settings'
+      label: 'Email Templates'
       mapping:
+        verification_subject:
+          type: string
+          label: 'Verification email subject'
+        verification_body:
+          type: text
+          label: 'Verification email body'
+        notification_subject:
+          type: string
+          label: 'Notification email subject'
+        notification_body:
+          type: text
+          label: 'Notification email body'
+
+    security:
+      type: mapping
+      label: 'Security Settings'
+      mapping:
+        require_verification:
+          type: boolean
+          label: 'Require email verification'
+        flood_control:
+          type: mapping
+          label: 'Flood Control Settings'
+          mapping:
+            ip_limit:
+              type: integer
+              label: 'IP-based attempt limit'
+            ip_window:
+              type: integer
+              label: 'IP-based time window (hours)'
+            identifier_limit:
+              type: integer
+              label: 'Email-based attempt limit'
+            identifier_window:
+              type: integer
+              label: 'Email-based time window (hours)'
+
+    spam_protection:
+      type: mapping
+      label: 'Spam Protection Settings'
+      mapping:
+        enable_modal:
+          type: boolean
+          label: 'Enable modal dialog'
+        enable_math_captcha:
+          type: boolean
+          label: 'Enable math captcha'
+        math_captcha_operator:
+          type: string
+          label: 'Math captcha operator'
+        captcha_point:
+          type: string
+          label: 'CAPTCHA point'
         captcha_type:
           type: string
-          label: 'Captcha Type'
+          label: 'CAPTCHA type'
         math_operator:
           type: string
-          label: 'Math Challenge Operator'
+          label: 'Math challenge operator'
         use_recaptcha:
           type: boolean
           label: 'Use reCAPTCHA if available'
\ No newline at end of file
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index a65c39d..d21017d 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -166,6 +166,52 @@ class SettingsForm extends ConfigFormBase {
       '#default_value' => $config->get('security.require_verification') ?? TRUE,
     ];
 
+     // Flood control settings
+     $form['security']['flood_control'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Flood Control Settings'),
+      '#open' => TRUE,
+      '#tree' => TRUE,
+    ];
+
+    $form['security']['flood_control']['ip_limit'] = [
+      '#type' => 'number',
+      '#title' => $this->t('IP-based attempt limit'),
+      '#description' => $this->t('Maximum number of subscription attempts allowed from a single IP address.'),
+      '#default_value' => $config->get('security.flood_control.ip_limit') ?? 5,
+      '#min' => 1,
+      '#required' => TRUE,
+    ];
+
+    $form['security']['flood_control']['ip_window'] = [
+      '#type' => 'number',
+      '#title' => $this->t('IP-based time window'),
+      '#description' => $this->t('Time window in hours for IP-based subscription attempts.'),
+      '#default_value' => $config->get('security.flood_control.ip_window') ?? 1,
+      '#min' => 1,
+      '#required' => TRUE,
+      '#field_suffix' => $this->t('hours'),
+    ];
+
+    $form['security']['flood_control']['identifier_limit'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Email-based attempt limit'),
+      '#description' => $this->t('Maximum number of subscription attempts allowed for the same email address.'),
+      '#default_value' => $config->get('security.flood_control.identifier_limit') ?? 3,
+      '#min' => 1,
+      '#required' => TRUE,
+    ];
+
+    $form['security']['flood_control']['identifier_window'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Email-based time window'),
+      '#description' => $this->t('Time window in hours for email-based subscription attempts.'),
+      '#default_value' => $config->get('security.flood_control.identifier_window') ?? 1,
+      '#min' => 1,
+      '#required' => TRUE,
+      '#field_suffix' => $this->t('hours'),
+    ];
+
     // Add spam prevention section
     $form['spam_prevention'] = [
       '#type' => 'details',
@@ -248,6 +294,10 @@ class SettingsForm extends ConfigFormBase {
       ->set('spam_prevention.captcha_type', $form_state->getValue('captcha_type'))
       ->set('spam_prevention.math_operator', $form_state->getValue('math_operator'))
       ->set('spam_prevention.use_recaptcha', $form_state->getValue('use_recaptcha'))
+      ->set('security.flood_control.ip_limit', $form_state->getValue(['flood_control', 'ip_limit']))
+      ->set('security.flood_control.ip_window', $form_state->getValue(['flood_control', 'ip_window']))
+      ->set('security.flood_control.identifier_limit', $form_state->getValue(['flood_control', 'identifier_limit']))
+      ->set('security.flood_control.identifier_window', $form_state->getValue(['flood_control', 'identifier_window']))
       ->save();
 
     parent::submitForm($form, $form_state);
diff --git a/src/Form/SubscriptionForm.php b/src/Form/SubscriptionForm.php
index e5a7297..7216a3a 100644
--- a/src/Form/SubscriptionForm.php
+++ b/src/Form/SubscriptionForm.php
@@ -10,11 +10,14 @@ use Drupal\page_notifications\Service\NotificationManagerInterface;
 use Drupal\page_notifications\Service\SpamPrevention;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Psr\Log\LoggerInterface;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\page_notifications\Traits\FloodControlTrait;
 
 /**
  * Provides a subscription form.
  */
 class SubscriptionForm extends FormBase {
+  use FloodControlTrait;
 
   /**
    * The notification manager service.
@@ -46,17 +49,30 @@ class SubscriptionForm extends FormBase {
 
   /**
    * Constructs a new SubscriptionForm.
+   *
+   * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager
+   *   The notification manager service.
+   * @param \Drupal\page_notifications\Service\SpamPrevention $spam_prevention
+   *   The spam prevention service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger instance.
+   * @param \Drupal\Core\Flood\FloodInterface $flood
+   *   The flood service.
    */
   public function __construct(
     NotificationManagerInterface $notification_manager,
     SpamPrevention $spam_prevention,
     ConfigFactoryInterface $config_factory,
-    LoggerInterface $logger
+    LoggerInterface $logger,
+    FloodInterface $flood
   ) {
     $this->notificationManager = $notification_manager;
     $this->spamPrevention = $spam_prevention;
-    $this->configFactory = $config_factory;
     $this->logger = $logger;
+    $this->configFactory = $config_factory;
+    $this->setFloodService($flood);
   }
 
   /**
@@ -67,7 +83,8 @@ class SubscriptionForm extends FormBase {
       $container->get('page_notifications.notification_manager'),
       $container->get('page_notifications.spam_prevention'),
       $container->get('config.factory'),
-      $container->get('logger.factory')->get('page_notifications')
+      $container->get('logger.factory')->get('page_notifications'),
+      $container->get('flood')
     );
   }
 
@@ -148,8 +165,16 @@ class SubscriptionForm extends FormBase {
    * {@inheritdoc}
    */
   public function validateForm(array &$form, FormStateInterface $form_state) {
-    if (!filter_var($form_state->getValue('email'), FILTER_VALIDATE_EMAIL)) {
+    $email = $form_state->getValue('email');
+
+    // Check flood control before other validation
+    if (!$this->checkFloodControl($email, $form_state)) {
+      return;
+    }
+
+    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
       $form_state->setErrorByName('email', $this->t('Please enter a valid email address.'));
+      return;
     }
 
     // Validate math challenge if enabled
@@ -177,6 +202,9 @@ class SubscriptionForm extends FormBase {
     $email = $form_state->getValue('email');
 
     try {
+      // Register flood control event
+      $this->registerFloodControl($email);
+
       $subscription = $this->notificationManager->createSubscription($email, $entity);
       $this->messenger()->addStatus($this->t('Thank you for subscribing. Please check your email to confirm your subscription.'));
     }
diff --git a/src/Traits/FloodControlTrait.php b/src/Traits/FloodControlTrait.php
new file mode 100644
index 0000000..5fc34b1
--- /dev/null
+++ b/src/Traits/FloodControlTrait.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Drupal\page_notifications\Traits;
+
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides flood control functionality for forms.
+ */
+trait FloodControlTrait {
+
+  /**
+   * The flood service.
+   *
+   * @var \Drupal\Core\Flood\FloodInterface|null
+   */
+  protected ?FloodInterface $flood = NULL;
+
+  /**
+   * Sets the flood service.
+   *
+   * @param \Drupal\Core\Flood\FloodInterface $flood
+   *   The flood service.
+   */
+  public function setFloodService(FloodInterface $flood) {
+    $this->flood = $flood;
+  }
+
+  /**
+   * Gets the flood service.
+   *
+   * @return \Drupal\Core\Flood\FloodInterface
+   *   The flood service.
+   */
+  protected function getFlood(): FloodInterface {
+    if (!$this->flood) {
+      $this->flood = \Drupal::service('flood');
+    }
+    return $this->flood;
+  }
+
+  /**
+   * Gets the flood control configuration.
+   *
+   * @return array
+   *   An array containing flood control settings.
+   */
+  protected function getFloodControlConfig() {
+    $config = $this->configFactory()->get('page_notifications.settings');
+
+    return [
+      'ip_limit' => $config->get('security.flood_control.ip_limit') ?? 5,
+      'ip_window' => ($config->get('security.flood_control.ip_window') ?? 1) * 3600,
+      'identifier_limit' => $config->get('security.flood_control.identifier_limit') ?? 3,
+      'identifier_window' => ($config->get('security.flood_control.identifier_window') ?? 1) * 3600,
+    ];
+  }
+
+  /**
+   * Checks if the current request should be allowed through flood control.
+   *
+   * @param string $identifier
+   *   The identifier to check (typically an email).
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   *
+   * @return bool
+   *   TRUE if the request should be allowed, FALSE otherwise.
+   */
+  protected function checkFloodControl($identifier, FormStateInterface $form_state) {
+    $ip = \Drupal::request()->getClientIp();
+    $settings = $this->getFloodControlConfig();
+    $flood = $this->getFlood();
+
+    // Generate unique event identifiers
+    $ip_event = 'page_notifications.subscribe_ip.' . $ip;
+    $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier;
+
+    // Check IP-based flooding
+    if (!$flood->isAllowed($ip_event, $settings['ip_limit'], $settings['ip_window'])) {
+      $form_state->setErrorByName('', $this->t('Too many subscription attempts from this IP address. Please try again in @hours hours.',
+        ['@hours' => floor($settings['ip_window'] / 3600)]
+      ));
+      $this->logSecurityEvent('flood_control_ip', ['ip' => $ip]);
+      return FALSE;
+    }
+
+    // Check identifier-based flooding
+    if (!$flood->isAllowed($identifier_event, $settings['identifier_limit'], $settings['identifier_window'])) {
+      $form_state->setErrorByName('email', $this->t('Too many subscription attempts for this email address. Please try again in @hours hours.',
+        ['@hours' => floor($settings['identifier_window'] / 3600)]
+      ));
+      $this->logSecurityEvent('flood_control_identifier', ['identifier' => $identifier]);
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Registers a flood event.
+   *
+   * @param string $identifier
+   *   The identifier for the flood event.
+   */
+  protected function registerFloodControl($identifier) {
+    $ip = \Drupal::request()->getClientIp();
+    $settings = $this->getFloodControlConfig();
+    $flood = $this->getFlood();
+
+    // Generate unique event identifiers
+    $ip_event = 'page_notifications.subscribe_ip.' . $ip;
+    $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier;
+
+    // Register both IP and identifier-based flood events
+    $flood->register($ip_event, $settings['ip_window']);
+    $flood->register($identifier_event, $settings['identifier_window']);
+  }
+
+  /**
+   * Logs a security event.
+   *
+   * @param string $type
+   *   The type of security event.
+   * @param array $data
+   *   Additional data to log.
+   */
+  protected function logSecurityEvent($type, array $data) {
+    \Drupal::logger('page_notifications_security')->warning(
+      '@type: @data',
+      ['@type' => $type, '@data' => json_encode($data)]
+    );
+  }
+}
\ No newline at end of file
-- 
GitLab


From 15c12b011e5f16f550d8b7a637ceccabeebb46a9 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Sat, 11 Jan 2025 05:24:34 -0500
Subject: [PATCH 19/49] Allow sites to disable flood protect if they don't want
 it.

---
 src/Form/SettingsForm.php        |  8 ++--
 src/Traits/FloodControlTrait.php | 74 +++++++++++++++++---------------
 2 files changed, 44 insertions(+), 38 deletions(-)

diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index d21017d..f8ccf77 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -186,9 +186,9 @@ class SettingsForm extends ConfigFormBase {
     $form['security']['flood_control']['ip_window'] = [
       '#type' => 'number',
       '#title' => $this->t('IP-based time window'),
-      '#description' => $this->t('Time window in hours for IP-based subscription attempts.'),
+      '#description' => $this->t('Time window in hours for IP-based subscription attempts. Set to 0 to disable IP-based flood control.'),
       '#default_value' => $config->get('security.flood_control.ip_window') ?? 1,
-      '#min' => 1,
+      '#min' => 0, // Changed from 1 to 0
       '#required' => TRUE,
       '#field_suffix' => $this->t('hours'),
     ];
@@ -205,9 +205,9 @@ class SettingsForm extends ConfigFormBase {
     $form['security']['flood_control']['identifier_window'] = [
       '#type' => 'number',
       '#title' => $this->t('Email-based time window'),
-      '#description' => $this->t('Time window in hours for email-based subscription attempts.'),
+      '#description' => $this->t('Time window in hours for email-based subscription attempts. Set to 0 to disable email-based flood control.'),
       '#default_value' => $config->get('security.flood_control.identifier_window') ?? 1,
-      '#min' => 1,
+      '#min' => 0, // Changed from 1 to 0
       '#required' => TRUE,
       '#field_suffix' => $this->t('hours'),
     ];
diff --git a/src/Traits/FloodControlTrait.php b/src/Traits/FloodControlTrait.php
index 5fc34b1..7ffdb68 100644
--- a/src/Traits/FloodControlTrait.php
+++ b/src/Traits/FloodControlTrait.php
@@ -58,26 +58,24 @@ trait FloodControlTrait {
   }
 
   /**
-   * Checks if the current request should be allowed through flood control.
-   *
-   * @param string $identifier
-   *   The identifier to check (typically an email).
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The form state.
-   *
-   * @return bool
-   *   TRUE if the request should be allowed, FALSE otherwise.
-   */
-  protected function checkFloodControl($identifier, FormStateInterface $form_state) {
-    $ip = \Drupal::request()->getClientIp();
-    $settings = $this->getFloodControlConfig();
-    $flood = $this->getFlood();
+ * Checks if the current request should be allowed through flood control.
+ *
+ * @param string $identifier
+ *   The identifier to check (typically an email).
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ *   The form state.
+ *
+ * @return bool
+ *   TRUE if the request should be allowed, FALSE otherwise.
+ */
+protected function checkFloodControl($identifier, FormStateInterface $form_state) {
+  $ip = \Drupal::request()->getClientIp();
+  $settings = $this->getFloodControlConfig();
+  $flood = $this->getFlood();
 
-    // Generate unique event identifiers
+  // Skip IP-based flood control if window is set to 0
+  if ($settings['ip_window'] > 0) {
     $ip_event = 'page_notifications.subscribe_ip.' . $ip;
-    $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier;
-
-    // Check IP-based flooding
     if (!$flood->isAllowed($ip_event, $settings['ip_limit'], $settings['ip_window'])) {
       $form_state->setErrorByName('', $this->t('Too many subscription attempts from this IP address. Please try again in @hours hours.',
         ['@hours' => floor($settings['ip_window'] / 3600)]
@@ -85,8 +83,11 @@ trait FloodControlTrait {
       $this->logSecurityEvent('flood_control_ip', ['ip' => $ip]);
       return FALSE;
     }
+  }
 
-    // Check identifier-based flooding
+  // Skip identifier-based flood control if window is set to 0
+  if ($settings['identifier_window'] > 0) {
+    $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier;
     if (!$flood->isAllowed($identifier_event, $settings['identifier_limit'], $settings['identifier_window'])) {
       $form_state->setErrorByName('email', $this->t('Too many subscription attempts for this email address. Please try again in @hours hours.',
         ['@hours' => floor($settings['identifier_window'] / 3600)]
@@ -94,29 +95,34 @@ trait FloodControlTrait {
       $this->logSecurityEvent('flood_control_identifier', ['identifier' => $identifier]);
       return FALSE;
     }
-
-    return TRUE;
   }
 
+  return TRUE;
+}
+
   /**
-   * Registers a flood event.
-   *
-   * @param string $identifier
-   *   The identifier for the flood event.
-   */
-  protected function registerFloodControl($identifier) {
-    $ip = \Drupal::request()->getClientIp();
-    $settings = $this->getFloodControlConfig();
-    $flood = $this->getFlood();
+ * Registers a flood event.
+ *
+ * @param string $identifier
+ *   The identifier for the flood event.
+ */
+protected function registerFloodControl($identifier) {
+  $ip = \Drupal::request()->getClientIp();
+  $settings = $this->getFloodControlConfig();
+  $flood = $this->getFlood();
 
-    // Generate unique event identifiers
+  // Only register IP-based flood events if window is greater than 0
+  if ($settings['ip_window'] > 0) {
     $ip_event = 'page_notifications.subscribe_ip.' . $ip;
-    $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier;
-
-    // Register both IP and identifier-based flood events
     $flood->register($ip_event, $settings['ip_window']);
+  }
+
+  // Only register identifier-based flood events if window is greater than 0
+  if ($settings['identifier_window'] > 0) {
+    $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier;
     $flood->register($identifier_event, $settings['identifier_window']);
   }
+}
 
   /**
    * Logs a security event.
-- 
GitLab


From 55c35a1b82164594b86457c5e60527190b0ba756 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Sat, 11 Jan 2025 05:33:54 -0500
Subject: [PATCH 20/49] Up flood defaults

---
 config/install/page_notifications.settings.yml | 4 ++--
 page_notifications.install                     | 7 ++++++-
 src/Form/SettingsForm.php                      | 4 ++--
 src/Traits/FloodControlTrait.php               | 4 ++--
 4 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml
index abcc808..651b055 100644
--- a/config/install/page_notifications.settings.yml
+++ b/config/install/page_notifications.settings.yml
@@ -36,9 +36,9 @@ email_templates:
 security:
   require_verification: 1
   flood_control:
-    ip_limit: 5
+    ip_limit: 200
     ip_window: 24
-    identifier_limit: 5
+    identifier_limit: 50
     identifier_window: 24
 
 spam_protection:
diff --git a/page_notifications.install b/page_notifications.install
index 3333b3b..cfebc87 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -107,7 +107,12 @@ Regards,
 [site:name] team')
     ->set('security.require_verification', 1)
     ->set('spam_protection.enable_math_captcha', 1)
-    ->set('spam_protection.math_captcha_operator', '+')
+    ->set('security.flood_control.ip_limit', 200)
+    ->set('security.flood_control.ip_window', 1)
+    ->set('security.flood_control.identifier_limit', 50)
+    ->set('security.flood_control.identifier_window', 1)
+    ->set('spam_prevention.captcha_type', 'none')
+    ->set('spam_prevention.math_operator', '+')
     ->save();
 
   // Migrate subscriptions
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index f8ccf77..798f9a8 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -178,7 +178,7 @@ class SettingsForm extends ConfigFormBase {
       '#type' => 'number',
       '#title' => $this->t('IP-based attempt limit'),
       '#description' => $this->t('Maximum number of subscription attempts allowed from a single IP address.'),
-      '#default_value' => $config->get('security.flood_control.ip_limit') ?? 5,
+      '#default_value' => $config->get('security.flood_control.ip_limit') ?? 200,
       '#min' => 1,
       '#required' => TRUE,
     ];
@@ -197,7 +197,7 @@ class SettingsForm extends ConfigFormBase {
       '#type' => 'number',
       '#title' => $this->t('Email-based attempt limit'),
       '#description' => $this->t('Maximum number of subscription attempts allowed for the same email address.'),
-      '#default_value' => $config->get('security.flood_control.identifier_limit') ?? 3,
+      '#default_value' => $config->get('security.flood_control.identifier_limit') ?? 50,
       '#min' => 1,
       '#required' => TRUE,
     ];
diff --git a/src/Traits/FloodControlTrait.php b/src/Traits/FloodControlTrait.php
index 7ffdb68..3e05541 100644
--- a/src/Traits/FloodControlTrait.php
+++ b/src/Traits/FloodControlTrait.php
@@ -50,9 +50,9 @@ trait FloodControlTrait {
     $config = $this->configFactory()->get('page_notifications.settings');
 
     return [
-      'ip_limit' => $config->get('security.flood_control.ip_limit') ?? 5,
+      'ip_limit' => $config->get('security.flood_control.ip_limit') ?? 200,
       'ip_window' => ($config->get('security.flood_control.ip_window') ?? 1) * 3600,
-      'identifier_limit' => $config->get('security.flood_control.identifier_limit') ?? 3,
+      'identifier_limit' => $config->get('security.flood_control.identifier_limit') ?? 50,
       'identifier_window' => ($config->get('security.flood_control.identifier_window') ?? 1) * 3600,
     ];
   }
-- 
GitLab


From f95d9c94d1fcf25745113cb4fd355548081c6c83 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Sat, 11 Jan 2025 08:29:21 -0500
Subject: [PATCH 21/49] add flood protection to prevent brute force
 unsubscribes

---
 src/Controller/UnsubscribeController.php | 27 ++++++++++++++++++++++--
 1 file changed, 25 insertions(+), 2 deletions(-)

diff --git a/src/Controller/UnsubscribeController.php b/src/Controller/UnsubscribeController.php
index 7399351..253dd9e 100644
--- a/src/Controller/UnsubscribeController.php
+++ b/src/Controller/UnsubscribeController.php
@@ -9,11 +9,14 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Url;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\page_notifications\Traits\FloodControlTrait;
 
 /**
  * Controller for handling unsubscribe requests.
  */
 class UnsubscribeController extends ControllerBase {
+  use FloodControlTrait;
 
   /**
    * The entity type manager.
@@ -25,8 +28,12 @@ class UnsubscribeController extends ControllerBase {
   /**
    * Constructs a new UnsubscribeController.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+  public function __construct(
+    EntityTypeManagerInterface $entity_type_manager,
+    FloodInterface $flood
+  ) {
     $this->entityTypeManager = $entity_type_manager;
+    $this->setFloodService($flood);
   }
 
   /**
@@ -34,7 +41,8 @@ class UnsubscribeController extends ControllerBase {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('entity_type.manager')
+      $container->get('entity_type.manager'),
+      $container->get('flood')
     );
   }
 
@@ -42,6 +50,17 @@ class UnsubscribeController extends ControllerBase {
    * Custom access check for unsubscribe URLs.
    */
   public function checkAccess($subscription, $token) {
+    // Check flood control first
+    $ip = \Drupal::request()->getClientIp();
+    $flood_config = $this->getFloodControlConfig();
+
+    if (!$this->flood->isAllowed('page_notifications.unsubscribe', $flood_config['ip_limit'], $flood_config['ip_window'], $ip)) {
+      return AccessResult::forbidden('Too many unsubscribe attempts from this IP address.');
+    }
+
+    // Register flood event for this attempt
+    $this->flood->register('page_notifications.unsubscribe', $flood_config['ip_window'], $ip);
+
     if (is_numeric($subscription)) {
       try {
         $subscription = $this->entityTypeManager
@@ -58,6 +77,10 @@ class UnsubscribeController extends ControllerBase {
     }
 
     if ($subscription->getUnsubscribeToken() !== $token) {
+      $this->logSecurityEvent('invalid_unsubscribe_token', [
+        'ip' => $ip,
+        'subscription_id' => $subscription->id(),
+      ]);
       return AccessResult::forbidden();
     }
 
-- 
GitLab


From 4d650139044bc744fb02943e5efcb1e73ae625fd Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Mon, 13 Jan 2025 22:40:13 -0500
Subject: [PATCH 22/49] Attempt to migrate all v3 settings to v4

---
 page_notifications.install | 147 ++++++++++++++++++++++++-------------
 1 file changed, 96 insertions(+), 51 deletions(-)

diff --git a/page_notifications.install b/page_notifications.install
index cfebc87..3e9b3e3 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -9,16 +9,41 @@
  * Implements hook_install().
  */
 function page_notifications_install() {
-  // Create default configuration.
+  // Create default configuration
   $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
   if ($config->isNew()) {
     $config
       ->set('notification_settings.from_email', '')
-      ->set('notification_settings.email_template', '')
       ->set('notification_settings.token_expiration', 48)
+      ->set('email_templates.verification_subject', 'Verify your subscription to [node:title]')
+      ->set('email_templates.verification_body', 'Hello,
+
+Please verify your email subscription to the page "[node:title]".
+Click the following link to confirm your subscription:
+[subscription:verify-url]
+
+This verification link will expire soon.
+Please verify your subscription promptly.
+
+If you did not request this subscription, please ignore this email.')
+      ->set('email_templates.notification_subject', '[node:title] has been updated')
+      ->set('email_templates.notification_body', 'Dear subscriber,
+
+The page "[node:title]" that you are subscribed to has been updated.
+
+[notification:notes]
+
+You can view the updated page here:
+[node:url]
+
+To unsubscribe from these notifications, click here:
+[subscription:unsubscribe-url]
+
+Regards,
+[site:name] team')
+      ->set('security.require_verification', 1)
       ->set('spam_prevention.captcha_type', 'none')
       ->set('spam_prevention.math_operator', '+')
-      ->set('spam_prevention.use_recaptcha', FALSE)
       ->save();
   }
 }
@@ -50,18 +75,20 @@ function page_notifications_update_10001(&$sandbox) {
   $entity_type = \Drupal::entityTypeManager()->getDefinition('page_notification_subscription');
   \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type);
 
-  // Install required views and configuration
+  // Check if this is a migration or fresh install by looking for v3 tables
+  $schema = \Drupal::database()->schema();
+  $has_v3_data = $schema->tableExists('page_notify_settings') &&
+                 $schema->tableExists('page_notify_email_template');
+
+  // Install required views and configuration regardless of migration status
   $module_path = \Drupal::service('extension.list.module')->getPath('page_notifications');
   $source = new \Drupal\Core\Config\FileStorage($module_path . '/config/install');
-
   $config_storage = \Drupal::service('config.storage');
 
   // List of all configurations to install
   $configs = [
-    // Views
     'views.view.page_notification_subscriptions',
     'views.view.top_subscribed_content',
-    // Settings and templates
     'page_notifications.settings',
   ];
 
@@ -76,50 +103,68 @@ function page_notifications_update_10001(&$sandbox) {
     }
   }
 
-  // Set default email templates and settings
-  $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
-  $config
-    ->set('email_templates.verification_subject', 'Verify your subscription to [node:title]')
-    ->set('email_templates.verification_body', 'Hello,
-
-Please verify your email subscription to the page "[node:title]".
-Click the following link to confirm your subscription:
-[subscription:verify-url]
-
-This verification link will expire soon.
-Please verify your subscription promptly.
-
-If you did not request this subscription, please ignore this email.')
-    ->set('email_templates.notification_subject', '[node:title] has been updated')
-    ->set('email_templates.notification_body', 'Dear subscriber,
-
-The page "[node:title]" that you are subscribed to has been updated.
-
-[notification:notes]
-
-You can view the updated page here:
-[node:url]
-
-To unsubscribe from these notifications, click here:
-[subscription:unsubscribe-url]
-
-Regards,
-[site:name] team')
-    ->set('security.require_verification', 1)
-    ->set('spam_protection.enable_math_captcha', 1)
-    ->set('security.flood_control.ip_limit', 200)
-    ->set('security.flood_control.ip_window', 1)
-    ->set('security.flood_control.identifier_limit', 50)
-    ->set('security.flood_control.identifier_window', 1)
-    ->set('spam_prevention.captcha_type', 'none')
-    ->set('spam_prevention.math_operator', '+')
-    ->save();
-
-  // Migrate subscriptions
-  $batch = \Drupal\page_notifications\Service\MigrationService::createMigrationBatch();
-  if ($batch) {
-    batch_set($batch);
+  // Only attempt migration if v3 tables exist
+  if ($has_v3_data) {
+    try {
+      // Get v3 settings
+      $v3_settings = \Drupal::database()->select('page_notify_settings', 'pns')
+        ->fields('pns')
+        ->execute()
+        ->fetchAssoc();
+
+      $v3_template = \Drupal::database()->select('page_notify_email_template', 'pnet')
+        ->fields('pnet')
+        ->execute()
+        ->fetchAssoc();
+
+      // Store v3 data
+      \Drupal::state()->set('page_notifications_v3_backup', [
+        'settings' => $v3_settings,
+        'template' => $v3_template,
+      ]);
+
+      // Map settings to v4
+      $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
+
+      // Migrate settings
+      if ($v3_template && $v3_settings) {
+        // Map email settings
+        $config->set('notification_settings.from_email', $v3_template['from_email'] ?? '');
+
+        // Map CAPTCHA settings
+        if (!empty($v3_settings['page_notify_recaptcha'])) {
+          $config->set('spam_prevention.captcha_type', 'recaptcha');
+          $config->set('spam_prevention.use_recaptcha', TRUE);
+        }
+        elseif (!empty($v3_settings['page_notify_captcha'])) {
+          $config->set('spam_prevention.captcha_type', 'math');
+        }
+
+        // Map email templates
+        $config->set('email_templates.verification_subject', $v3_template['verification_email_subject'] ?? '');
+        $config->set('email_templates.verification_body', $v3_template['verification_email_text'] ?? '');
+        $config->set('email_templates.notification_subject', $v3_template['general_email_template_subject'] ?? '');
+        $config->set('email_templates.notification_body', $v3_template['general_email_template'] ?? '');
+
+        $config->save();
+
+        \Drupal::logger('page_notifications')->notice('Migrated settings from v3 to v4.');
+      }
+
+      // Set up batch migration for subscriptions
+      $batch = \Drupal\page_notifications\Service\MigrationService::createMigrationBatch();
+      if ($batch) {
+        batch_set($batch);
+      }
+    }
+    catch (\Exception $e) {
+      \Drupal::logger('page_notifications')->error('Error during v3 to v4 migration: @message', [
+        '@message' => $e->getMessage()
+      ]);
+    }
   }
 
-  return t('Subscription data has been migrated, views and default configuration have been installed. Please review your Page Notifications settings at /admin/config/system/page-notifications');
+  return $has_v3_data ?
+    t('Subscription data has been migrated, views and default configuration have been installed. Please review your Page Notifications settings at /admin/config/system/page-notifications') :
+    t('Page Notifications v4 has been installed with default configuration.');
 }
\ No newline at end of file
-- 
GitLab


From 18842862ef63c0ca46945ef8c0facee8330c5656 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Mon, 13 Jan 2025 23:01:33 -0500
Subject: [PATCH 23/49] Map v3 tokens to v4 tokens, add missing email token

---
 page_notifications.install      | 43 +++++++++++++++++++++++++++++----
 src/Token/SubscriptionToken.php |  9 +++++++
 2 files changed, 47 insertions(+), 5 deletions(-)

diff --git a/page_notifications.install b/page_notifications.install
index 3e9b3e3..a19567b 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -131,6 +131,30 @@ function page_notifications_update_10001(&$sandbox) {
         // Map email settings
         $config->set('notification_settings.from_email', $v3_template['from_email'] ?? '');
 
+        /**
+         * Converts v3 tokens to v4 format in a string.
+         */
+        function page_notifications_convert_tokens($text) {
+          $token_map = [
+            '[notify_user_email]' => '[subscription:email]',
+            '[notify_verify_url]' => '[subscription:verify-url]',
+            '[notify_unsubscribe_url]' => '[subscription:unsubscribe-url]',
+            '[notify_node_title]' => '[node:title]',
+            '[notify_node_url]' => '[node:url]',
+            '[notify_notes]' => '[notification:notes]',
+            // Remove or convert deprecated tokens
+            '[notify_user_name]' => '',
+            '[notify_subscribe_url]' => '',
+            '[notify_user_subscribtions]' => '',
+          ];
+
+          return str_replace(
+            array_keys($token_map),
+            array_values($token_map),
+            $text
+          );
+        }
+
         // Map CAPTCHA settings
         if (!empty($v3_settings['page_notify_recaptcha'])) {
           $config->set('spam_prevention.captcha_type', 'recaptcha');
@@ -140,15 +164,24 @@ function page_notifications_update_10001(&$sandbox) {
           $config->set('spam_prevention.captcha_type', 'math');
         }
 
-        // Map email templates
-        $config->set('email_templates.verification_subject', $v3_template['verification_email_subject'] ?? '');
-        $config->set('email_templates.verification_body', $v3_template['verification_email_text'] ?? '');
-        $config->set('email_templates.notification_subject', $v3_template['general_email_template_subject'] ?? '');
-        $config->set('email_templates.notification_body', $v3_template['general_email_template'] ?? '');
+        // Map email templates with token conversion
+        $config->set('email_templates.verification_subject',
+        page_notifications_convert_tokens($v3_template['verification_email_subject'] ?? ''));
+
+        $config->set('email_templates.verification_body',
+        page_notifications_convert_tokens($v3_template['verification_email_text'] ?? ''));
+
+        $config->set('email_templates.notification_subject',
+        page_notifications_convert_tokens($v3_template['general_email_template_subject'] ?? ''));
+
+        $config->set('email_templates.notification_body',
+        page_notifications_convert_tokens($v3_template['general_email_template'] ?? ''));
 
         $config->save();
 
         \Drupal::logger('page_notifications')->notice('Migrated settings from v3 to v4.');
+        \Drupal::messenger()->addWarning(t('Email templates have been migrated from v3 to v4. Please review your templates as some tokens have changed. See documentation for the new token format.'));
+
       }
 
       // Set up batch migration for subscriptions
diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php
index 097c532..cfca16d 100644
--- a/src/Token/SubscriptionToken.php
+++ b/src/Token/SubscriptionToken.php
@@ -44,6 +44,11 @@ class SubscriptionToken {
       'description' => $this->t('Additional notes from manual notifications or revision log message'),
     ];
 
+    $tokens['subscription']['email'] = [
+      'name' => $this->t('Email'),
+      'description' => $this->t('The subscriber\'s email address'),
+    ];
+
     return [
       'types' => $types,
       'tokens' => $tokens,
@@ -77,6 +82,10 @@ class SubscriptionToken {
                 ['absolute' => TRUE]
               )->toString();
               break;
+
+          case 'email':
+            $replacements[$original] = $subscription->getEmail();
+            break;
         }
       }
     }
-- 
GitLab


From 55af341d82bc87f4e66134397c6caab0b60fd241 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Mon, 13 Jan 2025 23:01:44 -0500
Subject: [PATCH 24/49] 1hr default time frames

---
 config/install/page_notifications.settings.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml
index 651b055..88f3fd6 100644
--- a/config/install/page_notifications.settings.yml
+++ b/config/install/page_notifications.settings.yml
@@ -37,9 +37,9 @@ security:
   require_verification: 1
   flood_control:
     ip_limit: 200
-    ip_window: 24
+    ip_window: 1
     identifier_limit: 50
-    identifier_window: 24
+    identifier_window: 1
 
 spam_protection:
   enable_modal: 0
-- 
GitLab


From f4554cd95b0ff8642b23ff06a965c0c8bba40bdb Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Mon, 13 Jan 2025 23:11:56 -0500
Subject: [PATCH 25/49] Improve the uninstall removing all config and views

---
 page_notifications.install | 20 ++++++++++++++++----
 1 file changed, 16 insertions(+), 4 deletions(-)

diff --git a/page_notifications.install b/page_notifications.install
index a19567b..bbdcb5a 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -61,10 +61,22 @@ function page_notifications_schema() {
  * Implements hook_uninstall().
  */
 function page_notifications_uninstall() {
-  // Remove configuration.
-  \Drupal::configFactory()->getEditable('page_notifications.settings')->delete();
-   // Clean up state
-   \Drupal::state()->delete('page_notifications_v3_backup');
+  // Remove configuration
+  $config_factory = \Drupal::configFactory();
+
+  // List all config objects that need to be removed
+  $config_names = [
+    'page_notifications.settings',
+    'views.view.page_notification_subscriptions',
+    'views.view.top_subscribed_content'
+  ];
+
+  foreach ($config_names as $config_name) {
+    $config_factory->getEditable($config_name)->delete();
+  }
+
+  // Clean up state
+  \Drupal::state()->delete('page_notifications_v3_backup');
 }
 
 /**
-- 
GitLab


From c85995eafab95057f73ea3e75ce80da6dc35597c Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Tue, 21 Jan 2025 21:51:54 -0500
Subject: [PATCH 26/49] require token and fix Deprecated function: Creation of
 dynamic property

---
 page_notifications.info.yml         | 1 +
 src/Service/NotificationManager.php | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/page_notifications.info.yml b/page_notifications.info.yml
index 2505484..fbfa35b 100644
--- a/page_notifications.info.yml
+++ b/page_notifications.info.yml
@@ -6,5 +6,6 @@ configure: page_notifications.settings
 dependencies:
   - drupal:node
   - drupal:views
+  - token:token
 suggestions:
   - captcha:captcha
\ No newline at end of file
diff --git a/src/Service/NotificationManager.php b/src/Service/NotificationManager.php
index 7c37480..571ba2d 100644
--- a/src/Service/NotificationManager.php
+++ b/src/Service/NotificationManager.php
@@ -50,7 +50,7 @@ class NotificationManager implements NotificationManagerInterface {
    *
    * @var \Drupal\Core\Queue\QueueFactory
    */
-  protected $queue;
+  protected $queueFactory;
 
   /**
    * The logger factory.
-- 
GitLab


From 40cea6c4c617e56701b9c255c4854f3718892aea Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Tue, 21 Jan 2025 22:42:43 -0500
Subject: [PATCH 27/49] Testing HTML emails sent by module using mimemail and
 alter config to allow HTML templates in settings form.

---
 .../install/page_notifications.settings.yml   | 37 +++--------
 config/schema/page_notifications.schema.yml   | 27 +++-----
 page_notifications.info.yml                   |  1 +
 page_notifications.install                    | 49 +++++++++-----
 src/Form/SettingsForm.php                     | 64 +++++++++++++------
 src/Mail/PageNotificationsMailHandler.php     | 15 ++---
 6 files changed, 104 insertions(+), 89 deletions(-)

diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml
index 88f3fd6..dafe61a 100644
--- a/config/install/page_notifications.settings.yml
+++ b/config/install/page_notifications.settings.yml
@@ -1,38 +1,17 @@
 notification_settings:
   from_email: ''
   token_expiration: 48
-
+email_settings:
+  mail_format: full_html
 email_templates:
   verification_subject: 'Verify your subscription to [node:title]'
-  verification_body: |
-    Hello,
-
-    Please verify your email subscription to the page "[node:title]".
-    Click the following link to confirm your subscription:
-    [subscription:verify-url]
-
-    This verification link will expire soon.
-    Please verify your subscription promptly.
-
-    If you did not request this subscription, please ignore this email.
-
+  verification_body:
+    value: '<p>Hello,</p><p>Please verify your email subscription to the page "<strong>[node:title]</strong>".</p><p>Click the following link to confirm your subscription:<br><a href="[subscription:verify-url]">[subscription:verify-url]</a></p><p><em>This verification link will expire soon.<br>Please verify your subscription promptly.</em></p><p>If you did not request this subscription, please ignore this email.</p>'
+    format: full_html
   notification_subject: '[node:title] has been updated'
-  notification_body: |
-    Dear subscriber,
-
-    The page "[node:title]" that you are subscribed to has been updated.
-
-    [notification:notes]
-
-    You can view the updated page here:
-    [node:url]
-
-    To unsubscribe from these notifications, click here:
-    [subscription:unsubscribe-url]
-
-    Regards,
-    [site:name] team
-
+  notification_body:
+      value: '<p>Dear subscriber,</p><p>The page "<strong>[node:title]</strong>" that you are subscribed to has been updated.</p><p>[notification:notes]</p><p>You can view the updated page here:<br><a href="[node:url]">[node:url]</a></p><p>To unsubscribe from these notifications, click here:<br><a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p><p>Regards,<br>[site:name] team</p>'
+      format: full_html
 security:
   require_verification: 1
   flood_control:
diff --git a/config/schema/page_notifications.schema.yml b/config/schema/page_notifications.schema.yml
index 73ec57a..41ef06d 100644
--- a/config/schema/page_notifications.schema.yml
+++ b/config/schema/page_notifications.schema.yml
@@ -8,12 +8,16 @@ page_notifications.settings:
         from_email:
           type: string
           label: 'From email address'
-        email_template:
-          type: text
-          label: 'Email template'
         token_expiration:
           type: integer
           label: 'Token expiration time in hours (0 = never expire)'
+    email_settings:
+      type: mapping
+      label: 'Email Settings'
+      mapping:
+        mail_format:
+          type: string
+          label: 'Email text format'
     email_templates:
       type: mapping
       label: 'Email Templates'
@@ -22,15 +26,14 @@ page_notifications.settings:
           type: string
           label: 'Verification email subject'
         verification_body:
-          type: text
+          type: text_format
           label: 'Verification email body'
         notification_subject:
           type: string
           label: 'Notification email subject'
         notification_body:
-          type: text
+          type: text_format
           label: 'Notification email body'
-
     security:
       type: mapping
       label: 'Security Settings'
@@ -54,7 +57,6 @@ page_notifications.settings:
             identifier_window:
               type: integer
               label: 'Email-based time window (hours)'
-
     spam_protection:
       type: mapping
       label: 'Spam Protection Settings'
@@ -70,13 +72,4 @@ page_notifications.settings:
           label: 'Math captcha operator'
         captcha_point:
           type: string
-          label: 'CAPTCHA point'
-        captcha_type:
-          type: string
-          label: 'CAPTCHA type'
-        math_operator:
-          type: string
-          label: 'Math challenge operator'
-        use_recaptcha:
-          type: boolean
-          label: 'Use reCAPTCHA if available'
\ No newline at end of file
+          label: 'CAPTCHA point'
\ No newline at end of file
diff --git a/page_notifications.info.yml b/page_notifications.info.yml
index fbfa35b..e091881 100644
--- a/page_notifications.info.yml
+++ b/page_notifications.info.yml
@@ -7,5 +7,6 @@ dependencies:
   - drupal:node
   - drupal:views
   - token:token
+  - mimemail:mimemail
 suggestions:
   - captcha:captcha
\ No newline at end of file
diff --git a/page_notifications.install b/page_notifications.install
index bbdcb5a..3eb594a 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -9,6 +9,17 @@
  * Implements hook_install().
  */
 function page_notifications_install() {
+  // Try to find the best available text format
+  $preferred_formats = ['full_html', 'basic_html', 'restricted_html', 'plain_text'];
+  $selected_format = 'plain_text'; // Default fallback
+
+  $format_storage = \Drupal::entityTypeManager()->getStorage('filter_format');
+  foreach ($preferred_formats as $format_id) {
+    if ($format = $format_storage->load($format_id)) {
+      $selected_format = $format_id;
+      break;
+    }
+  }
   // Create default configuration
   $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
   if ($config->isNew()) {
@@ -16,31 +27,37 @@ function page_notifications_install() {
       ->set('notification_settings.from_email', '')
       ->set('notification_settings.token_expiration', 48)
       ->set('email_templates.verification_subject', 'Verify your subscription to [node:title]')
-      ->set('email_templates.verification_body', 'Hello,
+      ->set('email_templates.verification_body', [
+        'value' => '<p>Hello,</p>
 
-Please verify your email subscription to the page "[node:title]".
-Click the following link to confirm your subscription:
-[subscription:verify-url]
+<p>Please verify your email subscription to the page "<strong>[node:title]</strong>".</p>
+<p>Click the following link to confirm your subscription:<br>
+<a href="[subscription:verify-url]">[subscription:verify-url]</a></p>
 
-This verification link will expire soon.
-Please verify your subscription promptly.
+<p><em>This verification link will expire soon.<br>
+Please verify your subscription promptly.</em></p>
 
-If you did not request this subscription, please ignore this email.')
+<p>If you did not request this subscription, please ignore this email.</p>',
+        'format' => $selected_format,
+      ])
       ->set('email_templates.notification_subject', '[node:title] has been updated')
-      ->set('email_templates.notification_body', 'Dear subscriber,
+      ->set('email_templates.notification_body', [
+        'value' => '<p>Dear subscriber,</p>
 
-The page "[node:title]" that you are subscribed to has been updated.
+<p>The page "<strong>[node:title]</strong>" that you are subscribed to has been updated.</p>
 
-[notification:notes]
+<p>[notification:notes]</p>
 
-You can view the updated page here:
-[node:url]
+<p>You can view the updated page here:<br>
+<a href="[node:url]">[node:url]</a></p>
 
-To unsubscribe from these notifications, click here:
-[subscription:unsubscribe-url]
+<p>To unsubscribe from these notifications, click here:<br>
+<a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p>
 
-Regards,
-[site:name] team')
+<p>Regards,<br>
+[site:name] team</p>',
+        'format' => $selected_format,
+      ])
       ->set('security.require_verification', 1)
       ->set('spam_prevention.captcha_type', 'none')
       ->set('spam_prevention.math_operator', '+')
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index 798f9a8..26e52e9 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -9,6 +9,7 @@ use Drupal\Core\Mail\MailManagerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\filter\FilterFormatInterface;
 
 /**
  * Configures Page Notifications settings.
@@ -93,12 +94,34 @@ class SettingsForm extends ConfigFormBase {
     $form = parent::buildForm($form, $form_state);
     $config = $this->config('page_notifications.settings');
 
+    // Get available text formats for the current user
+    $formats = filter_formats(\Drupal::currentUser());
+    $format_options = [];
+    foreach ($formats as $format) {
+      $format_options[$format->id()] = $format->label();
+    }
+
+
     $form['email_settings'] = [
       '#type' => 'details',
       '#title' => $this->t('Email Settings'),
       '#open' => TRUE,
     ];
 
+    // text format selection
+    $form['email_settings']['mail_format'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Email Text Format'),
+      '#description' => $this->t('Select the text format to use for email content. Ensure the chosen format allows necessary HTML tags for links and formatting.'),
+      '#options' => $format_options,
+      '#default_value' => $config->get('email_settings.mail_format') ?? reset($format_options),
+      '#required' => TRUE,
+    ];
+
+    $selected_format = $config->get('email_settings.mail_format') ?? reset($format_options);
+    $verification_body = $config->get('email_templates.verification_body');
+    $notification_body = $config->get('email_templates.notification_body');
+
     $form['email_settings']['from_email'] = [
       '#type' => 'email',
       '#title' => $this->t('From Email Address'),
@@ -129,9 +152,10 @@ class SettingsForm extends ConfigFormBase {
     ];
 
     $form['email_templates']['verification_body'] = [
-      '#type' => 'textarea',
+      '#type' => 'text_format',
       '#title' => $this->t('Verification Email Body'),
-      '#default_value' => $config->get('email_templates.verification_body'),
+      '#default_value' => is_array($verification_body) ? $verification_body['value'] : $verification_body,
+      '#format' => is_array($verification_body) ? $verification_body['format'] : $selected_format,
       '#description' => $this->t('Available tokens: [subscription:verify-url], [subscription:email], [node:title], [node:url]'),
       '#required' => TRUE,
       '#rows' => 10,
@@ -145,9 +169,10 @@ class SettingsForm extends ConfigFormBase {
     ];
 
     $form['email_templates']['notification_body'] = [
-      '#type' => 'textarea',
+      '#type' => 'text_format',
       '#title' => $this->t('Update Notification Body'),
-      '#default_value' => $config->get('email_templates.notification_body'),
+      '#default_value' => is_array($notification_body) ? $notification_body['value'] : $notification_body,
+      '#format' => is_array($notification_body) ? $notification_body['format'] : $selected_format,
       '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [node:changed], [subscription:unsubscribe-url]'),
       '#required' => TRUE,
       '#rows' => 10,
@@ -283,21 +308,24 @@ class SettingsForm extends ConfigFormBase {
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
+    $values = $form_state->getValues();
+
     $this->config('page_notifications.settings')
-      ->set('notification_settings.from_email', $form_state->getValue('from_email'))
-      ->set('notification_settings.token_expiration', $form_state->getValue('token_expiration'))
-      ->set('email_templates.verification_subject', $form_state->getValue('verification_subject'))
-      ->set('email_templates.verification_body', $form_state->getValue('verification_body'))
-      ->set('email_templates.notification_subject', $form_state->getValue('notification_subject'))
-      ->set('email_templates.notification_body', $form_state->getValue('notification_body'))
-      ->set('security.require_verification', $form_state->getValue('require_verification'))
-      ->set('spam_prevention.captcha_type', $form_state->getValue('captcha_type'))
-      ->set('spam_prevention.math_operator', $form_state->getValue('math_operator'))
-      ->set('spam_prevention.use_recaptcha', $form_state->getValue('use_recaptcha'))
-      ->set('security.flood_control.ip_limit', $form_state->getValue(['flood_control', 'ip_limit']))
-      ->set('security.flood_control.ip_window', $form_state->getValue(['flood_control', 'ip_window']))
-      ->set('security.flood_control.identifier_limit', $form_state->getValue(['flood_control', 'identifier_limit']))
-      ->set('security.flood_control.identifier_window', $form_state->getValue(['flood_control', 'identifier_window']))
+      ->set('notification_settings.from_email', $values['from_email'])
+      ->set('notification_settings.token_expiration', $values['token_expiration'])
+      ->set('email_settings.mail_format', $values['mail_format'])
+      ->set('email_templates.verification_subject', $values['verification_subject'])
+      ->set('email_templates.verification_body', $values['verification_body'])
+      ->set('email_templates.notification_subject', $values['notification_subject'])
+      ->set('email_templates.notification_body', $values['notification_body'])
+      ->set('security.require_verification', $values['require_verification'])
+      ->set('security.flood_control.ip_limit', $values['flood_control']['ip_limit'])
+      ->set('security.flood_control.ip_window', $values['flood_control']['ip_window'])
+      ->set('security.flood_control.identifier_limit', $values['flood_control']['identifier_limit'])
+      ->set('security.flood_control.identifier_window', $values['flood_control']['identifier_window'])
+      ->set('spam_prevention.captcha_type', $values['captcha_type'])
+      ->set('spam_prevention.math_operator', $values['math_operator'])
+      ->set('spam_prevention.use_recaptcha', $values['use_recaptcha'] ?? FALSE)
       ->save();
 
     parent::submitForm($form, $form_state);
diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php
index 3540a8c..6928280 100644
--- a/src/Mail/PageNotificationsMailHandler.php
+++ b/src/Mail/PageNotificationsMailHandler.php
@@ -3,7 +3,6 @@
 namespace Drupal\page_notifications\Mail;
 
 use Drupal\Core\Mail\MailManagerInterface;
-use Drupal\Core\Mail\MailFormatHelper;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -69,10 +68,8 @@ class PageNotificationsMailHandler {
         break;
     }
 
-    // Ensure proper line endings for emails.
-    $message['body'] = array_map(function ($line) {
-      return MailFormatHelper::wrapMail($line);
-    }, $message['body']);
+    // Let Mime Mail know we want HTML.
+    $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed; delsp=yes';
   }
 
   /**
@@ -92,7 +89,7 @@ class PageNotificationsMailHandler {
     $body = $config->get('email_templates.verification_body');
 
     $message['subject'] = $this->token->replace($subject, $token_data);
-    $message['body'][] = $this->token->replace($body, $token_data);
+    $message['body'] = [$this->token->replace($body['value'], $token_data)];
   }
 
   /**
@@ -108,8 +105,8 @@ class PageNotificationsMailHandler {
     $token_data = [
       'subscription' => $params['subscription'],
       'node' => $params['entity'],
-      'entity' => $params['entity'], // Add this for the notification token type
-      'notification' => [], // Add empty array to trigger notification token type
+      'entity' => $params['entity'],
+      'notification' => [],
     ];
 
     $subject = $config->get('email_templates.notification_subject');
@@ -117,7 +114,7 @@ class PageNotificationsMailHandler {
 
     if ($subject && $body) {
       $message['subject'] = $this->token->replace($subject, $token_data);
-      $message['body'][] = $this->token->replace($body, $token_data);
+      $message['body'] = [$this->token->replace($body['value'], $token_data)];
     }
   }
 
-- 
GitLab


From b1010829dcc365c53e6f542b104b3cc867cbf99a Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Thu, 23 Jan 2025 21:46:03 -0500
Subject: [PATCH 28/49] Add already subscribed message and email for already
 verified subscribers.

---
 .../install/page_notifications.settings.yml   | 21 +++--
 page_notifications.install                    | 11 +++
 src/Form/SettingsForm.php                     | 20 +++++
 src/Mail/PageNotificationsMailHandler.php     | 28 +++++-
 src/Service/NotificationManager.php           | 86 ++++++++++++++-----
 5 files changed, 136 insertions(+), 30 deletions(-)

diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml
index dafe61a..314cfe0 100644
--- a/config/install/page_notifications.settings.yml
+++ b/config/install/page_notifications.settings.yml
@@ -10,18 +10,25 @@ email_templates:
     format: full_html
   notification_subject: '[node:title] has been updated'
   notification_body:
-      value: '<p>Dear subscriber,</p><p>The page "<strong>[node:title]</strong>" that you are subscribed to has been updated.</p><p>[notification:notes]</p><p>You can view the updated page here:<br><a href="[node:url]">[node:url]</a></p><p>To unsubscribe from these notifications, click here:<br><a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p><p>Regards,<br>[site:name] team</p>'
-      format: full_html
+    value: '<p>Dear subscriber,</p><p>The page "<strong>[node:title]</strong>" that you are subscribed to has been updated.</p><p>[notification:notes]</p><p>You can view the updated page here:<br><a href="[node:url]">[node:url]</a></p><p>To unsubscribe from these notifications, click here:<br><a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p><p>Regards,<br>[site:name] team</p>'
+    format: full_html
+  already_subscribed_subject: 'You are already subscribed to [node:title]'
+  already_subscribed_body:
+    value: '<p>Dear subscriber,</p><p>You are already subscribed to "<strong>[node:title]</strong>" and ready for future notifications.</p><p>You can view the content here:<br><a href="[node:url]">[node:url]</a></p><p>If you wish to unsubscribe, you can do so here:<br><a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p><p>Regards,<br>[site:name] team</p>'
+    format: full_html
 security:
-  require_verification: 1
+  require_verification: true
   flood_control:
     ip_limit: 200
     ip_window: 1
     identifier_limit: 50
     identifier_window: 1
-
 spam_protection:
-  enable_modal: 0
-  enable_math_captcha: 1
+  enable_modal: false
+  enable_math_captcha: true
   math_captcha_operator: +
-  captcha_point: null
\ No newline at end of file
+  captcha_point: null
+spam_prevention:
+  captcha_type: none
+  math_operator: +
+  use_recaptcha: false
\ No newline at end of file
diff --git a/page_notifications.install b/page_notifications.install
index 3eb594a..2b0fb30 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -40,6 +40,17 @@ Please verify your subscription promptly.</em></p>
 <p>If you did not request this subscription, please ignore this email.</p>',
         'format' => $selected_format,
       ])
+      ->set('email_templates.already_subscribed_subject', 'You are already subscribed to [node:title]')
+    ->set('email_templates.already_subscribed_body', [
+      'value' => '<p>Dear subscriber,</p>
+        <p>You are already subscribed to "<strong>[node:title]</strong>".</p>
+        <p>You can view the content here:<br>
+        <a href="[node:url]">[node:url]</a></p>
+        <p>If you wish to unsubscribe, you can do so here:<br>
+        <a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p>
+        <p>Regards,<br>[site:name] team</p>',
+      'format' => $selected_format,
+    ])
       ->set('email_templates.notification_subject', '[node:title] has been updated')
       ->set('email_templates.notification_body', [
         'value' => '<p>Dear subscriber,</p>
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index 26e52e9..f1b2d3a 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -121,6 +121,7 @@ class SettingsForm extends ConfigFormBase {
     $selected_format = $config->get('email_settings.mail_format') ?? reset($format_options);
     $verification_body = $config->get('email_templates.verification_body');
     $notification_body = $config->get('email_templates.notification_body');
+    $already_subscribed_body = $config->get('email_templates.already_subscribed_body');
 
     $form['email_settings']['from_email'] = [
       '#type' => 'email',
@@ -178,6 +179,23 @@ class SettingsForm extends ConfigFormBase {
       '#rows' => 10,
     ];
 
+    $form['email_templates']['already_subscribed_subject'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Already Subscribed Email Subject'),
+      '#default_value' => $config->get('email_templates.already_subscribed_subject') ?? 'You are already subscribed to [node:title]',
+      '#required' => TRUE,
+    ];
+    
+    $form['email_templates']['already_subscribed_body'] = [
+      '#type' => 'text_format',
+      '#title' => $this->t('Already Subscribed Email Body'),
+      '#default_value' => is_array($already_subscribed_body) ? $already_subscribed_body['value'] : $already_subscribed_body,
+      '#format' => is_array($already_subscribed_body) ? $already_subscribed_body['format'] : $selected_format,
+      '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [subscription:unsubscribe-url]'),
+      '#required' => TRUE,
+      '#rows' => 10,
+    ];
+
     $form['security'] = [
       '#type' => 'details',
       '#title' => $this->t('Security Settings'),
@@ -316,6 +334,8 @@ class SettingsForm extends ConfigFormBase {
       ->set('email_settings.mail_format', $values['mail_format'])
       ->set('email_templates.verification_subject', $values['verification_subject'])
       ->set('email_templates.verification_body', $values['verification_body'])
+      ->set('email_templates.already_subscribed_subject', $values['already_subscribed_subject'])
+->set('email_templates.already_subscribed_body', $values['already_subscribed_body'])
       ->set('email_templates.notification_subject', $values['notification_subject'])
       ->set('email_templates.notification_body', $values['notification_body'])
       ->set('security.require_verification', $values['require_verification'])
diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php
index 6928280..5488c0e 100644
--- a/src/Mail/PageNotificationsMailHandler.php
+++ b/src/Mail/PageNotificationsMailHandler.php
@@ -62,10 +62,14 @@ class PageNotificationsMailHandler {
       case 'verification':
         $this->buildVerificationEmail($message, $params);
         break;
-
+  
       case 'notification':
         $this->buildNotificationEmail($message, $params);
         break;
+  
+      case 'already_subscribed':
+        $this->buildAlreadySubscribedEmail($message, $params);
+        break;
     }
 
     // Let Mime Mail know we want HTML.
@@ -92,6 +96,28 @@ class PageNotificationsMailHandler {
     $message['body'] = [$this->token->replace($body['value'], $token_data)];
   }
 
+  /**
+   * Builds an "already subscribed" email.
+   */
+  protected function buildAlreadySubscribedEmail(array &$message, array $params) {
+    $config = $this->configFactory->get('page_notifications.settings');
+    
+    if (!isset($params['subscription']) || !isset($params['entity'])) {
+      return;
+    }
+
+    $token_data = [
+      'subscription' => $params['subscription'],
+      'node' => $params['entity'],
+    ];
+
+    $subject = $config->get('email_templates.already_subscribed_subject');
+    $body = $config->get('email_templates.already_subscribed_body');
+
+    $message['subject'] = $this->token->replace($subject, $token_data);
+    $message['body'] = [$this->token->replace($body['value'], $token_data)];
+  }
+
   /**
    * Builds a notification email.
    */
diff --git a/src/Service/NotificationManager.php b/src/Service/NotificationManager.php
index 571ba2d..306bb6a 100644
--- a/src/Service/NotificationManager.php
+++ b/src/Service/NotificationManager.php
@@ -128,8 +128,7 @@ class NotificationManager implements NotificationManagerInterface {
    */
   public function createSubscription(string $email, EntityInterface $entity, ?string $langcode = null) {
     try {
-
-      // Check for existing subscription.
+      // Check for existing subscription
       $existing_subscriptions = $this->entityTypeManager
         ->getStorage('page_notification_subscription')
         ->loadByProperties([
@@ -137,37 +136,54 @@ class NotificationManager implements NotificationManagerInterface {
           'subscribed_entity_id' => $entity->id(),
           'subscribed_entity_type' => $entity->getEntityTypeId(),
         ]);
-
-      // If subscription already exists, return it.
+  
       if (!empty($existing_subscriptions)) {
+        /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
         $subscription = reset($existing_subscriptions);
-        // If subscription exists but isn't verified, resend verification email
-        if (!$subscription->isActive() && $this->requiresVerification()) {
+  
+        // Handle different subscription states
+        if ($subscription->isActive()) {
+          // Already verified subscription - send "already subscribed" email
+          $this->sendAlreadySubscribedEmail($subscription, $entity);
+          $this->messenger->addStatus($this->t('You are already subscribed to this content.'));
+          return $subscription;
+        }
+        
+        // Check if token is expired
+        if ($this->isTokenExpired($subscription)) {
+          // Generate new token and update subscription
+          $subscription->setToken($this->generateToken());
+          $subscription->setCreatedTime($this->time->getRequestTime());
+          $subscription->save();
+        }
+        
+        // Resend verification email for unverified subscriptions
+        if ($this->requiresVerification()) {
           $this->sendVerificationEmail($subscription);
+          $this->messenger->addStatus($this->t('A new verification email has been sent to your address.'));
         }
+        
         return $subscription;
       }
-
-      $token = $this->generateToken();
-
-      /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
+  
+      // Create new subscription if none exists
       $subscription = $this->entityTypeManager
-      ->getStorage('page_notification_subscription')
-      ->create([
-        'email' => $email,
-        'subscribed_entity_id' => $entity->id(),
-        'subscribed_entity_type' => $entity->getEntityTypeId(),
-        'token' => $this->generateToken(),
-        'unsubscribe_token' => $this->generateToken(),
-        'status' => !$this->requiresVerification(),
-      ]);
-
+        ->getStorage('page_notification_subscription')
+        ->create([
+          'email' => $email,
+          'subscribed_entity_id' => $entity->id(),
+          'subscribed_entity_type' => $entity->getEntityTypeId(),
+          'token' => $this->generateToken(),
+          'unsubscribe_token' => $this->generateToken(),
+          'status' => !$this->requiresVerification(),
+        ]);
+  
       $subscription->save();
-
+  
       if ($this->requiresVerification()) {
         $this->sendVerificationEmail($subscription);
       }
-
+  
       return $subscription;
     }
     catch (\Exception $e) {
@@ -252,6 +268,32 @@ class NotificationManager implements NotificationManagerInterface {
     }
   }
 
+  /**
+   * Sends an "already subscribed" email notification.
+   *
+   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
+   *   The subscription entity.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being subscribed to.
+   */
+  protected function sendAlreadySubscribedEmail($subscription, $entity) {
+    $config = $this->configFactory->get('page_notifications.settings');
+    
+    $params = [
+      'subscription' => $subscription,
+      'entity' => $entity,
+    ];
+
+    $this->mailManager->mail(
+      'page_notifications',
+      'already_subscribed',
+      $subscription->getEmail(),
+      $subscription->getLanguageCode(),
+      $params,
+      $config->get('notification_settings.from_email')
+    );
+  }
+
   /**
    * Retrieves the queue for processing notifications.
    *
-- 
GitLab


From f31ee472e144cd39fa3ca4c43ef70380fbfbbe15 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Thu, 23 Jan 2025 21:50:23 -0500
Subject: [PATCH 29/49] remove duplicate settings, default goes in the install
 folder, and dynamic in our install hook

---
 page_notifications.install | 57 +++-----------------------------------
 1 file changed, 4 insertions(+), 53 deletions(-)

diff --git a/page_notifications.install b/page_notifications.install
index 2b0fb30..b0102f6 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -20,60 +20,11 @@ function page_notifications_install() {
       break;
     }
   }
-  // Create default configuration
+  // Set Dynamic configuration for email format
   $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
-  if ($config->isNew()) {
-    $config
-      ->set('notification_settings.from_email', '')
-      ->set('notification_settings.token_expiration', 48)
-      ->set('email_templates.verification_subject', 'Verify your subscription to [node:title]')
-      ->set('email_templates.verification_body', [
-        'value' => '<p>Hello,</p>
-
-<p>Please verify your email subscription to the page "<strong>[node:title]</strong>".</p>
-<p>Click the following link to confirm your subscription:<br>
-<a href="[subscription:verify-url]">[subscription:verify-url]</a></p>
-
-<p><em>This verification link will expire soon.<br>
-Please verify your subscription promptly.</em></p>
-
-<p>If you did not request this subscription, please ignore this email.</p>',
-        'format' => $selected_format,
-      ])
-      ->set('email_templates.already_subscribed_subject', 'You are already subscribed to [node:title]')
-    ->set('email_templates.already_subscribed_body', [
-      'value' => '<p>Dear subscriber,</p>
-        <p>You are already subscribed to "<strong>[node:title]</strong>".</p>
-        <p>You can view the content here:<br>
-        <a href="[node:url]">[node:url]</a></p>
-        <p>If you wish to unsubscribe, you can do so here:<br>
-        <a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p>
-        <p>Regards,<br>[site:name] team</p>',
-      'format' => $selected_format,
-    ])
-      ->set('email_templates.notification_subject', '[node:title] has been updated')
-      ->set('email_templates.notification_body', [
-        'value' => '<p>Dear subscriber,</p>
-
-<p>The page "<strong>[node:title]</strong>" that you are subscribed to has been updated.</p>
-
-<p>[notification:notes]</p>
-
-<p>You can view the updated page here:<br>
-<a href="[node:url]">[node:url]</a></p>
-
-<p>To unsubscribe from these notifications, click here:<br>
-<a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p>
-
-<p>Regards,<br>
-[site:name] team</p>',
-        'format' => $selected_format,
-      ])
-      ->set('security.require_verification', 1)
-      ->set('spam_prevention.captcha_type', 'none')
-      ->set('spam_prevention.math_operator', '+')
-      ->save();
-  }
+  $config
+    ->set('email_settings.mail_format', $selected_format)
+    ->save();
 }
 
 /**
-- 
GitLab


From 7a7bf1e0e0a13795c6704c71e3a1bc1baf94f9cc Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Thu, 23 Jan 2025 22:03:11 -0500
Subject: [PATCH 30/49] Add the ability to purge existing subscriptions to
 allow the module to be uninstalled if desired.

---
 page_notifications.routing.yml             |   8 ++
 src/Form/PurgeSubscriptionsConfirmForm.php | 135 +++++++++++++++++++++
 src/Form/SettingsForm.php                  |  85 +++++++++++++
 3 files changed, 228 insertions(+)
 create mode 100644 src/Form/PurgeSubscriptionsConfirmForm.php

diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index 017d6ae..fb37f61 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -64,5 +64,13 @@ page_notifications.subscription_add:
   defaults:
     _form: '\Drupal\page_notifications\Form\ManualSubscriptionAddForm'
     _title: 'Add Subscriptions'
+  requirements:
+    _permission: 'administer page notification subscriptions'
+    
+page_notifications.purge_subscriptions_confirm:
+  path: '/admin/config/system/page-notifications/purge-confirm'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\PurgeSubscriptionsConfirmForm'
+    _title: 'Confirm subscription purge'
   requirements:
     _permission: 'administer page notification subscriptions'
\ No newline at end of file
diff --git a/src/Form/PurgeSubscriptionsConfirmForm.php b/src/Form/PurgeSubscriptionsConfirmForm.php
new file mode 100644
index 0000000..ee37e5b
--- /dev/null
+++ b/src/Form/PurgeSubscriptionsConfirmForm.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class PurgeSubscriptionsConfirmForm extends ConfirmFormBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new PurgeSubscriptionsConfirmForm.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_purge_subscriptions_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    $count = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->accessCheck(FALSE)
+      ->count()
+      ->execute();
+
+    return $this->t('Are you sure you want to delete all @count subscriptions?', [
+      '@count' => $count,
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->t('This action cannot be undone. All subscription data will be permanently deleted.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('page_notifications.settings');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $batch = [
+      'title' => $this->t('Deleting all subscriptions...'),
+      'operations' => [
+        [[$this, 'purgeSubscriptionsBatch'], []],
+      ],
+      'finished' => [[$this, 'purgeSubscriptionsFinished']],
+    ];
+    batch_set($batch);
+    $form_state->setRedirect('page_notifications.settings');
+  }
+
+  /**
+   * Batch operation to purge subscriptions.
+   */
+  public function purgeSubscriptionsBatch(&$context) {
+    if (!isset($context['sandbox']['progress'])) {
+      $context['sandbox']['progress'] = 0;
+      $context['sandbox']['current_id'] = 0;
+      $context['sandbox']['max'] = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->getQuery()
+        ->accessCheck(FALSE)
+        ->count()
+        ->execute();
+    }
+
+    $subscription_ids = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('id', $context['sandbox']['current_id'], '>')
+      ->sort('id')
+      ->range(0, 50)
+      ->accessCheck(FALSE)
+      ->execute();
+
+    if (!empty($subscription_ids)) {
+      $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
+      $entities = $storage->loadMultiple($subscription_ids);
+      $storage->delete($entities);
+
+      $context['sandbox']['current_id'] = end($subscription_ids);
+      $context['sandbox']['progress'] += count($subscription_ids);
+    }
+
+    $context['finished'] = empty($subscription_ids) ? 1 : $context['sandbox']['progress'] / $context['sandbox']['max'];
+  }
+
+  /**
+   * Batch finished callback.
+   */
+  public function purgeSubscriptionsFinished($success, $results, $operations) {
+    if ($success) {
+      $this->messenger()->addStatus($this->t('Successfully deleted all subscriptions.'));
+    }
+    else {
+      $this->messenger()->addError($this->t('An error occurred while deleting subscriptions.'));
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index f1b2d3a..ca0cf4d 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -10,6 +10,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\filter\FilterFormatInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Link;
 
 /**
  * Configures Page Notifications settings.
@@ -310,6 +312,35 @@ class SettingsForm extends ConfigFormBase {
       ];
     }
 
+    $form['danger_zone'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Danger Zone'),
+      '#description' => $this->t('These actions cannot be undone.'),
+      '#open' => FALSE,
+      '#weight' => 100,
+    ];
+  
+    $subscription_count = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->accessCheck(FALSE)
+      ->count()
+      ->execute();
+  
+      $form['danger_zone']['purge_subscriptions'] = [
+        '#type' => 'link',
+        '#title' => $this->t('Delete all subscriptions (@count total)', ['@count' => $subscription_count]),
+        '#url' => Url::fromRoute('page_notifications.purge_subscriptions_confirm'),
+        '#attributes' => [
+          'class' => ['button', 'button--danger', 'use-ajax'],
+          'data-dialog-type' => 'modal',
+          'data-dialog-options' => json_encode([
+            'width' => 700,
+          ]),
+        ],
+        '#disabled' => ($subscription_count === 0),
+      ];
+
     return parent::buildForm($form, $form_state);
   }
 
@@ -322,6 +353,60 @@ class SettingsForm extends ConfigFormBase {
     }
   }
 
+  public function purgeSubscriptions(array &$form, FormStateInterface $form_state) {
+    $batch = [
+      'title' => $this->t('Deleting all subscriptions...'),
+      'operations' => [
+        [[$this, 'purgeSubscriptionsBatch'], []],
+      ],
+      'finished' => [[$this, 'purgeSubscriptionsFinished']],
+    ];
+    batch_set($batch);
+  }
+  
+  public function purgeSubscriptionsBatch(&$context) {
+    if (!isset($context['sandbox']['progress'])) {
+      $context['sandbox']['progress'] = 0;
+      $context['sandbox']['current_id'] = 0;
+      $context['sandbox']['max'] = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->getQuery()
+        ->accessCheck(FALSE)
+        ->count()
+        ->execute();
+    }
+  
+    // Process subscriptions in chunks of 50
+    $subscription_ids = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('id', $context['sandbox']['current_id'], '>')
+      ->sort('id')
+      ->range(0, 50)
+      ->accessCheck(FALSE)
+      ->execute();
+  
+    if (!empty($subscription_ids)) {
+      $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
+      $entities = $storage->loadMultiple($subscription_ids);
+      $storage->delete($entities);
+  
+      $context['sandbox']['current_id'] = end($subscription_ids);
+      $context['sandbox']['progress'] += count($subscription_ids);
+    }
+  
+    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+  }
+  
+  public function purgeSubscriptionsFinished($success, $results, $operations) {
+    if ($success) {
+      $this->messenger()->addStatus($this->t('Successfully deleted all subscriptions.'));
+    }
+    else {
+      $this->messenger()->addError($this->t('An error occurred while deleting subscriptions.'));
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
-- 
GitLab


From e6783d888c55b47249d381f61e0e87e1e75f8b45 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Thu, 23 Jan 2025 22:31:48 -0500
Subject: [PATCH 31/49] Add email headers and footer that can be configured as
 needed to make emails look more professional.

---
 README.md                                     |  30 +++++
 page_notifications.module                     |  37 ++++++
 page_notifications.services.yml               |   1 +
 src/Mail/PageNotificationsMailHandler.php     | 111 ++++++++++++++----
 ...page-notifications-email-wrapper.html.twig |  38 ++++++
 5 files changed, 196 insertions(+), 21 deletions(-)
 create mode 100644 templates/page-notifications-email-wrapper.html.twig

diff --git a/README.md b/README.md
index 1c33729..3fd27fc 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,36 @@ The module implements several security measures:
 * Automatic cleanup of unverified subscriptions
 * Permission-based access control
 
+## Email Customization
+
+### Template Override
+You can override the email template by copying 
+`templates/page-notifications-email-wrapper.html.twig` to your theme and modifying it.
+
+Template suggestions available:
+- page-notifications-email-wrapper--verification.html.twig
+- page-notifications-email-wrapper--notification.html.twig
+- page-notifications-email-wrapper--already-subscribed.html.twig
+
+### Programmatic Customization
+Implement these hooks in your custom module:
+
+```php
+/**
+ * Implements hook_page_notifications_email_variables_alter().
+ */
+function mymodule_page_notifications_email_variables_alter(&$variables) {
+  // Add custom variables to email template
+  $variables['my_variable'] = 'Custom content';
+}
+
+/**
+ * Implements hook_page_notifications_email_footer_alter().
+ */
+function mymodule_page_notifications_email_footer_alter(&$footer) {
+  $footer = 'Custom footer content';
+}
+
 ## API
 
 The module provides services and interfaces for developers to:
diff --git a/page_notifications.module b/page_notifications.module
index 8176a38..f1ea83a 100644
--- a/page_notifications.module
+++ b/page_notifications.module
@@ -112,5 +112,42 @@ function page_notifications_theme() {
       'template' => 'block--page-notifications-subscription',
       'base hook' => 'block',
     ],
+    'page_notifications_email_wrapper' => [
+      'variables' => [
+        'content' => NULL,
+        'email_type' => NULL,
+        'subscription' => NULL,
+        'entity' => NULL,
+        'logo_url' => \Drupal::request()->getSchemeAndHttpHost() . theme_get_setting('logo.url'),
+        'site_name' => \Drupal::config('system.site')->get('name'),
+        'footer' => NULL,
+      ],
+      'template' => 'page-notifications-email-wrapper',
+    ],
   ];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function page_notifications_theme_suggestions_page_notifications_email_wrapper(array $variables) {
+  $suggestions = [];
+  
+  if (!empty($variables['email_type'])) {
+    $suggestions[] = 'page_notifications_email_wrapper__' . $variables['email_type'];
+  }
+  
+  return $suggestions;
+}
+
+/**
+ * Implements hook_preprocess_page_notifications_email_wrapper().
+ */
+function page_notifications_preprocess_page_notifications_email_wrapper(&$variables) {
+  // Ensure logo URL is absolute
+  if (!empty($variables['logo_url']) && !preg_match('/^(http|https):\/\//', $variables['logo_url'])) {
+    $variables['logo_url'] = \Drupal::request()->getSchemeAndHttpHost() . $variables['logo_url'];
+  }
+  // Allow modules to alter email variables
+  \Drupal::moduleHandler()->alter('page_notifications_email_variables', $variables);
 }
\ No newline at end of file
diff --git a/page_notifications.services.yml b/page_notifications.services.yml
index 7cb7d3f..2012e8e 100644
--- a/page_notifications.services.yml
+++ b/page_notifications.services.yml
@@ -20,6 +20,7 @@ services:
       - '@renderer'
       - '@token'
       - '@string_translation'
+      - '@theme.manager'
   page_notifications.subscription_token:
     class: Drupal\page_notifications\Token\SubscriptionToken
     tags:
diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php
index 5488c0e..c8611d8 100644
--- a/src/Mail/PageNotificationsMailHandler.php
+++ b/src/Mail/PageNotificationsMailHandler.php
@@ -8,6 +8,7 @@ use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\token\TokenInterface;
+use Drupal\Core\Theme\ThemeManagerInterface;
 
 /**
  * Handles mail formatting for page notifications.
@@ -37,6 +38,13 @@ class PageNotificationsMailHandler {
    */
   protected $token;
 
+  /**
+   * The theme manager.
+   *
+   * @var \Drupal\Core\Theme\ThemeManagerInterface
+   */
+  protected $themeManager;
+
   /**
    * Constructs a new PageNotificationsMailHandler.
    */
@@ -44,12 +52,44 @@ class PageNotificationsMailHandler {
     ConfigFactoryInterface $config_factory,
     RendererInterface $renderer,
     TokenInterface $token,
-    TranslationInterface $translation
+    TranslationInterface $translation,
+    ThemeManagerInterface $theme_manager
   ) {
     $this->configFactory = $config_factory;
     $this->renderer = $renderer;
     $this->token = $token;
     $this->setStringTranslation($translation);
+    $this->themeManager = $theme_manager;
+  }
+
+   /**
+   * Wraps email content in themed template.
+   */
+  protected function wrapEmailContent($content, $email_type, $subscription, $entity) {
+    // Ensure content is properly structured as markup
+    $processed_content = [
+      '#type' => 'markup',
+      '#markup' => $content,
+    ];
+
+    return [
+      '#theme' => 'page_notifications_email_wrapper',
+      '#content' => $processed_content,
+      '#email_type' => $email_type,
+      '#subscription' => $subscription,
+      '#entity' => $entity,
+      '#footer' => $this->getEmailFooter(),
+    ];
+  }
+
+  /**
+   * Gets customized email footer.
+   */
+  protected function getEmailFooter() {
+    // Allow modules to alter the footer
+    $footer = '';
+    \Drupal::moduleHandler()->alter('page_notifications_email_footer', $footer);
+    return $footer;
   }
 
   /**
@@ -93,15 +133,25 @@ class PageNotificationsMailHandler {
     $body = $config->get('email_templates.verification_body');
 
     $message['subject'] = $this->token->replace($subject, $token_data);
-    $message['body'] = [$this->token->replace($body['value'], $token_data)];
+    
+    // Process the body content
+    $body_content = $this->token->replace($body['value'], $token_data);
+    $themed_content = $this->wrapEmailContent(
+      $body_content,
+      'verification',
+      $subscription,
+      $entity
+    );
+    
+    $message['body'] = [$this->renderer->render($themed_content)];
   }
 
   /**
-   * Builds an "already subscribed" email.
+   * Builds a notification email.
    */
-  protected function buildAlreadySubscribedEmail(array &$message, array $params) {
+  protected function buildNotificationEmail(array &$message, array $params) {
     $config = $this->configFactory->get('page_notifications.settings');
-    
+
     if (!isset($params['subscription']) || !isset($params['entity'])) {
       return;
     }
@@ -109,21 +159,35 @@ class PageNotificationsMailHandler {
     $token_data = [
       'subscription' => $params['subscription'],
       'node' => $params['entity'],
+      'entity' => $params['entity'],
+      'notification' => [],
     ];
 
-    $subject = $config->get('email_templates.already_subscribed_subject');
-    $body = $config->get('email_templates.already_subscribed_body');
+    $subject = $config->get('email_templates.notification_subject');
+    $body = $config->get('email_templates.notification_body');
 
-    $message['subject'] = $this->token->replace($subject, $token_data);
-    $message['body'] = [$this->token->replace($body['value'], $token_data)];
+    if ($subject && $body) {
+      $message['subject'] = $this->token->replace($subject, $token_data);
+      
+      // Process the body content
+      $body_content = $this->token->replace($body['value'], $token_data);
+      $themed_content = $this->wrapEmailContent(
+        $body_content,
+        'notification',
+        $params['subscription'],
+        $params['entity']
+      );
+      
+      $message['body'] = [$this->renderer->render($themed_content)];
+    }
   }
 
   /**
-   * Builds a notification email.
+   * Builds an "already subscribed" email.
    */
-  protected function buildNotificationEmail(array &$message, array $params) {
+  protected function buildAlreadySubscribedEmail(array &$message, array $params) {
     $config = $this->configFactory->get('page_notifications.settings');
-
+    
     if (!isset($params['subscription']) || !isset($params['entity'])) {
       return;
     }
@@ -131,17 +195,22 @@ class PageNotificationsMailHandler {
     $token_data = [
       'subscription' => $params['subscription'],
       'node' => $params['entity'],
-      'entity' => $params['entity'],
-      'notification' => [],
     ];
 
-    $subject = $config->get('email_templates.notification_subject');
-    $body = $config->get('email_templates.notification_body');
+    $subject = $config->get('email_templates.already_subscribed_subject');
+    $body = $config->get('email_templates.already_subscribed_body');
 
-    if ($subject && $body) {
-      $message['subject'] = $this->token->replace($subject, $token_data);
-      $message['body'] = [$this->token->replace($body['value'], $token_data)];
-    }
+    $message['subject'] = $this->token->replace($subject, $token_data);
+    
+    // Process the body content
+    $body_content = $this->token->replace($body['value'], $token_data);
+    $themed_content = $this->wrapEmailContent(
+      $body_content,
+      'already_subscribed',
+      $params['subscription'],
+      $params['entity']
+    );
+    
+    $message['body'] = [$this->renderer->render($themed_content)];
   }
-
 }
\ No newline at end of file
diff --git a/templates/page-notifications-email-wrapper.html.twig b/templates/page-notifications-email-wrapper.html.twig
new file mode 100644
index 0000000..d03308f
--- /dev/null
+++ b/templates/page-notifications-email-wrapper.html.twig
@@ -0,0 +1,38 @@
+{#
+/**
+ * @file
+ * Default template for Page Notifications emails.
+ *
+ * Available variables:
+ * - content: The main email content.
+ * - email_type: The type of email (verification, notification, etc.).
+ * - subscription: The subscription entity.
+ * - entity: The subscribed entity.
+ * - logo_url: The site logo URL.
+ * - site_name: The site name.
+ * - footer: Custom footer content.
+ */
+#}
+<div class="page-notifications-email">
+  {% if logo_url %}
+    <div class="email-header">
+      <img src="{{ logo_url }}" alt="{{ site_name }}" style="max-width: 200px; height: auto;">
+    </div>
+  {% endif %}
+
+  <div class="email-content">
+    {{ content }}
+  </div>
+
+  {% if footer %}
+    <div class="email-footer">
+      {{ footer }}
+    </div>
+  {% else %}
+    <div class="email-footer">
+      <p style="color: #666; font-size: 12px;">
+        © {{ "now"|date("Y") }} {{ site_name }}
+      </p>
+    </div>
+  {% endif %}
+</div>
\ No newline at end of file
-- 
GitLab


From 3cc1c8f0f7596fce928314411aa702e0753d15a6 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Fri, 24 Jan 2025 06:46:38 -0500
Subject: [PATCH 32/49] inital shot at makeing the block have a modal setting
 for improved UI/UX

---
 page_notifications.libraries.yml       |   6 +
 page_notifications.routing.yml         |  15 +-
 src/Controller/ModalFormController.php |  63 ++++++
 src/Form/ModalSubscriptionForm.php     | 269 +++++++++++++++++++++++++
 src/Plugin/Block/SubscriptionBlock.php |  63 +++++-
 5 files changed, 405 insertions(+), 11 deletions(-)
 create mode 100644 page_notifications.libraries.yml
 create mode 100644 src/Controller/ModalFormController.php
 create mode 100644 src/Form/ModalSubscriptionForm.php

diff --git a/page_notifications.libraries.yml b/page_notifications.libraries.yml
new file mode 100644
index 0000000..cdd66bd
--- /dev/null
+++ b/page_notifications.libraries.yml
@@ -0,0 +1,6 @@
+modal:
+  version: 1.x
+  dependencies:
+    - core/drupal.dialog.ajax
+    - core/drupal.ajax
+    - core/once
\ No newline at end of file
diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index fb37f61..3551bac 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -66,11 +66,22 @@ page_notifications.subscription_add:
     _title: 'Add Subscriptions'
   requirements:
     _permission: 'administer page notification subscriptions'
-    
+
 page_notifications.purge_subscriptions_confirm:
   path: '/admin/config/system/page-notifications/purge-confirm'
   defaults:
     _form: '\Drupal\page_notifications\Form\PurgeSubscriptionsConfirmForm'
     _title: 'Confirm subscription purge'
   requirements:
-    _permission: 'administer page notification subscriptions'
\ No newline at end of file
+    _permission: 'administer page notification subscriptions'
+page_notifications.modal_form:
+  path: '/page-notifications/modal-form/{node}'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\ModalSubscriptionForm'
+    _title: 'Subscribe to Updates'
+  requirements:
+    _access: 'TRUE'
+  options:
+    parameters:
+      node:
+        type: entity:node
\ No newline at end of file
diff --git a/src/Controller/ModalFormController.php b/src/Controller/ModalFormController.php
new file mode 100644
index 0000000..875c268
--- /dev/null
+++ b/src/Controller/ModalFormController.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\page_notifications\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Form\FormBuilderInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\node\NodeInterface;
+
+/**
+ * Controller for the subscription modal form.
+ */
+class ModalFormController extends ControllerBase {
+
+  /**
+   * The form builder.
+   *
+   * @var \Drupal\Core\Form\FormBuilderInterface
+   */
+  protected $formBuilder;
+
+  /**
+   * Constructs a new ModalFormController.
+   *
+   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
+   *   The form builder.
+   */
+  public function __construct(FormBuilderInterface $form_builder) {
+    $this->formBuilder = $form_builder;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('form_builder')
+    );
+  }
+
+  /**
+   * Returns the subscription form in a modal.
+   *
+   * @param \Drupal\node\NodeInterface $node
+   *   The node being subscribed to.
+   *
+   * @return array
+   *   The render array for the modal form.
+   */
+  public function content(NodeInterface $node) {
+    $build = [
+      '#prefix' => '<div id="modal-subscription-form-wrapper">',
+      '#suffix' => '</div>',
+      'status_messages' => [
+        '#type' => 'status_messages',
+      ],
+      'form' => $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node),
+    ];
+
+    return $build;
+  }
+
+}
\ No newline at end of file
diff --git a/src/Form/ModalSubscriptionForm.php b/src/Form/ModalSubscriptionForm.php
new file mode 100644
index 0000000..8e6d1cd
--- /dev/null
+++ b/src/Form/ModalSubscriptionForm.php
@@ -0,0 +1,269 @@
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\page_notifications\Service\NotificationManagerInterface;
+use Drupal\page_notifications\Service\SpamPrevention;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Psr\Log\LoggerInterface;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\page_notifications\Traits\FloodControlTrait;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseModalDialogCommand;
+use Drupal\Core\Ajax\MessageCommand;
+use Drupal\Core\Ajax\ReplaceCommand;
+
+/**
+ * Provides a subscription form for modal display.
+ */
+class ModalSubscriptionForm extends FormBase {
+  use FloodControlTrait;
+
+  /**
+   * The notification manager service.
+   *
+   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
+   */
+  protected $notificationManager;
+
+  /**
+   * The spam prevention service.
+   *
+   * @var \Drupal\page_notifications\Service\SpamPrevention
+   */
+  protected $spamPrevention;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The logger instance.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new ModalSubscriptionForm.
+   */
+  public function __construct(
+    NotificationManagerInterface $notification_manager,
+    SpamPrevention $spam_prevention,
+    ConfigFactoryInterface $config_factory,
+    LoggerInterface $logger,
+    FloodInterface $flood
+  ) {
+    $this->notificationManager = $notification_manager;
+    $this->spamPrevention = $spam_prevention;
+    $this->configFactory = $config_factory;
+    $this->logger = $logger;
+    $this->setFloodService($flood);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('page_notifications.notification_manager'),
+      $container->get('page_notifications.spam_prevention'),
+      $container->get('config.factory'),
+      $container->get('logger.factory')->get('page_notifications'),
+      $container->get('flood')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_modal_subscription_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $node = NULL) {
+    if (!$node) {
+      return [];
+    }
+
+    $form['#prefix'] = '<div id="modal-subscription-form-wrapper">';
+    $form['#suffix'] = '</div>';
+
+    // Store the node entity in the form state
+    $form_state->set('node', $node);
+
+    $form['email'] = [
+      '#type' => 'email',
+      '#title' => $this->t('Email address'),
+      '#required' => TRUE,
+      '#description' => $this->t('Enter your email address to receive notifications when this content is updated.'),
+    ];
+
+    // Add spam prevention based on configuration
+    $config = $this->configFactory->get('page_notifications.settings');
+    $captcha_type = $config->get('spam_prevention.captcha_type');
+
+    if ($captcha_type === 'math') {
+      if (!$form_state->getUserInput()) {
+        $challenge = $this->spamPrevention->generateMathChallenge();
+        $form['math_challenge_data'] = [
+          '#type' => 'hidden',
+          '#value' => json_encode($challenge),
+        ];
+      }
+      else {
+        $challenge = json_decode($form_state->getUserInput()['math_challenge_data'] ?? '{}', TRUE);
+      }
+
+      if (!empty($challenge)) {
+        $form['math_challenge_data'] = [
+          '#type' => 'hidden',
+          '#value' => json_encode($challenge),
+        ];
+
+        $form['math_challenge'] = [
+          '#type' => 'number',
+          '#title' => $challenge['question'],
+          '#required' => TRUE,
+          '#description' => $this->t('Please solve this simple math problem to prevent spam.'),
+        ];
+      }
+    }
+    elseif ($captcha_type === 'recaptcha' && $this->spamPrevention->isRecaptchaAvailable()) {
+      $form['captcha'] = [
+        '#type' => 'captcha',
+        '#captcha_type' => 'recaptcha/reCAPTCHA',
+      ];
+    }
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Subscribe'),
+      '#ajax' => [
+        'callback' => '::submitModalAjax',
+        'event' => 'click',
+        'progress' => [
+          'type' => 'throbber',
+          'message' => $this->t('Processing...'),
+        ],
+      ],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * AJAX callback for modal form submission.
+   */
+  public function submitModalAjax(array &$form, FormStateInterface $form_state) {
+    $response = new AjaxResponse();
+
+    if ($form_state->hasAnyErrors()) {
+      $response->addCommand(new ReplaceCommand(
+        '#modal-subscription-form-wrapper',
+        [
+          '#type' => 'container',
+          '#attributes' => ['id' => 'modal-subscription-form-wrapper'],
+          'status_messages' => [
+            '#type' => 'status_messages',
+          ],
+          'form' => $form,
+        ]
+      ));
+      return $response;
+    }
+
+    try {
+      // Get the node from form state
+      $node = $form_state->get('node');
+      if (!$node) {
+        throw new \Exception('No node found for subscription.');
+      }
+
+      $email = $form_state->getValue('email');
+      $subscription = $this->notificationManager->createSubscription($email, $node);
+
+      $response->addCommand(new CloseModalDialogCommand());
+      $response->addCommand(new MessageCommand(
+        $this->t('Thank you for subscribing. Please check your email to confirm your subscription.'),
+        NULL,
+        ['type' => 'status']
+      ));
+    }
+    catch (\Exception $e) {
+      $this->logger->error('Subscription error: @message', ['@message' => $e->getMessage()]);
+
+      $response->addCommand(new ReplaceCommand(
+        '#modal-subscription-form-wrapper',
+        [
+          '#type' => 'container',
+          '#attributes' => ['id' => 'modal-subscription-form-wrapper'],
+          'status_messages' => [
+            '#type' => 'status_messages',
+            '#message_list' => [
+              'error' => [$this->t('There was a problem creating your subscription. Please try again later.')],
+            ],
+          ],
+          'form' => $form,
+        ]
+      ));
+    }
+
+    return $response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $email = $form_state->getValue('email');
+
+    // Check flood control before other validation
+    if (!$this->checkFloodControl($email, $form_state)) {
+      return;
+    }
+
+    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+      $form_state->setErrorByName('email', $this->t('Please enter a valid email address.'));
+      return;
+    }
+
+    // Validate math challenge if enabled
+    $config = $this->configFactory->get('page_notifications.settings');
+    $captcha_type = $config->get('spam_prevention.captcha_type');
+
+    if ($captcha_type === 'math') {
+      $challenge_data = $form_state->getValue('math_challenge_data');
+      if ($challenge_data) {
+        $challenge = json_decode($challenge_data, TRUE);
+        $response = $form_state->getValue('math_challenge');
+
+        if (!$this->spamPrevention->validateMathResponse($response, $challenge)) {
+          $form_state->setErrorByName('math_challenge', $this->t('The answer to the math challenge is incorrect.'));
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Empty as everything is handled in the AJAX callback
+  }
+
+}
\ No newline at end of file
diff --git a/src/Plugin/Block/SubscriptionBlock.php b/src/Plugin/Block/SubscriptionBlock.php
index 8b2df1a..922a652 100644
--- a/src/Plugin/Block/SubscriptionBlock.php
+++ b/src/Plugin/Block/SubscriptionBlock.php
@@ -11,6 +11,7 @@ use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
 
 /**
  * Provides a subscription block.
@@ -98,6 +99,8 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
       'button_classes' => 'button button--primary',
       'form_classes' => 'subscription-form',
       'show_description' => TRUE,
+      'use_modal' => FALSE,
+      'modal_title' => $this->t('Subscribe to Updates'),
     ] + parent::defaultConfiguration();
   }
 
@@ -139,6 +142,25 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
       '#default_value' => $config['button_text'],
     ];
 
+    $form['appearance']['use_modal'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Use modal dialog'),
+      '#description' => $this->t('Display the subscription form in a modal dialog.'),
+      '#default_value' => $config['use_modal'],
+    ];
+
+    $form['appearance']['modal_title'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Modal Title'),
+      '#description' => $this->t('The title displayed at the top of the modal dialog.'),
+      '#default_value' => $config['modal_title'],
+      '#states' => [
+        'visible' => [
+          ':input[name="settings[appearance][use_modal]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+
     $form['styling'] = [
       '#type' => 'details',
       '#title' => $this->t('CSS Classes'),
@@ -171,6 +193,8 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
     $this->configuration['button_classes'] = $form_state->getValue(['styling', 'button_classes']);
     $this->configuration['form_classes'] = $form_state->getValue(['styling', 'form_classes']);
     $this->configuration['show_description'] = $form_state->getValue(['appearance', 'show_description']);
+    $this->configuration['use_modal'] = $form_state->getValue(['appearance', 'use_modal']);
+    $this->configuration['modal_title'] = $form_state->getValue(['appearance', 'modal_title']);
   }
 
   /**
@@ -183,13 +207,6 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
         return [];
       }
 
-      $form = $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node);
-
-      // Add our custom configuration to the form
-      $form['#attributes']['class'][] = $this->configuration['form_classes'];
-      $form['actions']['submit']['#value'] = $this->configuration['button_text'];
-      $form['actions']['submit']['#attributes']['class'] = explode(' ', $this->configuration['button_classes']);
-
       $build = [];
 
       if ($this->configuration['show_description']) {
@@ -200,9 +217,37 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
         ];
       }
 
-      $build['form'] = $form;
+      if ($this->configuration['use_modal']) {
+        // Modal trigger button
+        $url = Url::fromRoute('page_notifications.modal_form', [
+          'node' => $node->id(),
+        ]);
+
+        $build['modal_button'] = [
+          '#type' => 'link',
+          '#title' => $this->configuration['button_text'],
+          '#url' => $url,
+          '#attributes' => [
+            'class' => array_merge(['use-ajax'], explode(' ', $this->configuration['button_classes'])),
+            'data-dialog-type' => 'modal',
+            'data-dialog-options' => json_encode([
+              'width' => 500,
+              'title' => $this->configuration['modal_title'],
+            ]),
+          ],
+        ];
+
+        // Attach required libraries
+        $build['#attached']['library'][] = 'core/drupal.dialog.ajax';
+        $build['#attached']['library'][] = 'page_notifications/modal';
+      } else {
+        $form = $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node);
+        $form['#attributes']['class'][] = $this->configuration['form_classes'];
+        $form['actions']['submit']['#value'] = $this->configuration['button_text'];
+        $form['actions']['submit']['#attributes']['class'] = explode(' ', $this->configuration['button_classes']);
+        $build['form'] = $form;
+      }
 
-      // Add cache contexts and tags
       $build['#cache']['contexts'][] = 'url.path';
       $build['#cache']['tags'][] = 'node:' . $node->id();
 
-- 
GitLab


From 1a970604a94a79eaf07389547729729576b173cd Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Fri, 24 Jan 2025 06:50:58 -0500
Subject: [PATCH 33/49] Convert modal from node to entity type for future
 flexibility

---
 page_notifications.routing.yml         |  6 +++---
 src/Form/ModalSubscriptionForm.php     | 21 +++++++++++----------
 src/Plugin/Block/SubscriptionBlock.php |  3 ++-
 3 files changed, 16 insertions(+), 14 deletions(-)

diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml
index 3551bac..bab80c9 100644
--- a/page_notifications.routing.yml
+++ b/page_notifications.routing.yml
@@ -75,7 +75,7 @@ page_notifications.purge_subscriptions_confirm:
   requirements:
     _permission: 'administer page notification subscriptions'
 page_notifications.modal_form:
-  path: '/page-notifications/modal-form/{node}'
+  path: '/page-notifications/modal-form/{entity_type}/{entity}'
   defaults:
     _form: '\Drupal\page_notifications\Form\ModalSubscriptionForm'
     _title: 'Subscribe to Updates'
@@ -83,5 +83,5 @@ page_notifications.modal_form:
     _access: 'TRUE'
   options:
     parameters:
-      node:
-        type: entity:node
\ No newline at end of file
+      entity:
+        type: entity:{entity_type}
\ No newline at end of file
diff --git a/src/Form/ModalSubscriptionForm.php b/src/Form/ModalSubscriptionForm.php
index 8e6d1cd..52f11b7 100644
--- a/src/Form/ModalSubscriptionForm.php
+++ b/src/Form/ModalSubscriptionForm.php
@@ -91,16 +91,18 @@ class ModalSubscriptionForm extends FormBase {
   /**
    * {@inheritdoc}
    */
-  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $node = NULL) {
-    if (!$node) {
-      return [];
+  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL) {
+    if (!$entity) {
+      return [
+        '#markup' => $this->t('No content found for subscription.'),
+      ];
     }
 
     $form['#prefix'] = '<div id="modal-subscription-form-wrapper">';
     $form['#suffix'] = '</div>';
 
-    // Store the node entity in the form state
-    $form_state->set('node', $node);
+    // Store the entity in the form state
+    $form_state->set('entity', $entity);
 
     $form['email'] = [
       '#type' => 'email',
@@ -188,14 +190,13 @@ class ModalSubscriptionForm extends FormBase {
     }
 
     try {
-      // Get the node from form state
-      $node = $form_state->get('node');
-      if (!$node) {
-        throw new \Exception('No node found for subscription.');
+      $entity = $form_state->get('entity');
+      if (!$entity) {
+        throw new \Exception('No entity found for subscription.');
       }
 
       $email = $form_state->getValue('email');
-      $subscription = $this->notificationManager->createSubscription($email, $node);
+      $subscription = $this->notificationManager->createSubscription($email, $entity);
 
       $response->addCommand(new CloseModalDialogCommand());
       $response->addCommand(new MessageCommand(
diff --git a/src/Plugin/Block/SubscriptionBlock.php b/src/Plugin/Block/SubscriptionBlock.php
index 922a652..bdde092 100644
--- a/src/Plugin/Block/SubscriptionBlock.php
+++ b/src/Plugin/Block/SubscriptionBlock.php
@@ -220,7 +220,8 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
       if ($this->configuration['use_modal']) {
         // Modal trigger button
         $url = Url::fromRoute('page_notifications.modal_form', [
-          'node' => $node->id(),
+          'entity_type' => $node->getEntityTypeId(),
+          'entity' => $node->id(),
         ]);
 
         $build['modal_button'] = [
-- 
GitLab


From 2a7b3b6b8f194c9ae7dfa44fd71a4fcf59da00a6 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Fri, 24 Jan 2025 07:01:22 -0500
Subject: [PATCH 34/49] top subscribed content under the /admin/content route
 tabs

---
 page_notifications.links.task.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/page_notifications.links.task.yml b/page_notifications.links.task.yml
index 820c815..2f87446 100644
--- a/page_notifications.links.task.yml
+++ b/page_notifications.links.task.yml
@@ -25,5 +25,5 @@ page_notifications.subscription_migrate:
 page_notifications.top_subscribed:
   route_name: page_notifications.top_subscribed
   title: 'Top Subscribed Content'
-  base_route: page_notifications.admin_settings
-  weight: 3
\ No newline at end of file
+  base_route: system.admin_content  # This connects it to the admin content page
+  weight: 5
\ No newline at end of file
-- 
GitLab


From 73b1107e98b54cc6b5b15bf823cd57914b10110b Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Fri, 24 Jan 2025 07:37:08 -0500
Subject: [PATCH 35/49] bugfix: Form state cache issue after submit showing
 temp unavailable.

---
 src/Form/SubscriptionForm.php          |  7 +++++++
 src/Plugin/Block/SubscriptionBlock.php | 25 ++++++++++++++++++++-----
 2 files changed, 27 insertions(+), 5 deletions(-)

diff --git a/src/Form/SubscriptionForm.php b/src/Form/SubscriptionForm.php
index 7216a3a..1b0b840 100644
--- a/src/Form/SubscriptionForm.php
+++ b/src/Form/SubscriptionForm.php
@@ -207,11 +207,18 @@ class SubscriptionForm extends FormBase {
 
       $subscription = $this->notificationManager->createSubscription($email, $entity);
       $this->messenger()->addStatus($this->t('Thank you for subscribing. Please check your email to confirm your subscription.'));
+
+      // Clear the email field after successful submission
+      $form_state->setValue('email', '');
+      $form_state->setUserInput(['email' => '']);
     }
     catch (\Exception $e) {
       $this->messenger()->addError($this->t('There was a problem creating your subscription. Please try again later.'));
       $this->logger->error('Subscription creation failed: @message', ['@message' => $e->getMessage()]);
     }
+
+    // Set form to rebuild
+    $form_state->setRebuild(TRUE);
   }
 
 }
\ No newline at end of file
diff --git a/src/Plugin/Block/SubscriptionBlock.php b/src/Plugin/Block/SubscriptionBlock.php
index bdde092..03c1ad3 100644
--- a/src/Plugin/Block/SubscriptionBlock.php
+++ b/src/Plugin/Block/SubscriptionBlock.php
@@ -202,7 +202,15 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
    */
   public function build() {
     try {
+      // Get node from route match or current path
       $node = \Drupal::routeMatch()->getParameter('node');
+      if (!$node) {
+        $path_args = explode('/', \Drupal::service('path.current')->getPath());
+        if (isset($path_args[2]) && is_numeric($path_args[2])) {
+          $node = \Drupal::entityTypeManager()->getStorage('node')->load($path_args[2]);
+        }
+      }
+
       if (!$node) {
         return [];
       }
@@ -249,8 +257,17 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
         $build['form'] = $form;
       }
 
-      $build['#cache']['contexts'][] = 'url.path';
-      $build['#cache']['tags'][] = 'node:' . $node->id();
+       // Add cache contexts and tags
+      $build['#cache'] = [
+        'contexts' => [
+          'url.path',
+          'route',
+        ],
+        'tags' => [
+          'node:' . $node->id(),
+        ],
+        'max-age' => 0 // Disable caching for this block
+      ];
 
       return $build;
     }
@@ -259,9 +276,7 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
         'Error building subscription block: @message',
         ['@message' => $e->getMessage()]
       );
-      return [
-        '#markup' => $this->t('The subscription form is temporarily unavailable.'),
-      ];
+      return [];
     }
   }
 
-- 
GitLab


From 255cd1409c0f90c931bb51b471e6fb2ddff69b5b Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Fri, 24 Jan 2025 15:27:59 -0500
Subject: [PATCH 36/49] Inital shot at drupal 11 compatibility, not tested
 backwards yet to 10

---
 composer.json                             |  3 +-
 page_notifications.info.yml               |  4 +--
 page_notifications.install                | 11 +++---
 page_notifications.module                 | 11 +++---
 page_notifications.services.yml           |  1 +
 src/Form/SettingsForm.php                 | 25 +++++++------
 src/Mail/PageNotificationsMailHandler.php | 44 ++++++++++++++++-------
 src/Token/SubscriptionToken.php           |  4 +++
 8 files changed, 69 insertions(+), 34 deletions(-)

diff --git a/composer.json b/composer.json
index bcac47e..8a7154a 100644
--- a/composer.json
+++ b/composer.json
@@ -17,7 +17,8 @@
   },
   "require": {
     "php": ">=8.1",
-    "drupal/core": "^10"
+    "drupal/core": "^10",
+    "drupal/symfony_mailer_lite": "^2.0"
   },
   "suggest": {
     "drupal/captcha": "Provides additional spam protection options including reCAPTCHA integration"
diff --git a/page_notifications.info.yml b/page_notifications.info.yml
index e091881..b0d176e 100644
--- a/page_notifications.info.yml
+++ b/page_notifications.info.yml
@@ -1,12 +1,12 @@
 name: Page Notifications
 type: module
 description: 'Anonymous users can subscribe to pages to receive notifications about updates.'
-core_version_requirement: ^10
+core_version_requirement: ^10.3 || ^11
 configure: page_notifications.settings
 dependencies:
   - drupal:node
   - drupal:views
   - token:token
-  - mimemail:mimemail
+  - drupal:symfony_mailer_lite
 suggestions:
   - captcha:captcha
\ No newline at end of file
diff --git a/page_notifications.install b/page_notifications.install
index b0102f6..6a23b2a 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -1,5 +1,8 @@
 <?php
 
+use Drupal\Core\Config\FileStorage;
+use Drupal\page_notifications\Service\MigrationService;
+
 /**
  * @file
  * Install, update and uninstall functions for the page_notifications module.
@@ -10,7 +13,7 @@
  */
 function page_notifications_install() {
   // Try to find the best available text format
-  $preferred_formats = ['full_html', 'basic_html', 'restricted_html', 'plain_text'];
+  $preferred_formats = ['email', 'easy_email', 'full_html', 'basic_html', 'restricted_html', 'plain_text'];
   $selected_format = 'plain_text'; // Default fallback
 
   $format_storage = \Drupal::entityTypeManager()->getStorage('filter_format');
@@ -73,7 +76,7 @@ function page_notifications_update_10001(&$sandbox) {
 
   // Install required views and configuration regardless of migration status
   $module_path = \Drupal::service('extension.list.module')->getPath('page_notifications');
-  $source = new \Drupal\Core\Config\FileStorage($module_path . '/config/install');
+  $source = new FileStorage($module_path . '/config/install');
   $config_storage = \Drupal::service('config.storage');
 
   // List of all configurations to install
@@ -176,7 +179,7 @@ function page_notifications_update_10001(&$sandbox) {
       }
 
       // Set up batch migration for subscriptions
-      $batch = \Drupal\page_notifications\Service\MigrationService::createMigrationBatch();
+      $batch = MigrationService::createMigrationBatch();
       if ($batch) {
         batch_set($batch);
       }
@@ -191,4 +194,4 @@ function page_notifications_update_10001(&$sandbox) {
   return $has_v3_data ?
     t('Subscription data has been migrated, views and default configuration have been installed. Please review your Page Notifications settings at /admin/config/system/page-notifications') :
     t('Page Notifications v4 has been installed with default configuration.');
-}
\ No newline at end of file
+}
diff --git a/page_notifications.module b/page_notifications.module
index f1ea83a..97ade38 100644
--- a/page_notifications.module
+++ b/page_notifications.module
@@ -1,5 +1,6 @@
 <?php
 
+use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Form\FormStateInterface;
 
 /**
@@ -39,7 +40,7 @@ function page_notifications_token_info() {
 /**
  * Implements hook_tokens().
  */
-function page_notifications_tokens($type, $tokens, array $data, array $options, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) {
+function page_notifications_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
   return \Drupal::service('page_notifications.subscription_token')->hookTokens($type, $tokens, $data, $options, $bubbleable_metadata);
 }
 
@@ -118,7 +119,7 @@ function page_notifications_theme() {
         'email_type' => NULL,
         'subscription' => NULL,
         'entity' => NULL,
-        'logo_url' => \Drupal::request()->getSchemeAndHttpHost() . theme_get_setting('logo.url'),
+        'logo_url' =>  theme_get_setting('logo.url') ? \Drupal::request()->getSchemeAndHttpHost() . theme_get_setting('logo.url') : NULL,
         'site_name' => \Drupal::config('system.site')->get('name'),
         'footer' => NULL,
       ],
@@ -132,11 +133,11 @@ function page_notifications_theme() {
  */
 function page_notifications_theme_suggestions_page_notifications_email_wrapper(array $variables) {
   $suggestions = [];
-  
+
   if (!empty($variables['email_type'])) {
     $suggestions[] = 'page_notifications_email_wrapper__' . $variables['email_type'];
   }
-  
+
   return $suggestions;
 }
 
@@ -150,4 +151,4 @@ function page_notifications_preprocess_page_notifications_email_wrapper(&$variab
   }
   // Allow modules to alter email variables
   \Drupal::moduleHandler()->alter('page_notifications_email_variables', $variables);
-}
\ No newline at end of file
+}
diff --git a/page_notifications.services.yml b/page_notifications.services.yml
index 2012e8e..29192ea 100644
--- a/page_notifications.services.yml
+++ b/page_notifications.services.yml
@@ -21,6 +21,7 @@ services:
       - '@token'
       - '@string_translation'
       - '@theme.manager'
+      - '@symfony_mailer_lite.mailer'
   page_notifications.subscription_token:
     class: Drupal\page_notifications\Token\SubscriptionToken
     tags:
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index ca0cf4d..51d3bad 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -57,7 +57,12 @@ class SettingsForm extends ConfigFormBase {
     EntityTypeManagerInterface $entity_type_manager,
     ModuleHandlerInterface $module_handler
   ) {
-    parent::__construct($config_factory);
+    // Check Drupal version
+    if (version_compare(\Drupal::VERSION, '11.0', '>=')) {
+      parent::__construct($config_factory, \Drupal::service('config.typed'));
+  } else {
+      parent::__construct($config_factory);
+  }
     $this->mailManager = $mail_manager;
     $this->entityTypeManager = $entity_type_manager;
     $this->moduleHandler = $module_handler;
@@ -187,7 +192,7 @@ class SettingsForm extends ConfigFormBase {
       '#default_value' => $config->get('email_templates.already_subscribed_subject') ?? 'You are already subscribed to [node:title]',
       '#required' => TRUE,
     ];
-    
+
     $form['email_templates']['already_subscribed_body'] = [
       '#type' => 'text_format',
       '#title' => $this->t('Already Subscribed Email Body'),
@@ -319,14 +324,14 @@ class SettingsForm extends ConfigFormBase {
       '#open' => FALSE,
       '#weight' => 100,
     ];
-  
+
     $subscription_count = $this->entityTypeManager
       ->getStorage('page_notification_subscription')
       ->getQuery()
       ->accessCheck(FALSE)
       ->count()
       ->execute();
-  
+
       $form['danger_zone']['purge_subscriptions'] = [
         '#type' => 'link',
         '#title' => $this->t('Delete all subscriptions (@count total)', ['@count' => $subscription_count]),
@@ -363,7 +368,7 @@ class SettingsForm extends ConfigFormBase {
     ];
     batch_set($batch);
   }
-  
+
   public function purgeSubscriptionsBatch(&$context) {
     if (!isset($context['sandbox']['progress'])) {
       $context['sandbox']['progress'] = 0;
@@ -375,7 +380,7 @@ class SettingsForm extends ConfigFormBase {
         ->count()
         ->execute();
     }
-  
+
     // Process subscriptions in chunks of 50
     $subscription_ids = $this->entityTypeManager
       ->getStorage('page_notification_subscription')
@@ -385,19 +390,19 @@ class SettingsForm extends ConfigFormBase {
       ->range(0, 50)
       ->accessCheck(FALSE)
       ->execute();
-  
+
     if (!empty($subscription_ids)) {
       $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
       $entities = $storage->loadMultiple($subscription_ids);
       $storage->delete($entities);
-  
+
       $context['sandbox']['current_id'] = end($subscription_ids);
       $context['sandbox']['progress'] += count($subscription_ids);
     }
-  
+
     $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
   }
-  
+
   public function purgeSubscriptionsFinished($success, $results, $operations) {
     if ($success) {
       $this->messenger()->addStatus($this->t('Successfully deleted all subscriptions.'));
diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php
index c8611d8..04c66ed 100644
--- a/src/Mail/PageNotificationsMailHandler.php
+++ b/src/Mail/PageNotificationsMailHandler.php
@@ -9,6 +9,8 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\token\TokenInterface;
 use Drupal\Core\Theme\ThemeManagerInterface;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\Address;
 
 /**
  * Handles mail formatting for page notifications.
@@ -45,6 +47,8 @@ class PageNotificationsMailHandler {
    */
   protected $themeManager;
 
+  protected $mailer;
+
   /**
    * Constructs a new PageNotificationsMailHandler.
    */
@@ -53,13 +57,15 @@ class PageNotificationsMailHandler {
     RendererInterface $renderer,
     TokenInterface $token,
     TranslationInterface $translation,
-    ThemeManagerInterface $theme_manager
+    ThemeManagerInterface $theme_manager,
+    $mailer
   ) {
     $this->configFactory = $config_factory;
     $this->renderer = $renderer;
     $this->token = $token;
     $this->setStringTranslation($translation);
     $this->themeManager = $theme_manager;
+    $this->mailer = $mailer;
   }
 
    /**
@@ -102,18 +108,32 @@ class PageNotificationsMailHandler {
       case 'verification':
         $this->buildVerificationEmail($message, $params);
         break;
-  
+
       case 'notification':
         $this->buildNotificationEmail($message, $params);
         break;
-  
+
       case 'already_subscribed':
         $this->buildAlreadySubscribedEmail($message, $params);
         break;
     }
 
-    // Let Mime Mail know we want HTML.
-    $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed; delsp=yes';
+    // Convert Drupal Markup to string for Symfony Email
+    $htmlContent = (string) $message['body'][0];
+
+     // Create Symfony Email object
+     $email = (new Email())
+     ->subject($message['subject'])
+     ->html($htmlContent)
+     ->to($message['to']);
+
+   // Set from address if configured
+   if (!empty($config->get('notification_settings.from_email'))) {
+     $email->from($config->get('notification_settings.from_email'));
+   }
+
+   // Store the Email object
+   $message['symfony_email'] = $email;
   }
 
   /**
@@ -133,7 +153,7 @@ class PageNotificationsMailHandler {
     $body = $config->get('email_templates.verification_body');
 
     $message['subject'] = $this->token->replace($subject, $token_data);
-    
+
     // Process the body content
     $body_content = $this->token->replace($body['value'], $token_data);
     $themed_content = $this->wrapEmailContent(
@@ -142,7 +162,7 @@ class PageNotificationsMailHandler {
       $subscription,
       $entity
     );
-    
+
     $message['body'] = [$this->renderer->render($themed_content)];
   }
 
@@ -168,7 +188,7 @@ class PageNotificationsMailHandler {
 
     if ($subject && $body) {
       $message['subject'] = $this->token->replace($subject, $token_data);
-      
+
       // Process the body content
       $body_content = $this->token->replace($body['value'], $token_data);
       $themed_content = $this->wrapEmailContent(
@@ -177,7 +197,7 @@ class PageNotificationsMailHandler {
         $params['subscription'],
         $params['entity']
       );
-      
+
       $message['body'] = [$this->renderer->render($themed_content)];
     }
   }
@@ -187,7 +207,7 @@ class PageNotificationsMailHandler {
    */
   protected function buildAlreadySubscribedEmail(array &$message, array $params) {
     $config = $this->configFactory->get('page_notifications.settings');
-    
+
     if (!isset($params['subscription']) || !isset($params['entity'])) {
       return;
     }
@@ -201,7 +221,7 @@ class PageNotificationsMailHandler {
     $body = $config->get('email_templates.already_subscribed_body');
 
     $message['subject'] = $this->token->replace($subject, $token_data);
-    
+
     // Process the body content
     $body_content = $this->token->replace($body['value'], $token_data);
     $themed_content = $this->wrapEmailContent(
@@ -210,7 +230,7 @@ class PageNotificationsMailHandler {
       $params['subscription'],
       $params['entity']
     );
-    
+
     $message['body'] = [$this->renderer->render($themed_content)];
   }
 }
\ No newline at end of file
diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php
index cfca16d..ecdf348 100644
--- a/src/Token/SubscriptionToken.php
+++ b/src/Token/SubscriptionToken.php
@@ -64,6 +64,10 @@ class SubscriptionToken {
     if ($type == 'subscription' && !empty($data['subscription'])) {
       $subscription = $data['subscription'];
 
+      // Add cache metadata for the token
+    $bubbleable_metadata = new BubbleableMetadata();
+    $bubbleable_metadata->addCacheableDependency($subscription);
+
       foreach ($tokens as $name => $original) {
         switch ($name) {
           case 'verify-url':
-- 
GitLab


From 47ca62bc676a2ef90d989c6f96c1c6bce1524313 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Sat, 25 Jan 2025 12:51:57 -0500
Subject: [PATCH 37/49] Drupal 10 testing with symphony mailer lite and
 cleaning up mail code

---
 page_notifications.module                 |  15 ---
 src/Mail/PageNotificationsMailHandler.php | 141 ++++++----------------
 2 files changed, 34 insertions(+), 122 deletions(-)

diff --git a/page_notifications.module b/page_notifications.module
index 97ade38..3eb39ff 100644
--- a/page_notifications.module
+++ b/page_notifications.module
@@ -15,21 +15,6 @@ function page_notifications_mail($key, &$message, $params) {
   \Drupal::service('page_notifications.mail_handler')->mail($key, $message, $params);
 }
 
-/**
- * Implements hook_mail_alter().
- */
-function page_notifications_mail_alter(&$message) {
-  if (strpos($message['id'], 'page_notifications_') === 0) {
-    // Set format to HTML for our emails.
-    $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed; delsp=yes';
-
-    // Ensure body is processed as HTML.
-    if (is_array($message['body'])) {
-      $message['body'] = [implode("\n\n", $message['body'])];
-    }
-  }
-}
-
 /**
  * Implements hook_token_info().
  */
diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php
index 04c66ed..2f1d65e 100644
--- a/src/Mail/PageNotificationsMailHandler.php
+++ b/src/Mail/PageNotificationsMailHandler.php
@@ -9,8 +9,7 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\token\TokenInterface;
 use Drupal\Core\Theme\ThemeManagerInterface;
-use Symfony\Component\Mime\Email;
-use Symfony\Component\Mime\Address;
+
 
 /**
  * Handles mail formatting for page notifications.
@@ -47,8 +46,6 @@ class PageNotificationsMailHandler {
    */
   protected $themeManager;
 
-  protected $mailer;
-
   /**
    * Constructs a new PageNotificationsMailHandler.
    */
@@ -58,34 +55,12 @@ class PageNotificationsMailHandler {
     TokenInterface $token,
     TranslationInterface $translation,
     ThemeManagerInterface $theme_manager,
-    $mailer
   ) {
     $this->configFactory = $config_factory;
     $this->renderer = $renderer;
     $this->token = $token;
     $this->setStringTranslation($translation);
     $this->themeManager = $theme_manager;
-    $this->mailer = $mailer;
-  }
-
-   /**
-   * Wraps email content in themed template.
-   */
-  protected function wrapEmailContent($content, $email_type, $subscription, $entity) {
-    // Ensure content is properly structured as markup
-    $processed_content = [
-      '#type' => 'markup',
-      '#markup' => $content,
-    ];
-
-    return [
-      '#theme' => 'page_notifications_email_wrapper',
-      '#content' => $processed_content,
-      '#email_type' => $email_type,
-      '#subscription' => $subscription,
-      '#entity' => $entity,
-      '#footer' => $this->getEmailFooter(),
-    ];
   }
 
   /**
@@ -117,29 +92,12 @@ class PageNotificationsMailHandler {
         $this->buildAlreadySubscribedEmail($message, $params);
         break;
     }
-
-    // Convert Drupal Markup to string for Symfony Email
-    $htmlContent = (string) $message['body'][0];
-
-     // Create Symfony Email object
-     $email = (new Email())
-     ->subject($message['subject'])
-     ->html($htmlContent)
-     ->to($message['to']);
-
-   // Set from address if configured
-   if (!empty($config->get('notification_settings.from_email'))) {
-     $email->from($config->get('notification_settings.from_email'));
-   }
-
-   // Store the Email object
-   $message['symfony_email'] = $email;
   }
 
   /**
    * Builds a verification email.
    */
-  protected function buildVerificationEmail(array &$message, array $params) {
+  protected function buildEmail(array &$message, array $params, string $template_type) {
     $config = $this->configFactory->get('page_notifications.settings');
     $subscription = $params['subscription'];
     $entity = $params['entity'];
@@ -147,90 +105,59 @@ class PageNotificationsMailHandler {
     $token_data = [
       'subscription' => $subscription,
       'node' => $entity,
+      'notification' => $params['notification'] ?? [],
     ];
 
-    $subject = $config->get('email_templates.verification_subject');
-    $body = $config->get('email_templates.verification_body');
+    $subject_key = "email_templates.{$template_type}_subject";
+    $body_key = "email_templates.{$template_type}_body";
 
-    $message['subject'] = $this->token->replace($subject, $token_data);
+    $message['subject'] = $this->token->replace($config->get($subject_key), $token_data);
 
-    // Process the body content
-    $body_content = $this->token->replace($body['value'], $token_data);
+    // Set HTML body
+    $body = $this->token->replace($config->get($body_key)['value'], $token_data);
     $themed_content = $this->wrapEmailContent(
-      $body_content,
-      'verification',
+      $body,
+      $template_type,
       $subscription,
       $entity
     );
 
+    // Configure for HTML mail
+    $message['params']['format'] = 'text/html';
+    $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed';
     $message['body'] = [$this->renderer->render($themed_content)];
   }
 
   /**
-   * Builds a notification email.
+   * Wraps email content in themed template.
    */
-  protected function buildNotificationEmail(array &$message, array $params) {
-    $config = $this->configFactory->get('page_notifications.settings');
-
-    if (!isset($params['subscription']) || !isset($params['entity'])) {
-      return;
-    }
+  protected function wrapEmailContent($content, $email_type, $subscription, $entity) {
 
-    $token_data = [
-      'subscription' => $params['subscription'],
-      'node' => $params['entity'],
-      'entity' => $params['entity'],
-      'notification' => [],
+     // Ensure content is properly structured as markup
+    $processed_content = [
+      '#type' => 'markup',
+      '#markup' => $content,
     ];
 
-    $subject = $config->get('email_templates.notification_subject');
-    $body = $config->get('email_templates.notification_body');
-
-    if ($subject && $body) {
-      $message['subject'] = $this->token->replace($subject, $token_data);
+    return [
+      '#theme' => 'page_notifications_email_wrapper',
+      '#content' => $processed_content,
+      '#email_type' => $email_type,
+      '#subscription' => $subscription,
+      '#entity' => $entity,
+      '#footer' => $this->getEmailFooter(),
+    ];
+  }
 
-      // Process the body content
-      $body_content = $this->token->replace($body['value'], $token_data);
-      $themed_content = $this->wrapEmailContent(
-        $body_content,
-        'notification',
-        $params['subscription'],
-        $params['entity']
-      );
+  protected function buildVerificationEmail(array &$message, array $params) {
+    $this->buildEmail($message, $params, 'verification');
+  }
 
-      $message['body'] = [$this->renderer->render($themed_content)];
-    }
+  protected function buildNotificationEmail(array &$message, array $params) {
+    $this->buildEmail($message, $params, 'notification');
   }
 
-  /**
-   * Builds an "already subscribed" email.
-   */
   protected function buildAlreadySubscribedEmail(array &$message, array $params) {
-    $config = $this->configFactory->get('page_notifications.settings');
-
-    if (!isset($params['subscription']) || !isset($params['entity'])) {
-      return;
-    }
-
-    $token_data = [
-      'subscription' => $params['subscription'],
-      'node' => $params['entity'],
-    ];
-
-    $subject = $config->get('email_templates.already_subscribed_subject');
-    $body = $config->get('email_templates.already_subscribed_body');
-
-    $message['subject'] = $this->token->replace($subject, $token_data);
-
-    // Process the body content
-    $body_content = $this->token->replace($body['value'], $token_data);
-    $themed_content = $this->wrapEmailContent(
-      $body_content,
-      'already_subscribed',
-      $params['subscription'],
-      $params['entity']
-    );
-
-    $message['body'] = [$this->renderer->render($themed_content)];
+    $this->buildEmail($message, $params, 'already_subscribed');
   }
 }
\ No newline at end of file
-- 
GitLab


From 6e45486709ef1509cdcb7766b9545d6aaf07772f Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Sat, 25 Jan 2025 21:15:18 -0500
Subject: [PATCH 38/49] Bugfix: Drupal 11 token fix

---
 src/Mail/PageNotificationsMailHandler.php | 27 ++++++---
 src/Token/SubscriptionToken.php           | 68 +++++++++++------------
 2 files changed, 53 insertions(+), 42 deletions(-)

diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php
index 2f1d65e..20af34e 100644
--- a/src/Mail/PageNotificationsMailHandler.php
+++ b/src/Mail/PageNotificationsMailHandler.php
@@ -111,15 +111,28 @@ class PageNotificationsMailHandler {
     $subject_key = "email_templates.{$template_type}_subject";
     $body_key = "email_templates.{$template_type}_body";
 
-    $message['subject'] = $this->token->replace($config->get($subject_key), $token_data);
+    $subject_template = $config->get($subject_key);
+    $body_template = $config->get($body_key)['value'];
+
+    // For subject
+    $message['subject'] = \Drupal::token()->replace(
+        $subject_template,
+        $token_data,
+        ['clear' => TRUE]
+    );
+
+    // For body
+    $body = \Drupal::token()->replace(
+        $body_template,
+        $token_data,
+        ['clear' => TRUE]
+    );
 
-    // Set HTML body
-    $body = $this->token->replace($config->get($body_key)['value'], $token_data);
     $themed_content = $this->wrapEmailContent(
-      $body,
-      $template_type,
-      $subscription,
-      $entity
+        $body,
+        $template_type,
+        $subscription,
+        $entity
     );
 
     // Configure for HTML mail
diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php
index ecdf348..495b0f1 100644
--- a/src/Token/SubscriptionToken.php
+++ b/src/Token/SubscriptionToken.php
@@ -6,6 +6,8 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Url;
 use Drupal\node\NodeInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Implements hook_token_info() and hook_tokens().
@@ -13,46 +15,39 @@ use Drupal\node\NodeInterface;
 class SubscriptionToken {
   use StringTranslationTrait;
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static();
+  }
+
   /**
    * Implements hook_token_info().
    */
   public function hookTokenInfo() {
-    $types['subscription'] = [
+    $info['types']['subscription'] = [
       'name' => $this->t('Subscription'),
       'description' => $this->t('Tokens related to page notification subscriptions'),
       'needs-data' => 'subscription',
     ];
 
-    $types['notification'] = [
-      'name' => $this->t('Notification'),
-      'description' => $this->t('Tokens related to page notifications'),
-      'needs-data' => 'entity',
-    ];
-
-    $tokens['subscription']['verify-url'] = [
-      'name' => $this->t('Verification URL'),
-      'description' => $this->t('The URL to verify the subscription'),
-    ];
-
-    $tokens['subscription']['unsubscribe-url'] = [
-      'name' => $this->t('Unsubscribe URL'),
-      'description' => $this->t('The URL to unsubscribe from notifications'),
-    ];
-
-    $tokens['notification']['notes'] = [
-      'name' => $this->t('Notification Notes'),
-      'description' => $this->t('Additional notes from manual notifications or revision log message'),
+    $info['tokens']['subscription'] = [
+      'verify-url' => [
+        'name' => $this->t('Verification URL'),
+        'description' => $this->t('The URL to verify the subscription'),
+      ],
+      'unsubscribe-url' => [
+        'name' => $this->t('Unsubscribe URL'),
+        'description' => $this->t('The URL to unsubscribe from notifications'),
+      ],
+      'email' => [
+        'name' => $this->t('Email'),
+        'description' => $this->t('The subscriber\'s email address'),
+      ],
     ];
 
-    $tokens['subscription']['email'] = [
-      'name' => $this->t('Email'),
-      'description' => $this->t('The subscriber\'s email address'),
-    ];
-
-    return [
-      'types' => $types,
-      'tokens' => $tokens,
-    ];
+    return $info;
   }
 
   /**
@@ -61,12 +56,9 @@ class SubscriptionToken {
   public function hookTokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
     $replacements = [];
 
-    if ($type == 'subscription' && !empty($data['subscription'])) {
+    if (($type == 'subscription' || $type == 'page_notification_subscription') && !empty($data['subscription'])) {
       $subscription = $data['subscription'];
-
-      // Add cache metadata for the token
-    $bubbleable_metadata = new BubbleableMetadata();
-    $bubbleable_metadata->addCacheableDependency($subscription);
+      $bubbleable_metadata->addCacheableDependency($subscription);
 
       foreach ($tokens as $name => $original) {
         switch ($name) {
@@ -75,6 +67,7 @@ class SubscriptionToken {
               ['token' => $subscription->getToken()],
               ['absolute' => TRUE]
             )->toString();
+            $bubbleable_metadata->addCacheableDependency($subscription);
             break;
 
             case 'unsubscribe-url':
@@ -85,17 +78,21 @@ class SubscriptionToken {
                 ],
                 ['absolute' => TRUE]
               )->toString();
+              $bubbleable_metadata->addCacheableDependency($subscription);
               break;
 
           case 'email':
             $replacements[$original] = $subscription->getEmail();
+            $bubbleable_metadata->addCacheableDependency($subscription);
             break;
         }
       }
+      return $replacements;
     }
 
-    if ($type == 'notification' && !empty($data['entity'])) {
+    if ($type == 'page_notification_notification' && !empty($data['entity'])) {
       $entity = $data['entity'];
+      $bubbleable_metadata->addCacheableDependency($entity);
 
       foreach ($tokens as $name => $original) {
         switch ($name) {
@@ -114,6 +111,7 @@ class SubscriptionToken {
             if (\Drupal::state()->get('page_notifications_manual_notes_' . $entity->id())) {
               \Drupal::state()->delete('page_notifications_manual_notes_' . $entity->id());
             }
+            $bubbleable_metadata->addCacheableDependency($entity);
             break;
         }
       }
-- 
GitLab


From f991176efd37cd7067b4b196c24901c2b80cadaf Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Sat, 25 Jan 2025 21:33:18 -0500
Subject: [PATCH 39/49] Removed unused setting and tested with Drupal CMS to
 see if it installs correctly

---
 codebase.md                                   | 7034 +++++++++++++++++
 .../install/page_notifications.settings.yml   |    2 -
 config/schema/page_notifications.schema.yml   |    7 -
 page_notifications.install                    |   20 +-
 src/Form/SettingsForm.php                     |   17 +-
 5 files changed, 7053 insertions(+), 27 deletions(-)
 create mode 100644 codebase.md

diff --git a/codebase.md b/codebase.md
new file mode 100644
index 0000000..d1d5035
--- /dev/null
+++ b/codebase.md
@@ -0,0 +1,7034 @@
+# composer.json
+
+```json
+{
+  "name": "drupal/page_notifications",
+  "description": "Enables anonymous and authenticated users to subscribe to content updates and receive email notifications when changes occur.",
+  "type": "drupal-module",
+  "license": "GPL-2.0-or-later",
+  "homepage": "https://drupal.org/project/page_notifications",
+  "authors": [
+    {
+      "name": "Lidiya Grushetska",
+      "homepage": "https://www.drupal.org/u/lidia_ua",
+      "role": "Maintainer"
+    }
+  ],
+  "support": {
+    "issues": "https://drupal.org/project/issues/page_notifications",
+    "source": "https://git.drupalcode.org/project/page_notifications"
+  },
+  "require": {
+    "php": ">=8.1",
+    "drupal/core": "^10",
+    "drupal/symfony_mailer_lite": "^2.0"
+  },
+  "suggest": {
+    "drupal/captcha": "Provides additional spam protection options including reCAPTCHA integration"
+  },
+  "extra": {
+    "drush": {
+      "services": {
+        "drush.services.yml": "^10"
+      }
+    }
+  },
+  "minimum-stability": "dev",
+  "prefer-stable": true,
+  "config": {
+    "sort-packages": true
+  }
+}
+```
+
+# config/install/page_notifications.settings.yml
+
+```yml
+notification_settings:
+  from_email: ''
+  token_expiration: 48
+email_settings:
+  mail_format: full_html
+email_templates:
+  verification_subject: 'Verify your subscription to [node:title]'
+  verification_body:
+    value: '<p>Hello,</p><p>Please verify your email subscription to the page "<strong>[node:title]</strong>".</p><p>Click the following link to confirm your subscription:<br><a href="[subscription:verify-url]">[subscription:verify-url]</a></p><p><em>This verification link will expire soon.<br>Please verify your subscription promptly.</em></p><p>If you did not request this subscription, please ignore this email.</p>'
+    format: full_html
+  notification_subject: '[node:title] has been updated'
+  notification_body:
+    value: '<p>Dear subscriber,</p><p>The page "<strong>[node:title]</strong>" that you are subscribed to has been updated.</p><p>[notification:notes]</p><p>You can view the updated page here:<br><a href="[node:url]">[node:url]</a></p><p>To unsubscribe from these notifications, click here:<br><a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p><p>Regards,<br>[site:name] team</p>'
+    format: full_html
+  already_subscribed_subject: 'You are already subscribed to [node:title]'
+  already_subscribed_body:
+    value: '<p>Dear subscriber,</p><p>You are already subscribed to "<strong>[node:title]</strong>" and ready for future notifications.</p><p>You can view the content here:<br><a href="[node:url]">[node:url]</a></p><p>If you wish to unsubscribe, you can do so here:<br><a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p><p>Regards,<br>[site:name] team</p>'
+    format: full_html
+security:
+  require_verification: true
+  flood_control:
+    ip_limit: 200
+    ip_window: 1
+    identifier_limit: 50
+    identifier_window: 1
+spam_protection:
+  enable_modal: false
+  enable_math_captcha: true
+  math_captcha_operator: +
+  captcha_point: null
+spam_prevention:
+  captcha_type: none
+  math_operator: +
+  use_recaptcha: false
+```
+
+# config/install/views.view.page_notification_subscriptions.yml
+
+```yml
+langcode: en
+status: true
+dependencies:
+  module:
+    - node
+    - page_notifications
+id: page_notification_subscriptions
+label: 'Page Notification Subscriptions'
+module: views
+description: 'Lists all page notification subscriptions'
+tag: ''
+base_table: page_notification_subscription
+base_field: id
+display:
+  default:
+    id: default
+    display_title: Default
+    display_plugin: default
+    position: 0
+    display_options:
+      title: 'Page Notification Subscriptions'
+      fields:
+        operations:
+          id: operations
+          table: page_notification_subscription
+          field: operations
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: null
+          entity_field: null
+          plugin_id: entity_operations
+          label: Operations
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          destination: false
+        email:
+          id: email
+          table: page_notification_subscription
+          field: email
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: email
+          plugin_id: standard
+          label: Email
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+        id:
+          id: id
+          table: page_notification_subscription
+          field: id
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: id
+          plugin_id: field
+          label: ID
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings:
+            thousand_separator: ''
+            prefix_suffix: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        status:
+          id: status
+          table: page_notification_subscription
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: status
+          plugin_id: boolean
+          label: Status
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          type: yes-no
+          type_custom_true: ''
+          type_custom_false: ''
+          not: false
+        subscribed_entity_id:
+          id: subscribed_entity_id
+          table: page_notification_subscription
+          field: subscribed_entity_id
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          plugin_id: numeric
+          label: 'Subscribed Entity ID'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          set_precision: false
+          precision: 0
+          decimal: .
+          separator: ''
+          format_plural: false
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+        title:
+          id: title
+          table: node_field_data
+          field: title
+          relationship: subscribed_entity
+          group_type: group
+          admin_label: ''
+          entity_type: node
+          entity_field: title
+          plugin_id: field
+          label: Title
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        created:
+          id: created
+          table: page_notification_subscription
+          field: created
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: created
+          plugin_id: date
+          label: Created
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          date_format: 'raw time ago'
+          custom_date_format: ''
+          timezone: ''
+      pager:
+        type: mini
+        options:
+          offset: 0
+          pagination_heading_level: h4
+          items_per_page: 10
+          total_pages: null
+          id: 0
+          tags:
+            next: ››
+            previous: ‹‹
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      access:
+        type: none
+        options: {  }
+      cache:
+        type: tag
+        options: {  }
+      empty: {  }
+      sorts: {  }
+      arguments: {  }
+      filters: {  }
+      style:
+        type: table
+      row:
+        type: fields
+      query:
+        type: views_query
+        options:
+          query_comment: ''
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_tags: {  }
+      relationships:
+        subscribed_entity:
+          id: subscribed_entity
+          table: page_notification_subscription
+          field: subscribed_entity
+          relationship: none
+          group_type: group
+          admin_label: 'Subscribed Node'
+          entity_type: page_notification_subscription
+          plugin_id: standard
+          required: false
+      header: {  }
+      footer: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+      tags: {  }
+  page_1:
+    id: page_1
+    display_title: Page
+    display_plugin: page
+    position: 1
+    display_options:
+      display_extenders:
+        simple_sitemap_display_extender:
+          variants: {  }
+      path: admin/content/subscriptions
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+      tags: {  }
+
+```
+
+# config/install/views.view.top_subscribed_content.yml
+
+```yml
+langcode: en
+status: true
+dependencies:
+  module:
+    - node
+    - page_notifications
+id: top_subscribed_content
+label: 'Top Subscribed Content'
+module: views
+description: ''
+tag: ''
+base_table: page_notification_subscription
+base_field: id
+display:
+  default:
+    id: default
+    display_title: Default
+    display_plugin: default
+    position: 0
+    display_options:
+      title: 'Top Subscribed Content'
+      fields:
+        title:
+          id: title
+          table: node_field_data
+          field: title
+          relationship: subscribed_entity
+          group_type: group
+          admin_label: ''
+          entity_type: node
+          entity_field: title
+          plugin_id: field
+          label: Title
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: true
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        id:
+          id: id
+          table: page_notification_subscription
+          field: id
+          relationship: none
+          group_type: count
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: id
+          plugin_id: field
+          label: 'Subscription Count'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          set_precision: false
+          precision: 0
+          decimal: .
+          format_plural: 0
+          format_plural_string: !!binary MQNAY291bnQ=
+          prefix: ''
+          suffix: ''
+      pager:
+        type: mini
+        options:
+          offset: 0
+          pagination_heading_level: h4
+          items_per_page: 10
+          total_pages: null
+          id: 0
+          tags:
+            next: ››
+            previous: ‹‹
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      access:
+        type: none
+        options: {  }
+      cache:
+        type: tag
+        options: {  }
+      empty: {  }
+      sorts: {  }
+      arguments: {  }
+      filters:
+        status:
+          id: status
+          table: page_notification_subscription
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: page_notification_subscription
+          entity_field: status
+          plugin_id: boolean
+          operator: '='
+          value: '1'
+          group: 1
+          exposed: false
+          expose:
+            operator_id: ''
+            label: ''
+            description: ''
+            use_operator: false
+            operator: ''
+            operator_limit_selection: false
+            operator_list: {  }
+            identifier: ''
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+      style:
+        type: table
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          columns:
+            title: title
+            id: id
+            title_1: title_1
+          default: id
+          info:
+            title:
+              sortable: true
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            id:
+              sortable: true
+              default_sort_order: desc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+            title_1:
+              sortable: false
+              default_sort_order: asc
+              align: ''
+              separator: ''
+              empty_column: false
+              responsive: ''
+          override: true
+          sticky: false
+          summary: ''
+          empty_table: false
+          caption: ''
+          description: ''
+      row:
+        type: fields
+      query:
+        type: views_query
+        options:
+          query_comment: ''
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_tags: {  }
+      relationships:
+        subscribed_entity:
+          id: subscribed_entity
+          table: page_notification_subscription
+          field: subscribed_entity
+          relationship: none
+          group_type: group
+          admin_label: 'Subscribed Node'
+          entity_type: page_notification_subscription
+          plugin_id: standard
+          required: true
+      group_by: true
+      header: {  }
+      footer: {  }
+      display_extenders:
+        simple_sitemap_display_extender: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+      tags: {  }
+  page_1:
+    id: page_1
+    display_title: Page
+    display_plugin: page
+    position: 1
+    display_options:
+      display_extenders:
+        simple_sitemap_display_extender:
+          variants: {  }
+      path: admin/content/top-subscribed
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+      tags: {  }
+
+```
+
+# config/schema/page_notifications.schema.yml
+
+```yml
+page_notifications.settings:
+  type: config_object
+  label: 'Page Notifications settings'
+  mapping:
+    notification_settings:
+      type: mapping
+      mapping:
+        from_email:
+          type: string
+          label: 'From email address'
+        token_expiration:
+          type: integer
+          label: 'Token expiration time in hours (0 = never expire)'
+    email_settings:
+      type: mapping
+      label: 'Email Settings'
+      mapping:
+        mail_format:
+          type: string
+          label: 'Email text format'
+    email_templates:
+      type: mapping
+      label: 'Email Templates'
+      mapping:
+        verification_subject:
+          type: string
+          label: 'Verification email subject'
+        verification_body:
+          type: text_format
+          label: 'Verification email body'
+        notification_subject:
+          type: string
+          label: 'Notification email subject'
+        notification_body:
+          type: text_format
+          label: 'Notification email body'
+    security:
+      type: mapping
+      label: 'Security Settings'
+      mapping:
+        require_verification:
+          type: boolean
+          label: 'Require email verification'
+        flood_control:
+          type: mapping
+          label: 'Flood Control Settings'
+          mapping:
+            ip_limit:
+              type: integer
+              label: 'IP-based attempt limit'
+            ip_window:
+              type: integer
+              label: 'IP-based time window (hours)'
+            identifier_limit:
+              type: integer
+              label: 'Email-based attempt limit'
+            identifier_window:
+              type: integer
+              label: 'Email-based time window (hours)'
+    spam_protection:
+      type: mapping
+      label: 'Spam Protection Settings'
+      mapping:
+        enable_modal:
+          type: boolean
+          label: 'Enable modal dialog'
+        enable_math_captcha:
+          type: boolean
+          label: 'Enable math captcha'
+        math_captcha_operator:
+          type: string
+          label: 'Math captcha operator'
+        captcha_point:
+          type: string
+          label: 'CAPTCHA point'
+```
+
+# LICENSE.txt
+
+```txt
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+
+```
+
+# page_notifications.info.yml
+
+```yml
+name: Page Notifications
+type: module
+description: 'Anonymous users can subscribe to pages to receive notifications about updates.'
+core_version_requirement: ^10.3 || ^11
+configure: page_notifications.settings
+dependencies:
+  - drupal:node
+  - drupal:views
+  - token:token
+  - drupal:symfony_mailer_lite
+suggestions:
+  - captcha:captcha
+```
+
+# page_notifications.install
+
+```install
+<?php
+
+use Drupal\Core\Config\FileStorage;
+use Drupal\page_notifications\Service\MigrationService;
+
+/**
+ * @file
+ * Install, update and uninstall functions for the page_notifications module.
+ */
+
+/**
+ * Implements hook_install().
+ */
+function page_notifications_install() {
+  // Try to find the best available text format
+  $preferred_formats = ['email', 'easy_email', 'full_html', 'basic_html', 'restricted_html', 'plain_text'];
+  $selected_format = 'plain_text'; // Default fallback
+
+  $format_storage = \Drupal::entityTypeManager()->getStorage('filter_format');
+  foreach ($preferred_formats as $format_id) {
+    if ($format = $format_storage->load($format_id)) {
+      $selected_format = $format_id;
+      break;
+    }
+  }
+  // Set Dynamic configuration for email format
+  $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
+  $config
+    ->set('email_settings.mail_format', $selected_format)
+    ->save();
+}
+
+/**
+ * Implements hook_schema().
+ */
+function page_notifications_schema() {
+  $schema = [];
+  // Add any additional database tables needed beyond entities
+  return $schema;
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function page_notifications_uninstall() {
+  // Remove configuration
+  $config_factory = \Drupal::configFactory();
+
+  // List all config objects that need to be removed
+  $config_names = [
+    'page_notifications.settings',
+    'views.view.page_notification_subscriptions',
+    'views.view.top_subscribed_content'
+  ];
+
+  foreach ($config_names as $config_name) {
+    $config_factory->getEditable($config_name)->delete();
+  }
+
+  // Clean up state
+  \Drupal::state()->delete('page_notifications_v3_backup');
+}
+
+/**
+ * Install new schema, views, default configuration, and migrate subscriptions from v3 to v4.
+ */
+function page_notifications_update_10001(&$sandbox) {
+  // First ensure the new schema is installed
+  $entity_type = \Drupal::entityTypeManager()->getDefinition('page_notification_subscription');
+  \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type);
+
+  // Check if this is a migration or fresh install by looking for v3 tables
+  $schema = \Drupal::database()->schema();
+  $has_v3_data = $schema->tableExists('page_notify_settings') &&
+                 $schema->tableExists('page_notify_email_template');
+
+  // Install required views and configuration regardless of migration status
+  $module_path = \Drupal::service('extension.list.module')->getPath('page_notifications');
+  $source = new FileStorage($module_path . '/config/install');
+  $config_storage = \Drupal::service('config.storage');
+
+  // List of all configurations to install
+  $configs = [
+    'views.view.page_notification_subscriptions',
+    'views.view.top_subscribed_content',
+    'page_notifications.settings',
+  ];
+
+  foreach ($configs as $config_name) {
+    $config_record = $source->read($config_name);
+    if (is_array($config_record)) {
+      $config_storage->write($config_name, $config_record);
+      \Drupal::logger('page_notifications')->notice('Installed configuration: @config', ['@config' => $config_name]);
+    }
+    else {
+      \Drupal::logger('page_notifications')->error('Failed to read configuration: @config', ['@config' => $config_name]);
+    }
+  }
+
+  // Only attempt migration if v3 tables exist
+  if ($has_v3_data) {
+    try {
+      // Get v3 settings
+      $v3_settings = \Drupal::database()->select('page_notify_settings', 'pns')
+        ->fields('pns')
+        ->execute()
+        ->fetchAssoc();
+
+      $v3_template = \Drupal::database()->select('page_notify_email_template', 'pnet')
+        ->fields('pnet')
+        ->execute()
+        ->fetchAssoc();
+
+      // Store v3 data
+      \Drupal::state()->set('page_notifications_v3_backup', [
+        'settings' => $v3_settings,
+        'template' => $v3_template,
+      ]);
+
+      // Map settings to v4
+      $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
+
+      // Migrate settings
+      if ($v3_template && $v3_settings) {
+        // Map email settings
+        $config->set('notification_settings.from_email', $v3_template['from_email'] ?? '');
+
+        /**
+         * Converts v3 tokens to v4 format in a string.
+         */
+        function page_notifications_convert_tokens($text) {
+          $token_map = [
+            '[notify_user_email]' => '[subscription:email]',
+            '[notify_verify_url]' => '[subscription:verify-url]',
+            '[notify_unsubscribe_url]' => '[subscription:unsubscribe-url]',
+            '[notify_node_title]' => '[node:title]',
+            '[notify_node_url]' => '[node:url]',
+            '[notify_notes]' => '[notification:notes]',
+            // Remove or convert deprecated tokens
+            '[notify_user_name]' => '',
+            '[notify_subscribe_url]' => '',
+            '[notify_user_subscribtions]' => '',
+          ];
+
+          return str_replace(
+            array_keys($token_map),
+            array_values($token_map),
+            $text
+          );
+        }
+
+        // Map CAPTCHA settings
+        if (!empty($v3_settings['page_notify_recaptcha'])) {
+          $config->set('spam_prevention.captcha_type', 'recaptcha');
+          $config->set('spam_prevention.use_recaptcha', TRUE);
+        }
+        elseif (!empty($v3_settings['page_notify_captcha'])) {
+          $config->set('spam_prevention.captcha_type', 'math');
+        }
+
+        // Map email templates with token conversion
+        $config->set('email_templates.verification_subject',
+        page_notifications_convert_tokens($v3_template['verification_email_subject'] ?? ''));
+
+        $config->set('email_templates.verification_body',
+        page_notifications_convert_tokens($v3_template['verification_email_text'] ?? ''));
+
+        $config->set('email_templates.notification_subject',
+        page_notifications_convert_tokens($v3_template['general_email_template_subject'] ?? ''));
+
+        $config->set('email_templates.notification_body',
+        page_notifications_convert_tokens($v3_template['general_email_template'] ?? ''));
+
+        $config->save();
+
+        \Drupal::logger('page_notifications')->notice('Migrated settings from v3 to v4.');
+        \Drupal::messenger()->addWarning(t('Email templates have been migrated from v3 to v4. Please review your templates as some tokens have changed. See documentation for the new token format.'));
+
+      }
+
+      // Set up batch migration for subscriptions
+      $batch = MigrationService::createMigrationBatch();
+      if ($batch) {
+        batch_set($batch);
+      }
+    }
+    catch (\Exception $e) {
+      \Drupal::logger('page_notifications')->error('Error during v3 to v4 migration: @message', [
+        '@message' => $e->getMessage()
+      ]);
+    }
+  }
+
+  return $has_v3_data ?
+    t('Subscription data has been migrated, views and default configuration have been installed. Please review your Page Notifications settings at /admin/config/system/page-notifications') :
+    t('Page Notifications v4 has been installed with default configuration.');
+}
+
+```
+
+# page_notifications.libraries.yml
+
+```yml
+modal:
+  version: 1.x
+  dependencies:
+    - core/drupal.dialog.ajax
+    - core/drupal.ajax
+    - core/once
+```
+
+# page_notifications.links.menu.yml
+
+```yml
+page_notifications.settings:
+  title: 'Page Notifications'
+  description: 'Configure Page Notifications settings'
+  parent: system.admin_config_system
+  route_name: page_notifications.settings
+  weight: 0
+
+page_notifications.subscription_list:
+  title: 'Subscriptions'
+  description: 'Manage Page Notification subscriptions'
+  parent: system.admin_content
+  route_name: page_notifications.subscription_list
+  weight: 0
+```
+
+# page_notifications.links.task.yml
+
+```yml
+page_notifications.settings:
+  route_name: page_notifications.settings
+  title: 'Settings'
+  base_route: page_notifications.admin_settings
+  weight: 0
+
+page_notifications.send_manual:
+  route_name: page_notifications.send_manual
+  title: 'Send Notification'
+  base_route: page_notifications.admin_settings
+  weight: 1
+
+page_notifications.subscription_list:
+  route_name: page_notifications.subscription_list
+  title: 'Subscriptions'
+  base_route: page_notifications.admin_settings
+  weight: 2
+
+page_notifications.subscription_migrate:
+  route_name: page_notifications.subscription_migrate
+  title: 'Migrate Subscriptions'
+  base_route: page_notifications.admin_settings
+  weight: 4
+
+page_notifications.top_subscribed:
+  route_name: page_notifications.top_subscribed
+  title: 'Top Subscribed Content'
+  base_route: system.admin_content  # This connects it to the admin content page
+  weight: 5
+```
+
+# page_notifications.module
+
+```module
+<?php
+
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * @file
+ * Primary module hooks for Page Notifications module.
+ */
+
+/**
+ * Implements hook_mail().
+ */
+function page_notifications_mail($key, &$message, $params) {
+  \Drupal::service('page_notifications.mail_handler')->mail($key, $message, $params);
+}
+
+/**
+ * Implements hook_token_info().
+ */
+function page_notifications_token_info() {
+  return \Drupal::service('page_notifications.subscription_token')->hookTokenInfo();
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function page_notifications_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+  return \Drupal::service('page_notifications.subscription_token')->hookTokens($type, $tokens, $data, $options, $bubbleable_metadata);
+}
+
+/**
+ * Implements hook_form_BASE_FORM_ID_alter().
+ */
+function page_notifications_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  /** @var \Drupal\node\NodeInterface $node */
+  $node = $form_state->getFormObject()->getEntity();
+
+  // Get subscriber count
+  $subscriber_count = \Drupal::entityTypeManager()
+    ->getStorage('page_notification_subscription')
+    ->getQuery()
+    ->condition('subscribed_entity_id', $node->id())
+    ->condition('subscribed_entity_type', 'node')
+    ->condition('status', TRUE)
+    ->count()
+    ->accessCheck(FALSE)
+    ->execute();
+
+  // Add notification checkbox to the meta header region
+  $form['meta']['send_notification'] = [
+    '#type' => 'checkbox',
+    '#title' => t('Send notification to subscribers (@count active subscribers)', [
+      '@count' => $subscriber_count,
+    ]),
+    '#description' => $subscriber_count > 0 ?
+      t('If checked, subscribers will receive an email about this update. The revision log message will be included in the notification.') :
+      t('This content has no active subscribers.'),
+    '#default_value' => FALSE,
+    '#disabled' => $subscriber_count === 0,
+    '#group' => 'meta',
+    '#weight' => 30,
+    '#access' => \Drupal::currentUser()->hasPermission('send page notifications'),
+  ];
+
+  // Add custom submit handler
+  $form['actions']['submit']['#submit'][] = 'page_notifications_node_form_submit';
+}
+
+/**
+ * Submit handler for node form.
+ */
+function page_notifications_node_form_submit($form, FormStateInterface $form_state) {
+  $meta = $form_state->getValue('meta');
+  if (!empty($meta['send_notification'])) {
+    $node = $form_state->getFormObject()->getEntity();
+    /** @var \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager */
+    $notification_manager = \Drupal::service('page_notifications.notification_manager');
+    $notification_manager->notifySubscribers($node);
+
+    \Drupal::messenger()->addMessage(t('Notification queued for sending to subscribers.'));
+  }
+}
+
+/**
+ * Implements hook_cron().
+ */
+function page_notifications_cron() {
+  \Drupal::service('page_notifications.cron_manager')->processCron();
+}
+
+/**
+ * Implements hook_theme().
+ */
+function page_notifications_theme() {
+  return [
+    'block__page_notifications_subscription' => [
+      'template' => 'block--page-notifications-subscription',
+      'base hook' => 'block',
+    ],
+    'page_notifications_email_wrapper' => [
+      'variables' => [
+        'content' => NULL,
+        'email_type' => NULL,
+        'subscription' => NULL,
+        'entity' => NULL,
+        'logo_url' =>  theme_get_setting('logo.url') ? \Drupal::request()->getSchemeAndHttpHost() . theme_get_setting('logo.url') : NULL,
+        'site_name' => \Drupal::config('system.site')->get('name'),
+        'footer' => NULL,
+      ],
+      'template' => 'page-notifications-email-wrapper',
+    ],
+  ];
+}
+
+/**
+ * Implements hook_theme_suggestions_HOOK().
+ */
+function page_notifications_theme_suggestions_page_notifications_email_wrapper(array $variables) {
+  $suggestions = [];
+
+  if (!empty($variables['email_type'])) {
+    $suggestions[] = 'page_notifications_email_wrapper__' . $variables['email_type'];
+  }
+
+  return $suggestions;
+}
+
+/**
+ * Implements hook_preprocess_page_notifications_email_wrapper().
+ */
+function page_notifications_preprocess_page_notifications_email_wrapper(&$variables) {
+  // Ensure logo URL is absolute
+  if (!empty($variables['logo_url']) && !preg_match('/^(http|https):\/\//', $variables['logo_url'])) {
+    $variables['logo_url'] = \Drupal::request()->getSchemeAndHttpHost() . $variables['logo_url'];
+  }
+  // Allow modules to alter email variables
+  \Drupal::moduleHandler()->alter('page_notifications_email_variables', $variables);
+}
+
+```
+
+# page_notifications.permissions.yml
+
+```yml
+administer page notification subscriptions:
+  title: 'Administer page notification subscriptions'
+  description: 'Full administrative access to page notification subscriptions.'
+  restrict access: TRUE
+
+view page notification subscriptions:
+  title: 'View page notification subscriptions'
+  description: 'View existing page notification subscriptions.'
+
+create page notification subscriptions:
+  title: 'Create page notification subscriptions'
+  description: 'Create new page notification subscriptions.'
+
+edit page notification subscriptions:
+  title: 'Edit page notification subscriptions'
+  description: 'Edit existing page notification subscriptions.'
+
+delete page notification subscriptions:
+  title: 'Delete page notification subscriptions'
+  description: 'Delete existing page notification subscriptions.'
+
+view subscription list:
+  title: 'View subscription list'
+  description: 'Access the subscription list view'
+
+view top subscribed content:
+  title: 'View top subscribed content'
+  description: 'Access the top subscribed content list'
+```
+
+# page_notifications.routing.yml
+
+```yml
+page_notifications.settings:
+  path: '/admin/config/system/page-notifications'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\SettingsForm'
+    _title: 'Page Notifications Settings'
+  requirements:
+    _permission: 'administer page notification subscriptions'
+
+page_notifications.subscription.verify:
+  path: '/page-notifications/verify/{token}'
+  defaults:
+    _controller: 'page_notifications.notification_manager:verifySubscription'
+    _title: 'Verify Subscription'
+  requirements:
+    _access: 'TRUE'
+  options:
+    no_cache: TRUE
+
+# Secure unsubscribe for anonymous users
+page_notifications.subscription.unsubscribe:
+  path: '/page-notifications/unsubscribe/{subscription}/{token}'
+  defaults:
+    _controller: '\Drupal\page_notifications\Controller\UnsubscribeController::unsubscribe'
+    _title: 'Unsubscribe from Notifications'
+  requirements:
+    _custom_access: '\Drupal\page_notifications\Controller\UnsubscribeController::checkAccess'
+  options:
+    no_cache: TRUE
+
+page_notifications.send_manual:
+  path: '/admin/config/system/page-notifications/send'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\ManualNotificationForm'
+    _title: 'Send Manual Notification'
+  requirements:
+    _permission: 'administer page notification subscriptions'
+
+page_notifications.subscription_list:
+  path: '/admin/config/system/page-notifications/subscriptions'
+  defaults:
+    _controller: '\Drupal\page_notifications\Controller\SubscriptionListController::content'
+    _title: 'Subscriptions'
+  requirements:
+    _permission: 'view subscription list'
+
+page_notifications.top_subscribed:
+  path: '/admin/content/top-subscribed'
+  defaults:
+    _controller: '\Drupal\page_notifications\Controller\TopSubscribedController::content'
+    _title: 'Top Subscribed Content'
+  requirements:
+    _permission: 'view subscription list'
+
+page_notifications.subscription_migrate:
+  path: '/admin/config/system/page-notifications/migrate'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\SubscriptionMigrateForm'
+    _title: 'Migrate Subscriptions'
+  requirements:
+    _permission: 'administer page notification subscriptions'
+
+page_notifications.subscription_add:
+  path: '/admin/config/system/page-notifications/subscriptions/add'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\ManualSubscriptionAddForm'
+    _title: 'Add Subscriptions'
+  requirements:
+    _permission: 'administer page notification subscriptions'
+
+page_notifications.purge_subscriptions_confirm:
+  path: '/admin/config/system/page-notifications/purge-confirm'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\PurgeSubscriptionsConfirmForm'
+    _title: 'Confirm subscription purge'
+  requirements:
+    _permission: 'administer page notification subscriptions'
+page_notifications.modal_form:
+  path: '/page-notifications/modal-form/{entity_type}/{entity}'
+  defaults:
+    _form: '\Drupal\page_notifications\Form\ModalSubscriptionForm'
+    _title: 'Subscribe to Updates'
+  requirements:
+    _access: 'TRUE'
+  options:
+    parameters:
+      entity:
+        type: entity:{entity_type}
+```
+
+# page_notifications.services.yml
+
+```yml
+services:
+  page_notifications.notification_manager:
+    class: Drupal\page_notifications\Service\NotificationManager
+    arguments:
+      - '@config.factory'
+      - '@plugin.manager.mail'
+      - '@entity_type.manager'
+      - '@queue'
+      - '@logger.factory'
+      - '@event_dispatcher'
+      - '@datetime.time'
+      - '@string_translation'
+      - '@messenger'
+    calls:
+      - [setStringTranslation, ['@string_translation']]
+  page_notifications.mail_handler:
+    class: Drupal\page_notifications\Mail\PageNotificationsMailHandler
+    arguments:
+      - '@config.factory'
+      - '@renderer'
+      - '@token'
+      - '@string_translation'
+      - '@theme.manager'
+      - '@symfony_mailer_lite.mailer'
+  page_notifications.subscription_token:
+    class: Drupal\page_notifications\Token\SubscriptionToken
+    tags:
+      - { name: token.provider }
+  page_notifications.cron_manager:
+    class: Drupal\page_notifications\Service\CronManager
+    arguments:
+      - '@entity_type.manager'
+      - '@config.factory'
+      - '@queue'
+      - '@plugin.manager.queue_worker'
+      - '@logger.factory'
+      - '@datetime.time'
+  page_notifications.queue_worker:
+    class: Drupal\page_notifications\Plugin\QueueWorker\NotificationQueue
+    arguments:
+      - '@entity_type.manager'
+      - '@plugin.manager.mail'
+      - '@config.factory'
+      - '@logger.factory'
+    tags:
+      - { name: queue_worker, id: page_notifications_queue }
+  page_notifications.spam_prevention:
+    class: Drupal\page_notifications\Service\SpamPrevention
+    arguments:
+      - '@config.factory'
+      - '@module_handler'
+      - '@session_manager'
+      - '@string_translation'
+  page_notifications.migration:
+    class: Drupal\page_notifications\Service\MigrationService
+    arguments:
+      - '@database'
+      - '@entity_type.manager'
+      - '@config.factory'
+      - '@state'
+      - '@logger.factory'
+      - '@datetime.time'
+```
+
+# README.md
+
+```md
+# Page Notifications
+
+A Drupal module that enables anonymous and authenticated users to subscribe to content updates and receive email notifications when changes occur.
+
+## CONTENTS OF THIS FILE
+* Introduction
+* Features
+* Requirements
+* Installation
+* Configuration
+* Usage
+* Security
+* API
+* Maintainers
+
+## INTRODUCTION
+
+Page Notifications provides a flexible system for users to subscribe to content changes on your Drupal site. When subscribed content is updated, subscribers receive customizable email notifications about the changes.
+
+## FEATURES
+
+* Email subscription system for any content entity (nodes by default)
+* Configurable email templates with token support
+* Anti-spam protection with multiple options:
+  - Simple math CAPTCHA
+  - reCAPTCHA integration (requires reCAPTCHA module)
+* Subscription management:
+  - Email verification system
+  - Secure unsubscribe links
+  - Automatic cleanup of unverified subscriptions
+  - Subscription migration tools
+* Administration:
+  - Settings interface
+  - Subscription overview and management
+  - Manual notification sending capability
+* Queue-based notification processing
+* Token support for email templates
+* Block-based subscription forms
+* Drupal Views integration
+
+## REQUIREMENTS
+
+This module requires the following:
+* Drupal 10.x
+* Node module (enabled by default)
+* Views module (enabled by default)
+
+Optional but recommended:
+* reCAPTCHA module for enhanced spam protection
+
+## INSTALLATION
+
+1. Install the module via Composer:
+   \`\`\`bash
+   composer require drupal/page_notifications
+   \`\`\`
+   Or download and extract to your modules directory.
+
+2. Enable the module at `/admin/modules` or via Drush:
+   \`\`\`bash
+   drush en page_notifications
+   \`\`\`
+
+3. Place the subscription block in your desired region at `/admin/structure/block`. Use block configuration to set visibility as desired.
+
+## CONFIGURATION
+
+All module settings can be configured at `/admin/config/system/page-notifications`:
+
+### Email Settings
+* Configure "From" email address
+* Set verification token expiration time
+* Customize email templates for:
+  - Subscription verification
+  - Update notifications
+
+### Security Settings
+* Toggle email verification requirement
+* Configure unverified subscription cleanup
+* Set spam prevention method:
+  - None
+  - Math CAPTCHA
+  - reCAPTCHA (if module installed)
+
+### Subscription Management
+* View and manage subscriptions
+* Migrate subscriptions between content
+* Send manual notifications
+
+## USAGE
+
+### For Site Builders
+1. Place the subscription block on content types where you want to enable notifications
+2. Configure email templates and security settings
+3. Manage subscriptions through the administrative interface
+
+### For Content Editors
+1. Update content normally
+2. Option to send notifications appears in content edit form next to the revision log
+3. Can include custom notes with notifications
+
+### For Users
+1. Subscribe to content via the subscription block
+2. Receive verification email (if enabled)
+3. Get notifications when content is updated
+4. Unsubscribe via secure links in notification emails
+
+## SECURITY
+
+The module implements several security measures:
+* Email verification system
+* CAPTCHA/reCAPTCHA spam prevention
+* Secure unsubscribe tokens
+* Automatic cleanup of unverified subscriptions
+* Permission-based access control
+
+## Email Customization
+
+### Template Override
+You can override the email template by copying 
+`templates/page-notifications-email-wrapper.html.twig` to your theme and modifying it.
+
+Template suggestions available:
+- page-notifications-email-wrapper--verification.html.twig
+- page-notifications-email-wrapper--notification.html.twig
+- page-notifications-email-wrapper--already-subscribed.html.twig
+
+### Programmatic Customization
+Implement these hooks in your custom module:
+
+\`\`\`php
+/**
+ * Implements hook_page_notifications_email_variables_alter().
+ */
+function mymodule_page_notifications_email_variables_alter(&$variables) {
+  // Add custom variables to email template
+  $variables['my_variable'] = 'Custom content';
+}
+
+/**
+ * Implements hook_page_notifications_email_footer_alter().
+ */
+function mymodule_page_notifications_email_footer_alter(&$footer) {
+  $footer = 'Custom footer content';
+}
+
+## API
+
+The module provides services and interfaces for developers to:
+* Create and manage subscriptions programmatically
+* Customize notification handling
+* Extend spam prevention mechanisms
+* Integrate with other modules
+
+Key services:
+* `page_notifications.notification_manager`
+* `page_notifications.mail_handler`
+* `page_notifications.spam_prevention`
+
+## MAINTAINERS
+
+Current maintainers:
+* Lidiya Grushetska <grushetskl@chop.edu> is the original author https://www.drupal.org/u/lidia_ua.
+```
+
+# src/Controller/ModalFormController.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Form\FormBuilderInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\node\NodeInterface;
+
+/**
+ * Controller for the subscription modal form.
+ */
+class ModalFormController extends ControllerBase {
+
+  /**
+   * The form builder.
+   *
+   * @var \Drupal\Core\Form\FormBuilderInterface
+   */
+  protected $formBuilder;
+
+  /**
+   * Constructs a new ModalFormController.
+   *
+   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
+   *   The form builder.
+   */
+  public function __construct(FormBuilderInterface $form_builder) {
+    $this->formBuilder = $form_builder;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('form_builder')
+    );
+  }
+
+  /**
+   * Returns the subscription form in a modal.
+   *
+   * @param \Drupal\node\NodeInterface $node
+   *   The node being subscribed to.
+   *
+   * @return array
+   *   The render array for the modal form.
+   */
+  public function content(NodeInterface $node) {
+    $build = [
+      '#prefix' => '<div id="modal-subscription-form-wrapper">',
+      '#suffix' => '</div>',
+      'status_messages' => [
+        '#type' => 'status_messages',
+      ],
+      'form' => $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node),
+    ];
+
+    return $build;
+  }
+
+}
+```
+
+# src/Controller/SubscriptionListController.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Url;
+
+/**
+ * Controller for the subscription list page.
+ */
+class SubscriptionListController extends ControllerBase {
+
+  /**
+   * Displays the subscription list view.
+   *
+   * @return array
+   *   A render array for the view.
+   */
+  public function content() {
+    // Add the "Add Subscription" button
+    $build['add_form'] = [
+      '#type' => 'link',
+      '#title' => $this->t('Add Subscription'),
+      '#url' => Url::fromRoute('page_notifications.subscription_add'),
+      '#attributes' => [
+        'class' => ['button', 'button--action', 'button--primary'],
+      ],
+    ];
+
+    // Add some spacing after the button
+    $build['spacing'] = [
+      '#type' => 'html_tag',
+      '#tag' => 'div',
+      '#attributes' => [
+        'style' => 'margin: 1em 0;',
+      ],
+    ];
+
+    // Add the view
+    $build['view'] = views_embed_view('page_notification_subscriptions', 'default');
+
+    return $build;
+  }
+
+}
+```
+
+# src/Controller/TopSubscribedController.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Controller for the top subscribed content page.
+ */
+class TopSubscribedController extends ControllerBase {
+
+  /**
+   * Displays the top subscribed content view.
+   *
+   * @return array
+   *   A render array for the view.
+   */
+  public function content() {
+    $view = views_embed_view('top_subscribed_content', 'default');
+    return [
+      '#type' => 'container',
+      'view' => $view,
+    ];
+  }
+
+}
+```
+
+# src/Controller/UnsubscribeController.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Access\AccessResult;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\page_notifications\Traits\FloodControlTrait;
+
+/**
+ * Controller for handling unsubscribe requests.
+ */
+class UnsubscribeController extends ControllerBase {
+  use FloodControlTrait;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new UnsubscribeController.
+   */
+  public function __construct(
+    EntityTypeManagerInterface $entity_type_manager,
+    FloodInterface $flood
+  ) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->setFloodService($flood);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('flood')
+    );
+  }
+
+  /**
+   * Custom access check for unsubscribe URLs.
+   */
+  public function checkAccess($subscription, $token) {
+    // Check flood control first
+    $ip = \Drupal::request()->getClientIp();
+    $flood_config = $this->getFloodControlConfig();
+
+    if (!$this->flood->isAllowed('page_notifications.unsubscribe', $flood_config['ip_limit'], $flood_config['ip_window'], $ip)) {
+      return AccessResult::forbidden('Too many unsubscribe attempts from this IP address.');
+    }
+
+    // Register flood event for this attempt
+    $this->flood->register('page_notifications.unsubscribe', $flood_config['ip_window'], $ip);
+
+    if (is_numeric($subscription)) {
+      try {
+        $subscription = $this->entityTypeManager
+          ->getStorage('page_notification_subscription')
+          ->load($subscription);
+      }
+      catch (\Exception $e) {
+        return AccessResult::forbidden();
+      }
+    }
+
+    if (!$subscription) {
+      return AccessResult::forbidden();
+    }
+
+    if ($subscription->getUnsubscribeToken() !== $token) {
+      $this->logSecurityEvent('invalid_unsubscribe_token', [
+        'ip' => $ip,
+        'subscription_id' => $subscription->id(),
+      ]);
+      return AccessResult::forbidden();
+    }
+
+    return AccessResult::allowed();
+  }
+
+  /**
+   * Handles the unsubscribe request.
+   */
+  public function unsubscribe($subscription, $token) {
+    if (is_numeric($subscription)) {
+      try {
+        $subscription = $this->entityTypeManager
+          ->getStorage('page_notification_subscription')
+          ->load($subscription);
+      }
+      catch (\Exception $e) {
+        $this->messenger()->addError($this->t('An error occurred while processing your request.'));
+        return new RedirectResponse('/');
+      }
+    }
+
+    try {
+      if ($subscription && $subscription->getUnsubscribeToken() === $token) {
+        // Get the node ID and entity type before deleting the subscription
+        $entity_id = $subscription->getSubscribedEntityId();
+        $entity_type = $subscription->getSubscribedEntityType();
+
+        $subscription->delete();
+        $this->messenger()->addStatus($this->t('You have been successfully unsubscribed.'));
+
+        // Load the entity and get its URL
+        try {
+          $entity = $this->entityTypeManager
+            ->getStorage($entity_type)
+            ->load($entity_id);
+
+          if ($entity && $entity->hasLinkTemplate('canonical')) {
+            return new RedirectResponse($entity->toUrl()->toString());
+          }
+        }
+        catch (\Exception $e) {
+          \Drupal::logger('page_notifications')->error('Redirect error: @message', ['@message' => $e->getMessage()]);
+        }
+      }
+      else {
+        $this->messenger()->addError($this->t('Invalid unsubscribe link.'));
+      }
+    }
+    catch (\Exception $e) {
+      $this->messenger()->addError($this->t('An error occurred while processing your request.'));
+      \Drupal::logger('page_notifications')->error('Unsubscribe error: @message', ['@message' => $e->getMessage()]);
+    }
+
+    // Fallback to homepage if anything goes wrong
+    return new RedirectResponse('/');
+  }
+}
+```
+
+# src/Entity/Subscription.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Entity;
+
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\user\EntityOwnerTrait;
+
+/**
+ * Defines the Subscription entity.
+ *
+ * @ContentEntityType(
+ *   id = "page_notification_subscription",
+ *   label = @Translation("Page Notification Subscription"),
+ *   handlers = {
+ *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
+ *     "list_builder" = "Drupal\page_notifications\Entity\SubscriptionListBuilder",
+ *     "views_data" = "Drupal\page_notifications\Entity\SubscriptionViewsData",
+ *     "form" = {
+ *       "default" = "Drupal\page_notifications\Form\SubscriptionForm",
+ *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm"
+ *     },
+ *     "access" = "Drupal\page_notifications\Entity\SubscriptionAccessControlHandler",
+ *     "route_provider" = {
+ *       "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider"
+ *     }
+ *   },
+ *   base_table = "page_notification_subscription",
+ *   data_table = "page_notification_subscription_field_data",
+ *   admin_permission = "administer page notification subscriptions",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "owner" = "uid",
+ *     "langcode" = "langcode"
+ *   },
+ *   links = {
+ *     "canonical" = "/admin/content/subscriptions/{page_notification_subscription}",
+ *     "delete-form" = "/admin/content/subscriptions/{page_notification_subscription}/delete",
+ *     "collection" = "/admin/content/subscriptions"
+ *   }
+ * )
+ */
+class Subscription extends ContentEntityBase implements SubscriptionInterface {
+
+  use EntityChangedTrait;
+  use EntityOwnerTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEmail() {
+    return $this->get('email')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setEmail($email) {
+    $this->set('email', $email);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSubscribedEntityId() {
+    return $this->get('subscribed_entity_id')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSubscribedEntityId($id) {
+    $this->set('subscribed_entity_id', $id);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSubscribedEntityType() {
+    return $this->get('subscribed_entity_type')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSubscribedEntityType($entity_type) {
+    $this->set('subscribed_entity_type', $entity_type);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getToken() {
+    return $this->get('token')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setToken($token) {
+    $this->set('token', $token);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isActive() {
+    return (bool) $this->get('status')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setActive($status) {
+    $this->set('status', $status ? 1 : 0);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCreatedTime() {
+    return $this->get('created')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCreatedTime($timestamp) {
+    $this->set('created', $timestamp);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLanguageCode() {
+    return $this->get('langcode')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLanguageCode($langcode) {
+    $this->set('langcode', $langcode);
+    return $this;
+  }
+
+/**
+   * Gets the default langcode.
+   *
+   * @return string
+   *   The site's default language code.
+   */
+  public static function getDefaultLangcode() {
+    return \Drupal::config('system.site')->get('default_langcode') ?: 'en';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUnsubscribeToken() {
+    return $this->get('unsubscribe_token')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUnsubscribeToken($token) {
+    $this->set('unsubscribe_token', $token);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+    $fields += static::ownerBaseFieldDefinitions($entity_type);
+
+    $fields['email'] = BaseFieldDefinition::create('email')
+      ->setLabel(t('Email'))
+      ->setDescription(t('The email address of the subscriber.'))
+      ->setRequired(TRUE)
+      ->setTranslatable(TRUE)
+      ->setSettings([
+        'max_length' => 255,
+      ])
+      ->setDisplayOptions('view', [
+        'label' => 'above',
+        'type' => 'string',
+        'weight' => -5,
+      ])
+      ->setDisplayOptions('form', [
+        'type' => 'email_default',
+        'weight' => -5,
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['subscribed_entity_id'] = BaseFieldDefinition::create('integer')
+      ->setLabel(t('Subscribed Entity ID'))
+      ->setDescription(t('The ID of the entity being subscribed to.'))
+      ->setRequired(TRUE)
+      ->setTranslatable(FALSE);
+
+    $fields['subscribed_entity_type'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Subscribed Entity Type'))
+      ->setDescription(t('The type of the entity being subscribed to.'))
+      ->setRequired(TRUE)
+      ->setTranslatable(FALSE)
+      ->setSettings([
+        'max_length' => 32,
+      ]);
+
+    $fields['token'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Token'))
+      ->setDescription(t('The subscription verification token.'))
+      ->setRequired(TRUE)
+      ->setTranslatable(FALSE)
+      ->setSettings([
+        'max_length' => 64,
+      ]);
+
+    $fields['status'] = BaseFieldDefinition::create('boolean')
+      ->setLabel(t('Status'))
+      ->setDescription(t('A boolean indicating whether the subscription is active.'))
+      ->setDefaultValue(TRUE)
+      ->setTranslatable(FALSE)
+      ->setDisplayOptions('form', [
+        'type' => 'boolean_checkbox',
+        'weight' => 0,
+      ]);
+
+    $fields['created'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Created'))
+      ->setDescription(t('The time that the subscription was created.'))
+      ->setTranslatable(FALSE);
+
+    $fields['changed'] = BaseFieldDefinition::create('changed')
+      ->setLabel(t('Changed'))
+      ->setDescription(t('The time that the subscription was last edited.'))
+      ->setTranslatable(FALSE);
+
+    $fields['langcode'] = BaseFieldDefinition::create('language')
+    ->setLabel(t('Language'))
+    ->setDescription(t('The subscription language code.'))
+    ->setDefaultValueCallback(static::class . '::getDefaultLangcode')
+    ->setDisplayOptions('form', [
+      'type' => 'language_select',
+      'weight' => 2,
+    ]);
+
+    $fields['unsubscribe_token'] = BaseFieldDefinition::create('string')
+  ->setLabel(t('Unsubscribe Token'))
+  ->setDescription(t('The token required to unsubscribe from notifications.'))
+  ->setRequired(TRUE)
+  ->setTranslatable(FALSE)
+  ->setSettings([
+    'max_length' => 64,
+  ]);
+
+    return $fields;
+  }
+
+}
+```
+
+# src/Entity/SubscriptionAccessControlHandler.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Entity;
+
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Access\AccessResult;
+
+/**
+ * Access controller for page notification subscription entities.
+ */
+class SubscriptionAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $entity */
+    switch ($operation) {
+      case 'view':
+        return AccessResult::allowedIfHasPermission($account, 'view page notification subscriptions');
+
+      case 'update':
+        return AccessResult::allowedIfHasPermission($account, 'edit page notification subscriptions');
+
+      case 'delete':
+        // Allow deletion if user has permission or is the owner of the subscription
+        return AccessResult::allowedIfHasPermissions($account, [
+          'delete page notification subscriptions',
+          'administer page notification subscriptions',
+        ], 'OR')
+          ->orIf(AccessResult::allowedIf($account->isAuthenticated() && $account->id() === $entity->getOwnerId())
+            ->addCacheableDependency($entity));
+    }
+
+    return AccessResult::neutral();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    return AccessResult::allowedIfHasPermissions($account, [
+      'create page notification subscriptions',
+      'administer page notification subscriptions',
+    ], 'OR');
+  }
+
+}
+```
+
+# src/Entity/SubscriptionInterface.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\user\EntityOwnerInterface;
+
+/**
+ * Interface for Page Notification Subscription entities.
+ */
+interface SubscriptionInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface {
+
+  /**
+   * Gets the subscription email.
+   *
+   * @return string
+   *   The subscription email address.
+   */
+  public function getEmail();
+
+  /**
+   * Sets the subscription email.
+   *
+   * @param string $email
+   *   The subscription email address.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setEmail($email);
+
+  /**
+   * Gets the subscribed entity ID.
+   *
+   * @return int
+   *   The entity ID.
+   */
+  public function getSubscribedEntityId();
+
+  /**
+   * Sets the subscribed entity ID.
+   *
+   * @param int $id
+   *   The entity ID.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setSubscribedEntityId($id);
+
+  /**
+   * Gets the subscribed entity type.
+   *
+   * @return string
+   *   The entity type (e.g., 'node', 'taxonomy_term').
+   */
+  public function getSubscribedEntityType();
+
+  /**
+   * Sets the subscribed entity type.
+   *
+   * @param string $entity_type
+   *   The entity type.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setSubscribedEntityType($entity_type);
+
+  /**
+   * Gets the subscription token.
+   *
+   * @return string
+   *   The subscription token.
+   */
+  public function getToken();
+
+  /**
+   * Sets the subscription token.
+   *
+   * @param string $token
+   *   The subscription token.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setToken($token);
+
+  /**
+   * Gets the subscription status.
+   *
+   * @return bool
+   *   TRUE if the subscription is active, FALSE otherwise.
+   */
+  public function isActive();
+
+  /**
+   * Sets the subscription status.
+   *
+   * @param bool $status
+   *   The subscription status.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setActive($status);
+
+  /**
+   * Gets the subscription creation timestamp.
+   *
+   * @return int
+   *   Creation timestamp of the subscription.
+   */
+  public function getCreatedTime();
+
+  /**
+   * Sets the subscription creation timestamp.
+   *
+   * @param int $timestamp
+   *   The subscription creation timestamp.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setCreatedTime($timestamp);
+
+  /**
+   * Gets the subscription language code.
+   *
+   * @return string
+   *   The language code of the subscription.
+   */
+  public function getLanguageCode();
+
+  /**
+   * Sets the subscription language code.
+   *
+   * @param string $langcode
+   *   The language code.
+   *
+   * @return $this
+   *   The called subscription entity.
+   */
+  public function setLanguageCode($langcode);
+
+}
+```
+
+# src/Entity/SubscriptionListBuilder.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Entity;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityListBuilder;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+
+/**
+ * Provides a list builder for page notification subscriptions.
+ */
+class SubscriptionListBuilder extends EntityListBuilder {
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * Constructs a new SubscriptionListBuilder object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The entity storage class.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   */
+  public function __construct(
+    EntityTypeInterface $entity_type,
+    EntityStorageInterface $storage,
+    DateFormatterInterface $date_formatter
+  ) {
+    parent::__construct($entity_type, $storage);
+    $this->dateFormatter = $date_formatter;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('entity_type.manager')->getStorage($entity_type->id()),
+      $container->get('date.formatter')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header = [];
+    $header['id'] = $this->t('ID');
+    $header['email'] = $this->t('Email');
+    $header['subscribed_entity'] = $this->t('Subscribed To');
+    $header['status'] = $this->t('Status');
+    $header['created'] = $this->t('Created');
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $entity */
+    $row = [];
+    $row['id'] = $entity->id();
+    $row['email'] = $entity->getEmail();
+
+    // Get the subscribed entity and create a link if possible.
+    $entity_type = $entity->getSubscribedEntityType();
+    $entity_id = $entity->getSubscribedEntityId();
+    try {
+      $subscribed_entity = \Drupal::entityTypeManager()
+        ->getStorage($entity_type)
+        ->load($entity_id);
+      if ($subscribed_entity) {
+        $row['subscribed_entity'] = Link::createFromRoute(
+          $subscribed_entity->label(),
+          'entity.' . $entity_type . '.canonical',
+          [$entity_type => $entity_id]
+        );
+      }
+      else {
+        $row['subscribed_entity'] = $this->t('Entity not found (@type: @id)', [
+          '@type' => $entity_type,
+          '@id' => $entity_id,
+        ]);
+      }
+    }
+    catch (\Exception $e) {
+      $row['subscribed_entity'] = $this->t('Invalid entity reference');
+    }
+
+    $row['status'] = $entity->isActive() ? $this->t('Active') : $this->t('Inactive');
+    $row['created'] = $this->dateFormatter->format($entity->getCreatedTime(), 'short');
+
+    return $row + parent::buildRow($entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+protected function getDefaultOperations(EntityInterface $entity) {
+  $operations = parent::getDefaultOperations($entity);
+
+  // Add verify link if not active
+  if (!$entity->isActive()) {
+    $operations['verify'] = [
+      'title' => $this->t('Verify'),
+      'url' => Url::fromRoute('page_notifications.subscription.verify', [
+        'token' => $entity->getToken(),
+      ]),
+    ];
+  }
+
+  return $operations;
+}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $build = parent::render();
+    $build['table']['#empty'] = $this->t('No subscriptions found.');
+    return $build;
+  }
+
+}
+```
+
+# src/Entity/SubscriptionViewsData.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Entity;
+
+use Drupal\views\EntityViewsData;
+
+/**
+ * Provides Views data for Page Notification Subscription entities.
+ */
+class SubscriptionViewsData extends EntityViewsData {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewsData() {
+    $data = parent::getViewsData();
+
+    // Base table definition
+    $data['page_notification_subscription']['table']['base'] = [
+      'field' => 'id',
+      'title' => $this->t('Page Notification Subscription'),
+      'help' => $this->t('Contains subscription information for page notifications.'),
+      'weight' => -10,
+    ];
+
+
+    // Define the relationship to nodes
+    $data['page_notification_subscription']['subscribed_entity'] = [
+      'title' => $this->t('Subscribed Node'),
+      'help' => $this->t('The node this subscription is associated with.'),
+      'relationship' => [
+        'base' => 'node_field_data',
+        'base field' => 'nid',
+        'field' => 'subscribed_entity_id',
+        'id' => 'standard',
+        'label' => $this->t('Subscribed Node'),
+      ],
+    ];
+
+      // ID field
+      $data['page_notification_subscription']['subscribed_entity_id'] = [
+        'title' => $this->t('Subscribed Entity ID'),
+        'help' => $this->t('The ID of the entity being subscribed to.'),
+        'field' => [
+          'id' => 'numeric',
+        ],
+        'filter' => [
+          'id' => 'numeric',
+        ],
+        'sort' => [
+          'id' => 'standard',
+        ],
+        'argument' => [
+          'id' => 'numeric',
+        ],
+      ];
+
+      // Status field
+      $data['page_notification_subscription']['status'] = [
+        'title' => $this->t('Status'),
+        'help' => $this->t('The status of the subscription.'),
+        'field' => [
+          'id' => 'boolean',
+        ],
+        'filter' => [
+          'id' => 'boolean',
+          'label' => $this->t('Status'),
+          'type' => 'yes-no',
+        ],
+        'sort' => [
+          'id' => 'standard',
+        ],
+      ];
+
+      // Email field
+      $data['page_notification_subscription']['email'] = [
+        'title' => $this->t('Email'),
+        'help' => $this->t('The email address of the subscriber.'),
+        'field' => [
+          'id' => 'standard',
+        ],
+        'filter' => [
+          'id' => 'string',
+        ],
+        'sort' => [
+          'id' => 'standard',
+        ],
+      ];
+
+      // Created field
+      $data['page_notification_subscription']['created'] = [
+        'title' => $this->t('Created'),
+        'help' => $this->t('When the subscription was created.'),
+        'field' => [
+          'id' => 'date',
+        ],
+        'filter' => [
+          'id' => 'date',
+        ],
+        'sort' => [
+          'id' => 'date',
+        ],
+      ];
+
+    // Operations
+    $data['page_notification_subscription']['operations'] = [
+      'field' => [
+        'title' => $this->t('Operations'),
+        'help' => $this->t('Provides links to perform subscription operations.'),
+        'id' => 'entity_operations',
+      ],
+    ];
+
+    return $data;
+  }
+
+}
+```
+
+# src/EventSubscriber/NodeUpdateSubscriber.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\EventSubscriber;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\page_notifications\Service\NotificationManagerInterface;
+
+/**
+ * Node update subscriber for sending notifications.
+ */
+class NodeUpdateSubscriber implements EventSubscriberInterface {
+  // TODO: Implement event subscriber for node updates
+}
+```
+
+# src/Form/ManualNotificationForm.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\page_notifications\Service\NotificationManagerInterface;
+
+/**
+ * Form for manually sending notifications to subscribers.
+ */
+class ManualNotificationForm extends FormBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The notification manager service.
+   *
+   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
+   */
+  protected $notificationManager;
+
+  /**
+   * Constructs a new ManualNotificationForm.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager
+   *   The notification manager service.
+   */
+  public function __construct(
+    EntityTypeManagerInterface $entity_type_manager,
+    NotificationManagerInterface $notification_manager
+  ) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->notificationManager = $notification_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('page_notifications.notification_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_manual_notification_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Get content with active subscribers
+    $subscription_storage = $this->entityTypeManager->getStorage('page_notification_subscription');
+    $node_storage = $this->entityTypeManager->getStorage('node');
+
+    // Get unique node IDs that have active subscribers
+    $query = $subscription_storage->getQuery()
+      ->condition('status', TRUE)
+      ->condition('subscribed_entity_type', 'node')
+      ->accessCheck(FALSE);
+    $result = $query->execute();
+
+    if (empty($result)) {
+      $form['message'] = [
+        '#markup' => $this->t('There are no pages with active subscribers.'),
+      ];
+      return $form;
+    }
+
+    $subscriptions = $subscription_storage->loadMultiple($result);
+    $node_ids = [];
+    foreach ($subscriptions as $subscription) {
+      $node_ids[$subscription->getSubscribedEntityId()] = $subscription->getSubscribedEntityId();
+    }
+
+    // Load nodes and prepare options
+    $nodes = $node_storage->loadMultiple($node_ids);
+    $options = [];
+    foreach ($nodes as $node) {
+      $options[$node->id()] = $node->label();
+    }
+
+    $form['node'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Select Content'),
+      '#options' => $options,
+      '#required' => TRUE,
+      '#description' => $this->t('Select the content to send notifications about.'),
+    ];
+
+    $form['notes'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Notification Notes'),
+      '#description' => $this->t('Enter any additional notes to include in the notification email.'),
+      '#rows' => 4,
+    ];
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Send Notification'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $node_id = $form_state->getValue('node');
+    $notes = $form_state->getValue('notes');
+
+    try {
+      $node = $this->entityTypeManager->getStorage('node')->load($node_id);
+      if ($node) {
+        // Store notes in tempstore or pass through event system
+        \Drupal::state()->set('page_notifications_manual_notes_' . $node->id(), $notes);
+
+        $this->notificationManager->notifySubscribers($node);
+        $this->messenger()->addStatus($this->t('Notifications have been queued for sending.'));
+      }
+    }
+    catch (\Exception $e) {
+      $this->messenger()->addError($this->t('There was a problem sending the notifications.'));
+      \Drupal::logger('page_notifications')->error($e->getMessage());
+    }
+  }
+
+}
+```
+
+# src/Form/ManualSubscriptionAddForm.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Component\Utility\EmailValidatorInterface;
+
+/**
+ * Form for manually adding subscriptions.
+ */
+class ManualSubscriptionAddForm extends FormBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The messenger service.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * The email validator.
+   *
+   * @var \Drupal\Component\Utility\EmailValidatorInterface
+   */
+  protected $emailValidator;
+
+  /**
+   * Constructs a new ManualSubscriptionAddForm.
+   */
+  public function __construct(
+    EntityTypeManagerInterface $entity_type_manager,
+    MessengerInterface $messenger,
+    EmailValidatorInterface $email_validator
+  ) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->messenger = $messenger;
+    $this->emailValidator = $email_validator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('messenger'),
+      $container->get('email.validator')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_manual_subscription_add';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['node'] = [
+      '#type' => 'entity_autocomplete',
+      '#title' => $this->t('Content'),
+      '#description' => $this->t('Select the content to subscribe to.'),
+      '#target_type' => 'node',
+      '#required' => TRUE,
+      '#selection_handler' => 'default:node_enhanced',
+      '#selection_settings' => [
+        'target_bundles' => NULL,
+      ],
+    ];
+
+    $form['emails'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Email Addresses'),
+      '#description' => $this->t('Enter email addresses, one per line. These subscribers will be automatically verified.'),
+      '#required' => TRUE,
+      '#rows' => 10,
+    ];
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Add Subscriptions'),
+      '#button_type' => 'primary',
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $emails = explode("\n", $form_state->getValue('emails'));
+    $invalid_emails = [];
+
+    foreach ($emails as $email) {
+      $email = trim($email);
+      if (!empty($email) && !$this->emailValidator->isValid($email)) {
+        $invalid_emails[] = $email;
+      }
+    }
+
+    if (!empty($invalid_emails)) {
+      $form_state->setErrorByName('emails', $this->t('The following email addresses are invalid: @emails', [
+        '@emails' => implode(', ', $invalid_emails),
+      ]));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $node_id = $form_state->getValue('node');
+    $emails = array_filter(array_map('trim', explode("\n", $form_state->getValue('emails'))));
+    $added = 0;
+    $skipped = 0;
+
+    try {
+      foreach ($emails as $email) {
+        if (empty($email)) {
+          continue;
+        }
+
+        // Check for existing subscription
+        $existing = $this->entityTypeManager
+          ->getStorage('page_notification_subscription')
+          ->loadByProperties([
+            'email' => $email,
+            'subscribed_entity_id' => $node_id,
+            'subscribed_entity_type' => 'node',
+          ]);
+
+        if (!empty($existing)) {
+          $skipped++;
+          continue;
+        }
+
+        // Create new subscription
+        $subscription = $this->entityTypeManager
+          ->getStorage('page_notification_subscription')
+          ->create([
+            'email' => $email,
+            'subscribed_entity_id' => $node_id,
+            'subscribed_entity_type' => 'node',
+            'token' => bin2hex(random_bytes(32)),
+            'unsubscribe_token' => bin2hex(random_bytes(32)),
+            'status' => TRUE, // Automatically verified
+          ]);
+
+        $subscription->save();
+        $added++;
+      }
+
+      if ($added > 0) {
+        $this->messenger->addStatus($this->t('Successfully added @count subscription(s).', [
+          '@count' => $added,
+        ]));
+      }
+
+      if ($skipped > 0) {
+        $this->messenger->addWarning($this->t('Skipped @count existing subscription(s).', [
+          '@count' => $skipped,
+        ]));
+      }
+    }
+    catch (\Exception $e) {
+      $this->messenger->addError($this->t('An error occurred while adding subscriptions.'));
+      $this->getLogger('page_notifications')->error($e->getMessage());
+    }
+  }
+
+}
+```
+
+# src/Form/ModalSubscriptionForm.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\page_notifications\Service\NotificationManagerInterface;
+use Drupal\page_notifications\Service\SpamPrevention;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Psr\Log\LoggerInterface;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\page_notifications\Traits\FloodControlTrait;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseModalDialogCommand;
+use Drupal\Core\Ajax\MessageCommand;
+use Drupal\Core\Ajax\ReplaceCommand;
+
+/**
+ * Provides a subscription form for modal display.
+ */
+class ModalSubscriptionForm extends FormBase {
+  use FloodControlTrait;
+
+  /**
+   * The notification manager service.
+   *
+   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
+   */
+  protected $notificationManager;
+
+  /**
+   * The spam prevention service.
+   *
+   * @var \Drupal\page_notifications\Service\SpamPrevention
+   */
+  protected $spamPrevention;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The logger instance.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new ModalSubscriptionForm.
+   */
+  public function __construct(
+    NotificationManagerInterface $notification_manager,
+    SpamPrevention $spam_prevention,
+    ConfigFactoryInterface $config_factory,
+    LoggerInterface $logger,
+    FloodInterface $flood
+  ) {
+    $this->notificationManager = $notification_manager;
+    $this->spamPrevention = $spam_prevention;
+    $this->configFactory = $config_factory;
+    $this->logger = $logger;
+    $this->setFloodService($flood);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('page_notifications.notification_manager'),
+      $container->get('page_notifications.spam_prevention'),
+      $container->get('config.factory'),
+      $container->get('logger.factory')->get('page_notifications'),
+      $container->get('flood')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_modal_subscription_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL) {
+    if (!$entity) {
+      return [
+        '#markup' => $this->t('No content found for subscription.'),
+      ];
+    }
+
+    $form['#prefix'] = '<div id="modal-subscription-form-wrapper">';
+    $form['#suffix'] = '</div>';
+
+    // Store the entity in the form state
+    $form_state->set('entity', $entity);
+
+    $form['email'] = [
+      '#type' => 'email',
+      '#title' => $this->t('Email address'),
+      '#required' => TRUE,
+      '#description' => $this->t('Enter your email address to receive notifications when this content is updated.'),
+    ];
+
+    // Add spam prevention based on configuration
+    $config = $this->configFactory->get('page_notifications.settings');
+    $captcha_type = $config->get('spam_prevention.captcha_type');
+
+    if ($captcha_type === 'math') {
+      if (!$form_state->getUserInput()) {
+        $challenge = $this->spamPrevention->generateMathChallenge();
+        $form['math_challenge_data'] = [
+          '#type' => 'hidden',
+          '#value' => json_encode($challenge),
+        ];
+      }
+      else {
+        $challenge = json_decode($form_state->getUserInput()['math_challenge_data'] ?? '{}', TRUE);
+      }
+
+      if (!empty($challenge)) {
+        $form['math_challenge_data'] = [
+          '#type' => 'hidden',
+          '#value' => json_encode($challenge),
+        ];
+
+        $form['math_challenge'] = [
+          '#type' => 'number',
+          '#title' => $challenge['question'],
+          '#required' => TRUE,
+          '#description' => $this->t('Please solve this simple math problem to prevent spam.'),
+        ];
+      }
+    }
+    elseif ($captcha_type === 'recaptcha' && $this->spamPrevention->isRecaptchaAvailable()) {
+      $form['captcha'] = [
+        '#type' => 'captcha',
+        '#captcha_type' => 'recaptcha/reCAPTCHA',
+      ];
+    }
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Subscribe'),
+      '#ajax' => [
+        'callback' => '::submitModalAjax',
+        'event' => 'click',
+        'progress' => [
+          'type' => 'throbber',
+          'message' => $this->t('Processing...'),
+        ],
+      ],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * AJAX callback for modal form submission.
+   */
+  public function submitModalAjax(array &$form, FormStateInterface $form_state) {
+    $response = new AjaxResponse();
+
+    if ($form_state->hasAnyErrors()) {
+      $response->addCommand(new ReplaceCommand(
+        '#modal-subscription-form-wrapper',
+        [
+          '#type' => 'container',
+          '#attributes' => ['id' => 'modal-subscription-form-wrapper'],
+          'status_messages' => [
+            '#type' => 'status_messages',
+          ],
+          'form' => $form,
+        ]
+      ));
+      return $response;
+    }
+
+    try {
+      $entity = $form_state->get('entity');
+      if (!$entity) {
+        throw new \Exception('No entity found for subscription.');
+      }
+
+      $email = $form_state->getValue('email');
+      $subscription = $this->notificationManager->createSubscription($email, $entity);
+
+      $response->addCommand(new CloseModalDialogCommand());
+      $response->addCommand(new MessageCommand(
+        $this->t('Thank you for subscribing. Please check your email to confirm your subscription.'),
+        NULL,
+        ['type' => 'status']
+      ));
+    }
+    catch (\Exception $e) {
+      $this->logger->error('Subscription error: @message', ['@message' => $e->getMessage()]);
+
+      $response->addCommand(new ReplaceCommand(
+        '#modal-subscription-form-wrapper',
+        [
+          '#type' => 'container',
+          '#attributes' => ['id' => 'modal-subscription-form-wrapper'],
+          'status_messages' => [
+            '#type' => 'status_messages',
+            '#message_list' => [
+              'error' => [$this->t('There was a problem creating your subscription. Please try again later.')],
+            ],
+          ],
+          'form' => $form,
+        ]
+      ));
+    }
+
+    return $response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $email = $form_state->getValue('email');
+
+    // Check flood control before other validation
+    if (!$this->checkFloodControl($email, $form_state)) {
+      return;
+    }
+
+    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+      $form_state->setErrorByName('email', $this->t('Please enter a valid email address.'));
+      return;
+    }
+
+    // Validate math challenge if enabled
+    $config = $this->configFactory->get('page_notifications.settings');
+    $captcha_type = $config->get('spam_prevention.captcha_type');
+
+    if ($captcha_type === 'math') {
+      $challenge_data = $form_state->getValue('math_challenge_data');
+      if ($challenge_data) {
+        $challenge = json_decode($challenge_data, TRUE);
+        $response = $form_state->getValue('math_challenge');
+
+        if (!$this->spamPrevention->validateMathResponse($response, $challenge)) {
+          $form_state->setErrorByName('math_challenge', $this->t('The answer to the math challenge is incorrect.'));
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Empty as everything is handled in the AJAX callback
+  }
+
+}
+```
+
+# src/Form/PurgeSubscriptionsConfirmForm.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class PurgeSubscriptionsConfirmForm extends ConfirmFormBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new PurgeSubscriptionsConfirmForm.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_purge_subscriptions_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    $count = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->accessCheck(FALSE)
+      ->count()
+      ->execute();
+
+    return $this->t('Are you sure you want to delete all @count subscriptions?', [
+      '@count' => $count,
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->t('This action cannot be undone. All subscription data will be permanently deleted.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('page_notifications.settings');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $batch = [
+      'title' => $this->t('Deleting all subscriptions...'),
+      'operations' => [
+        [[$this, 'purgeSubscriptionsBatch'], []],
+      ],
+      'finished' => [[$this, 'purgeSubscriptionsFinished']],
+    ];
+    batch_set($batch);
+    $form_state->setRedirect('page_notifications.settings');
+  }
+
+  /**
+   * Batch operation to purge subscriptions.
+   */
+  public function purgeSubscriptionsBatch(&$context) {
+    if (!isset($context['sandbox']['progress'])) {
+      $context['sandbox']['progress'] = 0;
+      $context['sandbox']['current_id'] = 0;
+      $context['sandbox']['max'] = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->getQuery()
+        ->accessCheck(FALSE)
+        ->count()
+        ->execute();
+    }
+
+    $subscription_ids = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('id', $context['sandbox']['current_id'], '>')
+      ->sort('id')
+      ->range(0, 50)
+      ->accessCheck(FALSE)
+      ->execute();
+
+    if (!empty($subscription_ids)) {
+      $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
+      $entities = $storage->loadMultiple($subscription_ids);
+      $storage->delete($entities);
+
+      $context['sandbox']['current_id'] = end($subscription_ids);
+      $context['sandbox']['progress'] += count($subscription_ids);
+    }
+
+    $context['finished'] = empty($subscription_ids) ? 1 : $context['sandbox']['progress'] / $context['sandbox']['max'];
+  }
+
+  /**
+   * Batch finished callback.
+   */
+  public function purgeSubscriptionsFinished($success, $results, $operations) {
+    if ($success) {
+      $this->messenger()->addStatus($this->t('Successfully deleted all subscriptions.'));
+    }
+    else {
+      $this->messenger()->addError($this->t('An error occurred while deleting subscriptions.'));
+    }
+  }
+}
+```
+
+# src/Form/SettingsForm.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Mail\MailManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\filter\FilterFormatInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Link;
+
+/**
+ * Configures Page Notifications settings.
+ */
+class SettingsForm extends ConfigFormBase {
+
+  /**
+   * The mail manager.
+   *
+   * @var \Drupal\Core\Mail\MailManagerInterface
+   */
+  protected $mailManager;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Constructs a SettingsForm object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
+   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
+   *   The mail manager service.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *  The module handler service.
+   */
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    MailManagerInterface $mail_manager,
+    EntityTypeManagerInterface $entity_type_manager,
+    ModuleHandlerInterface $module_handler
+  ) {
+    // Check Drupal version
+    if (version_compare(\Drupal::VERSION, '11.0', '>=')) {
+      parent::__construct($config_factory, \Drupal::service('config.typed'));
+  } else {
+      parent::__construct($config_factory);
+  }
+    $this->mailManager = $mail_manager;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('plugin.manager.mail'),
+      $container->get('entity_type.manager'),
+      $container->get('module_handler')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_settings';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return ['page_notifications.settings'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildForm($form, $form_state);
+    $config = $this->config('page_notifications.settings');
+
+    // Get available text formats for the current user
+    $formats = filter_formats(\Drupal::currentUser());
+    $format_options = [];
+    foreach ($formats as $format) {
+      $format_options[$format->id()] = $format->label();
+    }
+
+
+    $form['email_settings'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Email Settings'),
+      '#open' => TRUE,
+    ];
+
+    // text format selection
+    $form['email_settings']['mail_format'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Email Text Format'),
+      '#description' => $this->t('Select the text format to use for email content. Ensure the chosen format allows necessary HTML tags for links and formatting.'),
+      '#options' => $format_options,
+      '#default_value' => $config->get('email_settings.mail_format') ?? reset($format_options),
+      '#required' => TRUE,
+    ];
+
+    $selected_format = $config->get('email_settings.mail_format') ?? reset($format_options);
+    $verification_body = $config->get('email_templates.verification_body');
+    $notification_body = $config->get('email_templates.notification_body');
+    $already_subscribed_body = $config->get('email_templates.already_subscribed_body');
+
+    $form['email_settings']['from_email'] = [
+      '#type' => 'email',
+      '#title' => $this->t('From Email Address'),
+      '#description' => $this->t('The email address that notifications will be sent from. If left empty, the site default will be used.'),
+      '#default_value' => $config->get('notification_settings.from_email'),
+    ];
+
+    $form['email_settings']['token_expiration'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Token Expiration'),
+      '#description' => $this->t('Number of hours before verification tokens expire. Enter 0 to never expire unverified subscriptions.'),
+      '#default_value' => $config->get('notification_settings.token_expiration') ?? 48,
+      '#min' => 1,
+      '#required' => TRUE,
+    ];
+
+    $form['email_templates'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Email Templates'),
+      '#open' => TRUE,
+    ];
+
+    $form['email_templates']['verification_subject'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Verification Email Subject'),
+      '#default_value' => $config->get('email_templates.verification_subject') ?? 'Verify your subscription to [node:title]',
+      '#required' => TRUE,
+    ];
+
+    $form['email_templates']['verification_body'] = [
+      '#type' => 'text_format',
+      '#title' => $this->t('Verification Email Body'),
+      '#default_value' => is_array($verification_body) ? $verification_body['value'] : $verification_body,
+      '#format' => is_array($verification_body) ? $verification_body['format'] : $selected_format,
+      '#description' => $this->t('Available tokens: [subscription:verify-url], [subscription:email], [node:title], [node:url]'),
+      '#required' => TRUE,
+      '#rows' => 10,
+    ];
+
+    $form['email_templates']['notification_subject'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Update Notification Subject'),
+      '#default_value' => $config->get('email_templates.notification_subject') ?? '[node:title] has been updated',
+      '#required' => TRUE,
+    ];
+
+    $form['email_templates']['notification_body'] = [
+      '#type' => 'text_format',
+      '#title' => $this->t('Update Notification Body'),
+      '#default_value' => is_array($notification_body) ? $notification_body['value'] : $notification_body,
+      '#format' => is_array($notification_body) ? $notification_body['format'] : $selected_format,
+      '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [node:changed], [subscription:unsubscribe-url]'),
+      '#required' => TRUE,
+      '#rows' => 10,
+    ];
+
+    $form['email_templates']['already_subscribed_subject'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Already Subscribed Email Subject'),
+      '#default_value' => $config->get('email_templates.already_subscribed_subject') ?? 'You are already subscribed to [node:title]',
+      '#required' => TRUE,
+    ];
+
+    $form['email_templates']['already_subscribed_body'] = [
+      '#type' => 'text_format',
+      '#title' => $this->t('Already Subscribed Email Body'),
+      '#default_value' => is_array($already_subscribed_body) ? $already_subscribed_body['value'] : $already_subscribed_body,
+      '#format' => is_array($already_subscribed_body) ? $already_subscribed_body['format'] : $selected_format,
+      '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [subscription:unsubscribe-url]'),
+      '#required' => TRUE,
+      '#rows' => 10,
+    ];
+
+    $form['security'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Security Settings'),
+      '#open' => TRUE,
+    ];
+
+    $form['security']['require_verification'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Require Email Verification'),
+      '#description' => $this->t('If checked, users must verify their email address before the subscription becomes active.'),
+      '#default_value' => $config->get('security.require_verification') ?? TRUE,
+    ];
+
+     // Flood control settings
+     $form['security']['flood_control'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Flood Control Settings'),
+      '#open' => TRUE,
+      '#tree' => TRUE,
+    ];
+
+    $form['security']['flood_control']['ip_limit'] = [
+      '#type' => 'number',
+      '#title' => $this->t('IP-based attempt limit'),
+      '#description' => $this->t('Maximum number of subscription attempts allowed from a single IP address.'),
+      '#default_value' => $config->get('security.flood_control.ip_limit') ?? 200,
+      '#min' => 1,
+      '#required' => TRUE,
+    ];
+
+    $form['security']['flood_control']['ip_window'] = [
+      '#type' => 'number',
+      '#title' => $this->t('IP-based time window'),
+      '#description' => $this->t('Time window in hours for IP-based subscription attempts. Set to 0 to disable IP-based flood control.'),
+      '#default_value' => $config->get('security.flood_control.ip_window') ?? 1,
+      '#min' => 0, // Changed from 1 to 0
+      '#required' => TRUE,
+      '#field_suffix' => $this->t('hours'),
+    ];
+
+    $form['security']['flood_control']['identifier_limit'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Email-based attempt limit'),
+      '#description' => $this->t('Maximum number of subscription attempts allowed for the same email address.'),
+      '#default_value' => $config->get('security.flood_control.identifier_limit') ?? 50,
+      '#min' => 1,
+      '#required' => TRUE,
+    ];
+
+    $form['security']['flood_control']['identifier_window'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Email-based time window'),
+      '#description' => $this->t('Time window in hours for email-based subscription attempts. Set to 0 to disable email-based flood control.'),
+      '#default_value' => $config->get('security.flood_control.identifier_window') ?? 1,
+      '#min' => 0, // Changed from 1 to 0
+      '#required' => TRUE,
+      '#field_suffix' => $this->t('hours'),
+    ];
+
+    // Add spam prevention section
+    $form['spam_prevention'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Spam Prevention'),
+      '#open' => TRUE,
+    ];
+
+    $captcha_options = [
+      'none' => $this->t('None'),
+      'math' => $this->t('Simple Math Challenge'),
+    ];
+
+    // Add reCAPTCHA option if the captcha module is installed
+    if ($this->moduleHandler->moduleExists('captcha')) {
+      $captcha_options['recaptcha'] = $this->t('reCAPTCHA');
+    }
+
+    $form['spam_prevention']['captcha_type'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Captcha Type'),
+      '#options' => $captcha_options,
+      '#default_value' => $config->get('spam_prevention.captcha_type') ?? 'none',
+      '#description' => $this->t('Select the type of spam prevention to use on the subscription form.'),
+    ];
+
+    $form['spam_prevention']['math_operator'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Math Challenge Operator'),
+      '#options' => [
+        '+' => $this->t('Addition (+)'),
+        '*' => $this->t('Multiplication (*)'),
+      ],
+      '#default_value' => $config->get('spam_prevention.math_operator') ?? '+',
+      '#description' => $this->t('Select the operator to use for the math challenge.'),
+      '#states' => [
+        'visible' => [
+          ':input[name="captcha_type"]' => ['value' => 'math'],
+        ],
+      ],
+    ];
+
+    if ($this->moduleHandler->moduleExists('captcha')) {
+      $form['spam_prevention']['use_recaptcha'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Use reCAPTCHA if available'),
+        '#default_value' => $config->get('spam_prevention.use_recaptcha') ?? FALSE,
+        '#description' => $this->t('Enable this to use reCAPTCHA if the captcha module is configured to use it.'),
+        '#states' => [
+          'visible' => [
+            ':input[name="captcha_type"]' => ['value' => 'recaptcha'],
+          ],
+        ],
+      ];
+    }
+
+    $form['danger_zone'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Danger Zone'),
+      '#description' => $this->t('These actions cannot be undone.'),
+      '#open' => FALSE,
+      '#weight' => 100,
+    ];
+
+    $subscription_count = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->accessCheck(FALSE)
+      ->count()
+      ->execute();
+
+      $form['danger_zone']['purge_subscriptions'] = [
+        '#type' => 'link',
+        '#title' => $this->t('Delete all subscriptions (@count total)', ['@count' => $subscription_count]),
+        '#url' => Url::fromRoute('page_notifications.purge_subscriptions_confirm'),
+        '#attributes' => [
+          'class' => ['button', 'button--danger', 'use-ajax'],
+          'data-dialog-type' => 'modal',
+          'data-dialog-options' => json_encode([
+            'width' => 700,
+          ]),
+        ],
+        '#disabled' => ($subscription_count === 0),
+      ];
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    if ($form_state->getValue('from_email') && !$this->mailManager->validateAddress($form_state->getValue('from_email'))) {
+      $form_state->setErrorByName('from_email', $this->t('The email address is not valid.'));
+    }
+  }
+
+  public function purgeSubscriptions(array &$form, FormStateInterface $form_state) {
+    $batch = [
+      'title' => $this->t('Deleting all subscriptions...'),
+      'operations' => [
+        [[$this, 'purgeSubscriptionsBatch'], []],
+      ],
+      'finished' => [[$this, 'purgeSubscriptionsFinished']],
+    ];
+    batch_set($batch);
+  }
+
+  public function purgeSubscriptionsBatch(&$context) {
+    if (!isset($context['sandbox']['progress'])) {
+      $context['sandbox']['progress'] = 0;
+      $context['sandbox']['current_id'] = 0;
+      $context['sandbox']['max'] = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->getQuery()
+        ->accessCheck(FALSE)
+        ->count()
+        ->execute();
+    }
+
+    // Process subscriptions in chunks of 50
+    $subscription_ids = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('id', $context['sandbox']['current_id'], '>')
+      ->sort('id')
+      ->range(0, 50)
+      ->accessCheck(FALSE)
+      ->execute();
+
+    if (!empty($subscription_ids)) {
+      $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
+      $entities = $storage->loadMultiple($subscription_ids);
+      $storage->delete($entities);
+
+      $context['sandbox']['current_id'] = end($subscription_ids);
+      $context['sandbox']['progress'] += count($subscription_ids);
+    }
+
+    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+  }
+
+  public function purgeSubscriptionsFinished($success, $results, $operations) {
+    if ($success) {
+      $this->messenger()->addStatus($this->t('Successfully deleted all subscriptions.'));
+    }
+    else {
+      $this->messenger()->addError($this->t('An error occurred while deleting subscriptions.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $values = $form_state->getValues();
+
+    $this->config('page_notifications.settings')
+      ->set('notification_settings.from_email', $values['from_email'])
+      ->set('notification_settings.token_expiration', $values['token_expiration'])
+      ->set('email_settings.mail_format', $values['mail_format'])
+      ->set('email_templates.verification_subject', $values['verification_subject'])
+      ->set('email_templates.verification_body', $values['verification_body'])
+      ->set('email_templates.already_subscribed_subject', $values['already_subscribed_subject'])
+->set('email_templates.already_subscribed_body', $values['already_subscribed_body'])
+      ->set('email_templates.notification_subject', $values['notification_subject'])
+      ->set('email_templates.notification_body', $values['notification_body'])
+      ->set('security.require_verification', $values['require_verification'])
+      ->set('security.flood_control.ip_limit', $values['flood_control']['ip_limit'])
+      ->set('security.flood_control.ip_window', $values['flood_control']['ip_window'])
+      ->set('security.flood_control.identifier_limit', $values['flood_control']['identifier_limit'])
+      ->set('security.flood_control.identifier_window', $values['flood_control']['identifier_window'])
+      ->set('spam_prevention.captcha_type', $values['captcha_type'])
+      ->set('spam_prevention.math_operator', $values['math_operator'])
+      ->set('spam_prevention.use_recaptcha', $values['use_recaptcha'] ?? FALSE)
+      ->save();
+
+    parent::submitForm($form, $form_state);
+  }
+
+}
+```
+
+# src/Form/SubscriptionDeleteForm.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Entity\ContentEntityDeleteForm;
+
+class SubscriptionDeleteForm extends ContentEntityDeleteForm {
+}
+```
+
+# src/Form/SubscriptionForm.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\page_notifications\Service\NotificationManagerInterface;
+use Drupal\page_notifications\Service\SpamPrevention;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Psr\Log\LoggerInterface;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\page_notifications\Traits\FloodControlTrait;
+
+/**
+ * Provides a subscription form.
+ */
+class SubscriptionForm extends FormBase {
+  use FloodControlTrait;
+
+  /**
+   * The notification manager service.
+   *
+   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
+   */
+  protected $notificationManager;
+
+  /**
+   * The spam prevention service.
+   *
+   * @var \Drupal\page_notifications\Service\SpamPrevention
+   */
+  protected $spamPrevention;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The logger instance.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new SubscriptionForm.
+   *
+   * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager
+   *   The notification manager service.
+   * @param \Drupal\page_notifications\Service\SpamPrevention $spam_prevention
+   *   The spam prevention service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger instance.
+   * @param \Drupal\Core\Flood\FloodInterface $flood
+   *   The flood service.
+   */
+  public function __construct(
+    NotificationManagerInterface $notification_manager,
+    SpamPrevention $spam_prevention,
+    ConfigFactoryInterface $config_factory,
+    LoggerInterface $logger,
+    FloodInterface $flood
+  ) {
+    $this->notificationManager = $notification_manager;
+    $this->spamPrevention = $spam_prevention;
+    $this->logger = $logger;
+    $this->configFactory = $config_factory;
+    $this->setFloodService($flood);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('page_notifications.notification_manager'),
+      $container->get('page_notifications.spam_prevention'),
+      $container->get('config.factory'),
+      $container->get('logger.factory')->get('page_notifications'),
+      $container->get('flood')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_subscription_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL) {
+    $form_state->set('entity', $entity);
+
+    $form['email'] = [
+      '#type' => 'email',
+      '#title' => $this->t('Email address'),
+      '#required' => TRUE,
+      '#description' => $this->t('Enter your email address to receive notifications when this content is updated.'),
+    ];
+
+    // Add spam prevention based on configuration
+    $config = $this->configFactory->get('page_notifications.settings');
+    $captcha_type = $config->get('spam_prevention.captcha_type');
+
+    if ($captcha_type === 'math') {
+      // Store challenge data in a hidden field to persist through form submissions
+      if (!$form_state->getUserInput()) {
+        // Only generate new challenge if this is the initial form build
+        $challenge = $this->spamPrevention->generateMathChallenge();
+
+        $form['math_challenge_data'] = [
+          '#type' => 'hidden',
+          '#value' => json_encode($challenge),
+        ];
+      }
+      else {
+        // Use existing challenge data from form input
+        $challenge = json_decode($form_state->getUserInput()['math_challenge_data'] ?? '{}', TRUE);
+      }
+
+      if (!empty($challenge)) {
+        $form['math_challenge_data'] = [
+          '#type' => 'hidden',
+          '#value' => json_encode($challenge),
+        ];
+
+        $form['math_challenge'] = [
+          '#type' => 'number',
+          '#title' => $challenge['question'],
+          '#required' => TRUE,
+          '#description' => $this->t('Please solve this simple math problem to prevent spam.'),
+        ];
+      }
+    }
+    elseif ($captcha_type === 'recaptcha' && $this->spamPrevention->isRecaptchaAvailable()) {
+      $form['captcha'] = [
+        '#type' => 'captcha',
+        '#captcha_type' => 'recaptcha/reCAPTCHA',
+      ];
+    }
+
+    $form['actions'] = [
+      '#type' => 'actions',
+    ];
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Subscribe'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $email = $form_state->getValue('email');
+
+    // Check flood control before other validation
+    if (!$this->checkFloodControl($email, $form_state)) {
+      return;
+    }
+
+    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+      $form_state->setErrorByName('email', $this->t('Please enter a valid email address.'));
+      return;
+    }
+
+    // Validate math challenge if enabled
+    $config = $this->configFactory->get('page_notifications.settings');
+    $captcha_type = $config->get('spam_prevention.captcha_type');
+
+    if ($captcha_type === 'math') {
+      $challenge_data = $form_state->getValue('math_challenge_data');
+      if ($challenge_data) {
+        $challenge = json_decode($challenge_data, TRUE);
+        $response = $form_state->getValue('math_challenge');
+
+        if (!$this->spamPrevention->validateMathResponse($response, $challenge)) {
+          $form_state->setErrorByName('math_challenge', $this->t('The answer to the math challenge is incorrect.'));
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $entity = $form_state->get('entity');
+    $email = $form_state->getValue('email');
+
+    try {
+      // Register flood control event
+      $this->registerFloodControl($email);
+
+      $subscription = $this->notificationManager->createSubscription($email, $entity);
+      $this->messenger()->addStatus($this->t('Thank you for subscribing. Please check your email to confirm your subscription.'));
+
+      // Clear the email field after successful submission
+      $form_state->setValue('email', '');
+      $form_state->setUserInput(['email' => '']);
+    }
+    catch (\Exception $e) {
+      $this->messenger()->addError($this->t('There was a problem creating your subscription. Please try again later.'));
+      $this->logger->error('Subscription creation failed: @message', ['@message' => $e->getMessage()]);
+    }
+
+    // Set form to rebuild
+    $form_state->setRebuild(TRUE);
+  }
+
+}
+```
+
+# src/Form/SubscriptionMigrateForm.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Form for migrating subscriptions between nodes.
+ */
+class SubscriptionMigrateForm extends FormBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new SubscriptionMigrateForm.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'page_notifications_subscription_migrate_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    // Check if there are any nodes with subscriptions
+    $subscription_count = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('subscribed_entity_type', 'node')
+      ->condition('status', TRUE)
+      ->count()
+      ->accessCheck(FALSE)
+      ->execute();
+
+    if ($subscription_count === 0) {
+      $form['message'] = [
+        '#markup' => $this->t('There are no nodes with active subscriptions.'),
+      ];
+      return $form;
+    }
+
+    $form['description'] = [
+      '#markup' => $this->t('This form will migrate all active subscriptions from one node to another.'),
+    ];
+
+    $form['source_node'] = [
+      '#type' => 'entity_autocomplete',
+      '#title' => $this->t('FROM: Source Node'),
+      '#description' => $this->t('Select the node from which to migrate subscriptions.'),
+      '#target_type' => 'node',
+      '#required' => TRUE,
+      '#selection_handler' => 'default:node_with_subscriptions',
+    ];
+
+    $form['target_node'] = [
+      '#type' => 'entity_autocomplete',
+      '#title' => $this->t('TO: Target Node'),
+      '#description' => $this->t('Select the node to which subscriptions will be migrated.'),
+      '#target_type' => 'node',
+      '#required' => TRUE,
+      '#selection_handler' => 'default:node_enhanced',
+      '#selection_settings' => [
+        'target_bundles' => NULL,
+      ],
+    ];
+
+    $form['actions']['#type'] = 'actions';
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Migrate Subscriptions'),
+      '#button_type' => 'primary',
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $source_nid = $form_state->getValue('source_node');
+    $target_nid = $form_state->getValue('target_node');
+
+    if ($source_nid === $target_nid) {
+      $form_state->setError($form['target_node'], $this->t('Source and target nodes must be different.'));
+      return;
+    }
+
+    // Verify the source node still has subscriptions (in case they were deleted)
+    $subscription_count = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('subscribed_entity_id', $source_nid)
+      ->condition('subscribed_entity_type', 'node')
+      ->condition('status', TRUE)
+      ->count()
+      ->accessCheck(FALSE)
+      ->execute();
+
+    if ($subscription_count === 0) {
+      $form_state->setError($form['source_node'], $this->t('The source node has no active subscriptions to migrate.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $source_nid = $form_state->getValue('source_node');
+    $target_nid = $form_state->getValue('target_node');
+
+    try {
+      $batch = [
+        'title' => $this->t('Migrating subscriptions'),
+        'operations' => [
+          [
+            [$this, 'processMigration'],
+            [$source_nid, $target_nid],
+          ],
+        ],
+        'finished' => [$this, 'migrationFinished'],
+      ];
+
+      batch_set($batch);
+    }
+    catch (\Exception $e) {
+      $this->messenger()->addError($this->t('An error occurred while preparing the migration: @error', [
+        '@error' => $e->getMessage(),
+      ]));
+    }
+  }
+
+  /**
+   * Batch operation callback for migrating subscriptions.
+   */
+  public function processMigration($source_nid, $target_nid, &$context) {
+    if (!isset($context['sandbox']['progress'])) {
+      $context['sandbox']['progress'] = 0;
+      $context['sandbox']['current_id'] = 0;
+      $context['sandbox']['max'] = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->getQuery()
+        ->condition('subscribed_entity_id', $source_nid)
+        ->condition('subscribed_entity_type', 'node')
+        ->count()
+        ->accessCheck(FALSE)
+        ->execute();
+    }
+
+    // Process subscriptions in chunks of 50
+    $subscription_ids = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('subscribed_entity_id', $source_nid)
+      ->condition('subscribed_entity_type', 'node')
+      ->condition('id', $context['sandbox']['current_id'], '>')
+      ->sort('id')
+      ->range(0, 50)
+      ->accessCheck(FALSE)
+      ->execute();
+
+    foreach ($subscription_ids as $id) {
+      /** @var \Drupal\page_notifications\Entity\Subscription $subscription */
+      $subscription = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->load($id);
+
+      // Simply update the entity ID
+      $subscription->setSubscribedEntityId($target_nid);
+      $subscription->save();
+
+      $context['sandbox']['progress']++;
+      $context['sandbox']['current_id'] = $id;
+    }
+
+    if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
+      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+    }
+  }
+
+  /**
+   * Batch finished callback.
+   */
+  public function migrationFinished($success, $results, $operations) {
+    if ($success) {
+      $this->messenger()->addStatus($this->t('Successfully migrated all subscriptions to the new node.'));
+    }
+    else {
+      $this->messenger()->addError($this->t('An error occurred while migrating subscriptions.'));
+    }
+  }
+
+}
+```
+
+# src/Mail/PageNotificationsMailHandler.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Mail;
+
+use Drupal\Core\Mail\MailManagerInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\token\TokenInterface;
+use Drupal\Core\Theme\ThemeManagerInterface;
+
+
+/**
+ * Handles mail formatting for page notifications.
+ */
+class PageNotificationsMailHandler {
+
+  use StringTranslationTrait;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The token service.
+   *
+   * @var \Drupal\token\TokenServiceInterface
+   */
+  protected $token;
+
+  /**
+   * The theme manager.
+   *
+   * @var \Drupal\Core\Theme\ThemeManagerInterface
+   */
+  protected $themeManager;
+
+  /**
+   * Constructs a new PageNotificationsMailHandler.
+   */
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    RendererInterface $renderer,
+    TokenInterface $token,
+    TranslationInterface $translation,
+    ThemeManagerInterface $theme_manager,
+  ) {
+    $this->configFactory = $config_factory;
+    $this->renderer = $renderer;
+    $this->token = $token;
+    $this->setStringTranslation($translation);
+    $this->themeManager = $theme_manager;
+  }
+
+  /**
+   * Gets customized email footer.
+   */
+  protected function getEmailFooter() {
+    // Allow modules to alter the footer
+    $footer = '';
+    \Drupal::moduleHandler()->alter('page_notifications_email_footer', $footer);
+    return $footer;
+  }
+
+  /**
+   * Implements callback_mail().
+   */
+  public function mail($key, &$message, $params) {
+    $config = $this->configFactory->get('page_notifications.settings');
+
+    switch ($key) {
+      case 'verification':
+        $this->buildVerificationEmail($message, $params);
+        break;
+
+      case 'notification':
+        $this->buildNotificationEmail($message, $params);
+        break;
+
+      case 'already_subscribed':
+        $this->buildAlreadySubscribedEmail($message, $params);
+        break;
+    }
+  }
+
+  /**
+   * Builds a verification email.
+   */
+  protected function buildEmail(array &$message, array $params, string $template_type) {
+    $config = $this->configFactory->get('page_notifications.settings');
+    $subscription = $params['subscription'];
+    $entity = $params['entity'];
+
+    $token_data = [
+      'subscription' => $subscription,
+      'node' => $entity,
+      'notification' => $params['notification'] ?? [],
+    ];
+
+    $subject_key = "email_templates.{$template_type}_subject";
+    $body_key = "email_templates.{$template_type}_body";
+
+    $subject_template = $config->get($subject_key);
+    $body_template = $config->get($body_key)['value'];
+
+    // For subject
+    $message['subject'] = \Drupal::token()->replace(
+        $subject_template,
+        $token_data,
+        ['clear' => TRUE]
+    );
+
+    // For body
+    $body = \Drupal::token()->replace(
+        $body_template,
+        $token_data,
+        ['clear' => TRUE]
+    );
+
+    $themed_content = $this->wrapEmailContent(
+        $body,
+        $template_type,
+        $subscription,
+        $entity
+    );
+
+    // Configure for HTML mail
+    $message['params']['format'] = 'text/html';
+    $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed';
+    $message['body'] = [$this->renderer->render($themed_content)];
+  }
+
+  /**
+   * Wraps email content in themed template.
+   */
+  protected function wrapEmailContent($content, $email_type, $subscription, $entity) {
+
+     // Ensure content is properly structured as markup
+    $processed_content = [
+      '#type' => 'markup',
+      '#markup' => $content,
+    ];
+
+    return [
+      '#theme' => 'page_notifications_email_wrapper',
+      '#content' => $processed_content,
+      '#email_type' => $email_type,
+      '#subscription' => $subscription,
+      '#entity' => $entity,
+      '#footer' => $this->getEmailFooter(),
+    ];
+  }
+
+  protected function buildVerificationEmail(array &$message, array $params) {
+    $this->buildEmail($message, $params, 'verification');
+  }
+
+  protected function buildNotificationEmail(array &$message, array $params) {
+    $this->buildEmail($message, $params, 'notification');
+  }
+
+  protected function buildAlreadySubscribedEmail(array &$message, array $params) {
+    $this->buildEmail($message, $params, 'already_subscribed');
+  }
+}
+```
+
+# src/Plugin/Block/SubscriptionBlock.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Plugin\Block;
+
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+/**
+ * Provides a subscription block.
+ *
+ * @Block(
+ *   id = "page_notifications_subscription",
+ *   admin_label = @Translation("Page Notifications Subscription"),
+ *   category = @Translation("Page Notifications")
+ * )
+ */
+class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The form builder.
+   *
+   * @var \Drupal\Core\Form\FormBuilderInterface
+   */
+  protected $formBuilder;
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * Constructs a new SubscriptionBlock instance.
+   *
+   * @param array $configuration
+   *   The plugin configuration.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
+   *   The form builder.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger factory.
+   */
+  public function __construct(
+    array $configuration,
+    $plugin_id,
+    $plugin_definition,
+    EntityTypeManagerInterface $entity_type_manager,
+    FormBuilderInterface $form_builder,
+    LoggerChannelFactoryInterface $logger_factory
+  ) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->formBuilder = $form_builder;
+    $this->loggerFactory = $logger_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('form_builder'),
+      $container->get('logger.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'block_description' => $this->t('Subscribe to receive notifications when this page is updated.'),
+      'button_text' => $this->t('Subscribe'),
+      'button_classes' => 'button button--primary',
+      'form_classes' => 'subscription-form',
+      'show_description' => TRUE,
+      'use_modal' => FALSE,
+      'modal_title' => $this->t('Subscribe to Updates'),
+    ] + parent::defaultConfiguration();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function blockForm($form, FormStateInterface $form_state) {
+    $form = parent::blockForm($form, $form_state);
+    $config = $this->getConfiguration();
+
+    $form['appearance'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Appearance Settings'),
+      '#open' => TRUE,
+    ];
+
+    $form['appearance']['show_description'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Show block description'),
+      '#default_value' => $config['show_description'],
+    ];
+
+    $form['appearance']['block_description'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Block Description'),
+      '#description' => $this->t('The text shown above the subscription form.'),
+      '#default_value' => $config['block_description'],
+      '#states' => [
+        'visible' => [
+          ':input[name="settings[appearance][show_description]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+
+    $form['appearance']['button_text'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Button Text'),
+      '#description' => $this->t('The text shown on the subscribe button.'),
+      '#default_value' => $config['button_text'],
+    ];
+
+    $form['appearance']['use_modal'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Use modal dialog'),
+      '#description' => $this->t('Display the subscription form in a modal dialog.'),
+      '#default_value' => $config['use_modal'],
+    ];
+
+    $form['appearance']['modal_title'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Modal Title'),
+      '#description' => $this->t('The title displayed at the top of the modal dialog.'),
+      '#default_value' => $config['modal_title'],
+      '#states' => [
+        'visible' => [
+          ':input[name="settings[appearance][use_modal]"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+
+    $form['styling'] = [
+      '#type' => 'details',
+      '#title' => $this->t('CSS Classes'),
+      '#open' => TRUE,
+    ];
+
+    $form['styling']['button_classes'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Button Classes'),
+      '#description' => $this->t('CSS classes to add to the subscribe button (space-separated).'),
+      '#default_value' => $config['button_classes'],
+    ];
+
+    $form['styling']['form_classes'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Form Classes'),
+      '#description' => $this->t('CSS classes to add to the subscription form wrapper (space-separated).'),
+      '#default_value' => $config['form_classes'],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function blockSubmit($form, FormStateInterface $form_state) {
+    $this->configuration['block_description'] = $form_state->getValue(['appearance', 'block_description']);
+    $this->configuration['button_text'] = $form_state->getValue(['appearance', 'button_text']);
+    $this->configuration['button_classes'] = $form_state->getValue(['styling', 'button_classes']);
+    $this->configuration['form_classes'] = $form_state->getValue(['styling', 'form_classes']);
+    $this->configuration['show_description'] = $form_state->getValue(['appearance', 'show_description']);
+    $this->configuration['use_modal'] = $form_state->getValue(['appearance', 'use_modal']);
+    $this->configuration['modal_title'] = $form_state->getValue(['appearance', 'modal_title']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    try {
+      // Get node from route match or current path
+      $node = \Drupal::routeMatch()->getParameter('node');
+      if (!$node) {
+        $path_args = explode('/', \Drupal::service('path.current')->getPath());
+        if (isset($path_args[2]) && is_numeric($path_args[2])) {
+          $node = \Drupal::entityTypeManager()->getStorage('node')->load($path_args[2]);
+        }
+      }
+
+      if (!$node) {
+        return [];
+      }
+
+      $build = [];
+
+      if ($this->configuration['show_description']) {
+        $build['description'] = [
+          '#type' => 'html_tag',
+          '#tag' => 'p',
+          '#value' => $this->configuration['block_description'],
+        ];
+      }
+
+      if ($this->configuration['use_modal']) {
+        // Modal trigger button
+        $url = Url::fromRoute('page_notifications.modal_form', [
+          'entity_type' => $node->getEntityTypeId(),
+          'entity' => $node->id(),
+        ]);
+
+        $build['modal_button'] = [
+          '#type' => 'link',
+          '#title' => $this->configuration['button_text'],
+          '#url' => $url,
+          '#attributes' => [
+            'class' => array_merge(['use-ajax'], explode(' ', $this->configuration['button_classes'])),
+            'data-dialog-type' => 'modal',
+            'data-dialog-options' => json_encode([
+              'width' => 500,
+              'title' => $this->configuration['modal_title'],
+            ]),
+          ],
+        ];
+
+        // Attach required libraries
+        $build['#attached']['library'][] = 'core/drupal.dialog.ajax';
+        $build['#attached']['library'][] = 'page_notifications/modal';
+      } else {
+        $form = $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node);
+        $form['#attributes']['class'][] = $this->configuration['form_classes'];
+        $form['actions']['submit']['#value'] = $this->configuration['button_text'];
+        $form['actions']['submit']['#attributes']['class'] = explode(' ', $this->configuration['button_classes']);
+        $build['form'] = $form;
+      }
+
+       // Add cache contexts and tags
+      $build['#cache'] = [
+        'contexts' => [
+          'url.path',
+          'route',
+        ],
+        'tags' => [
+          'node:' . $node->id(),
+        ],
+        'max-age' => 0 // Disable caching for this block
+      ];
+
+      return $build;
+    }
+    catch (\Exception $e) {
+      $this->loggerFactory->get('page_notifications')->error(
+        'Error building subscription block: @message',
+        ['@message' => $e->getMessage()]
+      );
+      return [];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function blockAccess(AccountInterface $account) {
+    try {
+      $node = \Drupal::routeMatch()->getParameter('node');
+      if (!$node) {
+        return AccessResult::forbidden();
+      }
+
+      return AccessResult::allowedIfHasPermission($account, 'access content');
+    }
+    catch (\Exception $e) {
+      $this->loggerFactory->get('page_notifications')->error(
+        'Error checking block access: @message',
+        ['@message' => $e->getMessage()]
+      );
+      return AccessResult::forbidden();
+    }
+  }
+
+}
+```
+
+# src/Plugin/EntityReferenceSelection/NodeEnhancedSelection.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Plugin\EntityReferenceSelection;
+
+use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection;
+
+/**
+ * Provides enhanced node selection with additional display information.
+ *
+ * @EntityReferenceSelection(
+ *   id = "default:node_enhanced",
+ *   label = @Translation("Node enhanced selection"),
+ *   entity_types = {"node"},
+ *   group = "default",
+ *   weight = 1
+ * )
+ */
+class NodeEnhancedSelection extends NodeSelection {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
+    $target_type = $this->configuration['target_type'];
+
+    $query = $this->buildEntityQuery($match, $match_operator);
+    if ($limit > 0) {
+      $query->range(0, $limit);
+    }
+
+    $result = $query->execute();
+
+    if (empty($result)) {
+      return [];
+    }
+
+    $options = [];
+    $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result);
+
+    foreach ($entities as $entity_id => $entity) {
+      $bundle = $entity->bundle();
+      $type_label = $entity->type->entity->label();
+
+      $label = sprintf(
+        '%s (ID: %d, Type: %s)',
+        $entity->label(),
+        $entity_id,
+        $type_label
+      );
+
+      $options[$bundle][$entity_id] = $label;
+    }
+
+    return $options;
+  }
+
+}
+```
+
+# src/Plugin/EntityReferenceSelection/NodeWithSubscriptionsSelection.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Plugin\EntityReferenceSelection;
+
+use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection;
+use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
+use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides specific access control for node entities with subscriptions.
+ *
+ * @EntityReferenceSelection(
+ *   id = "default:node_with_subscriptions",
+ *   label = @Translation("Node with subscriptions selection"),
+ *   entity_types = {"node"},
+ *   group = "default",
+ *   weight = 1
+ * )
+ */
+class NodeWithSubscriptionsSelection extends NodeSelection {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
+    $query = parent::buildEntityQuery($match, $match_operator);
+
+    // Get nodes that have active subscriptions
+    $subscription_query = $this->entityTypeManager
+      ->getStorage('page_notification_subscription')
+      ->getQuery()
+      ->condition('subscribed_entity_type', 'node')
+      ->condition('status', TRUE)
+      ->accessCheck(FALSE);
+
+    $subscriptions = $subscription_query->execute();
+
+    if (!empty($subscriptions)) {
+      // Get unique node IDs from subscriptions
+      $node_ids = [];
+      $subscriptions = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->loadMultiple($subscriptions);
+
+      foreach ($subscriptions as $subscription) {
+        $node_ids[] = $subscription->getSubscribedEntityId();
+      }
+
+      // Filter query to only include nodes with subscriptions
+      $query->condition('nid', $node_ids, 'IN');
+    }
+    else {
+      // If no subscriptions exist, return no results
+      $query->condition('nid', 0);
+    }
+
+    return $query;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
+    $target_type = $this->configuration['target_type'];
+
+    $query = $this->buildEntityQuery($match, $match_operator);
+    if ($limit > 0) {
+      $query->range(0, $limit);
+    }
+
+    $result = $query->execute();
+
+    if (empty($result)) {
+      return [];
+    }
+
+    $options = [];
+    $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result);
+
+    foreach ($entities as $entity_id => $entity) {
+      $bundle = $entity->bundle();
+      $type_label = $entity->type->entity->label();
+
+      // Get subscription count for this node
+      $subscription_count = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->getQuery()
+        ->condition('subscribed_entity_id', $entity_id)
+        ->condition('subscribed_entity_type', 'node')
+        ->condition('status', TRUE)
+        ->count()
+        ->accessCheck(FALSE)
+        ->execute();
+
+      $label = sprintf(
+        '%s (ID: %d, Type: %s, Subscriptions: %d)',
+        $entity->label(),
+        $entity_id,
+        $type_label,
+        $subscription_count
+      );
+
+      $options[$bundle][$entity_id] = $label;
+    }
+
+    return $options;
+  }
+
+}
+```
+
+# src/Plugin/QueueWorker/NotificationQueue.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Plugin\QueueWorker;
+
+use Drupal\Core\Queue\QueueWorkerBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Mail\MailManagerInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Language\LanguageInterface;
+
+/**
+ * Process notification queue.
+ *
+ * @QueueWorker(
+ *   id = "page_notifications_queue",
+ *   title = @Translation("Page Notifications Queue"),
+ *   cron = {"time" = 60}
+ * )
+ */
+class NotificationQueue extends QueueWorkerBase implements ContainerFactoryPluginInterface {
+  use StringTranslationTrait;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The mail manager.
+   *
+   * @var \Drupal\Core\Mail\MailManagerInterface
+   */
+  protected $mailManager;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * Constructs a new NotificationQueue worker.
+   */
+  public function __construct(
+    array $configuration,
+    $plugin_id,
+    array $plugin_definition,
+    EntityTypeManagerInterface $entity_type_manager,
+    MailManagerInterface $mail_manager,
+    ConfigFactoryInterface $config_factory,
+    LoggerChannelFactoryInterface $logger_factory
+  ) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->entityTypeManager = $entity_type_manager;
+    $this->mailManager = $mail_manager;
+    $this->configFactory = $config_factory;
+    $this->loggerFactory = $logger_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('plugin.manager.mail'),
+      $container->get('config.factory'),
+      $container->get('logger.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processItem($data) {
+    try {
+      // Load the subscription
+      $subscription = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->load($data['subscription_id']);
+
+      if (!$subscription || !$subscription->isActive()) {
+        $this->loggerFactory->get('page_notifications')
+          ->notice('Skipping notification for inactive or deleted subscription: @id',
+            ['@id' => $data['subscription_id']]);
+        return;
+      }
+
+      // Load the entity
+      $entity = $this->entityTypeManager
+        ->getStorage($data['entity_type'])
+        ->load($data['entity_id']);
+
+      if (!$entity) {
+        $this->loggerFactory->get('page_notifications')
+          ->error('Cannot send notification: Entity not found (@type: @id)',
+            ['@type' => $data['entity_type'], '@id' => $data['entity_id']]);
+        return;
+      }
+
+      $config = $this->configFactory->get('page_notifications.settings');
+
+      // Prepare mail parameters
+      $params = [
+        'subscription' => $subscription,
+        'entity' => $entity,
+        'token' => $subscription->getToken(),
+      ];
+
+      $langcode = $subscription->getLanguageCode() ?? LanguageInterface::LANGCODE_DEFAULT;
+      $from_email = $config->get('notification_settings.from_email');
+
+      // Send the email
+      $this->mailManager->mail(
+        'page_notifications',           // module
+        'notification',                 // key
+        $subscription->getEmail(),      // to
+        $langcode,                     // language
+        $params,                       // params
+        $from_email ?: NULL            // from
+      );
+
+      // Log the attempt regardless of the result
+      $this->loggerFactory->get('page_notifications')
+        ->info('Processed notification for @email regarding @type @id', [
+          '@email' => $subscription->getEmail(),
+          '@type' => $entity->getEntityTypeId(),
+          '@id' => $entity->id(),
+        ]);
+
+    }
+    catch (\Exception $e) {
+      // Log any unexpected errors
+      $this->loggerFactory->get('page_notifications')
+        ->error('Error processing notification: @message',
+          ['@message' => $e->getMessage()]);
+    }
+  }
+}
+```
+
+# src/Service/CronManager.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Service;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Queue\QueueWorkerManagerInterface;
+use Drupal\Core\Queue\QueueFactory;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Component\Datetime\TimeInterface;
+
+/**
+ * Service for handling page notifications cron operations.
+ */
+class CronManager {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The queue factory.
+   *
+   * @var \Drupal\Core\Queue\QueueFactory
+   */
+  protected $queueFactory;
+
+  /**
+   * The queue worker manager.
+   *
+   * @var \Drupal\Core\Queue\QueueWorkerManagerInterface
+   */
+  protected $queueWorkerManager;
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * Constructs a new CronManager.
+   */
+  public function __construct(
+    EntityTypeManagerInterface $entity_type_manager,
+    ConfigFactoryInterface $config_factory,
+    QueueFactory $queue_factory,
+    QueueWorkerManagerInterface $queue_worker_manager,
+    LoggerChannelFactoryInterface $logger_factory,
+    TimeInterface $time
+  ) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->configFactory = $config_factory;
+    $this->queueFactory = $queue_factory;
+    $this->queueWorkerManager = $queue_worker_manager;
+    $this->loggerFactory = $logger_factory;
+    $this->time = $time;
+  }
+
+  /**
+   * Processes cron tasks.
+   */
+  public function processCron() {
+    $this->processQueue();
+    $this->cleanupExpiredSubscriptions();
+  }
+
+  /**
+   * Process the notification queue.
+   */
+  protected function processQueue() {
+    $queue = $this->queueFactory->get('page_notifications_queue');
+    $queue_worker = $this->queueWorkerManager->createInstance('page_notifications_queue');
+
+    $time_limit = 30;
+    $end = $this->time->getRequestTime() + $time_limit;
+    $items_processed = 0;
+
+    while ($this->time->getRequestTime() < $end && ($item = $queue->claimItem())) {
+      try {
+        $queue_worker->processItem($item->data);
+        $queue->deleteItem($item);
+        $items_processed++;
+
+        if ($items_processed >= 50) {
+          break;
+        }
+      }
+      catch (\Exception $e) {
+        $queue->releaseItem($item);
+        $this->loggerFactory->get('page_notifications')->error(
+          'Error processing notification: @message',
+          ['@message' => $e->getMessage()]
+        );
+      }
+    }
+  }
+
+  /**
+   * Clean up expired unverified subscriptions.
+   */
+  protected function cleanupExpiredSubscriptions() {
+    $config = $this->configFactory->get('page_notifications.settings');
+    $expiration_hours = $config->get('notification_settings.token_expiration');
+
+    // Only cleanup if expiration is set (greater than 0)
+    if ($expiration_hours > 0) {
+      $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
+
+      $expired_ids = $storage->getQuery()
+        ->condition('status', FALSE)
+        ->condition('created', $this->time->getRequestTime() - ($expiration_hours * 3600), '<')
+        ->accessCheck(FALSE)
+        ->execute();
+
+      if (!empty($expired_ids)) {
+        $storage->delete($storage->loadMultiple($expired_ids));
+        $this->loggerFactory->get('page_notifications')->notice(
+          'Cleaned up @count expired unverified subscriptions',
+          ['@count' => count($expired_ids)]
+        );
+      }
+    }
+  }
+
+}
+```
+
+# src/Service/MigrationService.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Service;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+
+/**
+ * Service for migrating subscriptions from Page Notifications v3 to v4.
+ */
+class MigrationService {
+  use StringTranslationTrait;
+  use DependencySerializationTrait;
+
+  /**
+   * Creates a batch for migrating subscriptions.
+   *
+   * @return array
+   *   The batch definition.
+   */
+  public static function createMigrationBatch() {
+    // Get total count of subscriptions to migrate
+    $count = \Drupal::database()->select('node', 'n')
+      ->condition('n.type', 'page_notify_subscriptions')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+
+    if (!$count) {
+      \Drupal::logger('page_notifications')->notice('No v3 subscriptions found to migrate.');
+      return NULL;
+    }
+
+    \Drupal::logger('page_notifications')->notice('Found @count v3 subscriptions to migrate.', ['@count' => $count]);
+
+    $batch = [
+      'title' => t('Migrating Page Notifications subscriptions'),
+      'init_message' => t('Starting subscription migration...'),
+      'progress_message' => t('Processed @current out of @total subscriptions.'),
+      'error_message' => t('Error occurred during migration.'),
+      'operations' => [],
+      'finished' => [static::class, 'migrationFinished'],
+    ];
+
+    // Process subscriptions in batches of 25
+    for ($i = 0; $i < $count; $i += 25) {
+      $batch['operations'][] = [
+        [static::class, 'migrateSubscriptionsBatch'],
+        [$i, min(25, $count - $i)]
+      ];
+    }
+
+    return $batch;
+  }
+
+  /**
+   * Migrates a batch of subscriptions.
+   */
+  public static function migrateSubscriptionsBatch($start, $limit, &$context) {
+    try {
+      $database = \Drupal::database();
+
+      // Query v3 subscriptions
+      $query = $database->select('node', 'n');
+      $query->join('node_field_data', 'nfd', 'n.nid = nfd.nid');
+      $query->fields('n', ['nid'])
+        ->fields('nfd', ['created'])
+        ->condition('n.type', 'page_notify_subscriptions')
+        ->range($start, $limit);
+
+      // Join with field tables
+      $query->join('node__field_page_notify_email', 'e', 'n.nid = e.entity_id');
+      $query->join('node__field_page_notify_node_id', 'nid', 'n.nid = nid.entity_id');
+
+      $query->fields('e', ['field_page_notify_email_value']);
+      $query->fields('nid', ['field_page_notify_node_id_value']);
+
+      $results = $query->execute();
+
+      $subscription_storage = \Drupal::entityTypeManager()->getStorage('page_notification_subscription');
+      $time = \Drupal::time()->getRequestTime();
+
+      foreach ($results as $row) {
+        // Generate new tokens
+        $verify_token = bin2hex(random_bytes(16));
+        $unsubscribe_token = bin2hex(random_bytes(32));
+
+        \Drupal::logger('page_notifications')->debug('Migrating subscription for email: @email, node: @nid', [
+          '@email' => $row->field_page_notify_email_value,
+          '@nid' => $row->field_page_notify_node_id_value,
+        ]);
+
+        // Create new v4 subscription entity
+        $subscription = $subscription_storage->create([
+          'email' => $row->field_page_notify_email_value,
+          'subscribed_entity_id' => $row->field_page_notify_node_id_value,
+          'subscribed_entity_type' => 'node',
+          'token' => $verify_token,
+          'unsubscribe_token' => $unsubscribe_token,
+          'status' => TRUE,
+          'created' => $row->created ?? $time,
+          'changed' => $time,
+          'langcode' => \Drupal::languageManager()->getDefaultLanguage()->getId(),
+        ]);
+
+        try {
+          $subscription->save();
+
+          // Update progress
+          if (!isset($context['results']['subscriptions'])) {
+            $context['results']['subscriptions'] = 0;
+          }
+          $context['results']['subscriptions']++;
+
+          \Drupal::logger('page_notifications')->debug('Successfully migrated subscription @id', [
+            '@id' => $subscription->id(),
+          ]);
+        }
+        catch (\Exception $e) {
+          \Drupal::logger('page_notifications')->error('Failed to save subscription: @error', [
+            '@error' => $e->getMessage(),
+          ]);
+        }
+      }
+
+      $context['message'] = t('Migrated @count subscriptions', [
+        '@count' => $limit,
+      ]);
+    }
+    catch (\Exception $e) {
+      \Drupal::logger('page_notifications')->error(
+        'Failed to migrate subscriptions batch: @message',
+        ['@message' => $e->getMessage()]
+      );
+      throw $e;
+    }
+  }
+
+  /**
+   * Batch finished callback.
+   */
+  public static function migrationFinished($success, $results, $operations) {
+    if ($success) {
+      // Verify migration
+      $old_count = \Drupal::database()->select('node', 'n')
+        ->condition('n.type', 'page_notify_subscriptions')
+        ->countQuery()
+        ->execute()
+        ->fetchField();
+
+      $new_count = \Drupal::entityTypeManager()
+        ->getStorage('page_notification_subscription')
+        ->getQuery()
+        ->accessCheck(FALSE)
+        ->count()
+        ->execute();
+
+      $message = t('Migration completed. Migrated @migrated subscriptions (@old v3 subscriptions, @new v4 subscriptions).', [
+        '@migrated' => $results['subscriptions'] ?? 0,
+        '@old' => $old_count,
+        '@new' => $new_count,
+      ]);
+
+      \Drupal::logger('page_notifications')->notice($message);
+      \Drupal::messenger()->addStatus($message);
+
+      if ($old_count != $new_count) {
+        $warning = t('Warning: Number of migrated subscriptions (@new) does not match original count (@old).', [
+          '@new' => $new_count,
+          '@old' => $old_count,
+        ]);
+        \Drupal::logger('page_notifications')->warning($warning);
+        \Drupal::messenger()->addWarning($warning);
+      }
+    }
+    else {
+      $message = t('Migration failed. Please check the logs for details.');
+      \Drupal::logger('page_notifications')->error($message);
+      \Drupal::messenger()->addError($message);
+    }
+  }
+}
+```
+
+# src/Service/NotificationManager.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Service;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Mail\MailManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\Core\Queue\QueueFactory;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Messenger\MessengerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+
+/**
+ * Service for handling page notification operations.
+ */
+class NotificationManager implements NotificationManagerInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The mail manager.
+   *
+   * @var \Drupal\Core\Mail\MailManagerInterface
+   */
+  protected $mailManager;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The queue factory.
+   *
+   * @var \Drupal\Core\Queue\QueueFactory
+   */
+  protected $queueFactory;
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * The messenger service.
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+/**
+   * Constructs a new NotificationManager.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
+   *   The mail manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Queue\QueueFactory $queue_factory
+   *   The queue factory.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger factory.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The time service.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
+   *   The string translation service.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *  The messenger service.
+   */
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    MailManagerInterface $mail_manager,
+    EntityTypeManagerInterface $entity_type_manager,
+    QueueFactory $queue_factory,
+    LoggerChannelFactoryInterface $logger_factory,
+    EventDispatcherInterface $event_dispatcher,
+    TimeInterface $time,
+    TranslationInterface $translation,
+    MessengerInterface $messenger
+  ) {
+    $this->configFactory = $config_factory;
+    $this->mailManager = $mail_manager;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->queueFactory = $queue_factory;
+    $this->loggerFactory = $logger_factory;
+    $this->eventDispatcher = $event_dispatcher;
+    $this->time = $time;
+    $this->setStringTranslation($translation);
+    $this->messenger = $messenger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createSubscription(string $email, EntityInterface $entity, ?string $langcode = null) {
+    try {
+      // Check for existing subscription
+      $existing_subscriptions = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->loadByProperties([
+          'email' => $email,
+          'subscribed_entity_id' => $entity->id(),
+          'subscribed_entity_type' => $entity->getEntityTypeId(),
+        ]);
+  
+      if (!empty($existing_subscriptions)) {
+        /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
+        $subscription = reset($existing_subscriptions);
+  
+        // Handle different subscription states
+        if ($subscription->isActive()) {
+          // Already verified subscription - send "already subscribed" email
+          $this->sendAlreadySubscribedEmail($subscription, $entity);
+          $this->messenger->addStatus($this->t('You are already subscribed to this content.'));
+          return $subscription;
+        }
+        
+        // Check if token is expired
+        if ($this->isTokenExpired($subscription)) {
+          // Generate new token and update subscription
+          $subscription->setToken($this->generateToken());
+          $subscription->setCreatedTime($this->time->getRequestTime());
+          $subscription->save();
+        }
+        
+        // Resend verification email for unverified subscriptions
+        if ($this->requiresVerification()) {
+          $this->sendVerificationEmail($subscription);
+          $this->messenger->addStatus($this->t('A new verification email has been sent to your address.'));
+        }
+        
+        return $subscription;
+      }
+  
+      // Create new subscription if none exists
+      $subscription = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->create([
+          'email' => $email,
+          'subscribed_entity_id' => $entity->id(),
+          'subscribed_entity_type' => $entity->getEntityTypeId(),
+          'token' => $this->generateToken(),
+          'unsubscribe_token' => $this->generateToken(),
+          'status' => !$this->requiresVerification(),
+        ]);
+  
+      $subscription->save();
+  
+      if ($this->requiresVerification()) {
+        $this->sendVerificationEmail($subscription);
+      }
+  
+      return $subscription;
+    }
+    catch (\Exception $e) {
+      $this->loggerFactory->get('page_notifications')
+        ->error('Failed to create subscription: @message', ['@message' => $e->getMessage()]);
+      throw $e;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function verifySubscription(string $token) {
+    try {
+      // Find subscription by token
+      $subscriptions = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->loadByProperties(['token' => $token]);
+
+      if (!empty($subscriptions)) {
+        /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
+        $subscription = reset($subscriptions);
+
+        // Get entity details before setting active
+        $entity_id = $subscription->getSubscribedEntityId();
+        $entity_type = $subscription->getSubscribedEntityType();
+
+        // Activate the subscription
+        $subscription->setActive(TRUE);
+        $subscription->save();
+
+        $this->messenger->addStatus($this->t('Thank you! Your subscription has been verified.'));
+
+        // Load the entity and get its URL
+        try {
+          $entity = $this->entityTypeManager
+            ->getStorage($entity_type)
+            ->load($entity_id);
+
+          if ($entity && $entity->hasLinkTemplate('canonical')) {
+            return new RedirectResponse($entity->toUrl()->toString());
+          }
+        }
+        catch (\Exception $e) {
+          \Drupal::logger('page_notifications')->error('Verification redirect error: @message', ['@message' => $e->getMessage()]);
+        }
+      }
+      else {
+        $this->messenger->addError($this->t('Sorry, this verification link is invalid or has expired.'));
+      }
+    }
+    catch (\Exception $e) {
+      $this->messenger->addError($this->t('An error occurred while verifying your subscription.'));
+      \Drupal::logger('page_notifications')->error('Verification error: @message', ['@message' => $e->getMessage()]);
+    }
+
+    // Fallback to homepage if anything goes wrong
+    return new RedirectResponse('/');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function notifySubscribers(EntityInterface $entity) {
+    try {
+      $subscriptions = $this->entityTypeManager
+        ->getStorage('page_notification_subscription')
+        ->loadByProperties([
+          'subscribed_entity_id' => $entity->id(),
+          'subscribed_entity_type' => $entity->getEntityTypeId(),
+          'status' => TRUE,
+        ]);
+
+      foreach ($subscriptions as $subscription) {
+        $this->queueNotification($subscription, $entity);
+      }
+    }
+    catch (\Exception $e) {
+      $this->loggerFactory->get('page_notifications')
+        ->error('Failed to notify subscribers: @message', ['@message' => $e->getMessage()]);
+      throw $e;
+    }
+  }
+
+  /**
+   * Sends an "already subscribed" email notification.
+   *
+   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
+   *   The subscription entity.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being subscribed to.
+   */
+  protected function sendAlreadySubscribedEmail($subscription, $entity) {
+    $config = $this->configFactory->get('page_notifications.settings');
+    
+    $params = [
+      'subscription' => $subscription,
+      'entity' => $entity,
+    ];
+
+    $this->mailManager->mail(
+      'page_notifications',
+      'already_subscribed',
+      $subscription->getEmail(),
+      $subscription->getLanguageCode(),
+      $params,
+      $config->get('notification_settings.from_email')
+    );
+  }
+
+  /**
+   * Retrieves the queue for processing notifications.
+   *
+   * @return \Drupal\Core\Queue\QueueInterface
+   *   The queue.
+   */
+  protected function getQueue() {
+    return $this->queueFactory->get('page_notifications_queue');
+  }
+
+  /**
+   * Queues a notification for processing.
+   *
+   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
+   *   The subscription entity.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity that was updated.
+   */
+  protected function queueNotification($subscription, EntityInterface $entity) {
+    $queue = $this->getQueue();
+    $queue->createItem([
+      'subscription_id' => $subscription->id(),
+      'entity_id' => $entity->id(),
+      'entity_type' => $entity->getEntityTypeId(),
+    ]);
+  }
+
+  /**
+   * Sends a verification email to the subscriber.
+   *
+   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
+   *   The subscription entity.
+   */
+  protected function sendVerificationEmail($subscription) {
+    $config = $this->configFactory->get('page_notifications.settings');
+    $entity = $this->entityTypeManager
+      ->getStorage($subscription->getSubscribedEntityType())
+      ->load($subscription->getSubscribedEntityId());
+
+    $params = [
+      'subscription' => $subscription,
+      'entity' => $entity,
+      'verify_url' => Url::fromRoute('page_notifications.subscription.verify', [
+        'token' => $subscription->getToken(),
+      ])->setAbsolute(TRUE)
+        ->toString(),
+    ];
+
+    $this->mailManager->mail(
+      'page_notifications',
+      'verification',
+      $subscription->getEmail(),
+      $subscription->getLanguageCode(),
+      $params,
+      $config->get('notification_settings.from_email')
+    );
+  }
+
+  /**
+   * Generates a unique token for subscription verification.
+   *
+   * @return string
+   *   The generated token.
+   */
+  protected function generateToken() {
+    return bin2hex(random_bytes(32));
+  }
+
+  /**
+   * Checks if verification is required based on configuration.
+   *
+   * @return bool
+   *   TRUE if verification is required, FALSE otherwise.
+   */
+  protected function requiresVerification() {
+    return $this->configFactory
+      ->get('page_notifications.settings')
+      ->get('security.require_verification') ?? TRUE;
+  }
+
+  /**
+   * Checks if a subscription token has expired.
+   *
+   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
+   *   The subscription entity.
+   *
+   * @return bool
+   *   TRUE if the token has expired, FALSE otherwise.
+   */
+  protected function isTokenExpired($subscription) {
+    if ($subscription->isActive()) {
+      return FALSE;
+    }
+
+    $config = $this->configFactory->get('page_notifications.settings');
+    $expiration_hours = $config->get('notification_settings.token_expiration') ?? 48;
+    $expiration_timestamp = $subscription->getCreatedTime() + ($expiration_hours * 3600);
+
+    return $this->time->getRequestTime() > $expiration_timestamp;
+  }
+
+}
+```
+
+# src/Service/NotificationManagerInterface.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Service;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Language\LanguageInterface;
+
+/**
+ * Interface for notification management service.
+ */
+interface NotificationManagerInterface {
+
+  /**
+   * Creates a new subscription.
+   *
+   * @param string $email
+   *   The subscriber's email address.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being subscribed to.
+   * @param string|null $langcode
+   *   The language code for the subscription. Defaults to site's default language.
+   *
+   * @return \Drupal\page_notifications\Entity\SubscriptionInterface
+   *   The created subscription entity.
+   *
+   * @throws \Exception
+   *   If the subscription cannot be created.
+   */
+  public function createSubscription(string $email, EntityInterface $entity, string $langcode = LanguageInterface::LANGCODE_DEFAULT);
+
+  /**
+   * Verifies a subscription using a token.
+   *
+   * @param string $token
+   *   The verification token.
+   *
+   * @return bool
+   *   TRUE if verification was successful, FALSE otherwise.
+   */
+  public function verifySubscription(string $token);
+
+  /**
+   * Notifies subscribers about updates to an entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity that was updated.
+   *
+   * @throws \Exception
+   *   If notifications cannot be sent.
+   */
+  public function notifySubscribers(EntityInterface $entity);
+
+}
+```
+
+# src/Service/SpamPrevention.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Service;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Session\SessionManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+
+/**
+ * Service for handling spam prevention in Page Notifications.
+ */
+class SpamPrevention {
+  use StringTranslationTrait;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The session manager.
+   *
+   * @var \Drupal\Core\Session\SessionManagerInterface
+   */
+  protected $sessionManager;
+
+  /**
+   * Constructs a new SpamPrevention object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Session\SessionManagerInterface $session_manager
+   *   The session manager.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   */
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    ModuleHandlerInterface $module_handler,
+    SessionManagerInterface $session_manager,
+    TranslationInterface $string_translation
+  ) {
+    $this->configFactory = $config_factory;
+    $this->moduleHandler = $module_handler;
+    $this->sessionManager = $session_manager;
+    $this->setStringTranslation($string_translation);
+  }
+
+  /**
+   * Generates a math challenge.
+   *
+   * @return array
+   *   An array containing the challenge question, numbers, operator and answer.
+   */
+  public function generateMathChallenge() {
+    $config = $this->configFactory->get('page_notifications.settings');
+    $operator = $config->get('spam_prevention.math_operator') ?? '+';
+
+    // Generate two random numbers between 1 and 10
+    $num1 = rand(1, 10);
+    $num2 = rand(1, 10);
+
+    // Calculate the answer
+    $answer = $operator === '+' ? $num1 + $num2 : $num1 * $num2;
+
+    return [
+      'num1' => $num1,
+      'num2' => $num2,
+      'operator' => $operator,
+      'question' => $this->t('What is @num1 @operator @num2?', [
+        '@num1' => $num1,
+        '@operator' => $operator,
+        '@num2' => $num2,
+      ]),
+      'answer' => $answer,
+    ];
+  }
+
+  /**
+   * Validates a math challenge response.
+   *
+   * @param int $response
+   *   The user's response to the challenge.
+   * @param array $challenge
+   *   The original challenge array containing num1, num2, and operator.
+   *
+   * @return bool
+   *   TRUE if the response is correct, FALSE otherwise.
+   */
+  public function validateMathResponse($response, array $challenge) {
+    $answer = $challenge['operator'] === '+'
+      ? $challenge['num1'] + $challenge['num2']
+      : $challenge['num1'] * $challenge['num2'];
+
+    return (int) $response === (int) $answer;
+  }
+  /**
+   * Checks if reCAPTCHA is available and configured.
+   *
+   * @return bool
+   *   TRUE if reCAPTCHA is available and configured, FALSE otherwise.
+   */
+  public function isRecaptchaAvailable() {
+    return $this->moduleHandler->moduleExists('captcha') &&
+           $this->moduleHandler->moduleExists('recaptcha') &&
+           $this->configFactory->get('recaptcha.settings')->get('site_key');
+  }
+}
+```
+
+# src/Token/SubscriptionToken.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Token;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Url;
+use Drupal\node\NodeInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Implements hook_token_info() and hook_tokens().
+ */
+class SubscriptionToken {
+  use StringTranslationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static();
+  }
+
+  /**
+   * Implements hook_token_info().
+   */
+  public function hookTokenInfo() {
+    $info['types']['subscription'] = [
+      'name' => $this->t('Subscription'),
+      'description' => $this->t('Tokens related to page notification subscriptions'),
+      'needs-data' => 'subscription',
+    ];
+
+    $info['tokens']['subscription'] = [
+      'verify-url' => [
+        'name' => $this->t('Verification URL'),
+        'description' => $this->t('The URL to verify the subscription'),
+      ],
+      'unsubscribe-url' => [
+        'name' => $this->t('Unsubscribe URL'),
+        'description' => $this->t('The URL to unsubscribe from notifications'),
+      ],
+      'email' => [
+        'name' => $this->t('Email'),
+        'description' => $this->t('The subscriber\'s email address'),
+      ],
+    ];
+
+    return $info;
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  public function hookTokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+    $replacements = [];
+
+    if (($type == 'subscription' || $type == 'page_notification_subscription') && !empty($data['subscription'])) {
+      $subscription = $data['subscription'];
+      $bubbleable_metadata->addCacheableDependency($subscription);
+
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'verify-url':
+            $replacements[$original] = Url::fromRoute('page_notifications.subscription.verify',
+              ['token' => $subscription->getToken()],
+              ['absolute' => TRUE]
+            )->toString();
+            $bubbleable_metadata->addCacheableDependency($subscription);
+            break;
+
+            case 'unsubscribe-url':
+              $replacements[$original] = Url::fromRoute('page_notifications.subscription.unsubscribe',
+                [
+                  'subscription' => $subscription->id(),
+                  'token' => $subscription->getUnsubscribeToken(),
+                ],
+                ['absolute' => TRUE]
+              )->toString();
+              $bubbleable_metadata->addCacheableDependency($subscription);
+              break;
+
+          case 'email':
+            $replacements[$original] = $subscription->getEmail();
+            $bubbleable_metadata->addCacheableDependency($subscription);
+            break;
+        }
+      }
+      return $replacements;
+    }
+
+    if ($type == 'page_notification_notification' && !empty($data['entity'])) {
+      $entity = $data['entity'];
+      $bubbleable_metadata->addCacheableDependency($entity);
+
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'notes':
+            // First check for manual notification notes
+            $notes = \Drupal::state()->get('page_notifications_manual_notes_' . $entity->id(), '');
+
+            // If no manual notes and entity is a node, try to get revision log
+            if (empty($notes) && $entity instanceof NodeInterface) {
+              $notes = $entity->getRevisionLogMessage();
+            }
+
+            $replacements[$original] = $notes;
+
+            // Clean up stored manual notes if they exist
+            if (\Drupal::state()->get('page_notifications_manual_notes_' . $entity->id())) {
+              \Drupal::state()->delete('page_notifications_manual_notes_' . $entity->id());
+            }
+            $bubbleable_metadata->addCacheableDependency($entity);
+            break;
+        }
+      }
+    }
+
+    return $replacements;
+  }
+}
+```
+
+# src/Traits/FloodControlTrait.php
+
+```php
+<?php
+
+namespace Drupal\page_notifications\Traits;
+
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides flood control functionality for forms.
+ */
+trait FloodControlTrait {
+
+  /**
+   * The flood service.
+   *
+   * @var \Drupal\Core\Flood\FloodInterface|null
+   */
+  protected ?FloodInterface $flood = NULL;
+
+  /**
+   * Sets the flood service.
+   *
+   * @param \Drupal\Core\Flood\FloodInterface $flood
+   *   The flood service.
+   */
+  public function setFloodService(FloodInterface $flood) {
+    $this->flood = $flood;
+  }
+
+  /**
+   * Gets the flood service.
+   *
+   * @return \Drupal\Core\Flood\FloodInterface
+   *   The flood service.
+   */
+  protected function getFlood(): FloodInterface {
+    if (!$this->flood) {
+      $this->flood = \Drupal::service('flood');
+    }
+    return $this->flood;
+  }
+
+  /**
+   * Gets the flood control configuration.
+   *
+   * @return array
+   *   An array containing flood control settings.
+   */
+  protected function getFloodControlConfig() {
+    $config = $this->configFactory()->get('page_notifications.settings');
+
+    return [
+      'ip_limit' => $config->get('security.flood_control.ip_limit') ?? 200,
+      'ip_window' => ($config->get('security.flood_control.ip_window') ?? 1) * 3600,
+      'identifier_limit' => $config->get('security.flood_control.identifier_limit') ?? 50,
+      'identifier_window' => ($config->get('security.flood_control.identifier_window') ?? 1) * 3600,
+    ];
+  }
+
+  /**
+ * Checks if the current request should be allowed through flood control.
+ *
+ * @param string $identifier
+ *   The identifier to check (typically an email).
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ *   The form state.
+ *
+ * @return bool
+ *   TRUE if the request should be allowed, FALSE otherwise.
+ */
+protected function checkFloodControl($identifier, FormStateInterface $form_state) {
+  $ip = \Drupal::request()->getClientIp();
+  $settings = $this->getFloodControlConfig();
+  $flood = $this->getFlood();
+
+  // Skip IP-based flood control if window is set to 0
+  if ($settings['ip_window'] > 0) {
+    $ip_event = 'page_notifications.subscribe_ip.' . $ip;
+    if (!$flood->isAllowed($ip_event, $settings['ip_limit'], $settings['ip_window'])) {
+      $form_state->setErrorByName('', $this->t('Too many subscription attempts from this IP address. Please try again in @hours hours.',
+        ['@hours' => floor($settings['ip_window'] / 3600)]
+      ));
+      $this->logSecurityEvent('flood_control_ip', ['ip' => $ip]);
+      return FALSE;
+    }
+  }
+
+  // Skip identifier-based flood control if window is set to 0
+  if ($settings['identifier_window'] > 0) {
+    $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier;
+    if (!$flood->isAllowed($identifier_event, $settings['identifier_limit'], $settings['identifier_window'])) {
+      $form_state->setErrorByName('email', $this->t('Too many subscription attempts for this email address. Please try again in @hours hours.',
+        ['@hours' => floor($settings['identifier_window'] / 3600)]
+      ));
+      $this->logSecurityEvent('flood_control_identifier', ['identifier' => $identifier]);
+      return FALSE;
+    }
+  }
+
+  return TRUE;
+}
+
+  /**
+ * Registers a flood event.
+ *
+ * @param string $identifier
+ *   The identifier for the flood event.
+ */
+protected function registerFloodControl($identifier) {
+  $ip = \Drupal::request()->getClientIp();
+  $settings = $this->getFloodControlConfig();
+  $flood = $this->getFlood();
+
+  // Only register IP-based flood events if window is greater than 0
+  if ($settings['ip_window'] > 0) {
+    $ip_event = 'page_notifications.subscribe_ip.' . $ip;
+    $flood->register($ip_event, $settings['ip_window']);
+  }
+
+  // Only register identifier-based flood events if window is greater than 0
+  if ($settings['identifier_window'] > 0) {
+    $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier;
+    $flood->register($identifier_event, $settings['identifier_window']);
+  }
+}
+
+  /**
+   * Logs a security event.
+   *
+   * @param string $type
+   *   The type of security event.
+   * @param array $data
+   *   Additional data to log.
+   */
+  protected function logSecurityEvent($type, array $data) {
+    \Drupal::logger('page_notifications_security')->warning(
+      '@type: @data',
+      ['@type' => $type, '@data' => json_encode($data)]
+    );
+  }
+}
+```
+
+# templates/block--page-notifications-subscription.html.twig
+
+```twig
+{#
+/**
+ * @file
+ * Default theme implementation to display a Page Notifications subscription block.
+ *
+ * Available variables:
+ * - plugin_id: The ID of the block implementation.
+ * - label: The configured label of the block if visible.
+ * - configuration: A list of the block's configuration values.
+ *   - block_description: The configured description text.
+ *   - button_text: The configured button text.
+ *   - button_classes: CSS classes for the button.
+ *   - form_classes: CSS classes for the form wrapper.
+ * - content: The content of the block.
+ * - attributes: HTML attributes for the block wrapper.
+ *
+ * @ingroup themeable
+ */
+#}
+{% set classes = [
+  'block',
+  'block-' ~ configuration.provider|clean_class,
+  'block-' ~ plugin_id|clean_class,
+]%}
+
+<div{{ attributes.addClass(classes) }}>
+  {{ title_prefix }}
+  {% if label %}
+    <h2{{ title_attributes }}>{{ label }}</h2>
+  {% endif %}
+  {{ title_suffix }}
+
+  {% block content %}
+    <div{{ content_attributes.addClass('content') }}>
+      {{ content }}
+    </div>
+  {% endblock %}
+</div>
+```
+
+# templates/page-notifications-email-wrapper.html.twig
+
+```twig
+{#
+/**
+ * @file
+ * Default template for Page Notifications emails.
+ *
+ * Available variables:
+ * - content: The main email content.
+ * - email_type: The type of email (verification, notification, etc.).
+ * - subscription: The subscription entity.
+ * - entity: The subscribed entity.
+ * - logo_url: The site logo URL.
+ * - site_name: The site name.
+ * - footer: Custom footer content.
+ */
+#}
+<div class="page-notifications-email">
+  {% if logo_url %}
+    <div class="email-header">
+      <img src="{{ logo_url }}" alt="{{ site_name }}" style="max-width: 200px; height: auto;">
+    </div>
+  {% endif %}
+
+  <div class="email-content">
+    {{ content }}
+  </div>
+
+  {% if footer %}
+    <div class="email-footer">
+      {{ footer }}
+    </div>
+  {% else %}
+    <div class="email-footer">
+      <p style="color: #666; font-size: 12px;">
+        © {{ "now"|date("Y") }} {{ site_name }}
+      </p>
+    </div>
+  {% endif %}
+</div>
+```
+
+# upgrade-docs.md
+
+```md
+# Upgrading from Page Notifications 3.x to 4.x
+
+## Breaking Changes
+- Complete rewrite of the module's architecture
+- New configuration system
+- New subscription storage using custom entities
+
+## Migration Process
+1. Back up your database before upgrading
+2. Install the new version over the old one
+3. Run database updates (`drush updb` or visit /update.php)
+
+## Post-Migration Steps
+After upgrading, you will need to reconfigure your Page Notifications settings:
+
+1. Visit `/admin/config/system/page-notifications`
+2. Configure the following settings:
+   - Email settings
+   - Notification templates
+   - Spam protection settings
+   - Security settings
+
+## Previous Settings
+Your previous settings from v3 will not be automatically migrated. Make note of your current settings before upgrading:
+1. Email templates
+2. From email address
+3. CAPTCHA configuration
+4. Other customizations
+
+## Subscription Data
+All subscription data (email addresses, subscribed content, tokens) will be automatically migrated to the new system.
+
+```
+
diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml
index 314cfe0..54612b6 100644
--- a/config/install/page_notifications.settings.yml
+++ b/config/install/page_notifications.settings.yml
@@ -1,8 +1,6 @@
 notification_settings:
   from_email: ''
   token_expiration: 48
-email_settings:
-  mail_format: full_html
 email_templates:
   verification_subject: 'Verify your subscription to [node:title]'
   verification_body:
diff --git a/config/schema/page_notifications.schema.yml b/config/schema/page_notifications.schema.yml
index 41ef06d..de7a70a 100644
--- a/config/schema/page_notifications.schema.yml
+++ b/config/schema/page_notifications.schema.yml
@@ -11,13 +11,6 @@ page_notifications.settings:
         token_expiration:
           type: integer
           label: 'Token expiration time in hours (0 = never expire)'
-    email_settings:
-      type: mapping
-      label: 'Email Settings'
-      mapping:
-        mail_format:
-          type: string
-          label: 'Email text format'
     email_templates:
       type: mapping
       label: 'Email Templates'
diff --git a/page_notifications.install b/page_notifications.install
index 6a23b2a..1d94860 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -23,11 +23,23 @@ function page_notifications_install() {
       break;
     }
   }
-  // Set Dynamic configuration for email format
+  // Set Dynamic configuration for verification, notification and already_subscribed formats
   $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
-  $config
-    ->set('email_settings.mail_format', $selected_format)
-    ->save();
+ // Set default body values with selected format
+ $config
+ ->set('email_templates.verification_body', [
+   'value' => $config->get('email_templates.verification_body.value'),
+   'format' => $selected_format,
+ ])
+ ->set('email_templates.notification_body', [
+   'value' => $config->get('email_templates.notification_body.value'),
+   'format' => $selected_format,
+ ])
+ ->set('email_templates.already_subscribed_body', [
+   'value' => $config->get('email_templates.already_subscribed_body.value'),
+   'format' => $selected_format,
+ ])
+ ->save();
 }
 
 /**
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index 51d3bad..2fab3c6 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -115,17 +115,7 @@ class SettingsForm extends ConfigFormBase {
       '#open' => TRUE,
     ];
 
-    // text format selection
-    $form['email_settings']['mail_format'] = [
-      '#type' => 'select',
-      '#title' => $this->t('Email Text Format'),
-      '#description' => $this->t('Select the text format to use for email content. Ensure the chosen format allows necessary HTML tags for links and formatting.'),
-      '#options' => $format_options,
-      '#default_value' => $config->get('email_settings.mail_format') ?? reset($format_options),
-      '#required' => TRUE,
-    ];
 
-    $selected_format = $config->get('email_settings.mail_format') ?? reset($format_options);
     $verification_body = $config->get('email_templates.verification_body');
     $notification_body = $config->get('email_templates.notification_body');
     $already_subscribed_body = $config->get('email_templates.already_subscribed_body');
@@ -163,7 +153,7 @@ class SettingsForm extends ConfigFormBase {
       '#type' => 'text_format',
       '#title' => $this->t('Verification Email Body'),
       '#default_value' => is_array($verification_body) ? $verification_body['value'] : $verification_body,
-      '#format' => is_array($verification_body) ? $verification_body['format'] : $selected_format,
+      '#format' => is_array($verification_body) ? $verification_body['format'] : NULL,
       '#description' => $this->t('Available tokens: [subscription:verify-url], [subscription:email], [node:title], [node:url]'),
       '#required' => TRUE,
       '#rows' => 10,
@@ -180,7 +170,7 @@ class SettingsForm extends ConfigFormBase {
       '#type' => 'text_format',
       '#title' => $this->t('Update Notification Body'),
       '#default_value' => is_array($notification_body) ? $notification_body['value'] : $notification_body,
-      '#format' => is_array($notification_body) ? $notification_body['format'] : $selected_format,
+      '#format' => is_array($notification_body) ? $notification_body['format'] : NULL,
       '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [node:changed], [subscription:unsubscribe-url]'),
       '#required' => TRUE,
       '#rows' => 10,
@@ -197,7 +187,7 @@ class SettingsForm extends ConfigFormBase {
       '#type' => 'text_format',
       '#title' => $this->t('Already Subscribed Email Body'),
       '#default_value' => is_array($already_subscribed_body) ? $already_subscribed_body['value'] : $already_subscribed_body,
-      '#format' => is_array($already_subscribed_body) ? $already_subscribed_body['format'] : $selected_format,
+      '#format' => is_array($already_subscribed_body) ? $already_subscribed_body['format'] : NULL,
       '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [subscription:unsubscribe-url]'),
       '#required' => TRUE,
       '#rows' => 10,
@@ -421,7 +411,6 @@ class SettingsForm extends ConfigFormBase {
     $this->config('page_notifications.settings')
       ->set('notification_settings.from_email', $values['from_email'])
       ->set('notification_settings.token_expiration', $values['token_expiration'])
-      ->set('email_settings.mail_format', $values['mail_format'])
       ->set('email_templates.verification_subject', $values['verification_subject'])
       ->set('email_templates.verification_body', $values['verification_body'])
       ->set('email_templates.already_subscribed_subject', $values['already_subscribed_subject'])
-- 
GitLab


From b85610ec72cd912ceb3227a44222e0ce99bc84f5 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Sun, 26 Jan 2025 10:15:36 -0500
Subject: [PATCH 40/49] Emphasize subscriber count on edit form

---
 page_notifications.module | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/page_notifications.module b/page_notifications.module
index 3eb39ff..da9fae1 100644
--- a/page_notifications.module
+++ b/page_notifications.module
@@ -50,8 +50,9 @@ function page_notifications_form_node_form_alter(&$form, FormStateInterface $for
   // Add notification checkbox to the meta header region
   $form['meta']['send_notification'] = [
     '#type' => 'checkbox',
-    '#title' => t('Send notification to subscribers (@count active subscribers)', [
+    '#title' => t('Send notification to subscribers (<strong>@count active @subscribers</strong>)', [
       '@count' => $subscriber_count,
+      '@subscribers' => $subscriber_count == 1 ? 'subscriber' : 'subscribers',
     ]),
     '#description' => $subscriber_count > 0 ?
       t('If checked, subscribers will receive an email about this update. The revision log message will be included in the notification.') :
-- 
GitLab


From 023df3e0f05079558ae72163578000c0796b993c Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Sun, 26 Jan 2025 10:55:41 -0500
Subject: [PATCH 41/49] Added customizeable success message to modal, and keep
 modal open showing success message with a dedicated close button

---
 js/modal.js                                   | 11 +++++++
 page_notifications.libraries.yml              |  4 ++-
 page_notifications.module                     |  5 ++++
 src/Form/ModalSubscriptionForm.php            | 29 +++++++++++++++----
 src/Plugin/Block/SubscriptionBlock.php        | 18 ++++++++++--
 ...page-notifications-modal-success.html.twig | 10 +++++++
 6 files changed, 69 insertions(+), 8 deletions(-)
 create mode 100644 js/modal.js
 create mode 100644 templates/page-notifications-modal-success.html.twig

diff --git a/js/modal.js b/js/modal.js
new file mode 100644
index 0000000..3c63602
--- /dev/null
+++ b/js/modal.js
@@ -0,0 +1,11 @@
+(function ($, Drupal) {
+  Drupal.behaviors.pageNotificationsModal = {
+    attach: function (context, settings) {
+      once('pageNotificationsModal', '[data-drupal-selector="modal-close"]', context).forEach(function (button) {
+        $(button).on('click', function () {
+          $(this).closest('.ui-dialog-content').dialog('close');
+        });
+      });
+    }
+  };
+})(jQuery, Drupal);
\ No newline at end of file
diff --git a/page_notifications.libraries.yml b/page_notifications.libraries.yml
index cdd66bd..fdcbb3e 100644
--- a/page_notifications.libraries.yml
+++ b/page_notifications.libraries.yml
@@ -1,6 +1,8 @@
 modal:
   version: 1.x
+  js:
+    js/modal.js: {}
   dependencies:
     - core/drupal.dialog.ajax
-    - core/drupal.ajax
+    - core/jquery
     - core/once
\ No newline at end of file
diff --git a/page_notifications.module b/page_notifications.module
index da9fae1..5053e50 100644
--- a/page_notifications.module
+++ b/page_notifications.module
@@ -111,6 +111,11 @@ function page_notifications_theme() {
       ],
       'template' => 'page-notifications-email-wrapper',
     ],
+    'page_notifications_modal_success' => [
+      'variables' => [
+        'message' => NULL,
+      ],
+    ],
   ];
 }
 
diff --git a/src/Form/ModalSubscriptionForm.php b/src/Form/ModalSubscriptionForm.php
index 52f11b7..853c466 100644
--- a/src/Form/ModalSubscriptionForm.php
+++ b/src/Form/ModalSubscriptionForm.php
@@ -111,6 +111,14 @@ class ModalSubscriptionForm extends FormBase {
       '#description' => $this->t('Enter your email address to receive notifications when this content is updated.'),
     ];
 
+    // Add success message to form state if provided
+    if ($form_state->get('success_message')) {
+      return [
+        '#theme' => 'page_notifications_modal_success',
+        '#message' => $form_state->get('success_message'),
+      ];
+    }
+
     // Add spam prevention based on configuration
     $config = $this->configFactory->get('page_notifications.settings');
     $captcha_type = $config->get('spam_prevention.captcha_type');
@@ -198,11 +206,22 @@ class ModalSubscriptionForm extends FormBase {
       $email = $form_state->getValue('email');
       $subscription = $this->notificationManager->createSubscription($email, $entity);
 
-      $response->addCommand(new CloseModalDialogCommand());
-      $response->addCommand(new MessageCommand(
-        $this->t('Thank you for subscribing. Please check your email to confirm your subscription.'),
-        NULL,
-        ['type' => 'status']
+      // Get success message from block configuration
+      $block_config = $form_state->get('block_configuration');
+      $success_message = $block_config['success_message'] ?? $this->t('Thank you for subscribing. Please check your email to confirm your subscription.');
+
+      $form_state->set('success_message', $success_message);
+      $success_content = [
+        '#theme' => 'page_notifications_modal_success',
+        '#message' => $success_message,
+        '#attached' => [
+          'library' => ['page_notifications/modal']
+        ]
+      ];
+
+      $response->addCommand(new ReplaceCommand(
+        '#modal-subscription-form-wrapper',
+        $success_content
       ));
     }
     catch (\Exception $e) {
diff --git a/src/Plugin/Block/SubscriptionBlock.php b/src/Plugin/Block/SubscriptionBlock.php
index 03c1ad3..e5620e0 100644
--- a/src/Plugin/Block/SubscriptionBlock.php
+++ b/src/Plugin/Block/SubscriptionBlock.php
@@ -101,6 +101,7 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
       'show_description' => TRUE,
       'use_modal' => FALSE,
       'modal_title' => $this->t('Subscribe to Updates'),
+      'success_message' => $this->t('Thank you for subscribing. Please check your email to confirm your subscription.'),
     ] + parent::defaultConfiguration();
   }
 
@@ -160,6 +161,13 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
         ],
       ],
     ];
+    $form['appearance']['success_message'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Success Message'),
+      '#description' => $this->t('Message shown after successful subscription.'),
+      '#default_value' => $config['success_message'],
+      '#rows' => 2,
+    ];
 
     $form['styling'] = [
       '#type' => 'details',
@@ -195,6 +203,7 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
     $this->configuration['show_description'] = $form_state->getValue(['appearance', 'show_description']);
     $this->configuration['use_modal'] = $form_state->getValue(['appearance', 'use_modal']);
     $this->configuration['modal_title'] = $form_state->getValue(['appearance', 'modal_title']);
+    $this->configuration['success_message'] = $form_state->getValue(['appearance', 'success_message']);
   }
 
   /**
@@ -235,19 +244,24 @@ class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInter
         $build['modal_button'] = [
           '#type' => 'link',
           '#title' => $this->configuration['button_text'],
-          '#url' => $url,
+          '#url' => Url::fromRoute('page_notifications.modal_form', [
+            'entity_type' => $node->getEntityTypeId(),
+            'entity' => $node->id(),
+          ]),
           '#attributes' => [
             'class' => array_merge(['use-ajax'], explode(' ', $this->configuration['button_classes'])),
             'data-dialog-type' => 'modal',
             'data-dialog-options' => json_encode([
               'width' => 500,
               'title' => $this->configuration['modal_title'],
+              'block_configuration' => [
+                'success_message' => $this->configuration['success_message'],
+              ],
             ]),
           ],
         ];
 
         // Attach required libraries
-        $build['#attached']['library'][] = 'core/drupal.dialog.ajax';
         $build['#attached']['library'][] = 'page_notifications/modal';
       } else {
         $form = $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node);
diff --git a/templates/page-notifications-modal-success.html.twig b/templates/page-notifications-modal-success.html.twig
new file mode 100644
index 0000000..dc0ec98
--- /dev/null
+++ b/templates/page-notifications-modal-success.html.twig
@@ -0,0 +1,10 @@
+<div class="subscription-success">
+  <div class="success-message">
+    {{ message }}
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="button" data-drupal-selector="modal-close">
+      {{ 'Close'|t }}
+    </button>
+  </div>
+</div>
\ No newline at end of file
-- 
GitLab


From e1cb632180ea64ee1ad0aaf66000c72a22097de9 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Mon, 27 Jan 2025 14:26:31 -0500
Subject: [PATCH 42/49] On fresh install set this module to use Symphony Mailer
 Lite even if default is not configured for HTML emails. And remove on
 uninstall

---
 page_notifications.install | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/page_notifications.install b/page_notifications.install
index 1d94860..12abcbe 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -40,6 +40,23 @@ function page_notifications_install() {
    'format' => $selected_format,
  ])
  ->save();
+
+
+ try {
+  // Set mail system handler for page notifications
+  $settings = \Drupal::configFactory()->getEditable('mailsystem.settings');
+  $settings->set('modules.page_notifications.none', [
+    'formatter' => 'symfony_mailer_lite',
+    'sender' => 'symfony_mailer_lite',
+  ])->save();
+
+  \Drupal::logger('page_notifications')->notice('Successfully configured mail handler for Page Notifications.');
+}
+catch (\Exception $e) {
+  \Drupal::logger('page_notifications')->error('Failed to configure mail handler: @message', [
+    '@message' => $e->getMessage()
+  ]);
+}
 }
 
 /**
@@ -62,8 +79,11 @@ function page_notifications_uninstall() {
   $config_names = [
     'page_notifications.settings',
     'views.view.page_notification_subscriptions',
-    'views.view.top_subscribed_content'
+    'views.view.top_subscribed_content',
   ];
+  // remove mail system handler for page notifications
+  $settings = \Drupal::configFactory()->getEditable('mailsystem.settings');
+  $settings->clear('modules.page_notifications.none')->save();
 
   foreach ($config_names as $config_name) {
     $config_factory->getEditable($config_name)->delete();
-- 
GitLab


From 5c9cd0fae19dac5dd881887ec9b9a111ee0732bc Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Mon, 27 Jan 2025 14:30:56 -0500
Subject: [PATCH 43/49] And set the mail handler when upgrading from v3

---
 page_notifications.install | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/page_notifications.install b/page_notifications.install
index 12abcbe..b0b14cb 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -118,6 +118,13 @@ function page_notifications_update_10001(&$sandbox) {
     'page_notifications.settings',
   ];
 
+  // Set mail system handler for page notifications
+  $settings = \Drupal::configFactory()->getEditable('mailsystem.settings');
+  $settings->set('modules.page_notifications.none', [
+    'formatter' => 'symfony_mailer_lite',
+    'sender' => 'symfony_mailer_lite',
+  ])->save();
+
   foreach ($configs as $config_name) {
     $config_record = $source->read($config_name);
     if (is_array($config_record)) {
-- 
GitLab


From e5436d69773e7d9f22f48707e683331ddd652475 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Mon, 27 Jan 2025 14:51:47 -0500
Subject: [PATCH 44/49] Fix token name after testing different sites

---
 src/Token/SubscriptionToken.php | 22 ++++++++++++++++------
 1 file changed, 16 insertions(+), 6 deletions(-)

diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php
index 495b0f1..e59cac0 100644
--- a/src/Token/SubscriptionToken.php
+++ b/src/Token/SubscriptionToken.php
@@ -26,13 +26,15 @@ class SubscriptionToken {
    * Implements hook_token_info().
    */
   public function hookTokenInfo() {
-    $info['types']['subscription'] = [
-      'name' => $this->t('Subscription'),
+    $type = 'page_notification_subscription';
+
+    $info['types'][$type] = [
+      'name' => $this->t('Page Notification Subscription'),
       'description' => $this->t('Tokens related to page notification subscriptions'),
-      'needs-data' => 'subscription',
+      'needs-data' => $type,
     ];
 
-    $info['tokens']['subscription'] = [
+    $info['tokens'][$type] = [
       'verify-url' => [
         'name' => $this->t('Verification URL'),
         'description' => $this->t('The URL to verify the subscription'),
@@ -47,6 +49,14 @@ class SubscriptionToken {
       ],
     ];
 
+    // For backwards compatibility, add alias
+    $info['types']['subscription'] = [
+      'name' => $this->t('Subscription (Legacy)'),
+      'description' => $this->t('Legacy tokens for page notification subscriptions. Use page_notification_subscription instead.'),
+      'needs-data' => $type,
+    ];
+    $info['tokens']['subscription'] = $info['tokens'][$type];
+
     return $info;
   }
 
@@ -56,8 +66,8 @@ class SubscriptionToken {
   public function hookTokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
     $replacements = [];
 
-    if (($type == 'subscription' || $type == 'page_notification_subscription') && !empty($data['subscription'])) {
-      $subscription = $data['subscription'];
+    if (($type == 'page_notification_subscription') && !empty($data['page_notification_subscription'])) {
+      $subscription = $data['page_notification_subscription'];
       $bubbleable_metadata->addCacheableDependency($subscription);
 
       foreach ($tokens as $name => $original) {
-- 
GitLab


From b3a5b3cb7e60ef459542c008cc64f0344937b4a6 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Mon, 27 Jan 2025 14:56:35 -0500
Subject: [PATCH 45/49] Shorten name of tab

---
 page_notifications.links.task.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/page_notifications.links.task.yml b/page_notifications.links.task.yml
index 2f87446..c2db43a 100644
--- a/page_notifications.links.task.yml
+++ b/page_notifications.links.task.yml
@@ -24,6 +24,6 @@ page_notifications.subscription_migrate:
 
 page_notifications.top_subscribed:
   route_name: page_notifications.top_subscribed
-  title: 'Top Subscribed Content'
+  title: 'Top Subscribed'
   base_route: system.admin_content  # This connects it to the admin content page
   weight: 5
\ No newline at end of file
-- 
GitLab


From 3a98207d28e622a0a2a290f9b9af3e0889c8a1d2 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Mon, 27 Jan 2025 15:09:58 -0500
Subject: [PATCH 46/49] Remove symphony mailer lite and try to just test with
 multiple mail handlers like sendgrid, Mimemail etc.

---
 codebase.md                               | 7034 ---------------------
 composer.json                             |    3 +-
 page_notifications.info.yml               |    1 -
 page_notifications.install                |   23 -
 page_notifications.services.yml           |    1 -
 src/Mail/PageNotificationsMailHandler.php |   17 +-
 6 files changed, 15 insertions(+), 7064 deletions(-)
 delete mode 100644 codebase.md

diff --git a/codebase.md b/codebase.md
deleted file mode 100644
index d1d5035..0000000
--- a/codebase.md
+++ /dev/null
@@ -1,7034 +0,0 @@
-# composer.json
-
-```json
-{
-  "name": "drupal/page_notifications",
-  "description": "Enables anonymous and authenticated users to subscribe to content updates and receive email notifications when changes occur.",
-  "type": "drupal-module",
-  "license": "GPL-2.0-or-later",
-  "homepage": "https://drupal.org/project/page_notifications",
-  "authors": [
-    {
-      "name": "Lidiya Grushetska",
-      "homepage": "https://www.drupal.org/u/lidia_ua",
-      "role": "Maintainer"
-    }
-  ],
-  "support": {
-    "issues": "https://drupal.org/project/issues/page_notifications",
-    "source": "https://git.drupalcode.org/project/page_notifications"
-  },
-  "require": {
-    "php": ">=8.1",
-    "drupal/core": "^10",
-    "drupal/symfony_mailer_lite": "^2.0"
-  },
-  "suggest": {
-    "drupal/captcha": "Provides additional spam protection options including reCAPTCHA integration"
-  },
-  "extra": {
-    "drush": {
-      "services": {
-        "drush.services.yml": "^10"
-      }
-    }
-  },
-  "minimum-stability": "dev",
-  "prefer-stable": true,
-  "config": {
-    "sort-packages": true
-  }
-}
-```
-
-# config/install/page_notifications.settings.yml
-
-```yml
-notification_settings:
-  from_email: ''
-  token_expiration: 48
-email_settings:
-  mail_format: full_html
-email_templates:
-  verification_subject: 'Verify your subscription to [node:title]'
-  verification_body:
-    value: '<p>Hello,</p><p>Please verify your email subscription to the page "<strong>[node:title]</strong>".</p><p>Click the following link to confirm your subscription:<br><a href="[subscription:verify-url]">[subscription:verify-url]</a></p><p><em>This verification link will expire soon.<br>Please verify your subscription promptly.</em></p><p>If you did not request this subscription, please ignore this email.</p>'
-    format: full_html
-  notification_subject: '[node:title] has been updated'
-  notification_body:
-    value: '<p>Dear subscriber,</p><p>The page "<strong>[node:title]</strong>" that you are subscribed to has been updated.</p><p>[notification:notes]</p><p>You can view the updated page here:<br><a href="[node:url]">[node:url]</a></p><p>To unsubscribe from these notifications, click here:<br><a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p><p>Regards,<br>[site:name] team</p>'
-    format: full_html
-  already_subscribed_subject: 'You are already subscribed to [node:title]'
-  already_subscribed_body:
-    value: '<p>Dear subscriber,</p><p>You are already subscribed to "<strong>[node:title]</strong>" and ready for future notifications.</p><p>You can view the content here:<br><a href="[node:url]">[node:url]</a></p><p>If you wish to unsubscribe, you can do so here:<br><a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p><p>Regards,<br>[site:name] team</p>'
-    format: full_html
-security:
-  require_verification: true
-  flood_control:
-    ip_limit: 200
-    ip_window: 1
-    identifier_limit: 50
-    identifier_window: 1
-spam_protection:
-  enable_modal: false
-  enable_math_captcha: true
-  math_captcha_operator: +
-  captcha_point: null
-spam_prevention:
-  captcha_type: none
-  math_operator: +
-  use_recaptcha: false
-```
-
-# config/install/views.view.page_notification_subscriptions.yml
-
-```yml
-langcode: en
-status: true
-dependencies:
-  module:
-    - node
-    - page_notifications
-id: page_notification_subscriptions
-label: 'Page Notification Subscriptions'
-module: views
-description: 'Lists all page notification subscriptions'
-tag: ''
-base_table: page_notification_subscription
-base_field: id
-display:
-  default:
-    id: default
-    display_title: Default
-    display_plugin: default
-    position: 0
-    display_options:
-      title: 'Page Notification Subscriptions'
-      fields:
-        operations:
-          id: operations
-          table: page_notification_subscription
-          field: operations
-          relationship: none
-          group_type: group
-          admin_label: ''
-          entity_type: null
-          entity_field: null
-          plugin_id: entity_operations
-          label: Operations
-          exclude: false
-          alter:
-            alter_text: false
-            text: ''
-            make_link: false
-            path: ''
-            absolute: false
-            external: false
-            replace_spaces: false
-            path_case: none
-            trim_whitespace: false
-            alt: ''
-            rel: ''
-            link_class: ''
-            prefix: ''
-            suffix: ''
-            target: ''
-            nl2br: false
-            max_length: 0
-            word_boundary: true
-            ellipsis: true
-            more_link: false
-            more_link_text: ''
-            more_link_path: ''
-            strip_tags: false
-            trim: false
-            preserve_tags: ''
-            html: false
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: true
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: true
-          empty: ''
-          hide_empty: false
-          empty_zero: false
-          hide_alter_empty: true
-          destination: false
-        email:
-          id: email
-          table: page_notification_subscription
-          field: email
-          relationship: none
-          group_type: group
-          admin_label: ''
-          entity_type: page_notification_subscription
-          entity_field: email
-          plugin_id: standard
-          label: Email
-          exclude: false
-          alter:
-            alter_text: false
-            text: ''
-            make_link: false
-            path: ''
-            absolute: false
-            external: false
-            replace_spaces: false
-            path_case: none
-            trim_whitespace: false
-            alt: ''
-            rel: ''
-            link_class: ''
-            prefix: ''
-            suffix: ''
-            target: ''
-            nl2br: false
-            max_length: 0
-            word_boundary: true
-            ellipsis: true
-            more_link: false
-            more_link_text: ''
-            more_link_path: ''
-            strip_tags: false
-            trim: false
-            preserve_tags: ''
-            html: false
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: true
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: true
-          empty: ''
-          hide_empty: false
-          empty_zero: false
-          hide_alter_empty: true
-        id:
-          id: id
-          table: page_notification_subscription
-          field: id
-          relationship: none
-          group_type: group
-          admin_label: ''
-          entity_type: page_notification_subscription
-          entity_field: id
-          plugin_id: field
-          label: ID
-          exclude: false
-          alter:
-            alter_text: false
-            text: ''
-            make_link: false
-            path: ''
-            absolute: false
-            external: false
-            replace_spaces: false
-            path_case: none
-            trim_whitespace: false
-            alt: ''
-            rel: ''
-            link_class: ''
-            prefix: ''
-            suffix: ''
-            target: ''
-            nl2br: false
-            max_length: 0
-            word_boundary: true
-            ellipsis: true
-            more_link: false
-            more_link_text: ''
-            more_link_path: ''
-            strip_tags: false
-            trim: false
-            preserve_tags: ''
-            html: false
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: true
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: true
-          empty: ''
-          hide_empty: false
-          empty_zero: false
-          hide_alter_empty: true
-          click_sort_column: value
-          type: number_integer
-          settings:
-            thousand_separator: ''
-            prefix_suffix: true
-          group_column: value
-          group_columns: {  }
-          group_rows: true
-          delta_limit: 0
-          delta_offset: 0
-          delta_reversed: false
-          delta_first_last: false
-          multi_type: separator
-          separator: ', '
-          field_api_classes: false
-        status:
-          id: status
-          table: page_notification_subscription
-          field: status
-          relationship: none
-          group_type: group
-          admin_label: ''
-          entity_type: page_notification_subscription
-          entity_field: status
-          plugin_id: boolean
-          label: Status
-          exclude: false
-          alter:
-            alter_text: false
-            text: ''
-            make_link: false
-            path: ''
-            absolute: false
-            external: false
-            replace_spaces: false
-            path_case: none
-            trim_whitespace: false
-            alt: ''
-            rel: ''
-            link_class: ''
-            prefix: ''
-            suffix: ''
-            target: ''
-            nl2br: false
-            max_length: 0
-            word_boundary: true
-            ellipsis: true
-            more_link: false
-            more_link_text: ''
-            more_link_path: ''
-            strip_tags: false
-            trim: false
-            preserve_tags: ''
-            html: false
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: true
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: true
-          empty: ''
-          hide_empty: false
-          empty_zero: false
-          hide_alter_empty: true
-          type: yes-no
-          type_custom_true: ''
-          type_custom_false: ''
-          not: false
-        subscribed_entity_id:
-          id: subscribed_entity_id
-          table: page_notification_subscription
-          field: subscribed_entity_id
-          relationship: none
-          group_type: group
-          admin_label: ''
-          entity_type: page_notification_subscription
-          plugin_id: numeric
-          label: 'Subscribed Entity ID'
-          exclude: false
-          alter:
-            alter_text: false
-            text: ''
-            make_link: false
-            path: ''
-            absolute: false
-            external: false
-            replace_spaces: false
-            path_case: none
-            trim_whitespace: false
-            alt: ''
-            rel: ''
-            link_class: ''
-            prefix: ''
-            suffix: ''
-            target: ''
-            nl2br: false
-            max_length: 0
-            word_boundary: true
-            ellipsis: true
-            more_link: false
-            more_link_text: ''
-            more_link_path: ''
-            strip_tags: false
-            trim: false
-            preserve_tags: ''
-            html: false
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: true
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: true
-          empty: ''
-          hide_empty: false
-          empty_zero: false
-          hide_alter_empty: true
-          set_precision: false
-          precision: 0
-          decimal: .
-          separator: ''
-          format_plural: false
-          format_plural_string: !!binary MQNAY291bnQ=
-          prefix: ''
-          suffix: ''
-        title:
-          id: title
-          table: node_field_data
-          field: title
-          relationship: subscribed_entity
-          group_type: group
-          admin_label: ''
-          entity_type: node
-          entity_field: title
-          plugin_id: field
-          label: Title
-          exclude: false
-          alter:
-            alter_text: false
-            text: ''
-            make_link: false
-            path: ''
-            absolute: false
-            external: false
-            replace_spaces: false
-            path_case: none
-            trim_whitespace: false
-            alt: ''
-            rel: ''
-            link_class: ''
-            prefix: ''
-            suffix: ''
-            target: ''
-            nl2br: false
-            max_length: 0
-            word_boundary: true
-            ellipsis: true
-            more_link: false
-            more_link_text: ''
-            more_link_path: ''
-            strip_tags: false
-            trim: false
-            preserve_tags: ''
-            html: false
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: true
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: true
-          empty: ''
-          hide_empty: false
-          empty_zero: false
-          hide_alter_empty: true
-          click_sort_column: value
-          type: string
-          settings:
-            link_to_entity: true
-          group_column: value
-          group_columns: {  }
-          group_rows: true
-          delta_limit: 0
-          delta_offset: 0
-          delta_reversed: false
-          delta_first_last: false
-          multi_type: separator
-          separator: ', '
-          field_api_classes: false
-        created:
-          id: created
-          table: page_notification_subscription
-          field: created
-          relationship: none
-          group_type: group
-          admin_label: ''
-          entity_type: page_notification_subscription
-          entity_field: created
-          plugin_id: date
-          label: Created
-          exclude: false
-          alter:
-            alter_text: false
-            text: ''
-            make_link: false
-            path: ''
-            absolute: false
-            external: false
-            replace_spaces: false
-            path_case: none
-            trim_whitespace: false
-            alt: ''
-            rel: ''
-            link_class: ''
-            prefix: ''
-            suffix: ''
-            target: ''
-            nl2br: false
-            max_length: 0
-            word_boundary: true
-            ellipsis: true
-            more_link: false
-            more_link_text: ''
-            more_link_path: ''
-            strip_tags: false
-            trim: false
-            preserve_tags: ''
-            html: false
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: true
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: true
-          empty: ''
-          hide_empty: false
-          empty_zero: false
-          hide_alter_empty: true
-          date_format: 'raw time ago'
-          custom_date_format: ''
-          timezone: ''
-      pager:
-        type: mini
-        options:
-          offset: 0
-          pagination_heading_level: h4
-          items_per_page: 10
-          total_pages: null
-          id: 0
-          tags:
-            next: ››
-            previous: ‹‹
-          expose:
-            items_per_page: false
-            items_per_page_label: 'Items per page'
-            items_per_page_options: '5, 10, 25, 50'
-            items_per_page_options_all: false
-            items_per_page_options_all_label: '- All -'
-            offset: false
-            offset_label: Offset
-      exposed_form:
-        type: basic
-        options:
-          submit_button: Apply
-          reset_button: false
-          reset_button_label: Reset
-          exposed_sorts_label: 'Sort by'
-          expose_sort_order: true
-          sort_asc_label: Asc
-          sort_desc_label: Desc
-      access:
-        type: none
-        options: {  }
-      cache:
-        type: tag
-        options: {  }
-      empty: {  }
-      sorts: {  }
-      arguments: {  }
-      filters: {  }
-      style:
-        type: table
-      row:
-        type: fields
-      query:
-        type: views_query
-        options:
-          query_comment: ''
-          disable_sql_rewrite: false
-          distinct: false
-          replica: false
-          query_tags: {  }
-      relationships:
-        subscribed_entity:
-          id: subscribed_entity
-          table: page_notification_subscription
-          field: subscribed_entity
-          relationship: none
-          group_type: group
-          admin_label: 'Subscribed Node'
-          entity_type: page_notification_subscription
-          plugin_id: standard
-          required: false
-      header: {  }
-      footer: {  }
-      display_extenders: {  }
-    cache_metadata:
-      max-age: -1
-      contexts:
-        - 'languages:language_content'
-        - 'languages:language_interface'
-        - url.query_args
-      tags: {  }
-  page_1:
-    id: page_1
-    display_title: Page
-    display_plugin: page
-    position: 1
-    display_options:
-      display_extenders:
-        simple_sitemap_display_extender:
-          variants: {  }
-      path: admin/content/subscriptions
-    cache_metadata:
-      max-age: -1
-      contexts:
-        - 'languages:language_content'
-        - 'languages:language_interface'
-        - url.query_args
-      tags: {  }
-
-```
-
-# config/install/views.view.top_subscribed_content.yml
-
-```yml
-langcode: en
-status: true
-dependencies:
-  module:
-    - node
-    - page_notifications
-id: top_subscribed_content
-label: 'Top Subscribed Content'
-module: views
-description: ''
-tag: ''
-base_table: page_notification_subscription
-base_field: id
-display:
-  default:
-    id: default
-    display_title: Default
-    display_plugin: default
-    position: 0
-    display_options:
-      title: 'Top Subscribed Content'
-      fields:
-        title:
-          id: title
-          table: node_field_data
-          field: title
-          relationship: subscribed_entity
-          group_type: group
-          admin_label: ''
-          entity_type: node
-          entity_field: title
-          plugin_id: field
-          label: Title
-          exclude: false
-          alter:
-            alter_text: false
-            text: ''
-            make_link: false
-            path: ''
-            absolute: false
-            external: false
-            replace_spaces: false
-            path_case: none
-            trim_whitespace: false
-            alt: ''
-            rel: ''
-            link_class: ''
-            prefix: ''
-            suffix: ''
-            target: ''
-            nl2br: false
-            max_length: 0
-            word_boundary: true
-            ellipsis: true
-            more_link: false
-            more_link_text: ''
-            more_link_path: ''
-            strip_tags: false
-            trim: false
-            preserve_tags: ''
-            html: false
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: true
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: true
-          empty: ''
-          hide_empty: false
-          empty_zero: false
-          hide_alter_empty: true
-          click_sort_column: value
-          type: string
-          settings:
-            link_to_entity: true
-          group_column: value
-          group_columns: {  }
-          group_rows: true
-          delta_limit: 0
-          delta_offset: 0
-          delta_reversed: false
-          delta_first_last: false
-          multi_type: separator
-          separator: ', '
-          field_api_classes: false
-        id:
-          id: id
-          table: page_notification_subscription
-          field: id
-          relationship: none
-          group_type: count
-          admin_label: ''
-          entity_type: page_notification_subscription
-          entity_field: id
-          plugin_id: field
-          label: 'Subscription Count'
-          exclude: false
-          alter:
-            alter_text: false
-            text: ''
-            make_link: false
-            path: ''
-            absolute: false
-            external: false
-            replace_spaces: false
-            path_case: none
-            trim_whitespace: false
-            alt: ''
-            rel: ''
-            link_class: ''
-            prefix: ''
-            suffix: ''
-            target: ''
-            nl2br: false
-            max_length: 0
-            word_boundary: true
-            ellipsis: true
-            more_link: false
-            more_link_text: ''
-            more_link_path: ''
-            strip_tags: false
-            trim: false
-            preserve_tags: ''
-            html: false
-          element_type: ''
-          element_class: ''
-          element_label_type: ''
-          element_label_class: ''
-          element_label_colon: true
-          element_wrapper_type: ''
-          element_wrapper_class: ''
-          element_default_classes: true
-          empty: ''
-          hide_empty: false
-          empty_zero: false
-          hide_alter_empty: true
-          click_sort_column: value
-          type: number_integer
-          settings: {  }
-          group_column: value
-          group_columns: {  }
-          group_rows: true
-          delta_limit: 0
-          delta_offset: 0
-          delta_reversed: false
-          delta_first_last: false
-          multi_type: separator
-          separator: ', '
-          field_api_classes: false
-          set_precision: false
-          precision: 0
-          decimal: .
-          format_plural: 0
-          format_plural_string: !!binary MQNAY291bnQ=
-          prefix: ''
-          suffix: ''
-      pager:
-        type: mini
-        options:
-          offset: 0
-          pagination_heading_level: h4
-          items_per_page: 10
-          total_pages: null
-          id: 0
-          tags:
-            next: ››
-            previous: ‹‹
-          expose:
-            items_per_page: false
-            items_per_page_label: 'Items per page'
-            items_per_page_options: '5, 10, 25, 50'
-            items_per_page_options_all: false
-            items_per_page_options_all_label: '- All -'
-            offset: false
-            offset_label: Offset
-      exposed_form:
-        type: basic
-        options:
-          submit_button: Apply
-          reset_button: false
-          reset_button_label: Reset
-          exposed_sorts_label: 'Sort by'
-          expose_sort_order: true
-          sort_asc_label: Asc
-          sort_desc_label: Desc
-      access:
-        type: none
-        options: {  }
-      cache:
-        type: tag
-        options: {  }
-      empty: {  }
-      sorts: {  }
-      arguments: {  }
-      filters:
-        status:
-          id: status
-          table: page_notification_subscription
-          field: status
-          relationship: none
-          group_type: group
-          admin_label: ''
-          entity_type: page_notification_subscription
-          entity_field: status
-          plugin_id: boolean
-          operator: '='
-          value: '1'
-          group: 1
-          exposed: false
-          expose:
-            operator_id: ''
-            label: ''
-            description: ''
-            use_operator: false
-            operator: ''
-            operator_limit_selection: false
-            operator_list: {  }
-            identifier: ''
-            required: false
-            remember: false
-            multiple: false
-            remember_roles:
-              authenticated: authenticated
-          is_grouped: false
-          group_info:
-            label: ''
-            description: ''
-            identifier: ''
-            optional: true
-            widget: select
-            multiple: false
-            remember: false
-            default_group: All
-            default_group_multiple: {  }
-            group_items: {  }
-      style:
-        type: table
-        options:
-          grouping: {  }
-          row_class: ''
-          default_row_class: true
-          columns:
-            title: title
-            id: id
-            title_1: title_1
-          default: id
-          info:
-            title:
-              sortable: true
-              default_sort_order: asc
-              align: ''
-              separator: ''
-              empty_column: false
-              responsive: ''
-            id:
-              sortable: true
-              default_sort_order: desc
-              align: ''
-              separator: ''
-              empty_column: false
-              responsive: ''
-            title_1:
-              sortable: false
-              default_sort_order: asc
-              align: ''
-              separator: ''
-              empty_column: false
-              responsive: ''
-          override: true
-          sticky: false
-          summary: ''
-          empty_table: false
-          caption: ''
-          description: ''
-      row:
-        type: fields
-      query:
-        type: views_query
-        options:
-          query_comment: ''
-          disable_sql_rewrite: false
-          distinct: false
-          replica: false
-          query_tags: {  }
-      relationships:
-        subscribed_entity:
-          id: subscribed_entity
-          table: page_notification_subscription
-          field: subscribed_entity
-          relationship: none
-          group_type: group
-          admin_label: 'Subscribed Node'
-          entity_type: page_notification_subscription
-          plugin_id: standard
-          required: true
-      group_by: true
-      header: {  }
-      footer: {  }
-      display_extenders:
-        simple_sitemap_display_extender: {  }
-    cache_metadata:
-      max-age: -1
-      contexts:
-        - 'languages:language_content'
-        - 'languages:language_interface'
-        - url.query_args
-      tags: {  }
-  page_1:
-    id: page_1
-    display_title: Page
-    display_plugin: page
-    position: 1
-    display_options:
-      display_extenders:
-        simple_sitemap_display_extender:
-          variants: {  }
-      path: admin/content/top-subscribed
-    cache_metadata:
-      max-age: -1
-      contexts:
-        - 'languages:language_content'
-        - 'languages:language_interface'
-        - url.query_args
-      tags: {  }
-
-```
-
-# config/schema/page_notifications.schema.yml
-
-```yml
-page_notifications.settings:
-  type: config_object
-  label: 'Page Notifications settings'
-  mapping:
-    notification_settings:
-      type: mapping
-      mapping:
-        from_email:
-          type: string
-          label: 'From email address'
-        token_expiration:
-          type: integer
-          label: 'Token expiration time in hours (0 = never expire)'
-    email_settings:
-      type: mapping
-      label: 'Email Settings'
-      mapping:
-        mail_format:
-          type: string
-          label: 'Email text format'
-    email_templates:
-      type: mapping
-      label: 'Email Templates'
-      mapping:
-        verification_subject:
-          type: string
-          label: 'Verification email subject'
-        verification_body:
-          type: text_format
-          label: 'Verification email body'
-        notification_subject:
-          type: string
-          label: 'Notification email subject'
-        notification_body:
-          type: text_format
-          label: 'Notification email body'
-    security:
-      type: mapping
-      label: 'Security Settings'
-      mapping:
-        require_verification:
-          type: boolean
-          label: 'Require email verification'
-        flood_control:
-          type: mapping
-          label: 'Flood Control Settings'
-          mapping:
-            ip_limit:
-              type: integer
-              label: 'IP-based attempt limit'
-            ip_window:
-              type: integer
-              label: 'IP-based time window (hours)'
-            identifier_limit:
-              type: integer
-              label: 'Email-based attempt limit'
-            identifier_window:
-              type: integer
-              label: 'Email-based time window (hours)'
-    spam_protection:
-      type: mapping
-      label: 'Spam Protection Settings'
-      mapping:
-        enable_modal:
-          type: boolean
-          label: 'Enable modal dialog'
-        enable_math_captcha:
-          type: boolean
-          label: 'Enable math captcha'
-        math_captcha_operator:
-          type: string
-          label: 'Math captcha operator'
-        captcha_point:
-          type: string
-          label: 'CAPTCHA point'
-```
-
-# LICENSE.txt
-
-```txt
-                    GNU GENERAL PUBLIC LICENSE
-                       Version 2, June 1991
-
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The licenses for most software are designed to take away your
-freedom to share and change it.  By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users.  This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it.  (Some other Free Software Foundation software is covered by
-the GNU Lesser General Public License instead.)  You can apply it to
-your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
-  To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have.  You must make sure that they, too, receive or can get the
-source code.  And you must show them these terms so they know their
-rights.
-
-  We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
-  Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software.  If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
-  Finally, any free program is threatened constantly by software
-patents.  We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary.  To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                    GNU GENERAL PUBLIC LICENSE
-   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-  0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License.  The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language.  (Hereinafter, translation is included without limitation in
-the term "modification".)  Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope.  The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
-  1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
-  2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
-    a) You must cause the modified files to carry prominent notices
-    stating that you changed the files and the date of any change.
-
-    b) You must cause any work that you distribute or publish, that in
-    whole or in part contains or is derived from the Program or any
-    part thereof, to be licensed as a whole at no charge to all third
-    parties under the terms of this License.
-
-    c) If the modified program normally reads commands interactively
-    when run, you must cause it, when started running for such
-    interactive use in the most ordinary way, to print or display an
-    announcement including an appropriate copyright notice and a
-    notice that there is no warranty (or else, saying that you provide
-    a warranty) and that users may redistribute the program under
-    these conditions, and telling the user how to view a copy of this
-    License.  (Exception: if the Program itself is interactive but
-    does not normally print such an announcement, your work based on
-    the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole.  If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works.  But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
-  3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
-    a) Accompany it with the complete corresponding machine-readable
-    source code, which must be distributed under the terms of Sections
-    1 and 2 above on a medium customarily used for software interchange; or,
-
-    b) Accompany it with a written offer, valid for at least three
-    years, to give any third party, for a charge no more than your
-    cost of physically performing source distribution, a complete
-    machine-readable copy of the corresponding source code, to be
-    distributed under the terms of Sections 1 and 2 above on a medium
-    customarily used for software interchange; or,
-
-    c) Accompany it with the information you received as to the offer
-    to distribute corresponding source code.  (This alternative is
-    allowed only for noncommercial distribution and only if you
-    received the program in object code or executable form with such
-    an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it.  For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable.  However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
-  4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License.  Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
-  5. You are not required to accept this License, since you have not
-signed it.  However, nothing else grants you permission to modify or
-distribute the Program or its derivative works.  These actions are
-prohibited by law if you do not accept this License.  Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
-  6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions.  You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
-  7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all.  For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices.  Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
-  8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded.  In such case, this License incorporates
-the limitation as if written in the body of this License.
-
-  9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number.  If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation.  If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
-  10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission.  For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this.  Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
-                            NO WARRANTY
-
-  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
-  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
-    Gnomovision version 69, Copyright (C) year name of author
-    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary.  Here is a sample; alter the names:
-
-  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
-  `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
-  <signature of Ty Coon>, 1 April 1989
-  Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs.  If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.
-
-```
-
-# page_notifications.info.yml
-
-```yml
-name: Page Notifications
-type: module
-description: 'Anonymous users can subscribe to pages to receive notifications about updates.'
-core_version_requirement: ^10.3 || ^11
-configure: page_notifications.settings
-dependencies:
-  - drupal:node
-  - drupal:views
-  - token:token
-  - drupal:symfony_mailer_lite
-suggestions:
-  - captcha:captcha
-```
-
-# page_notifications.install
-
-```install
-<?php
-
-use Drupal\Core\Config\FileStorage;
-use Drupal\page_notifications\Service\MigrationService;
-
-/**
- * @file
- * Install, update and uninstall functions for the page_notifications module.
- */
-
-/**
- * Implements hook_install().
- */
-function page_notifications_install() {
-  // Try to find the best available text format
-  $preferred_formats = ['email', 'easy_email', 'full_html', 'basic_html', 'restricted_html', 'plain_text'];
-  $selected_format = 'plain_text'; // Default fallback
-
-  $format_storage = \Drupal::entityTypeManager()->getStorage('filter_format');
-  foreach ($preferred_formats as $format_id) {
-    if ($format = $format_storage->load($format_id)) {
-      $selected_format = $format_id;
-      break;
-    }
-  }
-  // Set Dynamic configuration for email format
-  $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
-  $config
-    ->set('email_settings.mail_format', $selected_format)
-    ->save();
-}
-
-/**
- * Implements hook_schema().
- */
-function page_notifications_schema() {
-  $schema = [];
-  // Add any additional database tables needed beyond entities
-  return $schema;
-}
-
-/**
- * Implements hook_uninstall().
- */
-function page_notifications_uninstall() {
-  // Remove configuration
-  $config_factory = \Drupal::configFactory();
-
-  // List all config objects that need to be removed
-  $config_names = [
-    'page_notifications.settings',
-    'views.view.page_notification_subscriptions',
-    'views.view.top_subscribed_content'
-  ];
-
-  foreach ($config_names as $config_name) {
-    $config_factory->getEditable($config_name)->delete();
-  }
-
-  // Clean up state
-  \Drupal::state()->delete('page_notifications_v3_backup');
-}
-
-/**
- * Install new schema, views, default configuration, and migrate subscriptions from v3 to v4.
- */
-function page_notifications_update_10001(&$sandbox) {
-  // First ensure the new schema is installed
-  $entity_type = \Drupal::entityTypeManager()->getDefinition('page_notification_subscription');
-  \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type);
-
-  // Check if this is a migration or fresh install by looking for v3 tables
-  $schema = \Drupal::database()->schema();
-  $has_v3_data = $schema->tableExists('page_notify_settings') &&
-                 $schema->tableExists('page_notify_email_template');
-
-  // Install required views and configuration regardless of migration status
-  $module_path = \Drupal::service('extension.list.module')->getPath('page_notifications');
-  $source = new FileStorage($module_path . '/config/install');
-  $config_storage = \Drupal::service('config.storage');
-
-  // List of all configurations to install
-  $configs = [
-    'views.view.page_notification_subscriptions',
-    'views.view.top_subscribed_content',
-    'page_notifications.settings',
-  ];
-
-  foreach ($configs as $config_name) {
-    $config_record = $source->read($config_name);
-    if (is_array($config_record)) {
-      $config_storage->write($config_name, $config_record);
-      \Drupal::logger('page_notifications')->notice('Installed configuration: @config', ['@config' => $config_name]);
-    }
-    else {
-      \Drupal::logger('page_notifications')->error('Failed to read configuration: @config', ['@config' => $config_name]);
-    }
-  }
-
-  // Only attempt migration if v3 tables exist
-  if ($has_v3_data) {
-    try {
-      // Get v3 settings
-      $v3_settings = \Drupal::database()->select('page_notify_settings', 'pns')
-        ->fields('pns')
-        ->execute()
-        ->fetchAssoc();
-
-      $v3_template = \Drupal::database()->select('page_notify_email_template', 'pnet')
-        ->fields('pnet')
-        ->execute()
-        ->fetchAssoc();
-
-      // Store v3 data
-      \Drupal::state()->set('page_notifications_v3_backup', [
-        'settings' => $v3_settings,
-        'template' => $v3_template,
-      ]);
-
-      // Map settings to v4
-      $config = \Drupal::configFactory()->getEditable('page_notifications.settings');
-
-      // Migrate settings
-      if ($v3_template && $v3_settings) {
-        // Map email settings
-        $config->set('notification_settings.from_email', $v3_template['from_email'] ?? '');
-
-        /**
-         * Converts v3 tokens to v4 format in a string.
-         */
-        function page_notifications_convert_tokens($text) {
-          $token_map = [
-            '[notify_user_email]' => '[subscription:email]',
-            '[notify_verify_url]' => '[subscription:verify-url]',
-            '[notify_unsubscribe_url]' => '[subscription:unsubscribe-url]',
-            '[notify_node_title]' => '[node:title]',
-            '[notify_node_url]' => '[node:url]',
-            '[notify_notes]' => '[notification:notes]',
-            // Remove or convert deprecated tokens
-            '[notify_user_name]' => '',
-            '[notify_subscribe_url]' => '',
-            '[notify_user_subscribtions]' => '',
-          ];
-
-          return str_replace(
-            array_keys($token_map),
-            array_values($token_map),
-            $text
-          );
-        }
-
-        // Map CAPTCHA settings
-        if (!empty($v3_settings['page_notify_recaptcha'])) {
-          $config->set('spam_prevention.captcha_type', 'recaptcha');
-          $config->set('spam_prevention.use_recaptcha', TRUE);
-        }
-        elseif (!empty($v3_settings['page_notify_captcha'])) {
-          $config->set('spam_prevention.captcha_type', 'math');
-        }
-
-        // Map email templates with token conversion
-        $config->set('email_templates.verification_subject',
-        page_notifications_convert_tokens($v3_template['verification_email_subject'] ?? ''));
-
-        $config->set('email_templates.verification_body',
-        page_notifications_convert_tokens($v3_template['verification_email_text'] ?? ''));
-
-        $config->set('email_templates.notification_subject',
-        page_notifications_convert_tokens($v3_template['general_email_template_subject'] ?? ''));
-
-        $config->set('email_templates.notification_body',
-        page_notifications_convert_tokens($v3_template['general_email_template'] ?? ''));
-
-        $config->save();
-
-        \Drupal::logger('page_notifications')->notice('Migrated settings from v3 to v4.');
-        \Drupal::messenger()->addWarning(t('Email templates have been migrated from v3 to v4. Please review your templates as some tokens have changed. See documentation for the new token format.'));
-
-      }
-
-      // Set up batch migration for subscriptions
-      $batch = MigrationService::createMigrationBatch();
-      if ($batch) {
-        batch_set($batch);
-      }
-    }
-    catch (\Exception $e) {
-      \Drupal::logger('page_notifications')->error('Error during v3 to v4 migration: @message', [
-        '@message' => $e->getMessage()
-      ]);
-    }
-  }
-
-  return $has_v3_data ?
-    t('Subscription data has been migrated, views and default configuration have been installed. Please review your Page Notifications settings at /admin/config/system/page-notifications') :
-    t('Page Notifications v4 has been installed with default configuration.');
-}
-
-```
-
-# page_notifications.libraries.yml
-
-```yml
-modal:
-  version: 1.x
-  dependencies:
-    - core/drupal.dialog.ajax
-    - core/drupal.ajax
-    - core/once
-```
-
-# page_notifications.links.menu.yml
-
-```yml
-page_notifications.settings:
-  title: 'Page Notifications'
-  description: 'Configure Page Notifications settings'
-  parent: system.admin_config_system
-  route_name: page_notifications.settings
-  weight: 0
-
-page_notifications.subscription_list:
-  title: 'Subscriptions'
-  description: 'Manage Page Notification subscriptions'
-  parent: system.admin_content
-  route_name: page_notifications.subscription_list
-  weight: 0
-```
-
-# page_notifications.links.task.yml
-
-```yml
-page_notifications.settings:
-  route_name: page_notifications.settings
-  title: 'Settings'
-  base_route: page_notifications.admin_settings
-  weight: 0
-
-page_notifications.send_manual:
-  route_name: page_notifications.send_manual
-  title: 'Send Notification'
-  base_route: page_notifications.admin_settings
-  weight: 1
-
-page_notifications.subscription_list:
-  route_name: page_notifications.subscription_list
-  title: 'Subscriptions'
-  base_route: page_notifications.admin_settings
-  weight: 2
-
-page_notifications.subscription_migrate:
-  route_name: page_notifications.subscription_migrate
-  title: 'Migrate Subscriptions'
-  base_route: page_notifications.admin_settings
-  weight: 4
-
-page_notifications.top_subscribed:
-  route_name: page_notifications.top_subscribed
-  title: 'Top Subscribed Content'
-  base_route: system.admin_content  # This connects it to the admin content page
-  weight: 5
-```
-
-# page_notifications.module
-
-```module
-<?php
-
-use Drupal\Core\Render\BubbleableMetadata;
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * @file
- * Primary module hooks for Page Notifications module.
- */
-
-/**
- * Implements hook_mail().
- */
-function page_notifications_mail($key, &$message, $params) {
-  \Drupal::service('page_notifications.mail_handler')->mail($key, $message, $params);
-}
-
-/**
- * Implements hook_token_info().
- */
-function page_notifications_token_info() {
-  return \Drupal::service('page_notifications.subscription_token')->hookTokenInfo();
-}
-
-/**
- * Implements hook_tokens().
- */
-function page_notifications_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-  return \Drupal::service('page_notifications.subscription_token')->hookTokens($type, $tokens, $data, $options, $bubbleable_metadata);
-}
-
-/**
- * Implements hook_form_BASE_FORM_ID_alter().
- */
-function page_notifications_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
-  /** @var \Drupal\node\NodeInterface $node */
-  $node = $form_state->getFormObject()->getEntity();
-
-  // Get subscriber count
-  $subscriber_count = \Drupal::entityTypeManager()
-    ->getStorage('page_notification_subscription')
-    ->getQuery()
-    ->condition('subscribed_entity_id', $node->id())
-    ->condition('subscribed_entity_type', 'node')
-    ->condition('status', TRUE)
-    ->count()
-    ->accessCheck(FALSE)
-    ->execute();
-
-  // Add notification checkbox to the meta header region
-  $form['meta']['send_notification'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Send notification to subscribers (@count active subscribers)', [
-      '@count' => $subscriber_count,
-    ]),
-    '#description' => $subscriber_count > 0 ?
-      t('If checked, subscribers will receive an email about this update. The revision log message will be included in the notification.') :
-      t('This content has no active subscribers.'),
-    '#default_value' => FALSE,
-    '#disabled' => $subscriber_count === 0,
-    '#group' => 'meta',
-    '#weight' => 30,
-    '#access' => \Drupal::currentUser()->hasPermission('send page notifications'),
-  ];
-
-  // Add custom submit handler
-  $form['actions']['submit']['#submit'][] = 'page_notifications_node_form_submit';
-}
-
-/**
- * Submit handler for node form.
- */
-function page_notifications_node_form_submit($form, FormStateInterface $form_state) {
-  $meta = $form_state->getValue('meta');
-  if (!empty($meta['send_notification'])) {
-    $node = $form_state->getFormObject()->getEntity();
-    /** @var \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager */
-    $notification_manager = \Drupal::service('page_notifications.notification_manager');
-    $notification_manager->notifySubscribers($node);
-
-    \Drupal::messenger()->addMessage(t('Notification queued for sending to subscribers.'));
-  }
-}
-
-/**
- * Implements hook_cron().
- */
-function page_notifications_cron() {
-  \Drupal::service('page_notifications.cron_manager')->processCron();
-}
-
-/**
- * Implements hook_theme().
- */
-function page_notifications_theme() {
-  return [
-    'block__page_notifications_subscription' => [
-      'template' => 'block--page-notifications-subscription',
-      'base hook' => 'block',
-    ],
-    'page_notifications_email_wrapper' => [
-      'variables' => [
-        'content' => NULL,
-        'email_type' => NULL,
-        'subscription' => NULL,
-        'entity' => NULL,
-        'logo_url' =>  theme_get_setting('logo.url') ? \Drupal::request()->getSchemeAndHttpHost() . theme_get_setting('logo.url') : NULL,
-        'site_name' => \Drupal::config('system.site')->get('name'),
-        'footer' => NULL,
-      ],
-      'template' => 'page-notifications-email-wrapper',
-    ],
-  ];
-}
-
-/**
- * Implements hook_theme_suggestions_HOOK().
- */
-function page_notifications_theme_suggestions_page_notifications_email_wrapper(array $variables) {
-  $suggestions = [];
-
-  if (!empty($variables['email_type'])) {
-    $suggestions[] = 'page_notifications_email_wrapper__' . $variables['email_type'];
-  }
-
-  return $suggestions;
-}
-
-/**
- * Implements hook_preprocess_page_notifications_email_wrapper().
- */
-function page_notifications_preprocess_page_notifications_email_wrapper(&$variables) {
-  // Ensure logo URL is absolute
-  if (!empty($variables['logo_url']) && !preg_match('/^(http|https):\/\//', $variables['logo_url'])) {
-    $variables['logo_url'] = \Drupal::request()->getSchemeAndHttpHost() . $variables['logo_url'];
-  }
-  // Allow modules to alter email variables
-  \Drupal::moduleHandler()->alter('page_notifications_email_variables', $variables);
-}
-
-```
-
-# page_notifications.permissions.yml
-
-```yml
-administer page notification subscriptions:
-  title: 'Administer page notification subscriptions'
-  description: 'Full administrative access to page notification subscriptions.'
-  restrict access: TRUE
-
-view page notification subscriptions:
-  title: 'View page notification subscriptions'
-  description: 'View existing page notification subscriptions.'
-
-create page notification subscriptions:
-  title: 'Create page notification subscriptions'
-  description: 'Create new page notification subscriptions.'
-
-edit page notification subscriptions:
-  title: 'Edit page notification subscriptions'
-  description: 'Edit existing page notification subscriptions.'
-
-delete page notification subscriptions:
-  title: 'Delete page notification subscriptions'
-  description: 'Delete existing page notification subscriptions.'
-
-view subscription list:
-  title: 'View subscription list'
-  description: 'Access the subscription list view'
-
-view top subscribed content:
-  title: 'View top subscribed content'
-  description: 'Access the top subscribed content list'
-```
-
-# page_notifications.routing.yml
-
-```yml
-page_notifications.settings:
-  path: '/admin/config/system/page-notifications'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\SettingsForm'
-    _title: 'Page Notifications Settings'
-  requirements:
-    _permission: 'administer page notification subscriptions'
-
-page_notifications.subscription.verify:
-  path: '/page-notifications/verify/{token}'
-  defaults:
-    _controller: 'page_notifications.notification_manager:verifySubscription'
-    _title: 'Verify Subscription'
-  requirements:
-    _access: 'TRUE'
-  options:
-    no_cache: TRUE
-
-# Secure unsubscribe for anonymous users
-page_notifications.subscription.unsubscribe:
-  path: '/page-notifications/unsubscribe/{subscription}/{token}'
-  defaults:
-    _controller: '\Drupal\page_notifications\Controller\UnsubscribeController::unsubscribe'
-    _title: 'Unsubscribe from Notifications'
-  requirements:
-    _custom_access: '\Drupal\page_notifications\Controller\UnsubscribeController::checkAccess'
-  options:
-    no_cache: TRUE
-
-page_notifications.send_manual:
-  path: '/admin/config/system/page-notifications/send'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\ManualNotificationForm'
-    _title: 'Send Manual Notification'
-  requirements:
-    _permission: 'administer page notification subscriptions'
-
-page_notifications.subscription_list:
-  path: '/admin/config/system/page-notifications/subscriptions'
-  defaults:
-    _controller: '\Drupal\page_notifications\Controller\SubscriptionListController::content'
-    _title: 'Subscriptions'
-  requirements:
-    _permission: 'view subscription list'
-
-page_notifications.top_subscribed:
-  path: '/admin/content/top-subscribed'
-  defaults:
-    _controller: '\Drupal\page_notifications\Controller\TopSubscribedController::content'
-    _title: 'Top Subscribed Content'
-  requirements:
-    _permission: 'view subscription list'
-
-page_notifications.subscription_migrate:
-  path: '/admin/config/system/page-notifications/migrate'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\SubscriptionMigrateForm'
-    _title: 'Migrate Subscriptions'
-  requirements:
-    _permission: 'administer page notification subscriptions'
-
-page_notifications.subscription_add:
-  path: '/admin/config/system/page-notifications/subscriptions/add'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\ManualSubscriptionAddForm'
-    _title: 'Add Subscriptions'
-  requirements:
-    _permission: 'administer page notification subscriptions'
-
-page_notifications.purge_subscriptions_confirm:
-  path: '/admin/config/system/page-notifications/purge-confirm'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\PurgeSubscriptionsConfirmForm'
-    _title: 'Confirm subscription purge'
-  requirements:
-    _permission: 'administer page notification subscriptions'
-page_notifications.modal_form:
-  path: '/page-notifications/modal-form/{entity_type}/{entity}'
-  defaults:
-    _form: '\Drupal\page_notifications\Form\ModalSubscriptionForm'
-    _title: 'Subscribe to Updates'
-  requirements:
-    _access: 'TRUE'
-  options:
-    parameters:
-      entity:
-        type: entity:{entity_type}
-```
-
-# page_notifications.services.yml
-
-```yml
-services:
-  page_notifications.notification_manager:
-    class: Drupal\page_notifications\Service\NotificationManager
-    arguments:
-      - '@config.factory'
-      - '@plugin.manager.mail'
-      - '@entity_type.manager'
-      - '@queue'
-      - '@logger.factory'
-      - '@event_dispatcher'
-      - '@datetime.time'
-      - '@string_translation'
-      - '@messenger'
-    calls:
-      - [setStringTranslation, ['@string_translation']]
-  page_notifications.mail_handler:
-    class: Drupal\page_notifications\Mail\PageNotificationsMailHandler
-    arguments:
-      - '@config.factory'
-      - '@renderer'
-      - '@token'
-      - '@string_translation'
-      - '@theme.manager'
-      - '@symfony_mailer_lite.mailer'
-  page_notifications.subscription_token:
-    class: Drupal\page_notifications\Token\SubscriptionToken
-    tags:
-      - { name: token.provider }
-  page_notifications.cron_manager:
-    class: Drupal\page_notifications\Service\CronManager
-    arguments:
-      - '@entity_type.manager'
-      - '@config.factory'
-      - '@queue'
-      - '@plugin.manager.queue_worker'
-      - '@logger.factory'
-      - '@datetime.time'
-  page_notifications.queue_worker:
-    class: Drupal\page_notifications\Plugin\QueueWorker\NotificationQueue
-    arguments:
-      - '@entity_type.manager'
-      - '@plugin.manager.mail'
-      - '@config.factory'
-      - '@logger.factory'
-    tags:
-      - { name: queue_worker, id: page_notifications_queue }
-  page_notifications.spam_prevention:
-    class: Drupal\page_notifications\Service\SpamPrevention
-    arguments:
-      - '@config.factory'
-      - '@module_handler'
-      - '@session_manager'
-      - '@string_translation'
-  page_notifications.migration:
-    class: Drupal\page_notifications\Service\MigrationService
-    arguments:
-      - '@database'
-      - '@entity_type.manager'
-      - '@config.factory'
-      - '@state'
-      - '@logger.factory'
-      - '@datetime.time'
-```
-
-# README.md
-
-```md
-# Page Notifications
-
-A Drupal module that enables anonymous and authenticated users to subscribe to content updates and receive email notifications when changes occur.
-
-## CONTENTS OF THIS FILE
-* Introduction
-* Features
-* Requirements
-* Installation
-* Configuration
-* Usage
-* Security
-* API
-* Maintainers
-
-## INTRODUCTION
-
-Page Notifications provides a flexible system for users to subscribe to content changes on your Drupal site. When subscribed content is updated, subscribers receive customizable email notifications about the changes.
-
-## FEATURES
-
-* Email subscription system for any content entity (nodes by default)
-* Configurable email templates with token support
-* Anti-spam protection with multiple options:
-  - Simple math CAPTCHA
-  - reCAPTCHA integration (requires reCAPTCHA module)
-* Subscription management:
-  - Email verification system
-  - Secure unsubscribe links
-  - Automatic cleanup of unverified subscriptions
-  - Subscription migration tools
-* Administration:
-  - Settings interface
-  - Subscription overview and management
-  - Manual notification sending capability
-* Queue-based notification processing
-* Token support for email templates
-* Block-based subscription forms
-* Drupal Views integration
-
-## REQUIREMENTS
-
-This module requires the following:
-* Drupal 10.x
-* Node module (enabled by default)
-* Views module (enabled by default)
-
-Optional but recommended:
-* reCAPTCHA module for enhanced spam protection
-
-## INSTALLATION
-
-1. Install the module via Composer:
-   \`\`\`bash
-   composer require drupal/page_notifications
-   \`\`\`
-   Or download and extract to your modules directory.
-
-2. Enable the module at `/admin/modules` or via Drush:
-   \`\`\`bash
-   drush en page_notifications
-   \`\`\`
-
-3. Place the subscription block in your desired region at `/admin/structure/block`. Use block configuration to set visibility as desired.
-
-## CONFIGURATION
-
-All module settings can be configured at `/admin/config/system/page-notifications`:
-
-### Email Settings
-* Configure "From" email address
-* Set verification token expiration time
-* Customize email templates for:
-  - Subscription verification
-  - Update notifications
-
-### Security Settings
-* Toggle email verification requirement
-* Configure unverified subscription cleanup
-* Set spam prevention method:
-  - None
-  - Math CAPTCHA
-  - reCAPTCHA (if module installed)
-
-### Subscription Management
-* View and manage subscriptions
-* Migrate subscriptions between content
-* Send manual notifications
-
-## USAGE
-
-### For Site Builders
-1. Place the subscription block on content types where you want to enable notifications
-2. Configure email templates and security settings
-3. Manage subscriptions through the administrative interface
-
-### For Content Editors
-1. Update content normally
-2. Option to send notifications appears in content edit form next to the revision log
-3. Can include custom notes with notifications
-
-### For Users
-1. Subscribe to content via the subscription block
-2. Receive verification email (if enabled)
-3. Get notifications when content is updated
-4. Unsubscribe via secure links in notification emails
-
-## SECURITY
-
-The module implements several security measures:
-* Email verification system
-* CAPTCHA/reCAPTCHA spam prevention
-* Secure unsubscribe tokens
-* Automatic cleanup of unverified subscriptions
-* Permission-based access control
-
-## Email Customization
-
-### Template Override
-You can override the email template by copying 
-`templates/page-notifications-email-wrapper.html.twig` to your theme and modifying it.
-
-Template suggestions available:
-- page-notifications-email-wrapper--verification.html.twig
-- page-notifications-email-wrapper--notification.html.twig
-- page-notifications-email-wrapper--already-subscribed.html.twig
-
-### Programmatic Customization
-Implement these hooks in your custom module:
-
-\`\`\`php
-/**
- * Implements hook_page_notifications_email_variables_alter().
- */
-function mymodule_page_notifications_email_variables_alter(&$variables) {
-  // Add custom variables to email template
-  $variables['my_variable'] = 'Custom content';
-}
-
-/**
- * Implements hook_page_notifications_email_footer_alter().
- */
-function mymodule_page_notifications_email_footer_alter(&$footer) {
-  $footer = 'Custom footer content';
-}
-
-## API
-
-The module provides services and interfaces for developers to:
-* Create and manage subscriptions programmatically
-* Customize notification handling
-* Extend spam prevention mechanisms
-* Integrate with other modules
-
-Key services:
-* `page_notifications.notification_manager`
-* `page_notifications.mail_handler`
-* `page_notifications.spam_prevention`
-
-## MAINTAINERS
-
-Current maintainers:
-* Lidiya Grushetska <grushetskl@chop.edu> is the original author https://www.drupal.org/u/lidia_ua.
-```
-
-# src/Controller/ModalFormController.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Drupal\Core\Form\FormBuilderInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\node\NodeInterface;
-
-/**
- * Controller for the subscription modal form.
- */
-class ModalFormController extends ControllerBase {
-
-  /**
-   * The form builder.
-   *
-   * @var \Drupal\Core\Form\FormBuilderInterface
-   */
-  protected $formBuilder;
-
-  /**
-   * Constructs a new ModalFormController.
-   *
-   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
-   *   The form builder.
-   */
-  public function __construct(FormBuilderInterface $form_builder) {
-    $this->formBuilder = $form_builder;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('form_builder')
-    );
-  }
-
-  /**
-   * Returns the subscription form in a modal.
-   *
-   * @param \Drupal\node\NodeInterface $node
-   *   The node being subscribed to.
-   *
-   * @return array
-   *   The render array for the modal form.
-   */
-  public function content(NodeInterface $node) {
-    $build = [
-      '#prefix' => '<div id="modal-subscription-form-wrapper">',
-      '#suffix' => '</div>',
-      'status_messages' => [
-        '#type' => 'status_messages',
-      ],
-      'form' => $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node),
-    ];
-
-    return $build;
-  }
-
-}
-```
-
-# src/Controller/SubscriptionListController.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Drupal\Core\Url;
-
-/**
- * Controller for the subscription list page.
- */
-class SubscriptionListController extends ControllerBase {
-
-  /**
-   * Displays the subscription list view.
-   *
-   * @return array
-   *   A render array for the view.
-   */
-  public function content() {
-    // Add the "Add Subscription" button
-    $build['add_form'] = [
-      '#type' => 'link',
-      '#title' => $this->t('Add Subscription'),
-      '#url' => Url::fromRoute('page_notifications.subscription_add'),
-      '#attributes' => [
-        'class' => ['button', 'button--action', 'button--primary'],
-      ],
-    ];
-
-    // Add some spacing after the button
-    $build['spacing'] = [
-      '#type' => 'html_tag',
-      '#tag' => 'div',
-      '#attributes' => [
-        'style' => 'margin: 1em 0;',
-      ],
-    ];
-
-    // Add the view
-    $build['view'] = views_embed_view('page_notification_subscriptions', 'default');
-
-    return $build;
-  }
-
-}
-```
-
-# src/Controller/TopSubscribedController.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-
-/**
- * Controller for the top subscribed content page.
- */
-class TopSubscribedController extends ControllerBase {
-
-  /**
-   * Displays the top subscribed content view.
-   *
-   * @return array
-   *   A render array for the view.
-   */
-  public function content() {
-    $view = views_embed_view('top_subscribed_content', 'default');
-    return [
-      '#type' => 'container',
-      'view' => $view,
-    ];
-  }
-
-}
-```
-
-# src/Controller/UnsubscribeController.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Controller;
-
-use Drupal\Core\Controller\ControllerBase;
-use Drupal\Core\Access\AccessResult;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-use Drupal\Core\Messenger\MessengerInterface;
-use Drupal\Core\Url;
-use Drupal\Core\Flood\FloodInterface;
-use Drupal\page_notifications\Traits\FloodControlTrait;
-
-/**
- * Controller for handling unsubscribe requests.
- */
-class UnsubscribeController extends ControllerBase {
-  use FloodControlTrait;
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * Constructs a new UnsubscribeController.
-   */
-  public function __construct(
-    EntityTypeManagerInterface $entity_type_manager,
-    FloodInterface $flood
-  ) {
-    $this->entityTypeManager = $entity_type_manager;
-    $this->setFloodService($flood);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('entity_type.manager'),
-      $container->get('flood')
-    );
-  }
-
-  /**
-   * Custom access check for unsubscribe URLs.
-   */
-  public function checkAccess($subscription, $token) {
-    // Check flood control first
-    $ip = \Drupal::request()->getClientIp();
-    $flood_config = $this->getFloodControlConfig();
-
-    if (!$this->flood->isAllowed('page_notifications.unsubscribe', $flood_config['ip_limit'], $flood_config['ip_window'], $ip)) {
-      return AccessResult::forbidden('Too many unsubscribe attempts from this IP address.');
-    }
-
-    // Register flood event for this attempt
-    $this->flood->register('page_notifications.unsubscribe', $flood_config['ip_window'], $ip);
-
-    if (is_numeric($subscription)) {
-      try {
-        $subscription = $this->entityTypeManager
-          ->getStorage('page_notification_subscription')
-          ->load($subscription);
-      }
-      catch (\Exception $e) {
-        return AccessResult::forbidden();
-      }
-    }
-
-    if (!$subscription) {
-      return AccessResult::forbidden();
-    }
-
-    if ($subscription->getUnsubscribeToken() !== $token) {
-      $this->logSecurityEvent('invalid_unsubscribe_token', [
-        'ip' => $ip,
-        'subscription_id' => $subscription->id(),
-      ]);
-      return AccessResult::forbidden();
-    }
-
-    return AccessResult::allowed();
-  }
-
-  /**
-   * Handles the unsubscribe request.
-   */
-  public function unsubscribe($subscription, $token) {
-    if (is_numeric($subscription)) {
-      try {
-        $subscription = $this->entityTypeManager
-          ->getStorage('page_notification_subscription')
-          ->load($subscription);
-      }
-      catch (\Exception $e) {
-        $this->messenger()->addError($this->t('An error occurred while processing your request.'));
-        return new RedirectResponse('/');
-      }
-    }
-
-    try {
-      if ($subscription && $subscription->getUnsubscribeToken() === $token) {
-        // Get the node ID and entity type before deleting the subscription
-        $entity_id = $subscription->getSubscribedEntityId();
-        $entity_type = $subscription->getSubscribedEntityType();
-
-        $subscription->delete();
-        $this->messenger()->addStatus($this->t('You have been successfully unsubscribed.'));
-
-        // Load the entity and get its URL
-        try {
-          $entity = $this->entityTypeManager
-            ->getStorage($entity_type)
-            ->load($entity_id);
-
-          if ($entity && $entity->hasLinkTemplate('canonical')) {
-            return new RedirectResponse($entity->toUrl()->toString());
-          }
-        }
-        catch (\Exception $e) {
-          \Drupal::logger('page_notifications')->error('Redirect error: @message', ['@message' => $e->getMessage()]);
-        }
-      }
-      else {
-        $this->messenger()->addError($this->t('Invalid unsubscribe link.'));
-      }
-    }
-    catch (\Exception $e) {
-      $this->messenger()->addError($this->t('An error occurred while processing your request.'));
-      \Drupal::logger('page_notifications')->error('Unsubscribe error: @message', ['@message' => $e->getMessage()]);
-    }
-
-    // Fallback to homepage if anything goes wrong
-    return new RedirectResponse('/');
-  }
-}
-```
-
-# src/Entity/Subscription.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Entity;
-
-use Drupal\Core\Entity\ContentEntityBase;
-use Drupal\Core\Entity\EntityChangedTrait;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\user\EntityOwnerTrait;
-
-/**
- * Defines the Subscription entity.
- *
- * @ContentEntityType(
- *   id = "page_notification_subscription",
- *   label = @Translation("Page Notification Subscription"),
- *   handlers = {
- *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
- *     "list_builder" = "Drupal\page_notifications\Entity\SubscriptionListBuilder",
- *     "views_data" = "Drupal\page_notifications\Entity\SubscriptionViewsData",
- *     "form" = {
- *       "default" = "Drupal\page_notifications\Form\SubscriptionForm",
- *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm"
- *     },
- *     "access" = "Drupal\page_notifications\Entity\SubscriptionAccessControlHandler",
- *     "route_provider" = {
- *       "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider"
- *     }
- *   },
- *   base_table = "page_notification_subscription",
- *   data_table = "page_notification_subscription_field_data",
- *   admin_permission = "administer page notification subscriptions",
- *   entity_keys = {
- *     "id" = "id",
- *     "uuid" = "uuid",
- *     "owner" = "uid",
- *     "langcode" = "langcode"
- *   },
- *   links = {
- *     "canonical" = "/admin/content/subscriptions/{page_notification_subscription}",
- *     "delete-form" = "/admin/content/subscriptions/{page_notification_subscription}/delete",
- *     "collection" = "/admin/content/subscriptions"
- *   }
- * )
- */
-class Subscription extends ContentEntityBase implements SubscriptionInterface {
-
-  use EntityChangedTrait;
-  use EntityOwnerTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getEmail() {
-    return $this->get('email')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setEmail($email) {
-    $this->set('email', $email);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSubscribedEntityId() {
-    return $this->get('subscribed_entity_id')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setSubscribedEntityId($id) {
-    $this->set('subscribed_entity_id', $id);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getSubscribedEntityType() {
-    return $this->get('subscribed_entity_type')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setSubscribedEntityType($entity_type) {
-    $this->set('subscribed_entity_type', $entity_type);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getToken() {
-    return $this->get('token')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setToken($token) {
-    $this->set('token', $token);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function isActive() {
-    return (bool) $this->get('status')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setActive($status) {
-    $this->set('status', $status ? 1 : 0);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCreatedTime() {
-    return $this->get('created')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setCreatedTime($timestamp) {
-    $this->set('created', $timestamp);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getLanguageCode() {
-    return $this->get('langcode')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setLanguageCode($langcode) {
-    $this->set('langcode', $langcode);
-    return $this;
-  }
-
-/**
-   * Gets the default langcode.
-   *
-   * @return string
-   *   The site's default language code.
-   */
-  public static function getDefaultLangcode() {
-    return \Drupal::config('system.site')->get('default_langcode') ?: 'en';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getUnsubscribeToken() {
-    return $this->get('unsubscribe_token')->value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setUnsubscribeToken($token) {
-    $this->set('unsubscribe_token', $token);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
-    $fields = parent::baseFieldDefinitions($entity_type);
-    $fields += static::ownerBaseFieldDefinitions($entity_type);
-
-    $fields['email'] = BaseFieldDefinition::create('email')
-      ->setLabel(t('Email'))
-      ->setDescription(t('The email address of the subscriber.'))
-      ->setRequired(TRUE)
-      ->setTranslatable(TRUE)
-      ->setSettings([
-        'max_length' => 255,
-      ])
-      ->setDisplayOptions('view', [
-        'label' => 'above',
-        'type' => 'string',
-        'weight' => -5,
-      ])
-      ->setDisplayOptions('form', [
-        'type' => 'email_default',
-        'weight' => -5,
-      ])
-      ->setDisplayConfigurable('form', TRUE)
-      ->setDisplayConfigurable('view', TRUE);
-
-    $fields['subscribed_entity_id'] = BaseFieldDefinition::create('integer')
-      ->setLabel(t('Subscribed Entity ID'))
-      ->setDescription(t('The ID of the entity being subscribed to.'))
-      ->setRequired(TRUE)
-      ->setTranslatable(FALSE);
-
-    $fields['subscribed_entity_type'] = BaseFieldDefinition::create('string')
-      ->setLabel(t('Subscribed Entity Type'))
-      ->setDescription(t('The type of the entity being subscribed to.'))
-      ->setRequired(TRUE)
-      ->setTranslatable(FALSE)
-      ->setSettings([
-        'max_length' => 32,
-      ]);
-
-    $fields['token'] = BaseFieldDefinition::create('string')
-      ->setLabel(t('Token'))
-      ->setDescription(t('The subscription verification token.'))
-      ->setRequired(TRUE)
-      ->setTranslatable(FALSE)
-      ->setSettings([
-        'max_length' => 64,
-      ]);
-
-    $fields['status'] = BaseFieldDefinition::create('boolean')
-      ->setLabel(t('Status'))
-      ->setDescription(t('A boolean indicating whether the subscription is active.'))
-      ->setDefaultValue(TRUE)
-      ->setTranslatable(FALSE)
-      ->setDisplayOptions('form', [
-        'type' => 'boolean_checkbox',
-        'weight' => 0,
-      ]);
-
-    $fields['created'] = BaseFieldDefinition::create('created')
-      ->setLabel(t('Created'))
-      ->setDescription(t('The time that the subscription was created.'))
-      ->setTranslatable(FALSE);
-
-    $fields['changed'] = BaseFieldDefinition::create('changed')
-      ->setLabel(t('Changed'))
-      ->setDescription(t('The time that the subscription was last edited.'))
-      ->setTranslatable(FALSE);
-
-    $fields['langcode'] = BaseFieldDefinition::create('language')
-    ->setLabel(t('Language'))
-    ->setDescription(t('The subscription language code.'))
-    ->setDefaultValueCallback(static::class . '::getDefaultLangcode')
-    ->setDisplayOptions('form', [
-      'type' => 'language_select',
-      'weight' => 2,
-    ]);
-
-    $fields['unsubscribe_token'] = BaseFieldDefinition::create('string')
-  ->setLabel(t('Unsubscribe Token'))
-  ->setDescription(t('The token required to unsubscribe from notifications.'))
-  ->setRequired(TRUE)
-  ->setTranslatable(FALSE)
-  ->setSettings([
-    'max_length' => 64,
-  ]);
-
-    return $fields;
-  }
-
-}
-```
-
-# src/Entity/SubscriptionAccessControlHandler.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Entity;
-
-use Drupal\Core\Entity\EntityAccessControlHandler;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Access\AccessResult;
-
-/**
- * Access controller for page notification subscription entities.
- */
-class SubscriptionAccessControlHandler extends EntityAccessControlHandler {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
-    /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $entity */
-    switch ($operation) {
-      case 'view':
-        return AccessResult::allowedIfHasPermission($account, 'view page notification subscriptions');
-
-      case 'update':
-        return AccessResult::allowedIfHasPermission($account, 'edit page notification subscriptions');
-
-      case 'delete':
-        // Allow deletion if user has permission or is the owner of the subscription
-        return AccessResult::allowedIfHasPermissions($account, [
-          'delete page notification subscriptions',
-          'administer page notification subscriptions',
-        ], 'OR')
-          ->orIf(AccessResult::allowedIf($account->isAuthenticated() && $account->id() === $entity->getOwnerId())
-            ->addCacheableDependency($entity));
-    }
-
-    return AccessResult::neutral();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
-    return AccessResult::allowedIfHasPermissions($account, [
-      'create page notification subscriptions',
-      'administer page notification subscriptions',
-    ], 'OR');
-  }
-
-}
-```
-
-# src/Entity/SubscriptionInterface.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Entity;
-
-use Drupal\Core\Entity\ContentEntityInterface;
-use Drupal\Core\Entity\EntityChangedInterface;
-use Drupal\user\EntityOwnerInterface;
-
-/**
- * Interface for Page Notification Subscription entities.
- */
-interface SubscriptionInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface {
-
-  /**
-   * Gets the subscription email.
-   *
-   * @return string
-   *   The subscription email address.
-   */
-  public function getEmail();
-
-  /**
-   * Sets the subscription email.
-   *
-   * @param string $email
-   *   The subscription email address.
-   *
-   * @return $this
-   *   The called subscription entity.
-   */
-  public function setEmail($email);
-
-  /**
-   * Gets the subscribed entity ID.
-   *
-   * @return int
-   *   The entity ID.
-   */
-  public function getSubscribedEntityId();
-
-  /**
-   * Sets the subscribed entity ID.
-   *
-   * @param int $id
-   *   The entity ID.
-   *
-   * @return $this
-   *   The called subscription entity.
-   */
-  public function setSubscribedEntityId($id);
-
-  /**
-   * Gets the subscribed entity type.
-   *
-   * @return string
-   *   The entity type (e.g., 'node', 'taxonomy_term').
-   */
-  public function getSubscribedEntityType();
-
-  /**
-   * Sets the subscribed entity type.
-   *
-   * @param string $entity_type
-   *   The entity type.
-   *
-   * @return $this
-   *   The called subscription entity.
-   */
-  public function setSubscribedEntityType($entity_type);
-
-  /**
-   * Gets the subscription token.
-   *
-   * @return string
-   *   The subscription token.
-   */
-  public function getToken();
-
-  /**
-   * Sets the subscription token.
-   *
-   * @param string $token
-   *   The subscription token.
-   *
-   * @return $this
-   *   The called subscription entity.
-   */
-  public function setToken($token);
-
-  /**
-   * Gets the subscription status.
-   *
-   * @return bool
-   *   TRUE if the subscription is active, FALSE otherwise.
-   */
-  public function isActive();
-
-  /**
-   * Sets the subscription status.
-   *
-   * @param bool $status
-   *   The subscription status.
-   *
-   * @return $this
-   *   The called subscription entity.
-   */
-  public function setActive($status);
-
-  /**
-   * Gets the subscription creation timestamp.
-   *
-   * @return int
-   *   Creation timestamp of the subscription.
-   */
-  public function getCreatedTime();
-
-  /**
-   * Sets the subscription creation timestamp.
-   *
-   * @param int $timestamp
-   *   The subscription creation timestamp.
-   *
-   * @return $this
-   *   The called subscription entity.
-   */
-  public function setCreatedTime($timestamp);
-
-  /**
-   * Gets the subscription language code.
-   *
-   * @return string
-   *   The language code of the subscription.
-   */
-  public function getLanguageCode();
-
-  /**
-   * Sets the subscription language code.
-   *
-   * @param string $langcode
-   *   The language code.
-   *
-   * @return $this
-   *   The called subscription entity.
-   */
-  public function setLanguageCode($langcode);
-
-}
-```
-
-# src/Entity/SubscriptionListBuilder.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Entity;
-
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityListBuilder;
-use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Datetime\DateFormatterInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-
-/**
- * Provides a list builder for page notification subscriptions.
- */
-class SubscriptionListBuilder extends EntityListBuilder {
-
-  /**
-   * The date formatter service.
-   *
-   * @var \Drupal\Core\Datetime\DateFormatterInterface
-   */
-  protected $dateFormatter;
-
-  /**
-   * Constructs a new SubscriptionListBuilder object.
-   *
-   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
-   *   The entity type definition.
-   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
-   *   The entity storage class.
-   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
-   *   The date formatter service.
-   */
-  public function __construct(
-    EntityTypeInterface $entity_type,
-    EntityStorageInterface $storage,
-    DateFormatterInterface $date_formatter
-  ) {
-    parent::__construct($entity_type, $storage);
-    $this->dateFormatter = $date_formatter;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
-    return new static(
-      $entity_type,
-      $container->get('entity_type.manager')->getStorage($entity_type->id()),
-      $container->get('date.formatter')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildHeader() {
-    $header = [];
-    $header['id'] = $this->t('ID');
-    $header['email'] = $this->t('Email');
-    $header['subscribed_entity'] = $this->t('Subscribed To');
-    $header['status'] = $this->t('Status');
-    $header['created'] = $this->t('Created');
-    return $header + parent::buildHeader();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildRow(EntityInterface $entity) {
-    /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $entity */
-    $row = [];
-    $row['id'] = $entity->id();
-    $row['email'] = $entity->getEmail();
-
-    // Get the subscribed entity and create a link if possible.
-    $entity_type = $entity->getSubscribedEntityType();
-    $entity_id = $entity->getSubscribedEntityId();
-    try {
-      $subscribed_entity = \Drupal::entityTypeManager()
-        ->getStorage($entity_type)
-        ->load($entity_id);
-      if ($subscribed_entity) {
-        $row['subscribed_entity'] = Link::createFromRoute(
-          $subscribed_entity->label(),
-          'entity.' . $entity_type . '.canonical',
-          [$entity_type => $entity_id]
-        );
-      }
-      else {
-        $row['subscribed_entity'] = $this->t('Entity not found (@type: @id)', [
-          '@type' => $entity_type,
-          '@id' => $entity_id,
-        ]);
-      }
-    }
-    catch (\Exception $e) {
-      $row['subscribed_entity'] = $this->t('Invalid entity reference');
-    }
-
-    $row['status'] = $entity->isActive() ? $this->t('Active') : $this->t('Inactive');
-    $row['created'] = $this->dateFormatter->format($entity->getCreatedTime(), 'short');
-
-    return $row + parent::buildRow($entity);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-protected function getDefaultOperations(EntityInterface $entity) {
-  $operations = parent::getDefaultOperations($entity);
-
-  // Add verify link if not active
-  if (!$entity->isActive()) {
-    $operations['verify'] = [
-      'title' => $this->t('Verify'),
-      'url' => Url::fromRoute('page_notifications.subscription.verify', [
-        'token' => $entity->getToken(),
-      ]),
-    ];
-  }
-
-  return $operations;
-}
-
-  /**
-   * {@inheritdoc}
-   */
-  public function render() {
-    $build = parent::render();
-    $build['table']['#empty'] = $this->t('No subscriptions found.');
-    return $build;
-  }
-
-}
-```
-
-# src/Entity/SubscriptionViewsData.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Entity;
-
-use Drupal\views\EntityViewsData;
-
-/**
- * Provides Views data for Page Notification Subscription entities.
- */
-class SubscriptionViewsData extends EntityViewsData {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getViewsData() {
-    $data = parent::getViewsData();
-
-    // Base table definition
-    $data['page_notification_subscription']['table']['base'] = [
-      'field' => 'id',
-      'title' => $this->t('Page Notification Subscription'),
-      'help' => $this->t('Contains subscription information for page notifications.'),
-      'weight' => -10,
-    ];
-
-
-    // Define the relationship to nodes
-    $data['page_notification_subscription']['subscribed_entity'] = [
-      'title' => $this->t('Subscribed Node'),
-      'help' => $this->t('The node this subscription is associated with.'),
-      'relationship' => [
-        'base' => 'node_field_data',
-        'base field' => 'nid',
-        'field' => 'subscribed_entity_id',
-        'id' => 'standard',
-        'label' => $this->t('Subscribed Node'),
-      ],
-    ];
-
-      // ID field
-      $data['page_notification_subscription']['subscribed_entity_id'] = [
-        'title' => $this->t('Subscribed Entity ID'),
-        'help' => $this->t('The ID of the entity being subscribed to.'),
-        'field' => [
-          'id' => 'numeric',
-        ],
-        'filter' => [
-          'id' => 'numeric',
-        ],
-        'sort' => [
-          'id' => 'standard',
-        ],
-        'argument' => [
-          'id' => 'numeric',
-        ],
-      ];
-
-      // Status field
-      $data['page_notification_subscription']['status'] = [
-        'title' => $this->t('Status'),
-        'help' => $this->t('The status of the subscription.'),
-        'field' => [
-          'id' => 'boolean',
-        ],
-        'filter' => [
-          'id' => 'boolean',
-          'label' => $this->t('Status'),
-          'type' => 'yes-no',
-        ],
-        'sort' => [
-          'id' => 'standard',
-        ],
-      ];
-
-      // Email field
-      $data['page_notification_subscription']['email'] = [
-        'title' => $this->t('Email'),
-        'help' => $this->t('The email address of the subscriber.'),
-        'field' => [
-          'id' => 'standard',
-        ],
-        'filter' => [
-          'id' => 'string',
-        ],
-        'sort' => [
-          'id' => 'standard',
-        ],
-      ];
-
-      // Created field
-      $data['page_notification_subscription']['created'] = [
-        'title' => $this->t('Created'),
-        'help' => $this->t('When the subscription was created.'),
-        'field' => [
-          'id' => 'date',
-        ],
-        'filter' => [
-          'id' => 'date',
-        ],
-        'sort' => [
-          'id' => 'date',
-        ],
-      ];
-
-    // Operations
-    $data['page_notification_subscription']['operations'] = [
-      'field' => [
-        'title' => $this->t('Operations'),
-        'help' => $this->t('Provides links to perform subscription operations.'),
-        'id' => 'entity_operations',
-      ],
-    ];
-
-    return $data;
-  }
-
-}
-```
-
-# src/EventSubscriber/NodeUpdateSubscriber.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\EventSubscriber;
-
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\page_notifications\Service\NotificationManagerInterface;
-
-/**
- * Node update subscriber for sending notifications.
- */
-class NodeUpdateSubscriber implements EventSubscriberInterface {
-  // TODO: Implement event subscriber for node updates
-}
-```
-
-# src/Form/ManualNotificationForm.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\page_notifications\Service\NotificationManagerInterface;
-
-/**
- * Form for manually sending notifications to subscribers.
- */
-class ManualNotificationForm extends FormBase {
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * The notification manager service.
-   *
-   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
-   */
-  protected $notificationManager;
-
-  /**
-   * Constructs a new ManualNotificationForm.
-   *
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity type manager.
-   * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager
-   *   The notification manager service.
-   */
-  public function __construct(
-    EntityTypeManagerInterface $entity_type_manager,
-    NotificationManagerInterface $notification_manager
-  ) {
-    $this->entityTypeManager = $entity_type_manager;
-    $this->notificationManager = $notification_manager;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('entity_type.manager'),
-      $container->get('page_notifications.notification_manager')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_manual_notification_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    // Get content with active subscribers
-    $subscription_storage = $this->entityTypeManager->getStorage('page_notification_subscription');
-    $node_storage = $this->entityTypeManager->getStorage('node');
-
-    // Get unique node IDs that have active subscribers
-    $query = $subscription_storage->getQuery()
-      ->condition('status', TRUE)
-      ->condition('subscribed_entity_type', 'node')
-      ->accessCheck(FALSE);
-    $result = $query->execute();
-
-    if (empty($result)) {
-      $form['message'] = [
-        '#markup' => $this->t('There are no pages with active subscribers.'),
-      ];
-      return $form;
-    }
-
-    $subscriptions = $subscription_storage->loadMultiple($result);
-    $node_ids = [];
-    foreach ($subscriptions as $subscription) {
-      $node_ids[$subscription->getSubscribedEntityId()] = $subscription->getSubscribedEntityId();
-    }
-
-    // Load nodes and prepare options
-    $nodes = $node_storage->loadMultiple($node_ids);
-    $options = [];
-    foreach ($nodes as $node) {
-      $options[$node->id()] = $node->label();
-    }
-
-    $form['node'] = [
-      '#type' => 'select',
-      '#title' => $this->t('Select Content'),
-      '#options' => $options,
-      '#required' => TRUE,
-      '#description' => $this->t('Select the content to send notifications about.'),
-    ];
-
-    $form['notes'] = [
-      '#type' => 'textarea',
-      '#title' => $this->t('Notification Notes'),
-      '#description' => $this->t('Enter any additional notes to include in the notification email.'),
-      '#rows' => 4,
-    ];
-
-    $form['actions'] = [
-      '#type' => 'actions',
-    ];
-
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Send Notification'),
-    ];
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $node_id = $form_state->getValue('node');
-    $notes = $form_state->getValue('notes');
-
-    try {
-      $node = $this->entityTypeManager->getStorage('node')->load($node_id);
-      if ($node) {
-        // Store notes in tempstore or pass through event system
-        \Drupal::state()->set('page_notifications_manual_notes_' . $node->id(), $notes);
-
-        $this->notificationManager->notifySubscribers($node);
-        $this->messenger()->addStatus($this->t('Notifications have been queued for sending.'));
-      }
-    }
-    catch (\Exception $e) {
-      $this->messenger()->addError($this->t('There was a problem sending the notifications.'));
-      \Drupal::logger('page_notifications')->error($e->getMessage());
-    }
-  }
-
-}
-```
-
-# src/Form/ManualSubscriptionAddForm.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Messenger\MessengerInterface;
-use Drupal\Component\Utility\EmailValidatorInterface;
-
-/**
- * Form for manually adding subscriptions.
- */
-class ManualSubscriptionAddForm extends FormBase {
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * The messenger service.
-   *
-   * @var \Drupal\Core\Messenger\MessengerInterface
-   */
-  protected $messenger;
-
-  /**
-   * The email validator.
-   *
-   * @var \Drupal\Component\Utility\EmailValidatorInterface
-   */
-  protected $emailValidator;
-
-  /**
-   * Constructs a new ManualSubscriptionAddForm.
-   */
-  public function __construct(
-    EntityTypeManagerInterface $entity_type_manager,
-    MessengerInterface $messenger,
-    EmailValidatorInterface $email_validator
-  ) {
-    $this->entityTypeManager = $entity_type_manager;
-    $this->messenger = $messenger;
-    $this->emailValidator = $email_validator;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('entity_type.manager'),
-      $container->get('messenger'),
-      $container->get('email.validator')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_manual_subscription_add';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $form['node'] = [
-      '#type' => 'entity_autocomplete',
-      '#title' => $this->t('Content'),
-      '#description' => $this->t('Select the content to subscribe to.'),
-      '#target_type' => 'node',
-      '#required' => TRUE,
-      '#selection_handler' => 'default:node_enhanced',
-      '#selection_settings' => [
-        'target_bundles' => NULL,
-      ],
-    ];
-
-    $form['emails'] = [
-      '#type' => 'textarea',
-      '#title' => $this->t('Email Addresses'),
-      '#description' => $this->t('Enter email addresses, one per line. These subscribers will be automatically verified.'),
-      '#required' => TRUE,
-      '#rows' => 10,
-    ];
-
-    $form['actions'] = [
-      '#type' => 'actions',
-    ];
-
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Add Subscriptions'),
-      '#button_type' => 'primary',
-    ];
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    $emails = explode("\n", $form_state->getValue('emails'));
-    $invalid_emails = [];
-
-    foreach ($emails as $email) {
-      $email = trim($email);
-      if (!empty($email) && !$this->emailValidator->isValid($email)) {
-        $invalid_emails[] = $email;
-      }
-    }
-
-    if (!empty($invalid_emails)) {
-      $form_state->setErrorByName('emails', $this->t('The following email addresses are invalid: @emails', [
-        '@emails' => implode(', ', $invalid_emails),
-      ]));
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $node_id = $form_state->getValue('node');
-    $emails = array_filter(array_map('trim', explode("\n", $form_state->getValue('emails'))));
-    $added = 0;
-    $skipped = 0;
-
-    try {
-      foreach ($emails as $email) {
-        if (empty($email)) {
-          continue;
-        }
-
-        // Check for existing subscription
-        $existing = $this->entityTypeManager
-          ->getStorage('page_notification_subscription')
-          ->loadByProperties([
-            'email' => $email,
-            'subscribed_entity_id' => $node_id,
-            'subscribed_entity_type' => 'node',
-          ]);
-
-        if (!empty($existing)) {
-          $skipped++;
-          continue;
-        }
-
-        // Create new subscription
-        $subscription = $this->entityTypeManager
-          ->getStorage('page_notification_subscription')
-          ->create([
-            'email' => $email,
-            'subscribed_entity_id' => $node_id,
-            'subscribed_entity_type' => 'node',
-            'token' => bin2hex(random_bytes(32)),
-            'unsubscribe_token' => bin2hex(random_bytes(32)),
-            'status' => TRUE, // Automatically verified
-          ]);
-
-        $subscription->save();
-        $added++;
-      }
-
-      if ($added > 0) {
-        $this->messenger->addStatus($this->t('Successfully added @count subscription(s).', [
-          '@count' => $added,
-        ]));
-      }
-
-      if ($skipped > 0) {
-        $this->messenger->addWarning($this->t('Skipped @count existing subscription(s).', [
-          '@count' => $skipped,
-        ]));
-      }
-    }
-    catch (\Exception $e) {
-      $this->messenger->addError($this->t('An error occurred while adding subscriptions.'));
-      $this->getLogger('page_notifications')->error($e->getMessage());
-    }
-  }
-
-}
-```
-
-# src/Form/ModalSubscriptionForm.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\page_notifications\Service\NotificationManagerInterface;
-use Drupal\page_notifications\Service\SpamPrevention;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Psr\Log\LoggerInterface;
-use Drupal\Core\Flood\FloodInterface;
-use Drupal\page_notifications\Traits\FloodControlTrait;
-use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Ajax\CloseModalDialogCommand;
-use Drupal\Core\Ajax\MessageCommand;
-use Drupal\Core\Ajax\ReplaceCommand;
-
-/**
- * Provides a subscription form for modal display.
- */
-class ModalSubscriptionForm extends FormBase {
-  use FloodControlTrait;
-
-  /**
-   * The notification manager service.
-   *
-   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
-   */
-  protected $notificationManager;
-
-  /**
-   * The spam prevention service.
-   *
-   * @var \Drupal\page_notifications\Service\SpamPrevention
-   */
-  protected $spamPrevention;
-
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The logger instance.
-   *
-   * @var \Psr\Log\LoggerInterface
-   */
-  protected $logger;
-
-  /**
-   * Constructs a new ModalSubscriptionForm.
-   */
-  public function __construct(
-    NotificationManagerInterface $notification_manager,
-    SpamPrevention $spam_prevention,
-    ConfigFactoryInterface $config_factory,
-    LoggerInterface $logger,
-    FloodInterface $flood
-  ) {
-    $this->notificationManager = $notification_manager;
-    $this->spamPrevention = $spam_prevention;
-    $this->configFactory = $config_factory;
-    $this->logger = $logger;
-    $this->setFloodService($flood);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('page_notifications.notification_manager'),
-      $container->get('page_notifications.spam_prevention'),
-      $container->get('config.factory'),
-      $container->get('logger.factory')->get('page_notifications'),
-      $container->get('flood')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_modal_subscription_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL) {
-    if (!$entity) {
-      return [
-        '#markup' => $this->t('No content found for subscription.'),
-      ];
-    }
-
-    $form['#prefix'] = '<div id="modal-subscription-form-wrapper">';
-    $form['#suffix'] = '</div>';
-
-    // Store the entity in the form state
-    $form_state->set('entity', $entity);
-
-    $form['email'] = [
-      '#type' => 'email',
-      '#title' => $this->t('Email address'),
-      '#required' => TRUE,
-      '#description' => $this->t('Enter your email address to receive notifications when this content is updated.'),
-    ];
-
-    // Add spam prevention based on configuration
-    $config = $this->configFactory->get('page_notifications.settings');
-    $captcha_type = $config->get('spam_prevention.captcha_type');
-
-    if ($captcha_type === 'math') {
-      if (!$form_state->getUserInput()) {
-        $challenge = $this->spamPrevention->generateMathChallenge();
-        $form['math_challenge_data'] = [
-          '#type' => 'hidden',
-          '#value' => json_encode($challenge),
-        ];
-      }
-      else {
-        $challenge = json_decode($form_state->getUserInput()['math_challenge_data'] ?? '{}', TRUE);
-      }
-
-      if (!empty($challenge)) {
-        $form['math_challenge_data'] = [
-          '#type' => 'hidden',
-          '#value' => json_encode($challenge),
-        ];
-
-        $form['math_challenge'] = [
-          '#type' => 'number',
-          '#title' => $challenge['question'],
-          '#required' => TRUE,
-          '#description' => $this->t('Please solve this simple math problem to prevent spam.'),
-        ];
-      }
-    }
-    elseif ($captcha_type === 'recaptcha' && $this->spamPrevention->isRecaptchaAvailable()) {
-      $form['captcha'] = [
-        '#type' => 'captcha',
-        '#captcha_type' => 'recaptcha/reCAPTCHA',
-      ];
-    }
-
-    $form['actions'] = [
-      '#type' => 'actions',
-    ];
-
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Subscribe'),
-      '#ajax' => [
-        'callback' => '::submitModalAjax',
-        'event' => 'click',
-        'progress' => [
-          'type' => 'throbber',
-          'message' => $this->t('Processing...'),
-        ],
-      ],
-    ];
-
-    return $form;
-  }
-
-  /**
-   * AJAX callback for modal form submission.
-   */
-  public function submitModalAjax(array &$form, FormStateInterface $form_state) {
-    $response = new AjaxResponse();
-
-    if ($form_state->hasAnyErrors()) {
-      $response->addCommand(new ReplaceCommand(
-        '#modal-subscription-form-wrapper',
-        [
-          '#type' => 'container',
-          '#attributes' => ['id' => 'modal-subscription-form-wrapper'],
-          'status_messages' => [
-            '#type' => 'status_messages',
-          ],
-          'form' => $form,
-        ]
-      ));
-      return $response;
-    }
-
-    try {
-      $entity = $form_state->get('entity');
-      if (!$entity) {
-        throw new \Exception('No entity found for subscription.');
-      }
-
-      $email = $form_state->getValue('email');
-      $subscription = $this->notificationManager->createSubscription($email, $entity);
-
-      $response->addCommand(new CloseModalDialogCommand());
-      $response->addCommand(new MessageCommand(
-        $this->t('Thank you for subscribing. Please check your email to confirm your subscription.'),
-        NULL,
-        ['type' => 'status']
-      ));
-    }
-    catch (\Exception $e) {
-      $this->logger->error('Subscription error: @message', ['@message' => $e->getMessage()]);
-
-      $response->addCommand(new ReplaceCommand(
-        '#modal-subscription-form-wrapper',
-        [
-          '#type' => 'container',
-          '#attributes' => ['id' => 'modal-subscription-form-wrapper'],
-          'status_messages' => [
-            '#type' => 'status_messages',
-            '#message_list' => [
-              'error' => [$this->t('There was a problem creating your subscription. Please try again later.')],
-            ],
-          ],
-          'form' => $form,
-        ]
-      ));
-    }
-
-    return $response;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    $email = $form_state->getValue('email');
-
-    // Check flood control before other validation
-    if (!$this->checkFloodControl($email, $form_state)) {
-      return;
-    }
-
-    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
-      $form_state->setErrorByName('email', $this->t('Please enter a valid email address.'));
-      return;
-    }
-
-    // Validate math challenge if enabled
-    $config = $this->configFactory->get('page_notifications.settings');
-    $captcha_type = $config->get('spam_prevention.captcha_type');
-
-    if ($captcha_type === 'math') {
-      $challenge_data = $form_state->getValue('math_challenge_data');
-      if ($challenge_data) {
-        $challenge = json_decode($challenge_data, TRUE);
-        $response = $form_state->getValue('math_challenge');
-
-        if (!$this->spamPrevention->validateMathResponse($response, $challenge)) {
-          $form_state->setErrorByName('math_challenge', $this->t('The answer to the math challenge is incorrect.'));
-        }
-      }
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    // Empty as everything is handled in the AJAX callback
-  }
-
-}
-```
-
-# src/Form/PurgeSubscriptionsConfirmForm.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\ConfirmFormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-class PurgeSubscriptionsConfirmForm extends ConfirmFormBase {
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * Constructs a new PurgeSubscriptionsConfirmForm.
-   */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
-    $this->entityTypeManager = $entity_type_manager;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('entity_type.manager')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_purge_subscriptions_confirm';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getQuestion() {
-    $count = $this->entityTypeManager
-      ->getStorage('page_notification_subscription')
-      ->getQuery()
-      ->accessCheck(FALSE)
-      ->count()
-      ->execute();
-
-    return $this->t('Are you sure you want to delete all @count subscriptions?', [
-      '@count' => $count,
-    ]);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getDescription() {
-    return $this->t('This action cannot be undone. All subscription data will be permanently deleted.');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCancelUrl() {
-    return new Url('page_notifications.settings');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $batch = [
-      'title' => $this->t('Deleting all subscriptions...'),
-      'operations' => [
-        [[$this, 'purgeSubscriptionsBatch'], []],
-      ],
-      'finished' => [[$this, 'purgeSubscriptionsFinished']],
-    ];
-    batch_set($batch);
-    $form_state->setRedirect('page_notifications.settings');
-  }
-
-  /**
-   * Batch operation to purge subscriptions.
-   */
-  public function purgeSubscriptionsBatch(&$context) {
-    if (!isset($context['sandbox']['progress'])) {
-      $context['sandbox']['progress'] = 0;
-      $context['sandbox']['current_id'] = 0;
-      $context['sandbox']['max'] = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->getQuery()
-        ->accessCheck(FALSE)
-        ->count()
-        ->execute();
-    }
-
-    $subscription_ids = $this->entityTypeManager
-      ->getStorage('page_notification_subscription')
-      ->getQuery()
-      ->condition('id', $context['sandbox']['current_id'], '>')
-      ->sort('id')
-      ->range(0, 50)
-      ->accessCheck(FALSE)
-      ->execute();
-
-    if (!empty($subscription_ids)) {
-      $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
-      $entities = $storage->loadMultiple($subscription_ids);
-      $storage->delete($entities);
-
-      $context['sandbox']['current_id'] = end($subscription_ids);
-      $context['sandbox']['progress'] += count($subscription_ids);
-    }
-
-    $context['finished'] = empty($subscription_ids) ? 1 : $context['sandbox']['progress'] / $context['sandbox']['max'];
-  }
-
-  /**
-   * Batch finished callback.
-   */
-  public function purgeSubscriptionsFinished($success, $results, $operations) {
-    if ($success) {
-      $this->messenger()->addStatus($this->t('Successfully deleted all subscriptions.'));
-    }
-    else {
-      $this->messenger()->addError($this->t('An error occurred while deleting subscriptions.'));
-    }
-  }
-}
-```
-
-# src/Form/SettingsForm.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\ConfigFormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Mail\MailManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\filter\FilterFormatInterface;
-use Drupal\Core\Url;
-use Drupal\Core\Link;
-
-/**
- * Configures Page Notifications settings.
- */
-class SettingsForm extends ConfigFormBase {
-
-  /**
-   * The mail manager.
-   *
-   * @var \Drupal\Core\Mail\MailManagerInterface
-   */
-  protected $mailManager;
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * The module handler service.
-   *
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface
-   */
-  protected $moduleHandler;
-
-  /**
-   * Constructs a SettingsForm object.
-   *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The factory for configuration objects.
-   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
-   *   The mail manager service.
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity type manager.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *  The module handler service.
-   */
-  public function __construct(
-    ConfigFactoryInterface $config_factory,
-    MailManagerInterface $mail_manager,
-    EntityTypeManagerInterface $entity_type_manager,
-    ModuleHandlerInterface $module_handler
-  ) {
-    // Check Drupal version
-    if (version_compare(\Drupal::VERSION, '11.0', '>=')) {
-      parent::__construct($config_factory, \Drupal::service('config.typed'));
-  } else {
-      parent::__construct($config_factory);
-  }
-    $this->mailManager = $mail_manager;
-    $this->entityTypeManager = $entity_type_manager;
-    $this->moduleHandler = $module_handler;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('config.factory'),
-      $container->get('plugin.manager.mail'),
-      $container->get('entity_type.manager'),
-      $container->get('module_handler')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_settings';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getEditableConfigNames() {
-    return ['page_notifications.settings'];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $form = parent::buildForm($form, $form_state);
-    $config = $this->config('page_notifications.settings');
-
-    // Get available text formats for the current user
-    $formats = filter_formats(\Drupal::currentUser());
-    $format_options = [];
-    foreach ($formats as $format) {
-      $format_options[$format->id()] = $format->label();
-    }
-
-
-    $form['email_settings'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Email Settings'),
-      '#open' => TRUE,
-    ];
-
-    // text format selection
-    $form['email_settings']['mail_format'] = [
-      '#type' => 'select',
-      '#title' => $this->t('Email Text Format'),
-      '#description' => $this->t('Select the text format to use for email content. Ensure the chosen format allows necessary HTML tags for links and formatting.'),
-      '#options' => $format_options,
-      '#default_value' => $config->get('email_settings.mail_format') ?? reset($format_options),
-      '#required' => TRUE,
-    ];
-
-    $selected_format = $config->get('email_settings.mail_format') ?? reset($format_options);
-    $verification_body = $config->get('email_templates.verification_body');
-    $notification_body = $config->get('email_templates.notification_body');
-    $already_subscribed_body = $config->get('email_templates.already_subscribed_body');
-
-    $form['email_settings']['from_email'] = [
-      '#type' => 'email',
-      '#title' => $this->t('From Email Address'),
-      '#description' => $this->t('The email address that notifications will be sent from. If left empty, the site default will be used.'),
-      '#default_value' => $config->get('notification_settings.from_email'),
-    ];
-
-    $form['email_settings']['token_expiration'] = [
-      '#type' => 'number',
-      '#title' => $this->t('Token Expiration'),
-      '#description' => $this->t('Number of hours before verification tokens expire. Enter 0 to never expire unverified subscriptions.'),
-      '#default_value' => $config->get('notification_settings.token_expiration') ?? 48,
-      '#min' => 1,
-      '#required' => TRUE,
-    ];
-
-    $form['email_templates'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Email Templates'),
-      '#open' => TRUE,
-    ];
-
-    $form['email_templates']['verification_subject'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Verification Email Subject'),
-      '#default_value' => $config->get('email_templates.verification_subject') ?? 'Verify your subscription to [node:title]',
-      '#required' => TRUE,
-    ];
-
-    $form['email_templates']['verification_body'] = [
-      '#type' => 'text_format',
-      '#title' => $this->t('Verification Email Body'),
-      '#default_value' => is_array($verification_body) ? $verification_body['value'] : $verification_body,
-      '#format' => is_array($verification_body) ? $verification_body['format'] : $selected_format,
-      '#description' => $this->t('Available tokens: [subscription:verify-url], [subscription:email], [node:title], [node:url]'),
-      '#required' => TRUE,
-      '#rows' => 10,
-    ];
-
-    $form['email_templates']['notification_subject'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Update Notification Subject'),
-      '#default_value' => $config->get('email_templates.notification_subject') ?? '[node:title] has been updated',
-      '#required' => TRUE,
-    ];
-
-    $form['email_templates']['notification_body'] = [
-      '#type' => 'text_format',
-      '#title' => $this->t('Update Notification Body'),
-      '#default_value' => is_array($notification_body) ? $notification_body['value'] : $notification_body,
-      '#format' => is_array($notification_body) ? $notification_body['format'] : $selected_format,
-      '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [node:changed], [subscription:unsubscribe-url]'),
-      '#required' => TRUE,
-      '#rows' => 10,
-    ];
-
-    $form['email_templates']['already_subscribed_subject'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Already Subscribed Email Subject'),
-      '#default_value' => $config->get('email_templates.already_subscribed_subject') ?? 'You are already subscribed to [node:title]',
-      '#required' => TRUE,
-    ];
-
-    $form['email_templates']['already_subscribed_body'] = [
-      '#type' => 'text_format',
-      '#title' => $this->t('Already Subscribed Email Body'),
-      '#default_value' => is_array($already_subscribed_body) ? $already_subscribed_body['value'] : $already_subscribed_body,
-      '#format' => is_array($already_subscribed_body) ? $already_subscribed_body['format'] : $selected_format,
-      '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [subscription:unsubscribe-url]'),
-      '#required' => TRUE,
-      '#rows' => 10,
-    ];
-
-    $form['security'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Security Settings'),
-      '#open' => TRUE,
-    ];
-
-    $form['security']['require_verification'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Require Email Verification'),
-      '#description' => $this->t('If checked, users must verify their email address before the subscription becomes active.'),
-      '#default_value' => $config->get('security.require_verification') ?? TRUE,
-    ];
-
-     // Flood control settings
-     $form['security']['flood_control'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Flood Control Settings'),
-      '#open' => TRUE,
-      '#tree' => TRUE,
-    ];
-
-    $form['security']['flood_control']['ip_limit'] = [
-      '#type' => 'number',
-      '#title' => $this->t('IP-based attempt limit'),
-      '#description' => $this->t('Maximum number of subscription attempts allowed from a single IP address.'),
-      '#default_value' => $config->get('security.flood_control.ip_limit') ?? 200,
-      '#min' => 1,
-      '#required' => TRUE,
-    ];
-
-    $form['security']['flood_control']['ip_window'] = [
-      '#type' => 'number',
-      '#title' => $this->t('IP-based time window'),
-      '#description' => $this->t('Time window in hours for IP-based subscription attempts. Set to 0 to disable IP-based flood control.'),
-      '#default_value' => $config->get('security.flood_control.ip_window') ?? 1,
-      '#min' => 0, // Changed from 1 to 0
-      '#required' => TRUE,
-      '#field_suffix' => $this->t('hours'),
-    ];
-
-    $form['security']['flood_control']['identifier_limit'] = [
-      '#type' => 'number',
-      '#title' => $this->t('Email-based attempt limit'),
-      '#description' => $this->t('Maximum number of subscription attempts allowed for the same email address.'),
-      '#default_value' => $config->get('security.flood_control.identifier_limit') ?? 50,
-      '#min' => 1,
-      '#required' => TRUE,
-    ];
-
-    $form['security']['flood_control']['identifier_window'] = [
-      '#type' => 'number',
-      '#title' => $this->t('Email-based time window'),
-      '#description' => $this->t('Time window in hours for email-based subscription attempts. Set to 0 to disable email-based flood control.'),
-      '#default_value' => $config->get('security.flood_control.identifier_window') ?? 1,
-      '#min' => 0, // Changed from 1 to 0
-      '#required' => TRUE,
-      '#field_suffix' => $this->t('hours'),
-    ];
-
-    // Add spam prevention section
-    $form['spam_prevention'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Spam Prevention'),
-      '#open' => TRUE,
-    ];
-
-    $captcha_options = [
-      'none' => $this->t('None'),
-      'math' => $this->t('Simple Math Challenge'),
-    ];
-
-    // Add reCAPTCHA option if the captcha module is installed
-    if ($this->moduleHandler->moduleExists('captcha')) {
-      $captcha_options['recaptcha'] = $this->t('reCAPTCHA');
-    }
-
-    $form['spam_prevention']['captcha_type'] = [
-      '#type' => 'select',
-      '#title' => $this->t('Captcha Type'),
-      '#options' => $captcha_options,
-      '#default_value' => $config->get('spam_prevention.captcha_type') ?? 'none',
-      '#description' => $this->t('Select the type of spam prevention to use on the subscription form.'),
-    ];
-
-    $form['spam_prevention']['math_operator'] = [
-      '#type' => 'select',
-      '#title' => $this->t('Math Challenge Operator'),
-      '#options' => [
-        '+' => $this->t('Addition (+)'),
-        '*' => $this->t('Multiplication (*)'),
-      ],
-      '#default_value' => $config->get('spam_prevention.math_operator') ?? '+',
-      '#description' => $this->t('Select the operator to use for the math challenge.'),
-      '#states' => [
-        'visible' => [
-          ':input[name="captcha_type"]' => ['value' => 'math'],
-        ],
-      ],
-    ];
-
-    if ($this->moduleHandler->moduleExists('captcha')) {
-      $form['spam_prevention']['use_recaptcha'] = [
-        '#type' => 'checkbox',
-        '#title' => $this->t('Use reCAPTCHA if available'),
-        '#default_value' => $config->get('spam_prevention.use_recaptcha') ?? FALSE,
-        '#description' => $this->t('Enable this to use reCAPTCHA if the captcha module is configured to use it.'),
-        '#states' => [
-          'visible' => [
-            ':input[name="captcha_type"]' => ['value' => 'recaptcha'],
-          ],
-        ],
-      ];
-    }
-
-    $form['danger_zone'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Danger Zone'),
-      '#description' => $this->t('These actions cannot be undone.'),
-      '#open' => FALSE,
-      '#weight' => 100,
-    ];
-
-    $subscription_count = $this->entityTypeManager
-      ->getStorage('page_notification_subscription')
-      ->getQuery()
-      ->accessCheck(FALSE)
-      ->count()
-      ->execute();
-
-      $form['danger_zone']['purge_subscriptions'] = [
-        '#type' => 'link',
-        '#title' => $this->t('Delete all subscriptions (@count total)', ['@count' => $subscription_count]),
-        '#url' => Url::fromRoute('page_notifications.purge_subscriptions_confirm'),
-        '#attributes' => [
-          'class' => ['button', 'button--danger', 'use-ajax'],
-          'data-dialog-type' => 'modal',
-          'data-dialog-options' => json_encode([
-            'width' => 700,
-          ]),
-        ],
-        '#disabled' => ($subscription_count === 0),
-      ];
-
-    return parent::buildForm($form, $form_state);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    if ($form_state->getValue('from_email') && !$this->mailManager->validateAddress($form_state->getValue('from_email'))) {
-      $form_state->setErrorByName('from_email', $this->t('The email address is not valid.'));
-    }
-  }
-
-  public function purgeSubscriptions(array &$form, FormStateInterface $form_state) {
-    $batch = [
-      'title' => $this->t('Deleting all subscriptions...'),
-      'operations' => [
-        [[$this, 'purgeSubscriptionsBatch'], []],
-      ],
-      'finished' => [[$this, 'purgeSubscriptionsFinished']],
-    ];
-    batch_set($batch);
-  }
-
-  public function purgeSubscriptionsBatch(&$context) {
-    if (!isset($context['sandbox']['progress'])) {
-      $context['sandbox']['progress'] = 0;
-      $context['sandbox']['current_id'] = 0;
-      $context['sandbox']['max'] = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->getQuery()
-        ->accessCheck(FALSE)
-        ->count()
-        ->execute();
-    }
-
-    // Process subscriptions in chunks of 50
-    $subscription_ids = $this->entityTypeManager
-      ->getStorage('page_notification_subscription')
-      ->getQuery()
-      ->condition('id', $context['sandbox']['current_id'], '>')
-      ->sort('id')
-      ->range(0, 50)
-      ->accessCheck(FALSE)
-      ->execute();
-
-    if (!empty($subscription_ids)) {
-      $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
-      $entities = $storage->loadMultiple($subscription_ids);
-      $storage->delete($entities);
-
-      $context['sandbox']['current_id'] = end($subscription_ids);
-      $context['sandbox']['progress'] += count($subscription_ids);
-    }
-
-    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
-  }
-
-  public function purgeSubscriptionsFinished($success, $results, $operations) {
-    if ($success) {
-      $this->messenger()->addStatus($this->t('Successfully deleted all subscriptions.'));
-    }
-    else {
-      $this->messenger()->addError($this->t('An error occurred while deleting subscriptions.'));
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $values = $form_state->getValues();
-
-    $this->config('page_notifications.settings')
-      ->set('notification_settings.from_email', $values['from_email'])
-      ->set('notification_settings.token_expiration', $values['token_expiration'])
-      ->set('email_settings.mail_format', $values['mail_format'])
-      ->set('email_templates.verification_subject', $values['verification_subject'])
-      ->set('email_templates.verification_body', $values['verification_body'])
-      ->set('email_templates.already_subscribed_subject', $values['already_subscribed_subject'])
-->set('email_templates.already_subscribed_body', $values['already_subscribed_body'])
-      ->set('email_templates.notification_subject', $values['notification_subject'])
-      ->set('email_templates.notification_body', $values['notification_body'])
-      ->set('security.require_verification', $values['require_verification'])
-      ->set('security.flood_control.ip_limit', $values['flood_control']['ip_limit'])
-      ->set('security.flood_control.ip_window', $values['flood_control']['ip_window'])
-      ->set('security.flood_control.identifier_limit', $values['flood_control']['identifier_limit'])
-      ->set('security.flood_control.identifier_window', $values['flood_control']['identifier_window'])
-      ->set('spam_prevention.captcha_type', $values['captcha_type'])
-      ->set('spam_prevention.math_operator', $values['math_operator'])
-      ->set('spam_prevention.use_recaptcha', $values['use_recaptcha'] ?? FALSE)
-      ->save();
-
-    parent::submitForm($form, $form_state);
-  }
-
-}
-```
-
-# src/Form/SubscriptionDeleteForm.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Entity\ContentEntityDeleteForm;
-
-class SubscriptionDeleteForm extends ContentEntityDeleteForm {
-}
-```
-
-# src/Form/SubscriptionForm.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\page_notifications\Service\NotificationManagerInterface;
-use Drupal\page_notifications\Service\SpamPrevention;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Psr\Log\LoggerInterface;
-use Drupal\Core\Flood\FloodInterface;
-use Drupal\page_notifications\Traits\FloodControlTrait;
-
-/**
- * Provides a subscription form.
- */
-class SubscriptionForm extends FormBase {
-  use FloodControlTrait;
-
-  /**
-   * The notification manager service.
-   *
-   * @var \Drupal\page_notifications\Service\NotificationManagerInterface
-   */
-  protected $notificationManager;
-
-  /**
-   * The spam prevention service.
-   *
-   * @var \Drupal\page_notifications\Service\SpamPrevention
-   */
-  protected $spamPrevention;
-
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The logger instance.
-   *
-   * @var \Psr\Log\LoggerInterface
-   */
-  protected $logger;
-
-  /**
-   * Constructs a new SubscriptionForm.
-   *
-   * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager
-   *   The notification manager service.
-   * @param \Drupal\page_notifications\Service\SpamPrevention $spam_prevention
-   *   The spam prevention service.
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The config factory service.
-   * @param \Psr\Log\LoggerInterface $logger
-   *   The logger instance.
-   * @param \Drupal\Core\Flood\FloodInterface $flood
-   *   The flood service.
-   */
-  public function __construct(
-    NotificationManagerInterface $notification_manager,
-    SpamPrevention $spam_prevention,
-    ConfigFactoryInterface $config_factory,
-    LoggerInterface $logger,
-    FloodInterface $flood
-  ) {
-    $this->notificationManager = $notification_manager;
-    $this->spamPrevention = $spam_prevention;
-    $this->logger = $logger;
-    $this->configFactory = $config_factory;
-    $this->setFloodService($flood);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('page_notifications.notification_manager'),
-      $container->get('page_notifications.spam_prevention'),
-      $container->get('config.factory'),
-      $container->get('logger.factory')->get('page_notifications'),
-      $container->get('flood')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_subscription_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL) {
-    $form_state->set('entity', $entity);
-
-    $form['email'] = [
-      '#type' => 'email',
-      '#title' => $this->t('Email address'),
-      '#required' => TRUE,
-      '#description' => $this->t('Enter your email address to receive notifications when this content is updated.'),
-    ];
-
-    // Add spam prevention based on configuration
-    $config = $this->configFactory->get('page_notifications.settings');
-    $captcha_type = $config->get('spam_prevention.captcha_type');
-
-    if ($captcha_type === 'math') {
-      // Store challenge data in a hidden field to persist through form submissions
-      if (!$form_state->getUserInput()) {
-        // Only generate new challenge if this is the initial form build
-        $challenge = $this->spamPrevention->generateMathChallenge();
-
-        $form['math_challenge_data'] = [
-          '#type' => 'hidden',
-          '#value' => json_encode($challenge),
-        ];
-      }
-      else {
-        // Use existing challenge data from form input
-        $challenge = json_decode($form_state->getUserInput()['math_challenge_data'] ?? '{}', TRUE);
-      }
-
-      if (!empty($challenge)) {
-        $form['math_challenge_data'] = [
-          '#type' => 'hidden',
-          '#value' => json_encode($challenge),
-        ];
-
-        $form['math_challenge'] = [
-          '#type' => 'number',
-          '#title' => $challenge['question'],
-          '#required' => TRUE,
-          '#description' => $this->t('Please solve this simple math problem to prevent spam.'),
-        ];
-      }
-    }
-    elseif ($captcha_type === 'recaptcha' && $this->spamPrevention->isRecaptchaAvailable()) {
-      $form['captcha'] = [
-        '#type' => 'captcha',
-        '#captcha_type' => 'recaptcha/reCAPTCHA',
-      ];
-    }
-
-    $form['actions'] = [
-      '#type' => 'actions',
-    ];
-
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Subscribe'),
-    ];
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    $email = $form_state->getValue('email');
-
-    // Check flood control before other validation
-    if (!$this->checkFloodControl($email, $form_state)) {
-      return;
-    }
-
-    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
-      $form_state->setErrorByName('email', $this->t('Please enter a valid email address.'));
-      return;
-    }
-
-    // Validate math challenge if enabled
-    $config = $this->configFactory->get('page_notifications.settings');
-    $captcha_type = $config->get('spam_prevention.captcha_type');
-
-    if ($captcha_type === 'math') {
-      $challenge_data = $form_state->getValue('math_challenge_data');
-      if ($challenge_data) {
-        $challenge = json_decode($challenge_data, TRUE);
-        $response = $form_state->getValue('math_challenge');
-
-        if (!$this->spamPrevention->validateMathResponse($response, $challenge)) {
-          $form_state->setErrorByName('math_challenge', $this->t('The answer to the math challenge is incorrect.'));
-        }
-      }
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $entity = $form_state->get('entity');
-    $email = $form_state->getValue('email');
-
-    try {
-      // Register flood control event
-      $this->registerFloodControl($email);
-
-      $subscription = $this->notificationManager->createSubscription($email, $entity);
-      $this->messenger()->addStatus($this->t('Thank you for subscribing. Please check your email to confirm your subscription.'));
-
-      // Clear the email field after successful submission
-      $form_state->setValue('email', '');
-      $form_state->setUserInput(['email' => '']);
-    }
-    catch (\Exception $e) {
-      $this->messenger()->addError($this->t('There was a problem creating your subscription. Please try again later.'));
-      $this->logger->error('Subscription creation failed: @message', ['@message' => $e->getMessage()]);
-    }
-
-    // Set form to rebuild
-    $form_state->setRebuild(TRUE);
-  }
-
-}
-```
-
-# src/Form/SubscriptionMigrateForm.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Form;
-
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Form for migrating subscriptions between nodes.
- */
-class SubscriptionMigrateForm extends FormBase {
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * Constructs a new SubscriptionMigrateForm.
-   *
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity type manager.
-   */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
-    $this->entityTypeManager = $entity_type_manager;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('entity_type.manager')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'page_notifications_subscription_migrate_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    // Check if there are any nodes with subscriptions
-    $subscription_count = $this->entityTypeManager
-      ->getStorage('page_notification_subscription')
-      ->getQuery()
-      ->condition('subscribed_entity_type', 'node')
-      ->condition('status', TRUE)
-      ->count()
-      ->accessCheck(FALSE)
-      ->execute();
-
-    if ($subscription_count === 0) {
-      $form['message'] = [
-        '#markup' => $this->t('There are no nodes with active subscriptions.'),
-      ];
-      return $form;
-    }
-
-    $form['description'] = [
-      '#markup' => $this->t('This form will migrate all active subscriptions from one node to another.'),
-    ];
-
-    $form['source_node'] = [
-      '#type' => 'entity_autocomplete',
-      '#title' => $this->t('FROM: Source Node'),
-      '#description' => $this->t('Select the node from which to migrate subscriptions.'),
-      '#target_type' => 'node',
-      '#required' => TRUE,
-      '#selection_handler' => 'default:node_with_subscriptions',
-    ];
-
-    $form['target_node'] = [
-      '#type' => 'entity_autocomplete',
-      '#title' => $this->t('TO: Target Node'),
-      '#description' => $this->t('Select the node to which subscriptions will be migrated.'),
-      '#target_type' => 'node',
-      '#required' => TRUE,
-      '#selection_handler' => 'default:node_enhanced',
-      '#selection_settings' => [
-        'target_bundles' => NULL,
-      ],
-    ];
-
-    $form['actions']['#type'] = 'actions';
-    $form['actions']['submit'] = [
-      '#type' => 'submit',
-      '#value' => $this->t('Migrate Subscriptions'),
-      '#button_type' => 'primary',
-    ];
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    $source_nid = $form_state->getValue('source_node');
-    $target_nid = $form_state->getValue('target_node');
-
-    if ($source_nid === $target_nid) {
-      $form_state->setError($form['target_node'], $this->t('Source and target nodes must be different.'));
-      return;
-    }
-
-    // Verify the source node still has subscriptions (in case they were deleted)
-    $subscription_count = $this->entityTypeManager
-      ->getStorage('page_notification_subscription')
-      ->getQuery()
-      ->condition('subscribed_entity_id', $source_nid)
-      ->condition('subscribed_entity_type', 'node')
-      ->condition('status', TRUE)
-      ->count()
-      ->accessCheck(FALSE)
-      ->execute();
-
-    if ($subscription_count === 0) {
-      $form_state->setError($form['source_node'], $this->t('The source node has no active subscriptions to migrate.'));
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    $source_nid = $form_state->getValue('source_node');
-    $target_nid = $form_state->getValue('target_node');
-
-    try {
-      $batch = [
-        'title' => $this->t('Migrating subscriptions'),
-        'operations' => [
-          [
-            [$this, 'processMigration'],
-            [$source_nid, $target_nid],
-          ],
-        ],
-        'finished' => [$this, 'migrationFinished'],
-      ];
-
-      batch_set($batch);
-    }
-    catch (\Exception $e) {
-      $this->messenger()->addError($this->t('An error occurred while preparing the migration: @error', [
-        '@error' => $e->getMessage(),
-      ]));
-    }
-  }
-
-  /**
-   * Batch operation callback for migrating subscriptions.
-   */
-  public function processMigration($source_nid, $target_nid, &$context) {
-    if (!isset($context['sandbox']['progress'])) {
-      $context['sandbox']['progress'] = 0;
-      $context['sandbox']['current_id'] = 0;
-      $context['sandbox']['max'] = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->getQuery()
-        ->condition('subscribed_entity_id', $source_nid)
-        ->condition('subscribed_entity_type', 'node')
-        ->count()
-        ->accessCheck(FALSE)
-        ->execute();
-    }
-
-    // Process subscriptions in chunks of 50
-    $subscription_ids = $this->entityTypeManager
-      ->getStorage('page_notification_subscription')
-      ->getQuery()
-      ->condition('subscribed_entity_id', $source_nid)
-      ->condition('subscribed_entity_type', 'node')
-      ->condition('id', $context['sandbox']['current_id'], '>')
-      ->sort('id')
-      ->range(0, 50)
-      ->accessCheck(FALSE)
-      ->execute();
-
-    foreach ($subscription_ids as $id) {
-      /** @var \Drupal\page_notifications\Entity\Subscription $subscription */
-      $subscription = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->load($id);
-
-      // Simply update the entity ID
-      $subscription->setSubscribedEntityId($target_nid);
-      $subscription->save();
-
-      $context['sandbox']['progress']++;
-      $context['sandbox']['current_id'] = $id;
-    }
-
-    if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
-      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
-    }
-  }
-
-  /**
-   * Batch finished callback.
-   */
-  public function migrationFinished($success, $results, $operations) {
-    if ($success) {
-      $this->messenger()->addStatus($this->t('Successfully migrated all subscriptions to the new node.'));
-    }
-    else {
-      $this->messenger()->addError($this->t('An error occurred while migrating subscriptions.'));
-    }
-  }
-
-}
-```
-
-# src/Mail/PageNotificationsMailHandler.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Mail;
-
-use Drupal\Core\Mail\MailManagerInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Render\RendererInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\StringTranslation\TranslationInterface;
-use Drupal\token\TokenInterface;
-use Drupal\Core\Theme\ThemeManagerInterface;
-
-
-/**
- * Handles mail formatting for page notifications.
- */
-class PageNotificationsMailHandler {
-
-  use StringTranslationTrait;
-
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The renderer service.
-   *
-   * @var \Drupal\Core\Render\RendererInterface
-   */
-  protected $renderer;
-
-  /**
-   * The token service.
-   *
-   * @var \Drupal\token\TokenServiceInterface
-   */
-  protected $token;
-
-  /**
-   * The theme manager.
-   *
-   * @var \Drupal\Core\Theme\ThemeManagerInterface
-   */
-  protected $themeManager;
-
-  /**
-   * Constructs a new PageNotificationsMailHandler.
-   */
-  public function __construct(
-    ConfigFactoryInterface $config_factory,
-    RendererInterface $renderer,
-    TokenInterface $token,
-    TranslationInterface $translation,
-    ThemeManagerInterface $theme_manager,
-  ) {
-    $this->configFactory = $config_factory;
-    $this->renderer = $renderer;
-    $this->token = $token;
-    $this->setStringTranslation($translation);
-    $this->themeManager = $theme_manager;
-  }
-
-  /**
-   * Gets customized email footer.
-   */
-  protected function getEmailFooter() {
-    // Allow modules to alter the footer
-    $footer = '';
-    \Drupal::moduleHandler()->alter('page_notifications_email_footer', $footer);
-    return $footer;
-  }
-
-  /**
-   * Implements callback_mail().
-   */
-  public function mail($key, &$message, $params) {
-    $config = $this->configFactory->get('page_notifications.settings');
-
-    switch ($key) {
-      case 'verification':
-        $this->buildVerificationEmail($message, $params);
-        break;
-
-      case 'notification':
-        $this->buildNotificationEmail($message, $params);
-        break;
-
-      case 'already_subscribed':
-        $this->buildAlreadySubscribedEmail($message, $params);
-        break;
-    }
-  }
-
-  /**
-   * Builds a verification email.
-   */
-  protected function buildEmail(array &$message, array $params, string $template_type) {
-    $config = $this->configFactory->get('page_notifications.settings');
-    $subscription = $params['subscription'];
-    $entity = $params['entity'];
-
-    $token_data = [
-      'subscription' => $subscription,
-      'node' => $entity,
-      'notification' => $params['notification'] ?? [],
-    ];
-
-    $subject_key = "email_templates.{$template_type}_subject";
-    $body_key = "email_templates.{$template_type}_body";
-
-    $subject_template = $config->get($subject_key);
-    $body_template = $config->get($body_key)['value'];
-
-    // For subject
-    $message['subject'] = \Drupal::token()->replace(
-        $subject_template,
-        $token_data,
-        ['clear' => TRUE]
-    );
-
-    // For body
-    $body = \Drupal::token()->replace(
-        $body_template,
-        $token_data,
-        ['clear' => TRUE]
-    );
-
-    $themed_content = $this->wrapEmailContent(
-        $body,
-        $template_type,
-        $subscription,
-        $entity
-    );
-
-    // Configure for HTML mail
-    $message['params']['format'] = 'text/html';
-    $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed';
-    $message['body'] = [$this->renderer->render($themed_content)];
-  }
-
-  /**
-   * Wraps email content in themed template.
-   */
-  protected function wrapEmailContent($content, $email_type, $subscription, $entity) {
-
-     // Ensure content is properly structured as markup
-    $processed_content = [
-      '#type' => 'markup',
-      '#markup' => $content,
-    ];
-
-    return [
-      '#theme' => 'page_notifications_email_wrapper',
-      '#content' => $processed_content,
-      '#email_type' => $email_type,
-      '#subscription' => $subscription,
-      '#entity' => $entity,
-      '#footer' => $this->getEmailFooter(),
-    ];
-  }
-
-  protected function buildVerificationEmail(array &$message, array $params) {
-    $this->buildEmail($message, $params, 'verification');
-  }
-
-  protected function buildNotificationEmail(array &$message, array $params) {
-    $this->buildEmail($message, $params, 'notification');
-  }
-
-  protected function buildAlreadySubscribedEmail(array &$message, array $params) {
-    $this->buildEmail($message, $params, 'already_subscribed');
-  }
-}
-```
-
-# src/Plugin/Block/SubscriptionBlock.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Plugin\Block;
-
-use Drupal\Core\Block\BlockBase;
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Form\FormBuilderInterface;
-use Drupal\Core\Logger\LoggerChannelFactoryInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
-
-/**
- * Provides a subscription block.
- *
- * @Block(
- *   id = "page_notifications_subscription",
- *   admin_label = @Translation("Page Notifications Subscription"),
- *   category = @Translation("Page Notifications")
- * )
- */
-class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInterface {
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * The form builder.
-   *
-   * @var \Drupal\Core\Form\FormBuilderInterface
-   */
-  protected $formBuilder;
-
-  /**
-   * The logger factory.
-   *
-   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
-   */
-  protected $loggerFactory;
-
-  /**
-   * Constructs a new SubscriptionBlock instance.
-   *
-   * @param array $configuration
-   *   The plugin configuration.
-   * @param string $plugin_id
-   *   The plugin_id for the plugin instance.
-   * @param mixed $plugin_definition
-   *   The plugin implementation definition.
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity type manager.
-   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
-   *   The form builder.
-   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
-   *   The logger factory.
-   */
-  public function __construct(
-    array $configuration,
-    $plugin_id,
-    $plugin_definition,
-    EntityTypeManagerInterface $entity_type_manager,
-    FormBuilderInterface $form_builder,
-    LoggerChannelFactoryInterface $logger_factory
-  ) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
-    $this->entityTypeManager = $entity_type_manager;
-    $this->formBuilder = $form_builder;
-    $this->loggerFactory = $logger_factory;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      $container->get('entity_type.manager'),
-      $container->get('form_builder'),
-      $container->get('logger.factory')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function defaultConfiguration() {
-    return [
-      'block_description' => $this->t('Subscribe to receive notifications when this page is updated.'),
-      'button_text' => $this->t('Subscribe'),
-      'button_classes' => 'button button--primary',
-      'form_classes' => 'subscription-form',
-      'show_description' => TRUE,
-      'use_modal' => FALSE,
-      'modal_title' => $this->t('Subscribe to Updates'),
-    ] + parent::defaultConfiguration();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function blockForm($form, FormStateInterface $form_state) {
-    $form = parent::blockForm($form, $form_state);
-    $config = $this->getConfiguration();
-
-    $form['appearance'] = [
-      '#type' => 'details',
-      '#title' => $this->t('Appearance Settings'),
-      '#open' => TRUE,
-    ];
-
-    $form['appearance']['show_description'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Show block description'),
-      '#default_value' => $config['show_description'],
-    ];
-
-    $form['appearance']['block_description'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Block Description'),
-      '#description' => $this->t('The text shown above the subscription form.'),
-      '#default_value' => $config['block_description'],
-      '#states' => [
-        'visible' => [
-          ':input[name="settings[appearance][show_description]"]' => ['checked' => TRUE],
-        ],
-      ],
-    ];
-
-    $form['appearance']['button_text'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Button Text'),
-      '#description' => $this->t('The text shown on the subscribe button.'),
-      '#default_value' => $config['button_text'],
-    ];
-
-    $form['appearance']['use_modal'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Use modal dialog'),
-      '#description' => $this->t('Display the subscription form in a modal dialog.'),
-      '#default_value' => $config['use_modal'],
-    ];
-
-    $form['appearance']['modal_title'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Modal Title'),
-      '#description' => $this->t('The title displayed at the top of the modal dialog.'),
-      '#default_value' => $config['modal_title'],
-      '#states' => [
-        'visible' => [
-          ':input[name="settings[appearance][use_modal]"]' => ['checked' => TRUE],
-        ],
-      ],
-    ];
-
-    $form['styling'] = [
-      '#type' => 'details',
-      '#title' => $this->t('CSS Classes'),
-      '#open' => TRUE,
-    ];
-
-    $form['styling']['button_classes'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Button Classes'),
-      '#description' => $this->t('CSS classes to add to the subscribe button (space-separated).'),
-      '#default_value' => $config['button_classes'],
-    ];
-
-    $form['styling']['form_classes'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Form Classes'),
-      '#description' => $this->t('CSS classes to add to the subscription form wrapper (space-separated).'),
-      '#default_value' => $config['form_classes'],
-    ];
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function blockSubmit($form, FormStateInterface $form_state) {
-    $this->configuration['block_description'] = $form_state->getValue(['appearance', 'block_description']);
-    $this->configuration['button_text'] = $form_state->getValue(['appearance', 'button_text']);
-    $this->configuration['button_classes'] = $form_state->getValue(['styling', 'button_classes']);
-    $this->configuration['form_classes'] = $form_state->getValue(['styling', 'form_classes']);
-    $this->configuration['show_description'] = $form_state->getValue(['appearance', 'show_description']);
-    $this->configuration['use_modal'] = $form_state->getValue(['appearance', 'use_modal']);
-    $this->configuration['modal_title'] = $form_state->getValue(['appearance', 'modal_title']);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function build() {
-    try {
-      // Get node from route match or current path
-      $node = \Drupal::routeMatch()->getParameter('node');
-      if (!$node) {
-        $path_args = explode('/', \Drupal::service('path.current')->getPath());
-        if (isset($path_args[2]) && is_numeric($path_args[2])) {
-          $node = \Drupal::entityTypeManager()->getStorage('node')->load($path_args[2]);
-        }
-      }
-
-      if (!$node) {
-        return [];
-      }
-
-      $build = [];
-
-      if ($this->configuration['show_description']) {
-        $build['description'] = [
-          '#type' => 'html_tag',
-          '#tag' => 'p',
-          '#value' => $this->configuration['block_description'],
-        ];
-      }
-
-      if ($this->configuration['use_modal']) {
-        // Modal trigger button
-        $url = Url::fromRoute('page_notifications.modal_form', [
-          'entity_type' => $node->getEntityTypeId(),
-          'entity' => $node->id(),
-        ]);
-
-        $build['modal_button'] = [
-          '#type' => 'link',
-          '#title' => $this->configuration['button_text'],
-          '#url' => $url,
-          '#attributes' => [
-            'class' => array_merge(['use-ajax'], explode(' ', $this->configuration['button_classes'])),
-            'data-dialog-type' => 'modal',
-            'data-dialog-options' => json_encode([
-              'width' => 500,
-              'title' => $this->configuration['modal_title'],
-            ]),
-          ],
-        ];
-
-        // Attach required libraries
-        $build['#attached']['library'][] = 'core/drupal.dialog.ajax';
-        $build['#attached']['library'][] = 'page_notifications/modal';
-      } else {
-        $form = $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node);
-        $form['#attributes']['class'][] = $this->configuration['form_classes'];
-        $form['actions']['submit']['#value'] = $this->configuration['button_text'];
-        $form['actions']['submit']['#attributes']['class'] = explode(' ', $this->configuration['button_classes']);
-        $build['form'] = $form;
-      }
-
-       // Add cache contexts and tags
-      $build['#cache'] = [
-        'contexts' => [
-          'url.path',
-          'route',
-        ],
-        'tags' => [
-          'node:' . $node->id(),
-        ],
-        'max-age' => 0 // Disable caching for this block
-      ];
-
-      return $build;
-    }
-    catch (\Exception $e) {
-      $this->loggerFactory->get('page_notifications')->error(
-        'Error building subscription block: @message',
-        ['@message' => $e->getMessage()]
-      );
-      return [];
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function blockAccess(AccountInterface $account) {
-    try {
-      $node = \Drupal::routeMatch()->getParameter('node');
-      if (!$node) {
-        return AccessResult::forbidden();
-      }
-
-      return AccessResult::allowedIfHasPermission($account, 'access content');
-    }
-    catch (\Exception $e) {
-      $this->loggerFactory->get('page_notifications')->error(
-        'Error checking block access: @message',
-        ['@message' => $e->getMessage()]
-      );
-      return AccessResult::forbidden();
-    }
-  }
-
-}
-```
-
-# src/Plugin/EntityReferenceSelection/NodeEnhancedSelection.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Plugin\EntityReferenceSelection;
-
-use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection;
-
-/**
- * Provides enhanced node selection with additional display information.
- *
- * @EntityReferenceSelection(
- *   id = "default:node_enhanced",
- *   label = @Translation("Node enhanced selection"),
- *   entity_types = {"node"},
- *   group = "default",
- *   weight = 1
- * )
- */
-class NodeEnhancedSelection extends NodeSelection {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
-    $target_type = $this->configuration['target_type'];
-
-    $query = $this->buildEntityQuery($match, $match_operator);
-    if ($limit > 0) {
-      $query->range(0, $limit);
-    }
-
-    $result = $query->execute();
-
-    if (empty($result)) {
-      return [];
-    }
-
-    $options = [];
-    $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result);
-
-    foreach ($entities as $entity_id => $entity) {
-      $bundle = $entity->bundle();
-      $type_label = $entity->type->entity->label();
-
-      $label = sprintf(
-        '%s (ID: %d, Type: %s)',
-        $entity->label(),
-        $entity_id,
-        $type_label
-      );
-
-      $options[$bundle][$entity_id] = $label;
-    }
-
-    return $options;
-  }
-
-}
-```
-
-# src/Plugin/EntityReferenceSelection/NodeWithSubscriptionsSelection.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Plugin\EntityReferenceSelection;
-
-use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection;
-use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
-use Drupal\Core\Entity\Query\QueryInterface;
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * Provides specific access control for node entities with subscriptions.
- *
- * @EntityReferenceSelection(
- *   id = "default:node_with_subscriptions",
- *   label = @Translation("Node with subscriptions selection"),
- *   entity_types = {"node"},
- *   group = "default",
- *   weight = 1
- * )
- */
-class NodeWithSubscriptionsSelection extends NodeSelection {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
-    $query = parent::buildEntityQuery($match, $match_operator);
-
-    // Get nodes that have active subscriptions
-    $subscription_query = $this->entityTypeManager
-      ->getStorage('page_notification_subscription')
-      ->getQuery()
-      ->condition('subscribed_entity_type', 'node')
-      ->condition('status', TRUE)
-      ->accessCheck(FALSE);
-
-    $subscriptions = $subscription_query->execute();
-
-    if (!empty($subscriptions)) {
-      // Get unique node IDs from subscriptions
-      $node_ids = [];
-      $subscriptions = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->loadMultiple($subscriptions);
-
-      foreach ($subscriptions as $subscription) {
-        $node_ids[] = $subscription->getSubscribedEntityId();
-      }
-
-      // Filter query to only include nodes with subscriptions
-      $query->condition('nid', $node_ids, 'IN');
-    }
-    else {
-      // If no subscriptions exist, return no results
-      $query->condition('nid', 0);
-    }
-
-    return $query;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
-    $target_type = $this->configuration['target_type'];
-
-    $query = $this->buildEntityQuery($match, $match_operator);
-    if ($limit > 0) {
-      $query->range(0, $limit);
-    }
-
-    $result = $query->execute();
-
-    if (empty($result)) {
-      return [];
-    }
-
-    $options = [];
-    $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result);
-
-    foreach ($entities as $entity_id => $entity) {
-      $bundle = $entity->bundle();
-      $type_label = $entity->type->entity->label();
-
-      // Get subscription count for this node
-      $subscription_count = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->getQuery()
-        ->condition('subscribed_entity_id', $entity_id)
-        ->condition('subscribed_entity_type', 'node')
-        ->condition('status', TRUE)
-        ->count()
-        ->accessCheck(FALSE)
-        ->execute();
-
-      $label = sprintf(
-        '%s (ID: %d, Type: %s, Subscriptions: %d)',
-        $entity->label(),
-        $entity_id,
-        $type_label,
-        $subscription_count
-      );
-
-      $options[$bundle][$entity_id] = $label;
-    }
-
-    return $options;
-  }
-
-}
-```
-
-# src/Plugin/QueueWorker/NotificationQueue.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Plugin\QueueWorker;
-
-use Drupal\Core\Queue\QueueWorkerBase;
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Mail\MailManagerInterface;
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Logger\LoggerChannelFactoryInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\Language\LanguageInterface;
-
-/**
- * Process notification queue.
- *
- * @QueueWorker(
- *   id = "page_notifications_queue",
- *   title = @Translation("Page Notifications Queue"),
- *   cron = {"time" = 60}
- * )
- */
-class NotificationQueue extends QueueWorkerBase implements ContainerFactoryPluginInterface {
-  use StringTranslationTrait;
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * The mail manager.
-   *
-   * @var \Drupal\Core\Mail\MailManagerInterface
-   */
-  protected $mailManager;
-
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The logger factory.
-   *
-   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
-   */
-  protected $loggerFactory;
-
-  /**
-   * Constructs a new NotificationQueue worker.
-   */
-  public function __construct(
-    array $configuration,
-    $plugin_id,
-    array $plugin_definition,
-    EntityTypeManagerInterface $entity_type_manager,
-    MailManagerInterface $mail_manager,
-    ConfigFactoryInterface $config_factory,
-    LoggerChannelFactoryInterface $logger_factory
-  ) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
-    $this->entityTypeManager = $entity_type_manager;
-    $this->mailManager = $mail_manager;
-    $this->configFactory = $config_factory;
-    $this->loggerFactory = $logger_factory;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      $container->get('entity_type.manager'),
-      $container->get('plugin.manager.mail'),
-      $container->get('config.factory'),
-      $container->get('logger.factory')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function processItem($data) {
-    try {
-      // Load the subscription
-      $subscription = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->load($data['subscription_id']);
-
-      if (!$subscription || !$subscription->isActive()) {
-        $this->loggerFactory->get('page_notifications')
-          ->notice('Skipping notification for inactive or deleted subscription: @id',
-            ['@id' => $data['subscription_id']]);
-        return;
-      }
-
-      // Load the entity
-      $entity = $this->entityTypeManager
-        ->getStorage($data['entity_type'])
-        ->load($data['entity_id']);
-
-      if (!$entity) {
-        $this->loggerFactory->get('page_notifications')
-          ->error('Cannot send notification: Entity not found (@type: @id)',
-            ['@type' => $data['entity_type'], '@id' => $data['entity_id']]);
-        return;
-      }
-
-      $config = $this->configFactory->get('page_notifications.settings');
-
-      // Prepare mail parameters
-      $params = [
-        'subscription' => $subscription,
-        'entity' => $entity,
-        'token' => $subscription->getToken(),
-      ];
-
-      $langcode = $subscription->getLanguageCode() ?? LanguageInterface::LANGCODE_DEFAULT;
-      $from_email = $config->get('notification_settings.from_email');
-
-      // Send the email
-      $this->mailManager->mail(
-        'page_notifications',           // module
-        'notification',                 // key
-        $subscription->getEmail(),      // to
-        $langcode,                     // language
-        $params,                       // params
-        $from_email ?: NULL            // from
-      );
-
-      // Log the attempt regardless of the result
-      $this->loggerFactory->get('page_notifications')
-        ->info('Processed notification for @email regarding @type @id', [
-          '@email' => $subscription->getEmail(),
-          '@type' => $entity->getEntityTypeId(),
-          '@id' => $entity->id(),
-        ]);
-
-    }
-    catch (\Exception $e) {
-      // Log any unexpected errors
-      $this->loggerFactory->get('page_notifications')
-        ->error('Error processing notification: @message',
-          ['@message' => $e->getMessage()]);
-    }
-  }
-}
-```
-
-# src/Service/CronManager.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Service;
-
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Queue\QueueWorkerManagerInterface;
-use Drupal\Core\Queue\QueueFactory;
-use Drupal\Core\Logger\LoggerChannelFactoryInterface;
-use Drupal\Component\Datetime\TimeInterface;
-
-/**
- * Service for handling page notifications cron operations.
- */
-class CronManager {
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The queue factory.
-   *
-   * @var \Drupal\Core\Queue\QueueFactory
-   */
-  protected $queueFactory;
-
-  /**
-   * The queue worker manager.
-   *
-   * @var \Drupal\Core\Queue\QueueWorkerManagerInterface
-   */
-  protected $queueWorkerManager;
-
-  /**
-   * The logger factory.
-   *
-   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
-   */
-  protected $loggerFactory;
-
-  /**
-   * The time service.
-   *
-   * @var \Drupal\Component\Datetime\TimeInterface
-   */
-  protected $time;
-
-  /**
-   * Constructs a new CronManager.
-   */
-  public function __construct(
-    EntityTypeManagerInterface $entity_type_manager,
-    ConfigFactoryInterface $config_factory,
-    QueueFactory $queue_factory,
-    QueueWorkerManagerInterface $queue_worker_manager,
-    LoggerChannelFactoryInterface $logger_factory,
-    TimeInterface $time
-  ) {
-    $this->entityTypeManager = $entity_type_manager;
-    $this->configFactory = $config_factory;
-    $this->queueFactory = $queue_factory;
-    $this->queueWorkerManager = $queue_worker_manager;
-    $this->loggerFactory = $logger_factory;
-    $this->time = $time;
-  }
-
-  /**
-   * Processes cron tasks.
-   */
-  public function processCron() {
-    $this->processQueue();
-    $this->cleanupExpiredSubscriptions();
-  }
-
-  /**
-   * Process the notification queue.
-   */
-  protected function processQueue() {
-    $queue = $this->queueFactory->get('page_notifications_queue');
-    $queue_worker = $this->queueWorkerManager->createInstance('page_notifications_queue');
-
-    $time_limit = 30;
-    $end = $this->time->getRequestTime() + $time_limit;
-    $items_processed = 0;
-
-    while ($this->time->getRequestTime() < $end && ($item = $queue->claimItem())) {
-      try {
-        $queue_worker->processItem($item->data);
-        $queue->deleteItem($item);
-        $items_processed++;
-
-        if ($items_processed >= 50) {
-          break;
-        }
-      }
-      catch (\Exception $e) {
-        $queue->releaseItem($item);
-        $this->loggerFactory->get('page_notifications')->error(
-          'Error processing notification: @message',
-          ['@message' => $e->getMessage()]
-        );
-      }
-    }
-  }
-
-  /**
-   * Clean up expired unverified subscriptions.
-   */
-  protected function cleanupExpiredSubscriptions() {
-    $config = $this->configFactory->get('page_notifications.settings');
-    $expiration_hours = $config->get('notification_settings.token_expiration');
-
-    // Only cleanup if expiration is set (greater than 0)
-    if ($expiration_hours > 0) {
-      $storage = $this->entityTypeManager->getStorage('page_notification_subscription');
-
-      $expired_ids = $storage->getQuery()
-        ->condition('status', FALSE)
-        ->condition('created', $this->time->getRequestTime() - ($expiration_hours * 3600), '<')
-        ->accessCheck(FALSE)
-        ->execute();
-
-      if (!empty($expired_ids)) {
-        $storage->delete($storage->loadMultiple($expired_ids));
-        $this->loggerFactory->get('page_notifications')->notice(
-          'Cleaned up @count expired unverified subscriptions',
-          ['@count' => count($expired_ids)]
-        );
-      }
-    }
-  }
-
-}
-```
-
-# src/Service/MigrationService.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Service;
-
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\DependencyInjection\DependencySerializationTrait;
-
-/**
- * Service for migrating subscriptions from Page Notifications v3 to v4.
- */
-class MigrationService {
-  use StringTranslationTrait;
-  use DependencySerializationTrait;
-
-  /**
-   * Creates a batch for migrating subscriptions.
-   *
-   * @return array
-   *   The batch definition.
-   */
-  public static function createMigrationBatch() {
-    // Get total count of subscriptions to migrate
-    $count = \Drupal::database()->select('node', 'n')
-      ->condition('n.type', 'page_notify_subscriptions')
-      ->countQuery()
-      ->execute()
-      ->fetchField();
-
-    if (!$count) {
-      \Drupal::logger('page_notifications')->notice('No v3 subscriptions found to migrate.');
-      return NULL;
-    }
-
-    \Drupal::logger('page_notifications')->notice('Found @count v3 subscriptions to migrate.', ['@count' => $count]);
-
-    $batch = [
-      'title' => t('Migrating Page Notifications subscriptions'),
-      'init_message' => t('Starting subscription migration...'),
-      'progress_message' => t('Processed @current out of @total subscriptions.'),
-      'error_message' => t('Error occurred during migration.'),
-      'operations' => [],
-      'finished' => [static::class, 'migrationFinished'],
-    ];
-
-    // Process subscriptions in batches of 25
-    for ($i = 0; $i < $count; $i += 25) {
-      $batch['operations'][] = [
-        [static::class, 'migrateSubscriptionsBatch'],
-        [$i, min(25, $count - $i)]
-      ];
-    }
-
-    return $batch;
-  }
-
-  /**
-   * Migrates a batch of subscriptions.
-   */
-  public static function migrateSubscriptionsBatch($start, $limit, &$context) {
-    try {
-      $database = \Drupal::database();
-
-      // Query v3 subscriptions
-      $query = $database->select('node', 'n');
-      $query->join('node_field_data', 'nfd', 'n.nid = nfd.nid');
-      $query->fields('n', ['nid'])
-        ->fields('nfd', ['created'])
-        ->condition('n.type', 'page_notify_subscriptions')
-        ->range($start, $limit);
-
-      // Join with field tables
-      $query->join('node__field_page_notify_email', 'e', 'n.nid = e.entity_id');
-      $query->join('node__field_page_notify_node_id', 'nid', 'n.nid = nid.entity_id');
-
-      $query->fields('e', ['field_page_notify_email_value']);
-      $query->fields('nid', ['field_page_notify_node_id_value']);
-
-      $results = $query->execute();
-
-      $subscription_storage = \Drupal::entityTypeManager()->getStorage('page_notification_subscription');
-      $time = \Drupal::time()->getRequestTime();
-
-      foreach ($results as $row) {
-        // Generate new tokens
-        $verify_token = bin2hex(random_bytes(16));
-        $unsubscribe_token = bin2hex(random_bytes(32));
-
-        \Drupal::logger('page_notifications')->debug('Migrating subscription for email: @email, node: @nid', [
-          '@email' => $row->field_page_notify_email_value,
-          '@nid' => $row->field_page_notify_node_id_value,
-        ]);
-
-        // Create new v4 subscription entity
-        $subscription = $subscription_storage->create([
-          'email' => $row->field_page_notify_email_value,
-          'subscribed_entity_id' => $row->field_page_notify_node_id_value,
-          'subscribed_entity_type' => 'node',
-          'token' => $verify_token,
-          'unsubscribe_token' => $unsubscribe_token,
-          'status' => TRUE,
-          'created' => $row->created ?? $time,
-          'changed' => $time,
-          'langcode' => \Drupal::languageManager()->getDefaultLanguage()->getId(),
-        ]);
-
-        try {
-          $subscription->save();
-
-          // Update progress
-          if (!isset($context['results']['subscriptions'])) {
-            $context['results']['subscriptions'] = 0;
-          }
-          $context['results']['subscriptions']++;
-
-          \Drupal::logger('page_notifications')->debug('Successfully migrated subscription @id', [
-            '@id' => $subscription->id(),
-          ]);
-        }
-        catch (\Exception $e) {
-          \Drupal::logger('page_notifications')->error('Failed to save subscription: @error', [
-            '@error' => $e->getMessage(),
-          ]);
-        }
-      }
-
-      $context['message'] = t('Migrated @count subscriptions', [
-        '@count' => $limit,
-      ]);
-    }
-    catch (\Exception $e) {
-      \Drupal::logger('page_notifications')->error(
-        'Failed to migrate subscriptions batch: @message',
-        ['@message' => $e->getMessage()]
-      );
-      throw $e;
-    }
-  }
-
-  /**
-   * Batch finished callback.
-   */
-  public static function migrationFinished($success, $results, $operations) {
-    if ($success) {
-      // Verify migration
-      $old_count = \Drupal::database()->select('node', 'n')
-        ->condition('n.type', 'page_notify_subscriptions')
-        ->countQuery()
-        ->execute()
-        ->fetchField();
-
-      $new_count = \Drupal::entityTypeManager()
-        ->getStorage('page_notification_subscription')
-        ->getQuery()
-        ->accessCheck(FALSE)
-        ->count()
-        ->execute();
-
-      $message = t('Migration completed. Migrated @migrated subscriptions (@old v3 subscriptions, @new v4 subscriptions).', [
-        '@migrated' => $results['subscriptions'] ?? 0,
-        '@old' => $old_count,
-        '@new' => $new_count,
-      ]);
-
-      \Drupal::logger('page_notifications')->notice($message);
-      \Drupal::messenger()->addStatus($message);
-
-      if ($old_count != $new_count) {
-        $warning = t('Warning: Number of migrated subscriptions (@new) does not match original count (@old).', [
-          '@new' => $new_count,
-          '@old' => $old_count,
-        ]);
-        \Drupal::logger('page_notifications')->warning($warning);
-        \Drupal::messenger()->addWarning($warning);
-      }
-    }
-    else {
-      $message = t('Migration failed. Please check the logs for details.');
-      \Drupal::logger('page_notifications')->error($message);
-      \Drupal::messenger()->addError($message);
-    }
-  }
-}
-```
-
-# src/Service/NotificationManager.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Service;
-
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Mail\MailManagerInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\StringTranslation\TranslationInterface;
-use Drupal\Core\Queue\QueueFactory;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Logger\LoggerChannelFactoryInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Drupal\Component\Datetime\TimeInterface;
-use Drupal\Core\Url;
-use Drupal\Core\Messenger\MessengerInterface;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-
-
-/**
- * Service for handling page notification operations.
- */
-class NotificationManager implements NotificationManagerInterface {
-
-  use StringTranslationTrait;
-
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The mail manager.
-   *
-   * @var \Drupal\Core\Mail\MailManagerInterface
-   */
-  protected $mailManager;
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * The queue factory.
-   *
-   * @var \Drupal\Core\Queue\QueueFactory
-   */
-  protected $queueFactory;
-
-  /**
-   * The logger factory.
-   *
-   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
-   */
-  protected $loggerFactory;
-
-  /**
-   * The event dispatcher.
-   *
-   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
-   */
-  protected $eventDispatcher;
-
-  /**
-   * The time service.
-   *
-   * @var \Drupal\Component\Datetime\TimeInterface
-   */
-  protected $time;
-
-  /**
-   * The messenger service.
-   * @var \Drupal\Core\Messenger\MessengerInterface
-   */
-  protected $messenger;
-
-/**
-   * Constructs a new NotificationManager.
-   *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The config factory.
-   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
-   *   The mail manager.
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity type manager.
-   * @param \Drupal\Core\Queue\QueueFactory $queue_factory
-   *   The queue factory.
-   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
-   *   The logger factory.
-   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
-   *   The event dispatcher.
-   * @param \Drupal\Component\Datetime\TimeInterface $time
-   *   The time service.
-   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
-   *   The string translation service.
-   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
-   *  The messenger service.
-   */
-  public function __construct(
-    ConfigFactoryInterface $config_factory,
-    MailManagerInterface $mail_manager,
-    EntityTypeManagerInterface $entity_type_manager,
-    QueueFactory $queue_factory,
-    LoggerChannelFactoryInterface $logger_factory,
-    EventDispatcherInterface $event_dispatcher,
-    TimeInterface $time,
-    TranslationInterface $translation,
-    MessengerInterface $messenger
-  ) {
-    $this->configFactory = $config_factory;
-    $this->mailManager = $mail_manager;
-    $this->entityTypeManager = $entity_type_manager;
-    $this->queueFactory = $queue_factory;
-    $this->loggerFactory = $logger_factory;
-    $this->eventDispatcher = $event_dispatcher;
-    $this->time = $time;
-    $this->setStringTranslation($translation);
-    $this->messenger = $messenger;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function createSubscription(string $email, EntityInterface $entity, ?string $langcode = null) {
-    try {
-      // Check for existing subscription
-      $existing_subscriptions = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->loadByProperties([
-          'email' => $email,
-          'subscribed_entity_id' => $entity->id(),
-          'subscribed_entity_type' => $entity->getEntityTypeId(),
-        ]);
-  
-      if (!empty($existing_subscriptions)) {
-        /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
-        $subscription = reset($existing_subscriptions);
-  
-        // Handle different subscription states
-        if ($subscription->isActive()) {
-          // Already verified subscription - send "already subscribed" email
-          $this->sendAlreadySubscribedEmail($subscription, $entity);
-          $this->messenger->addStatus($this->t('You are already subscribed to this content.'));
-          return $subscription;
-        }
-        
-        // Check if token is expired
-        if ($this->isTokenExpired($subscription)) {
-          // Generate new token and update subscription
-          $subscription->setToken($this->generateToken());
-          $subscription->setCreatedTime($this->time->getRequestTime());
-          $subscription->save();
-        }
-        
-        // Resend verification email for unverified subscriptions
-        if ($this->requiresVerification()) {
-          $this->sendVerificationEmail($subscription);
-          $this->messenger->addStatus($this->t('A new verification email has been sent to your address.'));
-        }
-        
-        return $subscription;
-      }
-  
-      // Create new subscription if none exists
-      $subscription = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->create([
-          'email' => $email,
-          'subscribed_entity_id' => $entity->id(),
-          'subscribed_entity_type' => $entity->getEntityTypeId(),
-          'token' => $this->generateToken(),
-          'unsubscribe_token' => $this->generateToken(),
-          'status' => !$this->requiresVerification(),
-        ]);
-  
-      $subscription->save();
-  
-      if ($this->requiresVerification()) {
-        $this->sendVerificationEmail($subscription);
-      }
-  
-      return $subscription;
-    }
-    catch (\Exception $e) {
-      $this->loggerFactory->get('page_notifications')
-        ->error('Failed to create subscription: @message', ['@message' => $e->getMessage()]);
-      throw $e;
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function verifySubscription(string $token) {
-    try {
-      // Find subscription by token
-      $subscriptions = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->loadByProperties(['token' => $token]);
-
-      if (!empty($subscriptions)) {
-        /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */
-        $subscription = reset($subscriptions);
-
-        // Get entity details before setting active
-        $entity_id = $subscription->getSubscribedEntityId();
-        $entity_type = $subscription->getSubscribedEntityType();
-
-        // Activate the subscription
-        $subscription->setActive(TRUE);
-        $subscription->save();
-
-        $this->messenger->addStatus($this->t('Thank you! Your subscription has been verified.'));
-
-        // Load the entity and get its URL
-        try {
-          $entity = $this->entityTypeManager
-            ->getStorage($entity_type)
-            ->load($entity_id);
-
-          if ($entity && $entity->hasLinkTemplate('canonical')) {
-            return new RedirectResponse($entity->toUrl()->toString());
-          }
-        }
-        catch (\Exception $e) {
-          \Drupal::logger('page_notifications')->error('Verification redirect error: @message', ['@message' => $e->getMessage()]);
-        }
-      }
-      else {
-        $this->messenger->addError($this->t('Sorry, this verification link is invalid or has expired.'));
-      }
-    }
-    catch (\Exception $e) {
-      $this->messenger->addError($this->t('An error occurred while verifying your subscription.'));
-      \Drupal::logger('page_notifications')->error('Verification error: @message', ['@message' => $e->getMessage()]);
-    }
-
-    // Fallback to homepage if anything goes wrong
-    return new RedirectResponse('/');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function notifySubscribers(EntityInterface $entity) {
-    try {
-      $subscriptions = $this->entityTypeManager
-        ->getStorage('page_notification_subscription')
-        ->loadByProperties([
-          'subscribed_entity_id' => $entity->id(),
-          'subscribed_entity_type' => $entity->getEntityTypeId(),
-          'status' => TRUE,
-        ]);
-
-      foreach ($subscriptions as $subscription) {
-        $this->queueNotification($subscription, $entity);
-      }
-    }
-    catch (\Exception $e) {
-      $this->loggerFactory->get('page_notifications')
-        ->error('Failed to notify subscribers: @message', ['@message' => $e->getMessage()]);
-      throw $e;
-    }
-  }
-
-  /**
-   * Sends an "already subscribed" email notification.
-   *
-   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
-   *   The subscription entity.
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The entity being subscribed to.
-   */
-  protected function sendAlreadySubscribedEmail($subscription, $entity) {
-    $config = $this->configFactory->get('page_notifications.settings');
-    
-    $params = [
-      'subscription' => $subscription,
-      'entity' => $entity,
-    ];
-
-    $this->mailManager->mail(
-      'page_notifications',
-      'already_subscribed',
-      $subscription->getEmail(),
-      $subscription->getLanguageCode(),
-      $params,
-      $config->get('notification_settings.from_email')
-    );
-  }
-
-  /**
-   * Retrieves the queue for processing notifications.
-   *
-   * @return \Drupal\Core\Queue\QueueInterface
-   *   The queue.
-   */
-  protected function getQueue() {
-    return $this->queueFactory->get('page_notifications_queue');
-  }
-
-  /**
-   * Queues a notification for processing.
-   *
-   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
-   *   The subscription entity.
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The entity that was updated.
-   */
-  protected function queueNotification($subscription, EntityInterface $entity) {
-    $queue = $this->getQueue();
-    $queue->createItem([
-      'subscription_id' => $subscription->id(),
-      'entity_id' => $entity->id(),
-      'entity_type' => $entity->getEntityTypeId(),
-    ]);
-  }
-
-  /**
-   * Sends a verification email to the subscriber.
-   *
-   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
-   *   The subscription entity.
-   */
-  protected function sendVerificationEmail($subscription) {
-    $config = $this->configFactory->get('page_notifications.settings');
-    $entity = $this->entityTypeManager
-      ->getStorage($subscription->getSubscribedEntityType())
-      ->load($subscription->getSubscribedEntityId());
-
-    $params = [
-      'subscription' => $subscription,
-      'entity' => $entity,
-      'verify_url' => Url::fromRoute('page_notifications.subscription.verify', [
-        'token' => $subscription->getToken(),
-      ])->setAbsolute(TRUE)
-        ->toString(),
-    ];
-
-    $this->mailManager->mail(
-      'page_notifications',
-      'verification',
-      $subscription->getEmail(),
-      $subscription->getLanguageCode(),
-      $params,
-      $config->get('notification_settings.from_email')
-    );
-  }
-
-  /**
-   * Generates a unique token for subscription verification.
-   *
-   * @return string
-   *   The generated token.
-   */
-  protected function generateToken() {
-    return bin2hex(random_bytes(32));
-  }
-
-  /**
-   * Checks if verification is required based on configuration.
-   *
-   * @return bool
-   *   TRUE if verification is required, FALSE otherwise.
-   */
-  protected function requiresVerification() {
-    return $this->configFactory
-      ->get('page_notifications.settings')
-      ->get('security.require_verification') ?? TRUE;
-  }
-
-  /**
-   * Checks if a subscription token has expired.
-   *
-   * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription
-   *   The subscription entity.
-   *
-   * @return bool
-   *   TRUE if the token has expired, FALSE otherwise.
-   */
-  protected function isTokenExpired($subscription) {
-    if ($subscription->isActive()) {
-      return FALSE;
-    }
-
-    $config = $this->configFactory->get('page_notifications.settings');
-    $expiration_hours = $config->get('notification_settings.token_expiration') ?? 48;
-    $expiration_timestamp = $subscription->getCreatedTime() + ($expiration_hours * 3600);
-
-    return $this->time->getRequestTime() > $expiration_timestamp;
-  }
-
-}
-```
-
-# src/Service/NotificationManagerInterface.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Service;
-
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Language\LanguageInterface;
-
-/**
- * Interface for notification management service.
- */
-interface NotificationManagerInterface {
-
-  /**
-   * Creates a new subscription.
-   *
-   * @param string $email
-   *   The subscriber's email address.
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The entity being subscribed to.
-   * @param string|null $langcode
-   *   The language code for the subscription. Defaults to site's default language.
-   *
-   * @return \Drupal\page_notifications\Entity\SubscriptionInterface
-   *   The created subscription entity.
-   *
-   * @throws \Exception
-   *   If the subscription cannot be created.
-   */
-  public function createSubscription(string $email, EntityInterface $entity, string $langcode = LanguageInterface::LANGCODE_DEFAULT);
-
-  /**
-   * Verifies a subscription using a token.
-   *
-   * @param string $token
-   *   The verification token.
-   *
-   * @return bool
-   *   TRUE if verification was successful, FALSE otherwise.
-   */
-  public function verifySubscription(string $token);
-
-  /**
-   * Notifies subscribers about updates to an entity.
-   *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The entity that was updated.
-   *
-   * @throws \Exception
-   *   If notifications cannot be sent.
-   */
-  public function notifySubscribers(EntityInterface $entity);
-
-}
-```
-
-# src/Service/SpamPrevention.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Service;
-
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Session\SessionManagerInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\StringTranslation\TranslationInterface;
-
-/**
- * Service for handling spam prevention in Page Notifications.
- */
-class SpamPrevention {
-  use StringTranslationTrait;
-
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The module handler.
-   *
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface
-   */
-  protected $moduleHandler;
-
-  /**
-   * The session manager.
-   *
-   * @var \Drupal\Core\Session\SessionManagerInterface
-   */
-  protected $sessionManager;
-
-  /**
-   * Constructs a new SpamPrevention object.
-   *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The config factory.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler.
-   * @param \Drupal\Core\Session\SessionManagerInterface $session_manager
-   *   The session manager.
-   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
-   *   The string translation service.
-   */
-  public function __construct(
-    ConfigFactoryInterface $config_factory,
-    ModuleHandlerInterface $module_handler,
-    SessionManagerInterface $session_manager,
-    TranslationInterface $string_translation
-  ) {
-    $this->configFactory = $config_factory;
-    $this->moduleHandler = $module_handler;
-    $this->sessionManager = $session_manager;
-    $this->setStringTranslation($string_translation);
-  }
-
-  /**
-   * Generates a math challenge.
-   *
-   * @return array
-   *   An array containing the challenge question, numbers, operator and answer.
-   */
-  public function generateMathChallenge() {
-    $config = $this->configFactory->get('page_notifications.settings');
-    $operator = $config->get('spam_prevention.math_operator') ?? '+';
-
-    // Generate two random numbers between 1 and 10
-    $num1 = rand(1, 10);
-    $num2 = rand(1, 10);
-
-    // Calculate the answer
-    $answer = $operator === '+' ? $num1 + $num2 : $num1 * $num2;
-
-    return [
-      'num1' => $num1,
-      'num2' => $num2,
-      'operator' => $operator,
-      'question' => $this->t('What is @num1 @operator @num2?', [
-        '@num1' => $num1,
-        '@operator' => $operator,
-        '@num2' => $num2,
-      ]),
-      'answer' => $answer,
-    ];
-  }
-
-  /**
-   * Validates a math challenge response.
-   *
-   * @param int $response
-   *   The user's response to the challenge.
-   * @param array $challenge
-   *   The original challenge array containing num1, num2, and operator.
-   *
-   * @return bool
-   *   TRUE if the response is correct, FALSE otherwise.
-   */
-  public function validateMathResponse($response, array $challenge) {
-    $answer = $challenge['operator'] === '+'
-      ? $challenge['num1'] + $challenge['num2']
-      : $challenge['num1'] * $challenge['num2'];
-
-    return (int) $response === (int) $answer;
-  }
-  /**
-   * Checks if reCAPTCHA is available and configured.
-   *
-   * @return bool
-   *   TRUE if reCAPTCHA is available and configured, FALSE otherwise.
-   */
-  public function isRecaptchaAvailable() {
-    return $this->moduleHandler->moduleExists('captcha') &&
-           $this->moduleHandler->moduleExists('recaptcha') &&
-           $this->configFactory->get('recaptcha.settings')->get('site_key');
-  }
-}
-```
-
-# src/Token/SubscriptionToken.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Token;
-
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\Render\BubbleableMetadata;
-use Drupal\Core\Url;
-use Drupal\node\NodeInterface;
-use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Implements hook_token_info() and hook_tokens().
- */
-class SubscriptionToken {
-  use StringTranslationTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static();
-  }
-
-  /**
-   * Implements hook_token_info().
-   */
-  public function hookTokenInfo() {
-    $info['types']['subscription'] = [
-      'name' => $this->t('Subscription'),
-      'description' => $this->t('Tokens related to page notification subscriptions'),
-      'needs-data' => 'subscription',
-    ];
-
-    $info['tokens']['subscription'] = [
-      'verify-url' => [
-        'name' => $this->t('Verification URL'),
-        'description' => $this->t('The URL to verify the subscription'),
-      ],
-      'unsubscribe-url' => [
-        'name' => $this->t('Unsubscribe URL'),
-        'description' => $this->t('The URL to unsubscribe from notifications'),
-      ],
-      'email' => [
-        'name' => $this->t('Email'),
-        'description' => $this->t('The subscriber\'s email address'),
-      ],
-    ];
-
-    return $info;
-  }
-
-  /**
-   * Implements hook_tokens().
-   */
-  public function hookTokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-    $replacements = [];
-
-    if (($type == 'subscription' || $type == 'page_notification_subscription') && !empty($data['subscription'])) {
-      $subscription = $data['subscription'];
-      $bubbleable_metadata->addCacheableDependency($subscription);
-
-      foreach ($tokens as $name => $original) {
-        switch ($name) {
-          case 'verify-url':
-            $replacements[$original] = Url::fromRoute('page_notifications.subscription.verify',
-              ['token' => $subscription->getToken()],
-              ['absolute' => TRUE]
-            )->toString();
-            $bubbleable_metadata->addCacheableDependency($subscription);
-            break;
-
-            case 'unsubscribe-url':
-              $replacements[$original] = Url::fromRoute('page_notifications.subscription.unsubscribe',
-                [
-                  'subscription' => $subscription->id(),
-                  'token' => $subscription->getUnsubscribeToken(),
-                ],
-                ['absolute' => TRUE]
-              )->toString();
-              $bubbleable_metadata->addCacheableDependency($subscription);
-              break;
-
-          case 'email':
-            $replacements[$original] = $subscription->getEmail();
-            $bubbleable_metadata->addCacheableDependency($subscription);
-            break;
-        }
-      }
-      return $replacements;
-    }
-
-    if ($type == 'page_notification_notification' && !empty($data['entity'])) {
-      $entity = $data['entity'];
-      $bubbleable_metadata->addCacheableDependency($entity);
-
-      foreach ($tokens as $name => $original) {
-        switch ($name) {
-          case 'notes':
-            // First check for manual notification notes
-            $notes = \Drupal::state()->get('page_notifications_manual_notes_' . $entity->id(), '');
-
-            // If no manual notes and entity is a node, try to get revision log
-            if (empty($notes) && $entity instanceof NodeInterface) {
-              $notes = $entity->getRevisionLogMessage();
-            }
-
-            $replacements[$original] = $notes;
-
-            // Clean up stored manual notes if they exist
-            if (\Drupal::state()->get('page_notifications_manual_notes_' . $entity->id())) {
-              \Drupal::state()->delete('page_notifications_manual_notes_' . $entity->id());
-            }
-            $bubbleable_metadata->addCacheableDependency($entity);
-            break;
-        }
-      }
-    }
-
-    return $replacements;
-  }
-}
-```
-
-# src/Traits/FloodControlTrait.php
-
-```php
-<?php
-
-namespace Drupal\page_notifications\Traits;
-
-use Drupal\Core\Flood\FloodInterface;
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * Provides flood control functionality for forms.
- */
-trait FloodControlTrait {
-
-  /**
-   * The flood service.
-   *
-   * @var \Drupal\Core\Flood\FloodInterface|null
-   */
-  protected ?FloodInterface $flood = NULL;
-
-  /**
-   * Sets the flood service.
-   *
-   * @param \Drupal\Core\Flood\FloodInterface $flood
-   *   The flood service.
-   */
-  public function setFloodService(FloodInterface $flood) {
-    $this->flood = $flood;
-  }
-
-  /**
-   * Gets the flood service.
-   *
-   * @return \Drupal\Core\Flood\FloodInterface
-   *   The flood service.
-   */
-  protected function getFlood(): FloodInterface {
-    if (!$this->flood) {
-      $this->flood = \Drupal::service('flood');
-    }
-    return $this->flood;
-  }
-
-  /**
-   * Gets the flood control configuration.
-   *
-   * @return array
-   *   An array containing flood control settings.
-   */
-  protected function getFloodControlConfig() {
-    $config = $this->configFactory()->get('page_notifications.settings');
-
-    return [
-      'ip_limit' => $config->get('security.flood_control.ip_limit') ?? 200,
-      'ip_window' => ($config->get('security.flood_control.ip_window') ?? 1) * 3600,
-      'identifier_limit' => $config->get('security.flood_control.identifier_limit') ?? 50,
-      'identifier_window' => ($config->get('security.flood_control.identifier_window') ?? 1) * 3600,
-    ];
-  }
-
-  /**
- * Checks if the current request should be allowed through flood control.
- *
- * @param string $identifier
- *   The identifier to check (typically an email).
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- *   The form state.
- *
- * @return bool
- *   TRUE if the request should be allowed, FALSE otherwise.
- */
-protected function checkFloodControl($identifier, FormStateInterface $form_state) {
-  $ip = \Drupal::request()->getClientIp();
-  $settings = $this->getFloodControlConfig();
-  $flood = $this->getFlood();
-
-  // Skip IP-based flood control if window is set to 0
-  if ($settings['ip_window'] > 0) {
-    $ip_event = 'page_notifications.subscribe_ip.' . $ip;
-    if (!$flood->isAllowed($ip_event, $settings['ip_limit'], $settings['ip_window'])) {
-      $form_state->setErrorByName('', $this->t('Too many subscription attempts from this IP address. Please try again in @hours hours.',
-        ['@hours' => floor($settings['ip_window'] / 3600)]
-      ));
-      $this->logSecurityEvent('flood_control_ip', ['ip' => $ip]);
-      return FALSE;
-    }
-  }
-
-  // Skip identifier-based flood control if window is set to 0
-  if ($settings['identifier_window'] > 0) {
-    $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier;
-    if (!$flood->isAllowed($identifier_event, $settings['identifier_limit'], $settings['identifier_window'])) {
-      $form_state->setErrorByName('email', $this->t('Too many subscription attempts for this email address. Please try again in @hours hours.',
-        ['@hours' => floor($settings['identifier_window'] / 3600)]
-      ));
-      $this->logSecurityEvent('flood_control_identifier', ['identifier' => $identifier]);
-      return FALSE;
-    }
-  }
-
-  return TRUE;
-}
-
-  /**
- * Registers a flood event.
- *
- * @param string $identifier
- *   The identifier for the flood event.
- */
-protected function registerFloodControl($identifier) {
-  $ip = \Drupal::request()->getClientIp();
-  $settings = $this->getFloodControlConfig();
-  $flood = $this->getFlood();
-
-  // Only register IP-based flood events if window is greater than 0
-  if ($settings['ip_window'] > 0) {
-    $ip_event = 'page_notifications.subscribe_ip.' . $ip;
-    $flood->register($ip_event, $settings['ip_window']);
-  }
-
-  // Only register identifier-based flood events if window is greater than 0
-  if ($settings['identifier_window'] > 0) {
-    $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier;
-    $flood->register($identifier_event, $settings['identifier_window']);
-  }
-}
-
-  /**
-   * Logs a security event.
-   *
-   * @param string $type
-   *   The type of security event.
-   * @param array $data
-   *   Additional data to log.
-   */
-  protected function logSecurityEvent($type, array $data) {
-    \Drupal::logger('page_notifications_security')->warning(
-      '@type: @data',
-      ['@type' => $type, '@data' => json_encode($data)]
-    );
-  }
-}
-```
-
-# templates/block--page-notifications-subscription.html.twig
-
-```twig
-{#
-/**
- * @file
- * Default theme implementation to display a Page Notifications subscription block.
- *
- * Available variables:
- * - plugin_id: The ID of the block implementation.
- * - label: The configured label of the block if visible.
- * - configuration: A list of the block's configuration values.
- *   - block_description: The configured description text.
- *   - button_text: The configured button text.
- *   - button_classes: CSS classes for the button.
- *   - form_classes: CSS classes for the form wrapper.
- * - content: The content of the block.
- * - attributes: HTML attributes for the block wrapper.
- *
- * @ingroup themeable
- */
-#}
-{% set classes = [
-  'block',
-  'block-' ~ configuration.provider|clean_class,
-  'block-' ~ plugin_id|clean_class,
-]%}
-
-<div{{ attributes.addClass(classes) }}>
-  {{ title_prefix }}
-  {% if label %}
-    <h2{{ title_attributes }}>{{ label }}</h2>
-  {% endif %}
-  {{ title_suffix }}
-
-  {% block content %}
-    <div{{ content_attributes.addClass('content') }}>
-      {{ content }}
-    </div>
-  {% endblock %}
-</div>
-```
-
-# templates/page-notifications-email-wrapper.html.twig
-
-```twig
-{#
-/**
- * @file
- * Default template for Page Notifications emails.
- *
- * Available variables:
- * - content: The main email content.
- * - email_type: The type of email (verification, notification, etc.).
- * - subscription: The subscription entity.
- * - entity: The subscribed entity.
- * - logo_url: The site logo URL.
- * - site_name: The site name.
- * - footer: Custom footer content.
- */
-#}
-<div class="page-notifications-email">
-  {% if logo_url %}
-    <div class="email-header">
-      <img src="{{ logo_url }}" alt="{{ site_name }}" style="max-width: 200px; height: auto;">
-    </div>
-  {% endif %}
-
-  <div class="email-content">
-    {{ content }}
-  </div>
-
-  {% if footer %}
-    <div class="email-footer">
-      {{ footer }}
-    </div>
-  {% else %}
-    <div class="email-footer">
-      <p style="color: #666; font-size: 12px;">
-        © {{ "now"|date("Y") }} {{ site_name }}
-      </p>
-    </div>
-  {% endif %}
-</div>
-```
-
-# upgrade-docs.md
-
-```md
-# Upgrading from Page Notifications 3.x to 4.x
-
-## Breaking Changes
-- Complete rewrite of the module's architecture
-- New configuration system
-- New subscription storage using custom entities
-
-## Migration Process
-1. Back up your database before upgrading
-2. Install the new version over the old one
-3. Run database updates (`drush updb` or visit /update.php)
-
-## Post-Migration Steps
-After upgrading, you will need to reconfigure your Page Notifications settings:
-
-1. Visit `/admin/config/system/page-notifications`
-2. Configure the following settings:
-   - Email settings
-   - Notification templates
-   - Spam protection settings
-   - Security settings
-
-## Previous Settings
-Your previous settings from v3 will not be automatically migrated. Make note of your current settings before upgrading:
-1. Email templates
-2. From email address
-3. CAPTCHA configuration
-4. Other customizations
-
-## Subscription Data
-All subscription data (email addresses, subscribed content, tokens) will be automatically migrated to the new system.
-
-```
-
diff --git a/composer.json b/composer.json
index 8a7154a..118e852 100644
--- a/composer.json
+++ b/composer.json
@@ -17,8 +17,7 @@
   },
   "require": {
     "php": ">=8.1",
-    "drupal/core": "^10",
-    "drupal/symfony_mailer_lite": "^2.0"
+    "drupal/core": "^10.3 || ^11"
   },
   "suggest": {
     "drupal/captcha": "Provides additional spam protection options including reCAPTCHA integration"
diff --git a/page_notifications.info.yml b/page_notifications.info.yml
index b0d176e..6810875 100644
--- a/page_notifications.info.yml
+++ b/page_notifications.info.yml
@@ -7,6 +7,5 @@ dependencies:
   - drupal:node
   - drupal:views
   - token:token
-  - drupal:symfony_mailer_lite
 suggestions:
   - captcha:captcha
\ No newline at end of file
diff --git a/page_notifications.install b/page_notifications.install
index b0b14cb..808d6be 100644
--- a/page_notifications.install
+++ b/page_notifications.install
@@ -41,22 +41,6 @@ function page_notifications_install() {
  ])
  ->save();
 
-
- try {
-  // Set mail system handler for page notifications
-  $settings = \Drupal::configFactory()->getEditable('mailsystem.settings');
-  $settings->set('modules.page_notifications.none', [
-    'formatter' => 'symfony_mailer_lite',
-    'sender' => 'symfony_mailer_lite',
-  ])->save();
-
-  \Drupal::logger('page_notifications')->notice('Successfully configured mail handler for Page Notifications.');
-}
-catch (\Exception $e) {
-  \Drupal::logger('page_notifications')->error('Failed to configure mail handler: @message', [
-    '@message' => $e->getMessage()
-  ]);
-}
 }
 
 /**
@@ -118,13 +102,6 @@ function page_notifications_update_10001(&$sandbox) {
     'page_notifications.settings',
   ];
 
-  // Set mail system handler for page notifications
-  $settings = \Drupal::configFactory()->getEditable('mailsystem.settings');
-  $settings->set('modules.page_notifications.none', [
-    'formatter' => 'symfony_mailer_lite',
-    'sender' => 'symfony_mailer_lite',
-  ])->save();
-
   foreach ($configs as $config_name) {
     $config_record = $source->read($config_name);
     if (is_array($config_record)) {
diff --git a/page_notifications.services.yml b/page_notifications.services.yml
index 29192ea..2012e8e 100644
--- a/page_notifications.services.yml
+++ b/page_notifications.services.yml
@@ -21,7 +21,6 @@ services:
       - '@token'
       - '@string_translation'
       - '@theme.manager'
-      - '@symfony_mailer_lite.mailer'
   page_notifications.subscription_token:
     class: Drupal\page_notifications\Token\SubscriptionToken
     tags:
diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php
index 20af34e..7683f3f 100644
--- a/src/Mail/PageNotificationsMailHandler.php
+++ b/src/Mail/PageNotificationsMailHandler.php
@@ -135,10 +135,21 @@ class PageNotificationsMailHandler {
         $entity
     );
 
-    // Configure for HTML mail
-    $message['params']['format'] = 'text/html';
+    // Set HTML mail parameters in multiple ways for maximum compatibility
     $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed';
-    $message['body'] = [$this->renderer->render($themed_content)];
+    $message['format'] = 'text/html';
+    $message['params']['format'] = 'text/html';
+    $message['params']['plain'] = FALSE;
+    $message['params']['convert'] = FALSE;
+
+    // Additional parameters that some mail modules look for
+    $message['params']['html'] = TRUE;
+    $message['params']['plaintext'] = FALSE;
+    $message['params']['ishtml'] = TRUE;
+
+    // Ensure rendered content is properly handled as markup
+    $rendered_content = $this->renderer->render($themed_content);
+    $message['body'] = [$rendered_content];
   }
 
   /**
-- 
GitLab


From 9ce7545a5494a71651c2673dbce14ff6e01d4db7 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Mon, 27 Jan 2025 15:24:05 -0500
Subject: [PATCH 47/49] Handle both token types

---
 .gitignore                      | 1 +
 src/Token/SubscriptionToken.php | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 .gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3f9daee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+codebase.md
\ No newline at end of file
diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php
index e59cac0..f71ca88 100644
--- a/src/Token/SubscriptionToken.php
+++ b/src/Token/SubscriptionToken.php
@@ -66,7 +66,8 @@ class SubscriptionToken {
   public function hookTokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
     $replacements = [];
 
-    if (($type == 'page_notification_subscription') && !empty($data['page_notification_subscription'])) {
+    // TODO find why some sites its 'page_notification_subscription' and other its 'subscription'
+    if (($type == 'page_notification_subscription' || $type == 'subscription') && (!empty($data['page_notification_subscription']) || !empty($data['subscription']))) {
       $subscription = $data['page_notification_subscription'];
       $bubbleable_metadata->addCacheableDependency($subscription);
 
-- 
GitLab


From ba67e87e38aa7bdb0cf50e32790c4018d6683db8 Mon Sep 17 00:00:00 2001
From: Nick <nstees@gmail.com>
Date: Mon, 27 Jan 2025 15:43:32 -0500
Subject: [PATCH 48/49] Ship some sane modal styles with module after testing
 some sites don't have close etc or inconsistent modal CSS in themes

---
 css/modal.css                    | 53 ++++++++++++++++++++++++++++++++
 page_notifications.libraries.yml |  3 ++
 2 files changed, 56 insertions(+)
 create mode 100644 css/modal.css

diff --git a/css/modal.css b/css/modal.css
new file mode 100644
index 0000000..133c457
--- /dev/null
+++ b/css/modal.css
@@ -0,0 +1,53 @@
+.ui-widget-overlay {
+    background: #000;
+    opacity: .5;
+    filter: Alpha(Opacity=50);
+}
+.ui-dialog {
+  background-color: #ffffff;
+  border-radius: 0.5rem;
+  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-lg */
+  width: 100%;
+  max-width: 32rem;
+  margin: 1.5rem;
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+
+.ui-dialog-titlebar {
+  background-color: #1f2937;
+  color: #ffffff;
+  border-top-left-radius: 0.5rem;
+  border-top-right-radius: 0.5rem;
+  padding: 0.5rem 1rem;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.ui-dialog-title {
+  font-weight: 600;
+}
+
+.ui-dialog-titlebar-close {
+  background: none;
+  border: none;
+  color: #ffffff;
+  font-size: 1.25rem;
+  cursor: pointer;
+}
+.ui-dialog-titlebar-close::before {
+  content: "\00d7"; /* Unicode for 'X' */
+  font-size: 1.25rem;
+  color: #ffffff;
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+
+.ui-dialog-content {
+  padding: 1rem;
+}
\ No newline at end of file
diff --git a/page_notifications.libraries.yml b/page_notifications.libraries.yml
index fdcbb3e..9ef4be7 100644
--- a/page_notifications.libraries.yml
+++ b/page_notifications.libraries.yml
@@ -1,5 +1,8 @@
 modal:
   version: 1.x
+  css:
+    theme:
+      css/modal.css: {}
   js:
     js/modal.js: {}
   dependencies:
-- 
GitLab


From 1b0f78538efb4008cc0f18c38d40367a35bee505 Mon Sep 17 00:00:00 2001
From: Nicholas Stees <nstees@gmail.com>
Date: Fri, 7 Feb 2025 07:37:57 -0500
Subject: [PATCH 49/49] Handle both token ids in the data side

---
 src/Token/SubscriptionToken.php | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php
index f71ca88..79cb38c 100644
--- a/src/Token/SubscriptionToken.php
+++ b/src/Token/SubscriptionToken.php
@@ -68,7 +68,9 @@ class SubscriptionToken {
 
     // TODO find why some sites its 'page_notification_subscription' and other its 'subscription'
     if (($type == 'page_notification_subscription' || $type == 'subscription') && (!empty($data['page_notification_subscription']) || !empty($data['subscription']))) {
-      $subscription = $data['page_notification_subscription'];
+      $subscription = !empty($data['page_notification_subscription']) ? 
+        $data['page_notification_subscription'] : 
+        $data['subscription'];
       $bubbleable_metadata->addCacheableDependency($subscription);
 
       foreach ($tokens as $name => $original) {
-- 
GitLab