diff --git a/src/global.d.ts b/src/global.d.ts index b9daab99..de9e4a9d 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -24,7 +24,7 @@ interface ImportMeta { MODE: 'development' | 'production'; PROD: boolean; VITE_APP_CTP_API_URL: string; - VITE_APP_CTP_AUTH_UR: string; + VITE_APP_CTP_AUTH_URL: string; VITE_APP_CTP_CLIENT_ID: string; VITE_APP_CTP_CLIENT_SECRET: string; VITE_APP_CTP_PROJECT_KEY: string; diff --git a/src/pages/CartPage/model/CartPageModel.ts b/src/pages/CartPage/model/CartPageModel.ts index 4bfc757a..7ec7fef2 100644 --- a/src/pages/CartPage/model/CartPageModel.ts +++ b/src/pages/CartPage/model/CartPageModel.ts @@ -4,6 +4,7 @@ import type { Page } from '@/shared/types/page.ts'; import SummaryModel from '@/entities/Summary/model/SummaryModel.ts'; import getCartModel from '@/shared/API/cart/model/CartModel.ts'; import getCustomerModel from '@/shared/API/customer/model/CustomerModel.ts'; +import { isChannelMessage } from '@/shared/API/types/validation.ts'; import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import getStore from '@/shared/Store/Store.ts'; import { setCurrentPage } from '@/shared/Store/actions.ts'; @@ -12,6 +13,7 @@ import { SERVER_MESSAGE_KEY } from '@/shared/constants/messages.ts'; import { PAGE_ID } from '@/shared/constants/pages.ts'; import { LOADER_SIZE } from '@/shared/constants/sizes.ts'; import { CartActive } from '@/shared/types/cart.ts'; +import { ChannelMessage } from '@/shared/types/channel.ts'; import { promoCodeAppliedMessage, promoCodeDeleteMessage } from '@/shared/utils/messageTemplates.ts'; import { showErrorMessage, showSuccessMessage } from '@/shared/utils/userMessage.ts'; import ProductOrderModel from '@/widgets/ProductOrder/model/ProductOrderModel.ts'; @@ -29,6 +31,8 @@ class CartPageModel implements Page { private cartCouponSummary: SummaryModel; + private channel: BroadcastChannel; + private productCouponSummary: SummaryModel; private productsItem: ProductOrderModel[] = []; @@ -36,19 +40,36 @@ class CartPageModel implements Page { private view: CartPageView; constructor(parent: HTMLDivElement) { + this.channel = new BroadcastChannel(`${import.meta.env.VITE_APP_CTP_PROJECT_KEY}`); + this.channel.onmessage = this.onChannelMessage.bind(this); this.cartCouponSummary = new SummaryModel(TITLE_SUMMARY.cart, this.deleteDiscountHandler.bind(this)); this.productCouponSummary = new SummaryModel(TITLE_SUMMARY.product, this.deleteDiscountHandler.bind(this)); this.view = new CartPageView( parent, this.cartCouponSummary, this.productCouponSummary, - this.clearCart.bind(this), + this.clearCartHandler.bind(this), this.addDiscountHandler.bind(this), ); this.init().catch(showErrorMessage); } + private addDiscount(cart: Cart): void { + this.cart = cart; + this.productsItem.forEach((productItem) => { + const idLine = productItem.getProduct().lineItemId; + const updateLine = this.cart?.products.find((item) => item.lineItemId === idLine); + if (updateLine) { + productItem.setProduct(updateLine); + productItem.updateProductHandler(CartActive.UPDATE).catch(showErrorMessage); + } + }); + this.cartCouponSummary.update(this.cart.discountsCart); + this.productCouponSummary.update(this.cart.discountsProduct); + this.view.updateTotal(this.cart); + } + private async addDiscountHandler(discountCode: string): Promise { if (discountCode.trim()) { if (discountCode.trim() === HAPPY_BIRTHDAY) { @@ -61,18 +82,8 @@ class CartPageModel implements Page { .then((cart) => { if (cart) { showSuccessMessage(promoCodeAppliedMessage(discountCode)); - this.cart = cart; - this.productsItem.forEach((productItem) => { - const idLine = productItem.getProduct().lineItemId; - const updateLine = this.cart?.products.find((item) => item.lineItemId === idLine); - if (updateLine) { - productItem.setProduct(updateLine); - productItem.updateProductHandler(CartActive.UPDATE).catch(showErrorMessage); - } - }); - this.cartCouponSummary.update(this.cart.discountsCart); - this.productCouponSummary.update(this.cart.discountsProduct); - this.view.updateTotal(this.cart); + this.channel.postMessage({ cart, type: ChannelMessage.ADD_DISCOUNT }); + this.addDiscount(cart); } }) .catch(showErrorMessage) @@ -80,17 +91,37 @@ class CartPageModel implements Page { } } - private changeProductHandler(cart: Cart): void { + private addProduct(cart: Cart): void { + this.cart = cart; + const newItem = new ProductOrderModel( + this.cart.products[this.cart.products.length - 1], + this.changeProductHandler.bind(this), + ); + this.productsItem.push(newItem); + if (this.productsItem.length > 1) { + this.view.addItem(newItem); + this.cartCouponSummary.update(this.cart.discountsCart); + this.productCouponSummary.update(this.cart.discountsProduct); + this.view.updateTotal(this.cart); + } else { + this.view.renderCart(this.productsItem); + } + } + + private changeProduct(cart: Cart): void { this.cart = cart; this.productsItem = this.productsItem.filter((productItem) => { const searchEl = this.cart?.products.find((item) => item.lineItemId === productItem.getProduct().lineItemId); + if (searchEl) { + productItem.setProduct(searchEl); + productItem.updateProductHandler(CartActive.UPDATE).catch(showErrorMessage); + } if (!searchEl) { productItem.getHTML().remove(); return false; } return true; }); - if (!this.productsItem.length) { this.view.renderEmpty(); } @@ -99,6 +130,11 @@ class CartPageModel implements Page { this.view.updateTotal(this.cart); } + private changeProductHandler(cart: Cart): void { + this.changeProduct(cart); + this.channel.postMessage({ cart, type: ChannelMessage.ITEM_CHANGE }); + } + private async checkBirthday(): Promise { if (!getStore().getState().isUserLoggedIn) { throw showErrorMessage(SERVER_MESSAGE_KEY.COUPON_NEED_LOGIN); @@ -124,21 +160,26 @@ class CartPageModel implements Page { } } - private async clearCart(): Promise { + private clearCart(cart: Cart | null): void { + this.cart = cart; + showSuccessMessage(SERVER_MESSAGE_KEY.SUCCESSFUL_CLEAR_CART); + this.productsItem = this.productsItem.filter((productItem) => { + const searchEl = this.cart?.products.find((item) => item.lineItemId === productItem.getProduct().lineItemId); + if (!searchEl) { + productItem.getHTML().remove(); + return false; + } + return true; + }); + this.renderCart(); + } + + private async clearCartHandler(): Promise { await getCartModel() .clearCart() .then((cart) => { - this.cart = cart; - showSuccessMessage(SERVER_MESSAGE_KEY.SUCCESSFUL_CLEAR_CART); - this.productsItem = this.productsItem.filter((productItem) => { - const searchEl = this.cart?.products.find((item) => item.lineItemId === productItem.getProduct().lineItemId); - if (!searchEl) { - productItem.getHTML().remove(); - return false; - } - return true; - }); - this.renderCart(); + this.channel.postMessage({ cart, type: ChannelMessage.CLEAR_CART }); + this.clearCart(cart); }) .catch((error: Error) => { showErrorMessage(error); @@ -146,6 +187,21 @@ class CartPageModel implements Page { }); } + private deleteDiscount(cart: Cart): void { + this.cart = cart; + this.productsItem.forEach((productItem) => { + const idLine = productItem.getProduct().lineItemId; + const updateLine = this.cart?.products.find((item) => item.lineItemId === idLine); + if (updateLine) { + productItem.setProduct(updateLine); + productItem.updateProductHandler(CartActive.UPDATE).catch(showErrorMessage); + } + }); + this.cartCouponSummary.update(this.cart.discountsCart); + this.productCouponSummary.update(this.cart.discountsProduct); + this.view.updateTotal(this.cart); + } + private async deleteDiscountHandler(coupon: Coupon): Promise { const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); this.view.getCouponButton().append(loader); @@ -154,18 +210,8 @@ class CartPageModel implements Page { .then((cart) => { if (cart) { showSuccessMessage(promoCodeDeleteMessage(coupon.discountCode)); - this.cart = cart; - this.productsItem.forEach((productItem) => { - const idLine = productItem.getProduct().lineItemId; - const updateLine = this.cart?.products.find((item) => item.lineItemId === idLine); - if (updateLine) { - productItem.setProduct(updateLine); - productItem.updateProductHandler(CartActive.UPDATE).catch(showErrorMessage); - } - }); - this.cartCouponSummary.update(this.cart.discountsCart); - this.productCouponSummary.update(this.cart.discountsProduct); - this.view.updateTotal(this.cart); + this.channel.postMessage({ cart, type: ChannelMessage.DELETE_COUPON }); + this.deleteDiscount(cart); } }) .catch(showErrorMessage) @@ -179,6 +225,34 @@ class CartPageModel implements Page { observeStore(selectCurrentLanguage, () => this.view.updateLanguage()); } + private onChannelMessage(event: MessageEvent): void { + if (isChannelMessage(event.data)) { + switch (event.data.type) { + case ChannelMessage.ADD_DISCOUNT: + this.addDiscount(event.data.cart); + break; + case ChannelMessage.ADD_PRODUCT: + this.addProduct(event.data.cart); + break; + case ChannelMessage.DELETE_COUPON: + this.deleteDiscount(event.data.cart); + break; + case ChannelMessage.CLEAR_CART: + this.clearCart(event.data.cart); + break; + case ChannelMessage.ITEM_CHANGE: + this.changeProduct(event.data.cart); + break; + default: + break; + } + + if (event.data.cart) { + getCartModel().dispatchUpdate(event.data.cart); + } + } + } + private renderCart(): void { if (this.cart) { this.cart.products.forEach((product) => { diff --git a/src/pages/CartPage/view/CartPageView.ts b/src/pages/CartPage/view/CartPageView.ts index 69fb60b0..67929e93 100644 --- a/src/pages/CartPage/view/CartPageView.ts +++ b/src/pages/CartPage/view/CartPageView.ts @@ -316,6 +316,11 @@ class CartPageView { }); } + public addItem(productsItem: ProductOrderModel): void { + this.productRow.push(productsItem.getHTML()); + this.tableBody?.append(productsItem.getHTML()); + } + public getCouponButton(): HTMLButtonElement { return this.couponButton; } @@ -332,7 +337,10 @@ class CartPageView { this.productRow.map((productEl) => productEl.remove()); this.productRow = []; this.addTableHeader(); - productsItem.forEach((productEl) => this.tableBody?.append(productEl.getHTML())); + productsItem.forEach((productEl) => { + this.productRow.push(productEl.getHTML()); + this.tableBody?.append(productEl.getHTML()); + }); this.addTotalInfo(); } diff --git a/src/shared/API/cart/model/CartModel.ts b/src/shared/API/cart/model/CartModel.ts index 52df6eb3..1f043028 100644 --- a/src/shared/API/cart/model/CartModel.ts +++ b/src/shared/API/cart/model/CartModel.ts @@ -12,6 +12,7 @@ import type { import getStore from '@/shared/Store/Store.ts'; import { setAnonymousCartId, setAnonymousId } from '@/shared/Store/actions.ts'; import { PRICE_FRACTIONS } from '@/shared/constants/product.ts'; +import { ChannelMessage } from '@/shared/types/channel.ts'; import { showErrorMessage } from '@/shared/utils/userMessage.ts'; import type { OptionsRequest } from '../../types/type.ts'; @@ -36,9 +37,12 @@ type Callback = (cart: Cart) => boolean; export class CartModel { private callback: Callback[] = []; + private channel: BroadcastChannel; + private root: CartApi; constructor() { + this.channel = new BroadcastChannel(`${import.meta.env.VITE_APP_CTP_PROJECT_KEY}`); this.root = new CartApi(); this.getCart() .then((cart) => { @@ -163,10 +167,6 @@ export class CartModel { await Promise.all(otherCarts.map((id) => this.root.deleteCart(id))); } - private dispatchUpdate(cart: Cart): void { - this.callback.forEach((callback) => callback(cart)); - } - private async getAnonymousCart(anonymousCartId: string): Promise { const data = await this.root.getAnonymCart(anonymousCartId); if (!data.body.customerId) { @@ -283,6 +283,7 @@ export class CartModel { const data = await this.root.updateCart(cart, actions); const result = this.getCartFromData(data); this.dispatchUpdate(result); + this.channel.postMessage({ cart: result, type: ChannelMessage.ADD_PRODUCT }); return result; } @@ -341,10 +342,14 @@ export class CartModel { ]; const data = await this.root.updateCart(cart, actions); const result = this.getCartFromData(data); - this.dispatchUpdate(cart); + this.dispatchUpdate(result); return result; } + public dispatchUpdate(cart: Cart): void { + this.callback.forEach((callback) => callback(cart)); + } + public async editProductCount(editCartItem: EditCartItem): Promise { const cart = await this.getCart(); @@ -358,7 +363,7 @@ export class CartModel { const data = await this.root.updateCart(cart, actions); const result = this.getCartFromData(data); - this.dispatchUpdate(cart); + this.dispatchUpdate(result); return result; } diff --git a/src/shared/API/sdk/client.ts b/src/shared/API/sdk/client.ts index bc4c7f0a..5dbd43f4 100644 --- a/src/shared/API/sdk/client.ts +++ b/src/shared/API/sdk/client.ts @@ -1,7 +1,7 @@ import type { UserCredentials } from '@/shared/types/user'; import getStore from '@/shared/Store/Store.ts'; -import { setAnonymousId } from '@/shared/Store/actions.ts'; +import { setAnonymousId, setAuthToken, switchIsUserLoggedIn } from '@/shared/Store/actions.ts'; import { type ByProjectKeyRequestBuilder, createApiBuilderFromCtpClient } from '@commercetools/platform-sdk'; import { type AnonymousAuthMiddlewareOptions, @@ -51,6 +51,9 @@ export class ApiClient { this.clientID = CLIENT_ID; this.clientSecret = CLIENT_SECRET; this.scopes = SCOPES.split(' '); + if (USE_SAVE_TOKEN) { + this.checkSaveToken(); + } if (USE_SAVE_TOKEN && getStore().getState().authToken) { this.authConnection = this.createAuthConnectionWithRefreshToken(); @@ -93,20 +96,48 @@ export class ApiClient { } } + private checkSaveToken(): void { + const { anonymousToken, authToken } = getStore().getState(); + if (authToken && authToken.expirationTime < Date.now()) { + localStorage.clear(); + getStore().dispatch(setAuthToken(null)); + getStore().dispatch(switchIsUserLoggedIn(false)); + getTokenCache(TokenType.AUTH).clear(); + } + if (anonymousToken && anonymousToken.expirationTime < Date.now()) { + localStorage.clear(); + getStore().dispatch(setAnonymousId(null)); + getTokenCache(TokenType.ANONYM).clear(); + } + } + private createAnonymConnection(): ByProjectKeyRequestBuilder { const defaultOptions = this.getDefaultOptions(TokenType.ANONYM); const client = this.getDefaultClient(); - const anonymousId = uuid(); - const anonymOptions: AnonymousAuthMiddlewareOptions = { + let anonymOptions: AnonymousAuthMiddlewareOptions = { ...defaultOptions, credentials: { ...defaultOptions.credentials, - anonymousId, }, }; + + const saveAnonymousId = getStore().getState().anonymousId; + if (!saveAnonymousId) { + const anonymousId = uuid(); + + anonymOptions = { + ...defaultOptions, + credentials: { + ...defaultOptions.credentials, + anonymousId, + }, + }; + getStore().dispatch(setAnonymousId(anonymousId)); + } + client.withAnonymousSessionFlow(anonymOptions); - getStore().dispatch(setAnonymousId(anonymousId)); + this.anonymConnection = this.getConnection(client.build()); return this.anonymConnection; } @@ -146,7 +177,7 @@ export class ApiClient { host: URL_AUTH, projectKey: this.projectKey, scopes: this.scopes, - tokenCache: USE_SAVE_TOKEN && tokenType === TokenType.AUTH ? getTokenCache(tokenType) : undefined, + tokenCache: USE_SAVE_TOKEN ? getTokenCache(tokenType) : undefined, }; } diff --git a/src/shared/API/types/validation.ts b/src/shared/API/types/validation.ts index 2be23a7b..c2432944 100644 --- a/src/shared/API/types/validation.ts +++ b/src/shared/API/types/validation.ts @@ -1,3 +1,4 @@ +import type { ChannelMessageType } from '@/shared/types/channel'; import type { AttributePlainEnumValue, Cart, @@ -260,3 +261,14 @@ export function isProductDiscountPagedQueryResponse(data: unknown): data is Prod Array.isArray(data.results), ); } + +export function isChannelMessage(data: unknown): data is ChannelMessageType { + return Boolean( + typeof data === 'object' && + data && + 'type' in data && + typeof data.type === 'string' && + 'cart' in data && + data.cart !== undefined, + ); +} diff --git a/src/shared/types/channel.ts b/src/shared/types/channel.ts new file mode 100644 index 00000000..de666b4b --- /dev/null +++ b/src/shared/types/channel.ts @@ -0,0 +1,14 @@ +import type { Cart } from './cart'; + +export enum ChannelMessage { + ADD_DISCOUNT = 'ADD_DISCOUNT', + ADD_PRODUCT = 'ADD_PRODUCT', + CLEAR_CART = 'CLEAR_CART', + DELETE_COUPON = 'DELETE_COUPON', + ITEM_CHANGE = 'ITEM_CHANGE', +} + +export interface ChannelMessageType { + cart: Cart; + type: ChannelMessage; +} diff --git a/src/widgets/ProductOrder/model/ProductOrderModel.ts b/src/widgets/ProductOrder/model/ProductOrderModel.ts index c6d27812..4303adf8 100644 --- a/src/widgets/ProductOrder/model/ProductOrderModel.ts +++ b/src/widgets/ProductOrder/model/ProductOrderModel.ts @@ -94,7 +94,7 @@ class ProductOrderModel { } } - public getHTML(): HTMLDivElement { + public getHTML(): HTMLTableRowElement { return this.view.getHTML(); } diff --git a/src/widgets/ProductOrder/view/ProductOrderView.ts b/src/widgets/ProductOrder/view/ProductOrderView.ts index 3889af7f..58af8a42 100644 --- a/src/widgets/ProductOrder/view/ProductOrderView.ts +++ b/src/widgets/ProductOrder/view/ProductOrderView.ts @@ -208,7 +208,7 @@ class ProductOrderView { return this.deleteButton; } - public getHTML(): HTMLDivElement { + public getHTML(): HTMLTableRowElement { return this.view; }