├── .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 |
--------------------------------------------------------------------------------