Forgot password and email confirmations.

This commit is contained in:
Burak Kaan Köse
2026-03-19 16:41:35 +01:00
parent 873a7eca12
commit c2320de5c4
20 changed files with 643 additions and 44 deletions
@@ -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>
<!-- Benefits cards -->
<StackPanel Spacing="8">
<StackPanel
x:Name="BenefitsPanel"
Spacing="8">
<Border
Padding="14"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
@@ -227,13 +229,32 @@
PlaceholderText="{x:Bind domain:Translator.WinoAccount_EmailPlaceholder}"
TextChanging="InputChanged" />
<PasswordBox
x:Name="PasswordBox"
Header="{x:Bind domain:Translator.WinoAccount_PasswordLabel}"
KeyDown="PasswordBox_KeyDown"
PasswordChanged="InputChanged" />
<StackPanel x:Name="PasswordPanel">
<PasswordBox
x:Name="PasswordBox"
Header="{x:Bind domain:Translator.WinoAccount_PasswordLabel}"
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>
<HyperlinkButton
x:Name="ModeToggleButton"
HorizontalAlignment="Left"
Click="ModeToggleButton_Click"
Content="{x:Bind domain:Translator.WinoAccount_LoginDialog_ForgotPasswordLink}" />
<ProgressRing
x:Name="BusyRing"
Width="20"
@@ -5,6 +5,7 @@ using Windows.System;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
using Wino.Mail.Api.Contracts.Auth;
using Wino.Mail.WinUI.Services;
namespace Wino.Dialogs;
@@ -12,14 +13,19 @@ namespace Wino.Dialogs;
public sealed partial class WinoAccountLoginDialog : ContentDialog
{
private readonly IWinoAccountProfileService _profileService;
private bool _isForgotPasswordMode;
public WinoAccountLoginDialog(IWinoAccountProfileService profileService)
{
_profileService = profileService;
InitializeComponent();
UpdateMode();
}
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)
{
@@ -36,7 +42,7 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
try
{
await PerformLoginAsync();
await PerformPrimaryActionAsync();
}
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();
if (!string.IsNullOrWhiteSpace(validationError))
@@ -58,10 +64,33 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
SetBusyState(true);
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);
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));
return;
}
@@ -82,6 +111,11 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
return Translator.WinoAccount_Validation_EmailRequired;
}
if (_isForgotPasswordMode)
{
return string.Empty;
}
if (string.IsNullOrWhiteSpace(PasswordBox.Password))
{
return Translator.WinoAccount_Validation_PasswordRequired;
@@ -90,10 +124,17 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
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 (_isForgotPasswordMode)
{
e.Handled = true;
await PerformPrimaryActionAsync();
return;
}
PasswordBox.Focus(FocusState.Programmatic);
e.Handled = true;
}
@@ -104,10 +145,18 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
if (e.Key == VirtualKey.Enter)
{
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(object sender, RoutedEventArgs e) => HideError();
@@ -131,4 +180,22 @@ public sealed partial class WinoAccountLoginDialog : ContentDialog
ErrorTextBlock.Text = string.Empty;
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 string? ConfirmationEmailAddress { get; private set; }
private async void RegisterClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
@@ -68,7 +69,7 @@ public sealed partial class WinoAccountRegistrationDialog : ContentDialog
return;
}
Result = result.Account;
ConfirmationEmailAddress = result.Account.Email;
Hide();
}
finally