Update MudBlazor integration, improve captcha handling, and upgrade project dependencies

This commit is contained in:
masoodafar-web
2025-11-14 09:32:19 +03:30
parent cce59612fa
commit 07ea8f0f47
15 changed files with 456 additions and 395 deletions

1
.gitignore vendored
View File

@@ -491,3 +491,4 @@ fabric.properties
# Android studio 3.1+ serialized cache file # Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser .idea/caches/build_file_checksums.ser
/src/.idea

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>6dab807c-c6d8-4711-bf64-11c69e8d39f4</UserSecretsId> <UserSecretsId>6dab807c-c6d8-4711-bf64-11c69e8d39f4</UserSecretsId>
@@ -14,11 +14,11 @@
<PackageReference Include="Foursat.FrontOffice.BFF.User.Protobuf" Version="0.0.115" /> <PackageReference Include="Foursat.FrontOffice.BFF.User.Protobuf" Version="0.0.115" />
<PackageReference Include="Foursat.FrontOffice.BFF.UserAddress.Protobuf" Version="0.0.114" /> <PackageReference Include="Foursat.FrontOffice.BFF.UserAddress.Protobuf" Version="0.0.114" />
<PackageReference Include="Foursat.FrontOffice.BFF.UserOrder.Protobuf" Version="0.0.112" /> <PackageReference Include="Foursat.FrontOffice.BFF.UserOrder.Protobuf" Version="0.0.112" />
<PackageReference Include="MudBlazor" Version="7.16.0" /> <PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" /> <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Mapster" Version="7.4.0" /> <PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Grpc.Net.Client.Web" Version="2.59.0" /> <PackageReference Include="Grpc.Net.Client.Web" Version="2.71.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.59.0" /> <PackageReference Include="Grpc.Net.ClientFactory" Version="2.71.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -10,7 +10,7 @@
<MudText Typo="Typo.h2" Align="Align.Center" Class="mb-3"> <MudText Typo="Typo.h2" Align="Align.Center" Class="mb-3">
آماده شنیدن صدای شما هستیم آماده شنیدن صدای شما هستیم
</MudText> </MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary mb-6" MaxWidth="600px"> <MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary mb-6" Style="max-width:600px">
سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست. سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست.
</MudText> </MudText>
</MudStack> </MudStack>
@@ -94,7 +94,7 @@
<MudItem xs="12"> <MudItem xs="12">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Start"> <MudStack Row="true" Spacing="2" AlignItems="AlignItems.Start">
<MudCheckBox T="bool" @bind-Checked="_contactForm.AcceptTerms" /> <MudCheckBox @bind-Value="_contactForm.AcceptTerms" />
<MudText Typo="Typo.body2" Class="mud-text-secondary"> <MudText Typo="Typo.body2" Class="mud-text-secondary">
با ارسال این فرم، با ارسال این فرم،
<MudLink Href="/privacy" Target="_blank">سیاست حفظ حریم خصوصی</MudLink> <MudLink Href="/privacy" Target="_blank">سیاست حفظ حریم خصوصی</MudLink>
@@ -234,4 +234,3 @@
</MudContainer> </MudContainer>
</section> </section>
</MudStack> </MudStack>

View File

@@ -193,7 +193,7 @@ else
</MudAvatar> </MudAvatar>
<div> <div>
<MudText Typo="Typo.body2" >@(review.UserName)</MudText> <MudText Typo="Typo.body2" >@(review.UserName)</MudText>
<MudRating ReadOnly="true" Value="review.Rating" Size="Size.Small" /> <MudRating ReadOnly="true" Value="@review.Rating" Size="Size.Small" />
</div> </div>
<MudSpacer /> <MudSpacer />
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(review.Date)</MudText> <MudText Typo="Typo.caption" Class="mud-text-secondary">@(review.Date)</MudText>

View File

@@ -8,7 +8,7 @@ namespace FrontOffice.Main.Pages.Profile.Components;
public partial class AddAddressDialog : ComponentBase 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!; [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
private MudForm? _form; private MudForm? _form;
@@ -41,5 +41,5 @@ public partial class AddAddressDialog : ComponentBase
} }
} }
private void Cancel() => MudDialog.Cancel(); private void Cancel() => MudDialog.Close(DialogResult.Cancel());
} }

View File

@@ -9,8 +9,9 @@ namespace FrontOffice.Main.Pages.Profile.Components;
public partial class EditAddressDialog : ComponentBase 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!; [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
// removed duplicate Snackbar injection; provided by Razor partial via _Imports
[Parameter] public GetAllUserAddressByFilterResponseModel? Model { get; set; } [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());
} }

View File

