feat(checkout): enhance countdown synchronization and cleanup logic

This commit is contained in:
ncoronel 2026-08-27 09:03:38 -03:00
parent 51d051733c
commit b6a52ceb8c
3 changed files with 32 additions and 6 deletions

View File

@ -54,4 +54,16 @@ describe('CheckoutCountdownService', () => {
expect(service.remainingSeconds()).toBeNull();
});
it('keeps the active countdown when a partial checkout response omits timing fields', () => {
service.synchronize({
expires_at: null,
expires_in_seconds: 10,
server_time: '2026-08-27T15:00:00.000Z',
});
service.synchronize({});
expect(service.remainingSeconds()).toBe(10);
});
});

View File

@ -16,9 +16,13 @@ export class CheckoutCountdownService implements OnDestroy {
readonly remainingSeconds = this.remainingSecondsState.asReadonly();
synchronize(timing: CheckoutTiming): void {
synchronize(timing: Partial<CheckoutTiming>): void {
const remainingSeconds = this.resolveRemainingSeconds(timing);
if (remainingSeconds === undefined) {
return;
}
if (remainingSeconds === null) {
this.clear();
return;
@ -43,9 +47,15 @@ export class CheckoutCountdownService implements OnDestroy {
this.clear();
}
private resolveRemainingSeconds(timing: CheckoutTiming): number | null {
const expiresAt = timing.expires_at ? Date.parse(timing.expires_at) : Number.NaN;
const serverTime = Date.parse(timing.server_time);
private resolveRemainingSeconds(timing: Partial<CheckoutTiming>): number | null | undefined {
if (timing.expires_at === null && timing.expires_in_seconds === null) {
return null;
}
const expiresAt =
typeof timing.expires_at === 'string' ? Date.parse(timing.expires_at) : Number.NaN;
const serverTime =
typeof timing.server_time === 'string' ? Date.parse(timing.server_time) : Number.NaN;
if (Number.isFinite(expiresAt) && Number.isFinite(serverTime)) {
return Math.max(0, Math.ceil((expiresAt - serverTime) / 1_000));
@ -62,7 +72,7 @@ export class CheckoutCountdownService implements OnDestroy {
return Math.max(0, Math.ceil((expiresAt - Date.now()) / 1_000));
}
return null;
return undefined;
}
private updateRemainingSeconds(): void {

View File

@ -177,7 +177,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.stopQrPolling();
this.stopTransferPolling();
this.checkoutCountdownService.clear();
}
private mapPurchaseItemToMock(item: PurchaseDetailItemResponse): CartItemMock {
@ -287,6 +286,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
const tenant = this.tenantService.tenant();
const purchaseId = this.createdPurchaseId();
if (!tenant || !purchaseId) {
this.checkoutCountdownService.clear();
return true;
}
@ -297,6 +297,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
await firstValueFrom(this.cartService.loadCart());
this.createdPurchaseId.set(null);
this.createdPurchase.set(null);
this.checkoutCountdownService.clear();
this.navigationStarted = true;
return true;
@ -310,6 +311,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
});
this.createdPurchaseId.set(null);
this.createdPurchase.set(null);
this.checkoutCountdownService.clear();
this.navigationStarted = true;
return true;
}
@ -671,6 +673,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
this.navigationStarted = true;
this.stopQrPolling();
this.stopTransferPolling();
this.checkoutCountdownService.clear();
void this.router.navigate(['/checkout/status', purchaseId]);
}
@ -792,6 +795,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
this.navigationStarted = true;
this.stopQrPolling();
this.stopTransferPolling();
this.checkoutCountdownService.clear();
void this.router.navigate(['/checkout/status', purchaseId], {
queryParams: { status: 'expired' },
});