More updates on wino acc.
This commit is contained in:
@@ -130,6 +130,7 @@ private string searchQuery = string.Empty;
|
|||||||
- Editing UWP project files instead of WinUI equivalents
|
- Editing UWP project files instead of WinUI equivalents
|
||||||
- Hardcoding strings instead of using Translator
|
- Hardcoding strings instead of using Translator
|
||||||
- Forgetting to unregister Messenger recipients (memory leaks)
|
- Forgetting to unregister Messenger recipients (memory leaks)
|
||||||
|
- Putting authentication validation, token refresh, account API calls, settings serialization/deserialization, or preference-application logic into ViewModels instead of the corresponding service
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
@@ -141,6 +142,7 @@ private string searchQuery = string.Empty;
|
|||||||
- Log errors via IWinoLogger
|
- Log errors via IWinoLogger
|
||||||
- For dependency properties in WinUI code, always prefer `[GeneratedDependencyProperty]` from CommunityToolkit over manual `DependencyProperty.Register(...)` declarations.
|
- For dependency properties in WinUI code, always prefer `[GeneratedDependencyProperty]` from CommunityToolkit over manual `DependencyProperty.Register(...)` declarations.
|
||||||
- In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`).
|
- In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`).
|
||||||
|
- ViewModels should only handle UI interaction/state and delegate business logic to services; account-management work belongs in `WinoAccountProfileService`, and preferences import/export/apply logic belongs in `PreferencesService`.
|
||||||
- In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`.
|
- In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`.
|
||||||
- Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML.
|
- Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML.
|
||||||
- Never subscribe to framework events like `Loaded`, `Unloaded`, or input events from constructors in `.xaml.cs` for XAML-backed controls and pages; wire them directly in XAML instead.
|
- Never subscribe to framework events like `Loaded`, `Unloaded`, or input events from constructors in `.xaml.cs` for XAML-backed controls and pages; wire them directly in XAML instead.
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.82.1" />
|
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.82.1" />
|
||||||
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
||||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.1" />
|
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.1" />
|
||||||
<PackageVersion Include="Wino.Mail.Contracts" Version="1.0.3" />
|
<PackageVersion Include="Wino.Mail.Contracts" Version="1.0.5" />
|
||||||
<PackageVersion Include="MimeKit" Version="4.15.1" />
|
<PackageVersion Include="MimeKit" Version="4.15.1" />
|
||||||
<PackageVersion Include="morelinq" Version="4.4.0" />
|
<PackageVersion Include="morelinq" Version="4.4.0" />
|
||||||
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
|
|||||||
@@ -67,6 +67,17 @@ public interface IPreferencesService : INotifyPropertyChanged
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsWinoAccountButtonHidden { get; set; }
|
bool IsWinoAccountButtonHidden { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes the current syncable preferences snapshot.
|
||||||
|
/// </summary>
|
||||||
|
string ExportPreferences();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializes and applies a preferences snapshot.
|
||||||
|
/// Returns the applied and failed property counts.
|
||||||
|
/// </summary>
|
||||||
|
(int appliedCount, int failedCount) ImportPreferences(string settingsJson);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Mail
|
#region Mail
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public interface IWinoAccountApiClient
|
|||||||
Task<ApiEnvelope<JsonElement>> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<JsonElement>> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<ApiEnvelope<string>> CreateCheckoutSessionAsync(string productId, CancellationToken cancellationToken = default);
|
||||||
Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default);
|
Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default);
|
||||||
Task<bool> SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default);
|
Task<bool> SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
@@ -12,6 +15,10 @@ public interface IWinoAccountProfileService
|
|||||||
Task<WinoAccountOperationResult> LoginAsync(string email, string password, CancellationToken cancellationToken = default);
|
Task<WinoAccountOperationResult> LoginAsync(string email, string password, CancellationToken cancellationToken = default);
|
||||||
Task<WinoAccountOperationResult> RefreshAsync(CancellationToken cancellationToken = default);
|
Task<WinoAccountOperationResult> RefreshAsync(CancellationToken cancellationToken = default);
|
||||||
Task<WinoAccount?> GetActiveAccountAsync();
|
Task<WinoAccount?> GetActiveAccountAsync();
|
||||||
|
Task<WinoAccount?> GetAuthenticatedAccountAsync(CancellationToken cancellationToken = default);
|
||||||
Task<bool> HasActiveAccountAsync();
|
Task<bool> HasActiveAccountAsync();
|
||||||
|
Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<ApiEnvelope<string>> CreateCheckoutSessionAsync(string productId, CancellationToken cancellationToken = default);
|
||||||
Task SignOutAsync(CancellationToken cancellationToken = default);
|
Task SignOutAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -878,6 +878,8 @@
|
|||||||
"WinoAccount_Management_AiPackPromoPrice": "$4.99 / mo",
|
"WinoAccount_Management_AiPackPromoPrice": "$4.99 / mo",
|
||||||
"WinoAccount_Management_AiPackPromoRequests": "1,000 requests",
|
"WinoAccount_Management_AiPackPromoRequests": "1,000 requests",
|
||||||
"WinoAccount_Management_AiPackGetButton": "Get AI Pack",
|
"WinoAccount_Management_AiPackGetButton": "Get AI Pack",
|
||||||
|
"WinoAccount_Management_PurchaseRequiresSignIn": "Sign in with your Wino Account to complete this purchase.",
|
||||||
|
"WinoAccount_Management_PurchaseStartFailed": "Wino could not start the checkout session for this add-on.",
|
||||||
"WinoAccount_Management_AiPackSubscriptionActive": "Your subscription is active",
|
"WinoAccount_Management_AiPackSubscriptionActive": "Your subscription is active",
|
||||||
"WinoAccount_Management_AiPackRenews": "Renews {0:d}",
|
"WinoAccount_Management_AiPackRenews": "Renews {0:d}",
|
||||||
"WinoAccount_Management_AiPackRequestsUsed": "Requests used this month",
|
"WinoAccount_Management_AiPackRequestsUsed": "Requests used this month",
|
||||||
@@ -1245,6 +1247,7 @@
|
|||||||
"WinoAccount_Error_AccountLocked": "This account is temporarily locked.",
|
"WinoAccount_Error_AccountLocked": "This account is temporarily locked.",
|
||||||
"WinoAccount_Error_AccountBanned": "This account has been banned.",
|
"WinoAccount_Error_AccountBanned": "This account has been banned.",
|
||||||
"WinoAccount_Error_AccountSuspended": "This account has been suspended.",
|
"WinoAccount_Error_AccountSuspended": "This account has been suspended.",
|
||||||
|
"WinoAccount_Error_EmailNotConfirmed": "Please confirm your email address before signing in.",
|
||||||
"WinoAccount_Error_RefreshTokenInvalid": "Your session is no longer valid. Please sign in again.",
|
"WinoAccount_Error_RefreshTokenInvalid": "Your session is no longer valid. Please sign in again.",
|
||||||
"WinoAccount_Error_EmailAlreadyRegistered": "This email address is already registered.",
|
"WinoAccount_Error_EmailAlreadyRegistered": "This email address is already registered.",
|
||||||
"WinoAccount_Error_ExternalLoginEmailRequired": "An email address is required to complete external sign-in.",
|
"WinoAccount_Error_ExternalLoginEmailRequired": "An email address is required to complete external sign-in.",
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel,
|
|||||||
private readonly IPreferencesService _preferencesService;
|
private readonly IPreferencesService _preferencesService;
|
||||||
private readonly IProviderService _providerService;
|
private readonly IProviderService _providerService;
|
||||||
private readonly IWinoAccountProfileService _profileService;
|
private readonly IWinoAccountProfileService _profileService;
|
||||||
private readonly IWinoAccountApiClient _apiClient;
|
|
||||||
private readonly IMailDialogService _dialogService;
|
private readonly IMailDialogService _dialogService;
|
||||||
private bool _isInitializingSettings;
|
private bool _isInitializingSettings;
|
||||||
private bool _isAppearanceSelectionPaused;
|
private bool _isAppearanceSelectionPaused;
|
||||||
@@ -136,7 +135,6 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel,
|
|||||||
IPreferencesService preferencesService,
|
IPreferencesService preferencesService,
|
||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
IWinoAccountProfileService profileService,
|
IWinoAccountProfileService profileService,
|
||||||
IWinoAccountApiClient apiClient,
|
|
||||||
IMailDialogService dialogService)
|
IMailDialogService dialogService)
|
||||||
{
|
{
|
||||||
_nativeAppService = nativeAppService;
|
_nativeAppService = nativeAppService;
|
||||||
@@ -148,7 +146,6 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel,
|
|||||||
_preferencesService = preferencesService;
|
_preferencesService = preferencesService;
|
||||||
_providerService = providerService;
|
_providerService = providerService;
|
||||||
_profileService = profileService;
|
_profileService = profileService;
|
||||||
_apiClient = apiClient;
|
|
||||||
_dialogService = dialogService;
|
_dialogService = dialogService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,15 +491,15 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel,
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var account = await EnsureAuthenticatedAccountAsync().ConfigureAwait(false);
|
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false);
|
||||||
if (account == null)
|
if (account == null)
|
||||||
{
|
{
|
||||||
await ResetWinoAccountStateAsync();
|
await ResetWinoAccountStateAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentUserResponse = await _apiClient.GetCurrentUserAsync().ConfigureAwait(false);
|
var currentUserResponse = await _profileService.GetCurrentUserAsync().ConfigureAwait(false);
|
||||||
var aiStatusResponse = await _apiClient.GetAiStatusAsync().ConfigureAwait(false);
|
var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
var resolvedEmail = currentUserResponse.IsSuccess && currentUserResponse.Result != null
|
var resolvedEmail = currentUserResponse.IsSuccess && currentUserResponse.Result != null
|
||||||
? currentUserResponse.Result.Email
|
? currentUserResponse.Result.Email
|
||||||
@@ -546,22 +543,6 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<WinoAccount> EnsureAuthenticatedAccountAsync()
|
|
||||||
{
|
|
||||||
var account = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
|
||||||
if (account == null) return null;
|
|
||||||
|
|
||||||
if (account.AccessTokenExpiresAtUtc <= DateTime.UtcNow.AddMinutes(1))
|
|
||||||
{
|
|
||||||
var refreshResult = await _profileService.RefreshAsync().ConfigureAwait(false);
|
|
||||||
if (!refreshResult.IsSuccess) return null;
|
|
||||||
|
|
||||||
account = refreshResult.Account ?? await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return account != null && !string.IsNullOrWhiteSpace(account.AccessToken) ? account : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAiPackState(AiStatusResultDto aiStatus)
|
private void UpdateAiPackState(AiStatusResultDto aiStatus)
|
||||||
{
|
{
|
||||||
var hasAiPack = aiStatus?.HasAiPack == true;
|
var hasAiPack = aiStatus?.HasAiPack == true;
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
@@ -17,17 +12,18 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Mail.Api.Contracts.Ai;
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
namespace Wino.Core.ViewModels;
|
namespace Wino.Core.ViewModels;
|
||||||
|
|
||||||
public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel
|
public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
||||||
|
IRecipient<WinoAccountSignedInMessage>,
|
||||||
|
IRecipient<WinoAccountSignedOutMessage>
|
||||||
{
|
{
|
||||||
private const string BuyAiPackUrl = "https://example.com/wino-ai-pack";
|
private const string AiPackProductId = "ai-pack-monthly";
|
||||||
private const string ManageAiPackUrl = "https://example.com/wino-ai-pack/manage";
|
private const string ManageAiPackUrl = "https://example.com/wino-ai-pack/manage";
|
||||||
|
|
||||||
private readonly IWinoAccountProfileService _profileService;
|
private readonly IWinoAccountProfileService _profileService;
|
||||||
private readonly IWinoAccountApiClient _apiClient;
|
|
||||||
private readonly IPreferencesService _preferencesService;
|
|
||||||
private readonly IMailDialogService _dialogService;
|
private readonly IMailDialogService _dialogService;
|
||||||
private readonly INativeAppService _nativeAppService;
|
private readonly INativeAppService _nativeAppService;
|
||||||
|
|
||||||
@@ -80,19 +76,20 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string AiUsageResetText { get; set; } = string.Empty;
|
public partial string AiUsageResetText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(CanBuyAiPack))]
|
||||||
|
public partial bool IsAiPackCheckoutInProgress { get; set; }
|
||||||
|
|
||||||
public bool IsSignedOut => !IsSignedIn;
|
public bool IsSignedOut => !IsSignedIn;
|
||||||
public bool CanShowAiUsage => HasAiPack;
|
public bool CanShowAiUsage => HasAiPack;
|
||||||
public bool CanShowBuyAiPack => IsSignedIn && !HasAiPack;
|
public bool CanShowBuyAiPack => IsSignedIn && !HasAiPack;
|
||||||
|
public bool CanBuyAiPack => !IsAiPackCheckoutInProgress;
|
||||||
|
|
||||||
public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService,
|
public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService,
|
||||||
IWinoAccountApiClient apiClient,
|
|
||||||
IPreferencesService preferencesService,
|
|
||||||
IMailDialogService dialogService,
|
IMailDialogService dialogService,
|
||||||
INativeAppService nativeAppService)
|
INativeAppService nativeAppService)
|
||||||
{
|
{
|
||||||
_profileService = profileService;
|
_profileService = profileService;
|
||||||
_apiClient = apiClient;
|
|
||||||
_preferencesService = preferencesService;
|
|
||||||
_dialogService = dialogService;
|
_dialogService = dialogService;
|
||||||
_nativeAppService = nativeAppService;
|
_nativeAppService = nativeAppService;
|
||||||
}
|
}
|
||||||
@@ -115,8 +112,6 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel
|
|||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||||
string.Format(Translator.WinoAccount_RegisterSuccessMessage, account.Email),
|
string.Format(Translator.WinoAccount_RegisterSuccessMessage, account.Email),
|
||||||
InfoBarMessageType.Success);
|
InfoBarMessageType.Success);
|
||||||
|
|
||||||
await LoadAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -131,8 +126,6 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel
|
|||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||||
string.Format(Translator.WinoAccount_LoginSuccessMessage, account.Email),
|
string.Format(Translator.WinoAccount_LoginSuccessMessage, account.Email),
|
||||||
InfoBarMessageType.Success);
|
InfoBarMessageType.Success);
|
||||||
|
|
||||||
await LoadAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -152,150 +145,102 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel
|
|||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
||||||
string.Format(Translator.WinoAccount_SignOut_SuccessMessage, account.Email),
|
string.Format(Translator.WinoAccount_SignOut_SuccessMessage, account.Email),
|
||||||
InfoBarMessageType.Success);
|
InfoBarMessageType.Success);
|
||||||
|
|
||||||
await ResetSignedOutStateAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task OpenBuyPageAsync() => await _nativeAppService.LaunchUriAsync(new Uri(BuyAiPackUrl));
|
private async Task BuyAiPackAsync()
|
||||||
|
{
|
||||||
|
if (IsAiPackCheckoutInProgress)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
||||||
|
Translator.WinoAccount_Management_PurchaseRequiresSignIn,
|
||||||
|
InfoBarMessageType.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExecuteUIThread(() => IsAiPackCheckoutInProgress = true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var checkoutSession = await _profileService.CreateCheckoutSessionAsync(AiPackProductId).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!checkoutSession.IsSuccess || string.IsNullOrWhiteSpace(checkoutSession.Result))
|
||||||
|
{
|
||||||
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
||||||
|
Translator.WinoAccount_Management_PurchaseStartFailed,
|
||||||
|
InfoBarMessageType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isLaunched = await _nativeAppService.LaunchUriAsync(new Uri(checkoutSession.Result)).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);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() => IsAiPackCheckoutInProgress = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ManageAiPackAsync() => await _nativeAppService.LaunchUriAsync(new Uri(ManageAiPackUrl));
|
private async Task ManageAiPackAsync() => await _nativeAppService.LaunchUriAsync(new Uri(ManageAiPackUrl));
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ExportSettingsAsync()
|
private Task ExportSettingsAsync() => Task.CompletedTask;
|
||||||
{
|
|
||||||
string settingsJson;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (await EnsureAuthenticatedAccountAsync().ConfigureAwait(false) == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var settings = CollectPreferencesSnapshot();
|
|
||||||
if (settings.Count == 0)
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
|
||||||
Translator.WinoAccount_Management_EmptyExport,
|
|
||||||
InfoBarMessageType.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsJson = SerializePreferencesSnapshot(settings);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(settingsJson) || settingsJson == "{}")
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
|
||||||
Translator.WinoAccount_Management_EmptyExport,
|
|
||||||
InfoBarMessageType.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
||||||
Translator.WinoAccount_Management_SerializeFailed,
|
|
||||||
InfoBarMessageType.Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (await EnsureAuthenticatedAccountAsync().ConfigureAwait(false) == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isSaved = await _apiClient.SaveSettingsAsync(settingsJson).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!isSaved)
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
||||||
Translator.WinoAccount_Management_ActionFailed,
|
|
||||||
InfoBarMessageType.Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
|
||||||
Translator.WinoAccount_Management_ExportSucceeded,
|
|
||||||
InfoBarMessageType.Success);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
||||||
Translator.WinoAccount_Management_ActionFailed,
|
|
||||||
InfoBarMessageType.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ImportSettingsAsync()
|
private Task ImportSettingsAsync() => Task.CompletedTask;
|
||||||
|
|
||||||
|
protected override void RegisterRecipients()
|
||||||
{
|
{
|
||||||
try
|
base.RegisterRecipients();
|
||||||
{
|
|
||||||
if (await EnsureAuthenticatedAccountAsync().ConfigureAwait(false) == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var settingsJson = await _apiClient.GetSettingsAsync().ConfigureAwait(false);
|
Messenger.Register<WinoAccountSignedInMessage>(this);
|
||||||
|
Messenger.Register<WinoAccountSignedOutMessage>(this);
|
||||||
if (string.IsNullOrWhiteSpace(settingsJson))
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
|
||||||
Translator.WinoAccount_Management_NoRemoteSettings,
|
|
||||||
InfoBarMessageType.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var document = JsonDocument.Parse(settingsJson);
|
|
||||||
if (document.RootElement.ValueKind != JsonValueKind.Object || !document.RootElement.EnumerateObject().Any())
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
|
||||||
Translator.WinoAccount_Management_ImportEmpty,
|
|
||||||
InfoBarMessageType.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var (appliedCount, failedCount) = ApplyPreferencesSnapshot(document.RootElement);
|
|
||||||
|
|
||||||
if (appliedCount == 0)
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
|
||||||
Translator.WinoAccount_Management_ImportEmpty,
|
|
||||||
InfoBarMessageType.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failedCount > 0)
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Warning,
|
|
||||||
string.Format(Translator.WinoAccount_Management_ImportPartial, appliedCount, failedCount),
|
|
||||||
InfoBarMessageType.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info,
|
|
||||||
string.Format(Translator.WinoAccount_Management_ImportSucceeded, appliedCount),
|
|
||||||
InfoBarMessageType.Success);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error,
|
|
||||||
Translator.WinoAccount_Management_ActionFailed,
|
|
||||||
InfoBarMessageType.Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void UnregisterRecipients()
|
||||||
|
{
|
||||||
|
base.UnregisterRecipients();
|
||||||
|
|
||||||
|
Messenger.Unregister<WinoAccountSignedInMessage>(this);
|
||||||
|
Messenger.Unregister<WinoAccountSignedOutMessage>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(WinoAccountSignedInMessage message)
|
||||||
|
=> _ = LoadAsync();
|
||||||
|
|
||||||
|
public void Receive(WinoAccountSignedOutMessage message)
|
||||||
|
=> _ = ResetSignedOutStateAsync();
|
||||||
|
|
||||||
private async Task LoadAsync()
|
private async Task LoadAsync()
|
||||||
{
|
{
|
||||||
await ExecuteUIThread(() => IsBusy = true);
|
await ExecuteUIThread(() => IsBusy = true);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var account = await EnsureAuthenticatedAccountAsync().ConfigureAwait(false);
|
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (account == null)
|
if (account == null)
|
||||||
{
|
{
|
||||||
@@ -303,8 +248,8 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentUserResponse = await _apiClient.GetCurrentUserAsync().ConfigureAwait(false);
|
var currentUserResponse = await _profileService.GetCurrentUserAsync().ConfigureAwait(false);
|
||||||
var aiStatusResponse = await _apiClient.GetAiStatusAsync().ConfigureAwait(false);
|
var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
var resolvedUser = currentUserResponse.IsSuccess && currentUserResponse.Result != null
|
var resolvedUser = currentUserResponse.IsSuccess && currentUserResponse.Result != null
|
||||||
? currentUserResponse.Result
|
? currentUserResponse.Result
|
||||||
@@ -359,34 +304,10 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel
|
|||||||
AiUsageLimit = 1;
|
AiUsageLimit = 1;
|
||||||
AiUsagePercentage = 0;
|
AiUsagePercentage = 0;
|
||||||
AiUsageResetText = string.Empty;
|
AiUsageResetText = string.Empty;
|
||||||
|
IsAiPackCheckoutInProgress = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<WinoAccount?> EnsureAuthenticatedAccountAsync()
|
|
||||||
{
|
|
||||||
var account = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
|
||||||
if (account == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.AccessTokenExpiresAtUtc <= DateTime.UtcNow.AddMinutes(1))
|
|
||||||
{
|
|
||||||
var refreshResult = await _profileService.RefreshAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!refreshResult.IsSuccess)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
account = refreshResult.Account ?? await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return account != null && !string.IsNullOrWhiteSpace(account.AccessToken)
|
|
||||||
? account
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAiPackState(AiStatusResultDto? aiStatus)
|
private void UpdateAiPackState(AiStatusResultDto? aiStatus)
|
||||||
{
|
{
|
||||||
var hasAiPack = aiStatus?.HasAiPack == true;
|
var hasAiPack = aiStatus?.HasAiPack == true;
|
||||||
@@ -450,176 +371,4 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel
|
|||||||
? displayName[..1].ToUpper(CultureInfo.CurrentCulture)
|
? displayName[..1].ToUpper(CultureInfo.CurrentCulture)
|
||||||
: string.Empty;
|
: string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<string, object?> CollectPreferencesSnapshot()
|
|
||||||
{
|
|
||||||
var settings = new Dictionary<string, object?>(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
foreach (var property in GetSyncablePreferenceProperties())
|
|
||||||
{
|
|
||||||
settings[property.Name] = property.GetValue(_preferencesService);
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string SerializePreferencesSnapshot(Dictionary<string, object?> settings)
|
|
||||||
{
|
|
||||||
using var stream = new MemoryStream();
|
|
||||||
using (var writer = new Utf8JsonWriter(stream))
|
|
||||||
{
|
|
||||||
writer.WriteStartObject();
|
|
||||||
|
|
||||||
foreach (var setting in settings)
|
|
||||||
{
|
|
||||||
WritePreferenceValue(writer, setting.Key, setting.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Encoding.UTF8.GetString(stream.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
private (int appliedCount, int failedCount) ApplyPreferencesSnapshot(JsonElement rootElement)
|
|
||||||
{
|
|
||||||
var appliedCount = 0;
|
|
||||||
var failedCount = 0;
|
|
||||||
|
|
||||||
foreach (var property in GetSyncablePreferenceProperties())
|
|
||||||
{
|
|
||||||
if (!rootElement.TryGetProperty(property.Name, out var value) || value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
property.SetValue(_preferencesService, ReadPreferenceValue(property.PropertyType, value));
|
|
||||||
appliedCount++;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
failedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (appliedCount, failedCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WritePreferenceValue(Utf8JsonWriter writer, string propertyName, object? value)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNull(propertyName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (value)
|
|
||||||
{
|
|
||||||
case string stringValue:
|
|
||||||
writer.WriteString(propertyName, stringValue);
|
|
||||||
return;
|
|
||||||
case bool boolValue:
|
|
||||||
writer.WriteBoolean(propertyName, boolValue);
|
|
||||||
return;
|
|
||||||
case int intValue:
|
|
||||||
writer.WriteNumber(propertyName, intValue);
|
|
||||||
return;
|
|
||||||
case long longValue:
|
|
||||||
writer.WriteNumber(propertyName, longValue);
|
|
||||||
return;
|
|
||||||
case double doubleValue:
|
|
||||||
writer.WriteNumber(propertyName, doubleValue);
|
|
||||||
return;
|
|
||||||
case float floatValue:
|
|
||||||
writer.WriteNumber(propertyName, floatValue);
|
|
||||||
return;
|
|
||||||
case Guid guidValue:
|
|
||||||
writer.WriteString(propertyName, guidValue);
|
|
||||||
return;
|
|
||||||
case TimeSpan timeSpanValue:
|
|
||||||
writer.WriteString(propertyName, timeSpanValue.ToString("c", CultureInfo.InvariantCulture));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var valueType = Nullable.GetUnderlyingType(value.GetType()) ?? value.GetType();
|
|
||||||
if (valueType.IsEnum)
|
|
||||||
{
|
|
||||||
writer.WriteString(propertyName, value.ToString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteString(propertyName, Convert.ToString(value, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static object? ReadPreferenceValue(Type propertyType, JsonElement value)
|
|
||||||
{
|
|
||||||
var targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
|
||||||
|
|
||||||
if (value.ValueKind == JsonValueKind.Null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == typeof(string))
|
|
||||||
{
|
|
||||||
return value.GetString() ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == typeof(bool))
|
|
||||||
{
|
|
||||||
return value.GetBoolean();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == typeof(int))
|
|
||||||
{
|
|
||||||
return value.GetInt32();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == typeof(long))
|
|
||||||
{
|
|
||||||
return value.GetInt64();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == typeof(double))
|
|
||||||
{
|
|
||||||
return value.GetDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == typeof(float))
|
|
||||||
{
|
|
||||||
return value.GetSingle();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == typeof(Guid))
|
|
||||||
{
|
|
||||||
return Guid.Parse(value.GetString() ?? string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType == typeof(TimeSpan))
|
|
||||||
{
|
|
||||||
return TimeSpan.Parse(value.GetString() ?? string.Empty, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetType.IsEnum)
|
|
||||||
{
|
|
||||||
return Enum.Parse(targetType, value.GetString() ?? string.Empty, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Convert.ChangeType(value.GetString(), targetType, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<PropertyInfo> GetSyncablePreferenceProperties()
|
|
||||||
{
|
|
||||||
foreach (var property in typeof(IPreferencesService).GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
|
||||||
{
|
|
||||||
if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length > 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return property;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
xmlns:domain="using:Wino.Core.Domain"
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
Title="{x:Bind domain:Translator.WinoAccount_RegisterDialog_Title}"
|
Title="{x:Bind domain:Translator.WinoAccount_RegisterDialog_Title}"
|
||||||
|
FullSizeDesired="True"
|
||||||
PrimaryButtonClick="RegisterClicked"
|
PrimaryButtonClick="RegisterClicked"
|
||||||
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
|
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
|
||||||
PrimaryButtonText="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrimaryButton}"
|
PrimaryButtonText="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrimaryButton}"
|
||||||
@@ -16,15 +17,14 @@
|
|||||||
<ContentDialog.Resources>
|
<ContentDialog.Resources>
|
||||||
<x:Double x:Key="ContentDialogMinWidth">560</x:Double>
|
<x:Double x:Key="ContentDialogMinWidth">560</x:Double>
|
||||||
<x:Double x:Key="ContentDialogMaxWidth">560</x:Double>
|
<x:Double x:Key="ContentDialogMaxWidth">560</x:Double>
|
||||||
|
<x:Double x:Key="ContentDialogMaxHeight">900</x:Double>
|
||||||
</ContentDialog.Resources>
|
</ContentDialog.Resources>
|
||||||
|
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Spacing="20">
|
<StackPanel Spacing="20">
|
||||||
|
|
||||||
<!-- Hero illustration area -->
|
<!-- Hero illustration area -->
|
||||||
<Border
|
<Border Height="140" CornerRadius="12">
|
||||||
Height="140"
|
|
||||||
CornerRadius="12">
|
|
||||||
<Border.Background>
|
<Border.Background>
|
||||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||||
<GradientStop Offset="0" Color="#1A818CF8" />
|
<GradientStop Offset="0" Color="#1A818CF8" />
|
||||||
@@ -99,9 +99,7 @@
|
|||||||
Height="28"
|
Height="28"
|
||||||
Fill="White" />
|
Fill="White" />
|
||||||
<!-- Person body -->
|
<!-- Person body -->
|
||||||
<Path
|
<Path Data="M28 68 A20 16 0 0 1 68 68" Fill="White" />
|
||||||
Data="M28 68 A20 16 0 0 1 68 68"
|
|
||||||
Fill="White" />
|
|
||||||
|
|
||||||
<!-- Plus badge -->
|
<!-- Plus badge -->
|
||||||
<Ellipse
|
<Ellipse
|
||||||
@@ -239,6 +237,12 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
x:Name="ErrorTextBlock"
|
||||||
|
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||||
|
TextWrapping="WrapWholeWords"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
|
||||||
<!-- Input fields -->
|
<!-- Input fields -->
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
<TextBox
|
<TextBox
|
||||||
@@ -266,9 +270,7 @@
|
|||||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||||
CornerRadius="12">
|
CornerRadius="12">
|
||||||
<StackPanel Spacing="10">
|
<StackPanel Spacing="10">
|
||||||
<TextBlock
|
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrivacyTitle}" />
|
||||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
|
||||||
Text="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrivacyTitle}" />
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
@@ -281,8 +283,8 @@
|
|||||||
<CheckBox
|
<CheckBox
|
||||||
x:Name="PrivacyPolicyCheckBox"
|
x:Name="PrivacyPolicyCheckBox"
|
||||||
Checked="InputChanged"
|
Checked="InputChanged"
|
||||||
Unchecked="InputChanged"
|
Content="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrivacyCheckbox}"
|
||||||
Content="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrivacyCheckbox}" />
|
Unchecked="InputChanged" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -293,12 +295,6 @@
|
|||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
IsActive="False"
|
IsActive="False"
|
||||||
Visibility="Collapsed" />
|
Visibility="Collapsed" />
|
||||||
|
|
||||||
<TextBlock
|
|
||||||
x:Name="ErrorTextBlock"
|
|
||||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
|
||||||
TextWrapping="WrapWholeWords"
|
|
||||||
Visibility="Collapsed" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentDialog>
|
</ContentDialog>
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
@@ -44,6 +48,59 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
|
|||||||
RenderPlaintextLinks = RenderPlaintextLinks
|
RenderPlaintextLinks = RenderPlaintextLinks
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public string ExportPreferences()
|
||||||
|
{
|
||||||
|
var settings = new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var property in GetSyncablePreferenceProperties())
|
||||||
|
{
|
||||||
|
settings[property.Name] = property.GetValue(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using (var writer = new Utf8JsonWriter(stream))
|
||||||
|
{
|
||||||
|
writer.WriteStartObject();
|
||||||
|
|
||||||
|
foreach (var setting in settings)
|
||||||
|
{
|
||||||
|
WritePreferenceValue(writer, setting.Key, setting.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(stream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public (int appliedCount, int failedCount) ImportPreferences(string settingsJson)
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(settingsJson);
|
||||||
|
var rootElement = document.RootElement;
|
||||||
|
var appliedCount = 0;
|
||||||
|
var failedCount = 0;
|
||||||
|
|
||||||
|
foreach (var property in GetSyncablePreferenceProperties())
|
||||||
|
{
|
||||||
|
if (!rootElement.TryGetProperty(property.Name, out var value) || value.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
property.SetValue(this, ReadPreferenceValue(property.PropertyType, value));
|
||||||
|
appliedCount++;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (appliedCount, failedCount);
|
||||||
|
}
|
||||||
|
|
||||||
public MailListDisplayMode MailItemDisplayMode
|
public MailListDisplayMode MailItemDisplayMode
|
||||||
{
|
{
|
||||||
get => _configurationService.Get(nameof(MailItemDisplayMode), MailListDisplayMode.Spacious);
|
get => _configurationService.Get(nameof(MailItemDisplayMode), MailListDisplayMode.Spacious);
|
||||||
@@ -368,6 +425,122 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
|
|||||||
|
|
||||||
return daysOfWeek;
|
return daysOfWeek;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void WritePreferenceValue(Utf8JsonWriter writer, string propertyName, object? value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
writer.WriteNull(propertyName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case string stringValue:
|
||||||
|
writer.WriteString(propertyName, stringValue);
|
||||||
|
return;
|
||||||
|
case bool boolValue:
|
||||||
|
writer.WriteBoolean(propertyName, boolValue);
|
||||||
|
return;
|
||||||
|
case int intValue:
|
||||||
|
writer.WriteNumber(propertyName, intValue);
|
||||||
|
return;
|
||||||
|
case long longValue:
|
||||||
|
writer.WriteNumber(propertyName, longValue);
|
||||||
|
return;
|
||||||
|
case double doubleValue:
|
||||||
|
writer.WriteNumber(propertyName, doubleValue);
|
||||||
|
return;
|
||||||
|
case float floatValue:
|
||||||
|
writer.WriteNumber(propertyName, floatValue);
|
||||||
|
return;
|
||||||
|
case Guid guidValue:
|
||||||
|
writer.WriteString(propertyName, guidValue);
|
||||||
|
return;
|
||||||
|
case TimeSpan timeSpanValue:
|
||||||
|
writer.WriteString(propertyName, timeSpanValue.ToString("c", CultureInfo.InvariantCulture));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var valueType = Nullable.GetUnderlyingType(value.GetType()) ?? value.GetType();
|
||||||
|
if (valueType.IsEnum)
|
||||||
|
{
|
||||||
|
writer.WriteString(propertyName, value.ToString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteString(propertyName, Convert.ToString(value, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? ReadPreferenceValue(Type propertyType, JsonElement value)
|
||||||
|
{
|
||||||
|
var targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
||||||
|
|
||||||
|
if (value.ValueKind == JsonValueKind.Null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType == typeof(string))
|
||||||
|
{
|
||||||
|
return value.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType == typeof(bool))
|
||||||
|
{
|
||||||
|
return value.GetBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType == typeof(int))
|
||||||
|
{
|
||||||
|
return value.GetInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType == typeof(long))
|
||||||
|
{
|
||||||
|
return value.GetInt64();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType == typeof(double))
|
||||||
|
{
|
||||||
|
return value.GetDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType == typeof(float))
|
||||||
|
{
|
||||||
|
return value.GetSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType == typeof(Guid))
|
||||||
|
{
|
||||||
|
return Guid.Parse(value.GetString() ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType == typeof(TimeSpan))
|
||||||
|
{
|
||||||
|
return TimeSpan.Parse(value.GetString() ?? string.Empty, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType.IsEnum)
|
||||||
|
{
|
||||||
|
return Enum.Parse(targetType, value.GetString() ?? string.Empty, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Convert.ChangeType(value.GetString(), targetType, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<PropertyInfo> GetSyncablePreferenceProperties()
|
||||||
|
{
|
||||||
|
foreach (var property in typeof(IPreferencesService).GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||||
|
{
|
||||||
|
if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length > 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return property;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public static class WinoAccountAuthErrorTranslator
|
|||||||
ApiErrorCodes.AccountLocked => Translator.WinoAccount_Error_AccountLocked,
|
ApiErrorCodes.AccountLocked => Translator.WinoAccount_Error_AccountLocked,
|
||||||
ApiErrorCodes.AccountBanned => Translator.WinoAccount_Error_AccountBanned,
|
ApiErrorCodes.AccountBanned => Translator.WinoAccount_Error_AccountBanned,
|
||||||
ApiErrorCodes.AccountSuspended => Translator.WinoAccount_Error_AccountSuspended,
|
ApiErrorCodes.AccountSuspended => Translator.WinoAccount_Error_AccountSuspended,
|
||||||
|
ApiErrorCodes.EmailNotConfirmed => Translator.WinoAccount_Error_EmailNotConfirmed,
|
||||||
ApiErrorCodes.RefreshTokenInvalid => Translator.WinoAccount_Error_RefreshTokenInvalid,
|
ApiErrorCodes.RefreshTokenInvalid => Translator.WinoAccount_Error_RefreshTokenInvalid,
|
||||||
ApiErrorCodes.EmailAlreadyRegistered => Translator.WinoAccount_Error_EmailAlreadyRegistered,
|
ApiErrorCodes.EmailAlreadyRegistered => Translator.WinoAccount_Error_EmailAlreadyRegistered,
|
||||||
ApiErrorCodes.ExternalLoginEmailRequired => Translator.WinoAccount_Error_ExternalLoginEmailRequired,
|
ApiErrorCodes.ExternalLoginEmailRequired => Translator.WinoAccount_Error_ExternalLoginEmailRequired,
|
||||||
|
|||||||
@@ -74,6 +74,99 @@
|
|||||||
Style="{StaticResource AccentButtonStyle}" />
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
<Button Command="{x:Bind ViewModel.RegisterCommand}" Content="{x:Bind domain:Translator.Buttons_CreateAccount}" />
|
<Button Command="{x:Bind ViewModel.RegisterCommand}" Content="{x:Bind domain:Translator.Buttons_CreateAccount}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,16,0,4"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackSectionHeader}" />
|
||||||
|
|
||||||
|
<controls:SettingsCard
|
||||||
|
MaxWidth="520"
|
||||||
|
Description="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoDescription}">
|
||||||
|
<controls:SettingsCard.HeaderIcon>
|
||||||
|
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||||
|
</controls:SettingsCard.HeaderIcon>
|
||||||
|
<controls:SettingsCard.Header>
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoTitle}" />
|
||||||
|
<Border
|
||||||
|
Padding="8,2"
|
||||||
|
Background="{ThemeResource SystemAccentColor}"
|
||||||
|
CornerRadius="4">
|
||||||
|
<TextBlock
|
||||||
|
FontSize="10"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="White"
|
||||||
|
Text="PRO" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock
|
||||||
|
MaxWidth="400"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoDescription}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
|
<FontIcon
|
||||||
|
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||||
|
FontSize="12"
|
||||||
|
Glyph="" />
|
||||||
|
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureTranslate}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
|
<FontIcon
|
||||||
|
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||||
|
FontSize="12"
|
||||||
|
Glyph="" />
|
||||||
|
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureRewrite}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
|
<FontIcon
|
||||||
|
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||||
|
FontSize="12"
|
||||||
|
Glyph="" />
|
||||||
|
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureSummarize}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<Border
|
||||||
|
Padding="12,8"
|
||||||
|
Background="{ThemeResource SystemFillColorCautionBackgroundBrush}"
|
||||||
|
CornerRadius="8">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WinoAccount_Management_PurchaseRequiresSignIn}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
</Border>
|
||||||
|
<StackPanel
|
||||||
|
Margin="0,4,0,0"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="12">
|
||||||
|
<Button
|
||||||
|
Command="{x:Bind ViewModel.BuyAiPackCommand}"
|
||||||
|
Content="{x:Bind domain:Translator.Buttons_Purchase}"
|
||||||
|
IsEnabled="{x:Bind ViewModel.CanBuyAiPack, Mode=OneWay}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
<ProgressRing
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
IsActive="True"
|
||||||
|
Visibility="{x:Bind ViewModel.IsAiPackCheckoutInProgress, Mode=OneWay}" />
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}">
|
||||||
|
<Run Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoPrice}" />
|
||||||
|
<Run Text=" · " />
|
||||||
|
<Run Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoRequests}" />
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</controls:SettingsCard.Header>
|
||||||
|
</controls:SettingsCard>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
@@ -181,9 +274,15 @@
|
|||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
Spacing="12">
|
Spacing="12">
|
||||||
<Button
|
<Button
|
||||||
Command="{x:Bind ViewModel.OpenBuyPageCommand}"
|
Command="{x:Bind ViewModel.BuyAiPackCommand}"
|
||||||
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackGetButton}"
|
Content="{x:Bind domain:Translator.Buttons_Purchase}"
|
||||||
|
IsEnabled="{x:Bind ViewModel.CanBuyAiPack, Mode=OneWay}"
|
||||||
Style="{StaticResource AccentButtonStyle}" />
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
<ProgressRing
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
IsActive="True"
|
||||||
|
Visibility="{x:Bind ViewModel.IsAiPackCheckoutInProgress, Mode=OneWay}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
|||||||
@@ -84,6 +84,20 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
public Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default)
|
public Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default)
|
||||||
=> SendAuthorizedRequestAsync("api/v1/ai/status", WinoAccountApiJsonContext.Default.ApiEnvelopeAiStatusResultDto, cancellationToken);
|
=> SendAuthorizedRequestAsync("api/v1/ai/status", WinoAccountApiJsonContext.Default.ApiEnvelopeAiStatusResultDto, cancellationToken);
|
||||||
|
|
||||||
|
public Task<ApiEnvelope<string>> CreateCheckoutSessionAsync(string productId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var endpoint = productId switch
|
||||||
|
{
|
||||||
|
"ai-pack-monthly" => "api/v1/billing/ai-pack/checkout-session",
|
||||||
|
"unlimited-accounts" => "api/v1/billing/unlimited-accounts/checkout-session",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(endpoint)
|
||||||
|
? Task.FromResult(ApiEnvelope<string>.Failure("UnknownProduct"))
|
||||||
|
: SendAuthorizedRequestAsync(HttpMethod.Post, endpoint, WinoAccountApiJsonContext.Default.ApiEnvelopeString, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default)
|
public async Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -150,11 +164,14 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
|
private Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
|
||||||
|
=> SendAuthorizedRequestAsync(HttpMethod.Get, endpoint, typeInfo, cancellationToken);
|
||||||
|
|
||||||
|
private async Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(HttpMethod method, string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var request = await CreateAuthorizedRequestAsync(HttpMethod.Get, endpoint).ConfigureAwait(false);
|
using var request = await CreateAuthorizedRequestAsync(method, endpoint).ConfigureAwait(false);
|
||||||
if (request == null)
|
if (request == null)
|
||||||
return ApiEnvelope<TResponse>.Failure("MissingAccessToken");
|
return ApiEnvelope<TResponse>.Failure("MissingAccessToken");
|
||||||
|
|
||||||
@@ -217,5 +234,6 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
|
||||||
|
[JsonSerializable(typeof(ApiEnvelope<string>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
||||||
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Serilog;
|
|||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
@@ -53,11 +54,20 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
||||||
if (account == null || string.IsNullOrWhiteSpace(account.RefreshToken))
|
if (account == null || string.IsNullOrWhiteSpace(account.RefreshToken))
|
||||||
{
|
{
|
||||||
|
_logger.Warning("Wino account token refresh skipped because there is no active account or refresh token.");
|
||||||
return WinoAccountOperationResult.Failure(ApiErrorCodes.RefreshTokenInvalid);
|
return WinoAccountOperationResult.Failure(ApiErrorCodes.RefreshTokenInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Information("Refreshing Wino account token for {Email}", account.Email);
|
||||||
var response = await _apiClient.RefreshAsync(account.RefreshToken, cancellationToken).ConfigureAwait(false);
|
var response = await _apiClient.RefreshAsync(account.RefreshToken, cancellationToken).ConfigureAwait(false);
|
||||||
return await PersistResponseAsync(response).ConfigureAwait(false);
|
var result = await PersistResponseAsync(response).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
_logger.Warning("Wino account token refresh failed for {Email}. Error code: {ErrorCode}", account.Email, result.ErrorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WinoAccount?> GetActiveAccountAsync()
|
public async Task<WinoAccount?> GetActiveAccountAsync()
|
||||||
@@ -66,9 +76,89 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WinoAccount?> GetAuthenticatedAccountAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(account.AccessToken))
|
||||||
|
{
|
||||||
|
_logger.Warning("Wino account {Email} is missing an access token.", account.Email);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.AccessTokenExpiresAtUtc > DateTime.UtcNow.AddMinutes(1))
|
||||||
|
{
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
var refreshResult = await RefreshAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!refreshResult.IsSuccess)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshResult.Account ?? await GetActiveAccountAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> HasActiveAccountAsync()
|
public async Task<bool> HasActiveAccountAsync()
|
||||||
=> await Connection.Table<WinoAccount>().CountAsync().ConfigureAwait(false) > 0;
|
=> await Connection.Table<WinoAccount>().CountAsync().ConfigureAwait(false) > 0;
|
||||||
|
|
||||||
|
public async Task<ApiEnvelope<AuthUserDto>> GetCurrentUserAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var account = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
return ApiEnvelope<AuthUserDto>.Failure("MissingAccessToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _apiClient.GetCurrentUserAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!response.IsSuccess)
|
||||||
|
{
|
||||||
|
_logger.Warning("Failed to load Wino account profile for {Email}. Error code: {ErrorCode}", account.Email, response.ErrorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiEnvelope<AiStatusResultDto>> GetAiStatusAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var account = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
return ApiEnvelope<AiStatusResultDto>.Failure("MissingAccessToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _apiClient.GetAiStatusAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!response.IsSuccess)
|
||||||
|
{
|
||||||
|
_logger.Warning("Failed to load AI status for Wino account {Email}. Error code: {ErrorCode}", account.Email, response.ErrorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiEnvelope<string>> CreateCheckoutSessionAsync(string productId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var account = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
return ApiEnvelope<string>.Failure("MissingAccessToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _apiClient.CreateCheckoutSessionAsync(productId, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!response.IsSuccess)
|
||||||
|
{
|
||||||
|
_logger.Warning("Failed to create checkout session for product {ProductId} and Wino account {Email}. Error code: {ErrorCode}", productId, account.Email, response.ErrorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SignOutAsync(CancellationToken cancellationToken = default)
|
public async Task SignOutAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
||||||
@@ -101,6 +191,7 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
{
|
{
|
||||||
if (!response.IsSuccess || response.Result == null)
|
if (!response.IsSuccess || response.Result == null)
|
||||||
{
|
{
|
||||||
|
_logger.Warning("Wino account operation failed. Error code: {ErrorCode}", response.ErrorCode);
|
||||||
return WinoAccountOperationResult.Failure(response.ErrorCode);
|
return WinoAccountOperationResult.Failure(response.ErrorCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user