Files
netbird/client/ui/tray_update.go
Zoltan Papp 9b4a5df925 [client] Use platform installer URL for manual update downloads (#6922)
The Wails UI regressed the non-enforced update download to the plain
GitHub releases page. Restore the old Fyne behavior: the tray update
item and the About card's Get installer button now open
version.DownloadUrl(), which points to the direct installer download
(pkgs.netbird.io) per OS/arch and falls back to the generic install page
where no installer exists.

Also fix the dead brew detection on darwin: exec.Command passed the
whole pipeline as a single argument to brew, so the check always failed.
Query the netbird formula and netbird-ui cask explicitly via exit codes
instead.

## Describe your changes

## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] This change does **not** modify the public API, gRPC protocols,
functionality behavior, CLI / service flags, or introduce a new feature
— **OR** I have discussed it with the NetBird team beforehand (link the
issue / Slack thread in the description). See
[CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first).

> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).

## Documentation
Select exactly one:

- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)

### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6922"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787764680&installation_model_id=427504&pr_number=6922&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6922&signature=3d9d640b25a17589114cc704e1d631895ffcd9f554947ff5790fe90d54410d2f"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a direct installer download option for manual application
updates.
* Non-enforced updates now open the appropriate platform-specific
installer instead of the general releases page.

* **Bug Fixes**
  * Improved macOS download behavior for Homebrew installations.
* Preserved architecture-specific downloads for Intel and Apple silicon
Macs.
* Updated the tray “About” links to use the GitHub repository and
documentation instead of the releases page.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-27 20:08:35 +02:00

199 lines
5.1 KiB
Go

//go:build !android && !ios && !freebsd && !js
package main
import (
"context"
"sync"
"time"
log "github.com/sirupsen/logrus"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
"github.com/netbirdio/netbird/client/ui/services"
"github.com/netbirdio/netbird/client/ui/updater"
"github.com/netbirdio/netbird/version"
)
// trayUpdater owns the tray UI that reacts to auto-update. Composed inside Tray.
type trayUpdater struct {
app *application.App
window *application.WebviewWindow
update *services.Update
notifier *notifications.NotificationService
loc *Localizer
onIconChange func()
// onMenuChange drives a full tray relayout: the update row lives in the
// About submenu, which KDE/Plasma caches on first open and never re-fetches
// on a plain SetLabel/SetHidden — only a relayout (fresh submenu ids) repaints.
onMenuChange func()
mu sync.Mutex
item *application.MenuItem
state updater.State
notifiedVersion string
progressWindowOpen bool
}
func newTrayUpdater(app *application.App, window *application.WebviewWindow, update *services.Update, notifier *notifications.NotificationService, loc *Localizer, onIconChange func(), onMenuChange func()) *trayUpdater {
u := &trayUpdater{
app: app,
window: window,
update: update,
notifier: notifier,
loc: loc,
onIconChange: onIconChange,
onMenuChange: onMenuChange,
}
app.Event.On(updater.EventStateChanged, u.onStateEvent)
// Seed from cached state to cover an event that fired before wiring completed.
u.state = update.GetState()
return u
}
// attach (re)binds the menu item on each Tray.buildMenu run. The caller owns the
// item's OnClick handler.
func (u *trayUpdater) attach(item *application.MenuItem) {
u.mu.Lock()
u.item = item
state := u.state
u.mu.Unlock()
u.refreshMenuItem(state)
}
// hasUpdate reports whether the tray should paint the "update available" icon.
func (u *trayUpdater) hasUpdate() bool {
u.mu.Lock()
defer u.mu.Unlock()
return u.state.Available
}
// applyLanguage re-renders the menu item label after a locale switch.
func (u *trayUpdater) applyLanguage() {
u.mu.Lock()
state := u.state
u.mu.Unlock()
u.refreshMenuItem(state)
}
// handleClick opens the installer download link when not Enforced, otherwise
// shows the progress page and asks the daemon to start the installer.
func (u *trayUpdater) handleClick() {
u.mu.Lock()
state := u.state
u.mu.Unlock()
if !state.Enforced {
_ = u.app.Browser.OpenURL(version.DownloadUrl())
return
}
u.openProgressWindow(state.Version)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if _, err := u.update.Trigger(ctx); err != nil {
log.Errorf("trigger update: %v", err)
}
}()
}
func (u *trayUpdater) onStateEvent(ev *application.CustomEvent) {
st, ok := ev.Data.(updater.State)
if !ok {
log.Warnf("update state event payload not UpdateState: %T", ev.Data)
return
}
u.applyState(st)
}
// applyState diffs st against the cached state and drives the resulting side
// effects: icon repaint, menu refresh, new-version notification, progress window.
func (u *trayUpdater) applyState(st updater.State) {
u.mu.Lock()
prev := u.state
u.state = st
sendNotify := st.Available && st.Version != "" && st.Version != u.notifiedVersion
if sendNotify {
u.notifiedVersion = st.Version
}
showWindow := st.Installing && !u.progressWindowOpen
if showWindow {
u.progressWindowOpen = true
} else if !st.Installing {
u.progressWindowOpen = false
}
u.mu.Unlock()
// Full relayout rather than in-place: KDE layout-caches the About submenu, so
// a direct SetLabel/SetHidden wouldn't paint. Fall back if no hook was wired.
if u.onMenuChange != nil {
u.onMenuChange()
} else {
u.refreshMenuItem(st)
}
if prev.Available != st.Available && u.onIconChange != nil {
u.onIconChange()
}
if sendNotify {
u.sendUpdateNotification(st)
}
if showWindow {
u.openProgressWindow(st.Version)
}
}
func (u *trayUpdater) refreshMenuItem(st updater.State) {
u.mu.Lock()
item := u.item
u.mu.Unlock()
if item == nil {
return
}
if !st.Available {
item.SetHidden(true)
return
}
if st.Enforced {
item.SetLabel(u.loc.T("tray.menu.installVersion", "version", st.Version))
} else {
item.SetLabel(u.loc.T("tray.menu.downloadLatest"))
}
item.SetHidden(false)
}
func (u *trayUpdater) sendUpdateNotification(st updater.State) {
if u.notifier == nil {
return
}
body := u.loc.T("notify.update.body", "version", st.Version)
if st.Enforced {
body += u.loc.T("notify.update.enforcedSuffix")
}
_ = safeSendNotification(u.notifier.SendNotification, "update", notifications.NotificationOptions{
ID: notifyIDUpdatePrefix + st.Version,
Title: u.loc.T("notify.update.title"),
Body: body,
})
}
// openProgressWindow points the main window at the /update progress page and
// brings it forward.
func (u *trayUpdater) openProgressWindow(version string) {
if u.window == nil {
return
}
url := "/#/update"
if version != "" {
url += "?version=" + version
}
u.window.SetURL(url)
u.window.Show()
u.window.Focus()
}