'per_user', // 'per_user' or 'flat_total' 'referrer_reward_amount' => 25.00, 'referrer_reward_type' => 'fixed', 'referee_reward_type' => 'percentage', // 'percentage' or 'fixed' 'referee_reward_amount' => 20, // 20% or $20 'referee_reward_applies_to' => 'first_order', // 'first_order' or 'all_orders' 'referral_role' => BASE.'client' ]; protected string $role = BASE.'client'; protected array $settings; public function __construct() { $this->defineTables(); global $wpdb; $this->wpdb = $wpdb; $this->cache = Cache::for('referrals', WEEK_IN_SECONDS); $this->requestCache = Cache::for('referral_requests', WEEK_IN_SECONDS)->connect('referrals', true); $this->statsCache = Cache::for('referral_stats', WEEK_IN_SECONDS)->connect('referrals', true); $this->referrals_table = $wpdb->prefix . BASE . 'referrals'; $this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards'; $this->referralPage = $this->getReferralPageId(); $this->settings = $this->getRewardSettings(); add_action('jvbUserRegistered', [$this, 'processRegistrationToken'], 10, 3); add_action('jvb_add_token_inputs', [$this, 'addLoginInputs'], 10, 1); add_action('user_register', [$this, 'processReferral'], 10, 1); // Add meta boxes for admin to manage referrals add_action('show_user_profile', [$this, 'displayUserReferralInfo']); add_action('edit_user_profile', [$this, 'displayUserReferralInfo']); // Save referral code changes add_action('personal_options_update', [$this, 'saveUserReferralCode']); add_action('edit_user_profile_update', [$this, 'saveUserReferralCode']); add_filter('jvbNewUserEmail', [$this, 'addReferralToWelcomeEmail'], 99, 2); add_filter('jvbAdditionalActions', [$this, 'outputShareWidget']); add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']); // Schedule cron jobs for reports $this->registerCronJobs(); // Add admin bar label for referral page add_action('admin_bar_menu', [$this, 'addReferralPageLabel'], 999); // Add admin notice to referral page edit screen add_action('admin_notices', [$this, 'showReferralPageNotice']); add_filter('jvbDashboardPage', [$this, 'renderDashPage'], 10, 2); // Handle settings save add_action('admin_init', [$this, 'registerSettings']); // Handle admin page form submission add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3); } protected function defineTables():void { $this->defineReferralsTable(); $this->defineCodeTable(); $this->defineJaneClientsTable(); $this->defineRewardsTable(); $this->defineTreatmentsTable(); } protected function defineReferralsTable():void { $table = CustomTable::for('referrals'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'from_user' => "{$table->getUserIDType()} NOT NULL", 'to_user' => "{$table->getUserIDType()} NOT NULL", 'to_name' => 'varchar(255) NOT NULL', 'to_email' => 'varchar(255) NOT NULL', 'to_phone' => 'varchar(50) NOT NULL', 'referral_code'=> 'varchar(50) NOT NULL', 'status' => "ENUM('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending'", 'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP', 'consulted_at'=> 'datetime DEFAULT NULL', 'treated_at'=> 'datetime DEFAULT NULL', 'treatment_count' => 'int DEFAULT 0', 'notes' => 'text DEFAULT NULL', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => 'id'], ['key' => 'UNIQUE', 'value' => '(`to_user`)'], 'from_user (`from_user`)', 'status (`status`)', 'code (`referral_code`)', 'date (`created_at`)', 'consult (`consulted_at`)' ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}referral_from_user_fk` FOREIGN KEY (`from_user`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", "CONSTRAINT `{$base}referral_to_user_fk` FOREIGN KEY (`to_user`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" ]); $table->defineTable(); $this->referrals = $table; } protected function defineCodeTable():void { $table = CustomTable::for('referrals_codes'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'user_id' => "{$table->getUserIDType()} NOT NULL", 'code' => 'varchar(50) NOT NULL', 'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => '(`id`)'], ['key' => 'UNIQUE', 'value' => '(`code`)'], 'user (`user_id`)', ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}referral_code_user_fk` FOREIGN KEY (`user_id`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", ]); $table->defineTable(); $this->codes = $table; } protected function defineJaneClientsTable():void { $table = CustomTable::for('referrals_jane_clients'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'patient_guid' => 'varchar(50) NOT NULL', 'user_id' => "{$table->getUserIDType()} NOT NULL", 'first_name' => 'varchar(100) NOT NULL', 'last_name' => 'varchar(100) NOT NULL', 'email' => 'varchar(255) NOT NULL', 'imported_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => '(`id`)'], ['key' => 'UNIQUE', 'value' => '(`patient_guid`)'], 'user (`user_id`)', 'email (`email`)', ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}jane_clients_user` FOREIGN KEY (`user_id`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", ]); $table->defineTable(); $this->janeClients = $table; } protected function defineRewardsTable():void { $table = CustomTable::for('referrals_rewards'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'referral_id' => 'bigint(20) unsigned NOT NULL', 'user_id' => "{$table->getUserIDType()} NOT NULL", 'reward_type' => "ENUM('referrer', 'referee') NOT NULL", 'amount' => 'decimal(10,2) NOT NULL', 'reward_calculation'=> "ENUM('percentage', 'fixed')", 'status' => "ENUM('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available'", 'created_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', 'updated_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'redeemed_at' => 'datetime DEFAULT NULL', 'expires_at' => 'datetime DEFAULT NULL', 'notes' => 'text DEFAULT NULL', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => '(`id`)'], 'referral (`referral_id`)', 'user (`user_id`)', 'status (`status`)', 'type (`reward_type`)' ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}reward_referral` FOREIGN KEY (`referral_id`) REFERENCES `{$this->referrals->getFullTableName()}` (`id`) ON DELETE CASCADE", "CONSTRAINT `{$base}reward_user` FOREIGN KEY (`user_id`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" ]); $table->defineTable(); $this->rewards = $table; } protected function defineTreatmentsTable():void { $table = CustomTable::for('referral_treatments'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'referral_id' => 'bigint(20) unsigned NOT NULL', 'user_id' => "{$table->getUserIDType()} NOT NULL", 'treatment_type'=> 'varchar(100) NOT NULL', //Tier 1-6, Brows, etc 'treatment_date'=> 'datetime NOT NULL', 'invoice_number'=> 'varchar(50) DEFAULT NULL', 'amount' => 'decimal(10,2) DEFAULT NULL', 'status' => "ENUM('completed', 'no_show', 'cancelled') DEFAULT 'completed'", 'imported_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => '(`id`)'], 'referral (`referral_id`)', 'user (`user_id`)', 'date (`treatment_date`)', 'type (`treatment_type`)', ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}treatment_referral` FOREIGN KEY (`referral_id`) REFERENCES `{$this->referrals->getFullTableName()}` (`id`) ON DELETE CASCADE", "CONSTRAINT `{$base}treatment_user` FOREIGN KEY (`user_id`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" ]); $table->defineTable(); $this->treatments = $table; } public function getSettings():array { return $this->settings; } public function getRole():string { return $this->role; } public function addLoginInputs(string $action):void { if (array_key_exists('ref', $_GET)) { echo ''; } } public function modifyLoginText(array $defaults):array { if (!array_key_exists('ref', $_GET)){ return $defaults; } $code = $_GET['ref']; $user = $this->getUserByReferralCode($code); $desc = ($user) ? strtok($user->display_name, ' ') . ' invited you ' : 'You\'ve been invited '; $defaults['title'] = 'Register your Account. Get Your Reward.'; $defaults['description'] = [ $desc.' to see the difference with us.', 'Oh, and you\'ll get 20% off your first treatment!', 'Finish this account creation (you\'ll still need to make an account on Jane App to book), or let us know when you come in for your appointment.' ]; $defaults['extra'] = [ 'Once you\'re in, you\'ll get your own code you can share.', 'Get a $25 credit for each person you send our way who comes in for their first treatment!' ]; return $defaults; } public function enqueueScripts():void { $requirements = [ 'jvb-utility', 'jvb-a11y', 'jvb-popup', 'jvb-tabs', 'jvb-data-store', ]; if (Site::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) { JVB()->connect('cloudflare')->enqueueTurnstileScripts(); } if (is_singular(BASE.'dash')) { $requirements[] = 'jvb-form'; $requirements[] = 'jvb-view'; wp_enqueue_script('jvb-referral-admin', JVB_URL.'assets/js/min/referralAdmin.min.js', ['jvb-referral'], '1.0.0', true); } wp_enqueue_script( 'jvb-referral', JVB_URL . 'assets/js/min/referral.min.js', $requirements, '1.0.0', true ); } /** * Register cron jobs for automated reporting */ protected function registerCronJobs(): void { add_action(BASE . 'referral_daily_report', [$this, 'sendDailyReport']); add_action(BASE . 'referral_weekly_report', [$this, 'sendWeeklyReport']); // Schedule if not already scheduled if (!wp_next_scheduled(BASE . 'referral_daily_report')) { wp_schedule_event(strtotime('tomorrow 9:00'), 'daily', BASE . 'referral_daily_report'); } if (!wp_next_scheduled(BASE . 'referral_weekly_report')) { wp_schedule_event(strtotime('next Monday 9:00'), 'weekly', BASE . 'referral_weekly_report'); } } public function createCode(int $user_id, string $code):string|false { $code = sanitize_title($code); $existing = $this->codes->get(['code' => $code]); if ($existing) { if ($existing['user_id'] !== $user_id) { return false; } return $code; } $success = $this->codes->insert([ 'user_id' => $user_id, 'code' => $code ]); if ($success) { return $code; } return false; } /** * Generate or get existing referral code for a user * * @param int $user_id * @param string|null $custom_code Optional custom code * @return string|WP_Error */ public function getUserReferralCode(int $user_id, ?string $custom_code = null):string|false { $user = get_userdata($user_id); if (!$user) { return false; } $existing = $this->codes->pluck('code', ['user_id' => $user_id],'created_at', 'DESC'); if (!empty($existing) && !$custom_code) { return $existing[0]; } if ($custom_code && !empty($existing) && !in_array($custom_code, $existing)) { $test = $this->createCode($user_id, $custom_code); if ($test) { return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC'); } else { return $existing[0]; } } // Generate new code if custom provided or none exists $code = $this->generateReferralCode($user); $success = $this->createCode($user_id, $code); if ($success) { return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC')[0]; } return false; } /** * Generate a referral code based on user info * * @param WP_User $user * @return string */ protected function generateReferralCode(WP_User $user): string { // Create code from first and last name $first = sanitize_title($user->first_name ?: 'user'); $last = sanitize_title($user->last_name ?: wp_generate_password(4, false)); $base_code = strtoupper(substr($first, 0, 4) . substr($last, 0, 4)); // Ensure uniqueness $code = $base_code; $suffix = 1; while ($this->isCodeTaken($code)) { $code = $base_code . $suffix; $suffix++; } return $code; } /** * Check if a referral code is already taken * * @param string $code * @return bool */ protected function isCodeTaken(string $code): bool { return (bool) $this->codes->get(['code' => $code]); } public function processRegistrationToken(int $user_id, string $email, array $data): void { // Check for referral code in data $code = $data['referral_code'] ?? ''; if (empty($code)) { return; } // Store in session/cookie for processReferral to pick up if (session_status() === PHP_SESSION_NONE) { session_start(); } $_SESSION[BASE . 'referral_code'] = sanitize_text_field($code); setcookie( BASE . 'referral_code', sanitize_text_field($code), time() + (86400 * 30), '/' ); } /** * Track a new referral when user registers * * @param int $user_id * @param array $userData * @return bool; */ public function processReferral(int $user_id, array $userData): bool { $referral = $this->referrals->get(['to_user' => $user_id]); if (empty($referral)) { $referral = $this->referrals->get(['to_email' => $userData['email']]); } if (empty($referral)) { // Check session/cookie if not in meta if (session_status() === PHP_SESSION_NONE) { session_start(); } $referral_code = $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? ''; if (!empty ($referral_code)) { $referral = [ 'to_user' => $user_id, 'referral_code' => $referral_code, 'to_email' => $userData['user_email'] ]; } } if (empty($referral)) { return false; // No referral code - regular registration } // Find the referrer $referrer = $this->codes->pluck('user_id', ['code' => $referral['referral_code']]); if (empty($referrer)) { //This should not happen, but whatever return false; } $referrer = $referrer[0]; $record = $this->referrals->findOrCreate([ 'to_user' => $user_id, 'referral_code' => $referral['referral_code'], ], [ 'from_user' => $referrer, 'to_email' => $referral['to_email'], 'to_name' => $userData['first_name'], // 'to_phone' => 'status' => 'pending' ]); if (!$record) { error_log('[ReferralManager]::processReferral Could not update record for user: '.print_r($referral, true)); return false; } // Clean up temp data delete_user_meta($user_id, BASE . 'pending_referral_code'); if (isset($_SESSION[BASE . 'referral_code'])) { unset($_SESSION[BASE . 'referral_code']); } if (isset($_COOKIE[BASE . 'referral_code'])) { setcookie(BASE . 'referral_code', '', time() - 3600, '/'); } // Clear caches $this->cache->flush(); // Fire action for tracking do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral['referral_code']); // Send notification to referrer $this->sendReferrerNotification($referrer->ID, $userData['display_name']); return true; } /** * Create a referral record in the database * * @param int $referrer_id * @param int $referee_id * @param string $code * @return int|false */ public function createReferral(int $referrer_id, int $referee_id, string $code) { $user = get_user_by('ID', $referee_id); return $this->wpdb->insert( $this->referrals_table, [ 'referrer_id' => $referrer_id, 'referee_id' => $referee_id, 'referee_name' => $user->display_name, 'referee_email' => $user->user_email, 'referee_phone' => get_user_meta($referee_id, BASE . 'phone', true) ?: '', 'referral_code' => $code, 'status' => 'pending', 'referred_at' => current_time('mysql') ], ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s'] ); } /** * Get user by referral code * * @param string $code * @return WP_User|null */ public function getUserByReferralCode(string $code): ?WP_User { return $this->cache->remember( $code, function () use ($code) { $users = get_users([ 'meta_key' => BASE . 'referral_code', 'meta_value' => $code, 'number' => 1 ]); return $users[0] ?? null; } ); } /** * Get referral record by referee ID * * @param int $referee_id * @return object|null */ public function getReferralByReferee(int $referee_id): ?object { $result = $this->wpdb->get_row($this->wpdb->prepare( "SELECT * FROM {$this->referrals_table} WHERE referee_id = %d", $referee_id )); return $result ?: null; } /** * Mark a referral as treated/rewarded * * @param int $referral_id * @param bool $treated * @return bool */ public function markAsTreated(int $referral_id, bool $treated = true): bool { $status = $treated ? 'treated' : 'pending'; $result = $this->wpdb->update( $this->referrals_table, [ 'status' => $status, 'treated_at' => $treated ? current_time('mysql') : null ], ['id' => $referral_id], ['%s', '%s'], ['%d'] ); if ($result && $treated) { // Create reward records when marking as treated $this->createRewardRecords($referral_id); } return $result !== false; } /** * Create reward records for both referrer and referee * * @param int $referral_id */ protected function createRewardRecords(int $referral_id): void { $referral = $this->wpdb->get_row($this->wpdb->prepare( "SELECT * FROM {$this->referrals_table} WHERE id = %d", $referral_id )); if (!$referral) { return; } // Create referrer reward $this->wpdb->insert( $this->rewards_table, [ 'referral_id' => $referral_id, 'user_id' => $referral->referrer_id, 'reward_type' => 'referrer', 'amount' => $this->settings['referrer_reward_amount'], 'status' => 'available', 'created_at' => current_time('mysql') ], ['%d', '%d', '%s', '%f', '%s', '%s'] ); // Create referee reward $referee_amount = $this->settings['referee_reward_type'] === 'percentage' ? $this->settings['referee_reward_amount'] // Store as percentage : $this->settings['referee_reward_amount']; // Store as fixed amount $this->wpdb->insert( $this->rewards_table, [ 'referral_id' => $referral_id, 'user_id' => $referral->referee_id, 'reward_type' => 'referee', 'amount' => $referee_amount, 'reward_calculation' => $this->settings['referee_reward_type'], 'status' => 'available', 'created_at' => current_time('mysql') ], ['%d', '%d', '%s', '%f', '%s', '%s', '%s'] ); } /** * Get referrals for a user * * @param int $user_id * @param array $args * @return array */ public function getUserReferrals(int $user_id, array $args = []): array { $defaults = [ 'status' => 'all', 'limit' => 100, 'offset' => 0, 'orderby' => 'referred_at', 'order' => 'DESC' ]; $args = wp_parse_args($args, $defaults); return $this->requestCache->remember( $this->requestCache->generateKey(array_merge(['user'=>$user_id], $args)), function() use ($user_id, $args) { $where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id); if ($args['status'] !== 'all') { $where .= $this->wpdb->prepare(" AND status = %s", $args['status']); } $query = "SELECT * FROM {$this->referrals_table} {$where} ORDER BY {$args['orderby']} {$args['order']} LIMIT {$args['limit']} OFFSET {$args['offset']}"; $results = $this->wpdb->get_results($query); return array_map(function($referral) { $last_invite = get_transient('referral_last_invite_' . md5($referral->referee_email)); $can_resend = !$last_invite || (time() - $last_invite) > WEEK_IN_SECONDS; $status = match($referral->status) { 'consulted' => 'Awaiting Treatment', 'treated' => 'Rewarded!', default => 'Pending', }; return [ 'id' => $referral->id, 'referee_name' => $referral->referee_name, 'referee_email' => $referral->referee_email, 'referred_at' => JVB()->routes('referral')->formatTimestamp($referral->referred_at), 'referral_status'=> $status, 'can_resend' => $can_resend ]; }, $results); } ); } /** * Get referral statistics for a user * * @param int $user_id * @return array */ public function getUserStats(int $user_id): array { return $this->statsCache->remember( $user_id, function() use ($user_id) { $stats = $this->wpdb->get_row($this->wpdb->prepare( "SELECT COUNT(*) as code_used, SUM(CASE WHEN status IN ('consulted', 'treated') THEN 1 ELSE 0 END) as consultations, SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treatments, SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending FROM {$this->referrals_table} WHERE referrer_id = %d", $user_id ), ARRAY_A); // Get total rewards earned (available + redeemed) $rewards = $this->wpdb->get_var($this->wpdb->prepare( "SELECT SUM(amount) FROM {$this->rewards_table} WHERE user_id = %d AND reward_type = 'referrer'", $user_id )); $stats['total_rewards'] = floatval($rewards ?? 0); $stats['user_id'] = $user_id; return $stats; } ); } /** * Get top referrers for a time period * * @param int $limit * @param string $period 'day'|'week'|'month'|'all' * @return array */ public function getTopReferrers(int $limit = 10, string $period = 'all'): array { return $this->statsCache->remember( $this->statsCache->generateKey(['limit'=>$limit, 'period' => $period]), function() use ($limit, $period) { $where = ''; if ($period !== 'all') { $date_where = match($period) { 'day' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)", 'week' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)", 'month' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)", default => "1=1" }; $where = "WHERE {$date_where}"; } $query = "SELECT referrer_id, COUNT(*) as referral_count, SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count FROM {$this->referrals_table} {$where} GROUP BY referrer_id ORDER BY referral_count DESC LIMIT {$limit}"; $results = $this->wpdb->get_results($query); // Enrich with user data foreach ($results as &$result) { $user = get_user_by('ID', $result->referrer_id); $result->user_name = $user ? $user->display_name : 'Unknown'; $result->user_email = $user ? $user->user_email : ''; } return $results; } ); } /** * Send daily report if there are new referrals */ public function sendDailyReport(): void { $yesterday = date('Y-m-d', strtotime('-1 day')); $new_referrals = $this->wpdb->get_results($this->wpdb->prepare( "SELECT r.*, u.display_name as referrer_name FROM {$this->referrals_table} r JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID WHERE DATE(r.referred_at) = %s ORDER BY r.referred_at DESC", $yesterday )); if (empty($new_referrals)) { return; } $content = JVB()->email()->h1('Daily Referral Report'); $content .= JVB()->email()->stat( count($new_referrals), count($new_referrals) === 1 ? 'New Referral' : 'New Referrals', 'From ' . $yesterday ); $content .= JVB()->email()->spacer(20); $content .= JVB()->email()->h2('New Referrals'); // Build list of referrals foreach ($new_referrals as $ref) { $cardContent = sprintf( '