@@ -7,15 +7,14 @@
<MudGrid Spacing="4" Justify="Justify.Center"> <MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" md="5"> <MudItem xs="12" md="5">
<MudStack Spacing="2" Class="mb-6"> <MudStack Spacing="2" Class="mb-6">
<MudChip T="string" <MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">ثبت‌نام سه
Color="Color.Secondary" مرحله‌ای</MudChip>
Variant="Variant.Filled"
Class="mb-2">ثبت‌نام سه مرحله‌ای</MudChip>
<MudText Typo="Typo.h3" Class="mb-2"> <MudText Typo="Typo.h3" Class="mb-2">
فقط در چند دقیقه حساب خود را فعال کنید فقط در چند دقیقه حساب خود را فعال کنید
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary"> <MudText Typo="Typo.body1" Class="mud-text-secondary">
اطلاعات اولیه را وارد کنید، مشخصات هویتی را تکمیل کنید و بعد از مطالعه قوانین و دانلود قرارداد، درخواست خود را ارسال کنید. اطلاعات اولیه را وارد کنید، مشخصات هویتی را تکمیل کنید و بعد از مطالعه قوانین و دانلود قرارداد،
درخواست خود را ارسال کنید.
</MudText> </MudText>
</MudStack> </MudStack>
@@ -42,15 +41,16 @@
<MudAvatar Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" /> <MudAvatar Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" />
<MudText Typo="Typo.h4">درخواست شما ثبت شد</MudText> <MudText Typo="Typo.h4">درخواست شما ثبت شد</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary"> <MudText Typo="Typo.body1" Class="mud-text-secondary">
تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام را دنبال کنید. تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام
را دنبال کنید.
</MudText> </MudText>
<MudStack Row="true" Spacing="2" Justify="Justify.Center"> <MudStack Row="true" Spacing="2" Justify="Justify.Center">
<MudButton Variant="Variant.Filled" <MudButton Variant="Variant.Filled" Color="Color.Primary"
Color="Color.Primary" OnClick="@(() => Navigation.NavigateTo(RouteConstants.Main.MainPage))">بازگشت به صفحه
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Main.MainPage))">بازگشت به صفحه اصلی</MudButton> اصلی</MudButton>
<MudButton Variant="Variant.Outlined" <MudButton Variant="Variant.Outlined" Color="Color.Primary"
Color="Color.Primary" OnClick="@(() => Navigation.NavigateTo(RouteConstants.Profile.Index))">مشاهده پروفایل
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Profile.Index))">مشاهده پروفایل</MudButton> </MudButton>
</MudStack> </MudStack>
</MudStack> </MudStack>
} }
@@ -59,7 +59,8 @@
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h4">ویزارد ثبت‌نام</MudText> <MudText Typo="Typo.h4">ویزارد ثبت‌نام</MudText>
<MudChip Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">۳ مرحله</MudChip> <MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">۳ مرحله
</MudChip>
</MudStack> </MudStack>
@if (_isSubmitting) @if (_isSubmitting)
@@ -67,115 +68,76 @@
<MudProgressLinear Color="Color.Primary" Indeterminate="true" /> <MudProgressLinear Color="Color.Primary" Indeterminate="true" />
} }
<MudStepper @bind-ActiveIndex="_activeStep" <MudStepper @bind-ActiveIndex="_activeStep" Elevation="0" DisableClick="true" Class="mb-4">
Elevation="0" <ChildContent>
DisableClick="true" <MudStep Label="تأیید موبایل" Icon="@Icons.Material.Filled.Smartphone">
Class="mb-4"> @* Inline AuthDialog with captcha enabled *@
<MudStep Label="تایید موبایل" Icon="@Icons.Material.Filled.Smartphone"> <AuthDialog @ref="_authDialog" InlineMode="true" EnableCaptcha="true" HideCancelButton="true" OnLoginSuccess="@(async () => { OnPhoneVerified(); })" />
<MudForm @ref="_stepOneForm">
<MudTextField Label="شماره موبایل"
Placeholder="مثال: 09121234567"
InputType="InputType.Tel"
Immediate="true"
MaxLength="11"
Variant="Variant.Outlined"
@bind-Value="_model.MobileNumber"
For="@(() => _model.MobileNumber)"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Outlined.Phone" />
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mt-4">
<MudPaper Elevation="1" Class="captcha-box d-flex align-center justify-center">
<MudText Typo="Typo.h5">@_captchaCode</MudText>
</MudPaper>
<MudButton Variant="Variant.Text"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Refresh"
Disabled="_isSubmitting"
OnClick="GenerateCaptcha">تازه‌سازی کد</MudButton>
</MudStack>
<MudTextField Label="کد کپچا"
Placeholder="کد نمایش داده شده"
Immediate="true"
Variant="Variant.Outlined"
@bind-Value="_model.CaptchaInput"
For="@(() => _model.CaptchaInput)" />
</MudForm>
</MudStep> </MudStep>
<MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Filled.Badge"> <MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Filled.Badge">
<MudForm @ref="_stepTwoForm"> <MudForm @ref="_stepTwoForm">
<MudTextField Label="نام" <MudTextField Label="نام" Variant="Variant.Outlined" Immediate="true"
Variant="Variant.Outlined" @bind-Value="_model.FirstName" For="@(() => _model.FirstName)" />
Immediate="true" <MudTextField Label="نام خانوادگی" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.FirstName" @bind-Value="_model.LastName" For="@(() => _model.LastName)" />
For="@(() => _model.FirstName)" /> <MudTextField Label="کد ملی" Variant="Variant.Outlined" Immediate="true"
<MudTextField Label="نام خانوادگی" MaxLength="10" @bind-Value="_model.NationalCode"
Variant="Variant.Outlined" For="@(() => _model.NationalCode)" InputType="InputType.Number" />
Immediate="true"
@bind-Value="_model.LastName"
For="@(() => _model.LastName)" />
<MudTextField Label="کد ملی"
Variant="Variant.Outlined"
Immediate="true"
MaxLength="10"
@bind-Value="_model.NationalCode"
For="@(() => _model.NationalCode)"
InputType="InputType.Number" />
</MudForm> </MudForm>
</MudStep> </MudStep>
<MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Filled.Rule"> <MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Filled.Rule">
<MudForm @ref="_stepThreeForm"> <MudForm @ref="_stepThreeForm">
<MudAlert Variant="Variant.Outlined" <MudAlert Variant="Variant.Outlined" Severity="Severity.Info" Class="mb-3">
Severity="Severity.Info" لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک
Class="mb-3"> تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره
لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره کنید. کنید.
</MudAlert> </MudAlert>
<MudPaper Elevation="0" Class="terms-box pa-4 mb-3"> <MudPaper Elevation="0" Class="terms-box pa-4 mb-3">
<MudText Typo="Typo.subtitle2" Class="mb-2">بخشی از قوانین:</MudText> <MudText Typo="Typo.subtitle2" Class="mb-2">بخشی از قوانین:</MudText>
<MudList Dense="true"> <MudList T="string" Dense="true">
<MudListItem>استفاده از اطلاعات کاربری صرفاً برای ثبت نام و احراز هویت مجاز است.</MudListItem> <MudListItem T="string">استفاده از اطلاعات کاربری صرفاً برای ثبت نام و
<MudListItem>تمامی فعالیت‌ها مطابق قوانین جمهوری اسلامی ایران انجام می‌شود.</MudListItem> احراز هویت مجاز است.</MudListItem>
<MudListItem>مسئولیت صحت اطلاعات وارد شده بر عهده متقاضی است.</MudListItem> <MudListItem T="string">تمامی فعالیت‌ها مطابق قوانین جمهوری اسلامی ایران
انجام می‌شود.</MudListItem>
<MudListItem T="string">مسئولیت صحت اطلاعات وارد شده بر عهده متقاضی است.
</MudListItem>
</MudList> </MudList>
</MudPaper> </MudPaper>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-2"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-2">
<MudButton Variant="Variant.Outlined" <MudButton Variant="Variant.Outlined" Color="Color.Primary"
Color="Color.Primary" StartIcon="@Icons.Material.Filled.Download" Disabled="_isSubmitting"
StartIcon="@Icons.Material.Filled.Download"
Disabled="_isSubmitting"
OnClick="DownloadContract"> OnClick="DownloadContract">
دانلود قرارداد نمونه دانلود قرارداد نمونه
</MudButton> </MudButton>
<MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: فایل متنی</MudText> <MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: فایل متنی
</MudText>
</MudStack> </MudStack>
<MudCheckBox @bind-Checked="_model.AcceptTerms" <MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Success"
Color="Color.Success"
For="@(() => _model.AcceptTerms)" For="@(() => _model.AcceptTerms)"
Label="قوانین و مقررات را مطالعه کرده‌ام و می‌پذیرم" /> Label="قوانین و مققرات را مطالعه کرده‌ام و می‌پذیرم" />
</MudForm> </MudForm>
</MudStep> </MudStep>
</MudStepper> </ChildContent>
<ActionContent Context="stepper">
<MudDivider Class="my-2" />
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"> <MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudButton Variant="Variant.Text" <MudButton Variant="Variant.Text" Color="Color.Secondary"
Color="Color.Secondary"
Disabled="_activeStep == 0 || _isSubmitting" Disabled="_activeStep == 0 || _isSubmitting"
OnClick="GoBack">مرحله قبل</MudButton> OnClick="@(async () => { await GoBack(stepper); })">مرحله قبل</MudButton>
<MudSpacer /> <MudSpacer />
<MudButton Variant="Variant.Filled" <MudButton Variant="Variant.Filled" Color="Color.Primary"
Color="Color.Primary"
Disabled="_isSubmitting" Disabled="_isSubmitting"
OnClick="GoNextAsync"> OnClick="@(async () => { await GoNextAsync(stepper); })">
@_nextButtonText @_nextButtonText
</MudButton> </MudButton>
</MudStack> </MudStack>
</ActionContent>
</MudStepper>
</MudStack> </MudStack>
} }
</MudPaper> </MudPaper>

