I'm working on integrating web-push notifications in my web-application. Everything works fine for Chrome and Firefox on desktop and Chrome on Android, but not for Firefox for Android. This question seems to discuss the same issue but has no responses.
I used this tutorial as a base for the service worker registration script. I have added some more prints/checks but it is mostly the same.
So, when calling the registerServiceWorker method from a button press on FF Android, the serviceWorker is installed, the subscribeUser function is called, but the pushManager.subscribe method will fail with the following error message:
DOMException: User denied permission to use the Push API.
This is not correct, even while paused on the error print line Notification.permission will return "granted".
Doing the same thing on the nightly build results in slightly different, but still incorrect behaviour. The pushManager.subscribe method does not throw an error. Instead the callback is ran but with a null value for the subscription argument. Therefore, the process still fails.
Service worker registration script:
'use strict';
function updateSubscriptionOnServer(subscription, apiEndpoint) {
return fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
subscription_json: JSON.stringify(subscription)
})
});
}
function subscribeUser(swRegistration, applicationServerPublicKey, apiEndpoint) {
// It seems browsers can take the base64 string directly.
// const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
console.log(`Subscribing pushManager with appkey ${applicationServerPublicKey}`);
swRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerPublicKey
})
.then(function(subscription) {
console.log('User is subscribed.');
console.log(`Sending subscription data to server (${apiEndpoint})`, subscription);
return updateSubscriptionOnServer(subscription, apiEndpoint);
})
.then(function(response) {
if (!response.ok) {
throw new Error('Bad status code from server.');
}
return response.json();
})
.then(function(responseData) {
console.log(responseData);
if (responseData.status!=="success") {
throw new Error('Bad response from server.');
}
})
.catch(function(err) {
// FF Android says "User denied permission to use the Push API."
console.log('Failed to subscribe the user: ', err);
console.log(err.stack);
});
}
function registerServiceWorker(serviceWorkerUrl, applicationServerPublicKey, apiEndpoint){
let swRegistration = null;
if ('serviceWorker' in navigator && 'PushManager' in window) {
console.log('Service Worker and Push is supported');
console.log(`Current Notification.permission = ${Notification.permission}`);
swRegistration = navigator.serviceWorker.register(serviceWorkerUrl)
.then(function(swReg) {
console.log('Service Worker is registered', swReg);
console.log(`Current Notification.permission = ${Notification.permission}`); // Will give "granted"
subscribeUser(swReg, applicationServerPublicKey, apiEndpoint);
})
.catch(function(error) {
console.error('Service Worker Error', error);
});
} else {
console.warn('Push messaging is not supported');
return false;
}
return swRegistration;
}
I cannot figure out how to get a working push-subscription. As said before, all other browsers that I have tried work fine. I hope someone can point me in the right direction. Is this a bug in Firefox Android or in my code?
Showing notifications manually using
new Notification("Hi there!");
does work, proving in principle that permissions are not the issue.
UPDATE:
FF Fenix team confirmed a bug while displaying the notifications
Feel free to track it here
Got curious regarding service worker support for Firefox mobile browser.
Tried hard to find a debug tool for Mobile Firefox as plugin or a 3rd party tool with no luck.
However, I've tested my web push application on Mobile Firefox and may totally confirm there is an issue with Service Worker Registration state.
In order to discard any issues within my code, I've used Matt Gaunt's Webpush Book
And I can tell Matt's service worker returns registration as simply as that:
async function registerSW() {
await navigator.serviceWorker.register('/demos/notification-examples/service-worker.js');
}
function getSW() {
return navigator.serviceWorker.getRegistration('/demos/notification-examples/service-worker.js');
}
Firefox for Android successfully requests permission for notification, but it doesn't display push whenever you launch the .showNotification function.
Here's an example of showNotification method within the Badge example in Matt's website:
async function onBadgeClick() {
const reg = await getSW();
/**** START badgeNotification ****/
const title = 'Badge Notification';
const options = {
badge: '/demos/notification-examples/images/badge-128x128.png'
};
reg.showNotification(title, options);
/**** END badgeNotification ****/
}
Looks fine and should be working fine, but for Firefox Mobile it doesn't work at all.
I guess this issue should be escalated to Mozilla.
UPDATE
New bug report was created on Github:
Related
I recently added a notification part for our web application and it's working fine on chrome and opera, but for firefox, initially, I got an error with this message:
The Notification permission may only be requested from inside a short running user-generated event handler.
So, I added a button for granting the notifications permission and now I find out there is a bigger bug; the 'onchange' event of navigator.permissions not working to activate notifications after getting permission from the user. here is my code:
self.addEventListener('activate', async () => {
if ('permissions' in navigator) { // inside of this if not executing on firefox
navigator.permissions.query({ name: 'notifications' }).then(function (permissionStatus) {
permissionStatus.onchange = function () {
// do thing in here...
};
});
}
//...
});
Although I know 'window' is the default variable in js files, I also tried 'window.navigator' instead of 'navigator' and nothing changed.
Finally, I found out since we are triggering ask permission by hand and by clicking on a button, the firefox mechanism is different and we should do our onchange action as a result of the permission request promise just like below:
Notification.requestPermission()
.then(async result => {
if (result === 'granted') {
// subscribe and do stuff in here...
}
})
.catch(err => {
console.log('Error: ', err);
});
You can still use the explained onchange method for other browsers like chrome.
After upgrading a react-native from 0.56 to 0.59.8 (using FBSDK 0.10.1), the facebook login don't work anymore on android.
when I fill the Fb login form and continue, LoginManager.logInWithPermissions promise does not resolve and never goes to .then() after logInWithPermissions()
here is my code:
loginWithFBSDKLoginManager() {
LoginManager.logOut();
const self = this;
return new Promise((resolve, reject) => {
LoginManager.logInWithPermissions(['public_profile', 'email']).then(function (result) {
if (result.isCancelled) {
return;
}
AccessToken.getCurrentAccessToken().then((data) => {
const accessToken = data.accessToken.toString();
const userID = data.userID.toString();
self
.getUserInfos(accessToken)
.then((response) => {
resolve({ ...response, accessToken, userID });
})
.catch(
function (error) {
reject(error);
}
);
});
});
});
}
I tried to put breakpoint almost everywhere but nothing help.
To most strange thing is that work perfrectly on iOS, this issue only occurs on Android.
I tried to debug my app using Android Studio and the only error found in the console is
I/chromium: [INFO:CONSOLE(0)] "Refused to display
'https://m.facebook.com/intern/common/referer_frame.php' in a frame
because it set 'X-Frame-Options' to 'deny'.", source:
https://m.facebook.com/v3.3/dialog/oauth?client_id=2129868160675609&e2e=%7B%22init%22%3A1562743341374%7D&sdk=android-5.0.3&scope=public_profile%2Cemail&state=%7B%220_auth_logger_id%22%3A%22edb48b96-de45-47e6-8331-f3db300e4eb2%22%2C%223_method%22%3A%22web_view%22%7D&default_audience=friends&redirect_uri=fbconnect%3A%2F%2Fsuccess&auth_type=rerequest&display=touch&response_type=token%2Csigned_request&return_scopes=true&ret=login&fbapp_pres=0&logger_id=edb48b96-de45-47e6-8331-f3db300e4eb2#= (0) I/chromium: [INFO:CONSOLE(53)] "ErrorUtils caught an error:
"Script error.". Subsequent errors won't be logged; see
https://fburl.com/debugjs.", source:
https://static.xx.fbcdn.net/rsrc.php/v3iEpX4/ys/l/fr_FR/LDgA15LzuMu.js
(53) I/chromium: [INFO:CONSOLE(262)] "Uncaught SecurityError: Blocked
a frame with origin "https://m.facebook.com" from accessing a frame
with origin "null". The frame requesting access has a protocol of
"https", the frame being accessed has a protocol of "data". Protocols
must match.
", source: https://static.xx.fbcdn.net/rsrc.php/v3iEpX4/ys/l/fr_FR/LDgA15LzuMu.js
(262)
Could somebody help me solving this? or guide me to find the root cause.
Thanks
I manageed to solve it by upgrading from v0.10.1 to v1.0.1. Remember to remove all the CallbackManager stuff in MainApplication.java
I have a service worker that I use to enable an offline version of my website. This works great. I also have an Android app that is basically just a wrapper around a webview that loads my website.
All was fine and dandy until about 2-3 weeks ago when the Fetch() request started immediately failing. It is only failing when running through the Android webview. Running through a browser works fine. If the resource is cached already (i.e. via the install event) then it works great, it's only when I get a page that is not cached.
The code in my service worker:
self.addEventListener("fetch", function (event) {
if (event.request.method !== 'GET'
|| event.request.url.toLowerCase().indexOf('//ws') > -1
|| event.request.url.toLowerCase().indexOf('localws') > -1) {
// Don't intercept requests made to the web service
// If we don't block the event as shown below, then the request will go to
// the network as usual.
return;
}
event.respondWith(async function () {
// override the default behavior
var oCache = await caches.open('cp_' + version);
var cached = await oCache.match(event.request.url);
if (cached && cached.status < 300) {
return cached;
}
// Need to make a call to the network
try {
var oResp = await fetch(event.request); // THIS LINE CAUSES THE PROBLEM!!!
return oResp;
} catch (oError) {
console.log('SW WORKER: fetch request to network failed.', event.request);
return new Response('<h1>Offline_sw.js: An error has occured. Please try again.</h1><br><h2>Could not load URL: ' + event.request.url + '</h2>', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'text/html'
})
});
}
}()); // event.respondwith
}); // fetch
The line:
var oResp = await fetch(event.request);
is called once I've determined it is not cached and seems to be the culprit. When it errors out I get the following error in my catch(): 'Failed to fetch'
This seems pretty generic and not helpful. Again, this works when going through a browser and so I know it's not a CORS issue, service worker in the wrong directory, etc. Again, it worked until about 3 weeks ago and now I'm getting reports from customers that it's not working.
Here's a screen shot of the actual event.request that I'm sending off:
In the chrome developer tools (used to debug the webview) I see the following:
Am I doing something wrong or is this a bug in the webview / chrome that was released recently? (I say that as chrome powers the webview)
Looks like it was a bug in chromium. See bugs.chromium.org/p/chromium/issues/detail?id=977784. Should be fixed in v 76.
As a work around (as the link mentioned), you can add the following to your android code:
ServiceWorkerController oSWController = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
oSWController = ServiceWorkerController.getInstance();
oSWController.setServiceWorkerClient(new ServiceWorkerClient(){
#Nullable
#Override
public WebResourceResponse shouldInterceptRequest(WebResourceRequest request){
return super.shouldInterceptRequest(request);
}
});
}
I can't seem to figure out any reason why a service worker would be deleted with the code that I have that registers or actually is the service worker.
But in this site, it shows up as deleted in the Service Workers section of the chrome dev tools (image below).
Yet it is also registering properly as logged in the console (same image below).
Here is the service worker registration code:
if('serviceWorker' in navigator){
navigator.serviceWorker.register('/earnie.min.js', { scope: '/'}).then(function(registration){
console.log('Registration successful, scope is:', registration.scope);
}).catch(function(error){
console.log('Service worker registration failed, error:', error);
});
}
Here is the service worker code:
var cachename="e2",cachelist=";;.;/;./;/privacyPolicy.html;/css/main.css;/css/normalize.css;".split(";");
self.addEventListener("install",function(a){
a.waitUntil(caches.open(cachename).then(function(a){
return a.addAll(cachelist)
}))
});
self.addEventListener("fetch",function(a){
a.respondWith(caches.match(a.request).then(function(b){
return b?b:fetch(a.request.clone(), { credentials: 'include', redirect: 'follow' })
}))
});
What is causing it to be deleted and not registering?
Registration succeeded, but installation actually fails. Your waitUntil() promise is not resolving, which causes InstallEvent event to fail, thus deleting the ServiceWorker. cachelist probably returns invalid/empty values when you run split(';')
I recommend ensuring that cachelist is an array with valid URI values, then you can debug within the install event**
self.addEventListener("install", event => {
event.waitUntil(
caches.open(cachename)
.then(cache => cache.addAll(cachelist))
.catch(error => console.error('💩', error))
)
})
**You'll most likely need "Preserve log" console option enabled in Chrome Dev Tools to see the console error.
With a PWA, we can handle when the device connection is down with offline mode. But how do we detect a fixed network connection and automatically reload/re-activate the application?
You could monitor the offline and online events, which are widely supported. Further, you could test connectivity by attempting to fetch HEAD from the target server URL:
// Test this by running the code snippet below and then
// use the "Offline" checkbox in DevTools Network panel
window.addEventListener('online', handleConnection);
window.addEventListener('offline', handleConnection);
function handleConnection() {
if (navigator.onLine) {
isReachable(getServerUrl()).then(function(online) {
if (online) {
// handle online status
console.log('online');
} else {
console.log('no connectivity');
}
});
} else {
// handle offline status
console.log('offline');
}
}
function isReachable(url) {
/**
* Note: fetch() still "succeeds" for 404s on subdirectories,
* which is ok when only testing for domain reachability.
*
* Example:
* https://google.com/noexist does not throw
* https://noexist.com/noexist does throw
*/
return fetch(url, { method: 'HEAD', mode: 'no-cors' })
.then(function(resp) {
return resp && (resp.ok || resp.type === 'opaque');
})
.catch(function(err) {
console.warn('[conn test failure]:', err);
});
}
function getServerUrl() {
return document.getElementById('serverUrl').value || window.location.origin;
}
<fieldset>
<label>
<span>Server URL for connectivity test:</span>
<input id="serverUrl" style="width: 100%">
</label>
</fieldset>
<script>document.getElementById('serverUrl').value = window.location.origin;</script>
<p>
<i>Use Network Panel in DevTools to toggle Offline status</i>
</p>
One technique of handling this:
Offline event
show offline icon/status
enable only features that are available offline (via cached data)
Online event
show online icon/status
enable all features
Be careful with the online event, that only tells the device if connected. It can be connected to a WiFi hotspot without actual Internet connectivity (because of credentials for example).
A common practice in PWAs is to follow the Application Shell approach to your application. This would allow you to cache the Application Shell upon entry, and then load the data based upon the connection.
The most common method for caching and serving in this approach is to serve from cache with fallback to network, where whenever the resource requested is not available in the cache then you send the request via the network and cache the response. Then serve from the cache.
This allows for a more graceful degradation when you are on a spotty connection, such as on the train.
An example of implementing this:
const cacheName = "my-cache-v1"
self.addEventListener('fetch', (event) => {
if (event.request.method === 'GET') {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}
return fetch(event.request).then((response) => {
return caches.open(cacheName).then((cache) => {
cache.put(event.request.url, response.clone());
return response;
});
});
})
);
}
});
In the above example (only one of the required steps in a Service Worker life cycle), you would also need to delete outdated cache entries.
Most of the services I've seen use the following practice: with an increasing to a certain value timeout, trying to contact the server. When the maximum timeout value is reached, an indicator with a manual recconect button appears which indicates in how many time the next attempt of reconnect will occur