%s (%s)

', esc_html($ref->referee_name), esc_html($ref->referee_email) ); $cardContent .= sprintf( '

Referred by: %s | Code: %s

', JVB()->email()->colours['dark-200'], esc_html($ref->referrer_name), JVB()->email()->badge($ref->referral_code, 'info') ); $content .= JVB()->email()->card($cardContent); } $to = get_option('admin_email'); $subject = sprintf( '[%s] %d New Referral%s', get_bloginfo('name'), count($new_referrals), count($new_referrals) !== 1 ? 's' : '' ); JVB()->email()->sendEmail($to, $subject, $content, 'DAILY REPORT'); } /** * Send weekly report with top referrers */ public function sendWeeklyReport(): void { $top_referrers = $this->getTopReferrers(10, 'week'); $total_referrals = $this->wpdb->get_var( "SELECT COUNT(*) FROM {$this->referrals_table} WHERE referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)" ); if ($total_referrals == 0) { return; } $content = JVB()->email()->h1('Weekly Referral Summary'); $content .= JVB()->email()->stat( $total_referrals, 'Total Referrals', 'This week' ); $content .= JVB()->email()->spacer(30); $content .= JVB()->email()->h2('Top 10 Referrers'); // Leaderboard style $rank = 1; foreach ($top_referrers as $referrer) { $rankBadge = $rank <= 3 ? JVB()->email()->badge('#' . $rank, $rank === 1 ? 'success' : 'info') : '#' . $rank . ''; $cardContent = sprintf( '

