Jake Vanderwerf
2025-11-10 e9967fa22781d922ba4eb8fb44fe72d200ac4b14
inc/managers/ReferralManager.php
@@ -1,6 +1,10 @@
<?php
namespace JVBase\managers;
use JVBase\managers\MagicLinkManager;
use JVBase\integrations\Cloudflare;
use JVBase\meta\MetaForm;
use JVBase\utility\Features;
use WP_User;
use WP_Error;
@@ -17,8 +21,10 @@
class ReferralManager
{
   protected $wpdb;
   protected MagicLinkManager $magic_link;
   protected CacheManager $cache;
   protected string $referrals_table;
   protected ?int $referralPage = null;
   protected string $rewards_table;
   // Default reward settings
@@ -31,16 +37,24 @@
      'referee_reward_applies_to' => 'first_order'  // 'first_order' or 'all_orders'
   ];
   protected array $settings;
   public function __construct()
   {
      global $wpdb;
      $this->wpdb = $wpdb;
      $this->cache = new CacheManager('referrals');
      $this->cache = CacheManager::for('referrals', WEEK_IN_SECONDS);
      $this->referrals_table = BASE . 'referrals';
      $this->rewards_table = BASE . 'referral_rewards';
      $this->magic_link = new MagicLinkManager();
      // Hook into user registration to track referrals
      add_action('user_register', [$this, 'processReferral'], 10, 1);
      $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('jvbUserRegistered', [$this, 'processReferral'], 10, 1);
      // Add meta boxes for admin to manage referrals
      add_action('show_user_profile', [$this, 'displayUserReferralInfo']);
@@ -50,15 +64,81 @@
      add_action('personal_options_update', [$this, 'saveUserReferralCode']);
      add_action('edit_user_profile_update', [$this, 'saveUserReferralCode']);
      add_filter(BASE.'new_user_email_content', [$this, 'addReferralToWelcomeEmail'], 99, 2);
      add_filter('jvbNewUserEmail', [$this, 'addReferralToWelcomeEmail'], 99, 2);
      if (is_user_logged_in()) {
         add_action('wp_footer', [$this, 'outputShareWidget']);
      }
      add_action('template_redirect', [$this, 'trackReferralCode']);;
      add_filter('jvbAdditionalActions', [$this, 'outputShareWidget']);
      add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
      // Schedule cron jobs for reports
      $this->registerCronJobs();
      // Register admin subpage
//    add_filter('jvbAdminSubpages', [$this, 'addSubpage'], 10, 1);
      // 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']);
   }
   public function addLoginInputs(string $action):void
   {
      if (array_key_exists('ref', $_GET)) {
         echo '<input type="hidden" name="referral_code" value="'.$_GET['ref'].'">';
      }
   }
   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 <b>each</b> 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',
      ];
      if (Features::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) {
         $requirements[] = 'cloudflare-turnstile';
      }
      wp_enqueue_script(
         'jvb-referral',
         JVB_URL . 'assets/js/min/referral.min.js',
         $requirements,
         '1.0.0',
         true
      );
   }
   /**
@@ -164,17 +244,47 @@
      return !empty($users);
   }
   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
    */
   public function processReferral(int $user_id): void
   public function processReferral(int $user_id, string $email, array $data): void
   {
      // Check if there's a referral code in the session/cookie
      $referral_code = $this->getReferralCodeFromSession();
      // Check if user was created via referral magic link
      // Try to get code from multiple sources
      $referral_code = $data['referral_code'] ??
      get_user_meta($user_id, BASE . 'pending_referral_code', true);
      if (!$referral_code) {
      // Check session/cookie if not in data
      if (empty($referral_code)) {
         if (session_status() === PHP_SESSION_NONE) {
            session_start();
         }
         $referral_code = $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? '';
      }
      if (empty($referral_code)) {
         return;
      }
@@ -182,20 +292,27 @@
      $referrer = $this->getUserByReferralCode($referral_code);
      if (!$referrer) {
         delete_user_meta($user_id, BASE . 'pending_referral_code');
         return;
      }
      // Check if this user was already referred (prevent duplicates)
      // Check for duplicates
      $existing = $this->getReferralByReferee($user_id);
      if ($existing) {
         delete_user_meta($user_id, BASE . 'pending_referral_code');
         return;
      }
      // Create referral record
      $this->createReferral($referrer->ID, $user_id, $referral_code);
      $result = $this->createReferral($referrer->ID, $user_id, $referral_code);
      // Clear the session
      $this->clearReferralSession();
      if ($result) {
         // Clean up temp meta
         delete_user_meta($user_id, BASE . 'pending_referral_code');
         // Fire action for tracking
         do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code);
      }
   }
   /**
@@ -206,7 +323,7 @@
    * @param string $code
    * @return int|false
    */
   protected function createReferral(int $referrer_id, int $referee_id, string $code)
   public function createReferral(int $referrer_id, int $referee_id, string $code)
   {
      $user = get_user_by('ID', $referee_id);
@@ -232,15 +349,19 @@
    * @param string $code
    * @return WP_User|null
    */
   protected function getUserByReferralCode(string $code): ?WP_User
   public function getUserByReferralCode(string $code): ?WP_User
   {
      $users = get_users([
         'meta_key' => BASE . 'referral_code',
         'meta_value' => $code,
         'number' => 1
      ]);
      return $users[0] ?? null;
      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;
         }
      );
   }
   /**
@@ -305,8 +426,6 @@
         return;
      }
      $settings = $this->getRewardSettings();
      // Create referrer reward
      $this->wpdb->insert(
         $this->rewards_table,
@@ -314,7 +433,7 @@
            'referral_id' => $referral_id,
            'user_id' => $referral->referrer_id,
            'reward_type' => 'referrer',
            'amount' => $settings['referrer_reward_amount'],
            'amount' => $this->settings['referrer_reward_amount'],
            'status' => 'available',
            'created_at' => current_time('mysql')
         ],
@@ -322,9 +441,9 @@
      );
      // Create referee reward
      $referee_amount = $settings['referee_reward_type'] === 'percentage'
         ? $settings['referee_reward_amount']  // Store as percentage
         : $settings['referee_reward_amount'];  // Store as fixed amount
      $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,
@@ -333,7 +452,7 @@
            'user_id' => $referral->referee_id,
            'reward_type' => 'referee',
            'amount' => $referee_amount,
            'reward_calculation' => $settings['referee_reward_type'],
            'reward_calculation' => $this->settings['referee_reward_type'],
            'status' => 'available',
            'created_at' => current_time('mysql')
         ],
@@ -465,40 +584,62 @@
    */
   public function sendDailyReport(): void
   {
      // Get referrals from the last 24 hours
      $referrals = $this->wpdb->get_results(
         "SELECT r.*, u.display_name as referrer_name, u.user_email as referrer_email
             FROM {$this->referrals_table} r
             LEFT JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
             WHERE r.referred_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)
             ORDER BY r.referred_at DESC"
      );
      $yesterday = date('Y-m-d', strtotime('-1 day'));
      if (empty($referrals)) {
         return;  // No referrals, no email
      // Get new referrals from yesterday
      $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
      ));
      // Only send if there's at least 1 new referral
      if (empty($new_referrals)) {
         return;
      }
      // Generate CSV
      $csv_content = $this->generateCSV($referrals);
      $csv_filename = 'referrals-' . date('Y-m-d') . '.csv';
      // Build email content
      $content = '<h2>Daily Referral Report</h2>';
      $content .= '<p><strong>' . count($new_referrals) . '</strong> new referral' .
         (count($new_referrals) !== 1 ? 's' : '') . ' yesterday (' . $yesterday . ')</p>';
      // Save CSV temporarily
      $upload_dir = wp_upload_dir();
      $csv_path = $upload_dir['basedir'] . '/' . $csv_filename;
      file_put_contents($csv_path, $csv_content);
      $content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">';
      $content .= '<thead><tr style="background: #f5f5f5;">';
      $content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Referee</th>';
      $content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Email</th>';
      $content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Referrer</th>';
      $content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Code</th>';
      $content .= '</tr></thead><tbody>';
      // Send email with attachment
      foreach ($new_referrals as $ref) {
         $content .= '<tr>';
         $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
            esc_html($ref->referee_name));
         $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
            esc_html($ref->referee_email));
         $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
            esc_html($ref->referrer_name));
         $content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
            esc_html($ref->referral_code));
         $content .= '</tr>';
      }
      $content .= '</tbody></table>';
      // Get admin email
      $to = get_option('admin_email');
      $subject = '[' . get_bloginfo('name') . '] Daily Referral Report - ' . date('F j, Y');
      $subject = sprintf('[%s] %d New Referral%s',
         get_bloginfo('name'),
         count($new_referrals),
         count($new_referrals) !== 1 ? 's' : '');
      $message = $this->generateReportEmail($referrals, 'daily');
      $attachments = [$csv_path];
      wp_mail($to, $subject, $message, ['Content-Type: text/html; charset=UTF-8'], $attachments);
      // Clean up temporary file
      unlink($csv_path);
      jvbMail($to, $subject, $content);
   }
   /**
@@ -644,35 +785,13 @@
    *
    * @return array
    */
   protected function getRewardSettings(): array
   public function getRewardSettings(): array
   {
      $saved = get_option(BASE . 'referral_settings', []);
      return wp_parse_args($saved, $this->default_settings);
   }
   /**
    * Session/Cookie handling for referral codes
    */
   protected function getReferralCodeFromSession(): ?string
   {
      if (session_status() === PHP_SESSION_NONE) {
         session_start();
      }
      return $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? null;
   }
   protected function clearReferralSession(): void
   {
      if (session_status() === PHP_SESSION_NONE) {
         session_start();
      }
      unset($_SESSION[BASE . 'referral_code']);
      setcookie(BASE . 'referral_code', '', time() - 3600, '/');
   }
   /**
    * Display referral info in user profile
    *
    * @param WP_User $user
@@ -834,28 +953,6 @@
      update_user_meta($user_id, BASE . 'referral_code', strtoupper($code));
   }
   public function trackReferralCode(): void
   {
      if (!isset($_GET['ref'])) {
         return;
      }
      $referral_code = strtoupper(sanitize_text_field($_GET['ref']));
      // Start session if not already started
      if (session_status() === PHP_SESSION_NONE) {
         session_start();
      }
      // Store in both session and cookie (30 day expiry)
      $_SESSION[BASE . 'referral_code'] = $referral_code;
      setcookie(BASE . 'referral_code', $referral_code, time() + (30 * DAY_IN_SECONDS), '/');
      // Optional: Redirect to clean URL (removes ?ref= from address bar)
      $clean_url = remove_query_arg('ref');
      wp_safe_redirect($clean_url);
      exit;
   }
   /**
    * Display user's referral code and share options
@@ -863,141 +960,233 @@
    *
    * @return string HTML output
    */
   public function outputShareWidget(): string
   public function outputShareWidget(array $actions):array
   {
      $user_id = get_current_user_id();
      $user_id = get_current_user_id();
      $content = '<aside class="jvb-referral right">';
      if (!$user_id) {
         $content .= $this->getUnloggedInReferral();
      } else {
         $content .= $this->getLoggedInReferral($user_id);
      }
      $content .= '</aside>';
      $actions[] =[
         'button' => '<button type="button" class="attn toggle-referral row" title="Your Referrals" data-action="toggle-referral" aria-label="Open Referral Sidebar" aria-controls="referral" aria-expanded="false">
               '.jvbIcon('hand-heart').'<span class="screen-reader-text"></span>
            </button>',
         'content'   => $content
      ];
      return $actions;
   }
   function getUnloggedInReferral(): string
   {
      ob_start();
      JVB()->connect('cloudflare')->renderTurnstile();
      $turnstile = ob_get_clean();
      $meta = new MetaForm();
      $codeForm = '<form id="referral-code-form">
               '.jvbFormStatus().$meta->return('referral_name', null, [
                  'required'  => true,
                  'type'      => 'text',
                  'label'     => 'Your Name',
                  'placeholder'=> 'Mister Meeseeks',
                  'autocomplete'=>'name'
               ]).
               $meta->return('referral_email', null, [
                  'required'  => true,
                  'type'      => 'email',
                  'label'     => 'Your Email',
                  'placeholder'=> 'look@me.com',
                  'autocomplete'=> 'email'
               ]).
               $meta->return('referral_code', null, [
                  'required'  => true,
                  'type'      => 'text',
                  'label'     => 'Referral Code',
                  'pattern'   => '[A-Za-z0-9]+',
                  'maxLength' => 20,
                  'autocomplete'=>'off'
               ]).'
               <button type="submit">
                  Get Started
               </button>
               <p class="helper-text">
                  We\'ll send you a link to complete your registration.
               </p>
               '.$turnstile.'
            </form><div class="success-content" hidden>
               <h3>Check Your Email!</h3>
               <p>We\'ve sent you a magic link to complete your registration. Click the link to activate your account and claim your reward!</p>
               <p class="hint">Can\'t find it? Check your spam folder.</p>
            </div>';
      $loginForm = '<form id ="login-form">
      '.jvbFormStatus().$meta->return('login_email', null, [
            'required'  => true,
            'type'      => 'email',
            'label'     => 'Your Email',
            'autocomplete'=>'email'
         ]).'
      '.$turnstile.'
      <button type="submit">Login With Magic Link</button>
