Source: notifications.js

/**
 * @module  notifications
 * @author  Flavio De Stefano <flavio.destefano@caffeinalab.com>
 */

/*
Include methods used in this module dynamically to avoid that Titanium 
static analysis doesn't include native-language methods.
 */
Ti.Network;
Ti.Network.registerForPushNotifications;

/**
 * @property config
 * @property {String} 	[config.driver="http"] 					The driver to use.
 * @property {Boolean} 	[config.autoReset=true] 				If true, set the badge count to 0 when you open/resume the application.
 * @property {String}	[config.androidModule="ti.goosh"]	The name of the Android module to load.
 * @property {Boolean}  [config.fakeRemoteDeviceUUID=false] Fake a device token
 * @type {Object}
 */
exports.config = _.extend({
	driver: 'http',
	autoReset: true,
	androidModule: 'ti.goosh',
	fakeRemoteDeviceUUID: false
}, Alloy.CFG.T ? Alloy.CFG.T.notifications : {});

var MODULE_NAME = 'notifications';

var Event = require('T/event');
var Util = require('T/util');
var Router = require('T/router');
var Q = require('T/ext/q');

// Because Ti.Goosh has the same syntax of Ti.Network,
// we can just assign the module and use it in the same way

var registered_for_push_notifications = false;

/**
 * The module used for push notifications
 * @type {Object}
 */
exports.PushModule = null;

/**
 * The options for the registerForPushNotifications method of the push module
 * @type {Object}
 */
exports.PushModuleOpt = {};

if (OS_IOS) {
	exports.PushModule = Ti.Network;
	exports.PushModuleOpt.types = [ Ti.Network.NOTIFICATION_TYPE_BADGE, Ti.Network.NOTIFICATION_TYPE_ALERT, Ti.Network.NOTIFICATION_TYPE_SOUND ];
} else if (OS_ANDROID) {
	exports.PushModule = require(exports.config.androidModule);
} else {
	throw new Error(MODULE_NAME + ': unsupported platform');
}

// The listeners for all received notification
exports.PushModuleOpt.callback = function(e) {

	// We must be sure that the processed payload is the same on every platform,
	// for this reason we have to parse the JSON as described
	// in Ti.Goosh documentation: https://github.com/caffeinalab/ti.goosh
	if (OS_ANDROID) {
		if (_.isString(e.data)) {
			try {
				e.data = JSON.parse(e.data);
			} catch (ex) {
				e.data = {};
			}
		}
	}

	// Auto-reset the badge when a notification is received
	// We are sure the the app is in foreground, otherwise this function is never called on background/killed state.
	if (exports.config.autoReset === true) {
		exports.resetBadge();
	}

	// Call the local module onReceived method
	if (_.isFunction(exports.onReceived)) {
		exports.onReceived(e);
	}

	// And trigger a global event with the payload
	exports.trigger('received', e);
};

var interactiveCategories = [];
var interactiveCategoriesCallbacks = {};

var INTERACTIVE_NOTIFICATIONS_CAPABLE = (OS_IOS && _.isFunction(Ti.App.iOS.createUserNotificationAction));

/**
 * Validate a device token using empiric methods.
 * It only checks the the argument is a string and its length is enough for a valid token.
 * @param  {String} 	token		The device token
 * @return {Boolean}
 */
exports.validateToken = function(token) {
	return token != null && token != "undefined" && token != "null" && token.length >= 32;
};

/**
 * @property onReceived
 * A callback called when a notification is received.
 * You can override this to handle incoming notifications, or just
 * listen to the global event "notifications.received"
 */
exports.onReceived = function(e) {
	Ti.API.info(MODULE_NAME + ': Received', e);
};

/**
 * Load a driver of current module
 */
exports.loadDriver = function(name) {
	return Alloy.Globals.Trimethyl.loadDriver(MODULE_NAME, name, {
		subscribe: function(opt) {},
		unsubscribe: function(opt) {}
	});
};

/**
 * Attach events to current module
 * @param  {String}   	name 		Event key
 * @param  {Function} 	cb 		Callback
 */
exports.on = exports.event = function(name, cb) {
	Event.on(MODULE_NAME + '.' + name, cb);
};

/**
 * Remove events to current module
 * @param  {String}   	name 		Event key
 * @param  {Function} 	cb 		Callback
 */
exports.off = function(name, cb) {
	Event.off(MODULE_NAME + '.' + name, cb);
};

/**
 * Trigger events from current module
 * @param  {String}   	name 		Event key
 * @param  {Function} 	cb 		The data
 */
exports.trigger = function(name, data) {
	Event.trigger(MODULE_NAME + '.' + name, data);
};

/**
 * Activate the notifications without a subscription to a service.
 * You can use this method to just ask to the user the permissions.
 * @param  {Object}		[opt]
 * @param  {Function}	[opt.success]	Callback to invoke on success
 * @return {Q.Promise}	[opt.error]		Callback to invoke on error
 */
