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:
@@ -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 { DEFAULT_PRODUCT_PAGE_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG, ProductPageConfig } from '../../../../shared/models/config';
|
||||||
import { getStockStatus, getTranslatedField } from '../../../../utils/item.utils';
|
import { getStockStatus, getTranslatedField } from '../../../../utils/item.utils';
|
||||||
import { ProductShareService } from '../../user-experience/services/product-share.service';
|
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 { ProductDeliveryInformationComponent } from '../components/delivery-information/delivery-information.component';
|
||||||
import { ProductActionsComponent } from '../components/product-actions/product-actions.component';
|
import { ProductActionsComponent } from '../components/product-actions/product-actions.component';
|
||||||
import { ProductGalleryComponent } from '../components/product-gallery/product-gallery.component';
|
import { ProductGalleryComponent } from '../components/product-gallery/product-gallery.component';
|
||||||
@@ -70,6 +71,7 @@ export class ProductDetailsContainerComponent {
|
|||||||
private readonly languageService = inject(LanguageService);
|
private readonly languageService = inject(LanguageService);
|
||||||
private readonly translate = inject(TranslateService);
|
private readonly translate = inject(TranslateService);
|
||||||
private readonly shareService = inject(ProductShareService);
|
private readonly shareService = inject(ProductShareService);
|
||||||
|
private readonly seoService = inject(SeoService);
|
||||||
|
|
||||||
readonly productPageConfigState = signal<Required<ProductPageConfig>>(this.resolveProductPageConfig());
|
readonly productPageConfigState = signal<Required<ProductPageConfig>>(this.resolveProductPageConfig());
|
||||||
readonly userExperienceConfig = signal(this.resolveUserExperienceConfig());
|
readonly userExperienceConfig = signal(this.resolveUserExperienceConfig());
|
||||||
@@ -229,6 +231,8 @@ export class ProductDetailsContainerComponent {
|
|||||||
this.route.paramMap
|
this.route.paramMap
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe(params => this.loadProduct(Number(params.get('id'))));
|
.subscribe(params => this.loadProduct(Number(params.get('id'))));
|
||||||
|
|
||||||
|
this.destroyRef.onDestroy(() => this.seoService.resetToDefaults());
|
||||||
}
|
}
|
||||||
|
|
||||||
loadProduct(productId: number): void {
|
loadProduct(productId: number): void {
|
||||||
@@ -261,6 +265,7 @@ export class ProductDetailsContainerComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.product.set(product);
|
this.product.set(product);
|
||||||
|
this.seoService.setItemMeta(product);
|
||||||
if (this.userExperienceConfig().recentlyViewed.enabled) {
|
if (this.userExperienceConfig().recentlyViewed.enabled) {
|
||||||
this.uxFacade.trackRecentlyViewed(product, this.userExperienceConfig().recentlyViewed.maxItems);
|
this.uxFacade.trackRecentlyViewed(product, this.userExperienceConfig().recentlyViewed.maxItems);
|
||||||
}
|
}
|
||||||
@@ -303,10 +308,10 @@ export class ProductDetailsContainerComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addToCart(): void {
|
addToCart(): Promise<void> {
|
||||||
const current = this.product();
|
const current = this.product();
|
||||||
if (!current) return;
|
if (!current) return Promise.resolve();
|
||||||
this.cartService.addItem(current.itemID, 1, {
|
return this.cartService.addItem(current.itemID, 1, {
|
||||||
colour: this.selectedColour() ?? undefined,
|
colour: this.selectedColour() ?? undefined,
|
||||||
size: this.selectedSize() ?? undefined,
|
size: this.selectedSize() ?? undefined,
|
||||||
price: this.effectivePrice(),
|
price: this.effectivePrice(),
|
||||||
@@ -314,9 +319,9 @@ export class ProductDetailsContainerComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
buyNow(): void {
|
async buyNow(): Promise<void> {
|
||||||
this.addToCart();
|
await this.addToCart();
|
||||||
this.router.navigate([`/${this.languageService.currentLanguage()}/cart`]);
|
void this.router.navigate([`/${this.languageService.currentLanguage()}/cart`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleWishlist(): void {
|
toggleWishlist(): void {
|
||||||
|
|||||||
@@ -203,20 +203,30 @@ export class CartService {
|
|||||||
return this.cartItems().findIndex(item => this.isSameCartLine(item, itemID, variant));
|
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
|
// 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 currentItems = this.cartItems();
|
||||||
const existingItem = currentItems.find(i => this.isSameCartLine(i, itemID, variant));
|
const existingItem = currentItems.find(i => this.isSameCartLine(i, itemID, variant));
|
||||||
|
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
// Item exists, increase quantity
|
// Item exists, increase quantity
|
||||||
this.updateQuantity(itemID, existingItem.quantity + quantity, variant);
|
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 }) => {
|
// Get item details from API and add to cart
|
||||||
|
this.addingItems.add(itemID);
|
||||||
|
return import('./api.service').then(({ ApiService }) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
this.injector.get(ApiService).getItem(itemID).subscribe({
|
this.injector.get(ApiService).getItem(itemID).subscribe({
|
||||||
next: (item) => {
|
next: (item) => {
|
||||||
const cartItem = this.normalizeCartItem({
|
const cartItem = this.normalizeCartItem({
|
||||||
@@ -229,17 +239,19 @@ export class CartService {
|
|||||||
});
|
});
|
||||||
this.cartItems.set([...this.cartItems(), cartItem]);
|
this.cartItems.set([...this.cartItems(), cartItem]);
|
||||||
this.addingItems.delete(itemID);
|
this.addingItems.delete(itemID);
|
||||||
|
resolve();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Error adding to cart:', err);
|
console.error('Error adding to cart:', err);
|
||||||
this.addingItems.delete(itemID);
|
this.addingItems.delete(itemID);
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catch((err) => {
|
})
|
||||||
console.error('Error loading API service:', err);
|
).catch((err) => {
|
||||||
this.addingItems.delete(itemID);
|
console.error('Error loading API service:', err);
|
||||||
});
|
this.addingItems.delete(itemID);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateQuantity(itemID: number, quantity: number, variant?: CartVariant): void {
|
updateQuantity(itemID: number, quantity: number, variant?: CartVariant): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user