</form>
   <div class="success-content" hidden>
      <h3>Check Your Email!</h3>
      <p>We\'ve sent you a magic link to log in - no password required! Click the link in your email to log in.</p>
      <p class="hint">Can\'t find it? Check your spam folder.</p>
   </div>';
      $tabs = [
         'enterCode' => [
            'title'  => 'Have a Code?',
            'description'  => [
               'Enter the code given to you to get 20% off your first treatment!'
            ],
            'content'   => $codeForm
         ],
         'login'  => [
            'title'     => 'Login',
            'description'  => [
               'Login to see your rewards'
            ],
            'content'   => $loginForm
         ]
      ];
      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();
      ?>
      <div class="success-icon">
         ✓
      </div>
      <div class="success-content">
         <h3>Success! Your Reward is Ready!</h3>
         <div class="reward-highlight">
            <p style="margin: 0 0 8px 0;">You'll receive:</p>
            <p style="margin: 0;"><strong><?php echo esc_html($reward_text); ?> your first treatment!</strong></p>
         </div>
         <p>Your referral code <strong><?php echo esc_html($code); ?></strong> has been applied. Book your free consultation now to claim your reward!</p>
         <a href="<?php echo esc_url($booking_url); ?>" class="cta-button">
            Book Your Free Consultation
         </a>
         <div class="referred-by">
            Referred by <strong><?php echo esc_html($referrer->display_name); ?></strong>
         </div>
      </div>
      <?php
      return ob_get_clean();
   }
   public function getLoggedInReferral(int $user_id):string
   {
      // Logged-in user widget
      $referral_code = get_user_meta($user_id, BASE . 'referral_code', true);
      // Generate code if user doesn't have one
      if (empty($referral_code)) {
         $manager = new \JVBase\managers\ReferralManager();
         $referral_code = $manager->getUserReferralCode($user_id);
         $referral_code = $this->getUserReferralCode($user_id);
         if (is_wp_error($referral_code)) {
            return '';
         }
      }
      $share_url = home_url('/?ref=' . $referral_code);
      $encoded_url = urlencode($share_url);
      $site_name = get_bloginfo('name');
      $share_url = $this->getShareURL($referral_code);
      ob_start();
      ?>
      <div class="jvb-referral-widget" style="background: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0;">
         <h3 style="margin-top: 0;">Share & Earn Rewards</h3>
         <p>Share your unique referral code with friends and earn rewards when they book!</p>
         <header>
            <h3>Share the ♡</h3>
            <p>Invite your friends.</p>
            <p>Earn rewards when they book!</p>
         </header>
         <div class="referral-code-display" style="background: white; padding: 15px; border-radius: 4px; margin: 15px 0; text-align: center;">
            <label style="display: block; font-size: 12px; color: #666; margin-bottom: 5px;">Your Referral Code</label>
            <div style="font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #2271b1;">
               <?php echo esc_html($referral_code); ?>
            </div>
         </div>
         <div class="referral-url" style="margin: 15px 0;">
            <label style="display: block; font-size: 12px; color: #666; margin-bottom: 5px;">Share Link</label>
            <div style="display: flex; gap: 10px;">
               <input type="text"
                     readonly
                     value="<?php echo esc_url($share_url); ?>"
                     id="referral-url-<?php echo $user_id; ?>"
                     style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace; font-size: 14px;">
               <button type="button"
                     onclick="jvbCopyReferralUrl('referral-url-<?php echo $user_id; ?>')"
                     style="padding: 8px 16px; background: #2271b1; color: white; border: none; border-radius: 4px; cursor: pointer;">
                  Copy
               </button>
            </div>
         </div>
         <div class="referral-share-buttons" style="display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap;">
            <a href="mailto:?subject=Check out <?php echo esc_attr($site_name); ?>&body=I thought you might like <?php echo esc_url($share_url); ?>"
               class="share-button"
               style="padding: 10px 20px; background: #666; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
               📧 Email
            </a>
            <a href="sms:?&body=Check out <?php echo esc_attr($site_name); ?>: <?php echo esc_url($share_url); ?>"
               class="share-button"
               style="padding: 10px 20px; background: #25D366; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
               💬 Text
            </a>
            <a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo $encoded_url; ?>"
               target="_blank"
               class="share-button"
               style="padding: 10px 20px; background: #1877f2; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
               f Facebook
            </a>
            <a href="https://twitter.com/intent/tweet?url=<?php echo $encoded_url; ?>&text=Check out <?php echo esc_attr($site_name); ?>"
               target="_blank"
               class="share-button"
               style="padding: 10px 20px; background: #1da1f2; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
               𝕏 Twitter
            </a>
         </div>
         <div id="referral-stats-<?php echo $user_id; ?>" class="referral-stats" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
            <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 15px; text-align: center;">
               <div>
                  <div style="font-size: 24px; font-weight: bold; color: #2271b1;" data-stat="total">-</div>
                  <div style="font-size: 12px; color: #666;">Total Referrals</div>
               </div>
               <div>
                  <div style="font-size: 24px; font-weight: bold; color: #00a32a;" data-stat="treated">-</div>
                  <div style="font-size: 12px; color: #666;">Completed</div>
               </div>
               <div>
                  <div style="font-size: 24px; font-weight: bold; color: #dba617;" data-stat="pending">-</div>
                  <div style="font-size: 12px; color: #666;">Pending</div>
               </div>
               <div>
                  <div style="font-size: 24px; font-weight: bold; color: #2271b1;" data-stat="rewards">$0</div>
                  <div style="font-size: 12px; color: #666;">Earned</div>
               </div>
            </div>
         </div>
      <div class="row even share-buttons">
         <a href="mailto:?subject=<?php echo urlencode('Check out ' . get_bloginfo('name')); ?>&body=<?php echo urlencode('I thought you might be interested: ' . $share_url); ?>"
            class="share-btn email-share">
            <?php echo jvbIcon('envelope', ['size' => 20]); ?>
            Email
         </a>
         <a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode($share_url); ?>"
            target="_blank"
            rel="noopener noreferrer"
            class="share-btn facebook-share">
            <?php echo jvbIcon('facebook-logo', ['size' => 20]); ?>
            Facebook
         </a>
         <a href="https://twitter.com/intent/tweet?url=<?php echo urlencode($share_url); ?>&text=<?php echo urlencode('Check this out!'); ?>"
            target="_blank"
            rel="noopener noreferrer"
            class="share-btn twitter-share">
            <?php echo jvbIcon('twitter-logo', ['size' => 20]); ?>
            Twitter
         </a>
      </div>
      <script>
         function jvbCopyReferralUrl(elementId) {
            const input = document.getElementById(elementId);
            input.select();
            document.execCommand('copy');
         <h4>Your Referral Link</h4>
         <div class="row btw">
            <code id="your-referral-link"><?= esc_url($share_url)?></code>
            <button type="button" class="copy" data-target="your-referral-link">
               Copy Link
            </button>
         </div>
            // Visual feedback
            const button = input.nextElementSibling;
            const originalText = button.textContent;
            button.textContent = 'Copied!';
            button.style.background = '#00a32a';
            setTimeout(() => {
               button.textContent = originalText;
               button.style.background = '#2271b1';
            }, 2000);
         }
         <h4>Your Code</h4>
         <div class="row btw">
            <code id="your-referral-code"><?=esc_html($referral_code)?></code>
            <button type="button" class="copy" data-target="your-referral-code">
               Copy Code
            </button>
         </div>
         // Load stats via AJAX
         (function() {
            fetch('<?php echo rest_url(BASE . '/v1/referrals/stats'); ?>', {
               headers: {
                  'X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'
               }
            })
               .then(response => response.json())
               .then(data => {
                  if (data.success && data.stats) {
                     const container = document.getElementById('referral-stats-<?php echo $user_id; ?>');
                     container.querySelector('[data-stat="total"]').textContent = data.stats.total_referrals || 0;
                     container.querySelector('[data-stat="treated"]').textContent = data.stats.treated_count || 0;
                     container.querySelector('[data-stat="pending"]').textContent = data.stats.pending_count || 0;
                     container.querySelector('[data-stat="rewards"]').textContent =
                        '$' + parseFloat(data.stats.available_rewards || 0).toFixed(2);
                  }
               })
               .catch(error => console.error('Error loading referral stats:', error));
         })();
      </script>
         <div class="row btw referral-stats">
            <div class="stat-item">
               <span class="stat-value" data-stat="total">-</span>
               <span class="stat-label">Total Referrals</span>
            </div>
            <div class="stat-item">
               <span class="stat-value" data-stat="treated">-</span>
               <span class="stat-label">Successful</span>
            </div>
            <div class="stat-item">
               <span class="stat-value" data-stat="pending">-</span>
               <span class="stat-label">Pending</span>
            </div>
            <div class="stat-item">
               <span class="stat-value" data-stat="rewards">$0.00</span>
               <span class="stat-label">Available Rewards</span>
            </div>
         </div>
      <?php
      return ob_get_clean();
   }
