Apply, Verify, and Validate Shopify Discount Codes

Shopify doesn't have an API to verify discount, however I have found a little work-around.

By making an AJAX call to /discount/(code), Shopify will set a cookie telling the checkout to auto-apply a discount on visit to the checkout page.

Next, making an AJAX call to /checkout, we're able to parse the HTML and determine if the discount code worked!

Below is a full ES6 script I commonly use, with the addition of the new discount code.

import Promise from 'promise';

/**
 * Ensure we get back JSON object from an XHR request.
 * @param {object|string} response - XHR response data.
 * @returns {object}
 */
export function transformResponse(response) {
  return response.constructor === String ? JSON.parse(response) : response;
}

/**
 * Shopify API.
 * @class
 */
class Shopify {
  /**
   * Handles the base URL.
   * @returns {string}
   */
  static get baseURI() {
    return `${window.location.protocol}//${window.location.hostname}`;
  }

  /**
   * Sends a request to cart.
   * @param {object} params - The params for the registry.
   * @param {string} params.method - The HTTP method.
   * @param {string} params.endpoint - The endpoint of the URL.
   * @param {object|undefined} params.data - The data to send to the endpoint.
   * @param {string} params.type - The type of request.
   * @returns {Promise}
   */
  static request({ method = 'GET', endpoint, data, type = 'json' }) {
    return new Promise((resolve, reject) => {
      // Generate the request object
      const xhr = new XMLHttpRequest();

      let adjustedEndpoint = endpoint;
      if (method === 'GET') {
        adjustedEndpoint += `?${Math.random().toString(36).substr(2, 10)}`;
      }

      // Build the request
      xhr.open(method, `${Shopify.baseURI}${adjustedEndpoint}`, true);
      xhr.responseType = type;
      if (type === 'json') {
        xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
      }

      // Resolve or rejust the promise, passing XHR to the promise
      xhr.onload = () => {
        if (xhr.status === 200) {
          resolve(xhr);
        } else {
          reject(xhr);
        }
      };

      // Send the data
      xhr.send(data ? JSON.stringify(data) : null);
    });
  }
}

/**
 * Shopify Checkout API.
 * @class
 */
export class Checkout extends Shopify {
  /**
   * Gets the checkout markup.
   * @returns {Promise}
   */
  static checkout() {
    return Checkout.request({ endpoint: '/checkout', type: 'text' }).then(xhr => xhr.responseText);
  }

  /**
   * Applies a discount via URL.
   * @param {string} discount - The discount code to apply.
   * @returns {Promise}
   */
  static applyDiscount(discount) {
    /*
     * Use HEAD so we don't follow redirects.
     * Shopify seems to set some sort of server var to tell checkout
     * to apply the discount (not the discount_code cookie either).
     */
    return Checkout.request({ method: 'HEAD', endpoint: `/discount/${discount}` });
  }

  /**
   * Verifies the discount code.
   * @param {string} discount - The discount code to verify.
   * @returns {Promise}
   */
  static verifyDiscount(discount) {
    return Checkout.checkout().then((response) => {
      const valid = response.indexOf('data-discount-success') > -1;
      if (valid) {
        // Parse the DOM, easy way
        const result = {
          code: response.match(/<span class="applied-reduction-code__information">(.*)<\/span>/)[1].trim(),
          discount: response.match(/data-checkout-discount-amount-target="([0-9]+)"/)[1],
          type: response.match(/data-discount-type="(.*)"/)[1],
        };

        if (result.code.toUpperCase() === discount.toUpperCase()) {
          // Same code, all good
          return result;
        }
      }

      // Not a valid code
      return false;
    });
  }
}

/**
 * Shopify Cart API.
 * @class
 */
export class Cart extends Shopify {
  /**
   * Get the cart object.
   * @param {boolean} cachedVersion - Cached cart or not.
   * @returns {Promise}
   */
  static get(cachedVersion = false) {
    return new Promise((resolve, reject) => {
      if (cachedVersion && Cart.data) {
        // Send the cached version
        resolve(Cart.data);
      } else {
        // Do a new request
        return Cart.request({ endpoint: '/cart.js' }).then((xhr) => {
          const response = transformResponse(xhr.response);

          Cart.data = response;
          resolve(response);
        }).catch(xhr => reject({ response: xhr.response, status: xhr.status }));
      }
    });
  }