View File

@@ -1,4 +1,8 @@
using System.ComponentModel.DataAnnotations; 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 Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
@@ -6,14 +10,16 @@ namespace FrontOffice.Main.Pages;
public partial class RegisterWizard public partial class RegisterWizard
{ {
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private readonly RegistrationModel _model = new(); private readonly RegistrationModel _model = new();
private MudForm? _stepOneForm;
private MudForm? _stepTwoForm; private MudForm? _stepTwoForm;
private MudForm? _stepThreeForm; private MudForm? _stepThreeForm;
private int _activeStep; private int _activeStep;
private bool _isSubmitting; private bool _isSubmitting;
private bool _completed; private bool _completed;
private string _captchaCode = string.Empty; private AuthDialog _authDialog;
private UpdateUserRequest _updateUserRequest = new();
private string _nextButtonText => _activeStep switch private string _nextButtonText => _activeStep switch
{ {
@@ -25,25 +31,18 @@ public partial class RegisterWizard
protected override void OnInitialized() protected override void OnInitialized()
{ {
base.OnInitialized(); base.OnInitialized();
GenerateCaptcha();
} }
private void GenerateCaptcha() private async Task GoBack(MudStepper mudStepper)
{
var random = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
_captchaCode = random;
_model.CaptchaInput = string.Empty;
}
private void GoBack()
{ {
if (_activeStep == 0 || _isSubmitting) if (_activeStep == 0 || _isSubmitting)
return; return;
_activeStep--; _activeStep--;
await mudStepper.PreviousStepAsync();
} }
private async Task GoNextAsync() private async Task GoNextAsync(MudStepper mudStepper)
{ {
if (_isSubmitting) if (_isSubmitting)
return; return;
@@ -51,18 +50,31 @@ public partial class RegisterWizard
switch (_activeStep) switch (_activeStep)
{ {
case 0: case 0:
if (!await ValidateStepAsync(_stepOneForm)) if (_authDialog._currentStep == AuthDialog.AuthStep.Phone)
return;
if (!string.Equals(_model.CaptchaInput?.Trim(), _captchaCode, StringComparison.OrdinalIgnoreCase))
{ {
Snackbar.Add("کد کپچا صحیح نیست.", Severity.Warning); await _authDialog.SendOtpAsync();
return; return;
} }
else
{
var verifyOtp = await _authDialog.VerifyOtpAsync();
if (!verifyOtp)
return;
_activeStep = 1; _activeStep = 1;
}
break; break;
case 1: case 1:
if (!await ValidateStepAsync(_stepTwoForm)) if (!await ValidateStepAsync(_stepTwoForm))
return; return;
var saveResult = await SavePersonalInfo();
if (!saveResult)
return;
_activeStep = 2; _activeStep = 2;
break; break;
case 2: case 2:
@@ -71,6 +83,8 @@ public partial class RegisterWizard
await SubmitAsync(); await SubmitAsync();
break; break;
} }
await mudStepper.NextStepAsync();
} }
private async Task<bool> ValidateStepAsync(MudForm? form) private async Task<bool> ValidateStepAsync(MudForm? form)
@@ -102,15 +116,49 @@ public partial class RegisterWizard
Navigation.NavigateTo("docs/sample-contract.txt", true); Navigation.NavigateTo("docs/sample-contract.txt", true);
} }
private void OnPhoneVerified()
{
// Move to next step after phone verification success
// _activeStep = 1;
// StateHasChanged();
}
private async Task<bool> 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 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 = "نام الزامی است.")] [Required(ErrorMessage = "نام الزامی است.")]
[StringLength(50, ErrorMessage = "حداکثر ۵۰ کاراکتر")] [StringLength(50, ErrorMessage = "حداکثر ۵۰ کاراکتر")]
public string? FirstName { get; set; } public string? FirstName { get; set; }
@@ -120,7 +168,7 @@ public partial class RegisterWizard
public string? LastName { get; set; } public string? LastName { get; set; }
[Required(ErrorMessage = "کد ملی الزامی است.")] [Required(ErrorMessage = "کد ملی الزامی است.")]
[RegularExpression(@"^\d{10}$", ErrorMessage = "کدملی باید ۱۰ رقم باشد.")] [RegularExpression("^\\d{10}$", ErrorMessage = "کدملی باید ۱۰ رقم باشد.")]
public string? NationalCode { get; set; } public string? NationalCode { get; set; }
[Range(typeof(bool), "true", "true", ErrorMessage = "برای ادامه باید قوانین را تایید کنید.")] [Range(typeof(bool), "true", "true", ErrorMessage = "برای ادامه باید قوانین را تایید کنید.")]

