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