fix: Buy Now raced ahead of addToCart, per-product SEO tags never applied

CartService.addItem() fired an async dynamic import + API call for
new items but returned void immediately - buyNow() navigated to /cart
before the item was actually added, landing the user on an empty or
stale cart. addItem() now returns a Promise that resolves once the
cart signal actually contains the item; buyNow() awaits it before
navigating.

Also wired SeoService.setItemMeta()/resetToDefaults() into the product
detail page - built and working, but never called anywhere, so every
product page rendered the site-wide default OG/Twitter tags instead of
per-product ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-13 09:06:46 +04:00
parent 9f784406d7
commit c0bce7feac
2 changed files with 36 additions and 19 deletions

View File

@@ -16,6 +16,7 @@ import { LanguageService } from '../../../../services/language.service';
import { DEFAULT_PRODUCT_PAGE_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG, ProductPageConfig } from '../../../../shared/models/config';
import { getStockStatus, getTranslatedField } from '../../../../utils/item.utils';
import { ProductShareService } from '../../user-experience/services/product-share.service';
import { SeoService } from '../../../../services/seo.service';
import { ProductDeliveryInformationComponent } from '../components/delivery-information/delivery-information.component';
import { ProductActionsComponent } from '../components/product-actions/product-actions.component';
import { ProductGalleryComponent } from '../components/product-gallery/product-gallery.component';
@@ -70,6 +71,7 @@ export class ProductDetailsContainerComponent {
private readonly languageService = inject(LanguageService);
private readonly translate = inject(TranslateService);
private readonly shareService = inject(ProductShareService);
private readonly seoService = inject(SeoService);
readonly productPageConfigState = signal<Required<ProductPageConfig>>(this.resolveProductPageConfig());
readonly userExperienceConfig = signal(this.resolveUserExperienceConfig());
@@ -229,6 +231,8 @@ export class ProductDetailsContainerComponent {
this.route.paramMap
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(params => this.loadProduct(Number(params.get('id'))));
this.destroyRef.onDestroy(() => this.seoService.resetToDefaults());
}
loadProduct(productId: number): void {
@@ -261,6 +265,7 @@ export class ProductDetailsContainerComponent {
}
this.product.set(product);
this.seoService.setItemMeta(product);
if (this.userExperienceConfig().recentlyViewed.enabled) {
this.uxFacade.trackRecentlyViewed(product, this.userExperienceConfig().recentlyViewed.maxItems);
}
@@ -303,10 +308,10 @@ export class ProductDetailsContainerComponent {
}
}
addToCart(): void {
addToCart(): Promise<void> {
const current = this.product();
if (!current) return;
this.cartService.addItem(current.itemID, 1, {
if (!current) return Promise.resolve();
return this.cartService.addItem(current.itemID, 1, {
colour: this.selectedColour() ?? undefined,
size: this.selectedSize() ?? undefined,
price: this.effectivePrice(),
@@ -314,9 +319,9 @@ export class ProductDetailsContainerComponent {
});
}
buyNow(): void {
this.addToCart();
this.router.navigate([`/${this.languageService.currentLanguage()}/cart`]);
async buyNow(): Promise<void> {
await this.addToCart();
void this.router.navigate([`/${this.languageService.currentLanguage()}/cart`]);
}
toggleWishlist(): void {

View File

@@ -203,9 +203,16 @@ export class CartService {
return this.cartItems().findIndex(item => this.isSameCartLine(item, itemID, variant));
}
addItem(itemID: number, quantity: number = 1, variant?: CartVariant): void {
/**
* Resolves once the item is actually present in the cart signal - not
* merely once the call was fired. New (not-yet-in-cart) items fetch their
* details from the API first, so callers that navigate straight after
* (e.g. Buy Now -> /cart) must await this rather than treat it as fired
* synchronously.
*/
addItem(itemID: number, quantity: number = 1, variant?: CartVariant): Promise<void> {
// Prevent duplicate API calls for same item
if (this.addingItems.has(itemID)) return;
if (this.addingItems.has(itemID)) return Promise.resolve();
const currentItems = this.cartItems();
const existingItem = currentItems.find(i => this.isSameCartLine(i, itemID, variant));
@@ -213,10 +220,13 @@ export class CartService {
if (existingItem) {
// Item exists, increase quantity
this.updateQuantity(itemID, existingItem.quantity + quantity, variant);
} else {
return Promise.resolve();
}
// Get item details from API and add to cart
this.addingItems.add(itemID);
import('./api.service').then(({ ApiService }) => {
return import('./api.service').then(({ ApiService }) =>
new Promise<void>((resolve) => {
this.injector.get(ApiService).getItem(itemID).subscribe({
next: (item) => {
const cartItem = this.normalizeCartItem({
@@ -229,18 +239,20 @@ export class CartService {
});
this.cartItems.set([...this.cartItems(), cartItem]);
this.addingItems.delete(itemID);
resolve();
},
error: (err) => {
console.error('Error adding to cart:', err);
this.addingItems.delete(itemID);
resolve();
}
});
}).catch((err) => {
})
).catch((err) => {
console.error('Error loading API service:', err);
this.addingItems.delete(itemID);
});
}
}
updateQuantity(itemID: number, quantity: number, variant?: CartVariant): void {
if (quantity <= 0) {