I am trying to send notifications to an Android app's users with firebase cloud messaging. I am using cloud firestore triggers but when trying to access a user node's properties, they are undefined.
Here is my index.js :
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.notifyNewMessage = functions.firestore
.document('conversations/{conversation}/messages/{message}')
.onCreate((docSnapshot, context) => {
const message = docSnapshot.data();
const recipientId = message['recipientId'];
const senderName = message['senderName'];
return admin.firestore().collection("users").doc(recipientId).get().then(userDoc => {
const registrationTokens = userDoc.registrationTokens;
console.log("registrationTokens = "+ registrationTokens);
const notificationBody = message['message'];
const payload = {
notification : {
title : senderName + " sent you a message,",
body: notificationBody,
clickAction: "ConversationActivity"
},
data : {
contactName : senderName,
userId : message['senderId']
}
}
return admin.messaging().sendToDevice(registrationTokens, payload).then(response => {
const stillRegisteredTokens = registrationTokens;
response.results.forEach((result, index) => {
const error = result.error;
if (error){
const failedRegistrationToken = stillRegisteredTokens['index'];
if (error.code === 'messaging/invalid-registration-token'
|| error.code === 'messaging/registration-token-not-registered') {
const failedIndex = stillRegisteredTokens.indexOf(failedRegistrationToken)
if (failedIndex > -1) {
stillRegisteredTokens.splice(failedIndex, 1);
}
}
}
})
return admin.firestore().doc("users/" + recipientId).update({
registrationTokens: stillRegisteredTokens
})
})
})
})
Because of that I get an error "sendDevice() argument must be non-empty array or non null string"
UPDATE
registrationTokens were undefined because I called userDoc instead of userDoc.data()
Now registrationTokens is not null nor empty but I still get the error :
Registration token(s) provided to sendToDevice() must be a non-empty string or a non-empty array.
Related
I want to send email to user on update of status field of user to value (true) using firestore and cloud functions.
My case :
I have a users(collections) which has more documents of userId(documents) and each document contains field and value.
One of the field is status: true | false (boolean).
I want to send email to that user if status of that user change to true using cloud functions and sendgrid api.
users
- dOpjjsjssdsk2121j131
- id : dOpjjsjssdsk2121j131
- status : false
- pspjjsjssdsdsk2121j131
- id : pspjjsjssdsdsk2121j131
- status : false
- yspjjsjssdsdsk2121j131
- id : yspjjsjssdsdsk2121j131
- status : false
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const SENDGRID_API_KEY = functions.config().sendgrid.key;
const sgMail = require('#sendgrid/mail');
sgMail.setApiKey(SENDGRID_API_KEY);
exports.sendEmail = functions.firestore.document('users/{userId}')
.onUpdate((change, context) => {
const after = change.after.val();
if(after.status === 'VERIFIED'){
console.log('profile verified')
const db = admin.firestore();
return db.collection('users').doc(userId)
.get()
.then(doc => {
const user = doc.data();
const msg = {
to: 'email',
from: 'email',
templateId: 'template id',
dynamic_template_data: {
subject: 'Profile verified',
name: 'name',
},
};
return sgMail.send(msg)
})
.then(() => console.log('email sent!') )
.catch(err => console.log(err) )
}
});
You are getting your error because in Firestore where is no val().
Try to change your change.after.val() to change.after.data(), so your code looks like this:
exports.sendEmail = functions.firestore.document('users/{userId}')
.onUpdate((change, context) => {
const after = change.after.data();
if(after.status === 'VERIFIED'){
console.log('profile verified')
const db = admin.firestore();
return db.collection('users').doc(context.params.userId) // get userId
.get()
.then(doc => {
const user = doc.data();
const msg = {
to: 'email',
from: 'email',
templateId: 'template id',
dynamic_template_data: {
subject: 'Profile verified',
name: 'name',
},
};
return sgMail.send(msg)
})
.then(() => console.log('email sent!') )
.catch(err => console.log(err) )
}
});
you can compare the target field by before and after data, if your target field is not the same. It means the field value has changed.
const beforedata=change.before.data()
const afterdata=change.after.data()
if(afterdata.status == beforedata.status){
// Status not changed
}else{
//status changed
}
All correct for the above answer except "context.params." is missing from (staffId)
Currently:
return db.collection('users').doc(userId) // get userId
Should Be:
> return db.collection('users').doc(context.params.userId) // get userId
This will also answer the above comment:
how to get the userId before return.db.collection('users').doc(userId) . ? – Fortray Sep 6 '19 at 9:13
I have this code that works correctly but I want to add the 'mensaje' in the body of the notification, the problem is that I do not know how to get it to be able to send it.
This is the structure of my data in firebase:
enter image description here
And this is the function:
const functions = require('firebase-functions');
let admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendPush = functions.database.ref('/chats/{chat}/{mensaje}/').onWrite((snapshot, context) => {
nombreChat = context.params.chat;
return cargarUsuarios(nombreChat).then(usuarios => {
let tokens = [];
for (let user of usuarios){
if(user.token !== undefined){
console.log('User', "Usuario: " + user.nombre);
console.log('User token', "Token: " + user.token);
tokens.push(user.token);
}
}
let payload = {
notification:{
title:'Trado GO',
body: 'Has recibido un nuevo mensaje',
sound: 'default',
badge: '1'
}
};
return admin.messaging().sendToDevice(tokens, payload);
});
});
function cargarUsuarios(chat){
var arrayParticipantesChat = chat.split(',');
let dbRef = admin.database().ref('/usuarios');
let defer = new Promise((resolve, reject) => {
dbRef.once('value', (snap) => {
let data = snap.val();
let usuarios = [];
for (var property in data){
usu=data[property];
if(arrayParticipantesChat.includes(usu['nombre'])){
usuarios.push(data[property]);
}
}
resolve(usuarios);
}, (err) => {
reject(err);
});
});
return defer;
}
Just trying to impletment Stripe Payment into my Android App.
The trouble i have is that my cloud function is triggered twice when i enter a credit card in my app. the first trigger returns an "error" status and the second trigger returns an "ok" status
Here is the code i use to save the Stripe token to my firebase realtime database:
if (cardToSave != null) {
stripe.createToken(
cardToSave,
object:TokenCallback {
override fun onSuccess(token: Token?) {
val currentUser = FirebaseAuth.getInstance().currentUser?.uid
val database = FirebaseDatabase.getInstance()
val pushId = database.getReference("stripe_customers/$currentUser/sources/").push().key
val ref = database.getReference("stripe_customers/$currentUser/sources/$pushId/token/")
//save the token id from the "token" object we received from Stripe
ref.setValue(token?.id)
.addOnSuccessListener {
Log.d(TAG, "Added Stripe Token to database successfully")
}
.addOnFailureListener {
Log.d(TAG, "Failed to add Token to database")
}
}
...
Here is the cloud function i copied straight from Stripe's example in their github repo:
// Add a payment source (card) for a user by writing a stripe payment source token to Realtime database
exports.addPaymentSource = functions.database
.ref('/stripe_customers/{userId}/sources/{pushId}/token').onWrite((change, context) => {
const source = change.after.val();
if (source === null){
return null;
}
return admin.database().ref(`/stripe_customers/${context.params.userId}/customer_id`)
.once('value').then((snapshot) => {
return snapshot.val();
}).then((customer) => {
return stripe.customers.createSource(customer, {source});
}).then((response) => {
return change.after.ref.parent.set(response);
}, (error) => {
return change.after.ref.parent.child('error').set(userFacingMessage(error));
}).then(() => {
return reportError(error, {user: context.params.userId});
});
});
Any help would be appreciated!
EDIT:
index.js
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const logging = require('#google-cloud/logging');
//functions.config() is firebase's environment variables
const stripe = require('stripe')(functions.config().stripe.token);
const currency = functions.config().stripe.currency || 'USD';
// [START chargecustomer]
// Charge the Stripe customer whenever an amount is written to the Realtime database
exports.createStripeCharge = functions.database.ref('/stripe_customers/{userId}/charges/{id}')
.onCreate((snap, context) => {
const val = snap.val();
// Look up the Stripe customer id written in createStripeCustomer
return admin.database().ref(`/stripe_customers/${context.params.userId}/customer_id`)
.once('value').then((snapshot) => {
return snapshot.val();
}).then((customer) => {
// Create a charge using the pushId as the idempotency key
// protecting against double charges
const amount = val.amount;
const idempotencyKey = context.params.id;
const charge = {amount, currency, customer};
if (val.source !== null) {
charge.source = val.source;
}
return stripe.charges.create(charge, {idempotency_key: idempotencyKey});
}).then((response) => {
// If the result is successful, write it back to the database
return snap.ref.set(response);
}).catch((error) => {
// We want to capture errors and render them in a user-friendly way, while
// still logging an exception with StackDriver
return snap.ref.child('error').set(userFacingMessage(error));
}).then(() => {
return reportError(error, {user: context.params.userId});
});
});
// [END chargecustomer]]
// When a user is created, register them with Stripe
exports.createStripeCustomer = functions.auth.user().onCreate((user) => {
return stripe.customers.create({
email: user.email,
}).then((customer) => {
return admin.database().ref(`/stripe_customers/${user.uid}/customer_id`).set(customer.id);
});
});
// Add a payment source (card) for a user by writing a stripe payment source token to Realtime database
exports.addPaymentSource = functions.database
.ref('/stripe_customers/{userId}/sources/{pushId}/token').onWrite((change, context) => {
const source = change.after.val();
if (source === null){
return null;
}
return admin.database().ref(`/stripe_customers/${context.params.userId}/customer_id`)
.once('value').then((snapshot) => {
return snapshot.val();
}).then((customer) => {
return stripe.customers.createSource(customer, {source:source});
}).then((response) => {
return change.after.ref.parent.set(response);
}, (error) => {
return change.after.ref.parent.child('error').set(userFacingMessage(error));
}).then(() => {
return reportError(error, {user: context.params.userId});
});
});
// When a user deletes their account, clean up after them
exports.cleanupUser = functions.auth.user().onDelete((user) => {
return admin.database().ref(`/stripe_customers/${user.uid}`).once('value').then(
(snapshot) => {
return snapshot.val();
}).then((customer) => {
return stripe.customers.del(customer.customer_id);
}).then(() => {
return admin.database().ref(`/stripe_customers/${user.uid}`).remove();
});
});
// To keep on top of errors, we should raise a verbose error report with Stackdriver rather
// than simply relying on console.error. This will calculate users affected + send you email
// alerts, if you've opted into receiving them.
// [START reporterror]
function reportError(err, context = {}) {
// This is the name of the StackDriver log stream that will receive the log
// entry. This name can be any valid log stream name, but must contain "err"
// in order for the error to be picked up by StackDriver Error Reporting.
const logName = 'errors';
const log = logging.log(logName);
// https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/MonitoredResource
const metadata = {
resource: {
type: 'cloud_function',
labels: {function_name: process.env.FUNCTION_NAME},
},
};
// https://cloud.google.com/error-reporting/reference/rest/v1beta1/ErrorEvent
const errorEvent = {
message: err.stack,
serviceContext: {
service: process.env.FUNCTION_NAME,
resourceType: 'cloud_function',
},
context: context,
};
// Write the error log entry
return new Promise((resolve, reject) => {
log.write(log.entry(metadata, errorEvent), (error) => {
if (error) {
return reject(error);
}
return resolve();
});
});
}
// [END reporterror]
// Sanitize the error message for the user
function userFacingMessage(error) {
return error.type ? error.message : 'An error occurred, developers have been alerted';
}
I've been thinking of using Google API directly to reward users (update user data on firestore) after verifying purchase through:
GET https://www.googleapis.com/androidpublisher/v3/applications/packageName/purchases/products/productId/tokens/token
But the authorization step is somewhat tricky. How do I achieve it with this Cloud Function I have got from one of you:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {google} = require("googleapis");
const publisher = google.androidpublisher('v2');
const authClient = new google.auth.JWT({
email: 'Service Account Email',
key: 'BEGIN PRIVATE KEY*************END PRIVATE KEY',
scopes: ['https://www.googleapis.com/auth/androidpublisher']
});
admin.initializeApp();
exports.validatePurchases = functions.database
.ref('/purchases/{uId}/{orderId}')
.onCreate((event, context) => {
const purchase = event.val();
if (purchase.is_processed === true) {
console.log('Purchase already processed!, exiting');
return null;
}
const orderId = context.params.orderId;
const dbRoot = event.ref.root;
const package_name = purchase.package_name;
const sku = purchase.sku;
const my_token = purchase.token;
authClient.authorize((err, result) => {
if (err) {
console.log(err);
}
publisher.purchases.products.get({
auth: authClient,
packageName: package_name,
productId: sku,
token: my_token
}, (err, response) => {
if (err) {
console.log(err);
}
// Result Status must be equals to 200 so that the purchase is valid
if (response.status === 200) {
return event.ref.child('is_validated').set(true);
} else {
return event.ref.child('is_validated').set(false);
}
});
});
return null;
});
Please spot errors and identify if it'll work. How do I authorize and deploy the function?
Thanks
I need help, I've been searching for solutions all day but I can't fix my issue, the code below won't read the device tokens.
Below contains my db structure. I manage to receive the log: 'We have a new News for you.' When I added a new post but I received the log "There are no notification tokens to send to." Which means it cannot detect the device tokens even though there is already ones. What am I doing wrong?
{
"Newsv2" : {
"All" : {
"-Ktr7ZkuChCjsUIMb_4f" : {
"title" : "",
"type" : "",
}
},
"Usersv2" : {
"h0RzzpdO7nZVLpAR4fi7xRWUqsT2" : {
"device_token" : "",
"name" : "",
"user_no" : ""
}
},
}
/--News
--All
--name
--desc
/--Usersv2
--{userID}
--device_token
exports.sendNotif = functions.database.ref('/Newsv2/All/{newsID}').onWrite(event => {
const newsID = event.params.newsID;
const userID = event.params.userID;
if (!event.data.val()) {
return console.log('News! ', newsID);
}
console.log('We have a new News for you!',newsID);
// Get the list of device notification tokens.
const getDeviceTokensPromise = admin.database().ref(`/Usersv2/${userid}/device_token`).once('value');
return Promise.all([getDeviceTokensPromise]).then(results => {
const tokensSnapshot = results[0];
//const follower = results[1];
// Check if there are any device tokens.
if (!tokensSnapshot.hasChildren()) {
return console.log('There are no notification tokens to send to.');
}
console.log('There are', tokensSnapshot.numChildren(), 'tokens to send notifications to.');
//console.log('Fetched follower profile', follower);
// Notification details.
const payload = {
notification: {
title: 'Test Message',
body: '',
icon: ''
}
};
// Listing all tokens.
const tokens = Object.keys(tokensSnapshot.val());
// Send notifications to all tokens.
return admin.messaging().sendToDevice(tokens, payload).then(response => {
// For each message check if there was an error.
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('Failure sending notification to', tokens[index], error);
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove());
}
}
});
return Promise.all(tokensToRemove);
});
});
});
To get the device token I store it in my firebase DB when a user registers or logs in.
private DatabaseReference mUserDatabase;
mUserDatabase = FirebaseDatabase.getInstance().getReference().child("Users/");
//and if the login/register is successful
mUserDatabase.child("device_token").setValue(deviceToken).addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Intent intent = new Intent(application.getApplicationContext(), MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |Intent.FLAG_ACTIVITY_NEW_TASK);
application.startActivity(intent);
}
});
as for my firebase funciton:
const deviceToken = admin.database().ref(`/Users/${unique_id}/device_token`).once('value');