d
Topic
mattsahr Shopify Partner thor-studio.com
Posts:
1
Last edited August 28, 2011

Remote add to cart, revisited This post is outdated

I have seen quite a number of posts wrestling with how to let Shopify play nicely with external add-to-cart requests.  I ended up rolling my own.

There are some helpful approaches available.
http://wiki.shopify.com/Adding_to_the_Cart_from_a_remote_website

But the available help didn't provide a solution for what I needed.  Specifically, I need my REMOTE site (not the shopify site) to be able to collect an assortment of items, and then add them all at once to Shopify when it's time to "view cart." 

There is certainly an aspect of re-inventing-the-wheel involved here.  I ended up making a limited version of a shopping-cart on the REMOTE site, and then I let it hand off its data to the Shopify cart when a visitor switches sites.  I mean really, why do we need two carts?

The fact is this.  I am much, much more comfortable in Wordpress than I am in Shopify.  For this project, I needed a content management system that I understood well, with the ability to add-to-cart and stay put on the wordpress page.  Also, I am allergic to iframes, which is one of the possible ways of doing remote-cart-add, supplied by Caroline Schnapps, whose amazing commentary in the forums solved well over half of my shopify-newbie problems.
http://11heavens.com/testing-shopify-add-to-cart-functionality-from-another-website-while-staying-put
It may be that iframes work just fine, but I hate dealing with them.

I imagine that other developers are in the same boat -- they like some other system/CMS better for making websites, and they want to integrate with Shopify at a deeper level than iframes.  So I wrote up the steps it took to get here: A remote shopify cart update.  I am interested to hear if I have made some greivous security error.  I don't think so, since I am not collecting sensitive info.  A list of products and quantities, not tied to a specific customer, just linked to their session_id.  Anyway, I'd love to hear feedback.

This method has 4 pieces. 

SHOPIFY LISTENER -- liquid, jQuery, and shopify JSON API
SHOPIFY SENDER -- liquid, jQuery and a hidden POST form
REMOTE LISTENER -- session handler PHP
REMOTE SENDER -- jQuery

1.  SHOPIFY LISTENER
----------------------------
The biggest stumbling block for me was that Shopify's JSON API won't handle more than one product at a time.  If you could send to cart/add.js a list of products-and-quantities in one bundle, most of this would be unnecessary.  But because cart/add.js handles only one product at a time, the SHOPIFY LISTENER takes a GET request from the remote site, serializes the variant.id-and-quantity list, and then feeds variants one by one as POST requests to cart/add.js.  When it's done sending items to the cart, it then passes along the site visitor to their appropriate destination.  Usually, the destination is YOUR-SHOPIFY-SITE/cart, but it will handle other destinations.

The LISTENER iterates through a shopify collection and checks to see if the GET request contains those items.
{% for product in collections.everyproduct.products %}
In this case, I made a shopify collection called "everyproduct" but obviously you can tailor this to your specific needs pretty easily.  None of my products have sub-variants, so this loop is quite basic.

The loop checks for variant.id, and if it finds one in the 'key', it sends the 'value' as a quantity to the cart.  Data is controlled by the shopify collection.  This way you can send the LISTENER tons of garbage in the POST request, and it doesn't care.  It only checks for real variant.ids.

Below is the custom page template that I put on my shopify site.  It uses the {% layout none %} tag so that nothing is loaded except the javascript below.

 

{% layout none %}

<script type="text/javascript" charset="utf-8" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"&gt;&lt;/script&gt;