View File

@@ -15,7 +15,8 @@
<component type="typeof(HeadOutlet)" render-mode="Server" /> <component type="typeof(HeadOutlet)" render-mode="Server" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" /> <!-- Ensure latest MudBlazor CSS is used (cache-busting) -->
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" asp-append-version="true" />
</head> </head>
<body> <body>
<component type="typeof(App)" render-mode="Server" /> <component type="typeof(App)" render-mode="Server" />
@@ -31,8 +32,9 @@
<a class="dismiss">🗙</a> <a class="dismiss">🗙</a>
</div> </div>
<!-- Load MudBlazor JS before Blazor to avoid early JS interop calls failing; add cache-busting -->
<script src="_content/MudBlazor/MudBlazor.min.js" asp-append-version="true"></script>
<script src="_framework/blazor.server.js"></script> <script src="_framework/blazor.server.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script> <script>
// elementId: id نوار (مثلاً "top") // elementId: id نوار (مثلاً "top")
// containerSelector: کانتینری که اسکرول می‌خوره؛ برای MudLayout معمولا ".mud-main-content" // containerSelector: کانتینری که اسکرول می‌خوره؛ برای MudLayout معمولا ".mud-main-content"
@@ -73,4 +75,3 @@
</script> </script>
</body> </body>
</html> </html>

View File

@@ -23,7 +23,14 @@ ValidatorOptions.Global.LanguageManager = new CustomFluentValidationLanguageMana
#endregion #endregion
var appSettings = builder.Configuration.Get<AppSettings>(); var appSettings = builder.Configuration.Get<AppSettings>();
UrlUtility.DownloadUrl = appSettings.DownloadUrl; if (!string.IsNullOrWhiteSpace(appSettings?.DownloadUrl))
{
UrlUtility.DownloadUrl = appSettings.DownloadUrl;
}
else
{
UrlUtility.DownloadUrl = string.Empty; // fallback to empty
}
builder.Services.Configure<EncryptionSettings>(builder.Configuration.GetSection("EncryptionSettings")); builder.Services.Configure<EncryptionSettings>(builder.Configuration.GetSection("EncryptionSettings"));
builder.Services.AddSingleton<MobileNumberEncryptor>(); builder.Services.AddSingleton<MobileNumberEncryptor>();
@@ -52,10 +59,10 @@ app.Run();
public class AppSettings public class AppSettings
{ {
public string DownloadUrl { get; set; } public required string DownloadUrl { get; set; }
} }
public class EncryptionSettings public class EncryptionSettings
{ {
public string Key { get; set; } public required string Key { get; set; }
public string IV { get; set; } public required string IV { get; set; }
} }

View File

