cco-web-app-bridge is now Open Source: Build your first SAP CCO Web App Plugin

A few weeks ago I published an article about a different approach to SAP Customer Checkout plugin development: embedding a standard Single Page Application inside a CCO plugin, communicating with the POS through a postMessage bridge, and shipping everything as a single offline-capable JAR. If you haven't read it yet, I recommend checking it out first for the full backstory and motivation.

Today I'm happy to announce that the library behind this concept -- cco-web-app-bridge -- is now open source and available on Maven Central. Alongside it, I'm releasing a complete demo plugin called FoodInfo that you can use as a reference or starting point for your own projects.

The concept in a nutshell

The idea is straightforward: instead of building complex plugin UIs in vanilla JavaScript using the CCO component model, you build a standard SPA (Angular, React, Vue -- whatever you prefer) and embed it in an iframe inside the POS. A JavaScript bridge handles the communication between your app and the CCO APIs -- receipt data, event bus, store access -- all exposed as a clean async API. The SPA gets bundled into the plugin JAR, so the whole thing works offline without any external web server.

Concept: your SPA runs in an iframe inside SAP CCO and talks to the bridge plugin via postMessage, which connects to the CCO APIs

From the SPA side, talking to the POS looks like this:

const pos = new POSBridge();
await pos.ready();

const receipt = await pos.getReceipt();
pos.on('receiptChanged', (data) => { /* react to changes */ });
pos.pushEvent('SHOW_MESSAGE', 'Hello from the iframe!');

Building a plugin step by step: The FoodInfo example

The FoodInfo plugin looks up product nutritional information from the Open Food Facts API based on the barcode of the currently selected receipt item. It has two views: a compact embedded card in the POS UI, and a full-screen popup with detailed product information, NutriScore, allergens, and nutrition facts.

Let me walk you through how to build a plugin like this from scratch.

Prerequisites

Step 1: Set up the Maven project

The cco-web-app-bridge library is published on Maven Central, so you can add it as a regular dependency. The ENV.jar is provided scope since CCO supplies it at runtime.

<dependencies>
    <dependency>
        <groupId>dev.baust.cco.webapp.bridge</groupId>
        <artifactId>cco-web-app-bridge</artifactId>
        <version>X.X.X</version>
    </dependency>
    <dependency>
        <groupId>com.sap.customercheckout.pos</groupId>
        <artifactId>ENV</artifactId>
        <version>3.X.X</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

To build the Angular frontend as part of the Maven build, add the frontend-maven-plugin. It installs Node automatically and runs npm install and npm run build during the generate-resources phase. The maven-shade-plugin then bundles everything -- Java classes, the bridge library, and the compiled SPA assets -- into a single JAR.

The FoodInfo demo's pom.xml shows the complete build configuration.

Step 2: Create the plugin class

This is the Java side. Your plugin class extends AbstractWebAppBridgePlugin and wires the CCO annotations to the bridge methods. The constructor takes a unique prefix string -- this prevents collisions when multiple bridge-based plugins run simultaneously.

public class FoodInfoPlugin extends AbstractWebAppBridgePlugin {

    public FoodInfoPlugin() {
        super("FOODINFO");
    }

    @Override
    public String getId() { return "FoodInfoPlugin"; }

    @Override
    public String getName() { return "Food Info Web App Bridge Plugin"; }

    @JSInject(targetScreen = "NGUI")
    public InputStream[] jsInject() {
        return getBridgeJsInject();
    }

    @CSSInject(targetScreen = "NGUI")
    public InputStream[] cssInject() {
        return new InputStream[]{};
    }

    @ListenToExit(exitName = "PluginServlet.callback.get")
    public void pluginServletGet(Object caller, Object[] args) throws Exception {
        handleBridgeServletGet(caller, args);
    }

    @ListenToExit(exitName = "PluginServlet.callback.post")
    public void pluginServletPost(Object caller, Object[] args) throws Exception {
        handleBridgeServletPost(caller, args);
    }

    @ListenToExit(exitName = PluginExitPoints.TECH_CONTROLLER_UI_EVENT_CHANNEL)
    public void uiEventChannel(Object calledBy, Object[] args) {
        handleBridgeUiEventChannel(calledBy, args);
    }
}

That's the entire Java side. The base class handles all the complexity -- serving the SPA files, managing postMessage communication, proxying external API calls, and translating between the iframe and CCO's stores and event bus.

Step 3: Create the SPA

Create a webapp/ directory in your project root for your Angular (or React, Vue, etc.) application.

