From cd835ae6f93485e11ca7660b94d2f353e18ee409 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 26 Jun 2026 16:13:18 +0330 Subject: [PATCH 1/3] feat(wallet): add credit wallet charge functionality and update payment callback - Introduced methods for initiating and verifying credit wallet charges in the WalletService. - Updated PaymentCallback.razor to handle credit wallet charge verification. - Added a button for charging the credit wallet in Wallet.razor. - Updated route constants to include the new credit wallet charge route. These changes enhance the wallet functionality by allowing users to charge their credit wallets and verify transactions seamlessly. --- docs/CUSTOMER-SERVICES.md | 15 ++- src/FrontOffice.Main/FrontOffice.Main.csproj | 2 +- .../Pages/Profile/ChargeCreditWallet.razor | 117 ++++++++++++++++++ .../Pages/Profile/ChargeCreditWallet.razor.cs | 56 +++++++++ .../Pages/Profile/PaymentCallback.razor | 33 +++++ .../Pages/Profile/Wallet.razor | 11 ++ .../Utilities/RouteConstants.cs | 1 + .../Utilities/WalletService.cs | 38 ++++++ 8 files changed, 266 insertions(+), 7 deletions(-) create mode 100644 src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor create mode 100644 src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs diff --git a/docs/CUSTOMER-SERVICES.md b/docs/CUSTOMER-SERVICES.md index a7031de..05ff8bd 100644 --- a/docs/CUSTOMER-SERVICES.md +++ b/docs/CUSTOMER-SERVICES.md @@ -67,10 +67,12 @@ | 4.5 | `WalletService.VerifyMagicChargeAsync` | `UserWalletContract.VerifyMagicCharge` | `/profile/payment-callback?type=magic-wallet` | تأیید شارژ Magic پس از درگاه | | 4.6 | `WalletService.InitiateDiscountChargeAsync` | `UserWalletContract.InitiateDiscountCharge` | `/profile/charge-discount-wallet` | شروع شارژ کیف پول تخفیفی → درگاه | | 4.7 | `WalletService.VerifyDiscountChargeAsync` | `UserWalletContract.VerifyDiscountCharge` | `/profile/payment-callback?type=discount-wallet` | تأیید شارژ کیف تخفیفی | -| 4.8 | `WalletService.GetWithdrawalsAsync` | `UserWalletContract.GetCustomerWithdrawals` | `/profile/withdrawal-requests` | لیست درخواست‌های برداشت | -| 4.9 | `WalletService.GetWithdrawalSettingsAsync` | `UserWalletContract.GetCustomerWithdrawalSettings` | `/profile/withdrawal-requests` | حداقل مبلغ برداشت | -| 4.10 | `WalletService.RequestWithdrawalAsync` | `UserWalletContract.CustomerWithdrawBalance` | `/profile/withdrawal-requests` | ثبت درخواست برداشت پاداش (با payoutId کمیسیون) | -| 4.11 | `CommissionService.GetWithdrawablePayoutsAsync` | `CommissionContract.GetMyCommissionPayouts` | `/profile/withdrawal-requests` | پاداش‌های قابل برداشت برای انتخاب | +| 4.8 | `WalletService.InitiateCreditChargeAsync` | `UserWalletContract.InitiateCreditCharge` | `/profile/charge-credit-wallet` | شروع شارژ کیف پول اصلی → درگاه | +| 4.9 | `WalletService.VerifyCreditChargeAsync` | `UserWalletContract.VerifyCreditCharge` | `/profile/payment-callback?type=credit-wallet` | تأیید شارژ کیف اصلی | +| 4.10 | `WalletService.GetWithdrawalsAsync` | `UserWalletContract.GetCustomerWithdrawals` | `/profile/withdrawal-requests` | لیست درخواست‌های برداشت | +| 4.11 | `WalletService.GetWithdrawalSettingsAsync` | `UserWalletContract.GetCustomerWithdrawalSettings` | `/profile/withdrawal-requests` | حداقل مبلغ برداشت | +| 4.12 | `WalletService.RequestWithdrawalAsync` | `UserWalletContract.CustomerWithdrawBalance` | `/profile/withdrawal-requests` | ثبت درخواست برداشت پاداش (با payoutId کمیسیون) | +| 4.13 | `CommissionService.GetWithdrawablePayoutsAsync` | `CommissionContract.GetMyCommissionPayouts` | `/profile/withdrawal-requests` | پاداش‌های قابل برداشت برای انتخاب | --- @@ -81,8 +83,9 @@ | 5.1 | `PackageContract.CustomerVerifyPackagePurchase` | `PackageContract` | `/profile/payment-callback` (type=package) | تأیید خرید پکیج | | 5.2 | `WalletService.VerifyMagicChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=magic-wallet) | تأیید شارژ Magic | | 5.3 | `WalletService.VerifyDiscountChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=discount-wallet) | تأیید شارژ تخفیفی | -| 5.4 | `DiscountOrderService.VerifyDiscountOrderPaymentAsync` | `DiscountOrderContract.CustomerVerifyDiscountOrderPayment` | `/profile/payment-callback` (type=discount-order) | تأیید پرداخت سفارش فروشگاه تخفیفی | -| 5.5 | `UserContract.GetUserForCustomer` + `AuthService.InitUserAuthInfo` | `UserContract` | `/profile/payment-callback` | به‌روزرسانی claims کاربر پس از پرداخت موفق | +| 5.4 | `WalletService.VerifyCreditChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=credit-wallet) | تأیید شارژ کیف اصلی | +| 5.5 | `DiscountOrderService.VerifyDiscountOrderPaymentAsync` | `DiscountOrderContract.CustomerVerifyDiscountOrderPayment` | `/profile/payment-callback` (type=discount-order) | تأیید پرداخت سفارش فروشگاه تخفیفی | +| 5.6 | `UserContract.GetUserForCustomer` + `AuthService.InitUserAuthInfo` | `UserContract` | `/profile/payment-callback` | به‌روزرسانی claims کاربر پس از پرداخت موفق | --- diff --git a/src/FrontOffice.Main/FrontOffice.Main.csproj b/src/FrontOffice.Main/FrontOffice.Main.csproj index a2e702d..ca06319 100644 --- a/src/FrontOffice.Main/FrontOffice.Main.csproj +++ b/src/FrontOffice.Main/FrontOffice.Main.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor new file mode 100644 index 0000000..6d30f81 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor @@ -0,0 +1,117 @@ +@attribute [Route(RouteConstants.Profile.ChargeCreditWallet)] +@attribute [Authorize] + +شارژ کیف‌پول اصلی + + + + + + @if (_paymentResult != null) + { + + @if (_paymentResult == "success") + { + شارژ کیف پول اصلی با موفقیت انجام شد! ✅ + } + else if (_paymentResult == "cancelled") + { + پرداخت توسط شما لغو شد. + } + else + { + پرداخت ناموفق بود. لطفاً دوباره تلاش کنید. + } + + } + + + + + شارژ موجودی اصلی + + + + مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اصلی شما اضافه می‌شود. + از موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش استفاده می‌شود. + + + + + + + @foreach (var preset in _presetAmounts) + { + + @FormatToman(preset) + + } + + + @if (_chargeAmount > 0) + { + + + مبلغ پرداخت: + @FormatToman(_chargeAmount) + + + موجودی دریافتی: + @FormatToman(_chargeAmount) + + + } + + + @if (_isProcessing) + { + + در حال انتقال به درگاه... + } + else + { + پرداخت با کارت بانکی + } + + + + + + + + + موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش قابل استفاده است + + + حداقل مبلغ شارژ: ۱۰,۰۰۰ تومان + + + مبلغ واریزی بدون ضریب به موجودی اضافه می‌شود (۱:۱) + + + پرداخت از طریق درگاه بانکی انجام می‌شود + + + + + + diff --git a/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs new file mode 100644 index 0000000..7ab98cb --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs @@ -0,0 +1,56 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Profile; + +public partial class ChargeCreditWallet : ComponentBase +{ + private bool _isProcessing; + private long _chargeAmount; + private string? _paymentResult; + + private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 }; + + [SupplyParameterFromQuery(Name = "payment")] + public string? PaymentQueryParam { get; set; } + + protected override void OnInitialized() + { + _paymentResult = PaymentQueryParam; + } + + private async Task StartCreditCharge() + { + if (_chargeAmount <= 0 || _isProcessing) return; + + _isProcessing = true; + StateHasChanged(); + + try + { + var (success, gatewayUrl, error) = await WalletService.InitiateCreditChargeAsync(_chargeAmount); + + if (success && !string.IsNullOrEmpty(gatewayUrl)) + { + Navigation.NavigateTo(gatewayUrl, forceLoad: true); + } + else + { + Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + } + finally + { + _isProcessing = false; + StateHasChanged(); + } + } + + private static string FormatToman(long toman) + => string.Format("{0:N0} تومان", toman); +} diff --git a/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor b/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor index 25cf450..c70178f 100644 --- a/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor +++ b/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor @@ -143,6 +143,9 @@ case "discount-wallet": await VerifyDiscountWalletCharge(); break; + case "credit-wallet": + await VerifyCreditWalletCharge(); + break; case "discount-order": await VerifyDiscountOrderPayment(); break; @@ -254,6 +257,35 @@ await UpdateWalletBalance(); } } + + /// + /// تأیید شارژ کیف پول اصلی + /// + private async Task VerifyCreditWalletCharge() + { + _successReturnUrl = "/profile/charge-credit-wallet?payment=success"; + _successReturnText = "بازگشت به شارژ کیف پول اصلی"; + _failReturnUrl = "/profile/charge-credit-wallet?payment=failed"; + _failReturnText = "بازگشت به شارژ کیف پول اصلی"; + + if (string.IsNullOrEmpty(Authority)) + { + _isSuccess = false; + _message = "کد Authority نامعتبر است"; + return; + } + + var (success, message) = await WalletService.VerifyCreditChargeAsync( + Authority, Status ?? "NOK"); + + _isSuccess = success; + _message = message; + + if (_isSuccess) + { + await UpdateWalletBalance(); + } + } /// /// تأیید پرداخت سفارش فروشگاه تخفیفی @@ -308,6 +340,7 @@ { "magic-wallet" => "/profile/magic-wallet", "discount-wallet" => "/profile/charge-discount-wallet", + "credit-wallet" => "/profile/charge-credit-wallet", "discount-order" => "/discount-store", _ => "/profile" }; diff --git a/src/FrontOffice.Main/Pages/Profile/Wallet.razor b/src/FrontOffice.Main/Pages/Profile/Wallet.razor index 219c273..6670c39 100644 --- a/src/FrontOffice.Main/Pages/Profile/Wallet.razor +++ b/src/FrontOffice.Main/Pages/Profile/Wallet.razor @@ -38,6 +38,17 @@ درخواست‌های برداشت + + + شارژ کیف‌پول اصلی + + InitiateCreditChargeAsync(long amount) + { + try + { + var response = await _client.InitiateCreditChargeAsync(new InitiateCreditChargeRequest + { + Amount = amount + }); + + if (response.IsSuccess) + return (true, response.GatewayUrl, null); + + return (false, null, response.ErrorMessage); + } + catch (Grpc.Core.RpcException ex) + { + return (false, null, ex.Status.Detail ?? "خطا در ارتباط با سرور"); + } + } + // ============= Wallet Verify Methods ============= public async Task<(bool Success, string Message)> VerifyMagicChargeAsync(string authority, string status) @@ -304,6 +324,24 @@ public class WalletService } } + public async Task<(bool Success, string Message)> VerifyCreditChargeAsync(string authority, string status) + { + try + { + var response = await _client.VerifyCreditChargeAsync(new VerifyWalletChargeRequest + { + Authority = authority, + Status = status + }); + + return (response.Success, response.Message); + } + catch (Grpc.Core.RpcException ex) + { + return (false, ex.Status.Detail ?? "خطا در بررسی وضعیت پرداخت"); + } + } + public int GetWalletMode() { try From d44e99f4ab909c3743c2575683c128aacb4b13c4 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 26 Jun 2026 18:17:19 +0330 Subject: [PATCH 2/3] feat(profile): enhance ChargeCreditWallet with return URL handling and continue shopping button - Added support for return URL handling in ChargeCreditWallet to improve user experience after payment. - Introduced a "Continue Shopping" button that allows users to navigate back to their previous shopping session after successfully charging their wallet. - Updated the component to manage pending return URLs and ensure they are saved and retrieved correctly. These changes enhance the functionality of the ChargeCreditWallet page, providing a smoother transition for users post-transaction. --- .../Pages/Profile/ChargeCreditWallet.razor | 11 +++- .../Pages/Profile/ChargeCreditWallet.razor.cs | 42 +++++++++++++ .../Pages/Store/CheckoutSummary.razor.cs | 60 +++++++++++++++--- .../Shared/InsufficientCreditDialog.razor | 61 +++++++++++++++++++ .../Utilities/CreditChargeNavigation.cs | 50 +++++++++++++++ 5 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 src/FrontOffice.Main/Shared/InsufficientCreditDialog.razor create mode 100644 src/FrontOffice.Main/Utilities/CreditChargeNavigation.cs diff --git a/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor index 6d30f81..611ecab 100644 --- a/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor +++ b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor @@ -13,7 +13,16 @@ Class="rounded-lg" Variant="Variant.Filled"> @if (_paymentResult == "success") { - شارژ کیف پول اصلی با موفقیت انجام شد! ✅ + + شارژ کیف پول اصلی با موفقیت انجام شد! ✅ + + ادامه خرید + + } else if (_paymentResult == "cancelled") { diff --git a/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs index 7ab98cb..0cd1561 100644 --- a/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs @@ -1,5 +1,6 @@ using FrontOffice.Main.Utilities; using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; using MudBlazor; namespace FrontOffice.Main.Pages.Profile; @@ -9,15 +10,38 @@ public partial class ChargeCreditWallet : ComponentBase private bool _isProcessing; private long _chargeAmount; private string? _paymentResult; + private string? _pendingReturnUrl; private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 }; [SupplyParameterFromQuery(Name = "payment")] public string? PaymentQueryParam { get; set; } + [SupplyParameterFromQuery(Name = "amount")] + public long? AmountQueryParam { get; set; } + + [SupplyParameterFromQuery(Name = "returnUrl")] + public string? ReturnUrlQueryParam { get; set; } + protected override void OnInitialized() { _paymentResult = PaymentQueryParam; + + if (AmountQueryParam is > 0) + _chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(AmountQueryParam.Value); + + if (CreditChargeNavigation.IsValidReturnUrl(ReturnUrlQueryParam)) + _pendingReturnUrl = ReturnUrlQueryParam; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl)) + { + await CreditChargeNavigation.SaveReturnUrlAsync(jsRuntime, _pendingReturnUrl); + } + + await base.OnAfterRenderAsync(firstRender); } private async Task StartCreditCharge() @@ -29,6 +53,15 @@ public partial class ChargeCreditWallet : ComponentBase try { + if (!string.IsNullOrEmpty(_pendingReturnUrl)) + await CreditChargeNavigation.SaveReturnUrlAsync(jsRuntime, _pendingReturnUrl); + else + { + var storedReturnUrl = await CreditChargeNavigation.GetReturnUrlAsync(jsRuntime); + if (CreditChargeNavigation.IsValidReturnUrl(storedReturnUrl)) + _pendingReturnUrl = storedReturnUrl; + } + var (success, gatewayUrl, error) = await WalletService.InitiateCreditChargeAsync(_chargeAmount); if (success && !string.IsNullOrEmpty(gatewayUrl)) @@ -51,6 +84,15 @@ public partial class ChargeCreditWallet : ComponentBase } } + private async Task ContinueShoppingAsync() + { + var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(jsRuntime); + await CreditChargeNavigation.ClearReturnUrlAsync(jsRuntime); + + if (CreditChargeNavigation.IsValidReturnUrl(returnUrl)) + Navigation.NavigateTo(returnUrl!); + } + private static string FormatToman(long toman) => string.Format("{0:N0} تومان", toman); } diff --git a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs index 6a50925..3841807 100644 --- a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs @@ -1,5 +1,6 @@ using CMSMicroservice.Protobuf.Protos.UserAddress; using CMSMicroservice.Protobuf.Protos.UserOrder; +using FrontOffice.Main.Shared; using FrontOffice.Main.Utilities; using Microsoft.AspNetCore.Components; using MudBlazor; @@ -33,7 +34,6 @@ public partial class CheckoutSummary : ComponentBase { await AuthDialogService.ShowAuthDialogAsync(); } - // لود سبد خرید (فقط اگر کاربر لاگین کرده باشد) await Cart.EnsureInitializedAsync(); await LoadAddresses(); await LoadWalletBalance(); @@ -43,7 +43,6 @@ public partial class CheckoutSummary : ComponentBase { if (firstRender) { - // بارگذاری نرخ VAT await VAT.LoadAsync(); } await base.OnAfterRenderAsync(firstRender); @@ -52,9 +51,7 @@ public partial class CheckoutSummary : ComponentBase private async Task LoadWalletBalance() { var walletResult = await WalletService.GetBalancesAsync(); - walletBalance = walletResult.CreditBalance - // + walletResult.NetworkBalance - ; + walletBalance = walletResult.CreditBalance; } private async Task LoadAddresses() @@ -93,11 +90,15 @@ public partial class CheckoutSummary : ComponentBase return; } + var totalRequired = VAT.AddVAT(Cart.Total); + if (await TryHandleInsufficientBalanceAsync(totalRequired)) + return; + try { var request = new SubmitShopBuyOrderRequest { - TotalAmount = VAT.AddVAT(Cart.Total) + TotalAmount = totalRequired }; var response = await UserOrderContract.SubmitShopBuyOrderAsync(request); @@ -107,17 +108,56 @@ public partial class CheckoutSummary : ComponentBase } catch (Exception ex) { + if (CreditChargeNavigation.IsInsufficientWalletBalance(ex)) + { + await LoadWalletBalance(); + await TryHandleInsufficientBalanceAsync(totalRequired); + return; + } + Snackbar.Add($"خطا در ثبت سفارش: {ex.Message}", Severity.Error); } } + private async Task TryHandleInsufficientBalanceAsync(long totalRequired) + { + if (walletBalance >= totalRequired) + return false; + + var shortfall = totalRequired - walletBalance; + var parameters = new DialogParameters + { + { x => x.CurrentBalance, walletBalance }, + { x => x.RequiredAmount, totalRequired }, + { x => x.ShortfallAmount, shortfall } + }; + + var dialog = await DialogService.ShowAsync( + "موجودی کافی نیست", + parameters, + new DialogOptions + { + CloseOnEscapeKey = true, + MaxWidth = MaxWidth.Small, + FullWidth = true + }); + + var result = await dialog.Result; + if (result is { Canceled: false, Data: long chargeShortfall }) + { + var chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(chargeShortfall); + Navigation.NavigateTo(CreditChargeNavigation.BuildChargeUrl( + chargeAmount, + RouteConstants.Store.CheckoutSummary)); + } + + return true; + } + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); - /// - /// محاسبه مالیات بر ارزش افزوده - /// private long CalculateVAT() => VAT.CalculateVAT(Cart.Total); private static string GetProductImageUrl(string? imageUrl) => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/'); -} \ No newline at end of file +} diff --git a/src/FrontOffice.Main/Shared/InsufficientCreditDialog.razor b/src/FrontOffice.Main/Shared/InsufficientCreditDialog.razor new file mode 100644 index 0000000..da14999 --- /dev/null +++ b/src/FrontOffice.Main/Shared/InsufficientCreditDialog.razor @@ -0,0 +1,61 @@ + + + + + موجودی کیف پول اصلی کافی نیست + + + + + + برای تکمیل این سفارش، موجودی اصلی شما کافی نیست. می‌توانید کیف پول را شارژ کنید و سپس سفارش را ثبت کنید. + + + + + موجودی فعلی: + @FormatPrice(CurrentBalance) + + + مبلغ سفارش: + @FormatPrice(RequiredAmount) + + + + کمبود: + @FormatPrice(ShortfallAmount) + + + + + + + بستن + + شارژ حساب اصلی + + + + +@code { + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] + public long CurrentBalance { get; set; } + + [Parameter] + public long RequiredAmount { get; set; } + + [Parameter] + public long ShortfallAmount { get; set; } + + private void Cancel() => MudDialog.Cancel(); + + private void GoToCharge() => MudDialog.Close(DialogResult.Ok(ShortfallAmount)); + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); +} diff --git a/src/FrontOffice.Main/Utilities/CreditChargeNavigation.cs b/src/FrontOffice.Main/Utilities/CreditChargeNavigation.cs new file mode 100644 index 0000000..0cc0bbf --- /dev/null +++ b/src/FrontOffice.Main/Utilities/CreditChargeNavigation.cs @@ -0,0 +1,50 @@ +using Grpc.Core; +using Microsoft.JSInterop; + +namespace FrontOffice.Main.Utilities; + +public static class CreditChargeNavigation +{ + public const long MinChargeAmount = 10_000; + public const string ReturnUrlStorageKey = "credit_charge_return_url"; + private const string InsufficientBalanceMarker = "موجودی کیف پول کافی نیست"; + + public static long NormalizeChargeAmount(long shortfall) => + Math.Max(shortfall, MinChargeAmount); + + public static string BuildChargeUrl(long amount, string? returnUrl) + { + var url = $"{RouteConstants.Profile.ChargeCreditWallet}?amount={amount}"; + if (IsValidReturnUrl(returnUrl)) + url += $"&returnUrl={Uri.EscapeDataString(returnUrl!)}"; + return url; + } + + public static bool IsValidReturnUrl(string? url) => + !string.IsNullOrWhiteSpace(url) + && url.StartsWith('/') + && !url.StartsWith("//", StringComparison.Ordinal); + + public static bool IsInsufficientWalletBalanceMessage(string? message) => + message?.Contains(InsufficientBalanceMarker, StringComparison.Ordinal) == true; + + public static bool IsInsufficientWalletBalance(Exception ex) => + ex switch + { + RpcException rpc => IsInsufficientWalletBalanceMessage(rpc.Status.Detail) + || IsInsufficientWalletBalanceMessage(rpc.Message), + _ => IsInsufficientWalletBalanceMessage(ex.Message) + }; + + public static async Task SaveReturnUrlAsync(IJSRuntime js, string returnUrl) + { + if (!IsValidReturnUrl(returnUrl)) return; + await js.InvokeVoidAsync("sessionStorage.setItem", ReturnUrlStorageKey, returnUrl); + } + + public static async Task GetReturnUrlAsync(IJSRuntime js) => + await js.InvokeAsync("sessionStorage.getItem", ReturnUrlStorageKey); + + public static async Task ClearReturnUrlAsync(IJSRuntime js) => + await js.InvokeVoidAsync("sessionStorage.removeItem", ReturnUrlStorageKey); +} From 81d139cdab9fc2a3a44f748a62bd24873ecd94f6 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 26 Jun 2026 18:47:13 +0330 Subject: [PATCH 3/3] fix(profile): standardize JSRuntime usage in ChargeCreditWallet component - Updated references to `jsRuntime` to `JSRuntime` for consistency across the ChargeCreditWallet component. - Improved the clarity of the code by ensuring uniform naming conventions in the handling of return URLs. These changes enhance code readability and maintainability in the ChargeCreditWallet functionality. --- .../Pages/Profile/ChargeCreditWallet.razor.cs | 10 +++++----- .../Pages/Store/CheckoutSummary.razor.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs index 0cd1561..4237af7 100644 --- a/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/ChargeCreditWallet.razor.cs @@ -38,7 +38,7 @@ public partial class ChargeCreditWallet : ComponentBase { if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl)) { - await CreditChargeNavigation.SaveReturnUrlAsync(jsRuntime, _pendingReturnUrl); + await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl); } await base.OnAfterRenderAsync(firstRender); @@ -54,10 +54,10 @@ public partial class ChargeCreditWallet : ComponentBase try { if (!string.IsNullOrEmpty(_pendingReturnUrl)) - await CreditChargeNavigation.SaveReturnUrlAsync(jsRuntime, _pendingReturnUrl); + await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl); else { - var storedReturnUrl = await CreditChargeNavigation.GetReturnUrlAsync(jsRuntime); + var storedReturnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime); if (CreditChargeNavigation.IsValidReturnUrl(storedReturnUrl)) _pendingReturnUrl = storedReturnUrl; } @@ -86,8 +86,8 @@ public partial class ChargeCreditWallet : ComponentBase private async Task ContinueShoppingAsync() { - var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(jsRuntime); - await CreditChargeNavigation.ClearReturnUrlAsync(jsRuntime); + var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime); + await CreditChargeNavigation.ClearReturnUrlAsync(JSRuntime); if (CreditChargeNavigation.IsValidReturnUrl(returnUrl)) Navigation.NavigateTo(returnUrl!); diff --git a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs index 3841807..19805ec 100644 --- a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs @@ -143,7 +143,7 @@ public partial class CheckoutSummary : ComponentBase }); var result = await dialog.Result; - if (result is { Canceled: false, Data: long chargeShortfall }) + if (result is { Canceled: false } && result.Data is long chargeShortfall) { var chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(chargeShortfall); Navigation.NavigateTo(CreditChargeNavigation.BuildChargeUrl(