diff --git a/.gitignore b/.gitignore index 0839c11..1ea85be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules/ .vscode -src/browser-notif.js +src/*.js src/Test.ts example/ \ No newline at end of file diff --git a/README.md b/README.md index 41e6705..5bede03 100755 --- a/README.md +++ b/README.md @@ -24,32 +24,26 @@ npm install browser-notif --save import BrowserNotif from './BrowserNotif' // If you want to explicitly call request permission. Usually this is only called once. -BrowserNotif.requestPermission(p => console.log(p)) +BrowserNotif.requestPermission().then(p => console.log(p)) // Create instance -let notif1 = new BrowserNotif() - -notif1 - .notify('First Notif', 'Hi there! Nice to meet you.', (n) => { - console.log('First Notif fired!', n) - }) - .click(() => { - window.open('https://www.ipanardian.com') - }) - -// With options -let notif2 = new BrowserNotif({ - icon: 'icon.png', +let notif1 = new BrowserNotif({ + icon: 'logo.png', lang: 'en-US', - timeout: 10 // How long notif will appear in seconds -}) + timeout: 10 // How long notif will be appear in seconds +}) -notif2 - .notify('Second Notif', 'Typescript has released new version, chek it out!', (n) => { - console.log('Second Notif fired!', n) +notif1 + .notify('First Notif', 'Hi there! Nice to meet you.', { + click() { + window.open('//ipanardian.com') + }, + error() { + //On error + } }) - .click(() => { - window.open('https://www.typescriptlang.org') + .then(() => { + //Do something }) //close notif pragmatically @@ -59,34 +53,36 @@ notif1.close() ### Javascript In Javascript BrowserNotif use UMD module pattern and Polyfill for ```Object.assign```. ```js -BrowserNotif.default.requestPermission(function(p) { - console.log(p) -}) +BrowserNotif.default.requestPermission().then(p => console.log(p)) var notif = new BrowserNotif.default({icon: 'icon.png'}) notif - .notify('First Notif', 'Hi there! Nice to meet you.', function(n) { - console.log('First Notif fired!', n) + .notify('First Notif', 'Hi there! Nice to meet you.', { + click: function() { + window.open('//ipanardian.com') + } }) - .click(function() { - window.open('https://www.ipanardian.com') + .then(function() { + //Do something }) ``` ## Notification On Mobile Devices Notification on mobile devices is using ```Service Worker```. A service worker is an event-driven worker registered against an origin and a path. Service worker runs in the background and only run over HTTPS. -> Put file 'sw.js' on root directory +> Put file 'sw.min.js' on root directory of application ```js -var notif = new BrowserNotif.default({icon: 'icon.png'}) +var notif = new BrowserNotif({icon: 'icon.png', serviceWorkerPath: 'sw.min.js'}) notif - .clickOnServiceWorker(function(){ - clients.openWindow('//ipanardian.com') - }) - .notify('Notif from Ipan Ardian', 'Hi there! Nice to meet you.', function(n) { - console.log(n) + .notify('First Notif', 'Hi there! Nice to meet you.', { + .clickOnServiceWorker(clients => { + clients.openWindow('//ipanardian.com') + }) + }) + .then(() => { + //Do something }) ``` ![gif](http://i.giphy.com/l3vRfm7aebpZjQHf2.gif) @@ -107,7 +103,7 @@ Check 'dist' folder. - BrowserNotif.js - BrowserNotif.min.js - BrowserNotif.min.js.map -- sw.js +- sw.min.js ## Browser compatibility If browser not support Notification API then native alert will be triggered. diff --git a/src/BrowserNotif.ts b/src/BrowserNotif.ts index ba5f2a8..160dbbc 100644 --- a/src/BrowserNotif.ts +++ b/src/BrowserNotif.ts @@ -11,60 +11,7 @@ /// /// -/** - * Interface for BrowserNotif configuration - * - * dir?: NotificationDirection; - * lang?: string; - * body?: string; - * tag?: string; - * image?: string; - * icon?: string; - * badge?: string; - * sound?: string; - * vibrate?: number | number[], - * timestamp?: number, - * renotify?: boolean; - * silent?: boolean; - * requireInteraction?: boolean; - * data?: any; - * actions?: NotificationAction[] - * timeout?: number; - * serviceWorkerPath?: string; Default : sw.js - */ -interface BrowserNotifOptions extends NotificationOptions { - [key: string]: any - timeout?: number - serviceWorkerPath?: string -} - -/** - * Interface for BrowserNotif - */ -interface BrowserNotifInterface { - notify(title: string, body: string, callback: (notif: Notification) => void): BrowserNotif - click(callback: () => void): BrowserNotif - close(): void - error(callback: () => void): BrowserNotif - isMobile(): boolean -} - -/** - * Interface for Permission - */ -interface PermissionInterface { - Default: string - Granted: string, - Denied: string -} - -/** - * Interface for Data - */ -interface Data { - [key: string]: any - clickOnServiceWorker?: string -} +import {BrowserNotifOptions, BrowserNotifInterface, BrowserNotifEvent, BrowserNotifData, PermissionInterface} from './Interface' export default class BrowserNotif implements BrowserNotifInterface { @@ -94,9 +41,9 @@ export default class BrowserNotif implements BrowserNotifInterface /** * Arbitrary data - * @type {Data} + * @type {BrowserNotifData} */ - protected data: Data = {} + protected data: BrowserNotifData = {} /** * How long notification will appear in second. Set to 0 to make always visible @@ -166,14 +113,15 @@ export default class BrowserNotif implements BrowserNotifInterface /** * Register serviceWorker and Get request permission - * @param {string} callback + * @param Promise */ - public static requestPermission(callback: (permission: NotificationPermission) => void): void { - Notification.requestPermission((permission: NotificationPermission) => { - if (typeof callback === 'function') { - callback.call(this, permission) - } - }); + public static requestPermission(): Promise { + return new Promise((resolve, reject) => { + Notification.requestPermission().then((permission: NotificationPermission) => { + resolve(permission) + }) + .catch(err => reject(err)) + }) } /** @@ -182,7 +130,7 @@ export default class BrowserNotif implements BrowserNotifInterface */ protected _registerServiceWorker(): void { if ('serviceWorker' in navigator) { - navigator.serviceWorker.register(this.options.serviceWorkerPath || 'sw.js').then(serviceWorkerRegistration => { + navigator.serviceWorker.register(this.options.serviceWorkerPath || 'sw.min.js').then(serviceWorkerRegistration => { console.log('Service Worker is ready :', serviceWorkerRegistration) }) .catch(e => console.warn('BrowserNotif: ', e)) @@ -192,73 +140,87 @@ export default class BrowserNotif implements BrowserNotifInterface /** * Show notification from serviceWorker * This is an experimental technology! - * @param {()} callback + * @return Promise */ - protected _showNotifServiceWorker(callback?: () => void): void { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - if (!this.notifOptions.tag) { - this.notifOptions.tag = 'browserNotif_'+ Math.random().toString().substr(3, 10) - } - if (Object.keys(this.data).length > 0) { - this.notifOptions.data = JSON.stringify(this.data) - } - registration.showNotification(this.title, this.notifOptions).then(() => { - callback.call(this) + protected _showNotifServiceWorker(): Promise { + return new Promise((resolve, reject) => { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + if (!this.notifOptions.tag) { + this.notifOptions.tag = 'browserNotif_'+ Math.random().toString().substr(3, 10) + } + if (Object.keys(this.data).length > 0) { + this.notifOptions.data = JSON.stringify(this.data) + } + registration.showNotification(this.title, this.notifOptions).then((notificationEvent) => { + resolve(notificationEvent) + }) }) - }) - .catch(e => console.error('BrowserNotif: ', e)) - } + .catch(e => { + throw new Error('BrowserNotif: '+ e) + }) + } + else { + throw new Error('BrowserNotif: serviceWorker not available') + } + }) } /** * Get notification object from serviceWorker - * @param {Notification} callback + * @return Promise */ - protected _getNotifServiceWorker(callback: (notification: Notification) => void): void { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - registration.getNotifications({tag: this.notifOptions.tag}).then(notifications => { - if (notifications.length > 0) { - callback.call(this, notifications[0]) - } - }) - }) - .catch(e => console.error('BrowserNotif: ', e)) - } + protected _getNotifServiceWorker(): Promise { + return new Promise((resolve, reject) => { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.getNotifications({tag: this.notifOptions.tag}).then(notifications => { + if (notifications.length > 0) { + resolve(notifications[0]) + } + else { + reject('BrowserNotif: Notification not found') + } + }) + }) + .catch(e => { + reject('BrowserNotif: '+ e) + }) + } + }) } /** - * Trigger notify + * Create notify * @param {string} title * @param {string} body - * @param {Event} callback - * @return {BrowserNotif} + * @param {BrowserNotifEvent} notifEvent + * @return Promise */ - public notify(title: string, body: string, callback?: (notif: Notification) => void): BrowserNotif { - if (!BrowserNotif.isSupported()) { - alert(`${title}\n\n${body}`) - return this - } - this._validateTitle(title) - - this.title = title; - this.notifOptions.body = body; - if (this.Permission.Granted === Notification.permission) { - this._notify(callback); - } - else if (this.Permission.Denied !== Notification.permission) { - BrowserNotif.requestPermission(permission => { - if (this.Permission.Granted === permission) { - this._notify(callback); - } - }); - } - else { - console.warn('User denied the notification permission') - } - - return this + public notify(title: string, body: string, notifEvent?: BrowserNotifEvent): Promise { + return new Promise((resolve, reject) => { + if (!BrowserNotif.isSupported()) { + alert(`${title}\n\n${body}`) + resolve() + } + this._validateTitle(title) + + this.title = title; + this.notifOptions.body = body; + if (this.Permission.Granted === Notification.permission) { + this._notify(notifEvent).then(() => resolve()) + } + else if (this.Permission.Denied !== Notification.permission) { + BrowserNotif.requestPermission().then(permission => { + if (this.Permission.Granted === permission) { + this._notify(notifEvent).then(() => resolve()) + } + }); + } + else { + reject('User denied the notification permission') + } + }) } /** @@ -276,31 +238,43 @@ export default class BrowserNotif implements BrowserNotifInterface /** * Create an instance of Notification API - * @param {Notification} callback + * @param {BrowserNotifEvent} notifEvent + * @return {Promise} */ - protected _notify(callback?: (notif: Notification) => void): void { - if (this.isMobile()) { - this._registerServiceWorker() - this._showNotifServiceWorker(() => { - this._getNotifServiceWorker(notification => { - this.notification = notification - if (typeof callback === 'function') { - callback.call(this, this.notification) - } + protected _notify(notifEvent?: BrowserNotifEvent): Promise { + return new Promise((resolve, reject) => { + if (this.isMobile()) { + Promise.resolve().then(() => { + this._registerServiceWorker() + this._prepareClickOnServiceWorker.apply(this, [notifEvent]) + return this._showNotifServiceWorker() + + }) + .then((notificationEvent) => { + this._getNotifServiceWorker().then(notification => { + this.notification = notification + resolve() + }) + }) + .catch(err => { + reject(err) }) - }) - } - else { - if (this.notification instanceof Notification) { - this.notification.close() } - this.notification = new Notification(this.title, this.notifOptions) - this._closeNotification() - if (typeof callback === 'function') { - callback.call(this, this.notification) + else { + Promise.resolve().then(() => { + if (this.notification instanceof Notification) { + this.notification.close() + } + this.notification = new Notification(this.title, this.notifOptions) + this._prepareNotifEvent.apply(this, [notifEvent]) + this._closeNotification() + resolve() + }) + .catch(err => { + reject(err) + }) } - } - + }) } /** @@ -311,32 +285,35 @@ export default class BrowserNotif implements BrowserNotifInterface setTimeout(this.notification.close.bind(this.notification), this.timeout * 1e3); } } - + /** - * Click event on Notification - * @param {} callback - * @return {BrowserNotif} + * Prepare for Notification Event + * @param {BrowserNotifEvent} notifEvent */ - public click(callback: () => void): BrowserNotif { - if (typeof callback === 'function' && this.notification instanceof Notification) { - this.notification.onclick = () => { - this.notification.close() - callback.call(this); + protected _prepareNotifEvent(notifEvent?: BrowserNotifEvent): void { + if (typeof notifEvent != 'undefined' && this.notification instanceof Notification) { + if (typeof notifEvent.click == 'function') { + this.notification.onclick = () => { + this.notification.close() + notifEvent.click.call(this) + } + } + if (typeof notifEvent.error == 'function') { + this.notification.onerror = () => { + notifEvent.error.call(this) + } } } - return this } /** - * Click event on serviceWorker Notification - * @param {} callback - * @return {BrowserNotif} + * Prepare for click event on serviceWorker Notification + * @param {BrowserNotifEvent} notifEvent */ - public clickOnServiceWorker(callback: () => void): BrowserNotif { - if (typeof callback === 'function') { - this.data.clickOnServiceWorker = callback.toString() + protected _prepareClickOnServiceWorker(notifEvent?: BrowserNotifEvent): void { + if (typeof notifEvent != 'undefined' && typeof notifEvent.clickOnServiceWorker == 'function') { + this.data.clickOnServiceWorker = notifEvent.clickOnServiceWorker.toString() } - return this } /** @@ -348,20 +325,6 @@ export default class BrowserNotif implements BrowserNotifInterface } } - /** - * Error of Notification - * @param {} callback - * @return {BrowserNotif} - */ - public error(callback: () => void): BrowserNotif { - if (typeof callback === 'function' && this.notification instanceof Notification) { - this.notification.onerror = () => { - callback.call(this); - } - } - return this - } - /** * Detect mobile device * @return {boolean} diff --git a/src/Interface.ts b/src/Interface.ts new file mode 100644 index 0000000..2c84516 --- /dev/null +++ b/src/Interface.ts @@ -0,0 +1,81 @@ +/** + * Interface for BrowserNotif configuration + * + * dir?: NotificationDirection; + * lang?: string; + * body?: string; + * tag?: string; + * image?: string; + * icon?: string; + * badge?: string; + * sound?: string; + * vibrate?: number | number[], + * timestamp?: number, + * renotify?: boolean; + * silent?: boolean; + * requireInteraction?: boolean; + * data?: any; + * actions?: NotificationAction[] + * timeout?: number; + * serviceWorkerPath?: string; Default : sw.js + */ +export interface BrowserNotifOptions extends NotificationOptions { + [key: string]: any + timeout?: number + serviceWorkerPath?: string +} + +/** + * Interface for BrowserNotif + */ +export interface BrowserNotifInterface { + notify(title: string, body: string, notifEvent?: BrowserNotifEvent): Promise + close(): void + isMobile(): boolean +} + +/** + * Interface for BrowserNotifEvent + */ +export interface BrowserNotifEvent { + click?(): void + clickOnServiceWorker?(clients: Clients): void + error?(): void +} + +/** + * Interface for Permission + */ +export interface PermissionInterface { + Default: string + Granted: string + Denied: string +} + +/** + * Interface for NotifData + */ +export interface BrowserNotifData { + [key: string]: any + clickOnServiceWorker?: string +} + +/** + * The Clients interface of the Service Workers API + */ +export interface Clients { + get(id: string): Promise + matchAll(options: Object): Promise + openWindow(url: string): void + claim(): Promise +} + +/** + * The Client interface of the Service Workers API + */ +export interface Client { + readonly frameType: string + readonly id: string + readonly url: string + postMessage(message: string, transfer?: Object): void +} \ No newline at end of file diff --git a/src/sw.ts b/src/sw.ts index 9e32bf9..a79c00b 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -27,7 +27,7 @@ self.onnotificationclick = function(event) { } try { if (data !== null && data.clickOnServiceWorker !== undefined) { - Function("(" +data['clickOnServiceWorker']+ ")()")() + Function("(" +data['clickOnServiceWorker']+ ")(clients)")() } } catch (error) { throw new Error('BrowserNotif: Error clickOnServiceWorker '+ error)