2026-02-26 23:09:20 +04:00
|
|
|
import { Component, computed, 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-02-28 17:18:24 +04:00
|
|
|
import { CartService, ApiService, LanguageService, AuthService } from '../../services';
|
2026-01-18 18:57:06 +04:00
|
|
|
import { Item, CartItem } from '../../models';
|
2026-06-02 00:57:36 +04:00
|
|
|
import { interval, of, Subscription } from 'rxjs';
|
|
|
|
|
import { catchError, switchMap, take } from 'rxjs/operators';
|
2026-01-18 18:57:06 +04:00
|
|
|
import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component';
|
2026-03-24 00:09:11 +04:00
|
|
|
import { TelegramLoginComponent } from '../../components/telegram-login/telegram-login.component';
|
2026-01-18 18:57:06 +04:00
|
|
|
import { environment } from '../../../environments/environment';
|
2026-03-24 00:09:11 +04:00
|
|
|
import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField } 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';
|
2026-03-06 18:40:58 +04:00
|
|
|
import { PAYMENT_POLL_INTERVAL_MS, PAYMENT_MAX_CHECKS, PAYMENT_TIMEOUT_CLOSE_MS, PAYMENT_ERROR_CLOSE_MS, LINK_COPIED_DURATION_MS } from '../../config/constants';
|
2026-01-18 18:57:06 +04:00
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'app-cart',
|
2026-03-24 00:09:11 +04:00
|
|
|
imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, TelegramLoginComponent, LangRoutePipe, TranslatePipe],
|
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;
|
|
|
|
|
termsAccepted = false;
|
|
|
|
|
isnovo = environment.theme === 'novo';
|
|
|
|
|
|
2026-02-26 23:09:20 +04:00
|
|
|
private i18n = inject(TranslateService);
|
2026-02-28 17:18:24 +04:00
|
|
|
private authService = inject(AuthService);
|
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-02 00:57:36 +04:00
|
|
|
paymentStatus = signal<'creating' | 'waiting' | 'success' | 'timeout' | 'error'>('creating');
|
2026-01-18 18:57:06 +04:00
|
|
|
qrCodeUrl = signal<string>('');
|
|
|
|
|
paymentUrl = signal<string>('');
|
|
|
|
|
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);
|
|
|
|
|
paidItems: CartItem[] = [];
|
|
|
|
|
|
2026-03-06 18:40:58 +04:00
|
|
|
maxChecks = PAYMENT_MAX_CHECKS;
|
2026-01-18 18:57:06 +04:00
|
|
|
private pollingSubscription?: Subscription;
|
|
|
|
|
private closeTimeout?: ReturnType<typeof setTimeout>;
|
2026-06-02 00:57:36 +04:00
|
|
|
private paymentBackend: 'websession' | 'qr' | 'cart' | null = null;
|
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,
|
|
|
|
|
private langService: LanguageService
|
2026-01-18 18:57:06 +04:00
|
|
|
) {
|
|
|
|
|
this.items = this.cartService.items;
|
|
|
|
|
this.itemCount = this.cartService.itemCount;
|
|
|
|
|
this.totalPrice = this.cartService.totalPrice;
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
removeItem(itemID: number): void {
|
|
|
|
|
this.cartService.removeItem(itemID);
|
|
|
|
|
this.swipedItemId.set(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateQuantity(itemID: number, quantity: number): void {
|
|
|
|
|
this.cartService.updateQuantity(itemID, quantity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
increaseQuantity(itemID: number, currentQuantity: number): void {
|
|
|
|
|
this.updateQuantity(itemID, currentQuantity + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
decreaseQuantity(itemID: number, currentQuantity: number): void {
|
|
|
|
|
if (currentQuantity <= 1) {
|
|
|
|
|
this.removeItem(itemID);
|
|
|
|
|
} else {
|
|
|
|
|
this.updateQuantity(itemID, currentQuantity - 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onSwipeStart(itemID: number, event: TouchEvent): void {
|
|
|
|
|
const item = event.currentTarget as HTMLElement;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearCart(): void {
|
2026-02-26 23:09:20 +04:00
|
|
|
if (confirm(this.i18n.t('cart.confirmClear'))) {
|
2026-01-18 18:57:06 +04:00
|
|
|
this.cartService.clearCart();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 01:23:25 +04:00
|
|
|
readonly getMainImage = getMainImage;
|
|
|
|
|
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-01-18 18:57:06 +04:00
|
|
|
|
|
|
|
|
checkout(): void {
|
|
|
|
|
if (!this.termsAccepted) {
|
2026-02-26 23:09:20 +04:00
|
|
|
alert(this.i18n.t('cart.acceptTerms'));
|
2026-01-18 18:57:06 +04:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-28 17:18:24 +04:00
|
|
|
// Auth gate: require Telegram login before payment
|
|
|
|
|
if (!this.authService.isAuthenticated()) {
|
|
|
|
|
this.authService.requestLogin();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-18 18:57:06 +04:00
|
|
|
this.openPaymentPopup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
openPaymentPopup(): void {
|
|
|
|
|
this.showPaymentPopup.set(true);
|
|
|
|
|
this.paymentStatus.set('creating');
|
2026-06-02 00:57:36 +04:00
|
|
|
this.paymentBackend = null;
|
|
|
|
|
this.paymentId.set('');
|
|
|
|
|
this.qrCodeUrl.set('');
|
|
|
|
|
this.paymentUrl.set('');
|
|
|
|
|
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.paidItems = [...this.items()];
|
|
|
|
|
this.createPayment();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closePaymentPopup(): void {
|
|
|
|
|
this.showPaymentPopup.set(false);
|
|
|
|
|
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.paymentBackend = null;
|
|
|
|
|
this.paymentId.set('');
|
|
|
|
|
this.qrCodeUrl.set('');
|
|
|
|
|
this.paymentUrl.set('');
|
|
|
|
|
this.linkCopied.set(false);
|
|
|
|
|
this.createPayment();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createPayment(): void {
|
|
|
|
|
const sessionId = this.authService.session()?.sessionId || '';
|
2026-03-24 02:25:50 +04:00
|
|
|
const cartItems = this.items().map((item: CartItem) => ({
|
|
|
|
|
itemID: item.itemID,
|
|
|
|
|
quantity: item.quantity,
|
|
|
|
|
colour: item.colour || '',
|
|
|
|
|
size: item.size || '',
|
|
|
|
|
price: item.discount > 0
|
|
|
|
|
? item.price * (1 - item.discount / 100)
|
|
|
|
|
: item.price,
|
|
|
|
|
}));
|
2026-01-18 18:57:06 +04:00
|
|
|
|
2026-06-02 00:57:36 +04:00
|
|
|
const paymentData = {
|
|
|
|
|
amount: this.totalPrice(),
|
|
|
|
|
currency: this.currentCurrency,
|
|
|
|
|
siteuserID: this.getPaymentUserId(),
|
|
|
|
|
siteorderID: this.generateOrderId(),
|
|
|
|
|
redirectUrl: '',
|
|
|
|
|
telegramUsername: this.getTelegramUsername(),
|
|
|
|
|
items: this.items().map((item: CartItem) => ({
|
|
|
|
|
itemID: item.itemID,
|
|
|
|
|
price: item.discount > 0
|
|
|
|
|
? item.price * (1 - item.discount / 100)
|
|
|
|
|
: item.price,
|
|
|
|
|
name: item.name,
|
|
|
|
|
quantity: item.quantity,
|
|
|
|
|
}))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const syncCart$ = sessionId
|
|
|
|
|
? this.apiService.addToCart(sessionId, cartItems).pipe(
|
|
|
|
|
catchError((err) => {
|
|
|
|
|
console.error('Error syncing cart:', err);
|
|
|
|
|
return of(null);
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
: of(null);
|
|
|
|
|
|
|
|
|
|
syncCart$
|
|
|
|
|
.pipe(
|
|
|
|
|
switchMap(() => this.apiService.createPayment(paymentData, sessionId || undefined))
|
|
|
|
|
)
|
|
|
|
|
.subscribe({
|
2026-03-24 02:25:50 +04:00
|
|
|
next: (response) => {
|
2026-06-02 00:57:36 +04:00
|
|
|
const qrId = this.apiService.resolvePaymentQrId(response.response);
|
|
|
|
|
const qrUrl = this.apiService.resolvePaymentQrUrl(response.response);
|
|
|
|
|
|
|
|
|
|
if (!qrId || !qrUrl) {
|
|
|
|
|
console.error('Payment response missing qr fields:', response.response);
|
|
|
|
|
this.setPaymentError();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.paymentBackend = response.backend;
|
|
|
|
|
this.paymentId.set(qrId);
|
|
|
|
|
this.qrCodeUrl.set(qrUrl);
|
|
|
|
|
this.paymentUrl.set(this.apiService.resolvePaymentLink(response.response));
|
2026-03-24 02:25:50 +04:00
|
|
|
this.paymentStatus.set('waiting');
|
|
|
|
|
this.startPolling();
|
|
|
|
|
},
|
|
|
|
|
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(): void {
|
2026-03-06 17:45:34 +04:00
|
|
|
this.stopPolling();
|
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), // maximum 36 checks (3 minutes)
|
|
|
|
|
switchMap(() => {
|
2026-03-24 02:25:50 +04:00
|
|
|
const sessionId = this.authService.session()?.sessionId || '';
|
2026-06-02 00:57:36 +04:00
|
|
|
return this.apiService.checkPaymentStatus(this.paymentId(), {
|
|
|
|
|
sessionId: sessionId || undefined,
|
|
|
|
|
backend: this.paymentBackend ?? undefined,
|
|
|
|
|
}).pipe(
|
|
|
|
|
catchError((err) => {
|
|
|
|
|
console.error('Error checking payment status:', err);
|
|
|
|
|
return of(null);
|
|
|
|
|
})
|
|
|
|
|
);
|
2026-01-18 18:57:06 +04:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.subscribe({
|
|
|
|
|
next: (response) => {
|
2026-06-02 00:57:36 +04:00
|
|
|
if (!response) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const paymentStatus = response.paymentStatus?.toUpperCase();
|
|
|
|
|
const paymentCode = response.code?.toUpperCase();
|
|
|
|
|
|
|
|
|
|
if (paymentStatus === 'EXPIRED' || paymentStatus === 'CANCELLED') {
|
|
|
|
|
this.paymentStatus.set('timeout');
|
|
|
|
|
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-02 00:57:36 +04:00
|
|
|
if ((paymentStatus === 'COMPLETED' || paymentStatus === 'SUCCESS') && paymentCode === 'SUCCESS') {
|
2026-01-18 18:57:06 +04:00
|
|
|
this.paymentStatus.set('success');
|
|
|
|
|
this.stopPolling();
|
|
|
|
|
// Clear cart but don't close popup - wait for email submission
|
|
|
|
|
this.cartService.clearCart();
|
|
|
|
|
}
|
|
|
|
|
// 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');
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setPaymentError(): void {
|
|
|
|
|
this.paymentStatus.set('error');
|
|
|
|
|
this.stopPolling();
|
|
|
|
|
this.paymentBackend = null;
|
|
|
|
|
if (this.closeTimeout) {
|
|
|
|
|
clearTimeout(this.closeTimeout);
|
|
|
|
|
this.closeTimeout = undefined;
|
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,
|
|
|
|
|
quantity: item.quantity
|
|
|
|
|
}))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.apiService.submitPurchaseEmail(emailData).subscribe({
|
|
|
|
|
next: () => {
|
|
|
|
|
this.emailSubmitting.set(false);
|
|
|
|
|
// Show success message
|
2026-02-26 23:09:20 +04:00
|
|
|
alert(this.i18n.t('cart.emailSuccess'));
|
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);
|
2026-02-26 23:09:20 +04:00
|
|
|
alert(this.i18n.t('cart.emailError'));
|
2026-01-18 18:57:06 +04:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getTelegramUserId(): string | null {
|
2026-06-02 00:57:36 +04:00
|
|
|
const sessionTelegramUserId = this.authService.session()?.telegramUserId;
|
|
|
|
|
if (sessionTelegramUserId) {
|
|
|
|
|
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 generateOrderId(): string {
|
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
const random = Math.random().toString(36).substring(2, 8);
|
|
|
|
|
return `order_${timestamp}_${random}`;
|
|
|
|
|
}
|
|
|
|
|
|
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('');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|