ionic-team / capacitor

Build cross-platform Native Progressive Web Apps for iOS, Android, and the Web ⚡️
https://capacitorjs.com
MIT License
12.22k stars 1.01k forks source link

bug: service worker server.url not injecting capacitor #5278

Closed dylanvdmerwe closed 10 months ago

dylanvdmerwe commented 2 years ago

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 commented 2 years ago

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

webuniverseio commented 2 years ago

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

goforu commented 2 years ago

The issue, anyone have a workaround?

dylanvdmerwe commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

dylanvdmerwe commented 2 years ago

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

jgoux commented 2 years ago

@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?

dylanvdmerwe commented 2 years ago

@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?

xEsk commented 2 years ago

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... 😂

dylanvdmerwe commented 2 years ago

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 commented 2 years ago

@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.

jgoux commented 2 years ago

@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? 😄

goforu commented 2 years ago

Nope, It's not working anymore.

chrisharris77 commented 2 years ago

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;
                        }
                    }
                }
dylanvdmerwe commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

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

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.

Aarbel commented 2 years ago

@dylanvdmerwe @webuniverseio @goforu @xEsk @chrisharris77

Any updates about this issue ?

Aarbel commented 2 years ago
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 !

dharmeshds commented 2 years ago

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.

Aarbel commented 2 years ago

@jcesarmobile

Do you need a budget to priorise this issue ?

BraulioMonroy commented 2 years ago

ffs image

Aarbel commented 2 years ago

Still nobody found a solution here ?

yoyo930021 commented 2 years ago

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.

Aarbel commented 2 years ago

Thanks a lot @yoyo930021

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

Aarbel commented 2 years ago

@jcesarmobile

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

Aarbel commented 2 years ago

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 ?

yoyo930021 commented 2 years ago

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.

petermakeswebsites commented 2 years ago

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?

teacoat commented 2 years ago

Something similar is happening to me, I've deployed my angular app to my server and I am getting some very weird behavior.

Error in emulator when loading from server.url, doesnt happen with local image

Works perfectly locally, but, I'm trying the following code: image

Here it is in the web app: image

Here it is in the emulator: image

No idea what is happening here. It makes no sense and I've been trying to tweak configs for 8 hours at this point.

yoyo930021 commented 2 years ago

I find a solution in Android.

  1. Add a plugin in android project:

    @NativePlugin
    class SetupPlugin: Plugin() {
    override fun load() {
        super.load()
        // Fix Capacitor native bridge failed when reload page with service worker
        // https://github.com/ionic-team/capacitor/issues/5278
        if (WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BASIC_USAGE)) {
            val swController = ServiceWorkerControllerCompat.getInstance()
    
            swController.setServiceWorkerClient(object : ServiceWorkerClientCompat() {
                override fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
                    return this@SetupPlugin.bridge.localServer.shouldInterceptRequest(request)
                }
            })
        }
    }
    }

    PS. ref: https://stackoverflow.com/questions/55894716/how-to-package-a-hosted-web-app-with-ionic-capacitor/55916297#55916297

  2. Add code in your service worker:

    
    const getRequestInit = async (request: Request) => ({
    method: request.method,
    headers: request.headers,
    body: ['GET', 'HEAD'].includes(request.method) ? undefined : await request.blob(),
    referrer: request.referrer,
    referrerPolicy: request.referrerPolicy,
    mode: request.mode,
    credentials: request.credentials,
    cache: request.cache,
    redirect: request.redirect,
    integrity: request.integrity
    })

const addAcceptHeaderWhenNavigate = async (req: Request) => { const headers = new Headers(req.headers) headers.set('accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8') return new Request(req.url, { ...await getRequestInit(req), headers }) }

const HTMLStrategy = (): Strategy => { class NetworkReplaceHost extends NetworkOnly { async _handle (request: Request, handler: StrategyHandler) { return super._handle( await addAcceptHeaderWhenNavigate(request), handler ) } }

return new NetworkReplaceHost() }

registerRoute(({ request }) => request.mode === 'navigate', HTMLStrategy())



