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
pnpmpackage 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:
- Rename the folder:
src/apps/my-custom-ui/→src/apps/your-app-id/ - Open
src/appConfig.tsand 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,
),
}
- Update the
?app=query parameter in your host application (for example, in your Salesforce configuration) to useyour-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 beforeCHANNEL-READYis 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:
| Prop | Type | Description |
|---|---|---|
messageHandlerService | IMessageHandlerService (optional) | Service for registering message handlers and sending payloads to the host |
config | AppConfig (optional) | App configuration metadata |
mock | boolean (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.