Adding eCommerce with Snipcart

Adding eCommerce to your serverless website is easy with Snipcart. Designed for the serverless ecosystem, it is a great add-on to start selling and monetizing your website.

Once you have signed up for an account at Snipcart, you can add the code below to start using the service. Snipcart includes many options and robust features including recurring payments and subscriptions, inventory management, discounts, shipping, invoicing, and customer dashboards to name a few. We are going to implement just the basic features to get you started and to keep the tutorial shorter. Be sure to check out Snipcart’s documentation to discover all they can provide.

How Snipcart works with the Example App

Snipcart's use of JavaScript and jQuery recognizes the button we will provide within a widget, and opens a shopping cart when the button is clicked. When checking out, Snipcart verifies that the products and prices match the data on your website to prevent any shenanigans via the browser console. What makes Snipcart such a time saver to integrate is that the shopping cart and admin interface are already built for you and that the add-on is presented as an overlay to your existing website.

We are going to show you how to add a Snipcart widget to your Scrivito website using the Example App as a reference. You should have a running Scrivito website and an account with Snipcart to get started. Once we have integrated the basic setup for Snipcart, we will create a SnipcartWidget to add products anywhere we want on our Scrivito-powered website.

Create the Snipcart component

First we need to add a Snipcart React component to the Scrivito Example App. This will be called in the application JS code so that it is rendered for every page. The Snipcart component will be added in the “src/Components” directory of the Example App. Here we set up the App to get the Snipcart API key from the homepage variables. While optional, it makes the code more universal without the need to have a developer change the keys from development/testing key to the production key.

src/Components/Snipcart.js
Copy
import * as React from "react";
import * as Scrivito from "scrivito";
import Helmet from "react-helmet";

class Snipcart extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      isSnipcartInstalled: false
    };
  }

  componentDidMount() {
    Scrivito.load(() => getSnipcartKey()).then(snipcartKey => {
      if (snipcartKey) {
        this.setState({ isSnipcartInstalled: true });
      }
    });
  }

  render() {
    if (!this.state.isSnipcartInstalled) {
      return null;
    }

    return (
      <Helmet>
        <script 
          src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"
        />
        <script
          src="https://cdn.snipcart.com/scripts/2.0/snipcart.js"
          id="snipcart"
          data-api-key={`${getSnipcartKey()}`}
        />
        <link
          href="https://cdn.snipcart.com/themes/2.0/base/snipcart.min.css"
          type="text/css"
          rel="stylesheet"
        />
      </Helmet>
    );
  }
}

function getSnipcartKey() {
  const rootPage = Scrivito.Obj.root();

  if (!rootPage) {
    return;
  }
  return rootPage.get("snipcartApiKey");
}

export default Scrivito.connect(Snipcart);

With a single-page application, we need to ensure that the Snipcart code is available each time the visitor navigates to a different page. The add-on is loaded at rendering time (via “/snipcart.js”), along with jQuery and the standard Snipcart theme.

src/App.js
Copy
// Other imports
import Snipcart from './Components/Snipcart';

export default function App() {
  return (
    <ErrorBoundary>
      <div>
        <div className="content-wrapper">
          <Navigation />
          <Scrivito.CurrentPage />
          <NotFoundErrorPage />
        </div>
        <Footer />
        <GoogleAnalytics />
        <CurrentPageMetaData />
        <Intercom />
        <Snipcart />
      </div>
    </ErrorBoundary>
  );
}

To have Snipcart’s required scripts to be requested and executed as well as jQuery’s, “https://cdn.snipcart.com”, “https://ajax.googleapis.com/", and 'unsafe-eval' need to be specified as permitted script sources in the CSP header included in the Example App

Provide a Snipcart API key input for editors

Here, we will extend the “Homepage” object to include an input and attribute for the Snipcart API key. If you decided to make the Snipcart API key an environment variable and omitted the getSnipcartKey() function above you can skip this step.

