Get notified when your favourite artists add new content
Stay in the loop with local flash days and events
Discover styles and artists that match your vision
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:
you are who you say you are
you work at the shop you listed
your certification
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:
We'll start looking into your information (usually within 24-48 hours)
You'll get a password reset email
Upon setting your password, you can start filling in your profile - but it won't go live until we've verified your information.
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:
provide goods or services that tattoo artists could use
provide goods or services that are tattoo adjacent (such as art, merch, etc)
provide goods or services that folks who love tattoos could also love
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();