From 5309fbf9ddaa3d3942064e425670bd3b4901b2a0 Mon Sep 17 00:00:00 2001 From: mkucmus Date: Thu, 4 Feb 2021 10:19:15 +0100 Subject: [PATCH 1/6] feat(default-theme): interceptor for wishlist --- .../composables/__tests__/useWishlist.spec.ts | 24 ++++++++++++- .../composables/src/logic/useIntercept.ts | 36 +++++++++++++++++++ packages/composables/src/logic/useWishlist.ts | 13 +++++++ .../src/logic/notifications/index.js | 9 +++++ .../src/plugins/notifications.js | 2 ++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/composables/__tests__/useWishlist.spec.ts b/packages/composables/__tests__/useWishlist.spec.ts index 6d0243ecb..f2e245d69 100644 --- a/packages/composables/__tests__/useWishlist.spec.ts +++ b/packages/composables/__tests__/useWishlist.spec.ts @@ -4,12 +4,24 @@ import Vue from "vue"; import VueCompositionApi, * as vueComp from "@vue/composition-api"; (vueComp.onMounted as any) = jest.fn(); Vue.use(VueCompositionApi); -import { useWishlist } from "@shopware-pwa/composables"; +import * as Composables from "@shopware-pwa/composables"; +jest.mock("@shopware-pwa/composables"); +const mockedComposables = Composables as jest.Mocked; +import { useWishlist } from "../src/logic/useWishlist"; describe("Composables - useWishlist", () => { const rootContextMock: any = { $shopwareApiInstance: jest.fn(), }; + const broadcastMock = jest.fn(); + const interceptMock = jest.fn(); + + mockedComposables.useIntercept.mockImplementation(() => { + return { + broadcast: broadcastMock, + intercept: interceptMock, + } as any; + }); beforeEach(() => { jest.clearAllMocks(); }); @@ -115,5 +127,15 @@ describe("Composables - useWishlist", () => { expect(vueComp.onMounted).toBeCalled(); }); }); + describe("onAddToWishlist", () => { + it("should add interceptor method", () => { + const { onAddToWishlist } = useWishlist(rootContextMock, null as any); + onAddToWishlist(() => {}); + expect(interceptMock).toHaveBeenCalledWith( + "addToWishlist", + expect.any(Function) + ); + }); + }); }); }); diff --git a/packages/composables/src/logic/useIntercept.ts b/packages/composables/src/logic/useIntercept.ts index 533928a1e..41b5a4de6 100644 --- a/packages/composables/src/logic/useIntercept.ts +++ b/packages/composables/src/logic/useIntercept.ts @@ -15,6 +15,12 @@ export const INTERCEPTOR_KEYS = { * As a parameter passes product added to cart and quantity. */ ADD_TO_CART: "addToCart", + /** + * Broadcasted by useWishlist composable on successful addToWishlist method invocation. + * As a parameter passes: + * - product object + */ + ADD_TO_WISHLIST: "addToWishlist", /** * Broadcasted by useCart composable on successful submitPromotionCode method invocation. * As a parameter passes used promotion code and response result. @@ -29,11 +35,41 @@ export const INTERCEPTOR_KEYS = { * - error - string - message of the error */ ERROR: "error", + /** + * Broadcasted by useCheckout, createOrder method. + * As a parameter passes: + * - order object + */ + ORDER_PLACE: "onOrderPlace", + /** + * Broadcasted by useSessionContext, setCurrency method. + * As a parameter passes: + * - currency object + */ + SESSION_SET_CURRENCY: "onCurrencyChange", + /** + * Broadcasted by useSessionContext, setPaymentMethod method. + * As a parameter passes: + * - payment method object + */ + SESSION_SET_PAYMENT_METHOD: "onPaymentMethodChange", + /** + * Broadcasted by useSessionContext, setShippingMethod method. + * As a parameter passes: + * - shipping method object + */ + SESSION_SET_SHIPPING_METHOD: "onShippingMethodChange", /** * Broadcasted after user is logged out. * Contains no params. */ USER_LOGOUT: "onUserLogout", + /** + * Broadcasted after user is logged in. + * As a parameter passes: + * - customer object + */ + USER_LOGIN: "onUserLogin", }; /** diff --git a/packages/composables/src/logic/useWishlist.ts b/packages/composables/src/logic/useWishlist.ts index f28d2592c..2dd9b5c01 100644 --- a/packages/composables/src/logic/useWishlist.ts +++ b/packages/composables/src/logic/useWishlist.ts @@ -2,6 +2,11 @@ import Vue from "vue"; import { ref, Ref, reactive, computed, onMounted } from "@vue/composition-api"; import { Product } from "@shopware-pwa/commons/interfaces/models/content/product/Product"; import { ApplicationVueContext, getApplicationContext } from "../appContext"; +import { + INTERCEPTOR_KEYS, + useIntercept, + IInterceptorCallbackFunction, +} from "@shopware-pwa/composables"; /** * interface for {@link useWishlist} composable @@ -12,6 +17,7 @@ export interface IUseWishlist { removeFromWishlist: (id: string) => void; clearWishlist: () => void; addToWishlist: () => void; + onAddToWishlist: (fn: (params: { product: Product }) => void) => void; isInWishlist: Ref; items: Ref; count: Ref; @@ -29,9 +35,12 @@ export const useWishlist = ( rootContext: ApplicationVueContext, product?: Product ): IUseWishlist => { + const { broadcast, intercept } = useIntercept(rootContext); getApplicationContext(rootContext, "useNotifications"); const localWishlist = reactive(sharedWishlist); const productId: Ref = ref(product?.id); + const onAddToWishlist = (fn: IInterceptorCallbackFunction) => + intercept(INTERCEPTOR_KEYS.ADD_TO_WISHLIST, fn); // update wishlist in localstorage const updateStorage = (): void => { @@ -83,6 +92,9 @@ export const useWishlist = ( if (!sharedWishlist.items.includes(productId.value)) { sharedWishlist.items.push(productId.value); updateStorage(); + broadcast(INTERCEPTOR_KEYS.ADD_TO_WISHLIST, { + product, + }); } }; @@ -106,5 +118,6 @@ export const useWishlist = ( clearWishlist, items, count, + onAddToWishlist, }; }; diff --git a/packages/default-theme/src/logic/notifications/index.js b/packages/default-theme/src/logic/notifications/index.js index aa74acea8..4336a9421 100644 --- a/packages/default-theme/src/logic/notifications/index.js +++ b/packages/default-theme/src/logic/notifications/index.js @@ -1,5 +1,14 @@ import { useNotifications } from "@shopware-pwa/composables" +export const addToWishlistNotification = (params, rootContext) => { + const { pushSuccess } = useNotifications(rootContext) + pushSuccess( + rootContext.$t( + `${params?.product?.translated?.name} has been added to wishlist.` + ) + ) +} + export const addToCartNotification = (product, rootContext) => { const { pushSuccess } = useNotifications(rootContext) pushSuccess( diff --git a/packages/default-theme/src/plugins/notifications.js b/packages/default-theme/src/plugins/notifications.js index dbea09e34..4821b9253 100644 --- a/packages/default-theme/src/plugins/notifications.js +++ b/packages/default-theme/src/plugins/notifications.js @@ -2,10 +2,12 @@ import { useIntercept, INTERCEPTOR_KEYS } from "@shopware-pwa/composables" import { addPromotionCodeNotification, addToCartNotification, + addToWishlistNotification, } from "@/logic/notifications" export default ({ app }) => { const { intercept } = useIntercept(app) intercept(INTERCEPTOR_KEYS.ADD_TO_CART, addToCartNotification) intercept(INTERCEPTOR_KEYS.ADD_PROMOTION_CODE, addPromotionCodeNotification) + intercept(INTERCEPTOR_KEYS.ADD_TO_WISHLIST, addToWishlistNotification) } From 2be970bf8991f5e735bb835b82c026c2ed4865b0 Mon Sep 17 00:00:00 2001 From: mkucmus Date: Thu, 4 Feb 2021 13:23:52 +0100 Subject: [PATCH 2/6] feat(composables): place order interceptor broadcast --- .../composables/__tests__/useCheckout.spec.ts | 41 +++++++++++------ packages/composables/src/logic/useCheckout.ts | 45 ++++++++++++++----- 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/packages/composables/__tests__/useCheckout.spec.ts b/packages/composables/__tests__/useCheckout.spec.ts index 0d6ff7d58..61c0fc802 100644 --- a/packages/composables/__tests__/useCheckout.spec.ts +++ b/packages/composables/__tests__/useCheckout.spec.ts @@ -34,7 +34,8 @@ describe("Composables - useCheckout", () => { }, $shopwareApiInstance: jest.fn(), }; - + const interceptMock = jest.fn(); + const broadcastMock = jest.fn(); const refreshCartMock = jest.fn(async () => {}); beforeEach(() => { jest.resetAllMocks(); @@ -57,6 +58,12 @@ describe("Composables - useCheckout", () => { sessionContext: sessionContextMock, } as any; }); + mockedComposables.useIntercept.mockImplementation(() => { + return { + broadcast: broadcastMock, + intercept: interceptMock, + } as any; + }); stateContext.value = null; consoleErrorSpy.mockImplementationOnce(() => {}); }); @@ -375,12 +382,11 @@ describe("Composables - useCheckout", () => { await expect(createOrder()).rejects.toEqual({ message: "some error", }); - expect(consoleErrorSpy).toBeCalledWith( - "[useCheckout][createOrder] isGuest:false", - { - message: "some error", - } - ); + expect(broadcastMock).toBeCalledWith("error", { + error: { message: "some error" }, + inputParams: {}, + methodName: "[useCheckout][createOrder]", + }); }); }); @@ -425,14 +431,23 @@ describe("Composables - useCheckout", () => { await expect(createOrder()).rejects.toEqual({ message: "some guest error", }); - expect(consoleErrorSpy).toBeCalledWith( - "[useCheckout][createOrder] isGuest:true", - { - message: "some guest error", - } - ); + expect(broadcastMock).toBeCalledWith("error", { + error: { message: "some guest error" }, + inputParams: {}, + methodName: "[useCheckout][createOrder]", + }); }); }); }); + describe("onOrderPlace", () => { + it("should add interceptor method", () => { + const { onOrderPlace } = useCheckout(rootContextMock); + onOrderPlace(() => {}); + expect(interceptMock).toHaveBeenCalledWith( + "onOrderPlace", + expect.any(Function) + ); + }); + }); }); }); diff --git a/packages/composables/src/logic/useCheckout.ts b/packages/composables/src/logic/useCheckout.ts index 6f34c5894..4d349470c 100644 --- a/packages/composables/src/logic/useCheckout.ts +++ b/packages/composables/src/logic/useCheckout.ts @@ -1,8 +1,9 @@ import Vue from "vue"; import { Ref, computed, reactive } from "@vue/composition-api"; -import { useUser, useCart } from "@shopware-pwa/composables"; import { ShippingMethod } from "@shopware-pwa/commons/interfaces/models/checkout/shipping/ShippingMethod"; import { PaymentMethod } from "@shopware-pwa/commons/interfaces/models/checkout/payment/PaymentMethod"; +import { ClientApiError } from "@shopware-pwa/commons/interfaces/errors/ApiError"; + import { GuestOrderParams, ShippingAddress, @@ -14,7 +15,14 @@ import { createGuestOrder, createOrder as createApiOrder, } from "@shopware-pwa/shopware-6-client"; -import { useSessionContext } from "@shopware-pwa/composables"; +import { + useUser, + useCart, + useSessionContext, + INTERCEPTOR_KEYS, + useIntercept, + IInterceptorCallbackFunction, +} from "@shopware-pwa/composables"; import { ApplicationVueContext, getApplicationContext } from "../appContext"; import { BillingAddress } from "@shopware-pwa/commons/interfaces/models/checkout/customer/BillingAddress"; @@ -41,6 +49,7 @@ export interface IUseCheckout { updateGuestOrderParams: (params: Partial) => void; shippingAddress: Readonly>; billingAddress: Readonly | undefined>>; + onOrderPlace: (fn: (params: { order: Order }) => void) => void; } const orderData: { @@ -61,8 +70,11 @@ const orderData: { export const useCheckout = ( rootContext: ApplicationVueContext ): IUseCheckout => { - const { apiInstance } = getApplicationContext(rootContext, "useCheckout"); - + const { apiInstance, contextName } = getApplicationContext( + rootContext, + "useCheckout" + ); + const { broadcast, intercept } = useIntercept(rootContext); const { isLoggedIn } = useUser(rootContext); const { refreshCart } = useCart(rootContext); const { sessionContext } = useSessionContext(rootContext); @@ -74,6 +86,8 @@ export const useCheckout = ( () => orderData.paymentMethods ); const localOrderData = reactive(orderData); + const onOrderPlace = (fn: IInterceptorCallbackFunction) => + intercept(INTERCEPTOR_KEYS.ORDER_PLACE, fn); const getShippingMethods = async ( { forceReload } = { forceReload: false } @@ -99,20 +113,28 @@ export const useCheckout = ( const createOrder = async () => { try { + let order; if (isGuestOrder.value) { - return await createGuestOrder( + order = await createGuestOrder( orderData.guestOrderParams as GuestOrderParams, apiInstance ); } else { - return await createApiOrder(apiInstance); + order = await createApiOrder(apiInstance); } + broadcast(INTERCEPTOR_KEYS.ORDER_PLACE, { + order, + }); + + return order; } catch (e) { - console.error( - "[useCheckout][createOrder] isGuest:" + isGuestOrder.value, - e - ); - throw e; + const err: ClientApiError = e; + broadcast(INTERCEPTOR_KEYS.ERROR, { + methodName: `[${contextName}][createOrder]`, + inputParams: {}, + error: err, + }); + throw err; } finally { await refreshCart(); } @@ -146,5 +168,6 @@ export const useCheckout = ( updateGuestOrderParams, shippingAddress, billingAddress, + onOrderPlace, }; }; From 080ed093a62ca274b06d329374660b40d25f9534 Mon Sep 17 00:00:00 2001 From: mkucmus Date: Thu, 4 Feb 2021 15:38:35 +0100 Subject: [PATCH 3/6] feat(composables): login and register interceptors --- .../models/checkout/customer/Customer.ts | 2 +- .../composables/__tests__/useUser.spec.ts | 18 ++++++++++++- packages/composables/src/hooks/useUser.ts | 26 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/commons/interfaces/models/checkout/customer/Customer.ts b/packages/commons/interfaces/models/checkout/customer/Customer.ts index 1a33a467d..2f72e438c 100644 --- a/packages/commons/interfaces/models/checkout/customer/Customer.ts +++ b/packages/commons/interfaces/models/checkout/customer/Customer.ts @@ -10,7 +10,7 @@ import { Tag } from "../../system/tag/Tag"; import { CustomField } from "../../common/CustomField"; /** - * @alpha + * @beta */ export interface Customer { id: string; diff --git a/packages/composables/__tests__/useUser.spec.ts b/packages/composables/__tests__/useUser.spec.ts index 99b215a80..9dd3e9745 100644 --- a/packages/composables/__tests__/useUser.spec.ts +++ b/packages/composables/__tests__/useUser.spec.ts @@ -74,6 +74,22 @@ describe("Composables - useUser", () => { }); describe("methods", () => { + describe("onUserLogin", () => { + it("should invoke an intercept function on onUserLogin event", async () => { + const { onUserLogin } = useUser(rootContextMock); + const callback = jest.fn(); + await onUserLogin(callback); + expect(interceptMock).toBeCalledTimes(1); + }); + }); + describe("onUserRegister", () => { + it("should invoke an intercept function on onUserRegister event", async () => { + const { onUserRegister } = useUser(rootContextMock); + const callback = jest.fn(); + await onUserRegister(callback); + expect(interceptMock).toBeCalledTimes(1); + }); + }); describe("onLogout", () => { it("should invoke an intercept function on onLogout event", async () => { const { onLogout } = useUser(rootContextMock); @@ -154,7 +170,7 @@ describe("Composables - useUser", () => { mockedApiClient.login.mockResolvedValueOnce({ "sw-context-token": "qweqwe", } as any); - mockedApiClient.getCustomer.mockResolvedValueOnce({ + mockedApiClient.getCustomer.mockResolvedValue({ id: "123", } as any); const { isLoggedIn, error, login } = useUser(rootContextMock); diff --git a/packages/composables/src/hooks/useUser.ts b/packages/composables/src/hooks/useUser.ts index d734a1c6e..a65624e53 100644 --- a/packages/composables/src/hooks/useUser.ts +++ b/packages/composables/src/hooks/useUser.ts @@ -91,6 +91,8 @@ export interface IUseUser { * React on user logout */ onLogout: (fn: () => void) => void; + onUserLogin: (fn: (params: { customer: Customer }) => void) => void; + onUserRegister: (fn: () => void) => void; } /** @@ -122,10 +124,19 @@ export const useUser = (rootContext: ApplicationVueContext): IUseUser => { error.value = null; try { await apiLogin({ username, password }, apiInstance); + await refreshUser(); + broadcast(INTERCEPTOR_KEYS.USER_LOGIN, { + user: user.value, + }); return true; } catch (e) { const err: ClientApiError = e; error.value = err.message; + broadcast(INTERCEPTOR_KEYS.ERROR, { + methodName: `[${contextName}][login]`, + inputParams: {}, + error: err, + }); return false; } finally { loading.value = false; @@ -140,10 +151,16 @@ export const useUser = (rootContext: ApplicationVueContext): IUseUser => { error.value = null; try { await apiRegister(params, apiInstance); + broadcast(INTERCEPTOR_KEYS.USER_REGISTER); return true; } catch (e) { const err: ClientApiError = e; error.value = err; + broadcast(INTERCEPTOR_KEYS.ERROR, { + methodName: `[${contextName}][register]`, + inputParams: {}, + error: err, + }); return false; } finally { loading.value = false; @@ -153,6 +170,7 @@ export const useUser = (rootContext: ApplicationVueContext): IUseUser => { const logout = async (): Promise => { try { await apiLogout(apiInstance); + console.warn("logout"); broadcast(INTERCEPTOR_KEYS.USER_LOGOUT); } catch (e) { const err: ClientApiError = e; @@ -169,6 +187,12 @@ export const useUser = (rootContext: ApplicationVueContext): IUseUser => { const onLogout = (fn: IInterceptorCallbackFunction) => intercept(INTERCEPTOR_KEYS.USER_LOGOUT, fn); + const onUserLogin = (fn: IInterceptorCallbackFunction) => + intercept(INTERCEPTOR_KEYS.USER_LOGIN, fn); + + const onUserRegister = (fn: IInterceptorCallbackFunction) => + intercept(INTERCEPTOR_KEYS.USER_REGISTER, fn); + const refreshUser = async (): Promise => { try { const user = await getCustomer(apiInstance); @@ -346,5 +370,7 @@ export const useUser = (rootContext: ApplicationVueContext): IUseUser => { loadCountry, country, onLogout, + onUserLogin, + onUserRegister, }; }; From 956ee183fabf6195dfa53c70e1706132914f7b55 Mon Sep 17 00:00:00 2001 From: mkucmus Date: Thu, 4 Feb 2021 15:39:13 +0100 Subject: [PATCH 4/6] fix(default-theme): megamenu missing category name --- packages/default-theme/src/components/SwMegaMenu.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/default-theme/src/components/SwMegaMenu.vue b/packages/default-theme/src/components/SwMegaMenu.vue index 27c985d88..01c5a6781 100644 --- a/packages/default-theme/src/components/SwMegaMenu.vue +++ b/packages/default-theme/src/components/SwMegaMenu.vue @@ -1,7 +1,7 @@