Why does JavaScript "window.postMessage" create duplicate messages? - javascript

I'm trying to open a popup window, do some stuff, and send a message back to the opening window so I can do some more stuff with the data it sends.
Essentially, I'm adapting the process outlined here.
This is my code on the "opening window". It runs when a social connect button is clicked. The code opens a popup window and assigns a listener event on the opening window to receive a message from the popup:
//Do the operation
let windowObjectReference = null;
let previousUrl = null;
const openSignInWindow = (url, name) => {
// remove any existing event listeners
window.removeEventListener('message', receiveMessage);
// window features
const strWindowFeatures =
'toolbar=no, menubar=no, width=600, height=700, top=100, left=100';
if (windowObjectReference === null || windowObjectReference.closed) {
/* if the pointer to the window object in memory does not exist
or if such pointer exists but the window was closed */
windowObjectReference = window.open(url, name, strWindowFeatures);
} else if (previousUrl !== url) {
/* if the resource to load is different,
then we load it in the already opened secondary window and then
we bring such window back on top/in front of its parent window. */
windowObjectReference = window.open(url, name, strWindowFeatures);
windowObjectReference.focus();
} else {
/* else the window reference must exist and the window
is not closed; therefore, we can bring it back on top of any other
window with the focus() method. There would be no need to re-create
the window or to reload the referenced resource. */
windowObjectReference.focus();
}
// add the listener for receiving a message from the popup
window.addEventListener('message', event => receiveMessage(event), false);
// assign the previous URL
previousUrl = url;
};
const receiveMessage = event => {
// Do we trust the sender of this message? (might be different from what we originally opened, for example).
if (event.origin !== websiteHomeUrlNoSlash) {
return;
}
const { data } = event;
console.log(data); //<--- THIS WHERE I'm SEEING DUPLICATES
};
//Invoke the function
openSignInWindow(url, name);
In the popup users login to their social account and then get redirected to a page on my app where the below code is run. The code posts a message back to the opening window and then closes the popup:
// Get the message data
const messageObj = {
pluginReason: pluginReasonVar,
displayName: displayNameVar,
provider: providerVar,
};
if (window.opener) {
// send them to the opening window
window.opener.postMessage(messageObj, websiteHomeUrlNoSlash);
// close the popup
if (closePopup) {
window.close();
}
}
Everything almost works as expected. Users can login to their social accounts and all the redirections and opening and closing of the popup works fine.
The Problem:
If users go through the Social Connect process multiple times without refreshing the page, then the message data that is printed to the console is duplicated more and more each run.
For example:
On the 1st run console.log(data) is printed once. So far this works as expected.
On the 2nd run console.log(data) prints twice. It should only be printed once.
On the 3rd run console.log(data) prints three times. It should only be printed once.
Each time the Social Connect process is run it should only print once. But somehow it's adding a duplicate copy on each subsequent run.
This duplication keeps growing until the users refreshes the page, which starts the count back at one.
I want to do more data manipulation at the point of the console.log(data) but I can't do that while it's creating duplicates copies on each subsequent run.
How do I stop that from happening?
Maybe it's because the listener event is not detaching? If so, how do I fix that?

You have created an anonymous method (event) => { } as a wrapper and attached it to the addEventListener method.
window.addEventListener('message', event => receiveMessage(event), false);
It can't be removed by
window.removeEventListener('message', receiveMessage);
To fix it, make changes like this:
window.addEventListener('message', receiveMessage, false);
Meanwhile, if the method receiveMessage gets lost every time the window has been closed, it's better to move the removeEventListener part inside the receiveMessage.
const receiveMessage = (event)=> {
window.removeEventListener('message', receiveMessage);
// do something else
}

Related

How to determine whether the user closes browser tab or refreshes the page

