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);
}
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:
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!
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:
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:
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.