@@ -1,107 +1,19 @@
<MudDialog> @if (InlineMode)
{
@* Inline rendering without MudDialog wrapper *@
<MudStack Spacing="2">
<MudText Typo="Typo.h5" Align="Align.Center">@GetDialogTitle()</MudText>
@PhoneOrVerifyContent()
</MudStack>
}
else
{
<MudDialog>
<TitleContent> <TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">@GetDialogTitle()</MudText> <MudText Typo="Typo.h4" Align="Align.Center">@GetDialogTitle()</MudText>
</TitleContent> </TitleContent>
<DialogContent> <DialogContent>
@if (_currentStep == AuthStep.Phone) @PhoneOrVerifyContent()
{
<!-- Phone Step -->
<MudText Typo="Typo.body2" Class="mb-4" Align="Align.Center">لطفاً شماره موبایل خود را وارد کنید تا رمز پویا ارسال شود.</MudText>
<MudForm @ref="_phoneForm" Model="_phoneRequest" Validation="@(_phoneRequestValidator.ValidateValue)">
<MudTextField @bind-Value="_phoneRequest.Mobile"
For="@(() => _phoneRequest.Mobile)"
Label="شماره موبایل"
InputType="InputType.Telephone"
InputMode="InputMode.tel"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
RequiredError="وارد کردن شماره موبایل الزامی است."
HelperText="مثال: 09121234567"
Class="mb-2" />
<MudCheckBox T="bool"
Label="شرایط و قوانین را می‌پذیرم"
Class="mb-1" />
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0" Class="mb-2">@_errorMessage</MudAlert>
}
</MudForm>
}
else if (_currentStep == AuthStep.Verify)
{
<!-- Verify Step -->
<MudText Typo="Typo.body2" Class="mb-4" Align="Align.Center">رمز پویا شش رقمی ارسال ‌شده به @_phoneNumber را وارد کنید.</MudText>
<MudForm @ref="_verifyForm" Model="_verifyRequest" Validation="@(_verifyRequestValidator.ValidateValue)">
<MudTextField @bind-Value="_verifyRequest.Code"
For="@(() => _verifyRequest.Code)"
Label="رمز پویا"
InputType="InputType.Telephone"
InputMode="InputMode.tel"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
RequiredError="وارد کردن رمز پویا الزامی است."
HelperText="کد ۶ رقمی"
Class="mb-2"
MaxLength="6" />
@* <MudText Typo="Typo.body1" Align="Align.Center" Class="mb-2">
تلاش باقی‌مانده: @_attemptsLeft از @MaxVerificationAttempts
</MudText> *@
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<MudAlert Severity="Severity.Error"
Dense="true"
Elevation="0"
Class="mb-2">
@(_errorMessage)
</MudAlert>
}
@if (!string.IsNullOrWhiteSpace(_infoMessage))
{
<MudAlert Severity="Severity.Success"
Dense="true"
Elevation="0"
Class="mb-2">
@(_infoMessage)
</MudAlert>
}
<MudStack Spacing="2">
<MudButton Variant="Variant.Text"
Color="Color.Secondary"
Disabled="_isBusy"
OnClick="ChangePhoneAsync">
تغییر شماره
</MudButton>
</MudStack>
<MudDivider Class="my-2" />
@if (_resendRemaining > 0)
{
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">
امکان ارسال مجدد تا @_resendRemaining ثانیه دیگر
</MudText>
}
else
{
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Disabled="_isBusy"
OnClick="ResendOtpAsync">
ارسال مجدد رمز پویا
</MudButton>
}
</MudForm>
}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
@if (!HideCancelButton) @if (!HideCancelButton)
@@ -131,4 +43,101 @@
</MudButton> </MudButton>
} }
</DialogActions> </DialogActions>
</MudDialog> </MudDialog>
}
@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();
<MudForm @ref="_phoneForm" Model="_phoneRequest" Validation="@(_phoneRequestValidator.ValidateValue)">
<MudTextField @bind-Value="_phoneRequest.Mobile"
For="@(() => _phoneRequest.Mobile)"
Label="شماره موبایل"
InputType="InputType.Telephone"
InputMode="InputMode.tel"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
RequiredError="وارد کردن شماره موبایل الزامی است."
HelperText="مثال: 09121234567"
Class="mb-2" />
@if (EnableCaptcha)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mt-2 mb-2">
<MudPaper Elevation="1" Class="captcha-box d-flex align-center justify-center" Style="min-width:100px;min-height:48px;">
<MudText Typo="Typo.h5">@_captchaCode</MudText>
</MudPaper>
<MudButton Variant="Variant.Text" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh" Disabled="_isBusy" OnClick="GenerateCaptcha">
تازه‌سازی کد
</MudButton>
</MudStack>
<MudTextField Label="کد کپچا" Placeholder="کد نمایش داده شده" Immediate="true"
Variant="Variant.Outlined" @bind-Value="_captchaInput" Required="true"
RequiredError="لطفاً کد کپچا را وارد کنید." />
}
<MudCheckBox T="bool"
Label="شرایط و قوانین را می‌پذیرم"
Class="mb-1" />
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0" Class="mb-2">@_errorMessage</MudAlert>
}
</MudForm>
}
else if (_currentStep == AuthStep.Verify)
{
// Verify Step
<MudText Typo="Typo.body2" Class="mb-4" Align="Align.Center">رمز پویا شش رقمی ارسال ‌شده به @_phoneNumber را وارد کنید.</MudText>
<MudForm @ref="_verifyForm" Model="_verifyRequest" Validation="@(_verifyRequestValidator.ValidateValue)">
<MudTextField @bind-Value="_verifyRequest.Code"
For="@(() => _verifyRequest.Code)"
Label="رمز پویا"
InputType="InputType.Telephone"
InputMode="InputMode.tel"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
RequiredError="وارد کردن رمز پویا الزامی است."
HelperText="کد ۶ رقمی"
Class="mb-2"
MaxLength="6" />
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0" Class="mb-2">@(_errorMessage)</MudAlert>
}
@if (!string.IsNullOrWhiteSpace(_infoMessage))
{
<MudAlert Severity="Severity.Success" Dense="true" Elevation="0" Class="mb-2">@(_infoMessage)</MudAlert>
}
<MudStack Spacing="2">
<MudButton Variant="Variant.Text" Color="Color.Secondary" Disabled="_isBusy" OnClick="ChangePhoneAsync">تغییر شماره</MudButton>
</MudStack>
<MudDivider Class="my-2" />
@if (_resendRemaining > 0)
{
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">امکان ارسال مجدد تا @_resendRemaining ثانیه دیگر</MudText>
}
else
{
<MudButton Variant="Variant.Text" Color="Color.Primary" Disabled="_isBusy" OnClick="ResendOtpAsync">ارسال مجدد رمز پویا</MudButton>
}
</MudForm>
}
};
}

View File