@@ -1010,27 +1199,702 @@
         return $content;
      }
      $settings = get_option(BASE . 'referral_settings', []);
      $reward_amount = $settings['referee_reward_amount'] ?? 20;
      $reward_type = $settings['referee_reward_type'] ?? 'percentage';
      $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 = '<div style="background: #e7f5ff; padding: 20px; border-radius: 8px; margin: 20px 0;">';
      $bonus_content .= '<h3 style="margin-top: 0; color: #2271b1;">🎉 Welcome Bonus!</h3>';
      $bonus_content .= '<p>Since you were referred by a friend, you\'ve earned <strong>' . $reward_text . '</strong> your first booking!</p>';
      $bonus_content .= '<p>Your reward will be automatically applied when you book.</p>';
      $bonus_content .= '</div>';
      $bonus_content = sprintf(
         '
         <h3>Thanks for the ♡</h3>
         <p>Since you were referred by a friend, you\'ve earned <strong>%s</strong> your first booking!</p>
         <p>Your reward will be automatically applied when you book.</p>',
         $reward_text,
      );
      // Insert bonus content after the first paragraph
      $parts = explode('</p>', $content, 2);
      if (count($parts) === 2) {
         return $parts[0] . '</p>' . $bonus_content . $parts[1];
      $code = $this->getUserReferralCode($user->ID);
      $yourCode = '';
      if (!is_wp_error($code)) {
         $share_url = $this->getShareURL($code);
         $yourCode = sprintf(
            '<div class="callout">
         <h3>Share the ♡ with Friends</h3>
         <p>If you find you love what we can do for you, you can share your own code!</p>
         <p>Your Referral Code: <strong>%s</strong></p>
         <p>Or click the button below:</p>
         %s
         </div>',
            jvbEmailLink($code),
            jvbMailButton($share_url, 'Share Your Code')
         );
      }
      return $content . $bonus_content;
      return $content . $bonus_content . $yourCode;
   }
   public function getShareURL(string $code):string
   {
      return add_query_arg(
         [
            'ref' => $code,
            'action' => 'register'
         ],
         wp_login_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
    * @return array|WP_Error Result with success/error
    */
   public function sendReferralInvitation(int $user_id, string $invitee_email, string $invitee_name):array|WP_Error
   {
      // Verify user exists
      if (!$this->checkUser($user_id)) {
         return new WP_Error('invalid_user', 'Invalid user ID');
      }
      // 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 this email has already been invited or registered
      if ($this->isEmailInvited($invitee_email)) {
         return new WP_Error('already_invited', 'This person has already been invited');
      }
      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 (is_wp_error($referral_code)) {
         return $referral_code;
      }
      // Get reward text for email
      $reward_text = $this->settings['referee_reward_type'] === 'percentage'
         ? "Get {$this->settings['referee_reward_amount']}% off your first treatment!"
         : "Get \${$this->settings['referee_reward_amount']} off your first treatment!";
      // Record the invitation attempt (for tracking)
      $this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name);
      // Send magic link via MagicLinkManager
      $result = $this->magic_link->sendMagicLink(
         $invitee_email,
         MagicLinkManager::TYPE_REFERRAL,
         [
            'name' => sanitize_text_field($invitee_name),
            'referral_code' => $referral_code,
            'referrer_id' => $user_id,
            'referrer_name' => $referrer->display_name,
            'reward_text' => $reward_text
         ]
      );
      if (is_wp_error($result)) {
         return $result;
      }
      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): 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);
         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']),
         'results' => $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
    *
    * @param array $subpages
    * @return array
    */
   public function addSubpage(array $subpages): array
   {
      $subpages[] = [
         'page_title' => 'Referral Settings',
         'menu_title' => 'Referrals',
         'capability' => 'manage_options',
         'menu_slug'  => 'jvb-referrals',
         'callback'   => [$this, 'renderAdminPage'],
         'icon'       => 'users',
      ];
      return $subpages;
   }
   /**
    * 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
   {
      // Handle form submission
      if (isset($_POST['submit']) && check_admin_referer(BASE . 'referral_settings_nonce')) {
         update_option(BASE . 'referral_page_id', absint($_POST[BASE . 'referral_page_id'] ?? 0));
         $reward_settings = [
            'referrer_reward_applies_to' => sanitize_text_field($_POST['referrer_reward_applies_to'] ?? 'per_user'),
            'referrer_reward_amount' => floatval($_POST['referrer_reward_amount'] ?? 25.00),
            'referrer_reward_type' => sanitize_text_field($_POST['referrer_reward_type'] ?? 'fixed'),
            'referee_reward_type' => sanitize_text_field($_POST['referee_reward_type'] ?? 'percentage'),
            'referee_reward_amount' => floatval($_POST['referee_reward_amount'] ?? 20),
            'referee_reward_applies_to' => sanitize_text_field($_POST['referee_reward_applies_to'] ?? 'first_order'),
         ];
         update_option(BASE . 'referral_reward_settings', $this->sanitizeRewardSettings($reward_settings));
         echo '<div class="notice notice-success is-dismissible"><p>Settings saved successfully.</p></div>';
      }
      echo $this->renderAdminHTML();
   }
   protected function renderAdminHTML():string
   {
      ob_start();
      ?>
      <div class="wrap">
         <h1>Referral Settings</h1>
         <form method="post" action="">
            <?php wp_nonce_field(BASE . 'referral_settings_nonce'); ?>
            <div class="card">
               <h2>Referral Page</h2>
               <p>Select the page where users can access their referral dashboard.</p>
               <table class="form-table">
                  <tr>
                     <th scope="row">
                        <label for="<?= BASE ?>referral_page_id">Referral Page</label>
                     </th>
                     <td>
                        <?php
                        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'
                        ]);
                        ?>
                        <p class="description">
                           This page will show "Referral Page" in the admin bar when editing.
                        </p>
                     </td>
                  </tr>
               </table>
            </div>
            <div class="card">
               <h2>Reward Settings</h2>
               <table class="form-table">
                  <tr>
                     <th colspan="2"><h3>Referrer Rewards</h3></th>
                  </tr>
                  <tr>
                     <th scope="row">
                        <label for="referrer_reward_type">Reward Type</label>
                     </th>
                     <td>
                        <select name="referrer_reward_type" id="referrer_reward_type">
                           <option value="fixed" <?php selected($this->settings['referrer_reward_type'], 'fixed'); ?>>Fixed Amount</option>
                           <option value="percentage" <?php selected($this->settings['referrer_reward_type'], 'percentage'); ?>>Percentage</option>
                        </select>
                     </td>
                  </tr>
                  <tr>
                     <th scope="row">
                        <label for="referrer_reward_amount">Reward Amount</label>
                     </th>
                     <td>
                        <input type="number"
                              name="referrer_reward_amount"
                              id="referrer_reward_amount"
                              value="<?= esc_attr($this->settings['referrer_reward_amount']) ?>"
                              step="0.01"
                              min="0">
                        <p class="description">Amount in dollars or percentage</p>
                     </td>
                  </tr>
                  <tr>
                     <th scope="row">
                        <label for="referrer_reward_applies_to">Applies To</label>
                     </th>
                     <td>
                        <select name="referrer_reward_applies_to" id="referrer_reward_applies_to">
                           <option value="per_user" <?php selected($this->settings['referrer_reward_applies_to'], 'per_user'); ?>>Per User Referred</option>
                           <option value="flat_total" <?php selected($this->settings['referrer_reward_applies_to'], 'flat_total'); ?>>Flat Total</option>
                        </select>
                     </td>
                  </tr>
                  <tr>
                     <th colspan="2"><h3>Referee (New User) Rewards</h3></th>
                  </tr>
                  <tr>
                     <th scope="row">
                        <label for="referee_reward_type">Reward Type</label>
                     </th>
                     <td>
                        <select name="referee_reward_type" id="referee_reward_type">
                           <option value="percentage" <?php selected($this->settings['referee_reward_type'], 'percentage'); ?>>Percentage</option>
                           <option value="fixed" <?php selected($this->settings['referee_reward_type'], 'fixed'); ?>>Fixed Amount</option>
                        </select>
                     </td>
                  </tr>
                  <tr>
                     <th scope="row">
                        <label for="referee_reward_amount">Reward Amount</label>
                     </th>
                     <td>
                        <input type="number"
                              name="referee_reward_amount"
                              id="referee_reward_amount"
                              value="<?= esc_attr($this->settings['referee_reward_amount']) ?>"
                              step="0.01"
                              min="0">
                        <p class="description">Amount in dollars or percentage</p>
                     </td>
                  </tr>
                  <tr>
                     <th scope="row">
                        <label for="referee_reward_applies_to">Applies To</label>
                     </th>
                     <td>
                        <select name="referee_reward_applies_to" id="referee_reward_applies_to">
                           <option value="first_order" <?php selected($this->settings['referee_reward_applies_to'], 'first_order'); ?>>First Order Only</option>
                           <option value="all_orders" <?php selected($this->settings['referee_reward_applies_to'], 'all_orders'); ?>>All Orders</option>
                        </select>
                     </td>
                  </tr>
               </table>
            </div>
            <p class="submit">
               <button type="submit" name="submit" class="button button-primary">Save Settings</button>
            </p>
         </form>
         <?= $this->renderReferralStats(true) ?>
      </div>
      <?php
      return ob_get_clean();
   }
   /**
    * Render referral statistics
    */
   protected function renderReferralStats(bool $wrapCard = false):string
   {
      ob_start();
      global $wpdb;
      $total_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table}");
      $pending_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table} WHERE status = 'pending'");
      $treated_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table} WHERE status = 'treated'");
      ?>
      <table class="widefat">
         <tr>
            <th>Total Referrals</th>
            <td><?= esc_html($total_referrals) ?></td>
         </tr>
         <tr>
            <th>Pending</th>
            <td><?= esc_html($pending_referrals) ?></td>
         </tr>
         <tr>
            <th>Treated</th>
            <td><?= esc_html($treated_referrals) ?></td>
         </tr>
      </table>
      <?php
      $table = ob_get_clean();
      if ($wrapCard) {
         $table = '<div class="card">
            <h2>Referral Statistics</h2>
            '.$table.'
         </div>';
      }
      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) {
         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 ($post->ID === $this->referralPage) {
         echo '<div class="notice notice-info">';
         echo '<p>' . __('This page is designated as the <strong>Referral Page</strong>.', 'jvbase') . '</p>';
         echo '</div>';
      }
   }
   public function renderDashPage(string $content, string $page):string
   {
      if ($page !== 'referrals') {
         return $content;
      }
      $out = '';
      if (current_user_can('manage_options')) {
         $out .= $this->renderAdminHTML();
      } else {
         $out .= $this->renderReferralStats(true);
      }
      return ($out === '') ? $content : '<form id="referrals" class="col" data-save="referrals">'.$out.'</form>';
   }
}