Skip to main content Accessibility Feedback

Conditional cost of living discounts with JavaScript and some API magic

Yesterday, I added an automatic cost of living discount feature to my site for Vanilla JS Pocket Guides.

If you visit the site from a country where the salary, cost of living, and exchange range with the US make the pocket guides unfairly expensive or unaffordable, you’re offered a custom discount code and amount that brings the price inline with what someone in the US would pay relative to what they make in a year.

If you’ve been holding off because my guides are too expensive where you live hopefully this helps you out.

Today, I wanted to show you how I make this all work.

Note: If your country doesn’t show up, please email me! So far, I’ve only added countries that people have emailed me about.

Also, I hope this is obvious, but FAKE_CODE won’t really work, and people in the US don’t get offered a cost-of-living discount.

Getting a visitor’s location #

Free GEO IP lets you get a person’s geographic information based on their IP address.

I use this to get a visitor’s ISO code (the two digit country code).

Some boring sever-side stuff #

My store is powered by Easy Digital Downloads (EDD).

I wrote a custom plugin that adds a new post type: Pricing Parity. When I create a new pricing parity discount, I choose a country and discount code from a set of dropdown menus. The plugin automatically populates the list of discount codes available in EDD.

The plugin also provides a short code I can use to generate my discount message.

It accepts variables for things like the country name, the discount code, and more, so I can quickly change my discount message on the fly if I want to.

[pricing_parity]Hi! Looks like you're from {{country}}, where my Vanilla JS Pocket Guides might be a bit expensive. You can use the code {{code}} at checkout to take {{amount}} off any of my guides. Cheers![/pricing_parity]

Custom JavaScript #

I don’t just drop the shortcode on my site and call it a day, though.

Since everyone’s cost-of-living discount is different, I can’t cache the discount shortcode, and that would add a lot of latency to page loads.

Instead, I drop the shortcode onto it’s own page, and use Ajax to get the content and drop it into the page when it’s ready.

I only display it on product and checkout pages, but I grab the discount message when someone first visits any page and store it with sessionStorage. That way, it’s immediately displayed when they hit a product sales page.

The code that makes it all work #

Here’s the custom JavaScript.

/**
 * Load pricing parity message
 */
;(function (window, document, undefined) {

    'use strict';

    // Render the pricing parity message
    var renderPricingParity = function (data) {

        // Make sure we have data to render
        if (!data) return;

        // Only render on sales pages
        if (!/\/guides\//.test(window.location.pathname) && !/\/checkout\//.test(window.location.pathname)) return;

        // Get the nav
        var nav = document.querySelector('header');
        if (!nav) return;

        // Create container
        var pricing = document.createElement('div');
        pricing.id = 'pricing-parity';
        pricing.className = 'bg-muted padding-top-small padding-bottom-small';
        pricing.innerHTML = data;

        // Insert into the DOM
        nav.parentNode.insertBefore(pricing, nav);

    };

    // Get the pricing parity message via Ajax
    var getPricingParity = function () {

        // Set up our HTTP request
        var xhr = new XMLHttpRequest();
        if (!('responseType' in xhr)) return;

        // Setup our listener to process compeleted requests
        xhr.onreadystatechange = function () {
            // Only run if the request is complete
            if ( xhr.readyState !== 4 ) return;

            // Process our return data
            if ( xhr.status === 200 ) {

                // Get the content and render it
                var pricing = xhr.response.querySelector('#pricing-parity-content');
                if (!pricing) return;
                renderPricingParity(pricing.innerHTML);

                // Save the content to sessionStorage
                sessionStorage.setItem('gmt-pricing-parity', pricing.innerHTML);

            }
        };

        // Create and send a GET request
        xhr.open('GET', '/pricing-parity');
        xhr.responseType = 'document';
        xhr.send();

    };

    // Don't run on the pricing parity page itself
    if (document.querySelector('#pricing-parity-content')) return;

    // Get and render pricing parity info
    var pricing = sessionStorage.getItem('gmt-pricing-parity');
    if (typeof pricing === 'string') {
        renderPricingParity(pricing);
    } else {
        getPricingParity();
    }

})(window, document);

I’m doing a few things here that I want to point out.

XHR.responseType #

I’m using the responseType property of XMLHttpRequest (XHR) to get my pricing parity page as a document. This let’s me search for content inside it with querySelector().

The response type property is only supported in IE10 and up, so I do a quick feature check before running it.

// Set up our HTTP request
var xhr = new XMLHttpRequest();
if (!('responseType' in xhr)) return;

On success, I can grab my #pricing-parity-content message with querySelector() and grab it’s contents with innerHTML.

// Get the content and render it
var pricing = xhr.response.querySelector('#pricing-parity-content');
if (!pricing) return;
renderPricingParity(pricing.innerHTML);

sessionStorage #

I also save my message to sessionStorage so that I only have to make the Ajax call once. After that, I can just pull the discount text directly from storage.

// Save the content to sessionStorage
sessionStorage.setItem('gmt-pricing-parity', pricing.innerHTML);

When the script first loads, I check to see if there’s an entry in sessionStorage, and if so, immediately run my renderPricingParity() method.

// Get and render pricing parity info
var pricing = sessionStorage.getItem('gmt-pricing-parity');
if (typeof pricing === 'string') {
    renderPricingParity(pricing);
} else {
    getPricingParity();
}

You’ll notice I’m checking if it’s a string, and not whether or not it exists. If there was no discount, its saved as an empty string, and if (pricing) {} would fail, triggering another, unnecessary Ajax call.

Creating a new element #

I use document.createElement to create a new div, and insertBefore() to inject it into the DOM. I add a handful of properties specific to my layout to give it some style.

// Create container
var pricing = document.createElement('div');
pricing.id = 'pricing-parity';
pricing.className = 'bg-muted padding-top-small padding-bottom-small';
pricing.innerHTML = data;

// Insert into the DOM
nav.parentNode.insertBefore(pricing, nav);

Only running it on sales pages #

Before rendering anything, I run a quick check to make sure the page is a product or checkout page.

I’m using a simple regex pattern to check for /guides/ or /checkout/ in the URL pathname. If it contains either of those, it’s a sales page. Otherwise, it’s not and I can bail.

// Only render on sales pages
if (!/\/guides\//.test(window.location.pathname) && !/\/checkout\//.test(window.location.pathname)) return;

If you live somewhere with a cost-of-living that makes buying my guides expensive, I hope this makes things a bit easier for you.


🔥 Hot off the press! I just launched a new pocket guide. Learn about ES6 arrow functions, let and const, function hoisting, and more.

Have any questions or comments about this post? Email me at chris@gomakethings.com or contact me on Twitter at @ChrisFerdinandi.

Get Daily Developer Tips