push event not triggered in service worker - push-notification

Following this tutorial until "Handle push event" section to setup a desktop notification system in my application, I face a problem:
When I click "push" to push a notification artificially with Chrome, no notification appear. No message in the console.
I allowed the notification from the website and the service-worker is well installed in my browser.
My service worker looks like this:
self.addEventListener('push', function (event) {
console.log('[Service Worker] Push Received.')
console.log(`[Service Worker] Push had this data: "${event.data.text()}"`)
const title = 'My App Name'
const options = {
body: event.data.text(),
icon: 'pwa/icon.png',
badge: 'pwa/badge.png'
}
const notificationPromise = self.registration.showNotification(title, options)
event.waitUntil(notificationPromise)
})
and my service worker registration (using register-service-worker npm package) looks like this:
import { register } from 'register-service-worker'
const applicationServerPublicKey = 'BI5qCj0NdNvjDcBYTIXiNccdcP74Egtb3WxuaXrHIVCLdM-MwqPkLplHozlMsM3ioINQ6S_HAexCM0UqKMvaYmg'
function urlB64ToUint8Array (base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/')
const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}
async function manageNotificationSubscription (registration) {
const subscription = await registration.pushManager.getSubscription()
let isSubscribed: boolean = !(subscription === null)
if (isSubscribed) {
console.log('User IS subscribed.')
} else {
console.log('User is NOT subscribed.')
const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey)
try {
await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey
})
console.log('User just subscribed.')
} catch (e) {
console.error('Failed to subscribe the user: ', e)
}
}
}
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.'
)
},
async registered (registration) {
console.log('Service worker has been registered.')
await manageNotificationSubscription(registration)
},
cached () {
console.log('Content has been cached for offline use.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}
It looks like the push event in the service-worker is not even triggered...
Did I do something wrong?

Related

Teams Tab App not working on mobile client

I have a teams app which serves as another front end for our application (I will call it App 1) and we just use the client id for App 1 to authenticate. This works fine on Teams desktop and web, but on teams mobile (Android) I can successfully obtain a bearer token, but the rest of the api calls return status cancelled.
My login work flows look like so:
LoginForm.js
const { teamsUserCredential } = useContext(TeamsFxContext);
const { loading, error, data, reload } = useGraphWithCredential(
async (graph, teamsUserCredential, scope) => {
const provider = new TeamsFxProvider(teamsUserCredential, scope);
Providers.globalProvider = provider;
Providers.globalProvider.setState(ProviderState.SignedIn);
if (teamsUserCredential.ssoToken === null) {
setTeamsLoginError(true); // flag to switch to web auth flow
}
if (error) {
setTeamsLoginError(true);
}
},
{ scope: ["User.Read"], credential: teamsUserCredential }
);
// for web auth flow
const onClick = async () = {
await login(instance)
}
App.js
const { theme, themeString, teamsUserCredential, error, loading } =
useTeamsUserCredential({
initiateLoginEndpoint:
"https://{teams app site name}.web.core.windows.net/auth-start.html",
clientId: "{client id for App 1}", // not the teams app client id
});
const {
instance,
accounts,
} = useMsal();
// SSO auth flow
if (!teamsLoginError) {
let newAccessToken;
try {
newAccessToken = await teamsUserCredential.getToken(
"api://{client id for App 1}/access_as_user"
);
} catch (error) {
console.log("error getting token");
}
const token = `Bearer ${newAccessToken.token}`;
}
// auth flow for web clients
else {
const accessTokenRequest = {
scopes: ["api://{client id for App 1}/access_as_user"],
account: accounts[0],
};
instance
.acquireTokenSilent(accessTokenRequest)
.then(async (accessTokenResponse) => {
let newAccessToken = accessTokenResponse.accessToken;
const token = `Bearer ${newAccessToken}`;
}

FCM very slow and unreliable when sending to a group of recipients through Cloud Function

I have the following Function that:
Listens for document (text message) creation
Grab IDs of members of a group chat
Get the FCM Tokens for each member
With a for-loop, send messages to group members
exports.sendChatMessage = functions.firestore
.document("chats/{mealID}/messages/{messageID}")
.onCreate((snap, context) => {
const data = snap.data();
const mealID = context.params.mealID;
const senderID = data.senderID;
const senderName = data.senderName;
const messageContent = data.content;
var docRef = db.collection("chats").doc(mealID);
docRef
.get()
.then((doc) => {
if (doc.exists) {
const docData = doc.data();
const mealName = docData.name;
const userStatus = docData.userStatus;
var users = docData.to;
var eligibleUsers = users.filter(
(user) => userStatus[user] == "accepted"
);
eligibleUsers.push(docData.from);
// get fcmTokens from eligibleUsers and send the messagme
db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {
var fcmTokens = [];
var thumbnailPicURL = "";
// get thumbnailpic of the sender and collect fcmTokens
snapshot.forEach((doc) => {
if (doc.data().uid == senderID) {
thumbnailPicURL =
doc.data().thumbnailPicURL == null
? "https://i.imgur.com/8wSudUk.png"
: doc.data().thumbnailPicURL;
} else {
fcmTokens.push(doc.data().fcmToken);
}
});
// send the message fcmTokens
fcmTokens.forEach((token) => {
if (token != "") {
const fcmMessage = {
message: {
token: token,
notification: {
title: mealName,
body: senderName + ": " + messageContent,
image: thumbnailPicURL,
},
apns: {
payload: {
aps: {
category: "MESSAGE_RECEIVED",
},
MEAL_ID: mealID,
},
},
},
};
tokenManger.sendFcmMessage(fcmMessage);
}
});
return true;
});
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
return false;
}
})
.catch((error) => {
console.log("Error getting document:", error);
return false;
});
return true;
});
My send function comes from a helper file that uses the HTTP V1 protocol to build the send-request:
const { google } = require("googleapis");
const https = require("https");
const MESSAGING_SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
const SCOPES = [MESSAGING_SCOPE];
const PROJECT_ID = MY_PROJECT_ID;
const HOST = "fcm.googleapis.com";
const PATH = "/v1/projects/" + PROJECT_ID + "/messages:send";
exports.getAccessToken = () => {
return new Promise(function (resolve, reject) {
const key = require("./service-account.json");
var jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
SCOPES,
null
);
jwtClient.authorize(function (err, tokens) {
if (err) {
reject(err);
return;
}
resolve(tokens.access_token);
});
});
};
//send message
exports.sendFcmMessage = (fcmMessage) => {
this.getAccessToken().then(function (accessToken) {
var options = {
hostname: HOST,
path: PATH,
method: "POST",
headers: {
Authorization: "Bearer " + accessToken,
},
// … plus the body of your notification or data message
};
var request = https.request(options, function (resp) {
resp.setEncoding("utf8");
resp.on("data", function (data) {
console.log("Message sent to Firebase for delivery, response:");
console.log(data);
});
});
request.on("error", function (err) {
console.log("Unable to send message to Firebase");
console.log(err);
});
request.write(JSON.stringify(fcmMessage));
request.end();
});
};
It worked all fine in the emulator but once deployed, there're significant delays (~3 mins):
I also noticed that the console says the cloud function finishes execution BEFORE sendFcmMessage logs success messages.
I did some research online, it appears that it might have something to do with the usage of Promise but I wasn't sure if that's the sole reason or it has something to do with my for-loop.
The Problem
To summarize the issue, you are creating "floating promises" or starting other asynchronous tasks (like in sendFcmMessage) where you aren't returning a promise because they use callbacks instead.
In a deployed function, as soon as the function returns its result or the Promise chain resolves, all further actions should be treated as if they will never be executed as documented here. An "inactive" function might be terminated at any time, is severely throttled and any network calls you make (like setting data in database or calling out to FCM) may never be executed.
An indicator that you haven't properly chained the promises is when you see the function completion log message ("Function execution took...") before other messages you are logging. When you see this, you need to look at the code you are running and confirm whether you have any "floating promises" or are using callback-based APIs. Once you have changed the callback-based APIs to use promises and then made sure they are all chained together properly, you should see a significant boost in performance.
The fixes
Sending the message data to FCM
In your tokenManger file, getAccessToken() could be reworked slightly and sendFcmMessage should be converted to return a Promise:
exports.getAccessToken = () => {
return new Promise(function (resolve, reject) {
const key = require("./service-account.json");
const jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
SCOPES,
null
);
jwtClient.authorize(
(err, tokens) => err ? reject(err) : resolve(tokens.access_token)
);
});
};
//send message
exports.sendFcmMessage = (fcmMessage) => {
// CHANGED: return the Promise
return this.getAccessToken().then(function (accessToken) {
const options = {
hostname: HOST,
path: PATH,
method: "POST",
headers: {
Authorization: "Bearer " + accessToken,
},
// … plus the body of your notification or data message
};
// CHANGED: convert to Promise:
return new Promise((resolve, reject) => {
const request = https.request(options, (resp) => {
resp.setEncoding("utf8");
resp.on("data", resolve);
resp.on("error", reject);
});
request.on("error", reject);
request.write(JSON.stringify(fcmMessage));
request.end();
});
});
};
However, the above code was built for googleapis ^52.1.0 and google-auth-library ^6.0.3. The modern versions of these modules are v92.0.0 and v7.11.0 respectively. This means you should really update the code to use these later versions like so:
// Import JWT module directly
const { JWT } = require('google-auth-library');
// FIREBASE_CONFIG is a JSON string available in Cloud Functions
const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG).projectId;
const FCM_ENDPOINT = `https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send`;
const FCM_SCOPES = ["https://www.googleapis.com/auth/firebase.messaging"];
exports.sendFcmMessage = (fcmMessage) => {
const key = require("./service-account.json"); // consider moving outside of function (so it throws an error during deployment if its missing)
const client = new JWT({
email: key.client_email,
key: key.private_key,
scopes: FCM_SCOPES
});
return client.request({ // <-- this uses `gaxios`, Google's fork of `axios` built for Promise-based APIs
url: FCM_ENDPOINT,
method: "POST",
data: fcmMessage
});
}
Better yet, just use the messaging APIs provided by the Firebase Admin SDKs that handle the details for you. Just feed it the message and tokens as needed.
import { initializeApp } from "firebase-admin/app";
import { getMessaging } from "firebase-admin/messaging";
initializeApp(); // initializes using default credentials provided by Cloud Functions
const fcm = getMessaging();
fcm.send(message) // send to one (uses the given token)
fcm.sendAll(messagesArr) // send to many at once (each message uses the given token)
fcm.sendMulticast(message) // send to many at once (uses a `tokens` array instead of `token`)
The Cloud Function
Updating the main Cloud Function, you'd get:
exports.sendChatMessage = functions.firestore
.document("chats/{mealID}/messages/{messageID}")
.onCreate((snap, context) => {
const mealID = context.params.mealID;
const { senderID, senderName, content: messageContent } = snap.data();
const docRef = db.collection("chats").doc(mealID);
/* --> */ return docRef
.get()
.then((doc) => {
if (!doc.exists) { // CHANGED: Fail fast and avoid else statements
console.log(`Could not find "chat:${mealID}"!`);
return false;
}
const { userStatus, to: users, name: mealName, from: fromUser } = doc.data();
const eligibleUsers = users.filter(
(user) => userStatus[user] == "accepted"
);
eligibleUsers.push(fromUser);
// get fcmTokens from eligibleUsers and send the message
/* --> */ return db.collection("users")
.where("uid", "in", eligibleUsers) // WARNING: This will only work for up to 10 users! You'll need to break it up into chunks of 10 if there are more.
.get()
.then(async (snapshot) => {
const fcmTokens = [];
let thumbnailPicURL = "";
// get thumbnailpic of the sender and collect fcmTokens
snapshot.forEach((doc) => {
if (doc.get("uid") == senderID) {
thumbnailPicURL = doc.get("thumbnailPicURL"); // update with given thumbnail pic
} else {
fcmTokens.push(doc.get("fcmToken"));
}
});
const baseMessage = {
notification: {
title: mealName,
body: senderName + ": " + messageContent,
image: thumbnailPicURL || "https://i.imgur.com/8wSudUk.png", // CHANGED: specified fallback image here
},
apns: {
payload: {
aps: {
category: "MESSAGE_RECEIVED",
},
MEAL_ID: mealID,
},
}
}
// log error if fcmTokens empty?
// ----- OPTION 1 -----
// send the message to each fcmToken
const messagePromises = fcmTokens.map((token) => {
if (!token) // handle "" and undefined
return; // skip
/* --> */ return tokenManger
.sendFcmMessage({
message: { ...baseMessage, token }
})
.catch((err) => { // catch the error here, so as many notifications are sent out as possible
console.error(`Failed to send message to "fcm:${token}"`, err);
})
});
await Promise.all(messagePromises); // wait for all messages to be sent out
// --------------------
// ----- OPTION 2 -----
// send the message to each fcmToken
await getMessaging().sendAll(
fcmTokens.map((token) => ({ ...baseMessage, token }))
);
// --------------------
return true;
})
.catch((error) => {
console.log("Error sending messages:", error);
return false;
});
})
.catch((error) => {
console.log("Error getting document:", error);
return false;
});
});
I found out that the culprit is my queries to db. Like #samthecodingman commented, I was creating floating Promises.
Originally, I have codes like:
db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {...}
All I needed to do is to return that call:
return db.collection("users")
.where("uid", "in", eligibleUsers)
.get()
.then((snapshot) => {...}
Although it's still not instant delivery, it's much faster now.

RNFirebase v6 Push Notifications are not coming both iOS&Android

I am trying to send notifications from firebase console to my react-native app
I followed the poor documentation here as much as I understand: https://invertase.io/oss/react-native-firebase/v6/messaging/quick-start
I installed #react-native-firebase/app and /messaging and here is my code in component:
componentDidMount() {
this.reqNotifications()
this.checkNotificationPermission()
}
reqNotifications() {
requestNotifications(['alert', 'badge', 'sound']).then(({status, settings}) => {
console.log('NOTIFICATION STATUS' + status)
});
}
async checkNotificationPermission() {
const enabled = await messaging().hasPermission();
if (enabled) {
console.log('APPROVED');
await messaging().registerForRemoteNotifications()
messaging().getToken().then(token => console.log('token: >> ' + token))
} else {
console.log('NOT APPROVED');
}
}
I am requesting permission via react-native-permissions and permission request is
working.
My Apple APNs are OK on Apple and Firebase console
And I am getting my token by getToken() method on the code
succesfully.
But I cant send anything to device from firebase; nothing happening on neither foreground nor background . I tried with-token test and also tried normal but no, nothing happens.
I added this code to componentDidMount:
messaging().onMessage(async remoteMessage => {
console.log('FCM Message Data:', remoteMessage.data);
});
As I understand this subscribes for cloud messages and when I send some cloud message notification from firebase-console, I should get console output; but nothing happens.
I dont know what am I missing but I think there is a big update on this package and most of docs are for previous version and I really stuck here thanks for assist
for rnfirebase.io V6
componentDidMount = async () => {
this.checkNotificationPermission();
await messaging().requestPermission({provisional: true});
await messaging().registerDeviceForRemoteMessages();
await this.getFCMToken();
if (Platform.OS === 'android') {
this.createAndroidNotificationChannel();
}
this.backgroundState();
this.foregroundState();
};
checkNotificationPermission = () => {
firebase
.messaging()
.hasPermission()
.then(enabled => {
if (!enabled) {
this.promptForNotificationPermission();
}
});
};
promptForNotificationPermission = () => {
firebase
.messaging()
.requestPermission({provisional: true})
.then(() => {
console.log('Permission granted.');
})
.catch(() => {
console.log('Permission rejected.');
});
};
createAndroidNotificationChannel() {
const channel = new firebase.notifications.Android.Channel(
'channelId',
'Push Notification',
firebase.notifications.Android.Importance.Max,
).setDescription('Turn on to receive push notification');
firebase.notifications().android.createChannel(channel);
}
foregroundState = () => {
const unsubscribe = messaging().onMessage(async notification => {
console.log('Message handled in the foregroundState!', notification);
});
return unsubscribe;
};
// Register background handler
backgroundState = () => {
messaging().setBackgroundMessageHandler(async notification => {
console.log('Message handled in the background!', notification);
});
};

Expo push notifications stopped working in production

I'm using Expo to develop both Android and iOS at same time. Notifications were working fine for several weeks, and then out of no where stopped working in production, even though I did not update the app during this time.
Server-side, everything is still fine, and notifications are being pushed. In dev, notifications are still being received and handled properly, but in production, it's crickets.
Has anyone else experienced this / what could be causing this?
Here is my code:
class Dashboard extends Component {
constructor(props) {
super(props);
this.state = {
notificationsSet: false,
}
}
componentDidMount() {
this.registerForPushNotificationsAsync(this.props.currentUser.currentUser.id, this.props.currentUser.authToken)
savePushToken = (userId, pushToken, token) => {
//API call to save push token to database
apiHelper
.savePushToken(userId, pushToken, token)
.then(res => {
return
})
.catch(err => console.log("err saving", err));
};
handleNotification = notification => {
this.props.setNotification({ notification })
}
registerForPushNotificationsAsync = async (userId, token) =>{
//requesting if user would like to turn on notifications
const { status: existingStatus } = await Permissions.getAsync(
Permissions.NOTIFICATIONS
);
//this checks if notifications is turned on for the app --- "granted"
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
finalStatus = status;
}
if (finalStatus !== "granted") {
return;
} //if "granted" then get push notifications and calls this.savepushtoken to save into the API
let pushToken = await Notifications.getExpoPushTokenAsync();
this.subscription = Notifications.addListener(this.handleNotification);
this.savePushToken(userId, pushToken, token);
};
render() {
return(...)
}
}

Duplicates in offline Meteor/React Native app

I usually find a solution to a problem with Google/Stackoverflow/Meteor forums or find someone to fix it. However, this time I’m completely stuck. Here is my issue:
I’ve created a field service management app with Meteor/React/React Native. Managers can create tasks/work orders on the web app, the field team can create reports/work logs on the mobile app.
However, the region I’m in has a poor internet connection in some areas. So the offline feature is essential for both viewing tasks/reports but also creating the reports (not the tasks). To solve the offline data access, I’ve followed Spencer Carli’s excellent tutorial https://hackernoon.com/offline-first-react-native-meteor-apps-2bee8e976ec7
It’s working very well.
Things go wrong for creating offline reports. Basically, I have created an action queue in redux store to handle offline reports creation. When connection is back online, actions are being mapped, reports are created on the server, actions are then deleted, and offline created reports are being deleted from mini mongo and redux because anyway, once created on the server, it’s auto synced again.
It’s working very well BUT sometimes, especially when internet connection is slow, duplicates are created. And like, 50+ duplicates of the same report sometimes.
Here is the action queue syncing:
async handleSync(props) {
const data = Meteor.getData();
const db = data && data.db;
if (props.loading === false && props.connectionStatus === 'connected' && Meteor.userId() && db && props.actionQueue && props.actionQueue.length > 0) {
for (let action of props.actionQueue) {
if (action.msg === 'createReport') {
const report = {
organizationId: action.doc.organizationId,
taskId: action.doc.taskId,
status: action.doc.status,
startedAt: action.doc.startedAt,
};
const result = await Meteor.call('Reports.create', report, (error, res) => {
if (error) {
Alert.alert(I18n.t('main.error'), `${I18n.t('main.errorSyncReport')} ${error.reason}`);
} else {
props.dispatch({ type: 'REMOVE_ACTION_QUEUE', payload: action.actionId });
props.dispatch({ type: 'REMOVE_OFFLINE_REPORT', payload: action.doc._id });
db['reports'].del(action.doc._id);
const task = {
organizationId: action.doc.organizationId,
taskId: action.doc.taskId,
};
Meteor.call('Tasks.updateTaskStatus', task);
}
});
return result;
}
else if (action.msg === 'completeReport') {
// this action is for completion of reports that have been created online
const report = {
organizationId: action.doc.organizationId,
reportId: action.doc._id,
comments: action.doc.comments,
isTaskCompleted: action.doc.isTaskCompleted,
completedAt: action.doc.completedAt,
fields: action.doc.fields,
};
const result = await Meteor.call('Reports.completeReport', report, (error, res) => {
if (error) {
Alert.alert(I18n.t('main.error'), `${I18n.t('main.errorSyncReport')} ${error.reason}`);
} else {
props.dispatch({ type: 'REMOVE_ACTION_QUEUE', payload: action.actionId });
const task = {
organizationId: action.doc.organizationId,
taskId: action.doc.taskId,
};
Meteor.call('Tasks.updateTaskStatus', task);
}
});
return result;
}
else if (action.msg === 'createCompletedReport') {
// this action is for completion of reports that have been created offline to avoid _id problems
// so a new completed report is created and the offline report is deleted
const report = {
organizationId: action.doc.organizationId,
taskId: action.doc.taskId,
comments: action.doc.comments,
isTaskCompleted: action.doc.isTaskCompleted,
fields: action.doc.fields,
status: action.doc.status,
startedAt: action.doc.startedAt,
completedAt: action.doc.completedAt,
};
const result = await Meteor.call('Reports.create', report, (error, res) => {
if (error) {
Alert.alert(I18n.t('main.error'), `${I18n.t('main.errorSyncReport')} ${error.reason}`);
} else {
props.dispatch({ type: 'REMOVE_ACTION_QUEUE', payload: action.actionId });
props.dispatch({ type: 'REMOVE_OFFLINE_REPORT', payload: action.doc._id });
db['reports'].del(action.doc._id);
const task = {
organizationId: action.doc.organizationId,
taskId: action.doc.taskId,
};
Meteor.call('Tasks.updateTaskStatus', task);
}
});
return result;
}
}
}
}
Here is the offline initialisation based on Spencer’s tutorial:
const onRehydration = (store) => {
const data = Meteor.getData();
const db = data && data.db;
if (db) {
_.each(store.getState(), (collectionData, collectionName) => {
if (collectionName !== 'offlineUser' && collectionName !== 'offlineOrg' && collectionName !== 'actionQueue' && collectionName !== 'clipboard') {
if (!db[collectionName]) {
db.addCollection(collectionName);
}
const collectionArr = _.map(collectionData, (doc, _id) => {
doc._id = _id;
return doc;
});
db[collectionName].upsert(collectionArr);
}
});
}
store.dispatch({type: 'CACHING', caching: false})
};
export const initializeOffline = (opts = {}) => {
let debug = false;
const logger = createLogger({ predicate: () => debug&&opts.log || false });
const store = createStore(reducers, applyMiddleware(logger), autoRehydrate());
persistStore(store, {
storage: AsyncStorage,
keyPrefix: 'offline:',
debounce: opts.debounce || 2000,
}, () => onRehydration(store));
store.dispatch({type: 'CACHING', caching: true})
Meteor.ddp.on('added', (payload) => {
store.dispatch({ type: 'DDP_ADDED', payload });
});
Meteor.ddp.on('changed', (payload) => {
store.dispatch({ type: 'DDP_CHANGED', payload });
});
Meteor.ddp.on('removed', (payload) => {
store.dispatch({ type: 'DDP_REMOVED', payload });
});
return store;
};
If someone has an idea of the problem or ever encountered a similar issue, I’d be grateful if you could share your solution :)

Resources