How I built a Contentful App combined with Commerce.js (IV)
Tags: contentful, javascript, cms, commmercejs
Published: 2021-10-08 18:28:46

##The road so far...

It's time folks! The end of our journey through a Contentful App is here.
We have gathered a lot of information in the first three parts. We have seen the theory behind it, we understood the why for all of this and we saw an integration between Contentful and a third-party platform like Commerce.js.

Now it's time to connects all the pieces and finally see our main customization, which is the reason why I'm here writing stuff on a monitor 😂.

In this final episode, we are going to see our customized entry field. Our focus will be the research and selection of a product from Commerce.js.

And with a little bit of sadness in my eyes, let's start our last take 🙁 🥺 😢.

---

##The Concept
So what do we want to build? For our field, we want to choose which type of URL to save: Product, Category or Content Page.
Based on this choice, we open a modal showing all the related entries, in our use case, all the products from Commerce.js.
After the user selects a product, we save our field with some info. Since this field is of type JSON, we can structure it as we want and the user will see a card preview of the selected product and not the JSON Object itself.

---

##Initial Setup
Inside our project, I created a lib folder where I'm going to save few files with constants and utility methods.
&nbsp;
###Constants
Our App has multiple purposes even if our focus is on the _URL + product_ behavior.
I have defined few constants to separate these logics.

```ts
export const APPEARANCE_TYPES = {
  URL: "url", // used in this demo
  PRODUCT: "product",
  PRODUCTS: "products",
  CATEGORY: "category",
  CATEGORIES: "categories",
};

export const TYPES = {
  PRODUCT: "product", // used in this demo
  CATEGORY: "category",
  PAGE: "page",
};
```
&nbsp;
###Utilities
After that, we create some utility methods that will be used in the upcoming sections.
First, we need a function that retrieves a specific product from Commerce.js:
```ts
import Commerce from "@chec/commerce.js";
import { Product } from "@chec/commerce.js/types/product";

export const getProduct = async (
  apiKey: string,
  productId: string,
  successCB?: (product: Product) => void,
  failCB?: (e: any) => void
): Promise<Product | any> => {
  try {
    const commerce = new Commerce(apiKey);
    const product = await commerce.products.retrieve(productId);
    if (!successCB) return product;
    successCB(product);
  } catch (e) {
    console.error(e);
    if (!failCB) return e;
    failCB(e);
  }
};
```
and then we need the function that retrieves all the products:
```ts
import Commerce from "@chec/commerce.js";
import { ProductCollection } from "@chec/commerce.js/features/products";

export const getProducts = async (
  apiKey: string,
  successCB?: (products: ProductCollection) => void,
  failCB?: (e: any) => void
): Promise<ProductCollection | any> => {
  try {
    const commerce = new Commerce(apiKey);
    const products = await commerce.products.list();
    if (!successCB) return products;
    successCB(products);
  } catch (e) {
    console.error(e);
    if (!failCB) return e;
    failCB(e);
  }
};
```
Both methods expect an API key as input and if you have read part 3 of this series, you know where it will come from 😉.

---

##Custom components
We are not limited to using the files provided by Contentful, we can also create our own.
&nbsp;
###Product Card
The Product Card Component will be used both in the modal when we are looking for products to select, and also after the selection to show a nice UI instead of the JSON object saved on Contentful.

```tsx
import { css } from "emotion";
import { Product } from "@chec/commerce.js/types/product";
import {
  Card,
  IconButton,
  Flex,
  Tag,
  Heading,
  HelpText,
} from "@contentful/forma-36-react-components";

interface IProductCard {
  product?: Product;
  showTag?: boolean;
  onClickCard?: (product: Product) => void;
  onClickIcon?: () => void;
}

export const ProductCard = (props: IProductCard) => {
  const { product, showTag, onClickCard, onClickIcon } = props;

  if (!product) return null;

  return (
    <Card
      className={css({
        height: "100%",
        boxSizing: "border-box",
        position: "relative",
      })}
      {...(onClickCard && { onClick: () => onClickCard(product) })}
    >
      {onClickIcon && (
        <IconButton
          iconProps={{ icon: "Close" }}
          buttonType="muted"
          className={css({
            position: "absolute",
            top: "3px",
            right: "3px",
          })}
          onClick={onClickIcon}
        />
      )}
      <Flex alignItems="center">
        {product.media && (
          <div className={css({ marginRight: "20px", width: "100px" })}>
            <img
              className={css({ maxWidth: "100%" })}
              src={product.media.source}
              alt={product.name}
            />
          </div>
        )}
        <Flex flexDirection="column">
          {showTag && <Tag>product</Tag>}
          <Heading>{product.name}</Heading>
          <HelpText
            className={css({
              fontStyle: "italic",
              fontSize: "12px",
              marginBottom: "10px",
            })}
          >
            SKU: {product.sku}
          </HelpText>
        </Flex>
      </Flex>
    </Card>
  );
};
```
We are importing some UI components from Forma36 and a Product type definition from Commerce.js.
Our custom IProductCard interface defines the properties available for the component:

