Whats new implementation.

This commit is contained in:
Burak Kaan Köse
2026-03-02 00:44:29 +01:00
parent e816e87f61
commit d45d3faa89
21 changed files with 496 additions and 9 deletions
@@ -72,10 +72,14 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
IAccountService accountService,
ICalendarService calendarService,
IAccountCalendarStateService accountCalendarStateService,
INavigationService navigationService)
INavigationService navigationService,
IDialogServiceBase dialogService,
IUpdateManager updateManager)
{
_accountService = accountService;
_calendarService = calendarService;
_dialogService = dialogService;
_updateManager = updateManager;
AccountCalendarStateService = accountCalendarStateService;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
@@ -123,9 +127,24 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
await InitializeAccountCalendarsAsync();
await ShowWhatIsNewIfNeededAsync();
TodayClicked();
}
private async Task ShowWhatIsNewIfNeededAsync()
{
if (!_updateManager.ShouldShowUpdateNotes())
return;
var notes = await _updateManager.GetLatestUpdateNotesAsync();
if (notes.Sections.Count == 0)
return;
await _dialogService.ShowWhatIsNewDialogAsync(notes);
}
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
{
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
@@ -271,6 +290,8 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
private DateTime? _navigationDate;
private readonly IAccountService _accountService;
private readonly ICalendarService _calendarService;
private readonly IDialogServiceBase _dialogService;
private readonly IUpdateManager _updateManager;
#region Commands
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Wino.Core.Domain.Models.Updates;
namespace Wino.Core.Domain;
@@ -8,4 +9,6 @@ namespace Wino.Core.Domain;
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(UpdateNotes))]
[JsonSerializable(typeof(List<UpdateNoteSection>))]
public partial class BasicTypesJsonContext : JsonSerializerContext;
+1
View File
@@ -19,6 +19,7 @@ public static class Constants
public const string ToastModeKey = nameof(ToastModeKey);
public const string ToastModeMail = nameof(ToastModeMail);
public const string ToastModeCalendar = nameof(ToastModeCalendar);
public const string ToastMigrationRequiredKey = nameof(ToastMigrationRequiredKey);
public const string ClientLogFile = "Client_.log";
public const string ServerLogFile = "Server_.log";
@@ -0,0 +1,15 @@
using System.Threading.Tasks;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Represents a one-time app or data migration that runs when a user updates to a new version.
/// </summary>
public interface IAppMigration
{
/// <summary>Gets the unique identifier for this migration, used to track completion in local settings.</summary>
string MigrationId { get; }
/// <summary>Executes the migration logic.</summary>
Task ExecuteAsync();
}
@@ -5,6 +5,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Common;
using Wino.Core.Domain.Models.Printing;
using Wino.Core.Domain.Models.Updates;
namespace Wino.Core.Domain.Interfaces;
@@ -30,4 +31,10 @@ public interface IDialogServiceBase
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
Task<string> PickFilePathAsync(string saveFileName);
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
/// <summary>
/// Presents the "What's New" dialog for the current version.
/// This dialog is undismissable and runs any pending migrations when the user clicks "Get Started".
/// </summary>
Task ShowWhatIsNewDialogAsync(UpdateNotes notes);
}
@@ -40,4 +40,10 @@ public interface INotificationBuilder
/// Creates a calendar reminder toast for the specified calendar item.
/// </summary>
Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds);
/// <summary>
/// Shows a notification that a migration is required for the new app version.
/// Synchronization is stopped and the user is prompted to open the app.
/// </summary>
void CreateMigrationRequiredNotification();
}
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Models.Updates;
namespace Wino.Core.Domain.Interfaces;
public interface IUpdateManager
{
/// <summary>Loads and parses the update notes for the current version from the bundled asset file.</summary>
Task<UpdateNotes> GetLatestUpdateNotesAsync();
/// <summary>Returns true if the current version's update notes have not yet been shown to the user.</summary>
bool ShouldShowUpdateNotes();
/// <summary>Stores a flag in local settings indicating the update notes for the current version have been seen.</summary>
void MarkUpdateNotesAsSeen();
/// <summary>Returns true if any registered migration has not yet been completed.</summary>
bool HasPendingMigrations();
/// <summary>Runs all pending migrations in order and marks each as completed in local settings.</summary>
Task RunPendingMigrationsAsync();
/// <summary>Registers migrations to be tracked and executed by this manager.</summary>
void RegisterMigrations(IEnumerable<IAppMigration> migrations);
}
@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace Wino.Core.Domain.Models.Updates;
public class UpdateNoteSection
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
[JsonPropertyName("imageUrl")]
public string ImageUrl { get; set; } = string.Empty;
[JsonPropertyName("imageWidth")]
public double? ImageWidth { get; set; }
[JsonPropertyName("imageHeight")]
public double? ImageHeight { get; set; }
/// <summary>Gets the image width for binding, returning NaN for auto-sizing when not specified.</summary>
public double ActualImageWidth => ImageWidth ?? double.NaN;
/// <summary>Gets the image height for binding, returning NaN for auto-sizing when not specified.</summary>
public double ActualImageHeight => ImageHeight ?? double.NaN;
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Wino.Core.Domain.Models.Updates;
public class UpdateNotes
{
[JsonPropertyName("hasMigrations")]
public bool HasMigrations { get; set; }
[JsonPropertyName("sections")]
public List<UpdateNoteSection> Sections { get; set; } = [];
}
@@ -983,5 +983,8 @@
"CalendarAccountSettings_DefaultShowAs": "Default Show As Status",
"CalendarAccountSettings_DefaultShowAsDescription": "Default availability status for new events created with this account",
"CalendarAccountSettings_PrimaryCalendar": "Primary Calendar",
"CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account"
"CalendarAccountSettings_PrimaryCalendarDescription": "Mark this calendar as the primary calendar for the account",
"WhatIsNew_GetStartedButton": "Get Started",
"WhatIsNew_MigrationNotification_Title": "Wino Mail Updated",
"WhatIsNew_MigrationNotification_Message": "Open the app to complete the update and see what's new."
}
+18 -1
View File
@@ -78,6 +78,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private readonly IMailDialogService _dialogService;
private readonly IMimeFileService _mimeFileService;
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
private readonly IUpdateManager _updateManager;
private readonly INativeAppService _nativeAppService;
private readonly IMailService _mailService;
@@ -100,7 +101,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
IStatePersistanceService statePersistanceService,
IConfigurationService configurationService,
IStartupBehaviorService startupBehaviorService,
IWebView2RuntimeValidatorService webView2RuntimeValidatorService)
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
IUpdateManager updateManager)
{
StatePersistenceService = statePersistanceService;
@@ -121,6 +123,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
_notificationBuilder = notificationBuilder;
_winoRequestDelegator = winoRequestDelegator;
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
_updateManager = updateManager;
}
protected override void OnDispatcherAssigned()
@@ -251,6 +254,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
await ForceAllAccountSynchronizationsAsync();
}
await ShowWhatIsNewIfNeededAsync();
await MakeSureEnableStartupLaunchAsync();
}
@@ -264,6 +268,19 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
}
}
private async Task ShowWhatIsNewIfNeededAsync()
{
if (!_updateManager.ShouldShowUpdateNotes())
return;
var notes = await _updateManager.GetLatestUpdateNotesAsync();
if (notes.Sections.Count == 0)
return;
await _dialogService.ShowWhatIsNewDialogAsync(notes);
}
private async Task MakeSureEnableStartupLaunchAsync()
{
if (!_configurationService.Get<bool>(IsActivateStartupLaunchAskedKey, false))
+50 -1
View File
@@ -22,6 +22,7 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Domain.Models.Updates;
using Wino.Mail.Services;
using Wino.Mail.ViewModels;
using Wino.Mail.WinUI.Activation;
@@ -153,8 +154,17 @@ public partial class App : WinoApplication,
_preferencesService = Services.GetRequiredService<IPreferencesService>();
_accountService = Services.GetRequiredService<IAccountService>();
// Check whether the new version requires a migration before starting sync.
var updateManager = Services.GetRequiredService<IUpdateManager>();
var updateNotes = await updateManager.GetLatestUpdateNotesAsync();
bool hasPendingMigration = updateNotes.HasMigrations && updateManager.HasPendingMigrations();
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
// Hold off sync loop when a migration is required in startup-task (tray-only) mode.
// In foreground mode the sync loop starts normally; the ViewModel dialog handles migrations before sync kicks in.
if (!hasPendingMigration || !IsStartupTaskLaunch())
RestartAutoSynchronizationLoop();
// Check if launched from toast notification.
@@ -178,13 +188,22 @@ public partial class App : WinoApplication,
// If startup task launch, keep window hidden (system tray only).
// Otherwise, activate the window normally.
if (isStartupTaskLaunch)
{
if (hasPendingMigration)
{
// Notify the user to open the app to complete the update before sync can resume.
Services.GetRequiredService<INotificationBuilder>().CreateMigrationRequiredNotification();
LogActivation("Migration required for new version. Sync skipped. User notified via toast.");
}
else
{
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
// Window is created but not activated. User can show it from system tray.
}
}
else
{
// Normal launch - show and activate the window.
// The What's New dialog is shown from MailAppShellViewModel.OnNavigatedTo once XamlRoot is ready.
MainWindow?.Activate();
LogActivation("Window created and activated.");
}
@@ -222,6 +241,13 @@ public partial class App : WinoApplication,
{
var toastArguments = ToastArguments.Parse(toastArgs.Argument);
// Check migration notification activation first.
if (toastArguments.Contains(Constants.ToastMigrationRequiredKey))
{
await HandleMigrationToastActivationAsync();
return;
}
// Check calendar reminder toast activation first.
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
calendarAction == Constants.ToastCalendarNavigateAction &&
@@ -251,6 +277,29 @@ public partial class App : WinoApplication,
}
}
/// <summary>
/// Handles activation from the migration-required toast notification.
/// Opens the app so the shell ViewModel can show the What's New dialog and run migrations.
/// </summary>
private async Task HandleMigrationToastActivationAsync()
{
LogActivation("Handling migration toast activation.");
if (!IsAppRunning())
{
await CreateAndActivateWindow(null!);
}
else
{
EnsureMainWindowVisibleAndForeground();
}
// The MailAppShellViewModel.OnNavigatedTo will detect ShouldShowUpdateNotes() == true
// and display the What's New dialog (including running migrations) once the XamlRoot is ready.
// Restart sync in case it was blocked.
RestartAutoSynchronizationLoop();
}
private async Task HandleCalendarToastNavigationAsync(Guid calendarItemId)
{
var calendarService = Services.GetRequiredService<ICalendarService>();
@@ -0,0 +1,40 @@
{
"hasMigrations": false,
"sections": [
{
"title": "# Wino Calendar is here!",
"description": "You can now create local or remote CalDAV-compatible calendars, manage recurring events, and respond to invitations — all from within Wino.",
"imageUrl": "ms-appx:///Assets/AboutPageTile.png",
"imageWidth": 128,
"imageHeight": 128
},
{
"title": "# S/MIME Signing & Encryption",
"description": "Wino now supports signing and encrypting your emails with personal certificates. Keep your communications secure and verifiable.",
"imageUrl": "ms-appx:///Assets/AboutPageTile.png",
"imageWidth": 128,
"imageHeight": 128
},
{
"title": "# Threaded Mail View",
"description": "Emails are now grouped by conversation, making it easier to follow long discussions without losing context.",
"imageUrl": "ms-appx:///Assets/AboutPageTile.png",
"imageWidth": 128,
"imageHeight": 128
},
{
"title": "# Smarter Notifications",
"description": "Act on your emails directly from toast notifications — mark as read, delete, or archive without opening the app.",
"imageUrl": "ms-appx:///Assets/AboutPageTile.png",
"imageWidth": 128,
"imageHeight": 128
},
{
"title": "# And much more...",
"description": "Folder management, swipe actions, keyboard shortcuts, a custom print dialog, and significant performance improvements are all included in this release.\n\nThank you for using Wino Mail!",
"imageUrl": "ms-appx:///Assets/AboutPageTile.png",
"imageWidth": 128,
"imageHeight": 128
}
]
}
@@ -0,0 +1,70 @@
<ContentDialog
x:Class="Wino.Dialogs.WhatIsNewDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:domain="using:Wino.Core.Domain"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:models="using:Wino.Core.Domain.Models.Updates"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Style="{StaticResource WinoDialogStyle}"
mc:Ignorable="d">
<ContentDialog.Resources>
<x:Double x:Key="ContentDialogMinWidth">480</x:Double>
<x:Double x:Key="ContentDialogMaxWidth">560</x:Double>
<x:Double x:Key="ContentDialogMinHeight">480</x:Double>
<x:Double x:Key="ContentDialogMaxHeight">700</x:Double>
</ContentDialog.Resources>
<Grid RowSpacing="12">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<FlipView
x:Name="UpdateFlipView"
ItemsSource="{x:Bind Sections, Mode=OneTime}"
SelectionChanged="OnFlipViewSelectionChanged">
<FlipView.ItemTemplate>
<DataTemplate x:DataType="models:UpdateNoteSection">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Spacing="16" Padding="12,8">
<controls:MarkdownTextBlock
Text="{x:Bind Title, Mode=OneTime}"
HorizontalAlignment="Center" />
<Image
Source="{x:Bind ImageUrl, Mode=OneTime}"
Width="{x:Bind ActualImageWidth, Mode=OneTime}"
Height="{x:Bind ActualImageHeight, Mode=OneTime}"
HorizontalAlignment="Center"
Stretch="Uniform" />
<controls:MarkdownTextBlock
Text="{x:Bind Description, Mode=OneTime}"
HorizontalAlignment="Stretch" />
</StackPanel>
</ScrollViewer>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
<PipsPager
x:Name="FlipViewPager"
Grid.Row="1"
HorizontalAlignment="Center"
SelectedPageIndex="0"
SelectedIndexChanged="OnPipsPagerSelectedIndexChanged" />
<Button
x:Name="GetStartedButton"
Grid.Row="2"
HorizontalAlignment="Right"
Content="{x:Bind domain:Translator.WhatIsNew_GetStartedButton}"
Visibility="Collapsed"
Click="OnGetStartedClicked" />
</Grid>
</ContentDialog>
@@ -0,0 +1,72 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Windows.System;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Updates;
namespace Wino.Dialogs;
public sealed partial class WhatIsNewDialog : ContentDialog
{
private readonly IUpdateManager _updateManager;
public List<UpdateNoteSection> Sections { get; }
public WhatIsNewDialog(UpdateNotes notes, IUpdateManager updateManager)
{
InitializeComponent();
_updateManager = updateManager;
Sections = notes.Sections;
// Set the number of pages in the pip pager after sections are assigned.
FlipViewPager.NumberOfPages = Sections.Count;
// Show the Get Started button immediately when there is only one page.
if (Sections.Count <= 1)
GetStartedButton.Visibility = Visibility.Visible;
}
protected override void OnKeyDown(KeyRoutedEventArgs e)
{
// Block ESC key to prevent accidental dismissal.
if (e.Key == VirtualKey.Escape)
{
e.Handled = true;
return;
}
base.OnKeyDown(e);
}
private void OnFlipViewSelectionChanged(object sender, SelectionChangedEventArgs e)
{
int selectedIndex = UpdateFlipView.SelectedIndex;
// Keep pip pager in sync with the flip view.
FlipViewPager.SelectedPageIndex = selectedIndex;
// Show Get Started button only on the last page.
GetStartedButton.Visibility = selectedIndex == Sections.Count - 1
? Visibility.Visible
: Visibility.Collapsed;
}
private void OnPipsPagerSelectedIndexChanged(PipsPager sender, PipsPagerSelectedIndexChangedEventArgs args)
{
UpdateFlipView.SelectedIndex = sender.SelectedPageIndex;
}
private async void OnGetStartedClicked(object sender, RoutedEventArgs e)
{
GetStartedButton.IsEnabled = false;
await _updateManager.RunPendingMigrationsAsync();
_updateManager.MarkUpdateNotesAsSeen();
Hide();
}
}
+2 -2
View File
@@ -27,9 +27,9 @@ public class DialogService : DialogServiceBase, IMailDialogService
{
public DialogService(INewThemeService themeService,
IConfigurationService configurationService,
IApplicationResourceManager<ResourceDictionary> applicationResourceManager) : base(themeService, configurationService, applicationResourceManager)
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
IUpdateManager updateManager) : base(themeService, configurationService, applicationResourceManager, updateManager)
{
}
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
+14 -1
View File
@@ -16,6 +16,7 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Common;
using Wino.Core.Domain.Models.Printing;
using Wino.Core.Domain.Models.Updates;
using Wino.Dialogs;
using Wino.Mail.WinUI.Dialogs;
using Wino.Mail.WinUI.Extensions;
@@ -30,14 +31,16 @@ public class DialogServiceBase : IDialogServiceBase
protected INewThemeService ThemeService { get; }
protected IConfigurationService ConfigurationService { get; }
protected IUpdateManager UpdateManager { get; }
protected IApplicationResourceManager<ResourceDictionary> ApplicationResourceManager { get; }
public DialogServiceBase(INewThemeService themeService, IConfigurationService configurationService, IApplicationResourceManager<ResourceDictionary> applicationResourceManager)
public DialogServiceBase(INewThemeService themeService, IConfigurationService configurationService, IApplicationResourceManager<ResourceDictionary> applicationResourceManager, IUpdateManager updateManager)
{
ThemeService = themeService;
ConfigurationService = configurationService;
ApplicationResourceManager = applicationResourceManager;
UpdateManager = updateManager;
}
protected XamlRoot? GetXamlRoot()
@@ -355,4 +358,14 @@ public class DialogServiceBase : IDialogServiceBase
return null!;
}
}
public async Task ShowWhatIsNewDialogAsync(UpdateNotes notes)
{
var dialog = new WhatIsNewDialog(notes, UpdateManager)
{
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
};
await HandleDialogPresentationAsync(dialog);
}
}
@@ -317,6 +317,20 @@ public class NotificationBuilder : INotificationBuilder
return Task.CompletedTask;
}
public void CreateMigrationRequiredNotification()
{
var builder = new ToastContentBuilder();
builder.SetToastScenario(ToastScenario.Default);
builder.AddText(Translator.WhatIsNew_MigrationNotification_Title);
builder.AddText(Translator.WhatIsNew_MigrationNotification_Message);
builder.AddArgument(Constants.ToastMigrationRequiredKey, bool.TrueString);
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
ShowToast(builder);
}
private static void ShowToast(ToastContentBuilder builder, string? tag = null)
{
var toastNotification = new ToastNotification(builder.GetToastContent().GetXml());
+1
View File
@@ -142,6 +142,7 @@
<Content Include="AppThemes\Snowflake.xaml" />
<Content Include="AppThemes\TestTheme.xaml" />
<Content Include="Assets\ReleaseNotes\vnext.md" />
<Content Include="Assets\UpdateNotes\vnext.json" />
<Content Include="Assets\Wino_Icon.ico" />
<Content Include="BackgroundImages\Acrylic.jpg" />
<Content Include="BackgroundImages\Clouds.jpg" />
+1
View File
@@ -29,5 +29,6 @@ public static class ServicesContainerSetup
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
services.AddTransient<ICalDavClient, CalDavClient>();
services.AddSingleton<IUpdateManager, UpdateManager>();
}
}
+88
View File
@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Wino.Core.Domain;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Updates;
namespace Wino.Services;
public class UpdateManager : IUpdateManager
{
private const string UpdateNotesResourcePath = "ms-appx:///Assets/UpdateNotes/vnext.json";
private const string UpdateNotesSeenKeyFormat = "UpdateNotes_{0}_Shown";
private const string MigrationCompletedKeyFormat = "Migration_{0}_Completed";
private readonly IFileService _fileService;
private readonly IConfigurationService _configurationService;
private readonly INativeAppService _nativeAppService;
private readonly List<IAppMigration> _migrations = [];
private string _versionSeenKey = string.Empty;
public UpdateManager(IFileService fileService,
IConfigurationService configurationService,
INativeAppService nativeAppService)
{
_fileService = fileService;
_configurationService = configurationService;
_nativeAppService = nativeAppService;
}
private string GetVersionSeenKey()
{
if (string.IsNullOrEmpty(_versionSeenKey))
{
var version = _nativeAppService.GetFullAppVersion();
var sanitized = version.Replace(".", "_");
_versionSeenKey = string.Format(UpdateNotesSeenKeyFormat, sanitized);
}
return _versionSeenKey;
}
public async Task<UpdateNotes> GetLatestUpdateNotesAsync()
{
try
{
var json = await _fileService.GetFileContentByApplicationUriAsync(UpdateNotesResourcePath);
if (string.IsNullOrEmpty(json))
return new UpdateNotes();
return JsonSerializer.Deserialize(json, BasicTypesJsonContext.Default.UpdateNotes) ?? new UpdateNotes();
}
catch (Exception)
{
return new UpdateNotes();
}
}
public bool ShouldShowUpdateNotes()
=> !_configurationService.Get(GetVersionSeenKey(), false);
public void MarkUpdateNotesAsSeen()
=> _configurationService.Set(GetVersionSeenKey(), true);
public bool HasPendingMigrations()
=> _migrations.Any(m => !_configurationService.Get(string.Format(MigrationCompletedKeyFormat, m.MigrationId), false));
public async Task RunPendingMigrationsAsync()
{
foreach (var migration in _migrations)
{
var key = string.Format(MigrationCompletedKeyFormat, migration.MigrationId);
if (!_configurationService.Get(key, false))
{
await migration.ExecuteAsync();
_configurationService.Set(key, true);
}
}
}
public void RegisterMigrations(IEnumerable<IAppMigration> migrations)
=> _migrations.AddRange(migrations);
}