Sendgrid & Firebase Functions: Error Sending Transactional Email with Dynamic Template Data - firebase

Once a new vendor is registered on my Firebase Realtime Database, I want to send the new vendor a welcome email via Sendgrid. I've constructed a Firebase function newVendorEmail() to do this in my app's functions/src/index.ts folder and configured everything there as per https://app.sendgrid.com/guide/integrate/langs/nodejs/verify. I'm also able to retrieve vendor details from Firebase via onCreate() in newVendorEmail() and pass them to the dynamic_template_data part of the msg object without any problem. But when the newVendorEmail() function was triggered in Firebase Functions the email was not sent and I got this response instead in my Firebase Functions Console: TypeError: Object.values is not a function at Mail.setDynamicTemplateData (/user_code/node_modules/#sendgrid/mail/node_modules/#sendgrid/helpers/classes/mail.js:342:12). Help, please?
I've tried upgrading to the latest #sendgrid/mail npm package v6.4.0, tried switching to a new Sendgrid API key, tried storing this new API key in process.env as per Sendgrid's github example https://github.com/sendgrid/sendgrid-nodejs/blob/master/use-cases/kitchen-sink.md instead of functions.config(), but to no avail.
in node/process.env:
{ SENDGRID_API_KEY:
'SG....E',
...
}
in functions/src/index.ts:
'use strict'
const functions = require('firebase-functions')
const admin = require('firebase-admin')
const sendgrid = require('#sendgrid/mail')
// init function
admin.initializeApp()
//init firebase ref const
const ref = admin.database().ref()
// set sendgrid api from process env
sendgrid.setApiKey(process.env.SENDGRID_API_KEY)
export const newVendorEmail = functions.database
.ref('users/{userId}/profile')
.onCreate((snapshot, context) => {
// call field data using snapshot.val()
let msg
const userData = snapshot.val()
if (userData.type === 'vendor') {
// set email data
msg = {
to: userData.email,
from: {
name: 'Blk. Party',
email: '...#blkparty.com'
},
// custom templates
templateId: '...',
dynamic_template_data: {
subject: 'Welcome to Blk. Party!',
name: userData.name,
regLink: userData.regLink
},
}
}
// send email via sendgrid
return sendgrid.send(msg)
})
in package.json:
...
"dependencies": {
"#sendgrid/mail": "^6.4.0",
"firebase-admin": "~6.0.0",
"firebase-functions": "^2.1.0"
},
"devDependencies": {
"#sendgrid/mail": "^6.4.0",
...
}
...
I expect emails to be sent without any error.

I had the same problem. In my case, the solution was to switch from node6 to node8 in firebase functions.

Related

Error when setting up Firebase automated backup with Google Cloud Functions

I'm trying to setup automatic backup of my Firestore using instructions here: https://firebase.google.com/docs/firestore/solutions/schedule-export
I get error:
firestoreExpert
g2o6pmdwatdp
TypeError: Cannot read properties of undefined (reading 'charCodeAt')
at peg$parsetemplate (/workspace/node_modules/google-gax/build/src/pathTemplateParser.js:304:17)
at Object.peg$parse [as parse] (/workspace/node_modules/google-gax/build/src/pathTemplateParser.js:633:18)
at new PathTemplate (/workspace/node_modules/google-gax/build/src/pathTemplate.js:55:54)
Any suggestions to debug this?
I've tried looking for errors in my permissions. E.g. I don't know how to check if the service has access to the specific bucket, although the GCL ran OK.
I've also tried looking for errors in the script.
index.js
const firestore = require('#google-cloud/firestore');
const client = new firestore.v1.FirestoreAdminClient();
// Replace BUCKET_NAME
const bucket = 'gs://EDITEDHERE.appspot.com'
exports.scheduledFirestoreExport = (event, context) => {
const databaseName = client.databasePath(
process.env.GCLOUD_PROJECT,
'(default)'
);
return client
.exportDocuments({
name: databaseName,
outputUriPrefix: bucket,
// Leave collectionIds empty to export all collections
// or define a list of collection IDs:
// collectionIds: ['users', 'posts']
collectionIds: [],
})
.then(responses => {
const response = responses[0];
console.log(`Operation Name: ${response['name']}`);
return response;
})
.catch(err => {
console.error(err);
});
};
and package.json
{
"dependencies": {
"#google-cloud/firestore": "^1.3.0"
}
}
I found these great video tutorials
How to schedule firestorm backups and
How To Transfer Firestore Data From One Project To Another