- _**product**_: this is the prop containing the whole product data coming from Commerce.js.
- _**showTag**_: this flag shows a tag that identifies this card as a product (this will be more clear later).
- _**onClickCard**_: this optional callback is used inside the products modal when the user selects a product. The function passes the product prop as a parameter.
- _**onClickIcon**_: this callback, when defined, shows an 'x' icon on the top right corner and is used when we want to clear our selection.

This card will have two possible layouts which we can see below:

![Product Card - Modal UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lb779gtl69yukg5hv7ot.png)

![Product Card - Selected UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yuakxz13hewuvx2miqpk.png)

The first one will be used inside the modal, the second in place of the JSON object when the product is already selected.
&nbsp;
###Product Wrapper
This component will be used inside the Dialog/Modal Location. It will contain all the products coming from Commerce.js.
Here the customer can click on a single card and select the product.

```tsx
import { css } from "emotion";
import { Grid, GridItem } from "@contentful/forma-36-react-components";
import { useEffect, useState } from "react";
import { getProducts } from "../lib/commerce";
import { ProductCollection } from "@chec/commerce.js/features/products";
import { Product } from "@chec/commerce.js/types/product";
import { ProductCard } from "./ProductCard";
import { TYPES } from "../lib/Constants";

interface IProductWrapper {
  publicKey: string;
  onSelectProduct: (data: { id: string; type: string; url: string }) => void;
}

export const ProductWrapper = (props: IProductWrapper) => {
  const { publicKey, onSelectProduct } = props;
  const [productCollection, setProductCollection] =
    useState<ProductCollection>();
  useEffect(() => {
    getProducts(publicKey, setProductCollection);
  }, [publicKey]);
  const onClickCard = (product: Product) => {
    onSelectProduct({
      id: product.id,
      type: TYPES.PRODUCT,
      url: `/p/${product.permalink}`,
    });
  };
  if (!productCollection) return <p>Loading...</p>;
  return (
    <Grid columns={3} rowGap="spacingS" className={css({ margin: "20px 0" })}>
      {productCollection.data.map((product) => (
        <GridItem key={product.id}>
          <ProductCard product={product} onClickCard={onClickCard} />
        </GridItem>
      ))}
    </Grid>
  );
};
```
As usual, we are using UI components from Forma36, in this case, Grid and GridItem. We are using also the previous _Product Card_ component in order to show all our dummy products.
If we look at the available props, we have:

- _**publicKey**_: this is the key used to call Commerce.js.
- _**onSelectProduct**_: this is the callback that will be called when the user clicks the card. It accepts an object as parameter. This object contains the data structure that will be saved on Contentful.

The component, thanks to the public key, calls Commerce.js with our utility method and saves products inside _productCollection_ inner state. While is waiting for a response from Commerce.js, the component shows a simple _*Loading...*_ paragraph to inform the reader.
We can see the UI below:

![Product Wrapper - Base UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8bhd97kbv6s99rivx7ij.png)

> _This is a simple demo with a simple UI and few products. Of course, if we want to do some improvements we could add an input field to search products and maybe also pagination if your Product Catalog is huge.
The only limit is your imagination (and your Business requirements/plan_ 😂_)_

