2026-03-18 09:00:26 +01:00
|
|
|
#nullable enable
|
|
|
|
|
using System;
|
2026-03-19 01:50:14 +01:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Collections.ObjectModel;
|
|
|
|
|
using System.Linq;
|
2026-03-18 09:00:26 +01:00
|
|
|
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.Enums;
|
|
|
|
|
using Wino.Core.Domain.Interfaces;
|
2026-03-19 01:50:14 +01:00
|
|
|
using Wino.Core.Domain.Models.Accounts;
|
2026-03-18 09:00:26 +01:00
|
|
|
using Wino.Core.Domain.Models.Navigation;
|
2026-03-19 01:50:14 +01:00
|
|
|
using Wino.Mail.Api.Contracts.Billing;
|
|
|
|
|
using Wino.Core.ViewModels.Data;
|
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
|
|
|
{
|
|
|
|
|
private readonly IWinoAccountProfileService _profileService;
|
2026-03-19 01:50:14 +01:00
|
|
|
private readonly IWinoAddOnService _addOnService;
|
2026-03-18 09:00:26 +01:00
|
|
|
private readonly IMailDialogService _dialogService;
|
|
|
|
|
private readonly INativeAppService _nativeAppService;
|
|
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
public ObservableCollection<WinoAddOnItemViewModel> AddOns { get; } = [];
|
|
|
|
|
|
2026-03-18 09:00:26 +01:00
|
|
|
[ObservableProperty]
|
|
|
|
|
public partial bool IsBusy { get; set; }
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsSignedOut))]
|
|
|
|
|
public partial bool IsSignedIn { get; set; }
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
public partial string AccountEmail { get; set; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
public partial string AccountStatusText { get; set; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-03-19 01:50:14 +01:00
|
|
|
[NotifyCanExecuteChangedFor(nameof(PurchaseAddOnCommand))]
|
|
|
|
|
public partial bool IsCheckoutInProgress { get; set; }
|
2026-03-18 17:43:56 +01:00
|
|
|
|
2026-03-18 09:00:26 +01:00
|
|
|
public bool IsSignedOut => !IsSignedIn;
|
|
|
|
|
|
|
|
|
|
public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService,
|
2026-03-19 01:50:14 +01:00
|
|
|
IWinoAddOnService addOnService,
|
2026-03-18 09:00:26 +01:00
|
|
|
IMailDialogService dialogService,
|
|
|
|
|
INativeAppService nativeAppService)
|
|
|
|
|
{
|
|
|
|
|
_profileService = profileService;
|
2026-03-19 01:50:14 +01:00
|
|
|
_addOnService = addOnService;
|
2026-03-18 09:00:26 +01:00
|
|
|
_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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
[RelayCommand(CanExecute = nameof(CanPurchaseAddOn))]
|
|
|
|
|
private async Task PurchaseAddOnAsync(WinoAddOnItemViewModel? addOn)
|
2026-03-18 09:00:26 +01:00
|
|
|
{
|
2026-03-19 01:50:14 +01:00
|
|
|
if (addOn == null)
|
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);
|
|
|
|
|
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-19 01:50:14 +01:00
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
IsCheckoutInProgress = true;
|
|
|
|
|
addOn.IsPurchaseInProgress = true;
|
|
|
|
|
});
|
2026-03-18 17:43:56 +01:00
|
|
|
|
2026-03-18 09:00:26 +01:00
|
|
|
try
|
|
|
|
|
{
|
2026-03-19 01:50:14 +01:00
|
|
|
var checkoutSession = await _profileService.CreateCheckoutSessionAsync(addOn.ProductType).ConfigureAwait(false);
|
2026-03-18 17:43:56 +01:00
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
if (!checkoutSession.IsSuccess || string.IsNullOrWhiteSpace(checkoutSession.Result?.Url))
|
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-19 01:50:14 +01:00
|
|
|
var isLaunched = await _nativeAppService.LaunchUriAsync(new Uri(checkoutSession.Result.Url)).ConfigureAwait(false);
|
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
|
|
|
|
|
{
|
2026-03-19 01:50:14 +01:00
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
IsCheckoutInProgress = false;
|
|
|
|
|
addOn.IsPurchaseInProgress = false;
|
|
|
|
|
});
|
2026-03-18 17:43:56 +01:00
|
|
|
}
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
private bool CanPurchaseAddOn(WinoAddOnItemViewModel? addOn)
|
|
|
|
|
=> addOn != null && !addOn.IsPurchased && !IsCheckoutInProgress;
|
|
|
|
|
|
2026-03-18 09:00:26 +01:00
|
|
|
[RelayCommand]
|
2026-03-19 01:50:14 +01:00
|
|
|
private async Task ManageAiPackAsync()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var portalSession = await _profileService.CreateCustomerPortalSessionAsync().ConfigureAwait(false);
|
|
|
|
|
if (!portalSession.IsSuccess || string.IsNullOrWhiteSpace(portalSession.Result?.Url))
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
|
|
|
Translator.WinoAccount_Management_PurchaseStartFailed,
|
|
|
|
|
InfoBarMessageType.Error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var isLaunched = await _nativeAppService.LaunchUriAsync(new Uri(portalSession.Result.Url)).ConfigureAwait(false);
|
|
|
|
|
if (!isLaunched)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
|
|
|
Translator.WinoAccount_Management_PurchaseStartFailed,
|
|
|
|
|
InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
|
|
|
Translator.WinoAccount_Management_PurchaseStartFailed,
|
|
|
|
|
InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
}
|
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)
|
2026-03-19 01:50:14 +01:00
|
|
|
=> _ = LoadAsync();
|
2026-03-18 17:43:56 +01:00
|
|
|
|
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-19 01:50:14 +01:00
|
|
|
var addOns = await _addOnService.GetAvailableAddOnsAsync().ConfigureAwait(false);
|
2026-03-18 09:00:26 +01:00
|
|
|
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
2026-03-19 01:50:14 +01:00
|
|
|
IsSignedIn = account != null;
|
|
|
|
|
AccountEmail = account?.Email ?? string.Empty;
|
|
|
|
|
AccountStatusText = account == null
|
|
|
|
|
? string.Empty
|
|
|
|
|
: string.Format(Translator.WinoAccount_Management_StatusLabel, account.AccountStatus);
|
2026-03-18 09:00:26 +01:00
|
|
|
});
|
|
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
await UpdateAddOnsAsync(addOns).ConfigureAwait(false);
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
|
|
|
Translator.WinoAccount_Management_LoadFailed,
|
|
|
|
|
InfoBarMessageType.Error);
|
2026-03-19 01:50:14 +01:00
|
|
|
await ResetStateAsync().ConfigureAwait(false);
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() => IsBusy = false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
private async Task ResetStateAsync()
|
2026-03-18 09:00:26 +01:00
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
IsSignedIn = false;
|
|
|
|
|
AccountEmail = string.Empty;
|
|
|
|
|
AccountStatusText = string.Empty;
|
2026-03-19 01:50:14 +01:00
|
|
|
IsCheckoutInProgress = false;
|
|
|
|
|
AddOns.Clear();
|
|
|
|
|
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
2026-03-18 09:00:26 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
private async Task UpdateAddOnsAsync(IReadOnlyList<WinoAddOnInfo> addOns)
|
2026-03-18 09:00:26 +01:00
|
|
|
{
|
2026-03-19 01:50:14 +01:00
|
|
|
var items = addOns.Select(CreateAddOnItem).ToList();
|
2026-03-18 09:00:26 +01:00
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
await ExecuteUIThread(() =>
|
2026-03-18 09:00:26 +01:00
|
|
|
{
|
2026-03-19 01:50:14 +01:00
|
|
|
AddOns.Clear();
|
2026-03-18 09:00:26 +01:00
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
foreach (var item in items)
|
|
|
|
|
{
|
|
|
|
|
AddOns.Add(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
2026-03-18 09:00:26 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
private WinoAddOnItemViewModel CreateAddOnItem(WinoAddOnInfo addOn)
|
2026-03-18 10:25:07 +01:00
|
|
|
{
|
2026-03-19 01:50:14 +01:00
|
|
|
var item = new WinoAddOnItemViewModel(addOn.ProductType)
|
|
|
|
|
{
|
|
|
|
|
IsPurchased = addOn.IsPurchased,
|
|
|
|
|
PurchaseCommand = PurchaseAddOnCommand,
|
|
|
|
|
ManageCommand = ManageAiPackCommand,
|
|
|
|
|
UsageCount = addOn.UsageCount ?? 0,
|
|
|
|
|
UsageLimit = addOn.UsageLimit is > 0 ? addOn.UsageLimit.Value : 1,
|
|
|
|
|
UsagePercentage = addOn.UsagePercentage
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (addOn.RenewalDateUtc is DateTimeOffset renewalDateUtc)
|
|
|
|
|
{
|
|
|
|
|
item.RenewalText = string.Format(Translator.WinoAccount_Management_AiPackRenews, renewalDateUtc.LocalDateTime);
|
|
|
|
|
item.UsageResetText = string.Format(Translator.WinoAccount_Management_AiPackResets, renewalDateUtc.LocalDateTime);
|
|
|
|
|
}
|
2026-03-18 10:25:07 +01:00
|
|
|
|
2026-03-19 01:50:14 +01:00
|
|
|
return item;
|
2026-03-18 10:25:07 +01:00
|
|
|
}
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|