exports.activate = function(opt) {
	opt = _.defaults(opt || {}, {
		success: Alloy.Globals.noop,
		error: Alloy.Globals.noop
	});

	return Q.promise(function(_resolve, _reject) {

		var resolve = function(e) {
			Ti.API.debug(MODULE_NAME + ': activation success', e);
			exports.trigger('activation.success', e);
			opt.success(e);
			_resolve(e);
		};

		var reject = function(e) {
			Ti.API.error(MODULE_NAME + ': activation error', e);
			exports.trigger('activation.error', e);
			opt.error(e);
			_reject(e);
		};

		if (exports.config.fakeRemoteDeviceUUID) {
			Ti.API.warn(MODULE_NAME + ': faking activation');
			resolve( exports.config.fakeRemoteDeviceUUID );
			return;
		}

		var registerForPushNotifications = function() {
			if (registered_for_push_notifications) {
				resolve( exports.PushModule.remoteDeviceUUID );
			} else {
				exports.PushModule.registerForPushNotifications(_.extend(exports.PushModuleOpt, {
					success: function(e) { 
						registered_for_push_notifications = true;
						resolve(e.deviceToken); 
					},
					error: reject
				}));
			}
		};

		if (INTERACTIVE_NOTIFICATIONS_CAPABLE) {

			var userNotificationsCallback = function(settings) {
				// Instantly remove this listener
				Ti.App.iOS.removeEventListener('usernotificationsettings', userNotificationsCallback);

				if (_.isEmpty(settings.types)) {
					// The user has not accepted the notifications permissions
					return reject({ disabled: true });
				}

				registerForPushNotifications();
			};

			Ti.App.iOS.addEventListener('usernotificationsettings', userNotificationsCallback);
			Ti.App.iOS.registerUserNotificationSettings({
				types: [
				Ti.App.iOS.USER_NOTIFICATION_TYPE_BADGE,
				Ti.App.iOS.USER_NOTIFICATION_TYPE_ALERT,
				Ti.App.iOS.USER_NOTIFICATION_TYPE_SOUND
				],
				categories: interactiveCategories
			});

		} else {
			registerForPushNotifications();
		}

	});
};

/**
 * Deactivate completely the notifications
 * Per Apple's documentation, it is rarely necessary to call this method.
 * For example, calling this method would be required if a new version of your application no longer supports push notifications.
 */
exports.deactivate = function() {
	exports.PushModule.unregisterForPushNotifications();
};

/**
 * The main method: It activate the notifications (asking to the user the permissions), and sends the device token to your driver.
 * @param {String} 	[channel] 		Channel name
 * @param {Object} 	[data] 			Additional data to pass to the driver
 * @param {Object} 	[opt] 			Additional options to pass to the driver
 * @return {Q.Promise}
 */
exports.subscribe = function(channel, data, opt) {
	opt = _.defaults(opt || {}, {
		success: Alloy.Globals.noop,
		error: Alloy.Globals.noop
	});

	return Q.promise(function(_resolve, _reject) {

		var resolve = function(e) {
			Ti.API.debug(MODULE_NAME + ': subscription success', e);
			exports.trigger('subscription.success', e);
			opt.success(e);
			_resolve(e);
		};

		var reject = function(e) {
			Ti.API.error(MODULE_NAME + ': subscription error', e);
			exports.trigger('subscription.error', e);
			opt.error(e);
			_reject(e);
		};

		if (false == Ti.Network.online) {
			return reject({
				offline: true
			});
		}

		exports.activate()
		.then(function(deviceToken) {

			var driver = exports.loadDriver(exports.config.driver);

			driver.subscribe(_.extend({}, opt, {
				deviceToken: deviceToken,
				channel: channel,
				data: data,
				success: resolve,
				error: reject
			}));

		})
		.fail(reject);

	});
};

/**
 * Unsubscribe from a channel using the specified driver.
 * @param {String} 	[channel] 		Channel name
 * @param {Object} 	[data] 			Additional data to pass to the driver
 * @param {Object} 	[opt] 			Additional options to pass to the driver
 * @return {Q.Promise}
 */
exports.unsubscribe = function(channel, data, opt) {
	opt = _.defaults(opt || {}, {
		success: Alloy.Globals.noop,
		error: Alloy.Globals.noop
	});

	return Q.promise(function(_resolve, _reject) {

		var resolve = function(e) {
			Ti.API.debug(MODULE_NAME + ': unsubscription success', e);
			exports.trigger('unsubscription.success', e);
			opt.success(e);
			_resolve(e);
		};

		var reject = function(e) {
			Ti.API.error(MODULE_NAME + ': unsubscription error', e);
			exports.trigger('unsubscription.error', e);
			opt.error(e);
			_reject(e);
		};

		if (false == Ti.Network.online) {
			return reject({
				offline: true
			});
		}

		var driver = exports.loadDriver(exports.config.driver);
		var device_token = exports.getRemoteDeviceUUID();

		driver.unsubscribe(_.extend({}, opt, {
			deviceToken: device_token,
			channel: channel,
			data: data,
			success: resolve,
			error: reject
		}));

	});
};

/**
 * Set the application badge number
 * @param {Number} 	value
 */