%s %s

', $rankBadge, esc_html($referrer->user_name) ); $stats = [ JVB()->email()->stat($referrer->referral_count, 'Total Referrals'), JVB()->email()->stat($referrer->treated_count, 'Treated') ]; $cardContent .= JVB()->email()->grid($stats, 2); $content .= JVB()->email()->card($cardContent); $rank++; } $to = get_option('admin_email'); $subject = '[' . get_bloginfo('name') . '] Weekly Referral Summary - ' . date('F j, Y'); JVB()->email()->sendEmail($to, $subject, $content, 'WEEKLY SUMMARY'); } /** * Generate CSV content from referrals * * @param array $referrals * @return string */ protected function generateCSV(array $referrals): string { $cache = Cache::for('referralCSV', HOUR_IN_SECONDS)->connect('referrals'); return $cache->remember( 'csv', function () use ($referrals) { $csv = "Referred By,Referee Name,Referee Email,Referee Phone,Referral Code,Status,Referred At,Treated At\n"; foreach ($referrals as $referral) { $csv .= sprintf( '"%s","%s","%s","%s","%s","%s","%s","%s"' . "\n", $referral->referrer_name ?? 'Unknown', $referral->referee_name, $referral->referee_email, $referral->referee_phone, $referral->referral_code, $referral->status, $referral->referred_at, $referral->treated_at ?? 'Not yet' ); } return $csv; } ); } /** * Generate HTML email for weekly report * * @param array $top_referrers * @param int $total_referrals * @return string */ protected function generateWeeklyReportEmail(array $top_referrers, int $total_referrals): string { $content = sprintf( '