&nbsp;
###Url Appearance Field
This is definitely the most complex component of the entire app.
This UI is rendered only when our instance parameter is set to 'URL'.
I'm going to analyze the file in the detail.
&nbsp;
####Url Appearance Field: imports
```tsx
import { useState, useEffect } from "react";
import { css } from "emotion";
import { Product } from "@chec/commerce.js/types/product";
import { Flex, Button } from "@contentful/forma-36-react-components";
import { APPEARANCE_TYPES, TYPES } from "../lib/Constants";
import { getProduct } from "../lib/commerce";
import { ProductCard } from "./ProductCard";
import { FieldExtensionSDK } from "@contentful/field-editor-shared";
```
This is pretty clear, we are importing  UI components, utilities, typescript types...🥱😪
&nbsp;
####Url Appearance Field: props interface
```tsx
interface IUrlAppearanceField {
  sdk: FieldExtensionSDK;
}
```
The interface is simple, we are expecting, as a prop, the SDK related to the field provided by Contentful.
&nbsp;
####Url Appearance Field: react hooks
Here we are extracting the Commerce.js public key from our `parameters.installation` and we are defining some react hooks.
```tsx
const { sdk } = props;
const { publicKey }: any = sdk.parameters.installation;
const [innerValue, setInnerValue] = useState(sdk.field.getValue());
const [product, setProduct] = useState<Product>();

useEffect(() => {
  if (innerValue?.type === TYPES.PRODUCT) {
    getProduct(publicKey, innerValue.id, setProduct);
  }
}, [publicKey, innerValue]);
```
We have two useState hooks: the first is an inner state containing the value saved on Contentful, extracted with the _getValue_ function provided by the SDK. The second contains the Commerce.js product.
The useEffect hook is called every time the innerValue changes and also on first load. The hook checks if the saved value is of type 'product' and if so, we call Commerce.js retrieving the full product, passing the _innerValue.id_ which contains the id of a specific product.
&nbsp;
####Url Appearance Field: the UI
Let's jump to the return statement:
```tsx
return (
    <>
      {product && (
        <>
          <ProductCard showTag product={product} onClickIcon={clearValue} />
          <div
            className={css({
              margin: "20px 0",
              borderTop: "1px solid #cfd9e0",
            })}
          />
        </>
      )}
      <Flex className={css({ marginTop: `${innerValue ? "0" : "10px"}` })}>
        <Button
          icon="ShoppingCart"
          buttonType="muted"
          className={css({ marginLeft: "10px", height: "2rem" })}
          onClick={() => openDialog(TYPES.PRODUCT)}
        >
          Choose a Product
        </Button>
      </Flex>
    </>
  );
```
We are showing a _Product Card_ and a separator when the user selects or has already selected a product.
The component has the showTag attribute set to true and the onClickIcon callback defined with a 'clearValue' function.
Finally, we have a Forma36 Button with a cart icon and a callback on the onClick event that opens a Dialog of type 'product'.
We can see the UI in the following screenshots:
![Url Appearance Field | base UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/89tatf2o1dg98qal4xha.png)

![Url Appearance Field | product selected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u4lq3foxi9u7m8hjols3.png)

As mentioned a million times 😅 we are focusing only on the product selection, the complete UI would instead be like this:
![Url Appearance Field | complete UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f0abrt4b95r2pmyb3dna.png)

