feat: add payment callback and referral link features
Build and Deploy / build (push) Failing after 1m21s

This commit is contained in:
masoodafar-web
2025-12-16 02:33:56 +03:30
parent 3c528aba48
commit 3af564be73
7 changed files with 425 additions and 33 deletions
+125 -30
View File
@@ -21,43 +21,74 @@
</div>
</MudStack>
<MudSpacer/>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Financing">تامین اعتبار خرید</MudButton>
@* <MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="OpenPurchaseOptions">تامین اعتبار</MudButton> *@
</MudStack>
<!-- Referral Code Section -->
<MudDivider />
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle1">کد دعوت شما</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Share"
OnClick="ShareReferralCode">
اشتراک‌گذاری
</MudButton>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید.
</MudText>
@if (CanShowReferralLink)
{
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle1">کد دعوت شما</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Share"
OnClick="ShareReferralCode">
اشتراک‌گذاری
</MudButton>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید.
</MudText>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudTextField @bind-Value="_userProfile.ReferralCode"
Label="کد دعوت"
Variant="Variant.Outlined"
ReadOnly="true"
Class="flex-grow-1"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.ContentCopy"
OnAdornmentClick="CopyReferralCode" />
</MudStack>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudTextField @bind-Value="_userProfile.ReferralCode"
Label="کد دعوت"
Variant="Variant.Outlined"
ReadOnly="true"
Class="flex-grow-1"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.ContentCopy"
OnAdornmentClick="CopyReferralCode" />
</MudStack>
@if (!string.IsNullOrWhiteSpace(_copyMessage))
{
<MudText Typo="Typo.caption" Color="Color.Success">@(_copyMessage)</MudText>
}
</MudStack>
@if (!string.IsNullOrWhiteSpace(_copyMessage))
{
<MudText Typo="Typo.caption" Color="Color.Success">@(_copyMessage)</MudText>
}
</MudStack>
}
else
{
<MudPaper Class="my-3 pa-3 border border-secondary">
<MudStack Spacing="3" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Large" Color="Color.Info" />
<MudText Typo="Typo.h6" Align="Align.Center" Color="Color.Info">
لینک دعوت شما هنوز فعال نشده است
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary">
برای فعال‌سازی لینک دعوت و دعوت دوستان خود به جمع مشتریان ویژه کارابازار، کافی است ابتدا <strong>پکیج پایه ۵۶ میلیونی</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
</MudText>
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">
پس از تکمیل این مراحل، می‌توانید از مزایای ویژه عضویت در شبکه فروش بهره‌مند شده و با معرفی دوستان، کمیسیون دریافت کنید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="OpenPurchaseOptions">
شروع فرآیند تامین اعتبار
</MudButton>
</MudStack>
</MudPaper>
}
</MudStack>
</MudPaper>
</MudItem>
@@ -199,3 +230,67 @@
</MudItem>
</MudGrid>
</MudContainer>
<!-- Purchase Options Bottom Sheet -->
<MudDialog @bind-Visible="_showPurchaseBottomSheet">
<DialogContent>
<MudStack Spacing="3" Class="pa-2">
<MudText Typo="Typo.h6" Align="Align.Center">
خرید پکیج پایه ۵۶ میلیون تومان
</MudText>
<MudDivider />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
برای خرید پکیج پایه، یکی از روش‌های زیر را انتخاب کنید:
</MudText>
<!-- Direct Payment Option -->
<MudPaper Elevation="0" Class="pa-4 rounded-lg" Style="border: 2px solid var(--mud-palette-primary);">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Color="Color.Primary" Size="Size.Medium" />
<MudText Typo="Typo.subtitle1" Color="Color.Primary"><b>پرداخت مستقیم</b></MudText>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
پرداخت آنی از طریق درگاه بانکی
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
StartIcon="@Icons.Material.Filled.Payment"
OnClick="DirectPayment">
پرداخت با کارت بانکی
</MudButton>
</MudStack>
</MudPaper>
<!-- Daya Loan Option -->
<MudPaper Elevation="0" Class="pa-4 rounded-lg" Style="border: 2px solid var(--mud-palette-tertiary);">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Tertiary" Size="Size.Medium" />
<MudText Typo="Typo.subtitle1" Color="Color.Tertiary"><b>اعتبار الماسی دایا</b></MudText>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
تأمین اعتبار از طریق خرید توکن الماس و دریافت وام
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Tertiary"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalance"
OnClick="DayaLoanPayment">
تأمین اعتبار الماسی
</MudButton>
</MudStack>
</MudPaper>
<MudButton Variant="Variant.Text"
Color="Color.Default"
FullWidth="true"
OnClick="ClosePurchaseOptions">
بستن
</MudButton>
</MudStack>
</DialogContent>
</MudDialog>
@@ -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<GetAllUserAddressByFilterResponseModel> _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");
}
@@ -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
<PageTitle>نتیجه پرداخت | کارا بازار سلامت</PageTitle>
<MudContainer MaxWidth="MaxWidth.Small" Class="py-8">
<MudPaper Class="pa-6 text-center">
@if (_isLoading)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.h6" Class="mt-4">در حال بررسی وضعیت پرداخت...</MudText>
}
else if (_isSuccess)
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" Style="font-size: 80px" />
<MudText Typo="Typo.h5" Color="Color.Success" Class="mt-4">پرداخت موفق</MudText>
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
@if (!string.IsNullOrEmpty(_refId))
{
<MudText Typo="Typo.body2" Class="mt-3">
<strong>کد پیگیری:</strong> @_refId
</MudText>
}
<MudDivider Class="my-4" />
<MudText Typo="Typo.body2" Color="Color.Secondary">
موجودی کیف پول: @FormatPrice(_walletBalance)
</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
موجودی اعتبار تخفیف: @FormatPrice(_discountBalance)
</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
Href="/profile">
بازگشت به پروفایل
</MudButton>
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Error" Color="Color.Error" Size="Size.Large" Style="font-size: 80px" />
<MudText Typo="Typo.h5" Color="Color.Error" Class="mt-4">پرداخت ناموفق</MudText>
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
Href="/profile">
بازگشت به پروفایل
</MudButton>
<MudButton Color="Color.Secondary" Variant="Variant.Outlined" Class="mt-3 mr-2"
OnClick="RetryPayment">
تلاش مجدد
</MudButton>
}
</MudPaper>
</MudContainer>
@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");
}
/// <summary>
/// Refresh token after successful payment to update PackagePurchaseMethod claim
/// </summary>
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);
}