React app communicate with iframe via postMessage: where to setup callbacks? - javascript

I have a website which communicates with a third party iframe.
There are 3 methods: login, sign and get information. They all work similar. I embed the third party iframe and send a message to it via window.postMessage(...) to it. After I have sent a message to the iframe I get back a response.
I can't come up with a clean architecture to encapsulate the communication.
Current idea:
class iFrameCommunicator{
init() {
window.addEventListener('message', (message) => {
if (message == "login_success"){
setGlobalLoginState(true)
}
if (message == "sign_success"){
setGlobalSignState(true)
}
})
}
}
login() {
window.postMessage({command: "login"})
}
sign(payload) {
window.postMessage({command: "sign", payload: payload})
}
}
Login page
<button onClick={() => iFrameCommunicator.login()}>Login</button>
With this approach I have to monitor the GlobalLoginState to see if the login call was successful. I would like the login function (and the other functions) to return a Promise instead.
Is there a way to refactor the code so that every function which communicates with the iframe returns a Promise without having to setup an addEventListener for every function call?

So 3rd party iframe takes three messages, presumably you can't control what you get back?
However if you know what you are getting back on success / fail can you add a listener for the success / fail in your app - a bit flaky as its 3rd party and message may change, but that is what we do in a loader / iframe setup that we control.
Sending and listening for messages either side - we also pass a function into the iframe for invocation, but you can't given its not your iframe so callbacks presumably won't be possible.
What did you end up doing?

Have you considered the usage of useEffect which depends on your GlobalLoginState?
Then updated your state based on other events? To be honest, it is quite very difficult to give an answer not knowing exactly the other context or availabilities within your webapp.

Related

WebUSB: How to call navigator.usb.requestDevice() if navigator.usb.getDevices() fails

I am writing a WebUSB based extension for Scratch3. In the extensions constructor i need to establish the WebUSB connection. Ideally i'd like to use navigator.usb.getDevices() to check for available/paired devices and if that fails i'd like to ask the user to select a device using navigator.usb.requestDevice().
The problem is that navigator.usb.requestDevice() needs to be called from a user gesture. In most cases the scratch3 extension constructor is being called from a user action and thus I can use navigator.usb.requestDevice() just fine.
But navigator.usb.getDevices() returns a Promise and invoking navigator.usb.requestDevice() from there fails with
"Must be handling a user gesture to show a permission request."
So the following works (but opens a request dialog every single time):
navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
// ...
})
But the following fails due to missing "user gesture":
navigator.usb.getDevices().then(devices => {
if(devices.length == 0) {
navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
// ...
})
}
})
I'd like to avoid messing with the Scratch 3 core and thus don't want to add another UI element.
Using only navigator.usb.requestDevice() as in the first snippet has two major disadvantages:
It opens the request dialog all the time although it's only necessary once
Sometimes later the constructor may be called from a non-interactive situation and navigator.usb.requestDevice() would completely fail while navigator.usb.getDevices() would succeed
Is there a way to make the approach from the second code snippet work?
I am not familiar with Scratch 3 but is it possible to call getDevices() when your extension is initialized and defer providing a button the user can click to trigger requestDevice() until after the code knows whether it already has permission to access a device?
The getDevices() method, and onconnect and ondisconnect event handlers are provided so that the page can keep its interface up to date with any available devices.
This has been fixed as of Chrome 72 through the User Activation v2 feature.

How to avoid race condition between `addEventListener` and `window.open`

