The New Expensify desktop app is built using Electron.js. We try our best to maintain Electron best practices, particularly when it comes to security.
The desktop app is organized in three pieces:
- The Electron main process
- Implemented in https://github.com/Expensify/App/blob/main/desktop/main.js.
- This file has access to the full set of Electron and Node.JS APIs.
- The Electron renderer process
- This is the webpack-bundled version of our react-native-web app (except using
index.desktop.js
files instead ofindex.website.js
, where applicable) - This is very similar to our web app, and code in this process should assume it will be run in the context of a browser (no access to
require
, Electron, or Node.js APis)
- This is the webpack-bundled version of our react-native-web app (except using
- The context bridge
- Implemented in https://github.com/Expensify/App/blob/main/desktop/contextBridge.js
- The context bridge enables communication between the main and renderer processes. For example, if the renderer process needs to make use of a Node.js or Electron API it must:
- Define an event in https://github.com/Expensify/App/blob/main/desktop/ELECTRON_EVENTS.js
- Add that event to the whitelist defined in the context bridge
- Set up a handler for the event in the main process that can respond to the renderer process back through the bridge, if necessary.
Testing the auto-update process can be a little involved. The most effective way to test this involves setting up your own release channel locally and making sure that you can notarize your builds.
Note: In order to test with a notarized build you'll need to have a paid Apple developer account.
You can inspect auto update related logs in the log file at ~/Library/Logs/new.expensify.desktop/main.log
Rather than pushing new builds to the production S3 bucket, the best way to test locally is to use Min.IO. Min.IO is an S3-compatible service that you can set up and deploy locally. In order to set up a local Min.IO instance to emulate an S3 bucket, follow these steps:
If you've already gone through the setup below, you can just run step 3 and then move on to the next section.
- Install Min.IO on your local machine via brew
brew install minio/stable/minio
brew install minio/stable/mc
- Create a directory in the root for electron-updater data
mkdir -p ~/data/electron-updater
- Start the local server. Take note of the RootUser and RootPass values that show up after running this command, we'll need these in the next step
minio server ~/data/
- Verify that the Min.IO local server is up-and-running by going to localhost:9000 in your browser, and logging in with the
RootUser
andRootPass
. You should then see an interface like this: - Create a testing bucket with mc (minio client)
mc config host add electron-builder http://YOUR_LOCAL_IP:9000 [RootUser_value] [RootPass_value]
mc mb electron-builder/electron-builder
-
Verify that the new bucket has been created by navigating to https://localhost:9000/minio/electron-builder in your browser and confriming you see an interface like this:
-
Set your testing bucket to be public, which will allow the NewExpensify.dmg access the local latest-mac.yml file we'll be publishing
mc policy set public electron-builder/electron-builder
Note: while the electron-updater
docs tell you to create a file named dev-app-update.yaml
, this will not be helpful. Setting that file will, in development, tell the auto-updater where to look for builds. Unfortunately, on Mac the auto-updater will not install the new build unless the app that is currently running is signed.
Once you have Min.IO setup and running, the next step is to temporarily revert some changes from https://github.com/Expensify/App/commit/b640b3010fd7a40783d1c04faf4489836e98038d, specifically
- Update the
desktop-build
command in package.json to add--publish always
at the end - Update electronBuilder.config.js to replace the
publish
value with the following:
publish: [{
provider: 's3',
bucket: 'electron-builder',
endpoint: 'http://localhost:9000',
acl: 'public-read',
channel: 'latest',
}]
If you've already created a Certificate Signing Request and an app-specific password for your local desktop testing app, you can continue to the next section.
Before you can upload a build, you need to make sure that you can notarize builds. For this you will need an Apple Developer account. Go to the Certificates, Identifiers, and Profiles page and create a new certificate for a Developer ID Application
(see the bottom option of the screenshot below)
Follow the instructions to create a Certificate Signing Request, and once the certificate has been created, add it to your keychain with the Keychain Access app.
You will need to pass your Apple ID (username) and an app-specific password to the environment of the local desktop build. Entering your normal password will not work, so generate an app-specific password before continuing. Make sure you write down the app-specific password since you'll need to pass it to the desktop-build command.
Now that your credentials have been set up properly, you can push a build to Min.IO. Start by updating the version value in package.json to be much higher than it is currently (1.0.0-0 -> 2.0.0-0) so that the uploaded version is always higher than the version you're testing on. Then run the following, where RootUserKey and RootPassKey are the RootUser and RootPass values from step 3:
AWS_ACCESS_KEY_ID=RootUserKey AWS_SECRET_ACCESS_KEY=RootPassKey APPLE_ID=YOUR_APPLE_ID APPLE_ID_PASSWORD=YOUR_APP_SPECIFIC_PW npm run desktop-build
This command will create a build, notarize it, and push your build to the server. Note that it can take around 10 minutes for the command to complete.
Once the command finishes, revert the version update in package.json
, remove --publish always
from the desktop-build
command, and again run the npm run desktop-build
command above including the args. After the build is done, you'll find NewExpensify.dmg
in the dist/
folder in the root of the project. Open the .dmg
and install the app. Your app will attempt to auto-update in the background.
To avoid bundling unnecessary node_modules
we use a 2 package structure
The root package.json serves for devDependencies
and shared (renderer) dependencies
The desktop/package.json serves for desktop (electron-main) specific dependencies
We use Webpack with a desktop specific config to bundle our js code
Half of the config takes care of packaging root package dependencies - everything related to rendering App in the Electron window. Packaged under dist/www
The other half is about bundling the main.js
script which initializes Electron and renders www
If you suspect unnecessary items might be getting packaged you can inspect the package content in desktop-build/
The app content (dist/www
) is archived under /New\ Expensify.app/Contents/Resources/app.asar
To see the actual app.asar
content run the following script
npx asar extract desktop-build/mac/New\ Expensify.app/Contents/Resources/app.asar ./unpacked-asar
The expected size of app.asar
= desktop/dist/www/
+ desktop/node_modules/
;
main
process logs are written to~/Library/Logs/new.expensify.desktop/main.log
renderer
logs can be observed live in the developer console (⌘ Cmd + ⌥ Option + I)