Files
Wino-Mail/Wino.Core.ViewModels/WinoAccountManagementPageViewModel.cs
T

375 lines
14 KiB
C#
Raw Normal View History

2026-03-18 09:00:26 +01:00
#nullable enable
using System;
using System.Globalization;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
2026-03-18 17:43:56 +01:00
using CommunityToolkit.Mvvm.Messaging;
2026-03-18 09:00:26 +01:00
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.Navigation;
using Wino.Mail.Api.Contracts.Ai;
using Wino.Mail.Api.Contracts.Auth;
2026-03-18 17:43:56 +01:00
using Wino.Messaging.UI;
2026-03-18 09:00:26 +01:00
namespace Wino.Core.ViewModels;
2026-03-18 17:43:56 +01:00
public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
IRecipient<WinoAccountSignedInMessage>,
IRecipient<WinoAccountSignedOutMessage>
2026-03-18 09:00:26 +01:00
{
2026-03-18 17:43:56 +01:00
private const string AiPackProductId = "ai-pack-monthly";
2026-03-18 10:25:07 +01:00
private const string ManageAiPackUrl = "https://example.com/wino-ai-pack/manage";
2026-03-18 09:00:26 +01:00
private readonly IWinoAccountProfileService _profileService;
private readonly IMailDialogService _dialogService;
private readonly INativeAppService _nativeAppService;
[ObservableProperty]
public partial bool IsBusy { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsSignedOut))]
[NotifyPropertyChangedFor(nameof(CanShowBuyAiPack))]
public partial bool IsSignedIn { get; set; }
2026-03-18 10:25:07 +01:00
[ObservableProperty]
public partial string AccountDisplayName { get; set; } = string.Empty;
[ObservableProperty]
public partial string AccountInitials { get; set; } = string.Empty;
2026-03-18 09:00:26 +01:00
[ObservableProperty]
public partial string AccountEmail { get; set; } = string.Empty;
[ObservableProperty]
public partial string AccountStatusText { get; set; } = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanShowAiUsage))]
[NotifyPropertyChangedFor(nameof(CanShowBuyAiPack))]
public partial bool HasAiPack { get; set; }
[ObservableProperty]
public partial string AiPackStateText { get; set; } = Translator.WinoAccount_Management_AiPackInactive;
[ObservableProperty]
public partial string AiUsageSummary { get; set; } = string.Empty;
[ObservableProperty]
public partial string AiBillingPeriodSummary { get; set; } = string.Empty;
2026-03-18 10:25:07 +01:00
[ObservableProperty]
public partial string AiPackRenewalText { get; set; } = string.Empty;
[ObservableProperty]
public partial int AiUsageCount { get; set; }
[ObservableProperty]
public partial int AiUsageLimit { get; set; } = 1;
[ObservableProperty]
public partial double AiUsagePercentage { get; set; }
[ObservableProperty]
public partial string AiUsageResetText { get; set; } = string.Empty;
2026-03-18 17:43:56 +01:00
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanBuyAiPack))]
public partial bool IsAiPackCheckoutInProgress { get; set; }
2026-03-18 09:00:26 +01:00
public bool IsSignedOut => !IsSignedIn;
public bool CanShowAiUsage => HasAiPack;
public bool CanShowBuyAiPack => IsSignedIn && !HasAiPack;
2026-03-18 17:43:56 +01:00
public bool CanBuyAiPack => !IsAiPackCheckoutInProgress;
2026-03-18 09:00:26 +01:00
public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService,
IMailDialogService dialogService,
INativeAppService nativeAppService)
{
_profileService = profileService;
_dialogService = dialogService;
_nativeAppService = nativeAppService;
}
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
_ = LoadAsync();
}
[RelayCommand]
private async Task RegisterAsync()
{
var account = await _dialogService.ShowWinoAccountRegistrationDialogAsync();
if (account == null)
{
return;
}
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
string.Format(Translator.WinoAccount_RegisterSuccessMessage, account.Email),
InfoBarMessageType.Success);
}
[RelayCommand]
private async Task SignInAsync()
{
var account = await _dialogService.ShowWinoAccountLoginDialogAsync();
if (account == null)
{
return;
}
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
string.Format(Translator.WinoAccount_LoginSuccessMessage, account.Email),
InfoBarMessageType.Success);
}
[RelayCommand]
private async Task SignOutAsync()
{
var account = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
if (account == null)
{
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
Translator.WinoAccount_SignOut_NoAccountMessage,
InfoBarMessageType.Warning);
return;
}
await _profileService.SignOutAsync().ConfigureAwait(false);
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
string.Format(Translator.WinoAccount_SignOut_SuccessMessage, account.Email),
InfoBarMessageType.Success);
}
[RelayCommand]
2026-03-18 17:43:56 +01:00
private async Task BuyAiPackAsync()
2026-03-18 09:00:26 +01:00
{
2026-03-18 17:43:56 +01:00
if (IsAiPackCheckoutInProgress)
2026-03-18 09:00:26 +01:00
{
2026-03-18 17:43:56 +01:00
return;
}
2026-03-18 09:00:26 +01:00
2026-03-18 17:43:56 +01:00
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false);
2026-03-18 09:00:26 +01:00
2026-03-18 17:43:56 +01:00
if (account == null)
2026-03-18 09:00:26 +01:00
{
2026-03-18 17:43:56 +01:00
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
Translator.WinoAccount_Management_PurchaseRequiresSignIn,
InfoBarMessageType.Warning);
2026-03-18 09:00:26 +01:00
return;
}
2026-03-18 17:43:56 +01:00
await ExecuteUIThread(() => IsAiPackCheckoutInProgress = true);
2026-03-18 09:00:26 +01:00
try
{
2026-03-18 17:43:56 +01:00
var checkoutSession = await _profileService.CreateCheckoutSessionAsync(AiPackProductId).ConfigureAwait(false);
if (!checkoutSession.IsSuccess || string.IsNullOrWhiteSpace(checkoutSession.Result))
2026-03-18 09:00:26 +01:00
{
2026-03-18 17:43:56 +01:00
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
Translator.WinoAccount_Management_PurchaseStartFailed,
InfoBarMessageType.Error);
2026-03-18 09:00:26 +01:00
return;
}
2026-03-18 17:43:56 +01:00
var isLaunched = await _nativeAppService.LaunchUriAsync(new Uri(checkoutSession.Result)).ConfigureAwait(false);
2026-03-18 09:00:26 +01:00
2026-03-18 17:43:56 +01:00
if (!isLaunched)
2026-03-18 09:00:26 +01:00
{
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
2026-03-18 17:43:56 +01:00
Translator.WinoAccount_Management_PurchaseStartFailed,
2026-03-18 09:00:26 +01:00
InfoBarMessageType.Error);
}
2026-03-18 17:43:56 +01:00
}
catch (OperationCanceledException)
{
2026-03-18 09:00:26 +01:00
}
catch (Exception)
{
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
2026-03-18 17:43:56 +01:00
Translator.WinoAccount_Management_PurchaseStartFailed,
2026-03-18 09:00:26 +01:00
InfoBarMessageType.Error);
}
2026-03-18 17:43:56 +01:00
finally
{
await ExecuteUIThread(() => IsAiPackCheckoutInProgress = false);
}
2026-03-18 09:00:26 +01:00
}
[RelayCommand]
2026-03-18 17:43:56 +01:00
private async Task ManageAiPackAsync() => await _nativeAppService.LaunchUriAsync(new Uri(ManageAiPackUrl));
2026-03-18 09:00:26 +01:00
2026-03-18 17:43:56 +01:00
[RelayCommand]
private Task ExportSettingsAsync() => Task.CompletedTask;
2026-03-18 09:00:26 +01:00
2026-03-18 17:43:56 +01:00
[RelayCommand]
private Task ImportSettingsAsync() => Task.CompletedTask;
2026-03-18 09:00:26 +01:00
2026-03-18 17:43:56 +01:00
protected override void RegisterRecipients()
{
base.RegisterRecipients();
2026-03-18 09:00:26 +01:00
2026-03-18 17:43:56 +01:00
Messenger.Register<WinoAccountSignedInMessage>(this);
Messenger.Register<WinoAccountSignedOutMessage>(this);
}
2026-03-18 09:00:26 +01:00
2026-03-18 17:43:56 +01:00
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
2026-03-18 09:00:26 +01:00
2026-03-18 17:43:56 +01:00
Messenger.Unregister<WinoAccountSignedInMessage>(this);
Messenger.Unregister<WinoAccountSignedOutMessage>(this);
2026-03-18 09:00:26 +01:00
}
2026-03-18 17:43:56 +01:00
public void Receive(WinoAccountSignedInMessage message)
=> _ = LoadAsync();
public void Receive(WinoAccountSignedOutMessage message)
=> _ = ResetSignedOutStateAsync();
2026-03-18 09:00:26 +01:00
private async Task LoadAsync()
{
await ExecuteUIThread(() => IsBusy = true);
try
{
2026-03-18 17:43:56 +01:00
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false);
2026-03-18 09:00:26 +01:00
if (account == null)
{
await ResetSignedOutStateAsync();
return;
}
2026-03-18 17:43:56 +01:00
var currentUserResponse = await _profileService.GetCurrentUserAsync().ConfigureAwait(false);
var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(false);
2026-03-18 09:00:26 +01:00
var resolvedUser = currentUserResponse.IsSuccess && currentUserResponse.Result != null
? currentUserResponse.Result
: new AuthUserDto(account.Id, account.Email, account.AccountStatus, account.HasPassword, account.HasGoogleLogin, account.HasFacebookLogin);
await ExecuteUIThread(() =>
{
IsSignedIn = true;
AccountEmail = resolvedUser.Email;
2026-03-18 10:25:07 +01:00
AccountDisplayName = ExtractDisplayName(resolvedUser.Email);
AccountInitials = ExtractInitials(resolvedUser.Email);
2026-03-18 09:00:26 +01:00
AccountStatusText = string.Format(Translator.WinoAccount_Management_StatusLabel, resolvedUser.AccountStatus);
});
UpdateAiPackState(aiStatusResponse.IsSuccess ? aiStatusResponse.Result : null);
if (!currentUserResponse.IsSuccess || !aiStatusResponse.IsSuccess)
{
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
Translator.WinoAccount_Management_LoadFailed,
InfoBarMessageType.Warning);
}
}
catch (Exception)
{
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
Translator.WinoAccount_Management_LoadFailed,
InfoBarMessageType.Error);
await ResetSignedOutStateAsync();
}
finally
{
await ExecuteUIThread(() => IsBusy = false);
}
}
private async Task ResetSignedOutStateAsync()
{
await ExecuteUIThread(() =>
{
IsSignedIn = false;
2026-03-18 10:25:07 +01:00
AccountDisplayName = string.Empty;
AccountInitials = string.Empty;
2026-03-18 09:00:26 +01:00
AccountEmail = string.Empty;
AccountStatusText = string.Empty;
HasAiPack = false;
AiPackStateText = Translator.WinoAccount_Management_AiPackInactive;
AiUsageSummary = string.Empty;
AiBillingPeriodSummary = string.Empty;
2026-03-18 10:25:07 +01:00
AiPackRenewalText = string.Empty;
AiUsageCount = 0;
AiUsageLimit = 1;
AiUsagePercentage = 0;
AiUsageResetText = string.Empty;
2026-03-18 17:43:56 +01:00
IsAiPackCheckoutInProgress = false;
2026-03-18 09:00:26 +01:00
});
}
private void UpdateAiPackState(AiStatusResultDto? aiStatus)
{
var hasAiPack = aiStatus?.HasAiPack == true;
var usageText = Translator.WinoAccount_Management_AiPackUnknownUsage;
var billingText = string.Empty;
2026-03-18 10:25:07 +01:00
var renewalText = string.Empty;
var usageCount = 0;
var usageLimit = 1;
var usagePercentage = 0d;
var resetText = string.Empty;
2026-03-18 09:00:26 +01:00
if (hasAiPack && aiStatus?.Used is int used && aiStatus.MonthlyLimit is int limit && aiStatus.Remaining is int remaining)
{
usageText = string.Format(Translator.WinoAccount_Management_AiPackUsage, used, limit, remaining);
2026-03-18 10:25:07 +01:00
usageCount = used;
usageLimit = limit > 0 ? limit : 1;
usagePercentage = (double)used / usageLimit * 100;
2026-03-18 09:00:26 +01:00
}
if (hasAiPack && aiStatus?.CurrentPeriodStartUtc is DateTimeOffset periodStart && aiStatus.CurrentPeriodEndUtc is DateTimeOffset periodEnd)
{
billingText = string.Format(Translator.WinoAccount_Management_AiPackBillingPeriod, periodStart.LocalDateTime, periodEnd.LocalDateTime);
2026-03-18 10:25:07 +01:00
renewalText = string.Format(Translator.WinoAccount_Management_AiPackRenews, periodEnd.LocalDateTime);
resetText = string.Format(Translator.WinoAccount_Management_AiPackResets, periodEnd.LocalDateTime);
2026-03-18 09:00:26 +01:00
}
_ = ExecuteUIThread(() =>
{
HasAiPack = hasAiPack;
AiPackStateText = hasAiPack
? Translator.WinoAccount_Management_AiPackActive
: Translator.WinoAccount_Management_AiPackInactive;
AiUsageSummary = hasAiPack ? usageText : string.Empty;
AiBillingPeriodSummary = hasAiPack ? billingText : string.Empty;
2026-03-18 10:25:07 +01:00
AiPackRenewalText = hasAiPack ? renewalText : string.Empty;
AiUsageCount = usageCount;
AiUsageLimit = usageLimit;
AiUsagePercentage = usagePercentage;
AiUsageResetText = hasAiPack ? resetText : string.Empty;
2026-03-18 09:00:26 +01:00
});
}
2026-03-18 10:25:07 +01:00
private static string ExtractDisplayName(string email)
{
if (string.IsNullOrWhiteSpace(email))
return string.Empty;
var atIndex = email.IndexOf('@');
var localPart = atIndex > 0 ? email[..atIndex] : email;
if (localPart.Length == 0)
return string.Empty;
return char.ToUpper(localPart[0], CultureInfo.CurrentCulture) + localPart[1..];
}
private static string ExtractInitials(string email)
{
var displayName = ExtractDisplayName(email);
return displayName.Length > 0
? displayName[..1].ToUpper(CultureInfo.CurrentCulture)
: string.Empty;
}
2026-03-18 09:00:26 +01:00
}