Flutter Cloud Messaging: how to send notification from the app (not from firebase console) - flutter

Is it possible to send the notification from within the app instead of a cloud function on firebase?
The reason is, I want to do something similar to: FCM Push Notifications for Flutter, where they have this function that will be deployed to firebase:
export const sendToTopic = functions.firestore
.document('puppies/{puppyId}')
.onCreate(async snapshot => {
const puppy = snapshot.data();
const payload: admin.messaging.MessagingPayload = {
notification: {
title: 'New Puppy!',
body: `${puppy.name} is ready for adoption`,
icon: 'your-icon-url',
click_action: 'FLUTTER_NOTIFICATION_CLICK' // required only for onResume or onLaunch callbacks
}
};
return fcm.sendToTopic('puppies', payload);
});
this method works as intended on firebase cloud functions, however I need the path
.document('puppies/{puppyId}')
to be dynamic depending on which chatroom a user is in, so he would get a notification everytime i new message is send, so the 'chatroom22' would be a variable:
.document('chatroom22/{roomId}')
So is it possible to do this in the app-code, or can this be done in the deployed function?
In response to Doug Stevensons answer
Okay, that makes sence, and works. However, now everybody get the notifications. I want only the people in a given chatroom to receive the notification. I've tried something like this, where the users device token is saved for a given chat-room, then I want to notiffy all those tokens:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().functions);
var newData;
exports.myTrigger = functions.firestore.document('messages/{messageId}/room/{roomId}/message/{messageId2}').onCreate(async (snapshot, context) => {
//
if (snapshot.empty) {
console.log('No Devices');
return;
}
newData = snapshot.data();
const deviceIdTokens = await admin
.firestore()
.collection('messages/{messageId}/room/{roomId}/tokens/{tokenId}')
.get();
var tokens = [];
for (var token of deviceIdTokens.docs) {
tokens.push(token.data().device_token);
}
var payload = {
notification: {
title: `${newData.sender}`,
body: `${newData.message}`,
sound: 'default',
},
data: {
push_key: 'Push Key Value',
key1: newData.message,
},
};
try {
const response = await admin.messaging().sendToDevice(tokens, payload);
console.log('Notification sent successfully');
} catch (err) {
console.log(err);
}
});
But it doesnt seem to work with wildcards.. how can I get the specific destination for each chatroom?

You can use a wildcard for the collection in the path:
functions.firestore.document('{collectionId}/{documentId}')
But this will trigger for all documents in all top-level collecitons, which is probably not what you want.
In fact, using variable names for top-level collections is actually not the preferred way to model data in Firestore. Consider instead having a top-level collection for all rooms, then use subcollections to contain their messages. If you do that, then you function becomes more more clearly defined as:
functions.firestore.document('rooms/{roomId}/messages/{messageId}')
Cloud Fuctions only allows wildcards for full path segments like this. There are no other patterns or regular expressions.

Related

Flutter Firebase Google Cloud functions error with .onCreate trigger to send notification to device with FCM token Type Error

