Source: fb/Connect.js

/**
 * Connect
 * 
 * @namespace fb
 * @author Georgi Popov
 * @version 1.0.4
 * @license http://www.gnu.org/licenses/gpl-3.0.en.html GPLv3
 * @requires com/magadanski/core/core.js
 * @requires com/magadanski/core/utils.js
 * @requires com/magadanski/core/EventDispatcher.js
 */

define('com.magadanski.fb.Connect', function () {
	var that;
	var utils = inc('com.magadanski.utils', true);
	
	// private properties
	/**
	 * @access private
	 * @inner
	 * @member {Boolean} domReady
	 * @memberOf Connect
	 * @default
	 */
	var domReady = false;
	
	/**
	 * @access private
	 * @inner
	 * @member {Boolean} fbReady
	 * @memberOf Connect
	 * @default
	 */
	var fbReady = false;
	
	/**
	 * @access private
	 * @inner
	 * @member {Boolean} initialized
	 * @memberOf Connect
	 * @default
	 */
	var initialized = false;
	
	// private methods
	/**
	 * @access private
	 * @inner
	 * @memberOf Connect
	 * @param  {Object} e
	 * @return {void}
	 */
	function init(e) {
		if (that.getDomReady() && that.getFbReady() && !initialized) {
			FB.init({
				appId: that.options.appId,
				xfbml: that.options.xfbml,
				version: that.options.version
			});
			
			FB.Event.subscribe('auth.statusChange', function(response) {
				switch (response.status) {
					case 'not_authorized':
						handleNotAuthorized(response);
						break;
					case 'connected':
						handleLogin(response);
						break;
					case 'unknown':
						handleLogout(response);
						break;
				}
			});
			
			FB.getLoginStatus();
			
			var loginButton = document.querySelector(that.options.loginButton);
			
			if (loginButton) {
				loginButton.addEventListener('click', function (e) {
					e.preventDefault();
					
					that.login();
				});
			}
			
			var logoutButton = document.querySelector(that.options.logoutButton);
			
			if (logoutButton) {
				logoutButton.addEventListener('click', function (e) {
					e.preventDefault();
					
					that.logout();
				});
			}
			
			initialized = true;
		}
	}
	
	/**
	 * @access private
	 * @inner
	 * @memberOf Connect
	 * @param  {Object} response
	 * @return {void}
	 */
	function handleNotAuthorized(response) {
		document.body.classList.add(that.options.guestBodyClass);
		document.body.classList.remove(that.options.userBodyClass);
		
		if (typeof(that.options.onNotAuthorized) === 'function') {
			that.options.onNotAuthorized(response);
		}
		
		/**
		 * Event dispatched when the user does not authorize the application.
		 * 
		 * When the "login" button is clicked or the `login` method is called
		 * directly a popup is shown to the user asking for authorization of
		 * the application.
		 * 
		 * If the user declines the application's authorization the `not_authorized`
		 * event will be dispatched.
		 * 
		 * @event Connect#not_authorized
		 * @type {Object}
		 * @property {Obect} response FB API's response
		 */
		that.dispatchEvent('not_authorized', { response: response });
	}
	
	/**
	 * @access private
	 * @inner
	 * @memberOf Connect
	 * @param  {Object} response
	 * @return {void}
	 */
	function handleLogin(response) {
		document.body.classList.add(that.options.userBodyClass);
		document.body.classList.remove(that.options.guestBodyClass);
		
		if (typeof(that.options.onConnected) === 'function') {
			that.options.onConnected(response);
		}
		
		/**
		 * Event dispatched when the user's account is connected to the application.
		 * 
		 * This is called upon successful connection of the user's profile either
		 * in response to them clicking on the "login" button or as some other direct
		 * call of the `login` method.
		 * 
		 * @event Connect#login
		 * @type {Object}
		 * @property {Object} response FB API's response
		 */
		that.dispatchEvent('login', { response: response });
	}
	
	/**
	 * @access private
	 * @inner
	 * @memberOf Connect
	 * @param  {Object} response
	 * @return {void}
	 */
	function handleLogout(response) {
		document.body.classList.add(that.options.guestBodyClass);
		document.body.classList.remove(that.options.userBodyClass);
		
		if (typeof(that.options.onLogout) === 'function') {
			that.options.onLogout(response);
		}
		
		/**
		 * Event dispatched when the user's account is disconnected from the application.
		 * 
		 * This is called upon successful disconnection of the user's profile either in
		 * response to them clicking on the "logout" button or as some other direct call
		 * of the `logout` method.
		 * 
		 * This does not mean the user has deauthorized the application, rather that
		 * they are simply logged-out of Facebook.
		 * 
		 * @event Connect#logout
		 * @type {Object}
		 * @property {Object} response FB API's response
		 */
		that.dispatchEvent('logout', { response: response });
	}
	
	/**
	 * Facebook login helper
	 * 
	 * When instantiated the class automatically asynchronously loads the FB SDK.
	 * 
	 * After the FB SDK is loaded and the DOMContentLoaded event fires the
	 * `init` method of the class. This method initializes the FB SDK, checks
	 * the current user's status (`connected`, `not_authorized` or `uknown`).
	 * 
	 * Based on this status a corresponding event of `login`, `not_authorized`
	 * or `logout` is dispatched.
	 * 
	 * If the status of the user changes then associated events are dispatched as well.
	 * 
	 * Based on options set for "login" and "logout" buttons necessary functionality
	 * is assigned to those.
	 * 
	 * @class Connect
	 * @since 1.0.0
	 * @extends {EventDispatcher}
	 * @param {Object} options See {@link Connect#defaultInitOptions|defaultInitOptions}
	 * @fires {@link Connect#login|login}
	 * @fires {@link Connect#logout|logout}
	 * @fires {@link Connect#not_authorized|not_authorized}
	 */
	var Connect = function (options) {
		that = this;
		
		// priviledged properties
		/**
		 * Initialization options
		 * 
		 * @access public
		 * @instance
		 * @member {Object} options
		 * @memberOf Connect
		 * @see {@link Connect#defaultInitOptions|defaultInitOptions}
		 */
		that.options = utils.extendOptions(that.defaultInitOptions, options);
		
		// priviledged methods
		/**
		 * Getter for the domReady private property
		 * 
		 * @access public
		 * @instance
		 * @memberOf Connect
		 * @method getDomReady
		 * @return {boolean} Whether the DOM is ready or not
		 */
		that.getDomReady = function () {
			return domReady = (document.readyState == "complete" || document.readyState == "loaded" || document.readyState == "interactive");
		};
		
		/**
		 * Setter, which calls the getter as the value is auto-set.
		 * 
		 * @access public
		 * @instance
		 * @memberOf Connect
		 * @method setDomReady
		 */
		that.setDomReady = function () {
			that.getDomReady();
		};
		
		/**
		 * Getter for the fbReady private property
		 * 
		 * @access public
		 * @instance
		 * @memberOf Connect
		 * @method getFbReady
		 * @return {boolean} Whether the FB JS SDK has been loaded and initialized
		 */
		that.getFbReady = function () {
			return fbReady;
		};
		
		/**
		 * Setter for the fbReady private property
		 * 
		 * @access public
		 * @instance
		 * @memberOf Connect
		 * @method  setFbReady
		 * @param {boolean} ready The new state for the property
		 */
		that.setFbReady = function (ready) {
			var oldValue = fbReady;
			
			fbReady = !!ready;
			
			if (oldValue !== fbReady) {
				that.dispatchEvent('fbReadyChanged');
			}
		};
		
		/**
		 * Getter for the initialized private property
		 * 
		 * @access public
		 * @instance
		 * @memberOf Connect
		 * @method getInitialized
		 * @return {boolean} Whether the FB Login has been initialized
		 */
		that.getInitialized = function () {
			return initialized;
		};
		
		/**
		 * Helper method for accessing FB.api
		 * 
		 * @access public
		 * @instance
		 * @memberOf Connect
		 * @method api
		 * @param {string}   method                 The api method you'd like to call. Use RESTful path.
		 * @param {Function} callback               The callback function to handle the result of the API call.
		 * @param {string}   doubleCheckPermissions (optional) Pass a string to double-check whether the necessary permissions for the API call are available. It is encouraged to always double-check special permissions when making API calls. If the permission is midding the method will automatically handle it and trigger a dialog screen to ask the user to grant the permission. This is essential as users may originally authenticate the application but remove specific permissions later on.
		 * @return {void}
		 */
		that.api = function (method, callback, doubleCheckPermissions) {
			FB.api(method, function (response) {
				var hasPermissions = false;
				
				if (typeof(doubleCheckPermissions) !== 'undefined') {
					for (var i=0; i < response.data.length; ++i) {
						if (response.data[i].permission === doubleCheckPermissions) {
							if (response.data[i].status === 'granted') {
								hasPermissions = true;
							}
						}
					}
				} else {
					hasPermissions = true;
				}
				
				if (hasPermissions) {
					callback(response);
				} else {
					FB.login(function (response) {
						handleLogin();
					}, { scope: doubleCheckPermissions, auth_type: 'rerequest' });
				}
			});
		};
		
		// constructor
		window.fbAsyncInit = function () {
			that.setFbReady(true);
		};
		
		// load FB JS SDK
		// make sure the SDK is not loaded prior to settin the window.fbAsyncInit event handler
		(function(d, s, id){
			var js, fjs = d.getElementsByTagName(s)[0];
			if (d.getElementById(id)) {return;}
			js = d.createElement(s); js.id = id;
			js.src = "//connect.facebook.net/" + that.options.locale + "/sdk.js";
			fjs.parentNode.insertBefore(js, fjs);
		}(document, 'script', 'facebook-jssdk'));

		document.addEventListener('DOMContentLoaded', function (e) {
			that.setDomReady();
		});
		
		that.addEventListener('domReadyChanged fbReadyChanged', init);
	}
	Connect.inherits(com.magadanski.core.EventDispatcher);
	com.magadanski.fb.Connect = Connect;
	
	// public properties
	/**
	 * The default initialization options.
	 * 
	 * Those options include:
	 * 
	 *  * `appId` _(string)_ -- the only required option
	 *  	
	 *  	If you do not pass that none of the functionality will get executed.
	 *  	
	 *  * `xfbml` _(boolean)_ -- default value us `true`
	 *  	
	 *  	Simply forwarded to FBs SDK, denoting whether XFBML tags on the page
	 *  	should be parsed.
	 *  	
	 *  * `version` _(string)_ -- default value is `'v2.7'`
	 *  	
	 *  	Another option passed to FBs SDK. This denotes that your code would
	 *  	expect functionality for that version of the API.
	 *  	
	 *  * `scope` _(string)_ -- default value is 'public_profile,email'
	 *  	
	 *  	You should pass a string with comma delimited permissions that your
	 *  	application needs as the `scope` option.
	 *  	
	 *  	Note that requesting permissions is not equal to receiving them.
	 *  	Facebook allows users to connect to your application providing
	 *  	access to just some of the information you've asked for. So if
	 *  	you're calling an API method that relies on special permissions
	 *  	it is a good idea to always double-check them.
	 *  	
	 *  	The `api` method of the class has a built-in way to double-check
	 *  	permissions, so just pass a string of those when calling it.
	 *  	
	 *  	Also, requesting too much permissions from the start may cause
	 *  	users reject your application (in case they are not acquainted
	 *  	with the option to simply disable them -- few users are).
	 *  	
	 *  	It is advised that the original login only requires some basic
	 *  	public profile information for the users and when you get to
	 *  	a meaningful situation where you need the permissions for some
	 *  	API method -- you can double check for the permissions even if
	 *  	those were not originally present in the `scope` option. This
	 *  	will just ask the users for additional permissions.
	 *  	
	 *  * `loginButton` _(string)_ -- default value is `'.login'`
	 *  	
	 *  	Pass a CSS selector for the `loginButton` option to hook proper login
	 *  	event handlers to the button.
	 *  	
	 *  * `logoutButton` _(string)_ -- default value is `'.logout'`
	 *  	
	 *  	Similar to the `loginButton` one.
	 *  	
	 *  * `locale` _(string)_ -- default value is `'en_US'`
	 *  	
	 *  	This is used when building the URL for the FB SDK to load.
	 *  	
	 *  	The SDK is available as multiple languages at different
	 *  	URLs, so you need to load the proper one in order for
	 *  	your application to be localized.
	 *  	
	 *  	You can pass a locale here as '`{language_code}_{country_code}`'
	 *  	where `{language_code}` is a two-lowercase representation
	 *  	of the language (for example 'en' or 'pt') and
	 *  	`{country_code}` is a two-lowercase representation of the
	 *  	country (for example 'US' or 'GB' in case for English
	 *  	and 'BR' or 'PT' for Portuguese).
	 *  	
	 *  * `userBodyClass` _(string)_ -- default value is `'user'`
	 *  	
	 *  	This class is assigned to the body tag after successful
	 *  	connection to the user's account is established.
	 *  
	 *  * `guestBodyClass` _(string)_ -- default value is `'guest'`
	 *  	
	 *  	This class is assigned to the body tag if a user logs
	 *  	out of FB when on the site or if they do not authorize
	 *  	the application.
	 *  
	 *  * `onConnected()` _(callback)_
	 *  
	 *  * `onLogout()` _(callback)_
	 *  
	 *  * `onNotAuthorized()` _(callback)_
	 * 
	 * There are three options that should be used as callback functions: `onConnected()`,
	 * `onLogout()` and `onNotAthorized()`. Those are called respectively when the user
	 * logs-in, when he logs-out and when the page is loaded by a user who has loged-into
	 * Facebook but has not yet authenticated your application.
	 * 
	 * Note that the `onNotAuthorized()` callback is executed on initialization of the
	 * Connect class (pretty-much on page load), so it may be annoying to the user to
	 * prompt them to log in right away.
	 * 
	 * @access public
	 * @instance
	 * @memberOf Connect
	 * @type {Object}
	 */
	Connect.prototype.defaultInitOptions = {
		appId: '',
		xfbml: true,
		version: 'v2.7',
		scope: 'public_profile,email',
		loginButton: '.login',
		logoutButton: '.logout',
		locale: 'en_US',
		userBodyClass: 'user',
		guestBodyClass: 'guest',
		onConnected: function (response) {},
		onLogout: function (response) {},
		onNotAuthorized: function (response) {}
	};
	
	// public methods
	
	/**
	 * You can manually call the `login` method.
	 * 
	 * If the user has not yet authorized the application they will see a popup
	 * asking them for permissions, according to the scope from the init options.
	 * 
	 * If the user is not logged-into Facebook the popup will first ask them to
	 * do so and only then they will see the permissions requirements.
	 * 
	 * If the user has previously logged-in this will just take a second and will
	 * fire the `login` event once connection is reestablished.
	 * 
	 * @access public
	 * @instance
	 * @memberOf Connect
	 * @return {void}
	 */
	Connect.prototype.login = function () {
		FB.login(function (response) {
			if (response.status === 'connected') {
				handleLogin(response);
			} else {
				handleNotAuthorized(response);
			}
		}, { scope: that.options.scope });
	};
	
	/**
	 * You can manualy call the `logout` method.
	 * 
	 * This will simply log a user out of Facebook. It will not de-authorize
	 * your application, restricting it from future access.
	 * 
	 * @access public
	 * @instance
	 * @memberOf Connect
	 * @return {void}
	 */
	Connect.prototype.logout = function () {
		FB.logout(function (response) {
			handleLogout(response);
		});
	};
});