Skip to content

Commit 818a8a4

Browse files
authored
Merge pull request Expensify#70833 from ShridharGoel/desktopLocation
Fetch macOS native location permission to improve user experience
2 parents b82c6e8 + a25e2f5 commit 818a8a4

15 files changed

Lines changed: 3378 additions & 946 deletions

.github/workflows/deploy.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,15 @@ jobs:
298298
with:
299299
IS_DESKTOP_BUILD: true
300300

301+
- name: Setup Python for node-gyp
302+
id: setup-python
303+
uses: actions/setup-python@v5
304+
with:
305+
python-version: '3.12'
306+
307+
- name: Ensure setuptools for node-gyp
308+
run: python -m pip install --upgrade pip setuptools
309+
301310
- name: Load Desktop credentials from 1Password
302311
id: load-credentials
303312
# v2
@@ -321,6 +330,7 @@ jobs:
321330
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
322331
GCP_GEOLOCATION_API_KEY: ${{ secrets.GCP_GEOLOCATION_API_KEY_PRODUCTION }}
323332
S3_BUCKET: ${{ github.ref == 'refs/heads/production' && vars.PRODUCTION_S3_BUCKET || vars.STAGING_S3_BUCKET }}
333+
PYTHON: ${{ steps.setup-python.outputs.python-path }}
324334

325335
- name: Upload desktop sourcemaps artifact
326336
# v4

.github/workflows/testBuild.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,15 @@ jobs:
185185
with:
186186
IS_DESKTOP_BUILD: true
187187

188+
- name: Setup python for node-gyp
189+
id: setup-python
190+
uses: actions/setup-python@v5
191+
with:
192+
python-version: '3.12'
193+
194+
- name: Ensure setuptools for node-gyp
195+
run: python -m pip install --upgrade pip setuptools
196+
188197
- name: Load Desktop credentials from 1Password
189198
id: load-credentials
190199
# v2
@@ -215,6 +224,7 @@ jobs:
215224
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
216225
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
217226
GCP_GEOLOCATION_API_KEY: ${{ secrets.GCP_GEOLOCATION_API_KEY_STAGING }}
227+
PYTHON: ${{ steps.setup-python.outputs.python-path }}
218228

219229
web:
220230
name: Build and deploy Web

