Repository: shopware/frontends
Pull request #893 (2024-04-21 10:55:31)

feat: added Pagination for Wishlist

vercel[bot]: [vc]: #UdyWUacWl30K+RzuKBIVekAFafsyV3+PAGMU6W6VaX0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzaG9wd2FyZS1mcm9udGVuZHMtZG9jcyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2RvY3MvIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3Nob3B3YXJlLWZyb250ZW5kcy9zaG9wd2FyZS1mcm9udGVuZHMtZG9jcy83UDIydW1zbkJFREYzQXhEWHZlaXM5ZlBzb3U0IiwicHJldmlld1VybCI6InNob3B3YXJlLWZyb250ZW5kcy1kb2NzLWdpdC1mb3JrLWtoYW4tODhkMzM5LXNob3B3YXJlLWZyb250ZW5kcy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IlBFTkRJTkcifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **shopware-frontends-docs** | 🔄 Building ([Inspect](https://vercel.com/shopware-frontends/shopware-frontends-docs/7P22umsnBEDF3AxDXveis9fPsou4)) | [Visit Preview](https://shopware-frontends-docs-git-fork-khan-88d339-shopware-frontends.vercel.app) | Apr 21, 2024 10:55am |


codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/shopware/frontends/branches/khanSoliheen:paginationForWishlistOrderHistoryPage)

### Merging #893 will **not alter performance**

<sub>Comparing <code>khanSoliheen:paginationForWishlistOrderHistoryPage</code> (abe3b88) with <code>main</code> (18d8528)</sub>



### Summary

`✅ 9` untouched benchmarks





BrocksiNet: @mkucmus, @kstala, and @khanSoliheen, I fixed the Test for the `useWishlist` composable.

Now, we have 100% coverage for the `useWishlist` composable. :tada: 
BrocksiNet: @mkucmus and @patzick fixed the ts errors, updated tests, tested orders pagination, and fixed it.

You can review it again. I added @kstala for testing, also.
kstala: @BrocksiNet can you take a look at this, I have 2 problems (both cases involve guest user) 
- as a guest user, adding a product to the wishlist doesn't update the number on the heart wishlist icon
https://github.com/shopware/frontends/assets/30267600/0548bf58-c9ff-4530-a59b-5bb52a191a9e

- pagination for wishlist doesn't work for guest users
Uploading Screen Recording 2024-07-08 at 09.29.05.mov…




BrocksiNet: FYI: I removed the wishlist call from `onMounted` because it is not needed information for SSR. I now make sure that the wishlist call is only executed once per page, not twice on the wishlist page (like it was before). So overall, this will improve the SSR times a bit.

Patch:
diff --git a/.changeset/light-cows-protect.md b/.changeset/light-cows-protect.md
new file mode 100644
index 00000000..f4740106
--- /dev/null
+++ b/.changeset/light-cows-protect.md
@@ -0,0 +1,5 @@
+---
+"vue-demo-store": minor
+---
+
+Added a pagination for the order history page and wishlist page.
diff --git a/.changeset/tough-queens-swim.md b/.changeset/tough-queens-swim.md
new file mode 100644
index 00000000..2706eac5
--- /dev/null
+++ b/.changeset/tough-queens-swim.md
@@ -0,0 +1,9 @@
+---
+"@shopware-pwa/composables-next": minor
+---
+
+- `useWishList`:
+  - Added `getCurrentPage` and `getTotalPagesCount` to the returned object
+  - Changed `getWishlistProducts` to accept `page` and `query` as optional parameters
+- `useSyncWishList`:
+  - Changed `getWishlistProducts`, added Parameter to pass default criterias
diff --git a/apps/docs/src/resources/integrations/payments/adyen.md b/apps/docs/src/resources/integrations/payments/adyen.md
index a1941e3d..671306e4 100644
--- a/apps/docs/src/resources/integrations/payments/adyen.md
+++ b/apps/docs/src/resources/integrations/payments/adyen.md
@@ -13,8 +13,6 @@ nav:
   position: 10
 ---
 
