We are in the midst of creating a PhoneGap-based app using AngularJS and the Ionic framework.
This app is a store management system which ties in with an existing web app using OAuth2.
The app includes an 'Orders' view which displays a list of orders the customer has received. Before the list of orders loads, the following function verifies that the user's access token is still valid, and if not, obtains a new one.
function verifyAccessToken() {
var now = new Date().getTime();
if (now > tokenStore.access_token_expiry_date) {
// renew access token
$http({
url: '*API URL*/token',
method: "POST",
data: {
refresh_token : tokenStore.refresh_token,
grant_type : 'refresh_token',
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectURI
},
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
transformRequest: function(obj) {
var str = [];
for(var p in obj)
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
return str.join("&");
}
})
.success(function (data, status, headers, config) {
tokenStore['access_token'] = data.access_token;
var expiresAt = now + parseInt(data.expires_in, 10) * 1000 - 10000;
tokenStore['access_token_expiry_date'] = expiresAt;
console.log(data.access_token);
})
.error(function (data, status, headers, config) {
if(status=='404') {
$rootScope.$emit('serviceUnavailable');
}
if(status=='400' || status=='401') {
$rootScope.$emit('tokenUnauthorized');
}
console.log(status);
console.log(data);
});
}
};
It then calls the list of orders using the new access token
return $http({method: 'GET', url: '*API URL*?access_token=' + tokenStore.access_token, params: {}})
.error(function(data, status, headers, config) {
if(status=='404') {
$rootScope.$emit('serviceUnavailable');
}
if(status=='401') {
$rootScope.$emit('tokenUnauthorized');
}
console.log(status);
console.log(data);
});
}
The problem is that the HTTP GET doesn't wait for the VerifyAccessToken function to complete.
How can this be structured to avoid this problem?
Any advice you can offer would be appreciated.
UPDATE 2 (after klyd's answer):
I have updated the two functions in my oauth-angular.js as described below:
The verifyAccessToken function now reads as follows:
function verifyAccessToken() {
var deferred = $q.defer();
if ( (new Date().getTime()) < tokenStore.access_token_expiry_date ) {
/* token is still valid, resolve the deferred and bail early */
deferred.resolve();
return deferred.promise;
}
/* token is not valid, renew it */
alert('getting new access token')
$http({
url: 'https://' + tokenStore.site_name + '.somedomain.com/api/oauth2/token',
method: "POST",
data: {
refresh_token : tokenStore.refresh_token,
grant_type : 'refresh_token',
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectURI
},
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
transformRequest: function(obj) {
var str = [];
for(var p in obj)
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
return str.join("&");
}
}).success(function (data, status, headers, config) {
tokenStore['access_token'] = data.access_token;
var now = new Date().getTime();
var expiresAt = now + parseInt(data.expires_in, 10) * 1000 - 10000;
tokenStore['access_token_expiry_date'] = expiresAt;
console.log(data.access_token);
deferred.resolve();
})
.error(function (data, status, headers, config) {
if(status=='404') {
$rootScope.$emit('serviceUnavailable');
}
if(status=='400' || status=='401') {
$rootScope.$emit('tokenUnauthorized');
}
console.log(status);
console.log(data);
deferred.reject(); // as the last step, reject the deferred, there was a failure
});
return deferred.promise;
}
and the getOrders function now reads as follows:
function getOrders() {
verifyAccessToken().then(
function() {
return $http({method: 'GET', url: 'https://' + tokenStore.site_name + '.somedomain.com/api/1.0/orders?access_token=' + tokenStore.access_token, params: {}})
.error(function(data, status, headers, config) {
if(status=='404') {
$rootScope.$emit('serviceUnavailable');
}
if(status=='401') {
$rootScope.$emit('tokenUnauthorized');
}
console.log(status);
console.log(data);
});
},
function() {
/* code to handle a failure of renewing the token */
});
}
My controllers.js file now throws the following error when executing the function getOrders.
TypeError: Cannot read property 'success' of undefined
.controller('OrdersCtrl', function ($scope, $stateParams, oauth, $ionicLoading) {
function loadOrders() {
$scope.show();
oauth.getOrders()
.success(function (result) {
$scope.hide();
$scope.orders = result;
console.log(result);
// Used with pull-to-refresh
$scope.$broadcast('scroll.refreshComplete');
})
.error(function(data) {
$scope.hide();
});
}
})
This previously worked with no problems. Any thoughts?
The return value of $http
is a promise. Whenever a function returns a promise its most likely about the perform an asynchronous operation. That means the function call is going to return an object immediately that you can then use to call other methods when that operation completes.
In this case you should re-arrange your verifyAccessToken
function to return a promise of its own.
Something like:
function verifyAccessToken() {
var deferred = $q.defer();
if ( (new Date().getTime()) < tokenStore.access_token_expiry_date ) {
/* token is still valid, resolve the deferred and bail early */
deferred.resolve();
return deferred.promise;
}
/* token is not valid, renew it */
$http({
/* stuff */
}).success(function() {
/* stuff */
deferred.resolve(); // resolve the deferred as the last step
})
.error(function() {
/* stuff */
deferred.reject(); // as the last step, reject the deferred, there was a failure
});
return deferred.promise;
}
Then when you go to call verifyAccessToken you would do something like:
/* stuff before the verifyAccessToken call */
verifyAccessToken().then(
function() {
/* any stuff after the verifyAccessToken call */
/* or any code that was dependent on verifying the access token */
},
function() {
/* code to handle a failure of renewing the token */
});
/*
there should be nothing after the call that depends on verifying the
access token. Remember this asynchronous so the initial call to verifyAccessToken
is going to return immediately. Then sometime in the future the success
or error call back will be called.
*/