@@ -1,20 +1,20 @@
using Blazored.LocalStorage; using Blazored.LocalStorage;
using FrontOffice.BFF.User.Protobuf.Protos.User; using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator; using FrontOffice.BFF.User.Protobuf.Validator;
using FrontOffice.Main.Utilities;
using Grpc.Core; using Grpc.Core;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
using MudBlazor; using MudBlazor;
using MetadataAlias = Grpc.Core.Metadata; // resolve ambiguity with MudBlazor.Metadata
namespace FrontOffice.Main.Shared; namespace FrontOffice.Main.Shared;
public partial class AuthDialog : IDisposable public partial class AuthDialog : IDisposable
{ {
[Parameter] [Parameter] public bool HideCancelButton { get; set; }
public bool HideCancelButton { get; set; } = false; [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; private const int DefaultResendCooldown = 120;
public const int MaxVerificationAttempts = 5; public const int MaxVerificationAttempts = 5;
private const string PhoneStorageKey = "auth:phone-number"; 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 TokenStorageKey = "auth:token";
private const string OtpPurpose = "Login"; private const string OtpPurpose = "Login";
private AuthStep _currentStep = AuthStep.Phone; public AuthStep _currentStep = AuthStep.Phone;
private CreateNewOtpTokenRequestValidator _phoneRequestValidator = new(); private readonly CreateNewOtpTokenRequestValidator _phoneRequestValidator = new();
private CreateNewOtpTokenRequest _phoneRequest = new(); private readonly CreateNewOtpTokenRequest _phoneRequest = new();
private MudForm? _phoneForm; private MudForm? _phoneForm;
private VerifyOtpTokenRequestValidator _verifyRequestValidator = new(); private readonly VerifyOtpTokenRequestValidator _verifyRequestValidator = new();
private VerifyOtpTokenRequest _verifyRequest = new(); private readonly VerifyOtpTokenRequest _verifyRequest = new();
private MudForm? _verifyForm; private MudForm? _verifyForm;
private bool _isBusy; private bool _isBusy;
@@ -41,10 +41,14 @@ public partial class AuthDialog : IDisposable
private int _attemptsLeft = MaxVerificationAttempts; private int _attemptsLeft = MaxVerificationAttempts;
private CancellationTokenSource? _operationCts; private CancellationTokenSource? _operationCts;
// Captcha fields
private string? _captchaCode;
private string? _captchaInput;
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!; [Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserClient { 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; } [Parameter] public EventCallback OnLoginSuccess { get; set; }
@@ -55,6 +59,11 @@ public partial class AuthDialog : IDisposable
_phoneRequest.Purpose = OtpPurpose; _phoneRequest.Purpose = OtpPurpose;
_verifyRequest.Purpose = OtpPurpose; _verifyRequest.Purpose = OtpPurpose;
if (EnableCaptcha)
{
GenerateCaptcha();
}
var storedPhone = await LocalStorage.GetItemAsync<string>(PhoneStorageKey); var storedPhone = await LocalStorage.GetItemAsync<string>(PhoneStorageKey);
if (!string.IsNullOrWhiteSpace(storedPhone)) 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; _errorMessage = null;
if (_phoneForm is null) if (_phoneForm is null)
@@ -72,6 +87,15 @@ public partial class AuthDialog : IDisposable
if (!_phoneForm.IsValid) if (!_phoneForm.IsValid)
return; return;
if (EnableCaptcha)
{
if (string.IsNullOrWhiteSpace(_captchaInput) || !string.Equals(_captchaInput.Trim(), _captchaCode, StringComparison.OrdinalIgnoreCase))
{
_errorMessage = "کد کپچا صحیح نیست.";
return;
}
}
_isBusy = true; _isBusy = true;
_operationCts?.Cancel(); _operationCts?.Cancel();
_operationCts?.Dispose(); _operationCts?.Dispose();
@@ -87,15 +111,9 @@ public partial class AuthDialog : IDisposable
} }
var metadata = await BuildAuthMetadataAsync(); var metadata = await BuildAuthMetadataAsync();
CreateNewOtpTokenResponse response; CreateNewOtpTokenResponse response = metadata is not null
if (metadata is not null) ? await UserClient.CreateNewOtpTokenAsync(_phoneRequest, metadata, cancellationToken: _operationCts.Token)
{ : await UserClient.CreateNewOtpTokenAsync(_phoneRequest, cancellationToken: _operationCts.Token);
response = await UserClient.CreateNewOtpTokenAsync(_phoneRequest, metadata, cancellationToken: _operationCts.Token);
}
else
{
response = await UserClient.CreateNewOtpTokenAsync(_phoneRequest, cancellationToken: _operationCts.Token);
}
if (response?.Success != true) if (response?.Success != true)
{ {
@@ -118,6 +136,7 @@ public partial class AuthDialog : IDisposable
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// ignored - user canceled operation
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -131,28 +150,28 @@ public partial class AuthDialog : IDisposable
} }
} }
private async Task VerifyOtpAsync() public async Task<bool> VerifyOtpAsync()
{ {
_errorMessage = null; _errorMessage = null;
_infoMessage = null; _infoMessage = null;
if (_verifyForm is null) if (_verifyForm is null)
return; return false;
await _verifyForm.Validate(); await _verifyForm.Validate();
if (!_verifyForm.IsValid) if (!_verifyForm.IsValid)
return; return false;
if (IsVerificationLocked) if (IsVerificationLocked)
{ {
_errorMessage = "تعداد تلاش‌های مجاز به پایان رسیده است. لطفاً رمز جدید دریافت کنید."; _errorMessage = "تعداد تلاش‌های مجاز به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
return; return false;
} }
if (string.IsNullOrWhiteSpace(_phoneNumber)) if (string.IsNullOrWhiteSpace(_phoneNumber))
{ {
_errorMessage = "شماره موبایل یافت نشد. لطفاً دوباره تلاش کنید."; _errorMessage = "شماره موبایل یافت نشد. لطفاً دوباره تلاش کنید.";
return; return false;
} }
_isBusy = true; _isBusy = true;
@@ -162,7 +181,6 @@ public partial class AuthDialog : IDisposable
{ {
_verifyRequest.Mobile = _phoneNumber; _verifyRequest.Mobile = _phoneNumber;
// Check for stored referral code and add it to the request
var storedReferralCode = await LocalStorage.GetItemAsync<string>("referral:code"); var storedReferralCode = await LocalStorage.GetItemAsync<string>("referral:code");
if (!string.IsNullOrWhiteSpace(storedReferralCode)) if (!string.IsNullOrWhiteSpace(storedReferralCode))
{ {
@@ -173,24 +191,18 @@ public partial class AuthDialog : IDisposable
if (!validationResult.IsValid) if (!validationResult.IsValid)
{ {
_errorMessage = string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct()); _errorMessage = string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct());
return; return false;
} }
var metadata = await BuildAuthMetadataAsync(); var metadata = await BuildAuthMetadataAsync();
VerifyOtpTokenResponse response; VerifyOtpTokenResponse response = metadata is not null
if (metadata is not null) ? await UserClient.VerifyOtpTokenAsync(_verifyRequest, metadata, cancellationToken: cancellationToken)
{ : await UserClient.VerifyOtpTokenAsync(_verifyRequest, cancellationToken: cancellationToken);
response = await UserClient.VerifyOtpTokenAsync(_verifyRequest, metadata, cancellationToken: cancellationToken);
}
else
{
response = await UserClient.VerifyOtpTokenAsync(_verifyRequest, cancellationToken: cancellationToken);
}
if (response is null) if (response is null)
{ {
_errorMessage = "تأیید رمز پویا انجام نشد. لطفاً دوباره تلاش کنید."; _errorMessage = "تأیید رمز پویا انجام نشد. لطفاً دوباره تلاش کنید.";
return; return false;
} }
if (response.Success) if (response.Success)
@@ -206,16 +218,17 @@ public partial class AuthDialog : IDisposable
await LocalStorage.RemoveItemAsync(PhoneStorageKey); await LocalStorage.RemoveItemAsync(PhoneStorageKey);
await LocalStorage.RemoveItemAsync(RedirectStorageKey); await LocalStorage.RemoveItemAsync(RedirectStorageKey);
// Clear referral code after successful registration/login
await LocalStorage.RemoveItemAsync("referral:code"); await LocalStorage.RemoveItemAsync("referral:code");
_attemptsLeft = MaxVerificationAttempts; _attemptsLeft = MaxVerificationAttempts;
_verifyRequest.Code = string.Empty; _verifyRequest.Code = string.Empty;
await OnLoginSuccess.InvokeAsync(); await OnLoginSuccess.InvokeAsync();
MudDialog.Close(); if (!InlineMode)
return; {
MudDialog?.Close();
}
return true;
} }
RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message); RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message);
@@ -226,6 +239,7 @@ public partial class AuthDialog : IDisposable
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// ignored - user canceled operation
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -237,6 +251,8 @@ public partial class AuthDialog : IDisposable
ClearOperationToken(); ClearOperationToken();
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
return false;
} }
private async Task HandleVerificationFailureAsync(RpcException rpcEx) private async Task HandleVerificationFailureAsync(RpcException rpcEx)
@@ -269,23 +285,15 @@ public partial class AuthDialog : IDisposable
private void RegisterFailedAttempt(string baseMessage) private void RegisterFailedAttempt(string baseMessage)
{ {
_attemptsLeft = Math.Max(0, _attemptsLeft - 1); _attemptsLeft = Math.Max(0, _attemptsLeft - 1);
_errorMessage = _attemptsLeft > 0
if (_attemptsLeft > 0) ? $"{baseMessage} {_attemptsLeft} تلاش باقی مانده است."
{ : $"{baseMessage} تلاش‌های مجاز شما به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
_errorMessage = $"{baseMessage} {_attemptsLeft} تلاش باقی مانده است.";
}
else
{
_errorMessage = $"{baseMessage} تلاش‌های مجاز شما به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
}
} }
private async Task ResendOtpAsync() private async Task ResendOtpAsync()
{ {
if (_resendRemaining > 0 || _isBusy || string.IsNullOrWhiteSpace(_phoneNumber)) if (_resendRemaining > 0 || _isBusy || string.IsNullOrWhiteSpace(_phoneNumber))
{
return; return;
}
_errorMessage = null; _errorMessage = null;
_infoMessage = null; _infoMessage = null;
@@ -294,22 +302,11 @@ public partial class AuthDialog : IDisposable
try try
{ {
var request = new CreateNewOtpTokenRequest var request = new CreateNewOtpTokenRequest { Mobile = _phoneNumber, Purpose = OtpPurpose };
{
Mobile = _phoneNumber,
Purpose = OtpPurpose
};
var metadata = await BuildAuthMetadataAsync(); var metadata = await BuildAuthMetadataAsync();
CreateNewOtpTokenResponse response; CreateNewOtpTokenResponse response = metadata is not null
if (metadata is not null) ? await UserClient.CreateNewOtpTokenAsync(request, metadata, cancellationToken: cancellationToken)
{ : await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: cancellationToken);
response = await UserClient.CreateNewOtpTokenAsync(request, metadata, cancellationToken: cancellationToken);
}
else
{
response = await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: cancellationToken);
}
if (response?.Success != true) if (response?.Success != true)
{ {
@@ -319,10 +316,7 @@ public partial class AuthDialog : IDisposable
return; return;
} }
_infoMessage = string.IsNullOrWhiteSpace(response.Message) _infoMessage = string.IsNullOrWhiteSpace(response.Message) ? "کد جدید ارسال شد." : response.Message;
? "کد جدید ارسال شد."
: response.Message;
_attemptsLeft = MaxVerificationAttempts; _attemptsLeft = MaxVerificationAttempts;
_verifyRequest.Code = string.Empty; _verifyRequest.Code = string.Empty;
StartResendCountdown(); StartResendCountdown();
@@ -333,6 +327,7 @@ public partial class AuthDialog : IDisposable
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// ignored
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -373,6 +368,8 @@ public partial class AuthDialog : IDisposable
_resendTimer?.Dispose(); _resendTimer?.Dispose();
_resendTimer = null; _resendTimer = null;
_resendRemaining = 0; _resendRemaining = 0;
if (EnableCaptcha)
GenerateCaptcha();
} }
private void StartResendCountdown(int seconds = DefaultResendCooldown) private void StartResendCountdown(int seconds = DefaultResendCooldown)
@@ -388,23 +385,15 @@ public partial class AuthDialog : IDisposable
_resendTimer?.Dispose(); _resendTimer?.Dispose();
_resendTimer = null; _resendTimer = null;
} }
_ = InvokeAsync(StateHasChanged); _ = InvokeAsync(StateHasChanged);
}, null, 1000, 1000); }, null, 1000, 1000);
} }
private async Task<Metadata?> BuildAuthMetadataAsync() private async Task<MetadataAlias?> BuildAuthMetadataAsync()
{ {
var token = await LocalStorage.GetItemAsync<string>(TokenStorageKey); var token = await LocalStorage.GetItemAsync<string>(TokenStorageKey);
if (string.IsNullOrWhiteSpace(token)) if (string.IsNullOrWhiteSpace(token)) return null;
{ return new MetadataAlias { { "Authorization", $"Bearer {token}" } };
return null;
}
return new Metadata
{
{ "Authorization", $"Bearer {token}" }
};
} }
private async Task ResetAuthenticationAsync() private async Task ResetAuthenticationAsync()
@@ -430,7 +419,8 @@ public partial class AuthDialog : IDisposable
private void Cancel() private void Cancel()
{ {
MudDialog.Cancel(); if (!InlineMode)
MudDialog?.Close(DialogResult.Cancel());
} }
public void Dispose() public void Dispose()
@@ -438,9 +428,9 @@ public partial class AuthDialog : IDisposable
_operationCts?.Cancel(); _operationCts?.Cancel();
_operationCts?.Dispose(); _operationCts?.Dispose();
_operationCts = null; _operationCts = null;
_resendTimer?.Dispose(); _resendTimer?.Dispose();
_resendTimer = null; _resendTimer = null;
} }
private string GetDialogTitle() => _currentStep == AuthStep.Phone ? "ورود/ثبت‌نام به حساب کاربری" : "تأیید رمز پویا"; private string GetDialogTitle() => _currentStep == AuthStep.Phone ? "ورود/ثبت‌نام به حساب کاربری" : "تأیید رمز پویا";
} }

