Deferred promise not deferring - angularjs

Hi in the following Angular controller i try to initiate facebook login with Parse.com.
So I created a promise triggered on fbLogIn. What it is supposed to do, is first login to facebook, and grab first_name and store it in fieldValuesService.ff.
THEN, it is supposed to access this value and do something with it. For illustration purpose I just used console logs.
What happens is that the second console.log in second then is triggered before the first one from first .then thus is undefined.
I don't understand why anything in the second .then can be triggered before first one in this situation.
Also second problem, after a logout, the fbLogIn function is sometime inactive: it won't trigger the login process again.
If you have a clue on this issue your help will be greatly appreciated.
.controller('logController',
function ($scope, $q, fieldValuesService) {
var defer = $q.defer();
defer.promise
.then(function() {
Parse.FacebookUtils.logIn(null, {
success: function(user) {
if (!user.existed()) {
alert("User signed up and logged in through Facebook!");
} else {
$scope.currentUser = user;
$scope.$apply();
FB.api('/me', function(response) {
fieldValuesService.ff = response.first_name;
console.log(fieldValuesService.ff); //logs bob
});
}
},
error: function(user, error) {
alert("User cancelled the Facebook login or did not fully authorize.");
}
});
})
.then(function(){
console.log(fieldValuesService.ff); //logs undefined
});
$scope.fbLogIn = function() {
defer.resolve();
};
// Parse log out
$scope.logOut = function(form) {
Parse.User.logOut();
$scope.currentUser = null;
};
});

Maybe if you restructure your code, things will become a little bit easier.
I recommend to refactor everything FB related into its own service like:
module.factory('FBService', function ($q) {
var login,
logout,
getInformation;
login = function () {
var defered = $q.defer();
Parse.FacebookUtils.logIn(null, {
success: function (user) {
defered.resolve(user);
},
error: function (user, error) {
defered.reject(user, error);
}
});
return defered.promise;
};
logout = function () {
var defered = $q.defer();
Parse.User.logOut();
defered.resolve();
return defered.promise;
};
getInformation = function () {
var defered = $q.defer();
FB.api('/me', function (response) {
defered.resolve(response);
});
return defered.promise;
}
return {
login: login,
logout: logout,
getInformation: getInformation
};
});
module.controller("LoginCtrl", function ($scope, FBService, fieldValuesService) {
$scope.fbLogIn = function () {
FBService.login().then(function (user) {
$scope.currentUser = user;
return FBService.getInformation();
}).then(function (information) {
fieldValuesService.ff = information.first_name;
console.log(fieldValuesService.ff);
});
};
$scope.logOut = function () {
FBService.logout().then(function () {
$scope.currentUser = null;
});
};
});

Related

How should you getCurrentUser after a page reload?