exports.setBadge = function(value) {
	var proxy = OS_IOS ? Ti.UI.iOS : exports.PushModule;
	proxy.setAppBadge(Math.max(value, 0));
};

/**
 * Get the application badge number
 * @return {Number}
 */
exports.getBadge = function() {
	var proxy = OS_IOS ? Ti.UI.iOS : exports.PushModule;
	return proxy.getAppBadge();
};

/**
 * Set the application badge to zero
 */
exports.resetBadge = function() {
	exports.setBadge(0);
};

/**
 * Increment the badge app
 * @param {Number} value 	The value to increment
 */
exports.incBadge = function(value) {
	exports.setBadge(exports.getBadge() + value);
};

/**
 * Get the stored device token.
 * Don't rely on this method to check if notifications are active, use {@link #isRemoteNotificationsEnabled} instead
 * @return {String}
 */
exports.getRemoteDeviceUUID = function() {
	if (exports.config.fakeRemoteDeviceUUID) {
		Ti.API.warn(MODULE_NAME + ': getRemoteDeviceUUID is returning a fake device token');
		return exports.config.fakeRemoteDeviceUUID;
	}

	return exports.PushModule.remoteDeviceUUID;
};

/**
 * Check if the remote notifications has been registered once.
 * Use this method at startup in conjunction with `activate()`
 * @return {Boolean} [description]
 */
exports.isRemoteNotificationsEnabled = function() {
	return exports.PushModule.remoteNotificationsEnabled;
};


///////////////////////////////
// Interactive notifications //
///////////////////////////////

// Create a UserNotificationAction Object based on a dictionary.
function createIntNotifAction(opt) {
	if (opt.id == null) throw new Error(MODULE_NAME + ': interactive notifications must have and ID');
	if (opt.title == null) throw new Error(MODULE_NAME + ': interactive notifications must have a title');

	return Ti.App.iOS.createUserNotificationAction({
		identifier: opt.id,
		title: opt.title,
		activationMode: Ti.App.iOS["USER_NOTIFICATION_ACTIVATION_MODE_" + (opt.openApplication == true ? "FOREGROUND" : "BACKGROUND")],
		destructive: !!opt.destructive,
		authenticationRequired: !!opt.authenticationRequired
	});
}

/**
 * @param {String}   id       	The ID of the category. It must be unique.
 * @param {Array}    dict     	An array of actions. Each action must have defined:
 * `id` (required), the ID of this action
 * `title` (required) the title to show
 * `callback` (required) the callback that handle the user click
 * `openApplication`, `destructive`, `authenticationRequired` are optionals.
 * @param {Function} callback 	The callback to invoke
 */
exports.addInteractiveNotificationCategory = function(id, dict) {
	if (false == INTERACTIVE_NOTIFICATIONS_CAPABLE) {
		Ti.API.error(MODULE_NAME + ': unable to create an interactive notification category, not supported');
		return;
	}

	var category = Ti.App.iOS.createUserNotificationCategory({
		identifier: id,
		actionsForDefaultContext: dict.map(createIntNotifAction)
	});

	// Add in the interactiveCategories array to register in the activate method
	interactiveCategories.push(category);
	interactiveCategoriesCallbacks[ id ] = dict.callback;
};

/**
 * Unmute all notifications
 * @param {Object} data 		Additional data
 */
exports.unmute = function(data) {
	return Q.promise(function(resolve, reject) {

		if (!Ti.Network.online) {
			return reject({
				offline: true
			});
		}

		exports.loadDriver(exports.config.driver).unmute({
			deviceToken: exports.getRemoteDeviceUUID(),
			data: data,
			success: function(response) {
				Event.trigger('notifications.unmute.success');
				defer.resolve(response);
			},
			error: function(err) {
				Event.trigger('notifications.unmute.error');
				defer.reject(err);
			}
		});

	});
};


/**
 * Mute all notifications
 * @param {Object} data 		Additional data
 */
exports.mute = function(data) {
	return Q.promise(function(resolve, reject) {

		if (!Ti.Network.online) {
			return reject({
				offline: true
			});
		}

		exports.loadDriver(exports.config.driver).mute({
			deviceToken: exports.getRemoteDeviceUUID(),
			data: data,
			success: function(response) {
				Event.trigger('notifications.mute.success');
				defer.resolve(response);
			},
			error: function(err) {
				Event.trigger('notifications.mute.error');
				defer.reject(err);
			}
		});

	});
};


//////////
// Init //
//////////

if (INTERACTIVE_NOTIFICATIONS_CAPABLE) {
	Ti.App.iOS.addEventListener('remotenotificationaction', function(e) {
		var func = interactiveCategoriesCallbacks[ e.category ];
		if (_.isFunction(func)) {
			func(e);
		} else {
			Ti.API.error(MODULE_NAME + ': remote notification with an unregistered category (' + e.category + ')');
		}
	});
}

if (exports.config.autoReset === true) {
	exports.resetBadge();
	Ti.App.addEventListener('resumed', exports.resetBadge);
}