capacitor icon indicating copy to clipboard operation
capacitor copied to clipboard

bug: service worker server.url not injecting capacitor

Open dylanvdmerwe opened this issue 3 years ago • 37 comments

Bug Report

Capacitor Version

Latest Dependencies:

  @capacitor/cli: 3.3.2
  @capacitor/core: 3.3.2
  @capacitor/android: 3.3.2
  @capacitor/ios: 3.3.2

Installed Dependencies:

  @capacitor/ios: not installed
  @capacitor/cli: 3.3.2
  @capacitor/android: 3.3.2
  @capacitor/core: 3.3.2

Platform(s)

Current Behavior

Calling any native plugins when an app is served from an Angular service worker causes the plugins to fail. Using server.url to load the site on Android. image

Platform ready does not help for this.

Expected Behavior

Capacitor needs to be injected when a site is served through a service worker.

Code Reproduction

app.module.ts

export function init_app(appLoadService: AppLoadService) {
  return () => appLoadService.initApp();
}

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule,
    ServiceWorkerModule.register('ngsw-worker.js', {
      enabled: environment.production,
      // Register the ServiceWorker as soon as the app is stable
      // or after 30 seconds (whichever comes first).
      registrationStrategy: 'registerWhenStable:30000',
    }),
  ],
  providers: [
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    {
      provide: APP_INITIALIZER,
      useFactory: init_app,
      deps: [AppLoadService],
      multi: true,
    },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

app-load-service.ts

@Injectable({
  providedIn: 'root',
})
export class AppLoadService {
  constructor(
    private device: DeviceInfo,
    private platform: Platform
  ) {}

  async initApp(): Promise<any> {
    await this.platform.ready();

    SplashScreen.hide(); // breaks
  }
}

dylanvdmerwe avatar Nov 26 '21 14:11 dylanvdmerwe

It actually seems that the capacitor script is not being injected after the site is served by a service worker.

I have included this on Android but still getting plugin errors: image

I don't believe that this works any more.

The site is picking up that its running on Android through a service worker: console.log('Platforms: ' + Capacitor.getPlatform()); console.log('Is Native: ' + Capacitor.isNativePlatform()); Platforms: android Is Native: true

image image

dylanvdmerwe avatar Nov 26 '21 15:11 dylanvdmerwe

Silly question, but for a workaround, it is possible to inject capacitor script manually?

webuniverseio avatar Nov 28 '21 02:11 webuniverseio

The issue, anyone have a workaround?

goforu avatar Nov 28 '21 08:11 goforu

Please see a repo here: https://github.com/happendev/TestCapacitor

The PWA site is hosted on https://testapp.happen.zone.

Android:

  1. When site loaded through the Android app without a service worker, native plugins work, capacitor works.
  2. When site is loaded through a service worker after the site has been cached, native plugins and capacitor do not work.

Maybe I have done something wrong?

dylanvdmerwe avatar Dec 02 '21 11:12 dylanvdmerwe

When the Android app is first installed you can see Capacitor is working: image

The site files are downloaded and served directly on first run: image

When the app is launched the second time, Capacitor is not injected: image

The files are now served from the service worker: image

image

dylanvdmerwe avatar Dec 02 '21 12:12 dylanvdmerwe

I have yet to do some additional testing, but I hope this issue is not the same on iOS.

dylanvdmerwe avatar Dec 02 '21 13:12 dylanvdmerwe

@jcesarmobile is there anything I can do to assist with this?

dylanvdmerwe avatar Dec 20 '21 08:12 dylanvdmerwe

@dylanvdmerwe I started looking into what should be done in order to support this use case (I'll need it as well).

This is what we talked about with @thomasvidas on Discord :

me : I think we need to introduce the ServiceWorkerClient in order to intercept requests from the service worker : https://stackoverflow.com/a/59606081 and https://stackoverflow.com/questions/62354423/how-to-use-serviceworkercontroller-on-android/63491172#63491172 The shouldInterceptRequest is the same as the WebviewClient's one so it should be a matter of calling them both

@thomasvidas : That seems pretty doable. I'd assume that WebViewLocalServer.shouldInterceptRequest() could be modified to check if it should serve from the service worker or from the normal local capacitor server. Maybe it would be better to change bridge.getLocalServer()? But I'm more wary of that since this doesn't work on API 21-23 and I try to avoid touching the bridge if I can since it touches so much of the codebase.

@dylanvdmerwe Did you test on iOS a well?

jgoux avatar Dec 20 '21 09:12 jgoux

@jgoux I have tried to get a service worker working so I can test this on iOS - but I did not come right with whatever needs to be enabled for this. Does anyone know what I missed in the repo? I would love to check out how the service worker is affected on iOS as well as part of this. Or is there another repo where this works that I can use for testing?

dylanvdmerwe avatar Dec 20 '21 12:12 dylanvdmerwe

I'm facing this "problem" too....

As a temporary "workaround" (in order to solve this), I force a site reload when the ServiceWorker is Ready. I know and I also don't like the solution, but I needed to implement this 💩 while this "issue/bug/feature" is addressed.

if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android') {
  const reboot = localStorage.getItem('reboot')
  if (reboot !== '1') {
    localStorage.setItem('reboot', '1')
    setTimeout(() => location.reload(), 1)
  } else {
    localStorage.setItem('reboot', '0')
  }
}

This code is working for me, at the time as I write this... (is working on production).

Forgive me... 😂

xEsk avatar Jan 09 '22 15:01 xEsk

Using this kind of functionality is primarily not for the app stores in our case, we really want to use this for apps that are rolled out (enterprise) to operators and employees at our clients. This would drastically reduce the building required to deploy updates to them, as all we would need to do is update the PWA.

Please advise if there is anything that I can do to assist or test to get server.url working for Android and iOS through a service worker on Capacitor.

dylanvdmerwe avatar Jan 16 '22 20:01 dylanvdmerwe

@jgoux Do you have any more feedback on the way forward on this?

Has anyone tried anything further as we would think this kind of thing would be incredible to get working through Capacitor apps.

dylanvdmerwe avatar Feb 07 '22 14:02 dylanvdmerwe

@dylanvdmerwe Hello Dylan,

I didn't have time to investigate yet.

Did you already try this solution : https://stackoverflow.com/a/55916297/1728627 ?

I believe @mhjam made a contribution to make it works here : https://github.com/ionic-team/capacitor/pull/1465, maybe he knows more about it? 😄

jgoux avatar Feb 07 '22 15:02 jgoux

Nope, It's not working anymore.

goforu avatar Feb 09 '22 01:02 goforu

If the path your service worker is loading contains a ".html" file extension, the the following update in WebViewLocalServer.handleProxyRequest() resolves the issue (if you also hook up a service worker client delegating to WebViewLocalServer like in https://stackoverflow.com/questions/62354423/how-to-use-serviceworkercontroller-on-android/63491172#63491172). Note that this is exactly the same hack that is in WebViewLocalServer.handleLocalRequest(), so if it's good enough for local requests it should be good enough for proxy ones too. I've opened a pull request for it: https://github.com/ionic-team/capacitor/pull/5428

                if (url.contains(".html")) {
                    isHtmlText = true;
                } else {
                    for (Map.Entry<String, String> header : headers.entrySet()) {
                        if (header.getKey().equalsIgnoreCase("Accept") && header.getValue().toLowerCase().contains("text/html")) {
                            isHtmlText = true;
                            break;
                        }
                    }
                }

chrisharris77 avatar Feb 11 '22 19:02 chrisharris77

@chrisharris77 No this does not appear to work, at least not when using a normal ionic app as per my repo.

https://github.com/happendev/TestCapacitor

https://testapp.happen.zone/

  1. Install app for the first time. The app works. It loads the files from the site and appears to install a service worker for offline. Plugins work.
  2. Restart the app, the app loads the files from the service worker's offline cache. Capacitor does not seem to be initialized and as such plugins fail and the app breaks when they are used.

dylanvdmerwe avatar Feb 17 '22 09:02 dylanvdmerwe

if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android') { const reboot = localStorage.getItem('reboot') if (reboot !== '1') { localStorage.setItem('reboot', '1') setTimeout(() => location.reload(), 1) } else { localStorage.setItem('reboot', '0') } }

Hi @xEsk, can you provide some more context, or a repo for this? I have tried, but even with this Capacitor is not initialized which means plugins don't work when loaded from the SW offline version. Any use of the plugins breaks.

dylanvdmerwe avatar Feb 17 '22 10:02 dylanvdmerwe

The first run (when not served by the service worker):

  • Platform: Android
  • Native: true.

image

This is the service worker cached PWA version of the app running on Android device.

As you can see, it still detects that the platform is Android and isNativePlatform is true. But it has not initialized any android plugin stuff which is why the plugins are breaking.

Is there maybe a way to force Capacitor to run in 'web' mode here (as a true PWA), as then the plugins should work? Ideally it should setup itself correctly and run in proper Android mode.

dylanvdmerwe avatar Feb 17 '22 14:02 dylanvdmerwe

@dylanvdmerwe @webuniverseio @goforu @xEsk @chrisharris77

Any updates about this issue ?

Aarbel avatar Mar 22 '22 23:03 Aarbel

if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android') {
  const reboot = localStorage.getItem('reboot')
  if (reboot !== '1') {
    localStorage.setItem('reboot', '1')
    setTimeout(() => location.reload(), 1)
  } else {
    localStorage.setItem('reboot', '0')
  }
}

Is this part still working ? Do service workers also work with this fix ? (managing dynamic modules download)

Thanks a lot !

Aarbel avatar Mar 22 '22 23:03 Aarbel

Is there any permanent resolution to this issue to make service workers work with capacitor plugins on Android? It is very frustrating that my Ionic6/Angular13 app works nicely with the service worker on browser but fails to register the same on Android. When I implement the workaround suggested, service worker loads but capacitor plugins fail to load on the device. Please update when there is any resolution or workaround for this.

dharmeshds avatar Mar 27 '22 00:03 dharmeshds

@jcesarmobile

Do you need a budget to priorise this issue ?

Aarbel avatar Mar 28 '22 08:03 Aarbel

ffs image

BraulioMonroy avatar Mar 31 '22 17:03 BraulioMonroy

Still nobody found a solution here ?

Aarbel avatar Apr 12 '22 10:04 Aarbel

I using this answer and work for me on android with capacitor 2. https://stackoverflow.com/questions/55894716/how-to-package-a-hosted-web-app-with-ionic-capacitor/55916297#55916297

but I replace ServiceWorkerController to ServiceWorkerControllerCompat for my case.

yoyo930021 avatar May 06 '22 01:05 yoyo930021

Thanks a lot @yoyo930021

I'm ready to pay someone as freelance to develop the supported module / fix for capacitor. Who wants here ?

Aarbel avatar May 06 '22 14:05 Aarbel

@jcesarmobile

Any info about updates on this issue ? Can we sponsor the fix ?

Aarbel avatar Jun 13 '22 14:06 Aarbel

I using this answer and work for me on android with capacitor 2. https://stackoverflow.com/questions/55894716/how-to-package-a-hosted-web-app-with-ionic-capacitor/55916297#55916297

but I replace ServiceWorkerController to ServiceWorkerControllerCompat for my case.

Works also on iOS ?

Aarbel avatar Jun 20 '22 12:06 Aarbel

I using this answer and work for me on android with capacitor 2. https://stackoverflow.com/questions/55894716/how-to-package-a-hosted-web-app-with-ionic-capacitor/55916297#55916297

but I replace ServiceWorkerController to ServiceWorkerControllerCompat for my case.

It is broken when sometimes. =_= The capacitor will inject a javascript file into any HTML resources. But the capacitor uses the Accept header value to decide on HTML resources. This value is broken when using a service worker.

Code: https://github.com/ionic-team/capacitor/blob/main/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java#L336

I can't find any other value to decide on HTML resources when service worker.

yoyo930021 avatar Jun 24 '22 10:06 yoyo930021

I would very much appreciate an externally hosted server PWA setup to interface properly with Capacitor's plugins on iOS and Android. I wonder if anyone above has come across any solutions?

petermakeswebsites avatar Jul 10 '22 12:07 petermakeswebsites