feat(desktop): in-app prompt to restart for an update
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s

The main process already showed a system notification when an update finished
downloading, and nothing in the renderer listened — so the only cue vanished on
its own and was missed by anyone not watching the desktop at that moment.

A prompt now stays in the window until it is acted on or dismissed, with the
restart wired to the install-update channel the preload already exposed.

Dismissing is 'not now', not 'never': a later version reopens the prompt, since
the version it refers to has changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 18:31:59 -03:00
co-authored by Claude Opus 5
parent 7f08384372
commit 71d2109b2f
4 changed files with 67 additions and 0 deletions
@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
import { isElectron } from '../../platform/platform';
import { useT } from '../../i18n';
/**
* Aviso de que uma atualização já baixou e só falta reiniciar.
*
* O processo principal já mostrava uma notificação do sistema, que some sozinha
* e passa despercebida se a pessoa não estiver olhando. Este aviso fica na
* janela até ser atendido ou dispensado.
*/
export function UpdateBanner() {
const t = useT();
const [version, setVersion] = useState<string | null>(null);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
if (!isElectron() || !window.backspace?.onUpdateDownloaded) return;
window.backspace.onUpdateDownloaded((info) => {
setVersion(info.version);
// Uma atualização nova reabre o aviso mesmo se a anterior foi dispensada:
// dispensar significa "agora não", não "nunca mais".
setDismissed(false);
});
}, []);
if (!version || dismissed) return null;
return (
<div className="fixed bottom-4 right-4 z-[400] max-w-[320px] glass rounded-xl shadow-xl p-3.5 animate-slide-up">
<div className="text-[13px] font-semibold text-txt-primary">{t('update.ready')}</div>
<p className="text-[12px] text-txt-secondary mt-0.5">
{t('update.readyVersion', { version })}
</p>
<div className="flex gap-2 mt-3">
<button
type="button"
onClick={() => window.backspace?.installUpdate?.()}
className="px-3 py-1.5 rounded-md text-[12px] font-medium bg-accent-primary text-white hover:brightness-110"
>
{t('update.restart')}
</button>
<button
type="button"
onClick={() => setDismissed(true)}
className="px-3 py-1.5 rounded-md text-[12px] font-medium bg-interactive-muted text-txt-secondary hover:text-txt-primary"
>
{t('update.later')}
</button>
</div>
</div>
);
}