ServiceWorker WindowClient.navigate promise rejected - push-notification

I'm using Firebase Cloud Messaging + Service worker to handle background push notifications.
When the notification (which contains some data + a URL) is clicked, I want to either:
Focus the window if it's already on the desired URL
Navigate to the URL and focus it if there is already an active tab open
Open a new window to the URL if neither of the above conditions are met
Points 1 and 3 work with the below SW code.
For some reason point #2 isn't working. The client.navigate() promise is being rejected with:
Uncaught (in promise) TypeError: Cannot navigate to URL: http://localhost:4200/tasks/-KMcCHZdQ2YKCgTA4ddd
I thought it might be due to a lack of https, but from my reading it appears as though localhost is whitelisted while developing with SW.
firebase-messaging-sw.js:
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/3.5.3/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/3.5.3/firebase-messaging.js');
// Initialize the Firebase app in the service worker by passing in the
// messagingSenderId.
firebase.initializeApp({
'messagingSenderId': 'XXXX'
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(payload => {
console.log('[firebase-messaging-sw.js] Received background message ', payload);
let notificationData = JSON.parse(payload.data.notification);
const notificationOptions = {
body: notificationData.body,
data: {
clickUrl: notificationData.clickUrl
}
};
return self.registration.showNotification(notificationData.title,
notificationOptions);
});
self.addEventListener('notificationclick', event => {
console.log('[firebase-messaging-sw.js] Notification OnClick: ', event);
// Android doesn’t close the notification when you click on it
// See: http://crbug.com/463146
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.notification.close();
let validUrls = /localhost:4200/;
let newUrl = event.notification.data.clickUrl || '';
function endsWith(str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
}
event.waitUntil(
clients.matchAll({
includeUncontrolled: true,
type: 'window'
})
.then(windowClients => {
for (let i = 0; i < windowClients.length; i++) {
let client = windowClients[i];
if (validUrls.test(client.url) && 'focus' in client) {
if (endsWith(client.url, newUrl)) {
console.log('URL already open, focusing.');
return client.focus();
} else {
console.log('Navigate to URL and focus', client.url, newUrl);
return client.navigate(newUrl).then(client => client.focus());
}
}
}
if (clients.openWindow) {
console.log('Opening new window', newUrl);
return clients.openWindow(newUrl);
}
})
);
});
The vast majority of my SW code is taken from:
https://gist.github.com/vibgy/0c5f51a8c5756a5c408da214da5aa7b0

I'd recommend leaving out includeUncontrolled: true from your clients.matchAll().
The WindowClient that you're acting on might not have the current service worker as its active service worker. As per item 4 in the specification for WindowClient.navigate():
If the context object’s associated service worker client’s active
service worker is not the context object’s relevant global object’s
service worker, return a promise rejected with a TypeError.
If you can reproduce the issue when you're sure the client is currently controlled by the service worker, then there might be something else going on, but that's what I'd try as a first step.

This worked for me:
1- create an observable and make sure not to call the messaging API before it resolves.
2- register the service worker yourself, and check first if its already registered
3- call event.waitUntil(clients.claim()); in your service worker
private isMessagingInitialized$: Subject<void>;
constructor(private firebaseApp: firebase.app.App) {
navigator.serviceWorker.getRegistration('/').then(registration => {
if (registration) {
// optionally update your service worker to the latest firebase-messaging-sw.js
registration.update().then(() => {
firebase.messaging(this.firebaseApp).useServiceWorker(registration);
this.isMessagingInitialized$.next();
});
}
else {
navigator.serviceWorker.register('firebase-messaging-sw.js', { scope:'/'}).then(
registration => {
firebase.messaging(this.firebaseApp).useServiceWorker(registration);
this.isMessagingInitialized$.next();
}
);
}
});
this.isMessagingInitialized$.subscribe(
() => {
firebase.messaging(this.firebaseApp).usePublicVapidKey('Your public api key');
firebase.messaging(this.firebaseApp).onTokenRefresh(() => {
this.getToken().subscribe((token: string) => {
})
});
firebase.messaging(this.firebaseApp).onMessage((payload: any) => {
});
}
);
}
firebase-messaging-sw.js
self.addEventListener('notificationclick', function (event) {
event.notification.close();
switch (event.action) {
case 'close': {
break;
}
default: {
event.waitUntil(clients.claim());// this
event.waitUntil(clients.matchAll({
includeUncontrolled: true,
type: "window"
}).then(function (clientList) {
...
clientList[i].navigate('you url');
...
}
}
}
}

Related

Unable to catch the 'auth' event in Hub.listen while calling Auth.federatedSignIn

I am using SolidJS and building a SPA (no server rendering). For authentication, I use the #aws-amplify/core and #aws-amplify/auth packages. At the application root I call the Hub.listen function:
Hub.listen('auth', ({ payload }) => console.log(payload));
In the SignUp component I call Auth.federatedSignIn:
const SignUp = () => {
return (
<button onClick={() => {
Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google });
}}>
Sign up
</button>
);
}
I have configured the Amplify as such:
Amplify.configure({
Auth: {
region: import.meta.env.VITE_AWS_REGION,
userPoolId: import.meta.env.VITE_AWS_POOL_ID,
userPoolWebClientId: import.meta.env.VITE_AWS_POOL_CLIENT_ID,
oauth: {
domain: import.meta.env.VITE_AUTH_URL,
responseType: 'code',
redirectSignIn: location.origin + '/account/external',
redirectSignOut: location.origin + '/my',
},
},
});
When I click on the button I am redirected to the import.meta.env.VITE_AUTH_URL (simply outside of my app), choose an account, and then return back to the /account/external page. At that time I expect a consoled payload object in Web tools, but there is nothing. I get it when I call Auth.signOut(), so I assume that I configured Amplify correctly and Hub is subscribed to the auth channel.
My thoughts were that Hub cannot catch any events because after returning the application basically renders again in a new context and Hub simply isn't able to catch anything (events aren't sent from AWS?). I tried to declare the urlOpener function under the oauth property in the config and Google's sign page opened in a new tab, but even then I couldn't get any events in the preserved old page (from which I called Auth.federatedSignIn).
Questions:
How should I organize the code to get the signIn and signUp events?
Can I pass some data into the Auth.federatedSignIn to get it back in the Hub.listen, so I will be able to join the CognitoUser with the data that existed at the time of starting Sign in/Sign up (I want to add a new login type to existed user)?
Here is an example regarding the first question. Just check that your listener is set before you call the Auth.federatedSignIn() method.
export default class SignInService {
constructor(private landingFacade: LandingFacade) {
this.setupAuthListeners(); // Should be called at the top level.
}
private setupAuthListeners() {
Hub.listen('auth', ({ payload: { event, data } }) => {
switch (event) {
case 'signIn':
this.landingFacade.signInSuccess();
break;
case 'signIn_failure':
console.log('Sign in failure', data);
break;
case 'configured':
console.log('the Auth module is configured', data);
}
});
}
public async signIn(): Promise<void> {
await Auth.federatedSignIn();
}
}
For the second one: I'll use a local state and set/query the object you need.

Firebase: Get notified just after the firebase-function trigger promise completed

In my app I pushed some object to my firebase-database and immediately after that (after the then-promise fully filled) I fetch the object (with the returned key) from the database (with the on-value method).
In addition, I make some changes on the pushed object using the firebase-functions.
How can I receive the object (in the app) just after the changes and not before? (like other ordinary backend services)
I hope this helps you, I have not tested this piece of code but it should help you in the right direction.
Also dont use this exact code in production, there is plenty room for improvement, this is just an example code.
exports.testFunction = functions.https.onRequest((req, res) => {
if (req && req.body) {
if (
req.body.hasOwnProperty('name') &&
req.body.hasOwnProperty('age')
) {
const person = {
name: req.body['name'],
age: req.body['age']
}
// Make some changes to the person object
person['hobby'] = 'Programmer';
// Add object to FireStore
admin
.firestore()
.collection('/persons')
.add(person)
.then((success) => {
// Return the added & changed person
res.status(201).send(JSON.stringify(person));
})
.catch((error) => {
// Error
console.error('Something broke', error)
res.status(500).send();
});
}
else {
// Error
res.status(500).send({err: 'Missing property'});
}
}
else {
// Error
res.status(500).send({err: 'Missing something'});
}
});

Communicating with an external service: internal server error 500

I'm trying to make a GET request to an HTTPS service ( https://broker.bronos.net ). This service is an API that communicates with a client on my LAN. I can't get it to work via functions.https.get(URL, (s,ss) => {});
Please help -- I'm very new to web development, let alone google actions.
I'm using the apiai-starter-app as the base, which functions perfectly fine until I add the line above which returns internal server error 500.
Note: I've tried before adding billing to the project and after as well. Neither work.
Edit:
using this
const https = require('https');
https.get('https://broker.bronos.net/v1/CLIENT_ID/ROOM_NAME/ACTION/PARAM', (resp) => {
let data = '';
// A chunk of data has been recieved.
resp.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received. Print out the result.
resp.on('end', () => {
if (requestSource === googleAssistantRequest) {
sendGoogleResponse(JSON.parse(data).explanation); // Send simple response to user
} else {
sendResponse(JSON.parse(data).explanation); // Send simple response to user
}
});
}).on("error", (err) => {
if (requestSource === googleAssistantRequest) {
sendResponse("Error: " + err.message); // Send simple response to user
} else {
sendResponse("Error: " + err.message); // Send simple response to user
}
});
Firebase's functions have limited access to external APIs on the free tier. By upgrading to Blaze or Flame plans you will be able to make external API calls.
Enabling Firebase Blaze plan + the following code worked
const https = require('https');
https.get('https://broker.bronos.net/v1/CLIENT_ID/Living%20Room/volume/20', (resp) => {
let data = '';
// A chunk of data has been recieved.
resp.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received. Print out the result.
resp.on('end', () => {
if (requestSource === googleAssistantRequest) {
sendGoogleResponse(JSON.parse(data).explanation); // Send simple response to user
} else {
sendResponse(JSON.parse(data).explanation); // Send simple response to user
}
});
}).on("error", (err) => {
if (requestSource === googleAssistantRequest) {
sendResponse("Error: " + err.message); // Send simple response to user
} else {
sendResponse("Error: " + err.message); // Send simple response to user
}
});

Communicate notification data from messaging.setBackgroundMessageHandler to webpage

I am trying to communicate data received by the service worker back to webpage.
On the webpage 'navigator.serviceWorker.controller' is null. The sevice worker has self.client as empty.
Any samples or directions will help
What you can do is get a list of window clients which will return a list of the tabs for your origin and then post a message to each window client. (This code would be in the setBackgroundMessageHandler() ):
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true
})
.then((windowClients) => {
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
windowClient.postMessage(data);
}
})
.then(() => {
return registration.showNotification('my notification title');
});
return promiseChain;
Then to receive the message in the page, add a listener like so:
navigator.serviceWorker.addEventListener('message', function(event) {
console.log('Received a message from service worker: ', event.data);
});