  /**
   * Adds an item to the cart.
   * @param {object|array} data - The data to add.
   * @returns {Promise}
   */
  static add(data) {
    if (data.constructor === Array) {
      // Sequental adding of products to prevent racing
      const tasks = [];
      data.forEach((cartData) => {
        tasks.push(() => Cart.request({ method: 'POST', endpoint: '/cart/add.js', data: cartData }));
      });
      return tasks.reduce((p, task) => p.then(task), Promise.resolve());
    }

    return Cart.request({
      method: 'POST',
      endpoint: '/cart/add.js',
      data,
    }).then(() => Cart.get());
  }

  /**
   * Updates the cart.
   * @param {object|array} updates - The data to update (id => qty | [qty, qty, ...]).
   * @param {string|undefined} notes - The note to pass along.
   * @returns {Promise}
   */
  static update(updates, note) {
    return Cart.request({
      method: 'POST',
      endpoint: '/cart/update.js',
      data: { updates, note },
    }).then(xhr => transformResponse(xhr.response));
  }

  /**
   * Updates the cart attibutes.
   * @param {object} attributes - The attribute to push.
   * @param {string|undefined} notes - The note to pass along.
   * @returns {Promise}
   */
  static attributes(attributes, note) {
    return Cart.request({
      method: 'POST',
      endpoint: '/cart/update.js',
      data: { attributes, note },
    }).then(xhr => transformResponse(xhr.response));
  }

  /**
   * Updates the cart note.
   * @param {string} note - The cart note.
   * @returns {Promise}
   */
  static note(note) {
    return Cart.request({
      method: 'POST',
      endpoint: '/cart/update.js',
      data: { note },
    }).then(xhr => transformResponse(xhr.response));
  }

  /**
   * Changes a cart item.
   * @param {object} data - The data to change.
   * @returns {Promise}
   */
  static change(data) {
    return Cart.request({
      method: 'POST',
      endpoint: '/cart/change.js',
      data,
    }).then(xhr => transformResponse(xhr.response));
  }

  /**
   * Removes an item from the cart.
   * @params {number} id - The ID to remove.
   * @returns {Promise}
   */
  static remove(id) {
    const data = {};
    data[id] = 0;
    return Cart.update(data);
  }

  /**
   * Removes an item from the cart by line index.
   * @params {number} line - The line index to remove.
   * @returns {Promise}
   */
  static removeByLine(line) {
    return Cart.change({ line, quantity: 0 });
  }

  /**
   * Clears the cart
   * @returns {Promise}
   */
  static clear() {
    return Cart.request({
      method: 'POST',
      endpoint: '/cart/clear.js',
    });
  }
}

/**
 * Shopify Product API.
 * @class
 */
export class Product extends Shopify {
  /**
   * Get the product by handle.
   * @param {string} handle - The product handle.
   * @returns {Promise}
   */
  static get(handle) {
    return this.request({ endpoint: `/products/${handle}.json` }).then((xhr) => {
      return transformResponse(xhr.response).product;
    });
  }
}

/**
 * Shopify Collection API.
 * @class
 */
export class Collection extends Shopify {
  /**
   * Get the collection by handle.
   * @param {string} handle - The collection handle.
   * @returns {Promise}
   */
  static get(handle) {
    return this.request({ endpoint: `/collections/${handle}.json` }).then((xhr) => {
      return transformResponse(xhr.response).collection;
    });
  }

  /**
   * Get the products in a collection.
   * @param {string} handle - The collection handle.
   * @returns {Promise}
   */
  static getProducts(handle) {
    return this.request({ endpoint: `/collections/${handle}/products.json` }).then((xhr) => {
      return transformResponse(xhr.response).products;
    });
  }
}

class Checkout contins the good stuff. The reason a HEAD request is done on the /discount URL is to prevent a redirect which Shopify issues, it would be a waste of a call.

Now, saving this as ./api/shopify.js, you can use as example:

import { Checkout } from './api/shopify';

// Or create your own input box, watch for the input from the customer, then apply the discount...
const code = 'LD287';
Checkout.applyDiscount(code).then(() => Checkout.verifyDiscount(code)).then((result) => {
    if (!result) {
        console.log('Discount failed, not a good code');
    }
    
    console.log(result.discount); // Returns how much discount, and it worked!
}

You could achieve similar with jQuery/Zepto, etc. Just exact the methodology. Hope it helps! Here's an example of it in action:

promo-verify

Show Comments