Removed migrations. New onboarding screen and wizard like steps.
This commit is contained in:
@@ -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<ImapCalDavSetupResult>();
|
||||
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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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<IProviderDetail> Providers { get; private set; } = [];
|
||||
public List<AppColorViewModel> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SpecialImapProvider, string> 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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user