├── .firebaserc ├── .gitignore ├── .idea ├── .gitignore ├── cloud_function_test.iml └── modules.xml ├── README.md ├── firebase.json └── functions ├── .eslintrc.js ├── .gitignore ├── index.js ├── package-lock.json └── package.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "test-c2955" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/cloud_function_test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Idea 2 | #### Test and try cloud functions with FCM locally and for free without upgrade to firebase blaze plan 🔥 3 | 4 | ## What you will learn 🧑‍💻 5 | - Setup NodeJs for cloud functions development 💚 6 | - Test Cloud Functions loclly (without deploy and pay for blaze plan) ❤️‍🔥 7 | - Firestore triggers 🔥 8 | - Send fcm (notifications) 🔔 9 | - Modifiy documents on cloud function 📄 10 | - Read collections/documents 📖 11 | - Test our cloud functions with client side in my case (Flutter App) 📱 12 | 13 | ## What do you need 14 | - Install [NodeJs](https://nodejs.org/en/download/) must be version(10,12,14,16) to support firebase cli 15 | - Install [Java](https://www.java.com/download/ie_manual.jsp) needed to start the local emulators 16 | - Now run this command to install Firebase CLI & tools 17 | 18 | ```bash 19 | npm install -g firebase-tools 20 | ``` 21 | 22 | ## Setup project 23 | - Before we start go to [firebase console](https://firebase.google.com/) and create firebase project 24 | - Login to your firebase account, in cmd run 25 | 26 | ```bash 27 | firebase login 28 | ``` 29 | If it says already logged in run this command (just to make sure your credintials are valid and you agreed to the latest firebase cli terms) 30 | 31 | ```bash 32 | firebase login --reauth 33 | ``` 34 | - Create folder for your cloud functions project (there must be no spacebars in path) for example: 35 | ```bash 36 | C:\Users\USERNAME\Desktop\cloud_function_test 37 | ``` 38 | - Cd to your project folder and run 39 | 40 | ```bash 41 | firebase init 42 | ``` 43 | (follow the video) 44 | 45 | https://user-images.githubusercontent.com/64028200/174799383-1319b807-dbcf-494b-b550-575276b4a8c4.mp4 46 | 47 | 48 | 49 | - Now lets setup FCM things so we can test it locally (download key and paste it on your folder project) 50 | 51 | https://user-images.githubusercontent.com/64028200/174799403-3c0802a6-f7e5-4291-a459-3f2d6c6fb2a4.mp4 52 | 53 | 54 | 55 | ## Coding 56 | ##### Open your functions/index.js file and remove all the code and let us start from the zero 57 | - Import firebase functions modules to access triggers functions & admin modules to access database(firestore) & cloud messaging 58 | ```js 59 | const functions = require("firebase-functions"); 60 | const admin = require("firebase-admin"); 61 | ``` 62 | - Initialize your admin sdk with your key credentail that you just download it 63 | ```js 64 | // for example ("C:\\Users\\HP\\Desktop\\cloud_function_test\\key_name.json") 65 | const keyPath = 'key_path'; 66 | 67 | admin.initializeApp( 68 | // TODO: remove this object when you finish and want to deploy to firebase 69 | { credential: admin.credential.cert(keyPath) } 70 | ); 71 | ``` 72 | - Write our first trigger function (onOrderCreated) you can name it whatever you want 73 | ```js 74 | /** fire when new document added to orders collection */ 75 | exports.onOrderCreated = functions.firestore.document('orders/{id}') 76 | .onCreate((change, context) => { 77 | /** do something here */ 78 | return null; 79 | }); 80 | // firestore have 4 triggers and they all have same syntax 81 | // onCreate => Fire whenever new document added 82 | // onUpdate => Fire when existing document modified 83 | // onDelete => Fire when deleting document 84 | // onWrite => Fire When create,update,delete document 85 | ``` 86 | - Explore params (change,context) with simple example: of changing any order name that gets created and add emoje next to it 87 | ```js 88 | exports.onOrderCreated = functions.firestore.document('orders/{id}') 89 | .onCreate((change, context) => { 90 | // id is identical to orders/{id} which mean if it was orders/{oId} it would be context.params.oId 91 | const createdDocumentId = context.params.id; 92 | // data of the created document 93 | const createdDocument = change.data(); 94 | // access field from document, if the field doesnt exist value will be undifined 95 | const orderName = createdDocument.name; 96 | // check if the field viewTimes defined (exist) 97 | if(orderName){ 98 | // modify document before saving it to firebase 99 | return change.ref.update({name: orderName + ' 📦'}); 100 | }else { 101 | // return null means you dont want to return any promise/action its (JS) thing 102 | // but do i have to return null in all triggers? actully yes, bcz by default 103 | // it will return (undifned) and that can cause some problems! 104 | return null; 105 | } 106 | }); 107 | ``` 108 | - Lets run our code and see how far we got 🦅 109 | ```bash 110 | firebase emulators:start 111 | ``` 112 | 113 | 114 | https://user-images.githubusercontent.com/64028200/174799487-939dce32-fe2d-49b1-83dd-03785eee2e11.mp4 115 | 116 | 117 | - Other triggers have different way to get data 118 | ```js 119 | /** onUpdate trigger */ 120 | exports.onOrderUpdated = functions.firestore.document('orders/{id}') 121 | .onUpdate((change, context) => { 122 | const oldDoc = change.before.data(); // before the edit 123 | const updatedDoc = change.after.data(); // after the edit 124 | return null; 125 | }); 126 | 127 | /** onDelete trigger */ 128 | exports.onOrderDeleted = functions.firestore.document('orders/{id}') 129 | .onDelete((change, context) => { 130 | const deletedDoc = change.data(); // you can do backup 131 | return null; 132 | }); 133 | 134 | /** onWrite trigger (fire when document update,delete,create) */ 135 | exports.onOrderStateChange = functions.firestore.document('orders/{id}') 136 | .onWrite((change, context) => { 137 | // only has value if its (update operation) otherwise it will be undefined 138 | const oldDoc = change.before.exists ? change.before.data() : null; 139 | const newDoc = change.after.data(); 140 | return null; 141 | }); 142 | ``` 143 | 144 | ## FCM (send notificaitons) 145 | - Now after we took a quick look of how things work on cloud function lets make some actions, we will send fcm to all users collection (who have fcm_token) whenever new order is created 146 | ```js 147 | exports.onOrderCreated = functions.firestore.document(`orders/{id}`) 148 | .onCreate(async (change, context) => { 149 | // *) read all users collection 150 | const usersRef = admin.firestore().collection('users'); 151 | const snapshot = await usersRef.get(); 152 | // *) hold users fcm tokens 153 | const usersFcmTokens = []; 154 | // *) loop on all users documents and check if each one has fcm token 155 | snapshot.forEach(doc => { 156 | if (doc.get('fcm_token') != null && doc.get('fcm_token').trim().length > 0) { 157 | usersFcmTokens.push(doc.get('fcm_token')); 158 | } 159 | }); 160 | // *) fcm options: 161 | // IMPORTANT: priority is a must because android & ios kill background process so if the priority is normal 162 | // or low the notification will not be shown when the app is terminated 163 | const options = { 164 | priority: "high", timeToLive: 60 * 60 * 24 165 | }; 166 | // *) title,body and data that will be sent with notification 167 | const payload = { 168 | notification: { 169 | title: 'Orders', 170 | body: 'There is a new order!', 171 | }, data: { 172 | 'name': 'Emad Beltaje', 173 | 'Note': 'Dont forget to rate repository 🌟' 174 | } 175 | }; 176 | // *) send notifications 177 | if (usersFcmTokens.length > 0) { 178 | await admin.messaging().sendToDevice(usersFcmTokens, payload, options); 179 | } 180 | return null; 181 | }); 182 | ``` 183 | 184 | ## lets test it with client side (in my case Flutter app) 185 | - first run this command to start your cloud functions emulators 186 | ``` 187 | firebase emulators:start 188 | ``` 189 | - You can use my [Flutter Repo](https://github.com/EmadBeltaje/flutter_getx_template) for quick start 190 | 191 | - Go to lib/utils/fcm_helper.dart 192 | 193 | - Now print the generated fcm token 194 | 195 | ``` 196 | static _sendFcmTokenToServer(){ 197 | var token = MySharedPref.getFcmToken(); 198 | Logger().e(token); // just print the token 199 | } 200 | ``` 201 | 202 | - add it to one of users in users collection (field name must be fcm_token) 203 | 204 | - create new order to trigger (onOrderCreated) function 205 | 206 | 207 | You can follow the video 208 | 209 | 210 | https://user-images.githubusercontent.com/64028200/175286984-f56c0feb-ab5f-453d-b75e-d1e9ba4e1e57.mp4 211 | 212 | 213 | 214 | ## Support 215 | 216 | For support, email emadbeltaje@gmail.com or Facebook [Emad Beltaje](https://www.facebook.com/EmadBeltaje/). 217 | Dont Forget to star the repo 🌟 218 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": [ 4 | "npm --prefix \"$RESOURCE_DIR\" run lint" 5 | ] 6 | }, 7 | "emulators": { 8 | "functions": { 9 | "port": 5001 10 | }, 11 | "firestore": { 12 | "port": 8080 13 | }, 14 | "ui": { 15 | "enabled": true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /functions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "google", 10 | ], 11 | rules: { 12 | quotes: ["error", "double"], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require("firebase-functions"); 2 | const admin = require("firebase-admin"); 3 | 4 | // TODO: add your private generated key path => for example ("C:\\Users\\HP\\Desktop\\cloud_function_test\\key_name.json") 5 | const keyPath = 'your_key_path.json'; 6 | 7 | admin.initializeApp( 8 | // TODO: remove this object when you finish and want to deploy to firebase 9 | { 10 | credential: admin.credential.cert(keyPath) 11 | } 12 | ); 13 | 14 | 15 | /** 16 | * fire when new order created 17 | * it will send FCM to all the users who have fcm_token 18 | * */ 19 | exports.onOrderCreated = functions.firestore.document(`orders/{id}`) 20 | .onCreate(async (change, context) => { 21 | // *) read all users collection 22 | const usersRef = admin.firestore().collection('users'); 23 | const snapshot = await usersRef.get(); 24 | // *) hold users fcm tokens 25 | const usersFcmTokens = []; 26 | // *) loop on all users documents and check if each one has fcm token 27 | snapshot.forEach(doc => { 28 | if (doc.get('fcm_token') != null && doc.get('fcm_token').trim().length > 0) { 29 | usersFcmTokens.push(doc.get('fcm_token')); 30 | } 31 | }); 32 | // *) fcm options: 33 | // IMPORTANT: priority is a must because android & ios kill background process so if the priority is normal 34 | // or low the notification will not be shown when the app is terminated 35 | const options = { 36 | priority: "high", timeToLive: 60 * 60 * 24 37 | }; 38 | // *) title,body and data that will be sent with notification 39 | const payload = { 40 | notification: { 41 | title: 'Orders', body: 'There is a new order!', 42 | }, data: { 43 | 'name': 'Emad Beltaje', 'Note': 'Dont forget to rate repository 🌟' 44 | } 45 | }; 46 | // *) send notifications 47 | if (usersFcmTokens.length > 0) { 48 | await admin.messaging().sendToDevice(usersFcmTokens, payload, options); 49 | } 50 | 51 | // *) onWrite expect return of type promise 52 | // but because we don't want to perform extra actions just return null (its JS things) 53 | return null; 54 | }); 55 | 56 | 57 | /** fire when existing order updated */ 58 | exports.onOrderUpdated = functions.firestore.document('orders/{id}') 59 | .onUpdate((change, context) => { 60 | const oldDoc = change.before.data(); 61 | const updatedDoc = change.after.data(); 62 | //const name = newDoc.get('name'); // access field inside document 63 | return null; 64 | 65 | // example of return action instead of null 66 | // let count = oldDoc.doc_update_times_count; 67 | // if (!count) { 68 | // count = 0; 69 | // } 70 | // return change.after.ref.set({ 71 | // doc_update_times_count: count + 1 72 | // }, {merge: true}); 73 | }); 74 | 75 | /** fire when existing order deleted */ 76 | exports.onOrderDeleted = functions.firestore.document('orders/{id}') 77 | .onDelete((change, context) => { 78 | const deletedDoc = change.data(); // you can do backup 79 | return null; 80 | }); 81 | 82 | /** fire when existing order created, deleted or updated */ 83 | exports.onOrderStateChange = functions.firestore.document('orders/{id}') 84 | .onWrite((change, context) => { 85 | const oldDoc = change.before.exists ? change.before.data() : null; // only has value if its (update operation) otherwise it will be undefined 86 | const newDoc = change.after.data(); 87 | return null; 88 | }); 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "serve": "firebase emulators:start --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "16" 14 | }, 15 | "main": "index.js", 16 | "dependencies": { 17 | "firebase-admin": "^10.0.2", 18 | "firebase-functions": "^3.18.0" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^8.9.0", 22 | "eslint-config-google": "^0.14.0", 23 | "firebase-functions-test": "^0.2.0" 24 | }, 25 | "private": true 26 | } 27 | --------------------------------------------------------------------------------