From 3aada9949d51024a92a8b5c6cb70d12f9c3cac16 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 21 Dec 2025 19:59:48 +0000
Subject: [PATCH] =auth refactored via rest, referral system set up for Jane, some javascript consolidation
---
inc/managers/ReferralManager.php | 695 ++++++++++++++++++++++++++++++++++++++++-----------------
1 files changed, 486 insertions(+), 209 deletions(-)
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 3f000e4..2f7468a 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -4,6 +4,8 @@
use JVBase\managers\MagicLinkManager;
use JVBase\integrations\Cloudflare;
use JVBase\meta\MetaForm;
+use JVBase\ui\CRUDSkeleton;
+use JVBase\ui\Tabs;
use JVBase\utility\Features;
use WP_User;
use WP_Error;
@@ -34,9 +36,12 @@
'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'
+ '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()
@@ -46,7 +51,6 @@
$this->cache = CacheManager::for('referrals', WEEK_IN_SECONDS);
$this->referrals_table = $wpdb->prefix . BASE . 'referrals';
$this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards';
- $this->magic_link = new MagicLinkManager();
$this->referralPage = $this->getReferralPageId();
$this->settings = $this->getRewardSettings();
@@ -88,6 +92,14 @@
add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3);
}
+ 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)) {
@@ -126,11 +138,16 @@
'jvb-a11y',
'jvb-popup',
'jvb-tabs',
+ 'jvb-data-store',
];
if (Features::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) {
$requirements[] = 'cloudflare-turnstile';
}
+ if (is_singular(BASE.'dash')) {
+ $requirements[] = 'jvb-form';
+ $requirements[] = 'jvb-view';
+ }
wp_enqueue_script(
'jvb-referral',
JVB_URL . 'assets/js/min/referral.min.js',
@@ -267,16 +284,15 @@
* Track a new referral when user registers
*
* @param int $user_id
+ * @return bool;
*/
- public function processReferral(int $user_id, string $email, array $data): void
+ public function processReferral(int $user_id): bool
{
- // 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);
+ // Try to get code from user meta first (set during registration)
+ $referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true);
- // Check session/cookie if not in data
if (empty($referral_code)) {
+ // Check session/cookie if not in meta
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
@@ -284,34 +300,63 @@
}
if (empty($referral_code)) {
- return;
+ return false; // No referral code - regular registration
}
// Find the referrer
$referrer = $this->getUserByReferralCode($referral_code);
-
if (!$referrer) {
delete_user_meta($user_id, BASE . 'pending_referral_code');
- return;
+ return false;
}
- // Check for duplicates
- $existing = $this->getReferralByReferee($user_id);
- if ($existing) {
- delete_user_meta($user_id, BASE . 'pending_referral_code');
- return;
+ $user = get_userdata($user_id);
+
+ // Check if referral already exists for this user
+ $existing = $this->wpdb->get_row($this->wpdb->prepare(
+ "SELECT * FROM {$this->referrals_table}
+ WHERE referrer_id = %d AND (referee_email = %s OR referee_id = %d)",
+ $referrer->ID,
+ $user->user_email,
+ $user_id
+ ));
+
+ if (!$existing) {
+ // Create new referral record - referred_at captures registration time
+ $this->wpdb->insert(
+ $this->referrals_table,
+ [
+ 'referrer_id' => $referrer->ID,
+ 'referee_id' => $user_id,
+ 'referee_name' => $user->display_name,
+ 'referee_email' => $user->user_email,
+ 'referee_phone' => get_user_meta($user_id, BASE . 'phone', true) ?: '',
+ 'referral_code' => $referral_code,
+ 'status' => 'pending', // pending first treatment
+ 'referred_at' => current_time('mysql') // When they registered
+ ],
+ ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s']
+ );
}
- // Create referral record
- $result = $this->createReferral($referrer->ID, $user_id, $referral_code);
-
- 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);
+ // 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->clear();
+
+ // Fire action for tracking
+ do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code);
+
+ // Send notification to referrer
+ $this->sendReferrerNotification($referrer->ID, $user->display_name);
+ return true;
}
/**
@@ -468,28 +513,52 @@
*/
public function getUserReferrals(int $user_id, array $args = []): array
{
- $defaults = [
- 'status' => 'all',
- 'limit' => 100,
- 'offset' => 0,
- 'orderby' => 'referred_at',
- 'order' => 'DESC'
- ];
+ return $this->cache->remember(
+ $user_id,
+ function() use ($user_id, $args) {
+ $defaults = [
+ 'status' => 'all',
+ 'limit' => 100,
+ 'offset' => 0,
+ 'orderby' => 'referred_at',
+ 'order' => 'DESC'
+ ];
- $args = wp_parse_args($args, $defaults);
+ $args = wp_parse_args($args, $defaults);
- $where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id);
+ $where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id);
- if ($args['status'] !== 'all') {
- $where .= $this->wpdb->prepare(" AND status = %s", $args['status']);
- }
+ if ($args['status'] !== 'all') {
+ $where .= $this->wpdb->prepare(" AND status = %s", $args['status']);
+ }
- $query = "SELECT * FROM {$this->referrals_table}
+ $query = "SELECT * FROM {$this->referrals_table}
{$where}
ORDER BY {$args['orderby']} {$args['order']}
LIMIT {$args['limit']} OFFSET {$args['offset']}";
- return $this->wpdb->get_results($query);
+ $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);
+ }
+ );
+
}
/**
@@ -509,26 +578,25 @@
$stats = $this->wpdb->get_row($this->wpdb->prepare(
"SELECT
- COUNT(*) as total_referrals,
- SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count,
- SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count
- FROM {$this->referrals_table}
- WHERE referrer_id = %d",
+ 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
- $rewards = $this->wpdb->get_row($this->wpdb->prepare(
- "SELECT
- SUM(CASE WHEN status = 'available' THEN amount ELSE 0 END) as available_rewards,
- SUM(CASE WHEN status = 'redeemed' THEN amount ELSE 0 END) as redeemed_rewards
- FROM {$this->rewards_table}
- WHERE user_id = %d AND reward_type = 'referrer'",
+ // 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
- ), ARRAY_A);
+ ));
- $stats = array_merge($stats, $rewards);
-
+ $stats['total_rewards'] = floatval($rewards ?? 0);
+ $stats['user_id'] = $user_id;
$this->cache->set($cache_key, $stats, HOUR_IN_SECONDS);
return $stats;
@@ -638,7 +706,7 @@
count($new_referrals) !== 1 ? 's' : '');
- jvbMail($to, $subject, $content);
+ JVB()->email()->sendEmail($to, $subject, $content);
}
/**
@@ -661,7 +729,7 @@
$message = $this->generateWeeklyReportEmail($top_referrers, $total_referrals);
- wp_mail($to, $subject, $message, ['Content-Type: text/html; charset=UTF-8']);
+ JVB()->email()->sendEmail($to, $subject, $message);
}
/**
@@ -883,6 +951,7 @@
</table>
<?php endif; ?>
+ <?php /**
<script>
function markReferralTreated(referralId) {
if (!confirm('Mark this referral as treated? This will create reward records.')) {
@@ -907,6 +976,7 @@
}
</script>
<?php
+ */
}
/**
@@ -963,7 +1033,7 @@
{
$user_id = get_current_user_id();
- $content = '<aside class="jvb-referral right">';
+ $content = '<aside class="main referral right">';
if (!$user_id) {
$content .= $this->getUnloggedInReferral();
} else {
@@ -1007,7 +1077,9 @@
' . ($referrer_name ? '<p>' . esc_html($referrer_name) . ' invited you to join us</p>' : '') . '
</div>
<form id="referral-code-form">
- '.jvbFormStatus().$meta->return('referral_name', null, [
+ '.jvbFormStatus(). '
+ <input type="hidden" name="user_select" value="' . esc_attr(get_option(BASE.'referral_role','client')) . '">
+ ' .$meta->return('referral_name', null, [
'required' => true,
'type' => 'text',
'label' => 'Your Name',
@@ -1158,20 +1230,24 @@
<div class="copy-section">
<h4>Your Referral Link</h4>
- <div class="copy-group">
+ <div class="copy-group row btw nowrap">
<code id="referral-link" class="copy-target"><?= esc_url($share_url) ?></code>
<button type="button" class="copy-btn" data-target="referral-link" aria-label="Copy referral link">
<?php echo jvbIcon('copy', ['size' => 16]); ?>
</button>
</div>
+ <p class="hint">Quickest and easiest: autofills your code.</p>
+
<h4>Your Code</h4>
- <div class="copy-group">
+ <div class="copy-group row btw nowrap">
<code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code>
<button type="button" class="copy-btn" data-target="referral-code" aria-label="Copy referral code">
<?php echo jvbIcon('copy', ['size' => 16]); ?>
</button>
</div>
+ <p class="hint">Manually copy and paste the code</p>
+
</div>
<div class="recent-referrals-section">
@@ -1203,6 +1279,7 @@
<a href="<?= get_home_url(null, '/dash/referrals')?>" class="view-dashboard-btn">
Dashboard <?= jvbIcon('arrow-right', ['size' => 16]); ?>
</a>
+ <p class="hint">Bulk-invite your friends via email - the link will pre-fill their name, email, and code!</p>
<?php
return ob_get_clean();
@@ -1244,8 +1321,8 @@
<p>Or click the button below:</p>
%s
</div>',
- jvbEmailLink($code),
- jvbMailButton($share_url, 'Share Your Code')
+ JVB()->email()->link($code),
+ JVB()->email()->button($share_url, 'Share Your Code')
);
}
@@ -1257,10 +1334,9 @@
{
return add_query_arg(
[
- 'ref' => $code,
- 'action' => 'register'
+ 'ref' => $code
],
- wp_login_url()
+ get_home_url()
);
}
@@ -1270,15 +1346,12 @@
* @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):array|WP_Error
+ public function sendReferralInvitation(int $user_id, string $invitee_email, string $invitee_name, string $subject, string $message):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) {
@@ -1291,11 +1364,7 @@
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');
- }
-
+ // Check if already registered
if (email_exists($invitee_email)) {
return new WP_Error('user_exists', 'This person already has an account');
}
@@ -1308,29 +1377,56 @@
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)
+ // Record the invitation attempt (for rate limiting only)
$this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name);
- // Send magic link via MagicLinkManager
- $result = $this->magic_link->sendMagicLink(
+ // 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(
+ '<h2>%s invited you to %s!</h2>
+ <p>%s</p>
+ <div class="callout">
+ <h3>Get %s your first treatment!</h3>
+ </div>
+ <p>Click the button below to register and claim your reward:</p>
+ %s
+ <p><small>This invitation expires in 30 days.</small></p>',
+ 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,
- 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
- ]
+ $subject,
+ $email_content
);
- if (is_wp_error($result)) {
- return $result;
+ if (!$sent) {
+ return new WP_Error('email_failed', 'Failed to send invitation email');
}
return [
@@ -1348,7 +1444,7 @@
* @param array $invitations Array of ['email' => '', 'name' => '']
* @return array Results with success/failed arrays
*/
- public function sendBatchReferralInvitations(int $user_id, array $invitations): array
+ public function sendBatchReferralInvitations(int $user_id, array $invitations, string $subject, string $message): array
{
$results = [
'success' => [],
@@ -1368,7 +1464,7 @@
continue;
}
- $result = $this->sendReferralInvitation($user_id, $email, $name);
+ $result = $this->sendReferralInvitation($user_id, $email, $name, $subject, $message);
if (is_wp_error($result)) {
$results['failed'][] = [
@@ -1389,7 +1485,7 @@
return [
'success' => !empty($results['success']),
- 'results' => $results,
+ 'result' => $results,
'summary' => sprintf(
'Sent %d invitations, %d failed',
count($results['success']),
@@ -1576,6 +1672,7 @@
/**
* Add referral settings subpage to admin menu
+ * Add referral settings subpage to admin menu
*
* @param array $subpages
* @return array
@@ -1730,7 +1827,7 @@
<!-- Settings Section -->
<?= $this->renderAdminHTML() ?>
</div>
-
+<?php /**
<style>
.jvb-upload-box {
padding: 20px;
@@ -1785,11 +1882,12 @@
margin: 10px 0;
}
</style>
-
+*/
+ if (is_admin()) {
+?>
<script>
jQuery(document).ready(function($) {
// Client upload
- // Client upload
$('#client-upload-form').on('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
@@ -1897,12 +1995,12 @@
const search = $('#referral-search').val();
$.ajax({
- url: '<?= rest_url('jvb/v1/referrals/list') ?>',
+ url: '<?= rest_url('jvb/v1/referrals') ?>',
method: 'GET',
data: {
- page: page,
- per_page: 20,
- status: status,
+ offset: page -1,
+ limit: 20,
+ status: status === '' ? 'all' : status,
search: search
},
beforeSend: function(xhr) {
@@ -1930,10 +2028,10 @@
html += '<th>Actions</th>';
html += '</tr></thead><tbody>';
- if (data.referrals.length === 0) {
+ if (data.items.length === 0) {
html += '<tr><td colspan="7" style="text-align: center;">No referrals found</td></tr>';
} else {
- data.referrals.forEach(function(ref) {
+ data.items.forEach(function(ref) {
html += '<tr>';
html += '<td>' + (ref.referrer_name || 'Unknown') + '</td>';
html += '<td>' + (ref.referee_display_name || ref.referee_name) + '</td>';
@@ -1976,9 +2074,12 @@
if (!confirm('Mark this referral as consulted? This will create the consultation reward.')) return;
$.ajax({
- url: '<?= rest_url('jvb/v1/referrals/mark-consulted') ?>',
+ url: '<?= rest_url('jvb/v1/referrals') ?>', // Changed from /mark-consulted
method: 'POST',
- data: JSON.stringify({ referral_id: id }),
+ data: JSON.stringify({
+ action: 'consulted', // Added action parameter
+ referral_id: id
+ }),
contentType: 'application/json',
beforeSend: function(xhr) {
xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>');
@@ -1999,9 +2100,12 @@
if (!confirm('Mark this referral as treated? This will create rewards for both parties.')) return;
$.ajax({
- url: '<?= rest_url('jvb/v1/referrals/mark-treated') ?>',
+ url: '<?= rest_url('jvb/v1/referrals') ?>', // Changed from /mark-treated
method: 'POST',
- data: JSON.stringify({ referral_id: id }),
+ data: JSON.stringify({
+ action: 'treated', // Added action parameter
+ referral_id: id
+ }),
contentType: 'application/json',
beforeSend: function(xhr) {
xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>');
@@ -2039,6 +2143,7 @@
});
</script>
<?php
+ }
}
protected function renderAdminHTML():string
@@ -2166,14 +2271,14 @@
</tr>
<tr>
<th scope="row">
- <label for="<?= BASE ?>client_import_role">Client Import Role</label>
+ <label for="<?= BASE ?>referral_role">Client Import Role</label>
</th>
<td>
<?php
- $selected_role = get_option(BASE . 'client_import_role', '');
+ $selected_role = get_option(BASE . 'referral_role', '');
$roles = wp_roles()->get_names();
?>
- <select name="<?= BASE ?>client_import_role" id="<?= BASE ?>client_import_role">
+ <select name="<?= BASE ?>referral_role" id="<?= BASE ?>referral_role">
<?php foreach ($roles as $role_value => $role_name): ?>
<option value="<?= esc_attr($role_value) ?>" <?php selected($selected_role, $role_value); ?>>
<?= esc_html($role_name) ?>
@@ -2339,116 +2444,195 @@
$referral_code = $this->getUserReferralCode($user_id);
}
- $stats = $this->getUserStats($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));
+
?>
<div class="referral-dashboard">
- <div class="referral-header">
- <h2>Your Referrals</h2>
- <p>Share your code and earn rewards when your referrals complete their first treatment!</p>
- </div>
+ <?= $tabs->render(true);?>
+ </div>
- <?php $this->getShareButtons($user_id); ?>
+ <?php
+ return ob_get_clean();
+ }
- <!-- Referral Code Card -->
- <div class="referral-code-card">
- <h3>Your Referral Code</h3>
- <div class="code-display">
- <span class="code"><?= esc_html($referral_code) ?></span>
- <button class="button copy-code" data-code="<?= esc_attr($referral_code) ?>">
- Copy Code
- </button>
- </div>
- <p class="share-link">
- Share link: <input type="text" readonly value="<?= home_url('/?ref=' . $referral_code) ?>"
- onclick="this.select()" style="width: 100%; margin-top: 5px;" />
- </p>
+ protected function shareDashboard(int $user_id, string $referral_code):string
+ {
+ ob_start();
+ ?>
+ <?php $this->getShareButtons($user_id); ?>
+
+ <!-- Referral Code Card -->
+ <div class="card">
+ <h3>Share Code</h3>
+ <div class="row btw nowrap">
+ <code class="code"><?= esc_html($referral_code) ?></code>
+ <button class="button copy-btn" data-code="<?= esc_attr($referral_code) ?>">
+ Copy Code
+ </button>
</div>
- <form class="invite">
- <?php
- $meta = new MetaForm();
- $field = [
- 'type' => 'repeater',
- 'label' => 'Invite Your Friends',
- 'fields' => [
- 'name' => [
- 'type' => 'text',
- 'label' => 'name',
- ],
- 'email' => [
- 'type' => 'email',
- 'label' => 'email',
- ]
+ <h3>Share Link</h3>
+ <div class="row btw nowrap">
+ <code class="share-link">
+ <?= home_url('/?ref=' . $referral_code) ?>
+ </code>
+ <button class="button copy-btn" data-code="<?= home_url('/?ref=' . $referral_code) ?>">
+ Copy Link
+ </button>
+ </div>
+ </div>
+ <form class="invite">
+ <h2>Invite your Friends</h2>
+ <p>Or, if you prefer, enter your friends name(s) and email(s), and we'll send off some emails.</p>
+ <p><small>(No data is stored. Your friends will get an email from our email.)</small></p>
+ <?php
+ $meta = new MetaForm();
+ $invite = [
+ 'type' => 'tag_list',
+ '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
]
- ];
- $meta->render('invite', [], $field);
+ ]
+ ];
+ $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.'
+ ]
+ ];
+ $meta->render('invite', [], $invite);
+ ?>
+ <details>
+ <summary class="icon icon-caret-down">Customize Message</summary>
+ <?php
+ foreach ($fields as $fieldName => $field) {
+ $value = (array_key_exists('value', $field)) ? $field['value'] : [];
+ $meta->render($fieldName, $value, $field);
+ }
?>
- </form>
+ </details>
- <!-- Stats Grid -->
- <div class="stats-grid">
- <div class="stat-card">
- <h4>Total Referrals</h4>
- <span class="stat-number"><?= esc_html($stats['total_referrals'] ?? 0) ?></span>
- </div>
- <div class="stat-card">
- <h4>Completed Treatments</h4>
- <span class="stat-number"><?= esc_html($stats['treated_count'] ?? 0) ?></span>
- </div>
- <div class="stat-card">
- <h4>Pending</h4>
- <span class="stat-number"><?= esc_html($stats['pending_count'] ?? 0) ?></span>
- </div>
- <div class="stat-card highlight">
- <h4>Available Rewards</h4>
- <span class="stat-number">$<?= number_format($stats['available_rewards'] ?? 0, 2) ?></span>
- </div>
+ <button type="submit"><?=jvbIcon('envelope')?>Send Invites</button>
+ </form>
+ <?php
+ return ob_get_clean();
+ }
+
+ protected function referralCRUD(int $user_id):string
+ {
+ $stats = $this->getUserStats($user_id);
+ ob_start();
+ ?>
+ <!-- Stats Grid with Updated Labels -->
+ <div class="item-grid stats">
+ <div class="card">
+ <h4>Code Used</h4>
+ <span class="stat-number" data-stat="code_used"><?= esc_html($stats['code_used'] ?? 0) ?></span>
+ <p class="hint">People who used your code</p>
</div>
-
- <!-- Referrals List -->
- <div class="referrals-list-card">
- <h3>Your Referrals</h3>
- <?php if (empty($referrals)): ?>
- <p>You haven't referred anyone yet. Share your code to get started!</p>
- <?php else: ?>
- <table class="referrals-table">
- <thead>
- <tr>
- <th>Name</th>
- <th>Email</th>
- <th>Status</th>
- <th>Referred Date</th>
- </tr>
- </thead>
- <tbody>
- <?php foreach ($referrals as $ref): ?>
- <tr>
- <td><?= esc_html($ref->referee_name) ?></td>
- <td><?= esc_html($ref->referee_email) ?></td>
- <td><span class="status-badge <?= esc_attr($ref->status) ?>"><?= esc_html(ucfirst($ref->status)) ?></span></td>
- <td><?= date('M j, Y', strtotime($ref->referred_at)) ?></td>
- </tr>
- <?php endforeach; ?>
- </tbody>
- </table>
- <?php endif; ?>
+ <div class="card">
+ <h4>Treatments</h4>
+ <span class="stat-number" data-stat="treatments"><?= esc_html($stats['treatments'] ?? 0) ?></span>
+ <p class="hint">Completed first treatment</p>
+ </div>
+ <div class="card highlight">
+ <h4>Total Rewards</h4>
+ <span class="stat-number" data-stat="total_rewards">$<?= number_format($stats['total_rewards'] ?? 0, 2) ?></span>
+ <p class="hint">Earned from referrals</p>
</div>
</div>
-
- <script>
- jQuery(document).ready(function($) {
- $('.copy-code').on('click', function() {
- const code = $(this).data('code');
- navigator.clipboard.writeText(code).then(function() {
- alert('Code copied to clipboard!');
- });
- });
- });
- </script>
<?php
+ // Configure CRUDSkeleton for referrals
+ $crud = new CRUDSkeleton();
+ $crud->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', '
+ <template class="emptyState">
+ <div class="empty-state">
+ <h3>' . jvbDashIcon('hand-heart') . 'Nothing Yet' . jvbDashIcon('hand-heart') . '</h3>
+ <p>Start sharing your referral code to earn rewards!</p>
+ <p><small><i>Share your code using the "Share" tab below.</i></small></p>
+ </div>
+ </template>
+ ');
+
+ $crud->render();
+
return ob_get_clean();
}
@@ -2473,8 +2657,8 @@
update_option(BASE . 'referral_page_id', $page_id);
// Save client import role
- $import_role = sanitize_text_field($post_data[BASE . 'client_import_role'] ?? JVB_USER);
- update_option(BASE . 'client_import_role', $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 = [
@@ -2573,5 +2757,98 @@
</nav>
<?php
}
+
+ /**
+ * Send notification to referrer when someone registers
+ *
+ * @param int $referrer_id
+ * @param string $referee_name
+ */
+ protected function sendReferrerNotification(int $referrer_id, string $referee_name): void
+ {
+ $referrer = get_userdata($referrer_id);
+ if (!$referrer) {
+ return;
+ }
+
+ $subject = sprintf('%s signed up with your referral code!', $referee_name);
+ $message = sprintf(
+ "Great news! %s just signed up using your referral code.\n\n" .
+ "View your referrals: %s",
+ $referee_name,
+ home_url('/dash/referrals')
+ );
+
+ JVB()->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(false); // 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();
+ ?>
+ <div class="welcome-banner referral-welcome">
+ <div class="banner-content">
+ <h3><?= jvbIcon('confetti') ?>Welcome! <small><b><?= esc_html($referrer_first_name) ?></b> invited you to save <b><?= esc_html($reward_text) ?></b>!</small></h3>
+ <p>But we're not done yet! Here's what happens next:</p>
+ <div class="callout">
+ <ol>
+ <li>Book your <b>free consultation</b></li>
+ <li>Come in and we'll assess your tattoo</li>
+ <li>Get <?= esc_html($reward_text) ?> your first treatment!</li>
+ </ol>
+ </div>
+ <p class="hint">
+ <strong>Important:</strong> If you book with a different email than
+ <strong><?= esc_html(wp_get_current_user()->user_email) ?></strong>,
+ please let us know so we can apply your reward!
+ </p>
+ <ul class="buttons">
+ <li><a href="<?= esc_url($estimate_url) ?>" class="button-secondary">
+ <?= jvbIcon('calculator') ?> Get an Estimate First
+ </a></li>
+ <li><a href="<?= esc_url($booking_url) ?>" class="button-primary">
+ <?= jvbIcon('calendar') ?> Book Free Consult
+ </a></li>
+ </ul>
+
+
+ </div>
+ </div>
+ <?php
+ return ob_get_clean();
+ }
}
--
Gitblit v1.10.0