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: Creating sample products
With a merchant created we have access to the Commerce.js dashboard that looks like this:
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.
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)
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.
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.
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;
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
and if you are using Typescript also this:
npm install --save-dev @types/chec__commerce.js
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";
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;
}
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>
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.
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 });
};
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;
}
};
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");
}
};
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:
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]);
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;
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 📻
❤️
Top comments (0)