I am building a two person game app using vue.js. The app uses vuex for state management and Firestore as the backend server.
If the user leaves the app by either closing the browser tab or navigating away, the games Firestore files need to be deleted. However, if the user refreshes the page, the Firestore files need to remain so that the reload process can repopulate the game.
So I need to determine if the user has refreshed the page as opposed to closing the browser or navigating away.
As shown below, in vue's created lifecycle I setup a "beforeunload" event Listener and also start my Firestore listeners
created() {
// This window event listener fires when the user
// navigates away from or closes the browser window
window.addEventListener("beforeunload", (event) => {
const isByRefresh = getUnloadInitiator();
if (!isByRefresh) {
this.stopFirestoreListeners("beforeunload");
}
// Cancel the event. This allows the user to cancel via popup. (for debug purposes)
event.preventDefault();
event.returnValue = "";
// the absence of a returnValue property on the event
// guarantees the browser unload happens
// delete event["returnValue"];
});
this.startFirestoreListeners("created");
},
The getUnloadInitiator function is shown below. This is where I need help. Right now all this function does is console.log various performance values.
function getUnloadInitiator() {
// check for feature support before continuing
if (performance.mark === undefined) {
console.log("performance.mark NOT supported");
return false;
}
console.log("===============================");
// Yes I know that performance.navigation is depreciated.
const nav = performance.navigation;
console.log("nav=", nav);
console.log("===============================");
// Use getEntriesByType() to just get the "navigation" events
var perfEntries = performance.getEntriesByType("navigation");
for (var i = 0; i < perfEntries.length; i++) {
var p = perfEntries[i];
console.log("= Navigation entry[" + i + "]=", p);
// other properties
console.log("type = " + p.type);
}
console.log("===============================");
performance.mark("beginLoop");
const entries = performance.getEntries({
name: "beginLoop",
entryType: "mark",
});
const firstEntry = entries[0];
console.log("firstEntry.type=", firstEntry.type);
console.log("===============================");
//TODO: Determine how unload was initiated
return true;
}
Below is the output from my console.logs. They are the same for refreshing the page, closing the browser tab, or navigating away. All show "reload" as the navigation type.
===============================
nav= PerformanceNavigation {type: 1, redirectCount: 0}
===============================
= Navigation entry[0]= PerformanceNavigationTiming {unloadEventStart: 25.399999976158142, unloadEventEnd: 25.69999998807907, domInteractive: 633, domContentLoadedEventStart: 633, domContentLoadedEventEnd: 633, …}
type = reload
===============================
firstEntry.type= reload
===============================
Any help on how to differentiate between refreshing the page, closing the browser tab, or navigating away would be appreciated. There must be away, because the native cancel browser popup I'm using for debug purposes differentiates between fresh and browser tab close.
Thanks
You can use a source of authority as persistence, be it firestore, local storage, or cookies. you are able to get the browser's tab ID with tab.id and compare it to an existing one should one exist.
browser.pageAction.show(tab.id);
Source: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Working_with_the_Tabs_API

postMessage once the window loads successfully