<script type="text/javascript" charset="utf-8">

    $(document).ready(function() {
    	cartQueue.start();
	});  // end document ready

	cartQueue = function() {
		var queue = [];
		var prodVariant = [{variant:1002004, quantity:5}]; // dummy data
		var prodIndex = 0;
		var prodQuantity = [];

			return  {  // public functions go here
				destination: 'cart',

				start: function ()  {  
				// check if we should empty cart, then move to cartQueue.collect
				  cartQueue.destination = cartQueue.getParameter('destination') || 'cart';
				  var toEmpty = cartQueue.getParameter('emptyTheCart');
				  if (toEmpty) {
					var params = {
						type: 'POST',
						url: '/cart/clear.js',
						data:  '',
						dataType: 'json',
						success: function(cart) { 
						  cartQueue.collect();
						},
						error: function(XMLHttpRequest, textStatus) {
						Shopify.onError(XMLHttpRequest, textStatus);
						}
					};
					jQuery.ajax(params);
				  } else {
					cartQueue.collect();
				  }
				},
				collect: function ()  {
					// put cart items (from cartQueue.getParameter) into the 'queue' array for 
					// sequential ajax loading via shopify cart/add.js method
					{% for product in collections.everyproduct.products %}
						prodIndex = {{ forloop.index }};
						 {% for variant in product.variants %}
							prodVariant[prodIndex] = {{ variant.id }};
							// console.log('product variant: ' + prodVariant[prodIndex]);
							prodQuantity[prodIndex] = cartQueue.getParameter(String('var'+prodVariant[prodIndex]));
							if (prodQuantity[prodIndex]) {
								queue.push(
									{ variant: prodVariant[prodIndex], quantity: prodQuantity[prodIndex] }
								);
							}
						{% endfor %}
					{% endfor %}
					// console.log('cartQueue: ');
					// console.log(queue);
					cartQueue.moveAlong();
				}, // END  cartQueue.collect()

				getParameter: function ( name ){ 
					// pulls parameters from the URL string.  This fuction came from here:   
					// http://stackoverflow.com/questions/901115/get-querystring-with-jquery
					name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
					var regexS = "[\\?&]"+name+"=([^&#]*)";
					var regex = new RegExp( regexS );
					var results = regex.exec( window.location.href );
					if( results == null ) {
						return "";
					} else {
						return decodeURIComponent(results[1].replace(/\+/g, " "));
					}
				},  // END  cartQueue.getParameter(name)

				moveAlong: function() {
					//  copied from [http://wiki.shopify.com/Ajax_API]
					// If we still have requests in the queue, let's process the next one.
					if (queue.length) {
						var nextItem = queue.shift();
						cartQueue.loadEmUp(nextItem.variant, nextItem.quantity);
					} else {
						if (cartQueue.destination == "home") {
                            var destUrl = "http://SHOPIFY.SITE.COM/";
							window.location.replace(destUrl);
						} else {
                            var destUrl = "http://SHOPIFY.SITE.COM/"+cartQueue.destination;
    						window.location.replace(destUrl);
						}
					}  
				}, // END  cartQueue.moveAlong()

				loadEmUp: function(variant, quantity) {
					// send the item to cart/add.js, then go back to moveAlong();
					// to see if there are more items to add.
					var quantity = quantity || 1;
					var params = {
						type: 'POST',
						url: '/cart/add.js',
						data: 'quantity=' + quantity + '&id=' + variant,
						dataType: 'json',
						success: function() { cartQueue.moveAlong(); },
						error: function(XMLHttpRequest, textStatus) {
						  Shopify.onError(XMLHttpRequest, textStatus);
						}
					};
					jQuery.ajax(params);
				}  // END  cartQueue.loadEmUp(variant, quantity)
				
			};  // END returned (public) functions

	}();  // END cartQueue

</script>

 

 

2.  SHOPIFY SITE -- SENDER

The "Sender" consists of two pieces.  In the footer of every Shopify page, I include this hidden form.

 

<form action="http://REMOTE-SITE.COM/cart/session.php" method="post" id="goBackForm" style="display:none;">
	<input type="hidden" name="fromShopify" value="yes" />
	{% for item in cart.items %}
		<input type="hidden" name="var{{item.variant.id}}" value="{{ item.quantity }}" />
	{% endfor %}
	<input type="submit" value="GO TO SESSION" />
</form>

This way I always have the cart contents ready to send out to the remote site.  Then I include this jQuery event listener.

 

$(document).ready(function() {
	$('a.goBack').click( function(event) {
		event.preventDefault();
		var goWhere = $(this).attr('href');
		$('#goBackForm').prepend('<input type="hidden" name="targetUrl" value="'+goWhere+'">');
		$('#goBackForm').submit();
	});
});  // end document ready

For any link on the Shopify site that points to REMOTE-SITE.COM, I add a class "goBack" to it.

<a class="goBack" href="REMOTE-SITE.COM/example.html" >to the remote site!</a>

The jQuery intercepts the click, and instead sends <form id="goBackForm"> to the REMOTE SITE LISTENER.

 

