Skip to main content

Launch the Simple Editor on your website

This tutorial walks you through building a small Vanilla TypeScript app that opens the Simple Editor for a configured Customer's Canvas product and submits the customization result for rendering. A small Express backend keeps the OAuth2 client credentials, the access token, and the storefront user token on the server, so the browser receives only the data needed to initialize the editor. The result is a sandbox project for experiments with Customer's Canvas Hub APIs and SDKs.

Prerequisites​

  • A Customer's Canvas Hub tenant prepared for frontend integration
  • The collected tenantId, storefrontId, OAuth2 client credentials, and a test productId
  • A workflow of the Workflow Elements type with the simple-editor component assigned to the product (see Workflow Elements Overview)
  • Node.js 18 or later

Set up the project​

  1. Create a project folder.

    mkdir simple-editor-quick-start
    cd simple-editor-quick-start
  2. Create a Vite app.

    npm create vite@latest . -- --template vanilla-ts
  3. Install the backend dependencies.

    npm install @aurigma/axios-storefront-api-client axios dotenv express
    npm install -D @types/express @types/node concurrently tsx
  4. Create vite.config.ts in the project root.

    import { defineConfig } from "vite";

    export default defineConfig({
    server: {
    host: "127.0.0.1",
    proxy: {
    "/api": "http://127.0.0.1:3001",
    },
    },
    });

    Vite runs on 127.0.0.1 and forwards browser requests from /api/* to the local backend.

  5. Update the dev script in package.json.

    {
    "scripts": {
    "dev": "concurrently vite \"tsx watch server/server.ts\""
    }
    }

    The dev script starts Vite and the Express backend together. Leave the other Vite scripts unchanged.

    If you run npm run dev now, it fails because the backend files don't exist yet.

Configure Customer's Canvas connection​

  1. Create an .env file in the project root.

  2. Add your Customer's Canvas values.

    CCHUB_BASE_URL=https://customerscanvashub.com
    CCHUB_API_GATEWAY_URL=https://api.customerscanvashub.com
    CCHUB_TENANT_ID=<tenant_id>
    CCHUB_STOREFRONT_ID=<storefront_id>
    CCHUB_CLIENT_ID=<client_id>
    CCHUB_CLIENT_SECRET=<client_secret>

    Use the EU or AU URLs when your tenant runs in another region. Keep CCHUB_CLIENT_SECRET only on the backend. Do not expose it in VITE_* variables or browser code.

Load the editor on the page​

Open index.html, add the Simple Editor script and styles to the <head>, and place the <au-simple-editor> tag where the editor must appear.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple Editor quick start</title>
<!-- The environment segment must match your tenant: us, eu, or au. -->
<link
href="https://staticjs-aurigma.azureedge.net/libs/us/workflow-elements/simple-editor/styles.css"
rel="stylesheet"
/>
<script
defer
src="https://staticjs-aurigma.azureedge.net/libs/us/workflow-elements/simple-editor/index.js"
></script>
<link rel="stylesheet" href="/src/style.css" />
</head>
<body>
<main id="status">Loading the Simple Editor...</main>
<au-simple-editor></au-simple-editor>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

The component tag is known in advance, so the page loads the editor statically. For the dynamic loading approach, refer to Workflow Elements.

Add the backend​

  1. Create a server folder in the project root.

  2. Create server/cchub-auth.ts.

  3. Add the Customer's Canvas connection and OAuth2 helper.

    import axios from "axios";
    import dotenv from "dotenv";

    dotenv.config();

    type AuthResponse = {
    access_token: string;
    expires_in: number;
    token_type: "Bearer";
    scope: string;
    };

    export const cchubEnv = {
    baseUrl: requireEnv("CCHUB_BASE_URL"),
    apiGatewayUrl: requireEnv("CCHUB_API_GATEWAY_URL"),
    tenantId: Number(requireEnv("CCHUB_TENANT_ID")),
    storefrontId: Number(requireEnv("CCHUB_STOREFRONT_ID")),
    clientId: requireEnv("CCHUB_CLIENT_ID"),
    clientSecret: requireEnv("CCHUB_CLIENT_SECRET"),
    };

    let cachedAccessToken: { value: string; expiresAt: number } | null = null;

    export async function getAccessToken(): Promise<string> {
    if (cachedAccessToken !== null && cachedAccessToken.expiresAt > Date.now()) {
    return cachedAccessToken.value;
    }

    const body = new URLSearchParams({
    client_id: cchubEnv.clientId,
    client_secret: cchubEnv.clientSecret,
    grant_type: "client_credentials",
    });

    const { data } = await axios.post<AuthResponse>(
    `${cchubEnv.baseUrl}/connect/token`,
    body,
    {
    headers: {
    "Content-Type": "application/x-www-form-urlencoded",
    },
    }
    );

    cachedAccessToken = {
    value: data.access_token,
    expiresAt: Date.now() + (data.expires_in - 60) * 1000,
    };

    return data.access_token;
    }

    function requireEnv(name: string): string {
    const value = process.env[name];

    if (value === undefined || value === "") {
    throw new Error(`Missing ${name} environment variable.`);
    }

    return value;
    }

    This module keeps OAuth2 client credentials on the backend, requests an access token through the Client Credentials flow, and caches the token until shortly before expiration.

  4. Create server/cchub-api.ts.

  5. Add Storefront API operations.

    import {
    ApiClientConfiguration,
    ProjectsApiClient,
    ProductsApiClient,
    ProjectItemProductType,
    StorefrontUsersApiClient,
    } from "@aurigma/axios-storefront-api-client";
    import type {
    CreateSingleItemProjectDto,
    CreateStorefrontUserDto,
    } from "@aurigma/axios-storefront-api-client";
    import { cchubEnv, getAccessToken } from "./cchub-auth";

    export type LineItem = {
    key: string;
    quantity: number;
    productId?: number;
    productVariantId?: number;
    productVersionId?: number;
    productLinkId?: number;
    sku?: string;
    properties: {
    _stateId: string[];
    _userId: string;
    };
    };

    export type WorkflowElementConfig = {
    component?: string;
    tagName?: string;
    configVersion?: number;
    resources?: Record<string, any>;
    settings?: Record<string, any>;
    localization?: Record<string, any>;
    };

    export async function getStorefrontUserToken(
    userId: string
    ): Promise<string> {
    const accessToken = await getAccessToken();
    const usersApi = new StorefrontUsersApiClient(createApiConfig(accessToken));

    try {
    const user: CreateStorefrontUserDto = {
    storefrontUserId: userId,
    isAnonymous: true,
    };

    await usersApi.create(cchubEnv.storefrontId, undefined, user);
    } catch (error) {
    if (getErrorStatus(error) !== 409) {
    throw new Error(`Cannot create storefront user. User ID = ${userId}.`, {
    cause: error,
    });
    }
    }

    try {
    return await usersApi.getToken(userId, cchubEnv.storefrontId);
    } catch (error) {
    throw new Error(`Cannot get storefront user token. User ID = ${userId}.`, {
    cause: error,
    });
    }
    }

    export async function getSimpleEditorWorkflowConfig(
    productId: number
    ): Promise<WorkflowElementConfig> {
    const accessToken = await getAccessToken();
    const productsApi = new ProductsApiClient(createApiConfig(accessToken));

    let content: any = "";

    try {
    const workflow = await productsApi.getPersonalizationWorkflow(
    productId,
    undefined,
    undefined,
    undefined,
    cchubEnv.tenantId
    );

    content = workflow.content;
    } catch (error) {
    throw new Error(`Cannot get product workflow. Product ID = ${productId}.`, {
    cause: error,
    });
    }

    if (content == null || content.length == 0) {
    throw new Error("The product personalization workflow is empty.");
    }

    return findSimpleEditorConfig(JSON.parse(content));
    }

    export async function createProject(lineItem: LineItem) {
    const accessToken = await getAccessToken();
    const projectsApi = new ProjectsApiClient(createApiConfig(accessToken));

    const body: CreateSingleItemProjectDto = {
    ownerId: lineItem.properties._userId,
    item: {
    productSpecifier: {
    type: ProjectItemProductType.Product,
    id: lineItem.productId,
    versionId: lineItem.productVersionId,
    variantId: lineItem.productVariantId,
    },
    designIds: lineItem.properties._stateId,
    sku: lineItem.sku,
    quantity: lineItem.quantity,
    },
    };

    try {
    return await projectsApi.createWithSingleItem(
    cchubEnv.storefrontId,
    undefined,
    body
    );
    } catch (error) {
    throw new Error("Cannot create a project from the Simple Editor result.", {
    cause: error,
    });
    }
    }

    function findSimpleEditorConfig(content: any): WorkflowElementConfig {
    const configs = Array.isArray(content) ? content : [content];
    const config = configs.find(x => x.component === "simple-editor");

    if (!config) {
    throw new Error("The product workflow does not contain the Simple Editor.");
    }

    return config;
    }

    export function getErrorStatus(error: any): number | undefined {
    return error?.response?.status
    ?? error?.status
    ?? (error instanceof Error ? getErrorStatus(error.cause) : undefined);
    }

    function createApiConfig(accessToken: string): ApiClientConfiguration {
    const config = new ApiClientConfiguration();
    config.apiUrl = cchubEnv.apiGatewayUrl;
    config.setAuthorizationToken(accessToken);

    return config;
    }

    This module registers a storefront user if needed, reads the personalization workflow assigned to the product through the GET /api/storefront/v1/products/{id}/personalization-workflow endpoint, extracts the Simple Editor configuration, and creates a project from the line item returned by the Simple Editor.

  6. Create server/server.ts.

  7. Add the Express routes.

    import express from "express";
    import {
    createProject,
    getErrorStatus,
    getSimpleEditorWorkflowConfig,
    getStorefrontUserToken,
    type LineItem,
    } from "./cchub-api";
    import { cchubEnv } from "./cchub-auth";

    const app = express();
    app.use(express.json());

    app.get("/", (_request, response) => {
    response.send("Simple Editor quick start backend is running.");
    });

    app.get("/api/editor-session", async (request, response) => {
    const userId = String(request.query.userId ?? "quick-start-user");
    const productId = Number(request.query.productId);

    if (!Number.isInteger(productId) || productId <= 0) {
    response
    .status(400)
    .json({ message: "The productId query parameter is required." });
    return;
    }

    try {
    const userToken = await getStorefrontUserToken(userId);
    const workflowConfig = await getSimpleEditorWorkflowConfig(productId);

    response.json({
    config: {
    ...workflowConfig,
    configVersion: 2,
    input: {
    productId,
    },
    integration: {
    tenantId: cchubEnv.tenantId,
    storefrontId: cchubEnv.storefrontId,
    cchubUrl: cchubEnv.baseUrl,
    cchubApiGatewayUrl: cchubEnv.apiGatewayUrl,
    user: {
    id: userId,
    token: userToken,
    },
    },
    resources: workflowConfig.resources ?? {},
    settings: workflowConfig.settings ?? {},
    localization: workflowConfig.localization ?? {
    language: "en",
    },
    },
    });
    } catch (error) {
    sendError(response, error);
    }
    });

    app.post("/api/projects", async (request, response) => {
    try {
    const lineItem = request.body as LineItem;
    const project = await createProject(lineItem);

    response.json(project);
    } catch (error) {
    sendError(response, error);
    }
    });

    app.listen(3001, () => {
    console.log("Backend is running at http://localhost:3001");
    });

    function sendError(response: express.Response, error: any): void {
    const status = getErrorStatus(error) ?? 500;
    const cause =
    error instanceof Error && error.cause != null
    ? ` ${String(error.cause)}`
    : "";
    const message =
    error instanceof Error ? `${error.message}${cause}` : "Unexpected error";

    response.status(status).json({ message });
    }

    It implements HTTP endpoints which work with Customer's Canvas Hub API to prepare the editor configuration and send the user's customization to the server for rendering.

  8. Run the dev script.

    npm run dev

    The terminal shows a Vite local URL and the backend message:

    Backend is running at http://localhost:3001

    Stop the command after both processes start.

Add the page code​

  1. Replace the content of src/main.ts. Replace 2502 with your product ID.

    const productId = 2502; // Replace with your product ID.

    await customElements.whenDefined("au-simple-editor");

    const editor = document.getElementsByTagName("au-simple-editor").item(0) as HTMLElement & {
    init(config: any): Promise<void>;
    };

    const session = await getEditorSession("quick-start-user", productId);

    editor.addEventListener("addtocart", async (event: any) => {
    const lineItem = event.detail;
    const project = await createProject(lineItem);
    console.log("Created project", project);
    });

    await editor.init(session.config);
    document.getElementById("status")?.remove();

    type EditorSession = {
    config: any;
    };

    async function getEditorSession(
    userId: string,
    productId: number
    ): Promise<EditorSession> {
    const params = new URLSearchParams({
    userId,
    productId: String(productId),
    });

    const response = await fetch(
    `/api/editor-session?${params.toString()}`
    );

    if (!response.ok) {
    throw new Error(await response.text());
    }

    return response.json();
    }

    async function createProject(lineItem: any) {
    const response = await fetch("/api/projects", {
    method: "POST",
    headers: {
    "Content-Type": "application/json",
    },
    body: JSON.stringify(lineItem),
    });

    if (!response.ok) {
    throw new Error(await response.text());
    }

    return response.json();
    }

    The page fetches the editor session from the backend and passes its config to init(). The OAuth2 client secret, the access token, and the storefront user token stay on the backend. In a real storefront, set productId from the current product page, route, or catalog state.

  2. Replace the content of src/style.css.

    html,
    body {
    height: 100%;
    margin: 0;
    }

    au-simple-editor {
    display: block;
    height: 100%;
    }

    #status {
    max-width: 720px;
    padding: 32px;
    font-family: system-ui, sans-serif;
    }

    The Simple Editor fills the browser viewport.

Run the app​

  1. Start the frontend and backend.

    npm run dev
  2. Open the local URL printed by Vite.

    The Simple Editor loads the product from the productId value in src/main.ts.

  3. Finish the customization flow and select Add to cart.

    The browser console prints the created project object.

Validate the result​

Make sure that:

  • The design preview loads on the left without errors in the browser console.
  • The input fields and product options appear on the right and update the preview when you change them.
  • After selecting Add to cart, the console prints an object with the properties._stateId array containing the saved design ID.

Check the project status and download the rendered result by visiting Projects in Customer's Canvas BackOffice.

Next steps​

Was this page helpful?