I am trying to implement following feature
User opens up a page 1 on which there is a button. On clicking that button another window opens up but before that program checks whether the window is already open or not if it is already open than re-open the existing window or else open the new window.
While opening a window , I am passing some data to newly / existing open window using postMessage.
Now, the problem is when I am opening a window ( if it is NOT already open) then sometimes window is not able to receive the message sent by postMessage` hence I have used the setTimeout in following code.
However, this is not a reliable solution so I need a way where in newly open window receive the message each time.
Following is the code of service worker
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
// if window is already open then just focus on window
matchingClient.focus();
// post the message
channel.postMessage({action: event.action,information:event.notification.data ? event.notification.data.info : ""});
} else {
//if window is not open yet, then open a new window
clients.openWindow(urlToOpen);
//have used setTimeout because opening a window may take time , however this is just a workaround
setTimeout(function(){
console.log("Delayed");
//post the message
channel.postMessage({action: event.action,information:event.notification.data ? event.notification.data.info : ""});
},4000);
}
});

JavaScript: calling function of parent window from child window

I am opening a popup window from a page. The process goes like this:
var newwindow = window.open("mydomain.com/a.html", "Testing", 'height=600,width=800');
if (window.focus) { newwindow.focus() }
mydomain.com/a.html opens a popup mydomain.com/b.html
mydomain.com/b.html redirects the user to another site(say payment gateway)
paymentgateway.com/authenticate.html
paymentgateway.com/authenticate.html redirects the user to mydomain.com/success.html
From mydomain.com/success.html I want to execute a function written on mydomain.com/a.html
I have written
window.parent.LaunchFunction();
window.close();
but it's not working. What can be the issue? Is it possible to achieve?
I'm offering another way - use the local storage for communication between apps at the same origin (same protocol+domain+port).
You can add listener, that is fired when somebody change any property in localstorage.
First instance is listening
window.addEventListener('storage', onStorageChange, false);
function onStorageChange (e) {
if (e.key === 'messageText') {
var messageText = localStorage.getItem('messageText');
if (messageText) { // Prevent to call when fired by `removeItem()` below
console.log('New message from another instance has come:', messageText);
localStorage.removeItem('messageText'); // this will fires the `onStorageChange` again.
}
}
}
Second instance is sending the messages
localStorage.setItem('messageText', 'Hello from second app instance');

Javascript detect closing popup loaded with another domain

I am opening a popup window and attaching an onbeforeunload event to it like this:
win = window.open("http://www.google.com", "", "width=300px,height=300px");
win.onbeforeunload = function() {
//do your stuff here
alert("Closed");
};
If I leave the URL empty, the new popup opens with "about:blank" as the address but when I close it, I see the alert.
If I open in as you see it (with an external URL), once it's closed, I cannot see the alert anymore. Any idea why this is happening?
As mentioned, same origin policy prevents Javascript from detecting such events. But there's a quite simple solution which allows you to detect closure of such windows.
Here's the JS code:
var openDialog = function(uri, name, options, closeCallback) {
var win = window.open(uri, name, options);
var interval = window.setInterval(function() {
try {
if (win == null || win.closed) {
window.clearInterval(interval);
closeCallback(win);
}
}
catch (e) {
}
}, 1000);
return win;
};
What it does: it creates new window with provided parameters and then sets the checker function with 1s interval. The function then checks if the window object is present and has its closed property set to false. If either ot these is not true, this means, that the window is (probably) closed and we should fire the 'closeCallback function' callback.
This function should work with all modern browsers. Some time ago Opera caused errors when checking properties from windows on other domains - thus the try..catch block. But I've tested it now and it seems it works quite ok.
We used this technique to create 'facebook-style' login popups for sites which doesn't support them via SDK (ehem... Twitter... ehem). This required a little bit of extra work - we couldn't get any message from Twitter itself, but the Oauth redireced us back to our domain, and then we were able to put some data in popup window object which were accessible from the opener. Then in the close callback function we parsed those data and presented the actual results.
One drawback of this method is that the callback is invoked AFTER the window has been closed. Well, this is the best I was able to achieve with cross domain policies in place.
You could listen to the 'focus' event of the opener window which fires when the user closes the popup.
Unfortunately, you're trying to communicate across domains which is prohibited by JavaScript's same origin policy. You'd have to use a server-side proxy or some other ugly hack to get around it.
You could try creating a page on your site that loads the external website in an iframe. You could then pop open that page and listen for it to unload.
I combined #ThomasZ's answer with this one to set an interval limit (didn't want to use setTimeout).
Example (in Typescript, declared anonymously so as not lose reference to "this"):
private _callMethodWithInterval = (url: string, callback: function, delay: number, repetitions: number) => {
const newWindow = window.open(url, "WIndowName", null, true);
let x = 0;
let intervalID = window.setInterval(() => {
//stops interval if newWindow closed or doesn't exist
try {
if (newWindow == null || newWindow.closed) {
console.info("window closed - interval cleared")
callback();
window.clearInterval(intervalID);
}
}
catch (e) {
console.error(`newWindow never closed or null - ${e}`)
}
//stops interval after number of intervals
if (++x === repetitions) {
console.info("max intervals reached - interval cleared")
window.clearInterval(intervalID);
}
}, delay)
}//end _callMethodWithInterval

Safari extension send message to a specific tab

Is there a way to send a message from the global page to a specific tab?
What I'm currently doing is, on tab creation the injected script creates a unique id and sends a message with this number to the global page and the global page saves this number.
If the global page needs to send some data to a tab (i.e: tab #3) then the global page will "broadcast" a message to all tabs with the number #3 as part of the data passed to the tabs (iterate over all tabs and send a message to each tab).
Is there something like Chrome: (i.e: chrome.tabs.sendRequest(tabID, {action: 'respond', params:[channel,msg,async]});)?
Right now what I'm doing is that on the injected script side, each script has a listener that will catch this message. If a content script unique number is equal to the number sent by the global page then this message is for it, else doNothing.
Is there an easier more elegant way to do this in Safari?
Within the global page's message event handler, event.target refers to the tab from which a message was received. For example:
function handleMessage(e) {
if (e.name === 'whatIsMyUrl?') {
e.target.page.dispatchMessage('yourUrlIs', e.target.url);
}
}
safari.application.addEventListener("message", handleMessage, false);
The Safari extension API doesn't have tab IDs, but you could just keep each tab in an associative array and use its index to address it later. For example:
function handleMessage(e) {
if (e.name === 'hereIsMyId') {
myTabs[e.message] = e.target;
}
}
safari.application.addEventListener("message", handleMessage, false);
// later...
myTabs[someId].page.dispatchMessage('haveSomeCake');
Safari Answer
In your global page save directly to the tab.. so for instance on message from injected script
// global page
safari.application.addEventListener("message", function(event){
switch(event.name){
case "saveData":
event.target.page.tabData = { data: myData }
break;
case "getData":
event.target.page.dispatchMessage("tabData", myData);
break;
}
}, false);
-
// injected page
// first save data
safari.self.tab.dispatchMessage("saveData", {firstname:"mike", age: 25} );
// setup listner to recevie data
safari.self.addEventListener("message", function(event){
switch(event.name){
case "tabData":
// get data for page
console.debug(event.message);
// { firstname: "mike", age: 25 }
break;
}
}, false);
// send message to trigger response
safari.self.tab.dispatchMessage("getData", {} );

Categories

Resources