This week you had %d total referral%s.

', $total_referrals, $total_referrals !== 1 ? 's' : '' ); $referrers = []; $rank = 1; foreach ($top_referrers as $referrer) { $referrers[] = [ 'label' => '#' . $rank++ . ' - ' . esc_html($referrer->user_name), 'value' => sprintf( 'Total Referrals: %d | Treated: %d', $referrer->referral_count, $referrer->treated_count ) ]; } $content .= JVB()->email()->table($referrers, 'Top 10 Referrers This Week'); return $content; } /** * Get reward settings * * @return array */ public function getRewardSettings(): array { $saved = get_option(BASE . 'referral_settings', []); return wp_parse_args($saved, $this->default_settings); } /** * Display referral info in user profile * * @param WP_User $user */ public function displayUserReferralInfo(WP_User $user): void { if (!current_user_can('edit_user', $user->ID)) { return; } $referral_code = get_user_meta($user->ID, BASE . 'referral_code', true); $stats = $this->getUserStats($user->ID); $referrals = $this->getUserReferrals($user->ID, ['limit' => 10]); ?>

Referral Information

Users can sign up with this code. Share link:

Referral Statistics

Total Referrals:
Treated:
Pending:
Available Rewards: $
Redeemed Rewards: $

Recent Referrals

