9

The context of my challenge

I'm building a headless WordPress / WooCommerce Store.

If you're not familiar with the concept of a headless CMS, I pull the store's content (Products, and their images, text) over the WordPress / WooCommerce REST API. This way, I have the benefit of a CMS dashboard for my client whilst I get to develop in a modern language / library, in my case - React!

If possible I'd like to keep the checkout in WordPress/WooCommerce/PHP. Depending on the project I apply this code / boilerplate to I suspect that I'll have to chop and change payment gateways, and making this secure and PCI compliant will be much easier in PHP/WordPress - there's a whole host of plugins for this.

This means the entire store / front-end will live in React, with the exception of the cart in which the user will be redirected to the CMS front-end (WordPress, PHP) when they wish to complete their order.

The Challenge

This makes managing cookies for the session rather unintuitive and unorthodox. When the user is redirected from the store (React site) to the checkout (WooCommerce/PHP site) the cart session has to persist between the two sites.

Additionally, requests to WooCommerce are routed through the Node/Express server which my React client sits ons. I do this because I want to keep the WordPress address obscured, and so I can apply GraphQL to clean up my requests & responses. This issue is that in this process, the cookies are lost because my client and my CMS are communicating through a middle man (my Node server) - I require extra logic to manually manage my cookies.

Rough sketch of architecture of solution

The Code

When I attempt to add something to a cart, from an action creator (I'm using Redux for state management) I hit the api corresponding endpoint on my Node/Express server:

export const addToCart = (productId, quantity) => async (dispatch) => {
  dispatch({type: ADD_TO_CART});
  try {
    // Manually append cookies somewhere here
    const payload = await axios.get(`${ROOT_API}/addtocart?productId=${productId}&quantity=${quantity}`, {
      withCredentials: true
    });
    dispatch(addToSuccess(payload));
  } catch (error) {
    dispatch(addToCartFailure(error));
  }
};

Then on the Node/Express server I make my request to WooCommerce:

app.get('/api/addtocart', async (req, res) => {
    try {
      // Manually retrieve & append cookies somewhere here
      const productId = parseInt(req.query.productId);
      const quantity = parseInt(req.query.quantity);
      const response = await axios.post(`${WP_API}/wc/v2/cart/add`, {
        product_id: productId,
        quantity
      });
      return res.json(response.data);
    } catch (error) {
      // Handle error
      return res.json(error);
    }
});
Allan of Sydney
  • 1,410
  • 14
  • 23
  • 1
    Have you tried setting `define( 'COOKIE_DOMAIN', 'somedomain.com' );` in your `wp-config.php`, this will make sure that wordpress generates cookies in the main domain instead of subdomain and they will both share the cookies with anything extra, so no code complexity like you currently have – Tarun Lalwani Jul 15 '18 at 17:21
  • Thanks for the tip @TarunLalwani - I haven't tried it, I'll give it a go! It would be amazing if this could solve all my cookie woes. My initial hesitation would be that **A)** Whether or not WooCommerce will observe this option, and **B)** That all REST API calls from React to WordPress are done server-side (node, express), rather than client-side (react). I imagine this would impede axios (my http client) ability to automatically add relevant browser cookies to the requests I am sending to WordPress – Allan of Sydney Jul 15 '18 at 21:36
  • A) Can be dispelled - I found confirmation from the Devs that: "WooCommerce uses the same cookie settings as WordPress (e.g WordPress login cookie)" – Allan of Sydney Jul 15 '18 at 22:10
  • For B you will need to send all browser cookies in your request to WP and do the same for return cookies also – Tarun Lalwani Jul 16 '18 at 02:46
  • You're right, I have to pass down my cookies from the client (React) to the server (Node) before hitting the CMS (WooCommerce via REST), and then similarly the opposite way, setting cookies for the client from within the node server. It's complicated, but it's nearly there! – Allan of Sydney Jul 16 '18 at 10:18
  • It should not be too complicated, if you have any specific issue let me know – Tarun Lalwani Jul 16 '18 at 11:24