The light blue product badge lets the user understand immediately which type of URL has been saved on the field.
&nbsp;
####Url Appearance Field: clearValue callback
The clearValue function lets the user clear his selection by clicking on the 'x' icon on the card.
```tsx
const clearValue = () => {
  setProduct(undefined);
  setInnerValue(undefined);
  sdk.field.setValue(undefined);
};
```
We are cleaning our react states and we are using the _setValue_ function provided by the SDK in order to reset the value also for Contentful.
&nbsp;
####Url Appearance Field: openDialog callback
The openDialog function is the core of the component.
It lets you open the Contentful Modal with the `dialogs.openCurrentApp` method passing few parameters. Basically, this function moves the focus of your app into the Dialog Location.
Since is a Promise, it waits until you close the modal and after that, you have access to a response object.
If you remember, in the Product Wrapper component we defined an object composed of an id, type and URL. This is the object received from the Promise when we select a product (if we click on the x of the modal we receive an undefined object).
Let's see the implementation:
```tsx
const openDialog = async (type: string) => {
    const res = await sdk.dialogs.openCurrentApp({
      position: "top",
      minHeight: "75vh",
      width: "fullWidth",
      shouldCloseOnOverlayClick: true,
      allowHeightOverflow: true,
      title: `Search ${type === TYPES.CATEGORY ? " Categories" : "Products"}`,
      parameters: { appearance: APPEARANCE_TYPES.URL, type },
    });
    if (res) {
      setInnerValue(res);
      sdk.field.setValue(res);
    }
  };
```
As you can see, if the res object is defined, it means that we have selected a product and we are saving this object into our react state but also into Contentful through the SDK.
{% details Here you can see the entire code related to the product %}
```tsx
import { useState, useEffect } from "react";
import { css } from "emotion";
import { Product } from "@chec/commerce.js/types/product";
import { Flex, Button } from "@contentful/forma-36-react-components";
import { APPEARANCE_TYPES, TYPES } from "../lib/Constants";
import { getProduct } from "../lib/commerce";
import { ProductCard } from "./ProductCard";
import { FieldExtensionSDK } from "@contentful/field-editor-shared";

interface IFieldUrl {
  sdk: FieldExtensionSDK;
}

export const UrlAppearanceField = (props: IFieldUrl) => {
  const { sdk } = props;
  const { publicKey }: any = sdk.parameters.installation;
  const [innerValue, setInnerValue] = useState(sdk.field.getValue());
  const [product, setProduct] = useState<Product>();

  useEffect(() => {
    if (innerValue?.type === TYPES.PRODUCT) {
      getProduct(publicKey, innerValue.id, setProduct);
    }
  }, [publicKey, innerValue]);

  const openDialog = async (type: string) => {
    const res = await sdk.dialogs.openCurrentApp({
      position: "top",
      minHeight: "75vh",
      width: "fullWidth",
      shouldCloseOnOverlayClick: true,
      allowHeightOverflow: true,
      title: `Search ${type === TYPES.CATEGORY ? " Categories" : "Products"}`,
      parameters: { appearance: APPEARANCE_TYPES.URL, type },
    });
    if (res) {
      setInnerValue(res);
      sdk.field.setValue(res);
    }
  };

  const clearValue = () => {
    setProduct(undefined);
    setInnerValue(undefined);
    sdk.field.setValue(undefined);
  };

  return (
    <>
      {product && (
        <>
          <ProductCard showTag product={product} onClickIcon={clearValue} />
          <div
            className={css({
              margin: "20px 0",
              borderTop: "1px solid #cfd9e0",
            })}
          />
        </>
      )}
      <Flex className={css({ marginTop: `${innerValue ? "0" : "10px"}` })}>
        <Button
          icon="ShoppingCart"
          buttonType="muted"
          className={css({ marginLeft: "10px", height: "2rem" })}
          onClick={() => openDialog(TYPES.PRODUCT)}
        >
          Choose a Product
        </Button>
      </Flex>
    </>
  );
};
```
{% enddetails %}

---

##Connecting dots
Now is the time to connect all the pieces of the puzzle.
In the previous section, we saw all our custom implementation and now we need to use the locations provided by Contentful and finish our implementation.

Our focus is on the `Dialog.tsx` and `Field.tsx` files. Let's start with the Modal.
&nbsp;
###Dialog Location
This file is used when we call the `dialogs.openCurrentApp` function that we saw previously.
```tsx
import { ModalContent } from "@contentful/forma-36-react-components";
import { DialogExtensionSDK } from "@contentful/app-sdk";
import { TYPES } from "../lib/Constants";
import { ProductWrapper } from "./ProductWrapper";

interface DialogProps {
  sdk: DialogExtensionSDK;
}

const Dialog = (props: DialogProps) => {
  const { type }: any = props.sdk.parameters.invocation;
  const { publicKey }: any = props.sdk.parameters.installation;
  return (
    <>
      <ModalContent>
        {type === TYPES.PRODUCT && (
          <ProductWrapper
            publicKey={publicKey}
            onSelectProduct={props.sdk.close}
          />
        )}
        {/* {type === TYPES.CATEGORY && (
          <CategoryWrapper
            publicKey={publicKey}
            onSelectCategory={props.sdk.close}
          />
        )} */}
      </ModalContent>
    </>
  );
};

export default Dialog;
```
We have a specific Typescript type definition for the SDK which is now `DialogExtensionSDK`. With this SDK, inside the `parameters.invocation` we have access to the type attribute that we passed when we called the modal. This attribute lets us know what type of content to provide to the modal as you can see in the return statement.
To our _Product Wrapper_ component, we are passing also the `close` SDK callback that we'll be used when we select a product passing back the object to save on Contentful.
&nbsp;
###Field Location
Based on our instance parameter 'type' we render a UI or another, in our demo will be always the _URL Appearance Field_
```tsx
import { useEffect } from "react";
import { FieldExtensionSDK } from "@contentful/app-sdk";
import { APPEARANCE_TYPES } from "../lib/Constants";
import { UrlAppearanceField } from "./UrlAppearanceField";

interface FieldProps {
  sdk: FieldExtensionSDK;
}

const Field = (props: FieldProps) => {
  const instanceProps: any = props.sdk.parameters.instance;
  useEffect(() => {
    props.sdk.window.startAutoResizer();
    return () => props.sdk.window.stopAutoResizer();
  }, [props]);

  return (
    <>
      {instanceProps.type === APPEARANCE_TYPES.URL && (
        <UrlAppearanceField sdk={props.sdk} />
      )}
      {/*
        {instanceProps.type === APPEARANCE_TYPES.PRODUCT && (
          <ProductAppearanceField sdk={props.sdk} />
        )}
        {instanceProps.type === APPEARANCE_TYPES.PRODUCTS && (
          <ProductsAppearanceField sdk={props.sdk} />
        )}
        {instanceProps.type === APPEARANCE_TYPES.CATEGORY && (
          <CategoryAppearanceField sdk={props.sdk} />
        )}
        {instanceProps.type === APPEARANCE_TYPES.CATEGORIES && (
          <CategoriesAppearanceField sdk={props.sdk} />
        )}
      */}
    </>
  );
};

export default Field;
```
Here, the only thing that I want to clarify is the useEffect hook. We are using the `window.startAutoResizer` feature. This function updates the height of the iframe every time something happens (for example when we select or remove the product card). Doing this, we have always our wrapper height updated with no white spaces or scrollbars.

