feat(app): check GitHub releases on startup and prompt for updates

Compares the running assembly version against the latest GitHub release
tag on every app launch (desktop and Android) and shows an in-app
overlay with the new version tag when one is available. Best-effort:
network/parse failures are silent, matching the app's existing
non-blocking startup checks.
This commit is contained in:
2026-07-02 21:33:14 +02:00
parent 2d017231eb
commit 34b4313a36
6 changed files with 153 additions and 0 deletions
+4
View File
@@ -63,6 +63,10 @@ public sealed class Loc
["help.tab.info"] = ["Info", "Info", "Info", "Info", "Info", "Info"],
["help.tab.donate"] = ["Dona", "Donate", "Donar", "Faire un don", "Doar", "Spenden"],
["help.bug.report"] = ["Segnala un bug", "Report a bug", "Informar un error", "Signaler un bug", "Reportar um bug", "Fehler melden"],
["update.title"] = ["Aggiornamento disponibile", "Update available", "Actualización disponible", "Mise à jour disponible", "Atualização disponível", "Update verfügbar"],
["update.message"] = ["È disponibile una nuova versione:", "A new version is available:", "Hay una nueva versión disponible:", "Une nouvelle version est disponible :", "Uma nova versão está disponível:", "Eine neue Version ist verfügbar:"],
["update.download"] = ["Scarica", "Download", "Descargar", "Télécharger", "Baixar", "Herunterladen"],
["update.dismiss"] = ["Ignora", "Dismiss", "Ignorar", "Ignorer", "Ignorar", "Verwerfen"],
["donate.desc"] = [
"Se questo wallet ti è utile, considera una piccola donazione allo sviluppatore.",
"If you find this wallet useful, consider a small donation to the developer.",
@@ -0,0 +1,36 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PalladiumWallet.Core.Net;
namespace PalladiumWallet.App.ViewModels;
public partial class MainWindowViewModel
{
// ---- update-available overlay ----
[ObservableProperty]
private bool isUpdateAvailableOpen;
[ObservableProperty]
private string updateAvailableTag = "";
public string UpdateReleaseUrl { get; private set; } = "";
[RelayCommand]
private void CloseUpdateAvailable() => IsUpdateAvailableOpen = false;
/// <summary>
/// Fire-and-forget check run once at startup. Best-effort: any failure or an
/// up-to-date app silently does nothing (see <see cref="UpdateChecker"/>).
/// </summary>
private async Task CheckForUpdatesAsync()
{
var latest = await UpdateChecker.CheckAsync(AppVersion);
if (latest is null) return;
UpdateReleaseUrl = latest.HtmlUrl;
UpdateAvailableTag = latest.Tag;
IsUpdateAvailableOpen = true;
}
}
@@ -158,6 +158,7 @@ public partial class MainWindowViewModel : ViewModelBase
_keepAliveTimer = new DispatcherTimer { Interval = System.TimeSpan.FromSeconds(20) };
_keepAliveTimer.Tick += async (_, _) => await KeepAliveTickAsync();
_keepAliveTimer.Start();
_ = CheckForUpdatesAsync();
}
private async System.Threading.Tasks.Task KeepAliveTickAsync()
+29
View File
@@ -1638,5 +1638,34 @@
</StackPanel>
</Border>
</Border>
<!-- ============ UPDATE AVAILABLE OVERLAY ============ -->
<!-- Same pattern as the help overlay: instant open/close. -->
<Border Grid.Row="0" Grid.RowSpan="3"
Background="{DynamicResource ScrimBrush}"
Tapped="OnUpdateAvailableOverlayBackdropTapped"
IsVisible="{Binding IsUpdateAvailableOpen}">
<Border Background="{DynamicResource OverlayCardBrush}"
BorderBrush="{DynamicResource BorderSubtleBrush}" BorderThickness="1" CornerRadius="8"
MaxWidth="440" Margin="16"
HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Margin="24" Spacing="16">
<TextBlock Text="{Binding Loc[update.title]}"
FontSize="18" FontWeight="Bold"/>
<TextBlock TextWrapping="Wrap">
<Run Text="{Binding Loc[update.message]}"/>
<Run Text=" "/>
<Run Text="{Binding UpdateAvailableTag}" FontWeight="Bold"/>
</TextBlock>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
<Button Content="{Binding Loc[update.download]}"
Classes="accent"
Click="OnOpenReleasePageClick"/>
<Button Content="{Binding Loc[update.dismiss]}"
Command="{Binding CloseUpdateAvailableCommand}"/>
</StackPanel>
</StackPanel>
</Border>
</Border>
</Grid>
</UserControl>
+17
View File
@@ -129,6 +129,13 @@ public partial class MainView : UserControl
vm.IsHelpOpen = false;
}
private void OnUpdateAvailableOverlayBackdropTapped(object? sender, TappedEventArgs e)
{
if (!ReferenceEquals(e.Source, sender)) return;
if (DataContext is MainWindowViewModel vm)
vm.IsUpdateAvailableOpen = false;
}
private async void OnOpenBugReportClick(object? sender, RoutedEventArgs e)
{
const string issueUrl = "https://github.com/davide3011/PalladiumWallet/issues/new?template=bug_report.yml";
@@ -137,6 +144,15 @@ public partial class MainView : UserControl
await launcher.LaunchUriAsync(new Uri(issueUrl));
}
private async void OnOpenReleasePageClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm || string.IsNullOrEmpty(vm.UpdateReleaseUrl)) return;
var launcher = TopLevel.GetTopLevel(this)?.Launcher;
if (launcher is not null)
await launcher.LaunchUriAsync(new Uri(vm.UpdateReleaseUrl));
vm.IsUpdateAvailableOpen = false;
}
// Esc (desktop) or Back (Android) closes the topmost open overlay.
protected override void OnKeyDown(KeyEventArgs e)
{
@@ -149,6 +165,7 @@ public partial class MainView : UserControl
if (vm.IsWalletInfoOpen) { vm.CloseWalletInfoCommand.Execute(null); e.Handled = true; return; }
if (vm.IsSettingsOpen) { vm.IsSettingsOpen = false; e.Handled = true; return; }
if (vm.IsHelpOpen) { vm.IsHelpOpen = false; e.Handled = true; return; }
if (vm.IsUpdateAvailableOpen) { vm.IsUpdateAvailableOpen = false; e.Handled = true; return; }
}
base.OnKeyDown(e);
}