2026-03-18 09:00:26 +01:00
|
|
|
#nullable enable
|
|
|
|
|
using System;
|
2026-03-19 01:50:14 +01:00
|
|
|
using System.Collections.ObjectModel;
|
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;
|
2026-04-04 20:23:20 +02:00
|
|
|
using Wino.Core.Domain.Entities.Shared;
|
2026-03-18 09:00:26 +01:00
|
|
|
using Wino.Core.Domain.Enums;
|
|
|
|
|
using Wino.Core.Domain.Interfaces;
|
2026-04-04 20:23:20 +02: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.Core.ViewModels.Data;
|
2026-03-19 16:41:35 +01:00
|
|
|
using Wino.Mail.Api.Contracts.Common;
|
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,
|
2026-03-19 10:26:17 +01:00
|
|
|
IRecipient<WinoAccountProfileUpdatedMessage>,
|
|
|
|
|
IRecipient<WinoAccountProfileDeletedMessage>,
|
|
|
|
|
IRecipient<WinoAccountAddOnPurchasedMessage>
|
2026-03-18 09:00:26 +01:00
|
|
|
{
|
|
|
|
|
private readonly IWinoAccountProfileService _profileService;
|
2026-04-04 20:23:20 +02:00
|
|
|
private readonly IWinoAccountDataSyncService _syncService;
|
2026-03-18 09:00:26 +01:00
|
|
|
private readonly IMailDialogService _dialogService;
|
2026-04-02 15:07:05 +02:00
|
|
|
private readonly IStoreManagementService _storeManagementService;
|
|
|
|
|
private readonly WinoAddOnItemViewModel _aiPackAddOn;
|
|
|
|
|
private readonly WinoAddOnItemViewModel _unlimitedAccountsAddOn;
|
2026-03-18 09:00:26 +01:00
|
|
|
|
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-04-04 20:23:20 +02:00
|
|
|
IWinoAccountDataSyncService syncService,
|
2026-03-18 09:00:26 +01:00
|
|
|
IMailDialogService dialogService,
|
2026-04-02 15:07:05 +02:00
|
|
|
IStoreManagementService storeManagementService)
|
2026-03-18 09:00:26 +01:00
|
|
|
{
|
|
|
|
|
_profileService = profileService;
|
2026-04-04 20:23:20 +02:00
|
|
|
_syncService = syncService;
|
2026-03-18 09:00:26 +01:00
|
|
|
_dialogService = dialogService;
|
2026-04-02 15:07:05 +02:00
|
|
|
_storeManagementService = storeManagementService;
|
|
|
|
|
|
|
|
|
|
_aiPackAddOn = CreateAddOnItem(WinoAddOnProductType.AI_PACK);
|
|
|
|
|
_unlimitedAccountsAddOn = CreateAddOnItem(WinoAddOnProductType.UNLIMITED_ACCOUNTS);
|
|
|
|
|
AddOns.Add(_aiPackAddOn);
|
|
|
|
|
AddOns.Add(_unlimitedAccountsAddOn);
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
|
|
|
|
{
|
|
|
|
|
base.OnNavigatedTo(mode, parameters);
|
2026-03-19 10:26:17 +01:00
|
|
|
_ = InitializeAsync();
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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 16:41:35 +01:00
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task ChangePasswordAsync()
|
|
|
|
|
{
|
2026-03-24 01:18:06 +01:00
|
|
|
var account = await _profileService.GetActiveAccountAsync();
|
2026-03-19 16:41:35 +01:00
|
|
|
if (account == null)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
|
|
|
|
Translator.WinoAccount_SignOut_NoAccountMessage,
|
|
|
|
|
InfoBarMessageType.Warning);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var shouldContinue = await _dialogService.ShowConfirmationDialogAsync(
|
|
|
|
|
string.Format(Translator.WinoAccount_ChangePassword_ConfirmationMessage, account.Email),
|
|
|
|
|
Translator.WinoAccount_ChangePassword_Title,
|
2026-03-24 01:18:06 +01:00
|
|
|
Translator.WinoAccount_ChangePassword_Action);
|
2026-03-19 16:41:35 +01:00
|
|
|
|
|
|
|
|
if (!shouldContinue)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 01:18:06 +01:00
|
|
|
var response = await _profileService.ForgotPasswordAsync(account.Email);
|
2026-03-19 16:41:35 +01:00
|
|
|
if (!response.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
|
|
|
TranslateForgotPasswordError(response.ErrorCode),
|
|
|
|
|
InfoBarMessageType.Error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
|
|
|
|
string.Format(Translator.WinoAccount_ForgotPasswordDialog_SuccessMessage, account.Email),
|
|
|
|
|
InfoBarMessageType.Success);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string TranslateForgotPasswordError(string? errorCode)
|
|
|
|
|
=> errorCode switch
|
|
|
|
|
{
|
|
|
|
|
ApiErrorCodes.EmailNotRegistered => Translator.WinoAccount_Error_EmailNotRegistered,
|
|
|
|
|
ApiErrorCodes.ValidationFailed => Translator.WinoAccount_Error_ValidationFailed,
|
|
|
|
|
_ when string.IsNullOrWhiteSpace(errorCode) => Translator.GeneralTitle_Error,
|
|
|
|
|
_ => errorCode!
|
|
|
|
|
};
|
|
|
|
|
|
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-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-04-02 15:07:05 +02:00
|
|
|
var purchaseResult = await _storeManagementService.PurchaseAsync(addOn.ProductType);
|
2026-03-18 17:43:56 +01:00
|
|
|
|
2026-04-02 15:07:05 +02:00
|
|
|
if (purchaseResult == StorePurchaseResult.NotPurchased)
|
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-04-02 15:07:05 +02:00
|
|
|
var syncResult = await _profileService.SyncStoreEntitlementsAsync().ConfigureAwait(false);
|
|
|
|
|
if (!syncResult.IsSuccess && !string.Equals(syncResult.ErrorCode, "MissingAccessToken", StringComparison.Ordinal))
|
2026-03-18 09:00:26 +01:00
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
2026-04-02 15:07:05 +02:00
|
|
|
TranslateStoreSyncError(syncResult.ErrorCode),
|
2026-03-18 09:00:26 +01:00
|
|
|
InfoBarMessageType.Error);
|
2026-04-02 15:07:05 +02:00
|
|
|
return;
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|
2026-04-02 15:07:05 +02:00
|
|
|
|
|
|
|
|
await HandleAddOnPurchasedAsync().ConfigureAwait(false);
|
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)
|
2026-04-02 15:07:05 +02:00
|
|
|
=> addOn != null && !addOn.IsPurchased && !addOn.IsLoading && !IsCheckoutInProgress;
|
2026-03-18 09:00:26 +01:00
|
|
|
|
2026-03-18 17:43:56 +01:00
|
|
|
[RelayCommand]
|
2026-04-04 20:23:20 +02:00
|
|
|
private async Task ExportSettingsAsync()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var result = await _dialogService.ShowWinoAccountExportDialogAsync().ConfigureAwait(false);
|
|
|
|
|
if (result == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.GeneralTitle_Info,
|
|
|
|
|
BuildExportSuccessMessage(result),
|
|
|
|
|
InfoBarMessageType.Success);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.GeneralTitle_Error,
|
|
|
|
|
ex.Message,
|
|
|
|
|
InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-18 09:00:26 +01:00
|
|
|
|
2026-03-18 17:43:56 +01:00
|
|
|
[RelayCommand]
|
2026-04-04 20:23:20 +02:00
|
|
|
private async Task ImportSettingsAsync()
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() => IsBusy = true);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var result = await _syncService.ImportAsync(new WinoAccountSyncSelection());
|
|
|
|
|
|
|
|
|
|
if (!result.HasAnyRemoteData)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.GeneralTitle_Info,
|
|
|
|
|
Translator.WinoAccount_Management_NoRemoteSettings,
|
|
|
|
|
InfoBarMessageType.Information);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var messageType = result.FailedPreferenceCount > 0
|
|
|
|
|
? InfoBarMessageType.Warning
|
|
|
|
|
: InfoBarMessageType.Success;
|
|
|
|
|
|
|
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
result.FailedPreferenceCount > 0 ? Translator.GeneralTitle_Warning : Translator.GeneralTitle_Info,
|
|
|
|
|
BuildImportMessage(result),
|
|
|
|
|
messageType);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(
|
|
|
|
|
Translator.GeneralTitle_Error,
|
|
|
|
|
ex.Message,
|
|
|
|
|
InfoBarMessageType.Error);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() => IsBusy = false);
|
|
|
|
|
}
|
|
|
|
|
}
|
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-19 10:26:17 +01:00
|
|
|
Messenger.Register<WinoAccountProfileUpdatedMessage>(this);
|
|
|
|
|
Messenger.Register<WinoAccountProfileDeletedMessage>(this);
|
|
|
|
|
Messenger.Register<WinoAccountAddOnPurchasedMessage>(this);
|
2026-03-18 17:43:56 +01:00
|
|
|
}
|
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-19 10:26:17 +01:00
|
|
|
Messenger.Unregister<WinoAccountProfileUpdatedMessage>(this);
|
|
|
|
|
Messenger.Unregister<WinoAccountProfileDeletedMessage>(this);
|
|
|
|
|
Messenger.Unregister<WinoAccountAddOnPurchasedMessage>(this);
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 10:26:17 +01:00
|
|
|
public void Receive(WinoAccountProfileUpdatedMessage message)
|
2026-03-18 17:43:56 +01:00
|
|
|
=> _ = LoadAsync();
|
|
|
|
|
|
2026-03-19 10:26:17 +01:00
|
|
|
public void Receive(WinoAccountProfileDeletedMessage message)
|
2026-03-19 01:50:14 +01:00
|
|
|
=> _ = LoadAsync();
|
2026-03-18 17:43:56 +01:00
|
|
|
|
2026-03-19 10:26:17 +01:00
|
|
|
public void Receive(WinoAccountAddOnPurchasedMessage message)
|
|
|
|
|
=> _ = HandleAddOnPurchasedAsync();
|
|
|
|
|
|
|
|
|
|
private async Task InitializeAsync()
|
|
|
|
|
{
|
|
|
|
|
await LoadAsync().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 09:00:26 +01:00
|
|
|
private async Task LoadAsync()
|
|
|
|
|
{
|
2026-04-02 15:07:05 +02:00
|
|
|
WinoAccount? cachedAccount = null;
|
|
|
|
|
|
2026-03-18 09:00:26 +01:00
|
|
|
try
|
|
|
|
|
{
|
2026-04-02 15:07:05 +02:00
|
|
|
cachedAccount = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
2026-03-20 00:15:10 +01:00
|
|
|
|
|
|
|
|
if (cachedAccount != null)
|
|
|
|
|
{
|
|
|
|
|
await ApplyAccountStateAsync(cachedAccount).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 15:07:05 +02:00
|
|
|
await ExecuteUIThread(() => IsBusy = true);
|
|
|
|
|
await ResetAddOnStatesAsync().ConfigureAwait(false);
|
|
|
|
|
var loadAiPackTask = LoadAiPackAddOnAsync();
|
|
|
|
|
var loadUnlimitedAccountsTask = LoadUnlimitedAccountsAddOnAsync();
|
2026-03-20 00:15:10 +01:00
|
|
|
|
2026-04-02 15:07:05 +02:00
|
|
|
var resolvedAccount = cachedAccount;
|
2026-03-18 09:00:26 +01:00
|
|
|
|
2026-04-02 15:07:05 +02:00
|
|
|
if (cachedAccount == null || IsAccessTokenExpired(cachedAccount))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false);
|
|
|
|
|
if (account != null)
|
|
|
|
|
{
|
|
|
|
|
resolvedAccount = account;
|
|
|
|
|
|
|
|
|
|
var refreshedProfileResult = await _profileService.RefreshProfileAsync().ConfigureAwait(false);
|
|
|
|
|
if (refreshedProfileResult.IsSuccess && refreshedProfileResult.Account != null)
|
|
|
|
|
{
|
|
|
|
|
resolvedAccount = refreshedProfileResult.Account;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
resolvedAccount ??= cachedAccount;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-20 00:15:10 +01:00
|
|
|
|
|
|
|
|
await ApplyAccountStateAsync(resolvedAccount).ConfigureAwait(false);
|
2026-04-02 15:07:05 +02:00
|
|
|
await Task.WhenAll(loadAiPackTask, loadUnlimitedAccountsTask).ConfigureAwait(false);
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
2026-04-02 15:07:05 +02:00
|
|
|
if (cachedAccount == null)
|
|
|
|
|
{
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
|
|
|
Translator.WinoAccount_Management_LoadFailed,
|
|
|
|
|
InfoBarMessageType.Error);
|
|
|
|
|
await ResetStateAsync().ConfigureAwait(false);
|
|
|
|
|
}
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() => IsBusy = false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 00:15:10 +01:00
|
|
|
private async Task ApplyAccountStateAsync(Wino.Core.Domain.Entities.Shared.WinoAccount? account)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
IsSignedIn = account != null;
|
|
|
|
|
AccountEmail = account?.Email ?? string.Empty;
|
|
|
|
|
AccountStatusText = account == null
|
|
|
|
|
? string.Empty
|
|
|
|
|
: string.Format(Translator.WinoAccount_Management_StatusLabel, account.AccountStatus);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 10:26:17 +01:00
|
|
|
private async Task HandleAddOnPurchasedAsync()
|
|
|
|
|
{
|
|
|
|
|
await LoadAsync().ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
_dialogService.InfoBarMessage(Translator.Info_PurchaseThankYouTitle,
|
|
|
|
|
Translator.Info_PurchaseThankYouMessage,
|
|
|
|
|
InfoBarMessageType.Success);
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
2026-03-18 09:00:26 +01:00
|
|
|
});
|
2026-04-02 15:07:05 +02:00
|
|
|
|
|
|
|
|
await ResetAddOnStatesAsync().ConfigureAwait(false);
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-02 15:07:05 +02:00
|
|
|
private WinoAddOnItemViewModel CreateAddOnItem(WinoAddOnProductType productType)
|
2026-03-18 09:00:26 +01:00
|
|
|
{
|
2026-04-02 15:07:05 +02:00
|
|
|
return new WinoAddOnItemViewModel(productType)
|
|
|
|
|
{
|
|
|
|
|
PurchaseCommand = PurchaseAddOnCommand,
|
|
|
|
|
UsageLimit = 1
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-03-18 09:00:26 +01:00
|
|
|
|
2026-04-02 15:07:05 +02:00
|
|
|
private async Task ResetAddOnStatesAsync()
|
|
|
|
|
{
|
2026-03-19 01:50:14 +01:00
|
|
|
await ExecuteUIThread(() =>
|
2026-03-18 09:00:26 +01:00
|
|
|
{
|
2026-04-02 15:07:05 +02:00
|
|
|
ResetAddOnItem(_aiPackAddOn);
|
|
|
|
|
ResetAddOnItem(_unlimitedAccountsAddOn);
|
2026-03-19 01:50:14 +01:00
|
|
|
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
2026-03-18 09:00:26 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 15:07:05 +02:00
|
|
|
private static void ResetAddOnItem(WinoAddOnItemViewModel addOn)
|
2026-03-18 10:25:07 +01:00
|
|
|
{
|
2026-04-02 15:07:05 +02:00
|
|
|
addOn.IsLoading = true;
|
|
|
|
|
addOn.IsPurchased = false;
|
|
|
|
|
addOn.IsPurchaseInProgress = false;
|
|
|
|
|
addOn.HasUsageData = false;
|
|
|
|
|
addOn.ErrorText = string.Empty;
|
|
|
|
|
addOn.UsageCount = 0;
|
|
|
|
|
addOn.UsageLimit = 1;
|
|
|
|
|
addOn.UsagePercentage = 0;
|
|
|
|
|
addOn.RenewalText = string.Empty;
|
|
|
|
|
addOn.UsageResetText = string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string TranslateStoreSyncError(string? errorCode)
|
|
|
|
|
=> errorCode switch
|
2026-03-19 01:50:14 +01:00
|
|
|
{
|
2026-04-02 15:07:05 +02:00
|
|
|
_ => Translator.WinoAccount_Management_StoreSyncFailed
|
2026-03-19 01:50:14 +01:00
|
|
|
};
|
|
|
|
|
|
2026-04-04 20:23:20 +02:00
|
|
|
private static string BuildExportSuccessMessage(WinoAccountSyncExportResult result)
|
|
|
|
|
{
|
|
|
|
|
var parts = new Collection<string>();
|
|
|
|
|
|
|
|
|
|
if (result.IncludedPreferences)
|
|
|
|
|
{
|
|
|
|
|
parts.Add(Translator.WinoAccount_Management_ExportPreferencesSucceeded);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.IncludedAccounts)
|
|
|
|
|
{
|
|
|
|
|
parts.Add(string.Format(Translator.WinoAccount_Management_ExportAccountsSucceeded, result.ExportedMailboxCount));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parts.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
parts.Add(Translator.WinoAccount_Management_ExportSucceeded);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return string.Join(" ", parts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string BuildImportMessage(WinoAccountSyncImportResult result)
|
|
|
|
|
{
|
|
|
|
|
var parts = new Collection<string>();
|
|
|
|
|
|
|
|
|
|
if (result.HadRemotePreferences)
|
|
|
|
|
{
|
|
|
|
|
parts.Add(result.FailedPreferenceCount > 0
|
|
|
|
|
? string.Format(Translator.WinoAccount_Management_ImportPartial, result.AppliedPreferenceCount, result.FailedPreferenceCount)
|
|
|
|
|
: string.Format(Translator.WinoAccount_Management_ImportPreferencesSucceeded, result.AppliedPreferenceCount));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.ImportedMailboxCount > 0)
|
|
|
|
|
{
|
|
|
|
|
parts.Add(string.Format(Translator.WinoAccount_Management_ImportAccountsSucceeded, result.ImportedMailboxCount));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.SkippedDuplicateMailboxCount > 0)
|
|
|
|
|
{
|
|
|
|
|
parts.Add(string.Format(Translator.WinoAccount_Management_ImportDuplicateAccountsSkipped, result.SkippedDuplicateMailboxCount));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parts.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
parts.Add(Translator.WinoAccount_Management_ImportEmpty);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.ImportedMailboxCount > 0)
|
|
|
|
|
{
|
|
|
|
|
parts.Add(Translator.WinoAccount_Management_ImportReloginReminder);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return string.Join(" ", parts);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 15:07:05 +02:00
|
|
|
private static bool IsAccessTokenExpired(WinoAccount account)
|
|
|
|
|
=> string.IsNullOrWhiteSpace(account.AccessToken) || account.AccessTokenExpiresAtUtc <= DateTime.UtcNow;
|
|
|
|
|
|
|
|
|
|
private async Task LoadUnlimitedAccountsAddOnAsync()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var hasUnlimitedAccounts = await _storeManagementService.HasProductAsync(WinoAddOnProductType.UNLIMITED_ACCOUNTS).ConfigureAwait(false);
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
_unlimitedAccountsAddOn.IsPurchased = hasUnlimitedAccounts;
|
|
|
|
|
_unlimitedAccountsAddOn.ErrorText = string.Empty;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
_unlimitedAccountsAddOn.ErrorText = Translator.WinoAccount_Management_AddOnLoadFailed;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
finally
|
2026-03-19 01:50:14 +01:00
|
|
|
{
|
2026-04-02 15:07:05 +02:00
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
_unlimitedAccountsAddOn.IsLoading = false;
|
|
|
|
|
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
|
|
|
|
});
|
2026-03-19 01:50:14 +01:00
|
|
|
}
|
2026-04-02 15:07:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadAiPackAddOnAsync()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var hasAiPack = await _storeManagementService.HasProductAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
_aiPackAddOn.IsPurchased = hasAiPack;
|
|
|
|
|
_aiPackAddOn.ErrorText = string.Empty;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!hasAiPack)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-18 10:25:07 +01:00
|
|
|
|
2026-04-02 15:07:05 +02:00
|
|
|
var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(false);
|
|
|
|
|
if (!aiStatusResponse.IsSuccess || aiStatusResponse.Result == null)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
_aiPackAddOn.HasUsageData = false;
|
|
|
|
|
_aiPackAddOn.ErrorText = Translator.WinoAccount_Management_AiPackUsageLoadFailed;
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var aiStatus = aiStatusResponse.Result;
|
|
|
|
|
if (aiStatus.MonthlyLimit is not int usageLimit || usageLimit <= 0 || aiStatus.Used is not int usageCount)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
_aiPackAddOn.HasUsageData = false;
|
|
|
|
|
_aiPackAddOn.ErrorText = Translator.WinoAccount_Management_AiPackUsageLoadFailed;
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
_aiPackAddOn.HasUsageData = true;
|
|
|
|
|
_aiPackAddOn.ErrorText = string.Empty;
|
|
|
|
|
_aiPackAddOn.UsageCount = usageCount;
|
|
|
|
|
_aiPackAddOn.UsageLimit = usageLimit;
|
|
|
|
|
_aiPackAddOn.UsagePercentage = usageLimit > 0 ? (double)usageCount / usageLimit * 100 : 0;
|
|
|
|
|
_aiPackAddOn.RenewalText = aiStatus.CurrentPeriodEndUtc is DateTimeOffset renewalDateUtc
|
|
|
|
|
? string.Format(Translator.WinoAccount_Management_AiPackRenews, renewalDateUtc.LocalDateTime)
|
|
|
|
|
: string.Empty;
|
|
|
|
|
_aiPackAddOn.UsageResetText = aiStatus.CurrentPeriodEndUtc is DateTimeOffset resetDateUtc
|
|
|
|
|
? string.Format(Translator.WinoAccount_Management_AiPackResets, resetDateUtc.LocalDateTime)
|
|
|
|
|
: string.Empty;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
_aiPackAddOn.HasUsageData = false;
|
|
|
|
|
_aiPackAddOn.ErrorText = Translator.WinoAccount_Management_AddOnLoadFailed;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
await ExecuteUIThread(() =>
|
|
|
|
|
{
|
|
|
|
|
_aiPackAddOn.IsLoading = false;
|
|
|
|
|
PurchaseAddOnCommand.NotifyCanExecuteChanged();
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-18 10:25:07 +01:00
|
|
|
}
|
2026-03-18 09:00:26 +01:00
|
|
|
}
|