View File

@@ -9,89 +9,82 @@ public static class CustomMudTheme
PaletteLight = new PaletteLight() PaletteLight = new PaletteLight()
{ {
Primary = "#0380C0", Primary = "#0380C0",
//Secondary = CustomColor.Secondary.Default,
//Tertiary = CustomColor.Tertiary.Default,
Background = "#F5F5F5", Background = "#F5F5F5",
AppbarBackground = "#F5F5F5", AppbarBackground = "#F5F5F5",
//PrimaryContrastText = "#FFFFFF",
//SecondaryContrastText = "#FFFFFF",
//ErrorContrastText = "#FFFFFF",
//SuccessContrastText = "#FFFFFF",
//InfoContrastText = "#FFFFFF",
//WarningContrastText = "#FFFFFF",
TextPrimary = Colors.Gray.Darken3, 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", Surface = "#FFFFFF",
Divider = "#B2BFCB", Divider = "#B2BFCB",
}, },
Typography = new Typography
Typography = new Typography()
{ {
// پایه Default = new DefaultTypography()
Default = new Default()
{ {
FontFamily = new[] { "Vazir", "Tahoma", "Segoe UI", "Arial", "sans-serif" } FontFamily = new[] { "Vazir", "Tahoma", "Segoe UI", "Arial", "sans-serif" }
}, },
H1 = new H1Typography()
// هدینگ‌ها (اسکیل متعادل برای وب)
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
{ {
FontFamily = new[] { "Vazir" }, FontFamily = new[] { "Vazir" }, FontSize = "2rem", LineHeight = "1.70", FontWeight = "800",
FontSize = "0.875rem", LetterSpacing = "normal"
LineHeight = 1.60, },
FontWeight = 600, H2 = new H2Typography()
LetterSpacing = "normal", {
TextTransform = "none" // حروف بزرگ اجباری غیرفعال 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"
} }
} }
}; };

