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; } }