Skip to Content
Technical Articles
Author's profile photo Thales Batista

Short Circuit Fiori Launchpad bits: Turn it into a daft WebApp (PWA Movement)

Fiori Client removal from app stores was a buzz few months ago, big enough to make SAP rethink about its phasing out plan: postpone removal date, change distribution channel from (manual) ONE Support Downloads into a Managed distribution through stores (some business steps involved, but easier easier for common employees install on device).

Wouldn’t be nice if they embraced Progressive Web App philosophy into Launchpad roots? They sell as Fiori apps, why not take it seriously as an app platform?

While you and me daydream about it, let’s toy around what we have today.


Few days before publishing I saw this answer from Gregor Wolf mentioning a video from an earlier UI5Con. I saw it quickly (2x speed) and looks partially to what I tell here.

That video also focus on a proper Service Worker; something that I stub here for a reason also mentioned on video: there are quirks to use it on Launchpad. The main one for me, at development view, is being hostage of Framework/Vendor: there is no public documentation / API for Cache Buster tokens and (at the time of video) some UI5 requests were doing synchronous calls that bypass Service Worker. You can reverse-engineer the standard and implement into a functional state. It is unlikely to break between upgrades but the risk exists.

Sorry about some mobile screenshots taxing your scroll, blog engine sanitize any max-width/max-height attempt.

Chrome doesn’t provide a way to manually change its display language on Linux (only ABAP sandbox have English as OS language), so I took the freedom to take Mobile screenshots in Portuguese too (silent protest against poor translation quality in some areas of Solution Manager).

A (very) brief intro to PWA

Read this page.

What? It’s brief to me, three words. That’s not cheating, it’s reuse. Who already heard about this will thank me for not wasting their time.

Ok, I will extend a bit: PWA is a way to make Web Applications (Web pages)  have a “native application” feeling to end-users, and one of those things is allowing them to install it (aka. Add to Home Screen, A2HS), the main focus today.

Requirements to PWA be installable

Read this other page or this one.

Stop with this laziness man!

Seriously, read them. There are common requirements for all browsers, but each one can have a different requirement, because PWA is still a draft specification, even seven years after its first public draft.

Quick story short: to enable installation on Chrome, Firefox and Edge you must have two files declared in HTML: a Web Application Manifest and a JavaScript to register a Service Worker. Service Worker operates in a separate file, you’ll also have to write it and serve it too.

