Js-buy-sdk: objectsEqual function doesn't work - when adding Variants with Properties

Created on 5 Oct 2016  路  6Comments  路  Source: Shopify/js-buy-sdk

The scenario is adding a variant with properties to the cart. If you add a variant with no properties first, and then try to add the same variant again but with properties, then adding to the cart fails, and an error is thrown.

But if you add the variant WITH properties first, then adding the variant without the properties afterwards works fine.

The reason is that the objectsEqual function does not work if you use an empty object (ie no properties) for the first parameter since the code tries to loop through the keys of the first object without caring about the number of keys of the second object.

> objectsEqual( {}, {test:1} )
true
> objectsEqual( {test:1}, {} )
false

objectsEqual will also give a wrong answer in the case where the 2nd object has all the same properties as the first object, but also has additional properties:

> objectsEqual( {a:1} , {a:1, b:2} )
true
> objectsEqual( {a:1, b:2} , {a:1} )
false

The following works for me as a drop-in replacement, though I haven't tested dates (the original function has some extra logic related to Date objects). Would be nice to see a fix make it to the CDN version though.

function objectsEqual(x, y) {
      return (x && y && typeof x === 'object' && typeof y === 'object') ?
      (Object.keys(x).length === Object.keys(y).length) &&
        Object.keys(x).every(function(key) {
                return objectsEqual(x[key], y[key]);
        }, true) : (x === y);
  }
bug

Most helpful comment

