From 82271746e5b7f57587e5bd7e73a9a2280500a404 Mon Sep 17 00:00:00 2001 From: Bartosz Herba Date: Thu, 20 Oct 2022 09:24:00 +0200 Subject: [PATCH] chore: release 1.1.0 --- .env.example | 8 + components/Header/SearchBar/SearchResults.vue | 20 +- components/NewProducts.vue | 18 +- composables/index.ts | 1 + composables/types.ts | 3 +- composables/useConfig/index.ts | 2 +- .../useContent/commands/loadBlocksCommand.ts | 2 +- .../useContent/commands/loadContentCommand.ts | 2 +- composables/useCountrySearch/index.ts | 2 +- .../useCurrency/__tests__/useCurrency.spec.ts | 2 +- composables/useCurrency/index.ts | 2 +- composables/useImage/index.ts | 6 +- .../commands/updateSubscriptionCommand.ts | 2 +- composables/useStore/index.ts | 4 +- composables/useUiHelpers/index.ts | 2 +- composables/useUrlResolver/index.ts | 2 +- helpers/cart/addToCart.ts | 14 +- helpers/getMetaInfo.ts | 37 ++ lang/de.js | 3 + lang/en.js | 3 + layouts/default.vue | 2 +- middleware.config.js | 8 +- middleware/__tests__/url-resolver.spec.js | 88 ++++ middleware/url-resolver.ts | 27 + modules/GraphQL/types.ts | 3 + .../__tests__/CategoryBreadcrumbs.spec.ts | 2 +- .../views/__tests__/productsMock.ts | 20 +- .../views/useProductsWithCommonCardProps.ts | 6 +- .../useCategory/categoryMeta.gql.ts | 12 + .../category/composables/useCategory/index.ts | 39 +- .../composables/useCategory/useCategory.ts | 7 + .../composables/useCategorySearch/index.ts | 2 +- .../composables/useFacet/getFacetData.gql.ts | 7 + .../__tests__/useTraverseCategory.spec.ts | 2 +- .../category/helpers/useTraverseCategory.ts | 2 +- modules/catalog/index.ts | 19 +- modules/catalog/pages/category.vue | 59 ++- modules/catalog/pages/product.vue | 19 +- .../components/ProductAddReviewForm.vue | 11 +- .../product/components/ProductsCarousel.vue | 12 +- .../product/components/RelatedProducts.vue | 8 +- .../product/components/UpsellProducts.vue | 8 +- .../configurable/ConfigurableProduct.vue | 9 +- .../grouped/GroupedProductSelector.vue | 2 +- .../components/product-types/styles.scss | 3 + .../product/components/tabs/ProductTabs.vue | 14 +- .../commands/getProductDetailsCommand.ts | 4 +- .../commands/getProductListCommand.ts | 4 +- .../product/composables/useProduct/index.ts | 7 + .../composables/useProduct/useProduct.ts | 4 + .../composables/useRelatedProducts/index.ts | 6 +- .../composables/useUpsellProducts/index.ts | 2 +- .../catalog/product/getters/productGetters.ts | 8 +- modules/checkout/components/CartSidebar.vue | 488 ++++++++++++++++++ .../components/__tests__/CartSidebar.spec.ts | 234 +++++++++ .../commands/saveBillingAddressCommand.ts | 3 +- .../checkout/composables/useBilling/index.ts | 8 +- .../useCart/commands/addItemCommand.ts | 59 ++- .../useCart/commands/applyCouponCommand.ts | 3 +- .../useCart/commands/loadTotalQtyCommand.ts | 6 +- .../useCart/commands/removeCouponCommand.ts | 4 +- .../useCart/commands/removeItemCommand.ts | 15 +- .../useCart/commands/updateItemQtyCommand.ts | 3 +- modules/checkout/composables/useCart/index.ts | 14 +- .../checkout/composables/useCartView/index.ts | 132 +++++ .../composables/useCartView/useCartView.ts | 63 +++ .../getCustomerShippingMethodsCommand.ts | 2 +- .../getGuestShippingMethodsCommand.ts | 6 +- .../commands/placeOrderCommand.ts | 6 +- .../composables/useMakeOrder/index.ts | 6 +- .../getAvailablePaymentMethodsCommand.ts | 11 +- .../commands/setPaymentMethodOnCartCommand.ts | 2 +- .../composables/usePaymentProvider/index.ts | 7 +- .../usePaymentProvider/usePaymentProvider.ts | 4 +- .../setShippingMethodsOnCartCommand.ts | 11 +- .../composables/useShippingProvider/index.ts | 8 +- modules/checkout/getters/orderGetters.ts | 10 +- modules/checkout/index.ts | 5 + modules/checkout/pages/Cart.vue | 473 +++++++++++++++++ .../GoogleFontsAPI/probeGoogleFontsApi.ts | 19 + modules/core/helpers/getLocaleSettings.ts | 18 + .../core/helpers/mapConfigToSetupObject.ts | 11 + modules/core/integrationPlugin/_proxyUtils.ts | 49 ++ modules/core/integrationPlugin/context.ts | 37 ++ modules/core/integrationPlugin/index.ts | 63 +++ modules/core/plugin.ts | 4 +- .../composables/useAddresses/index.ts | 24 +- .../composables/useAddresses/useAddresses.ts | 4 +- .../composables/useForgotPassword/index.ts | 4 +- .../commands/attachToCartCommand.ts | 2 +- modules/customer/composables/useUser/index.ts | 22 +- .../commands/createCustomerAddressCommand.ts | 6 +- .../commands/deleteCustomerAddressCommand.ts | 6 +- .../commands/updateCustomerAddressCommand.ts | 6 +- .../composables/useUserAddress/index.ts | 19 +- .../useUserAddress/useUserAddress.ts | 4 +- .../composables/useUserOrder/index.ts | 2 +- .../AddressesDetails/AddressForm.vue | 15 +- .../customer/pages/MyAccount/MyAccount.vue | 15 +- .../customer/pages/MyAccount/MyWishlist.vue | 25 +- .../MyAccount/OrderHistory/OrderHistory.vue | 5 +- .../OrderHistory/SingleOrder/SingleOrder.vue | 6 +- .../pages/MyAccount/useSidebarLinkGroups.ts | 27 +- .../useReview/commands/addReviewCommand.ts | 2 +- .../commands/loadCustomerReviewsCommand.ts | 2 +- .../commands/loadReviewMetadataCommand.ts | 2 +- .../commands/searchReviewsCommand.ts | 6 +- .../wishlist/components/WishlistSidebar.vue | 17 +- .../wishlist/composables/useWishlist/index.ts | 20 +- nuxt.config.js | 51 +- package.json | 5 +- pages/Cms.vue | 72 +++ pages/Home.vue | 27 +- pages/Page.vue | 99 +--- plugins/fcPlugin.ts | 2 +- routes.js | 21 +- stores/page.ts | 11 + tests/unit/mocks/index.js | 1 + tests/unit/mocks/useCartView.js | 409 +++++++++++++++ tests/unit/mocks/useRoute.ts | 4 +- types/core.ts | 2 + yarn.lock | 13 +- 122 files changed, 2861 insertions(+), 442 deletions(-) create mode 100644 helpers/getMetaInfo.ts create mode 100644 middleware/__tests__/url-resolver.spec.js create mode 100644 middleware/url-resolver.ts create mode 100644 modules/catalog/category/composables/useCategory/categoryMeta.gql.ts create mode 100644 modules/checkout/components/CartSidebar.vue create mode 100644 modules/checkout/components/__tests__/CartSidebar.spec.ts create mode 100644 modules/checkout/composables/useCartView/index.ts create mode 100644 modules/checkout/composables/useCartView/useCartView.ts create mode 100644 modules/checkout/pages/Cart.vue create mode 100644 modules/core/GoogleFontsAPI/probeGoogleFontsApi.ts create mode 100644 modules/core/helpers/getLocaleSettings.ts create mode 100644 modules/core/helpers/mapConfigToSetupObject.ts create mode 100644 modules/core/integrationPlugin/_proxyUtils.ts create mode 100644 modules/core/integrationPlugin/context.ts create mode 100644 modules/core/integrationPlugin/index.ts create mode 100644 pages/Cms.vue create mode 100644 stores/page.ts create mode 100644 tests/unit/mocks/useCartView.js diff --git a/.env.example b/.env.example index a9f0993..cb42a3b 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,11 @@ VSF_NUXT_APP_ENV=development VSF_NUXT_APP_PORT=3000 +VSF_NUXT_APP_HOST=localhost VSF_STORE_URL=https://localhost:3000 VSF_MIDDLEWARE_URL=https://localhost:3000/api/ +VSF_SSR_MIDDLEWARE_URL=http://localhost:3000/api/ + VSF_MAGENTO_BASE_URL={YOUR_SITE_FRONT_URL} VSF_MAGENTO_GRAPHQL_URL=https://{YOUR_SITE_FRONT_URL}/graphql @@ -28,4 +31,9 @@ VSF_RECAPTCHA_SIZE=invisible VSF_RECAPTCHA_MIN_SCORE=0.5 VSF_RECAPTCHA_VERSION=3 +VSF_COOKIE_HTTP_ONLY= +VSF_COOKIE_SECURE= +VSF_COOKIE_SAME_SITE= +VSF_COOKIE_PATH= + NODE_TLS_REJECT_UNAUTHORIZED=0 diff --git a/components/Header/SearchBar/SearchResults.vue b/components/Header/SearchBar/SearchResults.vue index 4fe1794..af766ef 100644 --- a/components/Header/SearchBar/SearchResults.vue +++ b/components/Header/SearchBar/SearchResults.vue @@ -55,13 +55,7 @@ }" :alt="productGetters.getName(product)" :title="productGetters.getName(product)" - :link=" - localePath( - `/p/${productGetters.getProductSku( - product - )}${productGetters.getSlug(product, product.categories[0])}` - ) - " + :link="localePath(getProductPath(product))" :wishlist-icon="false" /> @@ -88,13 +82,7 @@ }" :alt="productGetters.getName(product)" :title="productGetters.getName(product)" - :link=" - localePath( - `/p/${productGetters.getProductSku( - product - )}${productGetters.getSlug(product, product.categories[0])}` - ) - " + :link="localePath(getProductPath(product))" :wishlist-icon="false" /> @@ -148,7 +136,7 @@ import { import { defineComponent } from '@nuxtjs/composition-api'; import type { PropType } from '@nuxtjs/composition-api'; import productGetters from '~/modules/catalog/product/getters/productGetters'; -import { useImage } from '~/composables'; +import { useImage, useProduct } from '~/composables'; import SvgImage from '~/components/General/SvgImage.vue'; import type { Product } from '~/modules/catalog/product/types'; @@ -174,11 +162,13 @@ export default defineComponent({ }, setup() { const { getMagentoImage, imageSizes } = useImage(); + const { getProductPath } = useProduct(); return { productGetters, getMagentoImage, imageSizes, + getProductPath, }; }, }); diff --git a/components/NewProducts.vue b/components/NewProducts.vue index 79f222a..2a9c333 100644 --- a/components/NewProducts.vue +++ b/components/NewProducts.vue @@ -34,13 +34,7 @@ productGetters.getPrice(product).special && $fc(productGetters.getPrice(product).special) " - :link=" - localePath( - `/p/${productGetters.getProductSku( - product - )}${productGetters.getSlug(product, product.categories[0])}` - ) - " + :link="localePath(getProductPath(product))" :max-rating="5" :score-rating="productGetters.getAverageRating(product)" :reviews-count="productGetters.getTotalReviews(product)" @@ -65,7 +59,8 @@ import { computed, defineComponent, onMounted, ref, } from '@nuxtjs/composition-api'; import { - useImage, useProduct, + useImage, + useProduct, } from '~/composables'; import useWishlist from '~/modules/wishlist/composables/useWishlist'; import productGetters from '~/modules/catalog/product/getters/productGetters'; @@ -97,7 +92,11 @@ export default defineComponent({ }, setup() { const { isAuthenticated } = useUser(); - const { getProductList, loading } = useProduct(); + const { + getProductList, + loading, + getProductPath, + } = useProduct(); const { isInWishlist, addOrRemoveItem } = useWishlist(); const { addItemToCart, isInCart } = useAddToCart(); const products = ref([]); @@ -138,6 +137,7 @@ export default defineComponent({ productGetters, getMagentoImage, imageSizes, + getProductPath, }; }, }); diff --git a/composables/index.ts b/composables/index.ts index c104415..e8b1811 100644 --- a/composables/index.ts +++ b/composables/index.ts @@ -42,6 +42,7 @@ export * from '../modules/customer/composables/useUserAddress'; export * from '../modules/customer/composables/useUserOrder'; export * from '../modules/wishlist/composables/useWishlist'; export * from './useMagentoConfiguration'; +export * from '../modules/checkout/composables/useCartView'; export * from './types'; export * from '../modules/GraphQL/types'; diff --git a/composables/types.ts b/composables/types.ts index 071b916..8872a95 100644 --- a/composables/types.ts +++ b/composables/types.ts @@ -3,13 +3,14 @@ import type { CountriesListQuery, } from '~/modules/GraphQL/types'; -import type { CustomQuery } from '~/types/core'; +import type { CustomQuery, CustomHeaders } from '~/types/core'; export declare type AvailableStores = AvailableStoresQuery['availableStores']; export declare type Countries = CountriesListQuery['countries'][0]; export declare type ComposableFunctionArgs = T & { customQuery?: CustomQuery; + customHeaders?: CustomHeaders; }; export interface Totals { diff --git a/composables/useConfig/index.ts b/composables/useConfig/index.ts index 57de813..f63ccc7 100644 --- a/composables/useConfig/index.ts +++ b/composables/useConfig/index.ts @@ -24,7 +24,7 @@ export function useConfig(): UseConfigInterface { Logger.debug('useConfig/load'); try { - const { data } = await app.$vsf.$magento.api.storeConfig(params?.customQuery ?? null); + const { data } = await app.$vsf.$magento.api.storeConfig(params?.customQuery ?? null, params?.customHeaders); configStore.$patch((state) => { state.storeConfig = data.storeConfig || {}; }); diff --git a/composables/useContent/commands/loadBlocksCommand.ts b/composables/useContent/commands/loadBlocksCommand.ts index a4f02af..dd54039 100644 --- a/composables/useContent/commands/loadBlocksCommand.ts +++ b/composables/useContent/commands/loadBlocksCommand.ts @@ -5,7 +5,7 @@ export const loadBlocksCommand = { execute: async (context: VsfContext, params) => { Logger.debug('[Magento]: Load CMS Blocks content', { params }); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const { data } = await context.$magento.api.cmsBlocks(params.identifiers, params.customQuery ?? null); + const { data } = await context.$magento.api.cmsBlocks(params.identifiers, params.customQuery ?? null, params?.customHeaders); Logger.debug('[Result]:', { data }); diff --git a/composables/useContent/commands/loadContentCommand.ts b/composables/useContent/commands/loadContentCommand.ts index f7bddcc..4d6d59d 100644 --- a/composables/useContent/commands/loadContentCommand.ts +++ b/composables/useContent/commands/loadContentCommand.ts @@ -5,7 +5,7 @@ export const loadContentCommand = { execute: async (context: VsfContext, params) => { Logger.debug('[Magento]: Load CMS Page content', { params }); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const { data } = await context.$magento.api.cmsPage(params.identifier, params.customQuery ?? null); + const { data } = await context.$magento.api.cmsPage(params.identifier, params.customQuery ?? null, params?.customHeaders); Logger.debug('[Result]:', { data }); diff --git a/composables/useCountrySearch/index.ts b/composables/useCountrySearch/index.ts index b1c749c..216abc5 100644 --- a/composables/useCountrySearch/index.ts +++ b/composables/useCountrySearch/index.ts @@ -25,7 +25,7 @@ export function useCountrySearch(): UseCountrySearchInterface { Logger.debug('[Magento]: Search country information based on', { params }); - const { data } = await app.$vsf.$magento.api.country(params.id, params?.customQuery ?? null); + const { data } = await app.$vsf.$magento.api.country(params.id, params?.customQuery ?? null, params?.customHeaders ?? null); Logger.debug('[Result]:', { data }); diff --git a/composables/useCurrency/__tests__/useCurrency.spec.ts b/composables/useCurrency/__tests__/useCurrency.spec.ts index 709c470..3d45299 100644 --- a/composables/useCurrency/__tests__/useCurrency.spec.ts +++ b/composables/useCurrency/__tests__/useCurrency.spec.ts @@ -61,6 +61,6 @@ describe('useCurrency', () => { // then expect(appMock.app.$vsf.$magento.api.currency) - .toHaveBeenCalledWith({ currency: 'custom-currency-query' }); + .toHaveBeenCalledWith({ currency: 'custom-currency-query' }, null); }); }); diff --git a/composables/useCurrency/index.ts b/composables/useCurrency/index.ts index 404b73d..aeabdd7 100644 --- a/composables/useCurrency/index.ts +++ b/composables/useCurrency/index.ts @@ -32,7 +32,7 @@ export function useCurrency(): UseCurrencyInterface { Logger.debug('useCurrency/load'); try { - const { data } = await app.$vsf.$magento.api.currency(params?.customQuery ?? null); + const { data } = await app.$vsf.$magento.api.currency(params?.customQuery ?? null, params?.customHeaders ?? null); configStore.$patch((state) => { state.currency = data?.currency ?? {}; }); diff --git a/composables/useImage/index.ts b/composables/useImage/index.ts index 2ea1f8b..aabd749 100644 --- a/composables/useImage/index.ts +++ b/composables/useImage/index.ts @@ -36,11 +36,13 @@ export function useImage(): UseImageInterface { /** * Extract image path from Magento URL. * - * @param fullImageUrl {string} + * @param fullImageUrl {string | null} * * @return {string} */ - const getMagentoImage = (fullImageUrl: string) => { + const getMagentoImage = (fullImageUrl: string | null) => { + if (!fullImageUrl) return ''; + // @ts-ignore const { imageProvider, magentoBaseUrl } = context.$vsf.$magento.config; diff --git a/composables/useNewsletter/commands/updateSubscriptionCommand.ts b/composables/useNewsletter/commands/updateSubscriptionCommand.ts index 3c369fb..903846c 100644 --- a/composables/useNewsletter/commands/updateSubscriptionCommand.ts +++ b/composables/useNewsletter/commands/updateSubscriptionCommand.ts @@ -6,7 +6,7 @@ export const updateSubscriptionCommand = { execute: async (context: UseContextReturn, params: UseNewsletterUpdateSubscriptionParams): Promise => { const { data } = await context.app.$vsf.$magento.api.subscribeEmailToNewsletter({ email: params.email, - }, params?.customQuery ?? null); + }, params?.customQuery ?? null, params?.customHeaders ?? null); return data?.subscribeEmailToNewsletter?.status ?? null; }, diff --git a/composables/useStore/index.ts b/composables/useStore/index.ts index fe1d1e4..be5a368 100644 --- a/composables/useStore/index.ts +++ b/composables/useStore/index.ts @@ -24,13 +24,13 @@ export function useStore(): UseStoreInterface { const configStore = useConfigStore(); const { app } = useContext(); - const load = async (customQuery = { availableStores: 'availableStores' }): Promise => { + const load = async (customQuery = { availableStores: 'availableStores' }, customHeaders = {}): Promise => { Logger.debug('useStoreFactory.load'); error.value.load = null; try { loading.value = true; - const { data } = await app.$vsf.$magento.api.availableStores(customQuery); + const { data } = await app.$vsf.$magento.api.availableStores(customQuery, customHeaders); configStore.$patch((state) => { state.stores = data?.availableStores ?? []; diff --git a/composables/useUiHelpers/index.ts b/composables/useUiHelpers/index.ts index 79712d5..4d87967 100644 --- a/composables/useUiHelpers/index.ts +++ b/composables/useUiHelpers/index.ts @@ -72,7 +72,7 @@ export function useUiHelpers(): UseUiHelpersInterface { }; }; - const getCatLink = (category: CategoryTree): string => `/c/${category.url_path}${category.url_suffix || ''}`; + const getCatLink = (category: CategoryTree): string => `/${category.url_path}${category.url_suffix || ''}`; /** * Force push for a backward compatibility in other places, should be removed diff --git a/composables/useUrlResolver/index.ts b/composables/useUrlResolver/index.ts index 18b316a..c330975 100644 --- a/composables/useUrlResolver/index.ts +++ b/composables/useUrlResolver/index.ts @@ -31,7 +31,7 @@ export function useUrlResolver(): UseUrlResolverInterface { try { const clearUrl = path.replace(/[a-z]+\/[cp|]\//gi, ''); Logger.debug('[Magento] Find information based on URL', { clearUrl }); - const { data } = await context.$magento.api.route(clearUrl, params?.customQuery ?? null); + const { data } = await context.$magento.api.route(clearUrl, params?.customQuery ?? null, params?.customHeaders ?? null); results = data?.route ?? null; if (!results) nuxtError({ statusCode: 404 }); diff --git a/helpers/cart/addToCart.ts b/helpers/cart/addToCart.ts index aec08e0..253b259 100644 --- a/helpers/cart/addToCart.ts +++ b/helpers/cart/addToCart.ts @@ -1,7 +1,7 @@ import { useRouter, useContext } from '@nuxtjs/composition-api'; import type { Product } from '~/modules/catalog/product/types'; -import productGetters from '~/modules/catalog/product/getters/productGetters'; import useCart from '~/modules/checkout/composables/useCart'; +import { useProduct } from '~/modules/catalog/product/composables/useProduct'; export const useAddToCart = () => { const { @@ -10,6 +10,7 @@ export const useAddToCart = () => { } = useCart(); const router = useRouter(); const { app } = useContext(); + const { getProductPath } = useProduct(); const addItemToCart = async (params: { product: Product, quantity: number }) => { const { product, quantity } = params; // @ts-ignore @@ -26,16 +27,7 @@ export const useAddToCart = () => { case 'BundleProduct': case 'ConfigurableProduct': case 'GroupedProduct': - const sku = productGetters.getProductSku(product); - const slug = productGetters.getSlug(product).replace(/^\//, ''); // remove leading slash from getSlug - - const path = app.localeRoute({ - name: 'product', - params: { - id: sku, - slug, - }, - }); + const path = app.localeRoute(getProductPath(product)); await router.push(path); break; diff --git a/helpers/getMetaInfo.ts b/helpers/getMetaInfo.ts new file mode 100644 index 0000000..3816aa5 --- /dev/null +++ b/helpers/getMetaInfo.ts @@ -0,0 +1,37 @@ +import type { MetaInfo } from 'vue-meta'; + +export const getMetaInfo = (page: any, isNoIndex: boolean = false): MetaInfo => { + if (!page) { + return null; + } + + const seoTags: MetaInfo = { + meta: [], + }; + + if (page?.meta_title || page?.title || page?.name) { + seoTags.title = page?.meta_title || page?.title || page?.name; + } + if (page?.meta_description) { + seoTags.meta.push({ + hid: 'description', + name: 'description', + content: page.meta_description, + }); + } + if (page?.meta_keyword || page?.meta_keywords) { + seoTags.meta.push({ + hid: 'keywords', + name: 'keywords', + content: page?.meta_keyword || page?.meta_keywords, + }); + } + if (isNoIndex) { + seoTags.meta.push({ + name: 'robots', + content: 'noindex, nofollow', + }); + } + + return seoTags; +}; diff --git a/lang/de.js b/lang/de.js index 720cbb6..b436c8d 100644 --- a/lang/de.js +++ b/lang/de.js @@ -66,6 +66,7 @@ export default { "Go back shopping": "Zurück einkaufen", "Go back to shop": "Zurück zum Einkaufen", "Go to checkout": "Zur Kasse gehen", + "Go to cart": "Zur Warenkorb gehen", "Guarantee": "Garantie", "Help": "Hilfe", "Help & FAQs": "Hilfe & FAQs", @@ -305,4 +306,6 @@ export default { "Payment date":"Zahlungsdatum", "The user password was changed successfully updated!":"Das Benutzerpasswort wurde erfolgreich geändert aktualisiert!", "The user account data was successfully updated!":"Die Benutzerkontodaten wurden erfolgreich aktualisiert!", + "You submitted your review for moderation.": "Sie haben Ihre Bewertung zur Moderation eingereicht.", + "Starting at": "Beginnt um", }; diff --git a/lang/en.js b/lang/en.js index 2125ab4..066fbb9 100644 --- a/lang/en.js +++ b/lang/en.js @@ -64,6 +64,7 @@ export default { "Go back shopping": "Go back shopping", "Go back to shop": "Go back to shop", "Go to checkout": "Go to checkout", + "Go to cart": "Go to cart", "Guarantee": "Guarantee", "Help": "Help", "Help & FAQs": "Help & FAQs", @@ -303,4 +304,6 @@ export default { "Payment date":"Payment date", "The user password was changed successfully updated!":"The user password was changed successfully updated!", "The user account data was successfully updated!":"The user account data was successfully updated!", + "You submitted your review for moderation.": "You submitted your review for moderation.", + "Starting at": "Starting at", }; diff --git a/layouts/default.vue b/layouts/default.vue index e4fd2b9..1f9dbf1 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -42,7 +42,7 @@ export default defineComponent({ IconSprite, TopBar, AppFooter: () => import(/* webpackPrefetch: true */ '~/components/AppFooter.vue'), - CartSidebar: () => import(/* webpackPrefetch: true */ '~/components/CartSidebar.vue'), + CartSidebar: () => import(/* webpackPrefetch: true */ '~/modules/checkout/components/CartSidebar.vue'), WishlistSidebar: () => import(/* webpackPrefetch: true */ '~/modules/wishlist/components/WishlistSidebar.vue'), LoginModal: () => import(/* webpackPrefetch: true */ '~/modules/customer/components/LoginModal/LoginModal.vue'), Notification: () => import(/* webpackPrefetch: true */ '~/components/Notification.vue'), diff --git a/middleware.config.js b/middleware.config.js index db33334..d9bad13 100755 --- a/middleware.config.js +++ b/middleware.config.js @@ -29,10 +29,10 @@ module.exports = { ...cookieNames, }, cookiesDefaultOpts: { - httpOnly: false, - secure: true, - sameSite: 'Strict', - path: '/', + httpOnly: process.env.VSF_COOKIE_HTTP_ONLY || false, + secure: process.env.VSF_COOKIE_SECURE || false, + sameSite: process.env.VSF_COOKIE_SAME_SITE || 'Strict', + path: process.env.VSF_COOKIE_PATH || '/', }, defaultStore: 'default', externalCheckout: { diff --git a/middleware/__tests__/url-resolver.spec.js b/middleware/__tests__/url-resolver.spec.js new file mode 100644 index 0000000..8d80386 --- /dev/null +++ b/middleware/__tests__/url-resolver.spec.js @@ -0,0 +1,88 @@ +import { createPinia, setActivePinia } from 'pinia'; +import { usePageStore } from '~/stores/page'; +import urlResolverMiddleware from '../url-resolver'; + +const errRes = { + data: undefined, + errors: [ + { + message: 'Variable "$url" of required type "String!" was not provided.', + extensions: { + category: 'graphql', + }, + }, + ], +}; + +const emptyRes = { + data: { + route: null, + }, + errors: undefined, +}; + +const validRes = { + data: { + route: { + type: 'CATEGORY', + uid: 'MjEzMA==', + }, + }, + errors: undefined, +}; + +const contextMockFactory = (callbackResponse) => ({ + app: { + $vsf: { + $magento: { + api: { + route: () => callbackResponse, + }, + }, + }, + }, + route: { + path: '/default/set-of-sprite-yoga-straps.html', + }, + i18n: { + locale: 'default', + }, + error: jest.fn(), +}); + +describe('Url resolver middleware', () => { + beforeEach(() => { + jest.resetAllMocks(); + setActivePinia(createPinia()); + }); + + it('should redirect to 404 if response data is null', async () => { + const contextMock = contextMockFactory(emptyRes); + const pageStore = usePageStore(); + + await urlResolverMiddleware(contextMock); + + expect(pageStore.routeData).toBe(null); + expect(contextMock.error).toHaveBeenCalled(); + }); + + it('should redirect to 404 if response is error', async () => { + const contextMock = contextMockFactory(errRes); + const pageStore = usePageStore(); + + await urlResolverMiddleware(contextMock); + + expect(pageStore.routeData).toBe(null); + expect(contextMock.error).toHaveBeenCalled(); + }); + + it('should set a route data if response is data', async () => { + const contextMock = contextMockFactory(validRes); + const pageStore = usePageStore(); + + await urlResolverMiddleware(contextMock); + + expect(typeof pageStore.routeData).toBe('object'); + expect(contextMock.error).toHaveBeenCalledTimes(0); + }); +}); diff --git a/middleware/url-resolver.ts b/middleware/url-resolver.ts new file mode 100644 index 0000000..7d1560c --- /dev/null +++ b/middleware/url-resolver.ts @@ -0,0 +1,27 @@ +import { Middleware } from '@nuxt/types'; +import { usePageStore } from '~/stores/page'; +import { Logger } from '~/helpers/logger'; +import { RoutableInterface } from '~/modules/GraphQL/types'; + +const urlResolverMiddleware : Middleware = async (context) => { + const pageStore = usePageStore(); + const { path } = context.route; + + const clearUrl = path.replace(/[a-z]+\/[cp|]\//gi, '').replace(`/${context.i18n.locale}`, ''); + + Logger.debug('middleware/url-resolver', clearUrl); + + const { data, errors } = await context.app.$vsf.$magento.api.route(clearUrl); + + Logger.debug('middleware/url-resolver/result', { data, errors }); + + const results: RoutableInterface | null = data?.route ?? null; + + if (!results || errors?.length) context.error({ statusCode: 404 }); + + pageStore.$patch((state) => { + state.routeData = results; + }); +}; + +export default urlResolverMiddleware; diff --git a/modules/GraphQL/types.ts b/modules/GraphQL/types.ts index 66dfe76..33c1c7d 100644 --- a/modules/GraphQL/types.ts +++ b/modules/GraphQL/types.ts @@ -5344,6 +5344,9 @@ export interface RoutableInterface { relative_url?: Maybe; /** One of PRODUCT, CATEGORY, or CMS_PAGE. */ type?: Maybe; + sku?: Maybe; + uid?: Maybe; + identifier?: Maybe; } /** Comment item details */ export interface SalesCommentItem { diff --git a/modules/catalog/category/components/breadcrumbs/__tests__/CategoryBreadcrumbs.spec.ts b/modules/catalog/category/components/breadcrumbs/__tests__/CategoryBreadcrumbs.spec.ts index 89467f7..f45cd4c 100644 --- a/modules/catalog/category/components/breadcrumbs/__tests__/CategoryBreadcrumbs.spec.ts +++ b/modules/catalog/category/components/breadcrumbs/__tests__/CategoryBreadcrumbs.spec.ts @@ -26,7 +26,7 @@ jest.mock('~/modules/catalog/category/helpers/useTraverseCategory'); }); (useUiHelpers as jest.Mock).mockReturnValue({ getCatLink: jest.fn( - (category: CategoryTree): string => `/c/${category.url_path}${category.url_suffix || ''}`, + (category: CategoryTree): string => `/${category.url_path}${category.url_suffix || ''}`, ), }); diff --git a/modules/catalog/category/components/views/__tests__/productsMock.ts b/modules/catalog/category/components/views/__tests__/productsMock.ts index 4808122..40e068b 100644 --- a/modules/catalog/category/components/views/__tests__/productsMock.ts +++ b/modules/catalog/category/components/views/__tests__/productsMock.ts @@ -21,7 +21,7 @@ export const productsMock = [ review_count: 0, reviews: { __typename: 'ProductReviews', items: [] }, commonProps: { - title: 'Set of Sprite Yoga Straps', link: '/default/p/24-WG085_Group/set-of-sprite-yoga-straps.html', style: { '--index': 0 }, isAddedToCart: false, image: 'media/catalog/product/l/u/luma-yoga-strap-set.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$14.00', specialPrice: null, reviewsCount: 0, scoreRating: 0, + title: 'Set of Sprite Yoga Straps', link: '/default/set-of-sprite-yoga-straps.html', style: { '--index': 0 }, isAddedToCart: false, image: 'media/catalog/product/l/u/luma-yoga-strap-set.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$14.00', specialPrice: null, reviewsCount: 0, scoreRating: 0, }, }, { __typename: 'BundleProduct', @@ -45,7 +45,7 @@ export const productsMock = [ review_count: 0, reviews: { __typename: 'ProductReviews', items: [] }, commonProps: { - title: 'Sprite Yoga Companion Kit', link: '/default/p/24-WG080/sprite-yoga-companion-kit.html', style: { '--index': 1 }, isAddedToCart: false, image: 'media/catalog/product/l/u/luma-yoga-kit-2.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$61.00', specialPrice: null, reviewsCount: 0, scoreRating: 0, + title: 'Sprite Yoga Companion Kit', link: '/default/sprite-yoga-companion-kit.html', style: { '--index': 1 }, isAddedToCart: false, image: 'media/catalog/product/l/u/luma-yoga-kit-2.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$61.00', specialPrice: null, reviewsCount: 0, scoreRating: 0, }, }, { __typename: 'SimpleProduct', @@ -73,7 +73,7 @@ export const productsMock = [ review_count: 3, reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 100, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '5' }] }, { __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }] }, commonProps: { - title: 'Didi Sport Watch', link: '/default/p/24-WG02/didi-sport-watch.html', style: { '--index': 2 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg02-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$92.00', specialPrice: null, reviewsCount: 3, scoreRating: 3.666_666_666_666_666_5, + title: 'Didi Sport Watch', link: '/default/didi-sport-watch.html', style: { '--index': 2 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg02-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$92.00', specialPrice: null, reviewsCount: 3, scoreRating: 3.666_666_666_666_666_5, }, }, { __typename: 'SimpleProduct', @@ -97,7 +97,7 @@ export const productsMock = [ review_count: 3, reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }] }, commonProps: { - title: 'Clamber Watch', link: '/default/p/24-WG03/clamber-watch.html', style: { '--index': 3 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg03-gr-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$54.00', specialPrice: null, reviewsCount: 3, scoreRating: 2.666_666_666_666_666_5, + title: 'Clamber Watch', link: '/default/clamber-watch.html', style: { '--index': 3 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg03-gr-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$54.00', specialPrice: null, reviewsCount: 3, scoreRating: 2.666_666_666_666_666_5, }, }, { __typename: 'SimpleProduct', @@ -121,7 +121,7 @@ export const productsMock = [ review_count: 3, reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }] }, commonProps: { - title: 'Bolo Sport Watch', link: '/default/p/24-WG01/bolo-sport-watch.html', style: { '--index': 4 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg01-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$49.00', specialPrice: null, reviewsCount: 3, scoreRating: 3.333_333_333_333_333_5, + title: 'Bolo Sport Watch', link: '/default/bolo-sport-watch.html', style: { '--index': 4 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg01-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$49.00', specialPrice: null, reviewsCount: 3, scoreRating: 3.333_333_333_333_333_5, }, }, { __typename: 'SimpleProduct', @@ -145,7 +145,7 @@ export const productsMock = [ review_count: 2, reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }] }, commonProps: { - title: 'Luma Analog Watch', link: '/default/p/24-WG09/luma-analog-watch.html', style: { '--index': 5 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg09-gr-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$43.00', specialPrice: null, reviewsCount: 2, scoreRating: 4, + title: 'Luma Analog Watch', link: '/default/luma-analog-watch.html', style: { '--index': 5 }, isAddedToCart: false, image: 'media/catalog/product/w/g/wg09-gr-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$43.00', specialPrice: null, reviewsCount: 2, scoreRating: 4, }, }, { __typename: 'SimpleProduct', @@ -173,7 +173,7 @@ export const productsMock = [ review_count: 3, reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }] }, commonProps: { - title: 'Dash Digital Watch', link: '/default/p/24-MG02/dash-digital-watch.html', style: { '--index': 6 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg02-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$92.00', specialPrice: null, reviewsCount: 3, scoreRating: 3.666_666_666_666_666_5, + title: 'Dash Digital Watch', link: '/default/dash-digital-watch.html', style: { '--index': 6 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg02-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$92.00', specialPrice: null, reviewsCount: 3, scoreRating: 3.666_666_666_666_666_5, }, }, { __typename: 'SimpleProduct', @@ -201,7 +201,7 @@ export const productsMock = [ review_count: 4, reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 100, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '5' }] }, { __typename: 'ProductReview', average_rating: 80, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '4' }] }, { __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }, { __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }] }, commonProps: { - title: 'Cruise Dual Analog Watch', link: '/default/p/24-MG05/cruise-dual-analog-watch.html', style: { '--index': 7 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg05-br-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$55.00', specialPrice: null, reviewsCount: 4, scoreRating: 3.25, + title: 'Cruise Dual Analog Watch', link: '/default/cruise-dual-analog-watch.html', style: { '--index': 7 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg05-br-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$55.00', specialPrice: null, reviewsCount: 4, scoreRating: 3.25, }, }, { __typename: 'SimpleProduct', @@ -229,7 +229,7 @@ export const productsMock = [ review_count: 3, reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }, { __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }, { __typename: 'ProductReview', average_rating: 40, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '2' }] }] }, commonProps: { - title: 'Summit Watch', link: '/default/p/24-MG03/summit-watch.html', style: { '--index': 8 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg03-br-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$54.00', specialPrice: null, reviewsCount: 3, scoreRating: 2.333_333_333_333_333_5, + title: 'Summit Watch', link: '/default/summit-watch.html', style: { '--index': 8 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg03-br-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$54.00', specialPrice: null, reviewsCount: 3, scoreRating: 2.333_333_333_333_333_5, }, }, { __typename: 'SimpleProduct', @@ -253,6 +253,6 @@ export const productsMock = [ review_count: 3, reviews: { __typename: 'ProductReviews', items: [{ __typename: 'ProductReview', average_rating: 100, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '5' }] }, { __typename: 'ProductReview', average_rating: 100, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '5' }] }, { __typename: 'ProductReview', average_rating: 60, ratings_breakdown: [{ __typename: 'ProductReviewRating', name: 'Rating', value: '3' }] }] }, commonProps: { - title: 'Endurance Watch', link: '/default/p/24-MG01/endurance-watch.html', style: { '--index': 9 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg01-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$49.00', specialPrice: null, reviewsCount: 3, scoreRating: 4.333_333_333_333_333, + title: 'Endurance Watch', link: '/default/endurance-watch.html', style: { '--index': 9 }, isAddedToCart: false, image: 'media/catalog/product/m/g/mg01-bk-0.jpg', imageTag: 'nuxt-img', nuxtImgConfig: { fit: 'cover' }, isInWishlist: false, isInWishlistIcon: 'heart_fill', wishlistIcon: 'heart', regularPrice: '$49.00', specialPrice: null, reviewsCount: 3, scoreRating: 4.333_333_333_333_333, }, }]; diff --git a/modules/catalog/category/components/views/useProductsWithCommonCardProps.ts b/modules/catalog/category/components/views/useProductsWithCommonCardProps.ts index 7668c70..c591b79 100644 --- a/modules/catalog/category/components/views/useProductsWithCommonCardProps.ts +++ b/modules/catalog/category/components/views/useProductsWithCommonCardProps.ts @@ -4,8 +4,9 @@ import type { ImageModifiers } from '@nuxt/image'; import { useImage } from '~/composables'; import { useUser } from '~/modules/customer/composables/useUser'; import { useWishlist } from '~/modules/wishlist/composables/useWishlist'; +import { useProduct } from '~/modules/catalog/product/composables/useProduct'; import { - getName, getPrice, getProductSku, getProductThumbnailImage, getSlug, + getName, getPrice, getProductThumbnailImage, } from '~/modules/catalog/product/getters/productGetters'; import { getAverageRating, getTotalReviews } from '~/modules/review/getters/reviewGetters'; import { useAddToCart } from '~/helpers/cart/addToCart'; @@ -40,6 +41,7 @@ export const useProductsWithCommonProductCardProps = (products: Ref) const { isInWishlist } = useWishlist(); const { isAuthenticated } = useUser(); const { isInCart } = useAddToCart(); + const { getProductPath } = useProduct(); const context = useContext(); /** @@ -74,7 +76,7 @@ export const useProductsWithCommonProductCardProps = (products: Ref) scoreRating: getAverageRating(product), }; - const link = context.localeRoute({ name: 'product', params: { id: getProductSku(product), slug: getSlug(product).slice(1) } }); + const link = context.localeRoute(getProductPath(product)); const commonProps = { title: getName(product), diff --git a/modules/catalog/category/composables/useCategory/categoryMeta.gql.ts b/modules/catalog/category/composables/useCategory/categoryMeta.gql.ts new file mode 100644 index 0000000..f51ec27 --- /dev/null +++ b/modules/catalog/category/composables/useCategory/categoryMeta.gql.ts @@ -0,0 +1,12 @@ +export default ` + query categoryMeta($filters: CategoryFilterInput) { + categories(filters: $filters) { + items { + meta_title + meta_description + meta_keywords + name + } + } + } +`; diff --git a/modules/catalog/category/composables/useCategory/index.ts b/modules/catalog/category/composables/useCategory/index.ts index 6500e50..932fe14 100644 --- a/modules/catalog/category/composables/useCategory/index.ts +++ b/modules/catalog/category/composables/useCategory/index.ts @@ -1,10 +1,12 @@ import { Ref, ref, useContext } from '@nuxtjs/composition-api'; import { Logger } from '~/helpers/logger'; import type{ CategoryTree } from '~/modules/GraphQL/types'; +import categoryMetaGql from '~/modules/catalog/category/composables/useCategory/categoryMeta.gql'; import type { UseCategoryErrors, UseCategoryInterface, UseCategoryParamsInput, + UseCategoryMetaParamsInput, } from './useCategory'; /** @@ -72,6 +74,7 @@ export function useCategory(): UseCategoryInterface { const loading: Ref = ref(false); const error: Ref = ref({ load: null, + loadCategoryMeta: null, }); const categories: Ref> = ref(null); @@ -80,9 +83,9 @@ export function useCategory(): UseCategoryInterface { try { loading.value = true; - const { data } = await app.context.$vsf.$magento.api.categoryList(params, params?.customQuery ?? null); + const { data } = await app.context.$vsf.$magento.api.categoryList(params, params?.customQuery ?? null, params?.customHeaders); Logger.debug('[Result]:', { data }); - categories.value = data.categories.items; + categories.value = data?.categories?.items ?? []; error.value.load = null; } catch (err) { error.value.load = err; @@ -92,8 +95,40 @@ export function useCategory(): UseCategoryInterface { } }; + const loadCategoryMeta = async (params: UseCategoryMetaParamsInput): Promise => { + Logger.debug('useCategory/loadCategoryMeta', params); + let categoryMeta = null; + + try { + loading.value = true; + + const { data } = await app.context.$vsf.$magento.api.customQuery({ + query: categoryMetaGql, + queryVariables: { + filters: { + category_uid: { + eq: params.category_uid, + }, + }, + }, + customHeaders: params?.customHeaders, + }); + Logger.debug('[Result]:', { data }); + categoryMeta = data.categoryList?.[0] || null; + error.value.loadCategoryMeta = null; + } catch (err) { + error.value.loadCategoryMeta = err; + Logger.error('useCategory/loadCategoryMeta', err); + } finally { + loading.value = false; + } + + return categoryMeta; + }; + return { load, + loadCategoryMeta, loading, error, categories, diff --git a/modules/catalog/category/composables/useCategory/useCategory.ts b/modules/catalog/category/composables/useCategory/useCategory.ts index ba54618..afd558c 100644 --- a/modules/catalog/category/composables/useCategory/useCategory.ts +++ b/modules/catalog/category/composables/useCategory/useCategory.ts @@ -34,6 +34,7 @@ import type{ CategoryTree } from '~/modules/GraphQL/types'; export interface UseCategoryErrors { /** Error when loading categories fails, otherwise is `null`. */ load: Error; + loadCategoryMeta: Error; } /** The {@link useCategory} params object received by `load` function. */ @@ -41,6 +42,11 @@ export type UseCategoryParamsInput = ComposableFunctionArgs< { pageSize: number; }>; +/** The {@link useCategory} params object received by `loadCategoryMeta` function. */ +export type UseCategoryMetaParamsInput = ComposableFunctionArgs< { + category_uid: string; +}>; + /** * Data and methods returned from the {@link useCategory} composable * */ @@ -87,4 +93,5 @@ export interface UseCategoryInterface { * ``` */ load(params: ComposableFunctionArgs): Promise; + loadCategoryMeta(params: ComposableFunctionArgs): Promise; } diff --git a/modules/catalog/category/composables/useCategorySearch/index.ts b/modules/catalog/category/composables/useCategorySearch/index.ts index 1394e70..4afbd6d 100644 --- a/modules/catalog/category/composables/useCategorySearch/index.ts +++ b/modules/catalog/category/composables/useCategorySearch/index.ts @@ -24,7 +24,7 @@ export function useCategorySearch(): UseCategorySearchInterface { try { loading.value = true; const { filters } = params; - const { data } = await app.context.$vsf.$magento.api.categorySearch({ filters }, params?.customQuery ?? null); + const { data } = await app.context.$vsf.$magento.api.categorySearch({ filters }, params?.customQuery ?? null, params?.customHeaders); Logger.debug('[Result]:', { data }); diff --git a/modules/catalog/category/composables/useFacet/getFacetData.gql.ts b/modules/catalog/category/composables/useFacet/getFacetData.gql.ts index 41073bd..e71126a 100644 --- a/modules/catalog/category/composables/useFacet/getFacetData.gql.ts +++ b/modules/catalog/category/composables/useFacet/getFacetData.gql.ts @@ -44,6 +44,13 @@ export default ` } } } + ... on GroupedProduct { + items { + product { + sku + } + } + } } page_info { current_page diff --git a/modules/catalog/category/helpers/__tests__/useTraverseCategory.spec.ts b/modules/catalog/category/helpers/__tests__/useTraverseCategory.spec.ts index ea9699f..580bbf7 100644 --- a/modules/catalog/category/helpers/__tests__/useTraverseCategory.spec.ts +++ b/modules/catalog/category/helpers/__tests__/useTraverseCategory.spec.ts @@ -19,7 +19,7 @@ jest.mock('@nuxtjs/composition-api', () => { return { ...originalModule, useContext: jest.fn(() => ({ app: { localePath: (suffix: unknown) => `/default${suffix}` } })), - useRoute: jest.fn(() => ({ value: { path: '/default/c/what-is-new.html' } })), + useRoute: jest.fn(() => ({ value: { path: '/default/what-is-new.html' } })), }; }); diff --git a/modules/catalog/category/helpers/useTraverseCategory.ts b/modules/catalog/category/helpers/useTraverseCategory.ts index cf1c758..7a1e637 100644 --- a/modules/catalog/category/helpers/useTraverseCategory.ts +++ b/modules/catalog/category/helpers/useTraverseCategory.ts @@ -19,7 +19,7 @@ export function useTraverseCategory() { const activeCategory = computed(() => { // on localhost the default store is localhost:3000/default/ but in a multi-store Magento instance this can change const urlPathToFind = route.value.path - .replace(context.app.localePath('/c'), '') + .replace(context.app.localePath('/'), '') .replace(/^\//, '') .replace('.html', ''); diff --git a/modules/catalog/index.ts b/modules/catalog/index.ts index 1edc01a..e957a02 100644 --- a/modules/catalog/index.ts +++ b/modules/catalog/index.ts @@ -1,22 +1,5 @@ -import path from 'node:path'; -import url from 'node:url'; import type { Module } from '@nuxt/types'; -import type { NuxtRouteConfig } from '@nuxt/types/config/router'; -const nuxtModule : Module = function categoryModule() { - const themeDir = path.dirname(url.fileURLToPath(import.meta.url)); - - this.extendRoutes((routes: NuxtRouteConfig[]) => { - routes.unshift({ - name: 'category', - path: '/c/:slug_1/:slug_2?/:slug_3?/:slug_4?/:slug_5?', - component: path.resolve(themeDir, 'pages/category.vue'), - }, { - name: 'product', - path: '/p/:id/:slug/', - component: path.resolve(themeDir, 'pages/product.vue'), - }); - }); -}; +const nuxtModule : Module = function categoryModule() {}; export default nuxtModule; diff --git a/modules/catalog/pages/category.vue b/modules/catalog/pages/category.vue index 1602260..1dc0e87 100644 --- a/modules/catalog/pages/category.vue +++ b/modules/catalog/pages/category.vue @@ -113,30 +113,35 @@ import { } from '@storefront-ui/vue'; import { computed, - defineComponent, onMounted, ref, ssrRef, useFetch, + defineComponent, + onMounted, + ref, + ssrRef, + useFetch, } from '@nuxtjs/composition-api'; import { CacheTagPrefix, useCache } from '@vue-storefront/cache'; +import { usePageStore } from '~/stores/page'; import SkeletonLoader from '~/components/SkeletonLoader/index.vue'; import CategoryPagination from '~/modules/catalog/category/components/pagination/CategoryPagination.vue'; import { + useCategory, useFacet, useUiHelpers, useUiState, } from '~/composables'; import { useAddToCart } from '~/helpers/cart/addToCart'; -import { useUrlResolver } from '~/composables/useUrlResolver'; import { useWishlist } from '~/modules/wishlist/composables/useWishlist'; import { usePrice } from '~/modules/catalog/pricing/usePrice'; import { useCategoryContent } from '~/modules/catalog/category/components/cms/useCategoryContent'; import { useTraverseCategory } from '~/modules/catalog/category/helpers/useTraverseCategory'; import facetGetters from '~/modules/catalog/category/getters/facetGetters'; +import { getMetaInfo } from '~/helpers/getMetaInfo'; import CategoryNavbar from '~/modules/catalog/category/components/navbar/CategoryNavbar.vue'; import CategoryBreadcrumbs from '~/modules/catalog/category/components/breadcrumbs/CategoryBreadcrumbs.vue'; -import { isCategoryTreeRoute } from '~/modules/GraphQL/CategoryTreeRouteTypeguard'; -import type { ProductInterface, CategoryTree } from '~/modules/GraphQL/types'; +import type { ProductInterface } from '~/modules/GraphQL/types'; import type { SortingModel } from '~/modules/catalog/category/composables/useFacet/sortingOptions'; import type { Pagination } from '~/composables/types'; import type { Product } from '~/modules/catalog/product/types'; @@ -159,7 +164,9 @@ export default defineComponent({ }, transition: 'fade', setup() { + const { routeData } = usePageStore(); const { getContentData } = useCategoryContent(); + const { loadCategoryMeta } = useCategory(); const { addTags } = useCache(); const uiHelpers = useUiHelpers(); const cmsContent = ref(''); @@ -171,7 +178,6 @@ export default defineComponent({ const productContainerElement = ref(null); - const { search: resolveUrl } = useUrlResolver(); const { toggleFilterSidebar, changeToCategoryListView, @@ -180,6 +186,7 @@ export default defineComponent({ isFilterSidebarOpen, } = useUiState(); const { + load: loadWishlist, addItem: addItemToWishlistBase, isInWishlist, removeItem: removeItemFromWishlist, @@ -187,6 +194,8 @@ export default defineComponent({ const { result, search } = useFacet(); const { addItemToCart } = useAddToCart(); + const categoryMeta = ref(null); + const addItemToWishlist = async (product: Product) => { await (isInWishlist({ product }) ? removeItemFromWishlist({ product }) @@ -195,22 +204,21 @@ export default defineComponent({ const { activeCategory, loadCategoryTree } = useTraverseCategory(); const activeCategoryName = computed(() => activeCategory.value?.name ?? ''); - const routeData = ref(null); + + const categoryUid = routeData.uid; const { fetch } = useFetch(async () => { if (!activeCategory.value) { - loadCategoryTree(); + await loadCategoryTree(); } - const resolvedUrl = await resolveUrl(); - if (isCategoryTreeRoute(resolvedUrl)) routeData.value = resolvedUrl; - - const categoryUid = routeData.value?.uid; - const [content] = await Promise.all([ - getContentData(routeData.value?.uid), + const [content, categoryMetaData] = await Promise.all([ + getContentData(categoryUid as string), + loadCategoryMeta({ category_uid: routeData.value?.uid }), search({ ...uiHelpers.getFacetsFromURL(), category_uid: categoryUid }), ]); + categoryMeta.value = categoryMetaData; cmsContent.value = content?.cmsBlock?.content ?? ''; isShowCms.value = content.isShowCms; isShowProducts.value = content.isShowProducts; @@ -219,7 +227,7 @@ export default defineComponent({ sortBy.value = facetGetters.getSortOptions(result.value); pagination.value = facetGetters.getPagination(result.value); - const tags = [{ prefix: CacheTagPrefix.View, value: 'category' }]; + const tags = [{ prefix: CacheTagPrefix.View, value: routeData.uid }]; const productTags = products.value.map((product) => ({ prefix: CacheTagPrefix.Product, value: product.uid, @@ -229,16 +237,27 @@ export default defineComponent({ }); const isPriceLoaded = ref(false); + onMounted(async () => { + loadWishlist(); const { getPricesBySku } = usePrice(); if (products.value.length > 0) { const skus = products.value.map((item) => item.sku); const priceData = await getPricesBySku(skus, pagination.value.itemsPerPage); - products.value = products.value.map((product) => ({ - ...product, - price_range: priceData.items.find((item) => item.sku === product.sku)?.price_range, - })); + products.value = products.value.map((product) => { + const priceRange = priceData.items.find((item) => item.sku === product.sku)?.price_range; + + if (priceRange) { + return { + ...product, + price_range: priceRange, + }; + } + + return { ...product }; + }); } + isPriceLoaded.value = true; }); @@ -277,10 +296,14 @@ export default defineComponent({ routeData, doChangeItemsPerPage, productContainerElement, + categoryMeta, onReloadProducts, goToPage, }; }, + head() { + return getMetaInfo(this.categoryMeta); + }, }); diff --git a/modules/catalog/pages/product.vue b/modules/catalog/pages/product.vue index 733ef89..0013c32 100644 --- a/modules/catalog/pages/product.vue +++ b/modules/catalog/pages/product.vue @@ -43,9 +43,11 @@ import { useCache, CacheTagPrefix } from '@vue-storefront/cache'; import { SfBreadcrumbs, SfLoader } from '@storefront-ui/vue'; import { getBreadcrumbs } from '~/modules/catalog/product/getters/productGetters'; import { useProduct } from '~/modules/catalog/product/composables/useProduct'; +import { getMetaInfo } from '~/helpers/getMetaInfo'; +import { usePageStore } from '~/stores/page'; import { ProductTypeEnum } from '~/modules/catalog/product/enums/ProductTypeEnum'; +import { useWishlist, useApi } from '~/composables'; import LoadWhenVisible from '~/components/utils/LoadWhenVisible.vue'; -import { useApi } from '~/composables'; import type { Product } from '~/modules/catalog/product/types'; import type { ProductDetailsQuery } from '~/modules/GraphQL/types'; import ProductSkeleton from '~/modules/catalog/product/components/ProductSkeleton.vue'; @@ -69,6 +71,7 @@ export default defineComponent({ }, transition: 'fade', setup() { + const { routeData } = usePageStore(); const { query } = useApi(); const product = ref(null); const { addTags } = useCache(); @@ -76,8 +79,7 @@ export default defineComponent({ const route = useRoute(); const { getProductDetails, loading } = useProduct(); const { error: nuxtError } = useContext(); - const { params: { id } } = route.value; - + const { load: loadWishlist } = useWishlist(); const breadcrumbs = computed(() => { const productCategories = product.value?.categories ?? []; return getBreadcrumbs( @@ -89,7 +91,7 @@ export default defineComponent({ const getBaseSearchQuery = () => ({ filter: { sku: { - eq: id, + eq: routeData.sku, }, }, configurations: Object.entries(route.value.query) @@ -124,7 +126,7 @@ export default defineComponent({ const tags = [ { prefix: CacheTagPrefix.View, - value: `product-${id}`, + value: `product-${routeData.sku}`, }, ]; @@ -137,7 +139,9 @@ export default defineComponent({ addTags([...tags, ...productTags]); }); - onMounted(async () => fetchProductExtendedData()); + onMounted(async () => { + await Promise.all([fetchProductExtendedData(), loadWishlist()]); + }); return { renderer, @@ -147,6 +151,9 @@ export default defineComponent({ fetchProduct: fetchProductExtendedData, }; }, + head() { + return getMetaInfo(this.product); + }, }); diff --git a/modules/checkout/components/__tests__/CartSidebar.spec.ts b/modules/checkout/components/__tests__/CartSidebar.spec.ts new file mode 100644 index 0000000..ee6cf28 --- /dev/null +++ b/modules/checkout/components/__tests__/CartSidebar.spec.ts @@ -0,0 +1,234 @@ +import { render, waitFor, within } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; + +import { createLocalVue } from '@vue/test-utils'; +import { PiniaVuePlugin } from 'pinia'; +import { createTestingPinia } from '@pinia/testing'; +import { useUser } from '~/modules/customer/composables/useUser'; +import { useCart, useUiState, useCartView } from '~/composables'; + +import { + useCartMock, useUserMock, useUiStateMock, useEmptyCartMock, useCartViewMock, +} from '~/tests/unit/test-utils'; +import CartSidebar from '~/modules/checkout/components/CartSidebar.vue'; + +jest.mock('~/composables', () => ({ + ...(jest.requireActual('~/composables') as object), + useExternalCheckout: jest.fn(() => ({ initializeCheckout: jest.fn(() => '/checkout-url') })), + useUiState: jest.fn(), +})); + +jest.mock('~/modules/customer/composables/useUser'); +jest.mock('~/modules/checkout/composables/useCart'); +jest.mock('~/modules/checkout/composables/useCartView'); + +(useUser as jest.Mock).mockReturnValue(useUserMock()); +(useCart as jest.Mock).mockReturnValue(useCartMock()); +(useCartView as jest.Mock).mockReturnValue(useCartViewMock()); + +const localVue = createLocalVue(); +localVue.use(PiniaVuePlugin); + +describe('CartSidebar', () => { + it('should be not visible by default', () => { + (useUiState as jest.Mock).mockReturnValue(useUiStateMock()); + const { queryByText } = render(CartSidebar, { localVue, pinia: createTestingPinia() }); + expect(queryByText('My Cart')).toBeNull(); + }); + + describe('If the cart is empty', () => { + beforeAll(() => { + (useCart as jest.Mock).mockReturnValue(useEmptyCartMock()); + }); + describe('And the cart sidebar is open', () => { + it('renders empty state', () => { + (useUiState as jest.Mock).mockReturnValue(useUiStateMock({ isCartSidebarOpen: true })); + (useCartView as jest.Mock).mockReturnValue(useCartViewMock({ totalItems: 0 })); + const { queryByText } = render(CartSidebar, { localVue, pinia: createTestingPinia() }); + expect(queryByText('Your cart is empty')).toBeTruthy(); + }); + + it('clicking go back button closes the sidebar', async () => { + const uiStateMock = useUiStateMock({ isCartSidebarOpen: true }); + (useUiState as jest.Mock).mockReturnValue(uiStateMock); + const { queryByTestId } = render(CartSidebar, { localVue, pinia: createTestingPinia() }); + const closeSidebarBtn = queryByTestId('cart-sidebar-back'); + userEvent.click(closeSidebarBtn); + + await waitFor(() => { + expect(uiStateMock.toggleCartSidebar).toHaveBeenCalledTimes(1); + }); + }); + }); + }); + + describe('If the cart has three products', () => { + beforeAll(() => { + (useCart as jest.Mock).mockReturnValue(useCartMock()); + (useCartView as jest.Mock).mockReturnValue(useCartViewMock()); + (useUiState as jest.Mock).mockReturnValue(useUiStateMock({ isCartSidebarOpen: true })); + }); + + it('renders product cards', () => { + const { getAllByTestId } = render(CartSidebar, { localVue, pinia: createTestingPinia() }); + expect(getAllByTestId('cart-sidebar-collected-product')).toHaveLength(3); + }); + + it('displays proper item value', async () => { + const useCartMockInstance = useCartMock(); + const { getByTestId } = render(CartSidebar, { localVue, pinia: createTestingPinia() }); + const totalValue = getByTestId('cart-sidebar-total'); + const expectedTotal = String(useCartMockInstance.cart.value.prices.grand_total.value); + + await waitFor(() => { + expect(totalValue.textContent).toContain(expectedTotal); + }); + }); + + it('handles navigating to checkout', async () => { + const useCartViewMockInstance = useCartViewMock(); + (useCartView as jest.Mock).mockReturnValue(useCartViewMockInstance); + const mockedRouterPush = jest.fn(); + const { getByTestId } = render( + CartSidebar, + { + localVue, + pinia: createTestingPinia(), + mocks: { + $router: { + push: mockedRouterPush, + }, + }, + }, + ); + + const goToCheckoutButton = getByTestId('category-sidebar-go-to-checkout'); + userEvent.click(goToCheckoutButton); + + await waitFor(() => { + expect(useCartViewMockInstance.goToCheckout).toBeCalledTimes(1); + }); + }); + + it('handles navigating to cart', async () => { + const mockedRouterPush = jest.fn(); + const { getByTestId } = render( + CartSidebar, + { + localVue, + pinia: createTestingPinia(), + mocks: { + $router: { + push: mockedRouterPush, + }, + }, + }, + ); + + const goToCartButton = getByTestId('category-sidebar-go-to-cart'); + userEvent.click(goToCartButton); + + await waitFor(() => { + expect(mockedRouterPush).toHaveBeenCalledWith({ name: 'cart' }); + }); + }); + + it('shows configurable options', async () => { + const useCartViewMockInstance = useCartViewMock(); + (useCartView as jest.Mock).mockReturnValue(useCartViewMockInstance); + + const { getAllByTestId } = render( + CartSidebar, + { + localVue, + pinia: createTestingPinia(), + }, + ); + + const [{ configurable_options: attributes }] = useCartViewMockInstance.products; + + await waitFor(() => { + const attributeContainer = getAllByTestId('cart-sidebar-attribute-container')[0]; + attributes.forEach( + ({ option_label }) => expect(attributeContainer.textContent).toContain(option_label), + ); + }); + }); + + it('shows products from bundle', async () => { + const useCartViewMockInstance = useCartViewMock(); + (useCartView as jest.Mock).mockReturnValue(useCartViewMockInstance); + + const { getByTestId } = render( + CartSidebar, + { + localVue, + pinia: createTestingPinia(), + }, + ); + + const { bundle_options: bundleOptions } = useCartViewMockInstance.products[2]; + + await waitFor(() => { + const bundleContainer = getByTestId('cart-sidebar-bundle-container'); + bundleOptions.forEach( + ({ label }) => expect(bundleContainer.textContent).toContain(label), + ); + }); + }); + + it('increases product quantity', async () => { + const useCartViewMockInstance = useCartViewMock(); + (useCartView as jest.Mock).mockReturnValue(useCartViewMockInstance); + + const { getAllByTestId } = render( + CartSidebar, + { + localVue, + pinia: createTestingPinia(), + }, + ); + + const secondProductElement = getAllByTestId('cart-sidebar-collected-product')[1]; + const increaseQuantityButton = within(secondProductElement).getByTestId('increase'); + userEvent.click(increaseQuantityButton); + + await waitFor(() => { + const { uid: uidOfSecondProduct } = useCartViewMockInstance.products[1]; + expect(useCartViewMockInstance.delayedUpdateItemQty).toHaveBeenCalledWith({ + product: expect.objectContaining({ uid: uidOfSecondProduct }), + quantity: 2, + }); + }, { timeout: 4500 }); + }); + + it('removes products from cart', async () => { + const useCartViewMockInstance = useCartViewMock(); + (useCartView as jest.Mock).mockReturnValue(useCartViewMockInstance); + const { getByTestId, getAllByTestId } = render( + CartSidebar, + { + localVue, + pinia: createTestingPinia(), + }, + ); + userEvent.click(getAllByTestId('collected-product-desktop-remove')[0]); + userEvent.click(getByTestId('cart-sidebar-remove-item-yes')); + await waitFor(() => { + expect(useCartViewMockInstance.removeItemAndSendNotification).toHaveBeenCalledWith(useCartViewMockInstance.itemToRemove); + }); + }); + + it('renders promo code input', () => { + const { getByTestId } = render(CartSidebar, { localVue, pinia: createTestingPinia() }); + getByTestId('promo-code'); + }); + + describe('And exactly one product is out of stock', () => { + it('should display exactly one out of stock badge', () => { + const { getAllByText } = render(CartSidebar, { localVue, pinia: createTestingPinia() }); + expect(getAllByText('Out of stock')).toHaveLength(1); + }); + }); + }); +}); diff --git a/modules/checkout/composables/useBilling/commands/saveBillingAddressCommand.ts b/modules/checkout/composables/useBilling/commands/saveBillingAddressCommand.ts index f536fc1..0583234 100644 --- a/modules/checkout/composables/useBilling/commands/saveBillingAddressCommand.ts +++ b/modules/checkout/composables/useBilling/commands/saveBillingAddressCommand.ts @@ -2,7 +2,7 @@ import { Logger } from '~/helpers/logger'; import { BillingCartAddress, Maybe, SetBillingAddressOnCartInput } from '~/modules/GraphQL/types'; export const saveBillingAddressCommand = { - execute: async (context, cartId, billingDetails, customQuery): Promise> => { + execute: async (context, cartId, billingDetails, customQuery, customHeaders): Promise> => { const { apartment, neighborhood, @@ -30,6 +30,7 @@ export const saveBillingAddressCommand = { const { data } = await context.$vsf.$magento.api.setBillingAddressOnCart( setBillingAddressOnCartInput, customQuery, + customHeaders, ); Logger.debug('[Result]:', { data }); diff --git a/modules/checkout/composables/useBilling/index.ts b/modules/checkout/composables/useBilling/index.ts index a1d8a0e..c702b32 100644 --- a/modules/checkout/composables/useBilling/index.ts +++ b/modules/checkout/composables/useBilling/index.ts @@ -28,14 +28,14 @@ export function useBilling(): UseBillingInterface { save: null, }); - const load = async ({ customQuery = null }: UseBillingLoadParams = {}): Promise> => { + const load = async ({ customQuery = null, customHeaders = {} }: UseBillingLoadParams = {}): Promise> => { Logger.debug('useBilling.load'); let billingInfo = null; try { loading.value = true; if (!cart?.value?.billing_address) { - await loadCart({ customQuery }); + await loadCart({ customQuery, customHeaders }); } billingInfo = cart?.value?.billing_address ?? null; @@ -50,13 +50,13 @@ export function useBilling(): UseBillingInterface { return billingInfo; }; - const save = async ({ billingDetails, customQuery = null }: UseBillingSaveParams): Promise> => { + const save = async ({ billingDetails, customQuery = null, customHeaders = {} }: UseBillingSaveParams): Promise> => { Logger.debug('useBilling.save'); let billingInfo = null; try { loading.value = true; - billingInfo = await saveBillingAddressCommand.execute(context, cart.value.id, billingDetails, customQuery); + billingInfo = await saveBillingAddressCommand.execute(context, cart.value.id, billingDetails, customQuery, customHeaders); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument /** diff --git a/modules/checkout/composables/useCart/commands/addItemCommand.ts b/modules/checkout/composables/useCart/commands/addItemCommand.ts index 92e0c1d..4d4e608 100644 --- a/modules/checkout/composables/useCart/commands/addItemCommand.ts +++ b/modules/checkout/composables/useCart/commands/addItemCommand.ts @@ -4,7 +4,7 @@ import { Cart, CartItemInput, } from '~/modules/GraphQL/types'; -import { CustomQuery } from '~/types/core'; +import { CustomQuery, CustomHeaders } from '~/types/core'; /** Params object used to add items to a cart */ export declare type AddProductsToCartInput = { @@ -21,6 +21,7 @@ export const addItemCommand = { currentCart, productConfiguration, customQuery, + customHeaders, }, ) => { Logger.debug('[Magento]: Add item to cart', { @@ -48,7 +49,11 @@ export const addItemCommand = { ], }; - const simpleProduct = await context.$magento.api.addProductsToCart(simpleCartInput, customQuery as CustomQuery); + const simpleProduct = await context.$magento.api.addProductsToCart( + simpleCartInput, + customQuery as CustomQuery, + customHeaders as CustomHeaders, + ); Logger.debug('[Result]:', { data: simpleProduct.data }); @@ -75,7 +80,11 @@ export const addItemCommand = { ], }; - const configurableProduct = await context.$magento.api.addProductsToCart(configurableCartInput, customQuery as CustomQuery); + const configurableProduct = await context.$magento.api.addProductsToCart( + configurableCartInput, + customQuery as CustomQuery, + customHeaders as CustomHeaders, + ); Logger.debug('[Result]:', { data: configurableProduct.data }); if (configurableProduct.data.addProductsToCart.user_errors.length > 0) { @@ -105,7 +114,11 @@ export const addItemCommand = { ], }; - const bundleProduct = await context.$magento.api.addProductsToCart(bundleCartInput, customQuery as CustomQuery); + const bundleProduct = await context.$magento.api.addProductsToCart( + bundleCartInput, + customQuery as CustomQuery, + customHeaders as CustomHeaders, + ); Logger.debug('[Result]:', { data: bundleProduct }); @@ -129,7 +142,11 @@ export const addItemCommand = { ], }; - const downloadableProduct = await context.$magento.api.addProductsToCart(downloadableCartInput, customQuery as CustomQuery); + const downloadableProduct = await context.$magento.api.addProductsToCart( + downloadableCartInput, + customQuery as CustomQuery, + customHeaders as CustomHeaders, + ); Logger.debug('[Result DownloadableProduct]:', { data: downloadableProduct }); @@ -152,12 +169,16 @@ export const addItemCommand = { }, ], }; - const virtualProduct = await context.$magento.api.addProductsToCart(virtualCartInput, customQuery as CustomQuery); + const virtualProduct = await context.$magento.api.addProductsToCart( + virtualCartInput, + customQuery as CustomQuery, + customHeaders as CustomHeaders, + ); Logger.debug('[Result VirtualProduct]:', { data: virtualProduct }); - if (downloadableProduct.data.addProductsToCart.user_errors.length > 0) { - throw new Error(String(downloadableProduct.data.addProductsToCart.user_errors[0].message)); + if (virtualProduct.data.addProductsToCart.user_errors.length > 0) { + throw new Error(String(virtualProduct.data.addProductsToCart.user_errors[0].message)); } // eslint-disable-next-line consistent-return @@ -165,6 +186,28 @@ export const addItemCommand = { .data .addProductsToCart .cart as unknown as Cart; + case 'GroupedProduct': + const groupedCartInput: AddProductsToCartInput = { + cartId, + cartItems: product.items.map((item) => ({ + quantity, + sku: item.product.sku, + })), + }; + + const groupedProduct = await context.$magento.api.addProductsToCart(groupedCartInput, customQuery as CustomQuery); + + Logger.debug('[Result GroupedProduct]:', { data: groupedProduct }); + + if (groupedProduct.data.addProductsToCart.user_errors.length > 0) { + throw new Error(String(groupedProduct.data.addProductsToCart.user_errors[0].message)); + } + + // eslint-disable-next-line consistent-return + return groupedProduct + .data + .addProductsToCart + .cart as unknown as Cart; default: // eslint-disable-next-line no-underscore-dangle throw new Error(`Product Type ${product.__typename} not supported in add to cart yet`); diff --git a/modules/checkout/composables/useCart/commands/applyCouponCommand.ts b/modules/checkout/composables/useCart/commands/applyCouponCommand.ts index fd43b10..4054205 100644 --- a/modules/checkout/composables/useCart/commands/applyCouponCommand.ts +++ b/modules/checkout/composables/useCart/commands/applyCouponCommand.ts @@ -7,6 +7,7 @@ export const applyCouponCommand = { currentCart, couponCode, customQuery = { applyCouponToCart: 'applyCouponToCart' }, + customHeaders = {}, }) => { Logger.debug('[Magento]: Apply coupon on cart', { couponCode, @@ -16,7 +17,7 @@ export const applyCouponCommand = { const { data, errors } = await context.$magento.api.applyCouponToCart({ cart_id: currentCart.id, coupon_code: couponCode, - }, customQuery); + }, customQuery, customHeaders); Logger.debug('[Result]:', { data, errors }); diff --git a/modules/checkout/composables/useCart/commands/loadTotalQtyCommand.ts b/modules/checkout/composables/useCart/commands/loadTotalQtyCommand.ts index 2a02c90..5718f08 100644 --- a/modules/checkout/composables/useCart/commands/loadTotalQtyCommand.ts +++ b/modules/checkout/composables/useCart/commands/loadTotalQtyCommand.ts @@ -8,7 +8,11 @@ export const loadTotalQtyCommand = { const apiState = context.$magento.config.state; if (apiState.getCartId()) { - const { data } : any = await context.$magento.api.cartTotalQty(apiState.getCartId(), params?.customQuery ?? null); + const { data } : any = await context.$magento.api.cartTotalQty( + apiState.getCartId(), + params?.customQuery ?? null, + params?.customHeaders ?? null, + ); return data?.cart?.total_quantity ?? 0; } diff --git a/modules/checkout/composables/useCart/commands/removeCouponCommand.ts b/modules/checkout/composables/useCart/commands/removeCouponCommand.ts index 0b6d2fd..15da7f0 100644 --- a/modules/checkout/composables/useCart/commands/removeCouponCommand.ts +++ b/modules/checkout/composables/useCart/commands/removeCouponCommand.ts @@ -3,12 +3,12 @@ import { Cart } from '~/modules/GraphQL/types'; import { VsfContext } from '~/composables/context'; export const removeCouponCommand = { - execute: async (context: VsfContext, { currentCart, customQuery = { removeCouponFromCart: 'removeCouponFromCart' } }) => { + execute: async (context: VsfContext, { currentCart, customQuery = { removeCouponFromCart: 'removeCouponFromCart' }, customHeaders = {} }) => { Logger.debug('[Magento]: Remove coupon from cart', { currentCart }); const { data, errors } = await context.$magento.api.removeCouponFromCart({ cart_id: currentCart.id, - }, customQuery); + }, customQuery, customHeaders); Logger.debug('[Result]:', { data }); diff --git a/modules/checkout/composables/useCart/commands/removeItemCommand.ts b/modules/checkout/composables/useCart/commands/removeItemCommand.ts index 5f56694..0412532 100644 --- a/modules/checkout/composables/useCart/commands/removeItemCommand.ts +++ b/modules/checkout/composables/useCart/commands/removeItemCommand.ts @@ -1,12 +1,17 @@ import { Logger } from '~/helpers/logger'; import { Cart, RemoveItemFromCartInput } from '~/modules/GraphQL/types'; import { VsfContext } from '~/composables/context'; -import { CustomQuery } from '~/types/core'; +import { CustomQuery, CustomHeaders } from '~/types/core'; export const removeItemCommand = { execute: async ( context: VsfContext, - { currentCart, product, customQuery }, + { + currentCart, + product, + customQuery, + customHeaders, + }, ) => { Logger.debug('[Magento]: Remove item from cart', { product, @@ -24,7 +29,11 @@ export const removeItemCommand = { cart_item_uid: item.uid, }; - const { data } = await context.$magento.api.removeItemFromCart(removeItemParams, customQuery as CustomQuery); + const { data } = await context.$magento.api.removeItemFromCart( + removeItemParams, + customQuery as CustomQuery, + customHeaders as CustomHeaders, + ); Logger.debug('[Result]:', { data }); diff --git a/modules/checkout/composables/useCart/commands/updateItemQtyCommand.ts b/modules/checkout/composables/useCart/commands/updateItemQtyCommand.ts index 8c0a6cd..5a05641 100644 --- a/modules/checkout/composables/useCart/commands/updateItemQtyCommand.ts +++ b/modules/checkout/composables/useCart/commands/updateItemQtyCommand.ts @@ -8,6 +8,7 @@ export const updateItemQtyCommand = { product, quantity, customQuery = { updateCartItems: 'updateCartItems' }, + customHeaders = {}, }) => { Logger.debug('[Magento]: Update product quantity on cart', { product, @@ -25,7 +26,7 @@ export const updateItemQtyCommand = { ], }; - const { data } = await context.$magento.api.updateCartItems(updateCartParams, customQuery); + const { data } = await context.$magento.api.updateCartItems(updateCartParams, customQuery, customHeaders); Logger.debug('[Result]:', { data }); diff --git a/modules/checkout/composables/useCart/index.ts b/modules/checkout/composables/useCart/index.ts index c573572..624baa8 100644 --- a/modules/checkout/composables/useCart/index.ts +++ b/modules/checkout/composables/useCart/index.ts @@ -67,12 +67,12 @@ PRODUCT */ const isInCart = (product: PRODUCT): boolean => !!cart.value?.items?.find((cartItem) => cartItem?.product?.uid === product.uid); - const load = async ({ customQuery = {}, realCart = false } = { customQuery: { cart: 'cart' } }): Promise => { + const load = async ({ customQuery = {}, customHeaders = {}, realCart = false } = { customQuery: { cart: 'cart' }, customHeaders: {} }): Promise => { Logger.debug('useCart.load'); try { loading.value = true; - const loadedCart = await loadCartCommand.execute(context, { customQuery, realCart }); + const loadedCart = await loadCartCommand.execute(context, { customQuery, customHeaders, realCart }); cartStore.$patch((state) => { state.cart = loadedCart; }); @@ -85,13 +85,13 @@ PRODUCT } }; - const clear = async ({ customQuery } = { customQuery: { cart: 'cart' } }): Promise => { + const clear = async ({ customQuery, customHeaders } = { customQuery: { cart: 'cart' }, customHeaders: {} }): Promise => { Logger.debug('useCart.clear'); try { loading.value = true; context.$magento.config.state.removeCartId(); - const loadedCart = await loadCartCommand.execute(context, { customQuery }); + const loadedCart = await loadCartCommand.execute(context, { customQuery, customHeaders }); cartStore.$patch((state) => { state.cart = loadedCart; @@ -123,7 +123,7 @@ PRODUCT }; const addItem = async ({ - product, quantity, productConfiguration, customQuery, + product, quantity, productConfiguration, customQuery, customHeaders, }): Promise => { Logger.debug('useCart.addItem', { product, quantity }); @@ -140,6 +140,7 @@ PRODUCT quantity, productConfiguration, customQuery, + customHeaders, }); error.value.addItem = null; @@ -160,7 +161,7 @@ PRODUCT } }; - const removeItem = async ({ product, customQuery }) => { + const removeItem = async ({ product, customQuery, customHeaders }) => { Logger.debug('useCart.removeItem', { product }); try { @@ -169,6 +170,7 @@ PRODUCT currentCart: cart.value, product, customQuery, + customHeaders, }); error.value.removeItem = null; diff --git a/modules/checkout/composables/useCartView/index.ts b/modules/checkout/composables/useCartView/index.ts new file mode 100644 index 0000000..cecea69 --- /dev/null +++ b/modules/checkout/composables/useCartView/index.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { + computed, + ref, + useRouter, + useContext, + onMounted, +} from '@nuxtjs/composition-api'; +import { debounce } from 'lodash-es'; +import cartGetters from '~/modules/checkout/getters/cartGetters'; +import { + useUiNotification, + useExternalCheckout, + useImage, + useProduct, +} from '~/composables'; +import { useCart } from '~/modules/checkout/composables/useCart'; +import { useUser } from '~/modules/customer/composables/useUser'; +import type { UseCartViewInterface } from '~/modules/checkout/composables/useCartView/useCartView'; +import type { ConfigurableCartItem, BundleCartItem, CartItemInterface } from '~/modules/GraphQL/types'; +import { ProductStockStatus } from '~/modules/GraphQL/types'; + +/** + * Allows loading and manipulating cart view. + * + * See the {@link UseCartViewInterface} for a list of methods and values available in this composable. + */ +export function useCartView(): UseCartViewInterface { + const { localePath, app: { i18n } } = useContext(); + const { initializeCheckout } = useExternalCheckout(); + const { getMagentoImage, imageSizes } = useImage(); + const router = useRouter(); + const { getProductPath } = useProduct(); + const { + cart, + removeItem, + updateItemQty, + load: loadCart, + loading, + } = useCart(); + const { isAuthenticated } = useUser(); + const { send: sendNotification, notifications } = useUiNotification(); + + const products = computed(() => cartGetters + .getItems(cart.value) + .filter(Boolean) + .map((item) => ({ + ...item, + product: { + ...item.product, + ...[(item as ConfigurableCartItem).configured_variant ?? {}], + original_sku: item.product.sku, + }, + }))); + const totals = computed(() => cartGetters.getTotals(cart.value)); + const discount = computed(() => -cartGetters.getDiscountAmount(cart.value)); + const totalItems = computed(() => cartGetters.getTotalItems(cart.value)); + const getAttributes = (product: ConfigurableCartItem) => product.configurable_options || []; + const getBundles = (product: BundleCartItem) => product.bundle_options?.map((b) => b.values).flat() || []; + const isRemoveModalVisible = ref(false); + const itemToRemove = ref(); + + onMounted(() => { + if (!cart.value.id) { + loadCart(); + } + }); + + const goToCheckout = async () => { + const redirectUrl = initializeCheckout({ baseUrl: '/checkout/user-account' }); + await router.push(localePath(redirectUrl)); + }; + + const showRemoveItemModal = ({ product }: { product: CartItemInterface }) => { + if (notifications.value.length > 0) { + notifications.value[0].dismiss(); + } + + isRemoveModalVisible.value = true; + itemToRemove.value = product; + }; + + const removeItemAndSendNotification = async (product: CartItemInterface) => { + await removeItem({ product }); + isRemoveModalVisible.value = false; + + sendNotification({ + id: Symbol('product_removed'), + message: i18n.t('{0} has been successfully removed from your cart', { + 0: cartGetters.getItemName( + product, + ), + }) as string, + type: 'success', + icon: 'check', + persist: false, + title: 'Product removed', + }); + }; + + const delayedUpdateItemQty = debounce( + (params) => updateItemQty(params), + 1000, + ); + + const isInStock = (product: CartItemInterface) => cartGetters.getStockStatus(product) === ProductStockStatus.InStock; + + return { + showRemoveItemModal, + removeItemAndSendNotification, + delayedUpdateItemQty, + goToCheckout, + getAttributes, + getBundles, + isInStock, + getMagentoImage, + getProductPath, + loading, + isAuthenticated, + products, + isRemoveModalVisible, + itemToRemove, + totals, + totalItems, + imageSizes, + discount, + cartGetters, + }; +} + +export default useCartView; +export * from './useCartView'; diff --git a/modules/checkout/composables/useCartView/useCartView.ts b/modules/checkout/composables/useCartView/useCartView.ts new file mode 100644 index 0000000..be01295 --- /dev/null +++ b/modules/checkout/composables/useCartView/useCartView.ts @@ -0,0 +1,63 @@ +import { ComputedRef, Ref } from '@nuxtjs/composition-api'; +import { + BundleCartItem, + CartItemInterface, + ConfigurableCartItem, ProductInterface, + SelectedBundleOption, + SelectedConfigurableOption, +} from '~/modules/GraphQL/types'; +import { + ComposableFunctionArgs, + ImageSizes, + Totals, +} from '~/composables'; +import { CartGetters } from '~/modules/checkout/getters/cartGetters'; + +type UseCartViewDelayedUpdateItemQtyParams = ComposableFunctionArgs<{ + product: CartItemInterface; + quantity: number; +}>; + +/** + * Data and methods returned from the {@link useCartView} composable + */ +export interface UseCartViewInterface { + /** Shows a remove item modal and sets the `itemToRemove` */ + showRemoveItemModal(params: { product: CartItemInterface }): void, + /** Removes an item from the Cart and shows a notification */ + removeItemAndSendNotification(product: CartItemInterface): Promise, + /** Updates the quantity of an item in the Cart with a delay */ + delayedUpdateItemQty(params: UseCartViewDelayedUpdateItemQtyParams): Promise, + /** Redirects to the checkout page */ + goToCheckout(): Promise, + /** Gets configurable options from the item */ + getAttributes(product: ConfigurableCartItem): SelectedConfigurableOption[], + /** Gets a list of products that represent the values of the parent option */ + getBundles(product: BundleCartItem): SelectedBundleOption['values'], + /** Checks if an item is in stock or not */ + isInStock(product: CartItemInterface): boolean, + /** Extracts an image path from Magento URL. */ + getMagentoImage(fullImageUrl: string): string, + /** Get a product path from url_rewrites or url_key */ + getProductPath(product: ProductInterface): string, + /** Indicates whether any of the {@link useCart} composable methods is in progress. */ + loading: Readonly>; + /** Indicates whether the customer is authenticated or not */ + isAuthenticated: ComputedRef, + /** Returns the Items in the Cart */ + products: ComputedRef, + /** Indicates whether a remove item modal is visible or not */ + isRemoveModalVisible: Ref, + /** Returns a removable item */ + itemToRemove: Ref, + /** Returns total prices in the Cart */ + totals: ComputedRef, + /** Returns a total items count in the Cart */ + totalItems: ComputedRef, + /** Available image sizes */ + imageSizes: ImageSizes, + /** Returns a cart discount amount */ + discount: ComputedRef, + /** Methods returned from the {@link CartGetters} cartGetters */ + cartGetters: CartGetters, +} diff --git a/modules/checkout/composables/useGetShippingMethods/commands/getCustomerShippingMethodsCommand.ts b/modules/checkout/composables/useGetShippingMethods/commands/getCustomerShippingMethodsCommand.ts index a2ffa58..128067a 100644 --- a/modules/checkout/composables/useGetShippingMethods/commands/getCustomerShippingMethodsCommand.ts +++ b/modules/checkout/composables/useGetShippingMethods/commands/getCustomerShippingMethodsCommand.ts @@ -6,7 +6,7 @@ export const getCustomerShippingMethodsCommand = { execute: async (context: VsfContext, params: ComposableFunctionArgs<{}>): Promise => { const { data: { customerCart: { shipping_addresses: shippingAddresses } }, - } = await context.$magento.api.getAvailableCustomerShippingMethods(params?.customQuery ?? null); + } = await context.$magento.api.getAvailableCustomerShippingMethods(params?.customQuery ?? null, params?.customHeaders); return shippingAddresses[0]?.available_shipping_methods ?? []; }, }; diff --git a/modules/checkout/composables/useGetShippingMethods/commands/getGuestShippingMethodsCommand.ts b/modules/checkout/composables/useGetShippingMethods/commands/getGuestShippingMethodsCommand.ts index 2212e2b..c203f51 100644 --- a/modules/checkout/composables/useGetShippingMethods/commands/getGuestShippingMethodsCommand.ts +++ b/modules/checkout/composables/useGetShippingMethods/commands/getGuestShippingMethodsCommand.ts @@ -4,7 +4,11 @@ import { AvailableShippingMethod } from '~/modules/GraphQL/types'; export const getGuestShippingMethodsCommand = { execute: async (context: Context['app'], params: ComposableFunctionArgs<{ cartId: string }>): Promise => { - const { data } = await context.$vsf.$magento.api.getAvailableShippingMethods({ cartId: params.cartId }, params?.customQuery ?? null); + const { data } = await context.$vsf.$magento.api.getAvailableShippingMethods( + { cartId: params.cartId }, + params?.customQuery ?? null, + params?.customHeaders, + ); const hasAddresses = data.cart.shipping_addresses.length > 0; return hasAddresses ? data?.cart?.shipping_addresses[0]?.available_shipping_methods : []; diff --git a/modules/checkout/composables/useMakeOrder/commands/placeOrderCommand.ts b/modules/checkout/composables/useMakeOrder/commands/placeOrderCommand.ts index 63a505a..656c50f 100644 --- a/modules/checkout/composables/useMakeOrder/commands/placeOrderCommand.ts +++ b/modules/checkout/composables/useMakeOrder/commands/placeOrderCommand.ts @@ -4,7 +4,11 @@ import { ComposableFunctionArgs } from '~/composables'; export const placeOrderCommand = { execute: async (context: UseContextReturn, cartId: string, params?: ComposableFunctionArgs<{}>): Promise => { - const { data } = await context.app.$vsf.$magento.api.placeOrder({ cart_id: cartId }, params?.customQuery ?? null); + const { data } = await context.app.$vsf.$magento.api.placeOrder( + { cart_id: cartId }, + params?.customQuery ?? null, + params?.customHeaders, + ); return data?.placeOrder ?? null; }, diff --git a/modules/checkout/composables/useMakeOrder/index.ts b/modules/checkout/composables/useMakeOrder/index.ts index 3ec8b7a..f44cb38 100644 --- a/modules/checkout/composables/useMakeOrder/index.ts +++ b/modules/checkout/composables/useMakeOrder/index.ts @@ -22,7 +22,11 @@ export function useMakeOrder(): UseMakeOrderInterface { let placedOrder = null; try { loading.value = true; - placedOrder = await placeOrderCommand.execute(context, cart.value.id, params?.customQuery ?? null); + placedOrder = await placeOrderCommand.execute( + context, + cart.value.id, + params, + ); error.value.make = null; } catch (err) { error.value.make = err; diff --git a/modules/checkout/composables/usePaymentProvider/commands/getAvailablePaymentMethodsCommand.ts b/modules/checkout/composables/usePaymentProvider/commands/getAvailablePaymentMethodsCommand.ts index fb46c47..ffd6a7e 100644 --- a/modules/checkout/composables/usePaymentProvider/commands/getAvailablePaymentMethodsCommand.ts +++ b/modules/checkout/composables/usePaymentProvider/commands/getAvailablePaymentMethodsCommand.ts @@ -1,9 +1,14 @@ -import { CustomQuery, UseContextReturn } from '~/types/core'; +import { CustomQuery, UseContextReturn, CustomHeaders } from '~/types/core'; import type { AvailablePaymentMethod } from '~/modules/GraphQL/types'; export const getAvailablePaymentMethodsCommand = { - execute: async (context: UseContextReturn, cartId: string, customQuery?: CustomQuery): Promise => { - const { data } = await context.app.$vsf.$magento.api.getAvailablePaymentMethods({ cartId }, customQuery); + execute: async ( + context: UseContextReturn, + cartId: string, + customQuery?: CustomQuery, + customHeaders?: CustomHeaders, + ): Promise => { + const { data } = await context.app.$vsf.$magento.api.getAvailablePaymentMethods({ cartId }, customQuery, customHeaders); return data?.cart?.available_payment_methods ?? []; }, diff --git a/modules/checkout/composables/usePaymentProvider/commands/setPaymentMethodOnCartCommand.ts b/modules/checkout/composables/usePaymentProvider/commands/setPaymentMethodOnCartCommand.ts index b3c0e45..2c8eeb3 100644 --- a/modules/checkout/composables/usePaymentProvider/commands/setPaymentMethodOnCartCommand.ts +++ b/modules/checkout/composables/usePaymentProvider/commands/setPaymentMethodOnCartCommand.ts @@ -4,7 +4,7 @@ import type { PaymentMethodParams } from '../usePaymentProvider'; export const setPaymentMethodOnCartCommand = { execute: async (context: UseContextReturn, params: PaymentMethodParams): Promise => { - const { data } = await context.app.$vsf.$magento.api.setPaymentMethodOnCart(params, params?.customQuery ?? null); + const { data } = await context.app.$vsf.$magento.api.setPaymentMethodOnCart(params, params?.customQuery ?? null, params?.customHeaders); return data?.setPaymentMethodOnCart?.cart.available_payment_methods ?? []; }, diff --git a/modules/checkout/composables/usePaymentProvider/index.ts b/modules/checkout/composables/usePaymentProvider/index.ts index 1e5a184..9514f09 100644 --- a/modules/checkout/composables/usePaymentProvider/index.ts +++ b/modules/checkout/composables/usePaymentProvider/index.ts @@ -10,7 +10,7 @@ import type { UsePaymentProviderSaveParams, PaymentMethodParams, } from './usePaymentProvider'; -import { CustomQuery } from '~/types/core'; +import { CustomQuery, CustomHeaders } from '~/types/core'; /** * Allows loading the available payment @@ -39,6 +39,7 @@ export function usePaymentProvider(): UsePaymentProviderInterface { ...params.paymentMethod, }, customQuery: params.customQuery, + customHeaders: params?.customHeaders, }; result = await setPaymentMethodOnCartCommand.execute(context, paymentMethodParams); @@ -54,13 +55,13 @@ export function usePaymentProvider(): UsePaymentProviderInterface { return result; }; - const load = async (customQuery?: CustomQuery) => { + const load = async (customQuery?: CustomQuery, customHeaders?: CustomHeaders) => { Logger.debug('usePaymentProvider.load'); let result = null; try { loading.value = true; - result = await getAvailablePaymentMethodsCommand.execute(context, cart.value.id, customQuery); + result = await getAvailablePaymentMethodsCommand.execute(context, cart.value.id, customQuery, customHeaders); error.value.load = null; } catch (err) { error.value.load = err; diff --git a/modules/checkout/composables/usePaymentProvider/usePaymentProvider.ts b/modules/checkout/composables/usePaymentProvider/usePaymentProvider.ts index 1987a8f..f0199bf 100644 --- a/modules/checkout/composables/usePaymentProvider/usePaymentProvider.ts +++ b/modules/checkout/composables/usePaymentProvider/usePaymentProvider.ts @@ -1,7 +1,7 @@ import type { Ref } from '@nuxtjs/composition-api'; import type { ComposableFunctionArgs } from '~/composables/types'; import type { AvailablePaymentMethod, PaymentMethodInput } from '~/modules/GraphQL/types'; -import { CustomQuery } from '~/types/core'; +import { CustomQuery, CustomHeaders } from '~/types/core'; export type PaymentMethodParams = ComposableFunctionArgs<{ cart_id: string; @@ -40,7 +40,7 @@ export interface UsePaymentProviderInterface { error: Readonly>; /** Loads the available payment methods for current cart. */ - load(customQuery?: CustomQuery): Promise; + load(customQuery?: CustomQuery, customHeaders?: CustomHeaders): Promise; /** * Saves the payment method for current cart. It returns the updated available diff --git a/modules/checkout/composables/useShippingProvider/commands/setShippingMethodsOnCartCommand.ts b/modules/checkout/composables/useShippingProvider/commands/setShippingMethodsOnCartCommand.ts index 471b539..024ddbb 100644 --- a/modules/checkout/composables/useShippingProvider/commands/setShippingMethodsOnCartCommand.ts +++ b/modules/checkout/composables/useShippingProvider/commands/setShippingMethodsOnCartCommand.ts @@ -1,9 +1,14 @@ -import { CustomQuery, UseContextReturn } from '~/types/core'; +import { CustomQuery, CustomHeaders, UseContextReturn } from '~/types/core'; import type { SetShippingMethodsOnCartInput, Cart } from '~/modules/GraphQL/types'; export const setShippingMethodsOnCartCommand = { - execute: async (context: UseContextReturn, shippingMethodParams: SetShippingMethodsOnCartInput, customQuery: CustomQuery): Promise => { - const { data } = await context.app.$vsf.$magento.api.setShippingMethodsOnCart(shippingMethodParams, customQuery); + execute: async ( + context: UseContextReturn, + shippingMethodParams: SetShippingMethodsOnCartInput, + customQuery: CustomQuery, + customHeaders: CustomHeaders, + ): Promise => { + const { data } = await context.app.$vsf.$magento.api.setShippingMethodsOnCart(shippingMethodParams, customQuery, customHeaders); // TODO: Find out why 'Cart' doesn't match the type of the response data. return (data?.setShippingMethodsOnCart?.cart as unknown as Cart) ?? null; diff --git a/modules/checkout/composables/useShippingProvider/index.ts b/modules/checkout/composables/useShippingProvider/index.ts index 80e7932..404345f 100644 --- a/modules/checkout/composables/useShippingProvider/index.ts +++ b/modules/checkout/composables/useShippingProvider/index.ts @@ -26,7 +26,7 @@ export function useShippingProvider(): UseShippingProviderInterface { const { cart, setCart, load: loadCart } = useCart(); const context = useContext(); - const save = async ({ shippingMethod, customQuery = null }: UseShippingProviderSaveParams) => { + const save = async ({ shippingMethod, customQuery = null, customHeaders = {} }: UseShippingProviderSaveParams) => { Logger.debug('useShippingProvider.save'); let result = null; try { @@ -37,7 +37,7 @@ export function useShippingProvider(): UseShippingProviderInterface { shipping_methods: [shippingMethod], }; - const cartData = await setShippingMethodsOnCartCommand.execute(context, shippingMethodParams, customQuery); + const cartData = await setShippingMethodsOnCartCommand.execute(context, shippingMethodParams, customQuery, customHeaders); Logger.debug('[Result]:', { cartData }); setCart(cartData); @@ -55,13 +55,13 @@ export function useShippingProvider(): UseShippingProviderInterface { return result; }; - const load = async ({ customQuery = null }: UseShippingProviderLoadParams = {}) => { + const load = async ({ customQuery = null, customHeaders = {} }: UseShippingProviderLoadParams = {}) => { Logger.debug('useShippingProvider.load'); let result = null; try { loading.value = true; if (!cart?.value?.shipping_addresses[0]?.selected_shipping_method) { - await loadCart({ customQuery }); + await loadCart({ customQuery, customHeaders }); } result = cart.value?.shipping_addresses[0]?.selected_shipping_method; diff --git a/modules/checkout/getters/orderGetters.ts b/modules/checkout/getters/orderGetters.ts index 45b5bf3..fb38b5e 100644 --- a/modules/checkout/getters/orderGetters.ts +++ b/modules/checkout/getters/orderGetters.ts @@ -2,9 +2,9 @@ import type { Pagination } from '~/composables/types'; import type { CustomerOrders, CustomerOrder, OrderItemInterface } from '~/modules/GraphQL/types'; export const getDate = (order: CustomerOrder): string => new Date(order?.order_date?.replace(/ /g, 'T')).toLocaleDateString(); - -export const getPrice = (order: CustomerOrder): number => order?.total?.base_grand_total?.value ?? 0; - +export const getBaseGrandTotal = (order: CustomerOrder): number => order?.total?.base_grand_total?.value ?? 0; +export const getGrandTotal = (order: CustomerOrder): number => order?.total?.grand_total.value ?? 0; +export const getOrderCurrency = (order: CustomerOrder): string => order?.total?.subtotal.currency ?? 'USD'; export const getItemPrice = (item: OrderItemInterface): number => item?.product_sale_price?.value ?? 0; const getPagination = (orders: CustomerOrders): Pagination => ({ @@ -17,7 +17,9 @@ const getPagination = (orders: CustomerOrders): Pagination => ({ const orderGetters = { getDate, - getPrice, + getBaseGrandTotal, + getGrandTotal, + getOrderCurrency, getItemPrice, getPagination, }; diff --git a/modules/checkout/index.ts b/modules/checkout/index.ts index 89220fa..a3283ba 100644 --- a/modules/checkout/index.ts +++ b/modules/checkout/index.ts @@ -40,6 +40,11 @@ const nuxtModule : Module = function checkoutModule() { }, ], }, + { + name: 'cart', + path: '/cart', + component: path.resolve(moduleDir, 'pages/Cart.vue'), + }, ); }); }; diff --git a/modules/checkout/pages/Cart.vue b/modules/checkout/pages/Cart.vue new file mode 100644 index 0000000..3ff37c7 --- /dev/null +++ b/modules/checkout/pages/Cart.vue @@ -0,0 +1,473 @@ + + + + + diff --git a/modules/core/GoogleFontsAPI/probeGoogleFontsApi.ts b/modules/core/GoogleFontsAPI/probeGoogleFontsApi.ts new file mode 100644 index 0000000..9432c34 --- /dev/null +++ b/modules/core/GoogleFontsAPI/probeGoogleFontsApi.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; + +export const GOOGLE_FONT_API_URL = 'https://google-webfonts-helper.herokuapp.com/api/fonts'; + +/** + * Check + */ +export const probeGoogleFontsApi = async () => { + try { + await axios.get(GOOGLE_FONT_API_URL); + return true; + } catch (err) { + console.warn('GoogleFontsAPI is unavailable:', err); + } + + return false; +}; + +export default probeGoogleFontsApi; diff --git a/modules/core/helpers/getLocaleSettings.ts b/modules/core/helpers/getLocaleSettings.ts new file mode 100644 index 0000000..5e85087 --- /dev/null +++ b/modules/core/helpers/getLocaleSettings.ts @@ -0,0 +1,18 @@ +import { NuxtAppOptions } from '@nuxt/types'; +import { defaultConfig } from '~/modules/core/defaultConfig'; + +export const getLocaleSettings = (app: NuxtAppOptions, moduleOptions: Record, additionalProperties: any) => { + const localeSettings = moduleOptions.cookies + ? { + currency: additionalProperties.state.getCurrency(), + locale: additionalProperties.state.getLocale(), + country: additionalProperties.state.getCountry(), + } + : {}; + + return { + currency: localeSettings.currency || moduleOptions.currency || defaultConfig.currency || undefined, + locale: app.i18n.locale || localeSettings.locale || moduleOptions.locale || defaultConfig.locale || undefined, + country: localeSettings.country || moduleOptions.country || defaultConfig.country || undefined, + }; +}; diff --git a/modules/core/helpers/mapConfigToSetupObject.ts b/modules/core/helpers/mapConfigToSetupObject.ts new file mode 100644 index 0000000..868c3c8 --- /dev/null +++ b/modules/core/helpers/mapConfigToSetupObject.ts @@ -0,0 +1,11 @@ +import { NuxtAppOptions } from '@nuxt/types'; +import { defaultConfig } from '~/modules/core/defaultConfig'; +import { getLocaleSettings } from '~/modules/core/helpers/getLocaleSettings'; + +export const mapConfigToSetupObject = ({ app, moduleOptions, additionalProperties = {} } : +{ app: NuxtAppOptions, moduleOptions: Record, additionalProperties: Record }) => ({ + ...defaultConfig, + ...moduleOptions, + ...additionalProperties, + ...getLocaleSettings(app, moduleOptions, additionalProperties), +}); diff --git a/modules/core/integrationPlugin/_proxyUtils.ts b/modules/core/integrationPlugin/_proxyUtils.ts new file mode 100644 index 0000000..658f121 --- /dev/null +++ b/modules/core/integrationPlugin/_proxyUtils.ts @@ -0,0 +1,49 @@ +import { Context as NuxtContext } from '@nuxt/types'; +import { merge } from 'lodash-es'; +import { Logger } from '~/helpers/logger'; + +export type ApiClientMethod = (...args: any) => Promise; + +interface CreateProxiedApiParams { + nuxtCtx: NuxtContext; + givenApi: Record; + client: any; + tag: string; +} + +export const createProxiedApi = ({ + givenApi, client, tag, nuxtCtx, +}: CreateProxiedApiParams) => new Proxy(givenApi, { + get: (target, prop, receiver) => { + const functionName = String(prop); + if (Reflect.has(target, functionName)) { + return Reflect.get(target, prop, receiver); + } + + // eslint-disable-next-line @typescript-eslint/require-await + return async (...args) => client + .post(`/${tag}/${functionName}`, args) + .then((r) => r.data) + .catch((err) => { + Logger.debug(err); + nuxtCtx.error({ statusCode: err.statusCode, message: err }); + + return {}; + }); + }, +}); + +export const getCookies = (context: NuxtContext) => context?.req?.headers?.cookie ?? ''; + +export const getIntegrationConfig = (context: NuxtContext, configuration: any) => { + const cookie = getCookies(context); + const initialConfig = merge({ + axios: { + headers: { + ...(cookie ? { cookie } : {}), + }, + }, + }, configuration); + + return initialConfig; +}; diff --git a/modules/core/integrationPlugin/context.ts b/modules/core/integrationPlugin/context.ts new file mode 100644 index 0000000..7ac6bbe --- /dev/null +++ b/modules/core/integrationPlugin/context.ts @@ -0,0 +1,37 @@ +/* eslint-disable no-param-reassign */ + +import { Context as NuxtContext } from '@nuxt/types'; +import { Inject } from '@nuxt/types/app'; + +type Argument = { tag: string, nuxtCtx: NuxtContext, inject: Inject }; + +/** + * It extends given integartion, defined by `tag` in the context. + */ +export const createExtendIntegrationInCtx = ({ tag, nuxtCtx, inject } : Argument) => (integrationProperties: Record) => { + const integrationKey = `$${tag}`; + + if (!nuxtCtx.$vsf || !(nuxtCtx.$vsf as Record)[integrationKey]) { + inject('vsf', { [integrationKey]: {} }); + } + + Object.keys(integrationProperties) + .filter((k) => !['api', 'client', 'config'].includes(k)) + .forEach((key) => { + (nuxtCtx.$vsf as Record)[integrationKey][key] = integrationProperties[key]; + }); +}; + +/** + * It creates a function that adds an integration to the context under the given name, defined by `tag`. + */ +export const createAddIntegrationToCtx = ({ tag, nuxtCtx, inject } : Argument) => (integrationProperties: Record) => { + const integrationKey = `$${tag}`; + + if (nuxtCtx.$vsf && !(nuxtCtx.$vsf as Record)[integrationKey]) { + (nuxtCtx.$vsf as Record)[integrationKey] = integrationProperties; + return; + } + + inject('vsf', { [integrationKey]: integrationProperties }); +}; diff --git a/modules/core/integrationPlugin/index.ts b/modules/core/integrationPlugin/index.ts new file mode 100644 index 0000000..6d481ec --- /dev/null +++ b/modules/core/integrationPlugin/index.ts @@ -0,0 +1,63 @@ +import { Context as NuxtContext } from '@nuxt/types'; +import { Inject } from '@nuxt/types/app'; +import axios from 'axios'; +import { createExtendIntegrationInCtx, createAddIntegrationToCtx } from './context'; +import { getIntegrationConfig, createProxiedApi, ApiClientMethod } from './_proxyUtils'; + +interface IntegrationContext { + integration: { + configure: (tag: string, configuration: any) => void; + extend: (tag: string, integrationProperties: any) => void; + } +} + +type NuxtPluginWithIntegration = (ctx: NuxtContext & IntegrationContext, inject: Inject) => void | Promise; + +const parseCookies = (cookieString: string): Record => Object.fromEntries(cookieString + .split(';') + // eslint-disable-next-line unicorn/no-array-callback-reference + .filter(String) + .map((item) => item.split('=').map((part) => part.trim())) + .map(([name, value]) => [name, value])); + +const setCookieValues = (cookieValues: Record, cookieString = '') => { + const parsed = parseCookies(cookieString); + + // eslint-disable-next-line no-return-assign + Object.entries(cookieValues).forEach(([name, value]) => parsed[name] = value); + + return Object.entries(parsed).map(([name, value]) => `${name}=${value}`).join('; '); +}; + +export const integrationPlugin = (pluginFn: NuxtPluginWithIntegration) => (nuxtCtx: NuxtContext, inject: Inject) => { + const configure = (tag: string, configuration: { api: Record }) => { + const injectInContext = createAddIntegrationToCtx({ tag, nuxtCtx, inject }); + const config = getIntegrationConfig(nuxtCtx, configuration); + const { middlewareUrl, ssrMiddlewareUrl } = (nuxtCtx as any).$config; + + if (middlewareUrl || ssrMiddlewareUrl) { + config.axios.baseURL = process.server ? ssrMiddlewareUrl || middlewareUrl : middlewareUrl; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const client = axios.create(config.axios); + const api = createProxiedApi({ + givenApi: configuration.api || {}, client, tag, nuxtCtx, + }); + + if ((nuxtCtx.app.i18n as any).cookieValues) { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + client.defaults.headers.cookie = setCookieValues((nuxtCtx.app.i18n as any).cookieValues, (client.defaults.headers as any).cookie); + } + injectInContext({ api, client, config }); + }; + + const extend = (tag: string, integrationProperties: Record) => { + createExtendIntegrationInCtx({ tag, nuxtCtx, inject })(integrationProperties); + }; + + const integration = { configure, extend }; + + pluginFn({ ...nuxtCtx, integration }, inject); +}; diff --git a/modules/core/plugin.ts b/modules/core/plugin.ts index de8b865..5782f3d 100644 --- a/modules/core/plugin.ts +++ b/modules/core/plugin.ts @@ -1,7 +1,7 @@ import type { NuxtCookies, GetOptions } from 'cookie-universal-nuxt'; import type { CookieSerializeOptions } from 'cookie'; -import { integrationPlugin } from '~/helpers/integrationPlugin'; -import { mapConfigToSetupObject } from '~/modules/core/helpers'; +import { integrationPlugin } from '~/modules/core/integrationPlugin'; +import { mapConfigToSetupObject } from '~/modules/core/helpers/mapConfigToSetupObject'; import { defaultConfig } from '~/modules/core/defaultConfig'; const moduleOptions = JSON.parse('<%= JSON.stringify(options) %>'); diff --git a/modules/customer/composables/useAddresses/index.ts b/modules/customer/composables/useAddresses/index.ts index 5f8047b..fd5b46d 100644 --- a/modules/customer/composables/useAddresses/index.ts +++ b/modules/customer/composables/useAddresses/index.ts @@ -4,7 +4,7 @@ import { Logger } from '~/helpers/logger'; import { transformUserCreateAddressInput, transformUserUpdateAddressInput } from '~/modules/customer/helpers/userAddressManipulator'; import type { ComposableFunctionArgs } from '~/composables/types'; import type { UseAddressesInterface, UseAddressesParamsInput, UseAddressesErrors } from './useAddresses'; -import { CustomQuery } from '~/types/core'; +import { CustomQuery, CustomHeaders } from '~/types/core'; /** * @public @@ -25,13 +25,13 @@ export function useAddresses(): UseAddressesInterface { const { app } = useContext(); const context = app.$vsf; - const load = async (customQuery?: CustomQuery) => { + const load = async (customQuery?: CustomQuery, customHeaders?: CustomHeaders) => { Logger.debug('[Magento] load user addresses'); let results = null; try { loading.value = true; - const { data } = await context.$magento.api.getCustomerAddresses(customQuery); + const { data } = await context.$magento.api.getCustomerAddresses(customQuery, customHeaders); results = data?.customer?.addresses ?? []; Logger.debug('[Magento] load user addresses results:', results); error.value.load = null; @@ -51,7 +51,11 @@ export function useAddresses(): UseAddressesInterface { try { loading.value = true; - const { data } = await context.$magento.api.createCustomerAddress(transformUserCreateAddressInput(params), params?.customQuery ?? null); + const { data } = await context.$magento.api.createCustomerAddress( + transformUserCreateAddressInput(params), + params?.customQuery ?? null, + params?.customHeaders, + ); results = data?.createCustomerAddress ?? {}; Logger.debug('[Magento] save user address results:', params.address); error.value.save = null; @@ -71,7 +75,11 @@ export function useAddresses(): UseAddressesInterface { try { loading.value = true; - const { data } = await context.$magento.api.updateCustomerAddress(transformUserUpdateAddressInput(params), params?.customQuery ?? null); + const { data } = await context.$magento.api.updateCustomerAddress( + transformUserUpdateAddressInput(params), + params?.customQuery ?? null, + params?.customHeaders, + ); results = data?.updateCustomerAddress ?? {}; Logger.debug('[Magento] update user address results:', results); error.value.update = null; @@ -91,7 +99,11 @@ export function useAddresses(): UseAddressesInterface { try { loading.value = true; - const { data } = await context.$magento.api.deleteCustomerAddress(params.address.id, params?.customQuery ?? null); + const { data } = await context.$magento.api.deleteCustomerAddress( + params.address.id, + params?.customQuery ?? null, + params?.customHeaders, + ); results = !!data.deleteCustomerAddress; Logger.debug('[Magento] remove user address results:', results); error.value.remove = null; diff --git a/modules/customer/composables/useAddresses/useAddresses.ts b/modules/customer/composables/useAddresses/useAddresses.ts index ffd01f3..6ff6302 100644 --- a/modules/customer/composables/useAddresses/useAddresses.ts +++ b/modules/customer/composables/useAddresses/useAddresses.ts @@ -1,7 +1,7 @@ import type { Ref } from '@nuxtjs/composition-api'; import type { ComposableFunctionArgs } from '~/composables/types'; import type { CustomerAddress } from '~/modules/GraphQL/types'; -import { CustomQuery } from '~/types/core'; +import { CustomQuery, CustomHeaders } from '~/types/core'; /** * Errors that occured in the {@link useAddresses|useAddresses()} composable @@ -77,7 +77,7 @@ export interface UseAddressesInterface { * } * ``` */ - load(customQuery?: CustomQuery): Promise; + load(customQuery?: CustomQuery, customHeaders?: CustomHeaders): Promise; /** * Saves a new address in the profile of the current customer diff --git a/modules/customer/composables/useForgotPassword/index.ts b/modules/customer/composables/useForgotPassword/index.ts index 9cfbb73..36cac7f 100644 --- a/modules/customer/composables/useForgotPassword/index.ts +++ b/modules/customer/composables/useForgotPassword/index.ts @@ -39,7 +39,7 @@ export function useForgotPassword(): UseForgotPasswordInterface { loading.value = true; Logger.debug('[Magento]: Reset user password', resetPasswordParams); // eslint-disable-next-line max-len - const { data } = await app.context.$vsf.$magento.api.requestPasswordResetEmail({ email: resetPasswordParams.email, recaptchaToken: resetPasswordParams.recaptchaToken }, resetPasswordParams?.customQuery ?? null); + const { data } = await app.context.$vsf.$magento.api.requestPasswordResetEmail({ email: resetPasswordParams.email, recaptchaToken: resetPasswordParams.recaptchaToken }, resetPasswordParams?.customQuery ?? null, resetPasswordParams?.customHeaders); Logger.debug('[Result]:', { data }); error.value.request = data; result.value.resetPasswordResult = data?.requestPasswordResetEmail ?? false; @@ -61,7 +61,7 @@ export function useForgotPassword(): UseForgotPasswordInterface { newPassword: setNewPasswordParams.newPassword, resetPasswordToken: setNewPasswordParams.tokenValue, recaptchaToken: setNewPasswordParams.recaptchaToken, - }, setNewPasswordParams?.customQuery ?? null); + }, setNewPasswordParams?.customQuery ?? null, setNewPasswordParams?.customHeaders); Logger.debug('[Result]:', { data }); result.value.setNewPasswordResult = data?.resetPassword ?? false; diff --git a/modules/customer/composables/useGuestUser/commands/attachToCartCommand.ts b/modules/customer/composables/useGuestUser/commands/attachToCartCommand.ts index 9d423b9..e705b3f 100644 --- a/modules/customer/composables/useGuestUser/commands/attachToCartCommand.ts +++ b/modules/customer/composables/useGuestUser/commands/attachToCartCommand.ts @@ -15,6 +15,6 @@ export const attachToCartCommand = { await context.app.$vsf.$magento.api.setGuestEmailOnCart({ ...emailOnCartInput, - }, params?.customQuery ?? null); + }, params?.customQuery ?? null, params?.customHeaders); }, }; diff --git a/modules/customer/composables/useUser/index.ts b/modules/customer/composables/useUser/index.ts index a6e76e1..44a8da2 100644 --- a/modules/customer/composables/useUser/index.ts +++ b/modules/customer/composables/useUser/index.ts @@ -63,8 +63,8 @@ export function useUser(): UseUserInterface { }; // eslint-disable-next-line consistent-return - const updateUser = async ({ user: providedUser, customQuery }: UseUserUpdateUserParams) => { - Logger.debug('[Magento] Update user information', { providedUser, customQuery }); + const updateUser = async ({ user: providedUser, customQuery, customHeaders }: UseUserUpdateUserParams) => { + Logger.debug('[Magento] Update user information', { providedUser, customQuery, customHeaders }); resetErrorValue(); try { @@ -81,7 +81,7 @@ export function useUser(): UseUserInterface { }); } - const { data, errors } = await app.context.$vsf.$magento.api.updateCustomer(userData, customQuery); + const { data, errors } = await app.context.$vsf.$magento.api.updateCustomer(userData, customQuery, customHeaders); Logger.debug('[Result]:', { data }); if (errors) { @@ -100,14 +100,14 @@ export function useUser(): UseUserInterface { } }; - const logout = async ({ customQuery = {} }: UseUserLogoutParams = {}) => { + const logout = async ({ customQuery = {}, customHeaders = {} }: UseUserLogoutParams = {}) => { Logger.debug('[Magento] useUserFactory.logout'); resetErrorValue(); try { const apiState = app.context.$vsf.$magento.config.state; - await app.context.$vsf.$magento.api.revokeCustomerToken(customQuery); + await app.context.$vsf.$magento.api.revokeCustomerToken(customQuery, customHeaders); apiState.removeCustomerToken(); apiState.removeCartId(); @@ -121,7 +121,7 @@ export function useUser(): UseUserInterface { } }; - const load = async ({ customQuery = {} }: UseUserLoadParams = {}) => { + const load = async ({ customQuery = {}, customHeaders = {} }: UseUserLoadParams = {}) => { Logger.debug('[Magento] useUser.load'); resetErrorValue(); @@ -133,7 +133,7 @@ export function useUser(): UseUserInterface { return null; } try { - const { data } = await app.context.$vsf.$magento.api.customer(customQuery); + const { data } = await app.context.$vsf.$magento.api.customer(customQuery, customHeaders); Logger.debug('[Result]:', { data }); @@ -155,7 +155,7 @@ export function useUser(): UseUserInterface { }; // eslint-disable-next-line @typescript-eslint/require-await,no-empty-pattern - const login = async ({ user: providedUser, customQuery }: UseUserLoginParams) : Promise => { + const login = async ({ user: providedUser, customQuery, customHeaders }: UseUserLoginParams) : Promise => { Logger.debug('[Magento] useUser.login', providedUser); resetErrorValue(); @@ -170,6 +170,7 @@ export function useUser(): UseUserInterface { recaptchaToken: providedUser.recaptchaToken, }, customQuery || {}, + customHeaders || {}, ); Logger.debug('[Result]:', { data }); @@ -230,7 +231,7 @@ export function useUser(): UseUserInterface { }; // eslint-disable-next-line consistent-return - const register = async ({ user: providedUser, customQuery }: UseUserRegisterParams) : Promise => { + const register = async ({ user: providedUser, customQuery, customHeaders }: UseUserRegisterParams) : Promise => { Logger.debug('[Magento] useUser.register', providedUser); resetErrorValue(); @@ -252,6 +253,7 @@ export function useUser(): UseUserInterface { ...baseData, }, customQuery || {}, + customHeaders || {}, ); Logger.debug('[Result]:', { data }); @@ -314,7 +316,7 @@ export function useUser(): UseUserInterface { currentUser: customerStore.user, currentPassword: params.current, newPassword: params.new, - }, params.customQuery); + }, params.customQuery, params?.customHeaders); let joinedErrors = null; diff --git a/modules/customer/composables/useUserAddress/commands/createCustomerAddressCommand.ts b/modules/customer/composables/useUserAddress/commands/createCustomerAddressCommand.ts index c85b23c..044b103 100644 --- a/modules/customer/composables/useUserAddress/commands/createCustomerAddressCommand.ts +++ b/modules/customer/composables/useUserAddress/commands/createCustomerAddressCommand.ts @@ -1,9 +1,9 @@ -import { CustomQuery, UseContextReturn } from '~/types/core'; +import { CustomQuery, CustomHeaders, UseContextReturn } from '~/types/core'; import { CustomerAddressInput } from '~/modules/GraphQL/types'; export const createCustomerAddressCommand = { - execute: async (context: UseContextReturn, params: CustomerAddressInput, customQuery: CustomQuery) => { - const { data } = await context.app.$vsf.$magento.api.createCustomerAddress(params, customQuery); + execute: async (context: UseContextReturn, params: CustomerAddressInput, customQuery: CustomQuery, customHeaders: CustomHeaders) => { + const { data } = await context.app.$vsf.$magento.api.createCustomerAddress(params, customQuery, customHeaders); return data?.createCustomerAddress ?? {}; }, diff --git a/modules/customer/composables/useUserAddress/commands/deleteCustomerAddressCommand.ts b/modules/customer/composables/useUserAddress/commands/deleteCustomerAddressCommand.ts index 7ecc304..fe429a4 100644 --- a/modules/customer/composables/useUserAddress/commands/deleteCustomerAddressCommand.ts +++ b/modules/customer/composables/useUserAddress/commands/deleteCustomerAddressCommand.ts @@ -1,9 +1,9 @@ -import { CustomQuery, UseContextReturn } from '~/types/core'; +import { CustomQuery, CustomHeaders, UseContextReturn } from '~/types/core'; import { CustomerAddress } from '~/modules/GraphQL/types'; export const deleteCustomerAddressCommand = { - execute: async (context: UseContextReturn, address: CustomerAddress, customQuery: CustomQuery) => { - const { data } = await context.app.$vsf.$magento.api.deleteCustomerAddress(address.id, customQuery); + execute: async (context: UseContextReturn, address: CustomerAddress, customQuery: CustomQuery, customHeaders: CustomHeaders) => { + const { data } = await context.app.$vsf.$magento.api.deleteCustomerAddress(address.id, customQuery, customHeaders); return data?.deleteCustomerAddress ?? {}; }, diff --git a/modules/customer/composables/useUserAddress/commands/updateCustomerAddressCommand.ts b/modules/customer/composables/useUserAddress/commands/updateCustomerAddressCommand.ts index be9dfa0..b98d364 100644 --- a/modules/customer/composables/useUserAddress/commands/updateCustomerAddressCommand.ts +++ b/modules/customer/composables/useUserAddress/commands/updateCustomerAddressCommand.ts @@ -1,12 +1,12 @@ -import { CustomQuery, UseContextReturn } from '~/types/core'; +import { CustomQuery, CustomHeaders, UseContextReturn } from '~/types/core'; import { CustomerAddressInput } from '~/modules/GraphQL/types'; export const updateCustomerAddressCommand = { execute: async (context: UseContextReturn, params: { addressId: number; input: CustomerAddressInput; - }, customQuery: CustomQuery) => { - const { data } = await context.app.$vsf.$magento.api.updateCustomerAddress(params, customQuery); + }, customQuery: CustomQuery, customHeaders: CustomHeaders) => { + const { data } = await context.app.$vsf.$magento.api.updateCustomerAddress(params, customQuery, customHeaders); return data?.updateCustomerAddress ?? {}; }, diff --git a/modules/customer/composables/useUserAddress/index.ts b/modules/customer/composables/useUserAddress/index.ts index a2a956b..4189518 100644 --- a/modules/customer/composables/useUserAddress/index.ts +++ b/modules/customer/composables/useUserAddress/index.ts @@ -15,7 +15,7 @@ import type { UseUserAddressUpdateAddressParams, UseUserAddressSetDefaultAddressParams, } from './useUserAddress'; -import { CustomQuery } from '~/types/core'; +import { CustomQuery, CustomHeaders } from '~/types/core'; /** * Allows loading and manipulating addresses of the current user. @@ -35,7 +35,7 @@ export function useUserAddress(): UseUserAddressInterface { const { user, load: loadUser } = useUser(); const context = useContext(); - const addAddress = async ({ address, customQuery }: UseUserAddressAddAddressParams) => { + const addAddress = async ({ address, customQuery, customHeaders }: UseUserAddressAddAddressParams) => { Logger.debug('useUserAddress.addAddress', mask(address)); let result = {}; try { @@ -44,7 +44,7 @@ export function useUserAddress(): UseUserAddressInterface { address, shipping: shipping.value, }); - result = await createCustomerAddressCommand.execute(context, customerAddressInput, customQuery); + result = await createCustomerAddressCommand.execute(context, customerAddressInput, customQuery, customHeaders); error.value.addAddress = null; } catch (err) { error.value.addAddress = err; @@ -58,13 +58,13 @@ export function useUserAddress(): UseUserAddressInterface { return result; }; - const deleteAddress = async (address: CustomerAddress, customQuery: CustomQuery) => { + const deleteAddress = async (address: CustomerAddress, customQuery: CustomQuery, customHeaders: CustomHeaders) => { Logger.debug('useUserAddress.deleteAddress', address); let result = {}; try { loading.value = true; - result = await deleteCustomerAddressCommand.execute(context, address, customQuery); + result = await deleteCustomerAddressCommand.execute(context, address, customQuery, customHeaders); error.value.deleteAddress = null; } catch (err) { error.value.deleteAddress = err; @@ -78,7 +78,7 @@ export function useUserAddress(): UseUserAddressInterface { return result; }; - const updateAddress = async ({ address, customQuery }: UseUserAddressUpdateAddressParams) => { + const updateAddress = async ({ address, customQuery, customHeaders }: UseUserAddressUpdateAddressParams) => { Logger.debug('useUserAddress.updateAddress', mask(address)); let result = {}; @@ -88,7 +88,7 @@ export function useUserAddress(): UseUserAddressInterface { address, shipping: shipping.value, }); - result = await updateCustomerAddressCommand.execute(context, customerAddressInput, customQuery); + result = await updateCustomerAddressCommand.execute(context, customerAddressInput, customQuery, customHeaders); error.value.updateAddress = null; } catch (err) { error.value.updateAddress = err; @@ -120,7 +120,7 @@ export function useUserAddress(): UseUserAddressInterface { return user?.value; }; - const setDefaultAddress = async ({ address, customQuery }: UseUserAddressSetDefaultAddressParams) => { + const setDefaultAddress = async ({ address, customQuery, customHeaders }: UseUserAddressSetDefaultAddressParams) => { Logger.debug('useUserAddress.setDefaultAddress', mask(address)); let result = {}; @@ -130,9 +130,10 @@ export function useUserAddress(): UseUserAddressInterface { address, shipping: shipping.value, customQuery, + customHeaders, }); - result = await updateCustomerAddressCommand.execute(context, updateAddressInput, customQuery); + result = await updateCustomerAddressCommand.execute(context, updateAddressInput, customQuery, customHeaders); error.value.setDefaultAddress = null; } catch (err) { error.value.setDefaultAddress = err; diff --git a/modules/customer/composables/useUserAddress/useUserAddress.ts b/modules/customer/composables/useUserAddress/useUserAddress.ts index a584b84..c27d29f 100644 --- a/modules/customer/composables/useUserAddress/useUserAddress.ts +++ b/modules/customer/composables/useUserAddress/useUserAddress.ts @@ -2,7 +2,7 @@ import type { Ref, DeepReadonly } from '@nuxtjs/composition-api'; import type { CustomerAddress } from '~/modules/GraphQL/types'; import type { ComposableFunctionArgs } from '~/composables/types'; import { Customer } from '~/modules/GraphQL/types'; -import { CustomQuery } from '~/types/core'; +import { CustomQuery, CustomHeaders } from '~/types/core'; /** * Errors that occured in the {@link UseUserAddressErrors|UseUserAddressErrors()} composable @@ -77,7 +77,7 @@ export interface UseUserAddressInterface { * Internally, it calls the {@link @vue-storefront/magento-api#deleteCustomerAddress} API endpoint * and accepts the custom queries named `deleteCustomerAddress`. */ - deleteAddress(address: CustomerAddress, customQuery: CustomQuery): Promise; + deleteAddress(address: CustomerAddress, customQuery: CustomQuery, customHeaders: CustomHeaders): Promise; /** * Updates an existing address from the profile of the current user diff --git a/modules/customer/composables/useUserOrder/index.ts b/modules/customer/composables/useUserOrder/index.ts index 923ab25..76473b0 100644 --- a/modules/customer/composables/useUserOrder/index.ts +++ b/modules/customer/composables/useUserOrder/index.ts @@ -26,7 +26,7 @@ export function useUserOrder(): UseUserOrderInterface { Logger.debug('[Magento] search user orders', { params }); - const { data } = await app.$vsf.$magento.api.customerOrders(params, params?.customQuery ?? null); + const { data } = await app.$vsf.$magento.api.customerOrders(params, params?.customQuery ?? null, params?.customHeaders); Logger.debug('[Result]:', { data }); diff --git a/modules/customer/pages/MyAccount/AddressesDetails/AddressForm.vue b/modules/customer/pages/MyAccount/AddressesDetails/AddressForm.vue index f050cd2..fcb5be2 100644 --- a/modules/customer/pages/MyAccount/AddressesDetails/AddressForm.vue +++ b/modules/customer/pages/MyAccount/AddressesDetails/AddressForm.vue @@ -290,10 +290,7 @@ export default defineComponent({ const updateCountry = async (params: UseCountrySearchParams) => { country.value = await searchCountry(params); - form.value.region = { - // let region SfSelect know it should display initial state - ...(regionInformation.value.length > 0 ? { region_id: null } : {}), - }; + form.value.region = { region_id: null, region_code: '' }; }; watch(() => props.address, () => { @@ -318,9 +315,13 @@ export default defineComponent({ const submitForm = () => { const regionId = regionInformation.value.find((r) => r.abbreviation === form.value.region.region_code)?.id; - if (regionId) { - form.value.region.region_id = regionId; - } + form.value.region = regionId + ? { region_id: regionId } + : { + ...form.value.region, + region_code: '', + region_id: null, + }; emit('submit', { form: omitDeep(form.value, ['__typename']), diff --git a/modules/customer/pages/MyAccount/MyAccount.vue b/modules/customer/pages/MyAccount/MyAccount.vue index f63ed99..fa29e16 100644 --- a/modules/customer/pages/MyAccount/MyAccount.vue +++ b/modules/customer/pages/MyAccount/MyAccount.vue @@ -34,7 +34,7 @@ :label="$t(item.label)" :link="localeRoute(item.link)" class="sf-content-pages__menu" - v-on="item.listeners" + v-on="{ click: getHandler(item.id) }" /> @@ -83,17 +83,28 @@ export default defineComponent({ } }); - const { sidebarLinkGroups } = useSidebarLinkGroups(); + const { sidebarLinkGroups, logoutUser } = useSidebarLinkGroups(); const isOnSubpage = computed(() => route.value.matched.length > 1); const goToTopLevelRoute = () => router.push(localeRoute({ name: 'customer' })); const title = computed(() => i18n.t(route.value.matched.at(-1)?.meta.titleLabel as string)); + /** + * #tab-id: handler-name + */ + const handlers = { + 'log-out': logoutUser, + }; + + const getHandler = (id: string) => handlers[id] ?? {}; + return { sidebarLinkGroups, title, isOnSubpage, goToTopLevelRoute, + logoutUser, + getHandler, }; }, }); diff --git a/modules/customer/pages/MyAccount/MyWishlist.vue b/modules/customer/pages/MyAccount/MyWishlist.vue index 6e88d2c..e365853 100644 --- a/modules/customer/pages/MyAccount/MyWishlist.vue +++ b/modules/customer/pages/MyAccount/MyWishlist.vue @@ -40,16 +40,7 @@ fit: 'cover', }" :is-added-to-cart="isInCart(product.product)" - :link=" - localePath( - `/p/${productGetters.getProductSku( - product.product - )}${productGetters.getSlug( - product.product, - product.product.categories[0] - )}` - ) - " + :link="localePath(getProductPath(product.product))" :regular-price=" $fc(productGetters.getPrice(product.product).regular) " @@ -145,7 +136,7 @@ import { useCart } from '~/modules/checkout/composables/useCart'; import { useWishlistStore } from '~/modules/wishlist/store/wishlistStore'; import EmptyWishlist from '~/modules/wishlist/components/EmptyWishlist.vue'; import { ProductTypeEnum } from '~/modules/catalog/product/enums/ProductTypeEnum'; -import { useUiHelpers, useImage } from '~/composables'; +import { useUiHelpers, useImage, useProduct } from '~/composables'; export default defineComponent({ name: 'MyWishlist', @@ -167,6 +158,7 @@ export default defineComponent({ } = useWishlist(); const route = useRoute(); const { localeRoute } = useContext(); + const { getProductPath } = useProduct(); const { query: { page, itemsPerPage }, } = route.value; @@ -201,15 +193,7 @@ export default defineComponent({ case ProductTypeEnum.CONFIGURABLE_PRODUCT: case ProductTypeEnum.BUNDLE_PRODUCT: case ProductTypeEnum.GROUPED_PRODUCT: - const path = `/p/${productGetters.getProductSku( - product, - )}${productGetters.getSlug(product, product.categories[0])}`; - await router.push(localeRoute({ - path, - query: { - wishlist: 'true', - }, - })); + await router.push(localeRoute(getProductPath(product))); break; default: throw new Error( @@ -245,6 +229,7 @@ export default defineComponent({ th, getMagentoImage, imageSizes, + getProductPath, }; }, }); diff --git a/modules/customer/pages/MyAccount/OrderHistory/OrderHistory.vue b/modules/customer/pages/MyAccount/OrderHistory/OrderHistory.vue index f8fc199..42cb82a 100644 --- a/modules/customer/pages/MyAccount/OrderHistory/OrderHistory.vue +++ b/modules/customer/pages/MyAccount/OrderHistory/OrderHistory.vue @@ -45,7 +45,7 @@ > {{ order.number }} {{ getDate(order) }} - {{ $fc(getPrice(order)) }} + {{ $fc(getGrandTotal(order), '', {currency: getOrderCurrency(order)}) }} {{ order.status }} @@ -173,7 +173,8 @@ export default defineComponent({ getStatusTextClass, orderGetters, getDate: orderGetters.getDate, - getPrice: orderGetters.getPrice, + getGrandTotal: orderGetters.getGrandTotal, + getOrderCurrency: orderGetters.getOrderCurrency, orders: computed(() => rawCustomerOrders.value?.items ?? []), rawCustomerOrders, pagination, diff --git a/modules/customer/pages/MyAccount/OrderHistory/SingleOrder/SingleOrder.vue b/modules/customer/pages/MyAccount/OrderHistory/SingleOrder/SingleOrder.vue index 5b9887d..960f340 100644 --- a/modules/customer/pages/MyAccount/OrderHistory/SingleOrder/SingleOrder.vue +++ b/modules/customer/pages/MyAccount/OrderHistory/SingleOrder/SingleOrder.vue @@ -41,7 +41,7 @@ {{ item.quantity_ordered }} - {{ $fc(item.product_sale_price.value) }} + {{ $fc(item.product_sale_price.value, '', { currency: item.product_sale_price.currency }) }} @@ -76,7 +76,7 @@ {{ $t('Price') }} @@ -192,6 +192,8 @@ export default defineComponent({ ordersRoute, asyncData, getDate: orderGetters.getDate, + getGrandTotal: orderGetters.getGrandTotal, + getOrderCurrency: orderGetters.getOrderCurrency, }; }, }); diff --git a/modules/customer/pages/MyAccount/useSidebarLinkGroups.ts b/modules/customer/pages/MyAccount/useSidebarLinkGroups.ts index d871130..04b71eb 100644 --- a/modules/customer/pages/MyAccount/useSidebarLinkGroups.ts +++ b/modules/customer/pages/MyAccount/useSidebarLinkGroups.ts @@ -1,34 +1,39 @@ import type { RawLocation } from 'vue-router'; + import { useRouter, useContext } from '@nuxtjs/composition-api'; import { useUser } from '~/modules/customer/composables/useUser'; import { useCart } from '~/modules/checkout/composables/useCart'; type LinkGroup = { title: string, items: LinkGroupItem[] }; -type LinkGroupItem = { label: string, link?: RawLocation, listeners?: Record (Promise | void)> }; +type LinkGroupItem = { id: string, label: string, link?: RawLocation }; export const useSidebarLinkGroups = () => { const { localeRoute } = useContext(); const { logout } = useUser(); const { clear } = useCart(); - const router = useRouter(); + const sidebarLinkGroups : LinkGroup[] = [ { title: 'Personal details', items: [ { + id: 'my-profile', label: 'My profile', link: { name: 'customer-my-profile' }, }, { + id: 'address-details', label: 'Addresses details', link: { name: 'customer-addresses-details' }, }, { + id: 'my-newsletter', label: 'My newsletter', link: { name: 'customer-my-newsletter' }, }, { + id: 'my-wishlist', label: 'My wishlist', link: { name: 'customer-my-wishlist' }, }, @@ -38,26 +43,28 @@ export const useSidebarLinkGroups = () => { title: 'Order details', items: [ { + id: 'order-history', label: 'Order history', link: { name: 'customer-order-history' }, }, { + id: 'my-reviews', label: 'My reviews', link: { name: 'customer-my-reviews' }, }, { + id: 'log-out', label: 'Log out', - listeners: { - click: async () => { - await logout({}); - await clear({}); - await router.push(localeRoute({ name: 'home' })); - }, - }, }, ], }, ]; - return { sidebarLinkGroups }; + const logoutUser = async () => { + await logout({}); + await clear({}); + await router.push(localeRoute({ name: 'home' })); + }; + + return { sidebarLinkGroups, logoutUser }; }; diff --git a/modules/review/composables/useReview/commands/addReviewCommand.ts b/modules/review/composables/useReview/commands/addReviewCommand.ts index f472f1f..595f483 100644 --- a/modules/review/composables/useReview/commands/addReviewCommand.ts +++ b/modules/review/composables/useReview/commands/addReviewCommand.ts @@ -12,7 +12,7 @@ export const addReviewCommand = { ...input } = params; - const { data } = await context.$magento.api.createProductReview(input, params?.customQuery ?? null); + const { data } = await context.$magento.api.createProductReview(input, params?.customQuery ?? null, params?.customHeaders); Logger.debug('[Result]:', { data }); diff --git a/modules/review/composables/useReview/commands/loadCustomerReviewsCommand.ts b/modules/review/composables/useReview/commands/loadCustomerReviewsCommand.ts index 143570b..d1057f9 100644 --- a/modules/review/composables/useReview/commands/loadCustomerReviewsCommand.ts +++ b/modules/review/composables/useReview/commands/loadCustomerReviewsCommand.ts @@ -11,7 +11,7 @@ export const loadCustomerReviewsCommand = { execute: async (context: VsfContext, params?: ComposableFunctionArgs) => { Logger.debug('[Magento] load customer review based on:', { params }); - const { data } = await context.$magento.api.customerProductReview(params, params?.customQuery ?? null); + const { data } = await context.$magento.api.customerProductReview(params, params?.customQuery ?? null, params?.customHeaders); Logger.debug('[Result]:', { data }); diff --git a/modules/review/composables/useReview/commands/loadReviewMetadataCommand.ts b/modules/review/composables/useReview/commands/loadReviewMetadataCommand.ts index 0ccb979..a9184fa 100644 --- a/modules/review/composables/useReview/commands/loadReviewMetadataCommand.ts +++ b/modules/review/composables/useReview/commands/loadReviewMetadataCommand.ts @@ -6,7 +6,7 @@ export const loadReviewMetadataCommand = { execute: async (context: VsfContext, params?: ComposableFunctionArgs<{}>) => { Logger.debug('[Magento] load review metadata'); - const { data } = await context.$magento.api.productReviewRatingsMetadata(params?.customQuery ?? null); + const { data } = await context.$magento.api.productReviewRatingsMetadata(params?.customQuery ?? null, params?.customHeaders); Logger.debug('[Result]:', { data }); diff --git a/modules/review/composables/useReview/commands/searchReviewsCommand.ts b/modules/review/composables/useReview/commands/searchReviewsCommand.ts index 27eb84c..ba739e5 100644 --- a/modules/review/composables/useReview/commands/searchReviewsCommand.ts +++ b/modules/review/composables/useReview/commands/searchReviewsCommand.ts @@ -12,7 +12,11 @@ export const searchReviewsCommand = { ...input } = params; - const { data } = await context.$magento.api.productReview(input as GetProductSearchParams, params?.customQuery ?? null); + const { data } = await context.$magento.api.productReview( + input as GetProductSearchParams, + params?.customQuery ?? null, + params?.customHeaders, + ); Logger.debug('[Result]:', { data }); diff --git a/modules/wishlist/components/WishlistSidebar.vue b/modules/wishlist/components/WishlistSidebar.vue index 8d3b107..b041c90 100644 --- a/modules/wishlist/components/WishlistSidebar.vue +++ b/modules/wishlist/components/WishlistSidebar.vue @@ -132,9 +132,10 @@ import { useContext, useRouter, } from '@nuxtjs/composition-api'; -import productGetters from '~/modules/catalog/product/getters/productGetters'; import { - useUiState, useImage, + useUiState, + useImage, + useProduct, } from '~/composables'; import type { Price } from '~/modules/catalog/types'; import { useWishlist } from '~/modules/wishlist/composables/useWishlist'; @@ -164,6 +165,7 @@ export default defineComponent({ const { localeRoute } = useContext(); const router = useRouter(); const { isWishlistSidebarOpen, toggleWishlistSidebar } = useUiState(); + const { getProductPath } = useProduct(); const { removeItem, load: loadWishlist, loading, } = useWishlist(); @@ -207,15 +209,8 @@ export default defineComponent({ const getAttributes = (product: WishlistItemInterface) => (product?.product as ConfigurableProduct)?.configurable_options || []; const getBundles = (product: WishlistItemInterface) => (product?.product as BundleProduct)?.items?.map((b) => b.title).flat() || []; - const getItemLink = (item: WishlistItemInterface) => localeRoute({ - path: `/p/${item.product.sku}${productGetters.getSlug( - item.product, - item.product.categories[0], - )}`, - query: { - wishlist: 'true', - }, - }); + // @ts-ignore + const getItemLink = (item: WishlistItemInterface) => localeRoute(getProductPath(item.product)); const { getMagentoImage, imageSizes } = useImage(); const isShowGoToWishlistButton = computed(() => wishlistStore.wishlist.items_count > wishlistStore.wishlist?.items_v2?.items.length); diff --git a/modules/wishlist/composables/useWishlist/index.ts b/modules/wishlist/composables/useWishlist/index.ts index dca78d1..76498ac 100644 --- a/modules/wishlist/composables/useWishlist/index.ts +++ b/modules/wishlist/composables/useWishlist/index.ts @@ -43,7 +43,7 @@ export function useWishlist(): UseWishlistInterface { const apiState = app.$vsf.$magento.config.state; if (apiState.getCustomerToken()) { - const { data } = await app.$vsf.$magento.api.wishlist(params?.searchParams, params?.customQuery); + const { data } = await app.$vsf.$magento.api.wishlist(params?.searchParams, params?.customQuery ?? null, params?.customHeaders); Logger.debug('[Result]:', { data }); const loadedWishlist = data?.customer?.wishlists ?? []; @@ -77,7 +77,7 @@ export function useWishlist(): UseWishlistInterface { Logger.debug('useWishlist/setWishlist', newWishlist); }; - const removeItem = async ({ product, customQuery }: UseWishlistRemoveItemParams) => { + const removeItem = async ({ product, customQuery, customHeaders }: UseWishlistRemoveItemParams) => { Logger.debug('useWishlist/removeItem', product); try { @@ -86,13 +86,14 @@ export function useWishlist(): UseWishlistInterface { currentWishlist: wishlistStore.wishlist, product, customQuery, + customHeaders, }); const itemOnWishlist = findItemOnWishlist(wishlistStore.wishlist, product); const { data } = await app.context.$vsf.$magento.api.removeProductsFromWishlist({ id: '0', items: [itemOnWishlist.id], - }, customQuery); + }, customQuery, customHeaders); Logger.debug('[Result]:', { data }); error.value.removeItem = null; @@ -136,7 +137,7 @@ export function useWishlist(): UseWishlistInterface { }; // eslint-disable-next-line consistent-return - const addItem = async ({ product, customQuery }: UseWishlistAddItemParams) => { + const addItem = async ({ product, customQuery, customHeaders }: UseWishlistAddItemParams) => { Logger.debug('useWishlist/addItem', product); try { @@ -145,6 +146,7 @@ export function useWishlist(): UseWishlistInterface { currentWishlist: wishlistStore.wishlist, product, customQuery, + customHeaders, }); if (!wishlistStore.wishlist) { @@ -175,7 +177,7 @@ export function useWishlist(): UseWishlistInterface { sku: product.sku, quantity: 1, }], - }, customQuery); + }, customQuery, customHeaders); Logger.debug('[Result]:', { data }); @@ -193,7 +195,7 @@ export function useWishlist(): UseWishlistInterface { quantity: 1, parent_sku: product.sku, }], - }, customQuery); + }, customQuery, customHeaders); Logger.debug('[Result]:', { data: configurableProductData }); @@ -211,7 +213,7 @@ export function useWishlist(): UseWishlistInterface { quantity: 1, entered_options: [], }], - }, customQuery); + }, customQuery, customHeaders); Logger.debug('[Result]:', { data: bundleProductData }); @@ -293,8 +295,8 @@ export function useWishlist(): UseWishlistInterface { } }; - const addOrRemoveItem = async ({ product, customQuery }: UseWishlistAddItemParams) => { - await (isInWishlist({ product }) ? removeItem({ product, customQuery }) : addItem({ product, customQuery })); + const addOrRemoveItem = async ({ product, customQuery, customHeaders }: UseWishlistAddItemParams) => { + await (isInWishlist({ product }) ? removeItem({ product, customQuery, customHeaders }) : addItem({ product, customQuery, customHeaders })); }; return { diff --git a/nuxt.config.js b/nuxt.config.js index 9eb8a6f..78e12d7 100755 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -2,10 +2,9 @@ /* eslint-disable unicorn/prefer-module */ // @core-development-only-end import webpack from 'webpack'; -import fs from 'fs'; -import path from 'path'; import middleware from './middleware.config'; import { getRoutes } from './routes'; +import { probeGoogleFontsApi, GOOGLE_FONT_API_URL } from './modules/core/GoogleFontsAPI/probeGoogleFontsApi.ts'; const GoogleFontsPlugin = require('@beyonk/google-fonts-webpack-plugin'); @@ -27,13 +26,13 @@ const { }, } = middleware; -export default () => { +export default async () => { const baseConfig = { ssr: true, dev: process.env.VSF_NUXT_APP_ENV !== 'production', server: { port: process.env.VSF_NUXT_APP_PORT, - host: '0.0.0.0', + host: process.env.VSF_NUXT_APP_HOST || '0.0.0.0', }, head: { title: process.env.npm_package_name || '', @@ -132,6 +131,14 @@ export default () => { }, ], }], + ['@vue-storefront/http-cache/nuxt', { + default: 'max-age=300, s-maxage=3600, stale-while-revalidate=86400', + matchRoute: { + '/': 'max-age=1800, s-maxage=86400, stale-while-revalidate=86400', + '*/customer*': 'none', + '*/checkout*': 'none', + }, + }], ], i18n: { country: 'US', @@ -200,18 +207,6 @@ export default () => { lastCommit: process.env.LAST_COMMIT || '', }), }), - new GoogleFontsPlugin({ - fonts: [ - { family: 'Raleway', variants: ['300', '400', '500', '600', '700', '400italic'], display: 'swap' }, - { family: 'Roboto', variants: ['300', '400', '500', '700', '300italic', '400italic'], display: 'swap' }, - ], - name: 'fonts', - filename: 'fonts.css', - path: 'assets/fonts/', - local: true, - formats: ['eot', 'woff', 'woff2', 'ttf', 'svg'], - apiUrl: 'https://google-webfonts-helper.herokuapp.com/api/fonts', - }), ], transpile: [ 'vee-validate', @@ -242,9 +237,11 @@ export default () => { env: { VSF_MAGENTO_GRAPHQL_URL: process.env.VSF_MAGENTO_GRAPHQL_URL, }, - publicRuntimeConfig: { middlewareUrl: process.env.VSF_MIDDLEWARE_URL || 'http://localhost:3000/api/', + ssrMiddlewareUrl: process.env.VSF_SSR_MIDDLEWARE_URL + || process.env.VSF_MIDDLEWARE_URL + || 'http://localhost:3000/api/', }, }; @@ -286,13 +283,19 @@ export default () => { }; } - if (process.env.NODE_ENV === 'development' || process.env.VSF_NUXT_APP_ENV === 'development') { - baseConfig.server = { - https: { - key: fs.readFileSync(path.resolve(__dirname, 'localhost-key.pem')), - cert: fs.readFileSync(path.resolve(__dirname, 'localhost.pem')), - }, - }; + if (await probeGoogleFontsApi()) { + baseConfig.build.plugins.push(new GoogleFontsPlugin({ + fonts: [ + { family: 'Raleway', variants: ['300', '400', '500', '600', '700', '400italic'], display: 'swap' }, + { family: 'Roboto', variants: ['300', '400', '500', '700', '300italic', '400italic'], display: 'swap' }, + ], + name: 'fonts', + filename: 'fonts.css', + path: 'assets/fonts/', + local: true, + formats: ['eot', 'woff', 'woff2', 'ttf', 'svg'], + apiUrl: GOOGLE_FONT_API_URL, + })); } return baseConfig; diff --git a/package.json b/package.json index 4659233..4966f67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vue-storefront/magento-theme", - "version": "1.0.2", + "version": "1.1.0", "private": true, "license": "MIT", "homepage": "https://github.com/vuestorefront/magento2", @@ -38,7 +38,8 @@ "@pinia/nuxt": "^0.1.9", "@storefront-ui/vue": "^0.13.3", "@vue-storefront/cache": "~2.7.1", - "@vue-storefront/magento-api": "^1.0.2", + "@vue-storefront/http-cache": "^2.7.1", + "@vue-storefront/magento-api": "^1.1.0", "@vue-storefront/middleware": "~2.7.1", "@vue-storefront/nuxt": "~2.7.1", "@vue-storefront/redis-cache": "^1.0.1", diff --git a/pages/Cms.vue b/pages/Cms.vue new file mode 100644 index 0000000..2ad0d2b --- /dev/null +++ b/pages/Cms.vue @@ -0,0 +1,72 @@ + + + diff --git a/pages/Home.vue b/pages/Home.vue index 826b28e..a16ef3b 100644 --- a/pages/Home.vue +++ b/pages/Home.vue @@ -40,7 +40,7 @@ class="products" :button-text="$t('See more')" :title="$t('New Products')" - link="/c/women.html" + link="/women.html" /> @@ -70,11 +70,15 @@ import { ref, useContext, onMounted, + useFetch, } from '@nuxtjs/composition-api'; import LazyHydrate from 'vue-lazy-hydration'; import { useCache, CacheTagPrefix } from '@vue-storefront/cache'; import { SfBanner, SfBannerGrid } from '@storefront-ui/vue'; +import { CmsPage } from '~/modules/GraphQL/types'; import HeroSection from '~/components/HeroSection.vue'; +import { getMetaInfo } from '~/helpers/getMetaInfo'; +import { useContent } from '~/composables'; import LoadWhenVisible from '~/components/utils/LoadWhenVisible.vue'; export default defineComponent({ @@ -93,9 +97,12 @@ export default defineComponent({ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types setup() { const { addTags } = useCache(); + const { loadPage } = useContent(); const { app } = useContext(); const year = new Date().getFullYear(); const { isDesktop } = app.$device; + + const page = ref(null); const hero = ref({ title: app.i18n.t('Colorful summer dresses are already in store'), subtitle: app.i18n.t('SUMMER COLLECTION {year}', { year }), @@ -107,7 +114,7 @@ export default defineComponent({ fit: 'cover', format: 'webp', }, - link: '/c/women.html', + link: '/women.html', }); const banners = ref([ { @@ -131,7 +138,7 @@ export default defineComponent({ format: 'webp', }, class: 'sf-banner--slim desktop-only', - link: '/c/women/women-clothing-skirts', + link: '/women/women-clothing-skirts', }, { slot: 'banner-B', @@ -149,7 +156,7 @@ export default defineComponent({ format: 'webp', }, class: 'sf-banner--slim banner-central desktop-only', - link: '/c/women/women-clothing-dresses', + link: '/women/women-clothing-dresses', }, { slot: 'banner-C', @@ -163,7 +170,7 @@ export default defineComponent({ format: 'webp', }, class: 'sf-banner--slim banner__tshirt', - link: '/c/women/women-clothing-shirts', + link: '/women/women-clothing-shirts', }, { slot: 'banner-D', @@ -177,7 +184,7 @@ export default defineComponent({ format: 'webp', }, class: 'sf-banner--slim', - link: '/c/women/women-shoes-sandals', + link: '/women/women-shoes-sandals', }, ]); const callToAction = ref({ @@ -193,6 +200,10 @@ export default defineComponent({ }, }); + useFetch(async () => { + page.value = await loadPage({ identifier: 'home' }); + }); + onMounted(() => { addTags([{ prefix: CacheTagPrefix.View, value: 'home' }]); }); @@ -202,8 +213,12 @@ export default defineComponent({ banners, callToAction, hero, + page, }; }, + head() { + return getMetaInfo(this.page); + }, }); diff --git a/pages/Page.vue b/pages/Page.vue index b756034..0a5a0f7 100644 --- a/pages/Page.vue +++ b/pages/Page.vue @@ -1,90 +1,41 @@ + - diff --git a/plugins/fcPlugin.ts b/plugins/fcPlugin.ts index 97813cb..80a0044 100644 --- a/plugins/fcPlugin.ts +++ b/plugins/fcPlugin.ts @@ -20,7 +20,7 @@ declare module '@nuxt/types' { const plugin : Plugin = (context, inject) => { inject('fc', (value: number | string, locale?: string, options = {}): string => { // eslint-disable-next-line no-param-reassign - locale = locale || context.i18n?.localeProperties?.iso.replace('_', '-'); + locale = (locale || context.i18n?.localeProperties?.iso || '').replace('_', '-'); // eslint-disable-next-line no-param-reassign options = { currency: context.app.$vsf.$magento.config.state.getCurrency() || context.i18n?.localeProperties?.defaultCurrency, ...options }; return formatCurrency(value, locale, options); diff --git a/routes.js b/routes.js index 2f3f6aa..994f737 100644 --- a/routes.js +++ b/routes.js @@ -2,15 +2,16 @@ import path from 'node:path'; import url from 'node:url'; export function getRoutes(themeDir = path.dirname(url.fileURLToPath(import.meta.url))) { - return [{ - name: 'home', - path: '/', - component: path.resolve(themeDir, 'pages/Home.vue'), - }, - { - name: 'page', - path: '/:slug+', - component: path.resolve(themeDir, 'pages/Page.vue'), - }, + return [ + { + name: 'home', + path: '/', + component: path.resolve(themeDir, 'pages/Home.vue'), + }, + { + name: 'page', + path: '/:slug+', + component: path.resolve(themeDir, 'pages/Page.vue'), + }, ]; } diff --git a/stores/page.ts b/stores/page.ts new file mode 100644 index 0000000..741ddc6 --- /dev/null +++ b/stores/page.ts @@ -0,0 +1,11 @@ +import { defineStore } from 'pinia'; + +interface PageState { + routeData: any; +} + +export const usePageStore = defineStore('page', { + state: (): PageState => ({ + routeData: null, + }), +}); diff --git a/tests/unit/mocks/index.js b/tests/unit/mocks/index.js index 784dbf5..22f9ed0 100644 --- a/tests/unit/mocks/index.js +++ b/tests/unit/mocks/index.js @@ -9,3 +9,4 @@ export * from './useUser'; export * from './useUserBilling'; export * from './useReview'; export * from './cartGetters'; +export * from './useCartView'; diff --git a/tests/unit/mocks/useCartView.js b/tests/unit/mocks/useCartView.js new file mode 100644 index 0000000..2a52327 --- /dev/null +++ b/tests/unit/mocks/useCartView.js @@ -0,0 +1,409 @@ +import cartGettersMock from '~/tests/unit/mocks/cartGetters'; + +export const useCartViewMock = (cartViewData = {}) => ({ + showRemoveItemModal: jest.fn(), + removeItemAndSendNotification: jest.fn(), + delayedUpdateItemQty: jest.fn(), + goToCheckout: jest.fn(), + getAttributes: jest.fn((product) => product.configurable_options || []), + getBundles: jest.fn((product) => product.bundle_options?.map((b) => b.values).flat() || []), + isInStock: jest.fn((product) => product.product.stock_status === 'IN_STOCK'), + getMagentoImage: jest.fn(() => ''), + getProductPath: jest.fn(() => ''), + loading: false, + isAuthenticated: false, + products: [ + { + __typename: 'Product1', + uid: 'NDUw', + product: { + __typename: 'Product1', + uid: 'MTAzNA==', + sku: 'Product1', + name: 'Product1', + stock_status: 'OUT_OF_STOCK', + only_x_left_in_stock: null, + rating_summary: 0, + thumbnail: { + __typename: 'ProductImage', + url: 'https://xxx.jpg', + position: null, + disabled: null, + label: 'Product1', + }, + url_key: 'product-1', + url_rewrites: [ + { + __typename: 'UrlRewrite', + url: 'product-1.html', + }, + ], + price_range: { + __typename: 'PriceRange', + maximum_price: { + __typename: 'ProductPrice', + final_price: { + __typename: 'Money', + currency: 'USD', + value: 27, + }, + regular_price: { + __typename: 'Money', + currency: 'USD', + value: 27, + }, + }, + minimum_price: { + __typename: 'ProductPrice', + final_price: { + __typename: 'Money', + currency: 'USD', + value: 27, + }, + regular_price: { + __typename: 'Money', + currency: 'USD', + value: 27, + }, + }, + }, + categories: [ + { + __typename: 'CategoryTree', + uid: 'MTM=', + name: 'Bottoms', + + url_suffix: '.html', + url_path: 'men/bottoms-men', + breadcrumbs: [ + { + __typename: 'Breadcrumb', + category_name: 'Men', + category_url_path: 'men', + }, + ], + }, + { + __typename: 'CategoryTree', + uid: 'MTk=', + name: 'Shorts', + url_suffix: '.html', + url_path: 'men/bottoms-men/shorts-men', + breadcrumbs: [ + { + __typename: 'Breadcrumb', + category_name: 'Men', + category_url_path: 'men', + }, + { + __typename: 'Breadcrumb', + category_name: 'Bottoms', + category_url_path: 'men/bottoms-men', + }, + ], + }, + { + __typename: 'CategoryTree', + uid: 'MzE=', + name: 'Men Sale', + url_suffix: '.html', + url_path: 'promotions/men-sale', + breadcrumbs: null, + }, + ], + review_count: 0, + reviews: { + __typename: 'ProductReviews', + items: [], + }, + original_sku: '24-WG083', + }, + prices: { + __typename: 'CartItemPrices', + row_total: { + __typename: 'Money', + value: 27, + }, + row_total_including_tax: { + __typename: 'Money', + value: 27, + }, + total_item_discount: { + __typename: 'Money', + value: 0, + }, + }, + quantity: 1, + configurable_options: [ + { + __typename: 'SelectedConfigurableOption', + configurable_product_option_uid: 'Y29uZmlndXJhYmxlLzEwMzQvOTM=', + option_label: 'Color', + configurable_product_option_value_uid: 'Y29uZmlndXJhYmxlLzkzLzQ5', + value_label: 'Black', + }, + { + __typename: 'SelectedConfigurableOption', + configurable_product_option_uid: 'Y29uZmlndXJhYmxlLzEwMzQvMTQ0', + option_label: 'Size', + configurable_product_option_value_uid: 'Y29uZmlndXJhYmxlLzE0NC8xNzc=', + value_label: '34', + }, + ], + }, + { + __typename: 'Product2', + uid: 'NDUw2', + product: { + __typename: 'Product2', + uid: 'MTAzNA==', + sku: 'Product2', + name: 'Product2', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 0, + thumbnail: { + __typename: 'ProductImage', + url: 'https://xxx.jpg', + position: null, + disabled: null, + label: 'Product2', + }, + url_key: 'product-2', + url_rewrites: [ + { + __typename: 'UrlRewrite', + url: 'product-2.html', + }, + ], + price_range: { + __typename: 'PriceRange', + maximum_price: { + __typename: 'ProductPrice', + final_price: { + __typename: 'Money', + currency: 'USD', + value: 10, + }, + regular_price: { + __typename: 'Money', + currency: 'USD', + value: 10, + }, + }, + minimum_price: { + __typename: 'ProductPrice', + final_price: { + __typename: 'Money', + currency: 'USD', + value: 10, + }, + regular_price: { + __typename: 'Money', + currency: 'USD', + value: 10, + }, + }, + }, + categories: [ + { + __typename: 'CategoryTree', + uid: 'MTM=', + name: 'Bottoms', + url_suffix: '.html', + url_path: 'men/bottoms-men', + breadcrumbs: [ + { + __typename: 'Breadcrumb', + category_name: 'Men', + category_url_path: 'men', + }, + ], + }, + { + __typename: 'CategoryTree', + uid: 'MTk=', + name: 'Shorts', + url_suffix: '.html', + url_path: 'men/bottoms-men/shorts-men', + breadcrumbs: [ + { + __typename: 'Breadcrumb', + category_name: 'Men', + category_url_path: 'men', + }, + { + __typename: 'Breadcrumb', + category_name: 'Bottoms', + category_url_path: 'men/bottoms-men', + }, + ], + }, + { + __typename: 'CategoryTree', + uid: 'MzE=', + name: 'Men Sale', + url_suffix: '.html', + url_path: 'promotions/men-sale', + breadcrumbs: null, + }, + ], + review_count: 0, + reviews: { + __typename: 'ProductReviews', + items: [], + }, + original_sku: '24-WG081', + }, + prices: { + __typename: 'CartItemPrices', + row_total: { + __typename: 'Money', + value: 10, + }, + row_total_including_tax: { + __typename: 'Money', + value: 10, + }, + total_item_discount: { + __typename: 'Money', + value: 0, + }, + }, + quantity: 1, + configurable_options: [ + { + __typename: 'SelectedConfigurableOption', + configurable_product_option_uid: 'Y29uZmlndXJhYmxlLzEwMzQvOTM=', + option_label: 'Color', + configurable_product_option_value_uid: 'Y29uZmlndXJhYmxlLzkzLzQ5', + value_label: 'Black', + }, + { + __typename: 'SelectedConfigurableOption', + configurable_product_option_uid: 'Y29uZmlndXJhYmxlLzEwMzQvMTQ0', + option_label: 'Size', + configurable_product_option_value_uid: 'Y29uZmlndXJhYmxlLzE0NC8xNzc=', + value_label: '34', + }, + ], + }, + { + uid: 'MjYwNw==', + product: { + 0: {}, + uid: 'NDY=', + __typename: 'BundleProduct', + sku: '24-WG080', + name: 'Sprite Yoga Companion Kit', + stock_status: 'IN_STOCK', + only_x_left_in_stock: null, + rating_summary: 0, + thumbnail: { + url: 'https://magento2demo.frodigo.com/media/catalog/product/cache/6008460c710ac4e87d1a4c53dc478d67/l/u/luma-yoga-kit-2.jpg', position: null, disabled: null, label: 'Sprite Yoga Companion Kit', __typename: 'ProductImage', + }, + url_key: 'sprite-yoga-companion-kit', + url_rewrites: [{ url: 'sprite-yoga-companion-kit.html', __typename: 'UrlRewrite' }, { url: 'gear/sprite-yoga-companion-kit.html', __typename: 'UrlRewrite' }, { url: 'gear/fitness-equipment/sprite-yoga-companion-kit.html', __typename: 'UrlRewrite' }], + price_range: { maximum_price: { final_price: { currency: 'USD', value: 77, __typename: 'Money' }, regular_price: { currency: 'USD', value: 77, __typename: 'Money' }, __typename: 'ProductPrice' }, minimum_price: { final_price: { currency: 'USD', value: 61, __typename: 'Money' }, regular_price: { currency: 'USD', value: 61, __typename: 'Money' }, __typename: 'ProductPrice' }, __typename: 'PriceRange' }, + categories: [{ + uid: 'Mw==', name: 'Gear', url_suffix: '.html', url_path: 'gear', breadcrumbs: null, __typename: 'CategoryTree', + }, { + uid: 'NQ==', name: 'Fitness Equipment', url_suffix: '.html', url_path: 'gear/fitness-equipment', breadcrumbs: [{ category_name: 'Gear', category_url_path: 'gear', __typename: 'Breadcrumb' }], __typename: 'CategoryTree', + }], + review_count: 0, + reviews: { items: [], __typename: 'ProductReviews' }, + original_sku: '24-WG080', + }, + prices: { + __typename: 'CartItemPrices', + row_total: { + value: 70, + __typename: 'Money', + }, + row_total_including_tax: { + value: 70, + __typename: 'Money', + }, + total_item_discount: { + value: 0, + __typename: 'Money', + }, + }, + quantity: 1, + bundle_options: [{ + uid: 'YnVuZGxlLzE=', + label: 'Sprite Stasis Ball', + type: 'radio', + values: [{ + id: 3, label: 'Sprite Stasis Ball 75 cm', price: 32, quantity: 1, __typename: 'SelectedBundleOptionValue', + }], + __typename: 'SelectedBundleOption', + }, { + uid: 'YnVuZGxlLzI=', + label: 'Sprite Foam Yoga Brick', + type: 'radio', + values: [{ + id: 4, label: 'Sprite Foam Yoga Brick', price: 5, quantity: 1, __typename: 'SelectedBundleOptionValue', + }], + __typename: 'SelectedBundleOption', + }, { + uid: 'YnVuZGxlLzM=', + label: 'Sprite Yoga Strap', + type: 'radio', + values: [{ + id: 5, label: 'Sprite Yoga Strap 6 foot', price: 14, quantity: 1, __typename: 'SelectedBundleOptionValue', + }], + __typename: 'SelectedBundleOption', + }, { + uid: 'YnVuZGxlLzQ=', + label: 'Sprite Foam Roller', + type: 'radio', + values: [{ + id: 8, label: 'Sprite Foam Roller', price: 19, quantity: 1, __typename: 'SelectedBundleOptionValue', + }], + __typename: 'SelectedBundleOption', + }], + __typename: 'BundleCartItem', + }], + isRemoveModalVisible: false, + itemToRemove: null, + totals: { + total: 107, + subtotal: 107, + special: 107, + }, + totalItems: 2, + imageSizes: { + productCard: { + width: 216, + height: 268, + }, + productCardHorizontal: { + width: 140, + height: 200, + }, + checkout: { + imageWidth: 100, + imageHeight: 100, + }, + productGallery: { + thumbWidth: 160, + thumbHeight: 160, + imageWidth: 1080, + imageHeight: 1340, + }, + cart: { + imageWidth: 170, + imageHeight: 170, + }, + }, + discount: null, + cartGetters: cartGettersMock({ + getItemImage: jest.fn(), + getItemName: jest.fn(), + getItemPrice: jest.fn(() => ({ regular: 10, special: 10 })), + productHasSpecialPrice: jest.fn(() => false), + getItemQty: jest.fn(() => 1), + }), + ...cartViewData, +}); diff --git a/tests/unit/mocks/useRoute.ts b/tests/unit/mocks/useRoute.ts index 0200fb3..54c41d2 100644 --- a/tests/unit/mocks/useRoute.ts +++ b/tests/unit/mocks/useRoute.ts @@ -1,7 +1,7 @@ export const useRouteMock = (extend = {}) => ({ value: { - path: '/default/c/gear.html', - fullPath: '/default/c/gear.html', + path: '/default/gear.html', + fullPath: '/default/gear.html', }, ...extend, }); diff --git a/types/core.ts b/types/core.ts index 77c24db..d55ff17 100644 --- a/types/core.ts +++ b/types/core.ts @@ -4,6 +4,8 @@ export type UseContextReturn = ReturnType; export type CustomQuery = Record; +export type CustomHeaders = Record; + export type ApiClientMethods = { [K in keyof T]: T[K] extends (...args: any) => any ? diff --git a/yarn.lock b/yarn.lock index 18d0bdd..49482ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2836,10 +2836,15 @@ lodash-es "^4.17.15" vue "^2.6.11" -"@vue-storefront/magento-api@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@vue-storefront/magento-api/-/magento-api-1.0.2.tgz#ae71ce603f21122b2b9c6ceaac4fc2b312ceb5f0" - integrity sha512-RJ9djlhCsOAKH2UzGqkC1KGketEQZbFtfSdexp6NvBiZnf5sc1hUjkdMX0ZiKgDJc2oSLA2XZhIsPvjnhMXITQ== +"@vue-storefront/http-cache@^2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@vue-storefront/http-cache/-/http-cache-2.7.1.tgz#d33470efd404ebb24e73682707fb278d1d5d4b45" + integrity sha512-B5Qf39yodsS35dzKDCvLF5qgggyIFnbUq0iKc114VotLfkU68GXDFRkfE8mNhgtnjWOWAco04OmvWaxO5BI6PQ== + +"@vue-storefront/magento-api@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@vue-storefront/magento-api/-/magento-api-1.1.0.tgz#5fa3937732b049b661575ef86684adbacbdde9d4" + integrity sha512-xOfs9K/LKbm6ipuDSssaYww8fMacZA2CoL58cX7lUhMz4shD+XQoenVtTeXbT8ZW+vX44pBkZR5BdPlAln7vLw== dependencies: "@apollo/client" "^3.6.9" agentkeepalive "^4.2.1"