JS Firebase: Service worker unable to nagivate - javascript

I am working on a website that uses firebase messaging and for this a custom service worker for firebase is registered to the website.
The website is somewhat a webchat that synchronizes and stores the messages in firebase database and the website uses angular-ui-router for getting the messages key from the url, the website shows a notification when a new message is received from the client and the url is modified for the related message if the notification was clicked, however this has been a struggle for the service worker side when the website is not on focus because the service worker throws an error saying unable to navigate
the service worker code is this:
importScripts('https://www.gstatic.com/firebasejs/4.5.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.5.0/firebase-messaging.js');
firebase.initializeApp({
"messagingSenderId": "######"
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function(payload){
var title = payload.data.title;
var options = {
body: payload.data.message,
icon: "apple-touch-icon.png",
data: {
postId: payload.data.postKey
}
};
return self.registration.showNotification(title, options);
});
self.addEventListener("notificationclick", function(evt){
var postKey = evt.notification.data.postId;
var origin = evt.target.origin;
var urlTarget = origin + "/#!/" + postKey;
evt.notification.close();
evt.waitUntil(
self.clients.matchAll({
includeUncontrolled: true,
type: "window"
})
.then(function(clientList) {
if(clientList.length > 0){
var client = clientList[0];
client.navigate(urlTarget);
if(client.focus){
return client.focus();
}
}
if(self.clients.windowOpen){
return self.clients.windowOpen(urlTarget);
}
}));
});
the behavior i am expecting is as follow:
on focus:
Message received
Show notification
Modify url address when notification clicked
before notification click: https://example.com/#!/
after notification click: https://example.com/#!/-key
on non-focus:
Push received
Modify data for displaying notification
Show notification
Focus/Open website on notification click
Modify url address when focus/open
When focus/open: https://example.com/#!/-key
The code that shows the notification on focus works fine so i don't think that posting the code is necessary.
any help on this is appreciated, i have spent literally all the alternative options i have thought on this

As per the specification (see 4.2.10, step 4), one of the reasons why WindowClient.navigate() might reject is if the active service worker for the client is not the same as the current service worker that's executing the code. In other words, if the window isn't controlled by the current service worker, that service worker can't tell it to navigate to a given URL.
I'd recommend that you use
self.clients.matchAll({
includeUncontrolled: false,
type: 'window'
})
to ensure that you only get back WindowClient instances that are controlled by the current service worker.

Related

Consuming a message sent by OneSignal

I have vue js application and i am consuming messages sent from a php script like this
document.addEventListener("deviceready", OneSignalInit, false);
function OneSignalInit() {
// Uncomment to set OneSignal device logging to VERBOSE
// window.plugins.OneSignal.setLogLevel(6, 0);
// NOTE: Update the setAppId value below with your OneSignal AppId.
window["plugins"].OneSignal.setAppId("");
window["plugins"].OneSignal.setNotificationOpenedHandler(function(jsonData) {
console.log('notificationOpenedCallback: ' + JSON.stringify(jsonData));
});
// iOS - Prompts the user for notification permissions.
// * Since this shows a generic native prompt, we recommend instead using an In-App Message to prompt for notification permission (See step 6) to better communicate to your users what notifications they will get.
window["plugins"].OneSignal.setNotificationReceivedHandler(function(jsonData) {
this.$router.push({path: '/my_ads'});
});
window["plugins"].OneSignal.promptForPushNotificationsWithUserResponse(function(accepted) {
console.log("User accepted notifications: " + accepted);
});
}
In my code above, this gives me a notification that shows on android. I would like to send a message that do not require a notification prompt but i would like to consume the message sent inside my application i.e use the message sent and do something with it i.e go to another route
this.$router.push({path: '/another_route'});
How can i do that?

SignalR multi user live chat desynchronisation

I have a live chat in which multiple people would be connected simultaneously. All public messaging works fine, but sometimes private messaging to a specific id doesn't work. I believe i narrowed it down to when people disconnected and reconnected that they connected to a different instance (perhaps IIS had recycled and started a new hub).
I thought I had fixed it, but I haven't and now I'm here because I'm stuck. What I thought would fix it was changing the connection variable within the startChat() function to refresh it with the correct information.
This is a cut down version of the code, as I didnt thing the rest would be necesary.
Issue is that when connected to signalR recipients of a message directly to them doean't come through, even though the chat Id it's being sent to it correct. Possible hub/socket mismatch?
var chat = $.connection.chatHub;
$(document).ready(function () {
// Start the chat connection.
startChat();
//restart chat if disconnected
$.connection.hub.disconnected(function () {
setTimeout(startChat(), 5000);
});
$.connection.hub.error(function (error) {
$('#messagebar').html('Chat ' + error + '. If this message doesn\'t go away, refresh your page.');
});
chat.client.addToChat = function (response) {
$('#chat-' + response.Type).prepend(response.Message);
};
});
function startChat() {
chat = $.connection.chatHub;
$.connection.hub.start().done(function () {
//get recent chat from db and insert to page.
//also saves user's chat id to their user for lookup when private messaging
$.ajax({
method: 'POST',
url: '/api/Chat/SetupChat/'
});
$('#messagebar').html('Connected to chat.');
});
}
Any help appreciated, Thanks.
Not sure exactly how your message is going missing, but you should send messages to a User instead of by connection id. This way you be able to identify on the server that a User has at least one connection, and send messages to all connections for that User. if a given connection id is no longer valid, but another one is (because the client has refreshed the page for example) the message wont be lost.
From the docs https://learn.microsoft.com/en-us/aspnet/core/signalr/groups:
The user identifier for a connection can be accessed by the
Context.UserIdentifier property in the hub.
public Task SendPrivateMessage(string user, string message)
{
return Clients.User(user).SendAsync("ReceiveMessage", message);
}
Not sure how this relates exactly to your server code, but you could add it if you're unclear how to progress.

How can I ensure re-dispatch of original Google Chat message after OAuth2 authentication succeeds?

I'm writing a Google Hangouts Chat bot with Google Apps Script. The bot authenticates with a third party service with the apps script OAuth2 library. As documented in this How-To, when a message is received by the bot but authentication with the third party service is required, the bot sends a special REQUEST_CONFIG reply to Chat containing the configCompleteRedirectUrl.
var scriptProperties = PropertiesService.getScriptProperties();
function onMessage(event) {
var service = getThirdPartyService();
if (!service.hasAccess()) {
return requestThirdPartyAuth(service, event);
}
Logger.log('execution passed authentication');
return { text: 'Original message ' + event.message.argumentText };
}
function getThirdPartyService() {
var clientId = scriptProperties.getProperty('CLIENT_ID');
var clientSecret = scriptProperties.getProperty('CLIENT_SECRET');
return OAuth2.createService('ThirdPartyApp')
// Set the endpoint URLs.
.setAuthorizationBaseUrl('https://...')
.setTokenUrl('https://.../oauth/token')
// Set the client ID and secret.
.setClientId(clientId)
.setClientSecret(clientSecret)
// Set the name of the callback function that should be invoked to
// complete the OAuth flow.
.setCallbackFunction('authThirdPartyCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
.setCache(CacheService.getUserCache())
.setLock(LockService.getUserLock())
// Set the scope and other parameters.
.setScope('...')
.setParam('audience', '...')
.setParam('prompt', 'consent');
}
function requestThirdPartyAuth(service, event) {
Logger.log('Authorization requested');
return { "actionResponse": {
"type": "REQUEST_CONFIG",
"url": service.getAuthorizationUrl({
configCompleteRedirectUrl: event.configCompleteRedirectUrl
})
}};
/**
* Handles the OAuth callback.
*/
function authThirdPartyCallback(request) {
var service = getThirdPartyService();
var authorized = service.handleCallback(request);
if (authorized) {
Logger.log("user authorized");
//https://stackoverflow.com/a/48030297/9660
return HtmlService.createHtmlOutput("<script>window.top.location.href='" + request.parameter.configCompleteRedirectUrl + "';</script>");
} else {
Logger.log("user denied access");
return HtmlService.createHtmlOutput('Denied');
}
}
The service defines a callback authentication function, which in turn sends the browser to the configCompleteRedirectUrl. After this URL has been reached, the original message is supposed to be sent or re-dispatched a second time (see How-To step 7.3).
The authentication callback is successful because the last page displayed in the browser OAuth flow is the one specified in event.configCompleteRedirectUrl. Within the Chat window, the configuration prompt is erased, and the original message is changed to public. However, the original message is not dispatched again. The last log displayed in the apps script console is from the authentication callback event.
Is there something I have done incorrectly that prevents the original message from being dispatched again?
After much back and forth with a Google support team member, it turns out that there is a bug in the Hangouts Chat implementation when run against the V8 Apps Script runtime.
My appsscript.json file had "runtimeVersion": "V8" set. The re-dispatch does not work in this scenario. After I reverted to "runtimeVersion": "STABLE" in appsscript.json and re-deployed my scripts, the re-dispatch started working.

Can't get custom push notification event working in PWA (Firebase)

I've been searching for a few hours on how to get my custom push notification working. Here is how I've set up my project: no front-end framework, a Node.js/Express.js back-end, Firebase Cloud Messaging (FCM) as push manager and a custom service worker. I am currently hosting the app on localhost and I have HTTPS set up and a manifest.json that contains the minimum amount of fields to get started. The manifest.json contains a start_url field that points to /index.html, the landing page for the app. The app is bundled with webpack v. 4.
Back-end
On the back-end, I have set up the Firebase Admin SDK in a specific router and I send a custom notification object a bit like the following to the FCM server:
let fcMessage = {
data : {
title : 'Foo',
tag : 'url to view that opens bar.html'
}
};
When an interesting event occurs on the back-end, it retrieves a list of users that contains the FCM registration tokens and sends the notification to the FCM servers. This part works great.
Front-end
I have two service workers on the front-end. Inside my front-end index.js, I register a custom service worker named sw.js in the domain root and tell firebase to use that service worker like so:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js')
.then(registration => {
messaging.useServiceWorker(registration);
})
.catch(err => console.error(`OOps! ${err}`));
}
FCM and its credentials are set up and the user can subscribe to push notifications. I won't show that code here since it works and I don't believe it is the issue.
Now on to the service workers themselves. I have a firebase-messaging-sw.js file at the root of my domain. It contains the following code:
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js');
firebase.initializeApp(configuration);
const messaging = firebase.messaging();
Configuration is just a placeholder for all of the creds. Again that stuff works.
What I want to do is to NOT use the FCM push notification and instead create my own push notification that contains a url to a view that the user can click on and go to that view. The following code almost works and is a hack I found on another site:
class CustomPushEvent extends Event {
constructor(data) {
super('push');
Object.assign(this, data);
this.custom = true;
}
}
self.addEventListener('push', (e) => {
console.log('[Service Worker] heard a push ', e);
// Skip if event is our own custom event
if (e.custom) return;
// Keep old event data to override
let oldData = e.data;
// Create a new event to dispatch
let newEvent = new CustomPushEvent({
data: {
json() {
let newData = oldData.json();
newData._notification = newData.notification;
delete newData.notification;
return newData;
},
},
waitUntil: e.waitUntil.bind(e),
})
// Stop event propagation
e.stopImmediatePropagation();
// Dispatch the new wrapped event
dispatchEvent(newEvent);
});
messaging.setBackgroundMessageHandler(function(payload) {
if (payload.hasOwnProperty('_notification')) {
return self.registration.showNotification(payload.data.title,
{
body : payload.data.text,
actions : [
{
action : `${payload.data.tag}`,
title : 'Go to link'
}
]
});
} else {
return;
}
});
self.addEventListener('notificationclick', function(e) {
console.log('CLICK');
e.notification.close();
e.waitUntil(clients.matchAll({ type : 'window' })
.then(function(clientList) {
console.log('client List ', clientList);
const cLng = clientList.length;
if (clientList.length > 0) {
for (let i = 0; i < cLng; i++) {
const client = clientList[i];
if (client.url === '/' && 'focus' in client) {
return client.focus();
}
}
} else {
console.log('no clients ', e.action);
clients.openWindow(e.action)
.then(client => {
console.log('client ', client);
return client.navigate(e.action);
})
.catch(err => console.error(`[Service Worker] cannot open client : ${err} `));
}
}))
});
The hack is meant to capture a push event and the FCM default notification payload and instead serve that payload through a custom one made via the Notification API.
The code above works great but ONLY if I put it in the firebase-messaging-sw.js file. That's not what I really want to do: I want to put it in the sw.js file instead but when I do, the sw.js cannot hear any push events and instead I get the default FCM push notification. I've also tried importing the entire firebase-messaging-sw scripts into the custom service worker and it still won't hear the message events.
Why do I want to use it in my service worker instead of the Firebase one? It's to be able to open the app on the view passed into the 'tag' field on the notification's body. If I use the Firebase service worker, it tells me that it's not the active registered service worker and though the app does open in a new window, it only opens on /index.html.
Some minor observations I've made: the clients array is always empty when the last bit of code is added to the firebase-messaging-sw.js file. The custom service worker is installed properly because it handles the app shell cache and listens to all of the other events normally. The firebase-messaging-sw service worker is also installed properly.
After much pain and aggravation, I figured out what the problem was. It was a combination of the architecture of the app (that is, a traditional Multi-Page App) and a badly-formed url in the custom service worker, sw.js as so:
sw.js
self.addEventListener('fetch', e => {
// in this app, if a fetch event url calls the back-end, it contains api and we
// treat it differently from the app shell
if (!e.request.url.includes('api')) {
switch(e.request.url) {
case `${endpoint}/bar`: // <-- this is the problem right here
matcher(e, '/views/bar.html');
break;
case `${endpoint}/bar.js`:
matcher(e, '/scripts/bar.js');
break;
case `${endpoint}/index.js`:
matcher(e, '/index.js');
break;
case `${endpoint}/manifest.json`:
matcher(e, '/manifest.json');
break;
case `${endpoint}/baz/`:
matcher(e, '/views/bar.html');
break;
case `${endpoint}/baz.js`:
matcher(e, '/scripts/bar.js');
break;
default:
console.log('default');
matcher(e, '/index.html');
}
}
});
Matcher is the function that matches the request url with the file on the server. If the file already exists in the cache, it returns what is in the cache but if it doesn't exist in the cache, it fetches the file from the server.
Every time the user clicks on the notification, it's supposed to take him/her to the 'bar' html view. In the switch it must be:
case `${endpoint}/bar/`:
and not
case `${endpoint}/bar`:
Even though the message-related code is still in the firebase-messaging-sw.js file, what happens is it creates a new WindowClient when the browser is in the background. That WindowClient is under the influence of sw.js, not firebase-messaging-sw.js. As a result, when the window is opened, sw.js intercepts the call and takes over from firebase-messaging-sw.js.

Getting the Auth Token for the secondary Id from Google chrome extension using OAuth 2.0 & Client ID

I am fairly new to developing chrome extensions, more specifically to the user authentication part in chrome extensions. I am following User Identity example from Google Developer docs.
The example works perfectly fine. I was able to generate the client id for the chrome app, add the scope for API's in my case Gmail API. And finally get the Auth Token by adding the identitypermission in manifest.json as follows
"oauth2": {
"client_id": "MY CLIENT ID",
"scopes": [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.modify"
]
}
And my app.js is a content_script which has the following code.
chrome.identity.getAuthToken({ 'interactive': true }, function(token) {
/* With which I can use xhr requests to get data from Gmail API */
console.log('Access Token : '+token);
});
Now this token that I get gives me the result for the user with which I have logged into chrome. Meaning Let's say I have a UserA with email address user_a#gmail.com and I have used this log into the chrome browser.
Question
How do I get the associated accounts or the secondary accounts? For instance, let's say a User Blogs into Gmail from the chrome browser. Is it possible to access the Gmail API for that particular user who is currently logged in?
I have tried a couple of things here.
gapi.auth.authorize({
'client_id': CLIENT_ID,
'scope': SCOPES.join(' '),
'immediate': true
},
function(authResult){//do something});
In the above scenario, the client id and scopes are fetched from the manifest.json using chrome.runtime.getManifest();.
This method uses the client.js from google api's and makes use of gapi variable.
In this case, I get the access token for the user whom I generated the client id, not even the chrome application user.
Furthermore, When I open an incognito mode and access this plugin, still I get the same user's access token.
Additional Note
I tried the same gapi.auth.authorize() using a Web OAuth 2 Client Id. It works perfectly fine. I mean whenever this authorize is executed it fetches the current logged in user's data or it asks for a login where the user can log in and authenticate. How do I achieve the same thing in chrome extension? Kindly let me know if I am missing something here.
As of now, this is not possible using supported APIs in Google Chrome stable (Version 63). However, in the Dev channel and most likely with a future release, the following will be possible:
chrome.identity.getAccounts(function(accounts) {
// accounts is a list of accounts.
chrome.identity.getAuthToken({ 'interactive': true, 'account': accounts[0] }, function(token) {
/* With which i can use xhr requests to get data from gmail api */
console.log('Access Token : '+token);
});
});
See the documentation for getAccounts().
EDIT: Something that might work in the meantime is registering for the onSigninChanged event.
How I ended up handling is was this (summary):
In the page layer, on load, I send a message down the stack to the background layer.
I used launchWebAuthFlow() to https://accounts.google.com/o/oauth2/auth to get the access_token for the account.
I made an AJAX call to https://www.googleapis.com/oauth2/v4/token using the access_token to get a refresh token.
When a user changes which account they are using via the avatar button on the top-right, this process is triggered again, as it is initiated by onLoad for the page layer of the extension.
The things left out the description above are caching and error handling, which are super-important.
http://developer.streak.com/2014/10/how-to-use-gmail-api-in-chrome-extension.html
is the complete solution which I have recently implemented on background page to work with Gmail API.
The content script is calling popup window to authorize using generated URL and simple server endpoint to store the refresh token.
$.oauthpopup = function (options) {
options.windowName = options.windowName || 'ConnectWithOAuth'; // should not include space for IE
var left = (screen.width / 2) - (800 / 2);
var top = (screen.height / 2) - (500 / 1.4);
options.windowOptions = options.windowOptions || 'location=0,status=0,width=800,height=500,left=' + left + ',top=' + top;
options.callback = options.callback || function () {
window.location.reload();
};
var that = this;
debug('oauthpopup open separate _oauthWindow');
that._oauthWindow = window.open(options.path, options.windowName, options.windowOptions);
};
$.oauthpopup({
path: 'https://accounts.google.com/o/oauth2/auth?' +
'access_type=offline' +
'&approval_prompt=force' +
'&client_id=' + clientID +
'&redirect_uri=' + callBackUrl +
'&response_type=code' +
'&scope=https://mail.google.com/ email profile' +
'&state=' + login.timyoUUID +
'&user_id=' + login.user_email,
callback: function () {
// do callback stuff
},
});
callBackUrl is used to store refresh token on the server.
Here is the example how I set access token for every request to gapi
export function setTokenForGAPI(accessToken) {
return getGAPIClient()
.then(() => {
const isSameToken = (currentAccessToken === accessToken);
const noToken = ((accessToken === undefined) || (accessToken === ''));
if (isSameToken || noToken) return;
gapi.auth.setToken({
access_token: accessToken,
});
currentAccessToken = accessToken;
})
.catch(function (e) {
console.log('error in setTokenForGAPI', e);
});
}
export function getEmailsByThreadIds(accessToken, ids) {
return setTokenForGAPI(accessToken)
.then(groupedThreadDetailsRequests(ids))
.then(processEmailDetailsResponse);
}

Categories