config/electronBuilder.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ module.exports = {
4747
extraMetadata: {
4848
version,
4949
},
50+
asarUnpack: ['**/node-mac-permissions/bin/**'],
5051
mac: {
5152
category: 'public.app-category.finance',
5253
icon: macIcon[process.env.ELECTRON_ENV],
@@ -60,8 +61,11 @@ module.exports = {
6061
arch: ['universal'],
6162
},
6263
],
64+
x64ArchFiles: '**/node_modules/node-mac-permissions/bin/**',
6365
extendInfo: {
6466
CFBundleIconName: getMacBundleIconName(),
67+
NSLocationWhenInUseUsageDescription: 'This app uses location to help you track distance expenses.',
68+
NSLocationUsageDescription: 'This app uses location to help you track distance expenses.',
6569
},
6670
},
6771
dmg: {

config/webpack/webpack.desktop.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'path';
22
import type {Configuration} from 'webpack';
33
import webpack from 'webpack';
44
// eslint-disable-next-line @dword-design/import-alias/prefer-alias, import/no-relative-packages -- alias imports don't work for webpack
5-
import {dependencies as desktopDependencies} from '../../desktop/package.json';
5+
import {dependencies as desktopDependencies, optionalDependencies as desktopOptionalDependencies} from '../../desktop/package.json';
66
import type Environment from './types';
77
import getCommonConfiguration from './webpack.common';
88

@@ -38,7 +38,7 @@ const getConfiguration = (environment: Environment): Configuration[] => {
3838
},
3939
resolve: rendererConfig.resolve,
4040
plugins: [definePlugin],
41-
externals: [...Object.keys(desktopDependencies), 'fsevents'],
41+
externals: [...Object.keys(desktopDependencies), ...Object.keys(desktopOptionalDependencies), 'fsevents'],
4242
node: {
4343
/**
4444
* Disables webpack processing of __dirname and __filename, so it works like in node

desktop/ELECTRON_EVENTS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const ELECTRON_EVENTS = {
1515
DOWNLOAD_CANCELED: 'download-canceled',
1616
SILENT_UPDATE: 'silent-update',
1717
OPEN_LOCATION_SETTING: 'open-location-setting',
18+
CHECK_LOCATION_PERMISSION: 'check-location-permission',
1819
} as const;
1920

2021
export default ELECTRON_EVENTS;

desktop/contextBridge.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [
1919
ELECTRON_EVENTS.DOWNLOAD,
2020
ELECTRON_EVENTS.SILENT_UPDATE,
2121
ELECTRON_EVENTS.OPEN_LOCATION_SETTING,
22+
ELECTRON_EVENTS.CHECK_LOCATION_PERMISSION,
2223
] as const;
2324

2425
const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [

desktop/entitlements.mac.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@
66
<true/>
77
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
88
<true/>
9+
<key>com.apple.security.location</key>
10+
<true/>
911
</dict>
1012
</plist>

desktop/main.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {exec} from 'child_process';
2-
import {app, BrowserWindow, clipboard, dialog, ipcMain, Menu, shell} from 'electron';
32
import type {BaseWindow, BrowserView, MenuItem, MenuItemConstructorOptions, WebContents, WebviewTag} from 'electron';
3+
import {app, BrowserWindow, clipboard, dialog, ipcMain, Menu, shell} from 'electron';
44
import contextMenu from 'electron-context-menu';
5-
import log from 'electron-log';
65
import type {ElectronLog} from 'electron-log';
6+
import log from 'electron-log';
77
import {autoUpdater} from 'electron-updater';
8+
import type {AuthType, PermissionType} from 'node-mac-permissions';
89
import {machineId} from 'node-machine-id';
910
import checkForUpdates from '@libs/checkForUpdates';
1011
import {translate} from '@libs/Localize';
@@ -24,6 +25,19 @@ const createDownloadQueue = require<CreateDownloadQueueModule>('./createDownload
2425
const port = process.env.PORT ?? 8082;
2526
const {DESKTOP_SHORTCUT_ACCELERATOR} = CONST;
2627

28+
const MAC_PERMISSION_STATUSES = {
29+
AUTHORIZED: 'authorized',
30+
DENIED: 'denied',
31+
RESTRICTED: 'restricted',
32+
NOT_DETERMINED: 'not determined',
33+
} as const;
34+
35+
const LOCATION_PERMISSION_STATES = {
36+
GRANTED: 'granted',
37+
DENIED: 'denied',
38+
PROMPT: 'prompt',
39+
} as const;
40+
2741
// Setup google api key in process environment, we are setting it this way intentionally. It is required by the
2842
// geolocation api (window.navigator.geolocation.getCurrentPosition) to work on desktop.
2943
// Source: https://github.com/electron/electron/blob/98cd16d336f512406eee3565be1cead86514db7b/docs/api/environment-variables.md#google_api_key
@@ -374,6 +388,39 @@ const mainWindow = (): Promise<void> => {
374388
});
375389
});
376390
});
391+
392+
ipcMain.handle(ELECTRON_EVENTS.CHECK_LOCATION_PERMISSION, async () => {
393+
try {
394+
type MacPermissionsModule = {
395+
getAuthStatus?: (authType: AuthType) => PermissionType | 'not determined';
396+
};
397+
const macPermissionsModule = (await import('node-mac-permissions')) as MacPermissionsModule | {default: MacPermissionsModule};
398+
const macPermissions: MacPermissionsModule = ('default' in macPermissionsModule ? macPermissionsModule.default : macPermissionsModule) ?? {};
399+
const {getAuthStatus} = macPermissions;
400+
401+
if (!getAuthStatus || typeof getAuthStatus !== 'function') {
402+
log.warn('node-mac-permissions not available or invalid, defaulting to denied');
403+
return LOCATION_PERMISSION_STATES.DENIED;
404+
}
405+
406+
const status = getAuthStatus('location');
407+
408+
switch (status) {
409+
case MAC_PERMISSION_STATUSES.AUTHORIZED:
410+
return LOCATION_PERMISSION_STATES.GRANTED;
411+
case MAC_PERMISSION_STATUSES.DENIED:
412+
case MAC_PERMISSION_STATUSES.RESTRICTED:
413+
return LOCATION_PERMISSION_STATES.DENIED;
414+
case MAC_PERMISSION_STATUSES.NOT_DETERMINED:
415+
return LOCATION_PERMISSION_STATES.PROMPT;
416+
default:
417+
return LOCATION_PERMISSION_STATES.DENIED;
418+
}
419+
} catch (error) {
420+
log.warn('node-mac-permissions not available, defaulting to denied:', (error as Error)?.message);
421+
return LOCATION_PERMISSION_STATES.DENIED;
422+
}
423+
});
377424
/*
378425
* The default origin of our Electron app is app://- instead of https://new.expensify.com or https://staging.new.expensify.com
379426
* This causes CORS errors because the referer and origin headers are wrong and the API responds with an Access-Control-Allow-Origin that doesn't match app://-

0 commit comments

Comments
 (0)