-
 [<img src="../../../.assets/payment-icons/adyen.png" alt="Adyen Logo" class="mb-8 h-20" />](https://docs.adyen.com/)
 
-
-<!-- load: ../../../../../examples/adyen-dropin-component/README.md -->
\ No newline at end of file
+<!-- load: ../../../../../examples/adyen-dropin-component/README.md -->
diff --git a/apps/docs/src/resources/integrations/payments/mollie.md b/apps/docs/src/resources/integrations/payments/mollie.md
index a544413a..13a4cfd0 100644
--- a/apps/docs/src/resources/integrations/payments/mollie.md
+++ b/apps/docs/src/resources/integrations/payments/mollie.md
@@ -15,5 +15,4 @@ nav:
 
 [<img src="../../../.assets/payment-icons/mollie.webp" alt="mollie Logo" class="mb-8 h-20" />](https://docs.mollie.com/)
 
-
-<!-- load: ../../../../../examples/mollie-credit-card/README.md -->
\ No newline at end of file
+<!-- load: ../../../../../examples/mollie-credit-card/README.md -->
diff --git a/packages/cms-base/components/SwProductCard.vue b/packages/cms-base/components/SwProductCard.vue
index 10ce36e9..8824550c 100644
--- a/packages/cms-base/components/SwProductCard.vue
+++ b/packages/cms-base/components/SwProductCard.vue
@@ -83,11 +83,17 @@ const toggleWishlistProduct = async () => {
   isLoading.value = true;
 
   try {
-    if (!isInWishlist.value) await addToWishlist();
-    else await removeFromWishlist();
-    pushSuccess(
-      `${props.product?.translated?.name} ${isLoading.value ? translations.product.addedToWishlist : translations.product.removedFromTheWishlist}`,
-    );
+    if (!isInWishlist.value) {
+      await addToWishlist();
+      pushSuccess(
+        `${props.product?.translated?.name} ${translations.product.addedToWishlist}`,
+      );
+    } else {
+      await removeFromWishlist();
+      pushError(
+        `${props.product?.translated?.name} ${translations.product.removedFromTheWishlist}`,
+      );
+    }
   } catch (error) {
     if (error instanceof ApiClientError) {
       const reason = error.details.errors?.[0]?.detail
diff --git a/packages/composables/src/useCustomerOrders.ts b/packages/composables/src/useCustomerOrders.ts
index f6ea0fbb..c59ae438 100644
--- a/packages/composables/src/useCustomerOrders.ts
+++ b/packages/composables/src/useCustomerOrders.ts
@@ -1,5 +1,5 @@
-import { ref } from "vue";
-import type { Ref } from "vue";
+import { ref, computed } from "vue";
+import type { Ref, ComputedRef } from "vue";
 import { useShopwareContext } from "#imports";
 import type { Schemas } from "#shopware";
 
@@ -18,6 +18,18 @@ export type UseCustomerOrdersReturn = {
    * Fetches the orders list and assigns the result to the `orders` property
    */
   loadOrders(parameters?: Schemas["Criteria"]): Promise<void>;
+  /**
+   * Current page number
+   */
+  currentPage: ComputedRef<number>;
+  /**
+   * total pages
+   */
+  totalPages: ComputedRef<number>;
+  /**
+   * Limit of orders per page
+   */
+  limit: Ref<number>;
 };
 
 /**
@@ -30,21 +42,45 @@ export function useCustomerOrders(): UseCustomerOrdersReturn {
 
   const orders: Ref<Schemas["Order"][]> = ref([]);
 
+  const currentPaginationPage = ref<number>(1);
+  const limit = ref<number>(15);
+
+  const totalOrderItemsCount: Ref<number> = ref(0);
+
+  const currentParams = ref<Schemas["Criteria"]>({});
+
   const loadOrders = async (
     parameters: Schemas["Criteria"] = {},
   ): Promise<void> => {
+    const params = {
+      ...parameters,
+      limit: limit.value,
+    };
+    currentParams.value = params;
     const fetchedOrders = await apiClient.invoke("readOrder post /order", {
-      body: parameters,
+      body: { ...params, "total-count-mode": "exact" },
     });
     orders.value = fetchedOrders.data.orders.elements;
+    totalOrderItemsCount.value = fetchedOrders.data.orders.total ?? 0;
   };
 
-  const changeCurrentPage = async (pageNumber: number | string) =>
-    await loadOrders({ page: +pageNumber });
+  const changeCurrentPage = async (pageNumber: number) => {
+    await loadOrders({ ...currentParams.value, page: pageNumber });
+    currentPaginationPage.value = pageNumber;
+  };
+
+  const currentPage = computed(() => currentPaginationPage.value);
+
+  const totalPages = computed(() => {
+    return Math.ceil(totalOrderItemsCount.value / limit.value);
+  });
 
   return {
     orders,
     changeCurrentPage,
     loadOrders,
+    currentPage,
+    totalPages,
+    limit,
   };
 }
diff --git a/packages/composables/src/useSyncWishlist.ts b/packages/composables/src/useSyncWishlist.ts
index 28c9c588..10ce8a27 100644
--- a/packages/composables/src/useSyncWishlist.ts
+++ b/packages/composables/src/useSyncWishlist.ts
@@ -2,12 +2,15 @@ import { ref, computed } from "vue";
 import type { Ref, ComputedRef } from "vue";
 import { useShopwareContext } from "#imports";
 import { ApiClientError } from "@shopware/api-client";
+import type { Schemas } from "#shopware";
 
 export type UseSyncWishlistReturn = {
   /**
    * Get products from wishlist
    */
-  getWishlistProducts(): void;
+  getWishlistProducts(
+    defaultSearchCriteria?: Schemas["Criteria"],
+  ): Promise<void>;
   /**
    * Merge products with wishlist already existing in API wishlist
    */
@@ -28,9 +31,15 @@ export type UseSyncWishlistReturn = {
    * Wishlist items count
    */
   count: ComputedRef<number>;
+  /**
+   * Current page number
+   */
+  currentPage: ComputedRef<number>;
 };
 
 const _wishlistItems: Ref<string[]> = ref([]);
+const _currentPage: Ref<number> = ref(1);
+const totalWishlistItemsCount: Ref<number> = ref(0);
 
 /**
  * Composable to manage wishlist via API
@@ -39,7 +48,6 @@ const _wishlistItems: Ref<string[]> = ref([]);
  */
 export function useSyncWishlist(): UseSyncWishlistReturn {
   const { apiClient } = useShopwareContext();
-
   async function addToWishlistSync(id: string) {
     await apiClient.invoke(
       "addProductOnWishlist post /customer/wishlist/add/{productId}",
@@ -62,14 +70,19 @@ export function useSyncWishlist(): UseSyncWishlistReturn {
    * Fetch wishlist items
    * Only for logged-in users
    */
-  async function getWishlistProducts() {
+  async function getWishlistProducts(
+    defaultSearchCriteria?: Schemas["Criteria"],
+  ) {
     try {
       const response = await apiClient.invoke(
         "readCustomerWishlist post /customer/wishlist",
+        { body: { ...defaultSearchCriteria, "total-count-mode": "exact" } },
       );
       _wishlistItems.value = [
         ...response.data.products.elements.map((element) => element.id),
       ];
+      totalWishlistItemsCount.value = response.data.products.total ?? 0;
+      _currentPage.value = response.data.products.page ?? 1;
     } catch (e) {
       if (e instanceof ApiClientError) {
         // If 404 ignore printing error and reset wishlist
@@ -89,7 +102,8 @@ export function useSyncWishlist(): UseSyncWishlistReturn {
   }
 
   const items = computed(() => _wishlistItems.value);
-  const count = computed(() => items.value.length);
+  const count = computed(() => totalWishlistItemsCount.value);
+  const currentPage = computed(() => _currentPage.value);
 
   return {
     getWishlistProducts,
@@ -98,5 +112,6 @@ export function useSyncWishlist(): UseSyncWishlistReturn {
     mergeWishlistProducts,
     items,
     count,
+    currentPage,
   };
 }
diff --git a/packages/composables/src/useWishlist.ts b/packages/composables/src/useWishlist.ts
index 9378a9db..72a61759 100644
--- a/packages/composables/src/useWishlist.ts
+++ b/packages/composables/src/useWishlist.ts
@@ -1,9 +1,11 @@
 /**
  * Template composable
  */
-import { computed } from "vue";
+import { computed, ref } from "vue";
 import type { ComputedRef } from "vue";
 import { useUser, useLocalWishlist, useSyncWishlist } from "#imports";
+import type { Schemas } from "#shopware";
+import { defu } from "defu";
 
 export type UseWishlistReturn = {
   /**
@@ -13,7 +15,7 @@ export type UseWishlistReturn = {
   /**
    * Get products list added to wishlist
    */
-  getWishlistProducts(): void;
+  getWishlistProducts(query?: Schemas["Criteria"]): Promise<void>;
   /**
    * Clear wishlist
    */
@@ -23,9 +25,21 @@ export type UseWishlistReturn = {
    */
   items: ComputedRef<string[]>;
   /**
-   * Wishlist items count
+   * Current page number
+   */
+  currentPage: ComputedRef<number>;
+  /**
+   * total pages count
+   */
+  totalPagesCount: ComputedRef<number>;
+  /**
+   * total wishlist items count
    */
   count: ComputedRef<number>;
+  /**
+   * Indicates if the wishlist can be synced
+   */
+  canSyncWishlist: ComputedRef<boolean>;
 };
 
 /**
@@ -38,7 +52,6 @@ export function useWishlist(): UseWishlistReturn {
   const canSyncWishlist = computed(
     () => isLoggedIn.value && !isGuestSession.value,
   );
-
   const {
     getWishlistProducts: getWishlistProductsLocal,
     items: itemsLocal,
@@ -50,16 +63,34 @@ export function useWishlist(): UseWishlistReturn {
     items: itemsSync,
     mergeWishlistProducts: mergeWishlistProductsSync,
     removeFromWishlistSync,
+    count: countSync,
+    currentPage,
   } = useSyncWishlist();
 
-  const getWishlistProducts = async () => {
+  const limit = ref<number>(15);
+
+  const getWishlistProducts = async (query?: Schemas["Criteria"]) => {
+    const wishlistQuery = {} as Schemas["Criteria"];
+    if (query) {
+      defu(wishlistQuery, query);
+    }
+    if (query?.limit) {
+      limit.value = query.limit;
+    }
+
     if (canSyncWishlist.value) {
-      await getWishlistProductsSync();
+      await getWishlistProductsSync(query);
     } else {
-      await getWishlistProductsLocal();
+      getWishlistProductsLocal();
     }
   };
 
+  const items = computed(() =>
+    canSyncWishlist.value ? itemsSync.value : itemsLocal.value,
+  );
+
+  const totalPagesCount = computed(() => Math.ceil(count.value / limit.value));
+
   const clearWishlist = async () => {
     if (canSyncWishlist.value) {
       await Promise.all(items.value.map((id) => removeFromWishlistSync(id)));
@@ -74,19 +105,24 @@ export function useWishlist(): UseWishlistReturn {
       await mergeWishlistProductsSync(itemsLocal.value);
       clearWishlist();
     }
-    getWishlistProductsSync();
+    await getWishlistProductsSync();
   };
 
-  const items = computed(() =>
-    canSyncWishlist.value ? itemsSync.value : itemsLocal.value,
-  );
-  const count = computed(() => items.value.length);
+  const count = computed(() => {
+    if (canSyncWishlist.value) {
+      return countSync.value;
+    }
+    return items.value.length;
+  });
 
   return {
     mergeWishlistProducts,
     getWishlistProducts,
     clearWishlist,
     items,
+    currentPage,
+    totalPagesCount,
     count,
+    canSyncWishlist,
   };
 }
diff --git a/templates/vue-demo-store/app.vue b/templates/vue-demo-store/app.vue
index 1481c5e6..6a2c4766 100644
--- a/templates/vue-demo-store/app.vue
+++ b/templates/vue-demo-store/app.vue
@@ -109,9 +109,15 @@ if (languages.value?.elements.length && router.currentRoute.value.name) {
   provide("urlPrefix", prefix);
 }
 
+if (import.meta.client) {
+  // getting the wishlist products should not block SSR
+  if (!(router.currentRoute.value.name as string).includes("wishlist")) {
+    getWishlistProducts(); // initial page loading
+  }
+}
+
 onMounted(() => {
   refreshCart();
-  getWishlistProducts();
 });
 </script>
 
diff --git a/templates/vue-demo-store/components/layout/LayoutHeader.vue b/templates/vue-demo-store/components/layout/LayoutHeader.vue
index 0e391795..565ddcfe 100644
--- a/templates/vue-demo-store/components/layout/LayoutHeader.vue
+++ b/templates/vue-demo-store/components/layout/LayoutHeader.vue
@@ -51,7 +51,7 @@ const sidebarController = useModal();
                 class="w-7 h-7 i-carbon-favorite text-secondary-600 hover:text-primary hover:animate-count-infinite hover:animate-heart-beat"
               />
               <span
-                v-if="wishlistCount > 0"
+                v-if="wishlistCount"
                 class="text-3 font-sm text-white absolute bg-red-600 rounded-full min-w-5 min-h-5 top-0 right-0 leading-5"
               >
                 {{ wishlistCount }}
diff --git a/templates/vue-demo-store/i18n/de-DE/account.json b/templates/vue-demo-store/i18n/de-DE/account.json
index a83f8e23..f557da48 100644
--- a/templates/vue-demo-store/i18n/de-DE/account.json
+++ b/templates/vue-demo-store/i18n/de-DE/account.json
@@ -44,7 +44,9 @@
       "orderDate": "Bestelldatum",
       "orderStatus": "Bestellstatus",
       "paymentMethod": "Zahlungsmethode",
-      "repeatOrder": "Bestellung wiederholen"
+      "repeatOrder": "Bestellung wiederholen",
+      "order": "Bestellung",
+      "orders": "Bestellungen"
     },
     "personalData": {
       "infoBox": "Bearbeiten Sie Ihre Daten unten, damit Ihr Konto immer auf dem neuesten Stand ist.",
diff --git a/templates/vue-demo-store/i18n/en-GB/account.json b/templates/vue-demo-store/i18n/en-GB/account.json
index 1f7c0c22..be72a08b 100644
--- a/templates/vue-demo-store/i18n/en-GB/account.json
+++ b/templates/vue-demo-store/i18n/en-GB/account.json
@@ -46,7 +46,9 @@
       "orderDate": "Order date",
       "orderStatus": "Order status",
       "paymentMethod": "Payment method",
-      "repeatOrder": "Repeat order"
+      "repeatOrder": "Repeat order",
+      "order": "Order",
+      "orders": "Orders"
     },
     "personalData": {
       "infoBox": "Feel free to edit any of your details below so your account is always up to date",
diff --git a/templates/vue-demo-store/i18n/pl-PL/account.json b/templates/vue-demo-store/i18n/pl-PL/account.json
index 07374470..cdb3aa25 100644
--- a/templates/vue-demo-store/i18n/pl-PL/account.json
+++ b/templates/vue-demo-store/i18n/pl-PL/account.json
@@ -44,7 +44,9 @@
       "orderDate": "Data zamówienia",
       "orderStatus": "Status zamówienia",
       "paymentMethod": "Metoda płatności",
-      "repeatOrder": "Powtórz zamówienie"
+      "repeatOrder": "Powtórz zamówienie",
+      "order": "zamówienie",
+      "orders": "Zamówienia"
     },
     "personalData": {
       "infoBox": "Edytuj swoje dane poniżej, aby twoje konto zawsze było aktualne",
diff --git a/templates/vue-demo-store/pages/account/order.vue b/templates/vue-demo-store/pages/account/order.vue
index 7493cfa8..8bc18337 100644
--- a/templates/vue-demo-store/pages/account/order.vue
+++ b/templates/vue-demo-store/pages/account/order.vue
@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import { useRoute, useRouter } from "vue-router";
+
 defineOptions({
   name: "OrderHistory",
 });
@@ -6,8 +8,41 @@ definePageMeta({
   layout: "account",
 });
 
-const { orders, loadOrders } = useCustomerOrders();
+const {
+  orders,
+  loadOrders,
+  changeCurrentPage,
+  totalPages,
+  currentPage,
+  limit,
+} = useCustomerOrders();
 const { t } = useI18n();
+const route = useRoute();
+const router = useRouter();
+
+const defaultLimit = 15;
+const defaultPage = 1;
+
+type Translations = {
+  account: {
+    noOrders: string;
+    perPage: string;
+    order: {
+      order: string;
+      orders: string;
+    };
+  };
+};
+let translations: Translations = {
+  account: {
+    noOrders: "No orders found 😔",
+    perPage: "Per Page:",
+    order: {
+      order: "Order",
+      orders: "Orders",
+    },
+  },
+};
 
 useBreadcrumbs([
   {
@@ -20,8 +55,36 @@ useBreadcrumbs([
   },
 ]);
 
+limit.value = route.query.limit ? Number(route.query.limit) : defaultLimit;
+
+const changeLimit = async (ev: Event) => {
+  const select = ev.target as HTMLSelectElement;
+  limit.value = +select.value;
+
+  await router.push({
+    query: {
+      ...route.query,
+      p: defaultPage,
+      limit: select.value,
+    },
+  });
+  changeCurrentPage(+route.query.p!);
+};
+
+const changePage = async (page: number) => {
+  await router.push({
+    query: {
+      ...route.query,
+      p: page,
+    },
+  });
+  await changeCurrentPage(page);
+};
+
 await useAsyncData("getOrders", () => {
   return loadOrders({
+    limit: limit.value,
+    page: route.query.p ? Number(route.query.p) : defaultPage,
     associations: {
       stateMachineState: {},
     },
@@ -41,5 +104,57 @@ await useAsyncData("getOrders", () => {
       {{ $t("account.orderHistoryHeader") }}
     </h1>
     <AccountOrder v-for="order in orders" :key="order.id" :order="order" />
+    <div v-if="orders.length === 0" class="text-center text-secondary-600">
+      {{ translations.account.noOrders }}
+    </div>
+    <div
+      class="grid grid-cols-1 lg:flex lg:justify-center lg- gap-4 md:gap-6 lg:gap-8 p-4 md:p-6 lg:p-8"
+    >
+      <div class="text-center place-self-center">
+        <SwPagination
+          :total="totalPages"
+          :current="Number(currentPage)"
+          @change-page="changePage"
+        />
+      </div>
+      <div class="text-center place-self-center mt-2 lg:mt-0">
+        <div class="inline-block align-top text-center md:text-left">
+          <label for="limit" class="inline mr-4">{{
+            translations.account.perPage
+          }}</label>
+          <select
+            id="limit"
+            v-model="limit"
+            name="limitchoices"
+            class="inline appearance-none bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline"
+            @change="changeLimit"
+          >
+            <option :value="1">1 {{ translations.account.order.order }}</option>
+            <option :value="15">
+              15 {{ translations.account.order.orders }}
+            </option>
+            <option :value="30">
+              30 {{ translations.account.order.orders }}
+            </option>
+            <option :value="45">
+              45 {{ translations.account.order.orders }}
+            </option>
+          </select>
+          <div
+            class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
+          >
+            <svg
+              class="fill-current h-4 w-4"
+              xmlns="http://www.w3.org/2000/svg"
+              viewBox="0 0 20 20"
+            >
+              <path
+                d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
+              />
+            </svg>
+          </div>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
diff --git a/templates/vue-demo-store/pages/wishlist.vue b/templates/vue-demo-store/pages/wishlist.vue
index c9eac9db..f463d17e 100644
--- a/templates/vue-demo-store/pages/wishlist.vue
+++ b/templates/vue-demo-store/pages/wishlist.vue
@@ -1,23 +1,55 @@
 <script setup lang="ts">
 import type { Schemas } from "#shopware";
+import defu from "defu";
+import { useRoute, useRouter } from "vue-router";
 
+const defaultLimit = 15;
+const defaultPage = 1;
+
+type Translations = {
+  listing: {
+    noProducts: string;
+    perPage: string;
+    product: string;
+    products: string;
+  };
+};
+let translations: Translations = {
+  listing: {
+    noProducts: "No products found 😔",
+    perPage: "Per Page:",
+    product: "Product",
+    products: "Products",
+  },
+};
+translations = defu(translations, useCmsTranslations()) as Translations;
+
+const {
+  items,
+  clearWishlist,
+  getWishlistProducts,
+  currentPage,
+  totalPagesCount,
+  canSyncWishlist,
+} = useWishlist();
 defineOptions({
   name: "WishlistPage",
 });
-
-const { items, clearWishlist } = useWishlist();
 const { apiClient } = useShopwareContext();
 const products = ref<Schemas["Product"][]>([]);
 const isLoading = ref(false);
 const { t } = useI18n();
 const localePath = useLocalePath();
 const { formatLink } = useInternationalization(localePath);
+const route = useRoute();
+const router = useRouter();
 useBreadcrumbs([
   {
     name: t("breadcrumbs.wishlist"),
     path: "/wishlist",
   },
 ]);
+const limit = ref(route.query.limit ? Number(route.query.limit) : defaultLimit);
 const clearWishlistHandler = async () => {
   try {
     isLoading.value = true;
@@ -44,6 +76,30 @@ const loadProductsByItemIds = async (itemIds: string[]): Promise<void> => {
   isLoading.value = false;
 };
 
+const changeLimit = async (limit: Event) => {
+  const select = limit.target as HTMLSelectElement;
+  const query = {
+    ...route.query,
+    limit: Number(select.value),
+    page: defaultPage,
+  };
+
+  await router.push({
+    query: query,
+  });
+  await getWishlistProducts(query);
+};
+
+const changePage = async (page: number) => {
+  await router.push({
+    query: {
+      page: page,
+      limit: limit.value,
+    },
+  });
+  await getWishlistProducts(route.query);
+};
+
 watch(
   items,
   (items, oldItems) => {
@@ -59,6 +115,40 @@ watch(
     immediate: true,
   },
 );
+
+const initalRoute = defu(route);
+watch(
+  () => route,
+  (newRoute) => {
+    if (initalRoute.path !== newRoute.path) {
+      getWishlistProducts(); // update wishlist items when you leave wishlist page
+      return;
+    }
+    if (Object.keys(newRoute.query).length > 0) {
+      return;
+    }
+    // this fires to reset the page when query are removed/empty on client side navigation for the same page (without hard reload)
+    getWishlistProducts({
+      limit: defaultLimit,
+      page: defaultPage,
+    });
+    limit.value = defaultLimit;
+  },
+  { deep: true },
+);
+
+onMounted(() => {
+  const route = useRoute();
+  const limit = ref(
+    route.query.limit ? Number(route.query.limit) : defaultLimit,
+  );
+  const page = ref(route.query.page ? Number(route.query.page) : defaultPage);
+  // this request is needed to start with the first page and the current limit or default limit for the pagination on wishlist page
+  getWishlistProducts({
+    limit: limit.value,
+    page: page.value,
+  });
+});
 </script>
 
 <template>
@@ -87,13 +177,12 @@ watch(
         >
           {{ $t("wishlist.clearWishlist") }}
         </button>
-        <div
-          class="grid grid-cols-1 gap-y-10 gap-x-6 sm:grid-cols-2 lg:grid-cols-4 xl:gap-x-8"
-        >
+        <div class="flex justify-center flex-wrap p-4 md:p-6 lg:p-8">
           <ProductCard
             v-for="product in products"
             :key="product.id"
             :product="product"
+            class="w-full mb-8 sm:w-3/7 lg:w-2/7 2xl:w-7/24 mr-0 sm:mr-8 mb-8"
           />
         </div>
       </div>
@@ -118,6 +207,50 @@ watch(
       </div>
     </div>
   </div>
+  <div
+    v-if="canSyncWishlist && products.length > 0"
+    class="grid grid-cols-1 lg:flex lg:justify-center lg- gap-4 md:gap-6 lg:gap-8 p-4 md:p-6 lg:p-8"
+  >
+    <div class="text-center place-self-center">
+      <SwPagination
+        :total="Number(totalPagesCount)"
+        :current="currentPage"
+        @change-page="changePage"
+      />
+    </div>
+    <div class="text-center place-self-center mt-2 lg:mt-0">
+      <div class="inline-block align-top text-center md:text-left">
+        <label for="limit" class="inline mr-4">{{
+          translations.listing.perPage
+        }}</label>
+        <select
+          id="limit"
+          v-model="limit"
+          name="limitchoices"
+          class="inline appearance-none bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline"
+          @change="changeLimit"
+        >
+          <option :value="1">1 {{ translations.listing.product }}</option>
+          <option :value="15">15 {{ translations.listing.products }}</option>
+          <option :value="30">30 {{ translations.listing.products }}</option>
+          <option :value="45">45 {{ translations.listing.products }}</option>
+        </select>
+        <div
+          class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
+        >
+          <svg
+            class="fill-current h-4 w-4"
+            xmlns="http://www.w3.org/2000/svg"
+            viewBox="0 0 20 20"
+          >
+            <path
+              d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
+            />
+          </svg>
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
 <style scoped></style>


Test patch:
diff --git a/packages/composables/src/useCustomerOrders.test.ts b/packages/composables/src/useCustomerOrders.test.ts
index afac1f29..cfa108ad 100644
--- a/packages/composables/src/useCustomerOrders.test.ts
+++ b/packages/composables/src/useCustomerOrders.test.ts
@@ -20,12 +20,15 @@ describe("useCustomerOrders", () => {
     injections.apiClient.invoke.mockResolvedValue({
       data: { orders: { elements: [] } },
     });
+    vm.limit = 3;
     vm.changeCurrentPage(2);
     expect(injections.apiClient.invoke).toHaveBeenCalledWith(
       expect.stringContaining("readOrder"),
       expect.objectContaining({
         body: {
+          limit: 3,
           page: 2,
+          "total-count-mode": "exact",
         },
       }),
     );
diff --git a/packages/composables/src/useWishlist.test.ts b/packages/composables/src/useWishlist.test.ts
index 7501d2be..a5c393d1 100644
--- a/packages/composables/src/useWishlist.test.ts
+++ b/packages/composables/src/useWishlist.test.ts
@@ -1,37 +1,184 @@
 import { describe, expect, it, vi } from "vitest";
 import { useWishlist } from "./useWishlist";
+import { useSyncWishlist } from "./useSyncWishlist";
+import { useUser } from "./useUser";
 import { useSetup } from "./_test";
+import { computed, ref } from "vue";
+import type { Schemas } from "#shopware";
+
+const getMockedUser = (isLoggedIn: boolean, isGuestSession: boolean) =>
+  ({
+    isLoggedIn: ref(isLoggedIn),
+    isGuestSession: ref(isGuestSession),
+  }) as ReturnType<typeof useUser>;
+
+vi.mock("./useLocalWishlist.ts", () => ({
+  useLocalWishlist() {
+    return {
+      getWishlistProducts: () => undefined,
+      items: {
+        value: [
+          "local-testId-1",
+          "local-testId-2",
+          "local-testId-3",
+          "local-testId-4",
+          "local-testId-5",
+          "local-testId-6",
+          "local-testId-7",
+          "local-testId-8",
+          "local-testId-9",
+          "local-testId-10",
+          "local-testId-11",
+          "local-testId-12",
+          "local-testId-13",
+          "local-testId-14",
+          "local-testId-15",
+          "local-testId-16",
+          "local-testId-17",
+        ],
+      },
+      clearWishlist: () => undefined,
+      count: ref(17),
+    };
+  },
+}));
+
+vi.mock("./useSyncWishlist.ts");
+vi.mock("./useUser");
 
 describe("useWishlist - not logged in user", () => {
-  vi.mock("./useLocalWishlist.ts", () => ({
-    useLocalWishlist() {
-      return {
-        getWishlistProducts: () => undefined,
-        items: { value: ["local-testId333"] },
-        clearWishlist: () => undefined,
-      };
-    },
-  }));
-
-  it("mergeWishlistProducts", async () => {
+  it("mergeWishlistProducts", () => {
+    vi.mocked(useUser).mockReturnValue(getMockedUser(false, true));
+    vi.mocked(useSyncWishlist).mockReturnValue({
+      getWishlistProducts: () => new Promise<void>((resolve) => resolve()),
+      items: computed((): string[] => {
+        return ["test1"];
+      }),
+      addToWishlistSync: () => undefined,
+      mergeWishlistProducts: () => undefined,
+      removeFromWishlistSync: () => undefined,
+      count: computed(() => 17),
+      currentPage: computed(() => 2),
+    });
+    const { vm } = useSetup(() => useWishlist());
+
+    vm.clearWishlist();
+    vm.getWishlistProducts({ page: 2, limit: 15 });
+    vm.mergeWishlistProducts();
+
+    expect(vm.items.length).toBe(17);
+    expect(vm.count).toBe(17);
+    expect(Number(vm.totalPagesCount)).toBe(2);
+  });
+
+  it("getWishlistProducts", async () => {
+    vi.mocked(useUser).mockReturnValue(getMockedUser(false, true));
+    vi.mocked(useSyncWishlist).mockReturnValue({
+      getWishlistProducts: () => new Promise<void>((resolve) => resolve()),
+      items: computed((): string[] => {
+        return ["test1"];
+      }),
+      addToWishlistSync: () => undefined,
+      mergeWishlistProducts: () => undefined,
+      removeFromWishlistSync: () => undefined,
+      count: computed(() => 15),
+      currentPage: computed(() => 2),
+    });
     const { vm } = useSetup(() => useWishlist());
 
-    expect(vm.items.length).toBe(1);
-    expect(vm.count).toBe(1);
-    await vm.clearWishlist();
-    await vm.getWishlistProducts();
-    await vm.mergeWishlistProducts();
+    vm.getWishlistProducts();
+
+    expect(vm.items).toEqual([
+      "local-testId-1",
+      "local-testId-2",
+      "local-testId-3",
+      "local-testId-4",
+      "local-testId-5",
+      "local-testId-6",
+      "local-testId-7",
+      "local-testId-8",
+      "local-testId-9",
+      "local-testId-10",
+      "local-testId-11",
+      "local-testId-12",
+      "local-testId-13",
+      "local-testId-14",
+      "local-testId-15",
+      "local-testId-16",
+      "local-testId-17",
+    ]);
+
+    const query = {
+      limit: 1,
+      page: 2,
+    } as Schemas["Criteria"];
+    vm.getWishlistProducts(query);
+
+    expect(Number(vm.currentPage)).toBe(2);
   });
 });
 
 describe("useWishlist - logged in user", () => {
-  it("mergeWishlistProducts", async () => {
+  it("mergeWishlistProducts", () => {
+    vi.mocked(useUser).mockReturnValue(getMockedUser(true, false));
+    vi.mocked(useSyncWishlist).mockReturnValue({
+      getWishlistProducts: () => new Promise<void>((resolve) => resolve()),
+      items: computed((): string[] => {
+        return [
+          "local-test-1",
+          "local-test-2",
+          "local-test-3",
+          "local-test-4",
+          "local-test-5",
+          "local-test-6",
+          "local-test-7",
+          "local-test-8",
+          "local-test-9",
+          "local-test-10",
+          "local-test-11",
+          "local-test-12",
+          "local-test-13",
+          "local-test-14",
+          "local-test-15",
+        ];
+      }),
+      addToWishlistSync: () => undefined,
+      mergeWishlistProducts: () => undefined,
+      removeFromWishlistSync: () => undefined,
+      count: computed(() => 15),
+      currentPage: computed(() => 1),
+    });
+    const { vm } = useSetup(() => useWishlist());
+
+    vm.clearWishlist();
+    vm.getWishlistProducts();
+    vm.mergeWishlistProducts();
+
+    expect(vm.items.length).toBe(15);
+    expect(vm.count).toBe(15);
+  });
+
+  it("getWishlistProducts", () => {
+    vi.mocked(useUser).mockReturnValue(getMockedUser(true, false));
     const { vm } = useSetup(() => useWishlist());
+    vm.getWishlistProducts();
 
-    expect(vm.items.length).toBe(1);
-    expect(vm.count).toBe(1);
-    await vm.clearWishlist();
-    await vm.getWishlistProducts();
-    await vm.mergeWishlistProducts();
+    expect(vm.items).toEqual([
+      "local-test-1",
+      "local-test-2",
+      "local-test-3",
+      "local-test-4",
+      "local-test-5",
+      "local-test-6",
+      "local-test-7",
+      "local-test-8",
+      "local-test-9",
+      "local-test-10",
+      "local-test-11",
+      "local-test-12",
+      "local-test-13",
+      "local-test-14",
+      "local-test-15",
+    ]);
   });
 });