Closed dylanvdmerwe closed 10 months 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:
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
Silly question, but for a workaround, it is possible to inject capacitor script manually?
The issue, anyone have a workaround?
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?
When the Android app is first installed you can see Capacitor is working:
The site files are downloaded and served directly on first run:
When the app is launched the second time, Capacitor is not injected:
The files are now served from the service worker:
I have yet to do some additional testing, but I hope this issue is not the same on iOS.
@jcesarmobile is there anything I can do to assist with this?
@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 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?
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... 😂
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.
@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 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? 😄
Nope, It's not working anymore.
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 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
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.
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.
The first run (when not served by the service worker):
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 @webuniverseio @goforu @xEsk @chrisharris77
Any updates about this issue ?
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 !
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.
@jcesarmobile
Do you need a budget to priorise this issue ?
ffs
Still nobody found a solution here ?
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.
Thanks a lot @yoyo930021
I'm ready to pay someone as freelance to develop the supported module / fix for capacitor. Who wants here ?
@jcesarmobile
Any info about updates on this issue ? Can we sponsor the fix ?
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
toServiceWorkerControllerCompat
for my case.
Works also on iOS ?
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
toServiceWorkerControllerCompat
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.
I can't find any other value to decide on HTML resources when service worker.
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?
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
Works perfectly locally, but, I'm trying the following code:
Here it is in the web app:
Here it is in the emulator:
No idea what is happening here. It makes no sense and I've been trying to tweak configs for 8 hours at this point.
I find a solution in Android.
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)
}
})
}
}
}
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.
@yoyo930021 thanks a lot for your post, not solution yet on iOS ?
@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.
@yoyo930021 normally service workers are working on Ca[acitor wkwebview, which problems are you facing on this subject ?
@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?
@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
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?
@jcesarmobile do you have any news about this ?
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
im using capacitor v4 and still got the issue, non of the solutions worked for me :(
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
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.
@yoyo930021 I'll play around with the precaching stuff. I'm using workbox (using next-pwa)
@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.
@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
@mlynch On Android and iOS through a service worker?
@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
@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 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
Bug Report
Capacitor Version
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.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
app-load-service.ts