3.  REMOTE SITE -- LISTENER
------------------------
This PHP listener plays nicely with a standard WordPress install.  It's a basic session handler.  I'm sure it could be easily remade in other server-friendly languages.

REMOTE-SITE.COM/cart/session.php

 

<?php
	if( $_POST['killSession'] == 'yes' ) {
		session_start();
		session_destroy();
		echo json_encode('sessionKILLED');
		exit;
	}
	// POST from shopify
	if( $_POST['fromShopify'] == 'yes' ) {
		session_start();
		session_destroy();
		session_start();

		foreach($_POST as $key => $value){
			$_SESSION[$key] =  $value;
		}

		if ($_SESSION['submit']) {
			unset($_SESSION['submit']);
		}
		if ($_SESSION['fromShopify']) {
			unset($_SESSION['fromShopify']);
		}

		if ($_SESSION['targetUrl']) {
			$targUrl = 'Location: '.$_SESSION['targetUrl'];
			unset($_SESSION['targetUrl']);
		} else {
			$targUrl = 'Location: http://SHOPIFY.SITE.com';
		}
		header($targUrl);
		exit;

	} else {
		session_start();
		// POST from jQuery Ajax
		if( isset($_POST['queryIndex']) ){
			foreach($_POST as $key => $value){
				$postValue = intval($value);
				$sessionValue = intval($_SESSION[$key]);
				$_SESSION[$key] = intval( $sessionValue + $postValue );
				if ( $_SESSION[$key] < 1 ) { 
					unset($_SESSION[$key]); 
				}
			}
			echo json_encode($_SESSION);
		}
		$_POST = array();
  	}
?>

 

4. REMOTE SITE -- SENDER
==================================
The "Sender" consists of a few javascript functions that keep the REMOTE-LISTENER updated with additions and subtractions to the remote-site shopping cart.  Calling it a "shopping cart" is rather a stretch.  It's the simplest key-value list.  The key is a 'variant.id' and the value is an integer, how many in the cart.  No prices are tracked, no personal data is attached.

When a link is clicked with a class <a class="goToShopify">, the SENDER javascript captures the click, and sends instead a POST request back to the shopify site, with all the key-value pairs.

Any page on the REMOTE site that needs the shopping cart uses a few CSS classes to make links able to add and remove products.

THE HTML

 

<a class="addToCart" data-prodvar="var123456789" href="#">
	add to cart
</a>
<a class="removeFromCart" data-prodvar="var123456789" href="#">
	remove from cart
</a>
<span class="cartCount cartvar123456789">
	0
</span>
items in cart

For these <a> tags, each product has an "add to cart" link with its own unique [data-prodvar="var12345...."].  That unique number was generated by the Shopify system.
http://wiki.shopify.com/Variant

I added the prefix "var" in front of the the Shopify "variant.id' number.  I am not a great PHP coder, and my session-handler [/cart/session.php] was having a horrible time dealing with keys as numbers, so I stuck "var" on the front of each, to insure that it parsed as a string. 

For my limited system, I  hand-picked the products I wanted to have available on my REMOTE site, and I copied those variant.id numbers.  I'm sure there's a way to fetch a JSON list of variant id's and titles, etc, but I didn't get that far automated.

Here's the javascript to inlcude on your REMOTE site.

 

 

