DEV Community

Cover image for How I built a Contentful App combined with Commerce.js (III)
William Iommi
William Iommi

Posted on

How I built a Contentful App combined with Commerce.js (III)

Previously on...

In the first two parts of this series, we have talked about the App Framework, we defined our App and created a sample content type with a single JSON object field using our custom appearance. If you missed them, click on the links above 👆.

In this part, we'll start with a step away from Contentful. With our free Commerce.js account, we are going to define some sample products and jot down the given public key.
After this, we'll be back on Contentful, working on the App Configuration location in order to see why and how to use the key provided by Commerce.js.


An intro to Commerce.js

Commerce.js in a nutshell is a headless e-commerce backend platform that provides a set of API for products, categories, carts and so on. Of course, I simplified, however, since it's not the main scope of our series, if you want to know more about it, you can visit their website.

So why did I choose it? A JSON file with an array of products would have been enough for my customization on Contentful. However, I wanted to have something more realistic but still free (free for my needs of course).
Commerce.js with its free plan, give me access to a web application where I can create my dummy products and with its javascript SDK (more on that later) I can retrieve the data for my App.
 

Commerce.js: Creating a merchant sandbox

I'm skipping the part about how to create a Commerce.js account, let's assume we have it already.
So at first login, we need to define a merchant providing a name, an email and a currency:

Commerce.js - Merchant Form
 

Commerce.js: Creating sample products

With a merchant created we have access to the Commerce.js dashboard that looks like this:

Commerce.js Dashboard
 
In order to add some products, we need to click first on the Products menu voice on the sidebar and then click the add button.
 
Commerce.js Products View

As you can see I have already created some dummy products but let's create a new one just to understand what information will be used by the Contentful App.

For our scenario we need to define the following attributes:

  • Name
  • SKU
  • Price (only because is mandatory for Commerce.js)
  • An image
  • A Custom permalink (this will be used as our URL for the product)

Commerce.js - Adding Product info - 01

Commerce.js - Adding Product info - 02

When all fields are filled, leave the product as active and click save.
We have defined a new product on Commerce.js.
 

Commerce.js: API Keys

As I mentioned before, Commerce.js provides a set of API.
In order to use their SDK, we need to use a public key.
To get our key, we need to go to the developer section and take note of the public sandbox key. This value will be used in the following section.

Commerce.js - API Keys


Customizing Contentful App Configuration Location

We are back on Contentful and on our (running) local environment.
Now we need to design our first location: the App Configuration.
What is an App Configuration, where is it used and why we do want to customize it?
You can access it by clicking directly on your previously installed application (our Commerce Selector Demo App).
The next screenshot shows the default render of our react component which contains...well...nothing yet 😄... except for a title and a paragraph.

Config Screen - default render

The purpose of this location, which is not mandatory, is to give the customer a user-friendly interface where he can configure some global parameters needed by our application. So you need to design some sort of form where the user can insert and save some kind of information.
Unlike the instance parameters (defined in part 1), these kind of parameters (and their respective values) are accessible to every content type that has a field implementing our custom appearance.
All these parameters are saved inside an object called parameters.installation which is accessible thanks to the SDK provided by Contentful.
 
So why do we want to customize this section? The idea is to save here the public key provided by Commerce.js. We should implement an input field where the user can insert the key and maybe also a button to check if the key provided is valid or not.
 

App Configuration: Overview

The file to customize is the ConfigScreen.tsx, let's see the 'out of the box' implementation.

import React, { useCallback, useState, useEffect } from "react";
import { AppExtensionSDK } from "@contentful/app-sdk";
import {
  Heading,
  Form,
  Workbench,
  Paragraph,
} from "@contentful/forma-36-react-components";
import { css } from "emotion";

export interface AppInstallationParameters {}

interface ConfigProps {
  sdk: AppExtensionSDK;
}

const Config = (props: ConfigProps) => {
  const [parameters, setParameters] = useState<AppInstallationParameters>({});

  const onConfigure = useCallback(async () => {
    // This method will be called when a user clicks on "Install"
    // or "Save" in the configuration screen.
    // for more details see https://www.contentful.com/developers/docs/extensibility/ui-extensions/sdk-reference/#register-an-app-configuration-hook

    // Get current the state of EditorInterface and other entities
    // related to this app installation
    const currentState = await props.sdk.app.getCurrentState();

    return {
      // Parameters to be persisted as the app configuration.
      parameters,
      // In case you don't want to submit any update to app
      // locations, you can just pass the currentState as is
      targetState: currentState,
    };
  }, [parameters, props.sdk]);

  useEffect(() => {
    // `onConfigure` allows to configure a callback to be
    // invoked when a user attempts to install the app or update
    // its configuration.
    props.sdk.app.onConfigure(() => onConfigure());
  }, [props.sdk, onConfigure]);

  useEffect(() => {
    (async () => {
      // Get current parameters of the app.
      // If the app is not installed yet, `parameters` will be `null`.
      const currentParameters: AppInstallationParameters | null =
        await props.sdk.app.getParameters();

      if (currentParameters) {
        setParameters(currentParameters);
      }

      // Once preparation has finished, call `setReady` to hide
      // the loading screen and present the app to a user.
      props.sdk.app.setReady();
    })();
  }, [props.sdk]);

  return (
    <Workbench className={css({ margin: "80px" })}>
      <Form>
        <Heading>App Config</Heading>
        <Paragraph>
          Welcome to your contentful app. This is your config page.
        </Paragraph>
      </Form>
    </Workbench>
  );
};