Here is how I'm doing it so far:
angular
.module('mean-starter')
.factory('Auth', function($http, $state, $window, $cookies) {
var currentUser = {};
return {
signup: function(user) {
return $http
.post('/users', user)
.then(function(data, status, headers, config) {
angular.copy(data, currentUser);
$cookies.put('userId', data._id);
$window.location.href = '/';
})
;
},
login: function(user) {
return $http
.post('/login', user)
.then(function(data) {
angular.copy(data, currentUser);
$cookies.put('userId', data._id);
$window.location.href = '/';
})
;
},
logout: function() {
$http
.get('/logout')
.then(function() {
angular.copy({}, currentUser);
$cookies.remove('userId');
$window.location.href = '/';
})
.catch(function() {
console.log('Problem logging out.');
})
;
},
getCurrentUser: function() {
// user is logged in
if (currentUser._id) {
return currentUser;
}
// user is logged in, but page has been refreshed and currentUser is lost
if ($cookies.get('userId')) {
return $http.get('/current-user')
.then(function(data) {
angular.copy(data, currentUser);
})
;
}
// user isn't logged in
else {
return currentUser;
}
},
isLoggedIn: function() {
return !!currentUser._id;
}
};
})
;
After a page reload, the Auth factory gets re-run and currentUser is reassigned to {}. So if the user was logged in, currentUser won't reflect it. So I have to check for the case where !currentUser._id && $cookies.get('userId') and if so, query the database for the currently logged in user.
Now I want to access currentUser:
angular
.module('mean-starter')
.run(run)
;
function run($rootScope, Auth, $state) {
$rootScope.$on('$stateChangeStart', function(event, toState, toParams) {
if (typeof toState.authenticate !== 'undefined') {
var currentUser = Auth.getCurrentUser();
var admin = currentUser.role === 'admin';
var authorized = currentUser._id.toString() === toParams.id;
if (!Auth.isLoggedIn()) {
event.preventDefault();
alert('Must be logged in to access this route.');
$state.go('login');
}
else if (toState.authenticate.authorized) {
if (!admin && !authorized) {
event.preventDefault();
alert('You are not authorized to access that route.');
}
}
else if (toState.authenticate.isAdmin) {
if (!admin) {
event.preventDefault();
alert('You must be an admin to access this route.');
}
}
}
});
}
The problem is that I don't know whether or not Auth.getCurrentUser() will return the user or a promise. How can I check for this? How should this be architected?
Why not just always return a promise in your getCurrentUser with the help of $q?
So something like this
getCurrentUser: function() {
if (currentUser._id || !$cookies.get('userId')) {
// $q.when will wrap your currentUser into a promise
return $q.when(currentUser);
}
return $http.get('/current-user')
.then(function(data) {
angular.copy(data, currentUser);
return currentUser;
});
}
}
and in your controller:
Auth.getCurrentUser().then(function(currentUser) {
// Your code
})
You can adapt your function to return a promise in both cases using $q. In this case, all three logical paths should result in the same outcome, albeit by different sets of operations in between, therefore a promise would work perfectly here. Especially because you can have very specific control over the error handling flow (if needed)
http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/

Angular Service: Data not being retrieved after storage

