Flex Rock UTV Performance Additions

FR Home

About This Project

Custom development work for Flex Rock UTV Performance’s website which includes WooCommerce features, user experience improvements, loyalty systems, and internal business tools.

Flex Rock Logo

Features Added

Spinning Coupon Wheel

Gamified checkout experience with server-controlled randomized rewards.

Rewards

Flex Rock Bucks Loyalty

Complete rewards system: signup bonuses, purchase rewards, and gallery submissions.

Gallery

Customer UTV Image Gallery

A system built for customers to post their rides and engage with others in the community.

VA form

Veteran Discount System

Secure VA API verification with automatic 10% discount at checkout.

Technical Notes

Server-Controlled Spinning Coupon Wheel

To make checkout more engaging, I built a custom spinning coupon wheel that gives customers a chance to win discounts or free rewards. The wheel looks random to the customer, but the winning prize is selected securely on the server before the animation starts.

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)’;

If you were to plug in all the variables into the delta calculation this is what the final equation would be: (360 x 6) + (270 – ((60 x 0) + (60 / 2))) – 0. Here is a breakdown of why that happens:

  • (360 × 6): Adds multiple full rotations so the wheel spins six times for visual effect. This does not change the final position – it just makes the animation feel smoother.
  • (60 × 0) + (60 / 2): Calculates the center of the winning slice. With 6 slices, each slice is 60°, and index 0 lands at 30° (the center of the first slice).
  • 270 – 30: Determines how far the wheel needs to rotate so the slice’s center lines up with the pointer at the top (270° in CSS).
  • – 0: Adjusts for the wheel’s current rotation so the math works correctly no matter where the spin starts.
  • Final result: The wheel spins multiple full rotations, then lands exactly with the selected slice aligned under the pointer.
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.

Connecting the wheel to WooCommerce checkout and orders

The spinning wheel connects into WooCommerce through a few key hooks. These hooks handle loading the wheel on the frontend, processing the spin through AJAX, applying the prize during cart/checkout total calculation, and carrying free prize data into the final order.

// Load wheel assets and render the wheel UI
add_action('wp_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('wp_footer', [$this, 'render_wheel_ui']);

// Handle the spin and prize claim through AJAX
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']);

// Apply spin discounts during WooCommerce total calculation
add_action('woocommerce_cart_calculate_fees', [$this, 'apply_discounts_and_prizes'], 20, 1);

// Make free prize products $0 when flagged
add_action('woocommerce_before_calculate_totals', [$this, 'zero_price_for_free_tee'], 10, 1);

// Carry free prize flag into the final order item
add_action('woocommerce_checkout_create_order_line_item', [$this, 'carry_free_flag_to_order_item'], 10, 4);

// Show spin result in order emails/admin order meta
add_filter('woocommerce_email_order_meta_fields', [$this, 'email_order_meta_spin_line'], 10, 3);

This made the feature work with WooCommerce’s normal cart and checkout flow instead of forcing a separate checkout process. The customer can spin and claim a prize on the frontend, while WooCommerce still handles totals, line items, order creation, and email output through its existing hooks.

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

I built a custom rewards system that lets customers earn and redeem Flex Rock Bucks through WooCommerce account creation, purchases, and approved gallery submissions. The goal was to encourage customers to create accounts, complete purchases, and stay engaged with the brand after checkout.

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. This helped connect the rewards system to both sales activity and community engagement.

  • 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.

Veteran Discount Verification System

I built a custom veteran discount system that verifies eligibility through the VA API and applies a 10% discount directly within WooCommerce. This avoided the need for third-party verification subscriptions while giving the business full control over the verification flow, user experience, and checkout integration.

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 Affair’s 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

The system uses a frontend shortcode form where users enter the details required for veteran verification. On submission, the form data is sanitized, sent to the VA API, and the response determines whether the discount should be unlocked.

  • 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;

This makes it easy to show a clear success or failure message on the cart page without exposing the full API response to the user.

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.

SKILLS & TECH
PHP • CSS • JavaScript • jQuery • WordPress • Elementor • ACF