Forgot password and email confirmations.
This commit is contained in:
@@ -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.6" />
|
<PackageVersion Include="Wino.Mail.Contracts" Version="1.0.7" />
|
||||||
<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" />
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ public interface IWinoAccountApiClient
|
|||||||
Task<WinoAccountApiResult<AuthResultDto>> RegisterAsync(string email, string password, CancellationToken cancellationToken = default);
|
Task<WinoAccountApiResult<AuthResultDto>> RegisterAsync(string email, string password, CancellationToken cancellationToken = default);
|
||||||
Task<WinoAccountApiResult<AuthResultDto>> LoginAsync(string email, string password, CancellationToken cancellationToken = default);
|
Task<WinoAccountApiResult<AuthResultDto>> LoginAsync(string email, string password, CancellationToken cancellationToken = default);
|
||||||
Task<WinoAccountApiResult<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default);
|
Task<WinoAccountApiResult<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default);
|
||||||
|
Task<ApiEnvelope<EmailConfirmationResendResultDto>> ResendEmailConfirmationAsync(string endpoint, string ticket, CancellationToken cancellationToken = default);
|
||||||
|
Task<ApiEnvelope<JsonElement>> ForgotPasswordAsync(string email, CancellationToken cancellationToken = default);
|
||||||
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);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
@@ -18,6 +19,8 @@ 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<WinoAccountOperationResult> RefreshProfileAsync(CancellationToken cancellationToken = default);
|
Task<WinoAccountOperationResult> RefreshProfileAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<ApiEnvelope<EmailConfirmationResendResultDto>> ResendEmailConfirmationAsync(string endpoint, string ticket, CancellationToken cancellationToken = default);
|
||||||
|
Task<ApiEnvelope<JsonElement>> ForgotPasswordAsync(string email, CancellationToken cancellationToken = default);
|
||||||
Task<WinoAccount?> GetActiveAccountAsync();
|
Task<WinoAccount?> GetActiveAccountAsync();
|
||||||
Task<WinoAccount?> GetAuthenticatedAccountAsync(CancellationToken cancellationToken = default);
|
Task<WinoAccount?> GetAuthenticatedAccountAsync(CancellationToken cancellationToken = default);
|
||||||
Task<bool> HasActiveAccountAsync();
|
Task<bool> HasActiveAccountAsync();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Accounts;
|
namespace Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ public sealed class WinoAccountApiResult<T>
|
|||||||
public bool IsSuccess { get; init; }
|
public bool IsSuccess { get; init; }
|
||||||
public string? ErrorCode { get; init; }
|
public string? ErrorCode { get; init; }
|
||||||
public string? ErrorMessage { get; init; }
|
public string? ErrorMessage { get; init; }
|
||||||
|
public JsonElement? ErrorDetails { get; init; }
|
||||||
public T? Result { get; init; }
|
public T? Result { get; init; }
|
||||||
|
|
||||||
public static WinoAccountApiResult<T> Success(T result)
|
public static WinoAccountApiResult<T> Success(T result)
|
||||||
@@ -16,11 +18,12 @@ public sealed class WinoAccountApiResult<T>
|
|||||||
Result = result
|
Result = result
|
||||||
};
|
};
|
||||||
|
|
||||||
public static WinoAccountApiResult<T> Failure(string? errorCode, string? errorMessage = null)
|
public static WinoAccountApiResult<T> Failure(string? errorCode, string? errorMessage = null, JsonElement? errorDetails = null)
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
IsSuccess = false,
|
IsSuccess = false,
|
||||||
ErrorCode = errorCode,
|
ErrorCode = errorCode,
|
||||||
ErrorMessage = errorMessage
|
ErrorMessage = errorMessage,
|
||||||
|
ErrorDetails = errorDetails
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
using System.Text.Json;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Accounts;
|
namespace Wino.Core.Domain.Models.Accounts;
|
||||||
@@ -8,6 +9,7 @@ public sealed class WinoAccountOperationResult
|
|||||||
public bool IsSuccess { get; init; }
|
public bool IsSuccess { get; init; }
|
||||||
public string? ErrorCode { get; init; }
|
public string? ErrorCode { get; init; }
|
||||||
public string? ErrorMessage { get; init; }
|
public string? ErrorMessage { get; init; }
|
||||||
|
public JsonElement? ErrorDetails { get; init; }
|
||||||
public WinoAccount? Account { get; init; }
|
public WinoAccount? Account { get; init; }
|
||||||
|
|
||||||
public static WinoAccountOperationResult Success(WinoAccount account)
|
public static WinoAccountOperationResult Success(WinoAccount account)
|
||||||
@@ -17,11 +19,12 @@ public sealed class WinoAccountOperationResult
|
|||||||
Account = account
|
Account = account
|
||||||
};
|
};
|
||||||
|
|
||||||
public static WinoAccountOperationResult Failure(string? errorCode, string? errorMessage = null)
|
public static WinoAccountOperationResult Failure(string? errorCode, string? errorMessage = null, JsonElement? errorDetails = null)
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
IsSuccess = false,
|
IsSuccess = false,
|
||||||
ErrorCode = errorCode,
|
ErrorCode = errorCode,
|
||||||
ErrorMessage = errorMessage
|
ErrorMessage = errorMessage,
|
||||||
|
ErrorDetails = errorDetails
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1238,10 +1238,15 @@
|
|||||||
"WinoAccount_LoginDialog_BenefitsDescription": "Use your Wino Account to continue syncing settings across devices and to access paid add-ons such as Wino AI Pack.",
|
"WinoAccount_LoginDialog_BenefitsDescription": "Use your Wino Account to continue syncing settings across devices and to access paid add-ons such as Wino AI Pack.",
|
||||||
"WinoAccount_LoginDialog_DifferenceTitle": "This is not your email mailbox sign-in",
|
"WinoAccount_LoginDialog_DifferenceTitle": "This is not your email mailbox sign-in",
|
||||||
"WinoAccount_LoginDialog_DifferenceDescription": "Signing in here does not add or replace your Outlook, Gmail, or IMAP accounts in Wino. It only signs you in to Wino-specific services.",
|
"WinoAccount_LoginDialog_DifferenceDescription": "Signing in here does not add or replace your Outlook, Gmail, or IMAP accounts in Wino. It only signs you in to Wino-specific services.",
|
||||||
|
"WinoAccount_LoginDialog_ForgotPasswordLink": "Forgot password?",
|
||||||
"WinoAccount_EmailLabel": "Email",
|
"WinoAccount_EmailLabel": "Email",
|
||||||
"WinoAccount_EmailPlaceholder": "name@example.com",
|
"WinoAccount_EmailPlaceholder": "name@example.com",
|
||||||
"WinoAccount_PasswordLabel": "Password",
|
"WinoAccount_PasswordLabel": "Password",
|
||||||
"WinoAccount_ConfirmPasswordLabel": "Confirm password",
|
"WinoAccount_ConfirmPasswordLabel": "Confirm password",
|
||||||
|
"WinoAccount_ForgotPasswordDialog_Title": "Reset your password",
|
||||||
|
"WinoAccount_ForgotPasswordDialog_PrimaryButton": "Send reset email",
|
||||||
|
"WinoAccount_ForgotPasswordDialog_BackToSignIn": "Back to sign in",
|
||||||
|
"WinoAccount_ForgotPasswordDialog_Description": "Enter your Wino Account email address and we will send you a password reset link if the address is registered.",
|
||||||
"WinoAccount_Validation_EmailRequired": "Email is required.",
|
"WinoAccount_Validation_EmailRequired": "Email is required.",
|
||||||
"WinoAccount_Validation_PasswordRequired": "Password is required.",
|
"WinoAccount_Validation_PasswordRequired": "Password is required.",
|
||||||
"WinoAccount_Validation_PasswordMismatch": "Passwords do not match.",
|
"WinoAccount_Validation_PasswordMismatch": "Passwords do not match.",
|
||||||
@@ -1251,6 +1256,10 @@
|
|||||||
"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_EmailNotConfirmed": "Please confirm your email address before signing in.",
|
||||||
|
"WinoAccount_Error_EmailConfirmationRequired": "Please confirm your email address before signing in.",
|
||||||
|
"WinoAccount_Error_EmailConfirmationResendNotAvailable": "A new confirmation email is not available yet.",
|
||||||
|
"WinoAccount_Error_EmailConfirmationResendInvalid": "This confirmation request is no longer valid. Please try signing in again.",
|
||||||
|
"WinoAccount_Error_EmailNotRegistered": "This email address is not registered.",
|
||||||
"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.",
|
||||||
@@ -1261,6 +1270,21 @@
|
|||||||
"WinoAccount_Error_ValidationFailed": "The request is invalid. Please review the entered values.",
|
"WinoAccount_Error_ValidationFailed": "The request is invalid. Please review the entered values.",
|
||||||
"WinoAccount_RegisterSuccessMessage": "Wino Account registration completed for {0}.",
|
"WinoAccount_RegisterSuccessMessage": "Wino Account registration completed for {0}.",
|
||||||
"WinoAccount_LoginSuccessMessage": "Signed in to Wino Account as {0}.",
|
"WinoAccount_LoginSuccessMessage": "Signed in to Wino Account as {0}.",
|
||||||
|
"WinoAccount_EmailConfirmationSentDialog_Title": "Confirm your email address",
|
||||||
|
"WinoAccount_EmailConfirmationSentDialog_Message": "We sent an email confirmation to {0}. Please confirm it and try signing in again.",
|
||||||
|
"WinoAccount_EmailConfirmationPendingDialog_Title": "Email confirmation required",
|
||||||
|
"WinoAccount_EmailConfirmationPendingDialog_Message": "We are still waiting for you to confirm {0}.",
|
||||||
|
"WinoAccount_EmailConfirmationPendingDialog_ResendButton": "Resend confirmation email",
|
||||||
|
"WinoAccount_EmailConfirmationPendingDialog_Countdown": "You can resend the confirmation email in {0}.",
|
||||||
|
"WinoAccount_EmailConfirmationPendingDialog_ReadyToResend": "You can resend the confirmation email now.",
|
||||||
|
"WinoAccount_EmailConfirmationResentDialog_Title": "Confirmation email resent",
|
||||||
|
"WinoAccount_EmailConfirmationResentDialog_Message": "We sent another confirmation email to {0}. Please confirm it and try signing in again.",
|
||||||
|
"WinoAccount_ForgotPasswordDialog_SuccessTitle": "Password reset email sent",
|
||||||
|
"WinoAccount_ForgotPasswordDialog_SuccessMessage": "We sent a password reset email to {0}. Open that message to choose a new password.",
|
||||||
|
"WinoAccount_ChangePassword_Title": "Change password",
|
||||||
|
"WinoAccount_ChangePassword_Description": "Send a password reset email to this Wino Account.",
|
||||||
|
"WinoAccount_ChangePassword_Action": "Send reset email",
|
||||||
|
"WinoAccount_ChangePassword_ConfirmationMessage": "Do you want Wino to send a password reset email to {0}?",
|
||||||
"WinoAccount_SignOut_SuccessMessage": "Signed out from Wino Account {0}.",
|
"WinoAccount_SignOut_SuccessMessage": "Signed out from Wino Account {0}.",
|
||||||
"WinoAccount_SignOut_NoAccountMessage": "There is no active Wino Account to sign out.",
|
"WinoAccount_SignOut_NoAccountMessage": "There is no active Wino Account to sign out.",
|
||||||
"WinoAccount_Titlebar_SignedOutTitle": "Wino Account",
|
"WinoAccount_Titlebar_SignedOutTitle": "Wino Account",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
@@ -119,6 +120,57 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
|||||||
result.ErrorMessage.Should().Be("Password does not match this account.");
|
result.ErrorMessage.Should().Be("Password does not match this account.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LoginAsync_ShouldPreserveErrorDetails()
|
||||||
|
{
|
||||||
|
var details = JsonSerializer.SerializeToElement(new EmailConfirmationRequiredDetailsDto(
|
||||||
|
"/api/v1/auth/confirm-email/resend",
|
||||||
|
"ticket",
|
||||||
|
DateTimeOffset.UtcNow.AddMinutes(-2),
|
||||||
|
DateTimeOffset.UtcNow.AddMinutes(8)));
|
||||||
|
|
||||||
|
_apiClient
|
||||||
|
.Setup(x => x.LoginAsync("first@example.com", "pw", default))
|
||||||
|
.ReturnsAsync(WinoAccountApiResult<AuthResultDto>.Failure(ApiErrorCodes.EmailNotConfirmed, null, details));
|
||||||
|
|
||||||
|
var result = await _service.LoginAsync("first@example.com", "pw");
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeFalse();
|
||||||
|
result.ErrorDetails.Should().NotBeNull();
|
||||||
|
JsonSerializer.Deserialize<EmailConfirmationRequiredDetailsDto>(result.ErrorDetails!.Value.GetRawText())!
|
||||||
|
.ResendConfirmationTicket.Should().Be("ticket");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RegisterAsync_ShouldNotPersistAccountUntilEmailIsConfirmed()
|
||||||
|
{
|
||||||
|
var authResult = CreateAuthResult("register@example.com");
|
||||||
|
|
||||||
|
_apiClient
|
||||||
|
.Setup(x => x.RegisterAsync("register@example.com", "pw", default))
|
||||||
|
.ReturnsAsync(WinoAccountApiResult<AuthResultDto>.Success(authResult));
|
||||||
|
|
||||||
|
var result = await _service.RegisterAsync("register@example.com", "pw");
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Account.Should().NotBeNull();
|
||||||
|
|
||||||
|
var persisted = await _databaseService.Connection.Table<WinoAccount>().ToListAsync();
|
||||||
|
persisted.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ForgotPasswordAsync_ShouldForwardApiResponse()
|
||||||
|
{
|
||||||
|
_apiClient
|
||||||
|
.Setup(x => x.ForgotPasswordAsync("reset@example.com", default))
|
||||||
|
.ReturnsAsync(ApiEnvelope<JsonElement>.Success(default));
|
||||||
|
|
||||||
|
var result = await _service.ForgotPasswordAsync("reset@example.com");
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RefreshProfileAsync_ShouldPersistLatestProfileData()
|
public async Task RefreshProfileAsync_ShouldPersistLatestProfileData()
|
||||||
{
|
{
|
||||||
@@ -136,7 +188,8 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
|||||||
"Premium",
|
"Premium",
|
||||||
authResult.User.HasPassword,
|
authResult.User.HasPassword,
|
||||||
authResult.User.HasGoogleLogin,
|
authResult.User.HasGoogleLogin,
|
||||||
authResult.User.HasFacebookLogin)));
|
authResult.User.HasFacebookLogin,
|
||||||
|
authResult.User.HasUnlimitedAccounts)));
|
||||||
|
|
||||||
await _service.LoginAsync("first@example.com", "pw");
|
await _service.LoginAsync("first@example.com", "pw");
|
||||||
|
|
||||||
@@ -184,7 +237,7 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
|||||||
private static AuthResultDto CreateAuthResult(string email)
|
private static AuthResultDto CreateAuthResult(string email)
|
||||||
{
|
{
|
||||||
return new AuthResultDto(
|
return new AuthResultDto(
|
||||||
new AuthUserDto(Guid.NewGuid(), email, "Active", true, false, false),
|
new AuthUserDto(Guid.NewGuid(), email, "Active", true, false, false, false),
|
||||||
"access-token",
|
"access-token",
|
||||||
DateTimeOffset.UtcNow.AddMinutes(30),
|
DateTimeOffset.UtcNow.AddMinutes(30),
|
||||||
"refresh-token",
|
"refresh-token",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.ViewModels.Data;
|
using Wino.Core.ViewModels.Data;
|
||||||
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
namespace Wino.Core.ViewModels;
|
namespace Wino.Core.ViewModels;
|
||||||
@@ -112,6 +113,51 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
InfoBarMessageType.Success);
|
InfoBarMessageType.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ChangePasswordAsync()
|
||||||
|
{
|
||||||
|
var account = await _profileService.GetActiveAccountAsync().ConfigureAwait(false);
|
||||||
|
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,
|
||||||
|
Translator.WinoAccount_ChangePassword_Action).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!shouldContinue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _profileService.ForgotPasswordAsync(account.Email).ConfigureAwait(false);
|
||||||
|
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!
|
||||||
|
};
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanPurchaseAddOn))]
|
[RelayCommand(CanExecute = nameof(CanPurchaseAddOn))]
|
||||||
private async Task PurchaseAddOnAsync(WinoAddOnItemViewModel? addOn)
|
private async Task PurchaseAddOnAsync(WinoAddOnItemViewModel? addOn)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<ContentDialog
|
||||||
|
x:Class="Wino.Dialogs.WinoAccountEmailConfirmationRequiredDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
Title="{x:Bind domain:Translator.WinoAccount_EmailConfirmationPendingDialog_Title}"
|
||||||
|
PrimaryButtonClick="ResendClicked"
|
||||||
|
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
|
||||||
|
PrimaryButtonText="{x:Bind domain:Translator.WinoAccount_EmailConfirmationPendingDialog_ResendButton}"
|
||||||
|
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Close}"
|
||||||
|
Style="{StaticResource WinoDialogStyle}"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<ContentDialog.Resources>
|
||||||
|
<x:Double x:Key="ContentDialogMinWidth">520</x:Double>
|
||||||
|
<x:Double x:Key="ContentDialogMaxWidth">520</x:Double>
|
||||||
|
</ContentDialog.Resources>
|
||||||
|
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<Border
|
||||||
|
Padding="14"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||||
|
CornerRadius="12">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock
|
||||||
|
x:Name="MessageTextBlock"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
<TextBlock
|
||||||
|
x:Name="CountdownTextBlock"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ProgressRing
|
||||||
|
x:Name="BusyRing"
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
IsActive="False"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
x:Name="ErrorTextBlock"
|
||||||
|
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||||
|
TextWrapping="WrapWholeWords"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
</StackPanel>
|
||||||
|
</ContentDialog>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
|
using Wino.Mail.WinUI.Services;
|
||||||
|
|
||||||
|
namespace Wino.Dialogs;
|
||||||
|
|
||||||
|
public sealed partial class WinoAccountEmailConfirmationRequiredDialog : ContentDialog
|
||||||
|
{
|
||||||
|
private readonly IWinoAccountProfileService _profileService;
|
||||||
|
private readonly DispatcherTimer _countdownTimer;
|
||||||
|
private readonly string _email;
|
||||||
|
private readonly string _endpoint;
|
||||||
|
private readonly string _ticket;
|
||||||
|
private DateTimeOffset _resendAvailableAtUtc;
|
||||||
|
|
||||||
|
public WinoAccountEmailConfirmationRequiredDialog(IWinoAccountProfileService profileService, string email, EmailConfirmationRequiredDetailsDto details)
|
||||||
|
{
|
||||||
|
_profileService = profileService;
|
||||||
|
_email = email;
|
||||||
|
_endpoint = details.ResendConfirmationEndpoint;
|
||||||
|
_ticket = details.ResendConfirmationTicket;
|
||||||
|
_resendAvailableAtUtc = details.ResendAvailableAtUtc;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
MessageTextBlock.Text = string.Format(Translator.WinoAccount_EmailConfirmationPendingDialog_Message, email);
|
||||||
|
|
||||||
|
_countdownTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
_countdownTimer.Tick += CountdownTimer_Tick;
|
||||||
|
|
||||||
|
Closing += DialogClosing;
|
||||||
|
|
||||||
|
UpdateCountdown();
|
||||||
|
_countdownTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ResendSucceeded { get; private set; }
|
||||||
|
|
||||||
|
private async void ResendClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||||
|
{
|
||||||
|
args.Cancel = true;
|
||||||
|
|
||||||
|
if (DateTimeOffset.UtcNow < _resendAvailableAtUtc)
|
||||||
|
{
|
||||||
|
UpdateCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deferral = args.GetDeferral();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SetBusyState(true);
|
||||||
|
HideError();
|
||||||
|
|
||||||
|
var response = await _profileService.ResendEmailConfirmationAsync(_endpoint, _ticket);
|
||||||
|
if (!response.IsSuccess)
|
||||||
|
{
|
||||||
|
ShowError(WinoAccountAuthErrorTranslator.Translate(response.ErrorCode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResendSucceeded = true;
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetBusyState(false);
|
||||||
|
deferral.Complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CountdownTimer_Tick(object? sender, object e) => UpdateCountdown();
|
||||||
|
|
||||||
|
private void UpdateCountdown()
|
||||||
|
{
|
||||||
|
var remaining = _resendAvailableAtUtc - DateTimeOffset.UtcNow;
|
||||||
|
if (remaining <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
IsPrimaryButtonEnabled = true;
|
||||||
|
CountdownTextBlock.Text = Translator.WinoAccount_EmailConfirmationPendingDialog_ReadyToResend;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsPrimaryButtonEnabled = false;
|
||||||
|
CountdownTextBlock.Text = string.Format(
|
||||||
|
Translator.WinoAccount_EmailConfirmationPendingDialog_Countdown,
|
||||||
|
$"{Math.Max(0, (int)remaining.TotalMinutes):00}:{Math.Max(0, remaining.Seconds):00}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetBusyState(bool isBusy)
|
||||||
|
{
|
||||||
|
IsPrimaryButtonEnabled = !isBusy && DateTimeOffset.UtcNow >= _resendAvailableAtUtc;
|
||||||
|
IsSecondaryButtonEnabled = !isBusy;
|
||||||
|
BusyRing.IsActive = isBusy;
|
||||||
|
BusyRing.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowError(string message)
|
||||||
|
{
|
||||||
|
ErrorTextBlock.Text = message;
|
||||||
|
ErrorTextBlock.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideError()
|
||||||
|
{
|
||||||
|
ErrorTextBlock.Text = string.Empty;
|
||||||
|
ErrorTextBlock.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DialogClosing(ContentDialog sender, ContentDialogClosingEventArgs args)
|
||||||
|
{
|
||||||
|
_countdownTimer.Stop();
|
||||||
|
_countdownTimer.Tick -= CountdownTimer_Tick;
|
||||||
|
Closing -= DialogClosing;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -152,7 +152,9 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Benefits cards -->
|
<!-- Benefits cards -->
|
||||||
<StackPanel Spacing="8">
|
<StackPanel
|
||||||
|
x:Name="BenefitsPanel"
|
||||||
|
Spacing="8">
|
||||||
<Border
|
<Border
|
||||||
Padding="14"
|
Padding="14"
|
||||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||||
@@ -227,13 +229,32 @@
|
|||||||
PlaceholderText="{x:Bind domain:Translator.WinoAccount_EmailPlaceholder}"
|
PlaceholderText="{x:Bind domain:Translator.WinoAccount_EmailPlaceholder}"
|
||||||
TextChanging="InputChanged" />
|
TextChanging="InputChanged" />
|
||||||
|
|
||||||
<PasswordBox
|
<StackPanel x:Name="PasswordPanel">
|
||||||
x:Name="PasswordBox"
|
<PasswordBox
|
||||||
Header="{x:Bind domain:Translator.WinoAccount_PasswordLabel}"
|
x:Name="PasswordBox"
|
||||||
KeyDown="PasswordBox_KeyDown"
|
Header="{x:Bind domain:Translator.WinoAccount_PasswordLabel}"
|
||||||
PasswordChanged="InputChanged" />
|
KeyDown="PasswordBox_KeyDown"
|
||||||
|
PasswordChanged="InputChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
x:Name="ForgotPasswordInfoPanel"
|
||||||
|
Padding="14"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||||
|
CornerRadius="12"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<TextBlock
|
||||||
|
Text="{x:Bind domain:Translator.WinoAccount_ForgotPasswordDialog_Description}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<HyperlinkButton
|
||||||
|
x:Name="ModeToggleButton"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Click="ModeToggleButton_Click"
|
||||||
|
Content="{x:Bind domain:Translator.WinoAccount_LoginDialog_ForgotPasswordLink}" />
|
||||||
|
|
||||||
<ProgressRing
|
<ProgressRing
|
||||||
x:Name="BusyRing"
|
x:Name="BusyRing"
|
||||||
Width="20"
|
Width="20"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Windows.System;
|
|||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.WinUI.Services;
|
using Wino.Mail.WinUI.Services;
|
||||||
|
|
||||||
namespace Wino.Dialogs;
|
namespace Wino.Dialogs;
|
||||||
@@ -12,14 +13,19 @@ namespace Wino.Dialogs;
|
|||||||
public sealed partial class WinoAccountLoginDialog : ContentDialog
|
public sealed partial class WinoAccountLoginDialog : ContentDialog
|
||||||
{
|
{
|
||||||
private readonly IWinoAccountProfileService _profileService;
|
private readonly IWinoAccountProfileService _profileService;
|
||||||
|
private bool _isForgotPasswordMode;
|
||||||
|
|
||||||
public WinoAccountLoginDialog(IWinoAccountProfileService profileService)
|
public WinoAccountLoginDialog(IWinoAccountProfileService profileService)
|
||||||
{
|
{
|
||||||
_profileService = profileService;
|
_profileService = profileService;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
UpdateMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public WinoAccount? Result { get; private set; }
|
public WinoAccount? Result { get; private set; }
|
||||||
|
public string? PendingConfirmationEmailAddress { get; private set; }
|
||||||
|
public EmailConfirmationRequiredDetailsDto? EmailConfirmationRequiredDetails { get; private set; }
|
||||||
|
public string? PasswordResetEmailAddress { get; private set; }
|
||||||
|
|
||||||
private async void LoginClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
private async void LoginClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||||
{
|
{
|
||||||
@@ -36,7 +42,7 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await PerformLoginAsync();
|
await PerformPrimaryActionAsync();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -44,7 +50,7 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task PerformLoginAsync()
|
private async System.Threading.Tasks.Task PerformPrimaryActionAsync()
|
||||||
{
|
{
|
||||||
var validationError = ValidateInput();
|
var validationError = ValidateInput();
|
||||||
if (!string.IsNullOrWhiteSpace(validationError))
|
if (!string.IsNullOrWhiteSpace(validationError))
|
||||||
@@ -58,10 +64,33 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
|
|||||||
SetBusyState(true);
|
SetBusyState(true);
|
||||||
HideError();
|
HideError();
|
||||||
|
|
||||||
|
if (_isForgotPasswordMode)
|
||||||
|
{
|
||||||
|
var forgotPasswordResponse = await _profileService.ForgotPasswordAsync(EmailTextBox.Text.Trim());
|
||||||
|
if (!forgotPasswordResponse.IsSuccess)
|
||||||
|
{
|
||||||
|
ShowError(WinoAccountAuthErrorTranslator.Translate(forgotPasswordResponse.ErrorCode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PasswordResetEmailAddress = EmailTextBox.Text.Trim();
|
||||||
|
Hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _profileService.LoginAsync(EmailTextBox.Text.Trim(), PasswordBox.Password);
|
var result = await _profileService.LoginAsync(EmailTextBox.Text.Trim(), PasswordBox.Password);
|
||||||
|
|
||||||
if (!result.IsSuccess || result.Account == null)
|
if (!result.IsSuccess || result.Account == null)
|
||||||
{
|
{
|
||||||
|
var confirmationDetails = WinoAccountEmailConfirmationHelper.Parse(result.ErrorDetails);
|
||||||
|
if (WinoAccountEmailConfirmationHelper.IsEmailConfirmationRequiredError(result.ErrorCode) && confirmationDetails != null)
|
||||||
|
{
|
||||||
|
PendingConfirmationEmailAddress = EmailTextBox.Text.Trim();
|
||||||
|
EmailConfirmationRequiredDetails = confirmationDetails;
|
||||||
|
Hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ShowError(WinoAccountAuthErrorTranslator.Format(result.ErrorCode, result.ErrorMessage));
|
ShowError(WinoAccountAuthErrorTranslator.Format(result.ErrorCode, result.ErrorMessage));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -82,6 +111,11 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
|
|||||||
return Translator.WinoAccount_Validation_EmailRequired;
|
return Translator.WinoAccount_Validation_EmailRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_isForgotPasswordMode)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(PasswordBox.Password))
|
if (string.IsNullOrWhiteSpace(PasswordBox.Password))
|
||||||
{
|
{
|
||||||
return Translator.WinoAccount_Validation_PasswordRequired;
|
return Translator.WinoAccount_Validation_PasswordRequired;
|
||||||
@@ -90,10 +124,17 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EmailTextBox_KeyDown(object sender, KeyRoutedEventArgs e)
|
private async void EmailTextBox_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == VirtualKey.Enter)
|
if (e.Key == VirtualKey.Enter)
|
||||||
{
|
{
|
||||||
|
if (_isForgotPasswordMode)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
await PerformPrimaryActionAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
PasswordBox.Focus(FocusState.Programmatic);
|
PasswordBox.Focus(FocusState.Programmatic);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
@@ -104,10 +145,18 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
|
|||||||
if (e.Key == VirtualKey.Enter)
|
if (e.Key == VirtualKey.Enter)
|
||||||
{
|
{
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
await PerformLoginAsync();
|
await PerformPrimaryActionAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ModeToggleButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_isForgotPasswordMode = !_isForgotPasswordMode;
|
||||||
|
PasswordBox.Password = string.Empty;
|
||||||
|
HideError();
|
||||||
|
UpdateMode();
|
||||||
|
}
|
||||||
|
|
||||||
private void InputChanged(TextBox sender, TextBoxTextChangingEventArgs args) => HideError();
|
private void InputChanged(TextBox sender, TextBoxTextChangingEventArgs args) => HideError();
|
||||||
|
|
||||||
private void InputChanged(object sender, RoutedEventArgs e) => HideError();
|
private void InputChanged(object sender, RoutedEventArgs e) => HideError();
|
||||||
@@ -131,4 +180,22 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
|
|||||||
ErrorTextBlock.Text = string.Empty;
|
ErrorTextBlock.Text = string.Empty;
|
||||||
ErrorTextBlock.Visibility = Visibility.Collapsed;
|
ErrorTextBlock.Visibility = Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateMode()
|
||||||
|
{
|
||||||
|
Title = _isForgotPasswordMode
|
||||||
|
? Translator.WinoAccount_ForgotPasswordDialog_Title
|
||||||
|
: Translator.WinoAccount_LoginDialog_Title;
|
||||||
|
|
||||||
|
PrimaryButtonText = _isForgotPasswordMode
|
||||||
|
? Translator.WinoAccount_ForgotPasswordDialog_PrimaryButton
|
||||||
|
: Translator.Buttons_SignIn;
|
||||||
|
|
||||||
|
BenefitsPanel.Visibility = _isForgotPasswordMode ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
PasswordPanel.Visibility = _isForgotPasswordMode ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
ForgotPasswordInfoPanel.Visibility = _isForgotPasswordMode ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
ModeToggleButton.Content = _isForgotPasswordMode
|
||||||
|
? Translator.WinoAccount_ForgotPasswordDialog_BackToSignIn
|
||||||
|
: Translator.WinoAccount_LoginDialog_ForgotPasswordLink;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public sealed partial class WinoAccountRegistrationDialog : ContentDialog
|
|||||||
}
|
}
|
||||||
|
|
||||||
public WinoAccount? Result { get; private set; }
|
public WinoAccount? Result { get; private set; }
|
||||||
|
public string? ConfirmationEmailAddress { get; private set; }
|
||||||
|
|
||||||
private async void RegisterClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
private async void RegisterClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||||
{
|
{
|
||||||
@@ -68,7 +69,7 @@ public sealed partial class WinoAccountRegistrationDialog : ContentDialog
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result = result.Account;
|
ConfirmationEmailAddress = result.Account.Email;
|
||||||
Hide();
|
Hide();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -225,11 +225,16 @@ public class DialogService : DialogServiceBase, IMailDialogService
|
|||||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await HandleDialogPresentationAsync(dialog);
|
await HandleDialogPresentationAsync(dialog);
|
||||||
|
|
||||||
return result == ContentDialogResult.Primary
|
if (!string.IsNullOrWhiteSpace(dialog.ConfirmationEmailAddress))
|
||||||
? dialog.Result
|
{
|
||||||
: null;
|
await ShowMessageAsync(
|
||||||
|
string.Format(Translator.WinoAccount_EmailConfirmationSentDialog_Message, dialog.ConfirmationEmailAddress),
|
||||||
|
Translator.WinoAccount_EmailConfirmationSentDialog_Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WinoAccount?> ShowWinoAccountLoginDialogAsync()
|
public async Task<WinoAccount?> ShowWinoAccountLoginDialogAsync()
|
||||||
@@ -241,8 +246,37 @@ public class DialogService : DialogServiceBase, IMailDialogService
|
|||||||
|
|
||||||
var result = await HandleDialogPresentationAsync(dialog);
|
var result = await HandleDialogPresentationAsync(dialog);
|
||||||
|
|
||||||
return result == ContentDialogResult.Primary
|
if (dialog.EmailConfirmationRequiredDetails != null && !string.IsNullOrWhiteSpace(dialog.PendingConfirmationEmailAddress))
|
||||||
? dialog.Result
|
{
|
||||||
: null;
|
var confirmationDialog = new WinoAccountEmailConfirmationRequiredDialog(
|
||||||
|
_winoAccountProfileService,
|
||||||
|
dialog.PendingConfirmationEmailAddress,
|
||||||
|
dialog.EmailConfirmationRequiredDetails)
|
||||||
|
{
|
||||||
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
||||||
|
};
|
||||||
|
|
||||||
|
await HandleDialogPresentationAsync(confirmationDialog);
|
||||||
|
|
||||||
|
if (confirmationDialog.ResendSucceeded)
|
||||||
|
{
|
||||||
|
await ShowMessageAsync(
|
||||||
|
string.Format(Translator.WinoAccount_EmailConfirmationResentDialog_Message, dialog.PendingConfirmationEmailAddress),
|
||||||
|
Translator.WinoAccount_EmailConfirmationResentDialog_Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(dialog.PasswordResetEmailAddress))
|
||||||
|
{
|
||||||
|
await ShowMessageAsync(
|
||||||
|
string.Format(Translator.WinoAccount_ForgotPasswordDialog_SuccessMessage, dialog.PasswordResetEmailAddress),
|
||||||
|
Translator.WinoAccount_ForgotPasswordDialog_SuccessTitle);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialog.Result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ public static class WinoAccountAuthErrorTranslator
|
|||||||
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.EmailNotConfirmed => Translator.WinoAccount_Error_EmailNotConfirmed,
|
||||||
|
ApiErrorCodes.EmailConfirmationRequired => Translator.WinoAccount_Error_EmailConfirmationRequired,
|
||||||
|
ApiErrorCodes.EmailConfirmationResendNotAvailable => Translator.WinoAccount_Error_EmailConfirmationResendNotAvailable,
|
||||||
|
ApiErrorCodes.EmailConfirmationResendInvalid => Translator.WinoAccount_Error_EmailConfirmationResendInvalid,
|
||||||
|
ApiErrorCodes.EmailNotRegistered => Translator.WinoAccount_Error_EmailNotRegistered,
|
||||||
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,
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Services;
|
||||||
|
|
||||||
|
internal static class WinoAccountEmailConfirmationHelper
|
||||||
|
{
|
||||||
|
public static bool IsEmailConfirmationRequiredError(string? errorCode)
|
||||||
|
=> string.Equals(errorCode, Wino.Mail.Api.Contracts.Common.ApiErrorCodes.EmailNotConfirmed, StringComparison.Ordinal) ||
|
||||||
|
string.Equals(errorCode, Wino.Mail.Api.Contracts.Common.ApiErrorCodes.EmailConfirmationRequired, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public static EmailConfirmationRequiredDetailsDto? Parse(JsonElement? details)
|
||||||
|
{
|
||||||
|
if (details is not JsonElement element || element.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!TryGetString(element, "resendConfirmationEndpoint", out var endpoint) ||
|
||||||
|
!TryGetString(element, "resendConfirmationTicket", out var ticket) ||
|
||||||
|
!TryGetDateTimeOffset(element, "resendAvailableAtUtc", out var resendAvailableAtUtc))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset? latestSentUtc = null;
|
||||||
|
if (element.TryGetProperty("latestConfirmationEmailSentUtc", out var latestSentElement) &&
|
||||||
|
latestSentElement.ValueKind == JsonValueKind.String &&
|
||||||
|
DateTimeOffset.TryParse(latestSentElement.GetString(), out var latestParsed))
|
||||||
|
{
|
||||||
|
latestSentUtc = latestParsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EmailConfirmationRequiredDetailsDto(endpoint, ticket, latestSentUtc, resendAvailableAtUtc);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetString(JsonElement element, string propertyName, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
|
||||||
|
if (!element.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = property.GetString() ?? string.Empty;
|
||||||
|
return !string.IsNullOrWhiteSpace(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetDateTimeOffset(JsonElement element, string propertyName, out DateTimeOffset value)
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
|
||||||
|
if (!element.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTimeOffset.TryParse(property.GetString(), out value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
</TransitionCollection>
|
</TransitionCollection>
|
||||||
</StackPanel.ChildrenTransitions>
|
</StackPanel.ChildrenTransitions>
|
||||||
<controls:SettingsExpander
|
<controls:SettingsExpander
|
||||||
Description="{x:Bind domain:Translator.SettingsSignatureAndEncryption_MyCertificatesDescription, Mode=OneWay}"
|
Description="{x:Bind domain:Translator.SettingsSignatureAndEncryption_MyCertificatesDescription, Mode=OneTime}"
|
||||||
Header="{x:Bind domain:Translator.SettingsSignatureAndEncryption_MyCertificatesHeader, Mode=OneWay}"
|
Header="{x:Bind domain:Translator.SettingsSignatureAndEncryption_MyCertificatesHeader, Mode=OneTime}"
|
||||||
IsExpanded="True">
|
IsExpanded="True">
|
||||||
<controls:SettingsExpander.Items>
|
<controls:SettingsExpander.Items>
|
||||||
<controls:SettingsCard>
|
<controls:SettingsCard>
|
||||||
@@ -44,15 +44,15 @@
|
|||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_NameColumn, Mode=OneWay}" />
|
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_NameColumn, Mode=OneTime}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_ExpiresColumn, Mode=OneWay}" />
|
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_ExpiresColumn, Mode=OneTime}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="3"
|
Grid.Column="3"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_ThumbprintColumn, Mode=OneWay}" />
|
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_ThumbprintColumn, Mode=OneTime}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</ListView.Header>
|
</ListView.Header>
|
||||||
<ListView.ItemTemplate>
|
<ListView.ItemTemplate>
|
||||||
@@ -96,8 +96,8 @@
|
|||||||
</controls:SettingsExpander.Items>
|
</controls:SettingsExpander.Items>
|
||||||
</controls:SettingsExpander>
|
</controls:SettingsExpander>
|
||||||
<controls:SettingsExpander
|
<controls:SettingsExpander
|
||||||
Description="{x:Bind domain:Translator.SettingsSignatureAndEncryption_RecipientCertificatesDescription, Mode=OneWay}"
|
Description="{x:Bind domain:Translator.SettingsSignatureAndEncryption_RecipientCertificatesDescription, Mode=OneTime}"
|
||||||
Header="{x:Bind domain:Translator.SettingsSignatureAndEncryption_RecipientCertificatesHeader, Mode=OneWay}"
|
Header="{x:Bind domain:Translator.SettingsSignatureAndEncryption_RecipientCertificatesHeader, Mode=OneTime}"
|
||||||
IsExpanded="False">
|
IsExpanded="False">
|
||||||
<controls:SettingsExpander.Items>
|
<controls:SettingsExpander.Items>
|
||||||
<!--<StackPanel Padding="12" Spacing="12">-->
|
<!--<StackPanel Padding="12" Spacing="12">-->
|
||||||
@@ -125,15 +125,15 @@
|
|||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_NameColumn, Mode=OneWay}" />
|
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_NameColumn, Mode=OneTime}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_ExpiresColumn, Mode=OneWay}" />
|
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_ExpiresColumn, Mode=OneTime}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="3"
|
Grid.Column="3"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_ThumbprintColumn, Mode=OneWay}" />
|
Text="{x:Bind domain:Translator.SettingsSignatureAndEncryption_ThumbprintColumn, Mode=OneTime}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</ListView.Header>
|
</ListView.Header>
|
||||||
<ListView.ItemTemplate>
|
<ListView.ItemTemplate>
|
||||||
|
|||||||
@@ -315,6 +315,16 @@
|
|||||||
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||||
</controls:SettingsCard.HeaderIcon>
|
</controls:SettingsCard.HeaderIcon>
|
||||||
</controls:SettingsCard>
|
</controls:SettingsCard>
|
||||||
|
|
||||||
|
<controls:SettingsCard
|
||||||
|
Command="{x:Bind ViewModel.ChangePasswordCommand}"
|
||||||
|
Description="{x:Bind domain:Translator.WinoAccount_ChangePassword_Description}"
|
||||||
|
Header="{x:Bind domain:Translator.WinoAccount_ChangePassword_Title}"
|
||||||
|
IsClickEnabled="True">
|
||||||
|
<!--<controls:SettingsCard.HeaderIcon>
|
||||||
|
<SymbolIcon Symbol="Permission" />
|
||||||
|
</controls:SettingsCard.HeaderIcon>-->
|
||||||
|
</controls:SettingsCard>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
private readonly IDatabaseService _databaseService;
|
private readonly IDatabaseService _databaseService;
|
||||||
private readonly bool _ownsHttpClient;
|
private readonly bool _ownsHttpClient;
|
||||||
|
|
||||||
|
private const string ApiUrl = "https://localhost:7204/";
|
||||||
|
// private const string ApiUrl = "https://api.winomail.app/";
|
||||||
|
|
||||||
public WinoAccountApiClient(IDatabaseService databaseService, HttpClient? httpClient = null)
|
public WinoAccountApiClient(IDatabaseService databaseService, HttpClient? httpClient = null)
|
||||||
{
|
{
|
||||||
_databaseService = databaseService;
|
_databaseService = databaseService;
|
||||||
@@ -44,8 +47,9 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
|
|
||||||
_httpClient = new HttpClient(handler)
|
_httpClient = new HttpClient(handler)
|
||||||
{
|
{
|
||||||
BaseAddress = new Uri("https://api.winomail.app/")
|
BaseAddress = new Uri(ApiUrl)
|
||||||
};
|
};
|
||||||
|
|
||||||
_ownsHttpClient = true;
|
_ownsHttpClient = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +62,24 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
public Task<WinoAccountApiResult<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default)
|
public Task<WinoAccountApiResult<AuthResultDto>> RefreshAsync(string refreshToken, CancellationToken cancellationToken = default)
|
||||||
=> SendAuthRequestAsync("api/v1/auth/refresh", new RefreshRequest(refreshToken), WinoAccountApiJsonContext.Default.RefreshRequest, cancellationToken);
|
=> SendAuthRequestAsync("api/v1/auth/refresh", new RefreshRequest(refreshToken), WinoAccountApiJsonContext.Default.RefreshRequest, cancellationToken);
|
||||||
|
|
||||||
|
public Task<ApiEnvelope<EmailConfirmationResendResultDto>> ResendEmailConfirmationAsync(string endpoint, string ticket, CancellationToken cancellationToken = default)
|
||||||
|
=> SendAnonymousRequestAsync(
|
||||||
|
HttpMethod.Post,
|
||||||
|
endpoint,
|
||||||
|
new ResendEmailConfirmationRequest(ticket),
|
||||||
|
WinoAccountApiJsonContext.Default.ResendEmailConfirmationRequest,
|
||||||
|
WinoAccountApiJsonContext.Default.ApiEnvelopeEmailConfirmationResendResultDto,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public Task<ApiEnvelope<JsonElement>> ForgotPasswordAsync(string email, CancellationToken cancellationToken = default)
|
||||||
|
=> SendAnonymousRequestAsync(
|
||||||
|
HttpMethod.Post,
|
||||||
|
"api/v1/auth/forgot-password",
|
||||||
|
new ForgotPasswordRequest(email),
|
||||||
|
WinoAccountApiJsonContext.Default.ForgotPasswordRequest,
|
||||||
|
WinoAccountApiJsonContext.Default.ApiEnvelopeJsonElement,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
public async Task<ApiEnvelope<JsonElement>> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default)
|
public async Task<ApiEnvelope<JsonElement>> LogoutAsync(string refreshToken, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -173,8 +195,9 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
|
|
||||||
var errorCode = envelope?.ErrorCode ?? $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim();
|
var errorCode = envelope?.ErrorCode ?? $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim();
|
||||||
var errorMessage = ExtractErrorMessage(payload) ?? response.ReasonPhrase;
|
var errorMessage = ExtractErrorMessage(payload) ?? response.ReasonPhrase;
|
||||||
|
var errorDetails = ExtractDetails(payload);
|
||||||
|
|
||||||
return WinoAccountApiResult<AuthResultDto>.Failure(errorCode, errorMessage);
|
return WinoAccountApiResult<AuthResultDto>.Failure(errorCode, errorMessage, errorDetails);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -200,6 +223,29 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static JsonElement? ExtractDetails(string? payload)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payload))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(payload);
|
||||||
|
if (document.RootElement.ValueKind != JsonValueKind.Object || !document.RootElement.TryGetProperty("details", out var details))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return details.Clone();
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string? TryGetErrorMessage(JsonElement element)
|
private static string? TryGetErrorMessage(JsonElement element)
|
||||||
{
|
{
|
||||||
if (TryGetStringProperty(element, "errorMessage", out var errorMessage))
|
if (TryGetStringProperty(element, "errorMessage", out var errorMessage))
|
||||||
@@ -246,6 +292,35 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
private 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);
|
=> SendAuthorizedRequestAsync(HttpMethod.Get, endpoint, typeInfo, cancellationToken);
|
||||||
|
|
||||||
|
private async Task<ApiEnvelope<TResponse>> SendAnonymousRequestAsync<TRequest, TResponse>(HttpMethod method,
|
||||||
|
string endpoint,
|
||||||
|
TRequest requestBody,
|
||||||
|
JsonTypeInfo<TRequest> requestTypeInfo,
|
||||||
|
JsonTypeInfo<ApiEnvelope<TResponse>> responseTypeInfo,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var request = new HttpRequestMessage(method, endpoint)
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(requestBody, requestTypeInfo)
|
||||||
|
};
|
||||||
|
|
||||||
|
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
var envelope = string.IsNullOrWhiteSpace(payload)
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Deserialize(payload, responseTypeInfo);
|
||||||
|
|
||||||
|
return envelope ?? ApiEnvelope<TResponse>.Failure($"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiEnvelope<TResponse>.Failure(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(HttpMethod method, string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
|
private async Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(HttpMethod method, string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -310,7 +385,10 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
[JsonSerializable(typeof(LoginRequest))]
|
[JsonSerializable(typeof(LoginRequest))]
|
||||||
[JsonSerializable(typeof(RefreshRequest))]
|
[JsonSerializable(typeof(RefreshRequest))]
|
||||||
[JsonSerializable(typeof(LogoutRequest))]
|
[JsonSerializable(typeof(LogoutRequest))]
|
||||||
|
[JsonSerializable(typeof(ResendEmailConfirmationRequest))]
|
||||||
|
[JsonSerializable(typeof(ForgotPasswordRequest))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AuthResultDto>))]
|
||||||
|
[JsonSerializable(typeof(ApiEnvelope<EmailConfirmationResendResultDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AuthUserDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<CheckoutSessionResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<CheckoutSessionResultDto>))]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@@ -34,15 +35,14 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
public async Task<WinoAccountOperationResult> RegisterAsync(string email, string password, CancellationToken cancellationToken = default)
|
public async Task<WinoAccountOperationResult> RegisterAsync(string email, string password, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var response = await _apiClient.RegisterAsync(email, password, cancellationToken).ConfigureAwait(false);
|
var response = await _apiClient.RegisterAsync(email, password, cancellationToken).ConfigureAwait(false);
|
||||||
var result = await PersistResponseAsync(response).ConfigureAwait(false);
|
if (!response.IsSuccess || response.Result == null)
|
||||||
|
|
||||||
if (result.IsSuccess && result.Account != null)
|
|
||||||
{
|
{
|
||||||
PublishProfileUpdated(result.Account);
|
_logger.Warning("Wino account registration failed. Error code: {ErrorCode}. Error message: {ErrorMessage}", response.ErrorCode, response.ErrorMessage);
|
||||||
ReportUIChange(new WinoAccountSignedInMessage(result.Account));
|
return WinoAccountOperationResult.Failure(response.ErrorCode, response.ErrorMessage, response.ErrorDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
// Registration no longer signs the user in locally until the email address is confirmed.
|
||||||
|
return WinoAccountOperationResult.Success(Map(response.Result));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WinoAccountOperationResult> LoginAsync(string email, string password, CancellationToken cancellationToken = default)
|
public async Task<WinoAccountOperationResult> LoginAsync(string email, string password, CancellationToken cancellationToken = default)
|
||||||
@@ -59,6 +59,12 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<ApiEnvelope<EmailConfirmationResendResultDto>> ResendEmailConfirmationAsync(string endpoint, string ticket, CancellationToken cancellationToken = default)
|
||||||
|
=> _apiClient.ResendEmailConfirmationAsync(endpoint, ticket, cancellationToken);
|
||||||
|
|
||||||
|
public Task<ApiEnvelope<JsonElement>> ForgotPasswordAsync(string email, CancellationToken cancellationToken = default)
|
||||||
|
=> _apiClient.ForgotPasswordAsync(email, cancellationToken);
|
||||||
|
|
||||||
public async Task<WinoAccountOperationResult> RefreshAsync(CancellationToken cancellationToken = default)
|
public async Task<WinoAccountOperationResult> RefreshAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
var account = await GetActiveAccountAsync().ConfigureAwait(false);
|
||||||
@@ -310,7 +316,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}. Error message: {ErrorMessage}", response.ErrorCode, response.ErrorMessage);
|
_logger.Warning("Wino account operation failed. Error code: {ErrorCode}. Error message: {ErrorMessage}", response.ErrorCode, response.ErrorMessage);
|
||||||
return WinoAccountOperationResult.Failure(response.ErrorCode, response.ErrorMessage);
|
return WinoAccountOperationResult.Failure(response.ErrorCode, response.ErrorMessage, response.ErrorDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
var account = Map(response.Result);
|
var account = Map(response.Result);
|
||||||
|
|||||||
Reference in New Issue
Block a user