'use strict'; /** * Services that persists and retrieves user from localStorage. */ angular.module('userServices', []). factory('User', ['$rootScope', '$http', '$location', '$window', 'API_URL', 'STORAGE_USER_ID', 'STORAGE_SETTINGS_ID', function($rootScope, $http, $location, $window, API_URL, STORAGE_USER_ID, STORAGE_SETTINGS_ID) { var authenticated = false, defaultSettings = { auth: { apiId: '', apiToken: ''}, sync: { queue: [], //here OT will be queued up, this is NOT call-back queue! sent: [] //here will be OT which have been sent, but we have not got reply from server yet. }, fetching: false, // whether fetch() was called or no. this is to avoid race conditions online: false }, settings = {}, //habit mobile settings (like auth etc.) to be stored here user = {}; // this is stored as a reference accessible to all controllers, that way updates propagate //first we populate user with schema _.extend(user, $window.habitrpgShared.helpers.newUser()); user.apiToken = user._id = ''; // we use id / apitoken to determine if registered //than we try to load localStorage if (localStorage.getItem(STORAGE_USER_ID)) { _.extend(user, JSON.parse(localStorage.getItem(STORAGE_USER_ID))); } var syncQueue = function (cb) { if (!authenticated) { $window.alert("Not authenticated, can't sync, go to settings first."); return; } var queue = settings.sync.queue; var sent = settings.sync.sent; if (queue.length === 0) { // Sync: Queue is empty return; } if (settings.fetching) { // Sync: Already fetching return; } if (settings.online!==true) { // Sync: Not online return; } settings.fetching = true; // move all actions from queue array to sent array _.times(queue.length, function () { sent.push(queue.shift()); }); $http.post(API_URL + '/api/v1/user/batch-update', sent, {params: {data:+new Date, _v:user._v, siteVersion: $window.env && $window.env.siteVersion}}) .success(function (data, status, heacreatingders, config) { //make sure there are no pending actions to sync. If there are any it is not safe to apply model from server as we may overwrite user data. if (!queue.length) { //we can't do user=data as it will not update user references in all other angular controllers. // the user has been modified from another application, sync up if(data.wasModified) { delete data.wasModified; $rootScope.$emit('userUpdated', user); } // Update user _.extend(user, data); // Emit event when user is synced $rootScope.$emit('userSynced'); } sent.length = 0; settings.fetching = false; save(); if (cb) { cb(false) } syncQueue(); // call syncQueue to check if anyone pushed more actions to the queue while we were talking to server. }) .error(function (data, status, headers, config) { if(status === 400 && data.needRefresh === true){ alert("The site has been updated and the page needs to refresh. " + "The last action has not been recorded, please do it again once the page reloads." ); return location.reload(); } //move sent actions back to queue _.times(sent.length, function () { queue.push(sent.shift()) }); settings.fetching = false; //Notification.push({type:'text', text:"We're offline"}) }); } var save = function () { localStorage.setItem(STORAGE_USER_ID, JSON.stringify(user)); localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(settings)); }; var userServices = { user: user, online: function (status) { if (status===true) { settings.online = true; syncQueue(); } else { settings.online = false; }; }, authenticate: function (uuid, token, cb) { if (!!uuid && !!token) { $http.defaults.headers.common['x-api-user'] = uuid; $http.defaults.headers.common['x-api-key'] = token; authenticated = true; settings.auth.apiId = uuid; settings.auth.apiToken = token; settings.online = true; if (user && user._v) user._v--; // shortcut to always fetch new updates on page reload userServices.log({}, function(){ // If they don't have timezone, set it var offset = moment().zone(); // eg, 240 - this will be converted on server as -(offset/60) if (user.preferences.timezoneOffset !== offset) userServices.set('preferences.timezoneOffset', offset); cb && cb(); }); } else { alert('Please enter your ID and Token in settings.') } }, authenticated: function(){ return this.settings.auth.apiId !== ""; }, log: function (action, cb) { //push by one buy one if an array passed in. if (_.isArray(action)) { action.forEach(function (a) { settings.sync.queue.push(a); }); } else { settings.sync.queue.push(action); } save(); syncQueue(cb); }, sync: function(){ user._v--; userServices.log({}); }, /* Very simple path-set. `set('preferences.gender','m')` for example. We'll deprecate this once we have a complete API */ set: function(k, v) { var log = { op: 'set', data: {} }; $window.habitrpgShared.helpers.dotSet(k, v, this.user); log.data[k] = v; userServices.log(log); }, setMultiple: function(obj){ var log = { op: 'set', data: {} }; _.each(obj, function(v,k){ $window.habitrpgShared.helpers.dotSet(k, v, userServices.user); log.data[k] = v; }); userServices.log(log); }, revive: function(){ $window.habitrpgShared.algos.revive(user); userServices.log({ op: "revive" }); }, /** * For gem-unlockable preferences, (a) if owned, select preference (b) else, purchase * @param path: User.preferences <-> User.purchased maps like User.preferences.skin=abc <-> User.purchased.skin.abc. * Pass in this paramater as "skin.abc". Alternatively, pass as an array ["skin.abc", "skin.xyz"] to unlock sets */ unlock: function(path){ var self = userServices; //this; // why isn't this working? if (_.isArray(path)) { if (confirm("Purchase for 5 Gems?") !== true) return; if (user.balance < 1.25) return $rootScope.modals.buyGems = true; path = path.join(','); } else { if ($window.habitrpgShared.helpers.dotGet('purchased.' + path, user)) { var pref = path.split('.')[0], val = path.split('.')[1]; return self.set('preferences.' + pref, val); } else { if (confirm("Purchase for 2 Gems?") !== true) return; if (user.balance < 0.5) return $rootScope.modals.buyGems = true; } } $http.post(API_URL + '/api/v1/user/unlock?path=' + path) .success(function(data, status, headers, config){ self.log({}); // sync new unlocked & preferences }).error(function(data, status, headers, config){ alert(status + ': ' + data); //FIXME use method used elsewhere for handling this error, this is temp while developing }) }, save: save, settings: settings }; //load settings if we have them if (localStorage.getItem(STORAGE_SETTINGS_ID)) { //use extend here to make sure we keep object reference in other angular controllers _.extend(settings, JSON.parse(localStorage.getItem(STORAGE_SETTINGS_ID))); //if settings were saved while fetch was in process reset the flag. settings.fetching = false; //create and load if not } else { localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(defaultSettings)); _.extend(settings, defaultSettings); } //If user does not have ApiID that forward him to settings. if (!settings.auth.apiId || !settings.auth.apiToken) { //var search = $location.search(); // FIXME this should be working, but it's returning an empty object when at a root url /?_id=... var search = $location.search($window.location.search.substring(1)).$$search; // so we use this fugly hack instead if (search.err) return alert(search.err); if (search._id && search.apiToken) { userServices.authenticate(search._id, search.apiToken, function(){ $window.location.href='/'; }); } else { if ($window.location.pathname.indexOf('/static') !== 0){ localStorage.clear(); $window.location.href = '/logout'; } } } else { userServices.authenticate(settings.auth.apiId, settings.auth.apiToken) } return userServices; } ]);