View File

@@ -2,5 +2,5 @@
public static class UrlUtility public static class UrlUtility
{ {
public static string DownloadUrl { get; set; } public static string DownloadUrl { get; set; } = string.Empty; // initialize to avoid null
} }

View File

@@ -5,7 +5,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
src: url(../fonts/Vazir-Light.ttf) format('woff2'); src: url(../fonts/Vazir-Light.ttf) format('truetype');
} }
@font-face { @font-face {
@@ -13,7 +13,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(../fonts/Vazir-Regular.ttf) format('woff2'); src: url(../fonts/Vazir-Regular.ttf) format('truetype');
} }
@font-face { @font-face {
@@ -21,7 +21,7 @@
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
src: url(../fonts/Vazir-Medium.ttf) format('woff2'); src: url(../fonts/Vazir-Medium.ttf) format('truetype');
} }
@font-face { @font-face {
@@ -29,7 +29,7 @@
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url(../fonts/Vazir-Bold.ttf) format('woff2'); src: url(../fonts/Vazir-Bold.ttf) format('truetype');
} }
@font-face { @font-face {
@@ -37,8 +37,58 @@
font-style: normal; font-style: normal;
font-weight: 900; font-weight: 900;
font-display: swap; 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*/ /*#endregion*/
/*#region Layout Styles*/ /*#region Layout Styles*/