I'm learning angular, and I'm trying to use a service to store data from an HTTP request, and be able to access it later.
Problem:
Data object is empty every time I try to retrieve it, which causes it to make a new call. I'm using this in the context of a ui-router resolve(does this cause the service to re-instantiate and lose the data)?
Service:
evaApp.factory('userService', ['$http', '$q', function ($http, $q) {
var user = {};
return {
makeRequest : function(url, uid) {
var deferred = $q.defer();
if (!uid) { uid = ''; };
$http.get(url, { params : { userId : uid } }).then(function (res) {
deferred.resolve(res.data);
});
return deferred.promise;
},
getUser : function(userId) {
console.log(user); // user is always empty
if(!user || !user._id) {
user = this.makeRequest('/api/user/get', userId);
};
return user;
}
}
}]);
Addition:
Data storage is working using PSL's solution. Data retrieval is not: Link to question.
this.makeRequest returns a promise and it does not have a _.id property which is causing it to make the ajax call again (due the condition if(!user || !user._id) {). just return the promise itself from getUser and use it. Remember you are not assigning the user instead assigning a promise by doing user = this.makeRequest('/api/user/get', userId);
Instead just do:-
var user = {};
getUser : function(userId) {
return user[userId] || (user[userId] = this.makeRequest('/api/user/get', userId)
.catch(function(){ user = null })); //nullify in case of error for retry
}
and in make request just do:
makeRequest : function(url, uid) {
if (!uid) { uid = ''; };
return $http.get(url, { params : { userId : uid } }).then(function (res) {
return res.data;
});
},
and while making call from controller you would do:-
mySvc.getUser(userId).then(function(user){
myCtrlInstance.user = user;
});
Note: Avoid using deferred anti-pattern when you already have an operation that returns a promise.
You can make something like this:
evaApp.factory('userService', ['$http', '$q', function ($http, $q) {
var user = {};
return {
makeRequest : function(url, uid) {
var deferred = $q.defer();
if (!uid) { uid = ''; };
$http.get(url, { params : { userId : uid } }).then(function (res) {
user = res.data;
deferred.resolve(user);
});
return deferred.promise;
},
getUser : function(userId) {
console.log(user); // user is always empty
if(!user || !user._id) {
return this.makeRequest('/api/user/get', userId);
};
var deferred = $q.defer();
deferred.resolve(user);
return deferred.promise;
}
}
}]);
And then get the user details like this (the 1 is just for the example):
userService.getUser(1).then(
function( data ) {
console.log(data);
}
);

Jasmine unit testing angular service that returns promise doesn't resolve

Given this Test Code
it('can login', inject(function ($httpBackend,$rootScope) {
// Set up the mock http service responses
authRequestHandler = $httpBackend.when('POST', '/login')
.respond({success: true, user: {email: 'david#blah.com', roles: ['user']}});
var promise = dsAuth.authenticateUser('123', '123')
promise.then(function (success) {
console.log('Got login response');
expect(success).toBe(true);
expect(dsIdentity.isAuthenticated()).toBe(true);
console.log(dsIdentity.currentUser);
});
$rootScope.$digest(); //a solution found in on SO that doesn't work
}));
That promise (which gets returned from the auth service) never resolves? How can this be fixed ? the code in the .then() function is never called
Service Code :
(function(angular) {
angular.module('dsApp').factory('dsAuth',
['$http','$q',dsAuth]);
function dsAuth($http,$q) {
return {
authenticateUser: function(username,password) {
var dfd = $q.defer();
$http.post('/login', {username: username, password: password}).then(function (resp) {
console.log($resp);
if (resp.data.success) {
var user = new atUser();
angular.extend(user, resp.data.user);
atIdentity.currentUser = user;
dfd.resolve(true);
} else {
dfd.resolve(false);
}
});
return dfd.promise;
},
logoutUser: function() {
var dfd = $q.defer();
$http.post('/logout', {logout: true}).then(function () {
atIdentity.currentUser = undefined;
dfd.resolve();
});
return dfd.promise;
}
};
}
})(this.angular);
Jasmine doesn't work with asynchronous expects. The solution to this is to use the flush() function of httpBackend.
it('can login', inject(function ($httpBackend,$rootScope) {
// Set up the mock http service responses
authRequestHandler = $httpBackend.when('POST', '/login')
.respond({success: true, user: {email: 'david#blah.com', roles: ['user']}});
var promise = dsAuth.authenticateUser('123', '123')
var success = false;
promise.then(function (result) {
console.log('Got login response');
success = result;
console.log(dsIdentity.currentUser);
});
$httpBackend.flush();
expect(success).toBe(true);
expect(dsIdentity.isAuthenticated()).toBe(true);
}));
I'm not really sure where the dsIdentity comes from, but I presume you can figure that out in your own code. The pattern is the same - create a variable outside the closure and then set the value inside the closure. The flush() will cause the promise to fire and then you are good to go.

Resolving a promise in a dependent AngularJS service

I have a simple AngularJS app running in a Chrome Extension making use of the Storage API. Having an issue with the async nature of Storage; I've abstracted the storage away into a 'UserService' that sets and gets the data as a factory:
app.factory('UserService',
function($q, AppSettings) {
var defaults = {
api: {
token: AppSettings.environments[1].api.token
},
email: ''
};
var service = {
user: {},
save: function() {
chrome.storage.sync.set({'user': angular.toJson(service.user)});
},
restore: function() {
var deferred = $q.defer();
chrome.storage.sync.get('user', function(data) {
if(!data) {
chrome.storage.sync.set({'user': defaults});
service.user = defaults;
} else {
service.user = angular.fromJson(data.user);
}
deferred.resolve(service);
});
return deferred.promise;
}
};
// set the defaults
service.restore().then(function(data) {
console.log(data);
return data;
});
});
The console.log() call above dumps out the data as expected. However, when I am including the UserService in other factories (I have an APIService that makes use of a user-specific API token), the UserService parameter is being flagged as 'undefined' in the code below:
app.factory('APIService',
function($resource, $http, UserService, AppSettings) {
var token = UserService.user.api.token;
...
});
I am sure I am not fully grasping the Angular promise pattern in terms of consuming resolved promises throughout the app.
Updated code:
app.factory('UserService',
function($q, AppSettings) {
var defaults = {
api: {
token: AppSettings.environments[1].api.token
},
email: ''
};
var service = {
user: {},
save: function() {
chrome.storage.sync.set({'user': angular.toJson(service.user)});
},
restore: function() {
var deferred = $q.defer();
chrome.storage.sync.get('user', function(data) {
if(!data) {
chrome.storage.sync.set({'user': defaults});
service.user = defaults;
} else {
service.user = angular.fromJson(data.user);
}
deferred.resolve(service.user);
});
return deferred.promise;
}
};
// set the defaults
service.restore().then(function(data) {
console.log(data);
return data;
});
return service;
});
Edit/Additional Info:
Ok, getting close. Have refactored so that I am returning the object properly, but the issue now is that when the APIService gets created and tries to use the properties of the UserService object, they simply don't exist yet as they are only created after the async restore method is resolved. So it's not possible to access the UserService.user.api.token property, as it doesn't exist at that point, so the question is, how do I get that data in APIService when I need it if it is not available at that point? I'm trying to avoid having to put the entire contents of APIService into a callback that fires after a hypothetical new UserService.get() method that calls the callback on resolution of the promise. Any final guidance appreciated.
Your service is wrong. Please look at my fix:
app.factory('UserService',
function($q, AppSettings) {
var defaults = {
api: {
token: AppSettings.environments[1].api.token
},
email: ''
};
var service = {
user: {},
save: function() {
chrome.storage.sync.set({'user': angular.toJson(service.user)});
},
restore: function() {
var deferred = $q.defer();
chrome.storage.sync.get('user', function(data) {
if(!data) {
chrome.storage.sync.set({'user': defaults});
service.user = defaults;
} else {
service.user = angular.fromJson(data.user);
}
deferred.resolve(service.user); // <--- return the user in here
});
return deferred.promise;
}
};
// set the defaults
service.restore().then(function(data) {
console.log(data);
return data;
});
return service; // <--- return the service to be used injected when injected
});
[EDIT]
answer to your new question: Dont access user directly. create a new function in your service like getUser() that returns a promise. In that function return the user if it is already retreived otherwise return the restore() function:
var service = {
user: null,
getUser: function() {
if (service.user)
{
var deferred = $q.defer();
deferred.resolve(service.user);
return deferred.promise;
}
else
return service.restore();
},
save: function() {
chrome.storage.sync.set({'user': angular.toJson(service.user)});
},
restore: function() {
var deferred = $q.defer();
chrome.storage.sync.get('user', function(data) {
if(!data) {
chrome.storage.sync.set({'user': defaults});
service.user = defaults;
} else {
service.user = angular.fromJson(data.user);
}
deferred.resolve(service.user); // <--- return the user in here
});
return deferred.promise;
}
};
You're not returning an object from your factory. So when you try to inject your UserService parameter, it gives undefined because you haven't returned anything from your UserService function.
If you return your service variable, I think you'll get the behavior you're looking for.

Creating an AuthService to check if user is logged in

I have an AngularJS app which communicates with a Laravel PHP backend. Sending a GET request to /api/checkLogin will return { logged: false, username: undefined, id: undefined } if the user is not logged in, otherwise, it will return something like { logged: true, username: 'John', id: 123 }.
I am not too familiar with using AngularJS services, but I would like to set up a service called AuthService that can, well, perform my app's authentication services.
I would like to implement the following functions: AuthService.loggedIn, AuthService.isAdmin, AuthService.username, and AuthService.id.
I want these functions implemented in such a way that calling one will set the values for all the rest. For example, let's say I call AuthService.isAdmin. This function will check if isAdmin is set, if so, it will return the value of isAdmin. If isAdmin is not set, it will make an HTTP request to /api/checkLogin, set the values for loggedIn, isAdmin, username, and id, and then return the value of isAdmin. How can I accomplish this?
Here is the service I have tried putting together:
angular.module('myApp').factory('AuthService', ['$http', function($http) {
var loggedIn;
var isAdmin;
var username;
var id;
var checkLogin = function() {
if(loggedIn != undefined) {
return loggedIn
} else {
setUserData(checkLogin);
}
}
var checkAdmin = function() {
if(isAdmin != undefined) {
return isAdmin
} else {
setUserData(checkLogin);
}
}
var returnUsername = function() {
if(username != undefined) {
return username
} else {
setUserData(checkLogin);
}
}
var returnId = function() {
if(id != undefined) {
return id
} else {
setUserData(checkLogin);
}
}
// Our function call which will set our loggedIn, isAdmin, username, and id values
var setUserData = function(callback) {
$http.get(baseURL+'/api/checkLogin').success(function(data) {
loggedIn = data.logged;
if(loggedIn) {
isAdmin = data.is_admin;
username = data.username;
id = data.id;
}
callback();
});
}
return {
loggedIn: function() { return checkLogin(); },
isAdmin: function() { return checkAdmin(); },
username: function() { return returnUsername(); },
id: function() { return returnId(); },
}
}]);
It looks you want to use checkLogin as a callback, but instead of doing it the way you have it, return the promise back to checkLogin from setUserData. Then in checkLogin, create your own deferred to handle the results.
You are acting on asynchronous logic by introducing the $http call, so checkLogin is going to need to return a promise in all cases:
var checkLogin = function() {
// Create a custom deferred
var defer = $q.defer();
if(loggedIn != undefined) {
// Resolve your deferred with the value of logged in
defer.resolve(loggedIn);
} else {
setUserData().then(function (data) {
console.log('Set user data returned successfully');
loggedIn = data.logged;
if(loggedIn) {
isAdmin = data.is_admin;
username = data.username;
id = data.id;
defer.resolve(loggedIn);
} else {
defer.reject();
}
}, function () {
console.log('setUserData failed');
defer.reject();
});
}
return defer.promise;
}
var setUserData = function() {
return $http.get(baseURL+'/api/checkLogin');
}
AuthService.loggedIn() will now return a promise. You have to resolve the promise to get the value out of it:
AuthService.loggedIn().then(function (data) {
console.log(data);
});
The function passed to then above will be called when the promise is resolved with the value that the promise was resolved with. In this case, its the value of data.logged because thats what was passed to defer.resolve in your checkLogin function.
Here's some reading on Promises:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
http://www.html5rocks.com/en/tutorials/es6/promises/
https://docs.angularjs.org/api/ng/service/$q
use this
loginService this
var app=angular.module('myApp');
app.factory('loginService', function ($http, $location) {
return {
login: function (data, scope) {
var $promise = $http.post('api.php/site/login', data);
$promise.then(function (msg) {
var uId = msg.data.key;
if (msg.data.key) {
$location.path('/abAdmin/home');
} else {
$location.path('/abAdmin');
}
});
},
logout: function () {
$http.post('api.php/site/logout');
$location.path('/abAdmin');
},
isLogged: function () {
var $check = $http.post('api.php/site/checkSession');
return $check;
}
}
});
and your app.js
var app=angular.module('myApp');
app.run(function ($rootScope, $location, loginService) {
var routPermission = [
'/abAdmin/home',
'/abAdmin/category',
'/abAdmin/category/:id'];
$rootScope.$on('$routeChangeStart', function (e, current) {
if ( routPermission.indexOf(current.$$route.originalPath) != -1) {
var connected = loginService.isLogged();
connected.then(function (data) {
if (!data.data.isLogged) {
logged=true;
$location.path('abAdmin');
}
})
}
});
});

Resources