Skip to content

Commit 4c027b1

Browse files
authored
Merge pull request #1412 from jetstreamapp/bug/desktop-fixes
Disable search features without valid organization and allow duplicate filenames for zip exports
2 parents 276f7df + f67bffd commit 4c027b1

File tree

36 files changed

+753
-310
lines changed

36 files changed

+753
-310
lines changed

apps/api/src/app/db/web-extension.db.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ const SELECT = Prisma.validator<Prisma.WebExtensionTokenSelect>()({
1717
user: {
1818
select: {
1919
entitlements: { select: { chromeExtension: true, googleDrive: true, recordSync: true, desktop: true } },
20+
teamMembership: {
21+
select: {
22+
status: true,
23+
team: {
24+
select: {
25+
entitlements: { select: { chromeExtension: true, googleDrive: true, recordSync: true, desktop: true } },
26+
},
27+
},
28+
},
29+
},
2030
},
2131
},
2232
type: true,

apps/api/src/app/services/external-auth.service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,16 @@ export async function verifyToken(
7575
const decodedPayload = decoder(token) as JwtDecodedPayload;
7676

7777
const userAccessToken = await webExtDb.findByAccessTokenAndDeviceId({ deviceId, token, type: webExtDb.TOKEN_TYPE_AUTH });
78+
const entitlements = userAccessToken?.user?.teamMembership?.team?.entitlements || userAccessToken?.user?.entitlements;
7879
if (!userAccessToken) {
7980
throw new InvalidAccessToken('Access token is invalid for device');
81+
} else if (userAccessToken.user.teamMembership && userAccessToken.user.teamMembership.status !== 'ACTIVE') {
82+
throw new InvalidAccessToken('User is not active');
8083
} else if (decodedPayload?.userProfile?.id !== userAccessToken.userId) {
8184
throw new InvalidAccessToken('Access token is invalid for user');
82-
} else if (audience === AUDIENCE_WEB_EXT && !userAccessToken.user.entitlements?.chromeExtension) {
85+
} else if (audience === AUDIENCE_WEB_EXT && !entitlements?.chromeExtension) {
8386
throw new InvalidAccessToken('Browser extension is not enabled');
84-
} else if (audience === AUDIENCE_DESKTOP && !userAccessToken.user.entitlements?.desktop) {
87+
} else if (audience === AUDIENCE_DESKTOP && !entitlements?.desktop) {
8588
throw new InvalidAccessToken('Desktop application is not enabled');
8689
}
8790

apps/jetstream-desktop/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "jetstream",
33
"description": "The ultimate Salesforce administrator companion.",
4-
"version": "3.1.0",
4+
"version": "3.2.0",
55
"repository": {
66
"type": "git",
77
"url": "https://github.com/jetstreamapp/jetstream"
@@ -12,6 +12,8 @@
1212
"postinstall": "electron-builder install-app-deps",
1313
"build:mac": "electron-builder build --mac --config electron-builder.config.js",
1414
"build:win": "electron-builder build --win --config electron-builder.config.js",
15+
"publish:mac:dev": "electron-builder build --mac --config electron-builder.config.js --publish always --config.publish.provider=s3 --config.publish.endpoint=http://localhost:9000 --config.publish.bucket=desktop-updates --config.publish.path=jetstream/releases",
16+
"publish:win:dev": "electron-builder build --win --config electron-builder.config.js --publish always --config.publish.provider=s3 --config.publish.endpoint=http://localhost:9000 --config.publish.bucket=desktop-updates --config.publish.path=jetstream/releases",
1517
"publish:mac": "electron-builder build --mac --config electron-builder.config.js -p always",
1618
"publish:win": "electron-builder build --win --config electron-builder.config.js -p always"
1719
},
Lines changed: 77 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
import { BrowserWindow, dialog } from 'electron';
1+
import { UpdateStatus } from '@jetstream/desktop/types';
2+
import { BrowserWindow } from 'electron';
23
import logger from 'electron-log';
34
import { autoUpdater, UpdateInfo } from 'electron-updater';
45

56
// Configure logging
67
autoUpdater.logger = logger;
78

8-
// Disable auto-download - we'll control when to download
9-
autoUpdater.autoDownload = false;
9+
// Enable auto-download - non-blocking background download
10+
autoUpdater.autoDownload = true;
1011
autoUpdater.autoInstallOnAppQuit = true;
1112

12-
function getFocusedOrFirstWindow(): BrowserWindow | null {
13-
return BrowserWindow.getFocusedWindow() || BrowserWindow.getAllWindows()[0] || null;
13+
// State management
14+
let currentUpdateStatus: UpdateStatus = { status: 'idle' };
15+
let lastCheckTime = 0;
16+
const MIN_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes between checks to prevent spam
17+
18+
function sendUpdateStatus(status: UpdateStatus) {
19+
currentUpdateStatus = status;
20+
const windows = BrowserWindow.getAllWindows();
21+
windows.forEach((window) => {
22+
window.webContents.send('update-status', status);
23+
});
24+
logger.info('Update status:', status);
1425
}
1526

1627
export function initializeAutoUpdater() {
@@ -33,103 +44,106 @@ function setupAutoUpdaterListeners() {
3344

3445
autoUpdater.on('checking-for-update', () => {
3546
logger.info('Checking for update...');
47+
sendUpdateStatus({ status: 'checking' });
3648
});
3749

3850
autoUpdater.on('update-available', (info: UpdateInfo) => {
3951
logger.info('Update available:', info);
40-
41-
const updateWindow = getFocusedOrFirstWindow();
42-
if (!updateWindow) {
43-
return;
44-
}
45-
46-
dialog
47-
.showMessageBox(updateWindow, {
48-
type: 'info',
49-
title: 'Update Available',
50-
message: `A new version ${info.version} is available. Would you like to download it now?`,
51-
detail: 'The update will be installed when you restart the application.',
52-
buttons: ['Download', 'Later'],
53-
defaultId: 0,
54-
})
55-
.then((result) => {
56-
if (result.response === 0) {
57-
autoUpdater.downloadUpdate();
58-
}
59-
});
52+
sendUpdateStatus({
53+
status: 'available',
54+
version: info.version,
55+
});
56+
// Auto-download will start automatically since autoDownload is true
6057
});
6158

6259
autoUpdater.on('update-not-available', (info) => {
6360
logger.info('Update not available', info);
61+
sendUpdateStatus({ status: 'up-to-date' });
6462
});
6563

6664
autoUpdater.on('error', (err) => {
6765
logger.error('Error in auto-updater:', err);
66+
sendUpdateStatus({
67+
status: 'error',
68+
error: err.message || 'Unknown error occurred',
69+
});
6870
});
6971

7072
autoUpdater.on('download-progress', (progressObj) => {
71-
let logMessage = 'Download speed: ' + progressObj.bytesPerSecond;
72-
logMessage = logMessage + ' - Downloaded ' + progressObj.percent + '%';
73-
logMessage = logMessage + ' (' + progressObj.transferred + '/' + progressObj.total + ')';
73+
const logMessage =
74+
`Download speed: ${progressObj.bytesPerSecond} - ` +
75+
`Downloaded ${progressObj.percent}% ` +
76+
`(${progressObj.transferred}/${progressObj.total})`;
7477
logger.info(logMessage);
7578

76-
const updateWindow = getFocusedOrFirstWindow();
77-
// Send progress to renderer
78-
if (updateWindow) {
79-
updateWindow.webContents.send('update-download-progress', progressObj);
80-
}
79+
sendUpdateStatus({
80+
status: 'downloading',
81+
downloadProgress: {
82+
percent: progressObj.percent,
83+
transferred: progressObj.transferred,
84+
total: progressObj.total,
85+
},
86+
});
8187
});
8288

8389
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
8490
logger.info('Update downloaded:', info);
85-
86-
const updateWindow = getFocusedOrFirstWindow();
87-
if (!updateWindow) {
88-
return;
89-
}
90-
91-
dialog
92-
.showMessageBox(updateWindow, {
93-
type: 'info',
94-
title: 'Update Ready',
95-
message: 'Update downloaded',
96-
detail: `Version ${info.version} has been downloaded and will be automatically installed on restart.`,
97-
buttons: ['Restart Now', 'Later'],
98-
defaultId: 0,
99-
})
100-
.then((result) => {
101-
if (result.response === 0) {
102-
// Use default behavior for NSIS installers on Windows
103-
// This ensures proper quit sequence and allows installer to complete
104-
autoUpdater.quitAndInstall();
105-
}
106-
});
91+
sendUpdateStatus({
92+
status: 'ready',
93+
version: info.version,
94+
});
10795
});
10896
}
10997

110-
export function checkForUpdates(silent = false) {
98+
export function checkForUpdates(silent = false, userInitiated = false) {
99+
// Debounce automatic checks to prevent spam
100+
if (!userInitiated && Date.now() - lastCheckTime < MIN_CHECK_INTERVAL) {
101+
logger.info('Skipping update check - too soon since last check');
102+
return;
103+
}
104+
105+
lastCheckTime = Date.now();
106+
111107
if (silent) {
112108
autoUpdater
113109
.checkForUpdates()
114110
.then((result) => {
115-
if (result?.isUpdateAvailable) {
116-
logger.info('Update available', result?.isUpdateAvailable);
111+
if (result?.updateInfo) {
112+
logger.info('Update check result:', result.updateInfo.version);
117113
}
118114
})
119115
.catch((error) => {
120-
logger.error('Update failure', error);
116+
logger.error('Update check failed:', error);
117+
sendUpdateStatus({
118+
status: 'error',
119+
error: error.message || 'Failed to check for updates',
120+
});
121121
});
122122
} else {
123+
// User-initiated check - always show feedback
123124
autoUpdater
124-
.checkForUpdatesAndNotify()
125+
.checkForUpdates()
125126
.then((result) => {
126-
if (result?.isUpdateAvailable) {
127-
logger.info('Update available', result?.isUpdateAvailable);
127+
if (result?.updateInfo) {
128+
logger.info('Update check result:', result.updateInfo.version);
128129
}
129130
})
130131
.catch((err) => {
131132
logger.error('Update check failed:', err);
132-
dialog.showErrorBox('Update Error', `Failed to check for updates: ${err.message}`);
133+
sendUpdateStatus({
134+
status: 'error',
135+
error: err.message || 'Failed to check for updates',
136+
});
133137
});
134138
}
135139
}
140+
141+
export function installUpdate() {
142+
// Use default behavior for NSIS installers on Windows
143+
// This ensures proper quit sequence and allows installer to complete
144+
autoUpdater.quitAndInstall();
145+
}
146+
147+
export function getCurrentUpdateStatus(): UpdateStatus {
148+
return currentUpdateStatus;
149+
}

apps/jetstream-desktop/src/preload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const API: ElectronAPI = {
66
onAuthenticate: (callback) => ipcRenderer.on('authenticate', (_event, payload) => callback(payload)),
77
onOrgAdded: (callback) => ipcRenderer.on('orgAdded', (_event, payload) => callback(payload)),
88
onAction: (callback) => ipcRenderer.on('action', (_event, payload) => callback(payload)),
9+
onUpdateStatus: (callback) => ipcRenderer.on('update-status', (_event, payload) => callback(payload)),
910
// One-Way from Client
1011
login: () => ipcRenderer.invoke('login'),
1112
logout: () => ipcRenderer.invoke('logout'),
@@ -16,6 +17,9 @@ const API: ElectronAPI = {
1617
getPreferences: () => ipcRenderer.invoke('getPreferences'),
1718
setPreferences: (payload) => ipcRenderer.invoke('setPreferences', payload),
1819
request: (payload) => ipcRenderer.invoke('request', payload),
20+
checkForUpdates: (userInitiated) => ipcRenderer.invoke('checkForUpdates', userInitiated),
21+
getUpdateStatus: () => ipcRenderer.invoke('getUpdateStatus'),
22+
installUpdate: () => ipcRenderer.invoke('installUpdate'),
1923
};
2024

2125
contextBridge.exposeInMainWorld('electronAPI', API);

apps/jetstream-desktop/src/services/ipc.service.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import logger from 'electron-log';
1414
import { Method } from 'tiny-request-router';
1515
import { z } from 'zod';
1616
import { ENV } from '../config/environment';
17+
import { checkForUpdates, getCurrentUpdateStatus, installUpdate } from '../config/auto-updater';
1718
import { desktopRoutes } from '../controllers/desktop.routes';
1819
import { getOrgFromHeaderOrQuery } from '../utils/route.utils';
1920
import { logout, verifyAuthToken } from './api.service';
@@ -47,6 +48,10 @@ export function registerIpc(): void {
4748
registerHandler('setPreferences', handleSetPreferences);
4849
// Handle API requests to Salesforce
4950
registerHandler('request', handleRequestEvent);
51+
// Handle auto-update requests
52+
registerHandler('checkForUpdates', handleCheckForUpdatesEvent);
53+
registerHandler('getUpdateStatus', handleGetUpdateStatusEvent);
54+
registerHandler('installUpdate', handleInstallUpdateEvent);
5055
}
5156

5257
const handleSelectFolderEvent: MainIpcHandler<'selectFolder'> = async (event) => {
@@ -257,3 +262,17 @@ const handleRequestEvent: MainIpcHandler<'request'> = async (event, { url: urlSt
257262
body: await response.json(),
258263
};
259264
};
265+
266+
const handleCheckForUpdatesEvent: MainIpcHandler<'checkForUpdates'> = async (event, userInitiated) => {
267+
checkForUpdates(false, userInitiated);
268+
// Send current status immediately
269+
event.sender.send('update-status', getCurrentUpdateStatus());
270+
};
271+
272+
const handleGetUpdateStatusEvent: MainIpcHandler<'getUpdateStatus'> = async (_event) => {
273+
return getCurrentUpdateStatus();
274+
};
275+
276+
const handleInstallUpdateEvent: MainIpcHandler<'installUpdate'> = async (_event) => {
277+
installUpdate();
278+
};

apps/jetstream-desktop/src/services/menu.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function initAppMenu() {
1717
{ role: 'about' },
1818
{
1919
label: 'Check for Updates',
20-
click: () => checkForUpdates(),
20+
click: () => checkForUpdates(false, true),
2121
},
2222
{ type: 'separator' },
2323
{ role: 'services' },
@@ -44,7 +44,7 @@ export function initAppMenu() {
4444
: [
4545
{
4646
label: 'Check for Updates',
47-
click: () => checkForUpdates(),
47+
click: () => checkForUpdates(false, true),
4848
},
4949
]) as any[]),
5050
{ type: 'separator' },

custom-typings/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable no-var */
2+
import { ElectronAPI } from '@jetstream/desktop/types';
23
import 'vite/client';
34

45
// https://webpack.js.org/loaders/worker-loader/#integrating-with-typescript
@@ -14,6 +15,6 @@ declare global {
1415
var __IS_BROWSER_EXTENSION__: boolean | undefined;
1516
var __IS_DESKTOP__: boolean | undefined;
1617
interface Window {
17-
// placeholder for any global properties
18+
electronAPI?: ElectronAPI;
1819
}
1920
}

libs/connected/connected-ui/src/lib/useDescribeMetadata.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function useDescribeMetadata(
5050

5151
const loadDescribeMetadata = useCallback(
5252
async (clearCache = false) => {
53-
if (!selectedOrg) {
53+
if (!selectedOrg?.uniqueId) {
5454
return;
5555
}
5656
const uniqueId = selectedOrg.uniqueId;

libs/connected/connected-ui/src/lib/useListMetadata.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ export function useListMetadata(selectedOrg: SalesforceOrgUi) {
289289
skipCacheIfOlderThan?: number;
290290
} = {},
291291
) => {
292-
if (!selectedOrg || !types?.length) {
292+
if (!selectedOrg?.uniqueId || !types?.length) {
293293
return;
294294
}
295295

0 commit comments

Comments
 (0)