We have a Progressive Web App that prompts the user with the "Add to home screen" banner.
Adding to the homescreen works great, but after the user launches the page from the Home Screen it will sometimes still prompt them to install the app again. I'm posting here because all the resources I have found don't talk about this issue or how to solve it.
TL;DR Launching the app from the home screen still asks them to install the app with the "Add to home screen" prompt.
As suggested by #Mr.Rebot, I developed a little piece of code to solve the problem.
This is the result code:
window.addEventListener("beforeinstallprompt", (ev) => {
if (isStandalone()) {
// PWA already installed.
event.preventDefault();
return false;
} else {
// PWA not installed.
}
});
function isStandalone() {
// Check if device supports service workers
if (!('serviceWorker' in window.navigator)) return false;
// Check for Android
if (window.matchMedia('(display-mode: standalone)').matches) return true;
// Check for iOS
if (window.navigator["standalone"] == true) return true;
return false;
}
Can a "pure" HTML5/Javascript (progressive) web application intercept the mobile device back button in order to avoid the App to exit?
This question is similar to this one but I want to know if it is possible to achieve such behavior without depending on PhoneGap/Ionic or Cordova.
While the android back button cannot be directly hooked into from within a progressive web app context, there exists a history api which we can use to achieve your desired result.
First up, when there's no browser history for the page that the user is on, pressing the back button immediately closes the app.
We can prevent this by adding a previous history state when the app is first opens:
window.addEventListener('load', function() {
window.history.pushState({}, '')
})
The documentation for this function can be found on mdn:
pushState() takes three parameters: a state object, a title (which is currently ignored), and (optionally) a URL[...] if it isn't specified, it's set to the document's current URL.
So now the user has to press the back button twice. One press brings us back to the original history state, the next press closes the app.
Part two is we hook into the window's popstate event which is fired whenever the browser navigates backwards or forwards in history via a user action (so not when we call history.pushState).
A popstate event is dispatched to the window each time the active history entry changes between two history entries for the same document.
So now we have:
window.addEventListener('load', function() {
window.history.pushState({}, '')
})
window.addEventListener('popstate', function() {
window.history.pushState({}, '')
})
When the page is loaded, we immediately create a new history entry, and each time the user pressed 'back' to go to the first entry, we add the new entry back again!
Of course this solution is only so simple for single-page apps with no routing. It will have to be adapted for applications that already use the history api to keep the current url in sync with where the user navigates.
To do this, we will add an identifier to the history's state object. This will allow us to take advantage of the following aspect of the popstate event:
If the activated history entry was created by a call to history.pushState(), [...] the popstate event's state property contains a copy of the history entry's state object.
So now during our popstate handler we can distinguish between the history entry we are using to prevent the back-button-closes-app behaviour versus history entries used for routing within the app, and only re-push our preventative history entry when it specifically has been popped:
window.addEventListener('load', function() {
window.history.pushState({ noBackExitsApp: true }, '')
})
window.addEventListener('popstate', function(event) {
if (event.state && event.state.noBackExitsApp) {
window.history.pushState({ noBackExitsApp: true }, '')
}
})
The final observed behaviour is that when the back button is pressed, we either go back in the history of our progressive web app's router, or we remain on the first page seen when the app was opened.
#alecdwm, that is pure genius!
Not only does it work on Android (in Chrome and the Samsung browser), it also works in desktop web browsers. I tested it on Chrome, Firefox and Edge on Windows, and it's likely the results would be the same on Mac. I didn't test IE because eew. Even if you're mostly designing for iOS devices that have no back button, it's still a good idea to ensure that Android (and Windows Mobile... awww... poor Windows Mobile) back buttons are handled so that the PWA feels much more like a native app.
Attaching an event listener to the load event didn't work for me, so I just cheated and added it to an existing window.onload init function I already had anyhow.
Keep in mind that it might frustrate users who would actually want to really Go Back to whatever web page they were looking at before navigating to your PWA while browsing it as a standard web page. In that case, you can add a counter and if the user hits back twice, you can actually allow the "normal" back event to happen (or allow the app to close).
Chrome on Android also (for some reason) added an extra empty history state, so it took one additional Back to actually go back. If anyone has any insight on that, I'd be curious to know the reason.
Here's my anti-frustration code:
var backPresses = 0;
var isAndroid = navigator.userAgent.toLowerCase().indexOf("android") > -1;
var maxBackPresses = 2;
function handleBackButton(init) {
if (init !== true)
backPresses++;
if ((!isAndroid && backPresses >= maxBackPresses) ||
(isAndroid && backPresses >= maxBackPresses - 1)) {
window.history.back();
else
window.history.pushState({}, '');
}
function setupWindowHistoryTricks() {
handleBackButton(true);
window.addEventListener('popstate', handleBackButton);
}
This approach has a couple of improvements over existing answers:
Allows the user to exit if they press back twice within 2 seconds: The best duration is debatable but the idea of allowing an override option is common in Android apps so it's often the correct approach.
Only enables this behaviour when in standalone (PWA) mode: This ensures the website keeps behaving as the user would expect when within an Android web browser and only applies this workaround when the user sees the website presented as a "real app".
function isStandalone () {
return !!navigator.standalone || window.matchMedia('(display-mode: standalone)').matches;
}
// Depends on bowser but wouldn't be hard to use a
// different approach to identifying that we're running on Android
function exitsOnBack () {
return isStandalone() && browserInfo.os.name === 'Android';
}
// Everything below has to run at page start, probably onLoad
if (exitsOnBack()) handleBackEvents();
function handleBackEvents() {
window.history.pushState({}, '');
window.addEventListener('popstate', () => {
//TODO: Optionally show a "Press back again to exit" tooltip
setTimeout(() => {
window.history.pushState({}, '');
//TODO: Optionally hide tooltip
}, 2000);
});
}
i did not want to use native javascript functions to handle this inside of a react app, so i scoured solutions
that used react-router or react-dom-router, but in the end, up against a deadline, native js is
what got it working. Added the following listeners inside inside componentDidMount() and setting the history
to an empty state
window.addEventListener('load', function() {
window.history.pushState({}, '')
})
window.addEventListener('popstate', function() {
window.history.pushState({}, '')
})
this worked fine on the browser, but was still not working in the PWA on mobile
finally a colleague found out that triggering the history actions via code is what somehow initiated the listeners
and voila! everything fell in place
window.history.pushState(null, null, window.location.href);
window.history.back();
window.history.forward();
In my case, I had a SPA with different drawers on that page and I want them to close when User hits back button..
you can see different drawers in the image below:
I was managing states(eg open or close) of all drawers at a central location (Global state),
I added the followin code to a useEffect hook that runs only once on loading of web app
// pusing initial state on loading
window.history.pushState(
{ // Initial states of drawers
bottomDrawer,
todoDetailDrawer,
rightDrawer,
},
""
);
window.addEventListener("popstate", function () {
//dispatch to previous drawer states
// dispatch will run when window.history.back() is executed
dispatch({
type: "POP_STATE",
});
});
and here is what my dispatch "POP_STATE" was doing,
if (window.history.state !== null) {
const {
bottomDrawer,
rightDrawer,
todoDetailDrawer,
} = window.history.state; // <- retriving state from window.history
return { // <- setting the states
...state,
bottomDrawer,
rightDrawer,
todoDetailDrawer,
};
It was retriving the last state of drawers from window.history and setting it to current state,
Now the last part, when I was calling window.history.pushState({//object with current state}, "title", "url eg /RightDrawer") and window.history.back()
very simple,
window.history.pushState({//object with current state}, "title", "url eg /RightDrawer") on every onClick that opens the drawer
&
window.history.back() on every action that closes the drawer.
I want to close my app when user force close.
Any event arise for force close in ionic application ?
Any plugin for force close event handling in ionic app ?
example:
A common piece of functionality for native mobile applications is the ability to logged out if the user closes the application(force stop/force close).
How can this be achieved for a Cordova / Ionic / PhoneGap ?
Put the following code accordingly to your angular.run configuration, and that's it. You can even add some checks, for example using a boolean value in a Service or a check to the current state to decide if prompt to the user the closing app notice or not.
.run(function($ionicPlatform, $ionicPopup) {
$ionicPlatform.onHardwareBackButton(function () {
if(true) { // your check here
$ionicPopup.confirm({
title: 'System warning',
template: 'are you sure you want to exit?'
}).then(function(res){
if( res ){
navigator.app.exitApp();
}
})
}
})
});
More...
We have a Cordova app, intended for Android devices, which uses the bar code scanner plugin. The app itself as an overwrite to the default Android back button upon device ready:
document.addEventListener("backbutton", onBackKeyDown, false);
The issue is that when the user cancels the scan by pressing the back button, The camera closes and the app display the webview, and then launches the "backbutton" event (i.e. invoking the onBackKeyDown function). As if the back button was pressed on the webview itself and not on the scan activity.
we have tried some alternatives, for example - before starting the scan, remove the event listener:
function startScan() {
document.removeEventListener("backbutton", onBackKeyDown, false);
cordova.plugins.barcodeScanner.scan(
....
but it didn't help.
We cannot override it on the plugin itself, since it's not an activity. Meaning we must do it on the JavaScript.
Any solution is mostly appreciated.
Are you still on this? I'm having issues with the back button myself...
I use the barcode scanner plugin for phonegap too, and have an event listener for the back button.
What I ended up doing is adding a flag that I set to true on every scan and then whenever calling the onBackKeyDown function - if it is true then reset to back false without executing the rest of the function...
var in_barcode_scan = false;
function onBackKeyDown() {
if (in_barcode_scan) {
in_barcode_scan = false;
} else {
//do whatever you need when a legit back button is triggered.
}
}
function startScan() {
in_barcode_scan = true;
cordova.plugins.barcodeScanner.scan(.....
}
I'm now having other issues myself (cancelling the barcode scanner kills some other event listeners), but this should probably do the trick for you...
Jospeh.
This works for me, I added an messaje that the scanner was cancelled only with an alert validating the field "result.text]" like this:
function fileViewSuccess(result) {
console.log("We got a barcode Result: " + result.text);
if(result.cancelled == true){
alert("Was cancelled");
}else{
///do something...
}
}
If someone is still facing this (with android 9 and barcode scanner)
I managed to by pass it by registering a "backbutton" event listener when a cancel is received, and release the event listener after a second.
My handler for the "backbutton" dosen't do anything, it just returns false, so the navigation doesn't happens.
This listener is working on android only, which is good for this problem.
So just call the following "blockBack" function, when getting a "cancel" from the barcode plugin.
function blockBack(){
// stop back button (for 1 s)
// used by barcode camera (when canceling and returnin back)
// was sending the back event to the router, and left the screen
document.addEventListener("backbutton", onBackKeyDown, false);
setTimeout(function(){
document.removeEventListener("backbutton", onBackKeyDown, false)
}, 1000)
function onBackKeyDown() {
// swallow the back button - do nothing
return false;
}
}
Usually backbutton in ionic works based on observers. Removing them temporarily and then reassigning will prevent the page from going previous page. This worked for me. Just follow the below code. (This is angular. Just check for javascript equivalent of the block)
var observers=this.platform.backButton.observers;
this.platform.backButton.observers=[]; //empty array
this.barcode.scan(“Scanning”).then((data)=>{
setTimeout(()=>{this.platform.backButton.observers=observers},500); // important to provide some time to close barcode
});
I am building a phonegap app with jquery mobile and using build.phonegap.com
I have an event to change the page to the login screen after startup process have completed.
This works fine but it will not work on my andriod device unless a debugger is attached to it, in which case it works fine.
The code I have is
$.mobile.changePage("login.html");
I have put this in the mobileinit, pageshow, and now on document.ready function but it doesnt change the behaviour.
I've checked if $.mobile is a function and it is, Have tried everything and can not seem to figure out why this would be happening, any feedback would be much appreciated
I managed to put in a hack to work around this issue.
It was something to do with my phone being really old and slow, so something was getting a little messed up on old/slow andriod versions.
To prevent this from being an Issue I figured out this way that solves the issue and boots up my jquery mobile app on phone gap even if the phone is very slow.
$(document).bind('mobileinit', function () {
setTimeout(function () {
var html = $(".loading-status-text").html();
/* The html has Please Wait in the dom so we know it han't been touched by jQuery */
if (html == 'Please Wait') {
window.location.href = 'index.html';
}
}, 10000);
$(document).on("pageshow", function (e) {
var pageId = $.mobile.activePage.attr('id').toString();
if (pageId == 'loadingScreen') {
/* This wouldn't fire at first */
$(".loading-status-text").html("Welcome to Appname");
$.mobile.changePage("login.html");
}
});
});