api change
This commit is contained in:
@@ -29,11 +29,11 @@
|
|||||||
{{ 'auth.loginWithTelegram' | translate }}
|
{{ 'auth.loginWithTelegram' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if (loginUrl()) {
|
<!-- @if (loginUrl()) {
|
||||||
<a class="bot-link" [href]="loginUrl()" target="_blank" rel="noopener noreferrer">
|
<a class="bot-link" [href]="loginUrl()" target="_blank" rel="noopener noreferrer">
|
||||||
{{ loginUrl() }}
|
{{ loginUrl() }}
|
||||||
</a>
|
</a>
|
||||||
}
|
} -->
|
||||||
|
|
||||||
<div class="qr-section">
|
<div class="qr-section">
|
||||||
<p class="qr-hint">{{ 'auth.orScanQr' | translate }}</p>
|
<p class="qr-hint">{{ 'auth.orScanQr' | translate }}</p>
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ export const en: Translations = {
|
|||||||
paymentSuccessDesc: 'Enter your contact details and we will send your purchase within a few minutes',
|
paymentSuccessDesc: 'Enter your contact details and we will send your purchase within a few minutes',
|
||||||
sending: 'Sending...',
|
sending: 'Sending...',
|
||||||
send: 'Send',
|
send: 'Send',
|
||||||
|
paymentError: 'Unable to create payment',
|
||||||
|
paymentErrorDesc: 'We could not prepare the payment QR code right now. Please try again in a moment.',
|
||||||
|
retryPayment: 'Try again',
|
||||||
paymentTimeout: 'Payment timed out',
|
paymentTimeout: 'Payment timed out',
|
||||||
paymentTimeoutDesc: 'We did not receive payment confirmation within 3 minutes.',
|
paymentTimeoutDesc: 'We did not receive payment confirmation within 3 minutes.',
|
||||||
autoClose: 'Window will close automatically...',
|
autoClose: 'Window will close automatically...',
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ export const hy: Translations = {
|
|||||||
paymentSuccessDesc: 'Մուտքագրեք ձեր տվյալները, և մենք կուղարկենք գնումը մի քանի րոպեի ընթացքում',
|
paymentSuccessDesc: 'Մուտքագրեք ձեր տվյալները, և մենք կուղարկենք գնումը մի քանի րոպեի ընթացքում',
|
||||||
sending: 'Ուղարկվում է...',
|
sending: 'Ուղարկվում է...',
|
||||||
send: 'Ուղարկել',
|
send: 'Ուղարկել',
|
||||||
|
paymentError: 'Չհաջողվեց ստեղծել վճարումը',
|
||||||
|
paymentErrorDesc: 'Այս պահին չհաջողվեց պատրաստել վճարման QR կոդը։ Խնդրում ենք մի փոքր հետո կրկին փորձել։',
|
||||||
|
retryPayment: 'Փորձել կրկին',
|
||||||
paymentTimeout: 'Ժամանակը սպառվեց',
|
paymentTimeout: 'Ժամանակը սպառվեց',
|
||||||
paymentTimeoutDesc: 'Մենք չստացանք վճարման հաստատում 3 րոպեի ընթացքում։',
|
paymentTimeoutDesc: 'Մենք չստացանք վճարման հաստատում 3 րոպեի ընթացքում։',
|
||||||
autoClose: 'Պատուհանը կփակվի ավտոմատ...',
|
autoClose: 'Պատուհանը կփակվի ավտոմատ...',
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ export const ru: Translations = {
|
|||||||
paymentSuccessDesc: 'Введите ваши контактные данные, и мы отправим вам покупку в течение нескольких минут',
|
paymentSuccessDesc: 'Введите ваши контактные данные, и мы отправим вам покупку в течение нескольких минут',
|
||||||
sending: 'Отправка...',
|
sending: 'Отправка...',
|
||||||
send: 'Отправить',
|
send: 'Отправить',
|
||||||
|
paymentError: 'Не удалось создать платеж',
|
||||||
|
paymentErrorDesc: 'Сейчас не получилось подготовить QR-код для оплаты. Попробуйте еще раз через минуту.',
|
||||||
|
retryPayment: 'Попробовать снова',
|
||||||
paymentTimeout: 'Время ожидания истекло',
|
paymentTimeout: 'Время ожидания истекло',
|
||||||
paymentTimeoutDesc: 'Мы не получили подтверждение оплаты в течение 3 минут.',
|
paymentTimeoutDesc: 'Мы не получили подтверждение оплаты в течение 3 минут.',
|
||||||
autoClose: 'Окно закроется автоматически...',
|
autoClose: 'Окно закроется автоматически...',
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ export interface Translations {
|
|||||||
paymentSuccessDesc: string;
|
paymentSuccessDesc: string;
|
||||||
sending: string;
|
sending: string;
|
||||||
send: string;
|
send: string;
|
||||||
|
paymentError: string;
|
||||||
|
paymentErrorDesc: string;
|
||||||
|
retryPayment: string;
|
||||||
paymentTimeout: string;
|
paymentTimeout: string;
|
||||||
paymentTimeoutDesc: string;
|
paymentTimeoutDesc: string;
|
||||||
autoClose: string;
|
autoClose: string;
|
||||||
|
|||||||
@@ -795,11 +795,41 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
|
|||||||
qrId: 'mock-qr-' + Date.now(),
|
qrId: 'mock-qr-' + Date.now(),
|
||||||
qrStatus: 'NEW',
|
qrStatus: 'NEW',
|
||||||
qrExpirationDate: new Date(Date.now() + 180000).toISOString(),
|
qrExpirationDate: new Date(Date.now() + 180000).toISOString(),
|
||||||
Payload: 'https://example.com/pay/mock',
|
payload: 'https://example.com/pay/mock',
|
||||||
qrUrl: 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=mock-payment'
|
qrUrl: 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=mock-payment'
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── POST /qr (create payment QR directly)
|
||||||
|
if (url.endsWith('/qr') && req.method === 'POST') {
|
||||||
|
return respond({
|
||||||
|
qrId: 'mock-qr-' + Date.now(),
|
||||||
|
qrStatus: 'NEW',
|
||||||
|
qrExpirationDate: new Date(Date.now() + 180000).toISOString(),
|
||||||
|
payload: 'https://example.com/pay/mock',
|
||||||
|
qrUrl: 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=mock-payment'
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POST /cart (legacy create payment QR)
|
||||||
|
if (url.endsWith('/cart') && req.method === 'POST') {
|
||||||
|
const body = req.body;
|
||||||
|
const looksLikePaymentRequest = !!body
|
||||||
|
&& typeof body === 'object'
|
||||||
|
&& !Array.isArray(body)
|
||||||
|
&& 'amount' in body
|
||||||
|
&& 'items' in body;
|
||||||
|
if (looksLikePaymentRequest) {
|
||||||
|
return respond({
|
||||||
|
qrId: 'mock-qr-' + Date.now(),
|
||||||
|
qrStatus: 'NEW',
|
||||||
|
qrExpirationDate: new Date(Date.now() + 180000).toISOString(),
|
||||||
|
payload: 'https://example.com/pay/mock',
|
||||||
|
qrUrl: 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=mock-payment'
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── POST /items/:id/callback (review)
|
// ── POST /items/:id/callback (review)
|
||||||
if (url.match(/\/items\/\d+\/callback$/) && req.method === 'POST') {
|
if (url.match(/\/items\/\d+\/callback$/) && req.method === 'POST') {
|
||||||
return respond({ message: 'Review submitted (mock)' }, 200);
|
return respond({ message: 'Review submitted (mock)' }, 200);
|
||||||
@@ -813,7 +843,7 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
|
|||||||
// ── GET /websession/:id/:qrId (check QR payment status)
|
// ── GET /websession/:id/:qrId (check QR payment status)
|
||||||
if (url.match(/\/websession\/[^/]+\/[^/]+$/) && !url.match(/\/websession\/[^/]+\/qr$/) && req.method === 'GET') {
|
if (url.match(/\/websession\/[^/]+\/[^/]+$/) && !url.match(/\/websession\/[^/]+\/qr$/) && req.method === 'GET') {
|
||||||
return respond({
|
return respond({
|
||||||
paymentStatus: 'SUCCESS',
|
paymentStatus: 'COMPLETED',
|
||||||
code: 'SUCCESS',
|
code: 'SUCCESS',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
@@ -828,6 +858,25 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── GET /qr/payment/:qrId (legacy/direct payment status)
|
||||||
|
if (url.match(/\/qr\/payment\/[^/]+$/) && req.method === 'GET') {
|
||||||
|
return respond({
|
||||||
|
paymentStatus: 'COMPLETED',
|
||||||
|
code: 'SUCCESS',
|
||||||
|
amount: 0,
|
||||||
|
currency: 'RUB',
|
||||||
|
qrId: 'mock',
|
||||||
|
transactionId: 999,
|
||||||
|
transactionDate: new Date().toISOString(),
|
||||||
|
additionalInfo: '',
|
||||||
|
paymentPurpose: '',
|
||||||
|
createDate: new Date().toISOString(),
|
||||||
|
order: 'mock-order',
|
||||||
|
qrExpirationDate: new Date().toISOString(),
|
||||||
|
phoneNumber: '+70000000000'
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback — pass through
|
// Fallback — pass through
|
||||||
return next(req);
|
return next(req);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -227,6 +227,20 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (paymentStatus() === 'error') {
|
||||||
|
<div class="payment-status-screen error">
|
||||||
|
<div class="error-icon">!</div>
|
||||||
|
<h2>{{ 'cart.paymentError' | translate }}</h2>
|
||||||
|
<p>{{ 'cart.paymentErrorDesc' | translate }}</p>
|
||||||
|
|
||||||
|
<div class="payment-error-actions">
|
||||||
|
<button class="retry-payment-btn" (click)="retryPayment()">
|
||||||
|
{{ 'cart.retryPayment' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (paymentStatus() === 'success') {
|
@if (paymentStatus() === 'success') {
|
||||||
<div class="payment-status-screen success">
|
<div class="payment-status-screen success">
|
||||||
<div class="success-icon">✓</div>
|
<div class="success-icon">✓</div>
|
||||||
|
|||||||
@@ -1196,6 +1196,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
.error-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: #f97316;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-error-actions {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-payment-btn {
|
||||||
|
padding: 14px 24px;
|
||||||
|
background: #497671;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 13px;
|
||||||
|
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #3a5f5b;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(73, 118, 113, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.timeout {
|
&.timeout {
|
||||||
.timeout-icon {
|
.timeout-icon {
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Router, RouterLink } from '@angular/router';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { CartService, ApiService, LanguageService, AuthService } from '../../services';
|
import { CartService, ApiService, LanguageService, AuthService } from '../../services';
|
||||||
import { Item, CartItem } from '../../models';
|
import { Item, CartItem } from '../../models';
|
||||||
import { interval, Subscription } from 'rxjs';
|
import { interval, of, Subscription } from 'rxjs';
|
||||||
import { switchMap, take } from 'rxjs/operators';
|
import { catchError, switchMap, take } from 'rxjs/operators';
|
||||||
import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component';
|
import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component';
|
||||||
import { TelegramLoginComponent } from '../../components/telegram-login/telegram-login.component';
|
import { TelegramLoginComponent } from '../../components/telegram-login/telegram-login.component';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
@@ -39,7 +39,7 @@ export class CartComponent implements OnDestroy {
|
|||||||
|
|
||||||
// Payment popup states
|
// Payment popup states
|
||||||
showPaymentPopup = signal<boolean>(false);
|
showPaymentPopup = signal<boolean>(false);
|
||||||
paymentStatus = signal<'creating' | 'waiting' | 'success' | 'timeout'>('creating');
|
paymentStatus = signal<'creating' | 'waiting' | 'success' | 'timeout' | 'error'>('creating');
|
||||||
qrCodeUrl = signal<string>('');
|
qrCodeUrl = signal<string>('');
|
||||||
paymentUrl = signal<string>('');
|
paymentUrl = signal<string>('');
|
||||||
paymentId = signal<string>('');
|
paymentId = signal<string>('');
|
||||||
@@ -58,6 +58,7 @@ export class CartComponent implements OnDestroy {
|
|||||||
maxChecks = PAYMENT_MAX_CHECKS;
|
maxChecks = PAYMENT_MAX_CHECKS;
|
||||||
private pollingSubscription?: Subscription;
|
private pollingSubscription?: Subscription;
|
||||||
private closeTimeout?: ReturnType<typeof setTimeout>;
|
private closeTimeout?: ReturnType<typeof setTimeout>;
|
||||||
|
private paymentBackend: 'websession' | 'qr' | 'cart' | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cartService: CartService,
|
private cartService: CartService,
|
||||||
@@ -159,6 +160,11 @@ export class CartComponent implements OnDestroy {
|
|||||||
openPaymentPopup(): void {
|
openPaymentPopup(): void {
|
||||||
this.showPaymentPopup.set(true);
|
this.showPaymentPopup.set(true);
|
||||||
this.paymentStatus.set('creating');
|
this.paymentStatus.set('creating');
|
||||||
|
this.paymentBackend = null;
|
||||||
|
this.paymentId.set('');
|
||||||
|
this.qrCodeUrl.set('');
|
||||||
|
this.paymentUrl.set('');
|
||||||
|
this.linkCopied.set(false);
|
||||||
this.userEmail.set('');
|
this.userEmail.set('');
|
||||||
this.userPhone.set('');
|
this.userPhone.set('');
|
||||||
this.emailTouched.set(false);
|
this.emailTouched.set(false);
|
||||||
@@ -179,14 +185,23 @@ export class CartComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createPayment(): void {
|
retryPayment(): void {
|
||||||
const sessionId = this.authService.session()?.sessionId || '';
|
if (this.closeTimeout) {
|
||||||
if (!sessionId) {
|
clearTimeout(this.closeTimeout);
|
||||||
this.paymentStatus.set('timeout');
|
this.closeTimeout = undefined;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// First sync cart items to server via websession, then create QR
|
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 || '';
|
||||||
const cartItems = this.items().map((item: CartItem) => ({
|
const cartItems = this.items().map((item: CartItem) => ({
|
||||||
itemID: item.itemID,
|
itemID: item.itemID,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
@@ -197,35 +212,59 @@ export class CartComponent implements OnDestroy {
|
|||||||
: item.price,
|
: item.price,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.apiService.addToCart(sessionId, cartItems).subscribe({
|
const paymentData = {
|
||||||
next: () => {
|
amount: this.totalPrice(),
|
||||||
this.apiService.createPayment(sessionId).subscribe({
|
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({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.paymentId.set(response.qrId);
|
const qrId = this.apiService.resolvePaymentQrId(response.response);
|
||||||
this.qrCodeUrl.set(response.qrUrl);
|
const qrUrl = this.apiService.resolvePaymentQrUrl(response.response);
|
||||||
this.paymentUrl.set(response.Payload);
|
|
||||||
|
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));
|
||||||
this.paymentStatus.set('waiting');
|
this.paymentStatus.set('waiting');
|
||||||
this.startPolling();
|
this.startPolling();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Error creating payment:', err);
|
console.error('Error creating payment:', err);
|
||||||
this.paymentStatus.set('timeout');
|
this.setPaymentError();
|
||||||
if (this.closeTimeout) clearTimeout(this.closeTimeout);
|
|
||||||
this.closeTimeout = setTimeout(() => {
|
|
||||||
this.closePaymentPopup();
|
|
||||||
}, PAYMENT_ERROR_CLOSE_MS);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
console.error('Error syncing cart:', err);
|
|
||||||
this.paymentStatus.set('timeout');
|
|
||||||
if (this.closeTimeout) clearTimeout(this.closeTimeout);
|
|
||||||
this.closeTimeout = setTimeout(() => {
|
|
||||||
this.closePaymentPopup();
|
|
||||||
}, PAYMENT_ERROR_CLOSE_MS);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startPolling(): void {
|
startPolling(): void {
|
||||||
@@ -235,13 +274,38 @@ export class CartComponent implements OnDestroy {
|
|||||||
take(this.maxChecks), // maximum 36 checks (3 minutes)
|
take(this.maxChecks), // maximum 36 checks (3 minutes)
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
const sessionId = this.authService.session()?.sessionId || '';
|
const sessionId = this.authService.session()?.sessionId || '';
|
||||||
return this.apiService.checkPaymentStatus(sessionId, this.paymentId());
|
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if payment is successful
|
// Check if payment is successful
|
||||||
if (response.paymentStatus === 'SUCCESS' && response.code === 'SUCCESS') {
|
if ((paymentStatus === 'COMPLETED' || paymentStatus === 'SUCCESS') && paymentCode === 'SUCCESS') {
|
||||||
this.paymentStatus.set('success');
|
this.paymentStatus.set('success');
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
// Clear cart but don't close popup - wait for email submission
|
// Clear cart but don't close popup - wait for email submission
|
||||||
@@ -260,14 +324,6 @@ export class CartComponent implements OnDestroy {
|
|||||||
this.closePaymentPopup();
|
this.closePaymentPopup();
|
||||||
}, PAYMENT_TIMEOUT_CLOSE_MS);
|
}, PAYMENT_TIMEOUT_CLOSE_MS);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
console.error('Error checking payment status:', err);
|
|
||||||
// Continue checking even on error until time runs out
|
|
||||||
if (this.closeTimeout) clearTimeout(this.closeTimeout);
|
|
||||||
this.closeTimeout = setTimeout(() => {
|
|
||||||
this.closePaymentPopup();
|
|
||||||
}, PAYMENT_TIMEOUT_CLOSE_MS);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -275,6 +331,17 @@ export class CartComponent implements OnDestroy {
|
|||||||
stopPolling(): void {
|
stopPolling(): void {
|
||||||
if (this.pollingSubscription) {
|
if (this.pollingSubscription) {
|
||||||
this.pollingSubscription.unsubscribe();
|
this.pollingSubscription.unsubscribe();
|
||||||
|
this.pollingSubscription = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setPaymentError(): void {
|
||||||
|
this.paymentStatus.set('error');
|
||||||
|
this.stopPolling();
|
||||||
|
this.paymentBackend = null;
|
||||||
|
if (this.closeTimeout) {
|
||||||
|
clearTimeout(this.closeTimeout);
|
||||||
|
this.closeTimeout = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,12 +413,41 @@ export class CartComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getTelegramUserId(): string | null {
|
private getTelegramUserId(): string | null {
|
||||||
|
const sessionTelegramUserId = this.authService.session()?.telegramUserId;
|
||||||
|
if (sessionTelegramUserId) {
|
||||||
|
return sessionTelegramUserId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
|
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
|
||||||
return window.Telegram.WebApp.initDataUnsafe.user.id.toString();
|
return window.Telegram.WebApp.initDataUnsafe.user.id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
onPhoneInput(event: Event): void {
|
onPhoneInput(event: Event): void {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
let value = input.value.replace(/\D/g, ''); // Remove all non-digits
|
let value = input.value.replace(/\D/g, ''); // Remove all non-digits
|
||||||
|
|||||||
@@ -1,10 +1,62 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable, timer } from 'rxjs';
|
import { Observable, timer } from 'rxjs';
|
||||||
import { map, retry } from 'rxjs/operators';
|
import { catchError, map, retry } from 'rxjs/operators';
|
||||||
import { Category, Item, Subcategory } from '../models';
|
import { Category, Item, Subcategory } from '../models';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
|
type PaymentBackend = 'websession' | 'qr' | 'cart';
|
||||||
|
|
||||||
|
interface PaymentRequestItem {
|
||||||
|
itemID: number;
|
||||||
|
price: number;
|
||||||
|
name: string;
|
||||||
|
quantity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentRequest {
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
siteuserID: string;
|
||||||
|
siteorderID: string;
|
||||||
|
redirectUrl: string;
|
||||||
|
telegramUsername: string;
|
||||||
|
items: PaymentRequestItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentCreateResponse {
|
||||||
|
qrId?: string;
|
||||||
|
qrID?: string;
|
||||||
|
qrStatus?: string;
|
||||||
|
qrExpirationDate?: string;
|
||||||
|
payload?: string;
|
||||||
|
Payload?: string;
|
||||||
|
qrUrl?: string;
|
||||||
|
partnerID?: string | number;
|
||||||
|
partnerId?: string | number;
|
||||||
|
PartnerID?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentCreateResult {
|
||||||
|
backend: PaymentBackend;
|
||||||
|
response: PaymentCreateResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentStatusResponse {
|
||||||
|
additionalInfo: string;
|
||||||
|
paymentPurpose: string;
|
||||||
|
amount: number;
|
||||||
|
code: string;
|
||||||
|
createDate: string;
|
||||||
|
currency: string;
|
||||||
|
order: string;
|
||||||
|
paymentStatus: string;
|
||||||
|
qrId: string;
|
||||||
|
transactionDate: string;
|
||||||
|
transactionId: number;
|
||||||
|
qrExpirationDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
@@ -348,51 +400,74 @@ export class ApiService {
|
|||||||
return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/questiion`, body);
|
return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/questiion`, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payment - SBP Integration via websession QR
|
createPayment(paymentData: PaymentRequest, sessionId?: string): Observable<PaymentCreateResult> {
|
||||||
createPayment(sessionId: string): Observable<{
|
const directQrPayment$ = this.http.post<PaymentCreateResponse>(`${this.baseUrl}/qr`, paymentData).pipe(
|
||||||
qrId: string;
|
map(response => ({ backend: 'qr' as const, response }))
|
||||||
qrStatus: string;
|
);
|
||||||
qrExpirationDate: string;
|
const legacyCartPayment$ = this.http.post<PaymentCreateResponse>(`${this.baseUrl}/cart`, paymentData).pipe(
|
||||||
Payload: string;
|
map(response => ({ backend: 'cart' as const, response }))
|
||||||
qrUrl: string;
|
);
|
||||||
}> {
|
const directPayment$ = directQrPayment$.pipe(
|
||||||
return this.http.post<{
|
catchError(() => legacyCartPayment$)
|
||||||
qrId: string;
|
);
|
||||||
qrStatus: string;
|
|
||||||
qrExpirationDate: string;
|
if (!sessionId) {
|
||||||
Payload: string;
|
return directPayment$;
|
||||||
qrUrl: string;
|
}
|
||||||
}>(`${this.baseUrl}/websession/${sessionId}/qr`, {});
|
|
||||||
|
return this.http.post<PaymentCreateResponse>(`${this.baseUrl}/websession/${sessionId}/qr`, {}).pipe(
|
||||||
|
map(response => ({ backend: 'websession' as const, response })),
|
||||||
|
catchError(() => directPayment$)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkPaymentStatus(sessionId: string, qrId: string): Observable<{
|
checkPaymentStatus(qrId: string, options?: { sessionId?: string; backend?: PaymentBackend }): Observable<PaymentStatusResponse> {
|
||||||
additionalInfo: string;
|
const legacyStatus$ = this.http.get<PaymentStatusResponse>(`${this.baseUrl}/qr/payment/${qrId}`);
|
||||||
paymentPurpose: string;
|
|
||||||
amount: number;
|
if (options?.backend === 'websession' && options.sessionId) {
|
||||||
code: string;
|
return this.http.get<PaymentStatusResponse>(`${this.baseUrl}/websession/${options.sessionId}/${qrId}`).pipe(
|
||||||
createDate: string;
|
catchError(() => legacyStatus$)
|
||||||
currency: string;
|
);
|
||||||
order: string;
|
}
|
||||||
paymentStatus: string;
|
|
||||||
qrId: string;
|
if (options?.backend === 'qr' || options?.backend === 'cart') {
|
||||||
transactionDate: string;
|
return legacyStatus$;
|
||||||
transactionId: number;
|
}
|
||||||
qrExpirationDate: string;
|
|
||||||
}> {
|
if (options?.sessionId) {
|
||||||
return this.http.get<{
|
return this.http.get<PaymentStatusResponse>(`${this.baseUrl}/websession/${options.sessionId}/${qrId}`).pipe(
|
||||||
additionalInfo: string;
|
catchError(() => legacyStatus$)
|
||||||
paymentPurpose: string;
|
);
|
||||||
amount: number;
|
}
|
||||||
code: string;
|
|
||||||
createDate: string;
|
return legacyStatus$;
|
||||||
currency: string;
|
}
|
||||||
order: string;
|
|
||||||
paymentStatus: string;
|
resolvePaymentQrId(response: PaymentCreateResponse): string {
|
||||||
qrId: string;
|
return response.qrId ?? response.qrID ?? '';
|
||||||
transactionDate: string;
|
}
|
||||||
transactionId: number;
|
|
||||||
qrExpirationDate: string;
|
resolvePaymentQrUrl(response: PaymentCreateResponse): string {
|
||||||
}>(`${this.baseUrl}/websession/${sessionId}/${qrId}`);
|
if (response.qrUrl) {
|
||||||
|
return response.qrUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qrId = this.resolvePaymentQrId(response);
|
||||||
|
const partnerId = response.partnerID ?? response.partnerId ?? response.PartnerID;
|
||||||
|
|
||||||
|
if (!qrId) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partnerId != null) {
|
||||||
|
return `${this.baseUrl}/qr/dynamic/${encodeURIComponent(String(partnerId))}/${encodeURIComponent(qrId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${this.baseUrl}/qr/static/${encodeURIComponent(qrId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvePaymentLink(response: PaymentCreateResponse): string {
|
||||||
|
return response.payload ?? response.Payload ?? this.resolvePaymentQrUrl(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
submitPurchaseEmail(emailData: {
|
submitPurchaseEmail(emailData: {
|
||||||
|
|||||||
Reference in New Issue
Block a user