Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/apps/main/interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ export interface IElectronAPI {
getBackupFatalErrors(): Promise<Array<BackupErrorRecord>>;
onBackupProgress(func: (value: number) => void): () => void;
startRemoteSync(): Promise<void>;
getUpdateStatus(): Promise<{ version: string } | null>;
onUpdateAvailable(callback: (info: { version: string }) => void): () => void;
}

declare global {
Expand Down
21 changes: 16 additions & 5 deletions src/apps/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import './auth/handlers';
import './windows/settings';
import './windows/process-issues';
import './windows';

Check warning on line 23 in src/apps/main/main.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'./windows' imported multiple times.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AZ0GveCXknpSPE_Q1Los&open=AZ0GveCXknpSPE_Q1Los&pullRequest=279
import './issues/virtual-drive';
import './device/handlers';
import './../../backend/features/usage/handlers/handlers';
Expand All @@ -45,6 +45,7 @@
import { createAuthWindow, getAuthWindow } from './windows/auth';
import configStore from './config';
import { getTray, setTrayStatus } from './tray/tray';
import { broadcastToWindows } from './windows';

Check warning on line 48 in src/apps/main/main.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'./windows' imported multiple times.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AZ0GveCXknpSPE_Q1Lot&open=AZ0GveCXknpSPE_Q1Lot&pullRequest=279

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'./windows' imported multiple times.

import { openOnboardingWindow } from './windows/onboarding';
import { setupThemeListener, getTheme } from '../../core/theme';
import { installNautilusExtension } from './nautilus-extension/install';
Expand All @@ -63,6 +64,7 @@
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');
Expand All @@ -82,7 +84,18 @@
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;
}

Comment on lines +87 to +98

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could go in its own file instead of main.ts, perhaps inside src/core? Dont forget to add tests

try {
autoUpdater.logger = {
debug: (msg) => logger.debug({ msg: `AutoUpdater: ${msg}` }),
Expand All @@ -96,9 +109,7 @@
}
}

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
Expand Down Expand Up @@ -133,7 +144,7 @@
setTrayStatus('IDLE');
}

checkForUpdates();
await checkForUpdates();
registerAvailableUserProductsHandlers();
})
.catch((exc) => logger.error({ msg: 'Error starting app', exc }));
Expand Down
2 changes: 2 additions & 0 deletions src/apps/main/preload.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,5 +214,7 @@ declare interface Window {
) => () => void;
getDiskSpace: () => Promise<number>;
};
getUpdateStatus(): Promise<{ version: string } | null>;
onUpdateAvailable(callback: (info: { version: string }) => void): () => void;
};
}
9 changes: 9 additions & 0 deletions src/apps/main/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handler is used to check at runtime whether a new version is available for updating.

ipcRenderer.on(eventName, callbackWrapper);
return () => ipcRenderer.removeListener(eventName, callbackWrapper);
},
});
4 changes: 4 additions & 0 deletions src/apps/renderer/localize/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
4 changes: 4 additions & 0 deletions src/apps/renderer/localize/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
4 changes: 4 additions & 0 deletions src/apps/renderer/localize/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<UpdateAvailable />);

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(<UpdateAvailable />);

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(<UpdateAvailable />);

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(<UpdateAvailable />);

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(<UpdateAvailable />);
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(<UpdateAvailable />);

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();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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) => {

Check warning on line 11 in src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AZ0Gvd9-knpSPE_Q1Lop&open=AZ0Gvd9-knpSPE_Q1Lop&pullRequest=279
if (info) setUpdateInfo(info);
});

const unsubscribe = window.electron.onUpdateAvailable((info) => {

Check warning on line 15 in src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AZ0Gvd9-knpSPE_Q1Loq&open=AZ0Gvd9-knpSPE_Q1Loq&pullRequest=279
setUpdateInfo(info);
});

return unsubscribe;
}, []);

if (!updateInfo || dismissed) {
return <></>;
}

const openDownloadPage = () => window.electron.openUrl('https://github.com/internxt/drive-desktop-linux/releases/');

Check warning on line 26 in src/apps/renderer/pages/Widget/InfoBanners/Banners/UpdateAvailable.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AZ0Gvd9-knpSPE_Q1Lor&open=AZ0Gvd9-knpSPE_Q1Lor&pullRequest=279

return (
<div className="mx-3 mt-3 flex items-center gap-2.5 rounded-xl border border-primary/30 bg-primary/10 px-4 py-2 dark:bg-primary/20">
<Info size={16} weight="fill" className="shrink-0 text-primary" />
<div className="flex flex-1 flex-col">
<p className="text-xs text-gray-80">{translate('widget.banners.update-available.body')}</p>
<button
onClick={openDownloadPage}
className="self-start text-xs font-medium text-primary underline-offset-2 hover:underline">
{translate('widget.banners.update-available.action')}
</button>
</div>
<button
onClick={() => setDismissed(true)}
className="shrink-0 text-gray-50 hover:text-gray-80"
aria-label="Dismiss">
<X size={14} />
</button>
</div>
);
}
2 changes: 2 additions & 0 deletions src/apps/renderer/pages/Widget/InfoBanners/InfoBanners.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { DiscoverBackups } from './Banners/DiscoverBackups';
import { UpdateAvailable } from './Banners/UpdateAvailable';

export function InfoBanners() {
return (
<>
<UpdateAvailable />
<DiscoverBackups />
</>
);
Expand Down
91 changes: 91 additions & 0 deletions src/core/auto-update/check-for-updates-on-deb.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Loading
Loading