<?php /**
* Simple Points System for WordPress
* Replace myCRED with a lightweight, atomic points system
*
* Installation:
* 1. Add this code using code snippets plugin
* 2. Visit the WordPress admin to see the Points menu
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_Simple_Points {
private static $instance = null;
private $table_name;
private $hooks_option = 'wp_points_hooks';
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
global $wpdb;
$this->table_name = $wpdb->prefix . 'user_points_transactions';
// Core hooks
add_action('init', array($this, 'init'));
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('show_user_profile', array($this, 'show_user_points_field'));
add_action('edit_user_profile', array($this, 'show_user_points_field'));
add_action('personal_options_update', array($this, 'save_user_points_field'));
add_action('edit_user_profile_update', array($this, 'save_user_points_field'));
// Points earning hooks
add_action('user_register', array($this, 'points_on_register'));
add_action('wp_login', array($this, 'points_on_login'), 10, 2);
// AJAX handlers
add_action('wp_ajax_update_user_points', array($this, 'ajax_update_user_points'));
// Activation hook
register_activation_hook(__FILE__, array($this, 'create_tables'));
}
public function init() {
// Create tables if they don't exist
$this->create_tables();
// Register shortcode
add_shortcode('user_points', array($this, 'points_shortcode'));
}
public function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
amount decimal(10,2) NOT NULL,
balance_after decimal(10,2) NOT NULL,
description text NOT NULL,
transaction_type varchar(50) DEFAULT 'manual',
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY created_at (created_at)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
// Core Functions
/**
* Get user balance
*/
public function get_balance($user_id) {
global $wpdb;
// Get from user meta (cached value)
$balance = get_user_meta($user_id, 'points_balance', true);
if ($balance === '') {
// If no cached value, calculate from transactions
$balance = $wpdb->get_var($wpdb->prepare(
"SELECT balance_after FROM {$this->table_name}
WHERE user_id = %d
ORDER BY id DESC
LIMIT 1",
$user_id
));
$balance = $balance ? floatval($balance) : 0;
update_user_meta($user_id, 'points_balance', $balance);
}
return floatval($balance);
}
/**
* Add or subtract points (atomic operation)
*/
public function update_points($user_id, $amount, $description = '', $type = 'manual') {
global $wpdb;
// Start transaction for atomicity
$wpdb->query('START TRANSACTION');
try {
// Lock the user's transactions
$current_balance = $wpdb->get_var($wpdb->prepare(
"SELECT balance_after FROM {$this->table_name}
WHERE user_id = %d
ORDER BY id DESC
LIMIT 1
FOR UPDATE",
$user_id
));
if ($current_balance === null) {
$current_balance = 0;
}
$new_balance = floatval($current_balance) + floatval($amount);
// Prevent negative balance
if ($new_balance < 0) {
$wpdb->query('ROLLBACK');
return new WP_Error('insufficient_balance', 'Insufficient balance');
}
// Insert transaction
$result = $wpdb->insert(
$this->table_name,
array(
'user_id' => $user_id,
'amount' => $amount,
'balance_after' => $new_balance,
'description' => $description,
'transaction_type' => $type,
'created_at' => current_time('mysql')
),
array('%d', '%f', '%f', '%s', '%s', '%s')
);
if ($result === false) {
$wpdb->query('ROLLBACK');
return new WP_Error('db_error', 'Database error');
}
// Update cached balance
update_user_meta($user_id, 'points_balance', $new_balance);
// Commit transaction
$wpdb->query('COMMIT');
// Trigger action for other plugins
do_action('wp_points_balance_updated', $user_id, $amount, $new_balance, $description);
return $new_balance;
} catch (Exception $e) {
$wpdb->query('ROLLBACK');
return new WP_Error('transaction_error', $e->getMessage());
}
}
/**
* Add points
*/
public function add_points($user_id, $amount, $description = '', $type = 'manual') {
return $this->update_points($user_id, abs($amount), $description, $type);
}
/**
* Subtract points
*/
public function subtract_points($user_id, $amount, $description = '', $type = 'manual') {
return $this->update_points($user_id, -abs($amount), $description, $type);
}
// Admin UI
public function add_admin_menu() {
add_menu_page(
'Points System',
'Points',
'manage_options',
'wp-points',
array($this, 'admin_page'),
'dashicons-awards',
30
);
add_submenu_page(
'wp-points',
'Transactions',
'Transactions',
'manage_options',
'wp-points',
array($this, 'admin_page')
);
add_submenu_page(
'wp-points',
'Settings',
'Settings',
'manage_options',
'wp-points-settings',
array($this, 'settings_page')
);
}
public function admin_page() {
global $wpdb;
// Handle bulk actions
if (isset($_POST['action']) && $_POST['action'] === 'bulk_update' && check_admin_referer('points_bulk_action')) {
$this->handle_bulk_update();
}
// Get filter parameters
$user_id = isset($_GET['user_id']) ? intval($_GET['user_id']) : 0;
$page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$per_page = 50;
$offset = ($page - 1) * $per_page;
// Build query
$where = array('1=1');
$where_values = array();
if ($user_id > 0) {
$where[] = 'user_id = %d';
$where_values[] = $user_id;
}
$where_clause = implode(' AND ', $where);
// Get total count
$total_items = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->table_name} WHERE $where_clause",
...$where_values
));
// Get transactions
$transactions = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$this->table_name}
WHERE $where_clause
ORDER BY id DESC
LIMIT %d OFFSET %d",
array_merge($where_values, array($per_page, $offset))
));
// Get user balances for summary
$total_users = $wpdb->get_var(
"SELECT COUNT(DISTINCT user_id) FROM {$this->table_name}"
);
$total_points = $wpdb->get_var(
"SELECT SUM(balance_after) FROM (
SELECT user_id, balance_after
FROM {$this->table_name} t1
WHERE id = (
SELECT MAX(id)
FROM {$this->table_name} t2
WHERE t2.user_id = t1.user_id
)
) as latest_balances"
);
// Get recent activity stats
$today_transactions = $wpdb->get_var(
"SELECT COUNT(*) FROM {$this->table_name} WHERE DATE(created_at) = CURDATE()"
);
$active_users_today = $wpdb->get_var(
"SELECT COUNT(DISTINCT user_id) FROM {$this->table_name} WHERE DATE(created_at) = CURDATE()"
);
?>
<style>
.wrap { margin: 0; }
.points-wrap { background: #f0f0f1; min-height: 100vh; margin: 0; padding: 0; }
.points-header { background: #fff; padding: 20px 32px; margin: 0; box-shadow: 0 1px 3px rgba(0,0,0,0.1); position: relative; z-index: 10; }
.points-header h1 { margin: 0; font-size: 24px; font-weight: 600; color: #1d2327; line-height: 1.3; }
.points-content { padding: 32px 20px; }
.points-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-bottom: 32px; }
.stat-card { background: #fff; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); transition: transform 0.2s, box-shadow 0.2s; }
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.stat-card h3 { margin: 0 0 8px; font-size: 13px; font-weight: 500; color: #50575e; text-transform: uppercase; letter-spacing: 0.5px; }
.stat-card .value { font-size: 32px; font-weight: 600; color: #1d2327; line-height: 1; }
.stat-card .meta { font-size: 13px; color: #8c8f94; margin-top: 8px; }
.bulk-update-card { background: #fff; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 24px; }
.bulk-update-card h3 { margin: 0 0 20px; font-size: 16px; font-weight: 600; color: #1d2327; }
.bulk-form { display: grid; grid-template-columns: 2fr 1fr 1fr 2fr auto; gap: 16px; align-items: end; }
.form-group { display: flex; flex-direction: column; }
.form-group label { font-size: 13px; font-weight: 500; color: #1d2327; margin-bottom: 6px; }
.form-group input, .form-group select { padding: 8px 12px; border: 1px solid #dcdcde; border-radius: 4px; font-size: 14px; width: 100%; box-sizing: border-box; }
.form-group input:focus, .form-group select:focus { border-color: #2271b1; outline: none; box-shadow: 0 0 0 1px #2271b1; }
.btn-update { background: #2271b1; color: #fff; border: none; padding: 8px 24px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.2s; white-space: nowrap; }
.btn-update:hover { background: #135e96; }
.filter-bar { background: #fff; border-radius: 8px; padding: 16px 24px; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.filter-bar form { display: flex; align-items: center; gap: 12px; margin: 0; flex-wrap: wrap; }
.transactions-table { background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.transactions-table table { margin: 0; border: none; }
.transactions-table th { background: #f6f7f7; font-weight: 600; color: #1d2327; padding: 16px; text-align: left; border-bottom: 1px solid #e1e1e1; }
.transactions-table td { padding: 16px; vertical-align: middle; }
.transactions-table tr:hover td { background: #f6f7f7; }
.user-info { display: flex; align-items: center; gap: 12px; }
.user-avatar { width: 40px; height: 40px; border-radius: 50%; background: #f0f0f1; display: flex; align-items: center; justify-content: center; font-weight: 600; color: #50575e; flex-shrink: 0; }
.user-details a { color: #2271b1; text-decoration: none; font-weight: 500; }
.user-details a:hover { color: #135e96; }
.user-details small { display: block; color: #8c8f94; font-size: 12px; margin-top: 2px; }
.amount { font-weight: 600; font-size: 15px; }
.amount.positive { color: #00a32a; }
.amount.negative { color: #d63638; }
.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; }
.badge-manual { background: #e7f2fd; color: #135e96; }
.badge-registration { background: #edfaef; color: #00a32a; }
.badge-daily_login { background: #fcf0e4; color: #996800; }
.badge-admin_adjustment { background: #fef1f1; color: #d63638; }
.badge-bulk_update { background: #f0f0f1; color: #50575e; }
.pagination-wrap { margin-top: 24px; text-align: center; }
.pagination-wrap .page-numbers { padding: 6px 12px; margin: 0 2px; border: 1px solid #dcdcde; border-radius: 4px; text-decoration: none; color: #50575e; transition: all 0.2s; display: inline-block; }
.pagination-wrap .page-numbers:hover { border-color: #2271b1; color: #2271b1; }
.pagination-wrap .page-numbers.current { background: #2271b1; color: #fff; border-color: #2271b1; }
/* Responsive Design */
@media (max-width: 1200px) {
.bulk-form { grid-template-columns: 1fr 1fr; }
.bulk-form .btn-update { grid-column: 1 / -1; }
}
@media (max-width: 768px) {
.points-header { padding: 16px 20px; }
.points-header h1 { font-size: 20px; }
.points-content { padding: 20px 16px; }
.points-stats { grid-template-columns: 1fr; gap: 16px; }
.stat-card { padding: 20px; }
.stat-card .value { font-size: 28px; }
.bulk-form { grid-template-columns: 1fr; }
.transactions-table { overflow-x: auto; }
.transactions-table table { min-width: 600px; }
.filter-bar form { flex-direction: column; align-items: stretch; }
.filter-bar input, .filter-bar button { width: 100%; }
}
@media (max-width: 480px) {
.user-info { flex-direction: column; align-items: flex-start; gap: 8px; }
.user-avatar { width: 32px; height: 32px; font-size: 14px; }
.transactions-table th, .transactions-table td { padding: 12px; font-size: 13px; }
.amount { font-size: 14px; }
}
</style>
<div class="wrap">
<div class="points-wrap">
<div class="points-header">
<h1>🎯 Points System</h1>
</div>
<div class="points-content">
<!-- Summary Cards -->
<div class="points-stats">
<div class="stat-card">
<h3>Total Users</h3>
<div class="value"><?php echo number_format($total_users); ?></div>
<div class="meta">Active in points system</div>
</div>
<div class="stat-card">
<h3>Total Points</h3>
<div class="value"><?php echo number_format($total_points, 0); ?></div>
<div class="meta">Across all users</div>
</div>
<div class="stat-card">
<h3>Today's Activity</h3>
<div class="value"><?php echo number_format($today_transactions); ?></div>
<div class="meta"><?php echo number_format($active_users_today); ?> active users</div>
</div>
<div class="stat-card">
<h3>Average Balance</h3>
<div class="value"><?php echo $total_users > 0 ? number_format($total_points / $total_users, 0) : '0'; ?></div>
<div class="meta">Points per user</div>
</div>
</div>
<!-- Bulk Update Form -->
<div class="bulk-update-card">
<h3>Bulk Update Points</h3>
<form method="post" class="bulk-form">
<?php wp_nonce_field('points_bulk_action'); ?>
<input type="hidden" name="action" value="bulk_update">
<div class="form-group">
<label>User(s)</label>
<input type="text" name="user_ids" placeholder="User IDs (comma separated) or 'all'">
</div>
<div class="form-group">
<label>Action</label>
<select name="operation">
<option value="add">Add Points</option>
<option value="subtract">Subtract Points</option>
<option value="set">Set Balance</option>
</select>
</div>
<div class="form-group">
<label>Amount</label>
<input type="number" name="amount" step="0.01" required placeholder="0.00">
</div>
<div class="form-group">
<label>Description</label>
<input type="text" name="description" required placeholder="Reason for update">
</div>
<button type="submit" class="btn-update">Update Points</button>
</form>
</div>
<!-- Filters -->
<div class="filter-bar">
<form method="get">
<input type="hidden" name="page" value="wp-points">
<label style="margin: 0; font-weight: 500;">Filter by User:</label>
<input type="number" name="user_id" value="<?php echo $user_id; ?>" placeholder="User ID" style="width: 120px; padding: 6px 12px; border: 1px solid #dcdcde; border-radius: 4px;">
<button type="submit" class="button button-primary">Filter</button>
<?php if ($user_id): ?>
<a href="<?php echo admin_url('admin.php?page=wp-points'); ?>" class="button">Clear Filter</a>
<?php endif; ?>
</form>
</div>
<!-- Transactions Table -->
<div class="transactions-table">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width: 60px;">ID</th>
<th>User</th>
<th style="width: 120px;">Amount</th>
<th style="width: 120px;">Balance After</th>
<th>Description</th>
<th style="width: 120px;">Type</th>
<th style="width: 160px;">Date</th>
</tr>
</thead>
<tbody>
<?php if (empty($transactions)): ?>
<tr>
<td colspan="7" style="text-align: center; padding: 40px;">
No transactions found. Start by adding points to users.
</td>
</tr>
<?php else: ?>
<?php foreach ($transactions as $transaction): ?>
<?php $user = get_userdata($transaction->user_id); ?>
<tr>
<td style="color: #8c8f94;">#<?php echo $transaction->id; ?></td>
<td>
<div class="user-info">
<div class="user-avatar">
<?php echo $user ? strtoupper(substr($user->display_name, 0, 1)) : '?'; ?>
</div>
<div class="user-details">
<?php if ($user): ?>
<a href="<?php echo get_edit_user_link($transaction->user_id); ?>">
<?php echo esc_html($user->display_name); ?>
</a>
<small><?php echo esc_html($user->user_email); ?></small>
<?php else: ?>
<span style="color: #8c8f94;">User #<?php echo $transaction->user_id; ?> (deleted)</span>
<?php endif; ?>
</div>
</div>
</td>
<td>
<span class="amount <?php echo $transaction->amount >= 0 ? 'positive' : 'negative'; ?>">
<?php echo $transaction->amount >= 0 ? '+' : ''; ?><?php echo number_format($transaction->amount, 2); ?>
</span>
</td>
<td style="font-weight: 500;"><?php echo number_format($transaction->balance_after, 2); ?></td>
<td><?php echo esc_html($transaction->description); ?></td>
<td>
<span class="badge badge-<?php echo esc_attr($transaction->transaction_type); ?>">
<?php echo str_replace('_', ' ', esc_html($transaction->transaction_type)); ?>
</span>
</td>
<td style="color: #50575e;">
<?php echo date('M d, Y', strtotime($transaction->created_at)); ?><br>
<small style="color: #8c8f94;"><?php echo date('g:i A', strtotime($transaction->created_at)); ?></small>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php
$total_pages = ceil($total_items / $per_page);
if ($total_pages > 1):
$args = array(
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '← Previous',
'next_text' => 'Next →',
'total' => $total_pages,
'current' => $page
);
?>
<div class="pagination-wrap">
<?php echo paginate_links($args); ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php
}
public function settings_page() {
if (isset($_POST['submit']) && check_admin_referer('points_settings')) {
$hooks = array(
'register' => isset($_POST['hooks']['register']),
'register_points' => intval($_POST['register_points']),
'daily_login' => isset($_POST['hooks']['daily_login']),
'daily_login_points' => intval($_POST['daily_login_points'])
);
update_option($this->hooks_option, $hooks);
echo '<div class="notice notice-success is-dismissible" style="margin: 20px 20px 0;"><p>✅ Settings saved successfully!</p></div>';
}
$hooks = get_option($this->hooks_option, array(
'register' => true,
'register_points' => 100,
'daily_login' => true,
'daily_login_points' => 10
));
?>
<style>
.wrap { margin: 0; }
.settings-wrap { background: #f0f0f1; min-height: 100vh; margin: 0; padding: 0; }
.settings-header { background: #fff; padding: 20px 32px; margin: 0; box-shadow: 0 1px 3px rgba(0,0,0,0.1); position: relative; z-index: 10; }
.settings-header h1 { margin: 0; font-size: 24px; font-weight: 600; color: #1d2327; line-height: 1.3; }
.settings-content { padding: 32px 20px; }
.settings-card { background: #fff; border-radius: 8px; padding: 32px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 24px; }
.settings-card h2 { margin: 0 0 24px; font-size: 18px; font-weight: 600; color: #1d2327; padding-bottom: 16px; border-bottom: 1px solid #e1e1e1; }
.settings-row { display: flex; align-items: start; padding: 20px 0; border-bottom: 1px solid #f0f0f1; }
.settings-row:last-child { border-bottom: none; }
.settings-label { flex: 0 0 300px; padding-right: 32px; }
.settings-label h3 { margin: 0 0 4px; font-size: 15px; font-weight: 600; color: #1d2327; }
.settings-label p { margin: 0; font-size: 13px; color: #50575e; line-height: 1.5; }
.settings-control { flex: 1; }
.toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; }
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccd0d4; transition: .3s; border-radius: 24px; }
.toggle-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; }
input:checked + .toggle-slider { background-color: #2271b1; }
input:checked + .toggle-slider:before { transform: translateX(20px); }
.points-input { margin-top: 12px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.points-input input { width: 80px; padding: 8px 12px; border: 1px solid #dcdcde; border-radius: 4px; font-size: 14px; }
.points-input input:focus { border-color: #2271b1; outline: none; box-shadow: 0 0 0 1px #2271b1; }
.usage-examples { background: #f6f7f7; border-radius: 8px; padding: 24px; margin-top: 16px; }
.usage-examples h3 { margin: 0 0 16px; font-size: 16px; font-weight: 600; color: #1d2327; }
.code-example { background: #1d2327; color: #fff; padding: 16px 20px; border-radius: 4px; margin-bottom: 16px; font-family: 'Monaco', 'Consolas', monospace; font-size: 13px; line-height: 1.6; overflow-x: auto; }
.code-example strong { color: #72aee6; font-weight: normal; }
.code-comment { color: #8b8b8b; }
.submit-wrap { margin-top: 32px; padding-top: 24px; border-top: 1px solid #e1e1e1; }
.button-primary { background: #2271b1; border-color: #2271b1; color: #fff; padding: 8px 24px; font-size: 14px; font-weight: 500; border-radius: 4px; transition: background 0.2s; }
.button-primary:hover { background: #135e96; border-color: #135e96; }
.api-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; }
.api-card { background: #fff; border: 1px solid #e1e1e1; border-radius: 4px; padding: 16px; }
.api-card h4 { margin: 0 0 8px; font-size: 14px; font-weight: 600; color: #1d2327; }
.api-card code { background: #f0f0f1; padding: 2px 4px; border-radius: 2px; font-size: 12px; }
/* Responsive Design */
@media (max-width: 768px) {
.settings-header { padding: 16px 20px; }
.settings-header h1 { font-size: 20px; }
.settings-content { padding: 20px 16px; }
.settings-card { padding: 20px; }
.settings-card h2 { font-size: 16px; margin-bottom: 16px; padding-bottom: 12px; }
.settings-row { flex-direction: column; padding: 16px 0; }
.settings-label { flex: none; padding-right: 0; margin-bottom: 12px; }
.points-input { flex-direction: column; align-items: flex-start; }
.points-input input { width: 100px; }
.api-grid { grid-template-columns: 1fr; }
.usage-examples { padding: 16px; }
.code-example { padding: 12px 16px; font-size: 12px; }
}
</style>
<div class="wrap">
<div class="settings-wrap">
<div class="settings-header">
<h1>⚙️ Points Settings</h1>
</div>
<div class="settings-content">
<form method="post">
<?php wp_nonce_field('points_settings'); ?>
<div class="settings-card">
<h2>Automatic Points Rewards</h2>
<div class="settings-row">
<div class="settings-label">
<h3>Registration Bonus</h3>
<p>Award points automatically when new users sign up</p>
</div>
<div class="settings-control">
<label class="toggle-switch">
<input type="checkbox" name="hooks[register]" value="1" <?php checked($hooks['register']); ?>>
<span class="toggle-slider"></span>
</label>
<div class="points-input">
<span>Award</span>
<input type="number" name="register_points" value="<?php echo $hooks['register_points']; ?>" min="0">
<span>points on registration</span>
</div>
</div>
</div>
<div class="settings-row">
<div class="settings-label">
<h3>Daily Login Reward</h3>
<p>Give points to users who log in each day (once per day)</p>
</div>
<div class="settings-control">
<label class="toggle-switch">
<input type="checkbox" name="hooks[daily_login]" value="1" <?php checked($hooks['daily_login']); ?>>
<span class="toggle-slider"></span>
</label>
<div class="points-input">
<span>Award</span>
<input type="number" name="daily_login_points" value="<?php echo $hooks['daily_login_points']; ?>" min="0">
<span>points per daily login</span>
</div>
</div>
</div>
<div class="submit-wrap">
<?php submit_button('Save Settings', 'primary', 'submit', false); ?>
</div>
</div>
</form>
<div class="settings-card">
<h2>🚀 Developer Documentation</h2>
<div class="usage-examples">
<h3>Quick Start Guide</h3>
<div class="code-example">
<strong>// Get user's current balance</strong>
$balance = wp_points()->get_balance($user_id);
<span class="code-comment">// or use the helper function</span>
$balance = get_user_points($user_id);
</div>
<div class="code-example">
<strong>// Add points to user</strong>
wp_points()->add_points($user_id, 50, 'Completed profile');
<span class="code-comment">// or use the helper function</span>
add_user_points($user_id, 50, 'Completed profile');
</div>
<div class="code-example">
<strong>// Subtract points from user</strong>
wp_points()->subtract_points($user_id, 20, 'Premium feature usage');
<span class="code-comment">// or use the helper function</span>
subtract_user_points($user_id, 20, 'Premium feature usage');
</div>
<div class="code-example">
<strong>// Check balance before action</strong>
if (get_user_points($user_id) >= 100) {
<span class="code-comment">// User has enough points, proceed with action</span>
subtract_user_points($user_id, 100, 'Unlocked premium content');
<span class="code-comment">// Grant access to premium content</span>
} else {
<span class="code-comment">// Show insufficient balance message</span>
}
</div>
<div class="code-example">
<strong>// Display user balance with shortcode</strong>
[user_points]
<span class="code-comment">// or for specific user</span>
[user_points user_id="123"]
</div>
<div class="code-example">
<strong>// Hook into balance updates</strong>
add_action('wp_points_balance_updated', function($user_id, $amount, $new_balance, $description) {
<span class="code-comment">// Send email notification for large transactions</span>
if (abs($amount) >= 1000) {
wp_mail(get_userdata($user_id)->user_email, 'Points Update',
"Your balance changed by {$amount} points. New balance: {$new_balance}");
}
}, 10, 4);
</div>
</div>
<h3 style="margin: 24px 0 16px; font-size: 16px; font-weight: 600;">Available Functions</h3>
<div class="api-grid">
<div class="api-card">
<h4>wp_points()</h4>
<p style="margin: 0; font-size: 13px; color: #50575e;">Returns the main points system instance</p>
</div>
<div class="api-card">
<h4>get_user_points($user_id)</h4>
<p style="margin: 0; font-size: 13px; color: #50575e;">Get user's current balance</p>
</div>
<div class="api-card">
<h4>add_user_points($user_id, $amount, $description)</h4>
<p style="margin: 0; font-size: 13px; color: #50575e;">Add points to user balance</p>
</div>
<div class="api-card">
<h4>subtract_user_points($user_id, $amount, $description)</h4>
<p style="margin: 0; font-size: 13px; color: #50575e;">Subtract points from user balance</p>
</div>
</div>
<h3 style="margin: 24px 0 16px; font-size: 16px; font-weight: 600;">Available Hooks</h3>
<div class="api-grid">
<div class="api-card">
<h4>wp_points_balance_updated</h4>
<p style="margin: 0; font-size: 13px; color: #50575e;">Fired after any balance change<br>
<code>$user_id, $amount, $new_balance, $description</code></p>
</div>
<div class="api-card">
<h4>wp_points_after_registration</h4>
<p style="margin: 0; font-size: 13px; color: #50575e;">Add custom logic after user registration<br>
<code>$user_id</code></p>
</div>
</div>
</div>
</div>
</div>
</div>
<?php
}
// User Profile Integration
public function show_user_points_field($user) {
if (!current_user_can('edit_users')) {
return;
}
$balance = $this->get_balance($user->ID);
?>
<style>
.points-section { margin-top: 40px; }
.points-section h3 { font-size: 16px; font-weight: 600; margin-bottom: 20px; }
.points-balance-box { background: #f6f7f7; border: 1px solid #dcdcde; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.balance-display { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }
.balance-value { font-size: 32px; font-weight: 600; color: #1d2327; }
.balance-label { font-size: 14px; color: #50575e; }
.balance-input-wrap { margin-top: 16px; }
.balance-input-wrap input { width: 200px; padding: 8px 12px; border: 1px solid #dcdcde; border-radius: 4px; }
.points-transactions { margin-top: 30px; }
.points-transactions h4 { font-size: 15px; font-weight: 600; margin-bottom: 16px; }
.points-table { border: 1px solid #dcdcde; border-radius: 8px; overflow: hidden; background: #fff; }
.points-table table { margin: 0; border: none; }
.points-table th { background: #f6f7f7; font-weight: 500; font-size: 13px; color: #50575e; padding: 12px 16px; text-align: left; border-bottom: 1px solid #e1e1e1; }
.points-table td { padding: 12px 16px; font-size: 13px; }
.points-table tr:nth-child(even) td { background: #f9f9f9; }
.points-amount { font-weight: 600; }
.points-amount.positive { color: #00a32a; }
.points-amount.negative { color: #d63638; }
.view-all-link { display: inline-block; margin-top: 12px; color: #2271b1; text-decoration: none; font-size: 14px; }
.view-all-link:hover { color: #135e96; text-decoration: underline; }
.no-transactions { text-align: center; padding: 40px; color: #8c8f94; }
</style>
<div class="points-section">
<h3>💰 Points Balance</h3>
<div class="points-balance-box">
<div class="balance-display">
<span class="balance-value"><?php echo number_format($balance, 2); ?></span>
<span class="balance-label">points</span>
</div>
<div class="balance-input-wrap">
<label for="points_balance" style="display: block; margin-bottom: 8px; font-weight: 500;">Adjust Balance</label>
<input type="number" name="points_balance" id="points_balance" value="<?php echo $balance; ?>" step="0.01" />
<p class="description" style="margin-top: 8px;">Changing this value will create a manual adjustment transaction.</p>
</div>
</div>
<div class="points-transactions">
<h4>Recent Transactions</h4>
<?php
global $wpdb;
$transactions = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$this->table_name} WHERE user_id = %d ORDER BY id DESC LIMIT 10",
$user->ID
));
if ($transactions): ?>
<div class="points-table">
<table class="widefat">
<thead>
<tr>
<th>Date</th>
<th>Amount</th>
<th>Balance</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<?php foreach ($transactions as $t): ?>
<tr>
<td><?php echo date('M d, Y g:i A', strtotime($t->created_at)); ?></td>
<td>
<span class="points-amount <?php echo $t->amount >= 0 ? 'positive' : 'negative'; ?>">
<?php echo $t->amount >= 0 ? '+' : ''; ?><?php echo number_format($t->amount, 2); ?>
</span>
</td>
<td><?php echo number_format($t->balance_after, 2); ?></td>
<td><?php echo esc_html($t->description); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<a href="<?php echo admin_url('admin.php?page=wp-points&user_id=' . $user->ID); ?>" class="view-all-link">
View all transactions →
</a>
<?php else: ?>
<div class="points-table">
<div class="no-transactions">
No transactions found for this user yet.
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
public function save_user_points_field($user_id) {
if (!current_user_can('edit_users')) {
return;
}
if (isset($_POST['points_balance'])) {
$new_balance = floatval($_POST['points_balance']);
$current_balance = $this->get_balance($user_id);
$difference = $new_balance - $current_balance;
if ($difference != 0) {
$this->update_points(
$user_id,
$difference,
'Manual adjustment by ' . wp_get_current_user()->display_name,
'admin_adjustment'
);
}
}
}
// Points Earning Hooks
public function points_on_register($user_id) {
$hooks = get_option($this->hooks_option, array());
if (!empty($hooks['register'])) {
$points = isset($hooks['register_points']) ? intval($hooks['register_points']) : 100;
$this->add_points($user_id, $points, 'Welcome bonus', 'registration');
}
}
public function points_on_login($user_login, $user) {
$hooks = get_option($this->hooks_option, array());
if (!empty($hooks['daily_login'])) {
// Check if user already got points today
$last_login_bonus = get_user_meta($user->ID, 'last_login_bonus_date', true);
$today = date('Y-m-d');
if ($last_login_bonus !== $today) {
$points = isset($hooks['daily_login_points']) ? intval($hooks['daily_login_points']) : 10;
$this->add_points($user->ID, $points, 'Daily login bonus', 'daily_login');
update_user_meta($user->ID, 'last_login_bonus_date', $today);
}
}
}
// Shortcode
public function points_shortcode($atts) {
$atts = shortcode_atts(array(
'user_id' => get_current_user_id()
), $atts);
if (!$atts['user_id']) {
return '';
}
$balance = $this->get_balance($atts['user_id']);
return '<span class="user-points-balance">' . number_format($balance, 2) . '</span>';
}
// Helper Functions
private function handle_bulk_update() {
$user_ids = sanitize_text_field($_POST['user_ids']);
$operation = sanitize_text_field($_POST['operation']);
$amount = floatval($_POST['amount']);
$description = sanitize_text_field($_POST['description']);
if ($user_ids === 'all') {
$users = get_users(array('fields' => 'ID'));
} else {
$users = array_map('intval', explode(',', $user_ids));
}
$success = 0;
foreach ($users as $user_id) {
if ($operation === 'add') {
$result = $this->add_points($user_id, $amount, $description, 'bulk_update');
} elseif ($operation === 'subtract') {
$result = $this->subtract_points($user_id, $amount, $description, 'bulk_update');
} elseif ($operation === 'set') {
$current = $this->get_balance($user_id);
$diff = $amount - $current;
$result = $this->update_points($user_id, $diff, $description, 'bulk_update');
}
if (!is_wp_error($result)) {
$success++;
}
}
echo '<div class="notice notice-success is-dismissible" style="margin: 20px 20px 0; padding: 12px 20px; border-left: 4px solid #00a32a; background: #fff;">
<p style="margin: 0; font-size: 14px;">
<strong>✅ Success!</strong> Updated points for ' . $success . ' user' . ($success !== 1 ? 's' : '') . '.
</p>
</div>';
}
// AJAX Handler
public function ajax_update_user_points() {
check_ajax_referer('wp_points_nonce', 'nonce');
if (!current_user_can('edit_users')) {
wp_die('Unauthorized');
}
$user_id = intval($_POST['user_id']);
$amount = floatval($_POST['amount']);
$description = sanitize_text_field($_POST['description']);
$operation = sanitize_text_field($_POST['operation']);
if ($operation === 'add') {
$result = $this->add_points($user_id, $amount, $description);
} else {
$result = $this->subtract_points($user_id, $amount, $description);
}
if (is_wp_error($result)) {
wp_send_json_error($result->get_error_message());
} else {
wp_send_json_success(array('new_balance' => $result));
}
}
}
// Initialize the points system
function wp_points() {
return WP_Simple_Points::get_instance();
}
// Initialize on plugins_loaded
add_action('plugins_loaded', 'wp_points');
// Global helper functions for easy access
function get_user_points($user_id) {
return wp_points()->get_balance($user_id);
}
function add_user_points($user_id, $amount, $description = '') {
return wp_points()->add_points($user_id, $amount, $description);
}
function subtract_user_points($user_id, $amount, $description = '') {
return wp_points()->subtract_points($user_id, $amount, $description);
}
// Custom hooks for developers
add_action('wp_points_after_registration', function($user_id) {
// Example: Add extra points based on referral
if (isset($_COOKIE['referral_code'])) {
add_user_points($user_id, 50, 'Referral bonus');
}
}, 10, 1);
// Example usage in your code:
/*
// Get balance
$balance = get_user_points($user_id);
// Add points
add_user_points($user_id, 100, 'Purchased premium plan');
// Subtract points
subtract_user_points($user_id, 50, 'Used feature X');
// Check if user has enough points
if (get_user_points($user_id) >= 100) {
// Allow action
subtract_user_points($user_id, 100, 'Unlocked feature');
}
*/