diff --git a/access_code.info.yml b/access_code.info.yml
index 043a0d3..1da987d 100644
--- a/access_code.info.yml
+++ b/access_code.info.yml
@@ -1,7 +1,7 @@
 name: Access code
 description: Allows users to log in with an access code.
 type: module
-core_version_requirement: ^9 || ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 package: Security
 dependencies:
   - drupal:user
diff --git a/access_code.install b/access_code.install
index 37cf927..131cd2f 100644
--- a/access_code.install
+++ b/access_code.install
@@ -1,5 +1,7 @@
 <?php
 
+use Drupal\Component\Utility\DeprecationHelper;
+
 /**
  * @file
  * Install, update and uninstall functions for the access_code module.
@@ -48,5 +50,5 @@ function access_code_uninstall() {
   $config->clear('auto_code_format');
   $config->clear('expiration_default');
   $config->clear('display_input');
-  $config->save(TRUE);
+  DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $config->save(), fn() => $config->save(TRUE));
 }
diff --git a/access_code.module b/access_code.module
index 4eff5ae..9ba0ca6 100644
--- a/access_code.module
+++ b/access_code.module
@@ -3,33 +3,33 @@
  * @file
  * Allows users to log in with an access code.
  */
-
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\access_code\Hook\AccessCodeHooks;
 use \Drupal\Core\Form\FormStateInterface;
 use \Drupal\user\Entity\User;
 
 /**
  * Implements hook_form_FORM_ID_alter().
  */
-function access_code_form_user_register_form_alter(&$form, FormStateInterface $form_state) {
-  /* @var \Drupal\access_code\Service\AccessCodeManager $manager */
-  $manager = \Drupal::service('access_code.manager');
-  $manager->addFormFields($form, $form_state);
+#[LegacyHook]
+function access_code_form_user_register_form_alter(&$form, FormStateInterface $form_state)
+{
+    \Drupal::service(AccessCodeHooks::class)->formUserRegisterFormAlter($form, $form_state);
 }
 
 /**
  * Implements hook_form_FORM_ID_alter().
  */
-function access_code_form_user_form_alter(&$form, FormStateInterface $form_state) {
-  /* @var \Drupal\access_code\Service\AccessCodeManager $manager */
-  $manager = \Drupal::service('access_code.manager');
-  $manager->addFormFields($form, $form_state);
+#[LegacyHook]
+function access_code_form_user_form_alter(&$form, FormStateInterface $form_state)
+{
+    \Drupal::service(AccessCodeHooks::class)->formUserFormAlter($form, $form_state);
 }
 
 /**
  * Implements hook_user_delete().
  */
+#[LegacyHook]
 function access_code_user_delete(User $account) {
-  \Drupal::database()->delete('access_code')
-    ->condition('uid', $account->id())
-    ->execute();
+  \Drupal::service(AccessCodeHooks::class)->userDelete($account);
 }
diff --git a/access_code.services.yml b/access_code.services.yml
index 39c403d..9cc4983 100644
--- a/access_code.services.yml
+++ b/access_code.services.yml
@@ -2,3 +2,11 @@ services:
   access_code.manager:
     class: Drupal\access_code\Service\AccessCodeManager
     arguments: ['@config.factory', '@logger.factory', '@database', '@current_user', '@user.data', '@datetime.time', '@module_handler', '@request_stack', '@redirect.destination', '@messenger']
+
+  Drupal\access_code\Hook\AccessCodeHooks:
+    class: Drupal\access_code\Hook\AccessCodeHooks
+    autowire: true
+
+  Drupal\access_code\Hook\AccessCodeTokensHooks:
+    class: Drupal\access_code\Hook\AccessCodeTokensHooks
+    autowire: true
diff --git a/access_code.tokens.inc b/access_code.tokens.inc
index a9c4011..9a466b9 100644
--- a/access_code.tokens.inc
+++ b/access_code.tokens.inc
@@ -4,54 +4,24 @@
  * @file
  * Token integration for Access Code module.
  */
-
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\access_code\Hook\AccessCodeTokensHooks;
 use Drupal\Core\Render\BubbleableMetadata;
 
 /**
  * Implements hook_token_info().
  */
-function access_code_token_info() {
-  $info = [];
-
-  $info['tokens']['user']['access-code'] = [
-    'name' => t('Access code'),
-    'description' => t('The access code assigned to the user.'),
-  ];
-
-  $info['tokens']['user']['access-code-expiration'] = [
-    'name' => t('Access code expiration'),
-    'description' => t('The expiration date of the user\'s access code.'),
-  ];
-
-  return $info;
+#[LegacyHook]
+function access_code_token_info()
+{
+    return \Drupal::service(AccessCodeTokensHooks::class)->tokenInfo();
 }
 
 /**
  * Implements hook_tokens().
  */
-function access_code_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-  $replacements = [];
-
-  /* @var \Drupal\access_code\Service\AccessCodeManager $manager */
-  $manager = \Drupal::service('access_code.manager');
-
-  if ($type == 'user' && !empty($data['user'])) {
-    /** @var \Drupal\user\UserInterface $account */
-    $account = $data['user'];
-    $access_code = $manager->getAccessCode($account->id());
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'access-code':
-          $replacements[$original] = $access_code['code'] ?? NULL;
-          break;
-
-        case 'access-code-expiration':
-          $replacements[$original] = $access_code['expiration'] ?? NULL;
-          break;
-      }
-    }
-  }
-
-  return $replacements;
+#[LegacyHook]
+function access_code_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata)
+{
+    return \Drupal::service(AccessCodeTokensHooks::class)->tokens($type, $tokens, $data, $options, $bubbleable_metadata);
 }
