feat(checkout): implement mobile countdown display and logic
This commit is contained in:
@@ -179,7 +179,7 @@
|
|||||||
|
|
||||||
@if (checkoutRemainingTime(); as remainingTime) {
|
@if (checkoutRemainingTime(); as remainingTime) {
|
||||||
<div
|
<div
|
||||||
class="store-layout__checkout-timer ms-auto d-flex align-items-baseline gap-3"
|
class="store-layout__checkout-timer ms-auto d-none d-md-flex align-items-baseline gap-3"
|
||||||
role="timer"
|
role="timer"
|
||||||
aria-label="Tiempo restante de compra"
|
aria-label="Tiempo restante de compra"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -46,6 +46,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkout-page__cart-col">
|
<div class="checkout-page__cart-col">
|
||||||
|
@if (checkoutRemainingTime(); as remainingTime) {
|
||||||
|
<div
|
||||||
|
class="checkout-page__mobile-countdown d-flex d-md-none align-items-baseline justify-content-center gap-3"
|
||||||
|
role="timer"
|
||||||
|
aria-label="Tiempo restante de compra"
|
||||||
|
>
|
||||||
|
<span class="checkout-page__mobile-countdown-label">Tiempo restante de compra:</span>
|
||||||
|
<span class="checkout-page__mobile-countdown-value">{{ remainingTime }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<app-cart
|
<app-cart
|
||||||
title="COMPRA"
|
title="COMPRA"
|
||||||
[items]="mappedCartItems()"
|
[items]="mappedCartItems()"
|
||||||
|
|||||||
@@ -28,6 +28,24 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__mobile-countdown {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--tenant-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__mobile-countdown-label {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__mobile-countdown-value {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkout-page__loading {
|
.checkout-page__loading {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { TenantService } from '../../../../core/services/tenant.service';
|
|||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
import { CartEditingPolicy } from '../../../../core/services/tenant.interface';
|
import { CartEditingPolicy } from '../../../../core/services/tenant.interface';
|
||||||
|
import { CheckoutCountdownService } from '../../../../core/services/checkout-countdown.service';
|
||||||
import { CheckoutPageComponent } from './checkout-page.component';
|
import { CheckoutPageComponent } from './checkout-page.component';
|
||||||
|
|
||||||
describe('CheckoutPageComponent payment validation', () => {
|
describe('CheckoutPageComponent payment validation', () => {
|
||||||
@@ -129,6 +130,35 @@ describe('CheckoutPageComponent payment validation', () => {
|
|||||||
return { fixture, component: fixture.componentInstance as any };
|
return { fixture, component: fixture.componentInstance as any };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('does not expose an active countdown until the purchase timing is loaded', async () => {
|
||||||
|
const countdown = TestBed.inject(CheckoutCountdownService);
|
||||||
|
countdown.synchronize({
|
||||||
|
expires_at: null,
|
||||||
|
expires_in_seconds: 600,
|
||||||
|
server_time: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
checkoutServiceStub.getPurchase.mockResolvedValue({
|
||||||
|
id: 25,
|
||||||
|
status: 'created',
|
||||||
|
expires_at: '2026-08-27T15:10:00.000Z',
|
||||||
|
expires_in_seconds: 600,
|
||||||
|
server_time: '2026-08-27T15:00:00.000Z',
|
||||||
|
items: [],
|
||||||
|
subtotal: '0.00',
|
||||||
|
total: '0.00',
|
||||||
|
});
|
||||||
|
routeParamMap = convertToParamMap({ id: 25 });
|
||||||
|
|
||||||
|
const { component } = createComponent();
|
||||||
|
|
||||||
|
expect(component.checkoutRemainingTime()).toBeNull();
|
||||||
|
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(countdown.remainingSeconds()).toBe(600);
|
||||||
|
expect(component.checkoutRemainingTime()).toBe('10:00');
|
||||||
|
});
|
||||||
|
|
||||||
it('polls QR after five seconds and navigates only when payment is paid', async () => {
|
it('polls QR after five seconds and navigates only when payment is paid', async () => {
|
||||||
checkoutServiceStub.getPurchase
|
checkoutServiceStub.getPurchase
|
||||||
.mockResolvedValueOnce({ status: 'pending_payment' })
|
.mockResolvedValueOnce({ status: 'pending_payment' })
|
||||||
|
|||||||
@@ -97,6 +97,28 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
protected readonly createdPurchase = signal<PurchaseDetailResponse | null>(null);
|
protected readonly createdPurchase = signal<PurchaseDetailResponse | null>(null);
|
||||||
protected readonly isLoadingPurchase = signal(true);
|
protected readonly isLoadingPurchase = signal(true);
|
||||||
protected readonly checkoutStepIndex = signal(0);
|
protected readonly checkoutStepIndex = signal(0);
|
||||||
|
protected readonly checkoutRemainingTime = computed(() => {
|
||||||
|
const purchase = this.createdPurchase();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!purchase ||
|
||||||
|
(typeof purchase.expires_at !== 'string' &&
|
||||||
|
typeof purchase.expires_in_seconds !== 'number')
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingSeconds = this.checkoutCountdownService.remainingSeconds();
|
||||||
|
|
||||||
|
if (remainingSeconds === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minutes = Math.floor(remainingSeconds / 60);
|
||||||
|
const seconds = remainingSeconds % 60;
|
||||||
|
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
});
|
||||||
protected readonly cartSubtotal = computed(() => {
|
protected readonly cartSubtotal = computed(() => {
|
||||||
const purchase = this.createdPurchase();
|
const purchase = this.createdPurchase();
|
||||||
return purchase ? parseFloat(purchase.subtotal) : 0;
|
return purchase ? parseFloat(purchase.subtotal) : 0;
|
||||||
@@ -163,8 +185,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.checkoutCountdownService.clear();
|
|
||||||
|
|
||||||
const purchaseId = Number(this.route.snapshot.paramMap.get('id'));
|
const purchaseId = Number(this.route.snapshot.paramMap.get('id'));
|
||||||
if (!Number.isInteger(purchaseId) || purchaseId <= 0) {
|
if (!Number.isInteger(purchaseId) || purchaseId <= 0) {
|
||||||
void this.router.navigate(['/']);
|
void this.router.navigate(['/']);
|
||||||
|
|||||||
Reference in New Issue
Block a user