As part of the OAuth flow for my site, I'm using window.open to allow the user to sign in to the 3rd party provider, and postMessage to send the auth token back to my webpage. The basic flow looks something like this, though I've removed the extra code related to timeouts, cleanups, error handling, and so on:
const getToken = () => new Promise((resolve, reject) => {
const childWindow = window.open(buildUrl({
url: "https://third-party-provider.com/oauth/authorize",
query: {
redirect_uri: "my-site.com/oauth-callback"
}
})
window.addEventListener('message', event => {
if(
event.origin === window.location.origin &&
event.source === childWindow
) {
if(event.data.error) reject(event.data.error)
else resolve(event.data)
}
})
// Plus some extra code to remove the event listener and close
// the child window.
}
The basic idea is that, when the user clicks authorize, they are redircted to my-site.com/oauth-callback. That page completes the OAuth flow on the server side, and then loads a page containing a small amount of javascript which simply calls window.opener.postMessage(...)
Now, the crux of my question is that there's actually a race condition here, at least in theory. The issue is that the childWindow could hypothetically call postMessage before addEventListener is called. Conversely, if I call addEventListener first, it could receive messages before childWindow is set, which I think will throw and exception? The key issue seems to be that window.open isn't asynchronous, so I can't atomically spawn a window and set up a message handler.
Currently, my solution is to set up the event handler first, and assume that messages don't come in while that function is being executed. Is that a reasonable assumption? If not, is there a better way to ensure this flow is correct?
There shouldn't be a race condition here, because the window.open and window.addEventListener occur together in the same tick, and Javascript is both single-threaded and non-reentrant.
If the child window does manage to posts a message before the call to window.open completes, the message will simply go into the event queue for the current Javascript context. Your code is still running, so the addEventListener call happens next. Finally, at some future point that we don't see here, your code returns control to the browser, which ends the current tick.
Once the current tick is over, the browser can check out the event queue and dispatch the next piece of work (presumably the message). Your subscriber is already in place, so everything is fine!

Angular $http service, how to cancel / unsubscribe pending requests?

I have an AngularJS application which perform
- 1 request to fetch the main user profile, that contains references to the user friends,
- and then 1 request per friend to retrieve the friend profile.
When we click on a friend's profile, we load this profile as the main profile.
I am in the RDF / semantic web world, so I can't model these interactions differently, this is not the point of the question.
This is an early version of the application I'm trying to build, that can help you understand what's my problem: http://sebastien.lorber.free.fr/addressbook/app/
The code looks like:
$scope.currentProfileUri = 'http://bblfish.net/people/henry/card#me';
$scope.$watch('currentProfileUri', function() {
console.debug("Change current profile uri called with " + $scope.currentProfileUri);
loadFullProfile($scope.currentProfileUri);
})
// called when we click on a user's friend
$scope.changeCurrentProfileUri = function changeCurrentProfileUri(uri) {
$scope.currentProfileUri = uri;
}
function loadFullProfile(subject) {
$scope.personPg = undefined;
// we don't want to capture the scope variable in the closure because there may be concurrency issues is multiple calls to loadFullProfile are done
var friendsPgArray = [];
$scope.friendPgs = friendsPgArray;
fetchPerson(subject).then(function(personPg) {
$scope.personPg = personPg
fetchFriends(personPg,function onFriendFound(relationshipPg) {
friendsPgArray.push(relationshipPg);
});
},function(error) {
console.error("Can't retrieve full profile at "+uri+" because of: "+error);
});
}
So the friends are appended to the UI as they come, when the http response is available in the promise.
The problem is that the function changeCurrentProfileUri can be called multiple times, and it is possible that it is called by while there are still pending requests to load the current users's friends.
What I'd like to know is if it's possible, on changeCurrentProfileUri call, to cancel the previous http requests that are still pending? Because I don't need them anymore since I'm trying to load another user profile.
These pending requests will fill an instance of friendsPgArray that is not in the scope anymore and won't be put in the scope, so it is just useless.
Using Java Futures, or frameworks like RxScala or RxJava, I've seen there's generally some kind of "cancel" or "unsubscribe" method which permits to de-register interest for a future result. Is it possible to do such a thing with Javascript? and with AngularJS?
Yes, it is! Please, see this section of angular $http service docs. Note timeout field in config object. It does, I think, exactly what you need. You may resolve this pormise, then request will be marked as cancelled and on error handler will be called. In error handler you may filter out this cases by there http status code - it will be equal to 0.
Here is fiddle demonstrating this

Passing object between views (flash message)

What is the best way to pass message in the below scenario.
In the success scenario of $scope.p.$save, the result contains a message (res.message), which I like to display in the next view ($location.path("/test/"+res.reply.Id)). Without AngularJS, I may pass it in the url or save it in session cookies. But, I guess there might be a better way in AngularJS as there is no browser redirect and the state should be available. What is the best way to achieve this?
Setting it in rootScope shows it while I use browser back button, and the scope of the message should only for the first navigation to the new view.
function NewCtrl(Phone, $location, $rootScope, $scope) {
$scope.p = new Phone();
$scope.save = function () {
$scope.p.$save(
{},
function (res) {
$rootScope.message = res.message **//<-- this will cause message still set when using browser back button, etc**
$location.path("/test/"+res.reply.Id); **//<-- Req: needs to pass the message to next view**
}, function (res) {
//TODO
}
);
};
}
....
PhoneApp.factory('Phone', function ($resource) {
return $resource('/api/test/:_id')
});
You could use a service which displays the flash on $routeChangeSuccess.
Each time you set a flash message, add it to a queue, and when the route changes take the first item off the queue and set it to the current message.
Here's a demo:
http://plnkr.co/edit/3n8m1X?p=preview
I was looking to implement similar functionality, but actually wanted more of a growl style message.
I've updated the excellent plinkr code that Andy provided above to include a 'pop' method that leverages the toastr growl-style notification library.
My update also lets you to specify the notification type (info, warning, success, error) and title.
The 'pop' method skips adding the message to the queue, and instead pops it up on the screen immediately. The set/get functionality from Andy's previous plinkr remains mostly unchanged.
You can find my update here: http://plnkr.co/edit/MY2SXG?p=preview
I don't believe there's a way to do this default to AngularJS. Your best bet would just be passing the message (encoded) through a query string.

Facebook Connect - Single Sign On Causes Infinite Loop :(

I have a Facebook Connect (FBML) web application that has been live for a while now.
All is working well, however user's are reporting issues with the single sign on (and i have seen the issue on their computer, but have not been able to replicate locally).
With FBC, there is a "event" you can hook into to automatically determine if the user is authenticated to Facebook.
That is achieved via this:
FB.Connect.ifUserConnected(onUserConnected, null);
The onUserConnected function can then do whatever it needs to attempt a single sign on.
For me, what i do is a AJAX call to the server to see if the user has an active website account (based on the Facebook details - user id, which i store in my system when they connect).
If they do, i show a jQuery modal dialog ("Connecting with Facebook") and do a window.location.reload().
The server then kicks in, and does the Single Sign On. This code is executed on every page:
public static void SingleSignOn(string redirectUrl)
{
if (!HttpContext.Current.Request.IsAuthenticated) // If User is not logged in
{
// Does this user have an Account?
if (ActiveFacebookUser != null)
{
// Get the user.
var saUser = tblUserInfo.GetUserByUserId(ActiveFacebookUser.IdUserInfo);
if (saUser != null)
{
// Get the Membership user.
MembershipUser membershipUser = Membership.GetUser(saUser.UserID);
if (membershipUser != null && membershipUser.IsApproved)
{
// Log Them Automically.
FormsAuthentication.SetAuthCookie(membershipUser.UserName, true);
// At this point, the Forms Authentication Cookie is set in the HTTP Response Stream.
// But the current HTTP Context (IsAuthenticated) will read HTTP Request Cookies (which wont have the new cookie set).
// Therefore we need to terminate the execution of this current HTTP Request and refresh the page to reload the cookies.
HttpContext.Current.Response.Redirect(
!string.IsNullOrEmpty(redirectUrl) ? redirectUrl : HttpContext.Current.Request.RawUrl,
true);
}
else
{
HandleUnregisteredFacebookUser();
}
}
else
{
HandleUnregisteredFacebookUser();
}
}
else
{
HandleUnregisteredFacebookUser();
}
}
}
I have never experienced an issue with this, but user's are reporting an "infinite" loop where the dialog gets shown, the window is refreshed, dialog is shown, window is refreshed, etc.
Basically, my AJAX call is saying the user exists, but my single sign on isn't.
Which is hard to believe because the code is very similar:
This is the AJAX Web Service:
if (!HttpContext.Current.Request.IsAuthenticated) // If user is not yet authenticated
{
if (FacebookConnect.Authentication.IsConnected) // If user is authenticated to Facebook
{
long fbId;
Int64.TryParse(Authentication.UserId, out fbId);
if (fbId > 0)
{
tblFacebook activeUser = tblFacebook.Load(facebookUniqueId: fbId);
if (activeUser != null && activeUser.IsActive) // If user has an active account
{
return true;
}
}
}
}
So if the response of this WS is 'true', i do a window.location.reload();
So i have no idea what the issue is (as i cant replicate), sounds like the Single Sign On isn't adding the Forms Authentication cookie properly to the response stream, or the Response.Redirect(Request.RawUrl) isn't reloading the cookie properly.
How do other's handle this?
This is what should happen:
User logs into Facebook (on Facebook itself)
User comes to my site (which has been previously authorised)
My site compares the FBID with the FBID in my DB, and signs them in.
My site is an ASP.NET 4.0 Web Forms application, using the "old" Facebook Connect JavaScript API (FeatureLoader.js), and Forms Authentication.
The only other solution to an AJAX call/window.reload i can think of is an AJAX UpdatePanel.
Can anyone help me out?
EDIT
Also, i realise that i can also use 'reloadIfSessionStateChanged':true to do the reload (which stops the infinite loop), but the problem with this is i cannot show my nice fancy dialog.
So i found a couple of issues.
Firstly, i shouldn't be setting a persistent cookie:
FormsAuthentication.SetAuthCookie(membershipUser.UserName, true);
We should only do this when the user ticks a box such as "Remember Me".
Secondly, i was checking the FB Auth status on every page on the server, so this could have been getting out of sync in the client-side.
Here is my new solution - which is better, and has a failsafe for the dreaded 'infinite loop'.
I no longer check the FB Auth status on the server, i do on the client:
FB.Connect.ifUserConnected(onUserConnected, null); // runs on every page request, on client-side
In the onUserConnection function i do the following:
Call web service to see if user CAN be signed in automatically (same WS as above)
If ok, check a special "Single Sign On" cookie has not been set.
If it hasn't been set, redirect to FacebookSingleSignOn.aspx.
FacebookSingleSignOn.aspx does the following:
Signs the user in using Forms Authentication (same as above)
Creates a special "Single Sign On" cookie to signify a SSO has been attempted
Redirects to the homepage
So, at point 3 above - this is where the infinite loop "could" happen. (as the onUserConnected will run again)
But it is now impossible (i hope) for it to happen, as it will only attempt to do the SSO if they havent already tried.
I clear the SSO cookie on the Facebook Logout action.
Now works well, logic makes sense - and it's done on the client-side (where it should be, as this is where FB does it's magic).
Hope that helps someone else out.

Categories

Resources