I've been stuck a while now and would appreciate any help. I've never worked with Cloud Functions before and there may be a fairly easy solution here. I save the FCM token and other proper variables in my documents. The way my firestore database is organized is the following. Trips/{tripId}/proposedRides/{proposedRideId}/. The proposedRides subCollection creates a new document when a potential passenger requests to join a trip. This is where I want a notification sent to the driver via cloud functions and FCM.
I thought I could make a simple function like this. It is authenticated and setup properly to my knowledge. I added the .json credentials in an env variable and all that jazz. Here's what I've been trying:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.sendProposedRideNotification =
functions.region('southamerica-east1').firestore.document('proposedRides/{rideId}')
.onCreate(async(snapshot, context) => {
// Get the tripId and passengerName associated with the proposedRide
const tripData = snapshot.data();
if (!tripData.proposedByName || !tripData.proposedToDriverFCMToken) {
console.error('Required fields are missing');
return;
}
const passengerName = tripData.proposedByName;
const proposedToDriverFCMToken = tripData.proposedToDriverFCMToken;
// Send the notification to the driver's device
const payload = {
notification: {
title: 'New Proposed Ride',
body: `A new proposed ride has
been created from ${passengerName}!`
}
};
const options = {
priority: 'high',
timeToLive: 60 * 60 * 24
};
return admin.messaging().sendToDevice(proposedToDriverFCMToken, payload, options);
});
I'll then check out firebase functions:logs and get the error
Snapshot has no readTime. Using now()
and
TypeError: Cannot read properties of undefined (reading 'proposedByName')
This is strange because when I test the 'proposedByName' field is created in the document. I've tried changing the beginning of the function to
functions.region('southamerica-east1').firestore.document('trips/{tripId}/proposedRides/{rideId}')
.onCreate(async(snapshot, context) => {
but I had the same errors.

Amplify Datastore subscription cost

I am trying to understand the cost of Datastore. It seems that it subscribes to all Mutations. So if there are 50 users, then each message will be send 50 times, even if it not required.
As each real time mutation costs money, we will be paying unnecessary 49 times for this real time message mutation.
Also , it seems to me SyncExpression doesn't have any effect on this Subscription.
I am really stuck here. It will be great of someone can clarify
Amplify generates the datastore boilerplate code for you, but you still need to call it. You won't pay for every user and every mutation.
You will only subscribe to a mutation (explicitly call the code to listen for changes) on a per-user basis for things that user is interested in. e.g. if you are viewing a TODO item, you'd subscribe the user to that item and they'll immediately see if someone else modify it on another device.
UPDATE
Long story... I was triggering back-end computation via GraphQL by making a lambda resolver. The computation took too long and the GQL call would timeout. I updated the code so the GQL call called itself asynchronously (re-trigger the lambda), and returned immediately. Then when the long-running task completed in the spun-up lambda, I updated the a record in the database.
I update the record using AppSync instead of direct GQL so it would trigger mutations, and in the react client, I listen to a mutation for the specific record that will be updated. This way, there is just 1 user listening (if they've triggered the long running action) and that user is only notified about changes to the single DB record they're interested in, and not receiving other user's updates.
I don't know if all this is applicable to your situation. The code snippets below may help you, but they're somewhat out of context.
// In amplify/backend/api/projectname/schema.graphql
type Subscription {
onCouponWithIdUpdated(id: ID!): Coupon #aws_subscribe(mutations: ["updateCoupon"])
}
// In my useSendCoupon hook...
// Subscribe to coupon updates
useEffect(() => {
if (0 === couponId) {
return
}
console.log(`subscribe to coupon updates for couponId:`, couponId)
const onCouponWithIdUpdated = /* GraphQL */ `
subscription OnCouponWithIdUpdated($id: ID!) {
onCouponWithIdUpdated(id: $id) {
id
proofLink
owner
}
}
`
const subscription = API
.graphql(graphqlOperation(onCouponWithIdUpdated, { id: couponId }))
.subscribe({
next: ({ provider, value }) => {
const coupon = value.data.onCouponWithIdUpdated
//console.log(`Proof Link:`, coupon.proofLink)
setProofLinks([coupon.proofLink])
setSendCouponState(COUPON_STATE_PREVIEW_SUCCESS)
},
error: error => console.warn(error)
})
console.log('subscribed: ', subscription)
return () => {
console.log(`unsubscribe to coupon updates`)
subscription.unsubscribe()
}
}, [couponId])
// inside a lambda...
const updateCouponWithProof = async (authorization, couponId, proofLink) => {
const initializeClient = () => new AWSAppSyncClient({
url: process.env.API_XXXX_GRAPHQLAPIENDPOINTOUTPUT,
region: process.env.REGION,
auth: {
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
jwtToken: authorization
},
disableOffline: true,
})
const executeMutation = async (mutation, operationName, variables) => {
const client = initializeClient()
try {
const response = await client.mutate({
mutation: gql(mutation),
variables,
fetchPolicy: "no-cache",
})
return response.data[operationName]
} catch (err) {
console.log("Error while trying to mutate data", err)
throw JSON.stringify(err)
}
}
const updateCoupon = /* GraphQL */ `
mutation UpdateCoupon(
$input: UpdateCouponInput!
$condition: ModelCouponConditionInput
) {
updateCoupon(input: $input, condition: $condition) {
id
proofLink
owner
}
}
`
const variables = { input: { id: couponId, proofLink } }
try {
return await executeMutation(updateCoupon, 'updateCoupon', variables)
} catch (error) {
console.log(`executeMutation error`, error)
}
}

Can't access Firestore docs data after getting the doc object

I'm trying to fetch a single field value from a doc in a collection (stored in Firestore).
The following function is called (by the triggered function) to perform this query and return the result.
Firestore data structure:
After I fetch the query result into helper_token object - I cannot access the DATA (fields) in it.
I tried many things, including:
helper_token[0].device_token;
helper_token.data().device_token;
JSON.stringify(helper_token);
Nothing works for me.
The log always shows results like these:
helper_token = {}
helper_token = undefined
What am I missing? how can I get the device_token based on user?
const admin = require('firebase-admin'); //required to access the FB RT DB
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
function getHelperToken(helperId) {
//Get token from Firestore
const tokensRef = db.collection('tokens');
const helper_token = tokensRef.where('user', '==', 'TM1EOV4lYlgEIly0cnGHVmCnybT2').get();
if (helper_token.empty) {
functions.logger.log('helper_token EMPTY');
}
functions.logger.log('helper_token=' + JSON.stringify(helper_token));
return helper_token.device_token;
};
For completeness, adding here the full calling function to the above function:
//DB triggered function - upon writing a child in the HElpersInvitations reference
exports.sendHelperInvitation = functions.database.ref('/HelpersInvitations/{helper_invitation_id}')
.onCreate((snapshot, context) => {
const helperId = snapshot.val().helperId;
const title = snapshot.val().title;
const body = snapshot.val().body;
//Get the helper token by Id
functions.logger.log('HelperID=' + helperId);
functions.logger.log('getHelperToken=' + getHelperToken(helperId));
const helper_token2 = getHelperToken(helperId);
//Notification payload
const payload = {
notification: {
title: title,
body: body,
icon: 'default',
click_action: 'com.skillblaster.app.helperinvitationnotification'
}
}
// //Send the notification
functions.logger.log('helper_token [BEFORE sendToDevice]=' + helper_token2);
return admin.messaging().sendToDevice(helper_token2, payload);
});
You need to consider that the get() call is asynchornous and also that you get a list of documents and not a single doc. Can you try it with this code:
const admin = require("firebase-admin"); //required to access the FB RT DB
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
async function getHelperToken(helperId) {
//Get token from Firestore
const tokensRef = db.collection("tokens");
const helperTokens = await tokensRef
.where("user", "==", "TM1EOV4lYlgEIly0cnGHVmCnybT2")
.get();
let helper_token = "";
helperTokens.forEach((token) => {
helper_token = token.data();
});
functions.logger.log("helper_token=" + JSON.stringify(helper_token));
return helper_token.device_token;
}
As the get() call in Firestore is asynchronous you need to use an asynchronous function. You can go through this article to know more about why Firebase APIs are asynchronous. Next when we query with the where clause in Firestore we get a list of documents even if there is only one document in the list. So we have to run a for loop to get the document inside the list of documents. Now as you are returning the value from an asynchronous function the return value will be a promise in pending state. So to get the value from the promise we need to use the then() block while calling the function.
Also I think the parameter helperId you are using in the function definition is not used anywhere in the function. Though it will not make a difference I would suggest you remove it if it is not required in the function.
So consider using the following code -
const admin = require(‘firebase-admin’);
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
async function getHelperToken() {
//Get token from Firestore
const tokensRef = db.collection(‘tokens’);
const helper_token = await tokensRef.where(‘user’, ‘==’, ‘TM1EOV4lYlgEIly0cnGHVmCnybT2’).get();
let helper_token_needed;
helper_token.forEach((token) => {
helper_token_needed = token.data();
});
console.log(helper_token_needed.device_token);
return helper_token_needed.device_token;
}
//when calling to the function use then() block to get the value as a promise is returned from asynchronous function
getHelperToken().then((value)=>{console.log(value)});

Setting custom claims for Firebase auth from flutter

I'm using Firebase auth for an app, but as part of user creation I need to set some custom claims.
I've written a cloud function to set the claims when a user is created:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// On sign up.
exports.processSignUp = functions.auth.user().onCreate(user => {
let customClaims;
// Set custom user claims on this newly created user.
return admin.auth().setCustomUserClaims(user.uid, {
'https://hasura.io/jwt/claims': {
'x-hasura-default-role': 'user',
'x-hasura-allowed-roles': ['user'],
'x-hasura-user-id': user.uid
}
})
.then(() => {
// Update real-time database to notify client to force refresh.
const metadataRef = admin.database().ref("metadata/" + user.uid);
// Set the refresh time to the current UTC timestamp.
// This will be captured on the client to force a token refresh.
return metadataRef.set({
refreshTime: new Date().getTime()
});
})
.then(() => {
return admin.auth().getUser(user.uid);
})
.then(userRecord => {
console.log(userRecord);
return userRecord.toJSON();
})
.catch(error => {
console.log(error);
});
});
When I print out to the console the userRecord I can see the custom claims are set correctly.
Then in flutter I get the token from the created user, but it then doesn't seem to have the custom claims attached.
I'm using this code to create the user and print the claims in flutter
Future<FirebaseUser> signUp({String email, String password}) async {
final FirebaseUser user = (await auth.createUserWithEmailAndPassword(
email: email,
password: password,
)).user;
IdTokenResult result = await (user.getIdToken(refresh: true));
print('claims : ${result.claims}');
return user;
}
If I inspect the token itself in a jwt debugger I can see its not got the custom claims on it.
Is it that I need some additional steps to try and get an updated token once the claims have been set?
I've tried user.reload() and user.getIdToken(refresh: true) but they don't seem to help.
Any ideas on how to get the token that has the custom claims?
For future reference, I managed to get this working with Doug's suggestions.
Here's my firebase sdk admin function.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const firestore = admin.firestore();
const settings = {timestampsInSnapshots: true};
firestore.settings(settings);
// On sign up.
exports.processSignUp = functions.auth.user().onCreate(async user => {
// Check if user meets role criteria:
// Your custom logic here: to decide what roles and other `x-hasura-*` should the user get
let customClaims;
// Set custom user claims on this newly created user.
return admin.auth().setCustomUserClaims(user.uid, {
'https://hasura.io/jwt/claims': {
'x-hasura-default-role': 'user',
'x-hasura-allowed-roles': ['user'],
'x-hasura-user-id': user.uid
}
})
.then(async () => {
await firestore.collection('users').doc(user.uid).set({
createdAt: admin.firestore.FieldValue.serverTimestamp()
});
})
.catch(error => {
console.log(error);
});
});
Then on the flutter side of things
Future<FirebaseUser> signUp({String email, String password}) async {
final FirebaseUser user = (await auth.createUserWithEmailAndPassword(
email: email,
password: password,
)).user;
currentUser = user;
await waitForCustomClaims();
return user;
}
Future waitForCustomClaims() async {
DocumentReference userDocRef =
Firestore.instance.collection('users').document(currentUser.uid);
Stream<DocumentSnapshot> docs = userDocRef.snapshots(includeMetadataChanges: false);
DocumentSnapshot data = await docs.firstWhere((DocumentSnapshot snapshot) => snapshot?.data !=null && snapshot.data.containsKey('createdAt'));
print('data ${data.toString()}');
IdTokenResult idTokenResult = await (currentUser.getIdToken(refresh: true));
print('claims : ${idTokenResult.claims}');
}
Hopefully this will help somebody else looking to do similar.
The code you're showing is likely trying to get custom claims too soon after the account is created. It will take a few seconds for the function to trigger after you call auth.createUserWithEmailAndPassword. It runs asynchronously, and doesn't at all hold up the process of user creation. So, you will need to somehow wait for the function to complete before calling user.getIdToken(refresh: true).
This is precisely the thing I address in this blog post. The solution I offer does the following:
Client: Creates a user
Client: Waits for a document with the user's UID to be created in Firestore
Server: Auth onCreate function triggers
Server: Function does its work
Server: At the end, function writes data to a new document with the new user's UID
Client: Database listener triggers on the creation of the document
Then, you would add more more step on the client to refresh the ID token after it sees the new document.
The code given in the post is for web/javascript, but the process applies to any client. You just need to get the client to wait for the function to complete, and Firestore is a convenient place to relay that information, since the client can listen to it in real time.
Also read this post for a way to get a client to refresh its token immediately, based on claims written to a Firestore document.
Bottom line is that you're in for a fair amount of code to sync between the client and server.

display single record by id with vue js and axios

I have a mongodb express vue js app that displays a list of items in cards which are links to a detail view of each record. If I hover over the card the correct id for the link displays but click any card and it goes to the first document from mongo and the record does not display. The view retrieves an item but always the first one.
How to display a record of the ID of item clicked?
Report.vue
the backend request which works in postman is
// Get Simgle Report
router.get('/:id', async (req, res) => {
const reports = await loadReportsCollection()
await reports.findOne({_id: new mongodb.ObjectID( req.params.id)})
res.send(await reports.find({}).limit(1).toArray())
res.status(200).send()
}
)
ReportService.js looks like
//Find Single Report
static getReport(id) {
return axios.get(`${url}${id}`)
}
and the Report.vue file looks like
mounted () {
this.getReport()
},
methods: {
async getReport() {
try {
const response = await ReportService.getReport(this.$route.params.id)
this.report = response.data
} catch(err) {
this.err = err.message
}
},
}
many thanks for help!
It would seem you are trying to access a param in your api without passing one in your request. You ask for params here:
await reports.findOne({_id: new mongodb.ObjectID( req.params.id)})
but haven't passed any in your request. This should do it:
return axios.get('/:id', {
params: {
id: `${id}`
}
})
To not only get the first entry, but the one you are looking for you need to change your send() parameter.
Here is the working code:
// Get Simgle Report
router.get('/:id', async (req, res) => {
const reports = await loadReportsCollection()
const report = await reports.findOne({_id: new mongodb.ObjectID(req.params.id)})
res.send(await report)
res.status(200).send()
}
)
And as Andrew1325 stated you need to change your axios.get() call also to pass the correct params to it.