isRegistrationPage()) { $this->initRegistrationHooks(); } } /** * Check if we're on the registration page */ private function isRegistrationPage(): bool { return isset($_GET['action']) && $_GET['action'] === 'register'; } /** * 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); } /** * Combined login styles for both login and registration */ public function loginStyles(): void { do_action('jvbLoginStyles'); } /** * Login header - used for both login and registration */ public function loginHeader(): void { ?> 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 '

Join the Scene, '.$name.'

'.jvbCommaList($names).$message.'

'; } if (jvbSiteHasFavourites() && $this->fromFavourites()) { return '

'.JVB_LOGIN['login_from_favourite_header']??'Save your Favourites'.'

'; } return '

'.JVB_LOGIN['join_header'].'

'; } else { if (jvbSiteHasFavourites()) { $login = (!$this->fromFavourites()) ? '

'.JVB_LOGIN['login_header'].'

' : '

'.JVB_LOGIN['login_from_favourite_header'].'

'; } else { $login = '

'.JVB_LOGIN['login_header'].'

'; } return (empty($message)) ? $login : $login.$message; } } protected function fromFavourites():bool { return array_key_exists('type', $_GET) && $_GET['type'] === 'favourites'; } /** * 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 ); } /** * Handle successful login */ public function handleSuccessfulLogin(string $username, WP_User $user): void { if (isOurPeople() && !user_can($user, 'manage_options')) { wp_redirect(get_home_url(2, '/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; } ?> routes('invites')->verifyInvitation($token, $email); ?> document.querySelector('input#artist').checked = true; document.querySelector('#artist_first_name').value = 'name?>'; document.querySelector('#artist_email').value = ''; to_shop) { ?> document.querySelector('#artist_shop').value = 'shop?>'; let form = document.getElementById('registerform') let input = document.createElement('input'); let email = input.cloneNode(true); input.type = 'hidden'; input.name = 'invite_token'; input.value = ''; email.type = 'hidden'; email.name = 'invite_email'; email.value = ''; form.append(input); form.append(email); isRegistrationPage()) { return; } ?> '; ?>
'.$intro.'

'; } ?> fromFavourites()): ?>

1) { $this->renderUserTypeSelection(); } else { ?>

invitation_data) { ?> 'jvb_shop', 'hide_empty' => true )); $cities = get_terms(array( 'taxonomy' => 'jvb_city', 'hide_empty' => false, )); ?>
'; $descriptions = ''; foreach (JVB_USER as $role => $config) { if (jvbCheck('can_register', $config)) { $radio .= 'fromFavourites()) ? 'checked' : ''; $radio .= '>'; $descriptions .= '
'.is_array($config['join_description']) ? implode('', array_map(function ($item) { return '

'.$item.'

'; }, $config['join_description'])) : '

'.$config['join_description'].'

'.'
'; $i++; } } echo $radio; echo $descriptions; ?> fromFavourites()) ? 'checked' : '' ?>>

Save your favourites. Get notified.

Show off your work.

Support the community.

Welcome to the scene.

Sign up with your email to:

BONUS: Everything's free. And always will be. We work with partners chosen by and for the community to keep the lights on.

Welcome to the scene!

We'll start small, with the basics. Before your profile goes live, we need to verify:

Optional — 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.

Click to upload or drag and drop
JPG, PNG, GIF or PDF (max. 5MB)

Once you click register:

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 Instagram.

BONUS: Everything's free. And always will be. We work with partners chosen by and for the community to keep the lights on.

Howdy, partner!

We appreciate your interest!

edmonton.ink is a great place to showcase what you do, whether you:

We'll start with some basics, then we'll reach out to follow up (usually within 24-48 hours).

Note: — you must have good standing in the tattoo community to stay a partner of edmonton.ink.

If we receive multiple requests to terminate a partnership with you from member artists, we reserve the right to cancel your listings.

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; } /** * 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; } // 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']; switch ($user_type) { case 'artist': $user->set_role('jvb_artist'); $user->remove_role('subscriber'); $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); $link = $caps->addUserLink($user, 'artist'); $meta = new MetaManager($link, 'post'); $meta->setAll([ 'first_name' => $first, 'email' => $email ]); // 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); } 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'); } //Create approval request and notify verified users JVB()->routes('approvals')->createArtistApprovalRequest($user_id); //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'); 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; case 'partner': $user->set_role('jvb_partner'); $user->remove_role('subscriber'); $name = sanitize_text_field($_POST['partner_name']); $email = sanitize_email($_POST['partner_email']); $caps->setUserAs($user, 'partner'); $link = $caps->addUserLink($user, 'partner'); // 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'])); // 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; 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']); // Save enthusiast fields $temp = wp_update_user([ 'ID' => $user_id, 'first_name' => $name, 'user_email' => $email, ]); break; } // Handle file upload for artists if (isset($_POST['user_type']) && $_POST['user_type'] === 'artist' && !empty($_FILES['certification_file']['name'])) { $file = $_FILES['certification_file']; // Setup upload directory $upload_dir = wp_upload_dir(); $user_directory = 'artist-certifications/' . $user_id; $target_dir = $upload_dir['basedir'] . '/' . $user_directory; // Create directory if it doesn't exist wp_mkdir_p($target_dir); // 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 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 ($email) { JVB()->routes('favourites')->acceptListInvitation($token, $email, $user_id); } } } /** * 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!

" . "Password setup is in your inbox.
" . "Since you were invited by a shop, you can skip the verification wait and start building your profile right away! ♡"; 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!

Check your inbox - we've sent password setup details.
Get ready to build your dream artist collection! ♡"; break; case 'artist': $message = "HELL YEAH!

Password setup is in your inbox.
While we verify your info (24-48hrs), you can start building your profile.
Just remember - it stays underground until you're cleared. ♡"; break; case 'partner': $message = "ROCK ON!

Check your inbox - we've sent password setup details.
We'll check out your pitch in the next 24-48hrs.

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!

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 new LoginManager();