├── .gitignore ├── .npmignore ├── gulpfile.js ├── README.md ├── LICENSE.txt ├── .eslintrc ├── package.json ├── src ├── Push.js └── index.js └── dist ├── index.js └── Push.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | config.json 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | gulpfile.js 3 | .eslintrc 4 | .gitignore 5 | config.json 6 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | 4 | gulp.task('transpile-module', () => { 5 | return gulp.src(['./src/index.js', './src/Push.js']) 6 | .pipe(babel({ presets: ['es2015'] })) 7 | .pipe(gulp.dest('./dist/')); 8 | }); 9 | 10 | gulp.task('babel-module', () => { 11 | return gulp.watch(['./src/index.js', './src/Push.js'], ['transpile-module']); 12 | }); 13 | 14 | gulp.task('default', [ 15 | 'transpile-module', 16 | 'babel-module' 17 | ]); 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Push Notification Test Tool 2 | 3 | Simple CLI tool to send push notifications to Android and iOS devices. 4 | 5 | ## Install 6 | 7 | `npm install -g push-notification-test-tool` 8 | 9 | ## Setup 10 | 11 | To set up the tool run: 12 | ``` 13 | pushtester setup --androidSenderAPIKey YOUR_API_KEY --bundle YOUR_BUNDLE_ID --iosCert ABSOLUTE_PATH_TO_p8 --iosTeamId YOUR_TEAM_ID --iosKeyId YOUR_KEY_ID --iosEnv PRODUCTION_OR_DEVELOPMENT 14 | ``` 15 | **Note: this wipes and rebuilds the config file each time so make sure you include all fields.** 16 | 17 | ## Sending a Push Notification 18 | 19 | ``` 20 | pushtester send ios -t TITLE -m MESSAGE tool -d YOUR_DEVICE_TOKEN 21 | ``` 22 | 23 | ``` 24 | pushtester send android -t TITLE -m MESSAGE tool -d YOUR_DEVICE_TOKEN 25 | ``` 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Max Kaplan 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. 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "extends": "airbnb", 6 | "rules": { 7 | "no-use-before-define": ["error", { "functions": false, "classes": false }], 8 | "no-param-reassign": ["error", { "props": false }], 9 | "func-names": ["error", "never"], 10 | "space-before-function-paren": ["error", { 11 | "named": "never", 12 | "anonymous": "never" 13 | }], 14 | "arrow-parens": ["error", "as-needed"], 15 | "no-plusplus": ["error", {"allowForLoopAfterthoughts": true}], 16 | "consistent-return": 0, 17 | "comma-dangle": ["error", "never"], 18 | "arrow-body-style": ["error", "always"], 19 | "no-unused-expressions": 0, 20 | "no-tabs": 0, 21 | "indent": 0, 22 | "no-underscore-dangle": 0, 23 | "newline-per-chained-call": 0, 24 | "no-console": 0 25 | }, 26 | "globals": { 27 | "angular": true, 28 | "ionic": true, 29 | "store": true, 30 | "moment": true, 31 | "window": true, 32 | "PushNotification": true, 33 | "analytics": true, 34 | "io": true, 35 | "document": true, 36 | "AppRate": true, 37 | "AdMob": true, 38 | "facebookConnectPlugin": true, 39 | "StatusBar": true, 40 | "cordova": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "push-notification-test-tool", 3 | "version": "1.0.1", 4 | "description": "CLI tool to send push notifications for Android/iOS", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@github.com/kaplanmaxe/push-notification-test-tool.git" 12 | }, 13 | "keywords": [ 14 | "push", 15 | "notification", 16 | "android", 17 | "ios", 18 | "test", 19 | "tool", 20 | "cli" 21 | ], 22 | "bin": { 23 | "pushtester": "./dist/index.js" 24 | }, 25 | "author": "Max Kaplan", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/kaplanmaxe/push-notification-test-tool/issues" 29 | }, 30 | "homepage": "https://github.com/kaplanmaxe/push-notification-test-tool#readme", 31 | "devDependencies": { 32 | "babel-preset-es2015": "^6.24.0", 33 | "eslint": "^3.19.0", 34 | "eslint-config-airbnb": "^14.1.0", 35 | "eslint-plugin-import": "^2.2.0", 36 | "eslint-plugin-jsx-a11y": "^4.0.0", 37 | "eslint-plugin-react": "^6.10.3", 38 | "gulp": "^3.9.1", 39 | "gulp-babel": "^6.1.2" 40 | }, 41 | "dependencies": { 42 | "apn": "^2.1.4", 43 | "commander": "^2.9.0", 44 | "node-gcm": "^0.14.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Push.js: -------------------------------------------------------------------------------- 1 | import { Provider, Notification } from 'apn'; 2 | import { Sender, Message } from 'node-gcm'; 3 | 4 | export default class Push { 5 | 6 | /** 7 | * Send an iOS push notifications 8 | * 9 | * @param {object} config 10 | * @param {string} message 11 | * @param {string|array} tokens 12 | */ 13 | static ios(config, body, tokens) { 14 | const apnProvider = new Provider({ 15 | token: { 16 | key: config.iosCert, 17 | keyId: config.iosKeyId, 18 | teamId: config.iosTeamId 19 | }, 20 | production: config.iosEnv.toLowerCase() === 'production' 21 | }); 22 | const note = new Notification({ 23 | body: body.message, 24 | title: body.title, 25 | topic: config.bundle 26 | }); 27 | 28 | apnProvider.send(note, tokens) 29 | .then(() => { 30 | apnProvider.shutdown(); 31 | console.log('iOS Push Notification sent!'); 32 | process.exit(1); 33 | }, err => { 34 | console.log(err); 35 | }); 36 | } 37 | 38 | static android(config, body, tokens) { 39 | // Make sure devices is an array. 40 | const registrationTokens = typeof tokens === 'object' ? tokens : [tokens]; 41 | const sender = new Sender(config.androidSenderAPIKey); 42 | 43 | const notification = new Message({ 44 | restrictedPackageName: config.bundle, 45 | notification: { 46 | title: body.title, 47 | body: body.message 48 | } 49 | }); 50 | sender.send(notification, { registrationTokens }, err => { 51 | if (err) throw err; 52 | console.log('Android notification sent!'); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs'; 3 | import program from 'commander'; 4 | import Push from './Push'; 5 | 6 | program 7 | .version('1.0.0'); 8 | 9 | program 10 | .command('setup') 11 | .description('set up enviromentals to send push notifications') 12 | .option('--androidSenderAPIKey [apiKey]', 'Android API Key') 13 | .option('--iosCert ', 'iOS .p8 cert') 14 | .option('--iosTeamId [teamId]', 'iOS Team ID') 15 | .option('--iosKeyId [keyId]', 'iOS Key ID') 16 | .option('--iosEnv [env]', 'iOS Env (Sandbox | Production)') 17 | .option('--bundle [bundleId]', 'Bundle ID') 18 | .action(options => { 19 | fs.writeFile('./config.json', JSON.stringify({ 20 | androidSenderAPIKey: options.androidSenderAPIKey || '', 21 | iosCert: options.iosCert || '', 22 | iosTeamId: options.iosTeamId || '', 23 | iosKeyId: options.iosKeyId || '', 24 | iosEnv: options.iosEnv || '', 25 | bundle: options.bundle || '' 26 | }), err => { 27 | if (err) throw err; 28 | console.log('Config saved successfully!'); 29 | }); 30 | }); 31 | 32 | program 33 | .command('send [os]') 34 | .option('-t, --title [title]', 'Title of Push Notification') 35 | .option('-m, --message [message]', 'Push Notification Message') 36 | .option('-d, --devices [devices]', 'String or array of PN tokens for devices') 37 | .action((os, options) => { 38 | if (!['android', 'ios'].includes(os.toLowerCase())) throw new Error(`${os} is not supported.`); 39 | let config = {}; 40 | if (fs.existsSync('./config.json')) { 41 | config = JSON.parse(fs.readFileSync('./config.json', 'utf8')); 42 | } else { 43 | throw new Error('You must run "pushtester setup" first'); 44 | } 45 | 46 | if (!options.title || !options.message || !options.devices) { 47 | throw new Error('You must specify a title, message, and device tokens. Run pushtester send --help for more information.'); 48 | } 49 | if (os.toLowerCase() === 'ios') { 50 | Push.ios(config, { 51 | title: options.title, 52 | message: options.message 53 | }, options.devices); 54 | } else if (os.toLowerCase() === 'android') { 55 | Push.android(config, { 56 | title: options.title, 57 | message: options.message 58 | }, options.devices); 59 | } 60 | }); 61 | 62 | program.parse(process.argv); 63 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var _fs = require('fs'); 5 | 6 | var _fs2 = _interopRequireDefault(_fs); 7 | 8 | var _commander = require('commander'); 9 | 10 | var _commander2 = _interopRequireDefault(_commander); 11 | 12 | var _Push = require('./Push'); 13 | 14 | var _Push2 = _interopRequireDefault(_Push); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | _commander2.default.version('1.0.0'); 19 | 20 | _commander2.default.command('setup').description('set up enviromentals to send push notifications').option('--androidSenderAPIKey [apiKey]', 'Android API Key').option('--iosCert ', 'iOS .p8 cert').option('--iosTeamId [teamId]', 'iOS Team ID').option('--iosKeyId [keyId]', 'iOS Key ID').option('--iosEnv [env]', 'iOS Env (Sandbox | Production)').option('--bundle [bundleId]', 'Bundle ID').action(function (options) { 21 | _fs2.default.writeFile('./config.json', JSON.stringify({ 22 | androidSenderAPIKey: options.androidSenderAPIKey || '', 23 | iosCert: options.iosCert || '', 24 | iosTeamId: options.iosTeamId || '', 25 | iosKeyId: options.iosKeyId || '', 26 | iosEnv: options.iosEnv || '', 27 | bundle: options.bundle || '' 28 | }), function (err) { 29 | if (err) throw err; 30 | console.log('Config saved successfully!'); 31 | }); 32 | }); 33 | 34 | _commander2.default.command('send [os]').option('-t, --title [title]', 'Title of Push Notification').option('-m, --message [message]', 'Push Notification Message').option('-d, --devices [devices]', 'String or array of PN tokens for devices').action(function (os, options) { 35 | if (!['android', 'ios'].includes(os.toLowerCase())) throw new Error(os + ' is not supported.'); 36 | var config = {}; 37 | if (_fs2.default.existsSync('./config.json')) { 38 | config = JSON.parse(_fs2.default.readFileSync('./config.json', 'utf8')); 39 | } else { 40 | throw new Error('You must run "pushtester setup" first'); 41 | } 42 | 43 | if (!options.title || !options.message || !options.devices) { 44 | throw new Error('You must specify a title, message, and device tokens. Run pushtester send --help for more information.'); 45 | } 46 | if (os.toLowerCase() === 'ios') { 47 | _Push2.default.ios(config, { 48 | title: options.title, 49 | message: options.message 50 | }, options.devices); 51 | } else if (os.toLowerCase() === 'android') { 52 | _Push2.default.android(config, { 53 | title: options.title, 54 | message: options.message 55 | }, options.devices); 56 | } 57 | }); 58 | 59 | _commander2.default.parse(process.argv); -------------------------------------------------------------------------------- /dist/Push.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _apn = require('apn'); 12 | 13 | var _nodeGcm = require('node-gcm'); 14 | 15 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 16 | 17 | var Push = function () { 18 | function Push() { 19 | _classCallCheck(this, Push); 20 | } 21 | 22 | _createClass(Push, null, [{ 23 | key: 'ios', 24 | 25 | 26 | /** 27 | * Send an iOS push notifications 28 | * 29 | * @param {object} config 30 | * @param {string} message 31 | * @param {string|array} tokens 32 | */ 33 | value: function ios(config, body, tokens) { 34 | var apnProvider = new _apn.Provider({ 35 | token: { 36 | key: config.iosCert, 37 | keyId: config.iosKeyId, 38 | teamId: config.iosTeamId 39 | }, 40 | production: config.iosEnv.toLowerCase() === 'production' 41 | }); 42 | var note = new _apn.Notification({ 43 | body: body.message, 44 | title: body.title, 45 | topic: config.bundle 46 | }); 47 | 48 | apnProvider.send(note, tokens).then(function () { 49 | apnProvider.shutdown(); 50 | console.log('iOS Push Notification sent!'); 51 | process.exit(1); 52 | }, function (err) { 53 | console.log(err); 54 | }); 55 | } 56 | }, { 57 | key: 'android', 58 | value: function android(config, body, tokens) { 59 | // Make sure devices is an array. 60 | var registrationTokens = (typeof tokens === 'undefined' ? 'undefined' : _typeof(tokens)) === 'object' ? tokens : [tokens]; 61 | var sender = new _nodeGcm.Sender(config.androidSenderAPIKey); 62 | 63 | var notification = new _nodeGcm.Message({ 64 | restrictedPackageName: config.bundle, 65 | notification: { 66 | title: body.title, 67 | body: body.message 68 | } 69 | }); 70 | sender.send(notification, { registrationTokens: registrationTokens }, function (err) { 71 | if (err) throw err; 72 | console.log('Android notification sent!'); 73 | }); 74 | } 75 | }]); 76 | 77 | return Push; 78 | }(); 79 | 80 | exports.default = Push; --------------------------------------------------------------------------------