diff --git a/src/FrontOffice.Main/Pages/Auth/Phone.razor b/src/FrontOffice.Main/Pages/Auth/Phone.razor
index a6eeef5..375b176 100644
--- a/src/FrontOffice.Main/Pages/Auth/Phone.razor
+++ b/src/FrontOffice.Main/Pages/Auth/Phone.razor
@@ -6,7 +6,7 @@
ورود به حساب کاربری
- لطفاً شماره موبایل خود را وارد کنید تا رمز پویا ارسال شود.
+ لطفاً شماره موبایل خود را وارد کنید تا رمز پویا برایتان ارسال شود.
+ @if (_remainingAttempts.HasValue)
+ {
+
+ تعداد تلاشهای باقیمانده: @_remainingAttempts
+
+ }
+
+ @if (_cooldownSeconds > 0)
+ {
+
+ امکان درخواست مجدد تا @_cooldownSeconds ثانیه دیگر فعال میشود.
+
+ }
+
+ @if (!string.IsNullOrWhiteSpace(_infoMessage))
+ {
+ @_infoMessage
+ }
+
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
@_errorMessage
@@ -32,7 +52,8 @@
ارسال رمز پویا
@@ -45,4 +66,4 @@
-
\ No newline at end of file
+
diff --git a/src/FrontOffice.Main/Pages/Auth/Phone.razor.cs b/src/FrontOffice.Main/Pages/Auth/Phone.razor.cs
index 68b9584..eef8291 100644
--- a/src/FrontOffice.Main/Pages/Auth/Phone.razor.cs
+++ b/src/FrontOffice.Main/Pages/Auth/Phone.razor.cs
@@ -1,4 +1,8 @@
-using Blazored.LocalStorage;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Blazored.LocalStorage;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator;
using FrontOffice.Main.Utilities;
@@ -16,13 +20,18 @@ public partial class Phone : IDisposable
private const string TokenStorageKey = "auth:token";
private const string OtpPurpose = "Login";
- private CreateNewOtpTokenRequestValidator _requestValidator = new();
- private CreateNewOtpTokenRequest _request = new();
- private MudForm? _form;
+ private readonly CreateNewOtpTokenRequestValidator _requestValidator = new();
+ private readonly CreateNewOtpTokenRequest _request = new();
+ private MudForm? _form;
private bool _isBusy;
+ private bool _acceptTerms;
private string? _errorMessage;
+ private string? _infoMessage;
private string? _redirect;
+ private int? _remainingAttempts;
+ private int _cooldownSeconds;
+ private Timer? _cooldownTimer;
private CancellationTokenSource? _sendCts;
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
@@ -57,12 +66,18 @@ public partial class Phone : IDisposable
private async Task SendOtpAsync()
{
_errorMessage = null;
+ _infoMessage = null;
+
if (_form is null)
+ {
return;
-
+ }
+
await _form.Validate();
if (!_form.IsValid)
- return;
+ {
+ return;
+ }
_isBusy = true;
_sendCts?.Cancel();
@@ -79,24 +94,21 @@ public partial class Phone : IDisposable
}
var metadata = await BuildAuthMetadataAsync();
- CreateNewOtpTokenResponse response;
- if (metadata is not null)
- {
- response = await UserClient.CreateNewOtpTokenAsync(_request, metadata, cancellationToken: _sendCts.Token);
- }
- else
- {
- response = await UserClient.CreateNewOtpTokenAsync(_request, cancellationToken: _sendCts.Token);
- }
+ var response = metadata is not null
+ ? await UserClient.CreateNewOtpTokenAsync(_request, metadata, cancellationToken: _sendCts.Token)
+ : await UserClient.CreateNewOtpTokenAsync(_request, cancellationToken: _sendCts.Token);
if (response?.Success != true)
{
- _errorMessage = string.IsNullOrWhiteSpace(response?.Message)
- ? "ارسال رمز پویا با خطا مواجه شد. لطفاً دوباره تلاش کنید."
- : response!.Message;
+ _errorMessage = !string.IsNullOrWhiteSpace(response?.Message)
+ ? response!.Message
+ : "ارسال رمز پویا با خطا روبهرو شد. لطفاً دوباره تلاش کنید.";
+ ApplyServerState(response, false);
return;
}
+ ApplyServerState(response, true);
+
await LocalStorage.SetItemAsync(PhoneStorageKey, _request.Mobile);
if (!string.IsNullOrWhiteSpace(_redirect))
{
@@ -119,7 +131,7 @@ public partial class Phone : IDisposable
{
_errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail)
? rpcEx.Status.Detail
- : "ارسال رمز پویا با خطا مواجه شد. لطفاً دوباره تلاش کنید.";
+ : "ارسال رمز پویا با خطا روبهرو شد. لطفاً دوباره تلاش کنید.";
}
catch (OperationCanceledException)
{
@@ -137,6 +149,54 @@ public partial class Phone : IDisposable
}
}
+ private void ApplyServerState(CreateNewOtpTokenResponse? response, bool isSuccess)
+ {
+ if (response is null)
+ {
+ return;
+ }
+
+ _infoMessage = isSuccess
+ ? (string.IsNullOrWhiteSpace(response.Message) ? "رمز پویا ارسال شد." : response.Message)
+ : null;
+
+ if (response.RemainingAttempts >= 0)
+ {
+ _remainingAttempts = response.RemainingAttempts;
+ }
+
+ if (response.RemainingSeconds > 0)
+ {
+ StartCooldown(response.RemainingSeconds);
+ }
+ else if (isSuccess)
+ {
+ StopCooldown();
+ }
+ }
+
+ private void StartCooldown(int seconds)
+ {
+ StopCooldown();
+ _cooldownSeconds = seconds;
+ _cooldownTimer = new Timer(_ =>
+ {
+ var remaining = Interlocked.Decrement(ref _cooldownSeconds);
+ if (remaining <= 0)
+ {
+ StopCooldown();
+ }
+ _ = InvokeAsync(StateHasChanged);
+ }, null, 1000, 1000);
+ }
+
+ private void StopCooldown()
+ {
+ _cooldownTimer?.Dispose();
+ _cooldownTimer = null;
+ _cooldownSeconds = 0;
+ }
+
private async Task BuildAuthMetadataAsync()
{
var token = await LocalStorage.GetItemAsync(TokenStorageKey);
@@ -155,6 +215,6 @@ public partial class Phone : IDisposable
{
_sendCts?.Cancel();
_sendCts?.Dispose();
- _sendCts = null;
+ StopCooldown();
}
}
diff --git a/src/FrontOffice.Main/Pages/Auth/Verify.razor b/src/FrontOffice.Main/Pages/Auth/Verify.razor
index 5ecccb9..05ba045 100644
--- a/src/FrontOffice.Main/Pages/Auth/Verify.razor
+++ b/src/FrontOffice.Main/Pages/Auth/Verify.razor
@@ -5,14 +5,14 @@
- تأیید رمز پویا
+ کد ارسالشده را وارد کنید
@if (!string.IsNullOrWhiteSpace(_phoneNumber))
{
- کد ارسالشده به @_phoneNumber را وارد کنید.
+ کد ارسالشده به @_phoneNumber را وارد نمایید.
}
else
{
- کد ارسالشده را وارد کنید.
+ کد پیامک شده را وارد نمایید.
}
@@ -24,17 +24,19 @@
Immediate="true"
Required="true"
RequiredError="وارد کردن رمز پویا الزامی است."
- HelperText="کد ۶ رقمی"
+ HelperText="کد ۵ یا ۶ رقمی"
Class="mb-2"
MaxLength="6" />
-
- تلاش باقیمانده: @_attemptsLeft از @MaxVerificationAttempts
+
+ تلاشهای باقیمانده: @_attemptsLeft
- @if (!string.IsNullOrWhiteSpace(_errorMessage))
+ @if (_resendSeconds > 0)
{
- @_errorMessage
+
+ امکان ارسال مجدد تا @_resendSeconds ثانیه دیگر فعال میشود.
+
}
@if (!string.IsNullOrWhiteSpace(_infoMessage))
@@ -42,13 +44,19 @@
@_infoMessage
}
+ @if (!string.IsNullOrWhiteSpace(_errorMessage))
+ {
+ @_errorMessage
+ }
+
- تأیید و ورود
+ تأیید و ادامه
- @if (_resendRemaining > 0)
- {
-
- امکان ارسال مجدد تا @_resendRemaining ثانیه دیگر
-
- }
- else
- {
-
- ارسال مجدد رمز پویا
-
- }
+
+ ارسال دوباره رمز
+
-
-
-
diff --git a/src/FrontOffice.Main/Pages/Auth/Verify.razor.cs b/src/FrontOffice.Main/Pages/Auth/Verify.razor.cs
index 972542b..b3089ab 100644
--- a/src/FrontOffice.Main/Pages/Auth/Verify.razor.cs
+++ b/src/FrontOffice.Main/Pages/Auth/Verify.razor.cs
@@ -1,4 +1,8 @@
-using Blazored.LocalStorage;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Blazored.LocalStorage;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator;
using FrontOffice.Main.Utilities;
@@ -18,17 +22,17 @@ public partial class Verify : IDisposable
private const string RedirectStorageKey = "auth:redirect";
private const string TokenStorageKey = "auth:token";
- private VerifyOtpTokenRequestValidator _requestValidator = new();
- private VerifyOtpTokenRequest _request = new();
- private MudForm? _form;
+ private readonly VerifyOtpTokenRequestValidator _requestValidator = new();
+ private readonly VerifyOtpTokenRequest _request = new();
+ private MudForm? _form;
private bool _isBusy;
private string? _phoneNumber;
private string? _redirect;
private string? _errorMessage;
private string? _infoMessage;
private Timer? _resendTimer;
- private int _resendRemaining;
+ private int _resendSeconds;
private int _attemptsLeft = MaxVerificationAttempts;
private CancellationTokenSource? _operationCts;
@@ -75,7 +79,7 @@ public partial class Verify : IDisposable
}
}
- StartResendCountdown();
+ ResetResendCountdown();
}
private async Task VerifyOtpAsync()
@@ -84,15 +88,19 @@ public partial class Verify : IDisposable
_infoMessage = null;
if (_form is null)
- return;
+ {
+ return;
+ }
await _form.Validate();
if (!_form.IsValid)
+ {
return;
+ }
if (IsVerificationLocked)
{
- _errorMessage = "تعداد تلاشهای مجاز به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
+ _errorMessage = "تعداد تلاشهای مجاز شما به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
return;
}
@@ -117,15 +125,9 @@ public partial class Verify : IDisposable
}
var metadata = await BuildAuthMetadataAsync();
- VerifyOtpTokenResponse response;
- if (metadata is not null)
- {
- response = await UserClient.VerifyOtpTokenAsync(_request, metadata, cancellationToken: cancellationToken);
- }
- else
- {
- response = await UserClient.VerifyOtpTokenAsync(_request, cancellationToken: cancellationToken);
- }
+ var response = metadata is not null
+ ? await UserClient.VerifyOtpTokenAsync(_request, metadata, cancellationToken: cancellationToken)
+ : await UserClient.VerifyOtpTokenAsync(_request, cancellationToken: cancellationToken);
if (response is null)
{
@@ -133,36 +135,21 @@ public partial class Verify : IDisposable
return;
}
+ ApplyVerificationState(response);
+
if (response.Success)
{
- if (!string.IsNullOrWhiteSpace(response.Token))
- {
- await LocalStorage.SetItemAsync(TokenStorageKey, response.Token);
- }
- else
- {
- await LocalStorage.RemoveItemAsync(TokenStorageKey);
- }
-
+ await PersistTokenAsync(response.Token);
await LocalStorage.RemoveItemAsync(PhoneStorageKey);
await LocalStorage.RemoveItemAsync(RedirectStorageKey);
- _attemptsLeft = MaxVerificationAttempts;
- _request.Code = string.Empty;
-
var target = !string.IsNullOrWhiteSpace(_redirect) ? _redirect : RouteConstants.Main.MainPage;
- if (!string.IsNullOrWhiteSpace(target))
- {
- Navigation.NavigateTo(target, forceLoad: true);
- }
- return;
+ Navigation.NavigateTo(target ?? RouteConstants.Main.MainPage, forceLoad: true);
}
-
- RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message);
}
catch (RpcException rpcEx)
{
- await HandleVerificationFailureAsync(rpcEx);
+ await HandleVerifyRpcFailureAsync(rpcEx);
}
catch (OperationCanceledException)
{
@@ -179,19 +166,58 @@ public partial class Verify : IDisposable
}
}
- private async Task HandleVerificationFailureAsync(RpcException rpcEx)
+ private void ApplyVerificationState(VerifyOtpTokenResponse response)
+ {
+ if (response.RemainingSeconds > 0)
+ {
+ StartResendCountdown(response.RemainingSeconds);
+ }
+ else
+ {
+ ResetResendCountdown();
+ }
+
+ if (response.RemainingAttempts >= 0)
+ {
+ _attemptsLeft = response.RemainingAttempts;
+ }
+
+ if (response.Success)
+ {
+ _infoMessage = string.IsNullOrWhiteSpace(response.Message)
+ ? "ورود با موفقیت انجام شد."
+ : response.Message;
+ }
+ else
+ {
+ var baseMessage = string.IsNullOrWhiteSpace(response.Message)
+ ? "کد وارد شده صحیح نیست."
+ : response.Message;
+
+ _errorMessage = _attemptsLeft > 0
+ ? $"{baseMessage} {_attemptsLeft} تلاش باقیمانده است."
+ : $"{baseMessage} تعداد تلاشهای مجاز شما پایان یافته است. لطفاً رمز جدید دریافت کنید.";
+ }
+ }
+
+ private async Task HandleVerifyRpcFailureAsync(RpcException rpcEx)
{
switch (rpcEx.Status.StatusCode)
{
case StatusCode.PermissionDenied:
case StatusCode.InvalidArgument:
- RegisterFailedAttempt(!string.IsNullOrWhiteSpace(rpcEx.Status.Detail)
- ? rpcEx.Status.Detail
- : "کد نادرست است.");
+ ApplyVerificationState(new VerifyOtpTokenResponse
+ {
+ Success = false,
+ Message = string.IsNullOrWhiteSpace(rpcEx.Status.Detail) ? "کد وارد شده صحیح نیست." : rpcEx.Status.Detail,
+ RemainingAttempts = _attemptsLeft - 1
+ });
break;
case StatusCode.Unauthenticated:
await ResetAuthenticationAsync();
- _errorMessage = "نشست کاربری منقضی شده است. لطفاً دوباره وارد شوید.";
+ _errorMessage = string.IsNullOrWhiteSpace(rpcEx.Status.Detail)
+ ? "نشست کاربری منقضی شده است. لطفاً دوباره وارد شوید."
+ : rpcEx.Status.Detail;
Navigation.NavigateTo(RouteConstants.Auth.Phone, forceLoad: true);
break;
case StatusCode.DeadlineExceeded:
@@ -206,23 +232,9 @@ public partial class Verify : IDisposable
}
}
- private void RegisterFailedAttempt(string baseMessage)
- {
- _attemptsLeft = Math.Max(0, _attemptsLeft - 1);
-
- if (_attemptsLeft > 0)
- {
- _errorMessage = $"{baseMessage} {_attemptsLeft} تلاش باقی مانده است.";
- }
- else
- {
- _errorMessage = $"{baseMessage} تلاشهای مجاز شما به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
- }
- }
-
private async Task ResendOtpAsync()
{
- if (_resendRemaining > 0 || _isBusy || string.IsNullOrWhiteSpace(_phoneNumber))
+ if (_resendSeconds > 0 || _isBusy || string.IsNullOrWhiteSpace(_phoneNumber))
{
return;
}
@@ -241,35 +253,28 @@ public partial class Verify : IDisposable
};
var metadata = await BuildAuthMetadataAsync();
- CreateNewOtpTokenResponse response;
- if (metadata is not null)
- {
- response = await UserClient.CreateNewOtpTokenAsync(request, metadata, cancellationToken: cancellationToken);
- }
- else
- {
- response = await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: cancellationToken);
- }
+ var response = metadata is not null
+ ? await UserClient.CreateNewOtpTokenAsync(request, metadata, cancellationToken: cancellationToken)
+ : await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: cancellationToken);
if (response?.Success != true)
{
- _errorMessage = string.IsNullOrWhiteSpace(response?.Message)
- ? "ارسال مجدد رمز پویا با خطا مواجه شد."
- : response!.Message;
- return;
+ _errorMessage = !string.IsNullOrWhiteSpace(response?.Message)
+ ? response!.Message
+ : "ارسال مجدد رمز با خطا مواجه شد.";
+ }
+ else
+ {
+ _infoMessage = string.IsNullOrWhiteSpace(response.Message)
+ ? "رمز جدید ارسال شد."
+ : response.Message;
}
- _infoMessage = string.IsNullOrWhiteSpace(response.Message)
- ? "کد جدید ارسال شد."
- : response.Message;
-
- _attemptsLeft = MaxVerificationAttempts;
- _request.Code = string.Empty;
- StartResendCountdown();
+ ApplyResendState(response);
}
catch (RpcException rpcEx)
{
- await HandleResendFailureAsync(rpcEx);
+ await HandleResendRpcFailureAsync(rpcEx);
}
catch (OperationCanceledException)
{
@@ -286,50 +291,52 @@ public partial class Verify : IDisposable
}
}
- private async Task HandleResendFailureAsync(RpcException rpcEx)
+ private async Task HandleResendRpcFailureAsync(RpcException rpcEx)
{
switch (rpcEx.Status.StatusCode)
{
case StatusCode.Unauthenticated:
await ResetAuthenticationAsync();
- _errorMessage = "نشست کاربری منقضی شده است. لطفاً دوباره وارد شوید.";
+ _errorMessage = string.IsNullOrWhiteSpace(rpcEx.Status.Detail)
+ ? "نشست کاربری منقضی شده است. لطفاً دوباره وارد شوید."
+ : rpcEx.Status.Detail;
Navigation.NavigateTo(RouteConstants.Auth.Phone, forceLoad: true);
break;
default:
_errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail)
? rpcEx.Status.Detail
- : "ارسال مجدد رمز پویا با خطا مواجه شد.";
+ : "ارسال مجدد رمز با خطا مواجه شد.";
break;
}
}
- private async Task ChangePhoneAsync()
+ private void ApplyResendState(CreateNewOtpTokenResponse? response)
{
- await LocalStorage.RemoveItemAsync(PhoneStorageKey);
- NavigateBackToPhone();
- }
-
- private void NavigateBackToPhone()
- {
- var target = RouteConstants.Auth.Phone;
- if (!string.IsNullOrWhiteSpace(_redirect))
+ if (response?.RemainingAttempts >= 0)
{
- target += "?redirect=" + Uri.EscapeDataString(_redirect);
+ _attemptsLeft = response.RemainingAttempts;
}
- Navigation.NavigateTo(target, forceLoad: false);
+ if (response?.RemainingSeconds > 0)
+ {
+ StartResendCountdown(response.RemainingSeconds);
+ }
+ else
+ {
+ StartResendCountdown(DefaultResendCooldown);
+ }
}
- private void StartResendCountdown(int seconds = DefaultResendCooldown)
+ private void StartResendCountdown(int seconds)
{
- _resendRemaining = seconds;
+ _resendSeconds = seconds;
_resendTimer?.Dispose();
_resendTimer = new Timer(_ =>
{
- var remaining = Interlocked.Add(ref _resendRemaining, -1);
+ var remaining = Interlocked.Decrement(ref _resendSeconds);
if (remaining <= 0)
{
- Interlocked.Exchange(ref _resendRemaining, 0);
+ Interlocked.Exchange(ref _resendSeconds, 0);
_resendTimer?.Dispose();
_resendTimer = null;
}
@@ -338,6 +345,13 @@ public partial class Verify : IDisposable
}, null, 1000, 1000);
}
+ private void ResetResendCountdown()
+ {
+ _resendTimer?.Dispose();
+ _resendTimer = null;
+ _resendSeconds = 0;
+ }
+
private async Task BuildAuthMetadataAsync()
{
var token = await LocalStorage.GetItemAsync(TokenStorageKey);
@@ -352,6 +366,24 @@ public partial class Verify : IDisposable
};
}
+ private async Task PersistTokenAsync(string? token)
+ {
+ if (!string.IsNullOrWhiteSpace(token))
+ {
+ await LocalStorage.SetItemAsync(TokenStorageKey, token);
+ }
+ else
+ {
+ await LocalStorage.RemoveItemAsync(TokenStorageKey);
+ }
+ }
+
+ private async Task ChangePhoneAsync()
+ {
+ await LocalStorage.RemoveItemAsync(PhoneStorageKey);
+ NavigateBackToPhone();
+ }
+
private async Task ResetAuthenticationAsync()
{
await LocalStorage.RemoveItemAsync(TokenStorageKey);
@@ -359,6 +391,17 @@ public partial class Verify : IDisposable
await LocalStorage.RemoveItemAsync(RedirectStorageKey);
}
+ private void NavigateBackToPhone()
+ {
+ var target = RouteConstants.Auth.Phone;
+ if (!string.IsNullOrWhiteSpace(_redirect))
+ {
+ target += "?redirect=" + Uri.EscapeDataString(_redirect);
+ }
+
+ Navigation.NavigateTo(target, forceLoad: false);
+ }
+
private CancellationToken PrepareOperationToken()
{
_operationCts?.Cancel();
@@ -377,9 +420,6 @@ public partial class Verify : IDisposable
{
_operationCts?.Cancel();
_operationCts?.Dispose();
- _operationCts = null;
-
_resendTimer?.Dispose();
- _resendTimer = null;
}
}