Skip to main content

Custom Apps

Custom Apps let you register entirely new application experiences inside enosix Cloud UI. Use this approach when you need to build a bespoke workflow that goes beyond what component-level overrides can provide.


How It Works

When a user opens enosix Cloud UI from a host application like Salesforce, Cloud UI determines which experience to display based on a simple identifier passed along with the request.

Custom Apps extend this model. You build your own experience as a React component, give it a unique name, and register it with Cloud UI. From that point on, when the host application requests your app by name, Cloud UI loads your component — the same way it loads any built-in app.

Your custom app also participates in the same communication channel as the built-in apps, so it can receive context from the host (such as customer or order data) and send events back when the user completes or cancels an action.


Prerequisites

  • Node.js 18 or higher installed
  • pnpm package manager installed (npm install -g pnpm)
  • Access to the enosix npm registry (credentials required — contact your enosix representative)
  • Familiarity with React and TypeScript

Step-by-Step Guide

Step 1: Scaffold the project

Run the create-cloud-ui scaffolding tool:

npx @enosix/create-cloud-ui

The tool will prompt you for a few details. When asked to select a template, choose Custom UI Application:

Select a project template:
VC-UI Application - ...
SalesDoc UI Application - ...
❯ Custom UI Application - A cloud-ui template for building custom embedded applications with message channel support

Complete the remaining prompts (project name, display name, host URL, etc.). The tool will install dependencies automatically. When it finishes, you will see:

🎉 Project my-custom-app created successfully!

Next steps:
cd my-custom-app
pnpm run dev

For a full walkthrough of the scaffolding prompts, see the Setup and Deployment Guide.

The generated project already includes a working example app at src/apps/my-custom-ui/CustomApps.tsx and the wiring in src/appConfig.ts and src/main.tsx. The steps below explain how to rename it and build your own logic.


Step 2: Rename the example app

The scaffolded project uses my-custom-ui as a placeholder app ID. Rename it to match your use case:

  1. Rename the folder: src/apps/my-custom-ui/src/apps/your-app-id/
  2. Open src/appConfig.ts and update the key and import path to match:
export const CUSTOMER_APP_MAP: Record<string, LazyAppComponent> = {
'your-app-id': createOverridableComponent(
'your-app-id',
() => import('./apps/your-app-id/CustomApps'),
overrides,
),
}
  1. Update the ?app= query parameter in your host application (for example, in your Salesforce configuration) to use your-app-id.

Step 3: Build your app component

Open src/apps/your-app-id/CustomApps.tsx. This file is the entry point for your custom experience. Replace the placeholder content with your own UI and logic.

Your component receives AppComponentProps and is responsible for registering a message handler so the host application knows when it is ready.

// src/apps/your-app-id/CustomApps.tsx
import { useEffect, useRef, useState } from 'react'
import { useMessageChannel } from '@enosix/cloud-ui/core/MessageChannel/useMessageChannel'
import { CommonSubjects } from '@enosix/cloud-ui/shared/utilities/messageChannelEvents'
import { BaseContext } from '@enosix/cloud-ui/shared/types/context'
import { MessageChannelItem } from '@enosix/cloud-ui/shared/types/models'
import { AppComponentProps } from '@enosix/cloud-ui/core/types/app'
import LoadingSpinner from '@enosix/cloud-ui/shared/components/LoadingSpinner'
import { Box, Typography } from '@mui/material'

// Must match the name used when registering the handler
const HANDLER_NAME = 'MyCustomUi'

const MyCustomApp = ({ messageHandlerService }: AppComponentProps) => {
const [context, setContext] = useState<BaseContext | null>(null)
const registered = useRef(false)
const pendingContext = useRef<BaseContext | null>(null)
const mounted = useRef(false)

const handler = (ev: MessageEvent<MessageChannelItem<unknown>>) => {
const payload = ev.data
if (payload?.source !== 'host' || !payload?.subject) return

if (payload.subject === CommonSubjects.common.INITIAL_CONTEXT) {
const data = payload.data as BaseContext
if (mounted.current) {
setContext(data)
} else {
pendingContext.current = data
}
}
}

// Register synchronously on first render so the handler name is included
// in the channel wait list before CHANNEL-READY is sent to the host.
if (!registered.current && messageHandlerService) {
messageHandlerService.registerHandler(HANDLER_NAME, handler, [])
registered.current = true
}

useEffect(() => {
mounted.current = true
if (pendingContext.current) {
setContext(pendingContext.current)
pendingContext.current = null
}
return () => {
mounted.current = false
}
}, [])

useMessageChannel(HANDLER_NAME, handler, [context])

if (!context) {
return <LoadingSpinner />
}

return (
<Box sx={{ p: 3 }}>
<Typography variant="h5">My Custom App</Typography>
{/* Your application UI goes here */}
</Box>
)
}

export default MyCustomApp

Important: Register the message handler synchronously during the first render (before useEffect). This ensures the handler name is included in the channel wait list before CHANNEL-READY is sent to the host application.

Step 4: Verify in the Test Harness

Start the development server and open the Test Harness:

pnpm run dev

Your app ID should appear as an option in the Test Harness app selector. Select it and confirm your component loads and receives the initial context from the host.


Adding More Apps

To register additional custom apps alongside the one scaffolded by the template, add more entries to CUSTOMER_APP_MAP in src/appConfig.ts:

export const CUSTOMER_APP_MAP: Record<string, LazyAppComponent> = {
'app-one': createOverridableComponent('app-one', () => import('./apps/app-one/CustomApps'), overrides),
'app-two': createOverridableComponent('app-two', () => import('./apps/app-two/CustomApps'), overrides),
}

Create a corresponding src/apps/<app-id>/CustomApps.tsx file for each entry. Each app ID must match the ?app= query parameter the host uses when opening the iframe.


AppComponentProps Reference

Every custom app component must accept AppComponentProps:

PropTypeDescription
messageHandlerServiceIMessageHandlerService (optional)Service for registering message handlers and sending payloads to the host
configAppConfig (optional)App configuration metadata
mockboolean (optional)When true, the app runs in mock mode without a live SAP connection

Import the type from @enosix/cloud-ui/core/types/app.


Sending Messages Back to the Host

Use sendPayload from useMessageChannel to send events back to the host application (for example, to signal cancellation or completion):

import { MessageChannelPayload } from '@enosix/cloud-ui/shared/utilities/messageChannelPayload'

const { sendPayload } = useMessageChannel(HANDLER_NAME, handler, [context])

const handleCancel = () => {
sendPayload(
MessageChannelPayload({
source: 'custom-app',
subject: 'CANCEL',
}),
)
}

Refer to the host application's message channel configuration for the full list of supported subjects.