Headless Shopping Carts: Carts Users, and IDs
You want to avoid abandoned shopping carts. (Photo via LookAfterYourself)
My book Take My Money: Accepting Payments on the Web is about — wait for it — accepting payments on the web. Although the thrust of the book is dealing with all the complexity of managing money, we do talk about the user experience of interacting with a payment process.
Specifically, the book shows how to set up a shopping cart for users to hold on to items they want to buy. However, in the book’s implementation, you need to be a logged-in user in order to put an item in the cart. A friendly neighborhood reader pointed out that was, to put it mildly, unusual. Although many web sites require you to log in before you actually purchase something, almost nobody requires you to log in just to have a shopping cart, that seems a little user-hostile.
The honest answer, of course, is that the reader is correct, but since the book is not entitled Creating Shopping Carts on the Web, I sometimes choose to simplify parts of the application that aren’t directly related, and the code is a little simpler if you require a logged in user.
It’s not that much simpler. Maybe we can cover it in a blog post.
Your basic shopping cart is very simple. In fact, the first implementation in the book isn’t even an ActiveRecord model. It’s just a list of items, and a connection to a user. When the cart becomes a set of ActiveRecord objects, that might look like so:
- ShoppingCart, with a user_id attribute
- ShoppingCartLineItem with a shopping_cart_id and product_id attribute.
So, it’s a one-to-one relationship between carts and users, and a many-to-many relationship between products and carts.
You can perhaps see why it’s easy to have a logged-in user, because a logged-in users have a unique id that is easily associated with the shopping cart.
Before the user logs in, we don’t know their id, but they do have a session and a cookie, which we can use to bind the user’s interaction to a shopping cart. I think the easiest way to do this is to create the cart, and add the cart’s ID as session[:active_shopping_cart_id] or some such. This has the benefit of guaranteeing a unique id for the cart in the session, but has the possible weakness of an easily guessable sequential identifier. An alternative would to generate a unique random id, and make it an attribute of the shopping cart, and the value you put in the session. If you hold on until the end of this post, I’ll show another way to associate carts and sessions.
Associating a user session to the cart is not the tricky part. The tricky part is that you need to be able to continue to associate the cart to the user if they log in. Otherwise, a user would lose there shopping cart on log in, which seems bad. Well, I mean, you don’t have to change the association, you could continue to use the session, but by switching over to the user id, you can keep the cart persistent over more time and multiple devices, which is a bit more user friendly.
One way to handle the switch is to associate the user in some kind of after-login hook. Another would be to make the user association part of the normal shopping cart lookup. I kind of prefer the second option, in part for blog purposes because it’s less dependent on the details of the login tool.
You could write something like this code as the cart lookup, assuming that current_user returns the current logged in user or nil:
def current_cart(session_tag) cart = ShoppingCart.find_by(session_tag: session_tag) if !cart && current_user cart = ShoppingCart.find_by(user_id: current_user.id) end if cart && current_user && cart.user_id.nil? cart.update(user_id: current_user.id) end cart end
And you have a shopping cart that will transition to the user id after the user logs in. This version is a little odd in that it’s a query method — looking up the cart — with a side effect — adding the user is to the cart. I don’t think this bothers me — the side effect should be mostly transparent, but if it bothers you, then you’d probably prefer automatically adding the user id after login in a hook method.
If you are using Rails 4.2 or up, you can use Rails Global Id as the shopping cart ID that you place in the session. Doing so gives you an ID that is easy to associate with the shopping cart, and which can be made non-sequential and hard to guess. You can even give the global id an expiration time to limit the availability of the cart. (Thanks to Bradley Schaefer for pointing this out). I haven’t completely implemented this, it might look like this.
Code to create a new shopping cart:
def new_shopping_cart cart = ShoppingCart.create global_id = cart.to_signed_global_id(expires_in: 1.day) session[:active_shopping_cart_id] = global_id.to_s end
Then the lookup would be like this:
def current cart(session_global_id) cart = GlobalID::Locator.locate_signed(session_global_id) if !cart && current_user cart = ShoppingCart.find_by( user_id: current_user.to_signed_global_id.to_s) end if cart && current_user && cart.user_id.nil? cart.update( user_id: current_user.to_signed_global_id( expires_in: 1.day).to_s) end cart end
If the expiration time has passed, the locate_signed call will return nil even if the cart is still there. (It won’t delete the cart, you’ll have to sweep that yourself). I haven’t actually tried this yet, and there might be a kink or two to work out (I’m not sure if the lookup for the user will work as stated).
And that’s how I might implement shopping carts without a logged in user. I’d still recommend requiring the user to login before payment, however.