Jake Vanderwerf
2026-02-17 a24a06002081ad71a78ffeff9072725ba39cf121
inc/managers/LoginManager.php
@@ -1,1054 +1,1015 @@
<?php
namespace JVBase\managers;
use JVBase\meta\MetaManager;
use JVBase\forms\TaxonomySelector;
use JVBase\meta\Form;
use JVBase\utility\Features;
use WP_Error;
use WP_User;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
   exit;
}
class LoginManager
{
    private array|null $invitation_data = null;
    protected array $inviteData = [];
    private array $allowed_file_types = [
        'image/jpeg',
        'image/png',
        'image/gif',
        'application/pdf'
    ];
    private int $max_file_size = 5242880; // 5MB in bytes
   protected Features $siteFeatures;
   protected ?Form $form = null;
   protected Cache $cache;
    public function __construct()
    {
        // Common login page customization
        add_action('login_enqueue_scripts', array($this, 'loginStyles'));
        add_action('login_header', array($this, 'loginHeader'), 0);
        add_action('login_footer', array($this, 'loginFooter'));
        // Login page filters
        add_filter('login_headerurl', array($this, 'logoUrl'));
        add_filter('login_headertext', array($this, 'logoTitle'));
        add_filter('login_message', array($this, 'loginMessage'));
        add_filter('login_errors', array($this, 'loginErrors'));
   protected array $forms =[];
   protected array $labels = [];
   protected array $fields = [];
   protected ?string $action = null;
   protected string $title = '';
        // Login success handling
        add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
   // Token handlers registry
   protected array $messageHandlers = [];
        // Registration-specific hooks
        if ($this->isRegistrationPage()) {
            $this->initRegistrationHooks();
        }
    }
   private array $allowed_file_types = [
      'image/jpeg',
      'image/png',
      'image/gif',
      'application/pdf'
   ];
   private int $max_file_size = 5242880; // 5MB in bytes
    /**
     * Check if we're on the registration page
     */
    private function isRegistrationPage(): bool
    {
        return isset($_GET['action']) && $_GET['action'] === 'register';
    }
   public function __construct()
   {
      $this->siteFeatures = Features::forSite();
    /**
     * Initialize registration-specific hooks
     */
    private function initRegistrationHooks(): void
    {
        add_action('register_form', array($this, 'addRegistrationFields'));
        add_action('login_header', array($this, 'addRegistrationScript'));
        add_filter('registration_errors', array($this, 'registrationErrorsFilter'), 10, 3);
        add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2);
        add_action('login_head', array($this, 'modifyRegistrationForm'));
        add_action('register_form', array($this, 'addUploadSupport'));
        add_filter('pre_user_login', array($this, 'setUserLogin'), 1);
        add_filter('pre_user_email', array($this, 'setUserEmail'), 1);
        add_filter('register_message', array($this, 'customRegisterMessage'));
        add_filter('wp_login_errors', array($this, 'registrationSuccessMessage'), 10, 2);
        add_filter('login_form_top', array($this, 'loginFormTop'));
        add_filter('login_form_bottom', array($this, 'loginFormBottom'));
        add_filter('login_form_middle', array($this, 'loginFormMiddle'));
        // Remove default username requirement for registration
        remove_filter('registration_errors', 'registration_auth_pass_filter', 10);
    }
      $this->cache = Cache::for('login');
    /**
     * Combined login styles for both login and registration
     */
    public function loginStyles(): void
    {
        do_action('jvbLoginStyles');
    }
      // Initialize magic link support if enabled
      if ($this->siteFeatures->has('magicLink')) {
         $this->initMagicLinkSupport();
      }
    /**
     * Login header - used for both login and registration
     */
    public function loginHeader(): void
    {
        ?>
        <script type="text/javascript">
            document.addEventListener('DOMContentLoaded', function() {
                let loginLabel = document.querySelector('label[for="user_login"');
                loginLabel.innerHTML = '<?= jvbIcon('email', ['size' => 20]); ?> Your Email';
      // Create login page if it doesn't exist
      $this->ensureLoginPageExists();
                let passwordLabel = document.querySelector('label[for="user_pass"');
                passwordLabel.innerHTML = '<?= jvbIcon('password', ['size' => 20]); ?> Your Password';
                document.querySelector('form').classList.add('loaded');
            });
      // Redirect wp-login.php to custom page
      add_action('login_init', [$this, 'redirectToCustomLogin']);
      add_action('template_include', [$this, 'renderLoginPage']);
        </script>
        <?php
    }
        add_action('wp_enqueue_scripts', [$this, 'enqueueScripts'], 15);
    /**
     * Login footer with donate section
     */
    public function loginFooter(): void
    {
      do_action('jvbLoginFooter');
      // Login success handling
      add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
    }
      add_filter('lostpassword_url', [$this, 'resetPasswordUrl'], 10, 2);
      add_filter( 'login_url', [$this, 'loginUrl'], 10, 3 );
      add_filter( 'logout_url', [$this, 'logoutUrl'], 10, 2 );
      // Allow other features to register handlers
      do_action('jvbLoginManagerInit', $this);
      add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2);
      add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 10, 1);
   }
    /**
     * Logo URL
     */
    public function logoUrl(): string
    {
        return home_url();
    }
   public function excludeLoginSitemap(array $ids): array
   {
      $ids[] = $this->getLoginPage();
      return $ids;
   }
   /**************************************************************************
      * SETUP & CONFIGURATION
   **************************************************************************/
    /**
     * Logo title
     */
    public function logoTitle(): string
    {
        return get_bloginfo('name');
    }
   /**
    * Redirect wp-login.php to custom login page
    */
   public function redirectToCustomLogin(): void
   {
      // Handle interim login
      if (isset($_GET['interim-login'])) {
         // Don't redirect - let WP handle it
         return;
      }
      // Don't redirect if AJAX or REST
      if ((defined('DOING_AJAX') && DOING_AJAX) || (defined('REST_REQUEST') && REST_REQUEST)) {
         return;
      }
      // Build custom login URL with all query args
      $custom_login_page = home_url('/login/');
      $query_args = $_GET;
    /**
     * Login message - handles both login and registration
     */
    public function loginMessage(string $message): string
    {
        if ($this->isRegistrationPage()) {
            if (jvbSiteHasInvitations() && $this->fromInvite()) {
                $data = JVB()->routes('invites')->verifyInvitation(sanitize_text_field($_GET['invite']), sanitize_email($_GET['email']));
                $name = $data->name;
                $inviters = json_decode($data->inviters, true);
                $names = [];
                foreach ($inviters as $inviter) {
                    $artist = jvbContentFromUser((int)$inviter['user_id']);
                    $names[] = ($artist['name'] === '') ? $artist['display_name'] : $artist['name'];
                }
                $message = (count($names) > 1) ? 'are already here, and have invited you to join in!' : ' is already here, and invited you to join in!';
                return '<h2>Join the Scene, '.$name.'</h2>
                <p style="text-align:center;">'.jvbCommaList($names).$message.'</p>';
            }
            if (jvbSiteHasFavourites() && $this->fromFavourites()) {
                return '<h2>'.JVB_LOGIN['login_from_favourite_header']??'Save your Favourites'.'</h2>';
            }
            return '<h2>'.JVB_LOGIN['join_header'].'</h2>';
        } else {
         if (jvbSiteHasFavourites()) {
            $login = (!$this->fromFavourites()) ? '<h2>'.JVB_LOGIN['login_header'].'</h2>' : '<h2>'.JVB_LOGIN['login_from_favourite_header'].'</h2>';
         } else {
            $login = '<h2>'.JVB_LOGIN['login_header'].'</h2>';
      // Remove WordPress internal args
      unset($query_args['interim-login'], $query_args['wp-auth-check']);
      if (!empty($query_args)) {
         $custom_login_page = add_query_arg($query_args, $custom_login_page);
      }
      wp_safe_redirect($custom_login_page);
      exit;
   }
   protected function getRegistrationFormFields():array
   {
      $form = get_option(BASE.'registration_form_fields');
      if (!$form) {
         $form = [];
         $select = [];
         //Basic fields, for any
         $fields = [
            'user_name' => [
               'type'      => 'text',
               'required'  => true,
               'label'     => 'Your Name',
               'placeholder'=> 'Mister Meseeks'
            ],
            'user_email'   => [
               'type'      => 'email',
               'required'  => true,
               'label'     => 'Your Email',
               'placeholder'=> 'look@me.com'
            ]
         ];
         if (Features::forSite()->has('referrals')) {
            $fields['referral_code'] = [
               'type'   => 'text',
               'required'=> false,
               'label'  => 'Referral Code',
               'hint'   => 'Have a referral code? Paste it here!'
            ];
         }
         if (count(JVB_USER) > 1) {
            foreach (JVB_USER as $slug => $config) {
               if (!array_key_exists('can_register', $config) || !$config['can_register']) {
                  continue;
               }
               $icon = $config['icon'] ?? '';
               $icon = ($icon !== '') ? jvbIcon($icon) : '';
               $select[$slug] = '<span class="label">'.$icon.$config['label'].'</span><span class="text">'.$config['register']['text']??''.'</span>';
               if (!empty($config['register']['fields']??[])){
                  foreach ($config['register']['fields'] as $field) {
                     $field['condition'] = [
                        'field'  => 'user_select',
                        'value'  => $slug,
                        'operator'  => '=='
                     ];
                     $fields[] = $field;
                  }
               }
            }
            if (!empty($select)) {
               $select = array_merge(
                  [
                     'subscriber'   => 'Subscriber',
                  ],
                  $select
               );
               $form = array_merge(
                  [
                     'user_select' => [
                        'type'   => 'radio',
                        'label'  => 'Register as',
                        'options'   => $select,
                        'required'  => true,
                        'default'   => 'subscriber'
                     ]
                  ],
                  $fields
               );
            }
         }else {
            $form = $fields;
         }
         update_option(BASE.'registration_form_fields', $form);
      }
      return $form;
   }
   protected function setupFields():void
   {
      $fields = [];
      switch($this->action) {
         case 'register':
            $fields = $this->getRegistrationFormFields();
            break;
         case 'lostpassword':
         case 'magic':
            $fields = [
               'user_email' => [
                  'type' => 'email',
                  'label' => __('Email Address', 'jvb'),
                  'required' => true,
                  'placeholder' => 'look@me.com',
               ],
            ];
            break;
         case 'rp':
         case 'resetpass':
            $fields = [
               'pass1' => [
                  'type' => 'text',
                  'subtype' => 'password',
                  'label' => __('New Password', 'jvb'),
                  'required' => true,
               ],
               'pass2' => [
                  'type' => 'text',
                  'subtype' => 'password',
                  'label' => __('Confirm Password', 'jvb'),
                  'required' => true,
               ],
            ];
            break;
         case 'login':
            $fields = [
               'user_email' => [
                  'type' => 'email',
                  'label' => __('Email Address', 'jvb'),
                  'required' => true,
                  'autocomplete' => 'email',
                  'placeholder' => 'look@me.com',
               ],
               'user_password' => [
                  'type' => 'text',
                  'subtype'=> 'password',
                  'label' => __('Password', 'jvb'),
                  'autocomplete' => 'current-password',
                  'required' => true,
               ],
               'remember_me' => [
                  'type' => 'true_false',
                  'label' => __('Remember Me', 'jvb'),
                  'default' => true
               ]
            ];
            break;
         case 'postpass':
            $fields = [
                'post_password' => [
                    'type' => 'text',
                    'subtype' => 'password',
                    'label' => __('Password', 'jvb'),
                    'required' => true,
                    'hint' => 'This post is password protected. Please enter the password to view it.',
                ],
            ];
            break;
         case 'confirmaction':
            break;
      }
      $this->fields = $fields;
   }
   /**
    * Ensure login page exists
    */
   protected function ensureLoginPageExists(): void
   {
      $login_page = $this->getLoginPage();
      if (!$login_page || !is_int($login_page)) {
         $page_id = get_page_by_path('login');
         if (!$page_id) {
            $page_id = wp_insert_post([
               'post_title' => 'Login',
               'post_name' => 'login',
               'post_content' => '[jvb_login_form]',
               'post_status' => 'publish',
               'post_type' => 'page',
               'post_author' => 1
            ]);
         }
            return (empty($message)) ? $login : $login.$message;
        }
    }
   protected function fromFavourites():bool
         if ($page_id && !is_wp_error($page_id)) {
            if (is_object($page_id)) {
               $page_id = (int)$page_id->ID;
            }
            update_option(BASE.'login_page', $page_id);
            // Hide from menus/search
            update_post_meta($page_id, '_wp_page_template', 'default');
            update_post_meta($page_id, BASE . 'exclude_from_search', true);
         }
      }
   }
   public function loginUrl(string $login_url, string $redirect, bool $force_reauth):string
   {
      return array_key_exists('type', $_GET) && $_GET['type'] === 'favourites';
      // This will append /custom-login/ to you main site URL as configured in general settings (ie https://domain.com/custom-login/)
      $login_url = site_url( '/login/', 'login' );
      if ( ! empty( $redirect ) ) {
         $login_url = add_query_arg( 'redirect_to', urlencode( $redirect ), $login_url );
      }
      if ( $force_reauth ) {
         $login_url = add_query_arg( 'reauth', '1', $login_url );
      }
      return $login_url;
   }
    /**
     * Customize login error messages
     */
    public function loginErrors(string $error): string
    {
        return str_replace(
            array(
                'The password you entered for the username',
                'Invalid username',
                'Unknown username',
                'Unknown email address'
            ),
            array(
                'Wrong password',
                'We can\'t find that username',
                'We can\'t find that username',
                'We can\'t find that email'
            ),
            $error
        );
    }
   public function logoutUrl(string $logout_url, string $redirect): string
   {
      // Build custom logout URL
      $logout_url = site_url('/login/', 'login');
      $logout_url = add_query_arg('action', 'logout', $logout_url);
    /**
     * Handle successful login
     */
    public function handleSuccessfulLogin(string $username, WP_User $user): void
    {
        if (isOurPeople() && !user_can($user, 'manage_options')) {
            wp_redirect(get_home_url(null, '/dash'));
            exit;
        }
    }
    // ===== REGISTRATION-SPECIFIC METHODS =====
    /**
     * Set user login for registration
     */
    public function setUserLogin(string $login): string
    {
        $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
        if (!empty($user_type)) {
            $email_field = $user_type . '_email';
            if (isset($_POST[$email_field])) {
                $email = sanitize_email($_POST[$email_field]);
                if (is_email($email)) {
                    return $email;
                }
            }
        }
        return $login;
    }
    /**
     * Set user email for registration
     */
    public function setUserEmail(string $email): string
    {
        $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
        if (!empty($user_type)) {
            $email_field = $user_type . '_email';
            if (isset($_POST[$email_field])) {
                $email = sanitize_email($_POST[$email_field]);
                if (is_email($email)) {
                    return $email;
                }
            }
        }
        return $email;
    }
    /**
     * Modify registration form
     */
    public function modifyRegistrationForm(): void
    {
        if (!$this->isRegistrationPage()) {
            return;
        }
        ?>
        <script type="text/javascript">
            document.addEventListener('DOMContentLoaded', function() {
                // Hide default fields
                const defaultFields = document.getElementById('registerform').querySelectorAll('p');
                defaultFields.forEach(field => {
                    if (field.querySelector('label[for="user_login"]') ||
                        field.querySelector('label[for="user_email"]')) {
                        field.remove();
                    }
                });
                // Hide the default registration info text
                const regInfo = document.querySelector('.message.register');
                if (regInfo) {
                    regInfo.style.display = 'none';
                }
                <?php
                if ($this->fromInvite()) {
                    $this->handleArtistInvitation();
                }
                ?>
                // Move submit button to the end of the form
                const submitButton = document.getElementById('registerform').querySelector('.submit');
                if (submitButton) {
                    document.getElementById('registerform').appendChild(submitButton);
                }
            });
        </script>
        <?php
    }
    /**
     * Handle artist invitation pre-fill
     */
    protected function handleArtistInvitation(): void
    {
        $token = sanitize_text_field($_GET['invite']);
        $email = sanitize_email($_GET['email']);
        $data = JVB()->routes('invites')->verifyInvitation($token, $email);
        ?>
        document.querySelector('input#artist').checked = true;
        document.querySelector('#artist_first_name').value = '<?=$data->name?>';
        document.querySelector('#artist_email').value = '<?=$email?>';
        <?php
        if ($data->to_shop) {
            ?>
            document.querySelector('#artist_shop').value = '<?=$data->shop?>';
            <?php
        }
        ?>
        let form = document.getElementById('registerform')
        let input = document.createElement('input');
        let email = input.cloneNode(true);
        input.type = 'hidden';
        input.name = 'invite_token';
        input.value = '<?= $token ?>';
        email.type = 'hidden';
        email.name = 'invite_email';
        email.value = '<?= $email?>';
        form.append(input);
        form.append(email);
        <?php
    }
    /**
     * Add upload support for registration
     */
    public function addUploadSupport(): void
    {
        ?>
        <script>
            document.addEventListener('DOMContentLoaded', function() {
                const form = document.getElementById('registerform');
                if (form) {
                    form.enctype = 'multipart/form-data';
                }
            });
        </script>
        <?php
    }
    /**
     * Add registration script
     */
    public function addRegistrationScript(): void
    {
        if (!$this->isRegistrationPage()) {
            return;
        }
        ?>
        <script>
            document.addEventListener('DOMContentLoaded', function() {
                // Initialize user type selection
                function initUserTypeSelection() {
                    const userTypeRadios = document.querySelectorAll('input[name="user_type"]');
                    const fieldGroups = document.querySelectorAll('.field-group');
                    userTypeRadios.forEach(radio => {
                        radio.addEventListener('change', function() {
                            fieldGroups.forEach(group => group.classList.remove('active'));
                            const selectedType = this.value;
                            const targetGroup = document.querySelector(`.field-group[data-type="${selectedType}"]`);
                            if (targetGroup) {
                                targetGroup.classList.add('active');
                            }
                        });
                    });
                    const checkedRadio = document.querySelector('input[name="user_type"]:checked');
                    if (checkedRadio) {
                        const targetGroup = document.querySelector(`.field-group[data-type="${checkedRadio.value}"]`);
                        if (targetGroup) {
                            targetGroup.classList.add('active');
                        }
                    }
                }
                // Initialize shop selection
                function initShopSelection() {
                    let form = document.getElementById('registerform');
                    form.addEventListener('change', (e) => {
                        if(e.target.id === 'artist_shop' || e.target.id === 'artist_city'){
                            let next = e.target.parentNode.nextElementSibling;
                            let input = next.querySelector('input');
                            if(e.target.value === 'other'){
                                next.style.display = 'block';
                                next.style.animation = 'fadeIn 0.3s ease';
                                input.required = true;
                                input.focus();
                            }else{
                                input.required = false;
                                input.value = '';
                            }
                        }
                    });
                }
                // Initialize file upload handling
                function initFileUpload() {
                    const fileInput = document.getElementById('certification_file');
                    const filePreview = document.querySelector('.file-preview');
                    const filePreviewName = document.querySelector('.file-preview-name');
                    const fileError = document.querySelector('.file-error');
                    const removeButton = document.querySelector('.file-preview-remove');
                    if (!fileInput || !filePreview || !filePreviewName || !fileError || !removeButton) {
                        return;
                    }
                    const maxSize = parseInt(fileInput.dataset.maxSize || 5242880);
                    fileInput.addEventListener('change', function(e) {
                        const file = e.target.files[0];
                        fileError.classList.remove('active');
                        if (file) {
                            const validTypes = ['.jpg','.jpeg','.png','.gif','.pdf'];
                            const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
                            if (!validTypes.includes(fileExtension)) {
                                showError('Please upload a valid file type (JPG, PNG, GIF, or PDF)');
                                fileInput.value = '';
                                return;
                            }
                            if (file.size > maxSize) {
                                showError('File size must be less than 5MB');
                                fileInput.value = '';
                                return;
                            }
                            filePreviewName.textContent = file.name;
                            filePreview.classList.add('active');
                        } else {
                            filePreview.classList.remove('active');
                        }
                    });
                    removeButton.addEventListener('click', function() {
                        fileInput.value = '';
                        filePreview.classList.remove('active');
                        fileError.classList.remove('active');
                    });
                    function showError(message) {
                        fileError.textContent = message;
                        fileError.classList.add('active');
                        filePreview.classList.remove('active');
                    }
                }
                // Initialize all components
                initUserTypeSelection();
                initShopSelection();
                initFileUpload();
            });
        </script>
        <?php
    }
    /**
     * Add registration fields
     */
    public function addRegistrationFields(): void
    {
        echo '<input type="hidden" name="user_pass" value="' . wp_generate_password() . '">';
        ?>
        <div class="registration-intro">
            <?php
            foreach (JVB_LOGIN['join_intro']??[] as $intro) {
                echo '<p>'.$intro.'</p>';
            }
            ?>
            <?php if ($this->fromFavourites()): ?>
                <div class="favourites-login-message">
                    <ul class="benefits-list">
                        <?php
                        foreach (JVB_LOGIN['from_favourites_benefits']??[] as $benefit) {
                            echo '<li>'.$benefit.'</li>';
                        }
                        ?>
                    </ul>
                </div>
            <?php endif; ?>
        </div>
      <?php
      if (array_key_exists('choose', JVB_LOGIN)) {
         ?>
         <h3><?= JVB_LOGIN['choose']?></h3>
         <?php
      if (!empty($redirect)) {
         $logout_url = add_query_arg('redirect_to', urlencode($redirect), $logout_url);
      }
      ?>
      <?php
      if (count(JVB_USER) > 1) {
         $this->renderUserTypeSelection();
      // Add nonce for security
      return wp_nonce_url($logout_url, 'log-out');
   }
   public function resetPasswordUrl(string $url, string $redirect):string
   {
      error_log('reset Password Url:'.print_r($url, true));
      error_log('reset password redirect: '.print_r($redirect, true));
      return str_replace('wp_login.php', 'login/', $url);
   }
   public function getLoginPage():int|false
   {
      return (int)get_option(BASE.'login_page');
   }
   public function isLoginPage():bool
   {
      return is_page($this->getLoginPage());
   }
   public static function isLogin():bool
   {
      $self = new self;
      return $self->isLoginPage();
   }
   protected function initMagicLinkSupport(): void
   {
      if (!Features::forSite()->has('magicLink')) {
         return;
      }
   }
   /*********************************************************************
      RENDERING
   *********************************************************************/
   public function renderLoginPage(string $template):string
   {
      if (!$this->isLoginPage()) {
         return $template;
      }
      $this->setup();
      $page = $this->cache->remember(
            $this->getAction(),
            function() {
               return $this->renderPage();
            },
            5
         );
      echo $page;
      return '';
   }
   protected function renderPage() {
      ob_start();
      jvbInlineStyles('nav');
      jvbInlineStyles('dash');
      jvbInlineStyles('forms');
      $this->customStyles();
      $this->renderHeader();
      $this->renderForms();
      $this->renderFooter();
      return ob_get_clean();
   }
   protected function getAction():string
   {
      if (array_key_exists('action', $_GET)) {
         switch ($_GET['action']){
            case 'lostpassword':
            case 'retrievepassword': // Alias
               $action = 'lostpassword';
               break;
            case 'rp':
            case 'resetpass':
               $action = 'resetpass';
               break;
            default:
               $action = $_GET['action'];
         }
      } else {
         ?>
         <p>
            <label for="first_name" class="required-field">First Name</label>
            <input type="text" id="first_name" name="first_name" class="input">
         </p>
         <p>
            <label for="email" class="required-field">Email</label>
            <input type="email" id="email" name="email" class="input">
         </p>
         <?php
         $action = 'login';
      }
        if ($this->invitation_data) {
            ?>
            <script>
                document.addEventListener('DOMContentLoaded', function() {
                    const artistRadio = document.getElementById('artist');
                    if (artistRadio) {
                        artistRadio.checked = true;
                        artistRadio.dispatchEvent(new Event('change'));
                    }
      return $action;
   }
                    const emailField = document.getElementById('artist_email');
                    if (emailField) {
                        emailField.value = '<?= esc_js($this->invitation_data['email']); ?>';
                        emailField.readOnly = true;
                    }
   protected function setup():void
   {
      $this->action = $this->getAction();
      if ($this->action == 'logout' || array_key_exists('loggedout', $_GET)) {
         wp_logout();
         wp_redirect(esc_attr($_GET['redirect_to'] ?? get_home_url()));
         exit;
      }
      $this->setupLabels();
      $this->setupFields();
      $this->setupTitle();
   }
                    const shopSelect = document.getElementById('artist_shop');
                    if (shopSelect) {
                        shopSelect.value = '<?= esc_js($this->invitation_data['shop_id']); ?>';
                        shopSelect.readOnly = true;
                    }
                });
            </script>
            <input type="hidden" name="invitation_token" value="<?= sanitize_text_field($_GET['invite']) ?>">
            <input type="hidden" name="invitation_email" value="<?= sanitize_email($_GET['email']) ?>">
   protected function setupTitle():void
   {
      switch ($this->action) {
         case 'lostpassword':
            $title = 'Lost Your Password?';
            break;
         case 'resetpass':
            $title = 'Reset Your Password';
            break;
         case 'register':
            $title = 'Create Your Account';
            break;
         default:
            $title = 'Log In To Your Account';
      }
      $this->title = $title;
   }
   protected function customStyles():void
   {
      $logo = get_theme_mod('custom_logo');
      if ($logo) {
         $small = wp_get_attachment_image_src($logo, 'medium')[0]??'';
         $large = wp_get_attachment_image_src($logo, 'large')[0]??'';
      }
      echo '<style>
         .login header,
         .login footer {
            display: none;
         }
         .login main {
            display: flex;
            flex-direction: column;
            gap: 2rem;
            justify-content: center;
            position: relative;
         }
         .login .fstatus.fstatus {
            --wrap: nowrap;
            top:0;
            bottom:unset;
            right: 0;
         }
         .login main::before {
            background-size: 20vw;
            inset: 0;
            z-index: 0;
            content: "";
            background-image: url("'.$small.'");
            background-repeat: no-repeat;
            position: absolute;
            background-position: 40vw 1rem;
         }
         .login main .login-box {
            --gap: .75rem;
            padding: 1rem;
            border-radius: var(--outerRadius);
            background-color: var(--overlay-heavy);
            box-shadow: var(--shadow-right), var(--shadow-down);
            margin: 15vh auto 0!important;
         }
            .login main .login-box,
            .login main .navigation {
               z-index: 5;
               max-width: 90vw!important;
            }
            .login main .navigation {
               padding: 0 1rem;
               margin: 0 auto!important;
               font-size: var(--small);
            }
         .login-box .button {
            --height: 2.5rem;
            width: 100%;
         }
         .login-box .options {
            padding: 0 .5rem;
         }
         label[for="user_select-subscriber"] {
         position: absolute;
         left: var(--offScreen);
         }
         @media (min-width:768px) {
            .login main .navigation,
            .login main .login-box {
               max-width: 60vw!important;
               margin: 0 2rem 0 auto!important;
            }
            .login main .login-box {
               padding: 2rem;
               --gap: 2rem;
            }
            .login main .navigation {
               padding: 0 var(--offHeight);
            }
            .login-box .options {
               padding: 0 4rem;
            }
            .login main::before {
               background-size: 80vw;
               inset: -5vw;
               background-image: url("'.$large.'");
               opacity: .25;
               transform: rotate(-5deg);
               background-position: -10vw center;
            }
         }
         </style>';
   }
   protected function renderForms():void
   {
      $form = $this->action.'form';
      ?>
      <section class="login-box col btw">
         <h1><?=$this->labels['title']?></h1>
         <?= $this->labels['description'] ?>
         <form name="<?=$form?>" method="post" data-action="jvb_<?=$this->action?>">
            <?= jvbFormStatus() ?>
            <?php wp_nonce_field('jvb_'.$this->action, '_wpnonce'); ?>
            <input type="hidden" name="action" value="jvb_<?=$this->action?>">
            <input type="hidden" name="redirect_to" value="<?= esc_attr($_GET['redirect_to'] ?? '') ?>">
            <input type="hidden" name="request_id" value="<?= wp_generate_password(16, false) ?>">
            <?= ($this->action === 'magic') ? '<input type="hidden" name="type" value="login">' : '' ?>
            <?php
            do_action('jvb_add_token_inputs', $this->action);
            foreach ($this->fields as $name => $config) {
               echo Form::render($name, '', $config);
            }
            $this->maybeTurnstile();
             ?>
             <div class="row btw nowrap">
               <button type="submit" class="button button-primary button-large"><?=$this->labels['submit']?></button>
               <?php $this->maybeMagicLink(); ?>
            </div>
         </form>
         <?php
         if (is_array($this->labels['extra'])) {
            echo '<div class="extra">';
            foreach($this->labels['extra'] as $extra) {
               echo '<p>'.$extra.'</p>';
            }
            echo '</div>';
         } else if ($this->labels['extra']!=='') {
            echo '<div class="extra">'.$this->labels['extra'].'</div>';
         }
         ?>
         <div class="options row btw">
            <?php
            switch ($this->action) {
               case 'login': ?>
                  <a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a>
                  <a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a>
                  <?php
                  break;
               case 'register': ?>
                  <a href="<?= get_the_permalink() ?>">Or Login</a>
                  <a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a>
                  <?php
                  break;
               case 'lostpassword':
               case 'magic': ?>
                  <a href="<?= get_the_permalink() ?>">Login Instead</a>
                  <a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a>
                  <?php
                  break;
            }
             ?>
         </div>
      </section>
      <div class="navigation row btw">
         <a href="<?= get_home_url() ?>">Home</a>
         <?php
         $privacy = get_privacy_policy_url();
         if ($privacy !== '') { ?>
             <a href="<?= $privacy ?>">Our Privacy Policy</a>
         <?php } ?>
      </div>
      <?php
   }
   protected function renderHeader():void
    {
    ?>
        <!DOCTYPE html>
    <html <?php language_attributes(); ?>>
        <head>
            <title><?= $this->title ?> | <?= get_bloginfo('name') ?></title>
            <meta charset="<?php bloginfo('charset'); ?>">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <link rel="preconnect" href="<?= get_home_url()?>"/>
            <?php wp_head(); ?>
        </head>
    <body class="login">
        <?php jvbAccessibility();?>
        <header>
            <?php
        }
            $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
            $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
            echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
               <span class="screen-reader-text">Toggle dark mode</span>
                    <input class="theme-switch row" id="theme-switcher" name="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme name="dark-mode" aria-label="Toggle dark mode"><span class="slider">'.
               jvbIcon('sun-dim', ['title'=> 'Light Mode']).
               jvbIcon('moon', ['title'=>'Dark Mode']).
               '</span></label>';
            ?>
            <p class="title">
                <a href="<?= get_home_url(); ?>" rel="home" title="Back to Site">
                    <?php
                    $icon = (int) get_option( 'site_icon' );
               $out = '';
               if ($icon > 0) {
                  $url = wp_get_attachment_image_url( $icon);
                  if ($url) {
                     $out = '<img src="'.$url.'">';
                  }
               }
               if ($out == '') {
                  $out =jvbIcon('house');
               }
                    ?><?= $out ?>
                </a>
            </p>
        </header>
        <main>
    <?php
    }
   protected function renderUserTypeSelection():void
   protected function renderFooter():void
    {
        ?>
        <footer class="col">
         <?= $this->labels['footer'] ?>
         <?= jvbLoadingScreen() ?>
         <?= TaxonomySelector::outputSelectorModal() ?>
         <?php
         do_action('jvbLoginFooter');
         ?>
         <p>Made with ♡ by <a href="https://jakevan.ca/">JakeVan</a></p>
        </footer>
        <?php wp_footer(); ?>
        </body>
        </html>
        <?php
    }
   /**********************************************************************
      TOKEN PROCESSING
   **********************************************************************/
   protected function processTokenHandlers(int $user_id, string $email): void
   {
      foreach ($this->tokenHandlers as $priority => $handlers) {
         foreach ($handlers as $token_key => $handler) {
            if (isset($_POST[$token_key]) || isset($_GET[$token_key])) {
               $token_value = $_POST[$token_key] ?? $_GET[$token_key];
               call_user_func($handler, sanitize_text_field($token_value), $email, $user_id);
            }
         }
      }
   }
   /*************************************************************************
   *  SECURITY & VALIDATION
   *************************************************************************/
        // Get list of tattoo shops and cities
        $shops = get_terms(array(
            'taxonomy' => 'jvb_shop',
            'hide_empty' => true
        ));
   protected function checkRequestId(): bool
   {
      $request_id = $_POST['request_id'] ?? '';
      if (empty($request_id)) {
         return true; // No request_id provided, allow (for backward compat)
      }
        $cities = get_terms(array(
            'taxonomy' => 'jvb_city',
            'hide_empty' => false,
        ));
      $cache_key = 'request_' . $request_id;
      if (get_transient($cache_key)) {
         return false; // Duplicate request
      }
      // Store request ID for 1 minute to prevent duplicates
      set_transient($cache_key, true, 60);
      return true;
   }
   protected function maybeTurnstile(): void
   {
      if (!Features::hasIntegration('cloudflare')) {
         return;
      }
      JVB()->connect('cloudflare')->renderTurnstile();
   }
   protected function maybeTurnstileScripts(): void
   {
      if (!Features::hasIntegration('cloudflare')) {
         return;
      }
      JVB()->connect('cloudflare')->enqueueTurnstileScripts();
   }
   protected function verifyTurnstile(): bool
   {
      if (!Features::hasIntegration('cloudflare')) {
         return true; // Not enabled, pass verification
      }
      $token = $_POST['cf-turnstile-response'] ?? '';
      if (empty($token)) {
         return false;
      }
      return JVB()->connect('cloudflare')->verifyTurnstile($token);
   }
   /************************************************************************
      LABELS & UI
   ************************************************************************/
   protected function setupLabels(): void
   {
      $default = $this->getDefaultLabels();
      $default = apply_filters('jvbLoginLabels', $default, $_GET);
      if(array_key_exists('type', $_GET) && $_GET['type'] === 'favourites') {
         if (array_key_exists('favourites', JVB_LOGIN)) {
            foreach (JVB_LOGIN['favourites'] as $key => $value) {
               $default[$key] = $value;
            }
         }
      }
      foreach (['description', 'footer', 'extra'] as $location) {
         if ($default[$location] === '') {
            continue;
         }
         if (empty($default[$location])) {
            $default[$location] = '';
            continue;
         }
         $text = (!is_array($default[$location])) ? [$default[$location]] : $default[$location];
         if (!empty($text)) {
            $default[$location] = '<div class="'.$location.'">';
            foreach ($text as $d) {
               $default[$location] .= '<p>'.$d.'</p>';
            }
            $default[$location] .= '</div>';
         }
      }
      $this->labels = $default;
   }
   protected function getDefaultLabels(): array
   {
      switch ($this->action) {
         case 'register':
            return [
               'title' => JVB_LOGIN['register']['title'] ?? 'Create Your Account',
               'description' => JVB_LOGIN['register']['description'] ?? [],
               'extra' => JVB_LOGIN['register']['extra'] ?? [],
               'footer' => JVB_LOGIN['register']['footer'] ?? '',
               'email' => JVB_LOGIN['register']['email']['subject'] ?? '['.get_bloginfo('name').'] Finish Creating Your Account',
               'submit' => JVB_LOGIN['register']['submit'] ?? 'Create Account',
               'successTitle' => JVB_LOGIN['register']['success']['title'] ?? 'Success!',
               'successDescription' => JVB_LOGIN['register']['success']['description'] ?? ['See your email for next steps','(Check your spam folder if you cannot find it after a couple minutes.)'],
            ];
         case 'lostpassword':
            return [
               'title' => JVB_LOGIN['forgot_password']['title'] ?? 'Reset Password',
               'description' => JVB_LOGIN['forgot_password']['description'] ?? [],
               'extra' => JVB_LOGIN['forgot_password']['extra'] ?? [],
               'footer' => JVB_LOGIN['forgot_password']['footer'] ?? '',
               'submit' => JVB_LOGIN['forgot_password']['submit'] ?? 'Send Reset Link',
               'successTitle' => JVB_LOGIN['forgot_password']['success']['title'] ?? 'Success!',
               'successDescription' => JVB_LOGIN['forgot_password']['success']['description'] ?? ['Check your email for reset instructions'],
            ];
         case 'resetpass':
         case 'rp':
            return [
               'title' => JVB_LOGIN['reset_pass']['title'] ?? 'Reset Your Password',
               'description' => JVB_LOGIN['reset_pass']['description'] ?? [],
               'extra' => JVB_LOGIN['reset_pass']['extra'] ?? [],
               'footer' => JVB_LOGIN['reset_pass']['footer'] ?? '',
               'submit' => JVB_LOGIN['reset_pass']['submit'] ?? 'Reset Password',
            ];
         case 'logout':
            return [
               'title' => JVB_LOGIN['logout']['title'] ?? 'Logged Out!',
               'description' => JVB_LOGIN['logout']['description'] ?? [],
               'extra' => JVB_LOGIN['logout']['extra'] ?? [],
               'footer' => JVB_LOGIN['logout']['footer'] ?? '',
               'submit' => JVB_LOGIN['logout']['submit'] ?? '',
            ];
         case 'magic':
            return [
               'title' => JVB_LOGIN['magic']['title'] ?? 'Log in with Magic Link',
               'description' => JVB_LOGIN['magic']['description'] ?? ['Enter your email.','You\'ll get an email with a magic link.','Click it, and you\'re logged in!'],
               'extra' => JVB_LOGIN['magic']['extra'] ?? [],
               'footer' => JVB_LOGIN['magic']['footer'] ?? '',
               'submit' => JVB_LOGIN['magic']['submit'] ?? jvbIcon('magic-wand').'Send Magic Link',
            ];
         case 'login':
         default:
            return [
               'title' => JVB_LOGIN['login']['title'] ?? 'Sign in',
               'description' => JVB_LOGIN['login']['description'] ?? [],
               'extra' => JVB_LOGIN['login']['extra'] ?? [],
               'footer' => JVB_LOGIN['login']['footer'] ?? '',
               'submit' => JVB_LOGIN['login']['submit'] ?? 'Sign In',
            ];
      }
   }
   protected function maybeMagicLink(): void
   {
      if (!JVB()->magicLink() || !in_array($this->action, ['login', 'lostpassword'])) {
         return;
      }
      ?>
      <div class="user-type-section">
            <?php
            $i = 1;
            $radio = '<input type="radio" id="user0" name="user_type" value="subscriber" required checked>
            <label for="user0"></label>';
            $descriptions = '';
            foreach (JVB_USER as $role => $config) {
                if (jvbCheck('can_register', $config)) {
                    $radio .= '<input type="radio" id="user'.$i.'" name="user_type" value="'.$role.'" required';
                    $radio .= ($role === 'enthusiast' && $this->fromFavourites()) ? 'checked' : '';
                    $radio .= '><label for="user'.$i.'">'.jvbIcon($role, ['title' =>$config['label'], 'size'=>40]).'<h4>'.$config['label'].'</h4><p>';
                    $radio .=  $config['join_text']??'';
                    $radio .= '</p></label>';
                    $descriptions .= '<div class="user'.$i.'">'.is_array($config['join_description']) ? implode('', array_map(function ($item) { return '<p>'.$item.'</p>'; }, $config['join_description'])) : '<p>'.$config['join_description'].'</p>'.'</div>';
                    $i++;
                }
            }
            echo $radio;
            echo $descriptions;
            ?>
            <input type="radio" id="enthusiast" name="user_type" value="enthusiast" required <?= ($this->fromFavourites()) ? 'checked' : '' ?>>
            <label for="enthusiast"><?=jvbIcon('heart', ['title' =>'Enthusiast', 'size'=>40])?><h4>Enthusiast</h4><p>Start here.</p></label>
            <input type="radio" id="artist" name="user_type" value="artist" required>
            <label for="artist"><?=jvbIcon('tattoo', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label>
            <input type="radio" id="partner" name="user_type" value="partner" required>
            <label for="partner"><?=jvbIcon('partner', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label>
            <p class="enthusiast">Save your favourites. Get notified.</p>
            <p class="artist">Show off your work.</p>
            <p class="partner">Support the community.</p>
        </div>
        <!-- Enthusiast Fields -->
        <div class="field-group" data-type="enthusiast">
            <h4>Welcome to the scene.</h4>
            <p>Sign up with your email to:</p>
            <ul>
                <li>Save your favourites for easy access</li>
                <li>Get notified when your favourite artists add new content</li>
                <li>Stay in the loop with local flash days and events</li>
                <li>Discover styles and artists that match your vision</li>
            </ul>
            <p>
                <label for="enthusiast_first_name" class="required-field">First Name</label>
                <input type="text" id="enthusiast_first_name" name="enthusiast_first_name" class="input">
            </p>
            <p>
                <label for="enthusiast_email" class="required-field">Email</label>
                <input type="email" id="enthusiast_email" name="enthusiast_email" class="input">
            </p>
            <div><p><b>BONUS</b>: Everything's free. And always will be. We work with partners chosen by and for the community to keep the lights on.</p></div>
        </div>
        <!-- Artist Fields -->
        <div class="field-group" data-type="artist">
            <h4>Welcome to the scene!</h4>
            <p>We'll start small, with the basics. Before your profile goes live, we need to verify:</p>
            <ul>
                <li>you are who you say you are</li>
                <li>you work at the shop you listed</li>
                <li>your certification</li>
            </ul>
            <p>
                <label for="artist_first_name" class="required-field">First Name</label>
                <input type="text" id="artist_first_name" name="artist_first_name" class="input">
            </p>
            <p>
                <label for="artist_last_name" class="required-field">Last Name</label>
                <input type="text" id="artist_last_name" name="artist_last_name" class="input">
            </p>
            <p>
                <label for="artist_email" class="required-field">Email</label>
                <input type="email" id="artist_email" name="artist_email" class="input">
            </p>
            <p>
                <label for="artist_shop" class="required-field">Shop</label>
                <select id="artist_shop" name="artist_shop" class="input">
                    <option value="">Select a shop</option>
                    <option value="other">Add New Shop</option>
                    <?php foreach ($shops as $shop) : ?>
                        <option value="<?= esc_attr($shop->term_id); ?>"><?= esc_html($shop->name); ?></option>
                    <?php endforeach; ?>
                </select>
            </p>
            <p id="other_shop_field" style="display: none;">
                <label for="artist_shop_other" class="required-field">Shop Name</label>
                <input type="text" id="artist_shop_other" name="artist_shop_other" class="input" placeholder="Shop name">
            </p>
            <p>
                <label for="artist_type" class="required-field">Type</label>
                <input type="radio" id="type-tattoo-artist" name="artist_type" value="tattoo-artist">
                <label for="type-tattoo-artist">Tattoo Artist</label>
                <input type="radio" id="type-piercer" name="artist_type" value="piercer">
                <label for="type-piercer">Piercer</label>
                <input type="radio" id="type-other" name="artist_type" value="other">
                <label for="type-other">Other</label>
            </p>
            <p>
                <label for="artist_city" class="required-field">City</label>
                <select id="artist_city" name="artist_city" class="input">
                    <option value="">Select a city</option>
                    <option value="other">Add New City</option>
                    <?php foreach ($cities as $city) : ?>
                        <option value="<?= esc_attr($city->term_id); ?>"><?= esc_html($city->name); ?></option>
                    <?php endforeach; ?>
                </select>
            </p>
            <p id="other_city_field" style="display: none;">
                <label for="artist_city_other" class="required-field">City Name</label>
                <input type="text" id="artist_city_other" name="artist_city_other" class="input" placeholder="City">
            </p>
            <div class="file-upload-container">
                <label class="file-upload-label">Certification or Training Documents</label>
                <p><i>Optional</i> — If you've been certified in bloodborne pathogen safety, or any other tattoo safety course, pass along your certificate. This just eases the verification process.</p>
                <div class="file-upload-wrapper">
                    <input type="file" name="certification_file" id="certification_file" accept=".jpg,.jpeg,.png,.gif,.pdf" data-max-size="<?= $this->max_file_size; ?>">
                    <p class="file-upload-text">
                        <strong>Click to upload</strong> or drag and drop<br>
                        JPG, PNG, GIF or PDF (max. 5MB)
                    </p>
                </div>
                <div class="file-preview">
                    <div class="file-preview-content">
                        <span class="file-preview-name"></span>
                        <button type="button" class="file-preview-remove">Remove</button>
                    </div>
                </div>
                <div class="file-error"></div>
            </div>
            <p>Once you click register:</p>
            <ul>
                <li>We'll start looking into your information (usually within 24-48 hours)</li>
                <li>You'll get a password reset email</li>
                <li>Upon setting your password, you can start filling in your profile - but it won't go live until we've verified your information.</li>
            </ul>
            <p>If you have any questions or concerns - or anything you'd like to follow up on - email us at get@edmonton.ink or message us on <a target="_blank" href="https://www.instagram.com/edmonton.ink/" title="@edmonton.ink on Instagram">Instagram</a>.</p>
            <div><p><b>BONUS</b>: Everything's free. And always will be. We work with partners chosen by and for the community to keep the lights on.</p></div>
        </div>
        <!-- Partner Fields -->
        <div class="field-group" data-type="partner">
            <h4>Howdy, partner!</h4>
            <p>We appreciate your interest!</p>
            <p>edmonton.ink is a great place to showcase what you do, whether you:</p>
            <ul>
                <li>provide goods or services that tattoo artists could use</li>
                <li>provide goods or services that are tattoo adjacent (such as art, merch, etc)</li>
                <li>provide goods or services that folks who love tattoos could also love</li>
            </ul>
            <p>We'll start with some basics, then we'll reach out to follow up (usually within 24-48 hours).</p>
            <p>
                <label for="partner_name" class="required-field">Contact Name</label>
                <input type="text" id="partner_name" name="partner_name" class="input">
            </p>
            <p>
                <label for="partner_email" class="required-field">Email</label>
                <input type="email" id="partner_email" name="partner_email" class="input">
            </p>
            <p>
                <label for="partner_business" class="required-field">Business Name</label>
                <input type="text" id="partner_business" name="partner_business" class="input">
            </p>
            <p>
                <label for="partner_website">Business Website</label>
                <input type="url" id="partner_website" name="partner_website" class="input">
            </p>
            <p>
                <label for="partner_description">Why would you be a good fit?</label>
                <textarea id="partner_description" name="partner_description" rows="8"></textarea>
            </p>
            <p><i>Note:</i> — you must have good standing in the tattoo community to stay a partner of edmonton.ink.</p>
            <p>If we receive multiple requests to terminate a partnership with you from member artists, we reserve the right to cancel your listings.</p>
        </div>
      <a class="button" href="<?= add_query_arg('action', 'magic', wp_login_url()) ?>" title="Email yourself a link to log you in auto-magically!">
         <?= jvbIcon('magic-wand'); ?>
         Magic Link
      </a>
      <?php
   }
    /**
     * Registration errors filter
     */
    public function registrationErrorsFilter(WP_Error $errors, string $sanitized_user_login, string $user_email): WP_Error
    {
        error_log('Registration Data: '.print_r($_POST, true));
        $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
        if (empty($user_type)) {
            $errors->add('user_type_error', 'Please select your user type.');
            return $errors;
        }
        // Get email based on user type
        $email_field = $user_type . '_email';
        $email = isset($_POST[$email_field]) ? sanitize_email($_POST[$email_field]) : '';
        // Remove WordPress's default username error
        $errors = new WP_Error();
        // If this is an invited artist, validate the invitation
        $invite = (array_key_exists('invite_token', $_POST)) ? sanitize_text_field($_POST['invite_token']) : false;
        if ($invite && array_key_exists('role', $_POST)) {
            $handler = JVB()->routes('invites');
            $invitation = $handler->verifyInvitation($invite, sanitize_email($_POST['invite_email']), sanitize_text_field($_POST['role']));
            if (!$invitation) {
                $errors->add('invalid_invitation', 'Invalid invitation token.');
            } elseif (strtotime($invitation->expires_at) < current_time('timestamp')) {
                $errors->add('expired_invitation', 'This invitation has expired.');
            }
        }
        // Validate email first
        if (empty($email)) {
            $errors->add('email_error', 'Email is required.');
        } elseif (!is_email($email)) {
            $errors->add('email_error', 'Please enter a valid email address.');
        } elseif (email_exists($email)) {
            $errors->add('email_error', 'This email is already registered.');
        }
        switch ($user_type) {
            case 'enthusiast':
                if (empty($_POST['enthusiast_first_name'])) {
                    $errors->add('first_name_error', 'First name is required.');
                }
                break;
            case 'artist':
                $required_fields = array(
                    'artist_first_name' => 'First name',
                    'artist_last_name' => 'Last name',
                    'artist_shop' => 'Shop',
                    'artist_city' => 'City',
                    'artist_type' => 'Type',
                );
                foreach ($required_fields as $field => $label) {
                    if (empty($_POST[$field])) {
                        $errors->add($field . '_error', $label . ' is required.');
                    }
                }
                break;
            case 'partner':
                $required_fields = array(
                    'partner_name' => 'Contact name',
                    'partner_business' => 'Business name'
                );
                foreach ($required_fields as $field => $label) {
                    if (empty($_POST[$field])) {
                        $errors->add($field . '_error', $label . ' is required.');
                    }
                }
                break;
        }
        if (isset($_POST['user_type']) && $_POST['user_type'] === 'artist' && !empty($_FILES['certification_file']['name'])) {
            $file = $_FILES['certification_file'];
            // Validate file type
            if (!in_array($file['type'], $this->allowed_file_types)) {
                $errors->add('file_type_error', 'Please upload a valid file type (JPG, PNG, GIF, or PDF)');
            }
            // Validate file size
            if ($file['size'] > $this->max_file_size) {
                $errors->add('file_size_error', 'File size must be less than 5MB');
            }
        }
        return $errors;
   /************************************************************************
      SCRIPTS
   ************************************************************************/
   public function enqueueScripts(): void
{
    if (!$this->isLoginPage()) {
        return;
    }
    /**
     * Save registration fields
     */
    public function saveRegistrationFields(int $user_id, array $userdata): void
    {
        $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : false;
        if (!$user_type) {
            return;
        }
    $this->maybeTurnstileScripts();
    wp_enqueue_script('jvb-form');
    $action = $this->getAction();
        // Set user role based on type
        $user = new WP_User($user_id);
        $caps = JVB()->roles();
        $email = false;
        $upload_dir = wp_upload_dir();
        $base_dir = $upload_dir['basedir'];
    $redirect_to = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : '';
    $has_turnstile = Features::hasIntegration('cloudflare');
        switch ($user_type) {
            case 'artist':
                $user->set_role('jvb_artist');
                $user->remove_role('subscriber');
    ob_start();
                $email = sanitize_email($_POST['artist_email']);
                $first = sanitize_text_field($_POST['artist_first_name']);
                $last = sanitize_text_field($_POST['artist_last_name']);
                $display_name = $first . ' ' . $last;
    ?>
                // Save artist fields
                $temp = wp_update_user([
                    'ID' => $user_id,
                    'first_name' => $first,
                    'last_name' => $last,
                    'display_name' => $display_name
                ]);
                $user = get_userdata($temp);
   document.addEventListener('DOMContentLoaded', async function () {
      const hasTurnstile = <?= json_encode($has_turnstile) ?>;
      const redirectTo = <?= json_encode($redirect_to) ?>;
                $link = $caps->addUserLink($user, 'artist');
                $meta = new MetaManager($link, 'post');
            $meta->setAll([
               'first_name'   => $first,
               'email'        => $email
            ]);
      window.auth.subscribe(event => {
         if (event === 'auth-loaded') {
            const form = document.querySelector('.login form');
            if (!form || !window.jvbForm) return;
                // If this was an invited artist, handle the invitation
                if (array_key_exists('invite_token', $_POST)) {
                    $handler = JVB()->routes('invites');
                    $handler->acceptInvitation(sanitize_text_field($_POST['invite_token']), sanitize_email($_POST['invite_email']), $user->ID);
                }
            window.jvbForm.registerForm(form, {
               autosave: false,
               endpoint: '<?= $action ?>',
               formStatus: false,
               cache: false,
            });
                if (absint($_POST['artist_shop']) > 0) {
                    JVB()->routes('shop')->requestShopAdmission($user_id, absint($_POST['artist_shop']));
                }
                if (absint($_POST['artist_city']) > 0) {
                    wp_set_post_terms($link, (int)absint($_POST['artist_city']), BASE.'city');
                }
            window.jvbForm.subscribe((event, data) => {
               if (event === 'form-submit') {
                  const { config } = data;
                  const formElement = config.element;
                //Create approval request and notify verified users
                JVB()->routes('approvals')->createArtistApprovalRequest($user_id);
                  // Collect current form data
                  const formData = new FormData(formElement);
                  const formObject = Object.fromEntries(formData.entries());
                //Make base directories
                $artist_dir = $base_dir . '/artists/' . $user_id;
                wp_mkdir_p($artist_dir);
                wp_mkdir_p($artist_dir . '/artwork');
                wp_mkdir_p($artist_dir . '/events');
                wp_mkdir_p($artist_dir . '/profile');
                wp_mkdir_p($artist_dir . '/temp');
                  let params = new URLSearchParams(window.location.search);
                  if (params.has('key')) {
                     formObject['key'] = params.get('key');
                  }
                  if (params.has('login')) {
                     formObject['login'] = params.get('login');
                  }
                switch ($_POST['artist_type']) {
                    case 'tattoo-artist':
                        $caps->setUserAs($user, 'tattoo-artist');
                        $term = get_term_by('name', 'Tattoo Artists', BASE.'type');
                        if ($term && !is_wp_error($term)) {
                            wp_set_post_terms($link, $term->term_id, BASE.'type');
                        }
                        wp_mkdir_p($artist_dir . '/tattoos');
                        break;
                    case 'piercer':
                        $caps->setUserAs($user, 'piercer');
                        $term = get_term_by('name', 'Piercers', BASE.'type');
                        if ($term && !is_wp_error($term)) {
                            wp_set_post_terms($link, $term->term_id, BASE.'type');
                        }
                        wp_mkdir_p($artist_dir . '/piercings');
                        break;
                }
                break;
                  // Add redirect_to from URL
                  if (redirectTo) {
                     formObject.redirect_to = redirectTo;
                  }
            case 'partner':
                $user->set_role('jvb_partner');
                $user->remove_role('subscriber');
                $name = sanitize_text_field($_POST['partner_name']);
                $email = sanitize_email($_POST['partner_email']);
                  const submit = formElement.querySelector('[type=submit]');
                  const oldText = submit.textContent;
                $caps->setUserAs($user, 'partner');
                $link = $caps->addUserLink($user, 'partner');
                  window.jvbForm.showFormStatus(config.id, 'uploading');
                // Save partner fields
                update_user_meta($user_id, 'contact_name', sanitize_text_field($_POST['partner_name']));
                update_user_meta($user_id, 'business_name', sanitize_text_field($_POST['partner_business']));
                update_user_meta($user_id, 'business_website', esc_url_raw($_POST['partner_website']));
                  submit.disabled = true;
                  submit.textContent = 'Loading...';
                // Create partner base directory
                $partner_dir = $base_dir . '/partners/' . $user_id;
                wp_mkdir_p($partner_dir);
                wp_mkdir_p($partner_dir . '/offers');
                wp_mkdir_p($partner_dir . '/events');
                wp_mkdir_p($partner_dir . '/profile');
                wp_mkdir_p($partner_dir . '/temp');
                break;
                  window.auth.fetch(`${jvbSettings.api}auth/<?= $action ?>`, {
                     method: 'POST',
                     body: JSON.stringify(formObject)
                  })
                  .then(response => response.json().then(result => ({ response, result })))
                  .then(({ response, result }) => {
                     if (!response.ok) {
                        window.jvbForm.showFormStatus(config.id, 'error');
                        window.jvbForm.handleFormError(formElement, result);
                        return;
                     }
            case 'enthusiast':
                $user->set_role('jvb_enthusiast');
                $user->remove_role('subscriber');
                $caps->setUserAs($user, 'enthusiast');
                $name = sanitize_text_field($_POST['enthusiast_first_name']);
                $email = sanitize_email($_POST['enthusiast_email']);
                     window.jvbForm.showFormStatus(config.id, 'submitted');
                // Save enthusiast fields
                $temp = wp_update_user([
                    'ID' => $user_id,
                    'first_name' => $name,
                    'user_email' => $email,
                ]);
                break;
        }
                     if (result.message) {
                        window.jvbForm.handleFormSuccess(formElement, result);
                     }
        // Handle file upload for artists
        if (isset($_POST['user_type']) && $_POST['user_type'] === 'artist' && !empty($_FILES['certification_file']['name'])) {
            $file = $_FILES['certification_file'];
                     if (window.auth?.handleLogin && result.auth) {
                        return window.auth.handleLogin(result.auth).then(() => {
                           if (result.redirect) {
                              setTimeout(() => {
                                 window.location.href = result.redirect;
                              }, 100);
                           }
                        });
                     } else if (result.redirect) {
                        setTimeout(() => {
                           window.location.href = result.redirect;
                        }, 100);
                     }
                  })
                  .catch(error => {
                     console.error('Form submission error:', error);
                     window.jvbForm.showFormStatus(config.id, 'error');
                     window.jvbForm.handleFormError(formElement, {
                        message: 'Network error. Please check your connection and try again.',
                        code: 'network_error'
                     });
                  })
                  .finally(() => {
                     submit.textContent = oldText;
                     submit.disabled = false;
                  });
               }
            });
         }
      });
   });
            // Setup upload directory
            $upload_dir = wp_upload_dir();
            $user_directory = 'artist-certifications/' . $user_id;
            $target_dir = $upload_dir['basedir'] . '/' . $user_directory;
   <?php
      $script = ob_get_clean();
      wp_add_inline_script('jvb-form', $script);
   }
            // Create directory if it doesn't exist
            wp_mkdir_p($target_dir);
   /*************************************************************************
      SUCCESS HANDLING
   *************************************************************************/
   public function handleSuccessfulLogin(string $username, WP_User $user): void
   {
      if (isOurPeople() && !user_can($user, 'manage_options')) {
         wp_redirect(get_home_url(null, '/dash'));
         exit;
      }
   }
            // Generate unique filename
            $file_extension = pathinfo($file['name'], PATHINFO_EXTENSION);
            $filename = 'certification-' . time() . '.' . $file_extension;
            $target_file = $target_dir . '/' . $filename;
            // Move uploaded file
            if (move_uploaded_file($file['tmp_name'], $target_file)) {
                // Save file information in user meta
                update_user_meta($user_id, 'certification_file', array(
                    'url' => $upload_dir['baseurl'] . '/' . $user_directory . '/' . $filename,
                    'file' => $target_file,
                    'type' => $file['type'],
                    'original_name' => $file['name']
                ));
            }
        }
   /**
    * Handle login errors
    */
   protected function handleLoginError(WP_Error $error): void
   {
      $login_url = wp_login_url();
      $login_url = add_query_arg('login_error', urlencode($error->get_error_code()), $login_url);
        // Handle list invitation acceptance
        if (isset($_GET['list_token']) && !empty($_GET['list_token']) && isset($_GET['email'])) {
            $token = sanitize_text_field($_GET['list_token']);
            $email = sanitize_email($_GET['email']);
      if (isset($_REQUEST['redirect_to'])) {
         $login_url = add_query_arg('redirect_to', urlencode($_REQUEST['redirect_to']), $login_url);
      }
            if ($email) {
                JVB()->routes('favourites')->acceptListInvitation($token, $email, $user_id);
            }
        }
    }
      wp_safe_redirect($login_url);
      exit;
   }
    /**
     * Registration success message
     */
    public function registrationSuccessMessage(WP_Error $errors, string $redirect_to): WP_Error
    {
        if (isset($errors->errors['registered']) && isset($_POST['invitation_token'])) {
            // Custom message for invited artists
            $message = "WELCOME ABOARD!<br><br>" .
                "Password setup is in your inbox. <br>" .
                "Since you were invited by a shop, you can skip the verification wait and start building your profile right away! ♡";
   public function saveRegistrationFields(int $user_id, array $userdata):void
   {
            unset($errors->errors['registered']);
            $errors->add('registered', $message, 'message');
        }
        if (isset($errors->errors['registered'])) {
            $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : 'user';
            switch ($user_type) {
                case 'enthusiast':
                    $message = "YOU'RE IN!<br><br>Check your inbox - we've sent password setup details.<br>Get ready to build your dream artist collection! ♡";
                    break;
                case 'artist':
                    $message = "HELL YEAH!<br><br>Password setup is in your inbox. <br>While we verify your info (24-48hrs), you can start building your profile. <br>Just remember - it stays underground until you're cleared. ♡";
                    break;
                case 'partner':
                    $message = "ROCK ON!<br><br>Check your inbox - we've sent password setup details.<br>We'll check out your pitch in the next 24-48hrs. <br><br>Meanwhile, you can start prepping your presence - but you won't hit the streets until we give the nod. ♡";
                    break;
                default:
                    $message = "YOU'RE ON THE LIST!<br><br>Check your inbox for the next steps. ♡";
            }
            // Replace the default message
            unset($errors->errors['registered']);
            $errors->add('registered', $message, 'message');
        }
        return $errors;
    }
    /**
     * Check if registration is from invite
     */
    protected function fromInvite(): bool
    {
        return isset($_GET['invite']) && isset($_GET['email']);
    }
    /**
     * Custom register message
     */
    public function customRegisterMessage(string $message): string
    {
        return "Join Edmonton's tattoo community";
    }
   }
}
// Initialize the consolidated auth manager
// Initialize the login manager
new LoginManager();