In my typescript which is attached to the HTML being run by my app, I am creating a new window for my settings. From what I can tell, the preloaded script is also being loaded onto the new window once it's opened, but the window isn't receiving IPC messages from the main script.
Here is the preloaded script:
const { contextBridge, ipcRenderer } = require("electron");
console.log("preloaded!");
contextBridge.exposeInMainWorld("api", {
send: (channel, data) => {
let validChannels = ["toMain", "select-dirs", "toSettings", "fromSettings"];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = ["fromMain", "toSettings", "fromSettings"];
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
});
And here is the typescript file that I've attached to the second window's HTML.
(<any>window).api.receive("toSettings", (data: any) => {
console.log(data);
})
var closeButton: HTMLButtonElement;
var settings = "";
var settignsDir = "";
document.onreadystatechange = () => {
if (document.readyState == "interactive") {
(<any>window).api.send("fromSettings", "ready")
closeButton = <HTMLButtonElement>document.getElementById("closeButton");
closeButton.addEventListener("click", () => {
(<any>window).api.send("toMain", "refresh");
self.close();
});
}
};
I'm using the same preloaded script for my renderer and using the same receiving code, it's working just fine. And from the second window's typescript, I can properly send IPC messages to the main process. But I can't receive any messages on the second window. I'm thinking I need to re-preload the file directly to the second window via the features array in window.open(). Oh and here's the code that is opening the settings window.
window.open(
"./html/settings.html",
"_blank",
"top=200,left=600,frame=false,nodeIntegration=no"
);
According to the Electron documentation, you can also include a preload in the third-string but I can't figure out how as the documentation fails to have an example and I can't find one anywhere.
In response to Kdau:
Here is the requested code:
(<any>window).api.receive("fromSettings", (data: any) => {
(<any>window).api.send("toSettings", "WHAT!");
})
I was mainly using it to see if the settings or child window as you called it was receiving the message. If you could clarify what you mean by address to the child window because I thought that the preload script should be automatically parsing, "ok so this channel needs to go to this receiver".
I would like to point out that in the code snippet that you included returns this error:
Argument of type '({ url }: HandlerDetails) => { frame: boolean; webPreferences: { nodeIntegration: boolean; preload: string; }; } | undefined' is not assignable to parameter of type '(details: HandlerDetails) => { action: "deny"; } | { action: "allow"; overrideBrowserWindowOptions?: BrowserWindowConstructorOptions | undefined; }'.
Type '{ frame: boolean; webPreferences: { nodeIntegration: boolean; preload: string; }; } | undefined' is not assignable to type '{ action: "deny"; } | { action: "allow"; overrideBrowserWindowOptions?: BrowserWindowConstructorOptions | undefined; }'.
Type 'undefined' is not assignable to type '{ action: "deny"; } | { action: "allow"; overrideBrowserWindowOptions?: BrowserWindowConstructorOptions | undefined; }'.
I don't know what to do with this.
While you can pass a preload in the third argument, the more flexible and reliable approach is to use setWindowOpenHandler. So, just after constructing your main window (assuming the variable is mainWindow):
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url === './html/settings.html') {
return {
action: 'allow',
overrideBrowserWindowOptions: {
frame: false,
webPreferences: {
nodeIntegration: false,
preload: 'my-child-window-preload-script.js'
}
}
}
}
})
However, most (all??) options are inherited from the parent window anyway, and I would imagine that your preload has been inherited and that is why you can send messages from the child window.
As for not receiving messages in the child window, the messages aren't being addressed to that window's webContents by your code. Sending to the main window's webContents won't work. Unfortunately, the IPC in Electron isn't a broadcast system where every process receives every message filtered only by channel. The channel feature sits on top of process-based targeting. Each render process can only send messages to the main process, and the main process must address one specific render process at a time when sending.
So, to communicate between two render processes, use the main process as a relay. In the main process, to get the webContents of a window that was opened through window.open, you can listen for a message from that process (such as one you send as soon as it loads) and/or call webContents.getAllWebContents() and iterate through to find the right one.
Related
I'm updating an entire app written a few years ago using Electron v1.8.8. As far as I know, Electron changed its paradigm and now the default expected way of communicating between the main and renderer process is using a module in the middle named preload.js.
To get/set global variables in the old way you would first require the remote module:
var { remote } = require('electron');
And then getting/setting it like so:
remote.getGlobal('sharedObj').get('user')
I've been trying to expose an api object in preload.js to achieve the same old functionality, but with no success.
How I would achieve this without setting nodeIntegration: true, contextIsolation: false?
To send messages between the main thread and a render thread requires the understanding of Inter-Process Communication and Context Isolation.
You are correct in that preload.js acts like a middle man. From my experience, using preload.js as a script whose sole purpose is to communicate data between the main and render threads via the use of defined channels allows for a simple, easy to read, separation of concern. IE: Don't place domain specific functions within your preload.js script. Only add functions that are used for the purpose of performing specific methods of communication.
Below is a typical preload.js file. I have defined a couple of channels to communicate between your main thread and your render thread. PS: You can use any name / naming convention to name your channels.
preload.js (main thread)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [
'renderThread:saysHi'
],
// From main to render.
'receive': [
'mainThread:saysHi'
],
// From render to main and back again.
'sendReceive': []
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
Whilst your main.js file may look a little different than that shown below, the main points of interest are the reception and transmission of the defined channels renderThread:saysHi and mainThread:saysHi.
main.js (main thread)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require("path");
let appWindow;
function createAppWindow() {
const appWindow = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
fullscreen: false,
resizable: true,
movable: true,
minimizable: true,
maximizable: true,
enableLargerThanScreen: true,
closable: true,
focusable: true,
fullscreenable: true,
frame: true,
hasShadow: true,
backgroundColor: '#fff',
show: false,
icon: nodePath.join(__dirname, 'icon.png'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
worldSafeExecuteJavaScript: true,
enableRemoteModule: false,
devTools: (! electronApp.isPackaged),
preload: nodePath.join(__dirname, 'preload.js')
}
});
appWindow.loadFile('index.html')
.then(() => {
// Main thread saying hi to the render thread.
appWindow.webContents.send('mainThread:saysHi', 'Hello from the main thread.'); })
.then(() => {
appWindow.show(); })
return appWindow;
}
// Listen for the render thread saying hi.
electronIpcMain.on('renderThread:saysHi', (event, message) => {
console.log(message); });
}
electronApp.on('ready', () => {
appWindow = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
Your index.html file will import your Javascript file which would contain the code to listen out and send messages on the specified channels.
index.html (render thread)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="main-thread-message"></div>
<input type="button" id="render-thread-button" value="Click to say hi to the main thread">
</body>
<script type="module" src="index.js"></script>
</html>
index.js (render thread)
let mainThreadMessage = document.getElementById('main-thread-message');
let renderThreadButton = document.getElementById('render-thread-button');
// IIFE - Immediately Invoke Function Expression
(function() {
window.ipcRender.receive('mainThread:saysHi', (message) => {
mainThreadMessage.textContent = message;
});
renderThreadButton.addEventLister('click', () => {
window.ipcRender.send('renderThread:saysHi', 'Hello from the render thread.');
});
})();
In the above code, the global object window contains ipcRender.send and ipcRender.receive which is the structure used in your preload.js script underneath the line contextBridge.exposeInMainWorld. You can rename ipcRender and receive / send / invoke to anything you like, though if you do, you would also use the same reference when using it 'html js' side.
I'm making a text editor with electron js, and once the user presses ctrl + s, I want the file to be saved as a txt file. The thing is though, I can't seem to find a way to directly access the div that holds the text. I've tried using a preload, but that only works once the program is run. How can I hold the element as a variable?
Heres the main javascript code:
const { app, BrowserWindow, globalShortcut } = require('electron');
const path = require('path');
// Create the main window
const createWindow = () => {
// Adjust a few settings
const win = new BrowserWindow({
// What the height and width that you open up to
width: 500,
height: 600,
// Minimun width and height
minWidth: 400,
minHeight: 400,
icon: __dirname + '/icon.png',
// Change the window title
title: "text editor",
webPreferences: {
// Preload so that the javascript can access the text you write
preload: path.join(__dirname, 'preload.js'),
}
});
win.loadFile('index.html');
// Remove that ugly title bar and remove unnecessary keyboard shortcuts
win.removeMenu();
}
// Create window on ready so that no nasty errors happen
app.whenReady().then(() => {
createWindow();
});
app.whenReady().then(() => {
// Global shortcut so the user has the ablitiy to exit
globalShortcut.register('ctrl+e', () => {
console.log("exiting...");
app.exit();
});
globalShortcut.register('ctrl+s', () => {
console.log("saving...");
});
})
// when all windows close this app actually closes
app.on('window-all-closed', () => {
if (process !== 'darwin') app.quit();
})
To get the innerText (or equivalent) of the div element in your index.html window, you will need to send a message to your render thread requesting this information. Following this, you will then need your render thread to send the innerText back to your main thread for processing (saving).
Electron's Inter-Process Communication can be confusing at times but if implemented correctly it can be simple and safe.
To learn more about the processes involved you will want to read and try and understand the following links:
ipcMain.on()
webContents.send()
Context Isolation
contextBridge
Let's begin with building out your html document first. At a minimum it must include an editable <div> tag and a 'save' button.
index.html (render thread)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Test Editor</title>
<style>
#editor {
width: 50vw;
height: 50vh;
}
<style>
</head>
<body>
<div id="content" contenteditable="true"></div>
<input type="button" id="save" value="Save">
</body>
<script src="script.js"></script>
</html>
See Example: A simple but complete rich text editor for some cool ideas.
Now let's add the 'save' button and IPC message functionality.
script.js (render thread)
// IIFE (Immediately Invoked Function Expression)
(function() => {
let content = document.getElemetById('content').innerText;
document.getElementById('save').addEventListener('click', saveContent(content));
window.ipcRender.receive('editor:getContent', () => { saveContent(content); });
});
function saveContent(content) {
window.ipcRender.send('editor:saveContent', content);
}
Here is your main.js file with the following updates.
Add Electron's ipcMain module.
Add the win object to the top scope so it is noit garbage collected.
Listen for message(s) from the render thread (using an IFFE).
Add the saveContent() function (to be fully fleshed out by you).
Remove const from the new BrowserWindow line.
Return win from the createWindow() function so it can be referenced later on.
Update the globalShortcut ctrl+s function.
main.js (main thread)
const { app, BrowserWindow, globalShortcut, ipcMain } = require('electron');
const path = require('path');
let win = null;
// IIFE (Immediately Invoked Function Expression)
(function() => {
ipcMain.on('editor:saveContent', (event, content) => { saveContent(content); });
})();
function saveContent(content) {
console.log("saving...");
// Save content...
console.log("saved...");
}
// Create the main window
function createWindow() {
// Adjust a few settings
win = new BrowserWindow({
// What the height and width that you open up to
width: 500,
height: 600,
// Minimun width and height
minWidth: 400,
minHeight: 400,
icon: __dirname + '/icon.png',
// Change the window title
title: "text editor",
webPreferences: {
// Preload so that the javascript can access the text you write
preload: path.join(__dirname, 'preload.js'),
}
});
win.loadFile('index.html');
// Remove that ugly title bar and remove unnecessary keyboard shortcuts
win.removeMenu();
return win;
}
// Create window on ready so that no nasty errors happen
app.on('ready', () => {
// Create the window.
win = createWindow();
// Global shortcut so the user has the ability to exit
globalShortcut.register('ctrl+e', () => {
console.log("exiting...");
app.exit();
});
// Global shortcut to save editable content.
globalShortcut.register('ctrl+s', () => {
console.log('ctrl+s pressed.');
win.webContents.send('editor:getContent');
});
})
// when all windows close this app actually closes
app.on('window-all-closed', () => {
if (process !== 'darwin') app.quit();
})
Note that I have left the actual saving to the filesystem functionality to you. See Node.js: fs.writeFile() for more information.
Ok, the last piece of the puzzle is a working preload.js script. This is the script that grants the use of a list of whitelisted channels between the main and render threads.
In here we add the editor:saveContent and editor:getContent channel names.
preload.js (main thread)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [
'editor:saveContent'
],
// From main to render.
'receive': [
'editor:getContent'
],
// From render to main and back again.
'sendReceive': []
}
};
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
Note that I do not perform any functions so-to-speak in the preload script. I only manage a list
of channel names and the transfer of any data associated with those channel names.
So, basically I'm trying to receive a call from provider to my app. For that purpose Quickblox gives us a listener to receive the upcoming calls onCallListener. So here is my code snippet that should work but doesn't.
const calleesIds = [4104]
const sessionType = QB.webrtc.CallType.VIDEO
const additionalOptions = {}
let callSession = QB.webrtc.createNewSession(calleesIds, sessionType, null, additionalOptions)
console.log(callSession, "SESSION")
const mediaParams = {
audio: true,
video: true,
options: {
muted: true,
mirror: true,
},
elemId: "myVideoStream"
}
QB.webrtc.onCallListener = function(session: any, extension: object) {
callSession = session
console.log('asdasd')
// if you are going to take a call
session.getUserMedia(mediaParams, function (error: object, stream: object) {
if (error) {
console.error(error)
} else {
session.accept(extension)
session.attachMediaStream("videoStream", stream)
}
})
}
P.S. I also integrated chat which works perfect!
Found the solution by myself! Whenever you create a user and dialog id, search that user in the quickblox dashboard by the dialogId and change its settings: you will see that userId and providerId is the same which is wrong. So put your userId in the userId field and save that. After that you video calling listeners will work fine!)
P. S. also in the backend replace provider token with user token.
I have a package (Let's say PACKAGE_A) written to do some tasks. Then it is required by PACKAGE_B. PACKAGE_A is a node script for some automation work. It has this Notifier module to create and export an EventEmitter. (The Whole project is a Monorepo)
const EventEmitter = require('events');
let myNotifier = new EventEmitter();
module.exports = myNotifier;
So in some functions in PACKAGE_A it emits event by requiring myNotifier, and also in the index.js of PACKAGE_A, I export functions (API exposed to the other packages) and the myNotifier by requiring it again.
const myNotifier = require('./myNotifier);
const func1 = () => {
// some function
return something;
}
module.exports = {func1, myNotifier}
Then I import the PACKAGE_A in PACKAGE_B and use the API functions exposed with the notifier. PACKAGE_B is an electron app with a React UI.
Below is how the program works.
I have a console output window in the electron app (React UI, UI_A). <= (keep this in mind)
When I click a button in UI_A it fires a redux action (button_action). Inside the action, a notification is sent to an event which is listened in the electron code using ipcRenderer.
ipcRenderer.send('button-clicked', data); // <= this is not the full code of the action. It's bellow.
Then in the electron code (index.js), I require another file (UI_A_COM.js which houses the code related to UI_A in electron side). The reason is code separation. Here's part of the code in index.js related to the electron.
const ui_a_com = require('./electron/UI_A_COM');
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
resizable: false,
});
mainWindow.loadURL('http://localhost:3000');
const mainMenu = Menu.buildFromTemplate(menuTemplate);
ui_a_com (mainWindow);
};
Alright. Then in UI_A_COM.js, I listen to that triggered event button-clicked.
ipcMain.on('button-clicked', someFunction);
which runs the code from PACKAGE_A and return a result. So now when PACKAGE_A runs, it emits some events using myNotifier. I listen to them in the same file (UI_A_COM.js), and when those events are captured, I again send some events to React UI, which is subscribed when button_action fired.
myNotifier.on('pac_a_event_a', msg => {
mainWindow.webContents.send('ui_event_a', msg); // code in `UI_A_COM.js`
});
Here's the full code for the action. (Did not provide earlier because you'll get confused)
export const buttonAction = runs => {
return dispatch => {
ipcRenderer.send('button-clicked', data);
ipcRenderer.on('ui_event_a', (event, msg) => {
dispatch({ type: SOME_TYPE, payload: { type: msg } });
});
};
};
This will show the msg in the UI_A console.
So this is the task I'm doing. The problem is when I click the button; it works perfectly for the first time. But when I click the button on the second time, it received two messages. Then when I click the button again, three messages and it keeps growing. (but the functions in the PACKAGE_A only executes one time per button press).
Let's say the message from PACKAGE_A emitted is 'Hello there' per execution.
When I press the button 1st time a perfect result => Hello there, When I click the button again => Hello there Hello there, When I click it again => Hello there Hello there Hello there.
It's kept so on. I think my implementation of EventEmitter has some flows. So why it's happening like this? Is it EventEmitter or something else? What am I doing wrong here?
By default the electron-react boilerplate doesnt define the ipcRenderer.removeAllListeners method. So you have to first go to the main/preloads.ts file and add them :
removeListener(channel: string, func: (...args: unknown[]) => void) {
ipcRenderer.removeListener(channel, (_event, ...args) => func(...args));
},
removeAllListeners(channel: string) {
ipcRenderer.removeAllListeners(channel);
},
Then go to the renderer/preload.t.s declaration file and add the declarations too:
removeListener(
channel: string,
func: (...args: unknown[]) => void
): void;
removeAllListeners(channel: string): void;
After that make sure to clean all listeners in the cleanup function of your useEffects each time you listen to an event fired. This will prevent multiple firing.
useEffect(() => {
window.electron.ipcRenderer.on('myChannel', (event, arg) => {
// do stuffs
});
return () => {
window.electron.ipcRenderer.removeAllListeners('myChannel');
};
});
I think you should return a function that call ipcRenderer.removeAllListeners() in your component's useEffect().
Because every time you click your custom button, the ipcRenderer.on(channel, listener) is called, so you set a listener to that channel agin and agin...
Example:
useEffect(() => {
electron.ipcRenderer.on('myChannel', (event, arg) => {
dispatch({ type: arg });
});
return () => {
electron.ipcRenderer.removeAllListeners('myChannel');
};
});
I'm interested in how to implement OAuth in React using popup (window.open).
For example I have:
mysite.com — this is where I open the popup.
passport.mysite.com/oauth/authorize — popup.
The main question is how to create connection between window.open (popup) and window.opener (as it's known the window.opener is null due to cross-domain security therefore we can't use it anymore).
⇑ window.opener is removed whenever you navigate to a different host (for security reasons), there is no way around it. The only option should be doing the payment in a frame if it is possible. The top document needs to stay on the same host.
Scheme:
Possible solutions:
Check an opened window using setInterval described here.
Using cross-storage (not worth it imho ).
So what's the best recommended approach in 2019?
Wrapper for React - https://github.com/Ramshackle-Jamathon/react-oauth-popup
Suggested by Khanh TO. OAuth popup with localStorage. Based on react-oauth-popup.
Scheme:
Code:
oauth-popup.tsx:
import React, {PureComponent, ReactChild} from 'react'
type Props = {
width: number,
height: number,
url: string,
title: string,
onClose: () => any,
onCode: (params: any) => any,
children?: ReactChild,
}
export default class OauthPopup extends PureComponent<Props> {
static defaultProps = {
onClose: () => {},
width: 500,
height: 500,
url: "",
title: ""
};
externalWindow: any;
codeCheck: any;
componentWillUnmount() {
if (this.externalWindow) {
this.externalWindow.close();
}
}
createPopup = () => {
const {url, title, width, height, onCode} = this.props;
const left = window.screenX + (window.outerWidth - width) / 2;
const top = window.screenY + (window.outerHeight - height) / 2.5;
const windowFeatures = `toolbar=0,scrollbars=1,status=1,resizable=0,location=1,menuBar=0,width=${width},height=${height},top=${top},left=${left}`;
this.externalWindow = window.open(
url,
title,
windowFeatures
);
const storageListener = () => {
try {
if (localStorage.getItem('code')) {
onCode(localStorage.getItem('code'));
this.externalWindow.close();
window.removeEventListener('storage', storageListener);
}
} catch (e) {
window.removeEventListener('storage', storageListener);
}
}
window.addEventListener('storage', storageListener);
this.externalWindow.addEventListener('beforeunload', () => {
this.props.onClose()
}, false);
};
render() {
return (
<div onClick={this.createPopup)}>
{this.props.children}
</div>
);
}
}
app.tsx
import React, {FC} from 'react'
const onCode = async (): Promise<undefined> => {
try {
const res = await <your_fetch>
} catch (e) {
console.error(e);
} finally {
window.localStorage.removeItem('code'); //remove code from localStorage
}
}
const App: FC = () => (
<OAuthPopup
url={<your_url>}
onCode={onCode}
onClose={() => console.log('closed')}
title="<your_title>">
<button type="button">Enter</button>
</OAuthPopup>
);
export default App;
I once encounter an issue on my oauth login flow with window.open/window.opener bug on ms-edge
My flow before this issue was
On login button click open a popup
After successful login the oauth app redirect to my domain's page
Then i call a function of the parent window from with in the popup (window.opener.fn) with data from oauth response and the parent window then close the child popup window
My flow after this issue was
On login button click open a popup
Create a setinterval in case (window.opener is undefined)
After successful login the oauth app redirect to my domain's page
Check if window.opener is available then do #3 from the above flow and clearInterval
If window.opener is not available then since i am on my domains page i try to set localstorage and try to read the localstorage from inside the setInterval function in parent window then clear the localstorage and setInterval and proceed.
(for backward compatibility) If localstorage is also not available then set a client side cookie with the data with a short expiry (5-10 sec) time and try to read the cookie (document.cookie) inside the setInterval function in parent window and proceed.