From b29d70a9dbdff2fddd8e588c2026d16c3131d64c Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Thu, 19 Mar 2026 10:25:58 -0500 Subject: [PATCH 1/4] feat(update): add update banner and functionality for version checks --- src/apps/main/interface.d.ts | 2 + src/apps/main/main.ts | 21 ++++- src/apps/main/preload.d.ts | 2 + src/apps/main/preload.js | 9 ++ src/apps/renderer/localize/locales/en.json | 4 + src/apps/renderer/localize/locales/es.json | 4 + src/apps/renderer/localize/locales/fr.json | 4 + .../Banners/UpdateAvailable.test.tsx | 85 +++++++++++++++++ .../InfoBanners/Banners/UpdateAvailable.tsx | 52 +++++++++++ .../pages/Widget/InfoBanners/InfoBanners.tsx | 2 + .../check-for-updates-on-deb.test.ts | 91 +++++++++++++++++++ .../auto-update/check-for-updates-on-deb.ts | 50 ++++++++++ 12 files changed, 321 insertions(+), 5 deletions(-) create mode 100644 src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.test.tsx create mode 100644 src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx create mode 100644 src/core/auto-update/check-for-updates-on-deb.test.ts create mode 100644 src/core/auto-update/check-for-updates-on-deb.ts diff --git a/src/apps/main/interface.d.ts b/src/apps/main/interface.d.ts index e8c662943..7f68acf40 100644 --- a/src/apps/main/interface.d.ts +++ b/src/apps/main/interface.d.ts @@ -131,6 +131,8 @@ export interface IElectronAPI { getBackupFatalErrors(): Promise>; onBackupProgress(func: (value: number) => void): () => void; startRemoteSync(): Promise; + getUpdateStatus(): Promise<{ version: string } | null>; + onUpdateAvailable(callback: (info: { version: string }) => void): () => void; } declare global { diff --git a/src/apps/main/main.ts b/src/apps/main/main.ts index d4bdee974..50573f5b9 100644 --- a/src/apps/main/main.ts +++ b/src/apps/main/main.ts @@ -45,6 +45,7 @@ import { getOrCreateWidged, getWidget, setBoundsOfWidgetByPath } from './windows import { createAuthWindow, getAuthWindow } from './windows/auth'; import configStore from './config'; import { getTray, setTrayStatus } from './tray/tray'; +import { broadcastToWindows } from './windows'; import { openOnboardingWindow } from './windows/onboarding'; import { setupThemeListener, getTheme } from '../../core/theme'; import { installNautilusExtension } from './nautilus-extension/install'; @@ -63,6 +64,7 @@ import { version, release } from 'node:os'; import { INTERNXT_VERSION } from '../../core/utils/utils'; import { registerBackupHandlers } from '../../backend/features/backup/register-backup-handlers'; import { startBackupsIfAvailable } from '../../backend/features/backup/start-backups-if-available'; +import { checkForUpdatesOnDeb } from '../../core/auto-update/check-for-updates-on-deb'; const gotTheLock = app.requestSingleInstanceLock(); app.setAsDefaultProtocolClient('internxt'); @@ -82,7 +84,18 @@ logger.debug({ osRelease: release(), }); -function checkForUpdates() { +let pendingUpdateInfo: { version: string } | null = null; + +async function checkForUpdates() { + if (!process.env.APPIMAGE) { + const updateInfo = await checkForUpdatesOnDeb({ currentVersion: INTERNXT_VERSION }); + if (updateInfo) { + pendingUpdateInfo = updateInfo; + broadcastToWindows('update-available', updateInfo); + } + return; + } + try { autoUpdater.logger = { debug: (msg) => logger.debug({ msg: `AutoUpdater: ${msg}` }), @@ -96,9 +109,7 @@ function checkForUpdates() { } } -if (process.platform === 'darwin') { - app.dock.hide(); -} +ipcMain.handle('get-update-status', () => pendingUpdateInfo); if (process.env.NODE_ENV === 'production') { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -133,7 +144,7 @@ app setTrayStatus('IDLE'); } - checkForUpdates(); + await checkForUpdates(); registerAvailableUserProductsHandlers(); }) .catch((exc) => logger.error({ msg: 'Error starting app', exc })); diff --git a/src/apps/main/preload.d.ts b/src/apps/main/preload.d.ts index e74f594ef..4dd88b448 100644 --- a/src/apps/main/preload.d.ts +++ b/src/apps/main/preload.d.ts @@ -214,5 +214,7 @@ declare interface Window { ) => () => void; getDiskSpace: () => Promise; }; + getUpdateStatus(): Promise<{ version: string } | null>; + onUpdateAvailable(callback: (info: { version: string }) => void): () => void; }; } diff --git a/src/apps/main/preload.js b/src/apps/main/preload.js index 082fb5d34..ab7601b0d 100644 --- a/src/apps/main/preload.js +++ b/src/apps/main/preload.js @@ -407,4 +407,13 @@ contextBridge.exposeInMainWorld('electron', { }, getDiskSpace: () => ipcRenderer.invoke('cleaner:get-disk-space'), }, + getUpdateStatus() { + return ipcRenderer.invoke('get-update-status'); + }, + onUpdateAvailable(callback) { + const eventName = 'update-available'; + const callbackWrapper = (_, info) => callback(info); + ipcRenderer.on(eventName, callbackWrapper); + return () => ipcRenderer.removeListener(eventName, callbackWrapper); + }, }); diff --git a/src/apps/renderer/localize/locales/en.json b/src/apps/renderer/localize/locales/en.json index 0e47b6080..e3b6a4e0b 100644 --- a/src/apps/renderer/localize/locales/en.json +++ b/src/apps/renderer/localize/locales/en.json @@ -211,6 +211,10 @@ "button": "Mount" }, "banners": { + "update-available": { + "body": "A new version of Internxt is available.", + "action": "Download update" + }, "discover-backups": { "title": "INTERNXT BACKUPS", "body": "Keep a lifesaver copy of your most important folders and files.", diff --git a/src/apps/renderer/localize/locales/es.json b/src/apps/renderer/localize/locales/es.json index 929ffc4ba..ae70b8e7a 100644 --- a/src/apps/renderer/localize/locales/es.json +++ b/src/apps/renderer/localize/locales/es.json @@ -211,6 +211,10 @@ "button": "Montar" }, "banners": { + "update-available": { + "body": "Hay una nueva versión de Internxt disponible.", + "action": "Descargar actualización" + }, "discover-backups": { "title": " COPIAS DE SEGURIDAD DE INTERNXT", "body": "Mantén una copia de seguridad de tus carpetas y archivos más importantes.", diff --git a/src/apps/renderer/localize/locales/fr.json b/src/apps/renderer/localize/locales/fr.json index 15292d6bf..183263516 100644 --- a/src/apps/renderer/localize/locales/fr.json +++ b/src/apps/renderer/localize/locales/fr.json @@ -207,6 +207,10 @@ "button": "Monter" }, "banners": { + "update-available": { + "body": "Une nouvelle version d'Internxt est disponible.", + "action": "Télécharger la mise à jour" + }, "discover-backups": { "title": "INTERNXT SAUVEGARDES", "body": "Gardez une copie de secours de vos dossiers et fichiers les plus importants.", diff --git a/src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.test.tsx b/src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.test.tsx new file mode 100644 index 000000000..eb81545a2 --- /dev/null +++ b/src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.test.tsx @@ -0,0 +1,85 @@ +import { act, render, screen, fireEvent } from '@testing-library/react'; +import { UpdateAvailable } from './UpdateAvailable'; + +describe('UpdateAvailable', () => { + beforeEach(() => { + vi.mocked(window.electron.getUpdateStatus).mockResolvedValue(null); + }); + + it('renders nothing when no update is available', () => { + vi.mocked(window.electron.onUpdateAvailable).mockReturnValue(vi.fn()); + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('renders the banner when an update is available', () => { + vi.mocked(window.electron.onUpdateAvailable).mockImplementation((callback) => { + callback({ version: '2.6.0' }); + return vi.fn(); + }); + + render(); + + expect(screen.getByText('widget.banners.update-available.body')).toBeInTheDocument(); + expect(screen.getByText('widget.banners.update-available.action')).toBeInTheDocument(); + }); + + it('opens the download page when the action link is clicked', () => { + vi.mocked(window.electron.onUpdateAvailable).mockImplementation((callback) => { + callback({ version: '2.6.0' }); + return vi.fn(); + }); + + render(); + + fireEvent.click(screen.getByText('widget.banners.update-available.action')); + + expect(window.electron.openUrl).toBeCalledWith('https://github.com/internxt/drive-desktop-linux/releases/'); + }); + + it('dismisses the banner when the X button is clicked', () => { + vi.mocked(window.electron.onUpdateAvailable).mockImplementation((callback) => { + callback({ version: '2.6.0' }); + return vi.fn(); + }); + + render(); + + expect(screen.getByText('widget.banners.update-available.body')).toBeInTheDocument(); + + fireEvent.click(screen.getByLabelText('Dismiss')); + + expect(screen.queryByText('widget.banners.update-available.body')).not.toBeInTheDocument(); + }); + + it('calls the cleanup function on unmount', () => { + const unsubscribe = vi.fn(); + vi.mocked(window.electron.onUpdateAvailable).mockReturnValue(unsubscribe); + + const { unmount } = render(); + unmount(); + + expect(unsubscribe).toHaveBeenCalledTimes(1); + }); + + it('shows the banner when update-available event fires after mount', () => { + let capturedCallback: ((info: { version: string }) => void) | null = null; + + vi.mocked(window.electron.onUpdateAvailable).mockImplementation((callback) => { + capturedCallback = callback; + return vi.fn(); + }); + + render(); + + expect(screen.queryByText('widget.banners.update-available.body')).not.toBeInTheDocument(); + + act(() => { + capturedCallback!({ version: '3.0.0' }); + }); + + expect(screen.getByText('widget.banners.update-available.body')).toBeInTheDocument(); + }); +}); diff --git a/src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx b/src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx new file mode 100644 index 000000000..433079f94 --- /dev/null +++ b/src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react'; +import { useTranslationContext } from '../../../../context/LocalContext'; +import { Info, X } from '@phosphor-icons/react'; + +export function UpdateAvailable() { + const [updateInfo, setUpdateInfo] = useState<{ version: string } | null>(null); + const { translate } = useTranslationContext(); + const [dismissed, setDismissed] = useState(false); + + useEffect(() => { + window.electron.getUpdateStatus().then((info) => { + if (info) setUpdateInfo(info); + }); + + const unsubscribe = window.electron.onUpdateAvailable((info) => { + setUpdateInfo(info); + }); + + return unsubscribe; + }, []); + + if (!updateInfo || dismissed) { + return <>; + } + + const openDownloadPage = () => + window.electron.openUrl('https://github.com/internxt/drive-desktop-linux/releases/'); + + return ( +
+ +
+

+ {translate('widget.banners.update-available.body')} +

+ +
+ +
+ ); +} diff --git a/src/apps/renderer/pages/Widget/InfoBanners/InfoBanners.tsx b/src/apps/renderer/pages/Widget/InfoBanners/InfoBanners.tsx index ac9b5490f..cc1f10953 100644 --- a/src/apps/renderer/pages/Widget/InfoBanners/InfoBanners.tsx +++ b/src/apps/renderer/pages/Widget/InfoBanners/InfoBanners.tsx @@ -1,8 +1,10 @@ import { DiscoverBackups } from './Banners/DiscoverBackups'; +import { UpdateAvailable } from './Banners/UpdateAvailable'; export function InfoBanners() { return ( <> + ); diff --git a/src/core/auto-update/check-for-updates-on-deb.test.ts b/src/core/auto-update/check-for-updates-on-deb.test.ts new file mode 100644 index 000000000..c10b299a8 --- /dev/null +++ b/src/core/auto-update/check-for-updates-on-deb.test.ts @@ -0,0 +1,91 @@ +import { EventEmitter } from 'events'; +import { checkForUpdatesOnDeb } from './check-for-updates-on-deb'; + +const mockGet = vi.fn(); + +vi.mock('https', () => ({ + default: { get: (...args: unknown[]) => mockGet(...args) }, +})); + +function mockHttpsResponse(statusCode: number, body: string) { + const res = new EventEmitter() as EventEmitter & { statusCode: number }; + res.statusCode = statusCode; + + const req = new EventEmitter(); + + mockGet.mockImplementation((_options, callback) => { + callback(res); + res.emit('data', body); + res.emit('end'); + return req; + }); +} + +function mockHttpsError(error: Error) { + const req = new EventEmitter(); + + mockGet.mockImplementation(() => { + setTimeout(() => req.emit('error', error), 0); + return req; + }); +} + +describe('checkForUpdatesOnDeb', () => { + describe('when a newer version is available', () => { + it('returns the latest version', async () => { + mockHttpsResponse(200, JSON.stringify({ tag_name: 'v3.0.0' })); + + const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + + expect(result).toEqual({ version: '3.0.0' }); + }); + + it('strips the leading v from the tag name', async () => { + mockHttpsResponse(200, JSON.stringify({ tag_name: 'v2.6.0' })); + + const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + + expect(result).toEqual({ version: '2.6.0' }); + }); + }); + + describe('when the current version is up to date', () => { + it('returns null', async () => { + mockHttpsResponse(200, JSON.stringify({ tag_name: 'v2.5.3' })); + + const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + + expect(result).toBeNull(); + }); + }); + + describe('when the GitHub API returns a non-200 status', () => { + it('returns null', async () => { + mockHttpsResponse(403, ''); + + const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + + expect(result).toBeNull(); + }); + }); + + describe('when the GitHub API returns invalid JSON', () => { + it('returns null', async () => { + mockHttpsResponse(200, 'not-valid-json'); + + const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + + expect(result).toBeNull(); + }); + }); + + describe('when the request fails with a network error', () => { + it('returns null', async () => { + mockHttpsError(new Error('ECONNREFUSED')); + + const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + + expect(result).toBeNull(); + }); + }); +}); diff --git a/src/core/auto-update/check-for-updates-on-deb.ts b/src/core/auto-update/check-for-updates-on-deb.ts new file mode 100644 index 000000000..55cc54088 --- /dev/null +++ b/src/core/auto-update/check-for-updates-on-deb.ts @@ -0,0 +1,50 @@ +import https from 'https'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; + +const GITHUB_API_OPTIONS = { + hostname: 'api.github.com', + path: '/repos/internxt/drive-desktop-linux/releases/latest', + headers: { 'User-Agent': 'internxt-drive-desktop', Accept: 'application/vnd.github.v3+json' }, +}; + +type Props = { + currentVersion: string; +}; + +export async function checkForUpdatesOnDeb({ currentVersion }: Props) { + return new Promise<{ version: string } | null>((resolve) => { + https + .get(GITHUB_API_OPTIONS, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + try { + if (res.statusCode !== 200) { + logger.warn({ msg: `AutoUpdater (deb): GitHub API responded with ${res.statusCode}` }); + return resolve(null); + } + const release = JSON.parse(data) as { tag_name: string }; + const latestVersion = release.tag_name.replace(/^v/, ''); + + logger.debug({ msg: 'AutoUpdater (deb): version check', latestVersion, currentVersion }); + + if (latestVersion !== currentVersion) { + logger.debug({ msg: 'AutoUpdater (deb): update available', version: latestVersion }); + return resolve({ version: latestVersion }); + } + + resolve(null); + } catch (err) { + logger.error({ msg: 'AutoUpdater (deb): error parsing response', err }); + resolve(null); + } + }); + }) + .on('error', (err) => { + logger.error({ msg: 'AutoUpdater (deb): error checking for updates', err }); + resolve(null); + }); + }); +} From 5bb7103b92b281304e6727d0898725957d6df552 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Thu, 19 Mar 2026 10:27:50 -0500 Subject: [PATCH 2/4] style: clean up formatting in UpdateAvailable component --- .../Widget/InfoBanners/Banners/UpdateAvailable.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx b/src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx index 433079f94..bb8693ffe 100644 --- a/src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx +++ b/src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx @@ -23,28 +23,23 @@ export function UpdateAvailable() { return <>; } - const openDownloadPage = () => - window.electron.openUrl('https://github.com/internxt/drive-desktop-linux/releases/'); + const openDownloadPage = () => window.electron.openUrl('https://github.com/internxt/drive-desktop-linux/releases/'); return (
-

- {translate('widget.banners.update-available.body')} -

+

{translate('widget.banners.update-available.body')}

From c75aa6129edbbbf4932ef441b5d9682768723e1f Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Mon, 23 Mar 2026 22:03:19 -0500 Subject: [PATCH 3/4] feat: implement update checking functionality with GitHub API integration --- .../auto-update/check-for-updates.test.ts | 94 +++++++++++++++++++ .../main/auto-update/check-for-updates.ts | 30 ++++++ src/apps/main/main.ts | 35 ++----- ...ts => check-for-updates-on-github.test.ts} | 16 ++-- ...-deb.ts => check-for-updates-on-github.ts} | 14 +-- 5 files changed, 147 insertions(+), 42 deletions(-) create mode 100644 src/apps/main/auto-update/check-for-updates.test.ts create mode 100644 src/apps/main/auto-update/check-for-updates.ts rename src/core/auto-update/{check-for-updates-on-deb.test.ts => check-for-updates-on-github.test.ts} (76%) rename src/core/auto-update/{check-for-updates-on-deb.ts => check-for-updates-on-github.ts} (65%) diff --git a/src/apps/main/auto-update/check-for-updates.test.ts b/src/apps/main/auto-update/check-for-updates.test.ts new file mode 100644 index 000000000..f753699c9 --- /dev/null +++ b/src/apps/main/auto-update/check-for-updates.test.ts @@ -0,0 +1,94 @@ +import * as checkForUpdatesOnGithubModule from '../../../core/auto-update/check-for-updates-on-github'; +import * as autoUpdateModule from 'electron-updater'; +import { partialSpyOn } from 'tests/vitest/utils.helper'; +import { checkForUpdates } from './check-for-updates'; + +vi.mock('electron-updater', () => ({ + autoUpdater: { + checkForUpdatesAndNotify: vi.fn(), + logger: null, + }, +})); + +vi.mock('../../../core/auto-update/check-for-updates-on-github'); + +describe('checkForUpdates', () => { + const checkForUpdatesOnGithubMock = partialSpyOn(checkForUpdatesOnGithubModule, 'checkForUpdatesOnGithub'); + const checkForUpdatesAndNotifyMock = partialSpyOn(autoUpdateModule.autoUpdater, 'checkForUpdatesAndNotify'); + + const onUpdateAvailable = vi.fn(); + + const defaultProps = { + currentVersion: '2.5.3', + onUpdateAvailable, + }; + + beforeEach(() => { + delete process.env.APPIMAGE; + onUpdateAvailable.mockClear(); + }); + + describe('when running as .deb (no APPIMAGE env var)', () => { + describe('when an update is available', () => { + it('calls onUpdateAvailable with the update info', async () => { + checkForUpdatesOnGithubMock.mockResolvedValue({ version: '3.0.0' }); + + await checkForUpdates(defaultProps); + + expect(onUpdateAvailable).toHaveBeenCalledWith({ version: '3.0.0' }); + }); + }); + + describe('when already up to date', () => { + it('does not call onUpdateAvailable', async () => { + checkForUpdatesOnGithubMock.mockResolvedValue(null); + + await checkForUpdates(defaultProps); + + expect(onUpdateAvailable).not.toHaveBeenCalled(); + }); + }); + + it('does not call autoUpdater.checkForUpdatesAndNotify', async () => { + checkForUpdatesOnGithubMock.mockResolvedValue(null); + + await checkForUpdates(defaultProps); + + expect(checkForUpdatesAndNotifyMock).not.toHaveBeenCalled(); + }); + }); + + describe('when running as .AppImage', () => { + beforeEach(() => { + process.env.APPIMAGE = '/path/to/app.AppImage'; + }); + + it('calls autoUpdater.checkForUpdatesAndNotify', async () => { + await checkForUpdates(defaultProps); + + expect(checkForUpdatesAndNotifyMock).toHaveBeenCalled(); + }); + + it('does not call checkForUpdatesOnGithub', async () => { + await checkForUpdates(defaultProps); + + expect(checkForUpdatesOnGithubMock).not.toHaveBeenCalled(); + }); + + it('does not call onUpdateAvailable', async () => { + await checkForUpdates(defaultProps); + + expect(onUpdateAvailable).not.toHaveBeenCalled(); + }); + + describe('when autoUpdater throws', () => { + it('handles the error gracefully without throwing', async () => { + checkForUpdatesAndNotifyMock.mockImplementation(() => { + throw new Error('update error'); + }); + + await expect(checkForUpdates(defaultProps)).resolves.toBeUndefined(); + }); + }); + }); +}); diff --git a/src/apps/main/auto-update/check-for-updates.ts b/src/apps/main/auto-update/check-for-updates.ts new file mode 100644 index 000000000..667315252 --- /dev/null +++ b/src/apps/main/auto-update/check-for-updates.ts @@ -0,0 +1,30 @@ +import { autoUpdater } from 'electron-updater'; +import { logger } from '@internxt/drive-desktop-core/build/backend'; +import { checkForUpdatesOnGithub } from '../../../core/auto-update/check-for-updates-on-github'; + +type Props = { + currentVersion: string; + onUpdateAvailable: (updateInfo: { version: string }) => void; +}; + +export async function checkForUpdates({ currentVersion, onUpdateAvailable }: Props): Promise { + if (!process.env.APPIMAGE) { + const updateInfo = await checkForUpdatesOnGithub({ currentVersion }); + if (updateInfo) { + onUpdateAvailable(updateInfo); + } + return; + } + + try { + autoUpdater.logger = { + debug: (msg) => logger.debug({ msg: `AutoUpdater: ${msg}` }), + info: (msg) => logger.debug({ msg: `AutoUpdater: ${msg}` }), + error: (msg) => logger.error({ msg: `AutoUpdater: ${msg}` }), + warn: (msg) => logger.warn({ msg: `AutoUpdater: ${msg}` }), + }; + autoUpdater.checkForUpdatesAndNotify(); + } catch (err: unknown) { + logger.error({ msg: 'AutoUpdater Error:', err }); + } +} diff --git a/src/apps/main/main.ts b/src/apps/main/main.ts index 50573f5b9..e21dc915a 100644 --- a/src/apps/main/main.ts +++ b/src/apps/main/main.ts @@ -20,7 +20,6 @@ import './auto-launch/handlers'; import './auth/handlers'; import './windows/settings'; import './windows/process-issues'; -import './windows'; import './issues/virtual-drive'; import './device/handlers'; import './../../backend/features/usage/handlers/handlers'; @@ -37,7 +36,6 @@ import './../../backend/features/cleaner/ipc/handlers'; import './virtual-drive'; import { app, ipcMain } from 'electron'; -import { autoUpdater } from 'electron-updater'; import eventBus from './event-bus'; import { AppDataSource } from './database/data-source'; import { getIsLoggedIn } from './auth/handlers'; @@ -64,7 +62,7 @@ import { version, release } from 'node:os'; import { INTERNXT_VERSION } from '../../core/utils/utils'; import { registerBackupHandlers } from '../../backend/features/backup/register-backup-handlers'; import { startBackupsIfAvailable } from '../../backend/features/backup/start-backups-if-available'; -import { checkForUpdatesOnDeb } from '../../core/auto-update/check-for-updates-on-deb'; +import { checkForUpdates } from './auto-update/check-for-updates'; const gotTheLock = app.requestSingleInstanceLock(); app.setAsDefaultProtocolClient('internxt'); @@ -86,29 +84,6 @@ logger.debug({ let pendingUpdateInfo: { version: string } | null = null; -async function checkForUpdates() { - if (!process.env.APPIMAGE) { - const updateInfo = await checkForUpdatesOnDeb({ currentVersion: INTERNXT_VERSION }); - if (updateInfo) { - pendingUpdateInfo = updateInfo; - broadcastToWindows('update-available', updateInfo); - } - return; - } - - try { - autoUpdater.logger = { - debug: (msg) => logger.debug({ msg: `AutoUpdater: ${msg}` }), - info: (msg) => logger.debug({ msg: `AutoUpdater: ${msg}` }), - error: (msg) => logger.error({ msg: `AutoUpdater: ${msg}` }), - warn: (msg) => logger.warn({ msg: `AutoUpdater: ${msg}` }), - }; - autoUpdater.checkForUpdatesAndNotify(); - } catch (err: unknown) { - logger.error({ msg: 'AutoUpdater Error:', err }); - } -} - ipcMain.handle('get-update-status', () => pendingUpdateInfo); if (process.env.NODE_ENV === 'production') { @@ -144,7 +119,13 @@ app setTrayStatus('IDLE'); } - await checkForUpdates(); + await checkForUpdates({ + currentVersion: INTERNXT_VERSION, + onUpdateAvailable: (updateInfo) => { + pendingUpdateInfo = updateInfo; + broadcastToWindows('update-available', updateInfo); + }, + }); registerAvailableUserProductsHandlers(); }) .catch((exc) => logger.error({ msg: 'Error starting app', exc })); diff --git a/src/core/auto-update/check-for-updates-on-deb.test.ts b/src/core/auto-update/check-for-updates-on-github.test.ts similarity index 76% rename from src/core/auto-update/check-for-updates-on-deb.test.ts rename to src/core/auto-update/check-for-updates-on-github.test.ts index c10b299a8..ebfcf50d4 100644 --- a/src/core/auto-update/check-for-updates-on-deb.test.ts +++ b/src/core/auto-update/check-for-updates-on-github.test.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import { checkForUpdatesOnDeb } from './check-for-updates-on-deb'; +import { checkForUpdatesOnGithub } from './check-for-updates-on-github'; const mockGet = vi.fn(); @@ -30,12 +30,12 @@ function mockHttpsError(error: Error) { }); } -describe('checkForUpdatesOnDeb', () => { +describe('checkForUpdatesOnGithub', () => { describe('when a newer version is available', () => { it('returns the latest version', async () => { mockHttpsResponse(200, JSON.stringify({ tag_name: 'v3.0.0' })); - const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + const result = await checkForUpdatesOnGithub({ currentVersion: '2.5.3' }); expect(result).toEqual({ version: '3.0.0' }); }); @@ -43,7 +43,7 @@ describe('checkForUpdatesOnDeb', () => { it('strips the leading v from the tag name', async () => { mockHttpsResponse(200, JSON.stringify({ tag_name: 'v2.6.0' })); - const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + const result = await checkForUpdatesOnGithub({ currentVersion: '2.5.3' }); expect(result).toEqual({ version: '2.6.0' }); }); @@ -53,7 +53,7 @@ describe('checkForUpdatesOnDeb', () => { it('returns null', async () => { mockHttpsResponse(200, JSON.stringify({ tag_name: 'v2.5.3' })); - const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + const result = await checkForUpdatesOnGithub({ currentVersion: '2.5.3' }); expect(result).toBeNull(); }); @@ -63,7 +63,7 @@ describe('checkForUpdatesOnDeb', () => { it('returns null', async () => { mockHttpsResponse(403, ''); - const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + const result = await checkForUpdatesOnGithub({ currentVersion: '2.5.3' }); expect(result).toBeNull(); }); @@ -73,7 +73,7 @@ describe('checkForUpdatesOnDeb', () => { it('returns null', async () => { mockHttpsResponse(200, 'not-valid-json'); - const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + const result = await checkForUpdatesOnGithub({ currentVersion: '2.5.3' }); expect(result).toBeNull(); }); @@ -83,7 +83,7 @@ describe('checkForUpdatesOnDeb', () => { it('returns null', async () => { mockHttpsError(new Error('ECONNREFUSED')); - const result = await checkForUpdatesOnDeb({ currentVersion: '2.5.3' }); + const result = await checkForUpdatesOnGithub({ currentVersion: '2.5.3' }); expect(result).toBeNull(); }); diff --git a/src/core/auto-update/check-for-updates-on-deb.ts b/src/core/auto-update/check-for-updates-on-github.ts similarity index 65% rename from src/core/auto-update/check-for-updates-on-deb.ts rename to src/core/auto-update/check-for-updates-on-github.ts index 55cc54088..574774e8f 100644 --- a/src/core/auto-update/check-for-updates-on-deb.ts +++ b/src/core/auto-update/check-for-updates-on-github.ts @@ -1,4 +1,4 @@ -import https from 'https'; +import https from 'node:https'; import { logger } from '@internxt/drive-desktop-core/build/backend'; const GITHUB_API_OPTIONS = { @@ -11,7 +11,7 @@ type Props = { currentVersion: string; }; -export async function checkForUpdatesOnDeb({ currentVersion }: Props) { +export async function checkForUpdatesOnGithub({ currentVersion }: Props) { return new Promise<{ version: string } | null>((resolve) => { https .get(GITHUB_API_OPTIONS, (res) => { @@ -22,28 +22,28 @@ export async function checkForUpdatesOnDeb({ currentVersion }: Props) { res.on('end', () => { try { if (res.statusCode !== 200) { - logger.warn({ msg: `AutoUpdater (deb): GitHub API responded with ${res.statusCode}` }); + logger.warn({ msg: `AutoUpdater (github): GitHub API responded with ${res.statusCode}` }); return resolve(null); } const release = JSON.parse(data) as { tag_name: string }; const latestVersion = release.tag_name.replace(/^v/, ''); - logger.debug({ msg: 'AutoUpdater (deb): version check', latestVersion, currentVersion }); + logger.debug({ msg: 'AutoUpdater (github): version check', latestVersion, currentVersion }); if (latestVersion !== currentVersion) { - logger.debug({ msg: 'AutoUpdater (deb): update available', version: latestVersion }); + logger.debug({ msg: 'AutoUpdater (github): update available', version: latestVersion }); return resolve({ version: latestVersion }); } resolve(null); } catch (err) { - logger.error({ msg: 'AutoUpdater (deb): error parsing response', err }); + logger.error({ msg: 'AutoUpdater (github): error parsing response', err }); resolve(null); } }); }) .on('error', (err) => { - logger.error({ msg: 'AutoUpdater (deb): error checking for updates', err }); + logger.error({ msg: 'AutoUpdater (github): error checking for updates', err }); resolve(null); }); }); From 0ed0948f93d00775cf626c2e0ca8dac9dbd7de66 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Mon, 23 Mar 2026 22:09:43 -0500 Subject: [PATCH 4/4] fix: await autoUpdater.checkForUpdatesAndNotify for proper error handling --- src/apps/main/auto-update/check-for-updates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/main/auto-update/check-for-updates.ts b/src/apps/main/auto-update/check-for-updates.ts index 667315252..2984f6c3e 100644 --- a/src/apps/main/auto-update/check-for-updates.ts +++ b/src/apps/main/auto-update/check-for-updates.ts @@ -23,7 +23,7 @@ export async function checkForUpdates({ currentVersion, onUpdateAvailable }: Pro error: (msg) => logger.error({ msg: `AutoUpdater: ${msg}` }), warn: (msg) => logger.warn({ msg: `AutoUpdater: ${msg}` }), }; - autoUpdater.checkForUpdatesAndNotify(); + await autoUpdater.checkForUpdatesAndNotify(); } catch (err: unknown) { logger.error({ msg: 'AutoUpdater Error:', err }); }