(), {
@@ -256,8 +253,6 @@ const properties = computed(() =>
);
const price = computed(() => (props.product.hasVariations ? props.product.minVariationPrice : props.product.price));
-const { isComponentRegistered, getComponent, shouldRenderComponent, getComponentProps } = useCustomProductComponents();
-
function slideChanged(swiper: SwiperInstance) {
const activeIndex: number = swiper.activeIndex;
const lastIndex: number = props.product.images?.length ? props.product.images.length - 1 : 0;
diff --git a/client-app/shared/catalog/components/product-card-list.vue b/client-app/shared/catalog/components/product-card-list.vue
index f7241890a9..9a22074f4b 100644
--- a/client-app/shared/catalog/components/product-card-list.vue
+++ b/client-app/shared/catalog/components/product-card-list.vue
@@ -97,14 +97,11 @@
-
getProductRoute(props.product.id, props.product.slug));
const isDigital = computed(() => props.product.productType === ProductType.Digital);
const properties = computed(() =>
diff --git a/client-app/shared/catalog/components/product-card.vue b/client-app/shared/catalog/components/product-card.vue
index a33da78157..956dfbbc9a 100644
--- a/client-app/shared/catalog/components/product-card.vue
+++ b/client-app/shared/catalog/components/product-card.vue
@@ -51,14 +51,11 @@
:single-line="viewMode === 'grid'"
/>
-
();
const props = defineProps();
-const { isComponentRegistered, getComponent, shouldRenderComponent, getComponentProps } = useCustomProductComponents();
-
const { isEnabled } = useModuleSettings(CUSTOMER_REVIEWS_MODULE_ID);
const productReviewsEnabled = isEnabled(CUSTOMER_REVIEWS_ENABLED_KEY);
diff --git a/client-app/shared/catalog/components/product-sidebar.vue b/client-app/shared/catalog/components/product-sidebar.vue
index 14321c187a..55c69e43b7 100644
--- a/client-app/shared/catalog/components/product-sidebar.vue
+++ b/client-app/shared/catalog/components/product-sidebar.vue
@@ -30,15 +30,13 @@
-
+
(() => props.product.productType === ProductType.Digital);
diff --git a/client-app/shared/common/components/extension-point-list.vue b/client-app/shared/common/components/extension-point-list.vue
new file mode 100644
index 0000000000..1d0ca6c1ff
--- /dev/null
+++ b/client-app/shared/common/components/extension-point-list.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
diff --git a/client-app/shared/common/components/extension-point.vue b/client-app/shared/common/components/extension-point.vue
new file mode 100644
index 0000000000..96b941cb38
--- /dev/null
+++ b/client-app/shared/common/components/extension-point.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/client-app/shared/common/composables/extensionRegistry/README.md b/client-app/shared/common/composables/extensionRegistry/README.md
new file mode 100644
index 0000000000..699b61f96b
--- /dev/null
+++ b/client-app/shared/common/composables/extensionRegistry/README.md
@@ -0,0 +1,84 @@
+# ExtensionPoint system — quick reference
+
+### Purpose
+1. Plug extra Vue components into fixed UI slots without touching core code.
+2. Add new extension points anywhere in the app quickly.
+
+---
+
+## Core pieces
+
+| Item | Description |
+|------|-------------|
+| **`useExtensionRegistry()`** | Global store + API |
+| **`ExtensionPoint`** | Placeholder component to render registered extension; renders default slot for each unregistered entry |
+| **`ExtensionPointList`** | Placeholder component to render multiple registered extensions; accepts optional `names` array; renders default slot for each unregistered entry |
+| **`$canRenderExtensionPoint`** | Global helper that evaluates `condition` |
+
+---
+
+## Main Scenarios
+
+### 1. Defining extension points in the core app
+
+1. Extend the category map:
+ - In `client-app/shared/common/types/extensionRegistryMap.ts`, add your new category key to `ExtensionCategoryMapType` with the appropriate `ExtensionEntryType`.
+2. Initialize the registry placeholder:
+ - In `client-app/shared/common/constants/initialExtensionRegistry.ts`, add an empty object for the new category.
+3. Update `EXTENSION_NAMES` (for static extension identifiers):
+ - In `client-app/shared/common/constants/extensionPointsNames.ts`, add entries under your category for each extension name:
+ ```ts
+ export const EXTENSION_NAMES = merge({}, INITIAL_EXTENSION_NAMES, {
+ myCategory: {
+ myExtension: 'my-extension',
+ },
+ });
+ ```
+4. Declare extension points in templates:
+ - Insert `` or `` in your Vue components, specifying `category` and `name` (`names` for multi).
+5. (Optional) Provide fallback slot content for unregistered names:
+ ```vue
+
+ Fallback content when no extension is registered
+
+ ```
+
+### 2. Enriching the app from modules
+
+1. Import and register your extension:
+ ```ts
+ import { useExtensionRegistry } from '@/shared/common/composables/useExtensionRegistry';
+ import { EXTENSION_NAMES } from '@/shared/common/constants/extensionPointsNames.ts';
+ const { register } = useExtensionRegistry();
+ register(
+ 'productCard',
+ EXTENSION_NAMES.productCard.cardButton,
+ { component: MyComponent }
+ );
+ ```
+2. (Optional) Unregister on cleanup:
+ ```ts
+ import { onUnmounted } from 'vue';
+ const { unregister } = useExtensionRegistry();
+ onUnmounted(() => {
+ unregister('myCategory', 'myExtension');
+ });
+ ```
+3. Your registered components will then be automatically rendered at the corresponding extension points in the core app.
+
+> **Recommendation**
+>
+> For consistent extension identifiers and to avoid typos, import the `EXTENSION_NAMES` constant from `@/shared/common/constants/extensionPointsNames.ts` and use its properties:
+> ```ts
+> import { EXTENSION_NAMES } from '@/shared/common/constants/extensionPointsNames.ts';
+> const { register } = useExtensionRegistry();
+> register(
+> 'productCard',
+> EXTENSION_NAMES.productCard.cardButton,
+> { component: MyComponent }
+> );
+> ```
+
+> [!TIP]
+>
+> **Dev tip:** In dev mode the registry is available as `window.VCExtensionRegistry`.
\ No newline at end of file
diff --git a/client-app/shared/common/composables/extensionRegistry/useExtensionRegistry.test.ts b/client-app/shared/common/composables/extensionRegistry/useExtensionRegistry.test.ts
new file mode 100644
index 0000000000..2d0b13ea05
--- /dev/null
+++ b/client-app/shared/common/composables/extensionRegistry/useExtensionRegistry.test.ts
@@ -0,0 +1,151 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { defineComponent } from "vue";
+import { useExtensionRegistry } from "@/shared/common/composables/extensionRegistry/useExtensionRegistry";
+import type { Product } from "@/core/api/graphql/types";
+import type { ExtensionCategoryType } from "@/shared/common/types/extensionRegistry";
+
+// Mock Logger and initial registry to avoid flakiness
+const mockLogger = vi.hoisted(() => ({ warn: vi.fn(), error: vi.fn() }));
+vi.mock("@/core/utilities", () => ({ Logger: mockLogger }));
+vi.mock("@/shared/common/constants/initialExtensionRegistry", () => ({
+ initialExtensionRegistry: {
+ headerMenu: {},
+ mobileMenu: {},
+ accountMenu: {},
+ mobileHeader: {},
+ productCard: {},
+ },
+}));
+
+describe("useExtensionRegistry", () => {
+ const DummyComponent = defineComponent({ name: "DummyComponent" });
+ let registry: ReturnType;
+ const dummyProduct = {} as unknown as Product;
+
+ beforeEach(() => {
+ vi.resetModules();
+ registry = useExtensionRegistry();
+ vi.resetAllMocks();
+ });
+
+ describe("initial state", () => {
+ it("should start with no entries in productCard", () => {
+ const entries = registry.getEntries("productCard");
+ expect(Object.keys(entries)).toHaveLength(0);
+ });
+ });
+
+ describe("registration", () => {
+ it("should register and retrieve entries", () => {
+ registry.register("productCard", "a", { component: DummyComponent });
+ const entries = registry.getEntries("productCard");
+ expect(entries).toEqual({ a: { component: DummyComponent } });
+ expect(registry.getComponent("productCard", "a")).toBe(DummyComponent);
+ expect(registry.isRegistered("productCard", "a")).toBe(true);
+ expect(registry.canRender("productCard", "a", dummyProduct)).toBe(true);
+ });
+
+ it("should not override existing entries", () => {
+ const A = defineComponent({ name: "ComponentA" });
+ const B = defineComponent({ name: "ComponentB" });
+ registry.register("productCard", "dup", { component: A });
+ registry.register("productCard", "dup", { component: B });
+ expect(registry.getComponent("productCard", "dup")).toBe(A);
+ });
+
+ it("should unregister entries", () => {
+ registry.register("productCard", "toRemove", { component: DummyComponent });
+ expect(registry.isRegistered("productCard", "toRemove")).toBe(true);
+ registry.unregister("productCard", "toRemove");
+ expect(registry.getEntries("productCard")).not.toContain("toRemove");
+ expect(registry.isRegistered("productCard", "toRemove")).toBe(false);
+ expect(registry.getComponent("productCard", "toRemove")).toBeNull();
+ });
+
+ it("should not throw when unregistering non-existent entry", () => {
+ expect(() => registry.unregister("productCard", "nonexistent")).not.toThrow();
+ });
+
+ it("should share state across instances", () => {
+ const reg1 = useExtensionRegistry();
+ const reg2 = useExtensionRegistry();
+ reg1.register("productCard", "globalTest", { component: DummyComponent });
+ expect(reg2.getEntries("productCard")).toEqual(
+ expect.objectContaining({ globalTest: { component: DummyComponent } }),
+ );
+ });
+
+ it("should warn when registering duplicate entries", () => {
+ registry.register("productCard", "warnTest", { component: DummyComponent });
+ registry.register("productCard", "warnTest", { component: DummyComponent });
+ expect(mockLogger.warn).toHaveBeenCalledTimes(1);
+ expect(mockLogger.warn).toHaveBeenCalledWith(
+ 'useExtensionRegistry: Component "productCard/warnTest" already registered',
+ );
+ });
+ });
+
+ describe("props management", () => {
+ it("should return undefined props when not set", () => {
+ registry.register("productCard", "noProps", { component: DummyComponent });
+ expect(registry.getProps("productCard", "noProps")).toBeUndefined();
+ });
+
+ it("should set and retrieve props", () => {
+ const props = { product: dummyProduct };
+ registry.register("productCard", "withProps", { component: DummyComponent, props });
+ expect(registry.getProps("productCard", "withProps")).toEqual(props);
+ });
+ });
+
+ describe("condition evaluation", () => {
+ it("should handle condition function correctly", () => {
+ const otherProduct = {} as unknown as Product;
+ const condition = (product: Product) => product === dummyProduct;
+ registry.register("productCard", "cond", { component: DummyComponent, condition });
+ expect(registry.canRender("productCard", "cond", dummyProduct)).toBe(true);
+ expect(registry.canRender("productCard", "cond", otherProduct)).toBe(false);
+ });
+ });
+
+ describe("entries snapshot", () => {
+ it("should maintain immutability of entries snapshot", () => {
+ registry.register("productCard", "readonly", { component: DummyComponent });
+ const entries = registry.getEntries("productCard");
+ (entries as Record)["mutated"] = 123;
+ expect(registry.getEntries("productCard")).not.toContain("mutated");
+ });
+
+ it("should reflect entries keys with getEntries", () => {
+ const uniqueCategory = "testCategoryForEntriesSnapshot" as ExtensionCategoryType;
+
+ registry.register(uniqueCategory, "entryA", { component: DummyComponent });
+ registry.register(uniqueCategory, "entryB", { component: DummyComponent });
+
+ const entries = registry.getEntries(uniqueCategory);
+ expect(Object.keys(entries)).toEqual(["entryA", "entryB"]);
+ });
+ });
+
+ describe("error handling", () => {
+ it("should return false for unregistered entries", () => {
+ expect(registry.getComponent("productCard", "unknown")).toBeNull();
+ expect(registry.isRegistered("productCard", "unknown")).toBe(false);
+ expect(registry.canRender("productCard", "unknown", dummyProduct)).toBe(false);
+ });
+
+ it("should return false and log an error when condition throws", () => {
+ const errorCondition = () => {
+ throw new Error("Test condition error");
+ };
+ registry.register("productCard", "errTest", { component: DummyComponent, condition: errorCondition });
+ const result = registry.canRender("productCard", "errTest", dummyProduct);
+ expect(result).toBe(false);
+ expect(mockLogger.error).toHaveBeenCalledTimes(1);
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ 'useExtensionRegistry: Error in condition for component "productCard/errTest"',
+ expect.any(Error),
+ );
+ });
+ });
+});
diff --git a/client-app/shared/common/composables/extensionRegistry/useExtensionRegistry.ts b/client-app/shared/common/composables/extensionRegistry/useExtensionRegistry.ts
new file mode 100644
index 0000000000..2ac35d21b5
--- /dev/null
+++ b/client-app/shared/common/composables/extensionRegistry/useExtensionRegistry.ts
@@ -0,0 +1,114 @@
+import { createGlobalState } from "@vueuse/core";
+import pick from "lodash/pick";
+import { shallowReadonly, shallowRef } from "vue";
+import { IS_DEVELOPMENT } from "@/core/constants";
+import { Logger } from "@/core/utilities";
+import { initialExtensionRegistry } from "@/shared/common/constants/initialExtensionRegistry";
+import type { ExtensionCategoryType, ExtensionRegistryStateType } from "@/shared/common/types/extensionRegistry";
+
+function _useExtensionRegistry() {
+ const entries = shallowRef(initialExtensionRegistry);
+
+ function register(
+ category: C,
+ name: N,
+ item: ExtensionRegistryStateType[C][N],
+ ) {
+ if (!entries.value[category]) {
+ entries.value[category] = {};
+ }
+ if (!entries.value[category][name]) {
+ entries.value[category][name] = item;
+ } else {
+ Logger.warn(`useExtensionRegistry: Component "${category}/${name}" already registered`);
+ }
+ }
+
+ function unregister(category: C, name: string) {
+ delete entries.value[category]?.[name];
+ }
+
+ function getEntries(category: C, names?: string[]) {
+ if (names) {
+ return shallowReadonly(pick(entries.value[category], names));
+ }
+ return shallowReadonly(entries.value[category] ?? {});
+ }
+
+ function getComponent(
+ category: C,
+ name: N,
+ ) {
+ return entries.value[category]?.[name]?.component ?? null;
+ }
+
+ function isRegistered(
+ category: C,
+ name: N,
+ ) {
+ return entries.value[category]?.[name]?.component !== undefined;
+ }
+
+ function canRender(
+ category: C,
+ name: N,
+ parameter: Parameters>[0],
+ ): boolean {
+ if (!isRegistered(category, name)) {
+ return false;
+ }
+
+ const condition = entries.value[category]?.[name]?.condition as ExtensionRegistryStateType[C][N]["condition"];
+
+ if (condition && typeof condition === "function") {
+ try {
+ return condition(parameter);
+ } catch (error) {
+ Logger.error(`useExtensionRegistry: Error in condition for component "${category}/${name}"`, error);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function getProps(
+ category: C,
+ name: N,
+ ): ExtensionRegistryStateType[C][N]["props"] {
+ return entries.value[category]?.[name]?.props;
+ }
+
+ // To debug in development mode
+ if (IS_DEVELOPMENT) {
+ window.VCExtensionRegistry = {
+ entries,
+
+ register,
+ unregister,
+
+ getComponent,
+ getEntries,
+ getProps,
+
+ isRegistered,
+ canRender,
+ };
+ }
+
+ return {
+ entries,
+
+ register,
+ unregister,
+
+ getComponent,
+ getEntries,
+ getProps,
+
+ isRegistered,
+ canRender,
+ };
+}
+
+export const useExtensionRegistry = createGlobalState(_useExtensionRegistry);
diff --git a/client-app/shared/common/composables/index.ts b/client-app/shared/common/composables/index.ts
index 0b44c8ae44..77d41b307b 100644
--- a/client-app/shared/common/composables/index.ts
+++ b/client-app/shared/common/composables/index.ts
@@ -1,2 +1,2 @@
-export * from "./useCustomProductComponents";
+export * from "./extensionRegistry/useExtensionRegistry";
export * from "./useSlugInfo";
diff --git a/client-app/shared/common/composables/useCustomProductComponents.ts b/client-app/shared/common/composables/useCustomProductComponents.ts
deleted file mode 100644
index b0221898cf..0000000000
--- a/client-app/shared/common/composables/useCustomProductComponents.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { createGlobalState } from "@vueuse/core";
-import { shallowRef } from "vue";
-import { Logger } from "@/core/utilities";
-import type { Product } from "@/core/api/graphql/types";
-import type { DefineComponent } from "vue";
-
-export type ElementType = {
- id: string;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- component: DefineComponent<{ product: Product }, Record, any>;
- shouldRender?: (product: Product) => boolean;
- props?: Record;
-};
-
-function _useCustomProductComponents() {
- const customProductComponents = shallowRef<{ [key: string]: ElementType }>({});
-
- function registerComponent(element: ElementType) {
- if (!customProductComponents.value[element.id]) {
- customProductComponents.value[element.id] = element;
- } else {
- Logger.warn(`useCustomProductComponents: Custom product component with id ${element.id} already registered`);
- }
- }
-
- function getComponent(id: string) {
- return customProductComponents.value[id]?.component;
- }
-
- function isComponentRegistered(id: string) {
- return customProductComponents.value[id] !== undefined;
- }
-
- function shouldRenderComponent(id: string, product: Product) {
- return typeof customProductComponents.value[id]?.shouldRender === "function"
- ? customProductComponents.value[id]?.shouldRender(product)
- : true;
- }
-
- function getComponentProps(id: string) {
- return customProductComponents.value[id]?.props;
- }
-
- return {
- registerComponent,
- getComponent,
- isComponentRegistered,
- shouldRenderComponent,
- getComponentProps,
- };
-}
-
-export const useCustomProductComponents = createGlobalState(_useCustomProductComponents);
diff --git a/client-app/shared/common/constants/customProductComponents.ts b/client-app/shared/common/constants/customProductComponents.ts
deleted file mode 100644
index 59b42d1c96..0000000000
--- a/client-app/shared/common/constants/customProductComponents.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const CUSTOM_PRODUCT_COMPONENT_IDS = {
- CARD_BUTTON: "card-button",
- PAGE_SIDEBAR_BUTTON: "page-sidebar-button",
-};
diff --git a/client-app/shared/common/constants/extensionPointsNames.ts b/client-app/shared/common/constants/extensionPointsNames.ts
new file mode 100644
index 0000000000..e934ce8aa2
--- /dev/null
+++ b/client-app/shared/common/constants/extensionPointsNames.ts
@@ -0,0 +1,18 @@
+import merge from "lodash/merge";
+import { initialExtensionRegistry } from "./initialExtensionRegistry";
+
+const INITIAL_EXTENSION_NAMES = Object.fromEntries(
+ Object.entries(initialExtensionRegistry).map(([key, entries]) => [
+ key,
+ Object.fromEntries(Object.keys(entries).map((name) => [name, name])),
+ ]),
+);
+
+export const EXTENSION_NAMES = merge({}, INITIAL_EXTENSION_NAMES, {
+ productCard: {
+ cardButton: "card-button",
+ },
+ productPage: {
+ sidebarButton: "sidebar-button",
+ },
+});
diff --git a/client-app/shared/common/constants/index.ts b/client-app/shared/common/constants/index.ts
index 45805303e9..897490a8fe 100644
--- a/client-app/shared/common/constants/index.ts
+++ b/client-app/shared/common/constants/index.ts
@@ -1 +1,2 @@
-export * from "./customProductComponents";
+export * from "./extensionPointsNames";
+export * from "./initialExtensionRegistry";
diff --git a/client-app/shared/common/constants/initialExtensionRegistry.ts b/client-app/shared/common/constants/initialExtensionRegistry.ts
new file mode 100644
index 0000000000..7d641d7b4f
--- /dev/null
+++ b/client-app/shared/common/constants/initialExtensionRegistry.ts
@@ -0,0 +1,44 @@
+import { defineAsyncComponent } from "vue";
+import type { ExtensionRegistryStateType } from "@/shared/common/types/extensionRegistry";
+
+export const initialExtensionRegistry: ExtensionRegistryStateType = {
+ headerMenu: {
+ compare: {
+ component: defineAsyncComponent(
+ () => import("@/shared/layout/components/header/_internal/link-components/link-compare.vue"),
+ ),
+ },
+ cart: {
+ component: defineAsyncComponent(
+ () => import("@/shared/layout/components/header/_internal/link-components/link-cart.vue"),
+ ),
+ },
+ },
+ mobileMenu: {
+ cart: {
+ component: defineAsyncComponent(
+ () => import("@/shared/layout/components/header/_internal/mobile-menu/link-components/link-cart.vue"),
+ ),
+ },
+ compare: {
+ component: defineAsyncComponent(
+ () => import("@/shared/layout/components/header/_internal/mobile-menu/link-components/link-compare.vue"),
+ ),
+ },
+ },
+ accountMenu: {
+ orders: {
+ component: defineAsyncComponent(
+ () => import("@/shared/account/components/account-navigation-link-components/link-orders.vue"),
+ ),
+ },
+ lists: {
+ component: defineAsyncComponent(
+ () => import("@/shared/account/components/account-navigation-link-components/link-lists.vue"),
+ ),
+ },
+ },
+ mobileHeader: {},
+ productCard: {},
+ productPage: {},
+};
diff --git a/client-app/shared/common/types/extensionRegistry.ts b/client-app/shared/common/types/extensionRegistry.ts
new file mode 100644
index 0000000000..b58828d55e
--- /dev/null
+++ b/client-app/shared/common/types/extensionRegistry.ts
@@ -0,0 +1,7 @@
+import type { ExtensionCategoryMapType } from "@/shared/common/types/extensionRegistryMap";
+
+export type ExtensionRegistryStateType = {
+ [K in keyof ExtensionCategoryMapType]: Record;
+};
+
+export type ExtensionCategoryType = keyof ExtensionCategoryMapType;
diff --git a/client-app/shared/common/types/extensionRegistryMap.ts b/client-app/shared/common/types/extensionRegistryMap.ts
new file mode 100644
index 0000000000..f27f08dc0c
--- /dev/null
+++ b/client-app/shared/common/types/extensionRegistryMap.ts
@@ -0,0 +1,29 @@
+import type { Product } from "@/core/api/graphql/types";
+import type { ExtendedMenuLinkType } from "@/core/types";
+import type { Component } from "vue";
+
+type ExtensionEntryType<
+ Props = never,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ Condition extends (parameter: any) => boolean = never,
+> = {
+ component: Component;
+ condition?: Condition;
+ props?: Props;
+};
+
+/**
+ * Here we define the extension categories and the extension entries for each category.
+ * ExtensionEntryType is a type that defines the extension entry for a given category.
+ */
+export type ExtensionCategoryMapType = {
+ headerMenu: ExtensionEntryType<{ item: ExtendedMenuLinkType }>;
+ mobileMenu: ExtensionEntryType<{ item: ExtendedMenuLinkType }>;
+ accountMenu: ExtensionEntryType<{ item: ExtendedMenuLinkType }>;
+ mobileHeader: ExtensionEntryType;
+ productCard: ExtensionEntryType<
+ { product?: Product; isTextShown?: boolean; lazy?: boolean },
+ (product: Product) => boolean
+ >;
+ productPage: ExtensionEntryType<{ product?: Product }, (product: Product) => boolean>;
+};
diff --git a/client-app/shared/common/types/types.d.ts b/client-app/shared/common/types/types.d.ts
new file mode 100644
index 0000000000..1d892bc053
--- /dev/null
+++ b/client-app/shared/common/types/types.d.ts
@@ -0,0 +1,13 @@
+import type ExtensionPointList from "@/shared/common/components/extension-point-list.vue";
+import type ExtensionPoint from "@/shared/common/components/extension-point.vue";
+
+declare module "vue" {
+ // Global components is already declared interface which we want to augment
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ export interface GlobalComponents {
+ ExtensionPoint: typeof ExtensionPoint;
+ ExtensionPointList: typeof ExtensionPointList;
+ }
+}
+
+export {};
diff --git a/client-app/shared/common/types/window.d.ts b/client-app/shared/common/types/window.d.ts
new file mode 100644
index 0000000000..56de2fda33
--- /dev/null
+++ b/client-app/shared/common/types/window.d.ts
@@ -0,0 +1,9 @@
+import type { useExtensionRegistry } from "@/shared/common/composables/extensionRegistry/useExtensionRegistry";
+
+export {};
+
+declare global {
+ interface Window {
+ VCExtensionRegistry: Partial>;
+ }
+}
diff --git a/client-app/shared/layout/components/header/_internal/bottom-header.vue b/client-app/shared/layout/components/header/_internal/bottom-header.vue
index dfd1930a44..656324e260 100644
--- a/client-app/shared/layout/components/header/_internal/bottom-header.vue
+++ b/client-app/shared/layout/components/header/_internal/bottom-header.vue
@@ -41,7 +41,9 @@
@@ -78,7 +80,6 @@ import { useRoute, useRouter } from "vue-router";
import { useNavigations, useWhiteLabeling } from "@/core/composables";
import { useUser } from "@/shared/account/composables/useUser";
import { SearchBar } from "@/shared/layout";
-import { useCustomHeaderLinkComponents } from "@/shared/layout/composables/useCustomHeaderLinkComponents";
import CatalogMenu from "./catalog-menu.vue";
import type { StyleValue } from "vue";
import LinkDefault from "@/shared/layout/components/header/_internal/link-components/link-default.vue";
@@ -93,7 +94,6 @@ const router = useRouter();
const { organization } = useUser();
const { logoUrl } = useWhiteLabeling();
const { catalogMenuItems, desktopMainMenuItems } = useNavigations();
-const { customLinkComponents } = useCustomHeaderLinkComponents();
const bottomHeader = ref(null);
const catalogMenuElement = shallowRef(null);
diff --git a/client-app/shared/layout/components/header/_internal/mobile-header.vue b/client-app/shared/layout/components/header/_internal/mobile-header.vue
index 233404800e..76b1230595 100644
--- a/client-app/shared/layout/components/header/_internal/mobile-header.vue
+++ b/client-app/shared/layout/components/header/_internal/mobile-header.vue
@@ -53,7 +53,7 @@
-
+
@@ -141,7 +141,6 @@ import { QueryParamName } from "@/core/enums";
import { ROUTES } from "@/router/routes/constants";
import { useShortCart } from "@/shared/cart";
import { useNestedMobileHeader } from "@/shared/layout";
-import { useCustomMobileHeaderComponents } from "@/shared/layout/composables/useCustomMobileHeaderComponents";
import { useSearchBar } from "@/shared/layout/composables/useSearchBar";
import { ShipToSelector } from "@/shared/ship-to-location";
import MobileMenu from "./mobile-menu/mobile-menu.vue";
@@ -150,7 +149,6 @@ import type { RouteLocationRaw } from "vue-router";
import BarcodeScanner from "@/shared/layout/components/search-bar/barcode-scanner.vue";
const router = useRouter();
-const { customComponents } = useCustomMobileHeaderComponents();
const searchPhrase = ref("");
const searchPhraseInUrl = useRouteQueryParam(QueryParamName.SearchPhrase);
const mobileMenuVisible = ref(false);
diff --git a/client-app/shared/layout/components/header/_internal/mobile-menu/menus/default-menu.vue b/client-app/shared/layout/components/header/_internal/mobile-menu/menus/default-menu.vue
index b0ca1d3347..d9d6757efe 100644
--- a/client-app/shared/layout/components/header/_internal/mobile-menu/menus/default-menu.vue
+++ b/client-app/shared/layout/components/header/_internal/mobile-menu/menus/default-menu.vue
@@ -1,33 +1,31 @@
-
-
-
-
-
-
- {{ formattedText }}
-
-
+
+
+ {{ formattedText }}
+
+
+