Flex Rock UTV Performance Additions
About This Project
Custom development work for Flex Rock UTV Performance’s website, including WooCommerce features, user experience improvements, loyalty systems, customer engagement tools, and internal business functionality.

Project Overview
This page highlights four larger custom systems built for the Flex Rock website. Instead of separating the preview cards from the technical notes, each feature below is contained in its own section with the visual example, short explanation, and expandable technical details grouped together.
- Spinning Coupon Wheel
- Flex Rock Bucks Loyalty System
- Customer UTV Image Gallery
- Veteran Discount Verification System

Spinning Coupon Wheel
Gamified checkout experience with server-controlled randomized rewards. The wheel looks random to the customer, but the winning prize is selected securely on the server before the animation starts.
- Server-side prize selection
- Weighted reward probability
- WooCommerce session storage
- Cart and checkout discount integration
- Free product reward support
Building the available wheel prizes
The wheel starts with a prize catalog where each reward has a label, value, type, and weight. This made it easy to control which prizes appeared more often without changing the frontend animation logic.
private function prize_catalog() {
return [
'p5_percent' => ['label'=>'5% OFF', 'type'=>'percent', 'value'=>5, 'weight'=>24],
'p5_fixed' => ['label'=>'$5 OFF', 'type'=>'fixed', 'value'=>5, 'weight'=>20],
'none' => ['label'=>'Loser Vibes', 'type'=>'none', 'value'=>0, 'weight'=>14],
'p10_percent' => ['label'=>'10% OFF', 'type'=>'percent', 'value'=>10, 'weight'=>18],
'p10_fixed' => ['label'=>'$10 OFF', 'type'=>'fixed', 'value'=>10, 'weight'=>14],
'free_tee' => ['label'=>'FREE TEE','type'=>'free_tee','value'=>self::FREE_TEE_PRODUCT_ID, 'weight'=>10],
];
}
Selecting the winner with weighted logic
Instead of choosing a prize evenly at random, the plugin uses weighted probability. This gives the business control over how frequently each reward is awarded while still keeping the experience random for the customer.
private function pick_weighted_slice($slices) {
$total = array_sum(array_map(fn($s) => (int) $s['weight'], $slices));
$r = mt_rand(1, $total);
$cum = 0;
foreach ($slices as $i => $s) {
$cum += (int) $s['weight'];
if ($r <= $cum) {
return $i;
}
}
return 0;
}
Storing the selected prize server-side
Once the winner is selected, the result is saved in the WooCommerce session. This keeps the important prize data on the server, so the customer cannot simply modify the browser response to claim a different reward.
$slices = $this->wheel_slices();
$index = $this->pick_weighted_slice($slices);
WC()->session->set(self::SESSION_PRIZE, $slices[$index]);
WC()->session->set(self::SESSION_PRIZE_DONE, null);
WC()->session->set(self::SESSION_SPUN_AT, time());
wp_send_json([
'ok' => true,
'index' => $index,
'prize' => $slices[$index],
]);
Animating the wheel to match the server result
The frontend animation is used for presentation only. After the server returns the winning index, JavaScript calculates the rotation needed to line that slice up with the fixed pointer at the top of the wheel.
var START_AT_TOP = 270;
var per = 360 / labels.length;
var idx = data.index;
var theta = ((currentRot % 360) + 360) % 360;
var delta = (360 * 6) + (START_AT_TOP - (per * idx + per / 2)) - theta;
currentRot += delta;
rotor.style.transform = 'rotate(' + currentRot + 'deg)';
- (360 × 6): Adds multiple full rotations so the wheel spins six times for visual effect.
- (60 × 0) + (60 / 2): Calculates the center of the winning slice.
- 270 – 30: Rotates the slice center to the pointer.
- – 0: Adjusts for the wheel’s current rotation.
- Final result: The wheel spins multiple full rotations, then lands exactly with the selected slice aligned under the pointer.
Connecting the wheel to WooCommerce checkout and orders
The spinning wheel connects into WooCommerce through key hooks that load the wheel, process the spin through AJAX, apply prizes during cart and checkout calculation, and carry free prize data into the final order.
add_action('wp_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('wp_footer', [$this, 'render_wheel_ui']);
add_action('wp_ajax_swr_spin_wheel', [$this, 'ajax_spin_wheel']);
add_action('wp_ajax_nopriv_swr_spin_wheel', [$this, 'ajax_spin_wheel']);
add_action('wp_ajax_swr_claim_prize', [$this, 'ajax_claim_prize']);
add_action('wp_ajax_nopriv_swr_claim_prize', [$this, 'ajax_claim_prize']);
add_action('woocommerce_cart_calculate_fees', [$this, 'apply_discounts_and_prizes'], 20, 1);
add_action('woocommerce_before_calculate_totals', [$this, 'zero_price_for_free_tee'], 10, 1);
add_action('woocommerce_checkout_create_order_line_item', [$this, 'carry_free_flag_to_order_item'], 10, 4);
add_filter('woocommerce_email_order_meta_fields', [$this, 'email_order_meta_spin_line'], 10, 3);
Why this approach worked well
This approach kept the feature fun on the frontend while keeping the actual reward logic secure on the backend. The customer gets a polished spinning animation, but WooCommerce only applies the prize that was selected and stored server-side.
Future scalability
The prize catalog and weighted logic make the system easy to expand for future promotions. Additional rewards, seasonal offers, free products, or different prize weights can be added without rebuilding the entire wheel experience.

Flex Rock Bucks Loyalty System
A complete rewards system that lets customers earn and redeem Flex Rock Bucks through WooCommerce account creation, purchases, and approved gallery submissions.
- Signup rewards
- Purchase-based rewards
- Gallery submission rewards
- Cart and checkout redemption
- Admin-controlled reward settings
How customers earn points
Customers can earn Flex Rock Bucks in a few different ways: creating a WooCommerce account, completing a purchase, or submitting a UTV gallery post that gets approved by an admin.
- Account creation: Customers receive 50 points for creating an account.
- Completed purchase: Customers earn points based on the order subtotal. For example, an $84 purchase earns 84 points.
- Approved gallery submission: Customers receive 50 points when an admin approves their submitted UTV gallery post.
Awarding points after completed purchases
Purchase rewards are awarded when a WooCommerce order reaches completed status. The plugin calculates the merchandise subtotal, converts that into points, adds the points to the customer account, and leaves an order note for reference.
add_action('woocommerce_order_status_completed', [$this, 'award_points_on_complete']);
public function award_points_on_complete($order_id) {
$order = wc_get_order($order_id);
if (!$order || !$order->get_user_id()) return;
$s = $this->settings();
$subtotal = 0;
foreach ($order->get_items() as $item) {
$subtotal += (float) $item->get_subtotal();
}
$points = (int) floor($subtotal * (float) $s['earn_rate']);
if ($points > 0) {
$this->add_user_points($order->get_user_id(), $points);
$order->add_order_note(sprintf(
'Simple Woo Rewards: awarded %d points for $%.2f subtotal.',
$points,
$subtotal
));
}
}
Converting points into store credit
The point value is controlled in the rewards settings. For this setup, every 10 points equals $1.00, meaning each point is worth $0.10. So 84 points would equal $8.40 in store credit.
- Dollar value: Points × 0.10
- Example: 84 points × 0.10 = $8.40
- Equivalent: $1.00 = 10 points
$value = $pts * (float) $s['value_per_point'];
return '<span class="swr-my-points">'
. esc_html($pts)
. ' pts (~$'
. number_format($value, 2)
. ')</span>';
Displaying and redeeming points at cart/checkout
The plugin adds a rewards box to the cart and checkout flow. Logged-in customers can see their point balance, estimated dollar value, and the maximum number of points they can redeem on the current order.
add_action('woocommerce_before_cart_totals', [$this, 'render_redeem_box']);
add_action('woocommerce_review_order_before_payment', [$this, 'render_redeem_box_checkout']);
echo 'Your points: <strong>' . esc_html($points) . '</strong>. ';
echo 'Value: ~$' . number_format($points * (float) $s['value_per_point'], 2);
Applying redeemed points to the order
When a customer redeems points, the plugin stores the redemption amount in the WooCommerce session and creates a temporary coupon-style discount. This lets the rewards system work with WooCommerce’s normal cart and checkout calculations instead of bypassing them.
WC()->session->set(self::SESSION_REDEEM, $request_points);
$cash = $request_points * (float) $s['value_per_point'];
list($code, $cid) = $this->create_points_coupon(
min($cash, $eligible),
get_current_user_id()
);
WC()->session->set(self::SESSION_POINTS_COUPON, $code);
WC()->session->set(self::SESSION_POINTS_COUPON_ID, $cid);
WC()->cart->apply_coupon($code);
WC()->cart->calculate_totals();
Deducting redeemed points when the order is created
Redeemed points are attached to the WooCommerce order and deducted from the customer’s account during order creation. This keeps the reward balance tied to real checkout activity and prevents points from being used without being recorded on the order.
add_action('woocommerce_checkout_create_order', [$this, 'attach_redeem_to_order'], 10, 2);
$points = (int) (WC()->session->get(self::SESSION_REDEEM) ?: 0);
if ($points > 0) {
$have = $this->get_user_points($user_id);
$deduct = min($points, $have);
if ($deduct > 0) {
$this->add_user_points($user_id, -$deduct);
$order->update_meta_data('_swr_redeemed_points', $deduct);
}
WC()->session->set(self::SESSION_REDEEM, 0);
}
Encouraging account creation
The rewards system prompts customers to log in or create an account so their points can be saved. New customers are created as standard WooCommerce customers, allowing them to earn and redeem rewards without receiving admin privileges.
$user_id = wc_create_new_customer($email, $username, $pass);
if (is_wp_error($user_id)) {
wc_add_notice($user_id->get_error_message(), 'error');
return;
}
wc_set_customer_auth_cookie($user_id);
wc_add_notice(
'Account created! You are now logged in and can earn/redeem rewards.',
'success'
);
Reward settings in WordPress admin
The point values are managed from the WordPress admin in a custom area. This makes the system easier to maintain because the earn rate, point value, minimum redemption amount, and maximum cart coverage can be adjusted without editing the main reward logic.
$fields = [
['earn_rate', 'Earn rate (points per $1 subtotal)'],
['value_per_point', 'Point value in $ (e.g., 0.10 = 10¢/pt)'],
['min_redeem_points', 'Minimum points to redeem'],
['max_redeem_ratio', 'Max cart coverage (0–1)'],
];
Why this approach worked well
This approach worked well because it connected the loyalty system directly into WooCommerce’s existing account, cart, checkout, and order flow. Customers can earn points from real purchases and redeem them like store credit, while the business can adjust the reward value from the admin area.
Future scalability
The system can be expanded with seasonal bonus points, specific product-category rewards, referral bonuses, limited-time campaigns, or deeper reporting around how customers earn and redeem Flex Rock Bucks.

Customer UTV Image Gallery
A custom UTV gallery system that allows customers to upload images of their builds, have them reviewed by an admin, earn rewards once approved, vote on builds, and connect gallery posts back to WooCommerce products.
- Frontend customer submissions
- Admin approval workflow
- Flex Rock Bucks reward connection
- Voting and leaderboard logic
- Related WooCommerce product display
- Year / Make / Model product filtering
How submissions work
Users submit images through a frontend upload form. Each submission is stored as a custom post type and held in a pending state until reviewed by an admin.
$post_id = wp_insert_post([
'post_type' => 'uploaded_images',
'post_status' => 'pending',
'post_title' => sanitize_text_field($title),
]);
Admin approval workflow
Submissions are reviewed in the WordPress admin. Only when a post is approved does it appear in the gallery and trigger rewards. This keeps the gallery moderated while still allowing open user submissions.
Awarding rewards after approval
Once a gallery submission is approved, the system awards Flex Rock Bucks to the user. This connects engagement directly to store value.
add_action('transition_post_status', function($new, $old, $post) {
if ($post->post_type === 'uploaded_images' && $new === 'publish') {
$user_id = $post->post_author;
$points = 50;
if ($user_id) {
update_user_meta($user_id, 'fr_points',
(int) get_user_meta($user_id, 'fr_points', true) + $points
);
}
}
}, 10, 3);
Rendering the gallery
The gallery pulls approved posts and displays them dynamically on the frontend. This avoids manually managing content and keeps the gallery automatically updated.
$query = new WP_Query([
'post_type' => 'uploaded_images',
'post_status' => 'publish',
]);
while ($query->have_posts()) {
$query->the_post();
the_post_thumbnail();
}
Handling file uploads
Images are uploaded using WordPress’s media handling functions and attached to the submission post. This ensures compatibility with WordPress media management and avoids reinventing file storage logic.
$attachment_id = media_handle_upload('image', $post_id);
if (!is_wp_error($attachment_id)) {
set_post_thumbnail($post_id, $attachment_id);
}
Voting system and duplicate vote prevention
The gallery includes a community voting system so users can vote for their favorite builds. Votes are handled through AJAX and stored as post meta. To prevent duplicate voting, each post also stores an array of user IDs who have already voted.
function iuv_handle_vote() {
if (!is_user_logged_in()) {
wp_send_json_error('You must be logged in to vote.');
}
if (!isset($_POST['post_id']) || !isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'iuv_vote')) {
wp_send_json_error('Invalid request.');
}
$post_id = intval($_POST['post_id']);
$user_id = get_current_user_id();
$voted_users = get_post_meta($post_id, '_iuv_voted_users', true) ?: array();
if (in_array($user_id, $voted_users)) {
wp_send_json_error('You have already voted on this image.');
}
$votes = get_post_meta($post_id, '_iuv_votes', true) ?: 0;
$votes++;
update_post_meta($post_id, '_iuv_votes', $votes);
$voted_users[] = $user_id;
update_post_meta($post_id, '_iuv_voted_users', $voted_users);
wp_send_json_success(array('votes' => $votes));
}
add_action('wp_ajax_iuv_vote', 'iuv_handle_vote');
Leaderboard logic
The leaderboard pulls published gallery submissions and sorts them by vote count. This allows the highest-ranked UTV builds to be featured automatically without manually selecting winners.
$q = new WP_Query([
'post_type' => 'uploaded_image',
'post_status' => 'publish',
'posts_per_page' => 50,
'meta_key' => '_iuv_votes',
'orderby' => 'meta_value_num',
'order' => 'DESC',
'no_found_rows' => true,
'fields' => 'ids',
]);
The leaderboard plugin also accounts for ties by checking whether multiple posts share the same top vote count. This makes the ranking logic fair when more than one build has the same number of votes.
Pulling related WooCommerce products into gallery posts
Each gallery submission can store related WooCommerce product IDs. Those IDs are saved as post meta, then used on the single gallery page to pull product data directly from WooCommerce.
$product_ids = get_post_meta($pid, '_iuv_product_ids', true);
if (is_string($product_ids)) {
$product_ids = array_filter(array_map('absint', array_map('trim', explode(',', $product_ids))));
} elseif (is_array($product_ids)) {
$product_ids = array_filter(array_map('absint', $product_ids));
} else {
$product_ids = [];
}
Once the product IDs are normalized, the template uses WooCommerce functions to retrieve the product name, price, image, and permalink.
foreach ($product_ids as $prod_id) {
$product = wc_get_product($prod_id);
if (!$product) {
continue;
}
$url = get_permalink($prod_id);
$price = $product->get_price_html();
$image = get_the_post_thumbnail($prod_id, 'woocommerce_thumbnail');
echo '<a href="' . esc_url($url) . '">' . $image . '</a>';
echo '<h3>' . esc_html($product->get_name()) . '</h3>';
echo '<div>' . wp_kses_post($price) . '</div>';
}
Frontend product search for submissions
The upload form includes a product picker so users can connect the parts they used on their build. The product search runs through AJAX, checks a nonce for security, and uses WooCommerce product data to return matching products.
add_action('wp_ajax_iuv_search_products', 'iuv_search_products');
add_action('wp_ajax_nopriv_iuv_search_products', 'iuv_search_products');
function iuv_search_products() {
if (!isset($_GET['nonce']) || !wp_verify_nonce($_GET['nonce'], 'iuv_search_products')) {
wp_send_json_error('Invalid request.');
}
if (!function_exists('wc_get_products')) {
wp_send_json_error('WooCommerce is not available.');
}
$args = [
'status' => 'publish',
'limit' => 20,
'return' => 'ids',
];
$products = wc_get_products($args);
}
Filtering products by Year / Make / Model
The product picker can also filter WooCommerce products by Year, Make, and Model. This lets users find parts that match their UTV instead of searching through every product manually.
$product_ids = $wpdb->get_col($wpdb->prepare(
"SELECT DISTINCT product_id
FROM $table
WHERE %d BETWEEN year_from AND year_to
AND TRIM(make) = TRIM(%s)
AND TRIM(model) = TRIM(%s)",
$year,
$make,
$model
));
After the matching product IDs are found, the system maps variations back to their parent products and returns the product data needed for the frontend picker.
Why this approach worked well
This approach created a feedback loop between engagement and revenue. Customers are incentivized to share their builds, which generates authentic content for the brand while rewarding them with store credit. The use of WordPress custom post types and WooCommerce integration kept everything maintainable and scalable.
Future scalability
The system can be expanded with voting, leaderboards, featured builds, contests, filtering, tagging, and deeper community-driven features.