Functional stubs (at least on time of publication) for each resource are provided below for your laziness:
Web Application Manifest

    "name": "Bonder Lostpad",
    "short_name": "BLP",
    "icons": [
            "src": "/pwa/icon192.png",
            "sizes": "192x192",
            "type": "image/png"
            "src": "/pwa/icon512.png",
            "sizes": "512x512",
            "type": "image/png"
    "theme_color": "#ffffff",
    "background_color": "#000000",
    "display": "standalone",
    "scope": "/",
    "start_url": "/"

JavaScript to register a Service Worker

if('serviceWorker' in navigator) {
        .register('/pwa/sw.js', {scope: '/'})
        .then(function(registration) {
            console.log('Service Worker Registered with scope:',  registration.scope);

Service Worker itself

self.addEventListener('fetch', function(e) {
  // Hi, I'm Stubo!


Top tip: The easiest way to check if requirements to install are fulfilled is using Google Lighthouse, it pinpoints what is missing and what has error. This example, for example, will stop working in Chrome later in 2021, because it will be mandatory to have a offline mode.


Lighthouse results about our Launchpad enhanced with those stubs.

Now you know that the main focus to enable it lies in HTML and JavaScript running on it. Time to inspect Fiori Launchpad.

Bird’s-eye view onto Launchpad resources: identify which HTML pages are loaded

To know where to inject files we need to know what HTML pages make part of Launchpad experience. Developer console will aid you, it’s just a filter away.


Developer console tracing HTTP requests, filtered by Document (HTML).

Two HTML pages are identified: Login Screen and Launchpad itself. Too easy, I didn’t even sweat.

Non-invasive approach: Injection on HTTP response

The concept is very simple: ABAP system keeps untouched, we hijack some HTTP requests and inject PWA-related things into response, like a man-in-the-middle attack. Actually it is exactly like that and is legal: a Reverse Proxy doing content rewrite.

The one I used was Nginx, but you should be OK with any other like Apache. The needed features here are Content Rewrite and serve static files (for PWA specific resources).

Put the rules, spawn service and good to go!

# Only relevant bits of nginx config.
# It could be better written...
location  /pwa/ {
    alias /var/pwa_stuff/pwa/;
location = /pwa/sw.js {
    types { } default_type "text/javascript";
    add_header "Service-Worker-Allowed" "/";
    alias /var/pwa_stuff/pwa/sw.js;
location = / {
    proxy_pass https://system.somewhere/sap/bc/ui2/flp;
    # PWA subfilter: Inject the stuff
    sub_filter_once off;
    sub_filter '<head>' '<head><link rel="manifest" href="/pwa/manifest.webmanifest">';
    sub_filter '</body>' '<script type="text/javascript" src="/pwa/app.js"></script></body>';
    sub_filter '/sap/bc/ui5_ui5/ui2/ushell/shells/abap/FioriLaunchpad.html' '/';


Install prompt for Bonder Lostpad in desktop.


Install banner on mobile and also highlighting install option on browser menu.

Being installable, you can also have shortcuts for specific tasks (Fiori apps). Don’t expect too much, it is less flexible than native app shortcuts.


PWA shortcuts for Solution Manager specific apps.

Celebration tonight, celebrate, don’t wait too late…

“Aw man, you can’t stop now. This will not fulfill your blogging quota,” ten minutes rule said.

“But we’re just going to celebrate,” said me. “Can I Crescendolls ipsum it?” I thought.

Shoot, I forgot why I wanted to blog this. That’s what happens when you stop to listen some robots rocking (those aren’t the ones you are thinking of ). Let’s get back to the program.

One more time, ABAP approach

Many ABAP landscapes have a kind of Reverse Proxy running called SAP Web Dispatcher. I don’t recall any Rewrite Content feature on it (I could be wrong, it’s not my expertise), but still this software is handled as a sensitive part of Landscape and is out of bounds for a common Developer, the brave one picked in the crowd to make things like this work. Let’s try another approach: patch that in a developer way, there’s nothing wrong with just a little fun.

We saw that two HTML pages were loaded. Login one isn’t really needed to address, because it only makes sense to allow installation for users logged in.

Launchpad Page: Cimico Quo

From the other developer folk tale you should now know how to find the link between Fiori and ABAP, and this one is no different.


Mapping of developer console HTTP request and SICF handler implementation.

After some effort voyaging around the code you’ll find where SAP render Fiori Launchpad page, precisely at some bits of /UI2/CL_FLP_HTTP_HANDLER class. They even do some content injection on template.


A nice spot to implicit enhancement would be inside of inject_config_metatags method, where standard does content injection into Fiori HTML page.

We’ll take advantage of that aerodynamic part and buff it with an implicit enhancement, our injection.

constants pwa_resources_path type string value `/sap/public/zflp_pwa_res`.
data(content) = ev_html_with_config.
content = replace( val = content sub = `</head>` with = |<link rel="manifest" href="{ pwa_resources_path }/manifest.webmanifest"> </head>| occ = 1 ).
content = replace( val = content sub = `</body>` with = |<script type="text/javascript" src="{ pwa_resources_path }/app.js" async></script></body>| occ = 1 ).
ev_html_with_config = content.

Refreshing Launchpad you’ll get network errors, as expected, because two new resources were pointed (manifest.webmanifest and app.js) in HTML but ABAP server doesn’t know them, yet.

Serving static content: High Life on Public node

You can try but you won’t be able to directly create a PUBLIC child node. No worries about that, KBA 2478325 explains a bugnot-so-trivial feature to accomplish this (yes, you could avoid a bit of this creating as a child of an existing leaf, but you would lose all the fun).

Why am I serving from a public node? Nothing special. My first experiments had Login page as part of PWA experience, and then you have to go public (No-user user in action).

A node needs a handler class, time to implement it. I recommend using the MIME repository to store PWA files, because you get a nice API to get it back, either on code (it’s amazing what you’ll find sneaking around API word), or codeless if you know the drill (read documentation).


method if_http_extension~handle_request.
  check server->request->get_method( ) eq if_http_request=>co_request_method_get.
  data(path) = server->request->get_header_field( if_http_header_fields_sap=>path_info ).

  case path.
    when `/app.js`
      or `/manifest.webmanifest`
      or `/sw.js`
      or `/icon192.png` or `/icon512.png`.

      " Expected resources for PWA. You shall pass.
    when others.
      " Someone meddling with service. Mock them!
      server->response->set_cdata( `Ah ah ah! You didn't say the magic word!` ).
      server->response->set_status( code = 400 reason = if_http_status=>reason_400 ).

  data(mime_api) = cl_mime_repository_api=>get_api( ).
      i_url = |/SAP/PUBLIC/ZFLP_PWA_MIME{ path }|
      i_check_authority = abap_false " "No-user" user is used when not logged in, he has no authorizations.
      e_content = data(content)
      e_mime_type = data(content_type) ##type
      others = 0 ).
  server->response->set_data( content ).
  server->response->set_content_type( content_type ).

  if path eq `/sw.js`.
    server->response->set_header_field( name = `Service-Worker-Allowed` value = `/` ).


This is enough. Just load Web Manifest and Service Worker files into MIME and it is ready for the stage.

Can you see it? Oh can you see it


Install prompt directly from our ABAP changes.


Mobile install banner from our ABAP efforts.


Mobile homescreen after installing both cases.

The aftermath

You could also take both experience and knowledge to a next level, like fiorize installation experience through a Launchpad Plugin. You can toy with other things to improve PWA experience, but you’ll always have to think twice if that requires something that relies on ABAP side: “Am I jeopardizing myself doing that?” “Is that code released for customers?” “Is there a public API for that?” You’ll have to develop, mend, maintain, check on every support package upgrade.

Want the better experience? Be picky and make your company expenses worth by influencing the software, or meet us on some shady corner mumbling about immutable decisions took inside.

As said earlier, PWA is still a draft specification. I wanted to have a different set of shortcuts per user role, but that is not possible now.

A git repo thalesvb/flponprem-pwa is available for your joy. Ready-to-go, one BAdI implementation away to activate this feature, bundled with one example.

Our work is never over, but this blog is. Merci à vous et à bientôt!


Meta-task: All tracks of Discovery album were referenced in this post, some of them more than once. Why don’t you play the game? (this one doesn’t count).

Assigned Tags

      You must be Logged on to comment or reply to a post.
      Author's profile photo Martin Ceronio
      Martin Ceronio

      Brilliant, just what I was looking for. I need to implement a Web App manifest link in FioriLaunchpad.html as it is the only way I have managed to get the Launchpad in full screen on Android with Chrome.

      Fortunately Gregor Wolf pointed me to this post of yours.

      Thank you very much!

      Author's profile photo Martin Ceronio
      Martin Ceronio

      Hello Thales, not even a month after your blog post, the page you referred to that specified the criteria for offline mode for a PWA was updated with a notice that reads as follows:

      "Updated April 14th, 2021: We previously announced plans to update the installability criteria to ensure a PWA actually provides an offline experience. After listening to your feedback, and discovering a number of issues, we have decided to put those plans on hold. We strongly believe providing a valid page when the user is offline is critical to providing a good user experience."

      So, thankfully, (for now) that is not a requirement.

      Fortunately, providing an offline experience does not necessarily mean providing a fully functioning app. It could just be a custom fallback page, as they point out.

      Author's profile photo Thales Batista
      Thales Batista
      Blog Post Author

      Yep, but is still important.

      At least all standard (and real UI5) Fiori apps that I work with they all (don't) handle offline state (and act) as a sever error, throwing multiple flavors of "Please contact your system administrator". So handling offline at FLP level is a must to avoid unnecessary calls to support.

      Author's profile photo Martin Ceronio
      Martin Ceronio

      But the tech support guys are lonely and just waiting for a call.

      At least the guy in the picture does.

      Author's profile photo Martin Ceronio
      Martin Ceronio

      Hi, it's me again. I just wanted to add something related to the service worker.

      My problem is twofold: Firstly, I know little to nothing about service workers and, secondly, I obviously didn't read this blog post well enough.

      My problem, that I discovered after implementing this solution, was that calls from UI5 were going to the back-end twice, and the service worker somehow seemed responsible, from what I could tell in the network trace in Developer Tools.

      The problem, it seems, is, this service worker implementation that I had found somewhere else and had copied and pasted blindly:

      self.addEventListener('install', function(e) {});
      self.addEventListener('fetch', function(e) { fetch(e.request).then(function(response) { return response; })});

      I have now just used a fetch event handler as per this blog, where the callback function body is empty, and I think this solved the issue. (Expect another comment from me if it doesn't).

      Author's profile photo Salvatore Bruno
      Salvatore Bruno

      Hello Thales,

      I've some questions about your blog which opened my mind. With my colleague we are trying to activate the launchpad as PWA.

      I try to recap

      • There is a Launchpad page copy or you did all in the standard with enhancements?
      • How is SICF for ZFLP_PWA is configured? Have you a screenshot to show?
      • Method if_http_extension~handle_request. This is a code that has to put as enhancement or  is about the code in the SW SICF node?
      • You mentioned a BADI into the code snapshot you left but there is no example code: is mandatory to have it?
      • Where the index file has been changed

      Thanks for your time.

      Author's profile photo Thales Batista
      Thales Batista
      Blog Post Author


      1. There is no Launchpad page copy. The change is done with an implicit enhancement on inject_config_metatags standard method mentioned on blog. There, the Launchpad page (index file) is available in a variable, so you can modify it on runtime without "destroying" the original MIME SAP-provided page.
      2. Everything is on default value, and the class you coded if_http_extension~handle_request method should be assigned on Handler List tab. Unfortunately I don't have access anymore to any system with this in place, neither my NetWeaver box. From what I recall is pretty much the same done on this blog, no other change.
      3. The method if_http_extension~handle_request is implemented in a Customer-namespace class (that class implementing IF_HTTP_EXTENSION interface, a requirement to assign it to a SICF node). The class you create for this code is the one you will assign to the SICF ZFLP_PWA node (although on blog it is named as ZFLP_PWA_RES. The only important thing is SICF node name match the name you define on pwa_resources_path constant, otherwise you'll get HTTP 404 errors due not locating the resources).
      4. Not mandatory. The BAdI I mentioned was only a way to proof to SAP that they could adapt their code to allow (and make simple to) customers enable a PWA-aware Launchpad through a BAdI implementation. It is a refactored version from the code produced by the steps mentioned on this blog, and published over a GitHub repo.
      5. See answer #1.
      Author's profile photo Salvatore Bruno
      Salvatore Bruno

      Hello Thales, thanks for the answers.

      I'm stil missing a couple of things:

      1. Why the manifest file is not in source file as app.js and is not called from the new handler?
      2.  file sw.js: where I have to put it? I put all files on MIME repository under /SAP/PUBLIC/ZFLP_PWA_MIME (my folder) and seems the system doesn't like the sw file together with others.
      3. SICF: is correct at this level?


      EDIT: we did some steps forward, and we are getting this error

      Seems the scope of the WS does not match with something but Not sure where...


      Thanks again for your patience.



      Author's profile photo Thales Batista
      Thales Batista
      Blog Post Author

      I found out why you had this scope error, the snippet for handle_request method was not the latest version I generated to this blog... my bad.

      When it is processing to load sw.js file, the ABAP server should also return a Header field named Service-Worker-Allowed. You should put something like:

      if path eq `/sw.js`.
        server->response->set_header_field( name = `Service-Worker-Allowed` value = `/` ).

      I'll edit the post to reflect this.


      Author's profile photo Salvatore Bruno
      Salvatore Bruno

      Thales, it worked!

      Now we have the issue that Martin reported above and is that the SW is called twice. But we are at a very good point.

      Thank you very much, you deserved a fresh beer!



      Author's profile photo Martin Ceronio
      Martin Ceronio

      Hello Salvatore, I would be interested to know what was the cause in your case.

      In my case, I could see OData calls going to the back-end twice, but when I changed my SW implementation to have a blank fetch implementation as per my January 11 comment, the problem was went away.

      (Sorry, in that comment I posted the problem code, not the solution code).

      Author's profile photo Salvatore Bruno
      Salvatore Bruno

      Hello Martin,

      your code worked to avoid double SW installation. We didn't catch the route cause and we didn't have odata called twice. Another point, SW was not always installated twice, but randomly.

      Author's profile photo Brendan Farthing
      Brendan Farthing

      I'm curious if anyone has managed to do this with the SAP BTP (Cloud) Fiori Launchpad?

      I'd like to make our BTP Fiori Launchpad 'installable' as a PWA on iOS and Android devices.

      Came across this great blog in my search for information. Wonderful for our ABAP systems, but I need to do this on BTP, ideally with the standard Launchpad (or "SAP Build Workzone, standard edition" as it's now called).

      Author's profile photo Thales Batista
      Thales Batista
      Blog Post Author

      In theory should be possible.

      As long as you can deploy a Launchpad Plugin you can modify/inject tags into Launchpad HTML  through good ol' JavaScript DOM. Indeed, you can have all code and assets served as a Launchpad Plugin... everything worked on local development..

      The main issue is how to make it serve ServiceWorker file with Service-Worker-Allowed header to define its scope, otherwise it won't register (like one issue mentioned on other comment). This point could be tackled by appRouter, at least on paper...