If you are a startup and already have a website for your business, at some point in time you are starting to look in the direction of the mobile app.

Website to PWA and mobile

Your customers weekly ask you about the mobile app or the offline access to your website. Yes, you are ready to start your Mobile App journey, but your human resources are limited: customers also request new features for the website, so the mobile app development will make the web features progress slowly. Yes, your website has a mobile-friendly design but it doesn’t work offline (surprisingly), and there are no fancy push notifications and access to the mobile phone features.

In this case, we see 3 options for how to solve it:

  • To develop the mobile app that will provide minimum viable features to your product;
  • To develop a Progressive Web App (PWA) for your website;
  • To combine options 1 and 2 - and run your PWA in the mobile app!

Let’s review these options in detail.

1. The development of the mobile app.

In this case, you need to decompose your website features by priority level / must have / critical / etc. and create a mobile development roadmap. Define the minimum viable features for the first release and develop the minimum viable product (MVP). The features that were not delivered in the MVP (but you want them to be in the mobile app) will be delivered in future releases.

If your targets are iOS and Android markets, take a look at cross-platform frameworks (React Native, Ionic Framework, Flutter) and develop your app for both markets in parallel.

2. The development of Progressive Web App (PWA).

Your website works in the browser until the user has an internet connection. To use the website in offline mode, you need to cache (store the data in memory) all your website resources: HTML files, images, javascript files, CSS files and data you display on the pages. With the PWA you can store all the static resources (HTML, images, javascript, styles) to cache for future needs and implement a logic to store the data.

What is a Progressive Web App?

A Progressive Web App is a web app (your website) that delivers an app-like experience to users based on modern web capabilities. In fact, it's just your website that runs in a browser with some enhancements. It gives you the ability to:

  • Install it on a mobile home screen;
  • Access it when there is no internet connection;
  • Access the camera;
  • Get push notifications;
  • Do background synchronization and much more.

How does it work?

If the user has an internet connection - your website works as usual. If there is no connection - the app reads the cached (stored) files and continues to work with some limitations. The main actors in the PWA are Service Worker - that helps you to control storing the data and resources, and Cache API - where you store all this.

apply

What are the benefits?

Your website will work offline, also you can save an icon on the phone home screen and open the website not just in the browser, but in the standalone window like the installed mobile app.

That sounds good, but the limitations of PWA are the following: there are no push notifications, inability to install from the mobile apps markets (AppStore, Google Play), and the inability to use native libraries and mobile phone features (storage, BLE, etc.).

What do you need to do to develop PWA?

  • Your website must use HTTPS;
  • Create a manifest.json file:
{
  "name": "Appelian",
  "short-name": "Appelian",
  "description": "Appelian mobile app",
  "dir": "auto",
  "lang":"en",
  "icons": [
    {
      "src": "/icon-192x192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    ...
  ],
  "display":"standalone",
  "scope": "/",
  "orientation": "natural",
  "background_color": "#ffffff",
  "theme_color": "#ea6391",
  "start_url": "/",
  "categories": ["business", "technology", "web", "mobile"]
}
  • Connect manifest file in the index.html head tag:
<html lang="en">
  <head>
    ...
    <link rel="manifest" href="manifest.json" />
  </head>
  <body>
  </body>
</html>
  • Create a service worker:

const version = 'v1';
const staticResources = [
    '/',
    '/path-to-js-file.js',
    '/path-to-css-file.css',
    '/some-font.ttf',
    '/some-icons.png',
    '/website-image1.png',
    '/website-image2.png',
    '/en.json',
    '/any-other-static-resource',
    '/manifest.json',
];

self.addEventListener('install', (event) => {
    event.waitUntil(cacheStaticFiles());
});

const cacheStaticFiles = async () => {
    const cache = await caches.open(version);
    const keys = await cache.keys();

    return keys.length === 0 ? 
        cache.addAll(staticResources).then(self.skipWaiting())
        : return self.skipWaiting(); 
};

self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(keys => Promise.all(keys.map((key) => {
          return version !== key ? caches.delete(key) : Promise.resolve();
        }))).then(self.clients.claim())
    );
});

