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.