Name Email Status Referred At Actions
referee_name); ?> referee_email); ?> status)); ?> referred_at); ?> status === 'pending'): ?>
function markReferralTreated(referralId) { if (!confirm('Mark this referral as treated? This will create reward records.')) { return; } fetch('' + referralId + '/treat', { method: 'POST', headers: { 'X-WP-Nonce': '' } }) .then(response => response.json()) .then(data => { if (data.success) { alert('Referral marked as treated!'); location.reload(); } else { alert('Error: ' + (data.message || 'Unknown error')); } }); } add('invalid_referral_code', 'Referral code can only contain letters and numbers.'); }); return; } // Check if code is unique if ($this->isCodeTaken($code, $user_id)) { add_action('user_profile_update_errors', function($errors) { $errors->add('referral_code_taken', 'This referral code is already in use.'); }); return; } update_user_meta($user_id, BASE . 'referral_code', strtoupper($code)); } /** * Display user's referral code and share options * Use in templates or dashboard with: echo jvbReferralShareWidget(); * * @return string HTML output */ public function outputShareWidget(array $actions):array { $user_id = get_current_user_id(); $content = ''; $actions[] =[ 'button' => '', 'content' => $content ]; return $actions; } /** * Display referral sidebar for non-logged-in users */ function getUnloggedInReferral(): string { ob_start(); JVB()->connect('cloudflare')->renderTurnstile(); $turnstile = ob_get_clean(); $reward_text = $this->getRewardText(true); // Pre-fill code if from referral link $prefill_code = $_GET['ref'] ?? ''; $referrer_name = ''; if ($prefill_code) { $referrer = $this->getUserByReferralCode($prefill_code); $referrer_name = $referrer ? strtok($referrer->display_name, ' ') : ''; } $codeForm = '
'.jvbIcon('confetti').'

Get ' . esc_html($reward_text) . '!

' . ($referrer_name ? '

' . esc_html($referrer_name) . ' invited you to join us

' : '') . '
'.jvbFormStatus(). ' ' .Form::render('referral_name', '', [ 'required' => true, 'type' => 'text', 'label' => 'Your Name', 'placeholder'=> 'Mister Meeseeks', 'autocomplete'=>'name' ]). Form::render('referral_email', '', [ 'required' => true, 'type' => 'email', 'label' => 'Your Email', 'placeholder'=> 'look@me.com', 'autocomplete'=> 'email' ]). Form::render('referral_code', $prefill_code, [ 'required' => true, 'type' => 'text', 'label' => 'Referral Code', 'pattern' => '[A-Za-z0-9]+', 'maxLength' => 20, 'autocomplete'=>'off', 'data-referrer' => $referrer_name ]).' '.$turnstile.'

We\'ll send you a link to complete your registration.

'; $loginForm = '
'.jvbFormStatus().Form::render('login_email', null, [ 'required' => true, 'type' => 'email', 'label' => 'Your Email', 'autocomplete'=>'email' ]).' '.$turnstile.'
'; $footer = ''; $tabs = [ 'enterCode' => [ 'title' => 'Have a Code?', 'description' => [ 'Enter your referral code to get started' ], 'content' => $codeForm ], 'login' => [ 'title' => 'Login', 'description' => [ 'Already have an account? Log in to see your rewards' ], 'content' => $loginForm.$footer ] ]; return jvbRenderTabs($tabs, true); } protected function getReferralSuccessMessage(string $code): string { $referrer = $this->getUserByReferralCode($code); if (!$referrer) { return ''; } $reward_amount = $this->settings['referee_reward_amount'] ?? 20; $reward_type = $this->settings['referee_reward_type'] ?? 'percentage'; $reward_text = $reward_type === 'percentage' ? $reward_amount . '% off' : '$' . number_format($reward_amount, 2) . ' off'; $booking_url = apply_filters('jvb_referral_booking_url', home_url('/contact')); ob_start(); ?>

Success! Your Reward is Ready!

You'll receive:

your first treatment!

Your referral code has been applied. Book your free consultation now to claim your reward!

Book Your Free Consultation
Referred by display_name); ?>
getUserReferralCode($user_id); if (!$referral_code) { return ''; } $share_url = $this->getShareURL($referral_code); ob_start(); ?>

Share the ♡

Invite friends. Earn rewards.

getShareButtons($user_id); ?>

Your Referral Link

Quickest and easiest: autofills your code.

Your Code

Manually copy and paste the code

Recent Referrals

Loading...
Total Referrals -
Successful -
Pending -
Available Rewards $0.00
Dashboard 16]); ?>

Bulk-invite your friends via email - the link will pre-fill their name, email, and code!

