diff --git a/.gitignore b/.gitignore index 6fd37d3..fe8a569 100644 --- a/.gitignore +++ b/.gitignore @@ -491,3 +491,4 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser +/src/.idea diff --git a/src/FrontOffice.Main/FrontOffice.Main.csproj b/src/FrontOffice.Main/FrontOffice.Main.csproj index cdf34b0..0fdef73 100644 --- a/src/FrontOffice.Main/FrontOffice.Main.csproj +++ b/src/FrontOffice.Main/FrontOffice.Main.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable 6dab807c-c6d8-4711-bf64-11c69e8d39f4 @@ -14,11 +14,11 @@ - + - - + + diff --git a/src/FrontOffice.Main/Pages/Contact.razor b/src/FrontOffice.Main/Pages/Contact.razor index 34961a4..c98a984 100644 --- a/src/FrontOffice.Main/Pages/Contact.razor +++ b/src/FrontOffice.Main/Pages/Contact.razor @@ -10,7 +10,7 @@ آماده شنیدن صدای شما هستیم - + سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست. @@ -94,7 +94,7 @@ - + با ارسال این فرم، سیاست حفظ حریم خصوصی @@ -234,4 +234,3 @@ - diff --git a/src/FrontOffice.Main/Pages/PackageDetail.razor b/src/FrontOffice.Main/Pages/PackageDetail.razor index 98e840d..80f7a4e 100644 --- a/src/FrontOffice.Main/Pages/PackageDetail.razor +++ b/src/FrontOffice.Main/Pages/PackageDetail.razor @@ -193,7 +193,7 @@ else
@(review.UserName) - +
@(review.Date) diff --git a/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs b/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs index 3807eb3..0733d97 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs @@ -8,7 +8,7 @@ namespace FrontOffice.Main.Pages.Profile.Components; public partial class AddAddressDialog : ComponentBase { - [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!; + [CascadingParameter] private IDialogReference MudDialog { get; set; } = default!; [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; private MudForm? _form; @@ -41,5 +41,5 @@ public partial class AddAddressDialog : ComponentBase } } - private void Cancel() => MudDialog.Cancel(); + private void Cancel() => MudDialog.Close(DialogResult.Cancel()); } \ No newline at end of file diff --git a/src/FrontOffice.Main/Pages/Profile/Components/EditAddressDialog.razor.cs b/src/FrontOffice.Main/Pages/Profile/Components/EditAddressDialog.razor.cs index 00391d6..babb8cf 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/EditAddressDialog.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Components/EditAddressDialog.razor.cs @@ -9,8 +9,9 @@ namespace FrontOffice.Main.Pages.Profile.Components; public partial class EditAddressDialog : ComponentBase { - [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!; + [CascadingParameter] private IDialogReference MudDialog { get; set; } = default!; // updated type [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; + // removed duplicate Snackbar injection; provided by Razor partial via _Imports [Parameter] public GetAllUserAddressByFilterResponseModel? Model { get; set; } @@ -51,5 +52,5 @@ public partial class EditAddressDialog : ComponentBase } } - private void Cancel() => MudDialog.Cancel(); + private void Cancel() => MudDialog.Close(DialogResult.Cancel()); } \ No newline at end of file diff --git a/src/FrontOffice.Main/Pages/RegisterWizard.razor b/src/FrontOffice.Main/Pages/RegisterWizard.razor index acd5974..c52f4f8 100644 --- a/src/FrontOffice.Main/Pages/RegisterWizard.razor +++ b/src/FrontOffice.Main/Pages/RegisterWizard.razor @@ -7,15 +7,14 @@ - ثبت‌نام سه مرحله‌ای + ثبت‌نام سه + مرحله‌ای فقط در چند دقیقه حساب خود را فعال کنید - اطلاعات اولیه را وارد کنید، مشخصات هویتی را تکمیل کنید و بعد از مطالعه قوانین و دانلود قرارداد، درخواست خود را ارسال کنید. + اطلاعات اولیه را وارد کنید، مشخصات هویتی را تکمیل کنید و بعد از مطالعه قوانین و دانلود قرارداد، + درخواست خود را ارسال کنید. @@ -42,15 +41,16 @@ درخواست شما ثبت شد - تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام را دنبال کنید. + تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام + را دنبال کنید. - بازگشت به صفحه اصلی - مشاهده پروفایل + بازگشت به صفحه + اصلی + مشاهده پروفایل + } @@ -59,7 +59,8 @@ ویزارد ثبت‌نام - ۳ مرحله + ۳ مرحله + @if (_isSubmitting) @@ -67,119 +68,80 @@ } - - - - + + + + @* Inline AuthDialog with captcha enabled *@ + + - - - @_captchaCode + + + + + + + + + + + + لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک + تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره + کنید. + + + بخشی از قوانین: + + استفاده از اطلاعات کاربری صرفاً برای ثبت نام و + احراز هویت مجاز است. + تمامی فعالیت‌ها مطابق قوانین جمهوری اسلامی ایران + انجام می‌شود. + مسئولیت صحت اطلاعات وارد شده بر عهده متقاضی است. + + - تازه‌سازی کد - - - - + + + دانلود قرارداد نمونه + + فرمت: فایل متنی + + - - - - - - - - - - - - لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره کنید. - - - بخشی از قوانین: - - استفاده از اطلاعات کاربری صرفاً برای ثبت نام و احراز هویت مجاز است. - تمامی فعالیت‌ها مطابق قوانین جمهوری اسلامی ایران انجام می‌شود. - مسئولیت صحت اطلاعات وارد شده بر عهده متقاضی است. - - - - - - دانلود قرارداد نمونه - - فرمت: فایل متنی - - - - - + + + + + + + مرحله قبل + + + @_nextButtonText + + + - - - مرحله قبل - - - @_nextButtonText - - } - + \ No newline at end of file diff --git a/src/FrontOffice.Main/Pages/RegisterWizard.razor.cs b/src/FrontOffice.Main/Pages/RegisterWizard.razor.cs index fc77069..140f0b7 100644 --- a/src/FrontOffice.Main/Pages/RegisterWizard.razor.cs +++ b/src/FrontOffice.Main/Pages/RegisterWizard.razor.cs @@ -1,4 +1,8 @@ using System.ComponentModel.DataAnnotations; +using FrontOffice.BFF.User.Protobuf.Protos.User; +using FrontOffice.Main.Shared; +using FrontOffice.Main.Utilities; +using Google.Protobuf.WellKnownTypes; using Microsoft.AspNetCore.Components; using MudBlazor; @@ -6,14 +10,16 @@ namespace FrontOffice.Main.Pages; public partial class RegisterWizard { + [Inject] private UserContract.UserContractClient UserContract { get; set; } = default!; + private readonly RegistrationModel _model = new(); - private MudForm? _stepOneForm; private MudForm? _stepTwoForm; private MudForm? _stepThreeForm; private int _activeStep; private bool _isSubmitting; private bool _completed; - private string _captchaCode = string.Empty; + private AuthDialog _authDialog; + private UpdateUserRequest _updateUserRequest = new(); private string _nextButtonText => _activeStep switch { @@ -25,25 +31,18 @@ public partial class RegisterWizard protected override void OnInitialized() { base.OnInitialized(); - GenerateCaptcha(); } - private void GenerateCaptcha() - { - var random = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant(); - _captchaCode = random; - _model.CaptchaInput = string.Empty; - } - - private void GoBack() + private async Task GoBack(MudStepper mudStepper) { if (_activeStep == 0 || _isSubmitting) return; _activeStep--; + await mudStepper.PreviousStepAsync(); } - private async Task GoNextAsync() + private async Task GoNextAsync(MudStepper mudStepper) { if (_isSubmitting) return; @@ -51,18 +50,31 @@ public partial class RegisterWizard switch (_activeStep) { case 0: - if (!await ValidateStepAsync(_stepOneForm)) - return; - if (!string.Equals(_model.CaptchaInput?.Trim(), _captchaCode, StringComparison.OrdinalIgnoreCase)) + if (_authDialog._currentStep == AuthDialog.AuthStep.Phone) { - Snackbar.Add("کد کپچا صحیح نیست.", Severity.Warning); + await _authDialog.SendOtpAsync(); return; } - _activeStep = 1; + else + { + var verifyOtp = await _authDialog.VerifyOtpAsync(); + if (!verifyOtp) + return; + + + _activeStep = 1; + } + break; + case 1: if (!await ValidateStepAsync(_stepTwoForm)) return; + + var saveResult = await SavePersonalInfo(); + if (!saveResult) + return; + _activeStep = 2; break; case 2: @@ -71,6 +83,8 @@ public partial class RegisterWizard await SubmitAsync(); break; } + + await mudStepper.NextStepAsync(); } private async Task ValidateStepAsync(MudForm? form) @@ -102,15 +116,49 @@ public partial class RegisterWizard Navigation.NavigateTo("docs/sample-contract.txt", true); } + private void OnPhoneVerified() + { + // Move to next step after phone verification success + // _activeStep = 1; + // StateHasChanged(); + } + + private async Task SavePersonalInfo() + { + if (_stepTwoForm is null) return false; + + await _stepTwoForm.Validate(); + if (!_stepTwoForm.IsValid) return false; + + + try + { + // _updateUserRequest.AvatarPath="test"; + // _updateUserRequest.BirthDate=Timestamp.FromDateTime(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc)); + // _updateUserRequest.EmailNotifications = true; + // _updateUserRequest.PushNotifications = true; + // _updateUserRequest.SmsNotifications = true; + // + _updateUserRequest.FirstName = _model.FirstName; + _updateUserRequest.LastName = _model.LastName; + _updateUserRequest.NationalCode = _model.NationalCode.PersianToEnglish(); + await UserContract.UpdateUserAsync(request: _updateUserRequest); + Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success); + return true; + } + catch (Exception ex) + { + Snackbar.Add($"خطا در ذخیره اطلاعات: {ex.Message}", Severity.Error); + return false; + } + finally + { + await InvokeAsync(StateHasChanged); + } + } + private sealed class RegistrationModel { - [Required(ErrorMessage = "شماره موبایل الزامی است.")] - [RegularExpression(@"^09\d{9}$", ErrorMessage = "شماره موبایل معتبر نیست.")] - public string? MobileNumber { get; set; } - - [Required(ErrorMessage = "کد کپچا را وارد کنید.")] - public string? CaptchaInput { get; set; } - [Required(ErrorMessage = "نام الزامی است.")] [StringLength(50, ErrorMessage = "حداکثر ۵۰ کاراکتر")] public string? FirstName { get; set; } @@ -120,10 +168,10 @@ public partial class RegisterWizard public string? LastName { get; set; } [Required(ErrorMessage = "کد ملی الزامی است.")] - [RegularExpression(@"^\d{10}$", ErrorMessage = "کدملی باید ۱۰ رقم باشد.")] + [RegularExpression("^\\d{10}$", ErrorMessage = "کدملی باید ۱۰ رقم باشد.")] public string? NationalCode { get; set; } [Range(typeof(bool), "true", "true", ErrorMessage = "برای ادامه باید قوانین را تایید کنید.")] public bool AcceptTerms { get; set; } } -} +} \ No newline at end of file diff --git a/src/FrontOffice.Main/Pages/_Host.cshtml b/src/FrontOffice.Main/Pages/_Host.cshtml index 3a0af42..561a418 100644 --- a/src/FrontOffice.Main/Pages/_Host.cshtml +++ b/src/FrontOffice.Main/Pages/_Host.cshtml @@ -15,7 +15,8 @@ - + + @@ -31,8 +32,9 @@ 🗙 + + - - diff --git a/src/FrontOffice.Main/Program.cs b/src/FrontOffice.Main/Program.cs index 02ef306..7007249 100644 --- a/src/FrontOffice.Main/Program.cs +++ b/src/FrontOffice.Main/Program.cs @@ -23,7 +23,14 @@ ValidatorOptions.Global.LanguageManager = new CustomFluentValidationLanguageMana #endregion var appSettings = builder.Configuration.Get(); -UrlUtility.DownloadUrl = appSettings.DownloadUrl; +if (!string.IsNullOrWhiteSpace(appSettings?.DownloadUrl)) +{ + UrlUtility.DownloadUrl = appSettings.DownloadUrl; +} +else +{ + UrlUtility.DownloadUrl = string.Empty; // fallback to empty +} builder.Services.Configure(builder.Configuration.GetSection("EncryptionSettings")); builder.Services.AddSingleton(); @@ -52,10 +59,10 @@ app.Run(); public class AppSettings { - public string DownloadUrl { get; set; } + public required string DownloadUrl { get; set; } } public class EncryptionSettings { - public string Key { get; set; } - public string IV { get; set; } + public required string Key { get; set; } + public required string IV { get; set; } } \ No newline at end of file diff --git a/src/FrontOffice.Main/Shared/AuthDialog.razor b/src/FrontOffice.Main/Shared/AuthDialog.razor index 2f06612..26fcc6e 100644 --- a/src/FrontOffice.Main/Shared/AuthDialog.razor +++ b/src/FrontOffice.Main/Shared/AuthDialog.razor @@ -1,12 +1,63 @@ - - - @GetDialogTitle() +@if (InlineMode) +{ + @* Inline rendering without MudDialog wrapper *@ + + @GetDialogTitle() + @PhoneOrVerifyContent() + +} +else +{ + + + @GetDialogTitle() + @PhoneOrVerifyContent() + + + @if (!HideCancelButton) + { + لغو + } @if (_currentStep == AuthStep.Phone) + { + + ارسال رمز پویا + + } + else if (_currentStep == AuthStep.Verify) + { + + تأیید و ورود + + } + + +} + +@code { + private RenderFragment PhoneOrVerifyContent() => __builder => + { + if (_currentStep == AuthStep.Phone) { - - لطفاً شماره موبایل خود را وارد کنید تا رمز پویا ارسال شود. + // Phone Step + __builder.OpenComponent(0, typeof(MudText)); + __builder.AddAttribute(1, "Typo", Typo.body2); + __builder.AddAttribute(2, "Class", "mb-4"); + __builder.AddAttribute(3, "Align", Align.Center); + __builder.AddContent(4, "لطفاً شماره موبایل خود را وارد کنید تا رمز پویا ارسال شود."); + __builder.CloseComponent(); + @if (EnableCaptcha) + { + + + @_captchaCode + + + تازه‌سازی کد + + + + } + @@ -33,7 +99,7 @@ } else if (_currentStep == AuthStep.Verify) { - + // Verify Step رمز پویا شش رقمی ارسال ‌شده به @_phoneNumber را وارد کنید. @@ -50,85 +116,28 @@ Class="mb-2" MaxLength="6" /> - @* - تلاش باقی‌مانده: @_attemptsLeft از @MaxVerificationAttempts - *@ - @if (!string.IsNullOrWhiteSpace(_errorMessage)) { - - @(_errorMessage) - + @(_errorMessage) } - @if (!string.IsNullOrWhiteSpace(_infoMessage)) { - - @(_infoMessage) - + @(_infoMessage) } - - تغییر شماره - + تغییر شماره - - @if (_resendRemaining > 0) { - - امکان ارسال مجدد تا @_resendRemaining ثانیه دیگر - + امکان ارسال مجدد تا @_resendRemaining ثانیه دیگر } else { - - ارسال مجدد رمز پویا - + ارسال مجدد رمز پویا } } - - - @if (!HideCancelButton) - { - لغو - } - @if (_currentStep == AuthStep.Phone) - { - - ارسال رمز پویا - - } - else if (_currentStep == AuthStep.Verify) - { - - تأیید و ورود - - } - - + }; +} diff --git a/src/FrontOffice.Main/Shared/AuthDialog.razor.cs b/src/FrontOffice.Main/Shared/AuthDialog.razor.cs index ea8f7b7..7aa2dac 100644 --- a/src/FrontOffice.Main/Shared/AuthDialog.razor.cs +++ b/src/FrontOffice.Main/Shared/AuthDialog.razor.cs @@ -1,20 +1,20 @@ 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; +using MetadataAlias = Grpc.Core.Metadata; // resolve ambiguity with MudBlazor.Metadata namespace FrontOffice.Main.Shared; public partial class AuthDialog : IDisposable { - [Parameter] - public bool HideCancelButton { get; set; } = false; + [Parameter] public bool HideCancelButton { get; set; } + [Parameter] public bool EnableCaptcha { get; set; } + [Parameter] public bool InlineMode { get; set; } - private enum AuthStep { Phone, Verify } + public enum AuthStep { Phone, Verify } private const int DefaultResendCooldown = 120; public const int MaxVerificationAttempts = 5; private const string PhoneStorageKey = "auth:phone-number"; @@ -22,14 +22,14 @@ public partial class AuthDialog : IDisposable private const string TokenStorageKey = "auth:token"; private const string OtpPurpose = "Login"; - private AuthStep _currentStep = AuthStep.Phone; + public AuthStep _currentStep = AuthStep.Phone; - private CreateNewOtpTokenRequestValidator _phoneRequestValidator = new(); - private CreateNewOtpTokenRequest _phoneRequest = new(); + private readonly CreateNewOtpTokenRequestValidator _phoneRequestValidator = new(); + private readonly CreateNewOtpTokenRequest _phoneRequest = new(); private MudForm? _phoneForm; - private VerifyOtpTokenRequestValidator _verifyRequestValidator = new(); - private VerifyOtpTokenRequest _verifyRequest = new(); + private readonly VerifyOtpTokenRequestValidator _verifyRequestValidator = new(); + private readonly VerifyOtpTokenRequest _verifyRequest = new(); private MudForm? _verifyForm; private bool _isBusy; @@ -41,10 +41,14 @@ public partial class AuthDialog : IDisposable private int _attemptsLeft = MaxVerificationAttempts; private CancellationTokenSource? _operationCts; + // Captcha fields + private string? _captchaCode; + private string? _captchaInput; + [Inject] private ILocalStorageService LocalStorage { get; set; } = default!; [Inject] private UserContract.UserContractClient UserClient { get; set; } = default!; - [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!; + [CascadingParameter] private IDialogReference? MudDialog { get; set; } [Parameter] public EventCallback OnLoginSuccess { get; set; } @@ -55,6 +59,11 @@ public partial class AuthDialog : IDisposable _phoneRequest.Purpose = OtpPurpose; _verifyRequest.Purpose = OtpPurpose; + if (EnableCaptcha) + { + GenerateCaptcha(); + } + var storedPhone = await LocalStorage.GetItemAsync(PhoneStorageKey); if (!string.IsNullOrWhiteSpace(storedPhone)) { @@ -62,7 +71,13 @@ public partial class AuthDialog : IDisposable } } - private async Task SendOtpAsync() + private void GenerateCaptcha() + { + _captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant(); + _captchaInput = string.Empty; + } + + public async Task SendOtpAsync() { _errorMessage = null; if (_phoneForm is null) @@ -72,6 +87,15 @@ public partial class AuthDialog : IDisposable if (!_phoneForm.IsValid) return; + if (EnableCaptcha) + { + if (string.IsNullOrWhiteSpace(_captchaInput) || !string.Equals(_captchaInput.Trim(), _captchaCode, StringComparison.OrdinalIgnoreCase)) + { + _errorMessage = "کد کپچا صحیح نیست."; + return; + } + } + _isBusy = true; _operationCts?.Cancel(); _operationCts?.Dispose(); @@ -87,15 +111,9 @@ public partial class AuthDialog : IDisposable } var metadata = await BuildAuthMetadataAsync(); - CreateNewOtpTokenResponse response; - if (metadata is not null) - { - response = await UserClient.CreateNewOtpTokenAsync(_phoneRequest, metadata, cancellationToken: _operationCts.Token); - } - else - { - response = await UserClient.CreateNewOtpTokenAsync(_phoneRequest, cancellationToken: _operationCts.Token); - } + CreateNewOtpTokenResponse response = metadata is not null + ? await UserClient.CreateNewOtpTokenAsync(_phoneRequest, metadata, cancellationToken: _operationCts.Token) + : await UserClient.CreateNewOtpTokenAsync(_phoneRequest, cancellationToken: _operationCts.Token); if (response?.Success != true) { @@ -118,6 +136,7 @@ public partial class AuthDialog : IDisposable } catch (OperationCanceledException) { + // ignored - user canceled operation } catch (Exception ex) { @@ -131,28 +150,28 @@ public partial class AuthDialog : IDisposable } } - private async Task VerifyOtpAsync() + public async Task VerifyOtpAsync() { _errorMessage = null; _infoMessage = null; if (_verifyForm is null) - return; + return false; await _verifyForm.Validate(); if (!_verifyForm.IsValid) - return; + return false; if (IsVerificationLocked) { _errorMessage = "تعداد تلاش‌های مجاز به پایان رسیده است. لطفاً رمز جدید دریافت کنید."; - return; + return false; } if (string.IsNullOrWhiteSpace(_phoneNumber)) { _errorMessage = "شماره موبایل یافت نشد. لطفاً دوباره تلاش کنید."; - return; + return false; } _isBusy = true; @@ -162,7 +181,6 @@ public partial class AuthDialog : IDisposable { _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)) { @@ -173,24 +191,18 @@ public partial class AuthDialog : IDisposable if (!validationResult.IsValid) { _errorMessage = string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct()); - return; + return false; } var metadata = await BuildAuthMetadataAsync(); - VerifyOtpTokenResponse response; - if (metadata is not null) - { - response = await UserClient.VerifyOtpTokenAsync(_verifyRequest, metadata, cancellationToken: cancellationToken); - } - else - { - response = await UserClient.VerifyOtpTokenAsync(_verifyRequest, cancellationToken: cancellationToken); - } + VerifyOtpTokenResponse response = metadata is not null + ? await UserClient.VerifyOtpTokenAsync(_verifyRequest, metadata, cancellationToken: cancellationToken) + : await UserClient.VerifyOtpTokenAsync(_verifyRequest, cancellationToken: cancellationToken); if (response is null) { _errorMessage = "تأیید رمز پویا انجام نشد. لطفاً دوباره تلاش کنید."; - return; + return false; } if (response.Success) @@ -206,16 +218,17 @@ 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; await OnLoginSuccess.InvokeAsync(); - MudDialog.Close(); - return; + if (!InlineMode) + { + MudDialog?.Close(); + } + return true; } RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message); @@ -226,6 +239,7 @@ public partial class AuthDialog : IDisposable } catch (OperationCanceledException) { + // ignored - user canceled operation } catch (Exception ex) { @@ -237,6 +251,8 @@ public partial class AuthDialog : IDisposable ClearOperationToken(); await InvokeAsync(StateHasChanged); } + + return false; } private async Task HandleVerificationFailureAsync(RpcException rpcEx) @@ -269,23 +285,15 @@ public partial class AuthDialog : IDisposable private void RegisterFailedAttempt(string baseMessage) { _attemptsLeft = Math.Max(0, _attemptsLeft - 1); - - if (_attemptsLeft > 0) - { - _errorMessage = $"{baseMessage} {_attemptsLeft} تلاش باقی مانده است."; - } - else - { - _errorMessage = $"{baseMessage} تلاش‌های مجاز شما به پایان رسیده است. لطفاً رمز جدید دریافت کنید."; - } + _errorMessage = _attemptsLeft > 0 + ? $"{baseMessage} {_attemptsLeft} تلاش باقی مانده است." + : $"{baseMessage} تلاش‌های مجاز شما به پایان رسیده است. لطفاً رمز جدید دریافت کنید."; } private async Task ResendOtpAsync() { if (_resendRemaining > 0 || _isBusy || string.IsNullOrWhiteSpace(_phoneNumber)) - { return; - } _errorMessage = null; _infoMessage = null; @@ -294,22 +302,11 @@ public partial class AuthDialog : IDisposable try { - var request = new CreateNewOtpTokenRequest - { - Mobile = _phoneNumber, - Purpose = OtpPurpose - }; - + 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); - } + CreateNewOtpTokenResponse response = metadata is not null + ? await UserClient.CreateNewOtpTokenAsync(request, metadata, cancellationToken: cancellationToken) + : await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: cancellationToken); if (response?.Success != true) { @@ -319,10 +316,7 @@ public partial class AuthDialog : IDisposable return; } - _infoMessage = string.IsNullOrWhiteSpace(response.Message) - ? "کد جدید ارسال شد." - : response.Message; - + _infoMessage = string.IsNullOrWhiteSpace(response.Message) ? "کد جدید ارسال شد." : response.Message; _attemptsLeft = MaxVerificationAttempts; _verifyRequest.Code = string.Empty; StartResendCountdown(); @@ -333,6 +327,7 @@ public partial class AuthDialog : IDisposable } catch (OperationCanceledException) { + // ignored } catch (Exception ex) { @@ -373,6 +368,8 @@ public partial class AuthDialog : IDisposable _resendTimer?.Dispose(); _resendTimer = null; _resendRemaining = 0; + if (EnableCaptcha) + GenerateCaptcha(); } private void StartResendCountdown(int seconds = DefaultResendCooldown) @@ -388,23 +385,15 @@ public partial class AuthDialog : IDisposable _resendTimer?.Dispose(); _resendTimer = null; } - _ = InvokeAsync(StateHasChanged); }, null, 1000, 1000); } - private async Task BuildAuthMetadataAsync() + private async Task BuildAuthMetadataAsync() { var token = await LocalStorage.GetItemAsync(TokenStorageKey); - if (string.IsNullOrWhiteSpace(token)) - { - return null; - } - - return new Metadata - { - { "Authorization", $"Bearer {token}" } - }; + if (string.IsNullOrWhiteSpace(token)) return null; + return new MetadataAlias { { "Authorization", $"Bearer {token}" } }; } private async Task ResetAuthenticationAsync() @@ -430,7 +419,8 @@ public partial class AuthDialog : IDisposable private void Cancel() { - MudDialog.Cancel(); + if (!InlineMode) + MudDialog?.Close(DialogResult.Cancel()); } public void Dispose() @@ -438,9 +428,9 @@ public partial class AuthDialog : IDisposable _operationCts?.Cancel(); _operationCts?.Dispose(); _operationCts = null; - _resendTimer?.Dispose(); _resendTimer = null; } + private string GetDialogTitle() => _currentStep == AuthStep.Phone ? "ورود/ثبت‌نام به حساب کاربری" : "تأیید رمز پویا"; -} \ No newline at end of file +} diff --git a/src/FrontOffice.Main/Utilities/CustomMudTheme.cs b/src/FrontOffice.Main/Utilities/CustomMudTheme.cs index 2b36e13..20c9f65 100644 --- a/src/FrontOffice.Main/Utilities/CustomMudTheme.cs +++ b/src/FrontOffice.Main/Utilities/CustomMudTheme.cs @@ -9,89 +9,82 @@ public static class CustomMudTheme PaletteLight = new PaletteLight() { Primary = "#0380C0", - //Secondary = CustomColor.Secondary.Default, - //Tertiary = CustomColor.Tertiary.Default, - Background = "#F5F5F5", AppbarBackground = "#F5F5F5", - - //PrimaryContrastText = "#FFFFFF", - //SecondaryContrastText = "#FFFFFF", - //ErrorContrastText = "#FFFFFF", - //SuccessContrastText = "#FFFFFF", - //InfoContrastText = "#FFFFFF", - //WarningContrastText = "#FFFFFF", TextPrimary = Colors.Gray.Darken3, - - //// TextSecondary = "#FFFFFF", - - //Error = CustomColor.Error.Default, - //ErrorLighten = CustomColor.Error.Error100, - - //// Info = "#3977AD", - //InfoLighten = CustomColor.Info.Lighten, - //InfoDarken = CustomColor.Info.Darken, - - //// Success = "#05AF82", - //SuccessDarken = CustomColor.Success.Darken, - //SuccessLighten = CustomColor.Success.Lighten, - - ////Warning = "#EF7300", - //WarningDarken = CustomColor.Warning.Lighten, - //WarningLighten = CustomColor.Warning.Lighten, - - //BackgroundGrey = CustomColor.Other.Background, - //GrayDefault = CustomColor.Other.Background, - //GrayDark = CustomColor.Gray.Gray10, - //GrayLight = CustomColor.Gray.Gray60, - //GrayLighter = CustomColor.Gray.Gray80, - - - //TextDisabled = CustomColor.Other.DisableText, - //ActionDisabled = CustomColor.Other.DisableText, - //ActionDisabledBackground = CustomColor.Other.DisableBackground, - Surface = "#FFFFFF", Divider = "#B2BFCB", }, - - Typography = new Typography() + Typography = new Typography { - // پایه - Default = new Default() + Default = new DefaultTypography() { FontFamily = new[] { "Vazir", "Tahoma", "Segoe UI", "Arial", "sans-serif" } }, - - // هدینگ‌ها (اسکیل متعادل برای وب) - H1 = new H1 { FontFamily = new[] { "Vazir" }, FontSize = "2rem", LineHeight = 1.70, FontWeight = 800, LetterSpacing = "normal" }, // ~32px - H2 = new H2 { FontFamily = new[] { "Vazir" }, FontSize = "1.875rem", LineHeight = 1.65, FontWeight = 800, LetterSpacing = "normal" }, // ~30px - H3 = new H3 { FontFamily = new[] { "Vazir" }, FontSize = "1.5rem", LineHeight = 1.60, FontWeight = 800, LetterSpacing = "normal" }, // ~24px - H4 = new H4 { FontFamily = new[] { "Vazir" }, FontSize = "1.25rem", LineHeight = 1.55, FontWeight = 800, LetterSpacing = "normal" }, // ~20px - H5 = new H5 { FontFamily = new[] { "Vazir" }, FontSize = "1.125rem", LineHeight = 1.50, FontWeight = 800, LetterSpacing = "normal" }, // ~18px - H6 = new H6 { FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = 1.45, FontWeight = 800, LetterSpacing = "normal" }, // ~16px - - // Subtitles - Subtitle1 = new Subtitle1 { FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = 1.62, FontWeight = 500, LetterSpacing = "normal" }, - Subtitle2 = new Subtitle2 { FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = 1.60, FontWeight = 500, LetterSpacing = "normal" }, - - // Body text (برای خوانایی بازتر از هدینگ‌ها) - Body1 = new Body1 { FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = 1.85, FontWeight = 400, LetterSpacing = "normal" }, - Body2 = new Body2 { FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = 1.80, FontWeight = 400, LetterSpacing = "normal" }, - - // Small text - Caption = new Caption { FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = 1.60, FontWeight = 400, LetterSpacing = "normal" }, - Overline = new Overline { FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = 1.60, FontWeight = 500, LetterSpacing = "normal" }, - - // Buttons - Button = new Button + H1 = new H1Typography() { - FontFamily = new[] { "Vazir" }, - FontSize = "0.875rem", - LineHeight = 1.60, - FontWeight = 600, - LetterSpacing = "normal", - TextTransform = "none" // حروف بزرگ اجباری غیرفعال + FontFamily = new[] { "Vazir" }, FontSize = "2rem", LineHeight = "1.70", FontWeight = "800", + LetterSpacing = "normal" + }, + H2 = new H2Typography() + { + FontFamily = new[] { "Vazir" }, FontSize = "1.875rem", LineHeight = "1.65", FontWeight = "800", + LetterSpacing = "normal" + }, + H3 = new H3Typography() + { + FontFamily = new[] { "Vazir" }, FontSize = "1.5rem", LineHeight = "1.60", FontWeight = "800", + LetterSpacing = "normal" + }, + H4 = new H4Typography() + { + FontFamily = new[] { "Vazir" }, FontSize = "1.25rem", LineHeight = "1.55", FontWeight = "800", + LetterSpacing = "normal" + }, + H5 = new H5Typography() + { + FontFamily = new[] { "Vazir" }, FontSize = "1.125rem", LineHeight = "1.50", FontWeight = "800", + LetterSpacing = "normal" + }, + H6 = new H6Typography() + { + FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.45", FontWeight = "800", + LetterSpacing = "normal" + }, + Subtitle1 = new Subtitle1Typography() + { + FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.62", FontWeight = "500", + LetterSpacing = "normal" + }, + Subtitle2 = new Subtitle2Typography() + { + FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.60", FontWeight = "500", + LetterSpacing = "normal" + }, + Body1 = new Body1Typography() + { + FontFamily = new[] { "Vazir" }, FontSize = "1rem", LineHeight = "1.85", FontWeight = "400", + LetterSpacing = "normal" + }, + Body2 = new Body2Typography() + { + FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.80", FontWeight = "400", + LetterSpacing = "normal" + }, + Caption = new CaptionTypography() + { + FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = "1.60", FontWeight = "400", + LetterSpacing = "normal" + }, + Overline = new OverlineTypography() + { + FontFamily = new[] { "Vazir" }, FontSize = "0.75rem", LineHeight = "1.60", FontWeight = "500", + LetterSpacing = "normal" + }, + Button = new ButtonTypography() + { + FontFamily = new[] { "Vazir" }, FontSize = "0.875rem", LineHeight = "1.60", FontWeight = "600", + LetterSpacing = "normal", TextTransform = "none" } } }; diff --git a/src/FrontOffice.Main/Utilities/UrlUtility.cs b/src/FrontOffice.Main/Utilities/UrlUtility.cs index ae03683..25c20a4 100644 --- a/src/FrontOffice.Main/Utilities/UrlUtility.cs +++ b/src/FrontOffice.Main/Utilities/UrlUtility.cs @@ -2,5 +2,5 @@ public static class UrlUtility { - public static string DownloadUrl { get; set; } + public static string DownloadUrl { get; set; } = string.Empty; // initialize to avoid null } \ No newline at end of file diff --git a/src/FrontOffice.Main/wwwroot/css/site.css b/src/FrontOffice.Main/wwwroot/css/site.css index b6c893c..5808914 100644 --- a/src/FrontOffice.Main/wwwroot/css/site.css +++ b/src/FrontOffice.Main/wwwroot/css/site.css @@ -5,7 +5,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url(../fonts/Vazir-Light.ttf) format('woff2'); + src: url(../fonts/Vazir-Light.ttf) format('truetype'); } @font-face { @@ -13,7 +13,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(../fonts/Vazir-Regular.ttf) format('woff2'); + src: url(../fonts/Vazir-Regular.ttf) format('truetype'); } @font-face { @@ -21,7 +21,7 @@ font-style: normal; font-weight: 500; font-display: swap; - src: url(../fonts/Vazir-Medium.ttf) format('woff2'); + src: url(../fonts/Vazir-Medium.ttf) format('truetype'); } @font-face { @@ -29,7 +29,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: url(../fonts/Vazir-Bold.ttf) format('woff2'); + src: url(../fonts/Vazir-Bold.ttf) format('truetype'); } @font-face { @@ -37,8 +37,58 @@ font-style: normal; font-weight: 900; font-display: swap; - src: url(../fonts/Vazir-Bold.ttf) format('woff2'); + src: url(../fonts/Vazir-Bold.ttf) format('truetype'); } + +:root { + --app-font-family: 'Vazir', Tahoma, 'Segoe UI', Arial, sans-serif; +} + +html, body { + font-family: var(--app-font-family); + font-weight: 400; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Apply Vazir to common Mud components */ +.mud-typography, +.mud-typography-root, +.mud-button-label, +.mud-input-slot, +.mud-input-slot input, +.mud-input-slot textarea, +.mud-select, +.mud-snackbar, +.mud-chip, +.mud-breadcrumbs, +.mud-tooltip, +.mud-table, +.mud-textfield, +.mud-checkbox, +.mud-radio, +.mud-switch { + font-family: var(--app-font-family) !important; +} + +/* Typography variants override to match previous settings */ +.mud-typography-h1 { font-family: var(--app-font-family); font-size: 2rem; font-weight: 700; line-height: 1.70; letter-spacing: normal; } +.mud-typography-h2 { font-family: var(--app-font-family); font-size: 1.875rem; font-weight: 700; line-height: 1.65; letter-spacing: normal; } +.mud-typography-h3 { font-family: var(--app-font-family); font-size: 1.5rem; font-weight: 700; line-height: 1.60; letter-spacing: normal; } +.mud-typography-h4 { font-family: var(--app-font-family); font-size: 1.25rem; font-weight: 700; line-height: 1.55; letter-spacing: normal; } +.mud-typography-h5 { font-family: var(--app-font-family); font-size: 1.125rem; font-weight: 700; line-height: 1.50; letter-spacing: normal; } +.mud-typography-h6 { font-family: var(--app-font-family); font-size: 1rem; font-weight: 700; line-height: 1.45; letter-spacing: normal; } + +.mud-typography-subtitle1 { font-family: var(--app-font-family); font-size: 1rem; font-weight: 500; line-height: 1.62; letter-spacing: normal; } +.mud-typography-subtitle2 { font-family: var(--app-font-family); font-size: 0.875rem; font-weight: 500; line-height: 1.60; letter-spacing: normal; } + +.mud-typography-body1 { font-family: var(--app-font-family); font-size: 1rem; font-weight: 400; line-height: 1.85; letter-spacing: normal; } +.mud-typography-body2 { font-family: var(--app-font-family); font-size: 0.875rem; font-weight: 400; line-height: 1.80; letter-spacing: normal; } + +.mud-typography-caption { font-family: var(--app-font-family); font-size: 0.75rem; font-weight: 400; line-height: 1.60; letter-spacing: normal; } +.mud-typography-overline { font-family: var(--app-font-family); font-size: 0.75rem; font-weight: 500; line-height: 1.60; letter-spacing: normal; } + +.mud-button-label { text-transform: none; } /*#endregion*/ /*#region Layout Styles*/