diff --git a/src/EventSubscriber/InviteCleanupSubscriber.php b/src/EventSubscriber/InviteCleanupSubscriber.php
index 286f052..6c31386 100644
--- a/src/EventSubscriber/InviteCleanupSubscriber.php
+++ b/src/EventSubscriber/InviteCleanupSubscriber.php
@@ -39,7 +39,7 @@ class InviteCleanupSubscriber implements EventSubscriberInterface {
   /**
    * {@inheritdoc}
    */
-  public static function getSubscribedEvents() {
+  public static function getSubscribedEvents(): array {
     // Note: Drupal doesn't have a cron event by default.
     // We'll implement this via hook_cron in the .module file instead.
     return [];
diff --git a/src/EventSubscriber/InviteLoginRedirectSubscriber.php b/src/EventSubscriber/InviteLoginRedirectSubscriber.php
index d587cff..3b12e3a 100644
--- a/src/EventSubscriber/InviteLoginRedirectSubscriber.php
+++ b/src/EventSubscriber/InviteLoginRedirectSubscriber.php
@@ -53,7 +53,7 @@ class InviteLoginRedirectSubscriber implements EventSubscriberInterface {
   /**
    * {@inheritdoc}
    */
-  public static function getSubscribedEvents() {
+  public static function getSubscribedEvents(): array {
     // Run with high priority, before Drupal's access check denies the request.
     $events[KernelEvents::REQUEST][] = ['onRequest', 200];
     return $events;
diff --git a/src/EventSubscriber/RegistrationAccessSubscriber.php b/src/EventSubscriber/RegistrationAccessSubscriber.php
index b02e4e3..5cf78e3 100644
--- a/src/EventSubscriber/RegistrationAccessSubscriber.php
+++ b/src/EventSubscriber/RegistrationAccessSubscriber.php
@@ -58,7 +58,7 @@ class RegistrationAccessSubscriber implements EventSubscriberInterface {
   /**
    * {@inheritdoc}
    */
-  public static function getSubscribedEvents() {
+  public static function getSubscribedEvents(): array {
     // Run early to allow registration access before other checks.
     $events[KernelEvents::REQUEST][] = ['onRequest', 100];
     return $events;
diff --git a/src/EventSubscriber/UserRegistrationSubscriber.php b/src/EventSubscriber/UserRegistrationSubscriber.php
index 87474d0..8ce20ca 100644
--- a/src/EventSubscriber/UserRegistrationSubscriber.php
+++ b/src/EventSubscriber/UserRegistrationSubscriber.php
@@ -71,7 +71,7 @@ class UserRegistrationSubscriber implements EventSubscriberInterface {
   /**
    * {@inheritdoc}
    */
-  public static function getSubscribedEvents() {
+  public static function getSubscribedEvents(): array {
     return [
       KernelEvents::REQUEST => ['onRequest', 50],
     ];
diff --git a/src/Hook/UserReferenceInviteHooks.php b/src/Hook/UserReferenceInviteHooks.php
new file mode 100644
index 0000000..7d88c90
--- /dev/null
+++ b/src/Hook/UserReferenceInviteHooks.php
@@ -0,0 +1,624 @@
+<?php
+
+namespace Drupal\user_reference_invite\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\user\Entity\User;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for user_reference_invite.
+ */
+class UserReferenceInviteHooks
+{
+    use StringTranslationTrait;
+    /**
+     * Implements hook_config_schema_info_alter().
+     *
+     * Adds user_invitation mail template to user.mail config schema.
+     */
+    #[Hook('config_schema_info_alter')]
+    public static function configSchemaInfoAlter(&$definitions)
+    {
+        if (isset($definitions['user.mail'])) {
+            $definitions['user.mail']['mapping']['user_invitation'] = [
+                'type' => 'mail',
+                'label' => 'User invitation email',
+            ];
+            $definitions['user.mail']['mapping']['user_invitation_reminder'] = [
+                'type' => 'mail',
+                'label' => 'User invitation reminder email',
+            ];
+        }
+    }
+    /**
+     * Implements hook_theme().
+     */
+    #[Hook('theme')]
+    public static function theme($existing, $type, $theme, $path)
+    {
+        return [
+            'user_invite_notification' => [
+                'variables' => [
+                    'invite' => NULL,
+                    'invited_by' => NULL,
+                    'entity_label' => NULL,
+                    'register_url' => NULL,
+                    'login_url' => NULL,
+                    'expiry_date' => NULL,
+                    'roles' => [
+                    ],
+                    'user_exists' => FALSE,
+                ],
+                'template' => 'user-invite-notification',
+            ],
+            'user_invite_admin_list' => [
+                'variables' => [
+                    'invitations' => [
+                    ],
+                ],
+                'template' => 'user-invite-admin-list',
+            ],
+        ];
+    }
+    /**
+     * Implements hook_mail().
+     */
+    #[Hook('mail')]
+    public static function mail($key, &$message, $params)
+    {
+        switch ($key) {
+            case 'user_invitation':
+            case 'user_invitation_reminder':
+                // Subject and body will be populated by hook_mail_alter()
+                // using the email template from /admin/config/people/accounts.
+                $message['key'] = $key;
+                $message['params'] = $params;
+                break;
+        }
+    }
+    /**
+     * Implements hook_mail_alter().
+     *
+     * Integrates with Drupal's user account email templates at
+     * /admin/config/people/accounts.
+     */
+    #[Hook('mail_alter')]
+    public static function mailAlter(&$message)
+    {
+        $supported_keys = [
+            'user_invitation',
+            'user_invitation_reminder',
+        ];
+        if ($message['module'] === 'user_reference_invite' && in_array($message['key'], $supported_keys)) {
+            $params = $message['params'];
+            // Get the user invitation email template configuration.
+            $mail_config = \Drupal::config('user.mail');
+            $token_service = \Drupal::token();
+            // Use the appropriate email template key based on mail type.
+            $email_template_key = $message['key'];
+            // Get subject and body from config, or use defaults.
+            $subject = $mail_config->get($email_template_key . '.subject');
+            $body = $mail_config->get($email_template_key . '.body');
+            // Fallback to default if not configured.
+            if (empty($subject)) {
+                if ($email_template_key === 'user_invitation_reminder') {
+                    $subject = 'Reminder: You have been invited to [site:name]';
+                } else {
+                    $subject = 'You have been invited to [site:name]';
+                }
+            }
+            if (empty($body)) {
+                if ($email_template_key === 'user_invitation_reminder') {
+                    $body = "Hello,\n\n";
+                    $body .= "This is a reminder that [user_invite:invited_by] has invited you to [site:name].\n\n";
+                    $body .= "Click the link below to accept this invitation:\n[user_invite:accept_url]\n\n";
+                    $body .= "This invitation will expire on [user_invite:expiry_date].\n\n";
+                    $body .= "Entity: [user_invite:entity_label]\n";
+                } else {
+                    $body = "Hello,\n\n";
+                    $body .= "[user_invite:invited_by] has invited you to [site:name].\n\n";
+                    $body .= "Click the link below to accept this invitation:\n[user_invite:accept_url]\n\n";
+                    $body .= "This invitation will expire on [user_invite:expiry_date].\n\n";
+                    $body .= "Entity: [user_invite:entity_label]\n";
+                }
+            }
+            // Prepare token data.
+            $token_data = [
+                'user_invite' => $params,
+            ];
+            // Replace tokens in subject and body.
+            $message['subject'] = $token_service->replace($subject, $token_data, [
+                'clear' => TRUE,
+            ]);
+            $message['body'] = [
+            ];
+            $message['body'][] = $token_service->replace($body, $token_data, [
+                'clear' => TRUE,
+            ]);
+            // Enable HTML mail if htmlmail or similar module is enabled.
+            // This allows admins to use HTML in their email templates.
+            if (\Drupal::moduleHandler()->moduleExists('htmlmail') || \Drupal::moduleHandler()->moduleExists('swiftmailer') || \Drupal::moduleHandler()->moduleExists('symfony_mailer')) {
+                // Set content type to HTML for better formatting support.
+                $message['headers']['Content-Type'] = 'text/html; charset=UTF-8';
+                // Convert newlines to <br> for HTML display if not already HTML.
+                if (strpos($message['body'][0], '<') === FALSE) {
+                    $message['body'][0] = nl2br(htmlspecialchars($message['body'][0], ENT_QUOTES, 'UTF-8'));
+                }
+            }
+        }
+    }
+    /**
+     * Implements hook_token_info().
+     */
+    #[Hook('token_info')]
+    public function tokenInfo()
+    {
+        $info = [
+        ];
+        // Define a custom token type for user invitations.
+        $info['types']['user_invite'] = [
+            'name' => $this->t('User Invitation'),
+            'description' => $this->t('Tokens related to user invitations.'),
+        ];
+        // Define tokens for the user_invite type.
+        $info['tokens']['user_invite']['invited_by'] = [
+            'name' => $this->t('Invited by'),
+            'description' => $this->t('The name of the user who sent the invitation.'),
+        ];
+        $info['tokens']['user_invite']['entity_label'] = [
+            'name' => $this->t('Entity label'),
+            'description' => $this->t('The label of the entity the user is being invited to.'),
+        ];
+        $info['tokens']['user_invite']['register_url'] = [
+            'name' => $this->t('Registration URL'),
+            'description' => $this->t('URL for the user to register and accept the invitation.'),
+        ];
+        $info['tokens']['user_invite']['login_url'] = [
+            'name' => $this->t('Login URL'),
+            'description' => $this->t('URL for existing users to login and accept the invitation.'),
+        ];
+        $info['tokens']['user_invite']['accept_url'] = [
+            'name' => $this->t('Accept URL'),
+            'description' => $this->t('Direct URL to accept the invitation.'),
+        ];
+        $info['tokens']['user_invite']['expiry_date'] = [
+            'name' => $this->t('Expiry date'),
+            'description' => $this->t('The date when the invitation expires.'),
+        ];
+        $info['tokens']['user_invite']['email'] = [
+            'name' => $this->t('Invited email'),
+            'description' => $this->t('The email address of the invited user.'),
+        ];
+        $info['tokens']['user_invite']['roles'] = [
+            'name' => $this->t('Assigned roles'),
+            'description' => $this->t('Roles that will be assigned to the invited user.'),
+        ];
+        return $info;
+    }
+    /**
+     * Implements hook_tokens().
+     */
+    #[Hook('tokens')]
+    public static function tokens($type, $tokens, array $data, array $options)
+    {
+        $replacements = [
+        ];
+        if ($type === 'user_invite' && !empty($data['user_invite'])) {
+            $params = $data['user_invite'];
+            foreach ($tokens as $name => $original) {
+                switch ($name) {
+                    case 'invited_by':
+                        $replacements[$original] = $params['invited_by'] ?? '';
+                        break;
+                    case 'entity_label':
+                        $replacements[$original] = $params['entity_label'] ?? '';
+                        break;
+                    case 'register_url':
+                        $replacements[$original] = $params['register_url'] ?? '';
+                        break;
+                    case 'login_url':
+                        $replacements[$original] = $params['login_url'] ?? '';
+                        break;
+                    case 'accept_url':
+                        $replacements[$original] = $params['accept_url'] ?? '';
+                        break;
+                    case 'expiry_date':
+                        $replacements[$original] = $params['expiry_date'] ?? '';
+                        break;
+                    case 'email':
+                        if (!empty($params['invite'])) {
+                            $replacements[$original] = $params['invite']->getEmail();
+                        }
+                        break;
+                    case 'roles':
+                        if (!empty($params['roles'])) {
+                            $role_labels = [
+                            ];
+                            $role_storage = \Drupal::entityTypeManager()->getStorage('user_role');
+                            foreach ($params['roles'] as $role_id) {
+                                $role = $role_storage->load($role_id);
+                                if ($role) {
+                                    $role_labels[] = $role->label();
+                                }
+                            }
+                            $replacements[$original] = implode(', ', $role_labels);
+                        } else {
+                            $replacements[$original] = '';
+                        }
+                        break;
+                }
+            }
+        }
+        return $replacements;
+    }
+    /**
+     * Implements hook_form_FORM_ID_alter() for user_admin_settings.
+     *
+     * Adds user invitation email template to the user account settings page.
+     */
+    #[Hook('form_user_admin_settings_alter')]
+    public function formUserAdminSettingsAlter(&$form, $form_state, $form_id)
+    {
+        $config = \Drupal::config('user.mail');
+        // Add user invitation email settings.
+        $form['email_user_invitation'] = [
+            '#type' => 'details',
+            '#title' => $this->t('User Invitation'),
+            '#description' => $this->t('Edit the email messages sent to users when they are invited by the User Invite Reference module.'),
+            '#group' => 'email',
+            '#weight' => 10,
+        ];
+        $form['email_user_invitation']['user_invitation_subject'] = [
+            '#type' => 'textfield',
+            '#title' => $this->t('Subject'),
+            '#default_value' => $config->get('user_invitation.subject') ?: 'You have been invited to [site:name]',
+            '#maxlength' => 180,
+            '#description' => $this->t('The subject of the invitation email.'),
+        ];
+        $form['email_user_invitation']['user_invitation_body'] = [
+            '#type' => 'textarea',
+            '#title' => $this->t('Body'),
+            '#default_value' => $config->get('user_invitation.body') ?: "Hello,\n\n[user_invite:invited_by] has invited you to [site:name].\n\nClick the link below to accept this invitation:\n[user_invite:accept_url]\n\nThis invitation will expire on [user_invite:expiry_date].\n\nEntity: [user_invite:entity_label]",
+            '#rows' => 12,
+            '#description' => $this->t('The body of the invitation email.'),
+        ];
+        // Add user invitation reminder email settings.
+        $form['email_user_invitation_reminder'] = [
+            '#type' => 'details',
+            '#title' => $this->t('User Invitation Reminder'),
+            '#description' => $this->t('Edit the reminder email sent to users who have not yet accepted their invitation.'),
+            '#group' => 'email',
+            '#weight' => 11,
+        ];
+        $form['email_user_invitation_reminder']['user_invitation_reminder_subject'] = [
+            '#type' => 'textfield',
+            '#title' => $this->t('Subject'),
+            '#default_value' => $config->get('user_invitation_reminder.subject') ?: 'Reminder: You have been invited to [site:name]',
+            '#maxlength' => 180,
+            '#description' => $this->t('The subject of the invitation reminder email.'),
+        ];
+        $form['email_user_invitation_reminder']['user_invitation_reminder_body'] = [
+            '#type' => 'textarea',
+            '#title' => $this->t('Body'),
+            '#default_value' => $config->get('user_invitation_reminder.body') ?: "Hello,\n\nThis is a reminder that [user_invite:invited_by] has invited you to [site:name].\n\nClick the link below to accept this invitation:\n[user_invite:accept_url]\n\nThis invitation will expire on [user_invite:expiry_date].\n\nEntity: [user_invite:entity_label]",
+            '#rows' => 12,
+            '#description' => $this->t('The body of the invitation reminder email.'),
+        ];
+        // Add available tokens help.
+        if (\Drupal::moduleHandler()->moduleExists('token')) {
+            $form['email_user_invitation']['token_tree'] = [
+                '#theme' => 'token_tree_link',
+                '#token_types' => [
+                    'user_invite',
+                    'site',
+                ],
+                '#weight' => 100,
+            ];
+        } else {
+            $form['email_user_invitation']['token_help'] = [
+                '#type' => 'details',
+                '#title' => $this->t('Available tokens'),
+                '#weight' => 100,
+            ];
+            $tokens_info = [
+                '[user_invite:invited_by]' => $this->t('The name of the user who sent the invitation.'),
+                '[user_invite:entity_label]' => $this->t('The label of the entity the user is being invited to.'),
+                '[user_invite:register_url]' => $this->t('URL for the user to register and accept the invitation.'),
+                '[user_invite:login_url]' => $this->t('URL for existing users to login and accept the invitation.'),
+                '[user_invite:accept_url]' => $this->t('Direct URL to accept the invitation.'),
+                '[user_invite:expiry_date]' => $this->t('The date when the invitation expires.'),
+                '[user_invite:email]' => $this->t('The email address of the invited user.'),
+                '[user_invite:roles]' => $this->t('Roles that will be assigned to the invited user.'),
+                '[site:name]' => $this->t('The name of the site.'),
+                '[site:url]' => $this->t('The URL of the site.'),
+            ];
+            $token_list = '<ul>';
+            foreach ($tokens_info as $token => $description) {
+                $token_list .= '<li><strong>' . $token . '</strong>: ' . $description . '</li>';
+            }
+            $token_list .= '</ul>';
+            $form['email_user_invitation']['token_help']['list'] = [
+                '#markup' => $token_list,
+            ];
+        }
+        // Add custom submit handler to save invitation email settings.
+        $form['#submit'][] = 'user_reference_invite_user_admin_settings_submit';
+    }
+    /**
+     * Implements hook_entity_insert().
+     *
+     * Process pending invitations after entity is created.
+     */
+    #[Hook('entity_insert')]
+    public static function entityInsert(\Drupal\Core\Entity\EntityInterface $entity)
+    {
+        // Check for pending invitations in tempstore.
+        $tempstore = \Drupal::service('tempstore.private')->get('user_reference_invite');
+        $pending_invites = $tempstore->get('pending_invites');
+        if (empty($pending_invites)) {
+            return;
+        }
+        $invite_manager = \Drupal::service('user_reference_invite.invite_manager');
+        $messenger = \Drupal::messenger();
+        $success_count = 0;
+        $remaining_invites = [
+        ];
+        foreach ($pending_invites as $invite_data) {
+            // Only process invitations for this specific entity type.
+            // Don't process yet if it's for a different entity type.
+            if ($invite_data['entity_type'] !== $entity->getEntityTypeId()) {
+                $remaining_invites[] = $invite_data;
+                continue;
+            }
+            try {
+                $invite = $invite_manager->createInvitation($invite_data['email'], $entity->getEntityTypeId(), $entity->id(), $invite_data['field_name'], $invite_data['roles'], $invite_data['metadata']);
+                // Send email.
+                if ($invite_manager->sendInvitationEmail($invite)) {
+                    $success_count++;
+                }
+            } catch (\Exception $e) {
+                \Drupal::logger('user_reference_invite')->error('Failed to create invitation for @email: @message', [
+                    '@email' => $invite_data['email'],
+                    '@message' => $e->getMessage(),
+                ]);
+            }
+        }
+        // Update tempstore with remaining invites (for other entity types).
+        if (empty($remaining_invites)) {
+            $tempstore->delete('pending_invites');
+        } else {
+            $tempstore->set('pending_invites', $remaining_invites);
+        }
+        // Show success message if invitations were sent.
+        if ($success_count > 0) {
+            $messenger->addStatus(\Drupal::translation()->formatPlural($success_count, 'Invitation sent to 1 user.', 'Invitations sent to @count users.'));
+        }
+    }
+    /**
+     * Implements hook_entity_delete().
+     *
+     * Clean up invitations when parent entity is deleted.
+     */
+    #[Hook('entity_delete')]
+    public static function entityDelete(\Drupal\Core\Entity\EntityInterface $entity)
+    {
+        $storage = \Drupal::entityTypeManager()->getStorage('user_invite');
+        $ids = $storage->getQuery()->condition('entity_type', $entity->getEntityTypeId())->condition('entity_id', $entity->id())->accessCheck(FALSE)->execute();
+        if ($ids) {
+            $invites = $storage->loadMultiple($ids);
+            $storage->delete($invites);
+        }
+    }
+    /**
+     * Implements hook_ENTITY_TYPE_insert() for user entities.
+     *
+     * Process pending invitations when a new user registers.
+     */
+    #[Hook('user_insert')]
+    public function userInsert(\Drupal\Core\Entity\EntityInterface $user)
+    {
+        /** @var \Drupal\user\UserInterface $user */
+        $invite_manager = \Drupal::service('user_reference_invite.invite_manager');
+        // Check for pending invitations for this email.
+        $invites = $invite_manager->findPendingInvitationsByEmail($user->getEmail());
+        if (empty($invites)) {
+            return;
+        }
+        $messenger = \Drupal::messenger();
+        $user_updated = FALSE;
+        foreach ($invites as $invite) {
+            try {
+                // Accept invitation (without token verification since email matches).
+                $invite->setStatus('accepted');
+                $invite->set('accepted', \Drupal::time()->getRequestTime());
+                $invite->set('accepted_by', $user->id());
+                $invite->save();
+                // Assign roles from invitation to the user.
+                $roles_json = $invite->get('roles')->value;
+                if ($roles_json) {
+                    $roles = json_decode($roles_json, TRUE);
+                    if (is_array($roles) && !empty($roles)) {
+                        foreach ($roles as $role) {
+                            if (!$user->hasRole($role)) {
+                                $user->addRole($role);
+                                $user_updated = TRUE;
+                            }
+                        }
+                        \Drupal::logger('user_reference_invite')->info('Assigned roles @roles to user @uid from invitation', [
+                            '@roles' => implode(', ', $roles),
+                            '@uid' => $user->id(),
+                        ]);
+                    }
+                }
+                // Attach to field.
+                $invite_manager->attachUserToField($invite, $user->id());
+                // Load entity for message.
+                $entity = \Drupal::entityTypeManager()->getStorage($invite->getTargetEntityType())->load($invite->getTargetEntityId());
+                // Custom field assignment logic can be added here via hook impl.
+                if ($entity) {
+                    $messenger->addStatus($this->t('You have been added to @entity', [
+                        '@entity' => $entity->label(),
+                    ]));
+                }
+            } catch (\Exception $e) {
+                \Drupal::logger('user_reference_invite')->error('Failed to process invitation: @message', [
+                    '@message' => $e->getMessage(),
+                ]);
+            }
+        }
+        // Save user if any updates were made.
+        if ($user_updated) {
+            $user->save();
+        }
+    }
+    /**
+     * Implements hook_cron().
+     *
+     * Clean up expired invitations.
+     */
+    #[Hook('cron')]
+    public static function cron()
+    {
+        $invite_manager = \Drupal::service('user_reference_invite.invite_manager');
+        // Process automatic reminders for pending invitations nearing expiry.
+        $reminders_sent = $invite_manager->processAutomaticReminders();
+        if ($reminders_sent > 0) {
+            \Drupal::logger('user_reference_invite')->info('Sent @count automatic invitation reminders', [
+                '@count' => $reminders_sent,
+            ]);
+        }
+        // Clean up expired invitations.
+        $cleaned = $invite_manager->cleanupExpiredInvitations();
+        if ($cleaned > 0) {
+            \Drupal::logger('user_reference_invite')->info('Cleaned up @count expired invitations', [
+                '@count' => $cleaned,
+            ]);
+        }
+    }
+    /**
+     * Implements hook_form_FORM_ID_alter() for user_login_form.
+     *
+     * Handle invitation token in login form.
+     */
+    #[Hook('form_user_login_form_alter')]
+    public function formUserLoginFormAlter(&$form, $form_state, $form_id)
+    {
+        $request = \Drupal::request();
+        $token = $request->query->get('invite_token');
+        if (!$token) {
+            return;
+        }
+        \Drupal::logger('user_reference_invite')->info('Login form alter: Found token @token', [
+            '@token' => $token,
+        ]);
+        // Check if user is already logged in.
+        $current_user = \Drupal::currentUser();
+        if ($current_user->isAuthenticated()) {
+            // User is already logged in, process invitation immediately.
+            \Drupal::logger('user_reference_invite')->info('User @uid already logged in, processing invitation immediately', [
+                '@uid' => $current_user->id(),
+            ]);
+            $account = \Drupal\user\Entity\User::load($current_user->id());
+            user_reference_invite_process_invitation_for_user($token, $account);
+            return;
+        }
+        // Validate the invitation token.
+        $invite_manager = \Drupal::service('user_reference_invite.invite_manager');
+        $invite = $invite_manager->validateToken($token);
+        if (!$invite) {
+            \Drupal::messenger()->addError($this->t('The invitation link is invalid or has expired.'));
+            return;
+        }
+        // Store the token in tempstore for processing after login.
+        $tempstore = \Drupal::service('tempstore.private')->get('user_reference_invite');
+        $tempstore->set('pending_login_token', $token);
+        \Drupal::logger('user_reference_invite')->info('Stored token in tempstore for later processing');
+        // Load the entity to get its label.
+        $entity_label = $this->t('the content');
+        try {
+            $entity = \Drupal::entityTypeManager()->getStorage($invite->getTargetEntityType())->load($invite->getTargetEntityId());
+            if ($entity) {
+                $entity_label = $entity->label();
+            }
+        } catch (\Exception $e) {
+            \Drupal::logger('user_reference_invite')->error('Failed to load entity for invitation message: @message', [
+                '@message' => $e->getMessage(),
+            ]);
+        }
+        // Add a message to inform the user.
+        \Drupal::messenger()->addStatus($this->t('Please log in to accept your invitation to @entity.', [
+            '@entity' => $entity_label,
+        ]));
+        // Add a custom submit handler to process invitation after login.
+        array_unshift($form['#submit'], 'user_reference_invite_login_submit');
+    }
+    /**
+     * Implements hook_user_login().
+     *
+     * Process invitation when user logs in with invitation token.
+     */
+    #[Hook('user_login')]
+    public static function userLogin($account)
+    {
+        \Drupal::logger('user_reference_invite')->info('hook_user_login called for user @uid (@email)', [
+            '@uid' => $account->id(),
+            '@email' => $account->getEmail(),
+        ]);
+        // Check if there's a pending invitation token from login.
+        $tempstore = \Drupal::service('tempstore.private')->get('user_reference_invite');
+        $token = $tempstore->get('pending_login_token');
+        if (!$token) {
+            \Drupal::logger('user_reference_invite')->info('No pending login token found in tempstore');
+            return;
+        }
+        \Drupal::logger('user_reference_invite')->info('Found pending token in tempstore: @token', [
+            '@token' => $token,
+        ]);
+        // Clear the token from tempstore.
+        $tempstore->delete('pending_login_token');
+        // Process the invitation using the helper function.
+        user_reference_invite_process_invitation_for_user($token, $account);
+    }
+    /**
+     * Implements hook_form_FORM_ID_alter() for user_register_form.
+     *
+     * Allow registration when valid invitation token is present.
+     */
+    #[Hook('form_user_register_form_alter')]
+    public function formUserRegisterFormAlter(&$form, $form_state, $form_id)
+    {
+        $request = \Drupal::request();
+        $token = $request->query->get('invite_token');
+        if (!$token) {
+            return;
+        }
+        // Validate the invitation token.
+        $invite_manager = \Drupal::service('user_reference_invite.invite_manager');
+        $invite = $invite_manager->validateToken($token);
+        if (!$invite) {
+            \Drupal::messenger()->addError($this->t('The invitation link is invalid or has expired.'));
+            return;
+        }
+        // Pre-fill email if available.
+        $email = $invite->getEmail();
+        if ($email && !empty($form['account']['mail'])) {
+            $form['account']['mail']['#default_value'] = $email;
+            $form['account']['mail']['#disabled'] = TRUE;
+            $form['account']['mail']['#description'] = $this->t('This email address is from your invitation and cannot be changed.');
+        }
+        // Enable password fields so user can set password during registration.
+        // This prevents the "set password after first login" workflow.
+        if (isset($form['account']['pass'])) {
+            $form['account']['pass']['#access'] = TRUE;
+            $form['account']['pass']['#required'] = TRUE;
+        }
+        // Custom field pre-fill logic can be added here via hook implementation.
+        // Add a custom submit handler to process the invitation after registration.
+        $form['actions']['submit']['#submit'][] = 'user_reference_invite_register_submit';
+        // Add the token to form state so we can use it in the submit handler.
+        $form_state->set('invite_token', $token);
+        // Add a message to inform the user.
+        \Drupal::messenger()->addStatus($this->t('You are registering using an invitation. Please complete the form below.'));
+    }
+}
diff --git a/src/Plugin/Field/FieldWidget/UserReferenceInviteWidget.php b/src/Plugin/Field/FieldWidget/UserReferenceInviteWidget.php
index fb22af7..66f4ddc 100644
--- a/src/Plugin/Field/FieldWidget/UserReferenceInviteWidget.php
+++ b/src/Plugin/Field/FieldWidget/UserReferenceInviteWidget.php
@@ -451,7 +451,7 @@ class UserReferenceInviteWidget extends EntityReferenceAutocompleteWidget {
               else {
                 // Entity already saved, create invitation immediately.
                 // Check if user is already a member of this field.
-                $existing_user = user_load_by_mail($email);
+                $existing_user = array_values(\Drupal::entityTypeManager()->getStorage('user')->loadByProperties(['mail' => $email]))[0] ?? FALSE;
                 if ($existing_user) {
                   $field_values = $entity->get(
                     $this->fieldDefinition->getName()
diff --git a/user_reference_invite.info.yml b/user_reference_invite.info.yml
index b593dc9..6eca8aa 100644
--- a/user_reference_invite.info.yml
+++ b/user_reference_invite.info.yml
@@ -2,7 +2,7 @@ name: User Reference with Invite
 type: module
 description: 'Enhances User Reference fields with invitation workflow capabilities. Integrates with Drupal email templates and supports custom tokens.'
 package: User
-core_version_requirement: ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 dependencies:
   - drupal:user
   - drupal:field
diff --git a/user_reference_invite.module b/user_reference_invite.module
index bd98c24..fbf8730 100644
--- a/user_reference_invite.module
+++ b/user_reference_invite.module
@@ -4,7 +4,8 @@
  * @file
  * User Invite Reference module.
  */
-
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\user_reference_invite\Hook\UserReferenceInviteHooks;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\user\Entity\User;
 
@@ -13,59 +14,25 @@ use Drupal\user\Entity\User;
  *
  * Adds user_invitation mail template to user.mail config schema.
  */
+#[LegacyHook]
 function user_reference_invite_config_schema_info_alter(&$definitions) {
-  if (isset($definitions['user.mail'])) {
-    $definitions['user.mail']['mapping']['user_invitation'] = [
-      'type' => 'mail',
-      'label' => 'User invitation email',
-    ];
-    $definitions['user.mail']['mapping']['user_invitation_reminder'] = [
-      'type' => 'mail',
-      'label' => 'User invitation reminder email',
-    ];
-  }
+  \Drupal::service(UserReferenceInviteHooks::class)->configSchemaInfoAlter($definitions);
 }
 
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function user_reference_invite_theme($existing, $type, $theme, $path) {
-  return [
-    'user_invite_notification' => [
-      'variables' => [
-        'invite' => NULL,
-        'invited_by' => NULL,
-        'entity_label' => NULL,
-        'register_url' => NULL,
-        'login_url' => NULL,
-        'expiry_date' => NULL,
-        'roles' => [],
-        'user_exists' => FALSE,
-      ],
-      'template' => 'user-invite-notification',
-    ],
-    'user_invite_admin_list' => [
-      'variables' => [
-        'invitations' => [],
-      ],
-      'template' => 'user-invite-admin-list',
-    ],
-  ];
+  return \Drupal::service(UserReferenceInviteHooks::class)->theme($existing, $type, $theme, $path);
 }
 
 /**
  * Implements hook_mail().
  */
+#[LegacyHook]
 function user_reference_invite_mail($key, &$message, $params) {
-  switch ($key) {
-    case 'user_invitation':
-    case 'user_invitation_reminder':
-      // Subject and body will be populated by hook_mail_alter()
-      // using the email template from /admin/config/people/accounts.
-      $message['key'] = $key;
-      $message['params'] = $params;
-      break;
-  }
+  \Drupal::service(UserReferenceInviteHooks::class)->mail($key, $message, $params);
 }
 
 /**
@@ -74,193 +41,28 @@ function user_reference_invite_mail($key, &$message, $params) {
  * Integrates with Drupal's user account email templates at
  * /admin/config/people/accounts.
  */
-function user_reference_invite_mail_alter(&$message) {
-  $supported_keys = ['user_invitation', 'user_invitation_reminder'];
-  if ($message['module'] === 'user_reference_invite' && in_array($message['key'], $supported_keys)) {
-    $params = $message['params'];
-
-    // Get the user invitation email template configuration.
-    $mail_config = \Drupal::config('user.mail');
-    $token_service = \Drupal::token();
-
-    // Use the appropriate email template key based on mail type.
-    $email_template_key = $message['key'];
-
-    // Get subject and body from config, or use defaults.
-    $subject = $mail_config->get($email_template_key . '.subject');
-    $body = $mail_config->get($email_template_key . '.body');
-
-    // Fallback to default if not configured.
-    if (empty($subject)) {
-      if ($email_template_key === 'user_invitation_reminder') {
-        $subject = 'Reminder: You have been invited to [site:name]';
-      }
-      else {
-        $subject = 'You have been invited to [site:name]';
-      }
-    }
-
-    if (empty($body)) {
-      if ($email_template_key === 'user_invitation_reminder') {
-        $body = "Hello,\n\n";
-        $body .= "This is a reminder that [user_invite:invited_by] has invited you to [site:name].\n\n";
-        $body .= "Click the link below to accept this invitation:\n[user_invite:accept_url]\n\n";
-        $body .= "This invitation will expire on [user_invite:expiry_date].\n\n";
-        $body .= "Entity: [user_invite:entity_label]\n";
-      }
-      else {
-        $body = "Hello,\n\n";
-        $body .= "[user_invite:invited_by] has invited you to [site:name].\n\n";
-        $body .= "Click the link below to accept this invitation:\n[user_invite:accept_url]\n\n";
-        $body .= "This invitation will expire on [user_invite:expiry_date].\n\n";
-        $body .= "Entity: [user_invite:entity_label]\n";
-      }
-    }
-
-    // Prepare token data.
-    $token_data = [
-      'user_invite' => $params,
-    ];
-
-    // Replace tokens in subject and body.
-    $message['subject'] = $token_service->replace($subject, $token_data, ['clear' => TRUE]);
-    $message['body'] = [];
-    $message['body'][] = $token_service->replace($body, $token_data, ['clear' => TRUE]);
-
-    // Enable HTML mail if htmlmail or similar module is enabled.
-    // This allows admins to use HTML in their email templates.
-    if (\Drupal::moduleHandler()->moduleExists('htmlmail') ||
-        \Drupal::moduleHandler()->moduleExists('swiftmailer') ||
-        \Drupal::moduleHandler()->moduleExists('symfony_mailer')) {
-      // Set content type to HTML for better formatting support.
-      $message['headers']['Content-Type'] = 'text/html; charset=UTF-8';
-
-      // Convert newlines to <br> for HTML display if not already HTML.
-      if (strpos($message['body'][0], '<') === FALSE) {
-        $message['body'][0] = nl2br(htmlspecialchars($message['body'][0], ENT_QUOTES, 'UTF-8'));
-      }
-    }
-  }
+#[LegacyHook]
+function user_reference_invite_mail_alter(&$message)
+{
+    \Drupal::service(UserReferenceInviteHooks::class)->mailAlter($message);
 }
 
 /**
  * Implements hook_token_info().
  */
-function user_reference_invite_token_info() {
-  $info = [];
-
-  // Define a custom token type for user invitations.
-  $info['types']['user_invite'] = [
-    'name' => t('User Invitation'),
-    'description' => t('Tokens related to user invitations.'),
-  ];
-
-  // Define tokens for the user_invite type.
-  $info['tokens']['user_invite']['invited_by'] = [
-    'name' => t('Invited by'),
-    'description' => t('The name of the user who sent the invitation.'),
-  ];
-
-  $info['tokens']['user_invite']['entity_label'] = [
-    'name' => t('Entity label'),
-    'description' => t('The label of the entity the user is being invited to.'),
-  ];
-
-  $info['tokens']['user_invite']['register_url'] = [
-    'name' => t('Registration URL'),
-    'description' => t('URL for the user to register and accept the invitation.'),
-  ];
-
-  $info['tokens']['user_invite']['login_url'] = [
-    'name' => t('Login URL'),
-    'description' => t('URL for existing users to login and accept the invitation.'),
-  ];
-
-  $info['tokens']['user_invite']['accept_url'] = [
-    'name' => t('Accept URL'),
-    'description' => t('Direct URL to accept the invitation.'),
-  ];
-
-  $info['tokens']['user_invite']['expiry_date'] = [
-    'name' => t('Expiry date'),
-    'description' => t('The date when the invitation expires.'),
-  ];
-
-  $info['tokens']['user_invite']['email'] = [
-    'name' => t('Invited email'),
-    'description' => t('The email address of the invited user.'),
-  ];
-
-  $info['tokens']['user_invite']['roles'] = [
-    'name' => t('Assigned roles'),
-    'description' => t('Roles that will be assigned to the invited user.'),
-  ];
-
-  return $info;
+#[LegacyHook]
+function user_reference_invite_token_info()
+{
+    return \Drupal::service(UserReferenceInviteHooks::class)->tokenInfo();
 }
 
 /**
  * Implements hook_tokens().
  */
-function user_reference_invite_tokens($type, $tokens, array $data, array $options) {
-  $replacements = [];
-
-  if ($type === 'user_invite' && !empty($data['user_invite'])) {
-    $params = $data['user_invite'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'invited_by':
-          $replacements[$original] = $params['invited_by'] ?? '';
-          break;
-
-        case 'entity_label':
-          $replacements[$original] = $params['entity_label'] ?? '';
-          break;
-
-        case 'register_url':
-          $replacements[$original] = $params['register_url'] ?? '';
-          break;
-
-        case 'login_url':
-          $replacements[$original] = $params['login_url'] ?? '';
-          break;
-
-        case 'accept_url':
-          $replacements[$original] = $params['accept_url'] ?? '';
-          break;
-
-        case 'expiry_date':
-          $replacements[$original] = $params['expiry_date'] ?? '';
-          break;
-
-        case 'email':
-          if (!empty($params['invite'])) {
-            $replacements[$original] = $params['invite']->getEmail();
-          }
-          break;
-
-        case 'roles':
-          if (!empty($params['roles'])) {
-            $role_labels = [];
-            $role_storage = \Drupal::entityTypeManager()->getStorage('user_role');
-            foreach ($params['roles'] as $role_id) {
-              $role = $role_storage->load($role_id);
-              if ($role) {
-                $role_labels[] = $role->label();
-              }
-            }
-            $replacements[$original] = implode(', ', $role_labels);
-          }
-          else {
-            $replacements[$original] = '';
-          }
-          break;
-      }
-    }
-  }
-
-  return $replacements;
+#[LegacyHook]
+function user_reference_invite_tokens($type, $tokens, array $data, array $options)
+{
+    return \Drupal::service(UserReferenceInviteHooks::class)->tokens($type, $tokens, $data, $options);
 }
 
 /**
@@ -268,100 +70,10 @@ function user_reference_invite_tokens($type, $tokens, array $data, array $option
  *
  * Adds user invitation email template to the user account settings page.
  */
-function user_reference_invite_form_user_admin_settings_alter(&$form, $form_state, $form_id) {
-  $config = \Drupal::config('user.mail');
-
-  // Add user invitation email settings.
-  $form['email_user_invitation'] = [
-    '#type' => 'details',
-    '#title' => t('User Invitation'),
-    '#description' => t('Edit the email messages sent to users when they are invited by the User Invite Reference module.'),
-    '#group' => 'email',
-    '#weight' => 10,
-  ];
-
-  $form['email_user_invitation']['user_invitation_subject'] = [
-    '#type' => 'textfield',
-    '#title' => t('Subject'),
-    '#default_value' => $config->get('user_invitation.subject') ?: 'You have been invited to [site:name]',
-    '#maxlength' => 180,
-    '#description' => t('The subject of the invitation email.'),
-  ];
-
-  $form['email_user_invitation']['user_invitation_body'] = [
-    '#type' => 'textarea',
-    '#title' => t('Body'),
-    '#default_value' => $config->get('user_invitation.body') ?: "Hello,\n\n[user_invite:invited_by] has invited you to [site:name].\n\nClick the link below to accept this invitation:\n[user_invite:accept_url]\n\nThis invitation will expire on [user_invite:expiry_date].\n\nEntity: [user_invite:entity_label]",
-    '#rows' => 12,
-    '#description' => t('The body of the invitation email.'),
-  ];
-
-  // Add user invitation reminder email settings.
-  $form['email_user_invitation_reminder'] = [
-    '#type' => 'details',
-    '#title' => t('User Invitation Reminder'),
-    '#description' => t('Edit the reminder email sent to users who have not yet accepted their invitation.'),
-    '#group' => 'email',
-    '#weight' => 11,
-  ];
-
-  $form['email_user_invitation_reminder']['user_invitation_reminder_subject'] = [
-    '#type' => 'textfield',
-    '#title' => t('Subject'),
-    '#default_value' => $config->get('user_invitation_reminder.subject') ?: 'Reminder: You have been invited to [site:name]',
-    '#maxlength' => 180,
-    '#description' => t('The subject of the invitation reminder email.'),
-  ];
-
-  $form['email_user_invitation_reminder']['user_invitation_reminder_body'] = [
-    '#type' => 'textarea',
-    '#title' => t('Body'),
-    '#default_value' => $config->get('user_invitation_reminder.body') ?: "Hello,\n\nThis is a reminder that [user_invite:invited_by] has invited you to [site:name].\n\nClick the link below to accept this invitation:\n[user_invite:accept_url]\n\nThis invitation will expire on [user_invite:expiry_date].\n\nEntity: [user_invite:entity_label]",
-    '#rows' => 12,
-    '#description' => t('The body of the invitation reminder email.'),
-  ];
-
-  // Add available tokens help.
-  if (\Drupal::moduleHandler()->moduleExists('token')) {
-    $form['email_user_invitation']['token_tree'] = [
-      '#theme' => 'token_tree_link',
-      '#token_types' => ['user_invite', 'site'],
-      '#weight' => 100,
-    ];
-  }
-  else {
-    $form['email_user_invitation']['token_help'] = [
-      '#type' => 'details',
-      '#title' => t('Available tokens'),
-      '#weight' => 100,
-    ];
-
-    $tokens_info = [
-      '[user_invite:invited_by]' => t('The name of the user who sent the invitation.'),
-      '[user_invite:entity_label]' => t('The label of the entity the user is being invited to.'),
-      '[user_invite:register_url]' => t('URL for the user to register and accept the invitation.'),
-      '[user_invite:login_url]' => t('URL for existing users to login and accept the invitation.'),
-      '[user_invite:accept_url]' => t('Direct URL to accept the invitation.'),
-      '[user_invite:expiry_date]' => t('The date when the invitation expires.'),
-      '[user_invite:email]' => t('The email address of the invited user.'),
-      '[user_invite:roles]' => t('Roles that will be assigned to the invited user.'),
-      '[site:name]' => t('The name of the site.'),
-      '[site:url]' => t('The URL of the site.'),
-    ];
-
-    $token_list = '<ul>';
-    foreach ($tokens_info as $token => $description) {
-      $token_list .= '<li><strong>' . $token . '</strong>: ' . $description . '</li>';
-    }
-    $token_list .= '</ul>';
-
-    $form['email_user_invitation']['token_help']['list'] = [
-      '#markup' => $token_list,
-    ];
-  }
-
-  // Add custom submit handler to save invitation email settings.
-  $form['#submit'][] = 'user_reference_invite_user_admin_settings_submit';
+#[LegacyHook]
+function user_reference_invite_form_user_admin_settings_alter(&$form, $form_state, $form_id)
+{
+    \Drupal::service(UserReferenceInviteHooks::class)->formUserAdminSettingsAlter($form, $form_state, $form_id);
 }
 
 /**
@@ -382,72 +94,10 @@ function user_reference_invite_user_admin_settings_submit($form, $form_state) {
  *
  * Process pending invitations after entity is created.
  */
-function user_reference_invite_entity_insert(EntityInterface $entity) {
-  // Check for pending invitations in tempstore.
-  $tempstore = \Drupal::service('tempstore.private')->get('user_reference_invite');
-  $pending_invites = $tempstore->get('pending_invites');
-
-  if (empty($pending_invites)) {
-    return;
-  }
-
-  $invite_manager = \Drupal::service('user_reference_invite.invite_manager');
-  $messenger = \Drupal::messenger();
-  $success_count = 0;
-  $remaining_invites = [];
-
-  foreach ($pending_invites as $invite_data) {
-    // Only process invitations for this specific entity type.
-    // Don't process yet if it's for a different entity type.
-    if ($invite_data['entity_type'] !== $entity->getEntityTypeId()) {
-      $remaining_invites[] = $invite_data;
-      continue;
-    }
-
-    try {
-      $invite = $invite_manager->createInvitation(
-        $invite_data['email'],
-        $entity->getEntityTypeId(),
-        $entity->id(),
-        $invite_data['field_name'],
-        $invite_data['roles'],
-        $invite_data['metadata']
-      );
-
-      // Send email.
-      if ($invite_manager->sendInvitationEmail($invite)) {
-        $success_count++;
-      }
-    }
-    catch (\Exception $e) {
-      \Drupal::logger('user_reference_invite')->error(
-        'Failed to create invitation for @email: @message',
-        [
-          '@email' => $invite_data['email'],
-          '@message' => $e->getMessage(),
-        ]
-      );
-    }
-  }
-
-  // Update tempstore with remaining invites (for other entity types).
-  if (empty($remaining_invites)) {
-    $tempstore->delete('pending_invites');
-  }
-  else {
-    $tempstore->set('pending_invites', $remaining_invites);
-  }
-
-  // Show success message if invitations were sent.
-  if ($success_count > 0) {
-    $messenger->addStatus(
-      \Drupal::translation()->formatPlural(
-        $success_count,
-        'Invitation sent to 1 user.',
-        'Invitations sent to @count users.'
-      )
-    );
-  }
+#[LegacyHook]
+function user_reference_invite_entity_insert(EntityInterface $entity)
+{
+    \Drupal::service(UserReferenceInviteHooks::class)->entityInsert($entity);
 }
 
 /**
@@ -455,18 +105,10 @@ function user_reference_invite_entity_insert(EntityInterface $entity) {
  *
  * Clean up invitations when parent entity is deleted.
  */
-function user_reference_invite_entity_delete(EntityInterface $entity) {
-  $storage = \Drupal::entityTypeManager()->getStorage('user_invite');
-  $ids = $storage->getQuery()
-    ->condition('entity_type', $entity->getEntityTypeId())
-    ->condition('entity_id', $entity->id())
-    ->accessCheck(FALSE)
-    ->execute();
-
-  if ($ids) {
-    $invites = $storage->loadMultiple($ids);
-    $storage->delete($invites);
-  }
+#[LegacyHook]
+function user_reference_invite_entity_delete(EntityInterface $entity)
+{
+    \Drupal::service(UserReferenceInviteHooks::class)->entityDelete($entity);
 }
 
 /**
@@ -474,72 +116,10 @@ function user_reference_invite_entity_delete(EntityInterface $entity) {
  *
  * Process pending invitations when a new user registers.
  */
-function user_reference_invite_user_insert(EntityInterface $user) {
-  /** @var \Drupal\user\UserInterface $user */
-  $invite_manager = \Drupal::service('user_reference_invite.invite_manager');
-
-  // Check for pending invitations for this email.
-  $invites = $invite_manager->findPendingInvitationsByEmail($user->getEmail());
-
-  if (empty($invites)) {
-    return;
-  }
-
-  $messenger = \Drupal::messenger();
-  $user_updated = FALSE;
-
-  foreach ($invites as $invite) {
-    try {
-      // Accept invitation (without token verification since email matches).
-      $invite->setStatus('accepted');
-      $invite->set('accepted', \Drupal::time()->getRequestTime());
-      $invite->set('accepted_by', $user->id());
-      $invite->save();
-
-      // Assign roles from invitation to the user.
-      $roles_json = $invite->get('roles')->value;
-      if ($roles_json) {
-        $roles = json_decode($roles_json, TRUE);
-        if (is_array($roles) && !empty($roles)) {
-          foreach ($roles as $role) {
-            if (!$user->hasRole($role)) {
-              $user->addRole($role);
-              $user_updated = TRUE;
-            }
-          }
-          \Drupal::logger('user_reference_invite')->info('Assigned roles @roles to user @uid from invitation', [
-            '@roles' => implode(', ', $roles),
-            '@uid' => $user->id(),
-          ]);
-        }
-      }
-
-      // Attach to field.
-      $invite_manager->attachUserToField($invite, $user->id());
-
-      // Load entity for message.
-      $entity = \Drupal::entityTypeManager()
-        ->getStorage($invite->getTargetEntityType())
-        ->load($invite->getTargetEntityId());
-
-      // Custom field assignment logic can be added here via hook impl.
-      if ($entity) {
-        $messenger->addStatus(t('You have been added to @entity', [
-          '@entity' => $entity->label(),
-        ]));
-      }
-    }
-    catch (\Exception $e) {
-      \Drupal::logger('user_reference_invite')->error('Failed to process invitation: @message', [
-        '@message' => $e->getMessage(),
-      ]);
-    }
-  }
-
-  // Save user if any updates were made.
-  if ($user_updated) {
-    $user->save();
-  }
+#[LegacyHook]
+function user_reference_invite_user_insert(EntityInterface $user)
+{
+    \Drupal::service(UserReferenceInviteHooks::class)->userInsert($user);
 }
 
 /**
@@ -648,24 +228,10 @@ function user_reference_invite_process_invitation_for_user($token, $account) {
  *
  * Clean up expired invitations.
  */
-function user_reference_invite_cron() {
-  $invite_manager = \Drupal::service('user_reference_invite.invite_manager');
-
-  // Process automatic reminders for pending invitations nearing expiry.
-  $reminders_sent = $invite_manager->processAutomaticReminders();
-  if ($reminders_sent > 0) {
-    \Drupal::logger('user_reference_invite')->info('Sent @count automatic invitation reminders', [
-      '@count' => $reminders_sent,
-    ]);
-  }
-
-  // Clean up expired invitations.
-  $cleaned = $invite_manager->cleanupExpiredInvitations();
-  if ($cleaned > 0) {
-    \Drupal::logger('user_reference_invite')->info('Cleaned up @count expired invitations', [
-      '@count' => $cleaned,
-    ]);
-  }
+#[LegacyHook]
+function user_reference_invite_cron()
+{
+    \Drupal::service(UserReferenceInviteHooks::class)->cron();
 }
 
 /**
@@ -673,65 +239,10 @@ function user_reference_invite_cron() {
  *
  * Handle invitation token in login form.
  */
-function user_reference_invite_form_user_login_form_alter(&$form, $form_state, $form_id) {
-  $request = \Drupal::request();
-  $token = $request->query->get('invite_token');
-
-  if (!$token) {
-    return;
-  }
-
-  \Drupal::logger('user_reference_invite')->info('Login form alter: Found token @token', ['@token' => $token]);
-
-  // Check if user is already logged in.
-  $current_user = \Drupal::currentUser();
-  if ($current_user->isAuthenticated()) {
-    // User is already logged in, process invitation immediately.
-    \Drupal::logger('user_reference_invite')->info('User @uid already logged in, processing invitation immediately', ['@uid' => $current_user->id()]);
-    $account = User::load($current_user->id());
-    user_reference_invite_process_invitation_for_user($token, $account);
-    return;
-  }
-
-  // Validate the invitation token.
-  $invite_manager = \Drupal::service('user_reference_invite.invite_manager');
-  $invite = $invite_manager->validateToken($token);
-
-  if (!$invite) {
-    \Drupal::messenger()->addError(t('The invitation link is invalid or has expired.'));
-    return;
-  }
-
-  // Store the token in tempstore for processing after login.
-  $tempstore = \Drupal::service('tempstore.private')->get('user_reference_invite');
-  $tempstore->set('pending_login_token', $token);
-  \Drupal::logger('user_reference_invite')->info('Stored token in tempstore for later processing');
-
-  // Load the entity to get its label.
-  $entity_label = t('the content');
-  try {
-    $entity = \Drupal::entityTypeManager()
-      ->getStorage($invite->getTargetEntityType())
-      ->load($invite->getTargetEntityId());
-    if ($entity) {
-      $entity_label = $entity->label();
-    }
-  }
-  catch (\Exception $e) {
-    \Drupal::logger('user_reference_invite')->error('Failed to load entity for invitation message: @message', [
-      '@message' => $e->getMessage(),
-    ]);
-  }
-
-  // Add a message to inform the user.
-  \Drupal::messenger()->addStatus(
-    t('Please log in to accept your invitation to @entity.', [
-      '@entity' => $entity_label,
-    ])
-  );
-
-  // Add a custom submit handler to process invitation after login.
-  array_unshift($form['#submit'], 'user_reference_invite_login_submit');
+#[LegacyHook]
+function user_reference_invite_form_user_login_form_alter(&$form, $form_state, $form_id)
+{
+    \Drupal::service(UserReferenceInviteHooks::class)->formUserLoginFormAlter($form, $form_state, $form_id);
 }
 
 /**
@@ -739,28 +250,10 @@ function user_reference_invite_form_user_login_form_alter(&$form, $form_state, $
  *
  * Process invitation when user logs in with invitation token.
  */
-function user_reference_invite_user_login($account) {
-  \Drupal::logger('user_reference_invite')->info('hook_user_login called for user @uid (@email)', [
-    '@uid' => $account->id(),
-    '@email' => $account->getEmail(),
-  ]);
-
-  // Check if there's a pending invitation token from login.
-  $tempstore = \Drupal::service('tempstore.private')->get('user_reference_invite');
-  $token = $tempstore->get('pending_login_token');
-
-  if (!$token) {
-    \Drupal::logger('user_reference_invite')->info('No pending login token found in tempstore');
-    return;
-  }
-
-  \Drupal::logger('user_reference_invite')->info('Found pending token in tempstore: @token', ['@token' => $token]);
-
-  // Clear the token from tempstore.
-  $tempstore->delete('pending_login_token');
-
-  // Process the invitation using the helper function.
-  user_reference_invite_process_invitation_for_user($token, $account);
+#[LegacyHook]
+function user_reference_invite_user_login($account)
+{
+    \Drupal::service(UserReferenceInviteHooks::class)->userLogin($account);
 }
 
 /**
@@ -808,47 +301,10 @@ function user_reference_invite_login_submit($form, &$form_state) {
  *
  * Allow registration when valid invitation token is present.
  */
-function user_reference_invite_form_user_register_form_alter(&$form, $form_state, $form_id) {
-  $request = \Drupal::request();
-  $token = $request->query->get('invite_token');
-
-  if (!$token) {
-    return;
-  }
-
-  // Validate the invitation token.
-  $invite_manager = \Drupal::service('user_reference_invite.invite_manager');
-  $invite = $invite_manager->validateToken($token);
-
-  if (!$invite) {
-    \Drupal::messenger()->addError(t('The invitation link is invalid or has expired.'));
-    return;
-  }
-
-  // Pre-fill email if available.
-  $email = $invite->getEmail();
-  if ($email && !empty($form['account']['mail'])) {
-    $form['account']['mail']['#default_value'] = $email;
-    $form['account']['mail']['#disabled'] = TRUE;
-    $form['account']['mail']['#description'] = t('This email address is from your invitation and cannot be changed.');
-  }
-
-  // Enable password fields so user can set password during registration.
-  // This prevents the "set password after first login" workflow.
-  if (isset($form['account']['pass'])) {
-    $form['account']['pass']['#access'] = TRUE;
-    $form['account']['pass']['#required'] = TRUE;
-  }
-
-  // Custom field pre-fill logic can be added here via hook implementation.
-  // Add a custom submit handler to process the invitation after registration.
-  $form['actions']['submit']['#submit'][] = 'user_reference_invite_register_submit';
-
-  // Add the token to form state so we can use it in the submit handler.
-  $form_state->set('invite_token', $token);
-
-  // Add a message to inform the user.
-  \Drupal::messenger()->addStatus(t('You are registering using an invitation. Please complete the form below.'));
+#[LegacyHook]
+function user_reference_invite_form_user_register_form_alter(&$form, $form_state, $form_id)
+{
+    \Drupal::service(UserReferenceInviteHooks::class)->formUserRegisterFormAlter($form, $form_state, $form_id);
 }
 
 /**
diff --git a/user_reference_invite.services.yml b/user_reference_invite.services.yml
index e7c0d1d..e88bc3d 100644
--- a/user_reference_invite.services.yml
+++ b/user_reference_invite.services.yml
@@ -65,3 +65,7 @@ services:
       - '@user_reference_invite.invite_manager'
     tags:
       - { name: access_check, applies_to: _user_reference_invite_register_access }
+
+  Drupal\user_reference_invite\Hook\UserReferenceInviteHooks:
+    class: Drupal\user_reference_invite\Hook\UserReferenceInviteHooks
+    autowire: true
