"use strict";
(function (Q, $) {
var Users = Q.plugins.Users;
Q.onReady.add(function () {
if (Q.info.isCordova && (window.FCMPlugin || window.PushNotification)) {
Users.Device.appId = Q.cookie('Q_appId');
if (!Users.Device.appId) {
return console.warn("appId is not defined");
}
}
Users.Device.onInit.set(function () {
// update device id if device subscribed
Users.Device.subscribed(function (err, subscribed) {
if (!subscribed) {
return;
}
// resubscribe device
Users.Device.unsubscribe(Users.Device.subscribe);
});
}, 'Users.Device');
Users.Device.init(function () {
// Device adapter was initialized
Q.handle(Users.Device.onInit);
console.log('Users.Device adapter init: ' + Users.Device.adapter.adapterName);
});
}, 'Users.Device');
/**
* @class Users.Device
*/
Users.Device = {
/**
* Subscribe to listen for push notifications
* if the current environment supports it.
* (Web Push, Cordova, etc.)
* @method subscribe
* @static
* @param {Function} callback
* @param {Object} options
* @param {Boolean} options.userVisibleOnly whether the returned push subscription
* will only be used for messages whose effect is made visible to the user
* @param {String} options.applicationServerKey A public key your push server
* will use to send messages to client apps via a push server. This value is
* part of a signing key pair generated by your application server, and usable
* with elliptic curve digital signature (ECDSA), over the P-256 curve.
*/
subscribe: function (callback, options) {
Users.Device.getAdapter(function (err, adapter) {
if (err) {
return Q.handle(callback, null, [err]);
}
// check whether notification granted
Users.Device.notificationGranted(function (granted) {
// if user refuses notifications - do nothing
if (granted === false) {
return Q.handle(callback, null, [null, null]);
}
// check if the device is subscribed
Users.Device.subscribed(function (err, subscribed) {
if (err) {
Q.handle(callback, null, [err]);
}
// if the device is subscribed then do nothing
if (subscribed) {
return Q.handle(callback, null, [null, subscribed]);
}
// if the user already granted notifications but the device is not subscribed then just subscribe
// device without any confirmation dialog
if (granted === true) {
return adapter.subscribe(function (err, subscribed) {
Q.handle(callback, null, [err, subscribed]);
}, options);
}
// if the user is undecided with notifications then do call the confirmation
var userId = Q.Users.loggedInUserId();
var cache = Q.Cache.local('Users.Permissions.notifications');
var requested = cache.get(userId);
// if permissions already requested - don't request it again
if (Q.getObject(['cbpos'], requested) === true) {
return Q.handle(callback, null, [null, null]);
}
Q.Text.get('Users/content', function (err, text) {
text = Q.getObject(["notifications"], text);
if (!text) {
console.warn('Notifications confirmations texts not found');
}
// if not - ask
Q.confirm(text.prompt, function (res) {
if (!res) {
// save to cache that notifications requested
// only if user refused, because otherwise - notifications has granted
cache.set(userId, true);
return Q.handle(callback, null, [null, null]);
}
adapter.subscribe(function (err, subscribed) {
Q.handle(callback, null, [err, subscribed]);
}, options);
}, { ok: text.yes, cancel: text.no });
});
});
});
});
},
/**
* Unsubscribe to stop handling push notifications
* if we were previously subscribed
* @method unsubscribe
* @static
* @param {Function} callback
*/
unsubscribe: function (callback) {
this.getAdapter(function (err, adapter) {
if (err) {
Q.handle(callback, null, [err]);
} else {
adapter.unsubscribe(function (err) {
Q.handle(callback, null, [err]);
});
}
});
},
/**
* Checks whether the user already has a subscription.
* @method subscribed
* @static
* @param {Boolean} callback Whether the user already has a subscription
*/
subscribed: function (callback) {
this.getAdapter(function (err, adapter) {
if (err) {
Q.handle(callback, null, [err]);
} else {
adapter.subscribed(function (err, subscribed) {
Q.handle(callback, null, [err, subscribed]);
});
}
});
},
/**
* Return whether device have notifications granted or no
* @method notificationGranted
* @static
* @param {function} callback
* @return {string|bool} return true if granted, false if blocked, "default" if didn't make choise yet
*/
notificationGranted: function (callback) {
this.getAdapter(function (err, adapter) {
if (err) {
Q.handle(callback, null, [err]);
} else {
adapter.notificationGranted(callback);
}
});
},
/**
* Event occurs when a notification comes in to be processed by the app.
* The handlers you add are supposed to process it.
* The notification might have brought the app back from the background,
* or not. Please see the documentation here:
* https://github.com/katzer/cordova-plugin-local-notifications
* @event onNotification
*/
onNotification: new Q.Event(),
/**
* Event occurs when the device adapter was initialized.
* @event onInit
*/
onInit: new Q.Event(),
init: function (callback) {
if (Q.info.isCordova && Q.info.isAndroid()) {
// FCM adapter
if (!window.FCMPlugin) {
console.warn("FCMPlugin cordova plugin is not installed.");
return;
}
this.adapter = adapterFCM;
} else if (Q.info.isCordova && (Q.info.platform === 'ios')) {
// PushNotification adapter
if (!window.PushNotification) {
console.warn("PushNotification cordova plugin is not installed.");
return;
}
this.adapter = adapterPushNotification;
} else if ((Q.info.browser.name === 'chrome') || (Q.info.browser.name === 'firefox')) {
// Chrome and Firefox
this.adapter = adapterWeb;
} else if (Q.info.browser.name === 'safari') {
// TODO implement adapter for Safari Browser
this.adapter = adapterWeb;
}
if (this.adapter) {
this.adapter.init(callback);
} else {
console.info("Users.Device: No suitable adapter for push notifications");
}
},
getAdapter: function (callback) {
if (!this.adapter) {
Q.handle(callback, null, [new Error('There is no suitable adapter for this type of device')]);
return;
}
Q.handle(callback, null, [null, this.adapter]);
},
adapter: null,
serviceWorkerUpdate: function() {
if ((this.adapter.adapterName !== 'Web') || !('serviceWorker' in navigator)) {
return;
}
navigator.serviceWorker.getRegistrations().then(function (registrations) {
Q.each(registrations, function (index, registration) {
registration.update().then(function () {
console.log("Service worker " + registration.active.scriptURL + " has been successfully updated");
});
});
});
}
};
// Adapter for Chrome and Firefox
var adapterWeb = {
adapterName: 'Web',
init: function (callback) {
this.appConfig = Q.getObject('Q.Users.browserApps.' + Q.info.browser.name + '.' + Q.info.app);
if (!this.appConfig) {
console.warn('Unable to init adapter. App config is not defined.');
return;
}
Q.handle(callback);
},
subscribe: function (callback, options) {
var self = this;
this.getServiceWorkerRegistration(function (err, sw) {
if (err)
Q.handle(callback, null, [err]);
else {
var userVisibleOnly = true;
if (options && !options.userVisibleOnly) {
userVisibleOnly = false;
}
sw.pushManager.subscribe({
userVisibleOnly: userVisibleOnly,
applicationServerKey: _urlB64ToUint8Array(self.appConfig.publicKey)
}).then(function (subscription) {
_saveSubscription(subscription, self.appConfig, function (err, res) {
Q.handle(callback, null, [err, res]);
});
}).catch(function (err) {
Users.Device.notificationGranted(function (granted) {
if (granted) {
console.error('Users.Device: Unable to subscribe to push.', err);
} else {
console.error('Users.Device: Permission for Notifications was denied');
}
});
Q.handle(callback, null, [err]);
});
}
});
},
unsubscribe: function (callback) {
this.getServiceWorkerRegistration(function (err, sw) {
if (err)
Q.handle(callback, null, [err]);
else {
sw.pushManager.getSubscription()
.then(function (subscription) {
if (subscription) {
_deleteSubscription(subscription.endpoint, function (err, res) {
Q.handle(callback, null, [err, res]);
});
subscription.unsubscribe();
console.log('Users.Device: User is unsubscribed.');
}
});
}
});
},
subscribed: function (callback) {
this.getServiceWorkerRegistration(function (err, sw) {
if (err)
Q.handle(callback, null, [err]);
else {
sw.pushManager.getSubscription()
.then(function (subscription) {
Q.handle(callback, null, [null, subscription]);
}).catch(function (err) {
Q.handle(callback, null, [err]);
});
}
});
},
/**
*
* @param callback
* @return {string|bool} Possible values: true, false, 'default'
*/
notificationGranted: function (callback) {
if (window.Notification) {
var permission;
if (window.Notification.permission === 'granted') {
permission = true;
} else if (window.Notification.permission === 'denied') {
permission = false;
} else {
permission = window.Notification.permission;
}
return Q.handle(callback, window.Notification, [permission]);
}
Q.handle(callback, null, [false]);
},
getServiceWorkerRegistration: function (callback) {
var self = this;
if (this.serviceWorkerRegistration) {
return Q.handle(callback, null, [null, this.serviceWorkerRegistration]);
}
_registerServiceWorker.bind(this)(function (err, sw) {
if (err)
return Q.handle(callback, null, [err]);
else {
self.serviceWorkerRegistration = sw;
return Q.handle(callback, null, [null, sw]);
}
});
},
serviceWorkerRegistration: null,
appConfig: null
};
// Adapter for FCM
var adapterFCM = {
adapterName: 'FCM',
init: function (callback) {
FCMPlugin.onTokenRefresh(function (token) {
_registerDevice(token);
});
FCMPlugin.onNotification(function (data) {
// data.wasTapped is true: Notification was received on device tray and tapped by the user.
// data.wasTapped is false: Notification was received in foreground. Maybe the user needs to be notified.
Users.Device.onNotification.handle(data);
});
Q.handle(callback);
},
subscribe: function (callback) {
FCMPlugin.getToken(function (token) {
_registerDevice(token, callback);
});
},
unsubscribe: function (callback) {
var deviceId = _getFromStorage('deviceId');
_removeFromStorage('deviceId');
_deleteSubscription(deviceId, function (err, res) {
Q.handle(callback, null, [err, res]);
});
},
subscribed: function (callback) {
if (_getFromStorage('deviceId')) {
Q.handle(callback, null, [null, true]);
} else {
Q.handle(callback, null, [null, false]);
}
},
notificationGranted: function (callback) {
Q.handle(callback, null, [true]);
}
};
// Adapter for PushNotification
var adapterPushNotification = {
adapterName: 'PushNotification',
init: function (callback) {
if (_getFromStorage('deviceId')) {
_pushNotificationInit();
}
Q.handle(callback);
},
subscribe: function (callback) {
_pushNotificationInit();
Q.handle(callback);
},
unsubscribe: function (callback) {
var deviceId = _getFromStorage('deviceId');
_removeFromStorage('deviceId');
_deleteSubscription(deviceId, function (err, res) {
Q.handle(callback, null, [err, res]);
});
},
subscribed: function (callback) {
if (_getFromStorage('deviceId')) {
Q.handle(callback, null, [null, true]);
} else {
Q.handle(callback, null, [null, false]);
}
},
notificationGranted: function (callback) {
PushNotification.hasPermission(function (data) {
data.isEnabled ? Q.handle(callback, null, [true]) : Q.handle(callback, null, ['default']);
});
}
};
function _registerServiceWorker (callback) {
if (Q.info.url.substr(0, 8) !== 'https://') {
Q.handle(callback, null, [new Error("Push notifications require HTTPS")]);
return;
}
if (!(('serviceWorker' in navigator) && ('PushManager' in window))) {
Q.handle(callback, null, [new Error("Push messaging is not supported")]);
return;
}
navigator.serviceWorker.register('/Q/plugins/Users/js/sw.js')
.then(function (swReg) {
navigator.serviceWorker.addEventListener('message', function (event) {
Users.Device.onNotification.handle(event.data);
});
console.log('Service Worker is registered.');
Q.handle(callback, null, [null, swReg]);
})
.catch(function (error) {
Q.handle(callback, null, [error]);
console.error('Users.Device: Service Worker Error', error);
});
}
function _registerDevice (deviceId, callback) {
if (!deviceId || !Q.Users.loggedInUser) {
return Q.handle(callback, null, [new Error('Error while registering device. User must be logged in and deviceId must be set.')]);
}
var appId = Users.Device.appId;
if (!appId) {
return Q.handle(callback, null, [new Error('Error while registering device. AppId must be must be set.')]);
}
Q.req('Users/device', function (err, response) {
var msg = Q.firstErrorMessage(err, response && response.errors);
if (msg) {
return console.warn("Users.Device._registerDevice" + msg);
}
Q.handle(Users.onDevice, [response.data]);
_setToStorage('deviceId', deviceId);
Q.handle(callback, null, [err, response]);
}, {
method: 'post',
fields: {
appId: appId,
deviceId: deviceId
}
});
}
function _urlB64ToUint8Array (base64String) {
var padding = '='.repeat((4 - base64String.length % 4) % 4);
var base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
var rawData = window.atob(base64);
var outputArray = new Uint8Array(rawData.length);
for (var i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
function _saveSubscription (subscription, appConfig, callback) {
if (!subscription) {
return Q.handle(callback, null, [new Error('No subscription data')]);
}
subscription = JSON.parse(JSON.stringify(subscription));
Q.req('Users/device', function (err, response) {
if (!err) {
Q.handle(Users.onDevice, [response.data]);
}
Q.handle(callback, null, [err, response]);
}, {
method: 'post',
fields: {
deviceId: subscription.endpoint,
auth: subscription.keys.auth,
p256dh: subscription.keys.p256dh,
appId: appConfig.appId
}
});
}
function _deleteSubscription (deviceId, callback) {
if (!deviceId) {
return;
}
Q.req('Users/device', function (err, response) {
Q.handle(callback, null, [err, response]);
}, {
method: 'delete',
fields: {
deviceId: deviceId
}
});
}
function _pushNotificationInit () {
var push = PushNotification.init({
android: {},
browser: {
pushServiceURL: 'http://push.api.phonegap.com/v1/push'
},
ios: {
alert: true,
badge: true,
sound: true
},
windows: {}
});
push.on('registration', function (data) {
if (!Q.Users.loggedInUser) {
return;
}
Q.Users.Device.subscribed(function (err, subscribed) {
if (subscribed) {
return;
}
_registerDevice(data.registrationId, function (err, res) {
if (!err && !res.error) {
_setToStorage('deviceId', data.registrationId);
}
});
});
});
push.on('notification', function (data) {
Q.extend(data, data.additionalData);
Users.Device.onNotification.handle(data);
});
push.on('error', function (e) {
console.warn("Users.Device: ERROR", e);
});
Users.logout.options.onSuccess.set(function () {
PushNotification.setApplicationBadgeNumber(0);
}, 'Users.PushNotifications');
}
function _getFromStorage (type) {
return localStorage.getItem("Q\tUsers.Device." + type);
}
function _setToStorage (type, value) {
localStorage.setItem("Q\tUsers.Device." + type, value);
}
function _removeFromStorage (type) {
localStorage.removeItem("Q\tUsers.Device." + type);
}
// remove device info from localStorage when user logout
Users.onLogout.set(function () {
Users.Device.unsubscribe(function () {
console.log('Device is unsubscribed');
});
}, "Users.Device");
})(Q, jQuery);