diff --git a/src/Hook/AccessCodeHooks.php b/src/Hook/AccessCodeHooks.php
new file mode 100644
index 0000000..efca326
--- /dev/null
+++ b/src/Hook/AccessCodeHooks.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\access_code\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\user\Entity\User;
+use Drupal\Core\Hook\Attribute\Hook;
+/**
+ * Hook implementations for access_code.
+ */
+class AccessCodeHooks
+{
+    /**
+     * Implements hook_form_FORM_ID_alter().
+     */
+    #[Hook('form_user_register_form_alter')]
+    public static function formUserRegisterFormAlter(&$form, \Drupal\Core\Form\FormStateInterface $form_state)
+    {
+        /* @var \Drupal\access_code\Service\AccessCodeManager $manager */
+        $manager = \Drupal::service('access_code.manager');
+        $manager->addFormFields($form, $form_state);
+    }
+    /**
+     * Implements hook_form_FORM_ID_alter().
+     */
+    #[Hook('form_user_form_alter')]
+    public static function formUserFormAlter(&$form, \Drupal\Core\Form\FormStateInterface $form_state)
+    {
+        /* @var \Drupal\access_code\Service\AccessCodeManager $manager */
+        $manager = \Drupal::service('access_code.manager');
+        $manager->addFormFields($form, $form_state);
+    }
+    /**
+     * Implements hook_user_delete().
+     */
+    #[Hook('user_delete')]
+    public static function userDelete(\Drupal\user\Entity\User $account)
+    {
+        \Drupal::database()->delete('access_code')->condition('uid', $account->id())->execute();
+    }
+}
diff --git a/src/Hook/AccessCodeTokensHooks.php b/src/Hook/AccessCodeTokensHooks.php
new file mode 100644
index 0000000..9ada97e
--- /dev/null
+++ b/src/Hook/AccessCodeTokensHooks.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\access_code\Hook;
+
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for access_code.
+ */
+class AccessCodeTokensHooks
+{
+    use StringTranslationTrait;
+    /**
+     * Implements hook_token_info().
+     */
+    #[Hook('token_info')]
+    public function tokenInfo()
+    {
+        $info = [
+        ];
+        $info['tokens']['user']['access-code'] = [
+            'name' => $this->t('Access code'),
+            'description' => $this->t('The access code assigned to the user.'),
+        ];
+        $info['tokens']['user']['access-code-expiration'] = [
+            'name' => $this->t('Access code expiration'),
+            'description' => $this->t('The expiration date of the user\'s access code.'),
+        ];
+        return $info;
+    }
+    /**
+     * Implements hook_tokens().
+     */
+    #[Hook('tokens')]
+    public static function tokens($type, $tokens, array $data, array $options, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata)
+    {
+        $replacements = [
+        ];
+        /* @var \Drupal\access_code\Service\AccessCodeManager $manager */
+        $manager = \Drupal::service('access_code.manager');
+        if ($type == 'user' && !empty($data['user'])) {
+            /** @var \Drupal\user\UserInterface $account */
+            $account = $data['user'];
+            $access_code = $manager->getAccessCode($account->id());
+            foreach ($tokens as $name => $original) {
+                switch ($name) {
+                    case 'access-code':
+                        $replacements[$original] = $access_code['code'] ?? NULL;
+                        break;
+                    case 'access-code-expiration':
+                        $replacements[$original] = $access_code['expiration'] ?? NULL;
+                        break;
+                }
+            }
+        }
+        return $replacements;
+    }
+}
