diff --git a/src/FrontOffice.Main/App.razor.cs b/src/FrontOffice.Main/App.razor.cs index af2e0a1..5f555ca 100644 --- a/src/FrontOffice.Main/App.razor.cs +++ b/src/FrontOffice.Main/App.razor.cs @@ -1,6 +1,30 @@ -namespace FrontOffice.Main; +using Blazored.LocalStorage; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.WebUtilities; + +namespace FrontOffice.Main; public partial class App { + [Inject] private ILocalStorageService LocalStorage { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + // Check for referral code in URL query parameters + var uri = Navigation.ToAbsoluteUri(Navigation.Uri); + var query = QueryHelpers.ParseQuery(uri.Query); + + if (query.TryGetValue("ref", out var refValues)) + { + var referralCode = refValues.FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(referralCode)) + { + // Store referral code in local storage + await LocalStorage.SetItemAsync("referral:code", referralCode); + } + } + } } diff --git a/src/FrontOffice.Main/Pages/Auth/Phone.razor b/src/FrontOffice.Main/Pages/Auth/Phone.razor deleted file mode 100644 index a6eeef5..0000000 --- a/src/FrontOffice.Main/Pages/Auth/Phone.razor +++ /dev/null @@ -1,48 +0,0 @@ -@attribute [Route(RouteConstants.Auth.Phone)] - -ورود | تأیید شماره موبایل - - - - - ورود به حساب کاربری - لطفاً شماره موبایل خود را وارد کنید تا رمز پویا ارسال شود. - - - - - - - @if (!string.IsNullOrWhiteSpace(_errorMessage)) - { - @_errorMessage - } - - - ارسال رمز پویا - - - - - - با ورود، شرایط استفاده و سیاست حفظ حریم خصوصی را می‌پذیرید. - - - - \ 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 deleted file mode 100644 index 68b9584..0000000 --- a/src/FrontOffice.Main/Pages/Auth/Phone.razor.cs +++ /dev/null @@ -1,160 +0,0 @@ -using Blazored.LocalStorage; -using FrontOffice.BFF.User.Protobuf.Protos.User; -using FrontOffice.BFF.User.Protobuf.Validator; -using FrontOffice.Main.Utilities; -using Grpc.Core; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.WebUtilities; -using MudBlazor; - -namespace FrontOffice.Main.Pages.Auth; - -public partial class Phone : IDisposable -{ - private const string PhoneStorageKey = "auth:phone-number"; - private const string RedirectStorageKey = "auth:redirect"; - private const string TokenStorageKey = "auth:token"; - private const string OtpPurpose = "Login"; - - private CreateNewOtpTokenRequestValidator _requestValidator = new(); - private CreateNewOtpTokenRequest _request = new(); - private MudForm? _form; - - private bool _isBusy; - private string? _errorMessage; - private string? _redirect; - private CancellationTokenSource? _sendCts; - - [Inject] private ILocalStorageService LocalStorage { get; set; } = default!; - [Inject] private UserContract.UserContractClient UserClient { get; set; } = default!; - - protected override async Task OnInitializedAsync() - { - _request.Purpose = OtpPurpose; - var uri = Navigation.ToAbsoluteUri(Navigation.Uri); - var query = QueryHelpers.ParseQuery(uri.Query); - if (query.TryGetValue("redirect", out var redirectValues)) - { - _redirect = redirectValues.LastOrDefault(); - } - - var storedPhone = await LocalStorage.GetItemAsync(PhoneStorageKey); - if (!string.IsNullOrWhiteSpace(storedPhone)) - { - _request.Mobile = storedPhone; - } - - if (string.IsNullOrWhiteSpace(_redirect)) - { - var storedRedirect = await LocalStorage.GetItemAsync(RedirectStorageKey); - if (!string.IsNullOrWhiteSpace(storedRedirect)) - { - _redirect = storedRedirect; - } - } - } - - private async Task SendOtpAsync() - { - _errorMessage = null; - if (_form is null) - return; - - await _form.Validate(); - if (!_form.IsValid) - return; - - _isBusy = true; - _sendCts?.Cancel(); - _sendCts?.Dispose(); - _sendCts = new CancellationTokenSource(); - - try - { - var validationResult = _requestValidator.Validate(_request); - if (!validationResult.IsValid) - { - _errorMessage = string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct()); - return; - } - - 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); - } - - if (response?.Success != true) - { - _errorMessage = string.IsNullOrWhiteSpace(response?.Message) - ? "ارسال رمز پویا با خطا مواجه شد. لطفاً دوباره تلاش کنید." - : response!.Message; - return; - } - - await LocalStorage.SetItemAsync(PhoneStorageKey, _request.Mobile); - if (!string.IsNullOrWhiteSpace(_redirect)) - { - await LocalStorage.SetItemAsync(RedirectStorageKey, _redirect); - } - else - { - await LocalStorage.RemoveItemAsync(RedirectStorageKey); - } - - var target = $"{RouteConstants.Auth.Verify}?phone={Uri.EscapeDataString(_request.Mobile)}"; - if (!string.IsNullOrEmpty(_redirect)) - { - target += "&redirect=" + Uri.EscapeDataString(_redirect); - } - - Navigation.NavigateTo(target, forceLoad: false); - } - catch (RpcException rpcEx) - { - _errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail) - ? rpcEx.Status.Detail - : "ارسال رمز پویا با خطا مواجه شد. لطفاً دوباره تلاش کنید."; - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - _errorMessage = ex.Message; - } - finally - { - _isBusy = false; - _sendCts?.Dispose(); - _sendCts = null; - await InvokeAsync(StateHasChanged); - } - } - - private async Task BuildAuthMetadataAsync() - { - var token = await LocalStorage.GetItemAsync(TokenStorageKey); - if (string.IsNullOrWhiteSpace(token)) - { - return null; - } - - return new Metadata - { - { "Authorization", $"Bearer {token}" } - }; - } - - public void Dispose() - { - _sendCts?.Cancel(); - _sendCts?.Dispose(); - _sendCts = null; - } -} diff --git a/src/FrontOffice.Main/Pages/Auth/Verify.razor b/src/FrontOffice.Main/Pages/Auth/Verify.razor deleted file mode 100644 index 5ecccb9..0000000 --- a/src/FrontOffice.Main/Pages/Auth/Verify.razor +++ /dev/null @@ -1,84 +0,0 @@ -@attribute [Route(RouteConstants.Auth.Verify)] - -تأیید رمز پویا - - - - - تأیید رمز پویا - @if (!string.IsNullOrWhiteSpace(_phoneNumber)) - { - کد ارسال‌شده به @_phoneNumber را وارد کنید. - } - else - { - کد ارسال‌شده را وارد کنید. - } - - - - - - تلاش باقی‌مانده: @_attemptsLeft از @MaxVerificationAttempts - - - @if (!string.IsNullOrWhiteSpace(_errorMessage)) - { - @_errorMessage - } - - @if (!string.IsNullOrWhiteSpace(_infoMessage)) - { - @_infoMessage - } - - - - تأیید و ورود - - - تغییر شماره - - - - - - @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 deleted file mode 100644 index 972542b..0000000 --- a/src/FrontOffice.Main/Pages/Auth/Verify.razor.cs +++ /dev/null @@ -1,385 +0,0 @@ -using Blazored.LocalStorage; -using FrontOffice.BFF.User.Protobuf.Protos.User; -using FrontOffice.BFF.User.Protobuf.Validator; -using FrontOffice.Main.Utilities; -using Grpc.Core; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.WebUtilities; -using MudBlazor; - -namespace FrontOffice.Main.Pages.Auth; - -public partial class Verify : IDisposable -{ - private const int DefaultResendCooldown = 120; - public const int MaxVerificationAttempts = 5; - private const string OtpPurpose = "Login"; - private const string PhoneStorageKey = "auth:phone-number"; - private const string RedirectStorageKey = "auth:redirect"; - private const string TokenStorageKey = "auth:token"; - - 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 _resendRemaining; - private int _attemptsLeft = MaxVerificationAttempts; - private CancellationTokenSource? _operationCts; - - [Inject] private ILocalStorageService LocalStorage { get; set; } = default!; - [Inject] private UserContract.UserContractClient UserClient { get; set; } = default!; - - private bool IsVerificationLocked => _attemptsLeft <= 0; - - protected override async Task OnInitializedAsync() - { - _request.Purpose = OtpPurpose; - var uri = Navigation.ToAbsoluteUri(Navigation.Uri); - var query = QueryHelpers.ParseQuery(uri.Query); - if (query.TryGetValue("redirect", out var redirectValues)) - { - _redirect = redirectValues.LastOrDefault(); - } - - if (query.TryGetValue("phone", out var phoneValues)) - { - _phoneNumber = phoneValues.LastOrDefault(); - } - - if (string.IsNullOrWhiteSpace(_phoneNumber)) - { - _phoneNumber = await LocalStorage.GetItemAsync(PhoneStorageKey); - } - - if (string.IsNullOrWhiteSpace(_phoneNumber)) - { - await ResetAuthenticationAsync(); - Navigation.NavigateTo(RouteConstants.Auth.Phone, forceLoad: true); - return; - } - - await LocalStorage.SetItemAsync(PhoneStorageKey, _phoneNumber); - - if (string.IsNullOrWhiteSpace(_redirect)) - { - var storedRedirect = await LocalStorage.GetItemAsync(RedirectStorageKey); - if (!string.IsNullOrWhiteSpace(storedRedirect)) - { - _redirect = storedRedirect; - } - } - - StartResendCountdown(); - } - - private async Task VerifyOtpAsync() - { - _errorMessage = null; - _infoMessage = null; - - if (_form is null) - return; - - await _form.Validate(); - if (!_form.IsValid) - return; - - if (IsVerificationLocked) - { - _errorMessage = "تعداد تلاش‌های مجاز به پایان رسیده است. لطفاً رمز جدید دریافت کنید."; - return; - } - - if (string.IsNullOrWhiteSpace(_phoneNumber)) - { - await ResetAuthenticationAsync(); - Navigation.NavigateTo(RouteConstants.Auth.Phone, forceLoad: true); - return; - } - - _isBusy = true; - var cancellationToken = PrepareOperationToken(); - - try - { - _request.Mobile = _phoneNumber; - var validationResult = _requestValidator.Validate(_request); - if (!validationResult.IsValid) - { - _errorMessage = string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct()); - return; - } - - 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); - } - - if (response is null) - { - _errorMessage = "تأیید رمز پویا انجام نشد. لطفاً دوباره تلاش کنید."; - return; - } - - if (response.Success) - { - 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; - if (!string.IsNullOrWhiteSpace(target)) - { - Navigation.NavigateTo(target, forceLoad: true); - } - return; - } - - RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message); - } - catch (RpcException rpcEx) - { - await HandleVerificationFailureAsync(rpcEx); - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - _errorMessage = ex.Message; - } - finally - { - _isBusy = false; - ClearOperationToken(); - await InvokeAsync(StateHasChanged); - } - } - - private async Task HandleVerificationFailureAsync(RpcException rpcEx) - { - switch (rpcEx.Status.StatusCode) - { - case StatusCode.PermissionDenied: - case StatusCode.InvalidArgument: - RegisterFailedAttempt(!string.IsNullOrWhiteSpace(rpcEx.Status.Detail) - ? rpcEx.Status.Detail - : "کد نادرست است."); - break; - case StatusCode.Unauthenticated: - await ResetAuthenticationAsync(); - _errorMessage = "نشست کاربری منقضی شده است. لطفاً دوباره وارد شوید."; - Navigation.NavigateTo(RouteConstants.Auth.Phone, forceLoad: true); - break; - case StatusCode.DeadlineExceeded: - case StatusCode.NotFound: - _errorMessage = "کد منقضی شده است. لطفاً رمز جدید دریافت کنید."; - break; - default: - _errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail) - ? rpcEx.Status.Detail - : "تأیید رمز پویا انجام نشد. لطفاً دوباره تلاش کنید."; - break; - } - } - - 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)) - { - return; - } - - _errorMessage = null; - _infoMessage = null; - _isBusy = true; - var cancellationToken = PrepareOperationToken(); - - try - { - var request = new CreateNewOtpTokenRequest - { - Mobile = _phoneNumber, - Purpose = OtpPurpose - }; - - 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); - } - - 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 HandleResendFailureAsync(rpcEx); - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - _errorMessage = ex.Message; - } - finally - { - _isBusy = false; - ClearOperationToken(); - await InvokeAsync(StateHasChanged); - } - } - - private async Task HandleResendFailureAsync(RpcException rpcEx) - { - switch (rpcEx.Status.StatusCode) - { - case StatusCode.Unauthenticated: - await ResetAuthenticationAsync(); - _errorMessage = "نشست کاربری منقضی شده است. لطفاً دوباره وارد شوید."; - Navigation.NavigateTo(RouteConstants.Auth.Phone, forceLoad: true); - break; - default: - _errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail) - ? rpcEx.Status.Detail - : "ارسال مجدد رمز پویا با خطا مواجه شد."; - break; - } - } - - private async Task ChangePhoneAsync() - { - await LocalStorage.RemoveItemAsync(PhoneStorageKey); - NavigateBackToPhone(); - } - - private void NavigateBackToPhone() - { - 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.Add(ref _resendRemaining, -1); - if (remaining <= 0) - { - Interlocked.Exchange(ref _resendRemaining, 0); - _resendTimer?.Dispose(); - _resendTimer = null; - } - - _ = InvokeAsync(StateHasChanged); - }, null, 1000, 1000); - } - - private async Task BuildAuthMetadataAsync() - { - var token = await LocalStorage.GetItemAsync(TokenStorageKey); - if (string.IsNullOrWhiteSpace(token)) - { - return null; - } - - return new Metadata - { - { "Authorization", $"Bearer {token}" } - }; - } - - private async Task ResetAuthenticationAsync() - { - await LocalStorage.RemoveItemAsync(TokenStorageKey); - await LocalStorage.RemoveItemAsync(PhoneStorageKey); - await LocalStorage.RemoveItemAsync(RedirectStorageKey); - } - - private CancellationToken PrepareOperationToken() - { - _operationCts?.Cancel(); - _operationCts?.Dispose(); - _operationCts = new CancellationTokenSource(); - return _operationCts.Token; - } - - private void ClearOperationToken() - { - _operationCts?.Dispose(); - _operationCts = null; - } - - public void Dispose() - { - _operationCts?.Cancel(); - _operationCts?.Dispose(); - _operationCts = null; - - _resendTimer?.Dispose(); - _resendTimer = null; - } -} diff --git a/src/FrontOffice.Main/Pages/PackageDetail.razor b/src/FrontOffice.Main/Pages/PackageDetail.razor index 9c5c6ca..321a7b1 100644 --- a/src/FrontOffice.Main/Pages/PackageDetail.razor +++ b/src/FrontOffice.Main/Pages/PackageDetail.razor @@ -172,7 +172,7 @@ else برای ثبت نظر ابتدا - وارد حساب کاربری + @* وارد حساب کاربری *@ شوید. diff --git a/src/FrontOffice.Main/Shared/AuthDialog.razor.cs b/src/FrontOffice.Main/Shared/AuthDialog.razor.cs index 505547f..933c9a8 100644 --- a/src/FrontOffice.Main/Shared/AuthDialog.razor.cs +++ b/src/FrontOffice.Main/Shared/AuthDialog.razor.cs @@ -158,6 +158,14 @@ public partial class AuthDialog : IDisposable try { _verifyRequest.Mobile = _phoneNumber; + + // Check for stored referral code and add it to the request + var storedReferralCode = await LocalStorage.GetItemAsync("referral:code"); + if (!string.IsNullOrWhiteSpace(storedReferralCode)) + { + _verifyRequest.ParentReferralCode = storedReferralCode; + } + var validationResult = _verifyRequestValidator.Validate(_verifyRequest); if (!validationResult.IsValid) { @@ -196,6 +204,9 @@ public partial class AuthDialog : IDisposable await LocalStorage.RemoveItemAsync(PhoneStorageKey); await LocalStorage.RemoveItemAsync(RedirectStorageKey); + // Clear referral code after successful registration/login + await LocalStorage.RemoveItemAsync("referral:code"); + _attemptsLeft = MaxVerificationAttempts; _verifyRequest.Code = string.Empty; diff --git a/src/FrontOffice.Main/Shared/MainLayout.razor b/src/FrontOffice.Main/Shared/MainLayout.razor index 01195f8..e6622fd 100644 --- a/src/FrontOffice.Main/Shared/MainLayout.razor +++ b/src/FrontOffice.Main/Shared/MainLayout.razor @@ -49,7 +49,6 @@ else { ورود - شروع کنید } diff --git a/src/FrontOffice.Main/Utilities/RouteConstants.cs b/src/FrontOffice.Main/Utilities/RouteConstants.cs index 05ee293..7709548 100644 --- a/src/FrontOffice.Main/Utilities/RouteConstants.cs +++ b/src/FrontOffice.Main/Utilities/RouteConstants.cs @@ -7,12 +7,6 @@ public static class RouteConstants public const string MainPage = "/"; } - public static class Auth - { - public const string Phone = "/auth/phone"; - public const string Verify = "/auth/verify"; - } - public static class Profile { public const string Index = "/profile";