src/Objs/Homepage/HomepageObjClass.js
Copy
...
attributes: {
...
  snipcartApiKey: 'string',
...
src/Objs/Homepage/HomepageObjEditingConfig.js
Copy
...
attributes: {
  ...
  snipcartApiKey: {
    title: 'Snipcart API Key',
    description: 'Register at https://www.snipcart.com/',
  },
...
   properties: [
    ...
    'snipcartApiKey',
...

Create a Snipcart widget

Creating a Snipcart widget will allow you and your editors to add products to any page. The following code is a recommendation for a nice looking product widget. The only part of the component that is required for Snipcart to work is the <button> element, the rest is just eye candy. First we will create the widget class to define the attributes of the widget:

src/Widgets/SnipcartWidget/SnipcartWidgetClass.js
Copy
import * as Scrivito from "scrivito";

const SnipcartButtonWidget = Scrivito.provideWidgetClass(
  "SnipcartButtonWidget",
  {
    attributes: {
      image: "reference",
      productId: "string",
      name: "string",
      price: "string",
      url: "string",
      description: "string",
      buttonText: "string"
    }
  }
);

export default SnipcartButtonWidget;

Then we create the component to display the content:

src/Widgets/SnipcartWidget/SnipcartWidgetComponent.js
Copy
import * as React from "react";
import * as Scrivito from "scrivito";
import InPlaceEditingPlaceholder from "../../Components/InPlaceEditingPlaceholder";

Scrivito.provideComponent("SnipcartButtonWidget", ({ widget }) => {
  const pid = widget.id("productId");
  const name = widget.get("name");
  const price = widget.get("price");
  const url = widget.get("url");
  const description = widget.get("description");
  const buttonText = widget.get("buttonText");

  return (
    <div className="card" style={{ width: 18 + "rem" }}>
      <Scrivito.ImageTag
        content={widget}
        attribute="image"
        className="&quot;card-img-top&quot;"
      />
      <div className="card-body">
        <Scrivito.ContentTag
          className="h5"
          tag="h5"
          content={widget}
          attribute="name"
          className="card-title"
        />
        <Scrivito.ContentTag
          tag="p"
          content={widget}
          attribute="description"
          className="card-text"
        />
        <button
          className="snipcart-add-item"
          data-item-id={pid}
          data-item-name={name}
          data-item-price={price}
          data-item-url={url}
          data-item-description={description}
        >
          {buttonText}
        </button>
      </div>
    </div>
  );
});

Finally, we add the editing configuration the editors will need to define a product:

src/Widgets/SnipcartWidget/SnipcartWidgetEditingConfig.js
Copy
import * as Scrivito from 'scrivito';

Scrivito.provideEditingConfig('SnipcartButtonWidget', {
  title: 'Snipcart Product',
  attributes: {
    productId: {
      title: 'Product ID',
      description: "ID of the product.",
    },
    name: {
      title: 'Name',
      description: "Name of product.",
    },
    price: {
      title: 'Price',
      description: "Price of product.",
    },
    url: {
      title: 'URL',
      description: "URL of product for Snipcart to validate. Enter '/' followed by the page id.",
    },
    description: {
      title: 'Description',
      description: "Description of product.",
    },
    buttonText: {
      title: 'Button Text',
      description: "Text for the buy button.",
    },
  },
  properties: [
    'productId',
    'name',
    'price',
    'url',
    'description',
    'buttonText',
  ],
  initialContent: {
    name: 'Enter name of product',
    description: 'Enter a description',
    buttonText: 'Add to cart',
  },
});

Snipcart requires the data-item-id, data-item-name, data-item-price and data-item-url product attributes at a minimum.

There are some important things to keep in mind when inputting product details. The data-item-url needs to be in the format of “/” followed by either the path, permalink or page object ID to work with Snipcarts security features. For example, the path of this page is /adding-ecommerce-with-snipcart-32d3d504a64350c7. The object ID of the page (which can also be found in the page properties on the “System” tab) is unique and thus the safest and recommended option; for this page, one would use /32d3d504a64350c7. Note that if you place a product widget on your homepage, you will need to copy the Homepage's object ID from the page properties because the ID doesn’t show up in the URL path (“/”).

Also, the price should be in the format of 1.00 or 1, without the currency symbol. If you wish to use different currencies, please see Snipcart's multi-currency guide and update the data-item-price attribute as needed.

Adding products can take up to 24 hours to pre-render, causing an “item not found” error from Snipcart at checkout until a product has been pre-rendered. We are evaluating a workaround and will update this tutorial when it is resolved.

After adding the Snipcart API key to the properties of your homepage and fetching it using the above code you will be able to add products wherever you want, and they will be added to your “inventory” at Snipcart. Clicking the “Add to cart” button will launch the Snipcart UI and initiate the checkout process as needed. You can customize the above as much as you like and make it work for your particular store, company or products. Be sure to check out Snipcart's documentation for additional options and settings.