---

##Little Demo
This is it, we finally completed our app 🥳 🥳 🥳.
Here you can see a little demo:
![Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0h3azpahcqw5vip1hdqe.gif)
Here instead the snapshot of the entry where you can see what is saved on Contentful:
```json
{
    "metadata": {
        "tags": []
    },
    "sys": {
        "space": {
            "sys": {
                "type": "Link",
                "linkType": "Space",
                "id": "xxx"
            }
        },
        "id": "35MgIumMobPVc9qnCH0Xa0",
        "type": "Entry",
        "createdAt": "2021-10-02T16:55:24.957Z",
        "updatedAt": "2021-10-03T10:11:46.157Z",
        "environment": {
            "sys": {
                "id": "website",
                "type": "Link",
                "linkType": "Environment"
            }
        },
        "revision": 5,
        "contentType": {
            "sys": {
                "type": "Link",
                "linkType": "ContentType",
                "id": "testCommerceSelector"
            }
        },
        "locale": "en-US"
    },
    "fields": {
        "url": {
            "id": "prod_RqEv5xXO2d5Zz4",
            "type": "product",
            "url": "/p/shoe05"
        }
    }
}
```

---

##Deploying the app
Of course is not over yet 😉. We need to deploy our application, otherwise will be available only for us and with a local environment running.
Since our app is very small we don't need an external provider, we can deploy the app directly on Contentful, the process is pretty simple.
In our package.json file, we have scripts that build and upload the app for us.
The commands to run on the terminal are:

```sh
npm run build
npm run upload
```

Follow the instructions of the script and when finished we can see on Contentful our uploaded app.

![App Deployed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ajka0ikn7t6g0xcddrb6.png)

As you can see now the frontend is not anymore our local environment but is hosted by Contentful 😎.

---

##A step further...
It's not part of this series but I want to mention one further step that we can and should do for our app.
As we have seen, data about the product saved on Contentful, is minimal: product code and the permalink, nothing else. All other information for the product card is gathered in real-time by calling Commerce.js.
But what happens if a permalink changes or worse, the product on Commerce.js is deleted? Our website or mobile app that depends on Contentful data, can lead to a broken page.

Commerce.js provides a set of [webhooks](https://commercejs.com/docs/guides/webhooks/) that we can configure. For example, there is a specific webhook when the product has been updated or another one if is deleted.
For each one, we need to provide a URL that Commerce.js can call every time something happens.


My demo website uses my Contentful data. It's built with [Next.js](https://nextjs.org/) and deployed on [Vercel](https://vercel.com/) and I defined few [API routes](https://nextjs.org/docs/api-routes/introduction) that listen to Commerce.js webhooks.
Every time a product changes, my API route receives the updated product from Commerce.js and thanks to the Contentful [Content Management API](https://www.contentful.com/developers/docs/references/content-management-api/) I can update my content with the updated data or skip if nothing that I care about is changed.

Or for example, If I don't want to update my content automatically, when I receive the updated product I can send a notification and hopefully (😆) there will be someone that will update manually the broken link(s) on Contentful.

---

##Conclusion...
Wow...what a journey 🤯...probably not the best way to start my 'blog career' with a 4 articles series but it was fun, so much fun 😁.
Hope you enjoyed it too and if you are still here reading thank you very much, I appreciate it 🙇🏻‍♂️.
Now you can leave or drop a comment or a like or a follow or whatever 😆...
And if with 'the road so far...' at the beginning of this page you get which TV series I'm referring to, _carry on_, drop another comment below and let me know.

See ya 🤙 💪