capacitor
capacitor copied to clipboard
[Bug]: Capacitor on Android detected as web platform after redirecting with `allowNavigation`
Capacitor Version
💊 Capacitor Doctor 💊
Latest Dependencies:
@capacitor/cli: 6.0.0 @capacitor/core: 6.0.0 @capacitor/android: 6.0.0 @capacitor/ios: 6.0.0
Installed Dependencies:
@capacitor/cli: 6.0.0 @capacitor/core: 6.0.0 @capacitor/ios: 6.0.0 @capacitor/android: 6.0.0
[success] iOS looking great! 👌 [success] Android looking great! 👌
Other API Details
`npm --version`
> 10.7.0
`node --version`
> v20.11.0
Platforms Affected
- [ ] iOS
- [X] Android
- [ ] Web
Current Behavior
After redirecting to an external URL that is allowed in the server.allowNavigation
config setting, an android app reports being on a web platform instead of android.
This can be seen by calling Capacitor.getPlatform()
(which returns android
before redirecting and web
after redirecting) or by trying to use a native API, which will either fallback to the web version of the API, or fail to run at all
All this works on iOS, where after redirecting, the app can still use native iOS APIs
Expected Behavior
After redirecting, the android app should still have access to all the native Android APIs (and report itself as being an Android platform)
Project Reproduction
https://github.com/Dylancyclone/capacitor-android-redirect/tree/main
Additional Information
Reproduction details are in the repo, but the basic reproduction steps are as follows:
- Serve a valid capacitor app on your computer (i.e. with
npx serve .
) - Add your computer's IP to the
server.allowNavigation
capacitor config setting - Load the application on your (real or simulated) device
- Notice that
Capacitor.getPlatform()
returnsandroid
on android andios
on iOS - Redirect to the hosted server (any method works, can be
window.location.assign()
,window.location.href = ""
,window.location.replace()
, or redirecting through a form - Notice that
Capacitor.getPlatform()
now returnsweb
on android but stillios
on iOS
Possibly related to https://github.com/ionic-team/capacitor/issues/5455
Here are videos showing the issue. Even though these were recorded on a simulator, the results are identical to a physical device
https://github.com/ionic-team/capacitor/assets/19830705/076fc893-1732-4bab-9aea-b7a90b4ec142
https://github.com/ionic-team/capacitor/assets/19830705/4522dcef-bbdb-42e4-8085-7009f55176bf
Our use case is to have one app that connects to different backends on different subdomains, very similar to Slack. So far everything is working except the android app loses access to its native APIs
So it looks like there are two parts to this issue: redirecting to a local address, and capacitor not injecting the JS bridge into external pages. Again, both of these are only relevant to Android, the iOS side works perfectly
For the first issue, if we try to navigate to a computer on the local network (http, through ip address or [computername].local
) we would get the issue described above where the platform would be seen as web
and the plugins would use the web version or fail entirely. If we instead redirect to a remotely hosted server (https), we get a slightly different issue.
If we add our remote server to the server.allowNavigation
config and redirect to it using the steps above, the platform is reported as android
correctly, but all plugins are completely broken:
Investigating this issue, we found the root cause to be the JavaScript injection in Bridge.java. We notice that if WebViewFeature.DOCUMENT_START_SCRIPT
is supported, the javascript is only injected into the base URL (if it is NOT supported (i.e by commenting the if guard out), the injector always works). If the addDocumentStartJavaScript()
function were changed to inject on all allowedOriginRules
(the base URL and everything in the server.allowNavigation
setting) instead of just the base URL, all plugins work again.
Here's the diff:
Original: https://github.com/ionic-team/capacitor/blob/f8264ccae1f2fec553521bc62d767c4909ea6d79/android/capacitor/src/main/java/com/getcapacitor/Bridge.java#L254-L269
New:
// Start the local web server
JSInjector injector = getJSInjector();
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
WebViewCompat.addDocumentStartJavaScript(webView, injector.getScriptString(), allowedOriginRules);
injector = null;
}
localServer = new WebViewLocalServer(context, this, injector, authorities, html5mode);
I am new to capacitor and have no idea if this would have any side effects, but this seems to fix our issue completely and bring parity with the iOS version.
So to summarize:
The original issue still exists while redirecting to a computer on a local network (or just over http, I don't have the capability to test that right now)
There also appears to be a bug when redirecting to a remote server (again, maybe just over https) caused by the native bridge not injecting capacitor's Javascript on remote pages. A fix we found was to make the change above, but I'd rather ask if it is the correct approach before creating a pull request.
I'm experiencing this as well. I use the allowNavigation to switch between different stages of the app, and this prevents that workflow. Thank you for all of the digging you did, the detailed explanation, and workaround!
We are experiencing this issue as well, and I cannot thank you enough for having found this workaround 🙇 I can confirm that changing the addDocumentStartJavaScript
call to just use allowedOriginRules
fixes the issues for https requests on local networks. Using the fallback jsInjector
does not however.
I have hacked together a quick "patch" that we call in the MainActivity, to avoid editing the Bridge.Java file directly (as it gets messy with version control). Here it is in case anyone wants to use it (call it after onCreate
and before loading any other pages):
class MainActivity : BridgeActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/**
* ...
*/
patchJSInjection();
}
/**
* ...
*/
private fun patchJSInjection() {
try {
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
val getJsInjector = bridge::class.java.declaredMethods.single { it.name == "getJSInjector" };
getJsInjector.isAccessible = true;
val injector = getJsInjector.invoke(bridge);
val getScriptString = injector::class.java.declaredMethods.single { it.name === "getScriptString" };
val scriptString = getScriptString.invoke(injector) as String;
val allowedOrigins: MutableSet<String> = mutableSetOf();
// Add origins that the Capacitor JS Bridge should be injected into
allowedOrigins.add("https://www.foo.bar");
WebViewCompat.addDocumentStartJavaScript(bridge.webView, scriptString, allowedOrigins)
}
}catch (e: Exception) {
Log.e("Error", e.message ?: "");
}
}
}
I have hacked together a quick "patch" that we call in the MainActivity, to avoid editing the Bridge.Java file directly (as it gets messy with version control). Here it is in case anyone wants to use it (call it after
onCreate
and before loading any other pages):
@Vadinci huge thanks for that snippet. I hacked together similar in java:
private void patchJSInjection() {
try {
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
var getJsInjector = Arrays.stream(bridge.getClass().getDeclaredMethods())
.filter(method -> method.getName().equals("getJSInjector"))
.findFirst()
.get();
getJsInjector.setAccessible(true);
var injector = getJsInjector.invoke(bridge);
var getScriptString = Arrays.stream(injector.getClass().getDeclaredMethods())
.filter(method -> method.getName().equals("getScriptString"))
.findFirst()
.get();
var scriptString = (String) getScriptString.invoke(injector);
var allowedOrigins = Arrays.stream(bridge.getConfig().getAllowNavigation())
.filter(str -> str.contains("yourdomain.com") || str.contains("otherdomain.com"))
.filter(str -> str.contains("https://"))
// WebViewCompat likes things formatted particularly, trim trailing /*
.map(str -> str.replaceAll("/\\*$", ""))
.collect(Collectors.toSet());
Logger.info("patchJSInjection", "Injecting custom rules " + allowedOrigins);
WebViewCompat.addDocumentStartJavaScript(bridge.getWebView(), scriptString, allowedOrigins);
}
} catch (Exception e) {
Logger.error( e.getMessage(), e);
}
}
I got same issue too.
To resolve this issue, I apply a patch to Bridge.java
after it has been built
const xfs = require('fs/promises')
const { glob } = require('glob')
module.exports = async () => {
const originPatten = `\
// Start the local web server
JSInjector injector = getJSInjector();
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
String allowedOrigin = appUrl;
Uri appUri = Uri.parse(appUrl);
if (appUri.getPath() != null) {
if (appUri.getPath().equals("/")) {
allowedOrigin = appUrl.substring(0, appUrl.length() - 1);
} else {
allowedOrigin = appUri.toString().replace(appUri.getPath(), "");
}
}
WebViewCompat.addDocumentStartJavaScript(webView, injector.getScriptString(), Collections.singleton(allowedOrigin));
injector = null;
}
localServer = new WebViewLocalServer(context, this, injector, authorities, html5mode);`.split('\n')
const replacePatten = `\
// Patched local web server
JSInjector injector = getJSInjector();
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
WebViewCompat.addDocumentStartJavaScript(webView, injector.getScriptString(), allowedOriginRules);
injector = null;
}
localServer = new WebViewLocalServer(context, this, injector, authorities, html5mode);`.split('\n')
let modified = false
for (const path of await glob('node_modules/.pnpm/**/Bridge.java')) {
const source = await xfs.readFile(path, 'utf8')
const lines = source.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (line.includes('// Start the local web server')) {
if (lines.slice(i, i + originPatten.length).join('\n') !== originPatten.join('\n')) {
console.log('Skipped: Pattern not matched')
continue
}
console.log('Modified: Pattern matched')
lines.splice(i, originPatten.length, ...replacePatten)
modified = true
break
}
}
if (modified) {
await xfs.writeFile(path, lines.join('\n'), 'utf8')
break
}
}
}
In our case the issue was, that the script did not get injected on allowNavigation pages because it is not provided to the addDocumentStartJavaScript function:
i also recommend using https://www.npmjs.com/package/patch-package for all these patches until it is being resolved.
This issue has been labeled as type: bug
. This label is added to issues that that have been reproduced and are being tracked in our internal issue tracker.