getReferralByReferee($user->ID); if (!$referral) { return $content; } $reward_amount = $this->settings['referee_reward_amount'] ?? 20; $reward_type = $this->settings['referee_reward_type'] ?? 'percentage'; $reward_text = $reward_type === 'percentage' ? $reward_amount . '% off' : '$' . number_format($reward_amount, 2) . ' off'; $bonus_content = sprintf( '

Thanks for the ♡

Since you were referred by a friend, you\'ve earned %s your first booking!

Your reward will be automatically applied when you book.

', $reward_text, ); $code = $this->getUserReferralCode($user->ID); $yourCode = ''; if ($code) { $share_url = $this->getShareURL($code); $yourCode = sprintf( '

Share the ♡ with Friends

If you find you love what we can do for you, you can share your own code!

Your Referral Code: %s

Or click the button below:

%s
', JVB()->email()->link($code), JVB()->email()->button($share_url, 'Share Your Code') ); } return $content . $bonus_content . $yourCode; } public function getShareURL(string $code):string { return add_query_arg( [ 'ref' => $code ], get_home_url() ); } /** * Send referral invitation via email with magic link * * @param int $user_id Referrer's user ID * @param string $invitee_email Email of person to invite * @param string $invitee_name Name of person to invite * @param string $subject * @param string $message * @return array|WP_Error Result with success/error */ public function sendReferralInvitation(int $user_id, string $invitee_email, string $invitee_name, string $subject, string $message):array|WP_Error { // Check email rate limit (15/hour) $rate_check = $this->checkEmailRateLimit($user_id); if ($rate_check !== true) { return new WP_Error('rate_limit', 'You can only send 15 invitations per hour. Please try again later.'); } // Validate email $invitee_email = sanitize_email($invitee_email); if (!is_email($invitee_email)) { return new WP_Error('invalid_email', 'Invalid email address'); } // Check if already registered if (email_exists($invitee_email)) { return new WP_Error('user_exists', 'This person already has an account'); } // Get referrer info $referrer = get_user_by('ID', $user_id); $referral_code = $this->getUserReferralCode($user_id); if ($referral_code) { return $referral_code; } // Record the invitation attempt (for rate limiting only) $this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name); // Create registration URL with token (opens sidebar with prefilled form) $token_data = [ 'name' => sanitize_text_field($invitee_name), 'email' => $invitee_email, 'expires' => time() + (30 * DAY_IN_SECONDS) ]; // Encode the token $token = base64_encode(json_encode($token_data)); $registration_url = add_query_arg([ 'ref' => $referral_code, 'rname' => sanitize_text_field($invitee_name), 'remail'=> rawurlencode($invitee_email), ], home_url('/')); // Get reward text for email $reward_text = $this->settings['referee_reward_type'] === 'percentage' ? "{$this->settings['referee_reward_amount']}% off" : "\${$this->settings['referee_reward_amount']} off"; // Build email content $email_content = sprintf( '

%s invited you to %s!

%s

Get %s your first treatment!

Click the button below to register and claim your reward:

%s

This invitation expires in 30 days.

', esc_html($referrer->display_name), esc_html(get_bloginfo('name')), nl2br(esc_html($message)), esc_html($reward_text), JVB()->email()->button($registration_url, 'Register & Get Your Reward') ); // Send email $sent = JVB()->email()->sendEmail( $invitee_email, $subject, $email_content ); if (!$sent) { return new WP_Error('email_failed', 'Failed to send invitation email'); } return [ 'success' => true, 'message' => 'Invitation sent successfully', 'email' => $invitee_email, 'name' => $invitee_name ]; } /** * Send multiple referral invitations * * @param int $user_id Referrer's user ID * @param array $invitations Array of ['email' => '', 'name' => ''] * @return array Results with success/failed arrays */ public function sendBatchReferralInvitations(int $user_id, array $invitations, string $subject, string $message): array { $results = [ 'success' => [], 'failed' => [] ]; foreach ($invitations as $invite) { $email = $invite['email'] ?? ''; $name = $invite['name'] ?? ''; if (empty($email) || empty($name)) { $results['failed'][] = [ 'email' => $email, 'name' => $name, 'reason' => 'Missing email or name' ]; continue; } $result = $this->sendReferralInvitation($user_id, $email, $name, $subject, $message); if (is_wp_error($result)) { $results['failed'][] = [ 'email' => $email, 'name' => $name, 'reason' => $result->get_error_message() ]; } else { $results['success'][] = [ 'email' => $email, 'name' => $name ]; } // Small delay between sends to be respectful usleep(100000); // 0.1 seconds } return [ 'success' => !empty($results['success']), 'result' => $results, 'summary' => sprintf( 'Sent %d invitations, %d failed', count($results['success']), count($results['failed']) ) ]; } /** * Check email invitation rate limit (15 per hour) * * @param int $user_id * @return true|string True if allowed, error message if limited */ protected function checkEmailRateLimit(int $user_id):bool|string { $hourly_key = 'referral_invites_hour_' . $user_id; $count = (int) get_transient($hourly_key); if ($count >= 15) { return 'hourly_limit_reached'; } set_transient($hourly_key, $count + 1, HOUR_IN_SECONDS); return true; } /** * Check if an email has already been invited * * @param string $email * @return bool */ protected function isEmailInvited(string $email): bool { // Check invitation tracking table $invitation_key = 'referral_invite_' . md5($email); $invited = get_transient($invitation_key); if ($invited) { return true; } // Check if there's a pending referral for this email $existing = $this->wpdb->get_var($this->wpdb->prepare( "SELECT id FROM {$this->referrals_table} WHERE referee_email = %s", $email )); return !empty($existing); } /** * Record invitation attempt (for tracking and preventing duplicates) * * @param int $user_id * @param string $email * @param string $name */ protected function recordInvitationAttempt(int $user_id, string $email, string $name): void { // Store for 30 days (same as magic link invitation validity) $invitation_key = 'referral_invite_' . md5($email); $data = [ 'inviter_id' => $user_id, 'email' => $email, 'name' => $name, 'sent_at' => current_time('mysql') ]; set_transient($invitation_key, $data, 30 * DAY_IN_SECONDS); // Also log in user meta for tracking $sent_invites = get_user_meta($user_id, BASE . 'referral_invites_sent', true) ?: []; $sent_invites[] = [ 'email' => $email, 'name' => $name, 'sent_at' => current_time('mysql') ]; update_user_meta($user_id, BASE . 'referral_invites_sent', $sent_invites); } /** * Get user's invitation stats * * @param int $user_id * @return array */ public function getUserInvitationStats(int $user_id): array { $sent_invites = get_user_meta($user_id, BASE . 'referral_invites_sent', true) ?: []; // Count invites sent in last hour $one_hour_ago = strtotime('-1 hour'); $recent_count = 0; foreach ($sent_invites as $invite) { if (strtotime($invite['sent_at']) > $one_hour_ago) { $recent_count++; } } return [ 'total_sent' => count($sent_invites), 'sent_last_hour' => $recent_count, 'remaining_this_hour' => max(0, 15 - $recent_count), 'can_send_more' => $recent_count < 15 ]; } /** * Export referrals for Jane App cross-reference * * @param string $start_date Y-m-d format * @param string $end_date Y-m-d format * @return string CSV content */ public function exportReferrals(string $start_date, string $end_date): string { $referrals = $this->wpdb->get_results($this->wpdb->prepare( "SELECT r.id, r.referee_name, r.referee_email, r.referee_phone, r.referral_code, r.referred_at, r.status, r.treated_at, u.display_name as referrer_name, u.user_email as referrer_email FROM {$this->referrals_table} r JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID WHERE DATE(r.referred_at) BETWEEN %s AND %s ORDER BY r.referred_at DESC", $start_date, $end_date )); // Build CSV $csv_lines = []; // Headers $csv_lines[] = [ 'Referral ID', 'Referee Name', 'Referee Email', 'Referee Phone', 'Referral Code', 'Referred Date', 'Status', 'Treated Date', 'Referrer Name', 'Referrer Email' ]; // Data rows foreach ($referrals as $ref) { $csv_lines[] = [ $ref->id, $ref->referee_name, $ref->referee_email, $ref->referee_phone ?: 'N/A', $ref->referral_code, $ref->referred_at, ucfirst($ref->status), $ref->treated_at ?: 'N/A', $ref->referrer_name, $ref->referrer_email ]; } // Convert to CSV string $output = fopen('php://temp', 'r+'); foreach ($csv_lines as $line) { fputcsv($output, $line); } rewind($output); $csv_content = stream_get_contents($output); fclose($output); return $csv_content; } /** * Add referral settings subpage to admin menu * Add referral settings subpage to admin menu * * @param array $subpages * @return array */ public static function addSubpage():void { $subpage = [ 'page_title' => 'Referral System', 'menu_title' => 'Referrals', 'capability' => 'manage_options', 'menu_slug' => BASE . 'referral-admin', 'callback' => [self::class, 'renderAdminPageStatic'] ]; AdminPages::addSubPage(BASE.'referral-admin', $subpage); } /** * Static wrapper for renderAdminPage * Called by WordPress when admin page is rendered */ public static function renderAdminPageStatic(): void { // Get the properly initialized instance from JVB singleton JVB()->referrals()->renderAdminPage(); } /** * Register settings */ public function registerSettings(): void { register_setting( BASE . 'referral_settings', BASE . 'referral_page_id', [ 'type' => 'integer', 'sanitize_callback' => 'absint', 'default' => 0 ] ); register_setting( BASE . 'referral_settings', BASE . 'referral_reward_settings', [ 'type' => 'array', 'sanitize_callback' => [$this, 'sanitizeRewardSettings'], 'default' => $this->default_settings ] ); } /** * Sanitize reward settings */ public function sanitizeRewardSettings(array $settings): array { return [ 'referrer_reward_applies_to' => in_array($settings['referrer_reward_applies_to'] ?? '', ['per_user', 'flat_total']) ? $settings['referrer_reward_applies_to'] : 'per_user', 'referrer_reward_amount' => floatval($settings['referrer_reward_amount'] ?? 25.00), 'referrer_reward_type' => in_array($settings['referrer_reward_type'] ?? '', ['fixed', 'percentage']) ? $settings['referrer_reward_type'] : 'fixed', 'referee_reward_type' => in_array($settings['referee_reward_type'] ?? '', ['percentage', 'fixed']) ? $settings['referee_reward_type'] : 'percentage', 'referee_reward_amount' => floatval($settings['referee_reward_amount'] ?? 20), 'referee_reward_applies_to' => in_array($settings['referee_reward_applies_to'] ?? '', ['first_order', 'all_orders']) ? $settings['referee_reward_applies_to'] : 'first_order', ]; } /** * Render the admin settings page */ public function renderAdminPage(): void { ?>

Referral System Management

Import Data from Jane App

Upload your exported CSV files from Jane App to sync client and sales data.

Client List

Sales Export

Referrals Management

Loading referrals...
renderAdminHTML() ?>
.jvb-upload-box { padding: 20px; background: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; } .jvb-upload-box h3 { margin-top: 0; } .referrals-table { width: 100%; border-collapse: collapse; } .referrals-table th, .referrals-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; } .referrals-table th { background: #f5f5f5; font-weight: 600; } .referrals-table tr:hover { background: #f9f9f9; } .referral-status { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; } .referral-status.pending { background: #fff3cd; color: #856404; } .referral-status.consulted { background: #d1ecf1; color: #0c5460; } .referral-status.treated { background: #d4edda; color: #155724; } .referral-actions { display: flex; gap: 5px; } .notice.notice-success, .notice.notice-error { margin: 10px 0; } */ if (is_admin()) { ?>

Referral Settings

Referral Page

Select the page where users can access their referral dashboard.

referralPage) { $this->referralPage = $this->getReferralPageId(); } wp_dropdown_pages([ 'name' => BASE . 'referral_page_id', 'id' => BASE . 'referral_page_id', 'selected' => $this->referralPage, 'show_option_none' => __('— Select —', 'jvbase'), 'option_none_value' => '0' ]); ?>

This page will show "Referral Page" in the admin bar when editing.

Reward Settings

Referrer Rewards

Amount in dollars or percentage

Referee (New User) Rewards

Amount in dollars or percentage

get_names(); ?>

Role assigned to users imported from Jane App client list.

renderReferralStats(true) ?>
prefix . BASE . 'referrals'; // Use proper WordPress prepare even for COUNT $total_referrals = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$referrals_table}` WHERE 1=%d", 1 ) ); $pending_referrals = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s", 'pending' ) ); $treated_referrals = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s", 'treated' ) ); ?>
Total Referrals
Pending
Treated

Referral Statistics

' . $table . ' '; } return $table; } /** * Add "Referral Page" label to admin bar * * @param WP_Admin_Bar $wp_admin_bar */ public function addReferralPageLabel($wp_admin_bar): void { if (!is_admin()) { return; } if (!$this->referralPage) { $this->referralPage = $this->getReferralPageId(); } if (!$this->referralPage) { return; } global $pagenow, $post; // Check if we're editing the referral page if ('post.php' === $pagenow && $post && $post->ID === $this->referralPage) { $wp_admin_bar->add_node([ 'id' => 'referral-page', 'parent' => 'top-secondary', 'title' => __('Referral Page', 'jvbase'), 'meta' => [ 'class' => 'referral-page-notice' ] ]); } } /** * Get the referral page ID * * @return int|null */ public function getReferralPageId(): ?int { $page_id = get_option(BASE . 'referral_page_id'); return $page_id ? (int) $page_id : null; } /** * Show admin notice on referral page edit screen */ public function showReferralPageNotice(): void { global $pagenow, $post; if ('post.php' !== $pagenow || !$post) { return; } if (!$this->referralPage) { $this->referralPage = $this->getReferralPageId(); } if ($post->ID === $this->referralPage) { echo '
'; echo '

' . __('This page is designated as the Referral Page.', 'jvbase') . '

'; echo '
'; } } public function renderDashPage(string $content, string $page): string { if ($page !== 'Referrals') { return $content; } // Regular users get their referral dashboard $user_id = get_current_user_id(); $referral_code = $this->getUserReferralCode($user_id); if (!$referral_code) { $referral_code = $this->generateReferralCode($user_id); } $referrals = $this->getUserReferrals($user_id, ['limit' => 20]); ob_start(); $tabs = new Tabs(); $tabs->addTab('share') ->title('Share') ->icon('share-fat') ->description('Share your code and earn rewards when your referrals complete their first treatment!') ->content($this->shareDashboard($user_id, $referral_code)); $tabs->addTab('referrals') ->title('Your Referrals') ->icon('hand-heart') ->content($this->referralCRUD($user_id)); ?>
render(true);?>
getShareButtons($user_id); ?>

Share Link

Share Code

Invite your Friends

Or, if you prefer, enter your friends name(s) and email(s), and we'll send off some emails.

(No data is stored. Your friends will get an email from our email.)

'taglist', 'label' => 'Invite Your Friends', 'hint' => 'Add friends to send them a referral link', 'add_label' => 'Add Invite', 'tag_format' => '{{name}} ({{email}})', // or 'first_field', 'all_fields', 'email', etc. 'fields' => [ 'name' => [ 'type' => 'text', 'label' => 'Name', 'placeholder' => 'Full Name', 'required' => true ], 'email' => [ 'type' => 'email', 'label' => 'Email', 'placeholder' => 'email@example.com', 'required' => true ] ] ]; $fields = [ 'subject' => [ 'type' => 'text', 'label' => 'Email Subject', 'value' => 'Try Legacy for Tattoo Removal', ], 'message' => [ 'type' => 'textarea', 'label' => 'Customize message', 'value' => 'I had a great experience at Legacy Tattoo Removal! If you click the link below, you can get 20% off your first treatment with them.', 'hint' => 'We\'ll add your code and a link automatically.' ] ]; echo Form::render('invite', '', $invite); ?>
Customize Message $field) { $value = (array_key_exists('value', $field)) ? $field['value'] : []; echo Form::render($fieldName, $value, $field); } ?>
getUserStats($user_id); ob_start(); ?>

Code Used

People who used your code

Treatments

Completed first treatment

Total Rewards

$

Earned from referrals

title('Your Referrals', 'Track friends you\'ve invited and rewards earned') ->content('referral', 'Referral', 'Referrals') // ->initMeta('custom', 'referral') ->setFields([ 'referee_name' => [ 'label' => 'Name', 'type' => 'text', ], 'referee_email' => [ 'label' => 'Email', 'type' => 'text', ], 'referred_at' => [ 'label' => 'Code Used', 'type' => 'date', ], 'referral_status' => [ 'label' => 'Status', 'type' => 'text', ] ]) ->setStatuses(['all', 'unused', 'registered', 'consulted', 'completed']) ->addViews(['table', 'list']) ->defaultView('table') ->addCapabilities(['view']) ->addDateFilter('referred_at') ->showBulkControls(false) ->showFilters(false) ->useCRUDjs(false); // We'll use our custom Referral.js with DataStore // Add custom template for actions column $crud->addItemActions(['resend', 'trash']); $crud->defineItemAction('resend', [ 'title' => 'Resend Invitation', 'icon' => 'paper-plane-tilt' ]); $crud->defineItemAction('trash', [ 'title' => 'Remove from List' ]); // Custom empty state $crud->addTemplate('empty', ' '); $crud->render(); return ob_get_clean(); } /** * Handle admin page form submission * * @param mixed $result Previous result * @param string $page_slug Current page slug * @param array $post_data POST data * @return array|null Result array or null if not our page */ public function handleAdminSubmission($result, string $page_slug, array $post_data): ?array { // Only handle our page if ($page_slug !== BASE . 'referral-admin') { return $result; } try { // Save referral page $page_id = isset($post_data[BASE . 'referral_page_id']) ? absint($post_data[BASE . 'referral_page_id']) : 0; update_option(BASE . 'referral_page_id', $page_id); // Save client import role $import_role = sanitize_text_field($post_data[BASE . 'referral_role'] ?? JVB_USER); update_option(BASE . 'referral_role', $import_role); // Save reward settings $settings = [ 'referrer_reward_type' => sanitize_text_field($post_data['referrer_reward_type'] ?? 'fixed'), 'referrer_reward_amount' => floatval($post_data['referrer_reward_amount'] ?? 25.00), 'referrer_reward_applies_to' => sanitize_text_field($post_data['referrer_reward_applies_to'] ?? 'per_user'), 'referee_reward_type' => sanitize_text_field($post_data['referee_reward_type'] ?? 'percentage'), 'referee_reward_amount' => floatval($post_data['referee_reward_amount'] ?? 20), 'referee_reward_applies_to' => sanitize_text_field($post_data['referee_reward_applies_to'] ?? 'first_order') ]; update_option(BASE . 'referral_settings', $settings); return [ 'success' => true, 'message' => 'Referral settings saved successfully!' ]; } catch (\Exception $e) { return [ 'success' => false, 'message' => 'Failed to save settings: ' . $e->getMessage() ]; } } /** * Get formatted reward text for referee * * @param bool $full Include "off your first treatment" text * @return string */ public function getRewardText(bool $full = true): string { $reward_amount = $this->settings['referee_reward_amount'] ?? 20; $reward_type = $this->settings['referee_reward_type'] ?? 'percentage'; $reward_text = $reward_type === 'percentage' ? $reward_amount . '% off' : '$' . number_format($reward_amount, 2) . ' off'; if ($full) { $reward_text .= ' your first treatment'; } return $reward_text; } public function getShareButtons(int $user_id):void { $referral_code = $this->getUserReferralCode($user_id); if (!$referral_code) { return; } $share_url = $this->getShareURL($referral_code); $referral_page_id = $this->getReferralPageId(); // SMS share text $sms_text = urlencode("Check out " . get_bloginfo('name') . "! " . $share_url); // Share message $share_message = urlencode("I love " . get_bloginfo('name') . "! Thought you might want to check them out."); ?> email()->sendEmail( $referrer->user_email, $subject, $message ); } /** * Get welcome message for newly referred user * * @param int $user_id * @return string HTML content for welcome message */ public function getReferralWelcomeMessage(int $user_id): string { // Check if user was referred $referral = $this->getReferralByReferee($user_id); if (!$referral || $referral->status !== 'pending') { return ''; } // Only show for recent registrations (within 7 days) $registered_time = strtotime($referral->referred_at); if ((time() - $registered_time) > (7 * DAY_IN_SECONDS)) { return ''; } // Get referrer name $referrer = get_userdata($referral->referrer_id); $referrer_first_name = $referrer ? strtok($referrer->display_name, ' ') : 'Your friend'; // Get reward text $reward_text = $this->getRewardText(); // Just "20% off" or "$25 off" $booking_url = apply_filters('jvb_referral_booking_url', home_url('/contact')); $estimate_url = apply_filters('jvb_referral_estimate_url', home_url('/estimate')); ob_start(); ?>