RxJS wait for second observable then retry original observable on error - TypeScript/Angular 2

I am fairly new to Angular 2, TypeScript and RxJS and I am creating a simple application that leverages the Salesforce Ajax Toolkit connections library.
I am trying to write a handler to catch when a token has expired any time a method from the connections library is called. I have created a service that essentially wraps the connections library to use observables. For example if we look at the insert function I have created my own wrapper function:
public insert(object: sforce.SObject): Observable<any> {
return new Observable(observer => {
// successfully inserted the record
let insertSuccess = (result) => {
observer.next(result);
observer.complete();
}
// An error occured inserting the record
let insertError = (result) => {
// This does not work yet
if (result.faultcode.indexOf('INVALID_SESSION_ID') != -1) {
this.refreshToken();
}
else {
observer.error(result);
}
}
let callback = { onSuccess: insertSuccess, onFailure: insertError };
sforce.connection.create([object], callback);
});
}
I have another function that refreshes the access token:
public refreshToken(): void {
this.loginService.login().subscribe(
response => {
Globals.SESSION_TOKEN = response.access_token;
//initialize the salesforce connection
this.init(Globals.SESSION_TOKEN, this.loginService.AuthParams.SOAP_URL);
},
error => {
}
);
}
I essentially want the original insert function to wait for refreshToken to complete. If it is successful I want to retry the same insert again, otherwise I want the original insert observable to call observer.error.
I've looked into retry and retryWhen, however I haven't been able to figure out how to implement it to wait for the refreshToken() function to complete. Any guidance or advice on this matter would be greatly appreciated. Thank you in advance.
The catch operator accepts a function which processes an error and the source Observable. This means that if you catch an error you can determine whether you want to resubscribe to the original source in the catch block:
public insert(object: sforce.SObject): Observable<any> {
return new Observable(observer => {
// successfully inserted the record
let insertSuccess = (result) => {
observer.next(result);
observer.complete();
}
// An error occured inserting the record
let insertError = (result) => observer.error(result);
let callback = { onSuccess: insertSuccess, onFailure: insertError };
sforce.connection.create([object], callback);
}).catch((err, source) => {
if (err.faultcode.indexOf('INVALID_SESSION_ID') != -1) {
//This waits for the refresh to complete and then resubscribes
//to the source
//If the refresh errors then it will skip the resubscribe
return this.refreshToken().flatMapTo(source);
}
//Non-authentication error
return Observable.throw(err);
});
}
Then make your refreshToken function into something like so:
public refreshToken(): Observable<any> {
return this.loginService.login()
.tap(response => {
Globals.SESSION_TOKEN = response.access_token;
//initialize the salesforce connection
this.init(Globals.SESSION_TOKEN, this.loginService.AuthParams.SOAP_URL);
});
}

Resources