Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use Dropbox Chooser from inside Electron app

I am using Electron (formerly Atom-Shell) to create a desktop version of an existing Angular web app. Most things work pretty well out of the box, but I have encountered some problems with the Dropbox Chooser.

My web app allows the user to import files from Dropbox using the chooser. In Electron this causes a new BrowserWindow to be created for the chooser. However the window.opener property of the new window is null, which basically makes it impossible for the Chooser window to communicate with the original window. This makes it useless because selecting a file effectively does nothing.

I know the Slack desktop app uses Electron and somehow they have been able to overcome this problem (the Dropbox Chooser does work inside Slack).

Does anyone know if/how I can use the Dropbox Chooser from inside of an Electron app?

tl;dr I can't use the Dropbox Chooser from inside an Electron app because it opens a new BrowserWindow with window.opener set to null.

like image 643
TylerJames Avatar asked Jan 14 '16 20:01

TylerJames


1 Answers

Okay, I managed to get this working. The window.opener actually gets set, one of the problems is that you don't have a proper origin in the Chooser code that will ensure the window.opener.postMessage() works and messages arrive to the parent window. There is more though.

1. Electron's BrowserWindows

Dropbox Chooser popup window works in electron's BrowserWindow only if there is nodeIntegration and webSecurity set to false. Now, these are tricky, because if you open a child window from within your existing BrowserWindow, you can't change webSecurity in the child window anymore. You can change nodeIntegration in window.open() call by passing nodeIntegration=no in the 3rd argument.

For example:

window.open('chooser-window.html', '_blank', 'resizable,scrollbars,nodeIntegration=no')

However I came up with a better solution. Opening the chooser window from main process looks more promising as I can control both these parameters (and many others as well). Furthemore, I can hack around the target/origin based communication via window.opener easily (more in step 2.). Creating a BrowserWindow without node integration makes it unable to communicate with the main process. The require method and other node's goodies are unavailable. However, you can pass a preload script to this BrowserWindow, where the node stuff is available, and you can re-expose the ipcRenderer service in order to establish a communication to main process again.

When creating a BrowserWindow from main process for purposes of Dropbox Chooser, create it like this:

const dropboxProxyWindow = new BrowserWindow({
   webPreferences: {
     nodeIntegration: false,
     webSecurity: false,
     preload: path.join(__dirname, 'dropbox-proxy-preload.js'),
   },
})

and create a dropbox-proxy-preload.js in the same directory as main.js:

// NOTE: This file preloads ipc to hidden dropbox proxy window
//       where nodeIntegration is set to false.

global.ipcRenderer = require('electron').ipcRenderer

This way, we'll have a BrowserWindow that can communicate to the main process via ipc instead of a child window that communicates with parent window via window.opener.postMessage(). This BrowserWindow will be just a helper window for our electron app, only a proxy that will ensure the actual Dropbox Chooser can communicate back to our app.

2. Dropbox Chooser button and Chooser window

From Dropbox you'll get a nice button that you can paste into your JS/HTML and everything'll work out of the box (in browser). After clicking the button a new windows opens, you select files and they will get to your callback in JS. It looks something like this:

// in HTML
<script type="text/javascript" src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="YOUR-APP-KEY"></script>

// in JS via button
var button = Dropbox.createChooseButton(options);
document.getElementById("container").appendChild(button);

// or in JS directly
Dropbox.choose(options);

The dropins.js script ensures the communication to the Chooser window works as it should. Dropbox communicates to your window by calling window.opener.postMessage() with second argument targetOrigin prefilled automatically by dropins.js script. This origin URL has to match whatever you define in your Dropbox developers app administration.

In order to port this to electron, we need to "hack" the origin that is passed to the Chooser window, since the window.location in electron's HTML files is not URL you can set in Dropbox administration. We'll do this by opening a remote HTML file in a hidden BrowserWindow that will open the Chooser. Let's call the hidden BrowserWindow a proxy window. The remote HTML will be on a domain that we'll add to Dropbox admin and it will be able to communicate with Chooser. It'll be launched with a preload script that will ensure the communication to electron's main process. From there we can send the data to our application. After loading the hidden proxy window, we'll automatically click on the button so the Chooser opens.

3. Overriding dropins.js

There is one more catch though. If we'll have our proxy window hidden, all BrowserWindows opened from there will be hidden as well. So we need to override this option. We'll do it in dropins.js, in window.open() calls, in third argument. We'll add show=1. Since dropins.js is minified by default, I used Chrome DevTools to prettify the code and then I made the required changes. Here it is in a Gist.

Final code

In /main.js

const dropboxProxyWindow = new BrowserWindow({
   webPreferences: {
     nodeIntegration: false,
     webSecurity: false,
     preload: path.join(__dirname, 'dropbox-proxy-preload.js'),
   },
   show: false,
})

const DROPBOX_CHOOSER_LINK = 'https://cdn.yourapp.com/static/dropbox-chooser.html'
dropboxProxyWindow.loadURL(DROPBOX_CHOOSER_LINK)

// NOTE: Catch data from proxy window and send to main.
ipc.on('dropbox-chooser-data', (event, data) => {
  mainWindow.webContents.send('dropbox-chooser-data', data)
})

ipc.on('dropbox-chooser-cancel', () => {
  mainWindow.webContents.send('dropbox-chooser-cancel')
})

In /dropbox-proxy-preload.js:

global.ipcRenderer = require('electron').ipcRenderer

Remote in https://cdn.yourapp.com/static/dropins.js: gist

Remote in https://cdn.yourapp.com/static/dropbox-chooser.html:

<html>
  <head>
    <title>Dropbox Chooser</title>
    <script type="text/javascript" src="dropins.js" id="dropboxjs" data-app-key="xxx"></script>
  </head>
  <body>
    <div id="container"></div>
    <script type="text/javascript">
      var options = {
        success: function(files) {
          console.debug('Files from dropbox:', files)
          if (!window.ipcRenderer || typeof window.ipcRenderer.send !== 'function') {
            console.warn('Unable to send Dropbox data to App.')
            return
          }
          window.ipcRenderer.send('dropbox-chooser-data', JSON.stringify(files))
        },

        cancel: function() {
          if (!window.ipcRenderer || typeof window.ipcRenderer.send !== 'function') {
            console.warn('Unable to send Dropbox data to App.')
            return
          }
          window.ipcRenderer.send('dropbox-chooser-cancel')
        },

        linkType: "preview",
        multiselect: true,
        folderselect: false,
      };

      var button = Dropbox.createChooseButton(options);
      document.getElementById("container").appendChild(button);
      button.click() // automatically open click on the button so the Chooser opens
    </script>
  </body>
</html>
like image 200
Jakub Žitný Avatar answered Nov 08 '22 23:11

Jakub Žitný