export default Config;
Enter fullscreen mode Exit fullscreen mode

Apart from some well-known react imports, I want to focus on the AppExtensionSDK import, which is the Typescript definition type that lets us know how the Contentful SDK is implemented.
The other focus is on all the imports from @contentful/forma-36-react-components. Forma36 is the open-source design system created by Contentful. Using this could be helpful if you want to keep the same 'Contentful UI' also for all of your customizations without worrying too much about style. Anyway, nobody will stop you from building your UI from scratch 😄.
 

App Configuration: Commerce.js package

Before starting our customization one thing is missing. We need to download the Commerce.js SDK in order to check if the provided key is valid. Let's install it on our local environment with the following CLI command:

npm install @chec/commerce.js 
Enter fullscreen mode Exit fullscreen mode

and if you are using Typescript also this:

npm install --save-dev @types/chec__commerce.js
Enter fullscreen mode Exit fullscreen mode

 

App Configuration: Update imports

Cool, we are ready to go. Let's start importing some extra components (TextField, Button and Flex) from Forma36 and the default export from Commerce.js SDK:

import {
  Heading,
  Form,
  Workbench,
  Paragraph,
  TextField,
  Button,
  Flex,
} from "@contentful/forma-36-react-components";
import Commerce from "@chec/commerce.js";
Enter fullscreen mode Exit fullscreen mode

 

App Configuration: AppInstallationParameters Interface

Since we are using Typescript we need to change the AppInstallationParameters interface with the following new version:

export interface AppInstallationParameters {
  publicKey?: string;
}
Enter fullscreen mode Exit fullscreen mode

We defined a new installation parameter of type string called publicKey. Doing this, Typescript will not complain about our next lines of code. But remember, Typescript is not mandatory, you can refactor everything in plain javascript.
 

App Configuration: The new (outstanding 😄) UI

Let's jump now to the return method of our component and implement our new interface.

<Workbench
      className={css({
        margin: "80px auto",
        display: "flex",
        alignItems: "center",
      })}
    >
      <Form className={css({ padding: "20px" })}>
        <Heading>About Commerce Selector Demo</Heading>
        <Paragraph>
          The Commerce Selector Demo app allows editors to select products from their
          Commerce account and reference them inside of Contentful entries.
        </Paragraph>
        <hr />
        <Heading>Configuration</Heading>
        <Flex className={css({ alignItems: "flex-end" })}>
          <TextField
            className={css({ width: "50%", marginRight: "20px" })}
            labelText="Commerce.js Public Key"
            name="publicKey"
            id="publicKey"
            value={parameters.publicKey || ""}
            onChange={onPublicKeyChange}
            required
          />
          <Button disabled={!parameters.publicKey} onClick={onPublicKeyCheck}>
            Check Key
          </Button>
        </Flex>
      </Form>
    </Workbench>
Enter fullscreen mode Exit fullscreen mode

So apart from some random text and CSS 😄 let's see what we have.
We added an input field where the user can insert the key with an 'onPublicKeyChange' callback on the onChange event. We added also a Button (disabled if there is no key) used to check if the provided key is valid or not. For the latter, we defined an 'onPublicKeyCheck' callback on the onClick event.
Of course, at the moment the code is complaining because we haven't defined these callbacks yet but let's see any way how the new UI looks.

App Configuration: UI

Awesome right?!? 😂 ... Let's implement now those callbacks.
 

App Configuration: onPublicKeyChange

The first is pretty straightforward:

const onPublicKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setParameters({ ...parameters, publicKey: e.target.value });
  };
Enter fullscreen mode Exit fullscreen mode

We are leveraging on the inner state provided by the useState hook and we are saving the new value extracted from the event object. So on every input change, we have an updated version of our parameters.publicKey attribute.
 

App Configuration: onPublicKeyCheck

This is a little bit complex but not too much. Basically, we want to call Commerce.js asking if recognize the key.
We will also use another feature provided by the Contentful SDK to show a success or an error message to the user.
Let's start with a utility method that calls Commerce.js:

const checkPublicKey = async (key: string | undefined) => {
  try {
    if (!key) return false;
    const commerce = new Commerce(key);
    await commerce.merchants.about();
    return true;
  } catch (e) {
    console.error(e);
    return false;
  }
};
Enter fullscreen mode Exit fullscreen mode

This method receives in input the key, creates an instance of the Commerce class and calls the about method from the merchants service to check if everything is ok. If no errors are thrown we return a true otherwise a false.

Now let's see the the onPublicKeyCheck callback:

const onPublicKeyCheck = async () => {
    if (await checkPublicKey(parameters.publicKey)) {
      props.sdk.notifier.success("The provided key is valid");
    } else {
      props.sdk.notifier.error("The provided Key is not valid");
    }
  };
Enter fullscreen mode Exit fullscreen mode

This method calls the previously utility function and in both cases (valid or not) we are using the notifier feature provided by the Contentful SDK to show a success or an error message. We can see these two notifications in the following screenshots:

Api Key Error

Api Key Success
 

App Configuration: onConfigure

We are almost done. The only thing left is to check if the key is valid when we save/update the App. When we save the app, the onConfigure method is called so we need to use our utility method also in this case. We add this check before the default return. If the key is not valid we return false (showing an error message).

const onConfigure = useCallback(async () => {
    // ...
    // out of the box code
    // ...
    if (!(await checkPublicKey(parameters.publicKey))) {
      props.sdk.notifier.error("The provided Key is not valid");
      return false;
    }
    // ...
    // out of the box code
    // ...
  }, [parameters, props.sdk]);
Enter fullscreen mode Exit fullscreen mode

Now we are sure that every time we save our app the key is valid.
This is the final ConfigScreen.tsx after our customization:

import React, { useCallback, useState, useEffect } from "react";
import { AppExtensionSDK } from "@contentful/app-sdk";
import {
  Heading,
  Form,
  Workbench,
  Paragraph,
  TextField,
  Button,
  Flex,
} from "@contentful/forma-36-react-components";
import { css } from "emotion";
import Commerce from "@chec/commerce.js";

export interface AppInstallationParameters {
  publicKey?: string;
}

interface ConfigProps {
  sdk: AppExtensionSDK;
}

const checkPublicKey = async (key: string | undefined) => {
  if (!key) return false;

  try {
    if (key) {
      const commerce = new Commerce(key);
      await commerce.merchants.about();
      return true;
    }
  } catch (e) {
    console.log(e);
    return false;
  }
};

const Config = (props: ConfigProps) => {
  const [parameters, setParameters] = useState<AppInstallationParameters>({});

  const onConfigure = useCallback(async () => {
    // This method will be called when a user clicks on "Install"
    // or "Save" in the configuration screen.
    // for more details see https://www.contentful.com/developers/docs/extensibility/ui-extensions/sdk-reference/#register-an-app-configuration-hook

    // Get current the state of EditorInterface and other entities
    // related to this app installation
    const currentState = await props.sdk.app.getCurrentState();

    if (!(await checkPublicKey(parameters.publicKey))) {
      props.sdk.notifier.error("The provided Key is not valid");
      return false;
    }

    return {
      // Parameters to be persisted as the app configuration.
      parameters,
      // In case you don't want to submit any update to app
      // locations, you can just pass the currentState as is
      targetState: currentState,
    };
  }, [parameters, props.sdk]);

  const onPublicKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setParameters({ ...parameters, publicKey: e.target.value });
  };

  const onPublicKeyCheck = async () => {
    if (await checkPublicKey(parameters.publicKey)) {
      props.sdk.notifier.success("The provided key is valid");
    } else {
      props.sdk.notifier.error("The provided Key is not valid");
    }
  };

  useEffect(() => {
    // `onConfigure` allows to configure a callback to be
    // invoked when a user attempts to install the app or update
    // its configuration.
    props.sdk.app.onConfigure(() => onConfigure());
  }, [props.sdk, onConfigure]);

  useEffect(() => {
    (async () => {
      // Get current parameters of the app.
      // If the app is not installed yet, `parameters` will be `null`.
      const currentParameters: AppInstallationParameters | null =
        await props.sdk.app.getParameters();

      if (currentParameters) {
        setParameters(currentParameters);
      }

      // Once preparation has finished, call `setReady` to hide
      // the loading screen and present the app to a user.
      props.sdk.app.setReady();
    })();
  }, [props.sdk]);

  return (
    <Workbench
      className={css({
        margin: "80px auto",
        display: "flex",
        alignItems: "center",
      })}
    >
      <Form className={css({ padding: "20px" })}>
        <Heading>About Commerce Selector</Heading>
        <Paragraph>
          The Commerce Selector app allows editors to select products from their
          Commerce account and reference them inside of Contentful entries.
        </Paragraph>
        <hr />
        <Heading>Configuration</Heading>
        <Flex className={css({ alignItems: "flex-end" })}>
          <TextField
            className={css({ width: "50%", marginRight: "20px" })}
            labelText="Public Key"
            name="publicKey"
            id="publicKey"
            value={parameters.publicKey || ""}
            onChange={onPublicKeyChange}
            required
          />
          <Button disabled={!parameters.publicKey} onClick={onPublicKeyCheck}>
            Check Key
          </Button>
        </Flex>
      </Form>
    </Workbench>
  );
};

export default Config;
Enter fullscreen mode Exit fullscreen mode

In the next episode...

We are finally ready to customize our Entry Field. In the next and final part, we'll call again Commerce.js in order to retrieve our dummy products, show them and let the user select one.

Stay 📻
❤️

Discussion (0)