Create a user programatically using Firebase Auth emulator

I am trying to write jest tests using the Firebase Auth emulator and continue to receive the following CORS error.
console.error
Error: Headers X-Client-Version forbidden
at dispatchError (/Users/me/my-project/node_modules/jsdom/lib/jsdom/living/xhr/xhr-utils.js:62:19)
at validCORSPreflightHeaders (/Users/me/my-project/node_modules/jsdom/lib/jsdom/living/xhr/xhr-utils.js:99:5)
at Request.<anonymous> (/Users/me/my-project/node_modules/jsdom/lib/jsdom/living/xhr/xhr-utils.js:367:12)
at Request.emit (events.js:315:20)
at Request.onRequestResponse (/Users/me/my-project/node_modules/request/request.js:1059:10)
at ClientRequest.emit (events.js:315:20)
at HTTPParser.parserOnIncomingClient [as onIncoming] (_http_client.js:641:27)
at HTTPParser.parserOnHeadersComplete (_http_common.js:126:17)
at Socket.socketOnData (_http_client.js:509:22)
at Socket.emit (events.js:315:20) undefined
The test is very simple:
import { renderHook, act } from "#testing-library/react-hooks"
import faker from "faker"
import { useAuth, FirebaseProvider, firebase } from "./index"
const wrapper = ({ firebase, children }) => {
return <FirebaseProvider firebase={firebase}>{children}</FirebaseProvider>
}
const createUser = ({ email = faker.internet.email(), password = faker.internet.password({ length: 6 }) } = {}) => {
return firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(user => user)
}
const signUserIn = ({ email, password } = {}) => {
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(user => user)
}
describe("useAuth", () => {
it("will return the user", async () => {
const { result } = renderHook(() => useAuth(), { wrapper, initialProps: { firebase } })
const email = faker.internet.email()
const password = faker.internet.password()
await act(async () => {
const user = await createUser({ email, password }) // this fails
await signUserIn({ email, password }) //and so does this
})
expect(result.user).toEqual({ email, password })
})
})
And for reference, the index file:
const FirebaseProvider = ({ children, firebase }) => {
const firestore = firebase.firestore()
const auth = firebase.auth()
if (useEmulator()) {
firestore.useEmulator("localhost", 8080)
auth.useEmulator("http://localhost:9099/")
}
const value = { firestore, auth }
return <FirebaseContext.Provider value={value}>{children}</FirebaseContext.Provider>
}
const throwError = hook => {
throw new Error(`${hook} must be used within a FirebaseProvider`)
}
const useAuth = () => {
const context = useContext(FirebaseContext)
if (context === undefined) throwError("useAuth")
const [user, setUser] = useState()
useEffect(() => {
const cleanup = context.auth.onAuthStateChanged(authUser => {
authUser ? setUser(authUser) : setUser(null)
})
return () => cleanup()
})
return { ...context.auth, user }
}
I have tried using the REST endpoint that the actual emulator uses (below) and it errors in the same way.
http://localhost:9099/identitytoolkit.googleapis.com/v1/projects/<my-project>/accounts
Is there anyway to get this to run when using jest? Or do I need to create the accounts using the emulator UI, export them and re-import when I am running tests?
I have found I can use the REST endpoint below to make a user in the test, however it bypasses the emulator and makes a real user.
https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=<api-key>
Update jsdom version 16.5.2
This new version now supports wildcards for access-control-allow-headers, so updating to this version or using it as resolution, for projects created with Create React App, solves the problem.
Solution for jsdom prior to version 16.5.2
The error is thrown by jsdom because it doesn't support wildcard for access-control-allow-headers, but firebase uses the wildcard (see this issue for jsdom and this pull request related to firebase). There are two open pull requests to fix this issue: https://github.com/jsdom/jsdom/pull/3073 and https://github.com/jsdom/jsdom/pull/2867.
The issue can be fixed by either changing the relevant code manually in the node_modules folder or by using the fork as dependency in the package.json:
"jsdom": "silviot/jsdom#fix/allow-headers"
If jsdom isn't a direct dependency, then you can add the following to the package.json at the top level:
"resolutions": {
"jsdom": "silviot/jsdom#fix/allow-headers"
}
If the fork is used there are some auto-generated files missing in the jsdom folder. These can be generated by running npm install or yarn install in the folder. To automate this you can add a prepare script to the package.json:
"scripts": {
"prepare": "cd node_modules/jsdom && yarn"
},
I also had problems making users programaticly in the firebase auth emulator.
Instead of using
https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY]
You have to use the following format:
http://localhost:9099/identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY]
Then giving a JSON body like this, and hit post
{
"email": "test#test.com",
"password": "test12"
}
And voila! You have a user in your emulator. Combine this with fetch or axios and you seed your emulator with users. If you need to add custom claims or other info, create function in the functions emulator that triggers on user creation.
functions.auth.user().onCreate

