From c233845ce07b1a5b4130a7fa02329cec6890250f Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 28 Jul 2026 16:57:00 -0300 Subject: [PATCH] feat: add WhatsApp support and improve payment timeout handling in checkout process --- .../checkout-page.component.html | 2 + .../checkout-page.component.spec.ts | 8 +- .../checkout-page/checkout-page.component.ts | 8 +- .../checkout-payment-step.component.html | 3 +- .../checkout-payment-step.component.ts | 3 +- .../checkout-payment-qr.component.html | 73 +++++++++++-------- .../checkout-payment-qr.component.scss | 37 ++++++++++ .../checkout-payment-qr.component.spec.ts | 19 +++++ .../checkout-payment-qr.component.ts | 24 +++++- 9 files changed, 136 insertions(+), 41 deletions(-) diff --git a/src/app/features/store/pages/checkout-page/checkout-page.component.html b/src/app/features/store/pages/checkout-page/checkout-page.component.html index 71b7365..f062cd9 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.component.html +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.html @@ -31,6 +31,8 @@ [qrData]="qrData()" [qrPaymentStatus]="qrPaymentStatus()" [isCheckingQrPayment]="isCheckingQrPayment()" + [qrPaymentAmount]="cartTotal()" + [whatsappUrl]="whatsappUrl()" [transferValidationStatus]="transferValidationStatus()" (paymentMethodChange)="selectPaymentMethod($event)" (copyTransferValue)="copyTransferValue($event.field, $event.value)" diff --git a/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts b/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts index af9b7d0..3186cad 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts @@ -160,19 +160,19 @@ describe('CheckoutPageComponent payment validation', () => { fixture.destroy(); }); - it('stops QR polling after five minutes and can restart it', async () => { + it('stops QR polling after 45 seconds and can restart it', async () => { const { component } = createComponent(); await component.selectPaymentMethod('qr'); - await vi.advanceTimersByTimeAsync(300_000); + await vi.advanceTimersByTimeAsync(45_000); - expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(60); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(9); expect(component.qrPaymentStatus()).toBe('timed_out'); component.retryQrPolling(); expect(component.qrPaymentStatus()).toBe('waiting'); await vi.advanceTimersByTimeAsync(5_000); - expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(61); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(10); }); it('cancels QR polling when payment method changes or component is destroyed', async () => { diff --git a/src/app/features/store/pages/checkout-page/checkout-page.component.ts b/src/app/features/store/pages/checkout-page/checkout-page.component.ts index 502a303..e5fa990 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.component.ts +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.ts @@ -59,7 +59,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { private readonly authService = inject(AuthService); private readonly cartService = inject(CartService); private readonly qrPollingIntervalMs = 5_000; - private readonly qrPollingMaxAttempts = 60; + private readonly qrPollingMaxAttempts = 9; private qrPollingTimeoutId: ReturnType | null = null; private qrPollingAttempts = 0; @@ -119,6 +119,12 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { protected readonly qrPaymentStatus = signal('idle'); protected readonly isCheckingQrPayment = signal(false); protected readonly transferValidationStatus = signal('idle'); + protected readonly whatsappUrl = computed( + () => + this.tenantService + .tenant() + ?.social_media?.find((socialMedia) => socialMedia.code === 'whatsapp')?.url ?? null, + ); constructor() { this.form.statusChanges diff --git a/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html b/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html index 43605e3..2778b24 100644 --- a/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html +++ b/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html @@ -44,7 +44,8 @@ [qrData]="qrData()" [paymentStatus]="qrPaymentStatus()" [isCheckingPayment]="isCheckingQrPayment()" - (retryPolling)="retryQrPolling.emit()" + [paymentAmount]="qrPaymentAmount()" + [whatsappUrl]="whatsappUrl()" /> } @else if (selectedPaymentMethod() === 'transfer') { (null); readonly qrPaymentStatus = input('idle'); readonly isCheckingQrPayment = input(false); + readonly qrPaymentAmount = input(0); + readonly whatsappUrl = input(null); readonly transferValidationStatus = input('idle'); readonly paymentMethodChange = output(); @@ -37,7 +39,6 @@ export class CheckoutPaymentStepComponent { readonly cancelStep = output(); readonly complete = output(); readonly generateTransferIntent = output(); - readonly retryQrPolling = output(); protected selectPaymentMethod(method: PaymentMethod): void { this.paymentMethodChange.emit(method); diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html index 75f74e2..0cfb35d 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html @@ -1,34 +1,47 @@
-

Ingresa a tu billetera y escanea el siguiente QR

-
- -
- @if (qrData()) { - - } @else { - - - - } - - @if (isCheckingPayment()) { -
- - Verificando pago -
- } -
- @if (paymentStatus() === 'timed_out') { -

- TodavĂ­a no recibimos el pago. -

- - Volver a verificar - - } @else if (paymentStatus() === 'failed') { - + + } @else { +

Ingresa a tu billetera y escanea el siguiente QR

+
+ +
+ @if (qrData()) { + + } @else { + + + + } + + @if (isCheckingPayment()) { +
+ + Verificando pago +
+ } +
+ + @if (paymentStatus() === 'failed') { + + } }
diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss index 5d74160..6ec30a9 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss @@ -116,6 +116,43 @@ } } +.payment-timeout { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding-top: 0.85rem; + + &__icon { + display: grid; + width: 84px; + height: 84px; + place-items: center; + border: 1px solid var(--tenant-danger, #dc3545); + border-radius: 50%; + color: var(--tenant-danger, #dc3545); + font-size: 2.7rem; + } + + &__title { + max-width: 250px; + margin: 1.35rem 0 0; + color: var(--tenant-danger, #dc3545); + font-size: 1.05rem; + font-weight: 700; + line-height: 1.3; + } + + &__message { + max-width: 250px; + margin: 2rem 0 1.75rem; + color: #666666; + font-size: 1rem; + line-height: 1.25; + } + +} + .payment-verification { position: absolute; inset: 0; diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts index 3bb8378..f6f31dd 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts @@ -27,4 +27,23 @@ describe('CheckoutPaymentQrComponent', () => { expect(overlay.textContent).toContain('Verificando pago'); expect(overlay.querySelector('.payment-verification__spinner')).not.toBeNull(); }); + + it('shows the payment verification error and WhatsApp action after timeout', async () => { + await TestBed.configureTestingModule({ + imports: [CheckoutPaymentQrComponent], + }).compileComponents(); + + const fixture = TestBed.createComponent(CheckoutPaymentQrComponent); + fixture.componentRef.setInput('paymentStatus', 'timed_out'); + fixture.componentRef.setInput('paymentAmount', 300000); + fixture.componentRef.setInput('whatsappUrl', 'https://wa.me/543412602222'); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + const whatsapp = element.querySelector('.payment-timeout__whatsapp'); + expect(element.textContent).toMatch(/No pudimos verificar el pago de \$\s*300\.000\./); + expect(element.textContent).toContain('Por favor contactate con nosotros para resolverlo.'); + expect(whatsapp?.href).toBe('https://wa.me/543412602222'); + expect(element.textContent).not.toContain('Volver a verificar'); + }); }); diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts index 4c98eab..93b19a1 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts @@ -1,12 +1,12 @@ -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { QRCodeComponent } from '../../../../../../shared/components/qrcode/qrcode.component'; +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { ButtonComponent } from '../../../../../../shared/components/button/button.component'; +import { QRCodeComponent } from '../../../../../../shared/components/qrcode/qrcode.component'; import { QrPaymentStatus } from '../../checkout-page.models'; @Component({ selector: 'app-checkout-payment-qr', standalone: true, - imports: [QRCodeComponent, ButtonComponent], + imports: [ButtonComponent, QRCodeComponent], templateUrl: './checkout-payment-qr.component.html', styleUrl: './checkout-payment-qr.component.scss', changeDetection: ChangeDetectionStrategy.OnPush @@ -15,5 +15,21 @@ export class CheckoutPaymentQrComponent { readonly qrData = input(null); readonly paymentStatus = input('idle'); readonly isCheckingPayment = input(false); - readonly retryPolling = output(); + readonly paymentAmount = input(0); + readonly whatsappUrl = input(null); + protected readonly formattedAmount = computed(() => + new Intl.NumberFormat('es-AR', { + style: 'currency', + currency: 'ARS', + maximumFractionDigits: 0, + }).format(this.paymentAmount()), + ); + + protected openWhatsApp(): void { + const url = this.whatsappUrl(); + + if (url) { + window.open(url, '_blank', 'noopener,noreferrer'); + } + } }