$(document).ready(function(){
	// ===== ON DOC READY: Send a POST request to /cart/session.php
	// ===== to get any items already in the shopping cart
	cCart.init();
	// ===== CLICK: ADD AN ITEM TO THE CART ==========
	$('.addToCart').click(function(event) {
		event.preventDefault();
		var prodVar = $(this).data('prodvar');
		cCart.update(prodVar,"1");
	});
	// ======= CLICK: REMOVE ITEMS FROM CART ==========
	$('.removeFromCart').click(function(event) {
		event.preventDefault();
		var prodVar = $(this).data('prodvar');
		var prodQty = $('.cartCount.cart'+prodVar).html();
		prodQty = "-" + prodQty;
		$('.cartCount.cart'+prodVar).html("0");
		cCart.update(prodVar,prodQty);
	});
	// ======= CLICK: GO TO THE SHOPIFY SITE ==========	
	$('.goToShopify').click( function(event) { 
		event.preventDefault();
		var destination = $(this).data('destination') || 'home';
		cCart.toShopify(destination); 
	});
});   //  END DOCUMENT READY
cCart = function() {
	var cartShopify = 'http://YOUR-SHOPIFY-STORE.COM/pages/cart-process-engine'
	var cartSession = 'http://REMOTE-SITE.COM/cart/session.php';
	var cartData = { queryIndex: 0 };
	var itemsTotalQty = 0;
	return  {

		init: function() {
			var cartQuery = {};
			cartQuery['queryIndex'] = 1;
			$.ajax({
				type: 'POST',
				data: cartQuery,
				url: cartSession,
				// dataType: "json",
				success: function(data){
					// console.log(data);
					cartData = jQuery.parseJSON(data);
					cCart.display();
				},
				error:function (xhr, ajaxOptions, thrownError){
					// console.log(xhr);
				}  
			});
		}, // END cCart.init()

		update: function(variant, quantity) {
			var cartQuery = {};
			cartQuery['queryIndex'] = 1;
			cartQuery[ variant ] = quantity;
			$.ajax({
				type: 'POST',
				data: cartQuery,
				url: cartSession,
				success: function(data){
					cartData = jQuery.parseJSON(data);
					cCart.display();
				},
				error:function (xhr, ajaxOptions, thrownError){
					// console.log(xhr);
				}  
			});
		}, // END cCart.update(variant, quantity)
		
		display: function() {
			var itemsTotalQty = 0;
			for(var prop in cartData) {
				if (prop.indexOf("var") != -1) {
					$('.cartCount.cart'+prop).html(cartData[prop]);
					itemsTotalQty = parseInt( itemsTotalQty +  parseInt( cartData[prop]) );
					if ( (parseInt(cartData[prop])) > 0 ) {
						if ( $('.cart'+prop).hasClass('hiddenCart') ) {
							$('.cart'+prop).removeClass('hiddenCart').show();
						}
					}
				}
			}
			$('.cartDetails .itemsInCart').html(itemsTotalQty);
		}, // END cCart.display()

		toShopify: function(destination) {
			destination = destination || 'cart';
			var toEmpty = "nope";
			$('body').append('<form id="cartForm" style="display:none;" action="'+cartShopify+'" method="get"></form>');
			$('#cartForm').append('<input type="submit" value="GO TO SHOPIFY" />');
			for(var prop in cartData) {
				if ( prop.indexOf("var") != -1) {
					toEmpty = "yep";
			    	$('#cartForm').prepend(prop+': <input id="'+prop+'" name="'+prop+'" value="'+cartData[prop]+'" ><br />');
			    }
			};
			if (toEmpty == "yep") {
				$('#cartForm').append('<input name="emptyTheCart" value="yes" />');
			};
			$('#cartForm').append('<input name="destination" value="'+destination+'" />');			
			$('#cartForm').submit();
		}, // END cCart.toShopify(destination)

		kill: function() {
			var killQuery = {};
			killQuery['killSession'] = 'yes';
			$.ajax({
				type: 'POST',
				data: killQuery,
				url: cartSession,
				//		dataType: "json",
				success: function(data){
					cartData = { queryIndex: 0 };
				},
				error:function (xhr, ajaxOptions, thrownError){
					// console.log(xhr.statusText);
				}  
			});
		}  // END cCart.kill()

	};  // END returned (public) functions
}();
/* ===== END CCART =============== */

And that's all.  Having pasted all this in, I'm thinking there MUST be an easier way to do this.  But... this way works.

 

 

i
Replies
jori Member
Posts:
1
September 06, 2011

I am in need of help, and you seem like the guy! We are setting up a shopping cart so people can buy corporate gifts. In theory, one person could put 10 items in their cart, and have them shipped to 10 different addresses. I know this post isn't exactly pertaining to my issue, but it seems to be in the ballpark, and if you know of a solution, I would love to hear from you. 

Owen Hoskins Shopify Partner owenhoskins.com
Posts:
2
December 08, 2011

Hi Matt,

I am in a similar boat as you here. I was wondering if you had further explored getting Shopify varient.ids etc. into Wordpress in an automated fashion. This plugin says it lets you import your products from a CVS file, would this method help you obtain the varient.ids?

http://wordpress.org/extend/plugins/shopify/

Thanks for sharing your work!

 

Log in or sign up for an account to reply.

This thread has been closed! You will not be able to reply.