Include the POS Bridge SDK -- The bridge library ships a client-side SDK file called pos-bridge-sdk.js (you'll find it in the library's resources at src/main/resources/pos-bridge-sdk.js). Copy this file into your webapp project, for example into webapp/src/assets/pos-bridge-sdk.js. Then configure your build to place it at the root of the output directory. In Angular, you do this in angular.json:

"assets": [
  { "glob": "**/*", "input": "public" },
  { "glob": "pos-bridge-sdk.js", "input": "src/assets" }
]

And load it in your index.html:

<script src="pos-bridge-sdk.js"></script>

This gives your SPA access to the POSBridge class that handles all communication with the POS.

Routing with hash location -- CCO serves the iframe content through its PluginServlet, so you need hash-based routing:

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withHashLocation()),
    provideHttpClient(withInterceptors([proxyInterceptor])),
  ],
};

The two routes correspond to the two display modes: #/embedded for the inline view and #/popup for the full-screen overlay.

The CORS proxy interceptor -- When running inside the POS, your SPA can't make direct API calls to external services due to CORS restrictions. The bridge provides a proxy endpoint for this. A simple HTTP interceptor rewrites external requests to go through the proxy when running in the POS environment:

export const proxyInterceptor: HttpInterceptorFn = (req, next) => {
  const inPOS = window.location.pathname.includes('PluginServlet');
  const isExternal = req.url.startsWith('http');

  if (!inPOS || !isExternal) {
    return next(req);
  }

  const params = new URLSearchParams(window.location.search);
  const action = params.get('action') || '';
  const prefix = action.replace(/Servlet$/, '');
  const proxyUrl =
    `${window.location.origin}${window.location.pathname}?action=${prefix}Proxy`;

  const proxyReq = req.clone({
    method: 'POST',
    url: proxyUrl,
    body: {
      url: req.url,
      method: req.method,
      headers: {} as Record<string, string>,
      body: req.body ? JSON.stringify(req.body) : '',
    },
  });

  return next(proxyReq);
};

This interceptor is transparent -- when you run ng serve locally for development, external API calls go directly. When running inside CCO, they go through the proxy automatically.

Talking to the POS -- Create a service that wraps the POSBridge SDK. In the FoodInfo demo, the embedded view subscribes to ReceiptStore changes to automatically look up the barcode of the selected item:

ngOnInit(): void {
  const receiptStore = this.pos.store('ReceiptStore');

  this.pos.ready$.subscribe(async () => {
    const selectedItem = await receiptStore.getSelectedItem();
    this.lookup(selectedItem?.material?.gtin);
  });

  receiptStore.subscribe(async () => {
    const selectedItem = await receiptStore.getSelectedItem();
    this.lookup(selectedItem?.material?.gtin);
  });
}

The store access API is generic - you can call any method on any CCO store without needing changes to the bridge code.

To open the popup view from the embedded view, push the prefixed show event:

openPopup(): void {
  this.pos.pushEvent('FOODINFO_SHOW_WEBVIEW', { gtin: this.barcode() });
}

Step 4: Configure the embedded view in CCO

Add the embedded iframe to a Quickselection or layout using DynamicProperties. The key follows the pattern {PREFIX}_WEBVIEW_EMBEDDED:

{
  "complex": {
    "component": "ContainerComponent",
    "props": {
      "static": {},
      "dynamic": "#dynamicProperties:FOODINFO_WEBVIEW_EMBEDDED"
    }
  }
}

Step 5: Build and deploy

mvn clean package

This compiles the Java code, builds the Angular app, and bundles everything into a single JAR. Copy the JAR into the CCO plugins directory (using an isolated classloader subfolder like CL_foodinfo-plugin/ is recommended) and restart SAP Customer Checkout.

Screenshots

Embedded Popup

Development workflow

One of the biggest advantages of this approach is the development experience. You can run ng serve (or the equivalent for your framework) and work on the UI with hot-reload at http://localhost:4200. The views work standalone for UI development -- bridge calls simply won't connect until you run inside the POS. This makes iterating on the frontend much faster than the traditional CCO plugin development cycle.

What's included in the library

What's next

I plan to keep extending the library based on feedback. If you build something with it, I'd love to hear about it. Some areas I'm looking into:

The library is licensed under LGPL v3, the FoodInfo demo under MIT. Contributions and feedback are welcome.

Give it a try and let me know what you think -- I'm curious what use cases the community comes up with!

Links

ccoweb-app-bridgeopen-sourceangular