1 Answers1

7

With the clues given by @TarunLalwani (thanks a million!) in his comments, I've managed to formulate a solution.

Cookie Domain Setting

Since I was working with two seperate sites, in order for this to work I had to ensure they were both on the same domain, and that the domain was set in all cookies. This ensured cookies were included in my requests between the Node / Express server (sitting on eg. somedomain.com) and the WooCommerce CMS (sitting on eg. wp.somedomain.com), rather than being exclusive to the wp.somedomain subdomain. This was achieved by setting define( 'COOKIE_DOMAIN', 'somedomain.com' ); in my wp-config.php on the CMS.

Manually Getting and Setting Cookies

My code needed significant additional logic in order for cookies to be included whilst requests were routed through my Node / Express server through the client.

In React I had to check if the cookie existed, and if it did I had to send it through in the header of my GET request to the Node / Express server.

import Cookies from 'js-cookie';

export const getSessionData = () => {
  // WooCommerce session cookies are appended with a random hash.
  // Here I am tracking down the key of the session cookie.
  const cookies = Cookies.get();
  if (cookies) {
    const cookieKeys = Object.keys(cookies);
    for (const key of cookieKeys) {
      if (key.includes('wp_woocommerce_session_')) {
        return `${key}=${Cookies.get(key)};`;
      }
    }
  }
  return false;
};

export const addToCart = (productId, quantity) => async (dispatch) => {
  dispatch({type: ADD_TO_CART});
  const sessionData = getSessionData();
  const config = {};
  if (sessionData) config['session-data'] = sessionData;
  console.log('config', config);
  try {
    const payload = await axios.get(`${ROOT_API}/addtocart?productId=${productId}&quantity=${quantity}`, {
      withCredentials: true,
      headers: config
    });
    dispatch(addToSuccess(payload));
  } catch (error) {
    dispatch(addToCartFailure(error));
  }
};

On the Node / Express Server I had to check if I had included a cookie (saved in req.headers with the key session-data - it was illegal to use Cookie as a key here) from the client, and if I did, append that to the header of my request going to my CMS.

If I didn't find an appended cookie, it meant this was the first request in the session, so I had to manually grab the cookie from the response I got back from the CMS and save it to the client (setCookieFunc).

app.get('/api/addtocart', async (req, res) => {
  try {
    const productId = parseInt(req.query.productId);
    const quantity = parseInt(req.query.quantity);
    const sessionData = req.headers['session-data'];
    const headers = {};
    if (sessionData) headers.Cookie = sessionData;
    const response = await axios.post(`${WP_API}/wc/v2/cart/add`, {
      product_id: productId,
      quantity
    }, { headers });
    if (!sessionData) {
      const cookies = response.headers['set-cookie'];
      const setCookieFunc = (cookie) => {
        const [cookieKeyValue, ...cookieOptionsArr] = cookie.split('; ');
        const cookieKey = cookieKeyValue.split('=')[0];
        const cookieValue = decodeURIComponent(cookieKeyValue.split('=')[1]);
        const cookieOptions = { };
        cookieOptionsArr.forEach(option => (cookieOptions[option.split('=')[0]] = option.split('=')[1]));
        if (cookieOptions.expires) {
          const expires = new Date(cookieOptions.expires);
          cookieOptions.expires = expires;
        }
        res.cookie(cookieKey, cookieValue, cookieOptions);
      };
      cookies.map(cookie => setCookieFunc(cookie));
    }
    return res.json(response.data);
  } catch (error) {
    // Handle error
    return res.json(error);
  }
});

I'm not sure if this is the most elegant solution to the problem, but it worked for me.


Notes

I used the js-cookie library for interacting with cookies on my React client.


Gotchas

If you're trying to make this work in your development environment (using localhost) there's some extra work to be done. See Cookies on localhost with explicit domain

Allan of Sydney
  • 1,410
  • 14
  • 23