diff --git a/README.md b/README.md index c1531da..b2d41e0 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,9 @@ Add a `config.push.json` file in your project and configure credentials / keys / ```js { + "browser": { + //manifest.json contents goes here + }, "apn": { "passphrase": "xxxxxxxxx", "key": "apnProdKey.pem", diff --git a/browser/service-worker.js b/browser/service-worker.js new file mode 100644 index 0000000..454f9bf --- /dev/null +++ b/browser/service-worker.js @@ -0,0 +1,40 @@ +self.addEventListener('push', showNotification) +self.addEventListener('notificationclick', closeNotificationAndOpenWindow) + +function showNotification(event) { + console.log('Received a push message', event) + + var title = 'Yay a message.' + var body = 'We have received a push message.' + var icon = '/images/icon-192x192.png' + var tag = 'simple-push-demo-notification-tag' + + event.waitUntil( + self.registration.showNotification(title, { + body: body, + icon: icon, + tag: tag + }) + ) +} + +function closeNotificationAndOpenWindow(event) { + console.log('On notification click: ', event.notification.tag) + // Android doesn’t close the notification when you click on it + // See: http://crbug.com/463146 + event.notification.close() + + // This looks to see if the current is already open and + // focuses if it is + event.waitUntil(clients.matchAll({ + type: "window" + }).then(function(clientList) { + for (var i = 0; i < clientList.length; i++) { + var client = clientList[i] + if (client.url == '/' && 'focus' in client) + return client.focus() + } + if (clients.openWindow) + return clients.openWindow('/') + })) +} \ No newline at end of file diff --git a/lib/client/browser.js b/lib/client/browser.js index 31979f9..c4b4dc6 100644 --- a/lib/client/browser.js +++ b/lib/client/browser.js @@ -64,267 +64,86 @@ Push.Configure = function(options) { // Start token updates initPushUpdates(options.appName); - // Add support for the raix:iframe push solution Deprecate this at some - // point mid aug 2015 - if (options.iframe) { - - var coldstart = true; - var startupTime = new Date(); - var startupThreshold = 1000; // ms - - var _atStartup = function() { - // If startup time is less than startupThreshold ago then lets say this is - // at startup. - return (new Date() - startupTime < startupThreshold); - }; - - var _parsePayload = function(value) { - // Android actually parses payload into an object - this is not the case with - // iOS (here is it just a string) - if (value !== ''+value) { - value = JSON.stringify(value); - } - - // Run the string through ejson - try { - return EJSON.parse(value); - } catch(err) { - return { error: err }; - } - }; - - // Rig iframe event listeners - options.iframe.addEventListener('deviceready', function() { - - // Maintain properties - - // At initial startup set startup time - startupTime = new Date(); - - // Update flag if app coldstart - options.iframe.addEventListener("pause", function() { - coldstart = false; - }, false); - - options.iframe.addEventListener('resume', function() { - // Reset startup time at resume - startupTime = new Date(); - }); - - // EO Maintain properties - - options.iframe.addEventListener('pushLaunch', function(e) { - - if (e.event === 'message') { - // Android event - - var sound = e.soundname || e.payload.sound; - - // Only prefix sound if actual text found - if (sound && sound.length) { - sound = '/android_asset/www/' + sound; - } - - // XXX: Investigate if we need more defaults - var unifiedMessage = { - message: e.payload.message || e.msg || '', - sound: sound, - badge: e.payload.msgcnt, - // Coldstart on android is a bit inconsistent - its only set when the - // notification opens the app - coldstart: (e.coldstart === Boolean(e.coldstart)) ? e.coldstart : coldstart, - background: !e.foreground, - foreground: !!e.foreground, - // open: _atStartup(), // This is the iOS implementation - open: (e.coldstart === Boolean(e.coldstart)), // If set true / false its an open event - type: 'gcm.cordova' - }; - - // If payload.ejson this is an object - we hand it over to parsePayload, - // parsePayload will do the convertion for us - if (e.payload.ejson) { - unifiedMessage.payload = _parsePayload(e.payload.ejson); - } - - // Trigger notification - onNotification(unifiedMessage); - - } else { - // iOS event - var sound = e.sound; // jshint ignore: line - - // Only prefix sound if actual text found - if (sound && sound.length) { - sound = '' + sound; - } - - // XXX: Investigate if we need more defaults - var unifiedMessage = { // jshint ignore: line - message: e.alert, - sound: sound, - badge: e.badge, - coldstart: coldstart, - background: !e.foreground, - foreground: !!e.foreground, - open: _atStartup(), - type: 'apn.cordova' - }; - - // E.ejson should be a string - we send it directly to payload - if (e.ejson) { - unifiedMessage.payload = _parsePayload(e.ejson); - } - - // Trigger notification - onNotification(unifiedMessage); - - } - - }); - - - options.iframe.addEventListener('pushSuccess', function(evt) { - // Reformat into new event - self.emit('register', evt.success); - }); - - options.iframe.addEventListener('pushToken', function(evt) { - if (evt.androidToken) { - // Format the android token - Push.emitState('token', { gcm: evt.androidToken }); - } else if (evt.iosToken) { - // Format the ios token - Push.emitState('token', { apn: evt.iosToken }); - } - }); - - options.iframe.addEventListener('pushError', function(evt) { - Push.emit('error', { type: 'cordova.browser', error: evt.error || evt }); - }); - - }); - } // EO options iframe - - if (typeof chrome !== 'undefined' && chrome.gcm) { - // chrome.gcm api is supported! - // https://developer.chrome.com/extensions/gcm - - // Set max message size - // chrome.gcm.MAX_MESSAGE_SIZE = 4096; - - if (options.gcm.projectNumber) { - chrome.gcm.register(options.gcm.projectNumber, function(token) { - if (token) { - self.emitState('token', { gcm: token }); - } else { - // Error - self.emit('error', { type: 'gcm.browser', error: 'Access denied' }); - } - }); + Meteor.startup(function() { + if (Notification.permission === 'denied') { + console.warn('The user has blocked notifications.'); + return; } - - } else if ('safari' in window && 'pushNotification' in window.safari) { - // https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NotificationProgrammingGuideForWebsites/PushNotifications/PushNotifications.html#//apple_ref/doc/uid/TP40013225-CH3-SW1 - - if (options.apn) { - - Meteor.startup(function() { - // Ensure that the user can receive Safari Push Notifications. - var permissionData = window.safari.pushNotification.permission(options.apn.websitePushId); - checkRemotePermission(permissionData); - }); - - var checkRemotePermission = function (permissionData) { - if (permissionData.permission === 'default') { - // This is a new web service URL and its validity is unknown. - window.safari.pushNotification.requestPermission( - options.apn.webServiceUrl, // The web service URL. - options.apn.websitePushId, // The Website Push ID. - {}, // Data that you choose to send to your server to help you identify the user. - checkRemotePermission // The callback function. - ); - } - else if (permissionData.permission === 'denied') { - // alert('denied'); - // The user said no. - self.emit('error', { type: 'apn.browser', error: 'Access denied' }); - } - else if (permissionData.permission === 'granted') { - // alert('granted'); - // The web service URL is a valid push provider, and the user said yes. - // permissionData.deviceToken is now available to use. - self.emitState('token', { apn: permissionData.deviceToken }); - } - }; - + if (!('showNotification' in ServiceWorkerRegistration.prototype)) { + console.warn('Notifications aren\'t supported.'); + return; } - - - } else if (navigator && navigator.push && navigator.push.register && navigator.mozSetMessageHandler) { - // check navigator.mozPush should be enough? - // https://wiki.mozilla.org/WebAPI/SimplePush - - var channel = 'push'; - - // Store the pushEndpoint - var pushEndpoint; - - Meteor.startup(function() { - setupAppRegistrations(); - }); - - function setupAppRegistrations() { // jshint ignore: line - // Issue a register() call - // to register to listen for a notification, - // you simply call push.register - // Here, we'll register a channel for "email" updates. - // Channels can be for anything the app would like to get notifications for. - var requestAccess = navigator.push.register(); - - requestAccess.onsuccess = function(e) { - // Store the endpoint - pushEndpoint = e.target.result; - - self.emitState('token', { - SimplePush: { - channel: channel, - endPoint: pushEndpoint - } - }); - }; - + // Check if push messaging is supported + if (!('PushManager' in window)) { + console.warn('Push messaging isn\'t supported.'); + return; } - - // Once we've registered, the AppServer can send version pings to the EndPoint. - // This will trigger a 'push' message to be sent to this handler. - navigator.mozSetMessageHandler('push', function(message) { - if (message.pushEndpoint === pushEndpoint) { - // Did we launch or were we already running? - self.emit('startup', message); - } - }); - - // // to unregister, you simply call.. - // AppFramework.addEventListener('user-logout', function() { - // navigator.push.unregister(pushEndpoint); - // }); - - // error recovery mechanism - // will be called very rarely, but application - // should register again when it is called - navigator.mozSetMessageHandler('register', function(/* e */) { - setupAppRegistrations(); - }); - - - - } - -}; - -/* -TODO: - -add event listener api - -*/ + Push.browserInit(); + }); +} + +Push.browserInit = function(){ + $("body").append(''); + navigator.serviceWorker.register('/packages/raix_push/browser/service-worker.js').then(initialiseState); +} + +Push.browserUnsubscribe = function() { + navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) { + serviceWorkerRegistration.pushManager.getSubscription().then( + function(pushSubscription) { + if (!pushSubscription) { + isPushEnabled = false; + return; + } + + var subscriptionId = pushSubscription.subscriptionId; + pushSubscription.unsubscribe().then(function(successful) { + isPushEnabled = false; + }).catch(function(e) { + console.log('Unsubscription error: ', e); + }); + }).catch(function(e) { + console.error('Error thrown while unsubscribing from push messaging.', e); + }); + }); +} + +Push.browserSubscribe = function() { + navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) { + serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true}) + .then(function(subscription) { + return sendSubscriptionToServer(subscription); + }) + .catch(function(e) { + if (Notification.permission === 'denied') { + console.warn('Permission for Notifications was denied'); + } else { + console.error('Unable to subscribe to push.', e); + } + }); + }); +} + +function sendSubscriptionToServer(subscription){ + Push.emit('startup', subscription); + /*Push.emitState('token', { + SimplePush: { + channel: "push", + endPoint: subscription.endpoint + } + });*/ +} + +function initialiseState() { + navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) { + serviceWorkerRegistration.pushManager.getSubscription() + .then(function(subscription) { + if (subscription) { + sendSubscriptionToServer(subscription); + } + }) + .catch(function(err) { + console.warn('Error during getSubscription()', err); + }); + }); +} \ No newline at end of file diff --git a/lib/client/client.js b/lib/client/client.js index cd2c468..1f664db 100644 --- a/lib/client/client.js +++ b/lib/client/client.js @@ -75,6 +75,9 @@ Push.enabled = function(state) { if (state !== stored.enabled && stored.id) { // Latency compensation _setEnabled(state); + if(!Meteor.isCordova){ + Push.browserSubscribe(); + } // Update server Meteor.call('raix:push-enable', { id: stored.id, diff --git a/lib/server/manifest.js b/lib/server/manifest.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/server/push.api.js b/lib/server/push.api.js index 63d0a63..15bf84b 100644 --- a/lib/server/push.api.js +++ b/lib/server/push.api.js @@ -1,3 +1,9 @@ +// necessary to parse POST data +var connect = Npm.require('connect'); +// necessary for Collection use and other wrapped methods +var Fiber = Npm.require('fibers'); + +var bodyParser = Npm.require("body-parser"); /* A general purpose user CordovaPush ios, android, mail, twitter?, facebook?, sms?, snailMail? :) @@ -41,6 +47,23 @@ Push.Configure = function(options) { console.log('Push.Configure', options); } + WebApp.connectHandlers + .use(bodyParser.urlencoded()) + .use(bodyParser.json()) + .use('/push-manifest.json', function(req, res, next) { + + // necessary for Collection use and other wrapped methods + Fiber(function() { + res.writeHead(200, {'Content-Type': 'application/json'}); + //console.log(self.config) + + res.end(JSON.stringify(options.browser)); + //HTTP.get(Meteor.absoluteUrl("/config.push.json")).data; + //res.end(Assets.getText("config.push.json")); + + }).run(); + }); + // This function is called when a token is replaced on a device - normally // this should not happen, but if it does we should take action on it _replaceToken = function(currentToken, newToken) { diff --git a/package.js b/package.js index 97cb77d..6487bfd 100644 --- a/package.js +++ b/package.js @@ -9,6 +9,9 @@ Package.describe({ Npm.depends({ 'apn' : '1.6.2', // '1.7.4', // working: 1.6.2 'node-gcm' : '0.9.6', // '0.12.0' // working: 0.9.6 + 'connect': '3.4.1', + 'fibers': '1.0.13', + 'body-parser': '1.15.1' }); Cordova.depends({ @@ -48,11 +51,15 @@ Package.onUse(function(api) { 'check', 'mongo', 'underscore', + 'webapp', 'ejson' ], ['client', 'server']); api.use('mongo', 'server'); + // public for browser + api.addFiles('browser/service-worker.js', 'web.browser', {isAsset: true}); + // API api.addFiles('lib/client/cordova.js', 'web.cordova'); diff --git a/plugin/push.configuration.js b/plugin/push.configuration.js index 34a0849..523b5b9 100644 --- a/plugin/push.configuration.js +++ b/plugin/push.configuration.js @@ -4,6 +4,7 @@ var stripComments = Npm.require('strip-json-comments'); // Check the config and log errors var checkConfig = function(config) { // jshint ignore:line check(config, { + browser: Match.Optional(Object), apn: Match.Optional({ passphrase: String, cert: String, @@ -54,6 +55,7 @@ var clone = function(name, config, result) { }; var cloneCommon = function(config, result) { + clone('browser', config, result); clone('production', config, result); clone('sound', config, result); clone('badge', config, result); @@ -189,7 +191,6 @@ Plugin.registerSourceHandler('push.json', function(compileStep) { try { // Try parsing the json var config = JSON.parse(configString); - // Clone the relevant config var cloneConfig = archConfig[compileStep.arch];