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

441 lines
14 KiB
TypeScript
Raw Normal View History

2026-02-19 01:23:25 +04:00
import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy, OnInit } from '@angular/core';
import { DecimalPipe } from '@angular/common';
2026-01-18 18:57:06 +04:00
import { Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { CartService, ApiService } from '../../services';
import { Item, CartItem } from '../../models';
import { interval, Subscription } from 'rxjs';
import { switchMap, take } from 'rxjs/operators';
import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component';
import { environment } from '../../../environments/environment';
2026-02-19 01:23:25 +04:00
import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils';
2026-01-18 18:57:06 +04:00
@Component({
selector: 'app-cart',
2026-02-19 01:23:25 +04:00
imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent],
2026-01-18 18:57:06 +04:00
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CartComponent implements OnInit, OnDestroy {
items;
itemCount;
totalPrice;
termsAccepted = false;
isnovo = environment.theme === 'novo';
// Swipe state
swipedItemId = signal<number | null>(null);
// Payment popup states
showPaymentPopup = signal<boolean>(false);
paymentStatus = signal<'creating' | 'waiting' | 'success' | 'timeout'>('creating');
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[] = [];
maxChecks = 36; // 36 checks * 5 seconds = 180 seconds (3 minutes)
private pollingSubscription?: Subscription;
private closeTimeout?: ReturnType<typeof setTimeout>;
constructor(
private cartService: CartService,
private apiService: ApiService,
private router: Router
) {
this.items = this.cartService.items;
this.itemCount = this.cartService.itemCount;
this.totalPrice = this.cartService.totalPrice;
}
ngOnInit(): void {
// Component initialized
}
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 = () => {
document.removeEventListener('touchmove', onMove as any);
document.removeEventListener('touchend', cleanup);
};
document.addEventListener('touchmove', onMove as any);
document.addEventListener('touchend', cleanup);
}
clearCart(): void {
2026-02-19 01:23:25 +04:00
if (confirm('<27><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?')) {
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-01-18 18:57:06 +04:00
checkout(): void {
if (!this.termsAccepted) {
2026-02-19 01:23:25 +04:00
alert('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>.');
2026-01-18 18:57:06 +04:00
return;
}
this.openPaymentPopup();
}
openPaymentPopup(): void {
this.showPaymentPopup.set(true);
this.paymentStatus.set('creating');
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;
}
}
createPayment(): void {
const telegramUsername = this.getTelegramUsername();
const userId = this.getUserId();
const orderId = this.generateOrderId();
const paymentData = {
amount: this.totalPrice(),
currency: 'RUB',
siteuserID: userId,
siteorderID: orderId,
redirectUrl: '',
telegramUsername: telegramUsername,
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
}))
};
this.apiService.createPayment(paymentData).subscribe({
next: (response) => {
this.paymentId.set(response.qrId);
this.qrCodeUrl.set(response.qrUrl);
this.paymentUrl.set(response.payload);
this.paymentStatus.set('waiting');
this.startPolling();
},
error: (err) => {
console.error('Error creating payment:', err);
this.paymentStatus.set('timeout');
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, 4000);
}
});
}
startPolling(): void {
this.pollingSubscription = interval(5000) // every 5 seconds
.pipe(
take(this.maxChecks), // maximum 36 checks (3 minutes)
switchMap(() => {
return this.apiService.checkPaymentStatus(this.paymentId());
})
)
.subscribe({
next: (response) => {
// Check if payment is successful
if (response.paymentStatus === 'SUCCESS' && response.code === 'SUCCESS') {
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
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, 3000);
}
},
error: (err) => {
console.error('Error checking payment status:', err);
// Continue checking even on error until time runs out
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, 3000);
}
});
}
stopPolling(): void {
if (this.pollingSubscription) {
this.pollingSubscription.unsubscribe();
}
}
copyPaymentLink(): void {
const url = this.paymentUrl();
if (url) {
navigator.clipboard.writeText(url).then(() => {
this.linkCopied.set(true);
setTimeout(() => this.linkCopied.set(false), 2000);
}).catch(err => {
2026-02-19 01:23:25 +04:00
console.error('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', err);
2026-01-18 18:57:06 +04:00
});
}
}
private getTelegramUsername(): string {
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
const user = window.Telegram.WebApp.initDataUnsafe.user;
return user.username || 'nontelegram';
}
return 'nontelegram';
}
private getUserId(): string {
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
return window.Telegram.WebApp.initDataUnsafe.user.id.toString();
}
return `web_${Date.now()}`;
}
private generateOrderId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `order_${timestamp}_${random}`;
}
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-19 01:23:25 +04:00
alert('Email <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>! <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>.');
2026-01-18 18:57:06 +04:00
// Close popup and redirect to home page
setTimeout(() => {
this.closePaymentPopup();
this.router.navigate(['/']);
}, 500);
},
error: (err) => {
console.error('Error submitting email:', err);
this.emailSubmitting.set(false);
2026-02-19 01:23:25 +04:00
alert('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> email. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>.');
2026-01-18 18:57:06 +04:00
}
});
}
private getTelegramUserId(): string | null {
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
return window.Telegram.WebApp.initDataUnsafe.user.id.toString();
}
return null;
}
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-19 01:23:25 +04:00
this.phoneError.set('<27><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>');
2026-01-18 18:57:06 +04:00
} else if (digitsOnly.length < 11) {
2026-02-19 01:23:25 +04:00
this.phoneError.set(`<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> ${11 - digitsOnly.length} <20><><EFBFBD><EFBFBD>`);
2026-01-18 18:57:06 +04:00
} else if (digitsOnly.length > 11) {
2026-02-19 01:23:25 +04:00
this.phoneError.set('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>');
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-19 01:23:25 +04:00
this.emailError.set('Email <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>');
2026-01-18 18:57:06 +04:00
} else if (email.length < 5) {
2026-02-19 01:23:25 +04:00
this.emailError.set('Email <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 5 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)');
2026-01-18 18:57:06 +04:00
} else if (email.length > 100) {
2026-02-19 01:23:25 +04:00
this.emailError.set('Email <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 100 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)');
2026-01-18 18:57:06 +04:00
} else if (!email.includes('@')) {
2026-02-19 01:23:25 +04:00
this.emailError.set('Email <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> @');
2026-01-18 18:57:06 +04:00
} else if (!email.includes('.')) {
2026-02-19 01:23:25 +04:00
this.emailError.set('Email <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> (.com, .ru <20> <20>.<2E>.)');
2026-01-18 18:57:06 +04:00
} else {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
2026-02-19 01:23:25 +04:00
this.emailError.set('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> email');
2026-01-18 18:57:06 +04:00
} else {
this.emailError.set('');
}
}
}
}