Files
marketplaces/src/app/pages/cart/cart.component.ts

746 lines
25 KiB
TypeScript
Raw Normal View History

import { Component, ChangeDetectionStrategy, signal, OnDestroy, inject } from '@angular/core';
2026-02-19 01:23:25 +04:00
import { DecimalPipe } from '@angular/common';
2026-01-18 18:57:06 +04:00
import { Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
2026-06-28 22:18:35 +04:00
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
2026-02-28 17:18:24 +04:00
import { CartService, ApiService, LanguageService, AuthService } from '../../services';
2026-06-21 23:13:01 +04:00
import { Item, CartItem, DeliveryOption } from '../../models';
2026-06-29 23:22:00 +04:00
import { EMPTY, interval, of, Subscription } from 'rxjs';
2026-06-06 22:38:01 +04:00
import { catchError, exhaustMap, take, timeout } from 'rxjs/operators';
2026-06-21 23:13:01 +04:00
import { DeliverySelectorComponent } from '../../components/delivery-selector/delivery-selector.component';
2026-03-24 00:09:11 +04:00
import { TelegramLoginComponent } from '../../components/telegram-login/telegram-login.component';
import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField, onImageError } from '../../utils/item.utils';
2026-02-26 22:23:08 +04:00
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
2026-02-26 23:09:20 +04:00
import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service';
import { PAYMENT_POLL_INTERVAL_MS, PAYMENT_MIN_POLL_SECONDS, PAYMENT_TIMEOUT_CLOSE_MS, LINK_COPIED_DURATION_MS } from '../../config/constants';
import { IconComponent } from '../../shared/ui/icon/icon.component';
import { EmptyStateComponent } from '../../shared/ui/empty-state/empty-state.component';
import { ButtonComponent } from '../../shared/ui/button/button.component';
import { ConfigService } from '../../core/config/config.service';
import { TenantResolverService } from '../../core/config/tenant-resolver.service';
import { UserNotificationService } from '../../features/website/user-experience/services/user-notification.service';
import { ConfirmDialogComponent } from '../../shared/ui/confirm-dialog/confirm-dialog.component';
import { DialogComponent } from '../../shared/ui/dialog/dialog.component';
2026-01-18 18:57:06 +04:00
2026-06-29 00:06:18 +04:00
type PaymentMethod = 'qr' | 'card';
2026-01-18 18:57:06 +04:00
@Component({
selector: 'app-cart',
imports: [DecimalPipe, RouterLink, FormsModule, DeliverySelectorComponent, TelegramLoginComponent, LangRoutePipe, TranslatePipe, IconComponent, EmptyStateComponent, ButtonComponent, ConfirmDialogComponent, DialogComponent],
2026-01-18 18:57:06 +04:00
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
2026-02-26 21:54:21 +04:00
export class CartComponent implements OnDestroy {
2026-01-18 18:57:06 +04:00
items;
itemCount;
totalPrice;
2026-06-20 15:16:25 +04:00
totalDeliveryPrice;
totalWithDelivery;
hasDeliveryPrice;
2026-06-21 23:13:01 +04:00
allRequiredDeliveriesSelected;
2026-01-18 18:57:06 +04:00
termsAccepted = false;
2026-02-26 23:09:20 +04:00
private i18n = inject(TranslateService);
2026-02-28 17:18:24 +04:00
private authService = inject(AuthService);
private notifications = inject(UserNotificationService);
2026-02-26 23:09:20 +04:00
2026-03-24 00:09:11 +04:00
isAuthenticated = this.authService.isAuthenticated;
2026-01-18 18:57:06 +04:00
// Swipe state
swipedItemId = signal<number | null>(null);
// Payment popup states
showPaymentPopup = signal<boolean>(false);
2026-06-18 18:29:39 +04:00
paymentStatus = signal<'creating' | 'waiting' | 'success' | 'timeout' | 'error' | null>('creating');
2026-01-18 18:57:06 +04:00
qrCodeUrl = signal<string>('');
paymentUrl = signal<string>('');
2026-06-28 22:18:35 +04:00
bankPaymentUrl = signal<string>('');
bankPaymentFrameUrl = signal<SafeResourceUrl | null>(null);
showBankPaymentPopup = signal<boolean>(false);
2026-06-29 00:06:18 +04:00
selectedPaymentMethod = signal<PaymentMethod>('qr');
2026-01-18 18:57:06 +04:00
paymentId = signal<string>('');
linkCopied = signal<boolean>(false);
// Email collection after successful payment
userEmail = signal<string>('');
userPhone = signal<string>('');
emailTouched = signal<boolean>(false);
phoneTouched = signal<boolean>(false);
emailError = signal<string>('');
phoneError = signal<string>('');
emailSubmitting = signal<boolean>(false);
purchaseSubmitted = signal<boolean>(false);
2026-01-18 18:57:06 +04:00
paidItems: CartItem[] = [];
maxChecks = Math.ceil(PAYMENT_MIN_POLL_SECONDS / (PAYMENT_POLL_INTERVAL_MS / 1000));
2026-01-18 18:57:06 +04:00
private pollingSubscription?: Subscription;
private closeTimeout?: ReturnType<typeof setTimeout>;
private configService = inject(ConfigService);
private tenantResolver = inject(TenantResolverService);
2026-01-18 18:57:06 +04:00
constructor(
private cartService: CartService,
private apiService: ApiService,
2026-02-26 22:23:08 +04:00
private router: Router,
2026-06-28 22:18:35 +04:00
private langService: LanguageService,
private sanitizer: DomSanitizer
2026-01-18 18:57:06 +04:00
) {
this.items = this.cartService.items;
this.itemCount = this.cartService.itemCount;
this.totalPrice = this.cartService.totalPrice;
2026-06-20 15:16:25 +04:00
this.totalDeliveryPrice = this.cartService.totalDeliveryPrice;
this.totalWithDelivery = this.cartService.totalWithDelivery;
this.hasDeliveryPrice = this.cartService.hasDeliveryPrice;
2026-06-21 23:13:01 +04:00
this.allRequiredDeliveriesSelected = this.cartService.allRequiredDeliveriesSelected;
2026-03-24 00:09:11 +04:00
}
requestLogin(): void {
this.authService.requestLogin();
2026-01-18 18:57:06 +04:00
}
ngOnDestroy(): void {
this.stopPolling();
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
}
}
2026-07-05 01:12:07 +04:00
removeItem(item: CartItem): void {
this.cartService.removeItem(item.itemID, this.cartVariant(item));
2026-01-18 18:57:06 +04:00
this.swipedItemId.set(null);
}
2026-07-05 01:12:07 +04:00
updateQuantity(item: CartItem, quantity: number): void {
this.cartService.updateQuantity(item.itemID, quantity, this.cartVariant(item));
2026-01-18 18:57:06 +04:00
}
2026-07-05 01:12:07 +04:00
increaseQuantity(item: CartItem): void {
this.updateQuantity(item, item.quantity + 1);
2026-01-18 18:57:06 +04:00
}
2026-07-05 01:12:07 +04:00
decreaseQuantity(item: CartItem): void {
if (item.quantity <= 1) {
this.removeItem(item);
2026-01-18 18:57:06 +04:00
} else {
2026-07-05 01:12:07 +04:00
this.updateQuantity(item, item.quantity - 1);
2026-01-18 18:57:06 +04:00
}
}
2026-07-05 01:12:07 +04:00
private cartVariant(item: CartItem): { colour?: string; size?: string; price?: number; currency?: string } {
return {
colour: item.colour,
size: item.size,
price: item.price,
currency: item.currency,
};
}
2026-01-18 18:57:06 +04:00
onSwipeStart(itemID: number, event: TouchEvent): void {
const startX = event.touches[0].clientX;
const onMove = (e: TouchEvent) => {
const currentX = e.touches[0].clientX;
const diff = startX - currentX;
if (diff > 50) {
this.swipedItemId.set(itemID);
cleanup();
} else if (diff < -10) {
this.swipedItemId.set(null);
cleanup();
}
};
const cleanup = () => {
2026-02-26 21:54:21 +04:00
document.removeEventListener('touchmove', onMove);
2026-01-18 18:57:06 +04:00
document.removeEventListener('touchend', cleanup);
};
2026-02-26 21:54:21 +04:00
document.addEventListener('touchmove', onMove);
2026-01-18 18:57:06 +04:00
document.addEventListener('touchend', cleanup);
}
readonly clearCartConfirmOpen = signal(false);
2026-01-18 18:57:06 +04:00
clearCart(): void {
this.clearCartConfirmOpen.set(true);
}
confirmClearCart(): void {
this.cartService.clearCart();
this.clearCartConfirmOpen.set(false);
2026-01-18 18:57:06 +04:00
}
2026-02-19 01:23:25 +04:00
readonly getMainImage = getMainImage;
readonly onImageError = onImageError;
2026-02-19 01:23:25 +04:00
readonly trackByItemId = trackByItemId;
readonly getDiscountedPrice = getDiscountedPrice;
2026-02-20 10:44:03 +04:00
readonly getBadgeClass = getBadgeClass;
2026-01-18 18:57:06 +04:00
2026-03-24 00:09:11 +04:00
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
itemDesc(item: Item): string { return getTranslatedField(item, 'simpleDescription', this.langService.currentLanguage()); }
get currentCurrency(): string { return this.langService.currentCurrency(); }
2026-06-21 23:13:01 +04:00
get isCheckoutDisabled(): boolean { return !this.termsAccepted || !this.isAuthenticated() || !this.allRequiredDeliveriesSelected(); }
selectDelivery(itemID: number, selectedDelivery: DeliveryOption | null): void {
this.cartService.setSelectedDelivery(itemID, selectedDelivery);
}
2026-01-18 18:57:06 +04:00
2026-06-29 00:06:18 +04:00
checkout(paymentMethod: PaymentMethod): void {
2026-06-21 23:13:01 +04:00
if (!this.allRequiredDeliveriesSelected()) {
this.notifications.show(this.i18n.t('cart.deliveryRequired'), 'warning');
2026-06-21 23:13:01 +04:00
return;
}
2026-01-18 18:57:06 +04:00
if (!this.termsAccepted) {
this.notifications.show(this.i18n.t('cart.acceptTerms'), 'warning');
2026-01-18 18:57:06 +04:00
return;
}
2026-06-29 00:06:18 +04:00
this.openPaymentPopup(paymentMethod);
2026-01-18 18:57:06 +04:00
}
2026-06-29 00:06:18 +04:00
openPaymentPopup(paymentMethod: PaymentMethod): void {
2026-01-18 18:57:06 +04:00
this.showPaymentPopup.set(true);
2026-06-29 00:06:18 +04:00
this.selectedPaymentMethod.set(paymentMethod);
2026-01-18 18:57:06 +04:00
this.paymentStatus.set('creating');
2026-06-02 00:57:36 +04:00
this.paymentId.set('');
this.qrCodeUrl.set('');
this.paymentUrl.set('');
2026-06-28 22:18:35 +04:00
this.bankPaymentUrl.set('');
this.bankPaymentFrameUrl.set(null);
this.showBankPaymentPopup.set(false);
2026-06-02 00:57:36 +04:00
this.linkCopied.set(false);
2026-01-18 18:57:06 +04:00
this.userEmail.set('');
this.userPhone.set('');
this.emailTouched.set(false);
this.phoneTouched.set(false);
this.emailError.set('');
this.phoneError.set('');
this.emailSubmitting.set(false);
this.purchaseSubmitted.set(false);
2026-01-18 18:57:06 +04:00
this.paidItems = [...this.items()];
2026-06-29 00:06:18 +04:00
this.createPayment(paymentMethod);
2026-01-18 18:57:06 +04:00
}
closePaymentPopup(): void {
this.showPaymentPopup.set(false);
2026-06-28 22:18:35 +04:00
this.closeBankPaymentPopup();
2026-01-18 18:57:06 +04:00
this.stopPolling();
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
this.closeTimeout = undefined;
}
}
2026-06-02 00:57:36 +04:00
retryPayment(): void {
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
this.closeTimeout = undefined;
2026-03-24 02:25:50 +04:00
}
2026-01-18 18:57:06 +04:00
2026-06-02 00:57:36 +04:00
this.paymentStatus.set('creating');
this.paymentId.set('');
this.qrCodeUrl.set('');
this.paymentUrl.set('');
2026-06-28 22:18:35 +04:00
this.bankPaymentUrl.set('');
this.bankPaymentFrameUrl.set(null);
this.showBankPaymentPopup.set(false);
2026-06-02 00:57:36 +04:00
this.linkCopied.set(false);
2026-06-29 00:06:18 +04:00
this.createPayment(this.selectedPaymentMethod());
2026-06-02 00:57:36 +04:00
}
2026-06-29 00:06:18 +04:00
createPayment(paymentMethod: PaymentMethod): void {
2026-06-06 22:38:01 +04:00
const orderId = this.generateOrderId();
const paymentPayload = {
2026-06-20 15:16:25 +04:00
amount: Number(this.totalWithDelivery()),
2026-06-05 18:23:24 +04:00
currency: 'RUB' as const,
2026-06-06 22:38:01 +04:00
siteuserID: this.getPaymentUserId(),
siteorderID: orderId,
redirectUrl: '',
telegramUsername: this.getTelegramUsername(),
2026-06-29 00:06:18 +04:00
paymentMethod,
qrDescription: this.getPaymentDescription(),
customerID: this.getTelegramUserId() ?? undefined,
2026-06-06 22:38:01 +04:00
items: this.buildPaymentItems(),
2026-06-02 01:46:12 +04:00
};
2026-06-06 22:38:01 +04:00
this.apiService.createCartPayment(paymentPayload)
2026-06-02 00:57:36 +04:00
.subscribe({
2026-03-24 02:25:50 +04:00
next: (response) => {
2026-06-02 01:46:12 +04:00
const qrId = this.apiService.resolvePaymentQrId(response);
const qrUrl = this.apiService.resolvePaymentQrUrl(response);
const paymentLink = this.apiService.resolvePaymentLink(response);
2026-06-28 22:18:35 +04:00
const bankUrl = this.apiService.resolveBankPaymentUrl(response);
2026-06-02 00:57:36 +04:00
2026-06-29 00:06:18 +04:00
if (!qrId || (paymentMethod === 'qr' && !qrUrl) || (paymentMethod === 'card' && !bankUrl)) {
2026-06-28 22:18:35 +04:00
console.error('Payment response missing payment fields:', response);
2026-06-02 00:57:36 +04:00
this.setPaymentError();
return;
}
this.paymentId.set(qrId);
this.qrCodeUrl.set(qrUrl);
2026-06-02 01:46:12 +04:00
this.paymentUrl.set(paymentLink);
2026-06-28 22:18:35 +04:00
this.bankPaymentUrl.set(bankUrl);
2026-06-02 01:46:12 +04:00
2026-03-24 02:25:50 +04:00
this.paymentStatus.set('waiting');
this.startPolling(response.qrTTL);
2026-06-29 00:06:18 +04:00
if (paymentMethod === 'card') {
this.openBankPaymentPopup();
}
2026-03-24 02:25:50 +04:00
},
error: (err) => {
console.error('Error creating payment:', err);
2026-06-02 00:57:36 +04:00
this.setPaymentError();
2026-03-24 02:25:50 +04:00
}
});
2026-01-18 18:57:06 +04:00
}
startPolling(qrTTL?: number): void {
2026-03-06 17:45:34 +04:00
this.stopPolling();
2026-06-06 16:16:37 +04:00
if (!this.paymentId()) {
2026-06-02 01:46:12 +04:00
this.setPaymentError();
return;
}
const pollSeconds = Math.max(PAYMENT_MIN_POLL_SECONDS, (qrTTL ?? 0) * 60);
this.maxChecks = Math.ceil(pollSeconds / (PAYMENT_POLL_INTERVAL_MS / 1000));
2026-03-06 18:40:58 +04:00
this.pollingSubscription = interval(PAYMENT_POLL_INTERVAL_MS)
2026-01-18 18:57:06 +04:00
.pipe(
take(this.maxChecks), // qrTTL minutes from create response, minimum 1 minute
2026-06-05 17:57:18 +04:00
exhaustMap(() => {
2026-06-29 22:15:19 +04:00
const statusRequest = this.selectedPaymentMethod() === 'card'
? this.apiService.checkCartCardPaymentStatus(this.paymentId())
: this.apiService.checkCartPaymentStatus(this.paymentId());
return statusRequest.pipe(
2026-06-05 17:57:18 +04:00
timeout(8000),
2026-06-02 00:57:36 +04:00
catchError((err) => {
console.error('Error checking payment status:', err);
2026-06-29 23:22:00 +04:00
this.setPaymentError();
return EMPTY;
2026-06-02 00:57:36 +04:00
})
);
2026-01-18 18:57:06 +04:00
})
)
.subscribe({
next: (response) => {
2026-06-02 00:57:36 +04:00
if (!response) {
return;
}
2026-06-18 13:11:05 +04:00
const paymentStatus = response.status?.toUpperCase() || '';
2026-06-06 22:38:01 +04:00
const paymentCode = response.code?.toUpperCase() || '';
2026-06-02 00:57:36 +04:00
2026-06-06 16:16:37 +04:00
if (paymentStatus === 'FAILED' || paymentStatus === 'EXPIRED' || paymentStatus === 'CANCELLED' || paymentStatus === 'REJECTED') {
2026-06-02 00:57:36 +04:00
this.paymentStatus.set('timeout');
2026-06-28 22:18:35 +04:00
this.closeBankPaymentPopup();
2026-06-02 00:57:36 +04:00
this.stopPolling();
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, PAYMENT_TIMEOUT_CLOSE_MS);
return;
}
2026-01-18 18:57:06 +04:00
// Check if payment is successful
2026-06-06 22:38:01 +04:00
if (paymentStatus === 'COMPLETED' || paymentStatus === 'APPROVED' || paymentStatus === 'PAID' || paymentCode === 'SUCCESS') {
2026-01-18 18:57:06 +04:00
this.paymentStatus.set('success');
2026-06-28 22:18:35 +04:00
this.closeBankPaymentPopup();
2026-01-18 18:57:06 +04:00
this.stopPolling();
2026-06-18 18:30:20 +04:00
// Auto-submit purchase after 5 seconds
2026-06-18 15:09:56 +04:00
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
this.autoSubmitPurchase();
}, 5000);
2026-07-20 01:02:36 +04:00
this.recordOrder();
2026-06-18 18:30:20 +04:00
this.cartService.clearCart();
2026-07-20 01:02:36 +04:00
2026-06-18 18:30:20 +04:00
2026-01-18 18:57:06 +04:00
}
// Continue checking for 3 minutes regardless of other statuses
},
complete: () => {
this.stopPolling();
// If all checks are done but payment not completed
if (this.paymentStatus() === 'waiting') {
this.paymentStatus.set('timeout');
2026-06-28 22:18:35 +04:00
this.closeBankPaymentPopup();
2026-01-18 18:57:06 +04:00
// Close popup after showing timeout message
2026-03-06 17:45:34 +04:00
if (this.closeTimeout) clearTimeout(this.closeTimeout);
2026-01-18 18:57:06 +04:00
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
2026-03-06 18:40:58 +04:00
}, PAYMENT_TIMEOUT_CLOSE_MS);
2026-01-18 18:57:06 +04:00
}
}
});
}
stopPolling(): void {
if (this.pollingSubscription) {
this.pollingSubscription.unsubscribe();
2026-06-02 00:57:36 +04:00
this.pollingSubscription = undefined;
}
}
2026-06-28 22:18:35 +04:00
openBankPaymentPopup(): void {
const bankUrl = this.bankPaymentUrl();
if (!bankUrl) {
return;
}
this.bankPaymentFrameUrl.set(this.sanitizer.bypassSecurityTrustResourceUrl(bankUrl));
this.showBankPaymentPopup.set(true);
}
closeBankPaymentPopup(): void {
this.showBankPaymentPopup.set(false);
this.bankPaymentFrameUrl.set(null);
}
2026-06-02 00:57:36 +04:00
private setPaymentError(): void {
this.paymentStatus.set('error');
2026-06-28 22:18:35 +04:00
this.closeBankPaymentPopup();
2026-06-02 00:57:36 +04:00
this.stopPolling();
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
this.closeTimeout = undefined;
2026-01-18 18:57:06 +04:00
}
}
2026-07-20 01:02:36 +04:00
/**
* Records the just-paid cart as a backoffice AdminOrder (POST /orders) so it shows up
* in Backoffice Orders/Transactions. Best-effort and fire-and-forget: a failure here
* must never affect the already-confirmed payment or block autoSubmitPurchase.
*/
private recordOrder(): void {
const email = this.userEmail().trim();
const phone = this.userPhone().replace(/\D/g, '');
this.apiService.createOrder({
items: this.paidItems.map((item: CartItem) => ({
productId: String(item.itemID),
name: item.name,
quantity: item.quantity,
})),
customer: {
name: this.getTelegramUsername() || this.i18n.t('common.guest'),
2026-07-20 01:02:36 +04:00
email,
phone,
},
payment: {
method: this.selectedPaymentMethod(),
currency: 'RUB',
},
}).subscribe({
error: (err) => console.error('Error recording order:', err),
});
}
/**
* Fallback fired a few seconds after payment success if the user hasn't
* already submitted the email/phone form themselves (submitEmail()).
* Navigates home only once the submission result is known, never before -
* and sends whatever the user has typed so far instead of blank fields.
*/
2026-06-18 15:09:56 +04:00
private autoSubmitPurchase(): void {
if (this.purchaseSubmitted()) {
return;
}
2026-06-18 15:09:56 +04:00
const telegramUserId = this.getTelegramUserId();
// Telegram ID is mandatory for submitPurchaseEmail.
2026-06-18 15:09:56 +04:00
if (!telegramUserId) {
this.notifications.show(this.i18n.t('cart.telegramIdMissing'), 'warning');
2026-06-18 15:09:56 +04:00
this.emailSubmitting.set(false);
this.closePaymentPopup();
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]);
2026-06-18 15:09:56 +04:00
return;
}
2026-06-18 15:09:56 +04:00
this.emailSubmitting.set(true);
2026-06-18 15:09:56 +04:00
const emailData = {
email: this.userEmail().trim(),
phone: this.userPhone().replace(/\D/g, ''),
2026-06-18 15:09:56 +04:00
telegramUserId: telegramUserId,
items: this.paidItems.map((item: CartItem) => ({
itemID: item.itemID,
name: item.name,
price: item.discount > 0
2026-06-18 15:09:56 +04:00
? item.price * (1 - item.discount / 100)
: item.price,
currency: item.currency,
2026-06-21 23:13:01 +04:00
quantity: item.quantity,
2026-06-22 10:46:51 +04:00
...(item.selectedDelivery ? { delivery: [item.selectedDelivery] } : {})
2026-06-18 15:09:56 +04:00
}))
};
2026-06-18 15:09:56 +04:00
this.apiService.submitPurchaseEmail(emailData).subscribe({
next: () => {
this.purchaseSubmitted.set(true);
2026-06-18 15:09:56 +04:00
this.emailSubmitting.set(false);
this.closePaymentPopup();
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]);
},
error: (err) => {
console.error('Error submitting purchase:', err);
this.emailSubmitting.set(false);
// Still close popup and redirect even if submission fails
this.closePaymentPopup();
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]);
}
});
2026-06-18 18:29:39 +04:00
this.paymentStatus.set(null);
2026-06-18 15:09:56 +04:00
}
2026-01-18 18:57:06 +04:00
copyPaymentLink(): void {
const url = this.paymentUrl();
if (url) {
navigator.clipboard.writeText(url).then(() => {
this.linkCopied.set(true);
2026-03-06 18:40:58 +04:00
setTimeout(() => this.linkCopied.set(false), LINK_COPIED_DURATION_MS);
2026-01-18 18:57:06 +04:00
}).catch(err => {
2026-02-26 23:09:20 +04:00
console.error(this.i18n.t('cart.copyError'), err);
2026-01-18 18:57:06 +04:00
});
}
}
submitEmail(): void {
// Mark both fields as touched
this.emailTouched.set(true);
this.phoneTouched.set(true);
// Validate both fields
this.validateEmail();
const digitsOnly = this.userPhone().replace(/\D/g, '');
this.validatePhone(digitsOnly);
// Check if there are any errors
if (this.emailError() || this.phoneError()) {
return;
}
const email = this.userEmail().trim();
const phoneRaw = this.userPhone().replace(/\D/g, ''); // Remove all formatting, send only digits
this.emailSubmitting.set(true);
const emailData = {
email: email,
phone: phoneRaw,
telegramUserId: this.getTelegramUserId(),
items: this.paidItems.map((item: CartItem) => ({
itemID: item.itemID,
name: item.name,
price: item.discount > 0
? item.price * (1 - item.discount / 100)
: item.price,
currency: item.currency,
2026-06-21 23:13:01 +04:00
quantity: item.quantity,
2026-06-22 10:46:51 +04:00
...(item.selectedDelivery ? { delivery: [item.selectedDelivery] } : {})
2026-01-18 18:57:06 +04:00
}))
};
this.apiService.submitPurchaseEmail(emailData).subscribe({
next: () => {
this.purchaseSubmitted.set(true);
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
this.closeTimeout = undefined;
}
2026-01-18 18:57:06 +04:00
this.emailSubmitting.set(false);
this.notifications.show(this.i18n.t('cart.emailSuccess'), 'success');
2026-01-18 18:57:06 +04:00
// Close popup and redirect to home page
setTimeout(() => {
this.closePaymentPopup();
2026-02-26 22:23:08 +04:00
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]);
2026-01-18 18:57:06 +04:00
}, 500);
},
error: (err) => {
console.error('Error submitting email:', err);
this.emailSubmitting.set(false);
this.notifications.show(this.i18n.t('cart.emailError'), 'warning');
2026-01-18 18:57:06 +04:00
}
});
}
private getTelegramUserId(): string | null {
2026-06-19 12:43:25 +04:00
const sessionTelegramUserId = this.authService.session()?.userId;
2026-06-19 15:01:54 +04:00
if (sessionTelegramUserId !== null && sessionTelegramUserId !== undefined) {
2026-06-02 00:57:36 +04:00
return sessionTelegramUserId.toString();
}
2026-01-18 18:57:06 +04:00
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
return window.Telegram.WebApp.initDataUnsafe.user.id.toString();
}
2026-06-02 00:57:36 +04:00
2026-01-18 18:57:06 +04:00
return null;
}
2026-06-02 00:57:36 +04:00
private getTelegramUsername(): string {
const sessionUsername = this.authService.session()?.username;
if (sessionUsername) {
return sessionUsername;
}
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
return window.Telegram.WebApp.initDataUnsafe.user.username || 'nontelegram';
}
return 'nontelegram';
}
private getPaymentUserId(): string {
return this.getTelegramUserId() ?? `web_${Date.now()}`;
}
private getPaymentDescription(): string {
const brandName = this.configService.getBootstrapSnapshot()?.branding?.brandName?.trim();
if (brandName) {
return brandName;
}
const hostname = this.tenantResolver.getHostname();
if (hostname && !this.tenantResolver.isLocalhost()) {
return hostname;
}
return 'Покупка на Маркетплейсе';
}
2026-06-02 00:57:36 +04:00
private generateOrderId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `order_${timestamp}_${random}`;
}
2026-06-22 10:46:51 +04:00
private buildPaymentItems(): Array<{ itemID: number; price: number; name: string; quantity: number; delivery?: DeliveryOption[] }> {
2026-06-06 22:38:01 +04:00
return this.items().map((item: CartItem) => {
const unitPrice = item.discount > 0
? item.price * (1 - item.discount / 100)
: item.price;
const details = [item.colour, item.size].filter(Boolean).join(', ');
const translatedName = this.itemName(item).trim() || `Item ${item.itemID}`;
const name = details ? `${item.quantity} x ${translatedName} (${details})` : `${item.quantity} x ${translatedName}`;
return {
itemID: item.itemID,
price: unitPrice * item.quantity,
name,
2026-06-21 23:13:01 +04:00
quantity: item.quantity,
2026-06-22 10:46:51 +04:00
...(item.selectedDelivery ? { delivery: [item.selectedDelivery] } : {}),
2026-06-06 22:38:01 +04:00
};
});
2026-06-06 16:16:37 +04:00
}
2026-01-18 18:57:06 +04:00
onPhoneInput(event: Event): void {
const input = event.target as HTMLInputElement;
let value = input.value.replace(/\D/g, ''); // Remove all non-digits
// Auto-add +7 for Russian numbers
if (value.length > 0 && !value.startsWith('7') && !value.startsWith('8')) {
value = '7' + value;
}
// Convert 8 to 7 for Russian format
if (value.startsWith('8')) {
value = '7' + value.substring(1);
}
// Format: +7 (XXX) XXX-XX-XX
let formatted = '';
if (value.length > 0) {
formatted = '+7';
if (value.length > 1) {
formatted += ' (' + value.substring(1, 4);
}
if (value.length >= 4) {
formatted += ') ' + value.substring(4, 7);
}
if (value.length >= 7) {
formatted += '-' + value.substring(7, 9);
}
if (value.length >= 9) {
formatted += '-' + value.substring(9, 11);
}
}
this.userPhone.set(formatted);
this.validatePhone(value);
}
onPhoneBlur(): void {
this.phoneTouched.set(true);
const digitsOnly = this.userPhone().replace(/\D/g, '');
this.validatePhone(digitsOnly);
}
validatePhone(digitsOnly: string): void {
if (!this.phoneTouched() && digitsOnly.length === 0) {
this.phoneError.set('');
return;
}
if (digitsOnly.length === 0) {
2026-02-26 23:09:20 +04:00
this.phoneError.set(this.i18n.t('cart.phoneRequired'));
2026-01-18 18:57:06 +04:00
} else if (digitsOnly.length < 11) {
2026-02-26 23:09:20 +04:00
this.phoneError.set(this.i18n.t('cart.phoneMoreDigits', { count: 11 - digitsOnly.length }));
2026-01-18 18:57:06 +04:00
} else if (digitsOnly.length > 11) {
2026-02-26 23:09:20 +04:00
this.phoneError.set(this.i18n.t('cart.phoneTooMany'));
2026-01-18 18:57:06 +04:00
} else {
this.phoneError.set('');
}
}
onEmailInput(event: Event): void {
const input = event.target as HTMLInputElement;
this.userEmail.set(input.value);
if (this.emailTouched()) {
this.validateEmail();
}
}
onEmailBlur(): void {
this.emailTouched.set(true);
this.validateEmail();
}
validateEmail(): void {
const email = this.userEmail().trim();
if (!this.emailTouched() && email.length === 0) {
this.emailError.set('');
return;
}
if (email.length === 0) {
2026-02-26 23:09:20 +04:00
this.emailError.set(this.i18n.t('cart.emailRequired'));
2026-01-18 18:57:06 +04:00
} else if (email.length < 5) {
2026-02-26 23:09:20 +04:00
this.emailError.set(this.i18n.t('cart.emailTooShort'));
2026-01-18 18:57:06 +04:00
} else if (email.length > 100) {
2026-02-26 23:09:20 +04:00
this.emailError.set(this.i18n.t('cart.emailTooLong'));
2026-01-18 18:57:06 +04:00
} else if (!email.includes('@')) {
2026-02-26 23:09:20 +04:00
this.emailError.set(this.i18n.t('cart.emailNeedsAt'));
2026-01-18 18:57:06 +04:00
} else if (!email.includes('.')) {
2026-02-26 23:09:20 +04:00
this.emailError.set(this.i18n.t('cart.emailNeedsDomain'));
2026-01-18 18:57:06 +04:00
} else {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
2026-02-26 23:09:20 +04:00
this.emailError.set(this.i18n.t('cart.emailInvalid'));
2026-01-18 18:57:06 +04:00
} else {
this.emailError.set('');
}
}
}
}