├── .npmignore ├── .eslintignore ├── logo.png ├── browserstack.png ├── .prettierrc ├── .csslintrc ├── src ├── index.js ├── push │ ├── index.js │ ├── Messages.js │ ├── Util.js │ ├── Permission.js │ └── Push.js ├── agents │ ├── AbstractAgent.js │ ├── index.js │ ├── MobileFirefoxAgent.js │ ├── WebKitAgent.js │ ├── DesktopAgent.js │ ├── MSAgent.js │ └── MobileChromeAgent.js ├── types.js └── serviceWorker.js ├── .browserslistrc ├── .editorconfig ├── .babelrc ├── bower.json ├── .codeclimate.yml ├── .flowconfig ├── LICENSE.md ├── index.d.ts ├── bin ├── serviceWorker.min.js ├── serviceWorker.min.js.map ├── push.min.js └── push.min.js.map ├── .travis.yml ├── tests ├── browsers.conf.js ├── karma.conf.js └── push.tests.js ├── CONTRIBUTING.md ├── .gitignore ├── package.json ├── rollup.config.js ├── README.md └── .eslintrc.yml /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheTechsTech/push.js/master/logo.png -------------------------------------------------------------------------------- /browserstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheTechsTech/push.js/master/browserstack.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: true 3 | useTabs: false 4 | tabWidth: 4 5 | printWidth: 80 -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | --exclude-exts=.min.css 2 | --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Push } from 'push'; 3 | 4 | export default new Push(typeof window !== 'undefined' ? window : global); 5 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | Firefox >= 22 2 | Chrome >= 5 3 | Safari >= 6 4 | Opera >= 25 5 | Android >= 4.4 6 | Blackberry >= 10 7 | OperaMobile >= 37 8 | FirefoxAndroid >= 47 9 | Samsung >= 4 10 | Explorer >= 9 11 | -------------------------------------------------------------------------------- /src/push/index.js: -------------------------------------------------------------------------------- 1 | import Messages from './Messages'; 2 | import Permission from './Permission'; 3 | import Util from './Util'; 4 | import Push from './Push'; 5 | 6 | export { Messages, Permission, Util, Push }; 7 | -------------------------------------------------------------------------------- /src/agents/AbstractAgent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Global } from 'types'; 3 | 4 | export default class AbstractAgent { 5 | _win: Global; 6 | 7 | constructor(win: Global) { 8 | this._win = win; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | tab_width = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/preset-stage-2" 10 | ], 11 | "plugins": ["@babel/plugin-transform-flow-strip-types"] 12 | } 13 | -------------------------------------------------------------------------------- /src/agents/index.js: -------------------------------------------------------------------------------- 1 | import AbstractAgent from './AbstractAgent'; 2 | import DesktopAgent from './DesktopAgent'; 3 | import MobileChromeAgent from './MobileChromeAgent'; 4 | import MobileFirefoxAgent from './MobileFirefoxAgent'; 5 | import MSAgent from './MSAgent'; 6 | import WebKitAgent from './WebKitAgent'; 7 | 8 | export { 9 | AbstractAgent, 10 | DesktopAgent, 11 | MobileChromeAgent, 12 | MobileFirefoxAgent, 13 | MSAgent, 14 | WebKitAgent 15 | }; 16 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "push.js", 3 | "description": "A compact, cross-browser solution for the Javascript Notifications API", 4 | "main": "bin/push.js", 5 | "authors": [ 6 | "Tyler Nickerson" 7 | ], 8 | "license": "MIT", 9 | "homepage": "https://pushjs.org", 10 | "ignore": [ 11 | "**/.*", 12 | "coverage", 13 | "node_modules", 14 | "bower_components", 15 | "tests", 16 | "src", 17 | "build", 18 | "*.lock", 19 | "*.json" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | csslint: 4 | enabled: true 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - ruby 10 | - javascript 11 | - python 12 | - php 13 | eslint: 14 | enabled: true 15 | fixme: 16 | enabled: true 17 | ratings: 18 | paths: 19 | - "**.css" 20 | - "**.inc" 21 | - "**.js" 22 | - "**.jsx" 23 | - "**.module" 24 | - "**.php" 25 | - "**.py" 26 | - "**.rb" 27 | exclude_paths: 28 | - node_modules/ 29 | - tests/ 30 | - bin/ 31 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | 4 | [include] 5 | .*/src/.* 6 | 7 | [options] 8 | # Add emojis to status messages because emojis are cool 😎 9 | emoji=true 10 | 11 | # Enabled strict argument types 12 | experimental.strict_type_args=true 13 | 14 | # Only run flow on the following file extensions 15 | module.file_ext=.js 16 | 17 | module.name_mapper='^push$' -> '/src/push/' 18 | module.name_mapper='^types$' -> '/src/types.js' 19 | module.name_mapper='^agents$' -> '/src/agents' 20 | 21 | [version] 22 | ^0.57.3 23 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | export type GenericNotification = Notification | webkitNotifications; 2 | 3 | export type Global = { 4 | Notification?: Notification, 5 | webkitNotifications?: webkitNotifications 6 | }; 7 | 8 | export type PushOptions = { 9 | body?: string, 10 | icon?: string, 11 | link?: string, 12 | timeout?: number, 13 | tag?: string, 14 | requireInteraction?: boolean, 15 | vibrate?: boolean, 16 | silent?: boolean, 17 | onClick?: Function, 18 | onError?: Function 19 | }; 20 | 21 | export type PluginManifest = { 22 | plugin: {}, 23 | config?: {} 24 | }; 25 | -------------------------------------------------------------------------------- /src/push/Messages.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const errorPrefix = 'PushError:'; 3 | 4 | export default { 5 | errors: { 6 | incompatible: `${errorPrefix} Push.js is incompatible with browser.`, 7 | invalid_plugin: `${errorPrefix} plugin class missing from plugin manifest (invalid plugin). Please check the documentation.`, 8 | invalid_title: `${errorPrefix} title of notification must be a string`, 9 | permission_denied: `${errorPrefix} permission request declined`, 10 | sw_notification_error: `${errorPrefix} could not show a ServiceWorker notification due to the following reason: `, 11 | sw_registration_error: `${errorPrefix} could not register the ServiceWorker due to the following reason: `, 12 | unknown_interface: `${errorPrefix} unable to create notification: unknown interface` 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/push/Util.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default class Util { 3 | static isUndefined(obj) { 4 | return obj === undefined; 5 | } 6 | 7 | static isString(obj) { 8 | return typeof obj === 'string'; 9 | } 10 | 11 | static isFunction(obj) { 12 | return obj && {}.toString.call(obj) === '[object Function]'; 13 | } 14 | 15 | static isObject(obj) { 16 | return typeof obj === 'object'; 17 | } 18 | 19 | static objectMerge(target, source) { 20 | for (var key in source) { 21 | if ( 22 | target.hasOwnProperty(key) && 23 | this.isObject(target[key]) && 24 | this.isObject(source[key]) 25 | ) { 26 | this.objectMerge(target[key], source[key]); 27 | } else { 28 | target[key] = source[key]; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tyler Nickerson 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 | -------------------------------------------------------------------------------- /src/agents/MobileFirefoxAgent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { AbstractAgent } from 'agents'; 3 | import type { Global, PushOptions } from 'types'; 4 | 5 | /** 6 | * Notification agent for modern desktop browsers: 7 | * Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ 8 | */ 9 | export default class MobileFirefoxAgent extends AbstractAgent { 10 | _win: Global; 11 | 12 | /** 13 | * Returns a boolean denoting support 14 | * @returns {Boolean} boolean denoting whether webkit notifications are supported 15 | */ 16 | isSupported() { 17 | return this._win.navigator.mozNotification !== undefined; 18 | } 19 | 20 | /** 21 | * Creates a new notification 22 | * @param title - notification title 23 | * @param options - notification options array 24 | * @returns {Notification} 25 | */ 26 | create(title: string, options: PushOptions) { 27 | let notification = this._win.navigator.mozNotification.createNotification( 28 | title, 29 | options.body, 30 | options.icon 31 | ); 32 | 33 | notification.show(); 34 | 35 | return notification; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'push.js' { 2 | 3 | export default new Push(); 4 | 5 | class Push { 6 | Permission: PushPermission; 7 | 8 | create(title: string, params?: PushNotificationParams): Promise 9 | 10 | close(tag: string): void; 11 | 12 | clear(): void; 13 | 14 | config(params: PushParams) 15 | } 16 | 17 | export interface PushNotificationParams { 18 | body?: string; 19 | icon?: string; 20 | link?: string; 21 | timeout?: number; 22 | tag?: string; 23 | requireInteraction?: boolean; 24 | vibrate?: boolean; 25 | silent?: boolean; 26 | onClick?: Function; 27 | onError?: Function; 28 | } 29 | 30 | export interface PushParams { 31 | serviceWorker?: string; 32 | fallback?: Function; 33 | } 34 | 35 | export interface PushPermission { 36 | DEFAULT: string; 37 | GRANTED: string; 38 | DENIED: string; 39 | 40 | request(onGranted?: Function, onDenied?: Function): void; 41 | 42 | has(): boolean; 43 | 44 | get(): string; 45 | } 46 | 47 | export interface PushNotification { 48 | close(): void; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/agents/WebKitAgent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { AbstractAgent } from 'agents'; 3 | import type { Global, GenericNotification, PushOptions } from 'types'; 4 | 5 | /** 6 | * Notification agent for old Chrome versions (and some) Firefox 7 | */ 8 | export default class WebKitAgent extends AbstractAgent { 9 | _win: Global; 10 | 11 | /** 12 | * Returns a boolean denoting support 13 | * @returns {Boolean} boolean denoting whether webkit notifications are supported 14 | */ 15 | isSupported() { 16 | return this._win.webkitNotifications !== undefined; 17 | } 18 | 19 | /** 20 | * Creates a new notification 21 | * @param title - notification title 22 | * @param options - notification options array 23 | * @returns {Notification} 24 | */ 25 | create(title: string, options: PushOptions) { 26 | let notification = this._win.webkitNotifications.createNotification( 27 | options.icon, 28 | title, 29 | options.body 30 | ); 31 | 32 | notification.show(); 33 | 34 | return notification; 35 | } 36 | 37 | /** 38 | * Close a given notification 39 | * @param notification - notification to close 40 | */ 41 | close(notification: GenericNotification) { 42 | notification.cancel(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bin/serviceWorker.min.js: -------------------------------------------------------------------------------- 1 | "use strict";function isFunction(obj){return obj&&{}.toString.call(obj)==="[object Function]"}function runFunctionString(funcStr){if(funcStr.trim().length>0){var func=new Function(funcStr);if(isFunction(func)){func()}}}self.addEventListener("message",function(event){self.client=event.source});self.onnotificationclose=function(event){runFunctionString(event.notification.data.onClose);self.client.postMessage(JSON.stringify({id:event.notification.data.id,action:"close"}))};self.onnotificationclick=function(event){var link,origin,href;if(typeof event.notification.data.link!=="undefined"&&event.notification.data.link!==null){origin=event.notification.data.origin;link=event.notification.data.link;href=origin.substring(0,origin.indexOf("/",8))+"/";if(link[0]==="/"){link=link.length>1?link.substring(1,link.length):""}event.notification.close();event.waitUntil(clients.matchAll({type:"window"}).then(function(clientList){var client,full_url;for(var i=0;i ./cc-test-reporter 14 | - chmod +x ./cc-test-reporter 15 | - ./cc-test-reporter before-build 16 | - greenkeeper-lockfile-update 17 | script: 18 | - npm run test 19 | after_script: 20 | - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --exit-code 21 | $TRAVIS_TEST_RESULT; fi 22 | - greenkeeper-lockfile-upload 23 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 24 | -------------------------------------------------------------------------------- /bin/serviceWorker.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["src/serviceWorker.js"],"names":["isFunction","obj","toString","call","runFunctionString","funcStr","trim","length","func","Function","self","addEventListener","event","client","source","onnotificationclose","notification","data","onClose","postMessage","JSON","stringify","id","action","onnotificationclick","link","origin","href","substring","indexOf","close","waitUntil","clients","matchAll","type","then","clientList","full_url","i","url","focus","openWindow","catch","error","Error","message","onClick"],"mappings":"AAAA,aAEA,SAASA,WAAWC,KAChB,OAAOA,QAAUC,SAASC,KAAKF,OAAS,oBAG5C,SAASG,kBAAkBC,SACvB,GAAIA,QAAQC,OAAOC,OAAS,EAAG,CAC3B,IAAIC,KAAO,IAAIC,SAASJ,SACxB,GAAIL,WAAWQ,MAAO,CAClBA,SAKZE,KAAKC,iBAAiB,UAAW,SAASC,OACtCF,KAAKG,OAASD,MAAME,SAGxBJ,KAAKK,oBAAsB,SAASH,OAChCR,kBAAkBQ,MAAMI,aAAaC,KAAKC,SAG1CR,KAAKG,OAAOM,YACRC,KAAKC,WACDC,GAAIV,MAAMI,aAAaC,KAAKK,GAC5BC,OAAQ,YAKpBb,KAAKc,oBAAsB,SAASZ,OAChC,IAAIa,KAAMC,OAAQC,KAElB,UACWf,MAAMI,aAAaC,KAAKQ,OAAS,aACxCb,MAAMI,aAAaC,KAAKQ,OAAS,KACnC,CACEC,OAASd,MAAMI,aAAaC,KAAKS,OACjCD,KAAOb,MAAMI,aAAaC,KAAKQ,KAC/BE,KAAOD,OAAOE,UAAU,EAAGF,OAAOG,QAAQ,IAAK,IAAM,IAGrD,GAAIJ,KAAK,KAAO,IAAK,CACjBA,KAAOA,KAAKlB,OAAS,EAAIkB,KAAKG,UAAU,EAAGH,KAAKlB,QAAU,GAG9DK,MAAMI,aAAac,QAGnBlB,MAAMmB,UACFC,QACKC,UACGC,KAAM,WAETC,KAAK,SAASC,YACX,IAAIvB,OAAQwB,SAEZ,IAAK,IAAIC,EAAI,EAAGA,EAAIF,WAAW7B,OAAQ+B,IAAK,CACxCzB,OAASuB,WAAWE,GACpBD,SAAWV,KAAOF,KAGlB,GACIY,SAASA,SAAS9B,OAAS,KAAO,KAClCM,OAAO0B,IAAI1B,OAAO0B,IAAIhC,OAAS,KAAO,IACxC,CACE8B,UAAY,IAGhB,GAAIxB,OAAO0B,MAAQF,UAAY,UAAWxB,OAAQ,CAC9C,OAAOA,OAAO2B,SAItB,GAAIR,QAAQS,WAAY,CACpB,OAAOT,QAAQS,WAAW,IAAMhB,SAGvCiB,MAAM,SAASC,OACZ,MAAM,IAAIC,MACN,mCAAqCD,MAAME,YAM/DzC,kBAAkBQ,MAAMI,aAAaC,KAAK6B"} -------------------------------------------------------------------------------- /tests/browsers.conf.js: -------------------------------------------------------------------------------- 1 | var 2 | BROWSER_FIREFOX = 'Firefox', 3 | BROWSER_CHROME = 'Chrome', 4 | BROWSER_EDGE = 'Edge', 5 | BROWSER_IE = 'ie', 6 | BROWSER_OPERA = 'Opera', 7 | BROWSER_SAFARI = 'Safari'; 8 | 9 | function getWindowsDesktop(browser, version) { 10 | return { 11 | base: 'BrowserStack', 12 | browser: browser, 13 | browser_version: version.toString() + '.0', 14 | os: 'Windows', 15 | os_version: '10' 16 | } 17 | } 18 | 19 | function getOSXDesktop(browser, version, os) { 20 | var rounded; 21 | 22 | if (!os) 23 | os = (version < 25) ? 'Snow Leopard' : 'Sierra'; 24 | 25 | rounded = Math.floor(version); 26 | 27 | if (version == rounded) 28 | version = version.toString() + '.0'; 29 | 30 | return { 31 | base: 'BrowserStack', 32 | browser: browser, 33 | browser_version: version, 34 | os: 'OS X', 35 | os_version: os 36 | } 37 | } 38 | 39 | function getMobile(browser) { 40 | return { 41 | base: 'BrowserStack', 42 | real_mobile: true, 43 | device: 'Google Pixel', 44 | browser: 'Mobile ' + browser, 45 | os: 'android', 46 | os_version: '7.1' 47 | } 48 | } 49 | 50 | module.exports = { 51 | bs_firefox_mac: getOSXDesktop(BROWSER_FIREFOX, 54), 52 | bs_firefox_mac_old: getOSXDesktop(BROWSER_FIREFOX, 21), 53 | bs_chrome_mac: getOSXDesktop(BROWSER_CHROME, 59), 54 | bs_edge_win: getWindowsDesktop(BROWSER_EDGE, 15), 55 | bs_safari_mac: getOSXDesktop(BROWSER_SAFARI, 10.1, 'Sierra'), 56 | bs_opera_mac: getOSXDesktop(BROWSER_OPERA, 46), 57 | // bs_chrome_mac_old: getOSXDesktop(BROWSER_CHROME, 16), <-- issues testing Push on old Chrome versions in BrowserStack 58 | // bs_firefox_mobile: getMobile(BROWSER_CHROME) <-- can't work because of HTTPS requirement (wth dude) 59 | }; 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | So you want to contribute to Push, huh? Well lucky for you, it's really easy to do so, because you're just dealing with like, a few hundred lines of JavaScript. It's not hard. 3 | 4 | Alright. Now calm down and take a few deep breaths. Here we go. All you have to remember is two commands... think you can do that? 5 | 6 | To BUILD Push, just run: 7 | 8 | ``` 9 | npm run build 10 | ``` 11 | 12 | To TEST Push, run: 13 | 14 | ``` 15 | npm run test 16 | ``` 17 | 18 | See? Not hard at all. Unfortunately the Notifications API doesn't always play nicely with local sites, so don't get discouraged if you try running Push in a local HTML file and it doesn't work. 19 | 20 | ### Testing & Travis ### 21 | Push uses the [Karma](https://karma-runner.github.io/1.0/index.html) JavaScript test runner, so read up on that if you want to make changes to any of the tests that are run. These tests are run post-push by [Travis CI](https://travis-ci.org), so look into that if you want to make any Travis configuration changes. Although, at this point I'd say Travis is all set. The tests might want to be expanded though. 22 | 23 | ### REAL IMPORTANT STUFF ### 24 | **THERE IS ONLY ONE RULE TO PUSH CLUB** (and no, it's not that you can't talk about it). **WHENEVER** you make changes to `Push.js`, **RECOMPILE** and commit `push.min.js` as well. Until this build process can be wrapped into a sexy git hook of some sort, this is how changes to the library need to occur. **YOUR PR WILL NOT BE APPROVED UNLESS THIS HAPPENS**. That said, I did let it slide once because I wasn't thinking, but that's why I wrote this file to make sure it will never happen again. 25 | 26 | Outside of that, contributing should not be at all scary and should be a fun and positive process. Now go out and write some killer JS! Wait... is there even such a thing? 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### OSX ### 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | 31 | ### Node ### 32 | # Logs 33 | logs 34 | *.log 35 | 36 | # Runtime data 37 | pids 38 | *.pid 39 | *.seed 40 | 41 | # Directory for instrumented libs generated by jscoverage/JSCover 42 | lib-cov 43 | 44 | # Coverage directory used by tools like istanbul 45 | coverage 46 | 47 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 48 | .grunt 49 | 50 | # node-waf configuration 51 | .lock-wscript 52 | 53 | # Compiled binary addons (http://nodejs.org/api/addons.html) 54 | build/Release 55 | dist 56 | 57 | # Dependency directory 58 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 59 | node_modules 60 | 61 | .idea 62 | 63 | # User-specific stuff: 64 | .idea/workspace.xml 65 | .idea/tasks.xml 66 | 67 | # Sensitive or high-churn files: 68 | .idea/dataSources/ 69 | .idea/dataSources.ids 70 | .idea/dataSources.xml 71 | .idea/dataSources.local.xml 72 | .idea/sqlDataSources.xml 73 | .idea/dynamic.xml 74 | .idea/uiDesigner.xml 75 | 76 | # Gradle: 77 | .idea/gradle.xml 78 | .idea/libraries 79 | 80 | # Mongo Explorer plugin: 81 | .idea/mongoSettings.xml 82 | 83 | ## File-based project format: 84 | *.iws 85 | 86 | ## Plugin-specific files: 87 | 88 | # IntelliJ 89 | /out/ 90 | 91 | # mpeltonen/sbt-idea plugin 92 | .idea_modules/ 93 | 94 | # JIRA plugin 95 | atlassian-ide-plugin.xml 96 | 97 | # Crashlytics plugin (for Android Studio and IntelliJ) 98 | com_crashlytics_export_strings.xml 99 | crashlytics.properties 100 | crashlytics-build.properties 101 | fabric.properties 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "push.js", 3 | "version": "1.0.5", 4 | "description": 5 | "A compact, cross-browser solution for the Javascript Notifications API", 6 | "main": "bin/push.min.js", 7 | "scripts": { 8 | "clean": "rimraf bin/", 9 | "build": 10 | "rollup -c && uglifyjs --source-map -o bin/serviceWorker.min.js src/serviceWorker.js", 11 | "test": "npm run build && karma start tests/karma.conf.js", 12 | "prepublish": "npm run build", 13 | "precommit": "lint-staged && npm run build && git add ./bin" 14 | }, 15 | "files": ["bin", "*.md", "*.png"], 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/Nickersoft/push.js" 19 | }, 20 | "author": "Tyler Nickerson", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/Nickersoft/push.js/issues" 24 | }, 25 | "homepage": "https://github.com/Nickersoft/push.js", 26 | "devDependencies": { 27 | "@babel/core": "^7.0.0-beta.35", 28 | "@babel/plugin-transform-flow-strip-types": "^7.0.0-beta.35", 29 | "@babel/plugin-transform-strict-mode": "^7.0.0-beta.35", 30 | "@babel/polyfill": "^7.0.0-beta.35", 31 | "@babel/preset-env": "^7.0.0-beta.35", 32 | "@babel/preset-stage-2": "^7.0.0-beta.35", 33 | "browserify": "^16.0.0", 34 | "coveralls": "^3.0.0", 35 | "flow-bin": "^0.65.0", 36 | "husky": "^0.14.3", 37 | "jasmine-core": "^2.8.0", 38 | "js-yaml": "^3.10.0", 39 | "karma": "^2.0.0", 40 | "karma-browserstack-launcher": "^1.3.0", 41 | "karma-coverage": "^1.1.1", 42 | "karma-jasmine": "^1.1.1", 43 | "karma-mocha-reporter": "^2.2.5", 44 | "karma-sourcemap-loader": "^0.3.7", 45 | "lint-staged": "^7.0.0", 46 | "platform": "^1.3.4", 47 | "prettier": "^1.9.2", 48 | "rimraf": "^2.6.2", 49 | "rollup": "^0.56.0", 50 | "rollup-plugin-alias": "^1.4.0", 51 | "rollup-plugin-babel": "^4.0.0-beta.0", 52 | "rollup-plugin-commonjs": "^8.2.6", 53 | "rollup-plugin-node-resolve": "^3.0.0", 54 | "rollup-plugin-uglify": "^2.0.1", 55 | "uglify-es": "^3.2.2" 56 | }, 57 | "lint-staged": { 58 | "*.{js,json,css}": ["prettier --write", "git add"] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import babel from 'rollup-plugin-babel'; 4 | import commonjs from 'rollup-plugin-commonjs'; 5 | import alias from 'rollup-plugin-alias'; 6 | import uglify from 'rollup-plugin-uglify'; 7 | import { minify } from 'uglify-es'; 8 | 9 | const license = `/** 10 | * Push v1.0 11 | * ========= 12 | * A compact, cross-browser solution for the JavaScript Notifications API 13 | * 14 | * Credits 15 | * ------- 16 | * Tsvetan Tsvetkov (ttsvetko) 17 | * Alex Gibson (alexgibson) 18 | * 19 | * License 20 | * ------- 21 | * 22 | * The MIT License (MIT) 23 | * 24 | * Copyright (c) 2015-2017 Tyler Nickerson 25 | * 26 | * Permission is hereby granted, free of charge, to any person obtaining a copy 27 | * of this software and associated documentation files (the "Software"), to deal 28 | * in the Software without restriction, including without limitation the rights 29 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 30 | * copies of the Software, and to permit persons to whom the Software is 31 | * furnished to do so, subject to the following conditions: 32 | * 33 | * The above copyright notice and this permission notice shall be included in 34 | * all copies or substantial portions of the Software. 35 | * 36 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 37 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 38 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 39 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 40 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 41 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 42 | * THE SOFTWARE. 43 | */`; 44 | 45 | export default { 46 | input: 'src/index.js', 47 | output: { 48 | file: 'bin/push.min.js', 49 | format: 'umd', 50 | name: 'Push', 51 | sourcemap: true 52 | }, 53 | banner: license, 54 | plugins: [ 55 | babel({ 56 | exclude: 'node_modules/**' 57 | }), 58 | alias({ 59 | types: path.resolve(__dirname, 'src/types'), 60 | push: path.resolve(__dirname, 'src/push/index'), 61 | agents: path.resolve(__dirname, 'src/agents/index') 62 | }), 63 | commonjs(), 64 | resolve(), 65 | uglify( 66 | { 67 | output: { 68 | beautify: false, 69 | preamble: license 70 | } 71 | }, 72 | minify 73 | ) 74 | ] 75 | }; 76 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function isFunction(obj) { 4 | return obj && {}.toString.call(obj) === '[object Function]'; 5 | } 6 | 7 | function runFunctionString(funcStr) { 8 | if (funcStr.trim().length > 0) { 9 | var func = new Function(funcStr); 10 | if (isFunction(func)) { 11 | func(); 12 | } 13 | } 14 | } 15 | 16 | self.addEventListener('message', function(event) { 17 | self.client = event.source; 18 | }); 19 | 20 | self.onnotificationclose = function(event) { 21 | runFunctionString(event.notification.data.onClose); 22 | 23 | /* Tell Push to execute close callback */ 24 | self.client.postMessage( 25 | JSON.stringify({ 26 | id: event.notification.data.id, 27 | action: 'close' 28 | }) 29 | ); 30 | }; 31 | 32 | self.onnotificationclick = function(event) { 33 | var link, origin, href; 34 | 35 | if ( 36 | typeof event.notification.data.link !== 'undefined' && 37 | event.notification.data.link !== null 38 | ) { 39 | origin = event.notification.data.origin; 40 | link = event.notification.data.link; 41 | href = origin.substring(0, origin.indexOf('/', 8)) + '/'; 42 | 43 | /* Removes prepending slash, as we don't need it */ 44 | if (link[0] === '/') { 45 | link = link.length > 1 ? link.substring(1, link.length) : ''; 46 | } 47 | 48 | event.notification.close(); 49 | 50 | /* This looks to see if the current is already open and focuses if it is */ 51 | event.waitUntil( 52 | clients 53 | .matchAll({ 54 | type: 'window' 55 | }) 56 | .then(function(clientList) { 57 | var client, full_url; 58 | 59 | for (var i = 0; i < clientList.length; i++) { 60 | client = clientList[i]; 61 | full_url = href + link; 62 | 63 | /* Covers case where full_url might be http://example.com/john and the client URL is http://example.com/john/ */ 64 | if ( 65 | full_url[full_url.length - 1] !== '/' && 66 | client.url[client.url.length - 1] === '/' 67 | ) { 68 | full_url += '/'; 69 | } 70 | 71 | if (client.url === full_url && 'focus' in client) { 72 | return client.focus(); 73 | } 74 | } 75 | 76 | if (clients.openWindow) { 77 | return clients.openWindow('/' + link); 78 | } 79 | }) 80 | .catch(function(error) { 81 | throw new Error( 82 | 'A ServiceWorker error occurred: ' + error.message 83 | ); 84 | }) 85 | ); 86 | } 87 | 88 | runFunctionString(event.notification.data.onClick); 89 | }; 90 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Jul 21 2015 22:34:30 GMT-0400 (EDT) 3 | var browsers, selected_browsers; 4 | 5 | browsers = require('./browsers.conf'); 6 | selected_browsers = []; 7 | 8 | for (var browser in browsers) { 9 | selected_browsers.push(browser); 10 | } 11 | 12 | module.exports = function(config) { 13 | config.set({ 14 | // base path that will be used to resolve all patterns (eg. files, exclude) 15 | basePath: '../', 16 | 17 | browserStack: { 18 | username: 'Nickersoft', 19 | accessKey: 'peTScQRRBpSkOGjybGpd' 20 | }, 21 | 22 | coverageReporter: { 23 | // specify a common output directory 24 | dir: 'coverage', 25 | reporters: [ 26 | { 27 | type: 'lcov', 28 | subdir: '.' 29 | } 30 | ] 31 | }, 32 | 33 | // frameworks to use 34 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 35 | frameworks: ['jasmine'], 36 | 37 | plugins: [ 38 | 'karma-jasmine', 39 | 'karma-mocha-reporter', 40 | 'karma-coverage', 41 | 'karma-sourcemap-loader', 42 | 'karma-browserstack-launcher' 43 | ], 44 | 45 | // list of files / patterns to load in the browser 46 | files: [ 47 | './node_modules/platform/platform.js', 48 | './node_modules/@babel/polyfill/dist/polyfill.min.js', 49 | './bin/push.min.js', 50 | './tests/push.tests.js', 51 | './src/serviceWorker.min.js' 52 | ], 53 | 54 | // preprocess matching files before serving them to the browser 55 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 56 | preprocessors: { 57 | './bin/push.js': ['sourcemap', 'coverage'] 58 | }, 59 | 60 | // src results reporter to use 61 | // possible values: 'dots', 'progress' 62 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 63 | reporters: ['mocha', 'coverage'], 64 | 65 | // web server port 66 | port: 9876, 67 | 68 | // enable / disable colors in the output (reporters and logs) 69 | colors: true, 70 | 71 | // level of logging 72 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 73 | logLevel: config.LOG_INFO, 74 | 75 | // enable / disable watching file and executing src whenever any file changes 76 | autoWatch: false, 77 | 78 | // start these browsers 79 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 80 | browsers: selected_browsers, 81 | 82 | // Continuous Integration mode 83 | // if true, Karma captures browsers, runs the src and exits 84 | singleRun: true, 85 | 86 | // custom browser launchers for BrowserStack 87 | customLaunchers: browsers 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [](http://pushjs.org) 4 |

5 | 6 | [![Build Status](https://img.shields.io/travis/Nickersoft/push.js.svg)](https://travis-ci.org/Nickersoft/push.js) 7 | [![Coverage Status](https://img.shields.io/coveralls/Nickersoft/push.js.svg)](https://coveralls.io/github/Nickersoft/push.js?branch=master) 8 | [![Known Vulnerabilities](https://snyk.io/test/github/nickersoft/push.js/badge.svg)](https://snyk.io/test/github/nickersoft/push.js) 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/52747084d9786c1570df/maintainability)](https://codeclimate.com/github/Nickersoft/push.js/maintainability) 10 | 11 | [![npm version](https://img.shields.io/npm/v/push.js.svg)](https://npmjs.com/package/push.js) 12 | [![npm](https://img.shields.io/npm/dm/push.js.svg)](https://npmjs.com/package/push.js) 13 | [![Greenkeeper badge](https://badges.greenkeeper.io/Nickersoft/push.js.svg)](https://greenkeeper.io/) 14 | 15 | *Now a proud user of* 16 | 17 | [](https://browserstack.com) 18 | 19 |
20 | 21 | > ## Important Notice 22 | > Push is currently looking for co-maintainers of the repo. The guy who originally made this library, [Tyler Nickerson](https://tylernickerson.com), while still visiting this repo from time to time, is busy trying to work on his company [Linguistic](https://github.com/linguistic) right now. As a result, he may not have time to answer everyone or fix bugs as quickly as they would like him too. If you find it pretty easy to find your way around this code and think you could help some people out, shoot me a message at [nickersoft@gmail.com](mailto:nickersoft@gmail.com) and let's talk. 23 | 24 | ### What is Push? ### 25 | 26 | Push is the fastest way to get up and running with Javascript desktop notifications. A fairly new addition to the 27 | official specification, the Notification API allows modern browsers such as Chrome, Safari, Firefox, and IE 9+ to push 28 | notifications to a user's desktop. Push acts as a cross-browser solution to this API, falling back to use older 29 | implementations if the user's browser does not support the new API. 30 | 31 | You can quickly install Push via [npm](http://npmjs.com): 32 | 33 | ``` 34 | npm install push.js --save 35 | ``` 36 | 37 | Or, if you want something a little more lightweight, you can give [Bower](http://bower.io) a try: 38 | 39 | ``` 40 | bower install push.js --save 41 | ``` 42 | 43 | ### Full Documentation ### 44 | Full documentation for Push can be found at the project's new homepage [https://pushjs.org](https://pushjs.org). 45 | See you there! 46 | 47 | ### Development ### 48 | 49 | If you feel like this library is your jam and you want to contribute (or you think I'm an idiot who missed something), 50 | check out Push's neat [contributing guidelines](CONTRIBUTING.md) on how you can make your mark. 51 | 52 | ### Credits ### 53 | Push is based off work the following work: 54 | 55 | 1. [HTML5-Desktop-Notifications](https://github.com/ttsvetko/HTML5-Desktop-Notifications) by [Tsvetan Tsvetkov](https://github.com/ttsvetko) 56 | 2. [notify.js](https://github.com/alexgibson/notify.js) by [Alex Gibson](https://github.com/alexgibson) 57 | -------------------------------------------------------------------------------- /src/agents/MobileChromeAgent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Util, Messages } from 'push'; 3 | import { AbstractAgent } from 'agents'; 4 | import type { Global, GenericNotification, PushOptions } from 'types'; 5 | 6 | /** 7 | * Notification agent for modern desktop browsers: 8 | * Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ 9 | */ 10 | export default class MobileChromeAgent extends AbstractAgent { 11 | _win: Global; 12 | 13 | /** 14 | * Returns a boolean denoting support 15 | * @returns {Boolean} boolean denoting whether webkit notifications are supported 16 | */ 17 | isSupported() { 18 | return ( 19 | this._win.navigator !== undefined && 20 | this._win.navigator.serviceWorker !== undefined 21 | ); 22 | } 23 | 24 | /** 25 | * Returns the function body as a string 26 | * @param func 27 | */ 28 | getFunctionBody(func: () => void) { 29 | const str = func.toString().match(/function[^{]+{([\s\S]*)}$/); 30 | return typeof str !== 'undefined' && str !== null && str.length > 1 31 | ? str[1] 32 | : null; 33 | } 34 | 35 | /** 36 | * Creates a new notification 37 | * @param id ID of notification 38 | * @param title Title of notification 39 | * @param options Options object 40 | * @param serviceWorker ServiceWorker path 41 | * @param callback Callback function 42 | */ 43 | create( 44 | id: number, 45 | title: string, 46 | options: PushOptions, 47 | serviceWorker: string, 48 | callback: (GenericNotification[]) => void 49 | ) { 50 | /* Register ServiceWorker */ 51 | this._win.navigator.serviceWorker.register(serviceWorker); 52 | 53 | this._win.navigator.serviceWorker.ready 54 | .then(registration => { 55 | /* Local data the service worker will use */ 56 | let localData = { 57 | id: id, 58 | link: options.link, 59 | origin: document.location.href, 60 | onClick: Util.isFunction(options.onClick) 61 | ? this.getFunctionBody(options.onClick) 62 | : '', 63 | onClose: Util.isFunction(options.onClose) 64 | ? this.getFunctionBody(options.onClose) 65 | : '' 66 | }; 67 | 68 | /* Merge the local data with user-provided data */ 69 | if (options.data !== undefined && options.data !== null) 70 | localData = Object.assign(localData, options.data); 71 | 72 | /* Show the notification */ 73 | registration 74 | .showNotification(title, { 75 | icon: options.icon, 76 | body: options.body, 77 | vibrate: options.vibrate, 78 | tag: options.tag, 79 | data: localData, 80 | requireInteraction: options.requireInteraction, 81 | silent: options.silent 82 | }) 83 | .then(() => { 84 | registration.getNotifications().then(notifications => { 85 | /* Send an empty message so the ServiceWorker knows who the client is */ 86 | registration.active.postMessage(''); 87 | 88 | /* Trigger callback */ 89 | callback(notifications); 90 | }); 91 | }) 92 | .catch(function(error) { 93 | throw new Error( 94 | Messages.errors.sw_notification_error + 95 | error.message 96 | ); 97 | }); 98 | }) 99 | .catch(function(error) { 100 | throw new Error( 101 | Messages.errors.sw_registration_error + error.message 102 | ); 103 | }); 104 | } 105 | 106 | /** 107 | * Close all notification 108 | */ 109 | close() { 110 | // Can't do this with service workers 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/push/Permission.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Global } from 'types'; 3 | 4 | export default class Permission { 5 | // Private members 6 | _permissions: string[]; 7 | _win: Global; 8 | 9 | // Public members 10 | GRANTED: string; 11 | DEFAULT: string; 12 | DENIED: string; 13 | 14 | constructor(win: Global) { 15 | this._win = win; 16 | this.GRANTED = 'granted'; 17 | this.DEFAULT = 'default'; 18 | this.DENIED = 'denied'; 19 | this._permissions = [this.GRANTED, this.DEFAULT, this.DENIED]; 20 | } 21 | 22 | /** 23 | * Requests permission for desktop notifications 24 | * @param {Function} onGranted - Function to execute once permission is granted 25 | * @param {Function} onDenied - Function to execute once permission is denied 26 | * @return {void, Promise} 27 | */ 28 | request(onGranted: () => void, onDenied: () => void) { 29 | return arguments.length > 0 30 | ? this._requestWithCallback(...arguments) 31 | : this._requestAsPromise(); 32 | } 33 | 34 | /** 35 | * Old permissions implementation deprecated in favor of a promise based one 36 | * @deprecated Since V1.0.4 37 | * @param {Function} onGranted - Function to execute once permission is granted 38 | * @param {Function} onDenied - Function to execute once permission is denied 39 | * @return {void} 40 | */ 41 | _requestWithCallback(onGranted: () => void, onDenied: () => void) { 42 | const existing = this.get(); 43 | 44 | var resolve = (result = this._win.Notification.permission) => { 45 | if (typeof result === 'undefined' && this._win.webkitNotifications) 46 | result = this._win.webkitNotifications.checkPermission(); 47 | if (result === this.GRANTED || result === 0) { 48 | if (onGranted) onGranted(); 49 | } else if (onDenied) onDenied(); 50 | }; 51 | 52 | /* Permissions already set */ 53 | if (existing !== this.DEFAULT) { 54 | resolve(existing); 55 | } else if ( 56 | this._win.webkitNotifications && 57 | this._win.webkitNotifications.checkPermission 58 | ) { 59 | /* Safari 6+, Legacy webkit browsers */ 60 | this._win.webkitNotifications.requestPermission(resolve); 61 | } else if ( 62 | this._win.Notification && 63 | this._win.Notification.requestPermission 64 | ) { 65 | /* Chrome 23+ */ 66 | this._win.Notification 67 | .requestPermission() 68 | .then(resolve) 69 | .catch(function() { 70 | if (onDenied) onDenied(); 71 | }); 72 | } else if (onGranted) { 73 | /* Let the user continue by default */ 74 | onGranted(); 75 | } 76 | } 77 | 78 | /** 79 | * Requests permission for desktop notifications in a promise based way 80 | * @return {Promise} 81 | */ 82 | _requestAsPromise(): Promise { 83 | const existing = this.get(); 84 | 85 | let isGranted = result => result === this.GRANTED || result === 0; 86 | 87 | /* Permissions already set */ 88 | var hasPermissions = existing !== this.DEFAULT; 89 | 90 | /* Safari 6+, Chrome 23+ */ 91 | var isModernAPI = 92 | this._win.Notification && this._win.Notification.requestPermission; 93 | 94 | /* Legacy webkit browsers */ 95 | var isWebkitAPI = 96 | this._win.webkitNotifications && 97 | this._win.webkitNotifications.checkPermission; 98 | 99 | return new Promise((resolvePromise, rejectPromise) => { 100 | var resolver = result => 101 | isGranted(result) ? resolvePromise() : rejectPromise(); 102 | 103 | if (hasPermissions) { 104 | resolver(existing); 105 | } else if (isWebkitAPI) { 106 | this._win.webkitNotifications.requestPermission(result => { 107 | resolver(result); 108 | }); 109 | } else if (isModernAPI) { 110 | this._win.Notification 111 | .requestPermission() 112 | .then(result => { 113 | resolver(result); 114 | }) 115 | .catch(rejectPromise); 116 | } else resolvePromise(); 117 | }); 118 | } 119 | 120 | /** 121 | * Returns whether Push has been granted permission to run 122 | * @return {Boolean} 123 | */ 124 | has() { 125 | return this.get() === this.GRANTED; 126 | } 127 | 128 | /** 129 | * Gets the permission level 130 | * @return {Permission} The permission level 131 | */ 132 | get() { 133 | let permission; 134 | 135 | /* Safari 6+, Chrome 23+ */ 136 | if (this._win.Notification && this._win.Notification.permission) 137 | permission = this._win.Notification.permission; 138 | else if ( 139 | this._win.webkitNotifications && 140 | this._win.webkitNotifications.checkPermission 141 | ) 142 | /* Legacy webkit browsers */ 143 | permission = this._permissions[ 144 | this._win.webkitNotifications.checkPermission() 145 | ]; 146 | else if (navigator.mozNotification) 147 | /* Firefox Mobile */ 148 | permission = this.GRANTED; 149 | else if (this._win.external && this._win.external.msIsSiteMode) 150 | /* IE9+ */ 151 | permission = this._win.external.msIsSiteMode() 152 | ? this.GRANTED 153 | : this.DEFAULT; 154 | else permission = this.GRANTED; 155 | 156 | return permission; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | parserOptions: 3 | sourceType: module 4 | ecmaFeatures: 5 | jsx: true 6 | 7 | env: 8 | amd: true 9 | browser: true 10 | es6: true 11 | jquery: true 12 | node: true 13 | 14 | # http://eslint.org/docs/rules/ 15 | rules: 16 | # Possible Errors 17 | no-await-in-loop: off 18 | no-cond-assign: error 19 | no-console: off 20 | no-constant-condition: error 21 | no-control-regex: error 22 | no-debugger: error 23 | no-dupe-args: error 24 | no-dupe-keys: error 25 | no-duplicate-case: error 26 | no-empty-character-class: error 27 | no-empty: error 28 | no-ex-assign: error 29 | no-extra-boolean-cast: error 30 | no-extra-parens: off 31 | no-extra-semi: error 32 | no-func-assign: error 33 | no-inner-declarations: 34 | - error 35 | - functions 36 | no-invalid-regexp: error 37 | no-irregular-whitespace: error 38 | no-negated-in-lhs: error 39 | no-obj-calls: error 40 | no-prototype-builtins: off 41 | no-regex-spaces: error 42 | no-sparse-arrays: error 43 | no-template-curly-in-string: off 44 | no-unexpected-multiline: error 45 | no-unreachable: error 46 | no-unsafe-finally: off 47 | no-unsafe-negation: off 48 | use-isnan: error 49 | valid-jsdoc: off 50 | valid-typeof: error 51 | 52 | # Best Practices 53 | accessor-pairs: error 54 | array-callback-return: off 55 | block-scoped-var: off 56 | class-methods-use-this: off 57 | complexity: 58 | - error 59 | - 6 60 | consistent-return: off 61 | curly: off 62 | default-case: off 63 | dot-location: off 64 | dot-notation: off 65 | eqeqeq: error 66 | guard-for-in: error 67 | no-alert: error 68 | no-caller: error 69 | no-case-declarations: error 70 | no-div-regex: error 71 | no-else-return: off 72 | no-empty-function: off 73 | no-empty-pattern: error 74 | no-eq-null: error 75 | no-eval: error 76 | no-extend-native: error 77 | no-extra-bind: error 78 | no-extra-label: off 79 | no-fallthrough: error 80 | no-floating-decimal: off 81 | no-global-assign: off 82 | no-implicit-coercion: off 83 | no-implied-eval: error 84 | no-invalid-this: off 85 | no-iterator: error 86 | no-labels: 87 | - error 88 | - allowLoop: true 89 | allowSwitch: true 90 | no-lone-blocks: error 91 | no-loop-func: error 92 | no-magic-number: off 93 | no-multi-spaces: off 94 | no-multi-str: off 95 | no-native-reassign: error 96 | no-new-func: error 97 | no-new-wrappers: error 98 | no-new: error 99 | no-octal-escape: error 100 | no-octal: error 101 | no-param-reassign: off 102 | no-proto: error 103 | no-redeclare: error 104 | no-restricted-properties: off 105 | no-return-assign: error 106 | no-return-await: off 107 | no-script-url: error 108 | no-self-assign: off 109 | no-self-compare: error 110 | no-sequences: off 111 | no-throw-literal: off 112 | no-unmodified-loop-condition: off 113 | no-unused-expressions: error 114 | no-unused-labels: off 115 | no-useless-call: error 116 | no-useless-concat: error 117 | no-useless-escape: off 118 | no-useless-return: off 119 | no-void: error 120 | no-warning-comments: off 121 | no-with: error 122 | prefer-promise-reject-errors: off 123 | radix: error 124 | require-await: off 125 | vars-on-top: off 126 | wrap-iife: error 127 | yoda: off 128 | 129 | # Strict 130 | strict: off 131 | 132 | # Variables 133 | init-declarations: off 134 | no-catch-shadow: error 135 | no-delete-var: error 136 | no-label-var: error 137 | no-restricted-globals: off 138 | no-shadow-restricted-names: error 139 | no-shadow: off 140 | no-undef-init: error 141 | no-undef: off 142 | no-undefined: off 143 | no-unused-vars: off 144 | no-use-before-define: off 145 | 146 | # Node.js and CommonJS 147 | callback-return: error 148 | global-require: error 149 | handle-callback-err: error 150 | no-mixed-requires: off 151 | no-new-require: off 152 | no-path-concat: error 153 | no-process-env: off 154 | no-process-exit: error 155 | no-restricted-modules: off 156 | no-sync: off 157 | 158 | # Stylistic Issues 159 | array-bracket-spacing: off 160 | block-spacing: off 161 | brace-style: off 162 | camelcase: off 163 | capitalized-comments: off 164 | comma-dangle: 165 | - error 166 | - never 167 | comma-spacing: off 168 | comma-style: off 169 | computed-property-spacing: off 170 | consistent-this: off 171 | eol-last: off 172 | func-call-spacing: off 173 | func-name-matching: off 174 | func-names: off 175 | func-style: off 176 | id-length: off 177 | id-match: off 178 | indent: off 179 | jsx-quotes: off 180 | key-spacing: off 181 | keyword-spacing: off 182 | line-comment-position: off 183 | linebreak-style: off 184 | lines-around-comment: off 185 | lines-around-directive: off 186 | max-depth: off 187 | max-len: off 188 | max-nested-callbacks: off 189 | max-params: off 190 | max-statements-per-line: off 191 | max-statements: 192 | - error 193 | - 30 194 | multiline-ternary: off 195 | new-cap: off 196 | new-parens: off 197 | newline-after-var: off 198 | newline-before-return: off 199 | newline-per-chained-call: off 200 | no-array-constructor: off 201 | no-bitwise: off 202 | no-continue: off 203 | no-inline-comments: off 204 | no-lonely-if: off 205 | no-mixed-operators: off 206 | no-mixed-spaces-and-tabs: off 207 | no-multi-assign: off 208 | no-multiple-empty-lines: off 209 | no-negated-condition: off 210 | no-nested-ternary: off 211 | no-new-object: off 212 | no-plusplus: off 213 | no-restricted-syntax: off 214 | no-spaced-func: off 215 | no-tabs: off 216 | no-ternary: off 217 | no-trailing-spaces: off 218 | no-underscore-dangle: off 219 | no-unneeded-ternary: off 220 | object-curly-newline: off 221 | object-curly-spacing: off 222 | object-property-newline: off 223 | one-var-declaration-per-line: off 224 | one-var: off 225 | operator-assignment: off 226 | operator-linebreak: off 227 | padded-blocks: off 228 | quote-props: off 229 | quotes: off 230 | require-jsdoc: off 231 | semi-spacing: off 232 | semi: off 233 | sort-keys: off 234 | sort-vars: off 235 | space-before-blocks: off 236 | space-before-function-paren: off 237 | space-in-parens: off 238 | space-infix-ops: off 239 | space-unary-ops: off 240 | spaced-comment: off 241 | template-tag-spacing: off 242 | unicode-bom: off 243 | wrap-regex: off 244 | 245 | # ECMAScript 6 246 | arrow-body-style: off 247 | arrow-parens: off 248 | arrow-spacing: off 249 | constructor-super: off 250 | generator-star-spacing: off 251 | no-class-assign: off 252 | no-confusing-arrow: off 253 | no-const-assign: off 254 | no-dupe-class-members: off 255 | no-duplicate-imports: off 256 | no-new-symbol: off 257 | no-restricted-imports: off 258 | no-this-before-super: off 259 | no-useless-computed-key: off 260 | no-useless-constructor: off 261 | no-useless-rename: off 262 | no-var: off 263 | object-shorthand: off 264 | prefer-arrow-callback: off 265 | prefer-const: off 266 | prefer-destructuring: off 267 | prefer-numeric-literals: off 268 | prefer-rest-params: off 269 | prefer-reflect: off 270 | prefer-spread: off 271 | prefer-template: off 272 | require-yield: off 273 | rest-spread-spacing: off 274 | sort-imports: off 275 | symbol-description: off 276 | template-curly-spacing: off 277 | yield-star-spacing: off 278 | -------------------------------------------------------------------------------- /bin/push.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Push v1.0 3 | * ========= 4 | * A compact, cross-browser solution for the JavaScript Notifications API 5 | * 6 | * Credits 7 | * ------- 8 | * Tsvetan Tsvetkov (ttsvetko) 9 | * Alex Gibson (alexgibson) 10 | * 11 | * License 12 | * ------- 13 | * 14 | * The MIT License (MIT) 15 | * 16 | * Copyright (c) 2015-2017 Tyler Nickerson 17 | * 18 | * Permission is hereby granted, free of charge, to any person obtaining a copy 19 | * of this software and associated documentation files (the "Software"), to deal 20 | * in the Software without restriction, including without limitation the rights 21 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | * copies of the Software, and to permit persons to whom the Software is 23 | * furnished to do so, subject to the following conditions: 24 | * 25 | * The above copyright notice and this permission notice shall be included in 26 | * all copies or substantial portions of the Software. 27 | * 28 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 34 | * THE SOFTWARE. 35 | */ 36 | !function(i,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):i.Push=t()}(this,function(){"use strict";function i(t){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i})(t)}function t(i,t){if(!(i instanceof t))throw new TypeError("Cannot call a class as a function")}function n(i,t){for(var n=0;n0?this._requestWithCallback.apply(this,arguments):this._requestAsPromise()}},{key:"_requestWithCallback",value:function(i,t){var n=this,e=this.get(),o=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:n._win.Notification.permission;void 0===e&&n._win.webkitNotifications&&(e=n._win.webkitNotifications.checkPermission()),e===n.GRANTED||0===e?i&&i():t&&t()};e!==this.DEFAULT?o(e):this._win.webkitNotifications&&this._win.webkitNotifications.checkPermission?this._win.webkitNotifications.requestPermission(o):this._win.Notification&&this._win.Notification.requestPermission?this._win.Notification.requestPermission().then(o).catch(function(){t&&t()}):i&&i()}},{key:"_requestAsPromise",value:function(){var i=this,t=this.get(),n=t!==this.DEFAULT,e=this._win.Notification&&this._win.Notification.requestPermission,o=this._win.webkitNotifications&&this._win.webkitNotifications.checkPermission;return new Promise(function(r,s){var c=function(t){return function(t){return t===i.GRANTED||0===t}(t)?r():s()};n?c(t):o?i._win.webkitNotifications.requestPermission(function(i){c(i)}):e?i._win.Notification.requestPermission().then(function(i){c(i)}).catch(s):r()})}},{key:"has",value:function(){return this.get()===this.GRANTED}},{key:"get",value:function(){return this._win.Notification&&this._win.Notification.permission?this._win.Notification.permission:this._win.webkitNotifications&&this._win.webkitNotifications.checkPermission?this._permissions[this._win.webkitNotifications.checkPermission()]:navigator.mozNotification?this.GRANTED:this._win.external&&this._win.external.msIsSiteMode?this._win.external.msIsSiteMode()?this.GRANTED:this.DEFAULT:this.GRANTED}}]),i}(),u=function(){function n(){t(this,n)}return e(n,null,[{key:"isUndefined",value:function(i){return void 0===i}},{key:"isString",value:function(i){return"string"==typeof i}},{key:"isFunction",value:function(i){return i&&"[object Function]"==={}.toString.call(i)}},{key:"isObject",value:function(t){return"object"===i(t)}},{key:"objectMerge",value:function(i,t){for(var n in t)i.hasOwnProperty(n)&&this.isObject(i[n])&&this.isObject(t[n])?this.objectMerge(i[n],t[n]):i[n]=t[n]}}]),n}(),f=function i(n){t(this,i),this._win=n},l=function(i){function n(){return t(this,n),r(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return o(n,f),e(n,[{key:"isSupported",value:function(){return void 0!==this._win.Notification}},{key:"create",value:function(i,t){return new this._win.Notification(i,{icon:u.isString(t.icon)||u.isUndefined(t.icon)?t.icon:t.icon.x32,body:t.body,tag:t.tag,requireInteraction:t.requireInteraction})}},{key:"close",value:function(i){i.close()}}]),n}(),h=function(i){function n(){return t(this,n),r(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return o(n,f),e(n,[{key:"isSupported",value:function(){return void 0!==this._win.navigator&&void 0!==this._win.navigator.serviceWorker}},{key:"getFunctionBody",value:function(i){var t=i.toString().match(/function[^{]+{([\s\S]*)}$/);return void 0!==t&&null!==t&&t.length>1?t[1]:null}},{key:"create",value:function(i,t,n,e,o){var r=this;this._win.navigator.serviceWorker.register(e),this._win.navigator.serviceWorker.ready.then(function(e){var s={id:i,link:n.link,origin:document.location.href,onClick:u.isFunction(n.onClick)?r.getFunctionBody(n.onClick):"",onClose:u.isFunction(n.onClose)?r.getFunctionBody(n.onClose):""};void 0!==n.data&&null!==n.data&&(s=Object.assign(s,n.data)),e.showNotification(t,{icon:n.icon,body:n.body,vibrate:n.vibrate,tag:n.tag,data:s,requireInteraction:n.requireInteraction,silent:n.silent}).then(function(){e.getNotifications().then(function(i){e.active.postMessage(""),o(i)})}).catch(function(i){throw new Error(c.errors.sw_notification_error+i.message)})}).catch(function(i){throw new Error(c.errors.sw_registration_error+i.message)})}},{key:"close",value:function(){}}]),n}(),_=function(i){function n(){return t(this,n),r(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return o(n,f),e(n,[{key:"isSupported",value:function(){return void 0!==this._win.navigator.mozNotification}},{key:"create",value:function(i,t){var n=this._win.navigator.mozNotification.createNotification(i,t.body,t.icon);return n.show(),n}}]),n}(),v=function(i){function n(){return t(this,n),r(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return o(n,f),e(n,[{key:"isSupported",value:function(){return void 0!==this._win.external&&void 0!==this._win.external.msIsSiteMode}},{key:"create",value:function(i,t){return this._win.external.msSiteModeClearIconOverlay(),this._win.external.msSiteModeSetIconOverlay(u.isString(t.icon)||u.isUndefined(t.icon)?t.icon:t.icon.x16,i),this._win.external.msSiteModeActivate(),null}},{key:"close",value:function(){this._win.external.msSiteModeClearIconOverlay()}}]),n}(),d=function(i){function n(){return t(this,n),r(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return o(n,f),e(n,[{key:"isSupported",value:function(){return void 0!==this._win.webkitNotifications}},{key:"create",value:function(i,t){var n=this._win.webkitNotifications.createNotification(t.icon,i,t.body);return n.show(),n}},{key:"close",value:function(i){i.cancel()}}]),n}();return new(function(){function i(n){t(this,i),this._currentId=0,this._notifications={},this._win=n,this.Permission=new a(n),this._agents={desktop:new l(n),chrome:new h(n),firefox:new _(n),ms:new v(n),webkit:new d(n)},this._configuration={serviceWorker:"/serviceWorker.min.js",fallback:function(i){}}}return e(i,[{key:"_closeNotification",value:function(i){var t=!0,n=this._notifications[i];if(void 0!==n){if(t=this._removeNotification(i),this._agents.desktop.isSupported())this._agents.desktop.close(n);else if(this._agents.webkit.isSupported())this._agents.webkit.close(n);else{if(!this._agents.ms.isSupported())throw t=!1,new Error(c.errors.unknown_interface);this._agents.ms.close()}return t}return!1}},{key:"_addNotification",value:function(i){var t=this._currentId;return this._notifications[t]=i,this._currentId++,t}},{key:"_removeNotification",value:function(i){var t=!1;return this._notifications.hasOwnProperty(i)&&(delete this._notifications[i],t=!0),t}},{key:"_prepareNotification",value:function(i,t){var n,e=this;return n={get:function(){return e._notifications[i]},close:function(){e._closeNotification(i)}},t.timeout&&setTimeout(function(){n.close()},t.timeout),n}},{key:"_serviceWorkerCallback",value:function(i,t,n){var e=this,o=this._addNotification(i[i.length-1]);navigator&&navigator.serviceWorker&&(navigator.serviceWorker.addEventListener("message",function(i){var t=JSON.parse(i.data);"close"===t.action&&Number.isInteger(t.id)&&e._removeNotification(t.id)}),n(this._prepareNotification(o,t))),n(null)}},{key:"_createCallback",value:function(i,t,n){var e,o=this,r=null;if(t=t||{},e=function(i){o._removeNotification(i),u.isFunction(t.onClose)&&t.onClose.call(o,r)},this._agents.desktop.isSupported())try{r=this._agents.desktop.create(i,t)}catch(e){var s=this._currentId,c=this.config().serviceWorker,a=function(i){return o._serviceWorkerCallback(i,t,n)};this._agents.chrome.isSupported()&&this._agents.chrome.create(s,i,t,c,a)}else this._agents.webkit.isSupported()?r=this._agents.webkit.create(i,t):this._agents.firefox.isSupported()?this._agents.firefox.create(i,t):this._agents.ms.isSupported()?r=this._agents.ms.create(i,t):(t.title=i,this.config().fallback(t));if(null!==r){var f=this._addNotification(r),l=this._prepareNotification(f,t);u.isFunction(t.onShow)&&r.addEventListener("show",t.onShow),u.isFunction(t.onError)&&r.addEventListener("error",t.onError),u.isFunction(t.onClick)&&r.addEventListener("click",t.onClick),r.addEventListener("close",function(){e(f)}),r.addEventListener("cancel",function(){e(f)}),n(l)}n(null)}},{key:"create",value:function(i,t){var n,e=this;if(!u.isString(i))throw new Error(c.errors.invalid_title);return n=this.Permission.has()?function(n,o){try{e._createCallback(i,t,n)}catch(i){o(i)}}:function(n,o){e.Permission.request().then(function(){e._createCallback(i,t,n)}).catch(function(){o(c.errors.permission_denied)})},new Promise(n)}},{key:"count",value:function(){var i,t=0;for(i in this._notifications)this._notifications.hasOwnProperty(i)&&t++;return t}},{key:"close",value:function(i){var t;for(t in this._notifications)if(this._notifications.hasOwnProperty(t)&&this._notifications[t].tag===i)return this._closeNotification(t)}},{key:"clear",value:function(){var i,t=!0;for(i in this._notifications)this._notifications.hasOwnProperty(i)&&(t=t&&this._closeNotification(i));return t}},{key:"supported",value:function(){var i=!1;for(var t in this._agents)this._agents.hasOwnProperty(t)&&(i=i||this._agents[t].isSupported());return i}},{key:"config",value:function(i){return(void 0!==i||null!==i&&u.isObject(i))&&u.objectMerge(this._configuration,i),this._configuration}},{key:"extend",value:function(i){var t,n={}.hasOwnProperty;if(!n.call(i,"plugin"))throw new Error(c.errors.invalid_plugin);n.call(i,"config")&&u.isObject(i.config)&&null!==i.config&&this.config(i.config),t=new(0,i.plugin)(this.config());for(var e in t)n.call(t,e)&&u.isFunction(t[e])&&(this[e]=t[e])}}]),i}())("undefined"!=typeof window?window:global)}); 37 | //# sourceMappingURL=push.min.js.map 38 | -------------------------------------------------------------------------------- /src/push/Push.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Messages, Permission, Util } from 'push'; 3 | import type { PluginManifest, GenericNotification, PushOptions } from 'types'; 4 | 5 | /* Import notification agents */ 6 | import { 7 | DesktopAgent, 8 | MobileChromeAgent, 9 | MobileFirefoxAgent, 10 | MSAgent, 11 | WebKitAgent 12 | } from 'agents'; 13 | 14 | export default class Push { 15 | // Private members 16 | _agents: { 17 | desktop: DesktopAgent, 18 | chrome: MobileChromeAgent, 19 | firefox: MobileFirefoxAgent, 20 | ms: MSAgent, 21 | webkit: WebKitAgent 22 | }; 23 | _configuration: { 24 | serviceWorker: string, 25 | fallback: ({}) => void 26 | }; 27 | _currentId: number; 28 | _notifications: {}; 29 | _win: {}; 30 | 31 | // Public members 32 | Permission: Permission; 33 | 34 | constructor(win: {}) { 35 | /* Private variables */ 36 | 37 | /* ID to use for new notifications */ 38 | this._currentId = 0; 39 | 40 | /* Map of open notifications */ 41 | this._notifications = {}; 42 | 43 | /* Window object */ 44 | this._win = win; 45 | 46 | /* Public variables */ 47 | this.Permission = new Permission(win); 48 | 49 | /* Agents */ 50 | this._agents = { 51 | desktop: new DesktopAgent(win), 52 | chrome: new MobileChromeAgent(win), 53 | firefox: new MobileFirefoxAgent(win), 54 | ms: new MSAgent(win), 55 | webkit: new WebKitAgent(win) 56 | }; 57 | 58 | this._configuration = { 59 | serviceWorker: '/serviceWorker.min.js', 60 | fallback: function(payload) {} 61 | }; 62 | } 63 | 64 | /** 65 | * Closes a notification 66 | * @param id ID of notification 67 | * @returns {boolean} denotes whether the operation was successful 68 | * @private 69 | */ 70 | _closeNotification(id: number | string) { 71 | let success = true; 72 | const notification = this._notifications[id]; 73 | 74 | if (notification !== undefined) { 75 | success = this._removeNotification(id); 76 | 77 | /* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */ 78 | if (this._agents.desktop.isSupported()) 79 | this._agents.desktop.close(notification); 80 | else if (this._agents.webkit.isSupported()) 81 | /* Legacy WebKit browsers */ 82 | this._agents.webkit.close(notification); 83 | else if (this._agents.ms.isSupported()) 84 | /* IE9 */ 85 | this._agents.ms.close(); 86 | else { 87 | success = false; 88 | throw new Error(Messages.errors.unknown_interface); 89 | } 90 | 91 | return success; 92 | } 93 | 94 | return false; 95 | } 96 | 97 | /** 98 | * Adds a notification to the global dictionary of notifications 99 | * @param {Notification} notification 100 | * @return {Integer} Dictionary key of the notification 101 | * @private 102 | */ 103 | _addNotification(notification: GenericNotification) { 104 | const id = this._currentId; 105 | this._notifications[id] = notification; 106 | this._currentId++; 107 | return id; 108 | } 109 | 110 | /** 111 | * Removes a notification with the given ID 112 | * @param {Integer} id - Dictionary key/ID of the notification to remove 113 | * @return {Boolean} boolean denoting success 114 | * @private 115 | */ 116 | _removeNotification(id: number | string) { 117 | let success = false; 118 | 119 | if (this._notifications.hasOwnProperty(id)) { 120 | /* We're successful if we omit the given ID from the new array */ 121 | delete this._notifications[id]; 122 | success = true; 123 | } 124 | 125 | return success; 126 | } 127 | 128 | /** 129 | * Creates the wrapper for a given notification 130 | * 131 | * @param {Integer} id - Dictionary key/ID of the notification 132 | * @param {Map} options - Options used to create the notification 133 | * @returns {Map} wrapper hashmap object 134 | * @private 135 | */ 136 | _prepareNotification(id: number, options: PushOptions) { 137 | let wrapper; 138 | 139 | /* Wrapper used to get/close notification later on */ 140 | wrapper = { 141 | get: () => { 142 | return this._notifications[id]; 143 | }, 144 | 145 | close: () => { 146 | this._closeNotification(id); 147 | } 148 | }; 149 | 150 | /* Autoclose timeout */ 151 | if (options.timeout) { 152 | setTimeout(() => { 153 | wrapper.close(); 154 | }, options.timeout); 155 | } 156 | 157 | return wrapper; 158 | } 159 | 160 | /** 161 | * Find the most recent notification from a ServiceWorker and add it to the global array 162 | * @param notifications 163 | * @private 164 | */ 165 | _serviceWorkerCallback( 166 | notifications: GenericNotification[], 167 | options: PushOptions, 168 | resolve: ({} | null) => void 169 | ) { 170 | let id = this._addNotification(notifications[notifications.length - 1]); 171 | 172 | /* Listen for close requests from the ServiceWorker */ 173 | if (navigator && navigator.serviceWorker) { 174 | navigator.serviceWorker.addEventListener('message', event => { 175 | const data = JSON.parse(event.data); 176 | 177 | if (data.action === 'close' && Number.isInteger(data.id)) 178 | this._removeNotification(data.id); 179 | }); 180 | 181 | resolve(this._prepareNotification(id, options)); 182 | } 183 | 184 | resolve(null); 185 | } 186 | 187 | /** 188 | * Callback function for the 'create' method 189 | * @return {void} 190 | * @private 191 | */ 192 | _createCallback( 193 | title: string, 194 | options: PushOptions, 195 | resolve: ({} | null) => void 196 | ) { 197 | let notification = null; 198 | let onClose; 199 | 200 | /* Set empty settings if none are specified */ 201 | options = options || {}; 202 | 203 | /* onClose event handler */ 204 | onClose = id => { 205 | /* A bit redundant, but covers the cases when close() isn't explicitly called */ 206 | this._removeNotification(id); 207 | if (Util.isFunction(options.onClose)) { 208 | options.onClose.call(this, notification); 209 | } 210 | }; 211 | 212 | /* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */ 213 | if (this._agents.desktop.isSupported()) { 214 | try { 215 | /* Create a notification using the API if possible */ 216 | notification = this._agents.desktop.create(title, options); 217 | } catch (e) { 218 | const id = this._currentId; 219 | const sw = this.config().serviceWorker; 220 | const cb = notifications => 221 | this._serviceWorkerCallback( 222 | notifications, 223 | options, 224 | resolve 225 | ); 226 | /* Create a Chrome ServiceWorker notification if it isn't supported */ 227 | if (this._agents.chrome.isSupported()) { 228 | this._agents.chrome.create(id, title, options, sw, cb); 229 | } 230 | } 231 | /* Legacy WebKit browsers */ 232 | } else if (this._agents.webkit.isSupported()) 233 | notification = this._agents.webkit.create(title, options); 234 | else if (this._agents.firefox.isSupported()) 235 | /* Firefox Mobile */ 236 | this._agents.firefox.create(title, options); 237 | else if (this._agents.ms.isSupported()) 238 | /* IE9 */ 239 | notification = this._agents.ms.create(title, options); 240 | else { 241 | /* Default fallback */ 242 | options.title = title; 243 | this.config().fallback(options); 244 | } 245 | 246 | if (notification !== null) { 247 | const id = this._addNotification(notification); 248 | const wrapper = this._prepareNotification(id, options); 249 | 250 | /* Notification callbacks */ 251 | if (Util.isFunction(options.onShow)) 252 | notification.addEventListener('show', options.onShow); 253 | 254 | if (Util.isFunction(options.onError)) 255 | notification.addEventListener('error', options.onError); 256 | 257 | if (Util.isFunction(options.onClick)) 258 | notification.addEventListener('click', options.onClick); 259 | 260 | notification.addEventListener('close', () => { 261 | onClose(id); 262 | }); 263 | 264 | notification.addEventListener('cancel', () => { 265 | onClose(id); 266 | }); 267 | 268 | /* Return the wrapper so the user can call close() */ 269 | resolve(wrapper); 270 | } 271 | 272 | /* By default, pass an empty wrapper */ 273 | resolve(null); 274 | } 275 | 276 | /** 277 | * Creates and displays a new notification 278 | * @param {Array} options 279 | * @return {Promise} 280 | */ 281 | create(title: string, options: {}): Promise { 282 | let promiseCallback; 283 | 284 | /* Fail if no or an invalid title is provided */ 285 | if (!Util.isString(title)) { 286 | throw new Error(Messages.errors.invalid_title); 287 | } 288 | 289 | /* Request permission if it isn't granted */ 290 | if (!this.Permission.has()) { 291 | promiseCallback = (resolve: () => void, reject: string => void) => { 292 | this.Permission 293 | .request() 294 | .then(() => { 295 | this._createCallback(title, options, resolve); 296 | }) 297 | .catch(() => { 298 | reject(Messages.errors.permission_denied); 299 | }); 300 | }; 301 | } else { 302 | promiseCallback = (resolve: () => void, reject: string => void) => { 303 | try { 304 | this._createCallback(title, options, resolve); 305 | } catch (e) { 306 | reject(e); 307 | } 308 | }; 309 | } 310 | 311 | return new Promise(promiseCallback); 312 | } 313 | 314 | /** 315 | * Returns the notification count 316 | * @return {Integer} The notification count 317 | */ 318 | count() { 319 | let count = 0; 320 | let key; 321 | 322 | for (key in this._notifications) 323 | if (this._notifications.hasOwnProperty(key)) count++; 324 | 325 | return count; 326 | } 327 | 328 | /** 329 | * Closes a notification with the given tag 330 | * @param {String} tag - Tag of the notification to close 331 | * @return {Boolean} boolean denoting success 332 | */ 333 | close(tag: string) { 334 | let key, notification; 335 | 336 | for (key in this._notifications) { 337 | if (this._notifications.hasOwnProperty(key)) { 338 | notification = this._notifications[key]; 339 | 340 | /* Run only if the tags match */ 341 | if (notification.tag === tag) { 342 | /* Call the notification's close() method */ 343 | return this._closeNotification(key); 344 | } 345 | } 346 | } 347 | } 348 | 349 | /** 350 | * Clears all notifications 351 | * @return {Boolean} boolean denoting whether the clear was successful in closing all notifications 352 | */ 353 | clear() { 354 | let key, 355 | success = true; 356 | 357 | for (key in this._notifications) 358 | if (this._notifications.hasOwnProperty(key)) 359 | success = success && this._closeNotification(key); 360 | 361 | return success; 362 | } 363 | 364 | /** 365 | * Denotes whether Push is supported in the current browser 366 | * @returns {boolean} 367 | */ 368 | supported() { 369 | let supported = false; 370 | 371 | for (var agent in this._agents) 372 | if (this._agents.hasOwnProperty(agent)) 373 | supported = supported || this._agents[agent].isSupported(); 374 | 375 | return supported; 376 | } 377 | 378 | /** 379 | * Modifies settings or returns all settings if no parameter passed 380 | * @param settings 381 | */ 382 | config(settings?: {}) { 383 | if ( 384 | typeof settings !== 'undefined' || 385 | (settings !== null && Util.isObject(settings)) 386 | ) 387 | Util.objectMerge(this._configuration, settings); 388 | 389 | return this._configuration; 390 | } 391 | 392 | /** 393 | * Copies the functions from a plugin to the main library 394 | * @param plugin 395 | */ 396 | extend(manifest: PluginManifest) { 397 | var plugin, 398 | Plugin, 399 | hasProp = {}.hasOwnProperty; 400 | 401 | if (!hasProp.call(manifest, 'plugin')) { 402 | throw new Error(Messages.errors.invalid_plugin); 403 | } else { 404 | if ( 405 | hasProp.call(manifest, 'config') && 406 | Util.isObject(manifest.config) && 407 | manifest.config !== null 408 | ) { 409 | this.config(manifest.config); 410 | } 411 | 412 | Plugin = manifest.plugin; 413 | plugin = new Plugin(this.config()); 414 | 415 | for (var member in plugin) { 416 | if ( 417 | hasProp.call(plugin, member) && 418 | Util.isFunction(plugin[member]) 419 | ) 420 | // $FlowFixMe 421 | this[member] = plugin[member]; 422 | } 423 | } 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /tests/push.tests.js: -------------------------------------------------------------------------------- 1 | var 2 | BROWSER_CHROME = 'Chrome', 3 | BROWSER_FIREFOX = 'Firefox', 4 | BROWSER_EDGE = 'Edge', 5 | BROWSER_OPERA = 'Opera', 6 | 7 | TEST_TITLE = 'title', 8 | TEST_BODY = 'body', 9 | TEST_TIMEOUT = 1000, 10 | TEST_TAG = 'foo', 11 | TEST_TAG_2 = 'bar', 12 | TEST_ICON = 'icon', 13 | TEST_SW_DEFAULT = "/serviceWorker.min.js", 14 | 15 | NOOP = function () { 16 | return null; 17 | }; 18 | 19 | describe('proper support detection', function () { 20 | function isBrowser(browser) { 21 | return platform.name.toLowerCase() === browser.toLowerCase(); 22 | } 23 | 24 | function getVersion() { 25 | return parseFloat(platform.version); 26 | } 27 | 28 | function isSupported() { 29 | return Push.supported(); 30 | } 31 | 32 | it('should detect Firefox support correctly', function () { 33 | if (isBrowser(BROWSER_FIREFOX)) { 34 | if (getVersion() > 21) 35 | expect(isSupported()).toBeTruthy(); 36 | else 37 | expect(isSupported()).toBeFalsy(); 38 | } else { 39 | pending(); 40 | } 41 | }); 42 | 43 | it('should detect Chrome support correctly', function () { 44 | if (isBrowser(BROWSER_CHROME)) { 45 | if (getVersion() > 4) 46 | expect(isSupported()).toBeTruthy(); 47 | else 48 | expect(isSupported()).toBeFalsy(); 49 | } else { 50 | pending(); 51 | } 52 | }); 53 | 54 | it('should detect Opera support correctly', function () { 55 | if (isBrowser(BROWSER_OPERA)) { 56 | if (getVersion() > 23) 57 | expect(isSupported()).toBeTruthy(); 58 | else 59 | expect(isSupported()).toBeFalsy(); 60 | } else { 61 | pending(); 62 | } 63 | }); 64 | 65 | it('should detect Edge support correctly', function () { 66 | if (isBrowser(BROWSER_EDGE)) { 67 | if (getVersion() > 14) 68 | expect(isSupported()).toBeTruthy(); 69 | else 70 | expect(isSupported()).toBeFalsy(); 71 | } else { 72 | pending(); 73 | } 74 | }); 75 | }); 76 | 77 | describe('adding plugins', function () { 78 | it('reject invalid plugin manifests', function () { 79 | var testPlugin = function () { 80 | this.testFunc = function () { 81 | } 82 | }; 83 | 84 | expect(Push.extend.bind(Push, testPlugin)).toThrow() 85 | }); 86 | 87 | it('accept valid plugin manifests', function () { 88 | var testPlugin = { 89 | plugin: function () { 90 | this.testFunc = function () { 91 | } 92 | } 93 | }; 94 | 95 | Push.extend(testPlugin); 96 | 97 | expect(Push.testFunc).toBeDefined() 98 | }); 99 | 100 | it('only allow object-based configs', function () { 101 | spyOn(window.Push, 'config'); 102 | 103 | var testPlugin = { 104 | config: null, 105 | plugin: function () { 106 | this.testFunc = function () { 107 | } 108 | } 109 | }; 110 | 111 | Push.extend(testPlugin); 112 | 113 | expect(Push.config.calls.count()).toEqual(1); // config() is called one by default in extend() 114 | 115 | var testPlugin2 = { 116 | config: {}, 117 | plugin: function () { 118 | this.testFunc = function () { 119 | } 120 | } 121 | }; 122 | 123 | Push.extend(testPlugin2); 124 | 125 | expect(Push.config.calls.count()).toBeGreaterThan(1); 126 | }); 127 | }); 128 | 129 | describe('changing configuration', function () { 130 | it('returns the current configuration if no parameters passed', function () { 131 | var output = { 132 | serviceWorker: TEST_SW_DEFAULT, 133 | fallback: function (payload) { 134 | } 135 | }; 136 | 137 | expect(JSON.stringify(Push.config())).toBe(JSON.stringify(output)); 138 | }); 139 | 140 | it('adds a configuration if one is specified', function () { 141 | Push.config({ 142 | a: 1 143 | }); 144 | 145 | expect(Push.config().a).toBeDefined(); 146 | expect(Push.config().a).toBe(1); 147 | }); 148 | 149 | it('should be capable of performing a deep merge', function () { 150 | var input1 = { 151 | b: { 152 | c: 1, 153 | d: { 154 | e: 2, 155 | f: 3 156 | } 157 | } 158 | }; 159 | var input2 = { 160 | b: { 161 | d: { 162 | e: 2, 163 | f: 4 164 | }, 165 | g: 5 166 | } 167 | }; 168 | var output = { 169 | c: 1, 170 | d: { 171 | e: 2, 172 | f: 4, 173 | }, 174 | g: 5 175 | }; 176 | 177 | Push.config(input1); 178 | Push.config(input2); 179 | 180 | expect(Push.config().b).toBeDefined(); 181 | expect(JSON.stringify(Push.config().b)).toBe(JSON.stringify(output)); 182 | }); 183 | }); 184 | 185 | if (Push.supported()) { 186 | Push.config({ 187 | serviceWorker: TEST_SW_DEFAULT 188 | }); 189 | 190 | function initRequestSpy(granted) { 191 | var param_str, param_int; 192 | 193 | param_str = (granted) ? Push.Permission.GRANTED : Push.Permission.DEFAULT; 194 | param_int = (granted) ? 0 : 1; 195 | 196 | /* Safari 6+, Legacy webkit browsers */ 197 | if (window.webkitNotifications && window.webkitNotifications.checkPermission) { 198 | spyOn(window.webkitNotifications, 'requestPermission').and.callFake(function (cb) { 199 | cb(param_int); 200 | }); 201 | } 202 | /* Chrome 23+ */ 203 | else if (window.Notification && window.Notification.requestPermission) { 204 | spyOn(window.Notification, 'requestPermission').and.callFake(function () { 205 | return new Promise(function (resolve) { 206 | resolve(param_str); 207 | }); 208 | }); 209 | } 210 | } 211 | 212 | function getRequestObject() { 213 | var obj = {}; 214 | 215 | /* Safari 6+, Legacy webkit browsers */ 216 | if (window.webkitNotifications && window.webkitNotifications.checkPermission) 217 | return window.webkitNotifications.requestPermission; 218 | 219 | /* Chrome 23+ */ 220 | else if (window.Notification && window.Notification.requestPermission) 221 | return window.Notification.requestPermission; 222 | return null; 223 | } 224 | 225 | describe('initialization', function () { 226 | 227 | it('should create a new instance', function () { 228 | expect(window.Push !== undefined).toBeTruthy(); 229 | }); 230 | 231 | it('isSupported should return a boolean', function () { 232 | expect(typeof Push.supported()).toBe('boolean'); 233 | }); 234 | 235 | }); 236 | 237 | describe('permission', function () { 238 | var callback; // Callback spy 239 | 240 | beforeEach(function () { 241 | callback = jasmine.createSpy('callback'); 242 | }); 243 | 244 | it('should have permission stored as a string constant', function () { 245 | expect(typeof Push.Permission.get()).toBe('string'); 246 | }); 247 | 248 | it('should update permission value if permission is denied and execute callback (deprecated)', function (done) { 249 | spyOn(Push.Permission, 'get').and.returnValue(Push.Permission.DEFAULT); 250 | initRequestSpy(false); 251 | 252 | Push.Permission.request(NOOP, callback); 253 | 254 | setTimeout(function () { 255 | expect(Push.Permission.has()).toBe(false); 256 | expect(callback).toHaveBeenCalled(); 257 | done(); 258 | }, 500); 259 | }); 260 | 261 | it('should update permission value if permission is denied and execute callback (with promise)', function (done) { 262 | spyOn(Push.Permission, 'get').and.returnValue(Push.Permission.DEFAULT); 263 | initRequestSpy(false); 264 | 265 | Push.Permission.request().then(NOOP).catch(callback); 266 | 267 | setTimeout(function () { 268 | expect(Push.Permission.has()).toBe(false); 269 | expect(callback).toHaveBeenCalled(); 270 | done(); 271 | }, 500); 272 | }); 273 | 274 | it('should request permission if permission is not granted', function () { 275 | spyOn(Push.Permission, 'get').and.returnValue(Push.Permission.DEFAULT); 276 | initRequestSpy(false); 277 | 278 | Push.create(TEST_TITLE).then(function () { 279 | expect(getRequestObject()).toHaveBeenCalled(); 280 | }).catch(function () { 281 | }); 282 | }); 283 | 284 | it('should update permission value if permission is granted and execute callback (deprecated)', function (done) { 285 | spyOn(Push.Permission, 'get').and.returnValue(Push.Permission.GRANTED); 286 | initRequestSpy(true); 287 | 288 | Push.Permission.request(callback, NOOP); 289 | 290 | setTimeout(function () { 291 | expect(Push.Permission.has()).toBe(true); 292 | expect(callback).toHaveBeenCalled(); 293 | done(); 294 | }, 500); 295 | }); 296 | 297 | it('should update permission value if permission is granted and execute callback (with promise)', function (done) { 298 | spyOn(Push.Permission, 'get').and.returnValue(Push.Permission.GRANTED); 299 | initRequestSpy(true); 300 | 301 | Push.Permission.request().then(callback).catch(NOOP); 302 | 303 | setTimeout(function () { 304 | expect(Push.Permission.has()).toBe(true); 305 | expect(callback).toHaveBeenCalled(); 306 | done(); 307 | }, 500); 308 | }); 309 | 310 | it('should not request permission if permission is already granted', function () { 311 | spyOn(Push.Permission, 'get').and.returnValue(Push.Permission.GRANTED); 312 | initRequestSpy(true); 313 | 314 | Push.Permission.request(); 315 | Push.create(TEST_TITLE).then(function () { 316 | expect(getRequestObject()).not.toHaveBeenCalled(); 317 | }).catch(function () { 318 | }); 319 | }); 320 | }); 321 | 322 | describe('creating notifications', function () { 323 | beforeAll(function () { 324 | jasmine.clock().install(); 325 | spyOn(Push.Permission, 'get').and.returnValue(Push.Permission.DEFAULT); 326 | initRequestSpy(true); 327 | }); 328 | 329 | beforeEach(function () { 330 | Push.clear(); 331 | }); 332 | 333 | it('should throw exception if no title is provided', function () { 334 | expect(function () { 335 | Push.create(); 336 | }).toThrow(); 337 | }); 338 | 339 | it('should return a valid notification wrapper', function (done) { 340 | Push.create(TEST_TITLE).then(function (wrapper) { 341 | expect(wrapper).not.toBe(undefined); 342 | expect(wrapper.get).not.toBe(undefined); 343 | expect(wrapper.close).not.toBe(undefined); 344 | done(); 345 | }).catch(function () { 346 | }); 347 | }); 348 | 349 | it('should return promise successfully', function () { 350 | var promise = Push.create(TEST_TITLE).then(function () { 351 | expect(promise.then).not.toBe(undefined); 352 | }).catch(function () { 353 | }); 354 | }); 355 | 356 | it('should pass in all API options correctly', function (done) { 357 | // Vibrate omitted because Firefox will default to using the Notification API, not service workers 358 | // Timeout, requestPermission, and event listeners also omitted from this src :( 359 | Push.create(TEST_TITLE, { 360 | body: TEST_BODY, 361 | icon: TEST_ICON, 362 | tag: TEST_TAG, 363 | silent: true 364 | }).then(function (wrapper) { 365 | var notification = wrapper.get(); 366 | 367 | // Some browsers, like Safari, choose to omit this info 368 | if (notification.title) expect(notification.title).toBe(TEST_TITLE); 369 | if (notification.body) expect(notification.body).toBe(TEST_BODY); 370 | if (notification.icon) expect(notification.icon).toContain(TEST_ICON); // Some browsers append the document location, so we gotta use toContain() 371 | 372 | expect(notification.tag).toBe(TEST_TAG); 373 | 374 | if (notification.hasOwnProperty('silent')) 375 | expect(notification.silent).toBe(true); 376 | 377 | done(); 378 | }).catch(function () { 379 | }); 380 | }); 381 | 382 | it('should return the increase the notification count', function (done) { 383 | expect(Push.count()).toBe(0); 384 | 385 | Push.create(TEST_TITLE).then(function () { 386 | expect(Push.count()).toBe(1); 387 | done(); 388 | }).catch(function () { 389 | }); 390 | }); 391 | 392 | }); 393 | 394 | describe('event listeners', function () { 395 | var callback, // callback spy 396 | 397 | testListener = function (name, cb) { 398 | var event = new Event(name), 399 | options = {}, 400 | key, 401 | promise; 402 | 403 | key = 'on' + name[0].toUpperCase() + name.substr(1, name.length - 1); 404 | 405 | options[key] = callback; 406 | 407 | Push.create(TEST_TITLE, options).then(function (wrapper) { 408 | var notification = wrapper.get(); 409 | notification.dispatchEvent(event); 410 | expect(callback).toHaveBeenCalled(); 411 | cb(); 412 | }).catch(function () { 413 | }); 414 | }; 415 | 416 | beforeAll(function () { 417 | initRequestSpy(true); 418 | }); 419 | 420 | beforeEach(function () { 421 | callback = jasmine.createSpy('callback'); 422 | spyOn(Push.Permission, 'get').and.returnValue(Push.Permission.DEFAULT); 423 | }); 424 | 425 | it('should execute onClick listener correctly', function (done) { 426 | testListener('click', done); 427 | }); 428 | 429 | it('should execute onShow listener correctly', function (done) { 430 | testListener('show', done); 431 | }); 432 | 433 | it('should execute onError listener correctly', function (done) { 434 | testListener('error', done); 435 | }); 436 | 437 | it('should execute onClose listener correctly', function (done) { 438 | testListener('close', done); 439 | }); 440 | }); 441 | 442 | describe('closing notifications', function () { 443 | var callback; // Callback spy 444 | 445 | beforeAll(function () { 446 | initRequestSpy(true); 447 | }); 448 | 449 | beforeEach(function () { 450 | spyOn(window.Notification.prototype, 'close'); 451 | spyOn(Push.Permission, 'get').and.returnValue(Push.Permission.DEFAULT); 452 | Push.clear(); 453 | callback = jasmine.createSpy('callback'); 454 | }); 455 | 456 | it('should close notifications on close callback', function (done) { 457 | Push.create(TEST_TITLE, { 458 | onClose: callback 459 | }).then(function (wrapper) { 460 | var notification = wrapper.get(); 461 | expect(Push.count()).toBe(1); 462 | notification.dispatchEvent(new Event('close')); 463 | expect(Push.count()).toBe(0); 464 | done(); 465 | }).catch(function () { 466 | }); 467 | }); 468 | 469 | it('should close notifications using wrapper', function (done) { 470 | Push.create(TEST_TITLE, { 471 | onClose: callback 472 | }).then(function (wrapper) { 473 | expect(Push.count()).toBe(1); 474 | wrapper.close(); 475 | expect(window.Notification.prototype.close).toHaveBeenCalled(); 476 | expect(Push.count()).toBe(0); 477 | done(); 478 | }).catch(function () { 479 | }); 480 | }); 481 | 482 | it('should close notifications using given timeout', function (done) { 483 | Push.create(TEST_TITLE, { 484 | timeout: TEST_TIMEOUT 485 | }).then(function () { 486 | expect(Push.count()).toBe(1); 487 | expect(window.Notification.prototype.close).not.toHaveBeenCalled(); 488 | 489 | jasmine.clock().tick(TEST_TIMEOUT); 490 | 491 | expect(window.Notification.prototype.close).toHaveBeenCalled(); 492 | expect(Push.count()).toBe(0); 493 | done(); 494 | }).catch(function () { 495 | }); 496 | ; 497 | }); 498 | 499 | it('should close a notification given a tag', function (done) { 500 | Push.create(TEST_TITLE, { 501 | tag: TEST_TAG 502 | }).then(function () { 503 | expect(Push.count()).toBe(1); 504 | expect(Push.close(TEST_TAG)).toBeTruthy(); 505 | expect(window.Notification.prototype.close).toHaveBeenCalled(); 506 | expect(Push.count()).toBe(0); 507 | done(); 508 | }).catch(function () { 509 | }); 510 | ; 511 | }); 512 | 513 | it('should close all notifications when cleared', function (done) { 514 | Push.create(TEST_TITLE, { 515 | tag: TEST_TAG 516 | }).then(function () { 517 | Push.create('hello world!', { 518 | tag: TEST_TAG_2 519 | }).then(function () { 520 | expect(Push.count()).toBeGreaterThan(0); 521 | expect(Push.clear()).toBeTruthy(); 522 | expect(window.Notification.prototype.close).toHaveBeenCalled(); 523 | expect(Push.count()).toBe(0); 524 | done(); 525 | }).catch(function () { 526 | }); 527 | ; 528 | }).catch(function () { 529 | }); 530 | ; 531 | }); 532 | }); 533 | } else { 534 | describe('fallback functionality', function () { 535 | it('should ensure fallback method fires correctly', function () { 536 | var fallback = jasmine.createSpy('fallback'); 537 | 538 | Push.config({ 539 | fallback: fallback 540 | }); 541 | 542 | Push.create(TEST_TITLE).then(function (done) { 543 | expect(fallback).toHaveBeenCalled(); 544 | done(); 545 | }).catch(function() { 546 | }); 547 | }); 548 | 549 | it('should ensure all notification options are passed to the fallback', function (done) { 550 | Push.config({ 551 | fallback: function(payload) { 552 | expect(payload.title).toBe(TEST_TITLE); 553 | expect(payload.body).toBe(TEST_BODY); 554 | expect(payload.icon).toBe(TEST_ICON); 555 | expect(payload.tag).toBe(TEST_TAG); 556 | done(); 557 | } 558 | }); 559 | 560 | Push.create(TEST_TITLE, { 561 | body: TEST_BODY, 562 | icon: TEST_ICON, 563 | tag: TEST_TAG 564 | }).catch(function() { 565 | }); 566 | }); 567 | }); 568 | } 569 | -------------------------------------------------------------------------------- /bin/push.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"push.min.js","sources":["../rollupPluginBabelHelpers","../src/push/Messages.js","../src/push/Permission.js","../src/push/Util.js","../src/agents/AbstractAgent.js","../src/agents/DesktopAgent.js","../src/agents/MobileChromeAgent.js","../src/agents/MobileFirefoxAgent.js","../src/agents/MSAgent.js","../src/agents/WebKitAgent.js","../src/index.js","../src/push/Push.js"],"sourcesContent":["export { _typeof as typeof, _jsx as jsx, _asyncIterator as asyncIterator, _AwaitValue as AwaitValue, _AsyncGenerator as AsyncGenerator, _wrapAsyncGenerator as wrapAsyncGenerator, _awaitAsyncGenerator as awaitAsyncGenerator, _asyncGeneratorDelegate as asyncGeneratorDelegate, _asyncToGenerator as asyncToGenerator, _classCallCheck as classCallCheck, _createClass as createClass, _defineEnumerableProperties as defineEnumerableProperties, _defaults as defaults, _defineProperty as defineProperty, _extends as extends, _get as get, _inherits as inherits, _inheritsLoose as inheritsLoose, _instanceof as instanceof, _interopRequireDefault as interopRequireDefault, _interopRequireWildcard as interopRequireWildcard, _newArrowCheck as newArrowCheck, _objectDestructuringEmpty as objectDestructuringEmpty, _objectWithoutProperties as objectWithoutProperties, _assertThisInitialized as assertThisInitialized, _possibleConstructorReturn as possibleConstructorReturn, _set as set, _slicedToArray as slicedToArray, _slicedToArrayLoose as slicedToArrayLoose, _taggedTemplateLiteral as taggedTemplateLiteral, _taggedTemplateLiteralLoose as taggedTemplateLiteralLoose, _temporalRef as temporalRef, _readOnlyError as readOnlyError, _classNameTDZError as classNameTDZError, _temporalUndefined as temporalUndefined, _toArray as toArray, _toConsumableArray as toConsumableArray, _skipFirstGeneratorNext as skipFirstGeneratorNext, _toPropertyKey as toPropertyKey, _initializerWarningHelper as initializerWarningHelper, _initializerDefineProperty as initializerDefineProperty, _applyDecoratedDescriptor as applyDecoratedDescriptor };\n\nfunction _typeof(obj) {\n if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") {\n _typeof = function (obj) {\n return typeof obj;\n };\n } else {\n _typeof = function (obj) {\n return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n };\n }\n\n return _typeof(obj);\n}\n\nvar REACT_ELEMENT_TYPE;\n\nfunction _jsx(type, props, key, children) {\n if (!REACT_ELEMENT_TYPE) {\n REACT_ELEMENT_TYPE = typeof Symbol === \"function\" && Symbol.for && Symbol.for(\"react.element\") || 0xeac7;\n }\n\n var defaultProps = type && type.defaultProps;\n var childrenLength = arguments.length - 3;\n\n if (!props && childrenLength !== 0) {\n props = {};\n }\n\n if (props && defaultProps) {\n for (var propName in defaultProps) {\n if (props[propName] === void 0) {\n props[propName] = defaultProps[propName];\n }\n }\n } else if (!props) {\n props = defaultProps || {};\n }\n\n if (childrenLength === 1) {\n props.children = children;\n } else if (childrenLength > 1) {\n var childArray = new Array(childrenLength);\n\n for (var i = 0; i < childrenLength; i++) {\n childArray[i] = arguments[i + 3];\n }\n\n props.children = childArray;\n }\n\n return {\n $$typeof: REACT_ELEMENT_TYPE,\n type: type,\n key: key === undefined ? null : '' + key,\n ref: null,\n props: props,\n _owner: null\n };\n}\n\nfunction _asyncIterator(iterable) {\n if (typeof Symbol === \"function\") {\n if (Symbol.asyncIterator) {\n var method = iterable[Symbol.asyncIterator];\n if (method != null) return method.call(iterable);\n }\n\n if (Symbol.iterator) {\n return iterable[Symbol.iterator]();\n }\n }\n\n throw new TypeError(\"Object is not async iterable\");\n}\n\nfunction _AwaitValue(value) {\n this.wrapped = value;\n}\n\nfunction _AsyncGenerator(gen) {\n var front, back;\n\n function send(key, arg) {\n return new Promise(function (resolve, reject) {\n var request = {\n key: key,\n arg: arg,\n resolve: resolve,\n reject: reject,\n next: null\n };\n\n if (back) {\n back = back.next = request;\n } else {\n front = back = request;\n resume(key, arg);\n }\n });\n }\n\n function resume(key, arg) {\n try {\n var result = gen[key](arg);\n var value = result.value;\n var wrappedAwait = value instanceof _AwaitValue;\n Promise.resolve(wrappedAwait ? value.wrapped : value).then(function (arg) {\n if (wrappedAwait) {\n resume(\"next\", arg);\n return;\n }\n\n settle(result.done ? \"return\" : \"normal\", arg);\n }, function (err) {\n resume(\"throw\", err);\n });\n } catch (err) {\n settle(\"throw\", err);\n }\n }\n\n function settle(type, value) {\n switch (type) {\n case \"return\":\n front.resolve({\n value: value,\n done: true\n });\n break;\n\n case \"throw\":\n front.reject(value);\n break;\n\n default:\n front.resolve({\n value: value,\n done: false\n });\n break;\n }\n\n front = front.next;\n\n if (front) {\n resume(front.key, front.arg);\n } else {\n back = null;\n }\n }\n\n this._invoke = send;\n\n if (typeof gen.return !== \"function\") {\n this.return = undefined;\n }\n}\n\nif (typeof Symbol === \"function\" && Symbol.asyncIterator) {\n _AsyncGenerator.prototype[Symbol.asyncIterator] = function () {\n return this;\n };\n}\n\n_AsyncGenerator.prototype.next = function (arg) {\n return this._invoke(\"next\", arg);\n};\n\n_AsyncGenerator.prototype.throw = function (arg) {\n return this._invoke(\"throw\", arg);\n};\n\n_AsyncGenerator.prototype.return = function (arg) {\n return this._invoke(\"return\", arg);\n};\n\nfunction _wrapAsyncGenerator(fn) {\n return function () {\n return new _AsyncGenerator(fn.apply(this, arguments));\n };\n}\n\nfunction _awaitAsyncGenerator(value) {\n return new _AwaitValue(value);\n}\n\nfunction _asyncGeneratorDelegate(inner, awaitWrap) {\n var iter = {},\n waiting = false;\n\n function pump(key, value) {\n waiting = true;\n value = new Promise(function (resolve) {\n resolve(inner[key](value));\n });\n return {\n done: false,\n value: awaitWrap(value)\n };\n }\n\n ;\n\n if (typeof Symbol === \"function\" && Symbol.iterator) {\n iter[Symbol.iterator] = function () {\n return this;\n };\n }\n\n iter.next = function (value) {\n if (waiting) {\n waiting = false;\n return value;\n }\n\n return pump(\"next\", value);\n };\n\n if (typeof inner.throw === \"function\") {\n iter.throw = function (value) {\n if (waiting) {\n waiting = false;\n throw value;\n }\n\n return pump(\"throw\", value);\n };\n }\n\n if (typeof inner.return === \"function\") {\n iter.return = function (value) {\n return pump(\"return\", value);\n };\n }\n\n return iter;\n}\n\nfunction _asyncToGenerator(fn) {\n return function () {\n var self = this,\n args = arguments;\n return new Promise(function (resolve, reject) {\n var gen = fn.apply(self, args);\n\n function step(key, arg) {\n try {\n var info = gen[key](arg);\n var value = info.value;\n } catch (error) {\n reject(error);\n return;\n }\n\n if (info.done) {\n resolve(value);\n } else {\n Promise.resolve(value).then(_next, _throw);\n }\n }\n\n function _next(value) {\n step(\"next\", value);\n }\n\n function _throw(err) {\n step(\"throw\", err);\n }\n\n _next();\n });\n };\n}\n\nfunction _classCallCheck(instance, Constructor) {\n if (!(instance instanceof Constructor)) {\n throw new TypeError(\"Cannot call a class as a function\");\n }\n}\n\nfunction _defineProperties(target, props) {\n for (var i = 0; i < props.length; i++) {\n var descriptor = props[i];\n descriptor.enumerable = descriptor.enumerable || false;\n descriptor.configurable = true;\n if (\"value\" in descriptor) descriptor.writable = true;\n Object.defineProperty(target, descriptor.key, descriptor);\n }\n}\n\nfunction _createClass(Constructor, protoProps, staticProps) {\n if (protoProps) _defineProperties(Constructor.prototype, protoProps);\n if (staticProps) _defineProperties(Constructor, staticProps);\n return Constructor;\n}\n\nfunction _defineEnumerableProperties(obj, descs) {\n for (var key in descs) {\n var desc = descs[key];\n desc.configurable = desc.enumerable = true;\n if (\"value\" in desc) desc.writable = true;\n Object.defineProperty(obj, key, desc);\n }\n\n if (Object.getOwnPropertySymbols) {\n var objectSymbols = Object.getOwnPropertySymbols(descs);\n\n for (var i = 0; i < objectSymbols.length; i++) {\n var sym = objectSymbols[i];\n var desc = descs[sym];\n desc.configurable = desc.enumerable = true;\n if (\"value\" in desc) desc.writable = true;\n Object.defineProperty(obj, sym, desc);\n }\n }\n\n return obj;\n}\n\nfunction _defaults(obj, defaults) {\n var keys = Object.getOwnPropertyNames(defaults);\n\n for (var i = 0; i < keys.length; i++) {\n var key = keys[i];\n var value = Object.getOwnPropertyDescriptor(defaults, key);\n\n if (value && value.configurable && obj[key] === undefined) {\n Object.defineProperty(obj, key, value);\n }\n }\n\n return obj;\n}\n\nfunction _defineProperty(obj, key, value) {\n if (key in obj) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n } else {\n obj[key] = value;\n }\n\n return obj;\n}\n\nfunction _extends() {\n _extends = Object.assign || function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n\n return target;\n };\n\n return _extends.apply(this, arguments);\n}\n\nfunction _get(object, property, receiver) {\n if (object === null) object = Function.prototype;\n var desc = Object.getOwnPropertyDescriptor(object, property);\n\n if (desc === undefined) {\n var parent = Object.getPrototypeOf(object);\n\n if (parent === null) {\n return undefined;\n } else {\n return _get(parent, property, receiver);\n }\n } else if (\"value\" in desc) {\n return desc.value;\n } else {\n var getter = desc.get;\n\n if (getter === undefined) {\n return undefined;\n }\n\n return getter.call(receiver);\n }\n}\n\nfunction _inherits(subClass, superClass) {\n if (typeof superClass !== \"function\" && superClass !== null) {\n throw new TypeError(\"Super expression must either be null or a function\");\n }\n\n subClass.prototype = Object.create(superClass && superClass.prototype, {\n constructor: {\n value: subClass,\n enumerable: false,\n writable: true,\n configurable: true\n }\n });\n if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;\n}\n\nfunction _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n subClass.__proto__ = superClass;\n}\n\nfunction _instanceof(left, right) {\n if (right != null && typeof Symbol !== \"undefined\" && right[Symbol.hasInstance]) {\n return right[Symbol.hasInstance](left);\n } else {\n return left instanceof right;\n }\n}\n\nfunction _interopRequireDefault(obj) {\n return obj && obj.__esModule ? obj : {\n default: obj\n };\n}\n\nfunction _interopRequireWildcard(obj) {\n if (obj && obj.__esModule) {\n return obj;\n } else {\n var newObj = {};\n\n if (obj != null) {\n for (var key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {};\n\n if (desc.get || desc.set) {\n Object.defineProperty(newObj, key, desc);\n } else {\n newObj[key] = obj[key];\n }\n }\n }\n }\n\n newObj.default = obj;\n return newObj;\n }\n}\n\nfunction _newArrowCheck(innerThis, boundThis) {\n if (innerThis !== boundThis) {\n throw new TypeError(\"Cannot instantiate an arrow function\");\n }\n}\n\nfunction _objectDestructuringEmpty(obj) {\n if (obj == null) throw new TypeError(\"Cannot destructure undefined\");\n}\n\nfunction _objectWithoutProperties(source, excluded) {\n if (source == null) return {};\n var target = {};\n var sourceKeys = Object.keys(source);\n var key, i;\n\n for (i = 0; i < sourceKeys.length; i++) {\n key = sourceKeys[i];\n if (excluded.indexOf(key) >= 0) continue;\n target[key] = source[key];\n }\n\n if (Object.getOwnPropertySymbols) {\n var sourceSymbolKeys = Object.getOwnPropertySymbols(source);\n\n for (i = 0; i < sourceSymbolKeys.length; i++) {\n key = sourceSymbolKeys[i];\n if (excluded.indexOf(key) >= 0) continue;\n if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;\n target[key] = source[key];\n }\n }\n\n return target;\n}\n\nfunction _assertThisInitialized(self) {\n if (self === void 0) {\n throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");\n }\n\n return self;\n}\n\nfunction _possibleConstructorReturn(self, call) {\n if (call && (typeof call === \"object\" || typeof call === \"function\")) {\n return call;\n }\n\n if (self === void 0) {\n throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");\n }\n\n return self;\n}\n\nfunction _set(object, property, value, receiver) {\n var desc = Object.getOwnPropertyDescriptor(object, property);\n\n if (desc === undefined) {\n var parent = Object.getPrototypeOf(object);\n\n if (parent !== null) {\n _set(parent, property, value, receiver);\n }\n } else if (\"value\" in desc && desc.writable) {\n desc.value = value;\n } else {\n var setter = desc.set;\n\n if (setter !== undefined) {\n setter.call(receiver, value);\n }\n }\n\n return value;\n}\n\nfunction _sliceIterator(arr, i) {\n var _arr = [];\n var _n = true;\n var _d = false;\n var _e = undefined;\n\n try {\n for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {\n _arr.push(_s.value);\n\n if (i && _arr.length === i) break;\n }\n } catch (err) {\n _d = true;\n _e = err;\n } finally {\n try {\n if (!_n && _i[\"return\"] != null) _i[\"return\"]();\n } finally {\n if (_d) throw _e;\n }\n }\n\n return _arr;\n}\n\nfunction _slicedToArray(arr, i) {\n if (Array.isArray(arr)) {\n return arr;\n } else if (Symbol.iterator in Object(arr)) {\n return _sliceIterator(arr, i);\n } else {\n throw new TypeError(\"Invalid attempt to destructure non-iterable instance\");\n }\n}\n\nfunction _slicedToArrayLoose(arr, i) {\n if (Array.isArray(arr)) {\n return arr;\n } else if (Symbol.iterator in Object(arr)) {\n var _arr = [];\n\n for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) {\n _arr.push(_step.value);\n\n if (i && _arr.length === i) break;\n }\n\n return _arr;\n } else {\n throw new TypeError(\"Invalid attempt to destructure non-iterable instance\");\n }\n}\n\nfunction _taggedTemplateLiteral(strings, raw) {\n return Object.freeze(Object.defineProperties(strings, {\n raw: {\n value: Object.freeze(raw)\n }\n }));\n}\n\nfunction _taggedTemplateLiteralLoose(strings, raw) {\n strings.raw = raw;\n return strings;\n}\n\nfunction _temporalRef(val, name) {\n if (val === _temporalUndefined) {\n throw new ReferenceError(name + \" is not defined - temporal dead zone\");\n } else {\n return val;\n }\n}\n\nfunction _readOnlyError(name) {\n throw new Error(\"\\\"\" + name + \"\\\" is read-only\");\n}\n\nfunction _classNameTDZError(name) {\n throw new Error(\"Class \\\"\" + name + \"\\\" cannot be referenced in computed property keys.\");\n}\n\nvar _temporalUndefined = {};\n\nfunction _toArray(arr) {\n return Array.isArray(arr) ? arr : Array.from(arr);\n}\n\nfunction _toConsumableArray(arr) {\n if (Array.isArray(arr)) {\n for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];\n\n return arr2;\n } else {\n return Array.from(arr);\n }\n}\n\nfunction _skipFirstGeneratorNext(fn) {\n return function () {\n var it = fn.apply(this, arguments);\n it.next();\n return it;\n };\n}\n\nfunction _toPropertyKey(key) {\n if (typeof key === \"symbol\") {\n return key;\n } else {\n return String(key);\n }\n}\n\nfunction _initializerWarningHelper(descriptor, context) {\n throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and set to use loose mode. ' + 'To use proposal-class-properties in spec mode with decorators, wait for ' + 'the next major version of decorators in stage 2.');\n}\n\nfunction _initializerDefineProperty(target, property, descriptor, context) {\n if (!descriptor) return;\n Object.defineProperty(target, property, {\n enumerable: descriptor.enumerable,\n configurable: descriptor.configurable,\n writable: descriptor.writable,\n value: descriptor.initializer ? descriptor.initializer.call(context) : void 0\n });\n}\n\nfunction _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {\n var desc = {};\n Object['ke' + 'ys'](descriptor).forEach(function (key) {\n desc[key] = descriptor[key];\n });\n desc.enumerable = !!desc.enumerable;\n desc.configurable = !!desc.configurable;\n\n if ('value' in desc || desc.initializer) {\n desc.writable = true;\n }\n\n desc = decorators.slice().reverse().reduce(function (desc, decorator) {\n return decorator(target, property, desc) || desc;\n }, desc);\n\n if (context && desc.initializer !== void 0) {\n desc.value = desc.initializer ? desc.initializer.call(context) : void 0;\n desc.initializer = undefined;\n }\n\n if (desc.initializer === void 0) {\n Object['define' + 'Property'](target, property, desc);\n desc = null;\n }\n\n return desc;\n}","// @flow\nconst errorPrefix = 'PushError:';\n\nexport default {\n errors: {\n incompatible: `${errorPrefix} Push.js is incompatible with browser.`,\n invalid_plugin: `${errorPrefix} plugin class missing from plugin manifest (invalid plugin). Please check the documentation.`,\n invalid_title: `${errorPrefix} title of notification must be a string`,\n permission_denied: `${errorPrefix} permission request declined`,\n sw_notification_error: `${errorPrefix} could not show a ServiceWorker notification due to the following reason: `,\n sw_registration_error: `${errorPrefix} could not register the ServiceWorker due to the following reason: `,\n unknown_interface: `${errorPrefix} unable to create notification: unknown interface`\n }\n};\n","// @flow\nimport type { Global } from 'types';\n\nexport default class Permission {\n // Private members\n _permissions: string[];\n _win: Global;\n\n // Public members\n GRANTED: string;\n DEFAULT: string;\n DENIED: string;\n\n constructor(win: Global) {\n this._win = win;\n this.GRANTED = 'granted';\n this.DEFAULT = 'default';\n this.DENIED = 'denied';\n this._permissions = [this.GRANTED, this.DEFAULT, this.DENIED];\n }\n\n /**\n * Requests permission for desktop notifications\n * @param {Function} onGranted - Function to execute once permission is granted\n * @param {Function} onDenied - Function to execute once permission is denied\n * @return {void, Promise}\n */\n request(onGranted: () => void, onDenied: () => void) {\n return arguments.length > 0\n ? this._requestWithCallback(...arguments)\n : this._requestAsPromise();\n }\n\n /**\n * Old permissions implementation deprecated in favor of a promise based one\n * @deprecated Since V1.0.4\n * @param {Function} onGranted - Function to execute once permission is granted\n * @param {Function} onDenied - Function to execute once permission is denied\n * @return {void}\n */\n _requestWithCallback(onGranted: () => void, onDenied: () => void) {\n const existing = this.get();\n\n var resolve = (result = this._win.Notification.permission) => {\n if (typeof result === 'undefined' && this._win.webkitNotifications)\n result = this._win.webkitNotifications.checkPermission();\n if (result === this.GRANTED || result === 0) {\n if (onGranted) onGranted();\n } else if (onDenied) onDenied();\n };\n\n /* Permissions already set */\n if (existing !== this.DEFAULT) {\n resolve(existing);\n } else if (\n this._win.webkitNotifications &&\n this._win.webkitNotifications.checkPermission\n ) {\n /* Safari 6+, Legacy webkit browsers */\n this._win.webkitNotifications.requestPermission(resolve);\n } else if (\n this._win.Notification &&\n this._win.Notification.requestPermission\n ) {\n /* Chrome 23+ */\n this._win.Notification\n .requestPermission()\n .then(resolve)\n .catch(function() {\n if (onDenied) onDenied();\n });\n } else if (onGranted) {\n /* Let the user continue by default */\n onGranted();\n }\n }\n\n /**\n * Requests permission for desktop notifications in a promise based way\n * @return {Promise}\n */\n _requestAsPromise(): Promise {\n const existing = this.get();\n\n let isGranted = result => result === this.GRANTED || result === 0;\n\n /* Permissions already set */\n var hasPermissions = existing !== this.DEFAULT;\n\n /* Safari 6+, Chrome 23+ */\n var isModernAPI =\n this._win.Notification && this._win.Notification.requestPermission;\n\n /* Legacy webkit browsers */\n var isWebkitAPI =\n this._win.webkitNotifications &&\n this._win.webkitNotifications.checkPermission;\n\n return new Promise((resolvePromise, rejectPromise) => {\n var resolver = result =>\n isGranted(result) ? resolvePromise() : rejectPromise();\n\n if (hasPermissions) {\n resolver(existing);\n } else if (isWebkitAPI) {\n this._win.webkitNotifications.requestPermission(result => {\n resolver(result);\n });\n } else if (isModernAPI) {\n this._win.Notification\n .requestPermission()\n .then(result => {\n resolver(result);\n })\n .catch(rejectPromise);\n } else resolvePromise();\n });\n }\n\n /**\n * Returns whether Push has been granted permission to run\n * @return {Boolean}\n */\n has() {\n return this.get() === this.GRANTED;\n }\n\n /**\n * Gets the permission level\n * @return {Permission} The permission level\n */\n get() {\n let permission;\n\n /* Safari 6+, Chrome 23+ */\n if (this._win.Notification && this._win.Notification.permission)\n permission = this._win.Notification.permission;\n else if (\n this._win.webkitNotifications &&\n this._win.webkitNotifications.checkPermission\n )\n /* Legacy webkit browsers */\n permission = this._permissions[\n this._win.webkitNotifications.checkPermission()\n ];\n else if (navigator.mozNotification)\n /* Firefox Mobile */\n permission = this.GRANTED;\n else if (this._win.external && this._win.external.msIsSiteMode)\n /* IE9+ */\n permission = this._win.external.msIsSiteMode()\n ? this.GRANTED\n : this.DEFAULT;\n else permission = this.GRANTED;\n\n return permission;\n }\n}\n","// @flow\nexport default class Util {\n static isUndefined(obj) {\n return obj === undefined;\n }\n\n static isString(obj) {\n return typeof obj === 'string';\n }\n\n static isFunction(obj) {\n return obj && {}.toString.call(obj) === '[object Function]';\n }\n\n static isObject(obj) {\n return typeof obj === 'object';\n }\n\n static objectMerge(target, source) {\n for (var key in source) {\n if (\n target.hasOwnProperty(key) &&\n this.isObject(target[key]) &&\n this.isObject(source[key])\n ) {\n this.objectMerge(target[key], source[key]);\n } else {\n target[key] = source[key];\n }\n }\n }\n}\n","// @flow\nimport type { Global } from 'types';\n\nexport default class AbstractAgent {\n _win: Global;\n\n constructor(win: Global) {\n this._win = win;\n }\n}\n","// @flow\nimport { AbstractAgent } from 'agents';\nimport { Util } from 'push';\nimport type { PushOptions, GenericNotification, Global } from 'types';\n\n/**\n * Notification agent for modern desktop browsers:\n * Safari 6+, Firefox 22+, Chrome 22+, Opera 25+\n */\nexport default class DesktopAgent extends AbstractAgent {\n _win: Global;\n\n /**\n * Returns a boolean denoting support\n * @returns {Boolean} boolean denoting whether webkit notifications are supported\n */\n isSupported() {\n return this._win.Notification !== undefined;\n }\n\n /**\n * Creates a new notification\n * @param title - notification title\n * @param options - notification options array\n * @returns {Notification}\n */\n create(title: string, options: PushOptions) {\n return new this._win.Notification(title, {\n icon:\n Util.isString(options.icon) || Util.isUndefined(options.icon)\n ? options.icon\n : options.icon.x32,\n body: options.body,\n tag: options.tag,\n requireInteraction: options.requireInteraction\n });\n }\n\n /**\n * Close a given notification\n * @param notification - notification to close\n */\n close(notification: GenericNotification) {\n notification.close();\n }\n}\n","// @flow\nimport { Util, Messages } from 'push';\nimport { AbstractAgent } from 'agents';\nimport type { Global, GenericNotification, PushOptions } from 'types';\n\n/**\n * Notification agent for modern desktop browsers:\n * Safari 6+, Firefox 22+, Chrome 22+, Opera 25+\n */\nexport default class MobileChromeAgent extends AbstractAgent {\n _win: Global;\n\n /**\n * Returns a boolean denoting support\n * @returns {Boolean} boolean denoting whether webkit notifications are supported\n */\n isSupported() {\n return (\n this._win.navigator !== undefined &&\n this._win.navigator.serviceWorker !== undefined\n );\n }\n\n /**\n * Returns the function body as a string\n * @param func\n */\n getFunctionBody(func: () => void) {\n const str = func.toString().match(/function[^{]+{([\\s\\S]*)}$/);\n return typeof str !== 'undefined' && str !== null && str.length > 1\n ? str[1]\n : null;\n }\n\n /**\n * Creates a new notification\n * @param id ID of notification\n * @param title Title of notification\n * @param options Options object\n * @param serviceWorker ServiceWorker path\n * @param callback Callback function\n */\n create(\n id: number,\n title: string,\n options: PushOptions,\n serviceWorker: string,\n callback: (GenericNotification[]) => void\n ) {\n /* Register ServiceWorker */\n this._win.navigator.serviceWorker.register(serviceWorker);\n\n this._win.navigator.serviceWorker.ready\n .then(registration => {\n /* Local data the service worker will use */\n let localData = {\n id: id,\n link: options.link,\n origin: document.location.href,\n onClick: Util.isFunction(options.onClick)\n ? this.getFunctionBody(options.onClick)\n : '',\n onClose: Util.isFunction(options.onClose)\n ? this.getFunctionBody(options.onClose)\n : ''\n };\n\n /* Merge the local data with user-provided data */\n if (options.data !== undefined && options.data !== null)\n localData = Object.assign(localData, options.data);\n\n /* Show the notification */\n registration\n .showNotification(title, {\n icon: options.icon,\n body: options.body,\n vibrate: options.vibrate,\n tag: options.tag,\n data: localData,\n requireInteraction: options.requireInteraction,\n silent: options.silent\n })\n .then(() => {\n registration.getNotifications().then(notifications => {\n /* Send an empty message so the ServiceWorker knows who the client is */\n registration.active.postMessage('');\n\n /* Trigger callback */\n callback(notifications);\n });\n })\n .catch(function(error) {\n throw new Error(\n Messages.errors.sw_notification_error +\n error.message\n );\n });\n })\n .catch(function(error) {\n throw new Error(\n Messages.errors.sw_registration_error + error.message\n );\n });\n }\n\n /**\n * Close all notification\n */\n close() {\n // Can't do this with service workers\n }\n}\n","// @flow\nimport { AbstractAgent } from 'agents';\nimport type { Global, PushOptions } from 'types';\n\n/**\n * Notification agent for modern desktop browsers:\n * Safari 6+, Firefox 22+, Chrome 22+, Opera 25+\n */\nexport default class MobileFirefoxAgent extends AbstractAgent {\n _win: Global;\n\n /**\n * Returns a boolean denoting support\n * @returns {Boolean} boolean denoting whether webkit notifications are supported\n */\n isSupported() {\n return this._win.navigator.mozNotification !== undefined;\n }\n\n /**\n * Creates a new notification\n * @param title - notification title\n * @param options - notification options array\n * @returns {Notification}\n */\n create(title: string, options: PushOptions) {\n let notification = this._win.navigator.mozNotification.createNotification(\n title,\n options.body,\n options.icon\n );\n\n notification.show();\n\n return notification;\n }\n}\n","// @flow\nimport { AbstractAgent } from 'agents';\nimport { Util } from 'push';\nimport type { PushOptions, Global } from 'types';\n\n/**\n * Notification agent for IE9\n */\nexport default class MSAgent extends AbstractAgent {\n _win: Global;\n\n /**\n * Returns a boolean denoting support\n * @returns {Boolean} boolean denoting whether webkit notifications are supported\n */\n isSupported() {\n return (\n this._win.external !== undefined &&\n this._win.external.msIsSiteMode !== undefined\n );\n }\n\n /**\n * Creates a new notification\n * @param title - notification title\n * @param options - notification options array\n * @returns {Notification}\n */\n create(title: string, options: PushOptions) {\n /* Clear any previous notifications */\n this._win.external.msSiteModeClearIconOverlay();\n\n this._win.external.msSiteModeSetIconOverlay(\n Util.isString(options.icon) || Util.isUndefined(options.icon)\n ? options.icon\n : options.icon.x16,\n title\n );\n\n this._win.external.msSiteModeActivate();\n\n return null;\n }\n\n /**\n * Close a given notification\n * @param notification - notification to close\n */\n close() {\n this._win.external.msSiteModeClearIconOverlay();\n }\n}\n","// @flow\nimport { AbstractAgent } from 'agents';\nimport type { Global, GenericNotification, PushOptions } from 'types';\n\n/**\n * Notification agent for old Chrome versions (and some) Firefox\n */\nexport default class WebKitAgent extends AbstractAgent {\n _win: Global;\n\n /**\n * Returns a boolean denoting support\n * @returns {Boolean} boolean denoting whether webkit notifications are supported\n */\n isSupported() {\n return this._win.webkitNotifications !== undefined;\n }\n\n /**\n * Creates a new notification\n * @param title - notification title\n * @param options - notification options array\n * @returns {Notification}\n */\n create(title: string, options: PushOptions) {\n let notification = this._win.webkitNotifications.createNotification(\n options.icon,\n title,\n options.body\n );\n\n notification.show();\n\n return notification;\n }\n\n /**\n * Close a given notification\n * @param notification - notification to close\n */\n close(notification: GenericNotification) {\n notification.cancel();\n }\n}\n","// @flow\nimport { Push } from 'push';\n\nexport default new Push(typeof window !== 'undefined' ? window : global);\n","// @flow\nimport { Messages, Permission, Util } from 'push';\nimport type { PluginManifest, GenericNotification, PushOptions } from 'types';\n\n/* Import notification agents */\nimport {\n DesktopAgent,\n MobileChromeAgent,\n MobileFirefoxAgent,\n MSAgent,\n WebKitAgent\n} from 'agents';\n\nexport default class Push {\n // Private members\n _agents: {\n desktop: DesktopAgent,\n chrome: MobileChromeAgent,\n firefox: MobileFirefoxAgent,\n ms: MSAgent,\n webkit: WebKitAgent\n };\n _configuration: {\n serviceWorker: string,\n fallback: ({}) => void\n };\n _currentId: number;\n _notifications: {};\n _win: {};\n\n // Public members\n Permission: Permission;\n\n constructor(win: {}) {\n /* Private variables */\n\n /* ID to use for new notifications */\n this._currentId = 0;\n\n /* Map of open notifications */\n this._notifications = {};\n\n /* Window object */\n this._win = win;\n\n /* Public variables */\n this.Permission = new Permission(win);\n\n /* Agents */\n this._agents = {\n desktop: new DesktopAgent(win),\n chrome: new MobileChromeAgent(win),\n firefox: new MobileFirefoxAgent(win),\n ms: new MSAgent(win),\n webkit: new WebKitAgent(win)\n };\n\n this._configuration = {\n serviceWorker: '/serviceWorker.min.js',\n fallback: function(payload) {}\n };\n }\n\n /**\n * Closes a notification\n * @param id ID of notification\n * @returns {boolean} denotes whether the operation was successful\n * @private\n */\n _closeNotification(id: number | string) {\n let success = true;\n const notification = this._notifications[id];\n\n if (notification !== undefined) {\n success = this._removeNotification(id);\n\n /* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */\n if (this._agents.desktop.isSupported())\n this._agents.desktop.close(notification);\n else if (this._agents.webkit.isSupported())\n /* Legacy WebKit browsers */\n this._agents.webkit.close(notification);\n else if (this._agents.ms.isSupported())\n /* IE9 */\n this._agents.ms.close();\n else {\n success = false;\n throw new Error(Messages.errors.unknown_interface);\n }\n\n return success;\n }\n\n return false;\n }\n\n /**\n * Adds a notification to the global dictionary of notifications\n * @param {Notification} notification\n * @return {Integer} Dictionary key of the notification\n * @private\n */\n _addNotification(notification: GenericNotification) {\n const id = this._currentId;\n this._notifications[id] = notification;\n this._currentId++;\n return id;\n }\n\n /**\n * Removes a notification with the given ID\n * @param {Integer} id - Dictionary key/ID of the notification to remove\n * @return {Boolean} boolean denoting success\n * @private\n */\n _removeNotification(id: number | string) {\n let success = false;\n\n if (this._notifications.hasOwnProperty(id)) {\n /* We're successful if we omit the given ID from the new array */\n delete this._notifications[id];\n success = true;\n }\n\n return success;\n }\n\n /**\n * Creates the wrapper for a given notification\n *\n * @param {Integer} id - Dictionary key/ID of the notification\n * @param {Map} options - Options used to create the notification\n * @returns {Map} wrapper hashmap object\n * @private\n */\n _prepareNotification(id: number, options: PushOptions) {\n let wrapper;\n\n /* Wrapper used to get/close notification later on */\n wrapper = {\n get: () => {\n return this._notifications[id];\n },\n\n close: () => {\n this._closeNotification(id);\n }\n };\n\n /* Autoclose timeout */\n if (options.timeout) {\n setTimeout(() => {\n wrapper.close();\n }, options.timeout);\n }\n\n return wrapper;\n }\n\n /**\n * Find the most recent notification from a ServiceWorker and add it to the global array\n * @param notifications\n * @private\n */\n _serviceWorkerCallback(\n notifications: GenericNotification[],\n options: PushOptions,\n resolve: ({} | null) => void\n ) {\n let id = this._addNotification(notifications[notifications.length - 1]);\n\n /* Listen for close requests from the ServiceWorker */\n if (navigator && navigator.serviceWorker) {\n navigator.serviceWorker.addEventListener('message', event => {\n const data = JSON.parse(event.data);\n\n if (data.action === 'close' && Number.isInteger(data.id))\n this._removeNotification(data.id);\n });\n\n resolve(this._prepareNotification(id, options));\n }\n\n resolve(null);\n }\n\n /**\n * Callback function for the 'create' method\n * @return {void}\n * @private\n */\n _createCallback(\n title: string,\n options: PushOptions,\n resolve: ({} | null) => void\n ) {\n let notification = null;\n let onClose;\n\n /* Set empty settings if none are specified */\n options = options || {};\n\n /* onClose event handler */\n onClose = id => {\n /* A bit redundant, but covers the cases when close() isn't explicitly called */\n this._removeNotification(id);\n if (Util.isFunction(options.onClose)) {\n options.onClose.call(this, notification);\n }\n };\n\n /* Safari 6+, Firefox 22+, Chrome 22+, Opera 25+ */\n if (this._agents.desktop.isSupported()) {\n try {\n /* Create a notification using the API if possible */\n notification = this._agents.desktop.create(title, options);\n } catch (e) {\n const id = this._currentId;\n const sw = this.config().serviceWorker;\n const cb = notifications =>\n this._serviceWorkerCallback(\n notifications,\n options,\n resolve\n );\n /* Create a Chrome ServiceWorker notification if it isn't supported */\n if (this._agents.chrome.isSupported()) {\n this._agents.chrome.create(id, title, options, sw, cb);\n }\n }\n /* Legacy WebKit browsers */\n } else if (this._agents.webkit.isSupported())\n notification = this._agents.webkit.create(title, options);\n else if (this._agents.firefox.isSupported())\n /* Firefox Mobile */\n this._agents.firefox.create(title, options);\n else if (this._agents.ms.isSupported())\n /* IE9 */\n notification = this._agents.ms.create(title, options);\n else {\n /* Default fallback */\n options.title = title;\n this.config().fallback(options);\n }\n\n if (notification !== null) {\n const id = this._addNotification(notification);\n const wrapper = this._prepareNotification(id, options);\n\n /* Notification callbacks */\n if (Util.isFunction(options.onShow))\n notification.addEventListener('show', options.onShow);\n\n if (Util.isFunction(options.onError))\n notification.addEventListener('error', options.onError);\n\n if (Util.isFunction(options.onClick))\n notification.addEventListener('click', options.onClick);\n\n notification.addEventListener('close', () => {\n onClose(id);\n });\n\n notification.addEventListener('cancel', () => {\n onClose(id);\n });\n\n /* Return the wrapper so the user can call close() */\n resolve(wrapper);\n }\n\n /* By default, pass an empty wrapper */\n resolve(null);\n }\n\n /**\n * Creates and displays a new notification\n * @param {Array} options\n * @return {Promise}\n */\n create(title: string, options: {}): Promise {\n let promiseCallback;\n\n /* Fail if no or an invalid title is provided */\n if (!Util.isString(title)) {\n throw new Error(Messages.errors.invalid_title);\n }\n\n /* Request permission if it isn't granted */\n if (!this.Permission.has()) {\n promiseCallback = (resolve: () => void, reject: string => void) => {\n this.Permission\n .request()\n .then(() => {\n this._createCallback(title, options, resolve);\n })\n .catch(() => {\n reject(Messages.errors.permission_denied);\n });\n };\n } else {\n promiseCallback = (resolve: () => void, reject: string => void) => {\n try {\n this._createCallback(title, options, resolve);\n } catch (e) {\n reject(e);\n }\n };\n }\n\n return new Promise(promiseCallback);\n }\n\n /**\n * Returns the notification count\n * @return {Integer} The notification count\n */\n count() {\n let count = 0;\n let key;\n\n for (key in this._notifications)\n if (this._notifications.hasOwnProperty(key)) count++;\n\n return count;\n }\n\n /**\n * Closes a notification with the given tag\n * @param {String} tag - Tag of the notification to close\n * @return {Boolean} boolean denoting success\n */\n close(tag: string) {\n let key, notification;\n\n for (key in this._notifications) {\n if (this._notifications.hasOwnProperty(key)) {\n notification = this._notifications[key];\n\n /* Run only if the tags match */\n if (notification.tag === tag) {\n /* Call the notification's close() method */\n return this._closeNotification(key);\n }\n }\n }\n }\n\n /**\n * Clears all notifications\n * @return {Boolean} boolean denoting whether the clear was successful in closing all notifications\n */\n clear() {\n let key,\n success = true;\n\n for (key in this._notifications)\n if (this._notifications.hasOwnProperty(key))\n success = success && this._closeNotification(key);\n\n return success;\n }\n\n /**\n * Denotes whether Push is supported in the current browser\n * @returns {boolean}\n */\n supported() {\n let supported = false;\n\n for (var agent in this._agents)\n if (this._agents.hasOwnProperty(agent))\n supported = supported || this._agents[agent].isSupported();\n\n return supported;\n }\n\n /**\n * Modifies settings or returns all settings if no parameter passed\n * @param settings\n */\n config(settings?: {}) {\n if (\n typeof settings !== 'undefined' ||\n (settings !== null && Util.isObject(settings))\n )\n Util.objectMerge(this._configuration, settings);\n\n return this._configuration;\n }\n\n /**\n * Copies the functions from a plugin to the main library\n * @param plugin\n */\n extend(manifest: PluginManifest) {\n var plugin,\n Plugin,\n hasProp = {}.hasOwnProperty;\n\n if (!hasProp.call(manifest, 'plugin')) {\n throw new Error(Messages.errors.invalid_plugin);\n } else {\n if (\n hasProp.call(manifest, 'config') &&\n Util.isObject(manifest.config) &&\n manifest.config !== null\n ) {\n this.config(manifest.config);\n }\n\n Plugin = manifest.plugin;\n plugin = new Plugin(this.config());\n\n for (var member in plugin) {\n if (\n hasProp.call(plugin, member) &&\n Util.isFunction(plugin[member])\n )\n // $FlowFixMe\n this[member] = plugin[member];\n }\n }\n }\n}\n"],"names":["_typeof","obj","Symbol","iterator","constructor","prototype","_classCallCheck","instance","Constructor","TypeError","_defineProperties","target","props","i","length","descriptor","enumerable","configurable","writable","Object","defineProperty","key","_createClass","protoProps","staticProps","_inherits","subClass","superClass","create","value","setPrototypeOf","__proto__","_possibleConstructorReturn","self","call","ReferenceError","errorPrefix","Permission","win","_win","GRANTED","DEFAULT","DENIED","_permissions","this","onGranted","onDenied","arguments","_requestWithCallback","_requestAsPromise","existing","get","resolve","result","_this","Notification","permission","webkitNotifications","checkPermission","requestPermission","then","catch","hasPermissions","isModernAPI","isWebkitAPI","Promise","resolvePromise","rejectPromise","resolver","_this2","isGranted","navigator","mozNotification","external","msIsSiteMode","Util","undefined","toString","rollupPluginBabelHelpers.typeof","source","hasOwnProperty","isObject","objectMerge","AbstractAgent","DesktopAgent","title","options","isString","icon","isUndefined","x32","body","tag","requireInteraction","notification","close","MobileChromeAgent","serviceWorker","func","str","match","id","callback","register","ready","localData","link","document","location","href","isFunction","onClick","getFunctionBody","onClose","data","assign","showNotification","vibrate","silent","getNotifications","active","postMessage","notifications","error","Error","Messages","errors","sw_notification_error","message","sw_registration_error","MobileFirefoxAgent","createNotification","show","MSAgent","msSiteModeClearIconOverlay","msSiteModeSetIconOverlay","x16","msSiteModeActivate","WebKitAgent","cancel","_currentId","_notifications","_agents","_configuration","payload","success","_removeNotification","desktop","isSupported","webkit","ms","unknown_interface","wrapper","_closeNotification","timeout","_addNotification","addEventListener","JSON","parse","event","action","Number","isInteger","_prepareNotification","e","sw","config","cb","_this3","_serviceWorkerCallback","chrome","firefox","fallback","onShow","onError","promiseCallback","invalid_title","has","reject","_createCallback","request","permission_denied","count","supported","agent","settings","manifest","plugin","hasProp","invalid_plugin","member","window","global"],"mappings":"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"} --------------------------------------------------------------------------------