@keslert I've lost admin access to the client store I used it on so can't access the actual source, but conceptually it goes like this:

  • Modify the Checkout button code to test to see if there are any products with line item properties, and if NOT, just use the default behaviour
  • If there are line items with custom properties, then serialize the entire cart contents (eg quantity, variant_id, and [properties] array), using any url-safe serialization technique you prefer
  • redirect the browser to a page on the Shopify-hosted site, with the serialized data in the query-string. This could be a product page or just about any other page, but ideally is a mostly-blank page with some HTML that says something like "Please wait... redirecting to checkout".
  • On the Shopify-hosted page, add some Javascript that checks for the special query string. If found, it decodes it, and then one-by-one adds items to the cart using the regular Shopify JS API (which can't be done from a remote site/other domain). That API does support line-item properties, so that's how you add the items to the cart
  • Finally, when all products are added to the cart, redirect the browser to the standard checkout page.

Actually, I realized that I don't need admin access to get to the client-side JS (duh), so here's some snippets to save time:

//elsewhere...
config = { domain: 'your-store-here.myshopify.com' };

//handler for Checkout button in cart on remote site
//note, I used the 'cart' page as my destination for receiving the custom lineitems
$('.btn--cart-checkout').on('click', function() {
        var baseUrl = 'https://' + config.domain + '/cart';
        var variantPath = [];
        var hasProps = false;

        //check for existence of line items with custom properties
        if (cart.lineItems && cart.lineItems.length > 0) {
            for (var key in cart.lineItems) {
                var item = cart.lineItems[key].attrs;
                variantPath.push([item.variant_id, item.quantity, item.properties]);
                if (!$.isEmptyObject(item.properties)) {
                    hasProps = true;
                }
            }

            if (hasProps) {
                //this is the custom handler for carts with items that have custom properties
                var variantOutput = encodeURIComponent(JSON.stringify(variantPath));
                window.open(baseUrl + '?p=' + variantOutput, '_self');
            } else {
                //this is the standard handler
                var variantOutput = variantPath.map(function(item) {
                    return item[0] + ':' + item[1];
                });
                var query = 'api_key=' + config.apiKey;
                if (typeof ga === 'function') {
                    var linkerParam = void 0;
                    window.ga(function(tracker) {
                        linkerParam = tracker.get('linkerParam');
                    });
                    if (linkerParam) {
                        query += '&' + linkerParam;
                    }
                }
                window.open(baseUrl + '/' + variantOutput + '?' + query, '_self');
            }
        }
    });

And here's the javascript for the receiving custom page on your Shopify store. This is just inline in the page, in the example above I added it to the /cart page. If you want to disable browsing on the Shopify hosted store entirely, then you'll want to have this page redirect to your external site if anyone gets there by accident without a query-string payload of products to checkout with.

$(function() {

     //set up the queue used to add products to the cart asynchronously, one at a time
     Shopify.queue = [];
     Shopify.moveAlong = function() {
         // If we still have requests in the queue, let's process the next one.
         if (Shopify.queue.length) {
           var request = Shopify.queue.pop();
           jQuery.post('/cart/add.js', request, Shopify.moveAlong, 'json');
         }
         // If the queue is empty, we will redirect to the checkout page for payment
         else {
           window.top.location.href = '/checkout';
         }
    };

    //function to grab all the line items from the query string
    function getUrlVars() {
        var vars = [], hash;
        var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
        for(var i = 0; i < hashes.length; i++){
            hash = hashes[i].split('=');
            vars.push(hash[0]);
            vars[hash[0]] = hash[1];
        }
        return vars;
    }

    qs = getUrlVars();

    //I named the serialized data parameter 'p' so "qs.p" fetches that text
    if (qs.p && qs.p.length > 0){
      var payload = JSON.parse(decodeURIComponent(qs.p));
      for (var i =0; i < payload.length; i++){
        Shopify.queue.push({
          id: payload[i][0],
          quantity: payload[i][1],
          properties: payload[i][2]
        });
      }

      //clear the existing cart, and when done start adding the queued line items
      $.ajax({
        type: "POST",
        url: '/cart/clear.js',
        data: '',
        dataType: 'json',
        success: function() { 
            Shopify.moveAlong();
        },
        error: function(XMLHttpRequest, textStatus) {
          console.log('error');
        }
      });
      forwarded = true;
    } else {
      //on-load behaviour for when no valid query-string data is present, eg redirect to another page
    }
  });

All 6 comments

Also, if you're curious about why I'm using this SDK to add line items with Properties even though checkoutUrl doesn't support properties yet (see #105 )....

I have a custom handler on the Checkout button of the cart that determines if there are any lineitems with Properties, and if so instead of using checkoutUrl to create a cart permalink, it instead redirects to a page on my shop with all the cart data dumped as URL query string data-- and then JS on the shop page decodes the query string and populates a cart with the variants and properties via the regular API, and then redirects the browser to the checkout.

Hackish but it works!

Hey! Great find! Also, I'm glad you found a way to work with line item properties.

I'll take a look at the function and figure out if there's a better way to do this.

Thanks again for digging so deep!

@daveachuk Can you elaborate more on your solution? I need to do something similar and while hacky, it sounds like a straight forward approach.

@keslert I've lost admin access to the client store I used it on so can't access the actual source, but conceptually it goes like this:

  • Modify the Checkout button code to test to see if there are any products with line item properties, and if NOT, just use the default behaviour
  • If there are line items with custom properties, then serialize the entire cart contents (eg quantity, variant_id, and [properties] array), using any url-safe serialization technique you prefer
  • redirect the browser to a page on the Shopify-hosted site, with the serialized data in the query-string. This could be a product page or just about any other page, but ideally is a mostly-blank page with some HTML that says something like "Please wait... redirecting to checkout".
  • On the Shopify-hosted page, add some Javascript that checks for the special query string. If found, it decodes it, and then one-by-one adds items to the cart using the regular Shopify JS API (which can't be done from a remote site/other domain). That API does support line-item properties, so that's how you add the items to the cart
  • Finally, when all products are added to the cart, redirect the browser to the standard checkout page.

Actually, I realized that I don't need admin access to get to the client-side JS (duh), so here's some snippets to save time:

//elsewhere...
config = { domain: 'your-store-here.myshopify.com' };

//handler for Checkout button in cart on remote site
//note, I used the 'cart' page as my destination for receiving the custom lineitems
$('.btn--cart-checkout').on('click', function() {
        var baseUrl = 'https://' + config.domain + '/cart';
        var variantPath = [];
        var hasProps = false;

        //check for existence of line items with custom properties
        if (cart.lineItems && cart.lineItems.length > 0) {
            for (var key in cart.lineItems) {
                var item = cart.lineItems[key].attrs;
                variantPath.push([item.variant_id, item.quantity, item.properties]);
                if (!$.isEmptyObject(item.properties)) {
                    hasProps = true;
                }
            }

            if (hasProps) {
                //this is the custom handler for carts with items that have custom properties
                var variantOutput = encodeURIComponent(JSON.stringify(variantPath));
                window.open(baseUrl + '?p=' + variantOutput, '_self');
            } else {
                //this is the standard handler
                var variantOutput = variantPath.map(function(item) {
                    return item[0] + ':' + item[1];
                });
                var query = 'api_key=' + config.apiKey;
                if (typeof ga === 'function') {
                    var linkerParam = void 0;
                    window.ga(function(tracker) {
                        linkerParam = tracker.get('linkerParam');
                    });
                    if (linkerParam) {
                        query += '&' + linkerParam;
                    }
                }
                window.open(baseUrl + '/' + variantOutput + '?' + query, '_self');
            }
        }
    });

And here's the javascript for the receiving custom page on your Shopify store. This is just inline in the page, in the example above I added it to the /cart page. If you want to disable browsing on the Shopify hosted store entirely, then you'll want to have this page redirect to your external site if anyone gets there by accident without a query-string payload of products to checkout with.

$(function() {

     //set up the queue used to add products to the cart asynchronously, one at a time
     Shopify.queue = [];
     Shopify.moveAlong = function() {
         // If we still have requests in the queue, let's process the next one.
         if (Shopify.queue.length) {
           var request = Shopify.queue.pop();
           jQuery.post('/cart/add.js', request, Shopify.moveAlong, 'json');
         }
         // If the queue is empty, we will redirect to the checkout page for payment
         else {
           window.top.location.href = '/checkout';
         }
    };

    //function to grab all the line items from the query string
    function getUrlVars() {
        var vars = [], hash;
        var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
        for(var i = 0; i < hashes.length; i++){
            hash = hashes[i].split('=');
            vars.push(hash[0]);
            vars[hash[0]] = hash[1];
        }
        return vars;
    }

    qs = getUrlVars();

    //I named the serialized data parameter 'p' so "qs.p" fetches that text
    if (qs.p && qs.p.length > 0){
      var payload = JSON.parse(decodeURIComponent(qs.p));
      for (var i =0; i < payload.length; i++){
        Shopify.queue.push({
          id: payload[i][0],
          quantity: payload[i][1],
          properties: payload[i][2]
        });
      }

      //clear the existing cart, and when done start adding the queued line items
      $.ajax({
        type: "POST",
        url: '/cart/clear.js',
        data: '',
        dataType: 'json',
        success: function() { 
            Shopify.moveAlong();
        },
        error: function(XMLHttpRequest, textStatus) {
          console.log('error');
        }
      });
      forwarded = true;
    } else {
      //on-load behaviour for when no valid query-string data is present, eg redirect to another page
    }
  });

@daveachuk This is perfect, thanks!

@daveachuk your solution is really good, work's like a charm!

Was this page helpful?
0 / 5 - 0 ratings