It can solve [this problem](https://github.com/ionic-team/capacitor/issues/5278#issuecomment-1165442865).
PS. I use `workbox` in the service worker.
Aarbel commented 2 years ago

@yoyo930021 thanks a lot for your post, not solution yet on iOS ?

yoyo930021 commented 2 years ago

@yoyo930021 thanks a lot for your post, not solution yet on iOS ?

I can't enable service workers in my iOS app WKWebview, so I don't research it.

Aarbel commented 2 years ago

@yoyo930021 normally service workers are working on Ca[acitor wkwebview, which problems are you facing on this subject ?

dylanvdmerwe commented 2 years ago

@yoyo930021 normally service workers are working on Ca[acitor wkwebview, which problems are you facing on this subject ?

This is not quite right as there is a bit of setup you need to do in Xcode to get service workers to work properly on iOS. I was not able to get service workers working properly on iOS with Capacitor either. Does anyone have a guide for this?

yoyo930021 commented 2 years ago

@Aarbel @dylanvdmerwe You must to use app bound domains. https://webkit.org/blog/10882/app-bound-domains/

It doesn't fit my needs, so I won't research it for now

SkaveRat commented 2 years ago

We're also running into this problem. The workaround with the Plugin doesn't seem to work for us. Not sure why, and I can't see a way to check what goes wrong.

I had to convert the kotlin code above to java, so maybe something went wrong there? although the code does get called, when I hook the android debugger to it.

The Plugin java code:

@CapacitorPlugin
public class SetupPlugin extends Plugin {

    @Override
    public void load() {
        super.load();

        // Fix Capacitor native bridge failed when reload page with service worker
        // https://github.com/ionic-team/capacitor/issues/5278

        if (WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BASIC_USAGE)) {
            ServiceWorkerControllerCompat swController = ServiceWorkerControllerCompat.getInstance();

            SetupPlugin that = this;

            swController.setServiceWorkerClient(new ServiceWorkerClientCompat() {
                @Nullable
                @Override
                public WebResourceResponse shouldInterceptRequest(@NonNull WebResourceRequest request) {
                    return that.bridge.getLocalServer().shouldInterceptRequest(request);
                    // This line is called, but returns null
                }
            });
        }
    }
}

The return returns null on a request to [domain]/sw.js and I feel this is wrong?

Aarbel commented 2 years ago

@jcesarmobile do you have any news about this ?

leo-buneev commented 2 years ago

Full working workaround with latest Capacitor and latest Android - compiled from ideas of @yoyo930021

public class MainActivity extends BridgeActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

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

      swController.setServiceWorkerClient(new ServiceWorkerClient() {
        @Override
        public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
          if(request.getUrl().toString().contains("index.html")) {
            // Hack to help capacitor's "shouldInterceptRequest"
            request.getRequestHeaders().put("Accept", "text/html");
          }
          var result =  bridge.getLocalServer().shouldInterceptRequest(request);
          return result;
        }
      });
    }
  }
}

Note that depending on your SW setup root file may not be called "index.html". In that case feel free to adjust if(request.getUrl().toString().contains("index.html")) condition above according to your app's setup

m3di commented 1 year ago

im using capacitor v4 and still got the issue, non of the solutions worked for me :(

mlynch commented 1 year ago

On Android, I am testing this right now in Capacitor v4, Chrome WebView v101, Android 13 emulator (API 33), and this is working just fine for me:

public class MainActivity extends BridgeActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ServiceWorkerController swController = ServiceWorkerController.getInstance();

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

With this I am able to load a remote URL (not localhost), and have it access plugins which are correctly defined and routed to native plugin code.

Am I missing something for why this approach isn't working for others? cc @dylanvdmerwe

yoyo930021 commented 1 year ago

On Android, I am testing this right now in Capacitor v4, Chrome WebView v101, Android 13 emulator (API 33), and this is working just fine for me:

public class MainActivity extends BridgeActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ServiceWorkerController swController = ServiceWorkerController.getInstance();

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

With this I am able to load a remote URL (not localhost), and have it access plugins which are correctly defined and routed to native plugin code.

Am I missing something for why this approach isn't working for others? cc @dylanvdmerwe

It will happen when precache in service worker. In most case, we use workbox for service worker. https://web.dev/precache-with-workbox/

When precache, the request doesn't have accept header with html file.

mlynch commented 1 year ago

@yoyo930021 I'll play around with the precaching stuff. I'm using workbox (using next-pwa)

yoyo930021 commented 1 year ago

@yoyo930021 I'll play around with the precaching stuff. I'm using workbox (using next-pwa)

If you try to reload page after open? In my test, It's no service worker cache when open app. But if you waiting 1-3 minute and window.location.reload, service worker cache will work and capacitor not native.

mlynch commented 1 year ago

@yoyo930021 with that snippet it works always. App launch, reloading the app, later app launches, etc.

Want to try my sample? https://github.com/mlynch/capacitor-remote-offline-example

dylanvdmerwe commented 1 year ago

@mlynch On Android and iOS through a service worker?

mlynch commented 1 year ago

@dylanvdmerwe only looking at Android right now to isolate that. Yes, through a service worker. The sample above uses next-pwa which uses workbox and I can confirm the files are being served through it

mlynch commented 1 year ago

@dylanvdmerwe I just tested iOS using app bound domains, can confirm assets are loaded from service worker, and native plugin access works just fine.

I'm not an expert with SW's though, only using the defaults in next-pwa: https://github.com/shadowwalker/next-pwa

But all appears to be working

yoyo930021 commented 1 year ago

@yoyo930021 with that snippet it works always. App launch, reloading the app, later app launches, etc.

Want to try my sample? https://github.com/mlynch/capacitor-remote-offline-example

The reason is next-pwa. Next is a SSR framework, so it is not cache index.html by default. See https://github.com/shadowwalker/next-pwa#available-options cacheStartUrl dynamicStartUrl option and https://github.com/shadowwalker/next-pwa/blob/master/index.js#L168 截圖 2023-01-11 下午3 32 10