quasarframework / quasar

Quasar Framework - Build high-performance VueJS user interfaces in record time
https://quasar.dev
MIT License
25.87k stars 3.51k forks source link

Support deploying a PWA with capacitor as native app #10934

Open sluedecke opened 3 years ago

sluedecke commented 3 years ago

Is your feature request related to a problem? Please describe.

My web app uses a service worker to capture fetch requests from the browser and add an authentication header. E.g. for all <img src=.../> tags and similar. This works well for a web app deployed as pwa.

Another use case is to do some extensive prefetching within the service worker in order to go offline with the app.

This does not work at all as a capacitor app since:

Describe the solution you'd like

I would love to have an option to deploy my PWA as a capacitor project by adding a new mode to quasar dev/build or some modifier to the capacitor mode.

Describe alternatives you've considered

Alternative is to add the authentication header as a query parameter to the <img src=.../> and adapt my server to look for authentication in both the request headers and query parameters. This is implemented and works.

For prefetching one could move the logic from the service worker back into the app itself. I did not test that yet.

sluedecke commented 3 years ago

When it comes to alternative approaches, after some research I came up with the modifications below. They assume that you use capacitor v3.

Service Worker / PWA in native capacitor apps

A) Enable service workers in native brigdes

Android

Add this to MainActivity.java::onCreate (Sources: capacitor issue 1655, Stack Overflow answer):

 if(Build.VERSION.SDK_INT >= 24 ){
   ServiceWorkerController swController = ServiceWorkerController.getInstance();

   swController.setServiceWorkerClient(new ServiceWorkerClient() {
     @Override
     public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
       return bridge.getLocalServer().shouldInterceptRequest(request);
     }
   });
 }

iOS

Add a list of your domains incl. server.hostname / localhost to Info.plist, e.g.:

    <key>WKAppBoundDomains</key>
    <array>
        <string>192.168.0.3</string>
        <string>127.0.0.1</string>
        <string>localhost</string>
    </array>

Enable limitsNavigationsToAppBoundDomains in your src-capacitor/capacitor.config.json and set server.url:

{
    "appId": "your.fancy.app.id",
    "appName": "Your Fancy app",
    "bundledWebRuntime": false,
    "npmClient": "yarn",
    "webDir": "www",
    "ios": {
        "limitsNavigationsToAppBoundDomains": true
    },
    "server": {
        "url": "http://192.168.0.3"
    }
}

If you are stuck with capacitor v2, this might help:

Add this to node_modules/@capacitor/ios/Capacitor/Capacitor/CAPBridgeViewController.swift::configureWebView (Sources: capacitor issue 4122, flirt.dev on iOS 14 and service workers):

// 2021-10-07 GUIDE enable bound domains / service worker
if #available(iOS 14.0, *) {
   configuration.limitsNavigationsToAppBoundDomains = true
} else {
   // Fallback on earlier versions
}

B) Build and run (still somewhat shaky and probably not optimized, but it works for me)

yusufkandemir commented 3 years ago

@sluedecke I was also thinking about this for a while. The solution in my mind is to add some configuration to quasar.conf.js, similar to how SSR+PWA works. The default value will be false, when you set that option to true, or the workbox options object(e.g. quasar.conf.js > capacitor > pwa = true | { /* workboxOptions to be overridden */ } or quasar.conf.js > cordova > pwa = true | { /* workboxOptions to be overridden */ }) and then run quasar dev|build -m cordova|capacitor -T android|ios and it would also generate and include the service worker. I made some experimentation about the solution, but haven't finished or tested it yet. Please let me know what you think about the solution.

aggroot commented 2 years ago

@yusufkandemir This sounds awesome. It would be great to have a MOBILE + PWA combo and also from consistency pov, your proposal about how to enable it sounds the right way. +1

sluedecke commented 2 years ago

@yusufkandemir such a solution would be great! One might need to make the changes in the native bridge, too.

My alternative solution mentioned in the initial post (prefetching) does work properly on Android! BUT: if the installation of the service worker fails in any way (in my case the __WB_MANIFEST contained an entry for .gitignore which is an error), the service worker will silently be ignored.

sluedecke commented 2 years ago

And I made it work in iOS, too!

The IMPORTANT difference is: data included in the native app (all the JavaScript logic, pages, etc.) is loaded by capacitor itself, everything else (e.g. resources from the backend server) is handed over to the fetch mechanism of the platforms webview implementation. BUT:

Details on capacitor.config.json: https://capacitorjs.com/docs/config

The solution for iOS is to configure a server url in the capacitor.config.json manually (it will be overridden by quasars build mechanism). Although this is discouraged, there are quite some reports that such apps are accepted in the App Store.

I have updated https://github.com/quasarframework/quasar/issues/10934#issuecomment-939244614 accordingly.

mfyang commented 2 years ago

@sluedecke May I ask where is this 'MainActivity.java::onCreate' located?

sluedecke commented 2 years ago

@sluedecke May I ask where is this 'MainActivity.java::onCreate' located?

On my installation it can be found here (app package name is net.currit.guide, yours will differ):

src-capacitor/android/app/src/main/java/net/currit/guide/MainActivity.java

sluedecke commented 7 months ago

For the records: so ... since I did not get it to work properly after spending another day, I have chosen this way:

Code in build.gradle:

android {
  buildTypes {
    release {
      buildConfigField "String", "WEB_ADDRESS", '"https://somewhere.in.the.inter.net"'
    }
  }
}

Code in MainActivity.java::onCreate:

WebView webView = getBridge().getWebView();
WebSettings settings = webView.getSettings();
settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); // load from cache, even if expired
webView.loadUrl(BuildConfig.WEB_ADDRESS);