If you find you love what we can do for you, you can share your own code!
Your Referral Code: %s
Or click the button below:
%s
',
jvbEmailLink($code),
jvbMailButton($share_url, 'Share Your Code')
);
}
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 static function addSubpage():void
{
$subpage = [
'page_title' => 'Referral System',
'menu_title' => 'Referrals',
'capability' => 'manage_options',
'menu_slug' => BASE . 'referral-admin',
'callback' => [self::class, 'renderAdminPageStatic']
];
AdminPages::addSubPage(BASE.'referral-admin', $subpage);
}
/**
* Static wrapper for renderAdminPage
* Called by WordPress when admin page is rendered
*/
public static function renderAdminPageStatic(): void
{
// Get the properly initialized instance from JVB singleton
JVB()->referrals()->renderAdminPage();
}
/**
* Register settings
*/
public function registerSettings(): void
{
register_setting(
BASE . 'referral_settings',
BASE . 'referral_page_id',
[
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 0
]
);
register_setting(
BASE . 'referral_settings',
BASE . 'referral_reward_settings',
[
'type' => 'array',
'sanitize_callback' => [$this, 'sanitizeRewardSettings'],
'default' => $this->default_settings
]
);
}
/**
* Sanitize reward settings
*/
public function sanitizeRewardSettings(array $settings): array
{
return [
'referrer_reward_applies_to' => in_array($settings['referrer_reward_applies_to'] ?? '', ['per_user', 'flat_total'])
? $settings['referrer_reward_applies_to']
: 'per_user',
'referrer_reward_amount' => floatval($settings['referrer_reward_amount'] ?? 25.00),
'referrer_reward_type' => in_array($settings['referrer_reward_type'] ?? '', ['fixed', 'percentage'])
? $settings['referrer_reward_type']
: 'fixed',
'referee_reward_type' => in_array($settings['referee_reward_type'] ?? '', ['percentage', 'fixed'])
? $settings['referee_reward_type']
: 'percentage',
'referee_reward_amount' => floatval($settings['referee_reward_amount'] ?? 20),
'referee_reward_applies_to' => in_array($settings['referee_reward_applies_to'] ?? '', ['first_order', 'all_orders'])
? $settings['referee_reward_applies_to']
: 'first_order',
];
}
/**
* Render the admin settings page
*/
public function renderAdminPage(): void
{
?>
Referral System Management
Import Data from Jane App
Upload your exported CSV files from Jane App to sync client and sales data.
Client List
Sales Export
Referrals Management
Loading referrals...
= $this->renderAdminHTML() ?>
Referral Settings
= $this->renderReferralStats(true) ?>
prefix . BASE . 'referrals';
// Use proper WordPress prepare even for COUNT
$total_referrals = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM `{$referrals_table}` WHERE 1=%d",
1
)
);
$pending_referrals = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s",
'pending'
)
);
$treated_referrals = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s",
'treated'
)
);
?>
Total Referrals
= esc_html($total_referrals ?? 0) ?>
Pending
= esc_html($pending_referrals ?? 0) ?>
Treated
= esc_html($treated_referrals ?? 0) ?>
Referral Statistics
' . $table . '
';
}
return $table;
}
/**
* Add "Referral Page" label to admin bar
*
* @param WP_Admin_Bar $wp_admin_bar
*/
public function addReferralPageLabel($wp_admin_bar): void
{
if (!is_admin()) {
return;
}
if (!$this->referralPage) {
$this->referralPage = $this->getReferralPageId();
}
if (!$this->referralPage) {
return;
}
global $pagenow, $post;
// Check if we're editing the referral page
if ('post.php' === $pagenow && $post && $post->ID === $this->referralPage) {
$wp_admin_bar->add_node([
'id' => 'referral-page',
'parent' => 'top-secondary',
'title' => __('Referral Page', 'jvbase'),
'meta' => [
'class' => 'referral-page-notice'
]
]);
}
}
/**
* Get the referral page ID
*
* @return int|null
*/
public function getReferralPageId(): ?int
{
$page_id = get_option(BASE . 'referral_page_id');
return $page_id ? (int) $page_id : null;
}
/**
* Show admin notice on referral page edit screen
*/
public function showReferralPageNotice(): void
{
global $pagenow, $post;
if ('post.php' !== $pagenow || !$post) {
return;
}
if (!$this->referralPage) {
$this->referralPage = $this->getReferralPageId();
}
if ($post->ID === $this->referralPage) {
echo '
';
echo '
' . __('This page is designated as the Referral Page.', 'jvbase') . '
';
echo '
';
}
}
public function renderDashPage(string $content, string $page): string
{
if ($page !== 'Referrals') {
return $content;
}
// Regular users get their referral dashboard
$user_id = get_current_user_id();
$referral_code = get_user_meta($user_id, BASE . 'referral_code', true);
if (!$referral_code) {
$referral_code = $this->getUserReferralCode($user_id);
}
$stats = $this->getUserStats($user_id);
$referrals = $this->getUserReferrals($user_id, ['limit' => 20]);
ob_start();
?>
Your Referrals
Share your code and earn rewards when your referrals complete their first treatment!