Veteran Discount Verification System
A custom veteran discount system that verifies eligibility through the VA API and applies a 10% discount directly within WooCommerce.
- VA API verification
- Guest-friendly verification flow
- WooCommerce session integration
- Automatic 10% cart discount
- No recurring third-party verification dependency
Why I built a custom verification system
Instead of relying on third-party tools like VerifyPass or ID.me, I implemented a custom solution using the Department of Veteran Affairs API. This free solution removed a recurring vendor dependency cost while keeping the experience controlled directly inside the website.
- Reduced cost: Avoided roughly $600/year in third-party verification fees.
- Full control: The form, messaging, verification flow, and discount logic could be customized for the site.
- Better UX: Customers verify through a site-controlled flow instead of relying on an external vendor experience.
How the verification flow works
- User fills out the verification form.
- Form submission is protected with a WordPress nonce.
- Inputs are sanitized before being used.
- A JSON request is sent to the VA API.
- If veteran status is confirmed, a verified session flag is stored.
- The user is redirected back to the cart with a success or failure flag.
Starting a session for verification state
The plugin starts a native PHP session so the verified status can be stored temporarily across page loads. This keeps the verification state available without requiring the customer to create an account.
if (! session_id()) {
session_start();
}
Rendering and handling the verification form
The verification form is rendered with a shortcode and includes a WordPress nonce for CSRF protection. When the form is submitted, each field is sanitized before the request is sent to the VA API.
add_shortcode('va_api_verify_form', function () {
ob_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['va_verify_nonce'])) {
if (! wp_verify_nonce($_POST['va_verify_nonce'], 'va_verify_action')) {
return '<p>Verification failed. Please try again.</p>';
}
$first_name = sanitize_text_field($_POST['first_name'] ?? '');
$last_name = sanitize_text_field($_POST['last_name'] ?? '');
$birth_date = sanitize_text_field($_POST['birth_date'] ?? '');
// Build VA API request payload here.
}
wp_nonce_field('va_verify_action', 'va_verify_nonce');
return ob_get_clean();
});
Sending the verification request to the VA API
After the form is submitted, the plugin builds a JSON payload and sends it to the VA API using WordPress’s HTTP API. The response is then checked to determine whether veteran status was confirmed.
$response = wp_remote_post($url, [
'headers' => [
'Authorization' => 'Bearer <api_key>',
'Content-Type' => 'application/json',
],
'body' => wp_json_encode($data),
'timeout' => 10,
]);
if (is_wp_error($response)) {
// Show a generic failure message.
}
$code = wp_remote_retrieve_response_code($response);
if ($code !== 200) {
// Handle failed API response gracefully.
}
$body = json_decode(wp_remote_retrieve_body($response), true);
Storing confirmed veteran status
If the API response confirms veteran status, the plugin stores a simple session flag. This avoids storing unnecessary personal data while still allowing WooCommerce to know whether the discount should be applied.
if (isset($body['veteran_status']) && $body['veteran_status'] === 'confirmed') {
$_SESSION['verified_veteran'] = true;
$verified = true;
}
Redirecting with a success or failure flag
After verification, the customer is redirected back to the cart with a query parameter. A value of ?verified=1 means the customer was confirmed, while ?verified=0 means verification was not confirmed.
wp_redirect(wc_get_cart_url() . '?verified=' . ($verified ? '1' : '0'));
exit;
Displaying cart messaging and the verification call-to-action
The cart page checks the verification query parameter and shows either a success message, a failure message, or the “Have you served?” call-to-action if the customer has not verified yet.
add_action('woocommerce_cart_collaterals', function () {
if (isset($_GET['verified'])) {
if ($_GET['verified'] == '1') {
echo '<p>Veteran discount verified and applied.</p>';
} elseif ($_GET['verified'] == '0') {
echo '<p>We could not verify veteran status. Please try again.</p>';
}
}
if (empty($_SESSION['verified_veteran'])) {
echo '<a class="button" href="/veteran-discount/">Have you served?</a>';
}
});
Bridging PHP sessions with WooCommerce sessions
WooCommerce has its own session system, and checkout totals often recalculate through AJAX. To keep the discount active during those recalculations, I synchronized the native PHP session flag with the WooCommerce session flag.
add_action('wp', function () {
if (function_exists('WC') && WC()->session) {
if (isset($_SESSION['verified_veteran']) && $_SESSION['verified_veteran'] === true) {
WC()->session->set('verified_veteran', true);
}
if (WC()->session->get('verified_veteran') === true) {
$_SESSION['verified_veteran'] = true;
}
}
});
Applying the discount in WooCommerce
The discount is applied through WooCommerce’s woocommerce_cart_calculate_fees hook. This lets the 10% discount behave like a native cart adjustment and update correctly as WooCommerce recalculates cart and checkout totals.
add_action('woocommerce_cart_calculate_fees', function ($cart) {
if (is_admin() && ! defined('DOING_AJAX')) return;
$verified = (
(isset($_SESSION['verified_veteran']) && $_SESSION['verified_veteran'] === true) ||
(function_exists('WC') && WC()->session && WC()->session->get('verified_veteran') === true)
);
if ($verified) {
$subtotal = floatval($cart->get_subtotal());
if ($subtotal > 0) {
$cart->add_fee('Veteran Discount', -($subtotal * 0.10));
}
}
}, 20);
Clearing the discount after checkout or cart empty
The verification flag is cleared after an order is processed or when the cart is emptied. This prevents the discount from lingering beyond the intended purchase flow.
add_action('woocommerce_checkout_order_processed', function () {
if (function_exists('WC') && WC()->session) {
WC()->session->__unset('verified_veteran');
}
unset($_SESSION['verified_veteran']);
}, 20);
add_action('woocommerce_cart_emptied', function () {
if (function_exists('WC') && WC()->session) {
WC()->session->__unset('verified_veteran');
}
unset($_SESSION['verified_veteran']);
});
Why this approach worked well
This approach kept the verification flow lightweight, avoided recurring third-party vendor costs, and integrated directly with WooCommerce’s existing cart and checkout calculations. It also supported guest users while preventing the discount from persisting after checkout or an emptied cart.
Future scalability
This system could be extended to support account-based saved verification, additional eligibility programs, configurable discount percentages, admin reporting, or reusable verification status for returning customers.