fix(checkout): disable cart access
This commit is contained in:
@@ -79,10 +79,13 @@
|
||||
|
||||
@if (displayCart()) {
|
||||
<app-cart-icon
|
||||
[quantity]="cartQuantity()"
|
||||
ariaLabel="Carrito de compras"
|
||||
title="Carrito"
|
||||
(click)="cartClick.emit()"
|
||||
[quantity]="cartDisabled() ? null : cartQuantity()"
|
||||
[disabled]="cartDisabled()"
|
||||
[ariaLabel]="
|
||||
cartDisabled() ? 'Carrito no disponible durante el checkout' : 'Carrito de compras'
|
||||
"
|
||||
[title]="cartDisabled() ? 'Carrito no disponible durante el checkout' : 'Carrito'"
|
||||
(click)="onCartClick()"
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export class StoreHeaderComponent {
|
||||
readonly displayCategories = input(true);
|
||||
readonly displaySeachBar = input(true);
|
||||
readonly displayCart = input(true);
|
||||
readonly cartDisabled = input(false);
|
||||
readonly cartClick = output<void>();
|
||||
readonly ticketsClick = output<void>();
|
||||
readonly loginClick = output<void>();
|
||||
@@ -53,6 +54,14 @@ export class StoreHeaderComponent {
|
||||
protected readonly showSearchError = signal(false);
|
||||
protected readonly searchControl = new FormControl('', { nonNullable: true });
|
||||
|
||||
protected onCartClick(): void {
|
||||
if (this.cartDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cartClick.emit();
|
||||
}
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
protected onDocumentClick(event: MouseEvent): void {
|
||||
if (!this.isUserDropdownOpen() && !this.isCategoryDropdownOpen() && !this.isMobileMenuOpen()) {
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
[displayCategories]="tenant()?.display_categories ?? true"
|
||||
[displaySeachBar]="tenant()?.display_seach_bar ?? true"
|
||||
[displayCart]="displayCart()"
|
||||
(cartClick)="isCartOpen.set(!isCartOpen())"
|
||||
[cartDisabled]="isCheckoutRoute()"
|
||||
(cartClick)="onCartClick()"
|
||||
(ticketsClick)="onTicketsClick()"
|
||||
(loginClick)="onLoginClick()"
|
||||
(logoutClick)="onLogoutClick()"
|
||||
@@ -20,7 +21,7 @@
|
||||
(categorySelect)="onCategorySelect($event)"
|
||||
/>
|
||||
|
||||
@if (displayCart() && isCartOpen()) {
|
||||
@if (displayCart() && !isCheckoutRoute() && isCartOpen()) {
|
||||
<div class="store-layout__cart-overlay" (click)="isCartOpen.set(false)"></div>
|
||||
<div class="store-layout__cart-dropdown card border-0 shadow-lg">
|
||||
<app-cart
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ParamMap,
|
||||
provideRouter,
|
||||
Router,
|
||||
UrlSerializer,
|
||||
} from '@angular/router';
|
||||
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
@@ -22,6 +23,7 @@ import { AuthUser } from '../../services/auth/auth.interfaces';
|
||||
import { StoreLayoutComponent } from './store-layout.component';
|
||||
import { StoreHeaderComponent } from './store-header/store-header.component';
|
||||
import { CheckoutService } from '../../services/checkout.service';
|
||||
import { TenantUrlSerializer } from '../../services/tenant-url.serializer';
|
||||
|
||||
const tenant: Tenant = {
|
||||
id: 1,
|
||||
@@ -164,6 +166,7 @@ describe('StoreLayoutComponent', () => {
|
||||
imports: [StoreLayoutComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: UrlSerializer, useClass: TenantUrlSerializer },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: { queryParamMap: queryParamMapState.asObservable() },
|
||||
@@ -240,6 +243,33 @@ describe('StoreLayoutComponent', () => {
|
||||
expect((fixture.nativeElement as HTMLElement).querySelector('app-cart')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('disables the cart icon and prevents opening the popup during checkout with a tenant base path', () => {
|
||||
tenantState.set({ ...tenant, base_path: 'fiesta' });
|
||||
const router = TestBed.inject(Router);
|
||||
vi.spyOn(router, 'url', 'get').mockReturnValue('/fiesta/checkout?purchase=25');
|
||||
|
||||
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
const cartButton = element.querySelector<HTMLButtonElement>('app-cart-icon button');
|
||||
const header = fixture.debugElement.query(By.directive(StoreHeaderComponent));
|
||||
|
||||
expect(cartButton?.disabled).toBe(true);
|
||||
expect(cartButton?.getAttribute('aria-label')).toBe(
|
||||
'Carrito no disponible durante el checkout',
|
||||
);
|
||||
expect(element.querySelector('[data-testid="cart-badge"]')).toBeNull();
|
||||
|
||||
header.componentInstance.cartClick.emit();
|
||||
queryParamMapState.next(convertToParamMap({ openCart: 'true' }));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect((fixture.componentInstance as any).isCartOpen()).toBe(false);
|
||||
expect(element.querySelector('app-cart')).toBeNull();
|
||||
expect(element.querySelector('.store-layout__cart-overlay')).toBeNull();
|
||||
});
|
||||
|
||||
it('hides the configured header elements when the tenant disables them', () => {
|
||||
tenantState.set({
|
||||
...tenant,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Component, computed, DestroyRef, inject, OnInit, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
NavigationEnd,
|
||||
PRIMARY_OUTLET,
|
||||
Router,
|
||||
RouterOutlet,
|
||||
} from '@angular/router';
|
||||
import { filter } from 'rxjs';
|
||||
import { TenantService } from '../../services/tenant.service';
|
||||
import { CartService } from '../../services/cart/cart.service';
|
||||
import { StoreFooterComponent, StoreFooterSection } from './store-footer/store-footer.component';
|
||||
@@ -37,6 +44,7 @@ export class StoreLayoutComponent implements OnInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
protected readonly isCartOpen = signal(false);
|
||||
protected readonly isCheckoutRoute = signal(this.isCheckoutUrl(this.router.url));
|
||||
protected readonly isCreatingPurchase = signal(false);
|
||||
protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true);
|
||||
protected readonly cartEditingPolicy = computed(() => this.tenant()?.cart_editing_policy ?? null);
|
||||
@@ -149,8 +157,22 @@ export class StoreLayoutComponent implements OnInit {
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((event) => {
|
||||
const isCheckoutRoute = this.isCheckoutUrl(event.urlAfterRedirects);
|
||||
this.isCheckoutRoute.set(isCheckoutRoute);
|
||||
|
||||
if (isCheckoutRoute) {
|
||||
this.isCartOpen.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
this.route.queryParamMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
|
||||
if (params.get('openCart') === 'true') {
|
||||
if (params.get('openCart') === 'true' && !this.isCheckoutRoute()) {
|
||||
this.isCartOpen.set(true);
|
||||
}
|
||||
});
|
||||
@@ -160,10 +182,24 @@ export class StoreLayoutComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private isCheckoutUrl(url: string): boolean {
|
||||
const primarySegments = this.router.parseUrl(url).root.children[PRIMARY_OUTLET]?.segments ?? [];
|
||||
|
||||
return primarySegments[0]?.path === 'checkout';
|
||||
}
|
||||
|
||||
protected onLoginClick(): void {
|
||||
void this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
protected onCartClick(): void {
|
||||
if (this.isCheckoutRoute()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCartOpen.update((isOpen) => !isOpen);
|
||||
}
|
||||
|
||||
protected onSearch(term: string): void {
|
||||
void this.router.navigate(['/buscar'], {
|
||||
queryParams: { q: term, page: 1 },
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<button
|
||||
type="button"
|
||||
[disabled]="disabled()"
|
||||
[class.cart-icon--disabled]="disabled()"
|
||||
[attr.aria-label]="ariaLabel()"
|
||||
[attr.aria-disabled]="disabled()"
|
||||
class="cart-icon"
|
||||
>
|
||||
<div class="cart-icon__container">
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
color: #666666;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: color 0.15s ease-in-out, transform 0.1s ease-in-out;
|
||||
transition:
|
||||
color 0.15s ease-in-out,
|
||||
transform 0.1s ease-in-out;
|
||||
border-radius: 4px;
|
||||
|
||||
// Active state subtle scale down
|
||||
@@ -29,10 +31,12 @@
|
||||
}
|
||||
|
||||
// Disabled state
|
||||
&:disabled {
|
||||
color: #A0A0A0;
|
||||
&:disabled,
|
||||
&.cart-icon--disabled {
|
||||
color: #b8b8b8;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +63,11 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cart-icon:disabled .cart-icon__glyph,
|
||||
.cart-icon--disabled .cart-icon__glyph {
|
||||
color: #b8b8b8;
|
||||
}
|
||||
|
||||
.cart-icon__badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CartIconComponent } from './cart-icon.component';
|
||||
describe('CartIconComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CartIconComponent]
|
||||
imports: [CartIconComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('CartIconComponent', () => {
|
||||
|
||||
return {
|
||||
fixture,
|
||||
element: fixture.nativeElement as HTMLElement
|
||||
element: fixture.nativeElement as HTMLElement,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,9 +47,11 @@ describe('CartIconComponent', () => {
|
||||
});
|
||||
|
||||
it('disables the button when disabled is true', () => {
|
||||
const { element } = setup(undefined, true);
|
||||
const { element } = setup(3, true);
|
||||
const button = element.querySelector('button');
|
||||
expect(button?.disabled).toBe(true);
|
||||
expect(button?.classList).toContain('cart-icon--disabled');
|
||||
expect(element.querySelector('[data-testid="cart-badge"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('enables the button when disabled is false', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
||||
imports: [],
|
||||
templateUrl: './cart-icon.component.html',
|
||||
styleUrl: './cart-icon.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CartIconComponent {
|
||||
readonly quantity = input<number | null | undefined>(undefined);
|
||||
@@ -13,6 +13,10 @@ export class CartIconComponent {
|
||||
readonly ariaLabel = input<string>('Carrito de compras');
|
||||
|
||||
protected readonly hasQuantity = computed(() => {
|
||||
if (this.disabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const q = this.quantity();
|
||||
return q !== null && q !== undefined && q > 0;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user