connecting to firestore emulator with #firebase/testing

I am trying to test a firebase app locally.
I am running the test with firebase emulators:exec --only firestore 'mocha -r ts-node/register src/**/*.spec.ts
In my spec, I import #firebase/testing and setup my app and followed the directions from
https://firebase.google.com/docs/rules/unit-tests
I have a FirebaseService which is a singleton wrapper for my methods into which I inject my firebase app.
In production, I'll inject the firebase, and it gets initialized in the FirebaseService in testing, I initialize outside of the service.
The wrapper is fairly simple
export const FirebaseService = (function(): FirebaseSrvc {
let firebase;
const fbServiceObj: FirebaseSrvc = {
getInstance: (firebaseConfig, firebaseCore, initialize) => {
firebase = firebaseCore;
if (initialize && firebase.apps.length === 0) {
firebase.initializeApp(firebaseConfig);
}
return fbServiceObj;
},
createActivity: async (title: string) => {
try {
const firebaseUid = firebase.auth().currentuser.uid;
const newActivity: ActivityProps = {
title,
created_at: 123445,
created_by: firebaseUid,
public: false,
available_to: [firebaseUid],
};
console.log(' before create', newActivity);
const createResponse = await firebase
.firestore()
.collection('activities')
.doc(stringToSafeId(title))
.set(newActivity);
console.log('create response', createResponse);
return true;
} catch (e) {
console.log('error creating activity', e);
}
},
getActivity: async (title: string): Promise<ActivityProps> => {
try {
const actResponse: DocumentReferenceTo<ActivityProps> = await firebase
.firestore()
.collection('activities')
.doc(stringToSafeId(title))
.get();
return actResponse as ActivityProps;
} catch (e) {
console.log('error getting activity from firebase', e);
}
},
};
return fbServiceObj;
})();
The test I am attempting to run is
import * as firebase from '#firebase/testing';
import { assert } from 'chai';
import 'mocha';
import * as appConfig from '../../app-dev.json';
import { FirebaseService } from '../services/FirebaseService';
firebase.initializeTestApp({ ...appConfig.expo.extra.firebase, auth: { uid: 'random', email: 'test#test.com' } });
describe('Activity', async () => {
const fb = FirebaseService.getInstance(appConfig.expo.extra.firebase, testApp, false);
const activityData = new Activity(fb);
beforeEach(async () => await firebase.clearFirestoreData({ projectId }));
it('should create a new activity', async () => {
await activityData.set('test-activity'); // this runs FirebaseService.createActivity
const findActivity = await activityData.get('test-activity'); // this run FirebaseService.getActivity
assert(findActivity.title === 'test-activity');
});
});
When I run the test I get an error
Your API key is invalid, please check you have copied it correctly.] {
code: 'auth/invalid-api-key',
message: 'Your API key is invalid, please check you have copied it correctly.'
}
I can confirm that the API key which is passed into firebase.initializeTestApp matches the Web API Key in my firebase console.
I have also downloaded the google-services.json from my firebase console and lists
{
"api_key": [
{ "current_key": different_from_web_key}
]
}
And I have replaced my existing key with this new key, I still get the same error.
I have also tried setting up initializeTestApp({ projectId }) which is how the example from firebase docs sets it up, and I receive the same result.
I am using the same project details to run a project locally in android studio, and I am able to authenticate and write to firestore, so the API key I am using does work, but it appears to have issues being used in the test app.
This usually doesn't have a specific way to solve it. It might be that even a new copy and paste of the API key to the parameters, might make it work and the error to disappear.
I would recommend you to take a look at the following posts from the Community, that have some possible fixes for the error that you are facing.
Firebase Error: auth/invalid-api-key, Your API key is invalid, please check you have copied it correctly
Invalid API Key supplied using Firebase
In addition to that, since Firebase has free support offers, I think you reaching out to the Firebase support would help you fix this quickly. You should be able to contact directly for free.
Let me know if the information helped you!

