Using the gapi.auth.authorize function, the user can close the popup without clicking any option (no accept or deny button). When this case happens, my callback function doesn't fire, so that I can't handle this case. What's the way to resolve this scenario?
Thanks.
This question has been around for a while, but when I looked into the issue (I want to show a spinner while the google authentication window is open, and hide it if the user decides not to authenticate), and found that gapi is throwing an error popup_closed_by_user. There is a two-second delay (which is kind of long, Facebook's is instant) before it is thrown, but it does work. Hooray, Google!
Some sample code (angular 1.x), prompting is the attribute to show the spinner:
_google_obj.prompting = true;
gapi.auth2.getAuthInstance().signIn().then(function(googleResponse){
var token = googleResponse.getAuthResponse().id_token;
SVC_exec_.post('/q/goog', 1000, { token: token }, 'signing you in through Google', function (response) {
if (response.code === 'ok') {
// update the UI
}
_google_obj.prompting = false;
});
},
function(error){
$timeout(function () {
console.log('user probably closed the google popup window: '+error);
_google_obj.prompting = false;
});
});
They don't appear to mention it in any documentation, but gapi.auth.authorize() returns the popup Window. So you can save the returned Window and set an interval or timeout to check Window.closed.
So you the auth function from Google returns promise, not a window. But then you can wrap original window into the function, which will set interval, to check if opened window closed already.
// variable to store our deferred object
var authDefer = null;
function auth() {
// right before the auth call, wrap window.open
wrap();
// Call auth
authDefer = window.gapi.auth.authorize({
client_id: ...,
scope: ...,
immediate: ...
}).then(
// onSuccess,
// onReject,
// onNotify
);
}
function wrap() {
(function(wrapped) {
window.open = function() {
// re-assign the original window.open after one usage
window.open = wrapped;
var win = wrapped.apply(this, arguments);
var i = setInterval(function() {
if (win.closed) {
clearInterval(i);
if (authDefer) {
authDefer.cancel();
}
}
}, 100);
return win;
};
})(window.open);
}
Taken from one of the thread on Google forums. Really works.
External link to Source
Related
I am trying to get the token from JS using ADAL.js for authentication.
In the below code, I am trying to get the cached user and token. If the user and token is not cached, I open a pop-up to a dummy page in CRM and then cache the user and the token.
var getUser = function () {
return new Promise(function (resolve, reject) {
// If the user is cached, resolve the promise immediately.
var user = authContext.getCachedUser();
if (user) {
var cachedToken = authContext.getCachedToken(clientId);
resolve(cachedToken);
return;
}
// The user was not cached. Open a popup window which
// performs the OAuth login process, then signals
// the result.
authContext.config.displayCall = function (url) {
authContext.config.displayCall = null;
var popup = window.open(url, 'auth-popup', 'toolbar=no,status=no,menubar=no,scrollbars=no,resizable=no,left=10000, top=10000, width=10, height=10, visible=none');
var intervalId = window.setInterval(function () {
try {
if (popup.location.pathname.indexOf('/' + dummyAuthPage) >= 0) {
authContext.handleWindowCallback(popup.location.hash);
popup.close();
token = authContext.getCachedToken(clientId);
if (token) {
window.clearInterval(intervalId);
resolve(token);
}
else {
reject(authContext.getLoginError());
}
}
} catch (whatever) {
if (popup.closed) {
reject();
}
}
}, 100);
};
Do we have a better way to do the same especially without getting the pop-up?
I am currently trying to do the Silent Authentication. But here it is mentioned "In the tab's content page, call microsoftTeams.getContext() to get a login hint for the current user". This command is not returning anything in CRM.
Thanks
You can try silent authentication described in the following docs.microsoft Link Silent authentication
I'm using angular-http-auth for intercepting 401 response in order to display login dialogue and when the user is authorized, to retry failed request.
Since I'm using infinity-scroll I'm increasing an offset value, with every additional upload:
var upload = function () {
dataResource.query($scope.model).then(function (result) {
angular.forEach(result.items, function (value) {
$scope.items.push(value);
});
});
}
$scope.uploadMore = function () {
$scope.model.Offset = $scope.model.Offset + 10;
upload();
};
upload();
When my page loads up it immediately sends 2 request to server upload(), invoked from this directive, and uploadMore() by infinity-scroll.
However, after user has logged in, the page does not display the first 10 entries, instead it displays 11-20 items 2 times in a row.
When I tried to debug it, I noticed that when angular-http-auth retries requests it uses increased by 10 Offset value for both queries($scope.module argument).
Functions upload() and uploadMore() are running for 2 times before angular-http-auth, so I guess that is why interceptor uses updated argument for both queries.
Could somebody please help me with this problem?
So you can resolve this problem prevent execute request until previous will finish.
The faster way to do that is :
var pending = false;
var upload = function () {
if(!pending) {
pending = true;
dataResource.query($scope.model).then(function (result) {
pending = false;
angular.forEach(result.items, function (value) {
$scope.items.push(value);
});
});
}
}
I have a function that does request to API when the video is finished:
video.addEventListener('ended', example);
var example = function () {
VK.api('video.get', { owner_id: 123 }, function(data) {
/**...*/
}
}
And also I have a replay button (shows when video is finished), which the user can click faster than the response comes from API. And now I need to kill my function. How I can do it?
Link to API: https://vk.com/dev/video.get
There don't seems to be any method to cancel a all from this api.
So either you analyse the code to see how is handled that call (Can't do it for you because we need an auth).
I'd say that your best bet is to set a boolean flag to true while making the call and to false when asking for replay :
//Our flag
var userCanceledCalled = false;
var example = function () {
document.querySelector('span').innerHTML = "new example should be called in few seconds";
//Set it to fals at call
userCanceledCalled = false;
VK.api('video.get', { owner_id: 123 }, function(data) {
if(!userCanceledCalled){
alert(data);
}
});
}
function replay(){
//Set it to true on user action
document.querySelector('span').innerHTML = "new example as been canceled";
userCanceledCalled = true;
}
var b = document.querySelectorAll('button');
b[0].addEventListener('click', example);
b[1].addEventListener('click', replay);
var VK={api: function(data, useless, fn){setTimeout(function(){fn(data)}, 3000)}};
<button>Call a new Example</button></br>
<button>Replay</button>
<span id="log"></span>
There are two approaches:
Cancel the ongoing request
Prevent the callback function from doing work
I have no idea how to do 1. but you can probably ask at the Vkontakte issue tracker https://vk.com/bugs
As for the second one, you can check if the user has already clicked on replay button by the time the response comes back and turn your callback into a noop:
video.addEventListener('ended', function () {
VK.api('video.get', { owner_id: 123 }, function (data) {
// Let's check if user has pressed the replay button and
// if yes then we abort the function:
if (userHasPressedReplay) { return; }
/**...*/
};
});
This is not ideal because the request still hits the server... but it prevents execution of the callback function.
I can return a value if I send a sync message:
// frame script
var chromeBtnText = sendSyncMessage("getChromeToolbarButtonText");
if (chromeBtnText == 'blah') {
alert('tool is blah');
}
// chrome script
messageManager.addMessageListener("getChromeToolbarButtonText", listener);
function listener(message) {
return document.getElementById('myChromeToolbarButton').label.value;
}
How do I achieve this with a callback with sendAsyncMessage?
I was hoping to do something like:
// frame script
function myCallback(val) {
var chromeBtnText = val;
if (chromeBtnText == 'blah') {
alert('tool is blah');
}
}
var chromeBtnText = sendAsyncMessage("getChromeToolbarButtonText", null, myCallback);
There is no callback for replies. In fact, there is no reply at all. The return value from the chrome message listener is simply ignored for async messages.
To do fully async communication, you'd have to send another message containing the reply.
Frame script
addMessageListener("getChromeToolbarButtonTextReply", function(message) {
alert(message.data.btnText);
});
sendAsyncMessage("getChromeToolbarButtonText");
Chrome
messageManager.addMessageListener("getChromeToolbarButtonText", function(message) {
var btnText = document.getElementById('myChromeToolbarButton').label.value;
// Only send message to the frame script/message manager
// that actually asked for it.
message.target.messageManager.sendAsyncMessage(
"getChromeToolbarButtonTextReply",
{btnText: btnText}
);
});
PS: All messages share a namespace. So to avoid conflicts when another piece of code wants to use the same name getChromeToolbarButtonText, you better choose a more unique name in the first place, like prefixing your messages with your add-on name my-unique-addoon:getChromeToolbarButtonText or something like that. ;)
I was also hoping to do something similar:
messageManager.sendAsyncMessage("my-addon-framescript-message", null, myCallback);
I'm going the other direction so the myCallback would be in chrome but it's exactly the same principle.
I'd used similar approaches to #Noitidart and #nmaier before but in this new case I wanted to bind to some local data so myCallback can behave differently based on the application state at the time the first message was sent rather than at the time the callback is executed, all while allowing for the possibility of multiple message round-trips being in progress concurrently.
Chrome:
let someLocalState = { "hello": "world" };
let callbackName = "my-addon-somethingUnique"; // based on current state or maybe generate a UUID
let myCallback = function(message) {
messageManager.removeMessageListener(callbackName, myCallback);
//message.data.foo == "bar"
//someLocalState.hello == "world"
}.bind(this); // .bind(this) is optional but useful if the local state is attached to the current object
messageManager.addMessageListener(callbackName, myCallback);
messageManager.sendAsyncMessage("my-addon-framescript-message", { callbackName: callbackName } );
Framescript:
let messageHandler = function(message) {
let responseData = { foo: "bar" };
sendAsyncMessage(message.data.callbackName, responseData);
};
addMessageListener("my-addon-framescript-message", messageHandler);
There's a real-world example here: https://github.com/luckyrat/KeeFox/commit/c50f99033d2d07068140438816f8cc5e5e290da9
It should be possible for Firefox to be improved to encapsulate this functionality in the built-in messageManager one day but in the mean-time this approach works well and with a surprisingly small amount of boiler-plate code.
in this snippet below. i add the callback before sendAsyncMessage('my-addon-id#jetpack:getChromeToolbarbuttonText'... as i know it will send back. Then I remove it after callback executes. I know I don't have to but just to kind of make it act like real callback, just to kind of show people, maybe it helps someone understand.
Frame:
/////// frame script
function CALLBACK_getChromeToolbarButtonText(val) {
removeMessageListner('my-addon-id#jetpack:getChromeToolbarButtonTextCallbackMessage', CALLBACK_getChromeToolbarButtonText); //remove the callback
var chromeBtnText = val;
if (chromeBtnText == 'blah') {
alert('tool is blah');
}
}
addMessageListener('my-addon-id#jetpack:getChromeToolbarButtonTextCallbackMessage', CALLBACK_getChromeToolbarButtonText); //add the callback
var chromeBtnText = sendAsyncMessage("my-addon-id#jetpack:getChromeToolbarButtonText", null);
Chrome:
////// chrome script
messageManager.addMessageListener("my-addon-id#jetpack:getChromeToolbarButtonText", listener);
function listener() {
var val = document.getElementById('myChromeToolbarButton').label.value;
sendAsyncMessage('my-addon-id#jetpack:getChromeToolbarButtonTextCallbackMessage',val);
}
I want to intercept all route changes with Sammy to first check if there is a pending action. I have done this using the sammy.before API and I return false to cancel the route. This keeps the user on the 'page' but it still changes the hash in the browsers address bar and adds the route to the browsers' history. If I cancel the route, I dont want it in the address bar nor history, but instead I expect the address to stay the same.
Currently, to get around this I can either call window.history.back (yuk) to go back to the original spot in the history or sammy.redirect. Both of which are less than ideal.
Is there a way to make sammy truly cancel the route so it stays on the current route/page, leaves the address bar as is, and does not add to the history?
If not, is there another routing library that will do this?
sammy.before(/.*/, function () {
// Can cancel the route if this returns false
var response = routeMediator.canLeave();
if (!isRedirecting && !response.val) {
isRedirecting = true;
// Keep hash url the same in address bar
window.history.back();
//this.redirect('#/SpecificPreviousPage');
}
else {
isRedirecting = false;
}
return response.val;
});
In case someone else hits this, here is where I ended up. I decided to use the context.setLocation feature of sammy to handle resetting the route.
sammy.before(/.*/, function () {
// Can cancel the route if this returns false
var
context = this,
response = routeMediator.canLeave();
if (!isRedirecting && !response.val) {
isRedirecting = true;
toastr.warning(response.message); // toastr displays the message
// Keep hash url the same in address bar
context.app.setLocation(currentHash);
}
else {
isRedirecting = false;
currentHash = context.app.getLocation();
}
return response.val;
});
When using the code provided within the question and answer you have to notice that the route you cancelled will also be blocked for all future calls, routeMediator.canLeave will not be evaluated again. Calling a route twice and cancelling it depending on current state is not possible with this.
I could produce the same results as John Papa did when he used SammyJS on the SPA/Knockout course.
I used Crossroads JS as the router, which relies on Hasher JS to listen to URL changes "emitted" by the browser.
Code sample is:
hasher.changed.add(function(hash, oldHash) {
if (pageViewModel.isDirty()){
console.log('trying to leave from ' + oldHash + ' to ' + hash);
hasher.changed.active = false;
hasher.setHash(oldHash);
hasher.changed.active = true;
alert('cannot leave. dirty.');
}
else {
crossroads.parse(hash);
console.log('hash changed from ' + oldHash + ' to ' + hash);
}
});
After revisiting an older project and having a similar situation, I wanted to share another approach, just in case someone else is directed here.
What was needed was essentially a modern "auth guard" pattern for intercepting pages and redirecting based on credentials.
What worked well was using Sammy.around(callback) as defined here:
Sammy.js docs: Sammy.Application around(callback)
Then, simply do the following...
(function ($) {
var app = Sammy("body");
app.around(checkLoggedIn);
function canAccess(hash) {
/* access logic goes here */
return true;
}
// Authentication Guard
function authGuard(callback) {
var context = this;
var currentHash = app.getLocation();
if (!canAccess(currentHash)) {
// redirect
context.redirect("#/login");
}
else {
// execute the route path
callback();
}
};
})(jQuery);