diff --git a/.claude/instructions/winui3.instructions.md b/.claude/instructions/winui3.instructions.md
new file mode 100644
index 00000000..811306c6
--- /dev/null
+++ b/.claude/instructions/winui3.instructions.md
@@ -0,0 +1,160 @@
+---
+description: 'WinUI 3 and Windows App SDK coding guidelines. Prevents common UWP API misuse, enforces correct XAML namespaces, threading, windowing, and MVVM patterns for desktop Windows apps.'
+applyTo: '**/*.xaml, **/*.cs, **/*.csproj'
+---
+
+# WinUI 3 / Windows App SDK
+
+## Critical Rules — NEVER Use Legacy UWP APIs
+
+These UWP patterns are **wrong** for WinUI 3 desktop apps. Always use the Windows App SDK equivalent.
+
+- **NEVER** use `Windows.UI.Popups.MessageDialog`. Use `ContentDialog` with `XamlRoot` set.
+- **NEVER** show a `ContentDialog` without setting `dialog.XamlRoot = this.Content.XamlRoot` first.
+- **NEVER** use `CoreDispatcher.RunAsync` or `Dispatcher.RunAsync`. Use `DispatcherQueue.TryEnqueue`.
+- **NEVER** use `Window.Current`. Track the main window via a static `App.MainWindow` property.
+- **NEVER** use `Windows.UI.Xaml.*` namespaces. Use `Microsoft.UI.Xaml.*`.
+- **NEVER** use `Windows.UI.Composition`. Use `Microsoft.UI.Composition`.
+- **NEVER** use `Windows.UI.Colors`. Use `Microsoft.UI.Colors`.
+- **NEVER** use `ApplicationView` or `CoreWindow` for window management. Use `Microsoft.UI.Windowing.AppWindow`.
+- **NEVER** use `CoreApplicationViewTitleBar`. Use `AppWindowTitleBar`.
+- **NEVER** use `GetForCurrentView()` patterns (e.g., `UIViewSettings.GetForCurrentView()`). These do not exist in desktop WinUI 3. Use `AppWindow` APIs instead.
+- **NEVER** use UWP `PrintManager` directly. Use `IPrintManagerInterop` with a window handle.
+- **NEVER** use `DataTransferManager` directly for sharing. Use `IDataTransferManagerInterop` with a window handle.
+- **NEVER** use UWP `IBackgroundTask`. Use `Microsoft.Windows.AppLifecycle` activation.
+- **NEVER** use `WebAuthenticationBroker`. Use `OAuth2Manager` (Windows App SDK 1.7+).
+
+## XAML Patterns
+
+- The default XAML namespace maps to `Microsoft.UI.Xaml`, not `Windows.UI.Xaml`.
+- Prefer `{x:Bind}` over `{Binding}` for compiled, type-safe, higher-performance bindings.
+- Set `x:DataType` on `DataTemplate` elements when using `{x:Bind}` — this is required for compiled bindings in templates. On Page/UserControl, `x:DataType` enables compile-time binding validation but is not strictly required if the DataContext does not change.
+- Use `Mode=OneWay` for dynamic values, `Mode=OneTime` for static, `Mode=TwoWay` only for editable inputs.
+- Do not bind static constants — set them directly in XAML.
+
+## Threading
+
+- Use `DispatcherQueue.TryEnqueue(() => { ... })` to update UI from background threads.
+- `TryEnqueue` returns `bool`, not a `Task` — it is fire-and-forget.
+- Check thread access with `DispatcherQueue.HasThreadAccess` before dispatching.
+- WinUI 3 uses standard STA (not ASTA). No built-in reentrancy protection — be cautious with async code that pumps messages.
+
+## Windowing
+
+- Get the `AppWindow` from a WinUI 3 `Window` via `WindowNative.GetWindowHandle` → `Win32Interop.GetWindowIdFromWindow` → `AppWindow.GetFromWindowId`.
+- Use `AppWindow` for resize, move, title, and presenter operations.
+- Custom title bar: use `AppWindow.TitleBar` properties, not `CoreApplicationViewTitleBar`.
+- Track the main window as `App.MainWindow` (a static property set in `OnLaunched`).
+
+## Dialogs and Pickers
+
+- **ContentDialog**: Always set `dialog.XamlRoot = this.Content.XamlRoot` before calling `ShowAsync()`.
+- **File/Folder Pickers**: Initialize with `WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd)` where `hwnd` comes from `WindowNative.GetWindowHandle(App.MainWindow)`.
+- **Share/Print**: Use COM interop interfaces (`IDataTransferManagerInterop`, `IPrintManagerInterop`) with window handles.
+
+## MVVM and Data Binding
+
+- Prefer `CommunityToolkit.Mvvm` (`[ObservableProperty]`, `[RelayCommand]`) for MVVM infrastructure.
+- Use `Microsoft.Extensions.DependencyInjection` for service registration and injection.
+- Keep UI (Views) focused on layout and bindings; keep logic in ViewModels and services.
+- Use `async`/`await` for I/O and long-running work to keep the UI responsive.
+
+## Project Setup
+
+- Target `net10.0-windows10.0.22621.0` (or appropriate TFM for the project's target SDK).
+- Set `true` in the project file.
+- Reference the latest stable `Microsoft.WindowsAppSDK` NuGet package.
+- Use `System.Text.Json` with source generators for JSON serialization.
+
+## C# Code Style
+
+- Use file-scoped namespaces.
+- Enable nullable reference types. Use `is null` / `is not null` instead of `== null`.
+- Prefer pattern matching over `as`/`is` with null checks.
+- PascalCase for types, methods, properties. camelCase for private fields.
+- Allman brace style (opening brace on its own line).
+- Prefer explicit types for built-in types; use `var` only when the type is obvious.
+
+## Accessibility
+
+- Set `AutomationProperties.Name` on all interactive controls.
+- Use `AutomationProperties.HeadingLevel` on section headers.
+- Hide decorative elements with `AutomationProperties.AccessibilityView="Raw"`.
+- Ensure full keyboard navigation (Tab, Enter, Space, arrow keys).
+- Meet WCAG color contrast requirements.
+
+## Performance
+
+- Prefer `{x:Bind}` (compiled) over `{Binding}` (reflection-based).
+- **NativeAOT:** Under Native AOT compilation, `{Binding}` (reflection-based) does not work at all. Only `{x:Bind}` (compiled bindings) is supported. If the project uses NativeAOT, use `{x:Bind}` exclusively.
+- Use `x:Load` or `x:DeferLoadStrategy` for UI elements that are not immediately needed.
+- Use `ItemsRepeater` with virtualization for large lists.
+- Avoid deep layout nesting — prefer `Grid` over nested `StackPanel` chains.
+- Use `async`/`await` for all I/O; never block the UI thread.
+
+## App Settings (Packaged vs Unpackaged)
+
+- **Packaged apps**: `ApplicationData.Current.LocalSettings` works as expected.
+- **Unpackaged apps**: Use a custom settings file (e.g., JSON in `Environment.GetFolderPath(SpecialFolder.LocalApplicationData)`).
+- Do not assume `ApplicationData` is always available — check packaging status first.
+
+## Typography
+
+- **Always** use built-in TextBlock styles (`CaptionTextBlockStyle`, `BodyTextBlockStyle`, `BodyStrongTextBlockStyle`, `SubtitleTextBlockStyle`, `TitleTextBlockStyle`, `TitleLargeTextBlockStyle`, `DisplayTextBlockStyle`).
+- Prefer using the built-in TextBlock styles over hardcoding `FontSize`, `FontWeight`, or `FontFamily`.
+- Font: Segoe UI Variable is the default — do not change it.
+- Use sentence casing for all UI text.
+
+
+## Theming & Colors
+
+- **Always** use `{ThemeResource}` for brushes and colors to support Light, Dark, and High Contrast themes automatically.
+- **Never** hardcode color values (`#FFFFFF`, `Colors.White`, etc.) for UI elements. Use theme resources like `TextFillColorPrimaryBrush`, `CardBackgroundFillColorDefaultBrush`, `CardStrokeColorDefaultBrush`.
+- Use `SystemAccentColor` (and `Light1`–`Light3`, `Dark1`–`Dark3` variants) for the user's accent color palette.
+- For borders: use `CardStrokeColorDefaultBrush` or `ControlStrokeColorDefaultBrush`.
+
+## Spacing & Layout
+
+- Use a **4px grid system**: all margins, padding, and spacing values must be multiples of 4px.
+- Standard spacing: 4 (compact), 8 (controls), 12 (small gutters), 16 (content padding), 24 (large gutters).
+- Prefer `Grid` over deeply nested `StackPanel` chains for performance.
+- Use `Auto` for content-sized rows/columns, `*` for proportional sizing. Avoid fixed pixel sizes.
+- Use `VisualStateManager` with `AdaptiveTrigger` for responsive layouts at breakpoints (640px, 1008px).
+- Use `ControlCornerRadius` (4px) for small controls and `OverlayCornerRadius` (8px) for cards, dialogs, flyouts.
+
+## Materials & Elevation
+
+- Use **Mica** (`MicaBackdrop`) for the app window backdrop. Requires transparent layers above to show through.
+- Use **Acrylic** for transient surfaces only (flyouts, menus, navigation panes).
+- Use `LayerFillColorDefaultBrush` for content layers above Mica.
+- Use `ThemeShadow` with Z-axis `Translation` for elevation. Cards: 4–8 px, Flyouts: 32 px, Dialogs: 128 px.
+
+## Motion & Transitions
+
+- Use built-in theme transitions (`EntranceThemeTransition`, `RepositionThemeTransition`, `ContentThemeTransition`, `AddDeleteThemeTransition`).
+- Avoid custom storyboard animations when a built-in transition exists.
+
+## Control Selection
+
+- Use `NavigationView` for primary app navigation (not custom sidebars).
+- Use `InfoBar` for persistent in-app notifications (not custom banners).
+- Use `TeachingTip` for contextual guidance (not custom popups).
+- Use `NumberBox` for numeric input (not TextBox with manual validation).
+- Use `ToggleSwitch` for on/off settings (not CheckBox).
+- Use `ItemsView` as the modern collection control for displaying data with built-in selection, virtualization, and layout flexibility.
+- Use `ListView`/`GridView` for standard virtualized lists and grids, especially when built-in selection support is needed.
+- Use `ItemsRepeater` only for fully custom virtualizing layouts where you need complete control over rendering and do not need built-in selection or interaction handling.
+- Use `Expander` for collapsible sections (not custom visibility toggling).
+
+## Error Handling
+
+- Always wrap `async void` event handlers in try/catch to prevent unhandled crashes.
+- Use `InfoBar` (with `Severity = Error`) for user-facing error messages, not `ContentDialog` for routine errors.
+- Handle `App.UnhandledException` for logging and graceful recovery.
+
+## Testing
+
+- **NEVER** use a plain MSTest or xUnit project for tests that instantiate WinUI 3 XAML types. Use a **Unit Test App (WinUI in Desktop)** project, which provides the Xaml runtime and UI thread.
+- Use `[TestMethod]` for pure logic tests. Use `[UITestMethod]` for any test that creates or interacts with `Microsoft.UI.Xaml` types (controls, pages, user controls).
+- Place testable business logic in a **Class Library (WinUI in Desktop)** project, separate from the main app.
+- Build the solution before running tests to enable Visual Studio test discovery.
diff --git a/Wino.Core.Domain/BasicTypesJsonContext.cs b/Wino.Core.Domain/BasicTypesJsonContext.cs
index dbe88a7f..f6085c1c 100644
--- a/Wino.Core.Domain/BasicTypesJsonContext.cs
+++ b/Wino.Core.Domain/BasicTypesJsonContext.cs
@@ -10,6 +10,5 @@ namespace Wino.Core.Domain;
[JsonSerializable(typeof(List))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(UpdateNotes))]
-[JsonSerializable(typeof(UpdateMigration))]
[JsonSerializable(typeof(List))]
public partial class BasicTypesJsonContext : JsonSerializerContext;
diff --git a/Wino.Core.Domain/Constants.cs b/Wino.Core.Domain/Constants.cs
index 9a9bb2f6..8be631b2 100644
--- a/Wino.Core.Domain/Constants.cs
+++ b/Wino.Core.Domain/Constants.cs
@@ -19,8 +19,6 @@ 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";
public const string LogArchiveFileName = "WinoLogs.zip";
diff --git a/Wino.Core.Domain/Enums/AccountSetupStepStatus.cs b/Wino.Core.Domain/Enums/AccountSetupStepStatus.cs
new file mode 100644
index 00000000..a0bb99a3
--- /dev/null
+++ b/Wino.Core.Domain/Enums/AccountSetupStepStatus.cs
@@ -0,0 +1,9 @@
+namespace Wino.Core.Domain.Enums;
+
+public enum AccountSetupStepStatus
+{
+ Pending,
+ InProgress,
+ Succeeded,
+ Failed
+}
diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs
index c44b0f47..43637436 100644
--- a/Wino.Core.Domain/Enums/WinoPage.cs
+++ b/Wino.Core.Domain/Enums/WinoPage.cs
@@ -35,5 +35,9 @@ public enum WinoPage
EventDetailsPage,
SignatureAndEncryptionPage,
StoragePage,
- WelcomePageV2
+ WelcomePageV2,
+ WelcomeHostPage,
+ ProviderSelectionPage,
+ AccountSetupProgressPage,
+ SpecialImapCredentialsPage
}
diff --git a/Wino.Core.Domain/Interfaces/IAppMigration.cs b/Wino.Core.Domain/Interfaces/IAppMigration.cs
deleted file mode 100644
index cb2f31c2..00000000
--- a/Wino.Core.Domain/Interfaces/IAppMigration.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System.Threading.Tasks;
-
-namespace Wino.Core.Domain.Interfaces;
-
-///
-/// Represents a one-time app or data migration that runs when a user updates to a new version.
-///
-public interface IAppMigration
-{
- /// Gets the unique identifier for this migration, used to track completion in local settings.
- string MigrationId { get; }
-
- /// Executes the migration logic.
- Task ExecuteAsync();
-}
diff --git a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs
index bf43239a..aae5b2b2 100644
--- a/Wino.Core.Domain/Interfaces/INotificationBuilder.cs
+++ b/Wino.Core.Domain/Interfaces/INotificationBuilder.cs
@@ -40,10 +40,4 @@ public interface INotificationBuilder
/// Creates a calendar reminder toast for the specified calendar item.
///
Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds);
-
- ///
- /// 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.
- ///
- void CreateMigrationRequiredNotification();
}
diff --git a/Wino.Core.Domain/Interfaces/IUpdateManager.cs b/Wino.Core.Domain/Interfaces/IUpdateManager.cs
index cb5ecef9..d0e4a900 100644
--- a/Wino.Core.Domain/Interfaces/IUpdateManager.cs
+++ b/Wino.Core.Domain/Interfaces/IUpdateManager.cs
@@ -1,6 +1,6 @@
-using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Models.Updates;
+using System.Collections.Generic;
namespace Wino.Core.Domain.Interfaces;
@@ -18,12 +18,4 @@ public interface IUpdateManager
/// Stores a flag in local settings indicating the update notes for the current version have been seen.
void MarkUpdateNotesAsSeen();
- /// Returns true if any registered migration has not yet been completed.
- bool HasPendingMigrations();
-
- /// Runs all pending migrations in order and marks each as completed in local settings.
- Task RunPendingMigrationsAsync();
-
- /// Registers migrations to be tracked and executed by this manager.
- void RegisterMigrations(IEnumerable migrations);
}
diff --git a/Wino.Core.Domain/Models/Accounts/AccountSetupStepModel.cs b/Wino.Core.Domain/Models/Accounts/AccountSetupStepModel.cs
new file mode 100644
index 00000000..65befde6
--- /dev/null
+++ b/Wino.Core.Domain/Models/Accounts/AccountSetupStepModel.cs
@@ -0,0 +1,24 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Wino.Core.Domain.Enums;
+
+namespace Wino.Core.Domain.Models.Accounts;
+
+public partial class AccountSetupStepModel : ObservableObject
+{
+ public string Title { get; init; }
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsPending))]
+ [NotifyPropertyChangedFor(nameof(IsInProgress))]
+ [NotifyPropertyChangedFor(nameof(IsSucceeded))]
+ [NotifyPropertyChangedFor(nameof(IsFailed))]
+ public partial AccountSetupStepStatus Status { get; set; } = AccountSetupStepStatus.Pending;
+
+ [ObservableProperty]
+ public partial string ErrorMessage { get; set; }
+
+ public bool IsPending => Status == AccountSetupStepStatus.Pending;
+ public bool IsInProgress => Status == AccountSetupStepStatus.InProgress;
+ public bool IsSucceeded => Status == AccountSetupStepStatus.Succeeded;
+ public bool IsFailed => Status == AccountSetupStepStatus.Failed;
+}
diff --git a/Wino.Core.Domain/Models/Updates/UpdateMigration.cs b/Wino.Core.Domain/Models/Updates/UpdateMigration.cs
deleted file mode 100644
index 205e4dc4..00000000
--- a/Wino.Core.Domain/Models/Updates/UpdateMigration.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace Wino.Core.Domain.Models.Updates;
-
-public class UpdateMigration
-{
- [JsonPropertyName("titleKey")]
- public string TitleKey { get; set; } = string.Empty;
-
- [JsonPropertyName("descriptionKey")]
- public string DescriptionKey { get; set; } = string.Empty;
-}
diff --git a/Wino.Core.Domain/Models/Updates/UpdateNotes.cs b/Wino.Core.Domain/Models/Updates/UpdateNotes.cs
index abab57f1..354f6f6c 100644
--- a/Wino.Core.Domain/Models/Updates/UpdateNotes.cs
+++ b/Wino.Core.Domain/Models/Updates/UpdateNotes.cs
@@ -1,16 +1,7 @@
using System.Collections.Generic;
-using System.Text.Json.Serialization;
-
namespace Wino.Core.Domain.Models.Updates;
public class UpdateNotes
{
- [JsonPropertyName("hasPendingMigrations")]
- public bool HasPendingMigrations { get; set; }
-
- [JsonPropertyName("migration")]
- public UpdateMigration Migration { get; set; } = new();
-
- [JsonPropertyName("sections")]
public List Sections { get; set; } = [];
}
diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json
index 6a819ed5..1a12d997 100644
--- a/Wino.Core.Domain/Translations/en_US/resources.json
+++ b/Wino.Core.Domain/Translations/en_US/resources.json
@@ -1004,5 +1004,45 @@
"WelcomeWindow_SetupSubtitle": "Choose your email provider to get started",
"WelcomeWindow_AddAccountButton": "Add account",
"WelcomeWindow_SkipForNow": "Skip for now — I'll set it up later",
- "WelcomeWindow_AppDescription": "A fast, focused inbox — redesigned for Windows 11"
+ "WelcomeWindow_AppDescription": "A fast, focused inbox — redesigned for Windows 11",
+ "WelcomeWizard_Step1Title": "Welcome",
+ "WelcomeWizard_Step2Title": "Add Account",
+ "WelcomeWizard_Step3Title": "Finish Setup",
+ "ProviderSelection_Title": "Choose your email provider",
+ "ProviderSelection_Subtitle": "Select a provider below to add your email account to Wino Mail.",
+ "ProviderSelection_AccountNameHeader": "Account Name",
+ "ProviderSelection_AccountNamePlaceholder": "e.g. Personal, Work",
+ "ProviderSelection_DisplayNameHeader": "Display Name",
+ "ProviderSelection_DisplayNamePlaceholder": "e.g. John Doe",
+ "ProviderSelection_EmailHeader": "E-mail Address",
+ "ProviderSelection_EmailPlaceholder": "e.g. johndoe@example.com",
+ "ProviderSelection_AppPasswordHeader": "App-Specific Password",
+ "ProviderSelection_AppPasswordHelp": "How do I get an app-specific password?",
+ "ProviderSelection_CalendarModeHeader": "Calendar Integration",
+ "ProviderSelection_CalendarMode_DisabledTitle": "Disabled",
+ "ProviderSelection_CalendarMode_DisabledDescription": "No calendar integration",
+ "ProviderSelection_CalendarMode_CalDavTitle": "CalDAV Synchronization",
+ "ProviderSelection_CalendarMode_CalDavDescription_Apple": "Your calendar events are synced to Apple servers between your devices.",
+ "ProviderSelection_CalendarMode_CalDavDescription_Yahoo": "Your calendar events are synced to Yahoo servers between your devices.",
+ "ProviderSelection_CalendarMode_LocalTitle": "Local calendar",
+ "ProviderSelection_CalendarMode_LocalDescription": "Your events are stored only on your computer. No server connectivity.",
+ "ProviderSelection_ClearColor": "Clear color",
+ "ProviderSelection_ContinueButton": "Continue",
+ "ProviderSelection_SpecialImap_Subtitle": "Enter your account credentials to connect.",
+ "AccountSetup_Title": "Setting up your account",
+ "AccountSetup_Step_Authenticating": "Authenticating with {0}",
+ "AccountSetup_Step_TestingMailAuth": "Testing mail authentication",
+ "AccountSetup_Step_SyncingFolders": "Synchronizing folder metadata",
+ "AccountSetup_Step_FetchingProfile": "Fetching profile information",
+ "AccountSetup_Step_DiscoveringCalDav": "Discovering CalDAV settings",
+ "AccountSetup_Step_TestingCalendarAuth": "Testing calendar authentication",
+ "AccountSetup_Step_SavingAccount": "Saving account information",
+ "AccountSetup_Step_FetchingCalendarMetadata": "Fetching calendar metadata",
+ "AccountSetup_Step_SyncingAliases": "Synchronizing aliases",
+ "AccountSetup_Step_Finalizing": "Finalizing setup",
+ "AccountSetup_FailureMessage": "Setup failed. Go back to fix your settings, or try again later.",
+ "AccountSetup_SuccessMessage": "Your account has been set up successfully!",
+ "AccountSetup_GoBackButton": "Go Back",
+ "AccountSetup_TryAgainButton": "Try Again",
+ "ImapCalDavSettings_AutoDiscoveryFailed": "Auto-discovery failed. Please enter settings manually in the Advanced tab."
}
diff --git a/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs b/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
index 5d808e66..55941039 100644
--- a/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
+++ b/Wino.Core.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
@@ -11,12 +11,15 @@ public partial class BreadcrumbNavigationItemViewModel : ObservableObject
[ObservableProperty]
private bool isActive;
+ public int StepNumber { get; set; }
+
public BreadcrumbNavigationRequested Request { get; set; }
- public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive)
+ public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive, int stepNumber = 0)
{
Request = request;
Title = request.PageTitle;
IsActive = isActive;
+ StepNumber = stepNumber;
}
}
diff --git a/Wino.Core.ViewModels/WelcomeHostPageViewModel.cs b/Wino.Core.ViewModels/WelcomeHostPageViewModel.cs
new file mode 100644
index 00000000..5ee5dd40
--- /dev/null
+++ b/Wino.Core.ViewModels/WelcomeHostPageViewModel.cs
@@ -0,0 +1,13 @@
+using Wino.Core.Domain.Interfaces;
+
+namespace Wino.Core.ViewModels;
+
+public class WelcomeHostPageViewModel : CoreBaseViewModel
+{
+ public WelcomeHostPageViewModel(INavigationService navigationService)
+ {
+ NavigationService = navigationService;
+ }
+
+ public INavigationService NavigationService { get; }
+}
diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs
index ba566492..50c2d46f 100644
--- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs
+++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs
@@ -86,249 +86,7 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
return;
}
- MailAccount createdAccount = null;
- IAccountCreationDialog creationDialog = null;
- bool creationDialogClosed = false;
-
- try
- {
- var providers = ProviderService.GetAvailableProviders();
-
- // Select provider.
- var accountCreationDialogResult = await ExecuteUIThreadTaskAsync(() => MailDialogService.ShowAccountProviderSelectionDialogAsync(providers));
-
- if (accountCreationDialogResult != null)
- {
- CustomServerInformation customServerInformation = null;
-
- createdAccount = new MailAccount()
- {
- ProviderType = accountCreationDialogResult.ProviderType,
- Name = accountCreationDialogResult.AccountName,
- SpecialImapProvider = accountCreationDialogResult.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None,
- Id = Guid.NewGuid(),
- AccountColorHex = accountCreationDialogResult.AccountColorHex,
- IsCalendarAccessGranted = true // New accounts have calendar scopes
- };
-
- if (accountCreationDialogResult.ProviderType == MailProviderType.IMAP4)
- {
- if (createdAccount.SpecialImapProvider == SpecialImapProvider.iCloud || createdAccount.SpecialImapProvider == SpecialImapProvider.Yahoo)
- {
- var accountCreationCancellationTokenSource = new CancellationTokenSource();
- creationDialog = MailDialogService.GetAccountCreationDialog(accountCreationDialogResult);
-
- await ExecuteUIThreadTaskAsync(() => creationDialog.ShowDialogAsync(accountCreationCancellationTokenSource));
- await Task.Delay(500);
-
- await ExecuteUIThread(() => creationDialog.State = AccountCreationDialogState.SigningIn);
-
- customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(createdAccount, accountCreationDialogResult)
- ?? throw new AccountSetupCanceledException();
-
- customServerInformation.Id = Guid.NewGuid();
- customServerInformation.AccountId = createdAccount.Id;
-
- createdAccount.Address = accountCreationDialogResult.SpecialImapProviderDetails.Address;
- createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName;
- createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled;
- createdAccount.ServerInformation = customServerInformation;
-
- await ValidateSpecialImapConnectivityAsync(customServerInformation).ConfigureAwait(false);
- }
- else
- {
- var completionSource = new TaskCompletionSource();
- var setupContext = ImapCalDavSettingsNavigationContext.CreateForCreateMode(accountCreationDialogResult, completionSource);
-
- await ExecuteUIThread(() => Messenger.Send(new BreadcrumbNavigationRequested(
- Translator.ImapCalDavSettingsPage_TitleCreate,
- WinoPage.ImapCalDavSettingsPage,
- setupContext)));
-
- var setupResult = await completionSource.Task.ConfigureAwait(false)
- ?? throw new AccountSetupCanceledException();
-
- customServerInformation = setupResult.ServerInformation ?? throw new AccountSetupCanceledException();
- customServerInformation.Id = Guid.NewGuid();
- customServerInformation.AccountId = createdAccount.Id;
-
- createdAccount.Address = setupResult.EmailAddress;
- createdAccount.SenderName = setupResult.DisplayName;
- createdAccount.IsCalendarAccessGranted = setupResult.IsCalendarAccessGranted;
- createdAccount.ServerInformation = customServerInformation;
- }
- }
- else
- {
- var accountCreationCancellationTokenSource = new CancellationTokenSource();
- creationDialog = MailDialogService.GetAccountCreationDialog(accountCreationDialogResult);
-
- await ExecuteUIThreadTaskAsync(() => creationDialog.ShowDialogAsync(accountCreationCancellationTokenSource));
- await Task.Delay(500);
-
- await ExecuteUIThread(() => creationDialog.State = AccountCreationDialogState.SigningIn);
-
- // OAuth authentication is handled here.
- // Use SynchronizationManager to handle OAuth authentication.
-
- var authTokenInfo = await SynchronizationManager.Instance.HandleAuthorizationAsync(
- accountCreationDialogResult.ProviderType,
- createdAccount,
- createdAccount.ProviderType == MailProviderType.Gmail);
-
- bool creationCanceled = false;
- await ExecuteUIThread(() => creationCanceled = creationDialog.State == AccountCreationDialogState.Canceled);
-
- if (creationCanceled)
- throw new AccountSetupCanceledException();
-
- // Update account address with authenticated user information
- createdAccount.Address = authTokenInfo.AccountAddress;
- }
-
- // Address is still doesn't have a value for API synchronizers.
- // It'll be synchronized with profile information.
-
- await AccountService.CreateAccountAsync(createdAccount, customServerInformation);
-
- // Local account has been created.
-
- // Sync profile information if supported.
- if (createdAccount.IsProfileInfoSyncSupported)
- {
- // Start profile information synchronization.
- // It's only available for Outlook and Gmail synchronizers.
-
- var profileSynchronizationResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(createdAccount.Id);
-
- if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
- throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
-
- if (profileSynchronizationResult.ProfileInformation != null)
- {
- createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
- createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
-
- if (!string.IsNullOrEmpty(profileSynchronizationResult.ProfileInformation.AccountAddress))
- {
- createdAccount.Address = profileSynchronizationResult.ProfileInformation.AccountAddress;
- }
-
- await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
- }
- }
-
- if (creationDialog != null)
- await ExecuteUIThread(() => creationDialog.State = AccountCreationDialogState.PreparingFolders);
-
- var folderSynchronizationResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(createdAccount.Id);
-
- if (folderSynchronizationResult == null || folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
- throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
-
- if (createdAccount.IsCalendarAccessGranted)
- {
- if (creationDialog != null)
- await ExecuteUIThread(() => creationDialog.State = AccountCreationDialogState.CalendarMetadataFetch);
-
- var calendarMetadataSynchronizationResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
- {
- AccountId = createdAccount.Id,
- Type = CalendarSynchronizationType.CalendarMetadata
- });
-
- if (calendarMetadataSynchronizationResult == null || calendarMetadataSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
- throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
- }
-
- // Sync aliases if supported.
- if (createdAccount.IsAliasSyncSupported)
- {
- // Try to synchronize aliases for the account.
- var aliasSynchronizationResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(createdAccount.Id);
-
- if (aliasSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
- throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
- }
- else
- {
- // Create root primary alias for the account.
- // This is only available for accounts that do not support alias synchronization.
-
- await AccountService.CreateRootAliasAsync(createdAccount.Id, createdAccount.Address);
- }
-
- if (creationDialog != null)
- {
- await ExecuteUIThread(() => creationDialog.Complete(false));
- creationDialogClosed = true;
- }
-
- // Send changes to listeners.
- await ExecuteUIThread(() => ReportUIChange(new AccountCreatedMessage(createdAccount)));
-
- // Notify success.
- await ExecuteUIThread(() => DialogService.InfoBarMessage(Translator.Info_AccountCreatedTitle, string.Format(Translator.Info_AccountCreatedMessage, createdAccount.Address), InfoBarMessageType.Success));
- }
- }
- catch (Exception ex) when (ex.Message.Contains(nameof(GmailServiceDisabledException)))
- {
- // For Google Workspace accounts, Gmail API might be disabled by the admin.
- // Wino can't continue synchronization in this case.
- // We must notify the user about this and prevent account creation.
-
- await ExecuteUIThread(() => DialogService.InfoBarMessage(Translator.GmailServiceDisabled_Title, Translator.GmailServiceDisabled_Message, InfoBarMessageType.Error));
-
- if (createdAccount != null)
- {
- await AccountService.DeleteAccountAsync(createdAccount);
- }
- }
- catch (AccountSetupCanceledException)
- {
- // Ignore
- }
- catch (Exception ex) when (ex.Message.Contains(nameof(AccountSetupCanceledException)))
- {
- // Ignore
- }
- catch (ImapClientPoolException testClientPoolException) when (testClientPoolException.CustomServerInformation != null)
- {
- var properties = testClientPoolException.CustomServerInformation.GetConnectionProperties();
-
- properties.Add("ProtocolLog", testClientPoolException.ProtocolLog);
- properties.Add("DiagnosticId", PreferencesService.DiagnosticId);
-
- _winoLogger.TrackEvent("IMAP Test Failed", properties);
-
- await ExecuteUIThread(() => DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, testClientPoolException.Message, InfoBarMessageType.Error));
- }
- catch (ImapClientPoolException clientPoolException) when (clientPoolException.InnerException != null)
- {
- await ExecuteUIThread(() => DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, clientPoolException.InnerException.Message, InfoBarMessageType.Error));
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Failed to create account.");
-
- await ExecuteUIThread(() => DialogService.InfoBarMessage(Translator.Info_AccountCreationFailedTitle, ex.Message, InfoBarMessageType.Error));
-
- // Delete account in case of failure.
- if (createdAccount != null)
- {
- await AccountService.DeleteAccountAsync(createdAccount);
- }
- }
- finally
- {
- if (creationDialog != null && !creationDialogClosed)
- {
- bool isCanceled = false;
- await ExecuteUIThread(() => isCanceled = creationDialog.State == AccountCreationDialogState.Canceled);
- await ExecuteUIThread(() => creationDialog.Complete(isCanceled));
- }
- }
+ Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage));
}
public Task StartAddNewAccountAsync() => AddNewAccountAsync();
diff --git a/Wino.Mail.ViewModels/AccountSetupProgressPageViewModel.cs b/Wino.Mail.ViewModels/AccountSetupProgressPageViewModel.cs
new file mode 100644
index 00000000..85423bab
--- /dev/null
+++ b/Wino.Mail.ViewModels/AccountSetupProgressPageViewModel.cs
@@ -0,0 +1,477 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.Messaging;
+using Serilog;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Accounts;
+using Wino.Core.Domain.Models.Calendar;
+using Wino.Core.Domain.Exceptions;
+using Wino.Core.Domain.Models.Navigation;
+using Wino.Core.Domain.Models.Synchronization;
+using Wino.Core.Services;
+using Wino.Mail.ViewModels.Data;
+using Wino.Messaging.Client.Navigation;
+using Wino.Messaging.UI;
+
+namespace Wino.Mail.ViewModels;
+
+public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
+{
+ private readonly IAccountService _accountService;
+ private readonly ISpecialImapProviderConfigResolver _specialImapProviderConfigResolver;
+ private readonly ICalDavClient _calDavClient;
+ private readonly IMailDialogService _dialogService;
+
+ public WelcomeWizardContext WizardContext { get; }
+
+ public ObservableCollection Steps { get; } = [];
+
+ [ObservableProperty]
+ public partial bool IsSetupComplete { get; set; }
+
+ [ObservableProperty]
+ public partial bool IsSetupFailed { get; set; }
+
+ [ObservableProperty]
+ public partial string FailureMessage { get; set; }
+
+ private MailAccount _createdAccount;
+ private bool _dbWritten;
+
+ public AccountSetupProgressPageViewModel(
+ IAccountService accountService,
+ ISpecialImapProviderConfigResolver specialImapProviderConfigResolver,
+ ICalDavClient calDavClient,
+ IMailDialogService dialogService,
+ WelcomeWizardContext wizardContext)
+ {
+ _accountService = accountService;
+ _specialImapProviderConfigResolver = specialImapProviderConfigResolver;
+ _calDavClient = calDavClient;
+ _dialogService = dialogService;
+ WizardContext = wizardContext;
+ }
+
+ public override async void OnNavigatedTo(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedTo(mode, parameters);
+
+ // Only run on fresh navigation, not on back-navigation
+ if (mode == NavigationMode.Back) return;
+
+ await RunSetupAsync();
+ }
+
+ private void BuildSteps()
+ {
+ Steps.Clear();
+
+ if (WizardContext.IsOAuthProvider)
+ {
+ Steps.Add(new AccountSetupStepModel
+ {
+ Title = string.Format(Translator.AccountSetup_Step_Authenticating, WizardContext.SelectedProvider.Name)
+ });
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingProfile });
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingAliases });
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing });
+ }
+ else if (WizardContext.IsSpecialImapProvider)
+ {
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingMailAuth });
+
+ if (WizardContext.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
+ {
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_DiscoveringCalDav });
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingCalendarAuth });
+ }
+
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
+
+ if (WizardContext.CalendarSupportMode != ImapCalendarSupportMode.Disabled)
+ {
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
+ }
+
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing });
+ }
+ else // Generic IMAP
+ {
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
+
+ var setupResult = WizardContext.ImapCalDavSetupResult;
+ if (setupResult?.IsCalendarAccessGranted == true &&
+ setupResult.ServerInformation?.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
+ {
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_DiscoveringCalDav });
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingCalendarAuth });
+ }
+
+ if (setupResult?.IsCalendarAccessGranted == true)
+ {
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
+ }
+
+ Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing });
+ }
+ }
+
+ private int _currentStepIndex;
+
+ private void SetStepInProgress(string title)
+ {
+ for (int i = 0; i < Steps.Count; i++)
+ {
+ if (Steps[i].Title == title)
+ {
+ _currentStepIndex = i;
+ Steps[i].Status = AccountSetupStepStatus.InProgress;
+ return;
+ }
+ }
+ }
+
+ private void SetCurrentStepSucceeded()
+ {
+ if (_currentStepIndex < Steps.Count)
+ Steps[_currentStepIndex].Status = AccountSetupStepStatus.Succeeded;
+ }
+
+ private void SetCurrentStepFailed(string errorMessage)
+ {
+ if (_currentStepIndex < Steps.Count)
+ {
+ Steps[_currentStepIndex].Status = AccountSetupStepStatus.Failed;
+ Steps[_currentStepIndex].ErrorMessage = errorMessage;
+ }
+ }
+
+ private async Task RunSetupAsync()
+ {
+ IsSetupComplete = false;
+ IsSetupFailed = false;
+ FailureMessage = null;
+ _dbWritten = false;
+ _createdAccount = null;
+
+ BuildSteps();
+
+ try
+ {
+ CustomServerInformation customServerInformation = null;
+
+ // Build account in memory
+ _createdAccount = new MailAccount
+ {
+ Id = Guid.NewGuid(),
+ ProviderType = WizardContext.SelectedProvider.Type,
+ Name = WizardContext.AccountName,
+ SpecialImapProvider = WizardContext.SelectedProvider.SpecialImapProvider,
+ AccountColorHex = WizardContext.AccountColorHex,
+ IsCalendarAccessGranted = true
+ };
+
+ if (WizardContext.IsOAuthProvider)
+ {
+ // Step: Authenticating
+ SetStepInProgress(string.Format(Translator.AccountSetup_Step_Authenticating, WizardContext.SelectedProvider.Name));
+
+ var authTokenInfo = await SynchronizationManager.Instance.HandleAuthorizationAsync(
+ WizardContext.SelectedProvider.Type,
+ _createdAccount,
+ _createdAccount.ProviderType == MailProviderType.Gmail);
+
+ _createdAccount.Address = authTokenInfo.AccountAddress;
+ SetCurrentStepSucceeded();
+
+ // Step: Save to DB
+ SetStepInProgress(Translator.AccountSetup_Step_SavingAccount);
+ await _accountService.CreateAccountAsync(_createdAccount, null);
+ _dbWritten = true;
+ SetCurrentStepSucceeded();
+
+ // Step: Profile
+ SetStepInProgress(Translator.AccountSetup_Step_FetchingProfile);
+ var profileResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(_createdAccount.Id);
+ if (profileResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
+
+ if (profileResult.ProfileInformation != null)
+ {
+ _createdAccount.SenderName = profileResult.ProfileInformation.SenderName;
+ _createdAccount.Base64ProfilePictureData = profileResult.ProfileInformation.Base64ProfilePictureData;
+
+ if (!string.IsNullOrEmpty(profileResult.ProfileInformation.AccountAddress))
+ _createdAccount.Address = profileResult.ProfileInformation.AccountAddress;
+
+ await _accountService.UpdateProfileInformationAsync(_createdAccount.Id, profileResult.ProfileInformation);
+ }
+ SetCurrentStepSucceeded();
+
+ // Step: Folders
+ SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
+ var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
+ if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
+ SetCurrentStepSucceeded();
+
+ // Step: Calendar metadata
+ SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
+ if (_createdAccount.IsCalendarAccessGranted)
+ {
+ var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
+ {
+ AccountId = _createdAccount.Id,
+ Type = CalendarSynchronizationType.CalendarMetadata
+ });
+ if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
+ }
+ SetCurrentStepSucceeded();
+
+ // Step: Aliases
+ SetStepInProgress(Translator.AccountSetup_Step_SyncingAliases);
+ if (_createdAccount.IsAliasSyncSupported)
+ {
+ var aliasResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(_createdAccount.Id);
+ if (aliasResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
+ }
+ else
+ {
+ await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
+ }
+ SetCurrentStepSucceeded();
+ }
+ else if (WizardContext.IsSpecialImapProvider)
+ {
+ var dialogResult = WizardContext.BuildAccountCreationDialogResult();
+
+ customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(_createdAccount, dialogResult);
+ if (customServerInformation == null) throw new Exception("Failed to resolve server information.");
+
+ customServerInformation.Id = Guid.NewGuid();
+ customServerInformation.AccountId = _createdAccount.Id;
+
+ _createdAccount.Address = WizardContext.EmailAddress;
+ _createdAccount.SenderName = WizardContext.DisplayName;
+ _createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled;
+ _createdAccount.ServerInformation = customServerInformation;
+
+ // Step: Test IMAP
+ SetStepInProgress(Translator.AccountSetup_Step_TestingMailAuth);
+ await ValidateImapConnectivityAsync(customServerInformation);
+ SetCurrentStepSucceeded();
+
+ // Step: CalDAV discovery and testing (if applicable)
+ if (customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
+ {
+ SetStepInProgress(Translator.AccountSetup_Step_DiscoveringCalDav);
+ SetCurrentStepSucceeded();
+
+ SetStepInProgress(Translator.AccountSetup_Step_TestingCalendarAuth);
+ await ValidateCalDavConnectivityAsync(customServerInformation);
+ SetCurrentStepSucceeded();
+ }
+
+ // Step: Save to DB
+ SetStepInProgress(Translator.AccountSetup_Step_SavingAccount);
+ await _accountService.CreateAccountAsync(_createdAccount, customServerInformation);
+ _dbWritten = true;
+ SetCurrentStepSucceeded();
+
+ // Step: Folders
+ SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
+ var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
+ if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
+ SetCurrentStepSucceeded();
+
+ // Step: Calendar metadata (if not disabled)
+ if (_createdAccount.IsCalendarAccessGranted)
+ {
+ SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
+ var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
+ {
+ AccountId = _createdAccount.Id,
+ Type = CalendarSynchronizationType.CalendarMetadata
+ });
+ if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
+ SetCurrentStepSucceeded();
+ }
+
+ // Aliases for IMAP
+ await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
+ }
+ else // Generic IMAP
+ {
+ var setupResult = WizardContext.ImapCalDavSetupResult
+ ?? throw new Exception("IMAP setup was not completed.");
+
+ customServerInformation = setupResult.ServerInformation
+ ?? throw new Exception("Server information is missing.");
+
+ customServerInformation.Id = Guid.NewGuid();
+ customServerInformation.AccountId = _createdAccount.Id;
+
+ _createdAccount.Address = setupResult.EmailAddress;
+ _createdAccount.SenderName = setupResult.DisplayName;
+ _createdAccount.IsCalendarAccessGranted = setupResult.IsCalendarAccessGranted;
+ _createdAccount.ServerInformation = customServerInformation;
+
+ // Step: Save to DB
+ SetStepInProgress(Translator.AccountSetup_Step_SavingAccount);
+ await _accountService.CreateAccountAsync(_createdAccount, customServerInformation);
+ _dbWritten = true;
+ SetCurrentStepSucceeded();
+
+ // Step: Folders
+ SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
+ var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
+ if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
+ SetCurrentStepSucceeded();
+
+ // Step: CalDAV (if applicable)
+ if (setupResult.IsCalendarAccessGranted &&
+ customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
+ {
+ SetStepInProgress(Translator.AccountSetup_Step_DiscoveringCalDav);
+ SetCurrentStepSucceeded();
+
+ SetStepInProgress(Translator.AccountSetup_Step_TestingCalendarAuth);
+ await ValidateCalDavConnectivityAsync(customServerInformation);
+ SetCurrentStepSucceeded();
+ }
+
+ // Step: Calendar metadata
+ if (setupResult.IsCalendarAccessGranted)
+ {
+ SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
+ var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
+ {
+ AccountId = _createdAccount.Id,
+ Type = CalendarSynchronizationType.CalendarMetadata
+ });
+ if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
+ SetCurrentStepSucceeded();
+ }
+
+ // Aliases for IMAP
+ await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
+ }
+
+ // Step: Finalizing
+ SetStepInProgress(Translator.AccountSetup_Step_Finalizing);
+ SetCurrentStepSucceeded();
+
+ IsSetupComplete = true;
+
+ // Notify listeners — this triggers ShellWindow creation from App.xaml.cs
+ Messenger.Send(new AccountCreatedMessage(_createdAccount));
+ }
+ catch (AccountSetupCanceledException)
+ {
+ // User canceled authentication — go back silently, no error UI
+ Messenger.Send(new BackBreadcrumNavigationRequested(NavigationTransitionEffect.FromLeft));
+ }
+ catch (Exception ex) when (ex.Message.Contains(nameof(AccountSetupCanceledException)))
+ {
+ // Wrapped cancellation — same silent behavior
+ Messenger.Send(new BackBreadcrumNavigationRequested(NavigationTransitionEffect.FromLeft));
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Account setup failed.");
+
+ SetCurrentStepFailed(ex.Message);
+ IsSetupFailed = true;
+ FailureMessage = Translator.AccountSetup_FailureMessage;
+
+ // Rollback if DB write happened
+ if (_dbWritten && _createdAccount != null)
+ {
+ try
+ {
+ await _accountService.DeleteAccountAsync(_createdAccount);
+ }
+ catch (Exception deleteEx)
+ {
+ Log.Error(deleteEx, "Failed to rollback account creation.");
+ }
+
+ _dbWritten = false;
+ }
+ }
+ }
+
+ private async Task ValidateImapConnectivityAsync(CustomServerInformation serverInformation)
+ {
+ var connectivityResult = await SynchronizationManager.Instance
+ .TestImapConnectivityAsync(serverInformation, allowSSLHandshake: false);
+
+ if (connectivityResult.IsCertificateUIRequired)
+ {
+ var certificateMessage =
+ $"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row0}\n\n" +
+ $"{Translator.IMAPSetupDialog_CertificateIssuer}: {connectivityResult.CertificateIssuer}\n" +
+ $"{Translator.IMAPSetupDialog_CertificateValidFrom}: {connectivityResult.CertificateValidFromDateString}\n" +
+ $"{Translator.IMAPSetupDialog_CertificateValidTo}: {connectivityResult.CertificateExpirationDateString}\n\n" +
+ $"{Translator.IMAPSetupDialog_CertificateAllowanceRequired_Row1}";
+
+ var allowCertificate = await _dialogService.ShowConfirmationDialogAsync(
+ certificateMessage,
+ Translator.GeneralTitle_Warning,
+ Translator.Buttons_Allow);
+
+ if (!allowCertificate)
+ throw new InvalidOperationException(Translator.IMAPSetupDialog_CertificateDenied);
+
+ connectivityResult = await SynchronizationManager.Instance
+ .TestImapConnectivityAsync(serverInformation, allowSSLHandshake: true);
+ }
+
+ if (!connectivityResult.IsSuccess)
+ throw new InvalidOperationException(connectivityResult.FailedReason ?? Translator.IMAPSetupDialog_ConnectionFailedMessage);
+ }
+
+ private async Task ValidateCalDavConnectivityAsync(CustomServerInformation serverInformation)
+ {
+ if (string.IsNullOrWhiteSpace(serverInformation.CalDavServiceUrl))
+ throw new InvalidOperationException(Translator.ImapCalDavSettingsPage_CalDavUrlRequired);
+
+ var settings = new CalDavConnectionSettings
+ {
+ ServiceUri = new Uri(serverInformation.CalDavServiceUrl, UriKind.Absolute),
+ Username = serverInformation.CalDavUsername,
+ Password = serverInformation.CalDavPassword
+ };
+
+ await _calDavClient.DiscoverCalendarsAsync(settings);
+ }
+
+ [RelayCommand]
+ private void GoBack()
+ {
+ Messenger.Send(new BackBreadcrumNavigationRequested(NavigationTransitionEffect.FromLeft));
+ }
+
+ [RelayCommand]
+ private async Task TryAgainAsync()
+ {
+ await RunSetupAsync();
+ }
+}
diff --git a/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs b/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs
index b5a1372d..8a1bce39 100644
--- a/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs
+++ b/Wino.Mail.ViewModels/Data/ImapCalDavSettingsNavigationContext.cs
@@ -9,7 +9,8 @@ namespace Wino.Mail.ViewModels.Data;
public enum ImapCalDavSettingsPageMode
{
Create,
- Edit
+ Edit,
+ Wizard
}
public sealed class ImapCalDavSettingsNavigationContext
@@ -35,6 +36,16 @@ public sealed class ImapCalDavSettingsNavigationContext
Mode = ImapCalDavSettingsPageMode.Edit,
AccountId = accountId
};
+
+ public static ImapCalDavSettingsNavigationContext CreateForWizardMode(
+ AccountCreationDialogResult accountCreationDialogResult)
+ => new()
+ {
+ Mode = ImapCalDavSettingsPageMode.Wizard,
+ AccountCreationDialogResult = accountCreationDialogResult
+ };
+
+ public bool IsWizardMode => Mode == ImapCalDavSettingsPageMode.Wizard;
}
public sealed class ImapCalDavSetupResult
diff --git a/Wino.Mail.ViewModels/Data/WelcomeWizardContext.cs b/Wino.Mail.ViewModels/Data/WelcomeWizardContext.cs
new file mode 100644
index 00000000..fe38bd23
--- /dev/null
+++ b/Wino.Mail.ViewModels/Data/WelcomeWizardContext.cs
@@ -0,0 +1,79 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Wino.Core.Domain.Entities.Shared;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Accounts;
+
+namespace Wino.Mail.ViewModels.Data;
+
+public partial class WelcomeWizardContext : ObservableObject
+{
+ // Step 2 — Provider selection
+ [ObservableProperty]
+ public partial IProviderDetail SelectedProvider { get; set; }
+
+ [ObservableProperty]
+ public partial string AccountName { get; set; }
+
+ [ObservableProperty]
+ public partial string AccountColorHex { get; set; }
+
+ // Special IMAP fields (iCloud/Yahoo)
+ [ObservableProperty]
+ public partial string DisplayName { get; set; }
+
+ [ObservableProperty]
+ public partial string EmailAddress { get; set; }
+
+ [ObservableProperty]
+ public partial string AppSpecificPassword { get; set; }
+
+ [ObservableProperty]
+ public partial ImapCalendarSupportMode CalendarSupportMode { get; set; } = ImapCalendarSupportMode.Disabled;
+
+ // Generic IMAP — populated by ImapCalDavSettingsPage
+ public ImapCalDavSetupResult ImapCalDavSetupResult { get; set; }
+
+ // Computed helpers
+ public bool IsOAuthProvider => SelectedProvider?.Type is MailProviderType.Outlook or MailProviderType.Gmail;
+
+ public bool IsSpecialImapProvider =>
+ SelectedProvider?.SpecialImapProvider is SpecialImapProvider.iCloud or SpecialImapProvider.Yahoo;
+
+ public bool IsGenericImap =>
+ SelectedProvider?.Type == MailProviderType.IMAP4
+ && SelectedProvider?.SpecialImapProvider == SpecialImapProvider.None;
+
+ public SpecialImapProviderDetails BuildSpecialImapProviderDetails()
+ {
+ if (!IsSpecialImapProvider) return null;
+
+ return new SpecialImapProviderDetails(
+ EmailAddress,
+ AppSpecificPassword,
+ DisplayName,
+ SelectedProvider.SpecialImapProvider,
+ CalendarSupportMode);
+ }
+
+ public AccountCreationDialogResult BuildAccountCreationDialogResult()
+ {
+ return new AccountCreationDialogResult(
+ SelectedProvider.Type,
+ AccountName,
+ BuildSpecialImapProviderDetails(),
+ AccountColorHex);
+ }
+
+ public void Reset()
+ {
+ SelectedProvider = null;
+ AccountName = null;
+ AccountColorHex = null;
+ DisplayName = null;
+ EmailAddress = null;
+ AppSpecificPassword = null;
+ CalendarSupportMode = ImapCalendarSupportMode.Disabled;
+ ImapCalDavSetupResult = null;
+ }
+}
diff --git a/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs b/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs
index 97d52026..f1409956 100644
--- a/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs
+++ b/Wino.Mail.ViewModels/ImapCalDavSettingsPageViewModel.cs
@@ -25,6 +25,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private readonly ICalDavClient _calDavClient;
private readonly IAccountService _accountService;
private readonly IMailDialogService _mailDialogService;
+ private readonly WelcomeWizardContext _wizardContext;
private ImapCalDavSettingsPageMode _pageMode;
private Guid _editingAccountId;
@@ -256,12 +257,14 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
public ImapCalDavSettingsPageViewModel(IAutoDiscoveryService autoDiscoveryService,
ICalDavClient calDavClient,
IAccountService accountService,
- IMailDialogService mailDialogService)
+ IMailDialogService mailDialogService,
+ WelcomeWizardContext wizardContext)
{
_autoDiscoveryService = autoDiscoveryService;
_calDavClient = calDavClient;
_accountService = accountService;
_mailDialogService = mailDialogService;
+ _wizardContext = wizardContext;
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
@@ -278,7 +281,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
_localOnlyInfoShown = false;
SelectedSetupTabIndex = 0;
- if (_pageMode == ImapCalDavSettingsPageMode.Create)
+ if (_pageMode == ImapCalDavSettingsPageMode.Create || _pageMode == ImapCalDavSettingsPageMode.Wizard)
{
PageTitle = Translator.ImapCalDavSettingsPage_TitleCreate;
ApplyCreateContextDefaults(context.AccountCreationDialogResult);
@@ -301,6 +304,8 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
base.OnNavigatedFrom(mode, parameters);
}
+ public bool IsWizardMode => _pageMode == ImapCalDavSettingsPageMode.Wizard;
+
[RelayCommand]
private async Task AutoDiscoverSettingsAsync()
{
@@ -407,6 +412,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
IsCalDavValidationSucceeded = false;
}
+ if (_pageMode == ImapCalDavSettingsPageMode.Wizard)
+ {
+ CompleteWizardFlow(serverInformation);
+ return;
+ }
+
if (_pageMode == ImapCalDavSettingsPageMode.Create)
{
CompleteCreateFlow(serverInformation);
@@ -436,6 +447,22 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
Messenger.Send(new BackBreadcrumNavigationRequested());
}
+ private void CompleteWizardFlow(CustomServerInformation serverInformation)
+ {
+ serverInformation.Id = Guid.NewGuid();
+ serverInformation.AccountId = Guid.Empty;
+
+ _wizardContext.ImapCalDavSetupResult = new ImapCalDavSetupResult
+ {
+ DisplayName = DisplayName.Trim(),
+ EmailAddress = EmailAddress.Trim(),
+ IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled,
+ ServerInformation = serverInformation
+ };
+
+ Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step3Title, WinoPage.AccountSetupProgressPage));
+ }
+
[RelayCommand]
private Task ShowLocalCalendarExplanationAsync()
=> _mailDialogService.ShowMessageAsync(
diff --git a/Wino.Mail.ViewModels/ProviderSelectionPageViewModel.cs b/Wino.Mail.ViewModels/ProviderSelectionPageViewModel.cs
new file mode 100644
index 00000000..c5480a68
--- /dev/null
+++ b/Wino.Mail.ViewModels/ProviderSelectionPageViewModel.cs
@@ -0,0 +1,122 @@
+using System.Collections.Generic;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.Messaging;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Navigation;
+using Wino.Core.ViewModels.Data;
+using Wino.Mail.ViewModels.Data;
+using Wino.Messaging.Client.Navigation;
+
+namespace Wino.Mail.ViewModels;
+
+public partial class ProviderSelectionPageViewModel : MailBaseViewModel
+{
+ private readonly IProviderService _providerService;
+ private readonly INewThemeService _themeService;
+
+ public WelcomeWizardContext WizardContext { get; }
+
+ public List Providers { get; private set; } = [];
+ public List AvailableColors { get; private set; } = [];
+
+ [ObservableProperty]
+ public partial IProviderDetail SelectedProvider { get; set; }
+
+ [ObservableProperty]
+ public partial AppColorViewModel SelectedColor { get; set; }
+
+ [ObservableProperty]
+ public partial string AccountName { get; set; }
+
+ [ObservableProperty]
+ public partial bool CanProceed { get; set; }
+
+ public bool IsColorSelected => SelectedColor != null;
+
+ public ProviderSelectionPageViewModel(
+ IProviderService providerService,
+ INewThemeService themeService,
+ WelcomeWizardContext wizardContext)
+ {
+ _providerService = providerService;
+ _themeService = themeService;
+ WizardContext = wizardContext;
+ }
+
+ public override void OnNavigatedTo(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedTo(mode, parameters);
+
+ Providers = _providerService.GetAvailableProviders();
+ AvailableColors = _themeService.GetAvailableAccountColors()
+ .Select(hex => new AppColorViewModel(hex))
+ .ToList();
+
+ // Restore from wizard context if navigating back
+ if (WizardContext.SelectedProvider != null)
+ {
+ SelectedProvider = Providers.FirstOrDefault(p =>
+ p.Type == WizardContext.SelectedProvider.Type &&
+ p.SpecialImapProvider == WizardContext.SelectedProvider.SpecialImapProvider);
+ AccountName = WizardContext.AccountName;
+
+ if (WizardContext.AccountColorHex != null)
+ SelectedColor = AvailableColors.FirstOrDefault(c => c.Hex == WizardContext.AccountColorHex);
+ }
+
+ Validate();
+ }
+
+ partial void OnSelectedProviderChanged(IProviderDetail value) => Validate();
+ partial void OnAccountNameChanged(string value) => Validate();
+ partial void OnSelectedColorChanged(AppColorViewModel value) => OnPropertyChanged(nameof(IsColorSelected));
+
+ [RelayCommand]
+ private void ClearColor() => SelectedColor = null;
+
+ private void Validate()
+ {
+ CanProceed = SelectedProvider != null && !string.IsNullOrWhiteSpace(AccountName);
+ }
+
+ [RelayCommand]
+ private void Proceed()
+ {
+ if (!CanProceed) return;
+
+ // Persist to wizard context
+ WizardContext.SelectedProvider = SelectedProvider;
+ WizardContext.AccountName = AccountName?.Trim();
+ WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
+
+ if (WizardContext.IsGenericImap)
+ {
+ // Navigate to ImapCalDavSettingsPage in wizard mode
+ var context = ImapCalDavSettingsNavigationContext.CreateForWizardMode(
+ WizardContext.BuildAccountCreationDialogResult());
+
+ Messenger.Send(new BreadcrumbNavigationRequested(
+ Translator.ImapCalDavSettingsPage_TitleCreate,
+ WinoPage.ImapCalDavSettingsPage,
+ context));
+ }
+ else if (SelectedProvider.SpecialImapProvider is SpecialImapProvider.iCloud or SpecialImapProvider.Yahoo)
+ {
+ // Navigate to credentials page for special IMAP providers
+ Messenger.Send(new BreadcrumbNavigationRequested(
+ SelectedProvider.Name,
+ WinoPage.SpecialImapCredentialsPage));
+ }
+ else
+ {
+ // OAuth — go directly to progress page
+ Messenger.Send(new BreadcrumbNavigationRequested(
+ Translator.WelcomeWizard_Step3Title,
+ WinoPage.AccountSetupProgressPage));
+ }
+ }
+}
diff --git a/Wino.Mail.ViewModels/SpecialImapCredentialsPageViewModel.cs b/Wino.Mail.ViewModels/SpecialImapCredentialsPageViewModel.cs
new file mode 100644
index 00000000..9ba42445
--- /dev/null
+++ b/Wino.Mail.ViewModels/SpecialImapCredentialsPageViewModel.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.Messaging;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Navigation;
+using Wino.Mail.ViewModels.Data;
+using Wino.Messaging.Client.Navigation;
+
+namespace Wino.Mail.ViewModels;
+
+public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
+{
+ private static readonly Dictionary AppPasswordHelpLinks = new()
+ {
+ { SpecialImapProvider.iCloud, "https://support.apple.com/en-us/102654" },
+ { SpecialImapProvider.Yahoo, "http://help.yahoo.com/kb/SLN15241.html" },
+ };
+
+ private readonly INativeAppService _nativeAppService;
+
+ public WelcomeWizardContext WizardContext { get; }
+
+ [ObservableProperty]
+ public partial string DisplayName { get; set; }
+
+ [ObservableProperty]
+ public partial string EmailAddress { get; set; }
+
+ [ObservableProperty]
+ public partial string AppSpecificPassword { get; set; }
+
+ [ObservableProperty]
+ public partial int SelectedCalendarModeIndex { get; set; }
+
+ [ObservableProperty]
+ public partial bool CanProceed { get; set; }
+
+ public string AppPasswordHelpUrl
+ {
+ get
+ {
+ if (WizardContext.SelectedProvider == null) return null;
+ AppPasswordHelpLinks.TryGetValue(WizardContext.SelectedProvider.SpecialImapProvider, out var url);
+ return url;
+ }
+ }
+
+ public string CalendarModeCalDavDescription
+ => WizardContext.SelectedProvider?.SpecialImapProvider == SpecialImapProvider.iCloud
+ ? Translator.ProviderSelection_CalendarMode_CalDavDescription_Apple
+ : Translator.ProviderSelection_CalendarMode_CalDavDescription_Yahoo;
+
+ public SpecialImapCredentialsPageViewModel(
+ INativeAppService nativeAppService,
+ WelcomeWizardContext wizardContext)
+ {
+ _nativeAppService = nativeAppService;
+ WizardContext = wizardContext;
+ }
+
+ public override void OnNavigatedTo(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedTo(mode, parameters);
+
+ // Restore from context when navigating back
+ DisplayName = WizardContext.DisplayName;
+ EmailAddress = WizardContext.EmailAddress;
+ AppSpecificPassword = WizardContext.AppSpecificPassword;
+
+ SelectedCalendarModeIndex = WizardContext.CalendarSupportMode switch
+ {
+ ImapCalendarSupportMode.CalDav => 1,
+ ImapCalendarSupportMode.LocalOnly => 2,
+ _ => 0
+ };
+
+ OnPropertyChanged(nameof(AppPasswordHelpUrl));
+ OnPropertyChanged(nameof(CalendarModeCalDavDescription));
+
+ Validate();
+ }
+
+ partial void OnDisplayNameChanged(string value) => Validate();
+ partial void OnEmailAddressChanged(string value) => Validate();
+ partial void OnAppSpecificPasswordChanged(string value) => Validate();
+
+ private void Validate()
+ {
+ CanProceed = !string.IsNullOrWhiteSpace(DisplayName)
+ && !string.IsNullOrWhiteSpace(EmailAddress)
+ && EmailValidation.EmailValidator.Validate(EmailAddress ?? string.Empty)
+ && !string.IsNullOrWhiteSpace(AppSpecificPassword);
+ }
+
+ [RelayCommand]
+ private void Proceed()
+ {
+ if (!CanProceed) return;
+
+ WizardContext.DisplayName = DisplayName?.Trim();
+ WizardContext.EmailAddress = EmailAddress?.Trim();
+ WizardContext.AppSpecificPassword = AppSpecificPassword?.Trim();
+ WizardContext.CalendarSupportMode = SelectedCalendarModeIndex switch
+ {
+ 1 => ImapCalendarSupportMode.CalDav,
+ 2 => ImapCalendarSupportMode.LocalOnly,
+ _ => ImapCalendarSupportMode.Disabled
+ };
+
+ Messenger.Send(new BreadcrumbNavigationRequested(
+ Translator.WelcomeWizard_Step3Title,
+ WinoPage.AccountSetupProgressPage));
+ }
+
+ [RelayCommand]
+ private async Task OpenAppPasswordHelp()
+ {
+ var url = AppPasswordHelpUrl;
+ if (url != null)
+ await _nativeAppService.LaunchUriAsync(new Uri(url));
+ }
+}
diff --git a/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs b/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs
index 3e086a92..cb190e8b 100644
--- a/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs
+++ b/Wino.Mail.ViewModels/WelcomePageV2ViewModel.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Updates;
@@ -40,6 +42,8 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
[RelayCommand]
private void GetStarted()
{
- Messenger.Send(new GetStartedFromWelcomeRequested());
+ Messenger.Send(new BreadcrumbNavigationRequested(
+ Translator.WelcomeWizard_Step2Title,
+ WinoPage.ProviderSelectionPage));
}
}
diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs
index 526c0b13..1da0a846 100644
--- a/Wino.Mail.WinUI/App.xaml.cs
+++ b/Wino.Mail.WinUI/App.xaml.cs
@@ -23,9 +23,9 @@ using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
-using Wino.Core.Domain.Models.Updates;
using Wino.Mail.Services;
using Wino.Mail.ViewModels;
+using Wino.Mail.ViewModels.Data;
using Wino.Mail.WinUI.Activation;
using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models;
@@ -42,6 +42,7 @@ public partial class App : WinoApplication,
IRecipient,
IRecipient,
IRecipient,
+ IRecipient,
IRecipient
{
private const int InboxSyncsPerFullSync = 20;
@@ -146,6 +147,10 @@ public partial class App : WinoApplication,
services.AddTransient(typeof(AccountManagementViewModel));
services.AddTransient(typeof(WelcomePageViewModel));
services.AddTransient(typeof(WelcomePageV2ViewModel));
+ services.AddTransient(typeof(ProviderSelectionPageViewModel));
+ services.AddTransient(typeof(AccountSetupProgressPageViewModel));
+ services.AddTransient(typeof(SpecialImapCredentialsPageViewModel));
+ services.AddSingleton(typeof(WelcomeWizardContext));
services.AddTransient(typeof(ComposePageViewModel));
services.AddTransient(typeof(IdlePageViewModel));
@@ -218,18 +223,10 @@ public partial class App : WinoApplication,
return;
}
- // Check whether the new version requires a migration before starting sync.
- var updateManager = Services.GetRequiredService();
- var updateNotes = await updateManager.GetLatestUpdateNotesAsync();
- bool hasPendingMigration = updateNotes.HasPendingMigrations && 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();
+ RestartAutoSynchronizationLoop();
// Check if launched from toast notification.
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs))
@@ -253,16 +250,7 @@ public partial class App : WinoApplication,
// 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().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).");
- }
+ LogActivation("Launched by startup task. Window created but hidden (system tray only).");
}
else
{
@@ -305,13 +293,6 @@ 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 &&
@@ -341,29 +322,6 @@ public partial class App : WinoApplication,
}
}
- ///
- /// 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.
- ///
- 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();
@@ -593,7 +551,7 @@ public partial class App : WinoApplication,
InitializeNavigationDispatcher();
Services.GetRequiredService()
- .Navigate(WinoPage.WelcomePageV2, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
+ .Navigate(WinoPage.WelcomeHostPage, null, NavigationReferenceFrame.ShellFrame, NavigationTransitionType.None);
}
private void InitializeNavigationDispatcher()
@@ -627,6 +585,7 @@ public partial class App : WinoApplication,
WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
+ WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
}
@@ -699,6 +658,29 @@ public partial class App : WinoApplication,
});
}
+ public void Receive(AccountRemovedMessage message)
+ {
+ var windowManager = Services.GetRequiredService();
+
+ // Only handle when ShellWindow is active (not during wizard rollback)
+ if (windowManager.GetWindow(WinoWindowKind.Shell) == null)
+ return;
+
+ MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
+ {
+ var accounts = await _accountService!.GetAccountsAsync();
+ if (accounts.Any()) return;
+
+ // All accounts removed — go back to welcome wizard from step 1
+ Services.GetRequiredService().Reset();
+ StopAutoSynchronizationLoop();
+ CreateWelcomeWindow();
+ windowManager.HideWindow(WinoWindowKind.Shell);
+ await NewThemeService.ApplyThemeToActiveWindowAsync();
+ MainWindow?.Activate();
+ });
+ }
+
public void Receive(GetStartedFromWelcomeRequested message)
{
var windowManager = Services.GetRequiredService();
diff --git a/Wino.Mail.WinUI/AppThemes/Clouds.xaml b/Wino.Mail.WinUI/AppThemes/Clouds.xaml
index d0af4a96..ee46664a 100644
--- a/Wino.Mail.WinUI/AppThemes/Clouds.xaml
+++ b/Wino.Mail.WinUI/AppThemes/Clouds.xaml
@@ -4,7 +4,7 @@
xmlns:xaml="using:Microsoft.UI.Xaml">
Clouds
- ms-appx:///Wino.Mail.WinUI/BackgroundImages/Clouds.jpg
+ ms-appx:///BackgroundImages/Clouds.jpg
Transparent
diff --git a/Wino.Mail.WinUI/AppThemes/Forest.xaml b/Wino.Mail.WinUI/AppThemes/Forest.xaml
index 265a0843..8c8a26ba 100644
--- a/Wino.Mail.WinUI/AppThemes/Forest.xaml
+++ b/Wino.Mail.WinUI/AppThemes/Forest.xaml
@@ -4,7 +4,7 @@
xmlns:xaml="using:Microsoft.UI.Xaml">
Forest
- ms-appx:///Wino.Mail.WinUI/BackgroundImages/Forest.jpg
+ ms-appx:///BackgroundImages/Forest.jpg
Transparent
diff --git a/Wino.Mail.WinUI/AppThemes/Garden.xaml b/Wino.Mail.WinUI/AppThemes/Garden.xaml
index 830f43de..3dbe1256 100644
--- a/Wino.Mail.WinUI/AppThemes/Garden.xaml
+++ b/Wino.Mail.WinUI/AppThemes/Garden.xaml
@@ -4,7 +4,7 @@
xmlns:xaml="using:Microsoft.UI.Xaml">
Garden
- ms-appx:///Wino.Mail.WinUI/BackgroundImages/Garden.jpg
+ ms-appx:///BackgroundImages/Garden.jpg
Transparent
diff --git a/Wino.Mail.WinUI/AppThemes/Nighty.xaml b/Wino.Mail.WinUI/AppThemes/Nighty.xaml
index 382f0b04..b611d0c6 100644
--- a/Wino.Mail.WinUI/AppThemes/Nighty.xaml
+++ b/Wino.Mail.WinUI/AppThemes/Nighty.xaml
@@ -4,7 +4,7 @@
xmlns:xaml="using:Microsoft.UI.Xaml">
Nighty
- ms-appx:///Wino.Mail.WinUI/BackgroundImages/Nighty.jpg
+ ms-appx:///BackgroundImages/Nighty.jpg
Transparent
diff --git a/Wino.Mail.WinUI/AppThemes/Snowflake.xaml b/Wino.Mail.WinUI/AppThemes/Snowflake.xaml
index 9e857f3e..34137686 100644
--- a/Wino.Mail.WinUI/AppThemes/Snowflake.xaml
+++ b/Wino.Mail.WinUI/AppThemes/Snowflake.xaml
@@ -4,7 +4,7 @@
xmlns:xaml="using:Microsoft.UI.Xaml">
Snowflake
- ms-appx:///Wino.Mail.WinUI/BackgroundImages/Snowflake.jpg
+ ms-appx:///BackgroundImages/Snowflake.jpg
Transparent
diff --git a/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json b/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json
index a1509ff4..f0c08b09 100644
--- a/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json
+++ b/Wino.Mail.WinUI/Assets/UpdateNotes/vnext.json
@@ -1,9 +1,4 @@
{
- "hasPendingMigrations": true,
- "migration": {
- "titleKey": "WhatIsNew_MigrationPreparing_Title",
- "descriptionKey": "WhatIsNew_MigrationPreparing_Description"
- },
"sections": [
{
"title": "# Wino Calendar is here!",
diff --git a/Wino.Mail.WinUI/CoreUWPContainerSetup.cs b/Wino.Mail.WinUI/CoreUWPContainerSetup.cs
index 12780859..bf729cab 100644
--- a/Wino.Mail.WinUI/CoreUWPContainerSetup.cs
+++ b/Wino.Mail.WinUI/CoreUWPContainerSetup.cs
@@ -49,6 +49,7 @@ public static class CoreUWPContainerSetup
services.AddTransient(typeof(AboutPageViewModel));
services.AddTransient(typeof(SettingsPageViewModel));
services.AddTransient(typeof(ManageAccountsPagePageViewModel));
+ services.AddTransient(typeof(WelcomeHostPageViewModel));
services.AddTransient(typeof(KeyboardShortcutsPageViewModel));
}
}
diff --git a/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml b/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml
index c9dcfffe..56e857dd 100644
--- a/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml
+++ b/Wino.Mail.WinUI/Dialogs/WhatIsNewDialog.xaml
@@ -22,40 +22,16 @@
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
@@ -40,10 +36,7 @@
-
+
@@ -56,15 +49,14 @@
-
-
+
diff --git a/Wino.Mail.WinUI/Views/Abstract/AccountSetupProgressPageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/AccountSetupProgressPageAbstract.cs
new file mode 100644
index 00000000..8b0a2ee3
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Abstract/AccountSetupProgressPageAbstract.cs
@@ -0,0 +1,8 @@
+using Wino.Mail.ViewModels;
+using Wino.Mail.WinUI;
+
+namespace Wino.Mail.WinUI.Views.Abstract;
+
+public abstract class AccountSetupProgressPageAbstract : BasePage
+{
+}
diff --git a/Wino.Mail.WinUI/Views/Abstract/ProviderSelectionPageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/ProviderSelectionPageAbstract.cs
new file mode 100644
index 00000000..8c65495a
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Abstract/ProviderSelectionPageAbstract.cs
@@ -0,0 +1,8 @@
+using Wino.Mail.ViewModels;
+using Wino.Mail.WinUI;
+
+namespace Wino.Mail.WinUI.Views.Abstract;
+
+public abstract class ProviderSelectionPageAbstract : BasePage
+{
+}
diff --git a/Wino.Mail.WinUI/Views/Abstract/SpecialImapCredentialsPageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/SpecialImapCredentialsPageAbstract.cs
new file mode 100644
index 00000000..7d5fb3bb
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Abstract/SpecialImapCredentialsPageAbstract.cs
@@ -0,0 +1,8 @@
+using Wino.Mail.ViewModels;
+using Wino.Mail.WinUI;
+
+namespace Wino.Mail.WinUI.Views.Abstract;
+
+public abstract class SpecialImapCredentialsPageAbstract : BasePage
+{
+}
diff --git a/Wino.Mail.WinUI/Views/Abstract/WelcomeHostPageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/WelcomeHostPageAbstract.cs
new file mode 100644
index 00000000..8f91bcbd
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/Abstract/WelcomeHostPageAbstract.cs
@@ -0,0 +1,8 @@
+using Wino.Core.ViewModels;
+using Wino.Mail.WinUI;
+
+namespace Wino.Mail.WinUI.Views.Abstract;
+
+public abstract class WelcomeHostPageAbstract : BasePage
+{
+}
diff --git a/Wino.Mail.WinUI/Views/Account/ImapCalDavSettingsPage.xaml b/Wino.Mail.WinUI/Views/Account/ImapCalDavSettingsPage.xaml
index dfd77b9b..8b3dee34 100644
--- a/Wino.Mail.WinUI/Views/Account/ImapCalDavSettingsPage.xaml
+++ b/Wino.Mail.WinUI/Views/Account/ImapCalDavSettingsPage.xaml
@@ -78,13 +78,9 @@
Text="{x:Bind ViewModel.EmailAddress, Mode=TwoWay}" />
-
+
-
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/AccountSetupProgressPage.xaml.cs b/Wino.Mail.WinUI/Views/AccountSetupProgressPage.xaml.cs
new file mode 100644
index 00000000..e0802503
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/AccountSetupProgressPage.xaml.cs
@@ -0,0 +1,11 @@
+using Wino.Mail.WinUI.Views.Abstract;
+
+namespace Wino.Views;
+
+public sealed partial class AccountSetupProgressPage : AccountSetupProgressPageAbstract
+{
+ public AccountSetupProgressPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml
index 629cfc53..432712fe 100644
--- a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml
+++ b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml
@@ -267,10 +267,7 @@
-
+
diff --git a/Wino.Mail.WinUI/Views/ProviderSelectionPage.xaml b/Wino.Mail.WinUI/Views/ProviderSelectionPage.xaml
new file mode 100644
index 00000000..7ba1fe99
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/ProviderSelectionPage.xaml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/ProviderSelectionPage.xaml.cs b/Wino.Mail.WinUI/Views/ProviderSelectionPage.xaml.cs
new file mode 100644
index 00000000..4749969c
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/ProviderSelectionPage.xaml.cs
@@ -0,0 +1,17 @@
+using Microsoft.UI.Xaml.Controls;
+using Wino.Mail.WinUI.Views.Abstract;
+
+namespace Wino.Views;
+
+public sealed partial class ProviderSelectionPage : ProviderSelectionPageAbstract
+{
+ public ProviderSelectionPage()
+ {
+ InitializeComponent();
+ }
+
+ private void ProviderSelectionChanged(ItemsView sender, ItemsViewSelectionChangedEventArgs args)
+ {
+ ViewModel.SelectedProvider = sender.SelectedItem as Wino.Core.Domain.Interfaces.IProviderDetail;
+ }
+}
diff --git a/Wino.Mail.WinUI/Views/SpecialImapCredentialsPage.xaml b/Wino.Mail.WinUI/Views/SpecialImapCredentialsPage.xaml
new file mode 100644
index 00000000..70232e37
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/SpecialImapCredentialsPage.xaml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/SpecialImapCredentialsPage.xaml.cs b/Wino.Mail.WinUI/Views/SpecialImapCredentialsPage.xaml.cs
new file mode 100644
index 00000000..71c2b6e4
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/SpecialImapCredentialsPage.xaml.cs
@@ -0,0 +1,25 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Wino.Mail.WinUI.Views.Abstract;
+
+namespace Wino.Views;
+
+public sealed partial class SpecialImapCredentialsPage : SpecialImapCredentialsPageAbstract
+{
+ public SpecialImapCredentialsPage()
+ {
+ InitializeComponent();
+ }
+
+ private void CalendarModeSelectionChanged(object sender, SelectionChangedEventArgs args)
+ {
+ if (sender is ListView lv)
+ ViewModel.SelectedCalendarModeIndex = lv.SelectedIndex;
+ }
+
+ private void AppPasswordChanged(object sender, RoutedEventArgs e)
+ {
+ if (sender is PasswordBox pb)
+ ViewModel.AppSpecificPassword = pb.Password;
+ }
+}
diff --git a/Wino.Mail.WinUI/Views/WelcomeHostPage.xaml b/Wino.Mail.WinUI/Views/WelcomeHostPage.xaml
new file mode 100644
index 00000000..4d37e67e
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/WelcomeHostPage.xaml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Mail.WinUI/Views/WelcomeHostPage.xaml.cs b/Wino.Mail.WinUI/Views/WelcomeHostPage.xaml.cs
new file mode 100644
index 00000000..be051b59
--- /dev/null
+++ b/Wino.Mail.WinUI/Views/WelcomeHostPage.xaml.cs
@@ -0,0 +1,96 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using CommunityToolkit.Mvvm.Messaging;
+using Microsoft.UI.Xaml.Media.Animation;
+using Microsoft.UI.Xaml.Navigation;
+using MoreLinq;
+using Wino.Core.Domain.Enums;
+using Wino.Mail.ViewModels.Data;
+using Wino.Mail.WinUI.Views.Abstract;
+using Wino.Messaging.Client.Navigation;
+
+namespace Wino.Views;
+
+public sealed partial class WelcomeHostPage : WelcomeHostPageAbstract,
+ IRecipient,
+ IRecipient
+{
+ public ObservableCollection PageHistory { get; set; } = [];
+
+ public WelcomeHostPage()
+ {
+ InitializeComponent();
+ }
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ base.OnNavigatedTo(e);
+
+ WeakReferenceMessenger.Default.Register(this);
+ WeakReferenceMessenger.Default.Register(this);
+
+ // Navigate to the welcome/get-started page without adding it to the wizard breadcrumb.
+ // Breadcrumb steps only start after the user clicks "Get Started".
+ var welcomePageType = ViewModel.NavigationService.GetPageType(WinoPage.WelcomePageV2);
+ WizardFrame.Navigate(welcomePageType, null, new SuppressNavigationTransitionInfo());
+ }
+
+ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
+ {
+ WeakReferenceMessenger.Default.Unregister(this);
+ WeakReferenceMessenger.Default.Unregister(this);
+
+ base.OnNavigatingFrom(e);
+ }
+
+ public void Receive(BreadcrumbNavigationRequested message)
+ {
+ var pageType = ViewModel.NavigationService.GetPageType(message.PageType);
+ if (pageType == null) return;
+
+ WizardFrame.Navigate(pageType, message.Parameter, new SlideNavigationTransitionInfo
+ {
+ Effect = SlideNavigationTransitionEffect.FromRight
+ });
+
+ PageHistory.ForEach(a => a.IsActive = false);
+ PageHistory.Add(new BreadcrumbNavigationItemViewModel(message, isActive: true, stepNumber: PageHistory.Count + 1));
+ }
+
+ public void Receive(BackBreadcrumNavigationRequested message)
+ {
+ GoBackFrame();
+ }
+
+ private void GoBackFrame()
+ {
+ if (!WizardFrame.CanGoBack) return;
+
+ PageHistory.RemoveAt(PageHistory.Count - 1);
+ WizardFrame.GoBack(new SlideNavigationTransitionInfo
+ {
+ Effect = SlideNavigationTransitionEffect.FromLeft
+ });
+
+ if (PageHistory.Count > 0)
+ {
+ PageHistory.ForEach(a => a.IsActive = false);
+ PageHistory[PageHistory.Count - 1].IsActive = true;
+ }
+ }
+
+ private void BreadItemClicked(Microsoft.UI.Xaml.Controls.BreadcrumbBar sender, Microsoft.UI.Xaml.Controls.BreadcrumbBarItemClickedEventArgs args)
+ {
+ var clickedItem = PageHistory[args.Index];
+ var currentActive = PageHistory.FirstOrDefault(a => a.IsActive);
+
+ // Only allow navigating backwards (clicking items before current)
+ if (currentActive == null || args.Index >= PageHistory.IndexOf(currentActive))
+ return;
+
+ while (PageHistory.FirstOrDefault(a => a.IsActive) != clickedItem && WizardFrame.CanGoBack)
+ {
+ GoBackFrame();
+ }
+ }
+}
diff --git a/Wino.Mail.WinUI/WelcomeWindow.xaml.cs b/Wino.Mail.WinUI/WelcomeWindow.xaml.cs
index 0f192d14..63c16556 100644
--- a/Wino.Mail.WinUI/WelcomeWindow.xaml.cs
+++ b/Wino.Mail.WinUI/WelcomeWindow.xaml.cs
@@ -29,9 +29,6 @@ public sealed partial class WelcomeWindow : WindowEx
Width = 980;
Height = 720;
- //this.IsResizable = false;
- //this.IsMaximizable = false;
-
this.CenterOnScreen();
var themeService = WinoApplication.Current.Services.GetService();
diff --git a/Wino.Services/Migrations/VNextDelayMigration.cs b/Wino.Services/Migrations/VNextDelayMigration.cs
deleted file mode 100644
index 16790661..00000000
--- a/Wino.Services/Migrations/VNextDelayMigration.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Threading.Tasks;
-using Wino.Core.Domain.Interfaces;
-
-namespace Wino.Services.Migrations;
-
-public class VNextDelayMigration : IAppMigration
-{
- public string MigrationId => "vnext-delay";
-
- public Task ExecuteAsync() => Task.Delay(3000);
-}
diff --git a/Wino.Services/UpdateManager.cs b/Wino.Services/UpdateManager.cs
index b5be9131..a195068f 100644
--- a/Wino.Services/UpdateManager.cs
+++ b/Wino.Services/UpdateManager.cs
@@ -1,12 +1,10 @@
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;
-using Wino.Services.Migrations;
namespace Wino.Services;
@@ -15,12 +13,10 @@ public class UpdateManager : IUpdateManager
private const string UpdateNotesResourcePath = "ms-appx:///Assets/UpdateNotes/vnext.json";
private const string FeaturesResourcePath = "ms-appx:///Assets/UpdateNotes/features.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 _migrations = [];
private string _versionSeenKey = string.Empty;
private UpdateNotes _latestUpdateNotes = new();
@@ -32,7 +28,6 @@ public class UpdateManager : IUpdateManager
_fileService = fileService;
_configurationService = configurationService;
_nativeAppService = nativeAppService;
- _migrations.Add(new VNextDelayMigration());
}
private string GetVersionSeenKey()
@@ -69,7 +64,6 @@ public class UpdateManager : IUpdateManager
}
}
- // T
public bool ShouldShowUpdateNotes()
=> !_configurationService.Get(GetVersionSeenKey(), false);
@@ -92,35 +86,4 @@ public class UpdateManager : IUpdateManager
public void MarkUpdateNotesAsSeen()
=> _configurationService.Set(GetVersionSeenKey(), true);
-
- public bool HasPendingMigrations()
- {
- if (!_latestUpdateNotes.HasPendingMigrations)
- return false;
-
- return _migrations.Any(m => !_configurationService.Get(string.Format(MigrationCompletedKeyFormat, m.MigrationId), false));
- }
-
- public async Task RunPendingMigrationsAsync()
- {
- if (!_latestUpdateNotes.HasPendingMigrations)
- _latestUpdateNotes = await GetLatestUpdateNotesAsync();
-
- if (!_latestUpdateNotes.HasPendingMigrations)
- return;
-
- 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 migrations)
- => _migrations.AddRange(migrations);
}