feat(checkout): implement mobile countdown display and logic

This commit is contained in:
2026-08-27 09:20:24 -03:00
parent b6a52ceb8c
commit 91b8788c91
5 changed files with 82 additions and 3 deletions

View File

@@ -179,7 +179,7 @@
@if (checkoutRemainingTime(); as remainingTime) {
<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"
aria-label="Tiempo restante de compra"
>

View File

@@ -46,6 +46,17 @@
</div>
<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
title="COMPRA"
[items]="mappedCartItems()"

View File

@@ -28,6 +28,24 @@
display: flex;
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 {

View File

@@ -14,6 +14,7 @@ import { TenantService } from '../../../../core/services/tenant.service';
import { ToastService } from '../../../../core/services/toast.service';
import { CartService } from '../../../../core/services/cart/cart.service';
import { CartEditingPolicy } from '../../../../core/services/tenant.interface';
import { CheckoutCountdownService } from '../../../../core/services/checkout-countdown.service';
import { CheckoutPageComponent } from './checkout-page.component';
describe('CheckoutPageComponent payment validation', () => {
@@ -129,6 +130,35 @@ describe('CheckoutPageComponent payment validation', () => {
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 () => {
checkoutServiceStub.getPurchase
.mockResolvedValueOnce({ status: 'pending_payment' })

View File

@@ -97,6 +97,28 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
protected readonly createdPurchase = signal<PurchaseDetailResponse | null>(null);
protected readonly isLoadingPurchase = signal(true);
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(() => {
const purchase = this.createdPurchase();
return purchase ? parseFloat(purchase.subtotal) : 0;
@@ -163,8 +185,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.checkoutCountdownService.clear();
const purchaseId = Number(this.route.snapshot.paramMap.get('id'));
if (!Number.isInteger(purchaseId) || purchaseId <= 0) {
void this.router.navigate(['/']);