From 3af564be737a8ee8d3e69282ee497e01620a5c7c Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Tue, 16 Dec 2025 02:33:56 +0330 Subject: [PATCH] feat: add payment callback and referral link features --- src/FrontOffice.Main/FrontOffice.Main.csproj | 7 +- .../Components/ActivationSection.razor.cs | 16 ++ .../Pages/Profile/Index.razor | 155 +++++++++++++--- .../Pages/Profile/Index.razor.cs | 96 +++++++++- .../Pages/Profile/PaymentCallback.razor | 174 ++++++++++++++++++ src/FrontOffice.Main/Utilities/AuthService.cs | 7 + .../Utilities/UserAuthInfo.cs | 3 + 7 files changed, 425 insertions(+), 33 deletions(-) create mode 100644 src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor diff --git a/src/FrontOffice.Main/FrontOffice.Main.csproj b/src/FrontOffice.Main/FrontOffice.Main.csproj index 2431952..ed49ccd 100644 --- a/src/FrontOffice.Main/FrontOffice.Main.csproj +++ b/src/FrontOffice.Main/FrontOffice.Main.csproj @@ -14,7 +14,7 @@ - + @@ -34,6 +34,11 @@ + + + + + diff --git a/src/FrontOffice.Main/Pages/Club/Components/ActivationSection.razor.cs b/src/FrontOffice.Main/Pages/Club/Components/ActivationSection.razor.cs index dd7391c..2a9b0e3 100644 --- a/src/FrontOffice.Main/Pages/Club/Components/ActivationSection.razor.cs +++ b/src/FrontOffice.Main/Pages/Club/Components/ActivationSection.razor.cs @@ -1,4 +1,5 @@ using FrontOffice.Main.Utilities; +using FrontOffice.BFF.User.Protobuf.Protos.User; using Microsoft.AspNetCore.Components; using MudBlazor; @@ -7,6 +8,8 @@ namespace FrontOffice.Main.Pages.Club.Components; public partial class ActivationSection : ComponentBase { [Inject] private ClubMembershipService ClubService { get; set; } = default!; + [Inject] private AuthService AuthService { get; set; } = default!; + [Inject] private UserContract.UserContractClient UserContract { get; set; } = default!; [Parameter] public EventCallback OnActivationSuccess { get; set; } @@ -39,6 +42,19 @@ public partial class ActivationSection : ComponentBase if (result.Success) { Snackbar.Add($"عضویت فعال شد! مبلغ پرداختی: {result.AmountPaid:N0} تومان", Severity.Success); + + // Refresh Token to update Claims (IsClubMemberActive, HasPurchasedPackage) + try + { + var userResponse = await UserContract.GetUserAsync(new()); + await AuthService.InitUserAuthInfo(); + } + catch + { + // Token refresh failed but activation succeeded + Snackbar.Add("لطفاً مجدداً وارد شوید تا تغییرات اعمال شود.", Severity.Warning); + } + await OnActivationSuccess.InvokeAsync(); } else diff --git a/src/FrontOffice.Main/Pages/Profile/Index.razor b/src/FrontOffice.Main/Pages/Profile/Index.razor index 922c64e..16783bc 100644 --- a/src/FrontOffice.Main/Pages/Profile/Index.razor +++ b/src/FrontOffice.Main/Pages/Profile/Index.razor @@ -21,43 +21,74 @@ - تامین اعتبار خرید + @* تامین اعتبار *@ - - - کد دعوت شما - - - اشتراک‌گذاری - - - - این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید. - + + @if (CanShowReferralLink) + { + + + کد دعوت شما + + + اشتراک‌گذاری + + + + این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید. + - - - + + + - @if (!string.IsNullOrWhiteSpace(_copyMessage)) - { - @(_copyMessage) - } - + @if (!string.IsNullOrWhiteSpace(_copyMessage)) + { + @(_copyMessage) + } + + } + else + { + + + + + لینک دعوت شما هنوز فعال نشده است + + + برای فعال‌سازی لینک دعوت و دعوت دوستان خود به جمع مشتریان ویژه کارابازار، کافی است ابتدا پکیج پایه ۵۶ میلیونی را تهیه کنید و سپس در باشگاه مشتریان عضو شوید. + + + پس از تکمیل این مراحل، می‌توانید از مزایای ویژه عضویت در شبکه فروش بهره‌مند شده و با معرفی دوستان، کمیسیون دریافت کنید. + + + شروع فرآیند تامین اعتبار + + + + } @@ -199,3 +230,67 @@ + + + + + + + خرید پکیج پایه ۵۶ میلیون تومان + + + + + + برای خرید پکیج پایه، یکی از روش‌های زیر را انتخاب کنید: + + + + + + + + پرداخت مستقیم + + + پرداخت آنی از طریق درگاه بانکی + + + پرداخت با کارت بانکی + + + + + + + + + + اعتبار الماسی دایا + + + تأمین اعتبار از طریق خرید توکن الماس و دریافت وام + + + تأمین اعتبار الماسی + + + + + + بستن + + + + diff --git a/src/FrontOffice.Main/Pages/Profile/Index.razor.cs b/src/FrontOffice.Main/Pages/Profile/Index.razor.cs index e63b83f..e7e84a7 100644 --- a/src/FrontOffice.Main/Pages/Profile/Index.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Index.razor.cs @@ -3,6 +3,7 @@ using FrontOffice.BFF.User.Protobuf.Protos.User; using FrontOffice.BFF.User.Protobuf.Validator; using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress; using FrontOffice.BFF.UserAddress.Protobuf.Validator; +using FrontOffice.BFF.Package.Protobuf.Protos.Package; using FrontOffice.Main.Pages.Profile.Components; using FrontOffice.Main.Utilities; using Mapster; @@ -18,6 +19,8 @@ public partial class Index { [Inject] private UserContract.UserContractClient UserContract { get; set; } = default!; [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; + [Inject] private PackageContract.PackageContractClient PackageContract { get; set; } = default!; + [Inject] private AuthService AuthService { get; set; } = default!; private GetUserResponse _userProfile = new(); private UpdateUserRequest _updateUserRequest = new(); @@ -32,6 +35,15 @@ public partial class Index private DateTime? _date; + // Referral Link Control + private bool _hasPurchasedPackage; + private bool _isClubMemberActive; + private bool CanShowReferralLink => _hasPurchasedPackage && _isClubMemberActive; + + // Purchase Options Bottom Sheet + private bool _showPurchaseBottomSheet; + private bool _isProcessingPayment; + // Address management private List _addresses = new(); private bool _isLoadingAddresses; @@ -44,12 +56,29 @@ public partial class Index await base.OnAfterRenderAsync(firstRender); if (firstRender) { + await LoadUserAuthInfo(); await LoadUserProfile(); await LoadAddresses(); await LoadWallet(); } } + private async Task LoadUserAuthInfo() + { + try + { + var userInfo = await AuthService.GetUserAuthInfo(); + _hasPurchasedPackage = userInfo.HasPurchasedPackage; + _isClubMemberActive = userInfo.IsClubMemberActive; + } + catch + { + _hasPurchasedPackage = false; + _isClubMemberActive = false; + } + StateHasChanged(); + } + private string _walletCredit = "-"; private string _walletDiscount = "-"; private string _walletNetwork = "-"; @@ -272,33 +301,96 @@ public partial class Index } } - private async Task Financing() + private void OpenPurchaseOptions() { + _showPurchaseBottomSheet = true; + } + + private void ClosePurchaseOptions() + { + _showPurchaseBottomSheet = false; + } + + private async Task DirectPayment() + { + if (_isProcessingPayment) return; + + _isProcessingPayment = true; + _showPurchaseBottomSheet = false; + + try + { + // Create callback URL for payment verification + var callbackUrl = $"{Navigation.BaseUri}profile/payment-callback"; + + // Call BFF to initiate payment (UserId is taken from JWT token in BFF) + var response = await PackageContract.InitiateBasePackagePaymentAsync(new InitiateBasePackagePaymentRequest + { + CallbackUrl = callbackUrl + }); + + if (!response.Success) + { + Snackbar.Add(response.Message ?? "خطا در آغاز فرآیند پرداخت", Severity.Error); + return; + } + + // Redirect to payment gateway + if (!string.IsNullOrEmpty(response.PaymentGatewayUrl)) + { + Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true); + } + else + { + Snackbar.Add("خطا در دریافت آدرس درگاه پرداخت", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"خطا در فرآیند پرداخت: {ex.Message}", Severity.Error); + } + finally + { + _isProcessingPayment = false; + await InvokeAsync(StateHasChanged); + } + } + + private async Task DayaLoanPayment() + { + // Validate user info before redirecting if (string.IsNullOrWhiteSpace(_updateUserRequest.FirstName)) { + _showPurchaseBottomSheet = false; Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام وارد نشده)", Severity.Error); return; } if (string.IsNullOrWhiteSpace(_updateUserRequest.LastName)) { + _showPurchaseBottomSheet = false; Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام خانوادگی وارد نشده)", Severity.Error); return; } if (string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode)) { + _showPurchaseBottomSheet = false; Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (کدملی وارد نشده)", Severity.Error); return; } if (_updateUserRequest.BirthDate == null) { + _showPurchaseBottomSheet = false; Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error); return; } if (!_addresses.Any()) { - Snackbar.Add($"آدرس محل سکونت شما الزمای است!", Severity.Error); + _showPurchaseBottomSheet = false; + Snackbar.Add($"آدرس محل سکونت شما الزامی است!", Severity.Error); return; } + + _showPurchaseBottomSheet = false; var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364"; await JSRuntime.InvokeVoidAsync("open", url, "_blank"); } diff --git a/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor b/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor new file mode 100644 index 0000000..7aad246 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor @@ -0,0 +1,174 @@ +@page "/profile/payment-callback" +@attribute [Authorize] +@using Blazored.LocalStorage +@using FrontOffice.BFF.Package.Protobuf.Protos.Package +@using FrontOffice.BFF.User.Protobuf.Protos.User + +نتیجه پرداخت | کارا بازار سلامت + + + + @if (_isLoading) + { + + در حال بررسی وضعیت پرداخت... + } + else if (_isSuccess) + { + + پرداخت موفق + @_message + + @if (!string.IsNullOrEmpty(_refId)) + { + + کد پیگیری: @_refId + + } + + + + + موجودی کیف پول: @FormatPrice(_walletBalance) + + + موجودی اعتبار تخفیف: @FormatPrice(_discountBalance) + + + + بازگشت به پروفایل + + } + else + { + + پرداخت ناموفق + @_message + + + بازگشت به پروفایل + + + + تلاش مجدد + + } + + + +@code { + [Inject] private PackageContract.PackageContractClient PackageContractClient { get; set; } = default!; + [Inject] private UserContract.UserContractClient UserContractClient { get; set; } = default!; + [Inject] private ILocalStorageService LocalStorage { get; set; } = default!; + [Inject] private AuthService AuthService { get; set; } = default!; + [Inject] private NavigationManager NavManager { get; set; } = default!; + + private const string TokenStorageKey = "auth:token"; + + [SupplyParameterFromQuery(Name = "orderId")] + private long OrderId { get; set; } + + [SupplyParameterFromQuery(Name = "transactionId")] + private long TransactionId { get; set; } + + [SupplyParameterFromQuery(Name = "Authority")] + private string? Authority { get; set; } + + [SupplyParameterFromQuery(Name = "Status")] + private string? Status { get; set; } + + private bool _isLoading = true; + private bool _isSuccess; + private string _message = string.Empty; + private string? _refId; + private long _walletBalance; + private long _discountBalance; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await VerifyPayment(); + } + } + + private async Task VerifyPayment() + { + try + { + if (OrderId <= 0 || TransactionId <= 0 || string.IsNullOrEmpty(Authority)) + { + _isSuccess = false; + _message = "پارامترهای پرداخت نامعتبر است"; + _isLoading = false; + StateHasChanged(); + return; + } + + var response = await PackageContractClient.VerifyBasePackagePaymentAsync(new VerifyBasePackagePaymentRequest + { + OrderId = OrderId, + TransactionId = TransactionId, + Authority = Authority, + Status = Status ?? "NOK" + }); + + _isSuccess = response.Success; + _message = response.Message; + _refId = response.RefId; + _walletBalance = response.WalletBalance; + _discountBalance = response.DiscountBalance; + + // اگر پرداخت موفق بود، توکن را refresh می‌کنیم + // چون PackagePurchaseMethod تغییر کرده و باید در claims جدید باشد + if (_isSuccess) + { + await RefreshTokenAsync(); + } + } + catch (Exception ex) + { + _isSuccess = false; + _message = $"خطا در بررسی وضعیت پرداخت: {ex.Message}"; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private void RetryPayment() + { + NavManager.NavigateTo("/profile"); + } + + /// + /// Refresh token after successful payment to update PackagePurchaseMethod claim + /// + private async Task RefreshTokenAsync() + { + try + { + var userResponse = await UserContractClient.GetUserAsync(new Google.Protobuf.WellKnownTypes.Empty()); + if (!string.IsNullOrWhiteSpace(userResponse.Token)) + { + // ذخیره توکن جدید در localStorage + await LocalStorage.SetItemAsync(TokenStorageKey, userResponse.Token); + + // به‌روزرسانی UserAuthInfo با claims جدید + await AuthService.InitUserAuthInfo(); + } + } + catch + { + // اگر refresh ناموفق بود، پرداخت همچنان موفق است + // کاربر می‌تواند با logout/login دوباره token جدید بگیرد + } + } + + private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); +} diff --git a/src/FrontOffice.Main/Utilities/AuthService.cs b/src/FrontOffice.Main/Utilities/AuthService.cs index 8aa08a9..3eaac1a 100644 --- a/src/FrontOffice.Main/Utilities/AuthService.cs +++ b/src/FrontOffice.Main/Utilities/AuthService.cs @@ -60,16 +60,23 @@ public class AuthService { var claims = await GetTokenClaimsAsync(); _userAuthInfo.UserClaims= claims; + var userIdStr = claims.FirstOrDefault(c => c.key == "UserId" || c.key == "sub" || c.key == "nameid").value; var firstName = claims.FirstOrDefault(c => c.key == "FirstName").value; var lastName = claims.FirstOrDefault(c => c.key == "LastName").value; var nationalCode = claims.FirstOrDefault(c => c.key == "NationalCode").value; var mobileNumber = claims.FirstOrDefault(c => c.key == "MobileNumber").value; var isSignMainContractStr = claims.FirstOrDefault(c => c.key == "IsSignMainContract").value; + var hasPurchasedPackageStr = claims.FirstOrDefault(c => c.key == "HasPurchasedPackage").value; + var isClubMemberActiveStr = claims.FirstOrDefault(c => c.key == "IsClubMemberActive").value; + + _userAuthInfo.UserId = long.TryParse(userIdStr, out var userId) ? userId : 0; _userAuthInfo.FirstName = firstName ?? string.Empty; _userAuthInfo.LastName = lastName ?? string.Empty; _userAuthInfo.NationalCode = nationalCode ?? string.Empty; _userAuthInfo.MobileNumber = mobileNumber ?? string.Empty; _userAuthInfo.IsSignMainContract = bool.TryParse(isSignMainContractStr, out var isSignMainContract) && isSignMainContract; + _userAuthInfo.HasPurchasedPackage = bool.TryParse(hasPurchasedPackageStr, out var hasPurchased) && hasPurchased; + _userAuthInfo.IsClubMemberActive = bool.TryParse(isClubMemberActiveStr, out var isClubActive) && isClubActive; } private async Task?> GetTokenClaimsAsync() diff --git a/src/FrontOffice.Main/Utilities/UserAuthInfo.cs b/src/FrontOffice.Main/Utilities/UserAuthInfo.cs index b73845d..32ae68e 100644 --- a/src/FrontOffice.Main/Utilities/UserAuthInfo.cs +++ b/src/FrontOffice.Main/Utilities/UserAuthInfo.cs @@ -3,9 +3,12 @@ namespace FrontOffice.Main.Utilities; public class UserAuthInfo { public List<(string key, string value)> UserClaims { get; set; } + public long UserId { get; set; } public string? FirstName { get; set; } public string? LastName { get; set; } public string? NationalCode { get; set; } public string? MobileNumber { get; set; } public bool IsSignMainContract { get; set; } + public bool HasPurchasedPackage { get; set; } + public bool IsClubMemberActive { get; set; } } \ No newline at end of file