Removed migrations. New onboarding screen and wizard like steps.
This commit is contained in:
@@ -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<AccountSetupStepModel> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user