Skip to content
This repository has been archived by the owner on Apr 3, 2024. It is now read-only.

Commit

Permalink
Improve opening external browser
Browse files Browse the repository at this point in the history
This gets rid of the previous workarounds by using the beforeload event
introduced in apache/cordova-plugin-inappbrowser#276
  • Loading branch information
wvengen committed Jun 13, 2019
1 parent 93306c9 commit 0e3048f
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 142 deletions.
12 changes: 0 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,6 @@ links on your website would be opened in the system web browser.
This attribute can also be set by the website by any element with a `data-app-local-urls`
attribute (so the website can evolve without having to update this setting in the app).

It may be useful to know that there are two ways in which this is implemented. The first
one applies to `a` elements only. After a page has loaded, an event listener is installed
which disables navigation for external links and invokes the system web browser. The second
method kicks in when a script navigates to an external link (e.g. an embedded Google Map),
but this only happens _after_ the internal web browser has started loading the URL. This
request is then cancelled.

Please note that this last method triggered various subtle issues on iOS (and to a lesser
extent on Android), which have been worked around where possible, but it remains tricky.
So: be careful when opening pages from Javascript (work is
[on the way](https://issues.apache.org/jira/browse/CB-14188) to improve this).

## Barcode scanner

The website can initiate a barcode scan by pointing to the custom url `app://mobile-scan`.
Expand Down
4 changes: 2 additions & 2 deletions config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
</edit-config>
</platform>
<preference name="AppendUserAgent" value=" CordovaApp" />
<preference name="AllowedSchemes" value="app" />
<plugin name="cordova-plugin-whitelist" spec="^1.3.3" />
<plugin name="cordova-plugin-inappbrowser" spec="git+https://github.com/q-m/cordova-plugin-inappbrowser.git#0ca4dd7" />
<!-- use npm package when InAppBrowser releases a version _after_ 3.0.0 -->
<plugin name="cordova-plugin-inappbrowser" spec="git+https://github.com/apache/cordova-plugin-inappbrowser.git#94fec84d5c81e64b89b4c216d02938d58ba61dbc" />
<plugin name="cordova-plugin-network-information" spec="^2.0.1" />
<plugin name="phonegap-plugin-barcodescanner" spec="^7.1.2">
<variable name="ANDROID_SUPPORT_V4_VERSION" value="27.+" />
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"author": "wvengen",
"dependencies": {
"cordova-plugin-app-launcher": "^0.4.0",
"cordova-plugin-inappbrowser": "git+https://github.com/q-m/cordova-plugin-inappbrowser.git#0ca4dd7",
"cordova-plugin-inappbrowser": "git+https://github.com/apache/cordova-plugin-inappbrowser.git#94fec84d5c81e64b89b4c216d02938d58ba61dbc",
"cordova-plugin-network-information": "^2.0.1",
"cordova-plugin-whitelist": "^1.3.3",
"phonegap-plugin-barcodescanner": "^7.1.2"
Expand Down
163 changes: 36 additions & 127 deletions www/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ var Fsm = machina.Fsm.extend({
initialState: "starting",

appLastUrl: null,
loadedStopUrl: null, // for iOS external browser hack

states: {

Expand All @@ -56,12 +55,12 @@ var Fsm = machina.Fsm.extend({

// Load the page we want to show.
"loading": {
_onEnter : function(e) { this.onLoading(e); },
"app.loadstart" : function(e) { this.onNavigate(e); },
"app.loadstop" : "loaded",
"app.loaderror" : "failed",
"pause" : "paused",
"conn.offline" : "offline.blank",
_onEnter : function(e) { this.onLoading(e); },
"app.beforeload" : function(e, cb) { this.onNavigate(e, cb); },
"app.loadstop" : "loaded",
"app.loaderror" : "failed",
"pause" : "paused",
"conn.offline" : "offline.blank",
},

// Paused during page load.
Expand All @@ -75,12 +74,10 @@ var Fsm = machina.Fsm.extend({

// Page was succesfully loaded in the inAppBrowser.
"loaded": {
_onEnter : function() { this.onLoaded(); },
"app.loadstart" : function(e) { this.onNavigate(e); },
"app.loadstop" : function(e) { this.loadedStopUrl = e.url; }, // for iOS external browser open hack
"app.loaderror" : function(e) { this.loadedStopUrl = e.url; }, // for iOS external browser open hack
"app.exit" : function() { this.onBrowserBack(); }, // top of navigation and back pressed
"conn.offline" : "offline.loaded",
_onEnter : function() { this.onLoaded(); },
"app.beforeload" : function(e, cb) { this.onNavigate(e, cb); },
"app.exit" : function() { this.onBrowserBack(); }, // top of navigation and back pressed
"conn.offline" : "offline.loaded",
},

// Page load failed.
Expand All @@ -100,32 +97,7 @@ var Fsm = machina.Fsm.extend({
"offline.loaded": {
_onEnter : function() { this.onOfflineLoaded(); },
"conn.online" : "loaded",
},

// iOS-specific state while finishing load of page before opening external browser.
// If we don't wait but move on, after coming back and re-loading the page, the
// app often crashes (with `EXC_BAD_ACCESS (code=1, address=0x...)`).
"loading.ext": {
_onEnter : function(url) { this.extUrl = url; this.showMessage("loading"); },
"app.loadstop" : function(e) { if (e.url === this.extUrl) this.transition("paused.ext", this.extUrl); },
"app.loaderror" : function(e) { if (e.url === this.extUrl) this.transition("paused.ext", this.extUrl); },
"conn.offline" : "offline.blank",
},

// iOS-specific state during showing external browser.
"paused.ext": {
_onEnter : function(url) { this.openSystemBrowser(url); },
"resume" : function() { this.onResume(); },
"conn.offline" : "offline.blank",
},

// iOS-specific workaround for loading a URL with hash twice
"loading.hashfix": {
_onEnter : function(url) { this.hashfixUrl = url; this.showMessage("loading"); this._load("about:blank?gap-iab=hashfix"); },
"app.loadstop" : function(e) { this.load(this.hashfixUrl); },
"app.loaderror" : function(e) { this.load(this.hashfixUrl); },
"conn.offline" : "offline.blank",
},
}
},

initialize: function() {
Expand Down Expand Up @@ -165,11 +137,7 @@ var Fsm = machina.Fsm.extend({
load: function(url, messageCode) {
var _url = url || this.appLastUrl || LANDING_URL;

this.loadedStopUrl = null;
if (_url !== "about:blank?gap-iab=hashfix") this.appLastUrl = _url;

// if no code is given, it means: keep the same message as before (relevant for e.g. redirects)
if (messageCode) this.showMessage(messageCode);
this.appLastUrl = _url;

this._load(_url);
this.transition("loading", messageCode);
Expand All @@ -190,15 +158,9 @@ var Fsm = machina.Fsm.extend({
onLoading: function(messageCode) {
// if no code is given, it means: keep the same message as before (relevant for e.g. redirects)
if (messageCode) this.showMessage(messageCode);

// For iOS, this is enough; on Android we do this onLoaded too.
this.installClickListener();
},

onLoaded: function(e) {
this.loadedStopUrl = null;
// On Android loadstart is sometimes too early, do it here too.
if (window.cordova.platformId === 'android') this.installClickListener();
// Allow LOCAL_URLS to be set by the page.
this.app.executeScript({ code:
'(function () {\n' +
Expand All @@ -213,52 +175,19 @@ var Fsm = machina.Fsm.extend({
this.app.show();
},

onNavigate: function(e) {
if (this.isLocalUrl(e.url)) {
// Internal link followed.
debug("opening internal link: " + e.url);
this.appLastUrl = e.url;
this.transition("loading", null);
} else {
// External link opened. Should be unreachable code because of the onLoaded() code injection,
// but might happen if javascript opens a link (e.g. embedded Google Map).
debug("opening external link (not caught on page): " + e.url);
// Cancel navigation of inAppBrowser. This is a bit of a hack, so the event listener
// installed in onLoaded is preferable (which also avoids the initial request).
this.onNavigateExt(e.url);
}
},

onNavigateExt: _.debounce(function(url) {
// First try to get currently loaded page, so we can reload it when coming back.
// (debounce is needed on iOS, where onNavigate is called several times for one click)
this.app.executeScript({ code: 'window.location.toString()' }, wrapEventListener(function(a) {
this.showMessage("loading");
debug("got as current url: " + a[0]);
// Sometimes we get the previous URL, sometimes the current.
this.appLastUrl = this.isLocalUrl(a[0]) ? a[0] : LANDING_URL;
debug("on return will go back to: " + this.appLastUrl);
if (window.cordova.platformId === 'ios') {
// On iOS we need to take care to not mess up the WebView, use custom states for that.
// There may have been a loadstop event in the meantime. Detected by loadedStopUrl.
this.transition(this.loadedStopUrl === null ? "loading.ext" : "paused.ext", url);
} else {
// On Android we can just load the previous URL directly.
this.openSystemBrowser(url);
this.load(this.appLastUrl, "loading");
}
}.bind(this)));
}, 500, { rising: true, falling: false }),

onCustomScheme: function(e) {
debug("custom scheme: " + e.url);
onNavigate: function(e, cb) {
if (e.url.match(/^app:\/\/mobile-scan\b/)) {
// barcode scanner opened
var params = parseQueryString(e.url) || {};
this.openScan(params.ret, !!params.redirect);
} else if (e.url.match(/^app:\/\/open\b/)) {
var url = parseQueryString(e.url).url;
debug("opening external link:" + url);
this.openSystemBrowser(url);
} else if (this.isLocalUrl(e.url)) {
// don't interfere with local urls
debug("internal link: " + e.url);
this.appLastUrl = e.url;
cb(e.url);
} else {
// all other links are opened in the system web browser
this.openSystemBrowser(e.url);
}
},

Expand All @@ -285,30 +214,35 @@ var Fsm = machina.Fsm.extend({

openBrowser: function(url) {
var _url = url || this.appLastUrl || LANDING_URL;
this.app = cordova.InAppBrowser.open(_url, "_blank", "location=no,zoom=no,shouldPauseOnSuspend=yes,toolbar=no,hidden=yes");
this.app = cordova.InAppBrowser.open(_url, "_blank", "location=no,zoom=no,shouldPauseOnSuspend=yes,toolbar=no,hidden=yes,beforeload=yes");
// Connect state-machine to inAppBrowser events.
this.app.addEventListener("loadstart", wrapEventListener(this.handle.bind(this, "app.loadstart")), false);
this.app.addEventListener("loadstop", wrapEventListener(this.handle.bind(this, "app.loadstop")), false);
this.app.addEventListener("loaderror", wrapEventListener(function(e) {
if (window.cordova.platformId === 'ios' && e.code === -999) {
debug("ignoring cancelled load on iOS: " + e.url + ": " + e.message);
} else if (window.cordova.platformId === 'ios' && e.url.includes("#") &&
e.message === "CDVWebViewDelegate: Navigation started when state=1") {
debug("activating iOS hash navigation workaround for " + e.url + ": " + e.message);
this.transition("loading.hashfix", e.url);
} else {
debug("page load failed for " + e.url + ": " + e.message);
this.handle("app.loaderror", e);
}
}.bind(this)), false);
this.app.addEventListener("beforeload", wrapEventListener(this.handle.bind(this, "app.beforeload")), false);
this.app.addEventListener("exit", wrapEventListener(this.handle.bind(this, "app.exit")), false);
this.app.addEventListener("customscheme", wrapEventListener(this.onCustomScheme.bind(this)), false);
},

openSystemBrowser: function(url) {
// Do not use InAppBrowser because it messes up opened inAppBrowser state.
window.plugins.launcher.launch({uri: url}, function(data){
debug("successfully opened external link");
var launcher = window.plugins.launcher;
// Need FLAG_ACTIVITY_NEW_TASK on Android 6 to make it clear that the page is
// opened in another app. Also, the back button doesn't bring you back from
// the system web browser to this app on Android 6, with this flag it does.
launcher.launch({uri: url, flags: launcher.FLAG_ACTIVITY_NEW_TASK}, function(data) {
if (data.isLaunched) {
debug("successfully opened external link: " + url);
} else if (data.isActivityDone) {
debug("returned from opening external link: " + url);
} else {
debug("unknown response when opening external link: " + JSON.stringify(data));
}
}, function(errMsg) {
debug("could not open external link: " + errMsg);
});
Expand Down Expand Up @@ -390,31 +324,6 @@ var Fsm = machina.Fsm.extend({
var base = parts[1], path = parts[2];
return (base + path).match(this.localUrlRe) || (base === BASE_URL && path.match(this.localUrlRe));
}
},

installClickListener: function() {
// Catch links that were clicked to route external ones through our custom protocol.
// We'd rather not do this in the loadstart event, because the page then already started loading.
this.app.executeScript({ code:
'window.addEventListener("click", function(e) {\n' +
' if (e.target.tagName !== "A") return;\n' +
' var href = e.target.href;\n' +
' if (!href || href.startsWith("app:")) return;\n' +
' var BASE_URL = ' + JSON.stringify(BASE_URL) + ';\n' +
' var SPLIT_URL_RE = ' + SPLIT_URL_RE.toString() + ';\n' +
' var localUrlRe = ' + this.localUrlRe.toString() + ';\n' +
' var parts = href.match(SPLIT_URL_RE);\n' +
' var base = parts[1], path = parts[2];\n' +
' if (!(base + path).match(localUrlRe) && !(base === BASE_URL && path.match(localUrlRe))) {\n' +
' e.preventDefault();\n' +
' window.location.assign("app://open?url=" + encodeURIComponent(href));\n' +
' }\n' +
'});\n' +
'console.log("installed click event listener for external links");\n'
}, function() {
// Also log in app console.
debug("installed click event listener for external links");
});
}
});
var fsm = new Fsm();

0 comments on commit 0e3048f

Please sign in to comment.