Permissions Error with Cloud Function Adding Data to Firestore

I'm trying to setup a Cloud Function that, when ran by Cloud Scheduler, will insert certain data into my Firestore Database. I am doing this in Node.js using the Inline editor provided when creating my Cloud Function.
I keep getting the error:
"Error: function crashed. Details:
7 PERMISSION_DENIED: Missing or insufficient permissions."
On my Firebase dashboard, the logs show my function and the error that I get when I test my Cloud Function, so I'm assuming my function is hitting the database, just not adding the dummy data I was testing with.
index.js:
const Firestore = require('#google-cloud/firestore');
const PROJECTID = 'MY_PROJECT_ID';
const firestore = new Firestore({
projectId: PROJECTID,
timestampsInSnapshots: true,
});
/**
* Responds to any HTTP request.
*
* #param {!express:Request} req HTTP request context.
* #param {!express:Response} res HTTP response context.
*/
exports.helloWorld = (req, res) => {
return firestore.collection("users").add({
first: "Ada",
last: "Lovelace",
born: 1815
});
};
Package.json:
{
"name": "sample-http",
"version": "0.0.1",
"dependencies": {
"#google-cloud/firestore": "0.17.0",
"semver": "^5.5.1"
}
}
I also have my rules set for my database as:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
Thanks to #andresmijares I was able to fix my problem. I looked more into the quickstart and changed my index.js as follows (specifically everything before the helloWorld function).
const admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.applicationDefault()
});
const db = admin.firestore();
/**
* Responds to any HTTP request.
*
* #param {!express:Request} req HTTP request context.
* #param {!express:Response} res HTTP response context.
*/
exports.helloWorld = (req, res) => {
/* let message = req.query.message || req.body.message || 'Hello World!';
res.status(200).send(message);
*/
return db.collection("users").add({
first: "Ada",
last: "Lovelace",
born: 1815
});
};
And I got the error
"Code in file index.js can't be loaded. Did you list all required modules in the package.json dependencies? Detailed stack trace: Error: Cannot find module 'firebase-admin'"
Which I was able to fix by adding the 'firebase-admin' dependency into my package.json, as follows:
{
"name": "sample-http",
"version": "0.0.1",
"dependencies": {
"semver": "^5.5.1",
"#google-cloud/firestore": "^1.3.0",
"firebase-admin": "^7.1.1"
}
}
This was also all done in the inline editor provided when creating my Cloud Function, so no installation of anything was needed.
you need to download the sdk key, this is a json file that you can export from your firebase console Project Overview -> Project Settings -> Services Accounts
Then you can instantiate it like this:
var admin = require("firebase-admin");
var serviceAccount = require("path/to/serviceAccountKey.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
When using the firebase admin sdk, the firestore security rules do not apply (they are only for client-side operations)

Ionic3 Firebase notification not received on device

In brief: Firebase Notifications sent via Firebase Cloud Functions shows that message is sent. However, message is not received in the device. (Only tested in Android. Don't know about iOS)
Hello, I'm on a Ionic 3 project which uses Firebase Cloud Firestore, Cloud Functions and other Firebase services.
App workflow:
Upon new document creation (as in new reservation), the admin SDK should send push notification to the particular device which should arrive in the device.
Problem:
When checking in the Cloud Functions log, it shows message is successfully sent and the Triggering functions finished without any error. But no message has been received yet. However, when sending message from Firebase Notification Console, each message arrives perfectly.
Code:
index.ts (Cloud Functions)
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase);
exports.notifyOnNewBooking = functions.firestore
.document('users/{userId}/bookings/{bookingId}')
.onCreate( event => {
const bookingData = event.data.data();
// construct notification message here
const message = {
notification: {
title: 'Reservation confirmed!',
body: 'Your reservation at someplace at sometime is confirmed',
icon: 'https://image.ibb.co/iBwekx/icon.png'
}
};
// send notification with message right away
return admin.messaging().sendToDevice(bookingData.deviceFCMToken, message, {priority: 'high'})
.then(resp => {
console.log("sent successfully", resp);
})
.catch(err => {
console.error("could not send notification", err);
});
});
app.component.ts (Ionic)
...
// Ionic Native wrapper
import { FCM } from '#ionic-native/fcm';
....
#Component({
template: `
....
....
`
})
export class MyApp {
...
constructor(..., private fcm: FCM) {}
ngOnInit() {
this.fcm.onNotification()
.subscribe(resp => {});
}
}
Firebase Cloud Functions log shows this:
Ionic CLI info
cli packages: (/usr/local/lib/node_modules)
#ionic/cli-utils : 1.19.1
ionic (Ionic CLI) : 3.19.1
System:
Node : v9.3.0
npm : 5.5.1
OS : macOS High Sierra
Misc:
backend : pro
Cloud Functions package.json dependencies
"dependencies": {
"#google-cloud/functions-emulator": "^1.0.0-beta.3",
"firebase-admin": "~5.8.1",
"firebase-functions": "^0.8.1",
"firebase-tools": "^3.17.4",
"global": "^4.3.2"
},
config.xml
<plugin name="cordova-plugin-fcm" spec="^2.1.2">
<variable name="SENDER_ID" value="845539284400" />
</plugin>
Note: There is only one subscription which is at the root component in the app. And I'm on Firebase Spark plan which is free but often notifies in the log that - Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions.
Modified the function in Cloud Functions to this below and now notifications is being received in the Notification tray when the app is in Background and in the subscription response when the app is in Foreground.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase);
exports.notifyOnNewBooking = functions.firestore
.document('users/{userId}/bookings/{bookingId}')
.onCreate(event => {
const bookingData = event.data.data();
// construct notification message here
const message: admin.messaging.Message = {
token: bookingData.deviceFCMToken,
android: {
notification: {
title: 'Reservation successful',
body: `Your reservation at ${bookingData.restaurant_name} is confirmed.`,
icon: 'https://image.ibb.co/iBwekx/icon.png'
}
},
apns: {
headers: {
'apns-priority': '10'
},
payload: {
aps: {
alert: {
title: 'Reservation successful',
body: `Your reservation at ${bookingData.restaurant_name} is confirmed.`,
},
badge: 1
}
}
}
};
// send notification with message right away
return admin.messaging().send(message)
.then(resp => {
console.log("sent successfully", resp);
})
.catch(err => {
console.error("could not send notification", err);
});
});

Resources