self.addEventListener('fetch', (event) => {
    const isNetworkRequest = event.request.url.startsWith('http');
    const isPostRequest = event.request.method === 'POST';

    if (isNetworkRequest && !isPostRequest) {
        event.respondWith(caches.match(event.request.url, { 'ignoreSearch': true })
            .then(function (response) {
                if (response) {
                    /**
                     * use already cached response
                     */
                    return response;
                } else {
                    /**
                     * fetch from the network
                     */
                    return fetch(event.request)
                        .then(function (response) {
                            return caches.open(version)
                                .then(function (cache) {
                                    /**
                                     * save the response to cache and return the result
                                     */
                                    event.waitUntil(cache.put(event.request.url, response.clone()));
                                    return response;
                                });
                        })
                        .catch(function (e) {
                            /**
                             * fallback
                             */
                            console.log('An error occurred:', e);
                            return caches.open(version)
                                .then(function (cache) {
                                    return cache.match('/');
                                });
                        });
                }
            })
        );
    }
});
  • Connect the service worker in the html file:
<head>
</head>
<body>
  ...
  <script>
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js')
        .then(function(result) {
          console.log('A SW registered', result);
        })
        .catch(function(e) {
          console.log("A service worker registration error occurred", e);
        })
    }
  </script>
</body>
  • Store the website data. In my example, I have the Apollo GraphQL to communicate with the backend. To store the data, I’m using the apollo-cache-persist plugin, it helps to access the data in offline mode.

3. Combine the PWA and the mobile app.

The most interesting is to combine the PWA and the mobile app. How does it work? You have a mobile app. Inside the mobile app, you have a browser to display your website. This browser’s name is a WebView - it’s a special component to display webpages inside the native app.

apply

So, the following steps will be:

  • To develop the PWA for your website;
  • To create a fresh new mobile app based on React Native or Expo and define the name, icons, bundle ID and app description.
  • To install the react-native-webview plugin. It’s our browser component for the mobile app to display your website, it’s name is a WebView. Add the component to your mobile app entry point (index.js or App.js file);
  • To configure the WebView component to cache your website resources:
<WebView 
....
cacheEnabled={true} 
cacheMode={isOffline ? 'LOAD_CACHE_ONLY' : 'LOAD_DEFAULT'}
limitsNavigationsToAppBoundDomains={true} 
...
 />

On iOS 14+, wkwebiew (WKWebView is our WebView implementation for iOS and macOS) has service workers and cache API, to enable it in your mobile app you need to define the limitsNavigationsToAppBoundDomains and WKAppBoundDomains for iOS. You can define it manually in the Info.plist file.

<key>WKAppBoundDomains</key>
<array> 
<string>your-website.com</string> 
</array>

Or define it in the Expo configuration file (app.json) in “ios” section:

{
  ...
  "ios": {
    ...
    "infoPlist": {
      "WKAppBoundDomains" : [
        "your-website.com"
      ]
    }
  }
}

What we got as a result of combing PWA and a mobile app?

Now you are able to display all your website inside the mobile app, have the ability to connect push notifications, work in offline mode or use any other native library if needed.

To achieve the final goal and be published in the Appstore or/and Google Play - you need to build Android .aab and iOS .ipa files and go ahead to the stores!

To improve the build process for your mobile apps and take more control - read our PoC to MVP article where we helped to set up the builds automation.

Who am I?

My name is Aleksei, I am a Co-Founder at Appelian Software.

15+ years in the software development. Experienced Delivery Manager.

Appelian Software plans, designs, and develops MVPs ready to enter the market and be prepared for funding. On top of that, we help to migrate software to a new tech stack or improve the performance of the product.

StartupMVPPoCPWAMobileOfflineCacheService workerReact NativeCross-platformWebViewWebsite