├── admin └── src │ ├── translations │ ├── en.json │ └── fr.json │ ├── utils │ ├── getTrad.js │ └── axiosInstance.js │ ├── pluginId.js │ ├── components │ ├── Initializer │ │ └── index.js │ └── PluginIcon │ │ └── index.js │ ├── service │ └── api.js │ ├── pages │ ├── App │ │ └── index.js │ ├── HomePage │ │ └── index.js │ └── TargetsPage │ │ └── index.js │ └── index.js ├── server ├── policies │ └── index.js ├── middlewares │ └── index.js ├── destroy.js ├── config │ └── index.js ├── register.js ├── content-types │ ├── fcm-topic │ │ ├── index.js │ │ └── schema.json │ ├── index.js │ ├── fcm-plugin-configuration │ │ ├── index.js │ │ └── schema.json │ └── fcm-notification │ │ ├── schema.json │ │ └── index.js ├── routes │ ├── index.js │ ├── admin │ │ ├── index.js │ │ ├── fcm-target.js │ │ ├── fcm-plugin-configuration.js │ │ ├── fcm-topic.js │ │ └── fcm-notification.js │ └── content-api │ │ ├── index.js │ │ ├── fcm-target.js │ │ ├── fcm-plugin-configuration.js │ │ ├── fcm-topic.js │ │ └── fcm-notification.js ├── services │ ├── my-service.js │ ├── index.js │ ├── fcm-topic.js │ ├── fcm-plugin-configuration.js │ ├── fcm-notification.js │ └── fcm-target.js ├── bootstrap.js ├── controllers │ ├── my-controller.js │ ├── index.js │ ├── fcm-target.js │ ├── fcm-plugin-configuration.js │ ├── fcm-topic.js │ └── fcm-notification.js └── index.js ├── strapi-server.js ├── strapi-admin.js ├── public └── assets │ ├── topics.png │ ├── admin-fcm.png │ ├── permissions.png │ ├── configuration.png │ ├── admin-fcm-targets.png │ ├── firebase-logo-logomark.png │ └── strapi-plugin-fcm-logo.svg ├── .yarnrc.yml ├── .editorconfig ├── .gitignore ├── LICENSE ├── package.json ├── util └── fcm.js └── README.md /admin/src/translations/en.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /admin/src/translations/fr.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /server/policies/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /server/middlewares/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /strapi-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./server'); 4 | -------------------------------------------------------------------------------- /strapi-admin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./admin/src').default; 4 | -------------------------------------------------------------------------------- /public/assets/topics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itisnajim/strapi-plugin-fcm/HEAD/public/assets/topics.png -------------------------------------------------------------------------------- /server/destroy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ strapi }) => { 4 | // destroy phase 5 | }; 6 | -------------------------------------------------------------------------------- /public/assets/admin-fcm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itisnajim/strapi-plugin-fcm/HEAD/public/assets/admin-fcm.png -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | default: {}, 5 | validator() {}, 6 | }; 7 | -------------------------------------------------------------------------------- /server/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ strapi }) => { 4 | // registeration phase 5 | }; 6 | -------------------------------------------------------------------------------- /public/assets/permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itisnajim/strapi-plugin-fcm/HEAD/public/assets/permissions.png -------------------------------------------------------------------------------- /public/assets/configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itisnajim/strapi-plugin-fcm/HEAD/public/assets/configuration.png -------------------------------------------------------------------------------- /public/assets/admin-fcm-targets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itisnajim/strapi-plugin-fcm/HEAD/public/assets/admin-fcm-targets.png -------------------------------------------------------------------------------- /public/assets/firebase-logo-logomark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itisnajim/strapi-plugin-fcm/HEAD/public/assets/firebase-logo-logomark.png -------------------------------------------------------------------------------- /server/content-types/fcm-topic/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const schema = require('./schema'); 4 | 5 | module.exports = { 6 | schema, 7 | }; -------------------------------------------------------------------------------- /admin/src/utils/getTrad.js: -------------------------------------------------------------------------------- 1 | import pluginId from '../pluginId'; 2 | 3 | const getTrad = id => `${pluginId}.${id}`; 4 | 5 | export default getTrad; 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-sources.cjs 8 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | admin: require('./admin'), 5 | 'content-api': require('./content-api'), 6 | }; 7 | -------------------------------------------------------------------------------- /server/services/my-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = ({ strapi }) => ({ 4 | getWelcomeMessage() { 5 | return 'Welcome to Strapi 🚀'; 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /admin/src/pluginId.js: -------------------------------------------------------------------------------- 1 | const pluginPkg = require('../../package.json'); 2 | 3 | const pluginId = pluginPkg.name;// .replace(/^(@[^-,.][\w,-]+\/|strapi-)plugin-/i, ''); 4 | 5 | module.exports = pluginId; 6 | -------------------------------------------------------------------------------- /server/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fcmUtil = require('../util/fcm'); 4 | 5 | 6 | module.exports = ({ strapi }) => { 7 | // bootstrap phase 8 | fcmUtil.initialize(strapi); 9 | }; 10 | -------------------------------------------------------------------------------- /server/controllers/my-controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | index(ctx) { 5 | ctx.body = strapi 6 | .plugin('strapi-plugin-fcm') 7 | .service('myService') 8 | .getWelcomeMessage(); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /server/content-types/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 'fcm-topic': require('./fcm-topic'), 5 | 'fcm-notification': require('./fcm-notification'), 6 | 'fcm-plugin-configuration': require('./fcm-plugin-configuration'), 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | node_modules 4 | stats.json 5 | 6 | # Cruft 7 | .DS_Store 8 | npm-debug.log 9 | .idea 10 | 11 | #yarn 12 | .yarn/* 13 | !.yarn/patches 14 | !.yarn/releases 15 | !.yarn/plugins 16 | !.yarn/sdks 17 | !.yarn/versions 18 | .pnp.* -------------------------------------------------------------------------------- /server/services/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const myService = require('./my-service'); 4 | 5 | module.exports = { 6 | myService, 7 | 'fcm-target': require('./fcm-target'), 8 | 'fcm-notification': require('./fcm-notification'), 9 | 'fcm-topic': require('./fcm-topic'), 10 | 'fcm-plugin-configuration': require('./fcm-plugin-configuration') 11 | }; 12 | -------------------------------------------------------------------------------- /server/controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const myController = require('./my-controller'); 4 | 5 | module.exports = { 6 | myController, 7 | 'fcm-target': require('./fcm-target'), 8 | 'fcm-topic': require('./fcm-topic'), 9 | 'fcm-notification': require('./fcm-notification'), 10 | 'fcm-plugin-configuration': require('./fcm-plugin-configuration'), 11 | }; 12 | -------------------------------------------------------------------------------- /public/assets/strapi-plugin-fcm-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/routes/admin/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fcmTopic = require('./fcm-topic'); 4 | const fcmNotification = require('./fcm-notification'); 5 | const fcmTarget = require('./fcm-target'); 6 | const fcmPluginConfiguration = require('./fcm-plugin-configuration'); 7 | 8 | 9 | module.exports = { 10 | type: 'admin', 11 | routes: [...fcmTopic, ...fcmNotification, ...fcmTarget, ...fcmPluginConfiguration], 12 | }; 13 | -------------------------------------------------------------------------------- /server/routes/content-api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fcmTopic = require('./fcm-topic'); 4 | const fcmNotification = require('./fcm-notification'); 5 | const fcmTarget = require('./fcm-target'); 6 | const fcmPluginConfiguration = require('./fcm-plugin-configuration'); 7 | 8 | 9 | module.exports = { 10 | type: 'content-api', 11 | routes: [...fcmTopic, ...fcmNotification, ...fcmTarget, ...fcmPluginConfiguration], 12 | }; 13 | -------------------------------------------------------------------------------- /server/routes/content-api/fcm-target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // router. 4 | 5 | module.exports = [ 6 | { 7 | method: 'GET', 8 | path: '/fcm-targets/count', 9 | handler: 'fcm-target.count', 10 | config: { 11 | policies: [], 12 | }, 13 | }, 14 | { 15 | method: 'GET', 16 | path: '/fcm-targets', 17 | handler: 'fcm-target.find', 18 | config: { 19 | policies: [], 20 | }, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /admin/src/components/Initializer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Initializer 4 | * 5 | */ 6 | 7 | import { useEffect, useRef } from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import pluginId from '../../pluginId'; 10 | 11 | const Initializer = ({ setPlugin }) => { 12 | const ref = useRef(); 13 | ref.current = setPlugin; 14 | 15 | useEffect(() => { 16 | ref.current(pluginId); 17 | }, []); 18 | 19 | return null; 20 | }; 21 | 22 | Initializer.propTypes = { 23 | setPlugin: PropTypes.func.isRequired, 24 | }; 25 | 26 | export default Initializer; 27 | -------------------------------------------------------------------------------- /server/controllers/fcm-target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * controller 5 | */ 6 | 7 | const getService = () => { 8 | return strapi.plugin('strapi-plugin-fcm').service('fcm-target'); 9 | } 10 | 11 | module.exports = { 12 | async find(ctx) { 13 | try { 14 | ctx.body = await getService().find(ctx.query); 15 | } catch (err) { 16 | ctx.throw(500, err); 17 | } 18 | }, 19 | 20 | async count(ctx) { 21 | try { 22 | ctx.body = await getService().count(ctx.query); 23 | } catch (err) { 24 | ctx.throw(500, err); 25 | } 26 | } 27 | }; -------------------------------------------------------------------------------- /admin/src/components/PluginIcon/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * PluginIcon 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | const PluginIcon = () => ( 10 | 16 | 17 | 18 | ); 19 | 20 | export default PluginIcon; 21 | -------------------------------------------------------------------------------- /server/content-types/fcm-topic/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "fcm_topics", 4 | "info": { 5 | "singularName": "fcm-topic", 6 | "pluralName": "fcm-topics", 7 | "displayName": "FCM Topic" 8 | }, 9 | "options": { 10 | "draftAndPublish": true, 11 | "comment": "To create and manage FCM topics, which are used to send messages to devices from the Admin Dashboard." 12 | }, 13 | "attributes": { 14 | "name": { 15 | "type": "string", 16 | "required": true, 17 | "maxLength": 100 18 | }, 19 | "label": { 20 | "type": "string", 21 | "maxLength": 100 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const register = require('./register'); 4 | const bootstrap = require('./bootstrap'); 5 | const destroy = require('./destroy'); 6 | const config = require('./config'); 7 | const contentTypes = require('./content-types'); 8 | const controllers = require('./controllers'); 9 | const routes = require('./routes'); 10 | const middlewares = require('./middlewares'); 11 | const policies = require('./policies'); 12 | const services = require('./services'); 13 | 14 | module.exports = { 15 | register, 16 | bootstrap, 17 | destroy, 18 | config, 19 | controllers, 20 | routes, 21 | services, 22 | contentTypes, 23 | policies, 24 | middlewares, 25 | }; 26 | -------------------------------------------------------------------------------- /server/content-types/fcm-plugin-configuration/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fcmUtil = require('../../../util/fcm'); 4 | 5 | const uid = 'plugin::strapi-plugin-fcm.fcm-plugin-configuration'; 6 | module.exports = { 7 | schema: require('./schema'), 8 | lifecycles: { 9 | afterCreate(event) { 10 | console.log('afterCreate', event); 11 | if(event?.module?.uid === uid) { 12 | fcmUtil.initialize(strapi); 13 | } 14 | }, 15 | afterUpdate(event) { 16 | console.log('afterUpdate', event); 17 | if(event?.module?.uid === uid) { 18 | fcmUtil.initialize(strapi); 19 | } 20 | } 21 | }, 22 | }; -------------------------------------------------------------------------------- /admin/src/service/api.js: -------------------------------------------------------------------------------- 1 | import instance from "../utils/axiosInstance"; 2 | 3 | 4 | export default { 5 | getTargets: async (page, pageSize = 20) => { 6 | const { data } = await instance.request({ 7 | url: '/fcm-targets', 8 | method: 'get', 9 | params: { 10 | pagination: { 11 | page, 12 | pageSize 13 | }, 14 | populate: '*' 15 | } 16 | }); 17 | 18 | return data; 19 | }, 20 | sendFCMs: async (entries) => { 21 | const { data } = await instance.request({ 22 | url: '/fcm-notifications', 23 | method: 'post', 24 | data: {data: entries} 25 | }); 26 | 27 | return data; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /server/content-types/fcm-plugin-configuration/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "singleType", 3 | "collectionName": "fcm_plugin_configurations", 4 | "info": { 5 | "singularName": "fcm-plugin-configuration", 6 | "pluralName": "fcm-plugin-configurations", 7 | "displayName": "FCM Plugin Configuration" 8 | }, 9 | "options": { 10 | "draftAndPublish": false, 11 | "comment": "" 12 | }, 13 | "attributes": { 14 | "serviceAccount": { 15 | "type": "json", 16 | "required": true 17 | }, 18 | "devicesTokensCollectionName": { 19 | "type": "string", 20 | "default": "up_users" 21 | }, 22 | "deviceTokenFieldName": { 23 | "type": "string", 24 | "default": "device_token" 25 | }, 26 | "deviceLabelFieldName": { 27 | "type": "string", 28 | "default": "username" 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /server/content-types/fcm-notification/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "fcm_notifications", 4 | "info": { 5 | "singularName": "fcm-notification", 6 | "pluralName": "fcm-notifications", 7 | "displayName": "FCM Notification" 8 | }, 9 | "options": { 10 | "draftAndPublish": true, 11 | "comment": "" 12 | }, 13 | "attributes": { 14 | "title": { 15 | "type": "string", 16 | "required": true 17 | }, 18 | "body": { 19 | "type": "text" 20 | }, 21 | "payload": { 22 | "type": "json" 23 | }, 24 | "image": { 25 | "type": "string" 26 | }, 27 | "targetType": { 28 | "type": "enumeration", 29 | "enum": [ 30 | "topics", 31 | "tokens" 32 | ], 33 | "required": true 34 | }, 35 | "target": { 36 | "type": "text", 37 | "required": true 38 | }, 39 | "response": { 40 | "type": "json" 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /server/routes/content-api/fcm-plugin-configuration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = [ 4 | { 5 | method: 'GET', 6 | path: '/fcm-plugin-configurations', 7 | handler: 'fcm-plugin-configuration.find', 8 | config: { 9 | policies: [], 10 | }, 11 | }, 12 | { 13 | method: 'GET', 14 | path: '/fcm-plugin-configurations/:id', 15 | handler: 'fcm-plugin-configuration.findOne', 16 | config: { 17 | policies: [], 18 | }, 19 | }, 20 | { 21 | method: 'POST', 22 | path: '/fcm-plugin-configurations', 23 | handler: 'fcm-plugin-configuration.create', 24 | config: { 25 | policies: [], 26 | }, 27 | }, 28 | { 29 | method: 'PUT', 30 | path: '/fcm-plugin-configurations/:id', 31 | handler: 'fcm-plugin-configuration.update', 32 | config: { 33 | policies: [], 34 | }, 35 | } 36 | ]; 37 | -------------------------------------------------------------------------------- /server/routes/admin/fcm-target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // router. 4 | 5 | module.exports = [ 6 | { 7 | method: 'GET', 8 | path: '/fcm-targets/count', 9 | handler: 'fcm-target.count', 10 | config: { 11 | policies: [ 12 | { 13 | name: 'admin::hasPermissions', 14 | config: { 15 | actions: ['plugin::users-permissions.roles.read'], 16 | }, 17 | }, 18 | ], 19 | }, 20 | }, 21 | { 22 | method: 'GET', 23 | path: '/fcm-targets', 24 | handler: 'fcm-target.find', 25 | config: { 26 | policies: [ 27 | { 28 | name: 'admin::hasPermissions', 29 | config: { 30 | actions: ['plugin::users-permissions.roles.read'], 31 | }, 32 | }, 33 | ], 34 | }, 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /admin/src/utils/axiosInstance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * axios with a custom config. 3 | */ 4 | 5 | import axios from 'axios'; 6 | import { auth } from '@strapi/helper-plugin'; 7 | import pluginId from "../pluginId"; 8 | 9 | 10 | const instance = axios.create({ 11 | baseURL: process.env.STRAPI_ADMIN_BACKEND_URL + '/' + pluginId, 12 | }); 13 | 14 | instance.interceptors.request.use( 15 | async config => { 16 | config.headers = { 17 | Authorization: `Bearer ${auth.getToken()}`, 18 | Accept: 'application/json', 19 | 'Content-Type': 'application/json', 20 | }; 21 | 22 | return config; 23 | }, 24 | error => { 25 | Promise.reject(error); 26 | } 27 | ); 28 | 29 | instance.interceptors.response.use( 30 | response => response, 31 | error => { 32 | // whatever you want to do with the error 33 | if (error.response?.status === 401) { 34 | auth.clearAppStorage(); 35 | window.location.reload(); 36 | } 37 | 38 | throw error; 39 | } 40 | ); 41 | 42 | export default instance; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 itisnajim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /server/controllers/fcm-plugin-configuration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * controller 5 | */ 6 | 7 | const getService = () => { 8 | return strapi.plugin('strapi-plugin-fcm').service('fcm-plugin-configuration'); 9 | }; 10 | 11 | module.exports = { 12 | async find(ctx) { 13 | try { 14 | ctx.body = await getService().find(ctx.query); 15 | } catch (err) { 16 | ctx.throw(500, err); 17 | } 18 | }, 19 | 20 | async findOne(ctx) { 21 | try { 22 | ctx.body = await getService().findOne(ctx.params.id, ctx.query); 23 | } catch (err) { 24 | ctx.throw(500, err); 25 | } 26 | }, 27 | 28 | async create(ctx) { 29 | try { 30 | ctx.body = await getService().create(ctx.request.body); 31 | } catch (err) { 32 | ctx.throw(500, err); 33 | } 34 | }, 35 | 36 | async update(ctx) { 37 | try { 38 | ctx.body = await getService().update(ctx.params.id, ctx.request.body); 39 | } catch (err) { 40 | ctx.throw(500, err); 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strapi-plugin-fcm", 3 | "version": "1.1.0", 4 | "description": "Send notifications to users or topics using The Google's service: Firebase Cloud Messaging.", 5 | "strapi": { 6 | "displayName": "Firebase - Cloud Messaging", 7 | "name": "strapi-plugin-fcm", 8 | "description": "Send FCM notifications from your Strapi app.", 9 | "kind": "plugin" 10 | }, 11 | "publishConfig": { 12 | "access": "public", 13 | "registry": "https://registry.npmjs.org/" 14 | }, 15 | "peerDependencies": { 16 | "@strapi/strapi": "^4.0.0" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/itisnajim/strapi-plugin-fcm" 21 | }, 22 | "author": { 23 | "name": "itisnajim", 24 | "email": "itisnajim@gmail.com", 25 | "url": "https://itisnajim.com" 26 | }, 27 | "maintainers": [ 28 | { 29 | "name": "itisnajim", 30 | "email": "itisnajim@gmail.com", 31 | "url": "https://itisnajim.com" 32 | } 33 | ], 34 | "engines": { 35 | "node": ">=12.x.x <=20.x.x", 36 | "npm": ">=6.0.0" 37 | }, 38 | "license": "MIT", 39 | "dependencies": { 40 | "firebase-admin": "^12.3.0" 41 | }, 42 | "packageManager": "yarn@3.6.1" 43 | } 44 | -------------------------------------------------------------------------------- /server/routes/content-api/fcm-topic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // router. 4 | 5 | module.exports = [ 6 | { 7 | method: 'GET', 8 | path: '/fcm-topics/count', 9 | handler: 'fcm-topic.count', 10 | config: { 11 | policies: [], 12 | }, 13 | }, 14 | { 15 | method: 'GET', 16 | path: '/fcm-topics', 17 | handler: 'fcm-topic.find', 18 | config: { 19 | policies: [], 20 | }, 21 | }, 22 | { 23 | method: 'GET', 24 | path: '/fcm-topics/:id', 25 | handler: 'fcm-topic.findOne', 26 | config: { 27 | policies: [], 28 | }, 29 | }, 30 | { 31 | method: 'POST', 32 | path: '/fcm-topics', 33 | handler: 'fcm-topic.create', 34 | config: { 35 | policies: [], 36 | }, 37 | }, 38 | { 39 | method: 'PUT', 40 | path: '/fcm-topics/:id', 41 | handler: 'fcm-topic.update', 42 | config: { 43 | policies: [], 44 | }, 45 | }, 46 | { 47 | method: 'DELETE', 48 | path: '/fcm-topics/:id', 49 | handler: 'fcm-topic.delete', 50 | config: { 51 | policies: [], 52 | }, 53 | }, 54 | ]; -------------------------------------------------------------------------------- /server/controllers/fcm-topic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * controller 5 | */ 6 | 7 | const getService = () => { 8 | return strapi.plugin('strapi-plugin-fcm').service('fcm-topic'); 9 | } 10 | 11 | module.exports = { 12 | async find(ctx) { 13 | try { 14 | ctx.body = await getService().find(ctx.query); 15 | } catch (err) { 16 | ctx.throw(500, err); 17 | } 18 | }, 19 | 20 | async findOne(ctx) { 21 | try { 22 | ctx.body = await getService().findOne(ctx.params.id, ctx.query); 23 | } catch (err) { 24 | ctx.throw(500, err); 25 | } 26 | }, 27 | 28 | async delete(ctx) { 29 | try { 30 | ctx.body = await getService().delete(ctx.params.id); 31 | } catch (err) { 32 | ctx.throw(500, err); 33 | } 34 | }, 35 | 36 | async create(ctx) { 37 | try { 38 | ctx.body = await getService().create(ctx.request.body); 39 | } catch (err) { 40 | ctx.throw(500, err); 41 | } 42 | }, 43 | 44 | async update(ctx) { 45 | try { 46 | ctx.body = await getService().update(ctx.params.id, ctx.request.body); 47 | } catch (err) { 48 | ctx.throw(500, err); 49 | } 50 | }, 51 | 52 | async count(ctx) { 53 | try { 54 | ctx.body = await getService().count(ctx.query); 55 | } catch (err) { 56 | ctx.throw(500, err); 57 | } 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /admin/src/pages/App/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This component is the skeleton around the actual pages, and should only 4 | * contain code that should be seen on all pages. (e.g. navigation bar) 5 | * 6 | */ 7 | 8 | import React from 'react'; 9 | import { Switch, Route, useHistory } from 'react-router-dom'; 10 | 11 | import { NotFound } from '@strapi/helper-plugin'; 12 | import pluginId from '../../pluginId'; 13 | import HomePage from '../HomePage'; 14 | import TargetsPage from '../TargetsPage'; 15 | 16 | import Pencil from '@strapi/icons/Pencil'; 17 | import { Box } from '@strapi/design-system/Box'; 18 | import { Button } from '@strapi/design-system/Button'; 19 | 20 | 21 | 22 | const App = () => { 23 | const history = useHistory(); 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default App; 42 | -------------------------------------------------------------------------------- /server/routes/content-api/fcm-notification.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = [ 4 | { 5 | method: 'POST', 6 | path: '/fcm-notifications/send', 7 | handler: 'fcm-notification.send', 8 | config: { 9 | policies: [], 10 | }, 11 | }, 12 | { 13 | method: 'GET', 14 | path: '/fcm-notifications/count', 15 | handler: 'fcm-notification.count', 16 | config: { 17 | policies: [], 18 | }, 19 | }, 20 | { 21 | method: 'GET', 22 | path: '/fcm-notifications', 23 | handler: 'fcm-notification.find', 24 | config: { 25 | policies: [], 26 | }, 27 | }, 28 | { 29 | method: 'GET', 30 | path: '/fcm-notifications/:id', 31 | handler: 'fcm-notification.findOne', 32 | config: { 33 | policies: [], 34 | }, 35 | }, 36 | { 37 | method: 'POST', 38 | path: '/fcm-notifications', 39 | handler: 'fcm-notification.create', 40 | config: { 41 | policies: [], 42 | }, 43 | }, 44 | { 45 | method: 'PUT', 46 | path: '/fcm-notifications/:id', 47 | handler: 'fcm-notification.update', 48 | config: { 49 | policies: [], 50 | }, 51 | }, 52 | { 53 | method: 'DELETE', 54 | path: '/fcm-notifications/:id', 55 | handler: 'fcm-notification.delete', 56 | config: { 57 | policies: [], 58 | }, 59 | }, 60 | ]; -------------------------------------------------------------------------------- /server/controllers/fcm-notification.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * controller 5 | */ 6 | 7 | 8 | const getService = () => { 9 | return strapi.plugin('strapi-plugin-fcm').service('fcm-notification'); 10 | } 11 | 12 | module.exports = { 13 | async find(ctx) { 14 | try { 15 | ctx.body = await getService().find(ctx.query); 16 | } catch (err) { 17 | ctx.throw(500, err); 18 | } 19 | }, 20 | 21 | async findOne(ctx) { 22 | try { 23 | ctx.body = await getService().findOne(ctx.params.id, ctx.query); 24 | } catch (err) { 25 | ctx.throw(500, err); 26 | } 27 | }, 28 | 29 | async delete(ctx) { 30 | try { 31 | ctx.body = await getService().delete(ctx.params.id); 32 | } catch (err) { 33 | ctx.throw(500, err); 34 | } 35 | }, 36 | 37 | async create(ctx) { 38 | try { 39 | ctx.body = await getService().create(ctx.request.body); 40 | } catch (err) { 41 | ctx.throw(500, err); 42 | } 43 | }, 44 | 45 | async update(ctx) { 46 | try { 47 | ctx.body = await getService().update(ctx.params.id, ctx.request.body); 48 | } catch (err) { 49 | ctx.throw(500, err); 50 | } 51 | }, 52 | 53 | async count(ctx) { 54 | try { 55 | ctx.body = await getService().count(ctx.query); 56 | } catch (err) { 57 | ctx.throw(500, err); 58 | } 59 | }, 60 | 61 | async send(ctx) { 62 | try { 63 | ctx.body = await getService().send(ctx.request.body); 64 | } catch (err) { 65 | ctx.throw(500, err); 66 | } 67 | }, 68 | }; 69 | 70 | -------------------------------------------------------------------------------- /admin/src/index.js: -------------------------------------------------------------------------------- 1 | import { prefixPluginTranslations } from '@strapi/helper-plugin'; 2 | import pluginPkg from '../../package.json'; 3 | import pluginId from './pluginId'; 4 | import Initializer from './components/Initializer'; 5 | import PluginIcon from './components/PluginIcon'; 6 | 7 | const name = pluginPkg.strapi.name; 8 | 9 | export default { 10 | register(app) { 11 | app.addMenuLink({ 12 | to: `/plugins/${pluginId}`, 13 | icon: PluginIcon, 14 | intlLabel: { 15 | id: `${pluginId}.plugin.name`, 16 | defaultMessage: name, 17 | }, 18 | Component: async () => { 19 | const component = await import(/* webpackChunkName: "[request]" */ './pages/App'); 20 | 21 | return component; 22 | }, 23 | permissions: [ 24 | // Uncomment to set the permissions of the plugin here 25 | // { 26 | // action: '', // the action name should be plugin::plugin-name.actionType 27 | // subject: null, 28 | // }, 29 | ], 30 | }); 31 | app.registerPlugin({ 32 | id: pluginId, 33 | initializer: Initializer, 34 | isReady: false, 35 | name, 36 | }); 37 | }, 38 | 39 | bootstrap(app) {}, 40 | async registerTrads({ locales }) { 41 | const importedTrads = await Promise.all( 42 | locales.map(locale => { 43 | return import(`./translations/${locale}.json`) 44 | .then(({ default: data }) => { 45 | return { 46 | data: prefixPluginTranslations(data, pluginId), 47 | locale, 48 | }; 49 | }) 50 | .catch(() => { 51 | return { 52 | data: {}, 53 | locale, 54 | }; 55 | }); 56 | }) 57 | ); 58 | 59 | return Promise.resolve(importedTrads); 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /server/content-types/fcm-notification/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fcmUtil = require('../../../util/fcm'); 4 | const uid = 'plugin::strapi-plugin-fcm.fcm-notification'; 5 | const getService = () => { 6 | return strapi.plugin('strapi-plugin-fcm').service('fcm-notification'); 7 | } 8 | module.exports = { 9 | schema: require('./schema'), 10 | lifecycles: { 11 | async afterCreate(event) { 12 | // console.log('afterCreate', event); 13 | if (event?.model?.uid === uid && event?.params?.data?.publishedAt) { 14 | // await fcmUtil.send(event.result); 15 | } 16 | }, 17 | async afterUpdate(event) { 18 | // console.log('afterUpdate', event); 19 | if (event?.model?.uid === uid && event?.params?.data?.publishedAt) { // send if publishedAt is changed ? 20 | // await fcmUtil.send(event.result); 21 | const id = event?.params?.where?.id; 22 | console.log('afterUpdate id', id); 23 | if (id) { 24 | const entry = await getService().findOne(id); 25 | console.log('afterUpdate result', entry); 26 | if (entry?.id && (!entry?.response || (entry?.response && Object.keys(entry.response).length === 0))) { 27 | fcmUtil.send(entry) 28 | .then(async (fcmResponse) => { 29 | console.log('afterUpdate send response', fcmResponse); 30 | await getService().update(id, {data: {response: fcmResponse || {}}}); 31 | }) 32 | .catch(async (error) => { 33 | console.log('afterUpdate send error', error); 34 | await getService().update(id, {data: {response: error || {}}}); 35 | }); 36 | } 37 | } 38 | } 39 | } 40 | }, 41 | }; -------------------------------------------------------------------------------- /server/routes/admin/fcm-plugin-configuration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = [ 4 | { 5 | method: 'GET', 6 | path: '/fcm-plugin-configurations', 7 | handler: 'fcm-plugin-configuration.find', 8 | config: { 9 | policies: [ 10 | { 11 | name: 'admin::hasPermissions', 12 | config: { 13 | actions: ['plugin::users-permissions.roles.read'], 14 | }, 15 | }, 16 | ], 17 | }, 18 | }, 19 | { 20 | method: 'GET', 21 | path: '/fcm-plugin-configurations/:id', 22 | handler: 'fcm-plugin-configuration.findOne', 23 | config: { 24 | policies: [ 25 | { 26 | name: 'admin::hasPermissions', 27 | config: { 28 | actions: ['plugin::users-permissions.roles.read'], 29 | }, 30 | }, 31 | ], 32 | }, 33 | }, 34 | { 35 | method: 'POST', 36 | path: '/fcm-plugin-configurations', 37 | handler: 'fcm-plugin-configuration.create', 38 | config: { 39 | policies: [ 40 | { 41 | name: 'admin::hasPermissions', 42 | config: { 43 | actions: ['plugin::users-permissions.roles.create'], 44 | }, 45 | }, 46 | ], 47 | }, 48 | }, 49 | { 50 | method: 'PUT', 51 | path: '/fcm-plugin-configurations/:id', 52 | handler: 'fcm-plugin-configuration.update', 53 | config: { 54 | policies: [ 55 | { 56 | name: 'admin::hasPermissions', 57 | config: { 58 | actions: ['plugin::users-permissions.roles.update'], 59 | }, 60 | }, 61 | ], 62 | }, 63 | } 64 | ]; 65 | -------------------------------------------------------------------------------- /server/services/fcm-topic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { propOr } = require('lodash/fp'); 4 | 5 | const { 6 | getPaginationInfo, 7 | convertPagedToStartLimit, 8 | shouldCount, 9 | transformPaginationResponse, 10 | } = require('@strapi/strapi'); 11 | 12 | const { getFetchParams } = require('@strapi/strapi/lib/core-api/service'); 13 | 14 | const { 15 | hasDraftAndPublish, 16 | constants: { PUBLISHED_AT_ATTRIBUTE }, 17 | } = require('@strapi/utils').contentTypes; 18 | 19 | const setPublishedAt = data => { 20 | data[PUBLISHED_AT_ATTRIBUTE] = propOr(new Date(), PUBLISHED_AT_ATTRIBUTE, data); 21 | }; 22 | 23 | /** 24 | * service. 25 | */ 26 | 27 | const uid = 'plugin::strapi-plugin-fcm.fcm-topic'; 28 | module.exports = ({ strapi }) => ({ 29 | async find(params = {}) { 30 | 31 | const fetchParams = getFetchParams(params); 32 | const paginationInfo = getPaginationInfo(fetchParams); 33 | const data = await strapi.entityService.findMany(uid, { 34 | ...fetchParams, 35 | ...convertPagedToStartLimit(paginationInfo), 36 | }); 37 | 38 | if (shouldCount(fetchParams)) { 39 | const count = await strapi.entityService.count(uid, { ...fetchParams, ...paginationInfo }); 40 | 41 | return { 42 | data, 43 | pagination: transformPaginationResponse(paginationInfo, count), 44 | }; 45 | } 46 | 47 | return { 48 | data, 49 | pagination: paginationInfo, 50 | }; 51 | }, 52 | 53 | async findOne(entityId, params) { 54 | return strapi.entityService.findOne(uid, entityId, getFetchParams(params)); 55 | }, 56 | 57 | async create(params = {}) { 58 | const { data } = params; 59 | 60 | if (hasDraftAndPublish(contentType)) { 61 | setPublishedAt(data); 62 | } 63 | 64 | return strapi.entityService.create(uid, { ...params, data }); 65 | }, 66 | 67 | async update(entityId, params = {}) { 68 | const { data } = params; 69 | 70 | return strapi.entityService.update(uid, entityId, { ...params, data }); 71 | }, 72 | 73 | async delete(entityId, params = {}) { 74 | return strapi.entityService.delete(uid, entityId, params); 75 | }, 76 | 77 | count(params = {}) { 78 | return strapi.query(uid).count({ where: params }); 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /server/services/fcm-plugin-configuration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * service. 5 | */ 6 | 7 | var fcmUtil = require('../../util/fcm'); 8 | 9 | const { propOr } = require('lodash/fp'); 10 | 11 | const { 12 | getPaginationInfo, 13 | convertPagedToStartLimit, 14 | shouldCount, 15 | transformPaginationResponse, 16 | } = require('@strapi/strapi'); 17 | 18 | const { getFetchParams } = require('@strapi/strapi/lib/core-api/service'); 19 | 20 | const { 21 | hasDraftAndPublish, 22 | constants: { PUBLISHED_AT_ATTRIBUTE }, 23 | } = require('@strapi/utils').contentTypes; 24 | 25 | const setPublishedAt = data => { 26 | data[PUBLISHED_AT_ATTRIBUTE] = propOr(new Date(), PUBLISHED_AT_ATTRIBUTE, data); 27 | }; 28 | 29 | const uid = 'plugin::strapi-plugin-fcm.fcm-plugin-configuration'; 30 | module.exports = ({ strapi }) => ({ 31 | async find(params = {}) { 32 | 33 | const fetchParams = getFetchParams(params); 34 | const paginationInfo = getPaginationInfo(fetchParams); 35 | const data = await strapi.entityService.findMany(uid, { 36 | ...fetchParams, 37 | ...convertPagedToStartLimit(paginationInfo), 38 | }); 39 | 40 | if (shouldCount(fetchParams)) { 41 | const count = await strapi.entityService.count(uid, { ...fetchParams, ...paginationInfo }); 42 | 43 | return { 44 | data, 45 | pagination: transformPaginationResponse(paginationInfo, count), 46 | }; 47 | } 48 | 49 | return { 50 | data, 51 | pagination: paginationInfo, 52 | }; 53 | }, 54 | 55 | async findOne(entityId, params) { 56 | return strapi.entityService.findOne(uid, entityId, getFetchParams(params)); 57 | }, 58 | 59 | async create(params = {}) { 60 | const { data } = params; 61 | const count = strapi.query(uid).count(); 62 | if (count < 1) { 63 | return await strapi.entityService.create(uid, { ...params, data }); 64 | } else if (data.id) { 65 | return await strapi.entityService.update(uid, data.id, { ...params, data }); 66 | } 67 | return { 68 | error: 'Only one configuration is allowed, try passing the id to update the existing one.' 69 | }; 70 | }, 71 | 72 | async update(entityId, params = {}) { 73 | const { data } = params; 74 | const count = strapi.query(uid).count(); 75 | if (count < 1) { 76 | return await strapi.entityService.create(uid, { ...params, data }); 77 | } else { 78 | return await strapi.entityService.update(uid, entityId, { ...params, data }); 79 | } 80 | } 81 | }); 82 | -------------------------------------------------------------------------------- /server/routes/admin/fcm-topic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // router. 4 | 5 | module.exports = [ 6 | { 7 | method: 'GET', 8 | path: '/fcm-topics/count', 9 | handler: 'fcm-topic.count', 10 | config: { 11 | policies: [ 12 | { 13 | name: 'admin::hasPermissions', 14 | config: { 15 | actions: ['plugin::users-permissions.roles.read'], 16 | }, 17 | }, 18 | ], 19 | }, 20 | }, 21 | { 22 | method: 'GET', 23 | path: '/fcm-topics', 24 | handler: 'fcm-topic.find', 25 | config: { 26 | policies: [ 27 | { 28 | name: 'admin::hasPermissions', 29 | config: { 30 | actions: ['plugin::users-permissions.roles.read'], 31 | }, 32 | }, 33 | ], 34 | }, 35 | }, 36 | { 37 | method: 'GET', 38 | path: '/fcm-topics/:id', 39 | handler: 'fcm-topic.findOne', 40 | config: { 41 | policies: [ 42 | { 43 | name: 'admin::hasPermissions', 44 | config: { 45 | actions: ['plugin::users-permissions.roles.read'], 46 | }, 47 | }, 48 | ], 49 | }, 50 | }, 51 | { 52 | method: 'POST', 53 | path: '/fcm-topics', 54 | handler: 'fcm-topic.create', 55 | config: { 56 | policies: [ 57 | { 58 | name: 'admin::hasPermissions', 59 | config: { 60 | actions: ['plugin::users-permissions.roles.create'], 61 | }, 62 | }, 63 | ], 64 | } 65 | }, 66 | { 67 | method: 'PUT', 68 | path: '/fcm-topics/:id', 69 | handler: 'fcm-topic.update', 70 | config: { 71 | policies: [ 72 | { 73 | name: 'admin::hasPermissions', 74 | config: { 75 | actions: ['plugin::users-permissions.roles.update'], 76 | }, 77 | }, 78 | ], 79 | }, 80 | }, 81 | { 82 | method: 'DELETE', 83 | path: '/fcm-topics/:id', 84 | handler: 'fcm-topic.delete', 85 | config: { 86 | policies: [ 87 | { 88 | name: 'admin::hasPermissions', 89 | config: { 90 | actions: ['plugin::users-permissions.roles.delete'], 91 | }, 92 | }, 93 | ], 94 | }, 95 | }, 96 | ]; -------------------------------------------------------------------------------- /server/routes/admin/fcm-notification.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = [ 4 | { 5 | method: 'POST', 6 | path: '/fcm-notifications/send', 7 | handler: 'fcm-notification.send', 8 | config: { 9 | policies: [ 10 | { 11 | name: 'admin::hasPermissions', 12 | config: { 13 | actions: ['plugin::users-permissions.roles.create'], 14 | }, 15 | }, 16 | ], 17 | }, 18 | }, 19 | { 20 | method: 'GET', 21 | path: '/fcm-notifications/count', 22 | handler: 'fcm-notification.count', 23 | config: { 24 | policies: [ 25 | { 26 | name: 'admin::hasPermissions', 27 | config: { 28 | actions: ['plugin::users-permissions.roles.read'], 29 | }, 30 | }, 31 | ], 32 | }, 33 | }, 34 | { 35 | method: 'GET', 36 | path: '/fcm-notifications', 37 | handler: 'fcm-notification.find', 38 | config: { 39 | policies: [ 40 | { 41 | name: 'admin::hasPermissions', 42 | config: { 43 | actions: ['plugin::users-permissions.roles.read'], 44 | }, 45 | }, 46 | ], 47 | }, 48 | }, 49 | { 50 | method: 'GET', 51 | path: '/fcm-notifications/:id', 52 | handler: 'fcm-notification.findOne', 53 | config: { 54 | policies: [ 55 | { 56 | name: 'admin::hasPermissions', 57 | config: { 58 | actions: ['plugin::users-permissions.roles.read'], 59 | }, 60 | }, 61 | ], 62 | }, 63 | }, 64 | { 65 | method: 'POST', 66 | path: '/fcm-notifications', 67 | handler: 'fcm-notification.create', 68 | config: { 69 | policies: [ 70 | { 71 | name: 'admin::hasPermissions', 72 | config: { 73 | actions: ['plugin::users-permissions.roles.create'], 74 | }, 75 | }, 76 | ], 77 | }, 78 | }, 79 | { 80 | method: 'PUT', 81 | path: '/fcm-notifications/:id', 82 | handler: 'fcm-notification.update', 83 | config: { 84 | policies: [ 85 | { 86 | name: 'admin::hasPermissions', 87 | config: { 88 | actions: ['plugin::users-permissions.roles.update'], 89 | }, 90 | }, 91 | ], 92 | }, 93 | }, 94 | { 95 | method: 'DELETE', 96 | path: '/fcm-notifications/:id', 97 | handler: 'fcm-notification.delete', 98 | config: { 99 | policies: [ 100 | { 101 | name: 'admin::hasPermissions', 102 | config: { 103 | actions: ['plugin::users-permissions.roles.delete'], 104 | }, 105 | }, 106 | ], 107 | }, 108 | }, 109 | ]; -------------------------------------------------------------------------------- /server/services/fcm-notification.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * service. 5 | */ 6 | 7 | var fcmUtil = require('../../util/fcm'); 8 | 9 | const { propOr } = require('lodash/fp'); 10 | 11 | const { 12 | getPaginationInfo, 13 | convertPagedToStartLimit, 14 | shouldCount, 15 | transformPaginationResponse, 16 | } = require('@strapi/strapi'); 17 | 18 | const { getFetchParams } = require('@strapi/strapi/lib/core-api/service'); 19 | 20 | const { 21 | hasDraftAndPublish, 22 | constants: { PUBLISHED_AT_ATTRIBUTE }, 23 | } = require('@strapi/utils').contentTypes; 24 | 25 | const setPublishedAt = data => { 26 | data[PUBLISHED_AT_ATTRIBUTE] = propOr(new Date(), PUBLISHED_AT_ATTRIBUTE, data); 27 | }; 28 | 29 | const uid = 'plugin::strapi-plugin-fcm.fcm-notification'; 30 | module.exports = ({ strapi }) => ({ 31 | async find(params = {}) { 32 | 33 | const fetchParams = getFetchParams(params); 34 | const paginationInfo = getPaginationInfo(fetchParams); 35 | const data = await strapi.entityService.findMany(uid, { 36 | ...fetchParams, 37 | ...convertPagedToStartLimit(paginationInfo), 38 | }); 39 | 40 | if (shouldCount(fetchParams)) { 41 | const count = await strapi.entityService.count(uid, { ...fetchParams, ...paginationInfo }); 42 | 43 | return { 44 | data, 45 | pagination: transformPaginationResponse(paginationInfo, count), 46 | }; 47 | } 48 | 49 | return { 50 | data, 51 | pagination: paginationInfo, 52 | }; 53 | }, 54 | 55 | async findOne(entityId, params) { 56 | return strapi.entityService.findOne(uid, entityId, getFetchParams(params)); 57 | }, 58 | 59 | async delete(entityId, params = {}) { 60 | return strapi.entityService.delete(uid, entityId, params); 61 | }, 62 | 63 | async send(body) { 64 | return await fcmUtil.send(body.data); 65 | }, 66 | 67 | async create(params = {}) { 68 | const model = strapi.contentTypes[uid]; 69 | const setupEntry = async (entry) => { 70 | if (hasDraftAndPublish(model)) { 71 | setPublishedAt(entry); 72 | } 73 | if (entry[PUBLISHED_AT_ATTRIBUTE]) { 74 | const fcmResponse = await fcmUtil.send(entry); 75 | entry.response = fcmResponse || {}; 76 | } 77 | entry.payload = entry.payload || {}; 78 | return entry; 79 | }; 80 | const { data } = params; 81 | if (Array.isArray(data)) { 82 | if (data.length > 0) { 83 | const entries = await Promise.all(data.map(d => setupEntry(d))); 84 | return strapi.db.query(uid).createMany({ data: entries }); 85 | } else { 86 | throw Error('Data array is empty!'); 87 | } 88 | } else { 89 | const entry = await setupEntry(data); 90 | return strapi.entityService.create(uid, { data: entry }); 91 | } 92 | }, 93 | 94 | async update(entityId, params = {}) { 95 | const { data } = params; 96 | if (Object.keys(data.response || {}).length === 0) { 97 | const fcmResponse = await fcmUtil.send(data); 98 | data.response = fcmResponse || {}; 99 | } 100 | data.payload = data.payload || {}; 101 | return strapi.entityService.update(uid, entityId, { ...params, data }); 102 | }, 103 | 104 | count(params = {}) { 105 | return strapi.query(uid).count({ where: params }); 106 | }, 107 | }); 108 | -------------------------------------------------------------------------------- /server/services/fcm-target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | getPaginationInfo, 5 | convertPagedToStartLimit, 6 | shouldCount, 7 | transformPaginationResponse, 8 | } = require('@strapi/strapi'); 9 | 10 | const { getFetchParams } = require('@strapi/strapi/lib/core-api/service'); 11 | 12 | 13 | // we will use this for now, until we have a better way. 14 | const getQuery = ( 15 | devicesTokensCollectionName, 16 | deviceTokenFieldName, 17 | deviceLabelFieldName, 18 | offset = 0, 19 | limit = 25 20 | ) => { 21 | if (!devicesTokensCollectionName) { 22 | return `select fcm_topics.name as label, 'topic' as type, fcm_topics.name as value from fcm_topics limit ${limit} offset ${offset}`; 23 | } 24 | return `(select fcm_topics.label as label, 'topic' as type, fcm_topics.name as value from fcm_topics) 25 | union all 26 | (select ${devicesTokensCollectionName}.${deviceLabelFieldName} as label, 'token' as type, ${devicesTokensCollectionName}.${deviceTokenFieldName} as value from ${devicesTokensCollectionName} where coalesce(TRIM(${devicesTokensCollectionName}.${deviceTokenFieldName}), '') <> '') limit ${limit} offset ${offset}`; 27 | 28 | }; 29 | 30 | const countQuery = ( 31 | devicesTokensCollectionName, 32 | deviceTokenFieldName 33 | ) => { 34 | if (!devicesTokensCollectionName) { 35 | return `select count(*) as count from fcm_topics`; 36 | } 37 | return `select count(*) from ((select fcm_topics.name as value from fcm_topics) 38 | union all 39 | (select ${devicesTokensCollectionName}.${deviceTokenFieldName} as value from ${devicesTokensCollectionName} where coalesce(TRIM(${devicesTokensCollectionName}.${deviceTokenFieldName}), '') <> '')) as targets; 40 | `; 41 | }; 42 | 43 | const getConfigurationService = () => { 44 | return strapi.plugin('strapi-plugin-fcm').service('fcm-plugin-configuration'); 45 | } 46 | 47 | module.exports = ({ strapi }) => ({ 48 | 49 | async find(params = {}) { 50 | 51 | const fetchParams = getFetchParams(params); 52 | const paginationInfo = getPaginationInfo(fetchParams); 53 | const startLimit = convertPagedToStartLimit(paginationInfo); 54 | // console.log('startLimit', startLimit, 'paginationInfo', paginationInfo); 55 | 56 | const knex = strapi.db.connection; 57 | 58 | const configs = (await getConfigurationService().find()).data; 59 | // console.log('fcm-target configs', configs); 60 | 61 | const devicesTokensCollectionName = configs.devicesTokensCollectionName; 62 | const deviceTokenFieldName = configs.deviceTokenFieldName; 63 | const deviceLabelFieldName = configs.deviceLabelFieldName; 64 | 65 | const results = await knex.raw( 66 | getQuery(devicesTokensCollectionName, 67 | deviceTokenFieldName, 68 | deviceLabelFieldName, 69 | startLimit.start || 0, 70 | startLimit.limit || 25) 71 | ); 72 | let rows 73 | switch (knex.client.config.client) { 74 | case 'better-sqlite3': { 75 | rows = results 76 | break; 77 | } 78 | default: { 79 | rows = results.rows || results[0]; 80 | break; 81 | } 82 | } 83 | // console.log('fcm-target results', rows); 84 | if (shouldCount(fetchParams)) { 85 | const countResult = await knex.raw(countQuery(devicesTokensCollectionName, deviceTokenFieldName, deviceLabelFieldName)); 86 | const count = (countResult.rows || countResult[0])?.[0]?.count; 87 | // console.log('fcm-target countResult', count); 88 | return { 89 | data: rows, 90 | pagination: transformPaginationResponse(paginationInfo, Number(count)), 91 | }; 92 | } 93 | 94 | return { 95 | data: rows, 96 | pagination: paginationInfo, 97 | }; 98 | 99 | }, 100 | count(params = {}) { 101 | const knex = strapi.db.connection; 102 | return knex.raw(countQuery('fcm_tokens', 'token', 'label')); 103 | }, 104 | 105 | }); 106 | -------------------------------------------------------------------------------- /util/fcm.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const admin = require("firebase-admin"); 4 | 5 | module.exports = { 6 | /* 7 | * Send a message to a device(s) or a topic. 8 | * @param {Object} entry - of type: see the attributes in schema ../server/content-types/fcm-notification/schema.json 9 | * @returns {Promise} 10 | * */ 11 | send: async (entry) => { 12 | console.log('send to FCM', entry); 13 | let payload = { 14 | notification: { 15 | title: entry.title 16 | } 17 | }; 18 | if (entry.body) { 19 | payload.notification.body = entry.body; 20 | } 21 | if (entry.image) { 22 | payload.notification.imageUrl = entry.image; 23 | } 24 | 25 | if (entry.payload) { 26 | try { 27 | let jsonPayload = JSON.parse(entry.payload); 28 | payload = { ...payload, ...jsonPayload }; 29 | } catch { 30 | console.log("parsing failed so sending without payload") 31 | } 32 | } 33 | 34 | let options = { 35 | mutableContent: true 36 | } 37 | 38 | // console.log('payload', payload, 'target is ', entry.target); 39 | let res = null; 40 | if (entry.targetType === 'tokens') { 41 | const tokens = entry.target.split(','); 42 | 43 | /* 44 | * Deprecated Endpoints: 45 | * The Firebase Admin SDK has deprecated the sendMulticast and sendToDevice methods. 46 | * These were causing issues with errors like: 47 | * FirebaseMessagingError: An unknown server error was returned. 48 | * Raw server response: "{"error":"Deprecated endpoint, see https://firebase.google.com/docs/cloud-messaging/migrate-v1"}" 49 | * 50 | * Replacements: 51 | * - sendMulticast() -> sendEachForMulticast() 52 | * - sendToDevice() -> send() with token field 53 | */ 54 | 55 | if (tokens.length > 1) { 56 | // Using sendEachForMulticast() instead of the deprecated sendMulticast() 57 | res = await admin.messaging().sendEachForMulticast({ 58 | tokens: tokens, 59 | notification: payload.notification, 60 | }); 61 | } else { 62 | // Using send() with token instead of the deprecated sendToDevice() 63 | res = await admin.messaging().send({ 64 | token: entry.target, 65 | notification: payload.notification, 66 | }); 67 | } 68 | } else { 69 | const topics = entry.target.split(','); 70 | 71 | /* 72 | * Deprecated Endpoints: 73 | * The Firebase Admin SDK has deprecated the sendToTopic and sendToCondition methods. 74 | * These were causing issues with deprecated endpoint errors. 75 | * 76 | * Replacements: 77 | * - sendToTopic() -> send() with topic field 78 | * - sendToCondition() -> send() with condition field 79 | */ 80 | 81 | if (topics.length > 1) { 82 | // Using send() with condition instead of the deprecated sendToCondition() 83 | const condition = topics.map(t => `'${t}' in topics`).join(' || '); 84 | res = await admin.messaging().send({ 85 | condition: condition, 86 | notification: payload.notification, 87 | }); 88 | } else { 89 | // Using send() with topic instead of the deprecated sendToTopic() 90 | res = await admin.messaging().send({ 91 | topic: entry.target, 92 | notification: payload.notification, 93 | }); 94 | } 95 | } 96 | console.log('send to FCM res', JSON.stringify(res)); 97 | return res; 98 | }, 99 | /* 100 | * Initialize or reinitialize the firebase app 101 | * */ 102 | initialize: async (strapi) => { 103 | // console.log('initialize FCM'); 104 | const data = await strapi.db.query('plugin::strapi-plugin-fcm.fcm-plugin-configuration').findOne({ 105 | select: ['serviceAccount'] 106 | }); 107 | // console.log('serviceAccount', serviceAccount); 108 | // console.log('admin.apps?.length', admin.apps?.length); 109 | if (data !== null && data.serviceAccount) { 110 | if (admin.apps?.length > 1) { 111 | Promise.all(admin.apps.map(app => app.delete())).then(() => { 112 | admin.initializeApp({ 113 | credential: admin.credential.cert(data.serviceAccount) 114 | }); 115 | }); 116 | } else { 117 | admin.initializeApp({ 118 | credential: admin.credential.cert(data.serviceAccount), 119 | }); 120 | } 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Strapi v4 - FCM plugin

4 |

Send FCM notifications from Strapi.

5 | 6 |

7 | 8 | NPM Version 9 | 10 | 11 | Monthly download on NPM 12 | 13 | 14 | codecov.io 15 | 16 |

17 |
18 | 19 |
20 | 21 | ## Installation 22 | 23 | ### From NPM: 24 | ```bash 25 | npm install strapi-plugin-fcm 26 | ``` 27 | 28 | ### From YARN: 29 | ```bash 30 | yarn add strapi-plugin-fcm 31 | ``` 32 | 33 | ### From Git: 34 | 35 | 1. Clone the plugin into your Strapi project 36 | 37 | ```bash 38 | cd //src 39 | 40 | # create plugins folder if not exists 41 | # mkdir plugins 42 | 43 | # go to plugins folder 44 | cd plugins 45 | 46 | # clone the plugin code into a folder and skip the prefix 47 | git clone https://github.com/itisnajim/strapi-plugin-fcm.git strapi-plugin-fcm 48 | # install dependencies 49 | cd strapi-plugin-fcm && yarn install # or npm install 50 | ``` 51 | 52 | 2. Enable the plugin in `/config/plugins.js` . 53 | 54 | ```javascript 55 | module.exports = { 56 | // ... 57 | 'strapi-plugin-fcm': { 58 | enabled: true, 59 | resolve: './src/plugins/strapi-plugin-fcm' // path to plugin folder 60 | }, 61 | // ... 62 | } 63 | ``` 64 | 65 | 3. Build the plugin 66 | 67 | ```bash 68 | # back to project root and build the plugin 69 | yarn build # or npm run build 70 | # start 71 | yarn develop # or npm run develop 72 | ``` 73 | 74 | ## Configuration 75 | - In the Firebase console, open Settings > [Service Accounts](https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk). 76 | - Click Generate New Private Key, then confirm by clicking Generate Key. 77 | - Past the content of your downloaded service account json file into FCM Plugin Configuration > serviceAccount. (like in the picture below, then you may need to restart the server) 78 |
79 | Configuration 80 |
81 | 82 | - In the same interface 'FCM Plugin Configuration', optionally you can provide where the devices tokens are stored, in the picture example above, I store them in User -> **deviceToken** (strapi generate the users database table with the name up_users). 83 | 84 | - Optionally you can provide all the topics you have, in the 'FCM Topic' collection type (via the dashboard or via the api - Post requests). 85 |
86 | Topics 87 |
88 | 89 | ## Usage 90 | ### Via the dashboard 91 | - Enter the notification content. 92 | - Select targets to send to. 93 | - Click send! 94 |
95 | Admin Fcm 96 |
97 |
98 | Admin Fcm 99 |
100 | 101 | ### Another way via the dashboard. 102 | - you can create a new entry in the 'FCM Notification' collection type and click publish to send to the FCM. 103 | 104 | ### Via the api 105 | - First you have to enable and give routes permissions to a specific role or roles. 106 |
107 | Permissions 108 |
109 | - Then via an Http Client (axios, ajax, postman, curl or whatever) send a post request with the body data: 110 | 111 | ```json 112 | { 113 | "data": { 114 | "title": "OKey", 115 | "body": "Test body", 116 | "image": "", 117 | "payload": "", 118 | "targetType": "topics", 119 | //or "targetType": "tokens", 120 | "target": "client_android", 121 | //or multiple topics "target": "client_android,client_ios", 122 | //or "target": "eyJhbGciOiJFUzI1...", 123 | //publishedAt: null //<<- uncomment this if you want to just add an entry as a draft to 'FCM Notification' collection without publishing and sending FCM. 124 | } 125 | } 126 | ``` 127 | 128 | - You can send an array too: 129 | ```json 130 | { 131 | "data": [{...entry1}, {...entry2}, {...entry3}, ...] 132 | } 133 | ``` 134 | 135 | ## Trick 136 | If you have saved the entries in the FCM Notification collection as drafts, you can scheduled them to be sent to FCM at a later time. 137 | - [Scheduled publication](https://docs.strapi.io/developer-docs/latest/guides/scheduled-publication.html) 138 | - [strapi-plugin-publisher 139 | ](https://github.com/ComfortablyCoding/strapi-plugin-publisher) 140 | 141 |
142 | 143 | ## References 144 | 145 | - [Strapi v4 developer documentation](https://docs.strapi.io/) 146 | -------------------------------------------------------------------------------- /admin/src/pages/HomePage/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * HomePage 4 | * 5 | */ 6 | 7 | import React, { memo, useReducer, useState } from 'react'; 8 | import { useHistory, useRouteMatch } from 'react-router-dom'; 9 | 10 | 11 | // import PropTypes from 'prop-types'; 12 | import pluginId from '../../pluginId'; 13 | 14 | import Information from '@strapi/icons/Information'; 15 | import ArrowRight from '@strapi/icons/ArrowRight'; 16 | 17 | 18 | import { Grid, GridItem } from '@strapi/design-system/Grid'; 19 | import { Button } from '@strapi/design-system/Button'; 20 | import { Tooltip } from '@strapi/design-system/Tooltip'; 21 | import { TextInput } from '@strapi/design-system/TextInput'; 22 | import { Textarea } from '@strapi/design-system/Textarea'; 23 | 24 | 25 | const HomePage = () => { 26 | const match = useRouteMatch(); 27 | const history = useHistory(); 28 | 29 | const [title, setTitle] = useState(''); 30 | const [body, setBody] = useState(''); 31 | const [payload, setPayload] = useState(''); 32 | const [image, setImage] = useState(''); 33 | 34 | const handleEntry = event => { 35 | event.preventDefault(); 36 | console.log('You have submitted the form.'); 37 | // check title 38 | if (title.trim().length !== 0) { 39 | const entry = { 40 | title: title, 41 | body: body, 42 | payload: payload, 43 | image: image, 44 | }; 45 | // send data to local storage 46 | localStorage.setItem('fcmLastNotification', JSON.stringify(entry)); 47 | history.push({ pathname: match.url + '/targets', state: entry }); 48 | 49 | } else { 50 | alert('Please enter a title'); 51 | } 52 | }; 53 | return ( 54 |
55 | 60 | 61 |
62 | setTitle(e.target.value)} value={title} 65 | labelAction={ 66 | 73 | } /> 74 |
75 |
76 | 77 |
78 | 89 |
90 |
91 | 92 |
93 | setImage(e.target.value)} value={image} hint="https://example.com/image.png" labelAction={ 94 | 101 | } /> 102 |
103 |
104 | 105 |
106 | Extra payload 107 |
108 | 119 |
120 |
121 |
122 | 123 | 126 | 127 | 128 |
129 |
130 | ); 131 | }; 132 | 133 | export default memo(HomePage); 134 | -------------------------------------------------------------------------------- /admin/src/pages/TargetsPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useState, useEffect, useMemo } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | 4 | import { Box } from '@strapi/design-system/Box'; 5 | import { Typography } from '@strapi/design-system/Typography'; 6 | import { Checkbox } from '@strapi/design-system/Checkbox'; 7 | import { Table, Thead, Tbody, Tr, Td, Th } from '@strapi/design-system/Table'; 8 | import { Dots, NextLink, PageLink, Pagination, PreviousLink } from '@strapi/design-system/Pagination'; 9 | import { Button } from '@strapi/design-system/Button'; 10 | import { Grid, GridItem } from '@strapi/design-system/Grid'; 11 | import PaperPlane from '@strapi/icons/PaperPlane'; 12 | import { Alert } from '@strapi/design-system/Alert'; 13 | 14 | import api from '../../service/api'; 15 | 16 | 17 | const TargetsPage = (props) => { 18 | const history = useHistory(); 19 | 20 | let entry = props?.location?.state; 21 | if (!entry) { 22 | entry = JSON.parse(localStorage.getItem('fcmLastNotification')); 23 | } 24 | if (!entry) { 25 | history.goBack(); 26 | } 27 | 28 | const defaultPageSize = 20; 29 | const [page, setPage] = useState(1); 30 | const [allTargets, setAllTargets] = useState(null); 31 | const [targets, setTargets] = useState(null); 32 | const [fetching, setFetching] = useState(true); 33 | const [selectedTargets, setSelectedTargets] = useState(null); 34 | const [targetsCheckedState, setTargetsCheckedState] = useState({ unchecked: true }); 35 | const [paginationInfo, setPaginationInfo] = useState(null); 36 | const [alertToShow, setAlertToShow] = useState(null); 37 | 38 | 39 | const fetchTargets = async (page, pageSize = defaultPageSize) => { 40 | setFetching(true); 41 | let items = paginate(page, pageSize); 42 | if (items && items.length > 0) { 43 | console.log('targets', items); 44 | setTargets(items); 45 | setFetching(false); 46 | } else { 47 | const res = await api.getTargets(page, pageSize); 48 | items = res.data; 49 | console.log('items', items); 50 | if (!paginationInfo && res.pagination && res.pagination.pageCount > 0) { 51 | console.log('res.pagination', res.pagination); 52 | setPaginationInfo(res.pagination); 53 | } 54 | if (items && items.length > 0) { 55 | insertTargetsAtOffset(items, (page - 1) * pageSize); 56 | setTargets(items); 57 | setFetching(false); 58 | setPage(page + 1); 59 | } 60 | } 61 | }; 62 | useEffect(() => { 63 | fetchTargets(1); 64 | }, []); 65 | 66 | const toggleCheckTarget = (target) => { 67 | let selected = selectedTargets || []; 68 | const index = selected.indexOf(target); 69 | index >= 0 ? selected.splice(index, 1) : selected.push(target); 70 | //console.log('selected', selected.length, 'allTargets', allTargets.length, 'index', index); 71 | if (selected.length >= allTargets.length && selected.length !== 0) { 72 | setTargetsCheckedState({ checked: true }); 73 | } else if (selected.length === 0) { 74 | setTargetsCheckedState({ unchecked: true }); 75 | } else {// Indeterminate state is used to show partially checked states. 76 | setTargetsCheckedState({ indeterminate: true }); 77 | } 78 | setSelectedTargets(selected); 79 | }; 80 | 81 | const toggleCheckAllTargets = () => { 82 | //console.log('allTargets', allTargets, 'selectedTargets', selectedTargets); 83 | let selected = selectedTargets || []; 84 | if (selected.length > 0) { // unselect all 85 | setSelectedTargets([]); 86 | setTargetsCheckedState({ unchecked: true }); 87 | } else { 88 | setSelectedTargets(allTargets ? [...allTargets] : []); 89 | setTargetsCheckedState({ checked: true }); 90 | } 91 | }; 92 | 93 | const isTargetChecked = (target) => { 94 | return (selectedTargets || []).indexOf(target) > -1; 95 | } 96 | 97 | const paginate = (page, pageSize = defaultPageSize) => { 98 | const all = allTargets || [] 99 | return all.slice((page - 1) * pageSize, page * pageSize); 100 | }; 101 | 102 | const insertTargetsAtOffset = (items, offset) => { 103 | const all = allTargets ? [...allTargets] : []; 104 | all.splice(offset, items.length, ...items); 105 | setAllTargets(all); 106 | }; 107 | 108 | const range = (size, startAt = 0) => { 109 | return [...Array(size).keys()].map(i => i + startAt); 110 | }; 111 | 112 | const startPartPagination = () => { 113 | const size = paginationInfo?.pageCount || 1; 114 | if (size < 7) { 115 | return range(size); 116 | } else if (page <= 2) { 117 | return range(4); 118 | } else { 119 | return [0]; 120 | } 121 | /** 122 | 1 ... 3 4 5 ... 7 >> page > 2 && size > 5 123 | 1 2 3 4 ... 7 124 | 1 2 3 4 5 6 125 | */ 126 | }; 127 | 128 | const middlePartPagination = () => { 129 | const size = paginationInfo?.pageCount || 1; 130 | if (size >= 7) { 131 | if (page >= 3 && page < size - 3) { 132 | return range(3, page - 1); 133 | } 134 | } 135 | return undefined; 136 | 137 | /** 138 | 1 ... 3 4 5 ... 7 >> page >= 3 && page < size - 3 139 | 1 ... 18 19 20 21 140 | 1 2 3 4 ... 21 141 | */ 142 | }; 143 | 144 | const endPartPagination = () => { 145 | const size = paginationInfo?.pageCount || 1; 146 | if (size > 5) { 147 | if (page > 3 && page >= size - 3) { 148 | return range(4, size - 4); 149 | } else { 150 | return [size - 1]; 151 | } 152 | } 153 | return undefined; 154 | /** 155 | 1 ... 3 4 5 ... [7] >> size > 5 && 156 | 1 ... 4 5 6 7 >> page > 3 && page >= size - 3 157 | 1 2 3 4 ... 7 >> page < 5 && page < size - 3 158 | */ 159 | 160 | }; 161 | 162 | const sendToSelected = async () => { 163 | const selected = selectedTargets || []; 164 | if (selected.length < 1) { 165 | setAlertToShow({ 166 | title: 'Error', 167 | message: 'One or more targets should be selected to send the fcm message.', 168 | variant: 'danger' 169 | }); 170 | return; 171 | } 172 | 173 | console.log('selected', selected); 174 | const typesValues = selected.reduce((p, n) => { 175 | console.log('p', p, 'n', n, n.type === 'token', n.type === 'topic'); 176 | if (n.type === 'token') { 177 | p.tokens = p.tokens || []; 178 | p.tokens.push(n.value); 179 | }else if (n.type === 'topic') { 180 | p.topics = p.topics || []; 181 | p.topics.push(n.value); 182 | } 183 | return p; 184 | }, {}); 185 | console.log('typesValues', typesValues); 186 | const payload = { 187 | title: entry.title, 188 | body: entry.body, 189 | image: entry.image, 190 | payload: entry.payload 191 | } 192 | const entries = []; 193 | if(typesValues.tokens?.length > 0){ 194 | entries.push({...payload, ...{targetType: 'tokens', target: typesValues.tokens.join(',')}}); 195 | } 196 | if(typesValues.topics?.length > 0){ 197 | entries.push({...payload, ...{targetType: 'topics', target: typesValues.topics.join(',')}}); 198 | } 199 | console.log('entries', entries); 200 | try { 201 | const response = await api.sendFCMs(entries); 202 | console.log('response', response); 203 | setAlertToShow({ 204 | title: 'Sent', 205 | message: 'FCM sent successfully.', 206 | variant: 'success' 207 | }); 208 | } catch (err) { 209 | setAlertToShow({ 210 | title: 'Error', 211 | message: 'Failed to send to FCM. '+JSON.stringify(err || {}), 212 | variant: 'danger' 213 | }); 214 | } 215 | }; 216 | 217 | return ( 218 | <> 219 | {fetching &&
220 | Loading... 221 |
} 222 | {targets && targets.length > 0 ? ( 223 | <> 224 | 225 | 226 | 227 | 230 | 233 | 236 | 239 | 242 | 243 | 244 | 245 | {targets.map((target, idx) => { 246 | return ( 247 | 250 | 253 | 256 | 259 | 262 | ) 263 | })} 264 | 265 |
228 | toggleCheckAllTargets()} {...targetsCheckedState}> 229 | 231 | # 232 | 234 | Label 235 | 237 | Type 238 | 240 | Target 241 |
248 | toggleCheckTarget(target)} checked={isTargetChecked(target)}> 249 | 251 | {idx + 1} 252 | 254 | {target.label} 255 | 257 | {target.type} 258 | 260 | {target.value} 261 |
266 | 267 | 268 | 0) ? page - 1 : 1} pageCount={paginationInfo?.pageCount || 1}> 269 | Go to previous page 270 | {startPartPagination().map(el => 271 | Go to page {el + 1} 272 | 273 | )} 274 | 275 | {middlePartPagination() && Other pages} 276 | {middlePartPagination() && middlePartPagination().map(el => 277 | Go to page {el + 1} 278 | 279 | )} 280 | 281 | {endPartPagination() && Other pages} 282 | {endPartPagination() && endPartPagination().map(el => 283 | Go to page {el + 1} 284 | 285 | )} 286 | Go to next page 287 | 288 | 289 | 290 | ) : 291 | (
292 | No targets found. 293 |

Add topics to 'FCM Topic' Collection, and optionally configure which collection contains the devices tokens.

294 |
)} 295 | 296 | 297 | 298 | Targets Selected: {(selectedTargets || []).length} 299 | 300 | {alertToShow && 301 | setAlertToShow(null)} 303 | closeLabel="Close alert" title={alertToShow.title} variant={alertToShow.variant}>{alertToShow.message} 304 | } 305 | 306 | 311 | 312 | 313 | 314 | 315 | ); 316 | }; 317 | 318 | export default memo(TargetsPage); 319 | --------------------------------------------------------------------------------