| | |
| | | <?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; |
| | | |
| | |
| | | class ReferralManager |
| | | { |
| | | protected $wpdb; |
| | | protected MagicLinkManager $magic_link; |
| | | protected CacheManager $cache; |
| | | protected string $referrals_table; |
| | | protected string $rewards_table; |
| | |
| | | $this->cache = new CacheManager('referrals'); |
| | | $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); |
| | |
| | | |
| | | add_filter(BASE.'new_user_email_content', [$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(); |
| | | } |
| | | |
| | | 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 |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Register cron jobs for automated reporting |
| | | */ |
| | |
| | | */ |
| | | public function processReferral(int $user_id): 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 |
| | | $referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true); |
| | | |
| | | if (!$referral_code) { |
| | | return; |
| | |
| | | $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); |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | * @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', |
| | |
| | | */ |
| | | 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); |
| | | } |
| | | |
| | | /** |
| | |
| | | * |
| | | * @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 |
| | |
| | | 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 |
| | |
| | | * |
| | | * @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="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 ''; |
| | | } |
| | | |
| | | $settings = $this->getRewardSettings(); |
| | | $reward_amount = $settings['referee_reward_amount'] ?? 20; |
| | | $reward_type = $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(); |
| | | } |
| | | |
| | | 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'); |
| | | |
| | | 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', ['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', ['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(); |
| | | } |
| | |
| | | |
| | | return $content . $bonus_content; |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | $settings = $this->getRewardSettings(); |
| | | $reward_text = $settings['referee_reward_type'] === 'percentage' |
| | | ? "Get {$settings['referee_reward_amount']}% off your first treatment!" |
| | | : "Get \${$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; |
| | | } |
| | | } |
| | | |