docs
  1. SCAYLE Resource Center
  2. Support and Resources
  3. Upgrade Guides
  4. Migrate to Storefront v8

Migrate to Storefront v8

This guide provides a comprehensive step-by-step process for migrating your Storefront Boilerplate-based application utilizing the SCAYLE Storefront SDK packages (storefront-core & storefront-nuxt) from version 7 to version 8. This major release marks a significant milestone in the evolution of SCAYLE Storefront, as we've undertaken a substantial architectural overhaul throughout the year. With the arrival of version 8, we're taking the opportunity to streamline the SDK packages, removing legacy code and deprecations, while also sunsetting support for outdated approaches like faceted search (e.g. useFacet). This migration guide will act as your roadmap, ensuring a smooth transition to the enhanced performance, stability, and developer experience that SCAYLE Storefront v8 offers.

The complexity of your migration from SCAYLE Storefront v7 to v8 will depend significantly on the initial version of the SCAYLE Storefront Boilerplate used as the foundation for your project and update frequency. While projects consistently updated with the latest Storefront SDK versions will experience a smoother transition, those based on older versions, particularly Release Candidate (RC) versions of the SCAYLE Storefront Boilerplate, will require additional effort. This may involve manually incorporating architectural changes introduced in subsequent Boilerplate releases. Please assess your project's starting point to accurately estimate the migration scope and resources required.


A Short Overview of Breaking Changes


Unified Storage Configuration: Legacy Storage Configuration Removed

We've streamlined the storage configuration process in v8. The deprecated transformLegacyConfig function has been removed, replaced by the new storefront.storage configuration system.

What this means for you:

These changes might impact your environment variables used for deployments. Please check your infrastructure and deployment setup and adapt accordingly!

Here's a quick example to guide you:

No Longer Supported: Legacy Storage Setup (nuxt.config.ts):

export default {
  // ...
  runtimeConfig: {
    // ...
    storefront: {
      // ...
      redis: {
        host: 'localhost',
        port: 6379,
        prefix: '',
        user: '',
        password: '',
        sslTransit: false,
      },
      // ...
      session: {
        provider: 'redis',
      },
      // ...
    },
    // ...
  },
  // ...
}

Old Environment Variables (used during runtime) / .env:

NUXT_STOREFRONT_REDIS_HOST=''
NUXT_STOREFRONT_REDIS_PORT=''
NUXT_STOREFRONT_REDIS_PREFIX=''
NUXT_STOREFRONT_REDIS_USER=''
NUXT_STOREFRONT_REDIS_PASSWORD=''
NUXT_STOREFRONT_REDIS_SSL_TRANSIT=true|false

NUXT_STOREFRONT_SESSION_PROVIDER='redis'

Migrate from getBadgeLabel: Implement Custom Badge Logic

Starting with v8, we're empowering you to have more control over the display of badge labels within your applications.

What's changing?

  • The getBadgeLabel helper function is no longer exported.
  • You will now implement the logic for generating your badge labels directly within your application code.

A detailed example of creating a "sustainability" badge can be found in our documentation: Getting Started > Quick Start > Create a Sustainability Badge.

Good News for Some: If your project uses the SCAYLE Storefront Boilerplate v1.0 or later, this change won't affect you, as the getBadgeLabel function was already removed in that release.

For users with projects based on older boilerplate versions, or those directly utilizing getBadgeLabel, you can refer to the previous implementation below as a reference for your custom implementation:

Reference Implementation: getBadgeLabel
const BadgeLabel = {
  NEW: 'new',
  SOLD_OUT: 'sold_out',
  ONLINE_EXCLUSIVE: 'online_exclusive',
  SUSTAINABLE: 'sustainable',
  PREMIUM: 'premium',
  DEFAULT: '',
} as const

type BadgeLabelParamsKeys =
  | 'isNew'
  | 'isSoldOut'
  | 'isOnlineOnly'
  | 'isSustainable'
  | 'isPremium'
type BadgeLabelParams = Partial<Record<BadgeLabelParamsKeys, boolean>>

const getBadgeLabel = (params: BadgeLabelParams = {}): string => {
  if (!params) {
    return BadgeLabel.DEFAULT
  }
  const { isNew, isSoldOut, isOnlineOnly, isSustainable, isPremium } = params

  if (isNew) {
    return BadgeLabel.NEW
  } else if (isSoldOut) {
    return BadgeLabel.SOLD_OUT
  } else if (isOnlineOnly) {
    return BadgeLabel.ONLINE_EXCLUSIVE
  } else if (isSustainable) {
    return BadgeLabel.SUSTAINABLE
  } else if (isPremium) {
    return BadgeLabel.PREMIUM
  } else {
    return BadgeLabel.DEFAULT
  }
}

💥 Migrate to shops Configuration (Replacing store)

We're streamlining how you configure multiple countries in v8. While we've been letting the old store configuration option hang around for compatibility, it's time to fully embrace its future proof successor: shops!

What's changing?

  • The store option in your module configuration is officially retired.
  • You'll now use the shops keyword to configure your storefront settings.

This transition to the shops configuration offers a more robust and scalable solution for handling multiple storefronts. You may already be familiar with this change as it was initially introduced in v7.84.0 (released 1. August 2024) on to ensure a smooth migration path.

These changes might impact your environment variables used for deployments. Please check your infrastructure and deployment setup and adapt accordingly!

Need a helping hand with the new configuration? Our documentation has you covered: Storefront Guide / Developer Guide / Basic Setup / Introduction - Shops

Here's a quick example to guide you:

nuxt.config.ts:

export default {
    // ...
    runtimeConfig: {
        // ...
        storefront: {
            // ...
            shops: {
                // ... 
            },
            // ...
        },
        // ...
  },
  // ...
}

New Environment Variable structure (used during runtime) / .env:

NUXT_STOREFRONT_STORE_{UNIQUE_IDENTIFIER}_CHECKOUT_USER=''
NUXT_STOREFRONT_STORE_{UNIQUE_IDENTIFIER}_CHECKOUT_TOKEN=''
NUXT_STOREFRONT_STORE_{UNIQUE_IDENTIFIER}_CHECKOUT_SECRET=''
NUXT_STOREFRONT_STORE_{UNIQUE_IDENTIFIER}_CHECKOUT_HOST=''

💥 Simplified Key Handling in RPC Composables: key value migrated to a dedicated parameter

To better align with the current Nuxt 3 architecture and make key handling more explicit, we've simplified how you interact with the key parameter in RPC composables from version 8 onwards.

What's changing?

  • Key as a Direct Argument: Instead of placing the key within the composable's options object, you'll now provide it as the second argument when calling the composable.

This adjustment promotes consistency and improves code readability by making the key's purpose more explicit.

Here's a quick example to guide you:

useProduct({
  // ...
  key: 'productKey',
})

💥 Updating API Configuration References: Transitioning from bapi to sapi

We've updated our configuration to use sapi (shorthand for Storefront API) consistently, replacing the deprecated bapi keyword for improved clarity and consistency.

What's changing?

  • All instances of bapi in the codebase have been replaced with sapi.
  • The initBapi function has been removed and is no longer available.
  • The bapiClient property been replaced with sapiClient within the RPCContext.

These changes impact your environment variables used for deployments. Please check your infrastructure and deployment setup and adapt accordingly.

To make sure your projects are compatible with the latest version, we recommend updating your codebases as shown in the example:

nuxt.config.ts:

export default {
  // ...
  runtimeConfig: {
    // ...
    storefront: {
      // ...
      bapi: {
        host: '...',
        token: '...',
      },
      // ...
    },
    // ...
  },
  // ...
}

Legacy Environment Variables (used during runtime) / .env:

NUXT_STOREFRONT_BAPI_HOST='...'
NUXT_STOREFRONT_BAPI_TOKEN='...'

💥 Streamlining Cache Control: Replacing AY_CACHE_DISABLED with storefront.cache.enabled

We've simplified the way you manage caching behavior in v8 by replacing the legacy AY_CACHE_DISABLED environment variable with more intuitive options.

What's changing?

  • The AY_CACHE_DISABLED environment variable is no longer supported.
  • You can now control cache behavior using either of the following:
    • The NUXT_STOREFRONT_CACHE_ENABLED environment variable.
    • The storefront.cache.enabled option within your nuxt.config.ts file.

These changes provide greater flexibility and clarity when configuring your cache settings.

Environment Variables (used during runtime) / .env:

AY_CACHE_DISABLED=true|false

💥 Migrating to Enhanced Session Management: Now Opt-In

We've improved session management for multi-storefront setups in @scayle/storefront-nuxt to provide greater stability and ease of implementation.

What's changing?

  • Starting with version @scayle/[email protected] (released on 13 May 2024), each shop utilizes a unique session cookie name instead of relying on the Path attribute. This simplifies handling multiple storefronts and enhances session security.
  • The automatic migration of legacy session data introduced in @scayle/[email protected] (released on 13 May 2024) has been disabled by default and now need to have a feature flag storefront.legacy.enableSessionMigration enabled.
Storefront Config: storefront.legacy.enableSessionMigration

In nuxt.config.ts:

export default {
  runtimeConfig: {
    storefront: {
      // ...
      legacy: {
        enableSessionMigration: true,
      },
      // ...
    },
  },
}

Important considerations for upgrading and ensuring a smooth transition:

Direct upgrades from versions prior to @scayle/[email protected] (released on 13 May 2024) will result in the loss of user sessions without enabling the storefront.legacy.enableSessionMigration feature flag.

  1. Deploy version @scayle/[email protected] (released on 13 May 2024) or higher in your production environment or alternatively deploy version @scalye/[email protected] or higher while enabling storefront.legacy.enableSessionMigration.
  2. Keep this version running for a period exceeding your configured session TTL (time-to-live). This allows all legacy session data to migrate.
  3. Once the session TTL period has passed, you can safely disable storefront.legacy.enableSessionMigration

Cookie Format Changes: This change primarily affects internal session management. However, for reference, here's a comparison of the old and new cookie formats:

Before @scayle/[email protected] (released on 13 May 2024):

Set-Cookie: $session=s:fa3746f9-88c8-4065-a6c9-0c7bee473dd8.pSoaN6Q7iFHHyWKE7s9gQAqdDzGb9fS8a478P7PHLxw; Path=/de

💥 Accessing Basket Data: New Response Structure and Error Handling

This update changes how you interact with the basket through our API. Instead of receiving the basket object directly from certain methods, it will now be nested within the response body. We've also standardized error handling for adding items and improved how our useBasket composable handles partial successes.

What's changing?

  • The following methods no longer return the basket object directly:
    • getBasket
    • removeItemFromBasket
    • addItemsToBasket
    • addItemToBasket
  • The basket object will now be accessible as a property within the response body of these methods.
  • addItemToBasket and addItemsToBasket will now return HTTP 400 for errors.
  • Error details for addItemToBasket and addItemsToBasket can be found in the error property of the response.
  • useBasket now gracefully handles cases where adding items to the basket results in a smaller quantity being added than originally requested by checking against AddToBasketFailureKind.

For reference, here's a comparison of the old and new implementation on how to utilize the newly returned HTTP code and `useBasket` quantity improvements for improved error handling:

Excerpt from utils/basket.ts

import {
  type BasketItem,
  type BasketResponseData,
  FetchError,
  getFirstAttributeValue,
  HttpStatusCode,
} from '@scayle/storefront-nuxt'

// ...

export const getBasketToastErrorMessageKey = (error: unknown) => {
  if (error instanceof FetchError) {
    if (error.response.status === HttpStatusCode.PRECONDITION_FAILED) {
      return 'basket.notification.add_to_basket_variant_out_of_stock_error'
    } else if (error.response.status === HttpStatusCode.CONTENT_TOO_LARGE) {
      return 'basket.notification.add_to_basket_max_basket_items_error'
    }
  }
  return 'basket.notification.add_to_basket_error'
}

// ...

💥 Streamlining Data Fetching parameter: Transitioning to immediate and removing autoFetch

As of @scayle/[email protected] (released 7 June 2024), we've standardized on the immediate option to align with the parameters used in Nuxt 3's useAsyncData function. This change simplifies the data fetching process and ensures greater consistency across our composables.

What's changing?

  • The autoFetch option within useRpc has been deprecated and replaced with immediate.
  • This update affects all data fetching composables offered by @scayle/storefront-nuxt.

Here's a quick example to guide you:

useRpc('rpcMethod', key, params, { autoFetch: true })

useUser({ autoFetch: true })

💥 Accessing User Access Tokens: Migrating to the getAccessToken RPC

We've updated how you can access a user's access token. Instead of directly accessing the storefrontAccessToken field on the UserAuthentication interface, you will now need to utilize the dedicated getAccessToken RPC. This change provides a more secure and streamlined approach to handling user tokens within the application.

What's changing?

  • The storefrontAccessToken field is removed from the UserAuthentication interface.
  • You must now use the getAccessToken RPC to retrieve the user's access token.

Here's a quick example to guide you:

const { data, fetching, fetch, error, status } = useRpc(
  'getUser',
  // ...
)

data.value.user.authentication.storefrontAccessToken

💥 Standardized Error Handling in RPC Methods: BAPIError & BaseError Removed

This update streamlines error handling in RPC methods for better performance and alignment with web standards.

What's changing?

  • We're removing the special handling for BAPIError and BaseError in RPC methods.
  • These custom error classes are now treated like any other Error object.
  • Uncaught BAPIError and BaseError exceptions in RPC methods now result in a standard 500 status code response.
  • BAPIError and BaseError have been removed from @scayle/storefront-nuxt.
  • To specify a custom status code for an RPC method response, you should now return a Response object directly.

How does this impact you?

For most users accustomed to our core RPC methods, this change won't require any action. However, if you've implemented custom RPC methods, we recommend reviewing and updating their error handling to utilize the standard Response object for custom status codes. This approach leverages the native capabilities of the Response object and provides consistency across your application.

To illustrate this change, consider the following example:

function myCustomRpc() {
  // ...
  throw new BaseError(404)
}

💥 Simplified API Access in Multi-Shop Setups: Global API Routing for Path-Based Shops

We've updated how API routes are managed for shops using path-based selection (path or path_except_default).

What's changing?

  • API routes are now mounted globally: Instead of being nested under each shop's path, API routes will be available under a single, global path (default: /api).
  • Shop-level overrides removed: The option to customize the apiBasePath on a per-shop basis has been removed.

This change aims to simplify API route management and improve consistency across multi-shop environments.

If you are purging the cache via the API, make sure you use the correct endpoint and include the X-Shop-Id header.

curl -X POST 'http://localhost:3000/en/api/purge/all'

💥Aligning with Nuxt useAsyncData: Replacing return values pending with status and fetch with refresh

We've given the useRpc composable an upgrade! It now offers a smoother and more powerful data fetching experience, aligning perfectly with the latest Nuxt 3 useAsyncData functionality.

What's changing?

  • Unified Refresh: Say goodbye to the separate fetch function! You can now refresh data using a single, intuitive refresh function on the useRpc composable.1
  • Enhanced Status Insights: The pending boolean flag is replaced by a more detailed status return value. This provides a clearer view of the fetching lifecycle with states like: idle, pending, success, and error.
  • Modernized Interface: The overall interface now mirrors the streamlined approach of Nuxt 3's useAsyncData, making for a more consistent and familiar developer experience.

This update not only impacts the useRpc composable directly, but also extends to other RPC composables relying on it, such as useProducts and useCategories.

Here's a quick example of how to move from refresh() to fetch():

Example from Storefront Boilerplate middleware/authGuard.global.ts:

// ...

  if (!nuxt.ssrContext && !userComposable.user.value) {
    await userComposable.fetch()
  }
  
// ...

Here's another quick example of how to move from pending / fetching to status:

Example from Storefront Boilerplate pages/c/[...categories]/[...slug]-[id].vue:

// template

    // ...

    <CategoryBreadcrumbs
        v-if="currentCategory"
        :products-fetching="productsFetching"
        :category="currentCategory"
        :products-count="totalProductsCount"
    />

    // ...

    <ProductList
        :products="products"
        :pagination="pagination"
        :current-category="currentCategory"
        :loading="productsFetching"
        class="mt-8"
        @click:product="trackProductClick"
        @intersect:row="trackViewListing"
    />

    // ...
// script setup
const {
    products,
    pagination,
    fetching: productsFetching,
    totalProductsCount,
    paginationOffset,
} = useProductsByCategory(currentCategoryId, route, {

// ...

const {
    data: currentCategory,
    fetching: isCategoryFetching,
    error: categoryError,
} = currentCategoryPromise

const validateCategoryExists = async () => {
    await currentCategoryPromise

    if (categoryError.value || (!isCategoryFetching && !currentCategory.value)) {
        throw createError({ statusCode: HttpStatusCode.NOT_FOUND, fatal: true })
    }
}

💥 Moving to Search v2: ReplacingsearchProducts and useSearch

As part of our transition to SCAYLE Search v2, we're relying on a new, streamlined search experience that prioritizes category pages. To simplify integration and ensure consistency, we're consolidating search functionality around the useStorefrontSearch composable from the storefront-nuxt package.

What's changing?

  • Removal of searchProducts RPC method: This method is no longer available.
  • Introduction of getSearchSuggestions: Use this method for retrieving search suggestions provided by the useStorefrontSearch composable from the storefront-nuxt package.
  • Two Primary Suggestion Types:
    • Product Suggestions: Triggered by entering a product ID (productId or referenceKey) in the search bar.
    • Category Suggestions: Appear when typing category-like terms. These suggestions visually indicate applicable filters and redirect to filtered category pages upon selection.
  • Replacement of useSearch with useStorefrontSearch: We're transitioning to the useStorefrontSearch composable for improved search functionality.
  • Introduction of the useSearchData composable: This composable simplifies search interactions by acting as a central proxy for useStorefrontSearch. It manages the searchQuery and provides access to computed search data, debounced suggestions, and resolved routes, enhancing code reusability and maintainability.
  • useStorefrontSearch composable consistency: The composable useStorefrontSearch now is consistent with useRpc return values. The fetching boolean is replaced by status with states idle, pending, error and success.

For detailed implementation guidelines and further information, refer to Storefront Guide / Developer Guide / Features / Search.

Here's a quick example of how to migrate from the searchProducts to getSearchSuggestions RPC method:

Using searchProducts RPC directly:

const getSearchSuggestionsRpc = useRpcCall('searchProducts')

data.value = await searchProducts({
  term: String(searchQuery.value),
  ...params,
})

Here's a quick example of how to migrate from useSearch to useStorefrontSearch:

Using useSearch (Search v1):

const searchTerm = ref()

const { data, resolveSearch } = useStorefrontSearch(searchTerm, { categoryId: ..., with: { ... } }, 'search')
//data contains search suggestions

resolveSearch()
//data contains search results

Here's a quick example of how to adapt the new return value changes of useStorefrontSearchfrom fetching to status:

const {
  data,
  resolveSearch,
  getSearchSuggestions,
  fetching,
  ...searchData
} = useStorefrontSearch(searchQuery, { key })

fetching.value // true or false

💥 Renaming of useNavigationTree: Now useNavigationTreeById

We've updated the useNavigationTree composable for better clarity and functionality.

What's changing?

  • The useNavigationTree composable is now renamed to useNavigationTreeById. Its functionality, parameters and return values stay identical.

This change aims to improve the naming convention, making the purpose of the composable clearer.


💥 Removed Composable useFacet: Using dedicated Category-Specific Composables

The previous reliance on the useFacet composable for category-related product listings often led to unintended complexity. While aiming for versatility, its broad approach to faceting sometimes created excessive abstraction, making it difficult to grasp the underlying logic and implement customized solutions for specific category needs. Additionally, the internal state management within useFacet could sometimes lead to challenges with data consistency and multiple sources of truth, potentially introducing subtle bugs. To address these concerns, we moved to a more focused strategy using dedicated composables.

What's changing?

  • Instead of using useFacet for category-related product listings, you'll now use the specialized composables:
    • useProductsByCategory to fetch products within a specific category.
    • useFilters or useProductListFilters to manage and apply filters to your product list.

How to migrate:

Transitioning to these purpose-built composables provides clearer separation of concerns and potentially improves performance. You can find detailed usage examples and API documentation under Storefront Guide / Developer Guide / Features / Product Listing Page.

Here's a rough example of how to use the dedicated composables:

const {products, category, filters, ...} = useFacet()

We strongly encourage all projects to adopt these new composables as soon as possible. However, if immediate migration proves difficult, you can find the source code for the deprecated useFacet composable in the section below:

Please be aware that useFacet is no longer officially supported. Continuing to use it in your project is considered custom code and falls outside the scope of standard support.

Legacy Implementation: useFacet (Source Code)
import type {
  CacheOptions,
  FilterParams,
  ProductCategoryWith,
  ProductWith,
} from '@scayle/storefront-core'
import { useState } from '#app/composables/state'
import { computed } from 'vue'
import { 
  useCategories, 
  useFilters, 
  useProducts,
   useProductsCount,
} from '#storefront/composables'
import { extendPromise } from '@scayle/storefront-nuxt'

type FacetParams =
  & Partial<{
    with: {
      product?: ProductWith
    }
    includedFilters: string[]
    includeSoldOut: boolean
    includeSellableForFree: boolean
  }>
  & { initialPath?: string }

type Options = Partial<{
  params: FacetParams
  /** @deprecated use the second argument of the composable to define the key */
  key: string
}>

export function useFacet({
  key: optionsKey,
  params = { initialPath: '/' },
}: Options = {}, _key?: string) {
  const key = optionsKey ?? _key ?? 'useFacet'
  // Configurable options
  const currentPath = useState<string>(
    `${key}-path`,
    () => params.initialPath ?? '/',
  )
  const currentPage = useState<number | undefined>(
    `${key}-currentPage`,
    () => 1,
  )
  const currentPerPage = useState(`${key}-itemsPerPage`, () => 20)
  const currentWhereCondition = useState<FilterParams['where']>(
    `${key}-whereCondition`,
  )
  const currentSorting = useState<FilterParams['sort']>(`${key}-sorting`)
  const currentPricePromotionKey = useState<string>(`${key}-pricePromotionKey`)
  const currentCacheParams = useState<CacheOptions | undefined>(
    `${key}-currentCacheParams`,
  )
  const currentOrFiltersOperator = useState<FilterParams['orFiltersOperator']>(
    `${key}-orFilterOperator`,
  )

  const productCountWhere = useState<FilterParams['where']>(
    `${key}-productCountWhereCondition`,
  )

  const categoriesPromise = useCategories({
    params: () => ({
      path: currentPath.value,
      includeHidden: (params.with?.product?.categories as ProductCategoryWith)
        ?.includeHidden || undefined,
    }),
    options: { immediate: false },
    key: `${key}-categories`,
  })

  const productsPromise = useProducts({
    params: () => ({
      page: currentPage.value,
      perPage: currentPerPage.value,
      with: params.with?.product,
      category: currentPath.value,
      includeSoldOut: params.includeSoldOut,
      includeSellableForFree: params.includeSellableForFree,
      where: currentWhereCondition.value,
      sort: currentSorting.value,
      pricePromotionKey: currentPricePromotionKey.value,
      cache: currentCacheParams.value,
      orFiltersOperator: currentOrFiltersOperator.value,
    }),
    options: { immediate: false },
    key: `${key}-products`,
  })

  const productsCountPromise = useProductsCount({
    params: () => ({
      category: currentPath.value,
      includedFilters: params.includedFilters,
      includeSoldOut: params.includeSoldOut,
      includeSellableForFree: params.includeSellableForFree,
      where: productCountWhere.value,
      orFiltersOperator: currentOrFiltersOperator.value,
    }),
    options: { immediate: false },
    key: `${key}-productCount`,
  })

  const filtersPromise = useFilters({
    params: () => ({
      category: currentPath.value,
      includedFilters: params.includedFilters,
      includeSoldOut: params.includeSoldOut,
      includeSellableForFree: params.includeSellableForFree,
      where: currentWhereCondition.value,
      orFiltersOperator: currentOrFiltersOperator.value,
    }),
    options: { immediate: false },
    key: `${key}-filters`,
  })

  const {
    data: categoryData,
    fetch: refreshCategories,
    fetching: categoriesFetching,
    error: categoriesError,
    status: categoriesStatus,
  } = categoriesPromise

  const {
    data: productData,
    fetch: _refreshProducts,
    fetching: productsFetching,
    error: productError,
    status: productStatus,
  } = productsPromise

  const {
    data: productCountData,
    fetch: _refreshProductCount,
    fetching: productCountFetching,
    error: productCountError,
    status: productCountStatus,
  } = productsCountPromise

  const {
    data: filterData,
    fetch: refreshFilters,
    fetching: filtersFetching,
    error: filterError,
    status: filterStatus,
  } = filtersPromise

  const categories = computed(() => categoryData.value?.categories)
  const selectedCategory = computed(() => categoryData.value?.activeNode)

  const pagination = computed(() => productData.value?.pagination)
  const products = computed(() => productData.value?.products)

  // const filters = computed(() => filtersData.value?.filters)
  const filters = computed(() => filterData.value?.filters)
  const unfilteredCount = computed(() => filterData.value?.unfilteredCount)

  type ExtendedFilterParams = FilterParams & {
    path: string
    pricePromotionKey?: string
    cache?: CacheOptions
  }

  const refreshProductCount = async ({
    where = undefined,
  }: FilterParams = {}): Promise<void> => {
    productCountWhere.value = where
    if (productCountFetching.value) {
      return
    }

    await _refreshProductCount()
  }

  const fetchProducts = async ({
    path,
    page = 1,
    perPage = 20,
    where = undefined,
    sort = undefined,
    pricePromotionKey = '',
    cache,
    orFiltersOperator = undefined,
  }: ExtendedFilterParams) => {
    currentPath.value = path
    currentPage.value = page
    currentPerPage.value = perPage
    currentWhereCondition.value = where
    currentSorting.value = sort
    currentPricePromotionKey.value = pricePromotionKey
    currentCacheParams.value = cache
    currentOrFiltersOperator.value = orFiltersOperator

    if (
      categoriesFetching.value ||
      productsFetching.value ||
      filtersFetching.value
    ) {
      return
    }

    await Promise.all([
      refreshCategories(),
      _refreshProducts(),
      refreshFilters(),
    ])

    return true
  }

  const filterProducts = async ({
    where = undefined,
    page,
    sort = undefined,
    orFiltersOperator = undefined,
  }: FilterParams) => {
    currentWhereCondition.value = where
    currentPage.value = page
    currentSorting.value = sort
    currentOrFiltersOperator.value = orFiltersOperator

    await _refreshProducts()
  }

  const fetchPage = async (pageToFetch: number) => {
    currentPage.value = pageToFetch
    await _refreshProducts()
  }

  return extendPromise(
    Promise.all([
      categoriesPromise,
      productsPromise,
      productsCountPromise,
      filtersPromise,
    ]).then(() => ({})),
    {
      // filter data
      filters,
      filtersFetching,
      unfilteredCount,
      filterStatus,
      filterError,

      // filter preview
      productCountData,
      refreshProductCount,
      productCountFetching,
      productCountError,
      productCountStatus,

      // product data
      products,
      pagination,
      productsFetching,
      filterProducts,
      productError,
      productStatus,

      // category data
      categories,
      selectedCategory,
      categoriesFetching,
      categoriesError,
      categoriesStatus,

      // other
      fetchProducts,
      fetchPage,
    },
  )
}

💥 Removed Composable useQueryFilterState: Improving Filter Management with useFilter and useAppliedFilters

The previous useQueryFilterState composable, while functional, lacked the granularity needed for streamlined and transparent filter management within our storefront applications. To address this, we're introducing two new composables: useFilter and useAppliedFilters. This separation allows for a more organized and understandable approach to handling filters, making it easier to customize their behavior and integrate them seamlessly into your components.

What's changing?

  • We're replacing the useQueryFilterState composable with two more specialized options:
    • useFilter (see Reference Implementation below) provides a centralized way to fetch available filters, apply selections, reset filters, and manage filter-related logic. The exact application process depends on the type of filter being used.
    • useAppliedFilters (from @scayle/storefront-product-listing) offers a direct way to access and work with the currently applied filters in the product listing context. It formats these active filters into a ProductSearchQuery object, crucial for fetching filtered results and understanding user selections. Additionally, it provides computed properties like the total count of applied filters.

Understanding the New Filter Behavior

The order of filters displayed in the Storefront Boilerplate is determined by a combination of the SCAYLE Panel configuration and the order of filters returned by the Storefront API. For more details check Storefront Guide / Developer Guide / Features / Pages / Product Listing Page.

const {
  activeFilters,
  applyFilters,
  resetFilterUrl,
  productConditions,
} = useQueryFilterState()

applyFilters({ brand: 23, sale: true, maxPrice: 100, minPrice: 0 })
Reference Implementation: useFilter.ts
import { extendPromise } from '@scayle/storefront-nuxt'
import { type MaybeRefOrGetter, readonly, ref } from 'vue'
import type { LocationQuery } from 'vue-router'
import type { RangeTuple } from '@scayle/storefront-product-listing'
import { useTrackingEvents, useToast } from '~/composables'
import { useRoute, useRouter } from '#app/composables/router'
import { useI18n } from '#i18n'
import {
  useProductListFilter,
  getNewQueryFilters,
  getClearedFilterQueryByKey,
  createNewBoolAttributeQuery,
  createNewAttributeQuery,
  createNewPriceQuery,
  useAppliedFilters,
} from '#storefront-product-listing'

export function useFilter(
  currentCategoryId?: MaybeRefOrGetter<number | undefined>,
  options: { immediate?: boolean; keyPrefix?: string } = {},
) {
  const route = useRoute()
  const router = useRouter()
  const areFiltersCleared = ref(false)
  const areFiltersUpdated = ref(false)

  const { appliedFiltersCount, appliedFilter } = useAppliedFilters(route)

  const sort = ref(route.query.sort)

  const filterData = useProductListFilter(route, currentCategoryId, options)

  const { clearedPriceQuery } = filterData

  const { trackFilterApply, trackFilterFlyout } = useTrackingEvents()
  const { t } = useI18n()

  const { show } = useToast()

  const applyAttributeFilter = async (slug: string, id: number) => {
    const filters = createNewAttributeQuery(route, appliedFilter.value, {
      slug,
      id,
    })
    trackFilterApply(slug, id.toString())
    await applyFilters(filters)
  }

  const applyBooleanFilter = async (slug: string, value: boolean) => {
    const filters = createNewBoolAttributeQuery(route, appliedFilter.value, {
      slug,
      value,
    })
    trackFilterApply(slug, value.toString())
    await applyFilters(filters)
  }

  const applyPriceFilter = async (prices: RangeTuple) => {
    const filters = createNewPriceQuery(route, appliedFilter.value, prices)
    trackFilterApply('prices', prices.join(','))
    await applyFilters(filters)
  }

  const onSlideInOpen = () => trackFilterFlyout('open', 'true')

  const onSlideInClose = () => {
    trackFilterFlyout('close', 'true')
    const isSortUpdated = sort.value !== route.query.sort

    if (areFiltersUpdated.value && isSortUpdated) {
      show(t('filter.updated_notification_all'), { type: 'SUCCESS' })
      areFiltersUpdated.value = false
      sort.value = route.query.sort
    } else if (areFiltersUpdated.value) {
      show(t('filter.updated_notification_filter'), { type: 'SUCCESS' })
      areFiltersUpdated.value = false
    } else if (isSortUpdated) {
      show(t('filter.updated_notification_sort'), { type: 'SUCCESS' })
      sort.value = route.query.sort
    }
  }

  const resetPriceFilter = async () => {
    await applyFilters(clearedPriceQuery.value)
  }

  const resetFilter = async (key: string) => {
    await applyFilters(getClearedFilterQueryByKey(route, key))
  }

  const resetFilters = async () => {
    await applyFilters({})
    areFiltersCleared.value = true
    areFiltersUpdated.value = false

    setTimeout(() => {
      areFiltersCleared.value = false
    }, 3000)
  }

  const applyFilters = async (filter?: LocationQuery, scrollToTop = true) => {
    if (!filter) {
      return
    }

    // Should not apply reset all filter if appliedFilter is empty
    if (!appliedFiltersCount.value && !Object.keys(filter).length) {
      return
    }
    const { page, ...query } = getNewQueryFilters(route, filter)
    await router.push({ query })

    areFiltersUpdated.value = true

    if (scrollToTop) {
      window.scroll({ behavior: 'smooth', top: 0 })
    }
  }

  return extendPromise(filterData, {
    onSlideInOpen,
    onSlideInClose,
    applyPriceFilter,
    applyBooleanFilter,
    applyAttributeFilter,
    trackFilterFlyout,
    resetFilters,
    resetPriceFilter,
    resetFilter,
    areFiltersCleared: readonly(areFiltersCleared),
  })
}
Reference Implementation: useFilter.test.ts
import { describe, it, vi, expect, beforeEach } from 'vitest'
import type { CentAmount } from '@scayle/storefront-nuxt'
import { useFilter } from './useFilter'

const mocks = vi.hoisted(() => {
  return {
    route: { query: {} },
    router: { push: vi.fn() },
    useAppliedFilters: {
      productConditions: { value: '' },
      appliedFilter: { value: {} },
      appliedFiltersCount: { value: 0 },
    },
    useTrackingEvents: {
      trackFilterApply: vi.fn(),
      trackFilterFlyout: vi.fn(),
    },
    useProductListFilter: { clearedPriceQuery: { value: {} } },
    useToast: { show: vi.fn() },
    useI18n: { t: vi.fn().mockImplementation((key) => key) },
  }
})

vi.mock('#app/composables/router', () => ({
  useRoute: vi.fn().mockReturnValue(mocks.route),
  useRouter: vi.fn().mockReturnValue(mocks.router),
}))

vi.mock('#i18n', () => ({
  useI18n: vi.fn().mockReturnValue(mocks.useI18n),
}))

vi.mock('~/composables', () => ({
  useTrackingEvents: vi.fn().mockReturnValue(mocks.useTrackingEvents),
  useToast: vi.fn().mockReturnValue(mocks.useToast),
}))

vi.mock('#storefront-product-listing', async () => {
  const actual = await vi.importActual('#storefront-product-listing')

  return {
    ...actual,
    useProductListFilter: vi.fn().mockReturnValue(mocks.useProductListFilter),
    useAppliedFilters: vi.fn().mockReturnValue(mocks.useAppliedFilters),
  }
})

describe('useFilter', () => {
  beforeEach(() => {
    mocks.useAppliedFilters.appliedFilter.value = { attributes: [] }
    mocks.useAppliedFilters.appliedFiltersCount.value = 0
    mocks.route.query = {}

    vi.clearAllMocks()
  })

  describe('onSlideInClose', () => {
    it('should show toast message on filter applied and modal closed', async () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [
          { key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
        ],
      }

      const { applyAttributeFilter, onSlideInClose } = useFilter()
      await applyAttributeFilter('newAttribute', 1)
      onSlideInClose()
      expect(mocks.useToast.show).toBeCalledWith(
        'filter.updated_notification_filter',
        { type: 'SUCCESS' },
      )
    })
    it('should show toast message on sort applied and modal closed', async () => {
      mocks.route.query = {
        sort: 'price',
      }

      const { onSlideInClose } = useFilter()
      mocks.route.query = {
        sort: 'new',
      }
      onSlideInClose()
      expect(mocks.useToast.show).toBeCalledWith(
        'filter.updated_notification_sort',
        { type: 'SUCCESS' },
      )
    })

    it('should show toast message on filter and sort applied and modal closed', async () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [
          { key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
        ],
      }
      mocks.route.query = {
        sort: 'price',
      }

      const { applyAttributeFilter, onSlideInClose } = useFilter()
      mocks.route.query = {
        ...mocks.route.query,
        sort: 'new',
      }
      await applyAttributeFilter('newAttribute', 1)
      onSlideInClose()
      expect(mocks.useToast.show).toBeCalledWith(
        'filter.updated_notification_all',
        { type: 'SUCCESS' },
      )
    })

    it('should not show toast message on modal closed', async () => {
      const { onSlideInClose } = useFilter()
      onSlideInClose()
      expect(mocks.useToast.show).not.toBeCalled()
    })
  })

  describe('applyAttributeFilter', () => {
    it('should add new filter and value to query', () => {
      mocks.useAppliedFilters.appliedFilter.value = { attributes: [] }

      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 1)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newAttribute]': '1',
        },
      })
    })

    it('should add new filter and value to query and merge with existing query', () => {
      mocks.useAppliedFilters.appliedFilter.value = { attributes: [] }
      mocks.route.query = {
        'filters[otherAttributes]': '1, 2, 3',
        sort: 'asc',
        term: 'term',
      }
      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 1)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newAttribute]': '1',
          'filters[otherAttributes]': '1, 2, 3',
          sort: 'asc',
          term: 'term',
        },
      })
    })

    it('should add new value to query', () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [
          { key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
        ],
      }

      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 1)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newAttribute]': '2,3,4,1',
        },
      })
    })

    it('should remove existing value from query', () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [
          { key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
        ],
      }

      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 2)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newAttribute]': '3,4',
        },
      })
    })

    it('should remove existing value and filter from query', () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [{ key: 'newAttribute', values: [2], type: 'attributes' }],
      }

      mocks.useAppliedFilters.appliedFiltersCount.value = 1

      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 2)
      expect(mocks.router.push).toBeCalledWith({ query: {} })
    })

    it('should reset page param when attribute filter is applied', () => {
      mocks.route.query = {
        page: '1',
      }

      const { applyAttributeFilter } = useFilter()
      applyAttributeFilter('newAttribute', 1)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newAttribute]': '1',
        },
      })
    })
  })

  describe('applyBooleanFilter', () => {
    it('should add boolean filter and value to query if true', () => {
      const { applyBooleanFilter } = useFilter()
      applyBooleanFilter('newBool', true)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newBool]': 'true',
        },
      })
    })

    it('should remove boolean filter and value from query if false', () => {
      mocks.useAppliedFilters.appliedFiltersCount.value = 1
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [{ key: 'newBool', value: true, type: 'boolean' }],
      }

      const { applyBooleanFilter } = useFilter()
      applyBooleanFilter('newBool', false)
      expect(mocks.router.push).toBeCalledWith({
        query: {},
      })
    })

    it('should reset page param when boolean filter is applied', () => {
      mocks.route.query = {
        page: '1',
      }

      const { applyBooleanFilter } = useFilter()
      applyBooleanFilter('newBool', true)
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[newBool]': 'true',
        },
      })
    })
  })

  describe('applyPriceFilter', () => {
    it('should apply prices correctly', () => {
      const { applyPriceFilter } = useFilter()
      applyPriceFilter([99 as CentAmount, 2345 as CentAmount])
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[minPrice]': '99',
          'filters[maxPrice]': '2345',
        },
      })
    })

    it('should reset page param when price filter is applied', () => {
      mocks.route.query = {
        page: '1',
      }

      const { applyPriceFilter } = useFilter()
      applyPriceFilter([99 as CentAmount, 2345 as CentAmount])
      expect(mocks.router.push).toBeCalledWith({
        query: {
          'filters[minPrice]': '99',
          'filters[maxPrice]': '2345',
        },
      })
    })
  })

  describe('resetFilter', () => {
    it('should reset correctly', () => {
      mocks.useAppliedFilters.appliedFilter.value = {
        attributes: [
          { key: 'newAttribute', values: [1, 2, 3, 4], type: 'attributes' },
        ],
      }
      mocks.useAppliedFilters.appliedFiltersCount.value = 1
      mocks.route.query = {
        'filters[newAttribute]': [1, 2, 3, 4],
        page: '1',
      }
      const { resetFilter } = useFilter()
      resetFilter('newAttribute')
      expect(mocks.router.push).toBeCalledWith({
        query: {},
      })
    })

    it('should not reset if slug is not applied', () => {
      const { resetFilter } = useFilter()
      resetFilter('newAttribute')
      expect(mocks.router.push).not.toBeCalledWith({
        query: {},
      })
    })
  })

  describe('resetPriceFilter', () => {
    it('should reset correctly', () => {
      mocks.useAppliedFilters.appliedFiltersCount.value = 1
      mocks.route.query = {
        'filters[minPrice]': '1',
        'filters[maxPrice]': '12',
      }
      const { resetPriceFilter } = useFilter()
      resetPriceFilter()
      expect(mocks.router.push).toBeCalledWith({
        query: {},
      })
    })

    it('should not reset if price is not applied', () => {
      const { resetPriceFilter } = useFilter()
      resetPriceFilter()
      expect(mocks.router.push).not.toBeCalledWith({
        query: {},
      })
    })
  })

  describe('resetFilters', () => {
    it('should reset all correctly', () => {
      mocks.useAppliedFilters.appliedFiltersCount.value = 3
      mocks.route.query = {
        'filters[minPrice]': '1',
        'filters[maxPrice]': '12',
        'filters[attribute]': '[1,2,3,4,]',
        'filters[sale]': true,
      }
      const { resetFilters } = useFilter()
      resetFilters()
      expect(mocks.router.push).toBeCalledWith({
        query: {},
      })
    })

    it('should not reset if no filter is not applied', () => {
      const { resetFilters } = useFilter()
      resetFilters()
      expect(mocks.router.push).not.toBeCalledWith({
        query: {},
      })
    })
  })
})

We strongly encourage a swift transition to useFilter and useAppliedFilters for a more robust filter experience. However, if immediate migration presents difficulties, you can find the source code for the deprecated useQueryFilterState in the following section:

Please be aware that useQueryFilterState is no longer officially supported. Continuing its use in your project is considered custom code and falls outside the scope of standard support.

Legacy Implementation: useQueryFilterState (Source Code)
import {
  type FilterParams,
  type TransformedFilter,
  type Filter,
  isFilterActive,
  transformToWhereCondition,
  serializeFilters,
  deserializeFilters,
  type SortingValueKey,
  getSortByValue,
} from '@scayle/storefront-core'
import { computed } from 'vue'
import { useRoute, useRouter } from '#app/composables/router'
import type { LocationQuery, LocationQueryValue } from '#vue-router'

const DEFAULT_QUERY_KEYS = ['page', 'sort', 'term']

type Options = {
  defaultSort?: SortingValueKey
}

function queryValueToString(
  value: LocationQueryValue | LocationQueryValue[],
): string | undefined {
  return Array.isArray(value)
    ? value.map(v => v?.toString()).filter(Boolean).join(',')
    : value?.toString()
}

const isEqual = <T>(x: T, y: T): boolean => {
  if (Object.is(x, y)) {
    return true
  }

  // For non-objects or null, use strict equality
  if (
    typeof x !== 'object' || x === null || typeof y !== 'object' || y === null
  ) {
    return x === y
  }

  // Compare objects
  const keys = Object.keys(x) as (keyof T)[]
  return keys.length === Object.keys(y).length &&
    keys.every(key => Reflect.has(y, key) && isEqual(x[key], y[key]))
}

export const getCurrentPage = (query: LocationQuery): number | undefined => {
  return typeof query.page === 'string' ? parseInt(query.page, 10) : undefined
}

/** @deprecated The Storefront Boilerplate contains a `useQueryFilterState` composable you can use instead. **/
export function useQueryFilterState(options: Options = {}) {
  const router = useRouter()
  const route = useRoute()

  const activeFilters = computed(() => {
    const allowedFilters = Object.fromEntries(
      Object.entries(route.query)
        .filter(
          ([key]) => !DEFAULT_QUERY_KEYS.includes(key),
        )
        .map(([key, value]) => [key, queryValueToString(value)])
        .filter(([_key, value]) => !!value),
    )

    return deserializeFilters(allowedFilters)
  })

  const isActiveFilter = (filter?: TransformedFilter) => {
    if (!filter) {
      return Object.values(activeFilters.value).length === 0
    }

    return isFilterActive(activeFilters.value, filter)
  }

  const applyFilters = async (filter?: Filter, scrollToTop = true) => {
    const newQuery = {
      sort: route.query.sort,
      term: route.query.term,

      ...(filter ? serializeFilters(filter) : {}),
    }

    if (isEqual(route.query, newQuery)) {
      return
    }

    await router.push({
      query: { ...newQuery },
    })

    if (scrollToTop) {
      window.scroll({
        behavior: 'smooth',
        top: 0,
      })
    }
  }

  const resetFilterUrl = async () => {
    await router.replace({ query: { term: route.query.term } })
  }

  const generateFilterParams = (): FilterParams => {
    return {
      where: { ...transformToWhereCondition(activeFilters.value) },
      page: getCurrentPage(route.query),
      sort: getSortByValue(route.query?.sort || '', options?.defaultSort),
    }
  }

  return {
    activeFilters,
    applyFilters,
    isActiveFilter,
    resetFilterUrl,
    productConditions: computed(() => generateFilterParams()),
  }
}

💥 Simplified IDP Integration: Using loginIDP for Callback Handling

We've optimized the Identity Provider (IDP) login process by replacing the handleIDPLoginCallback RPC method from the useIDP composable with the loginIDP function within the useAuthentication composable.

What's changing?

  • The handleIDPLoginCallback RPC method is removed from the useIDP composable.
  • Use the loginIDP function from the useAuthentication composable for handling IDP login callbacks.

Here's a quick guide on updating your implementation:

const { handleIDPLoginCallback } = await useIDP()

watch(
  () => route.query,
  async (query) => {
    if (query.code && isString(query.code)) {
      await handleIDPLoginCallback(query.code)
    }
  },
  { immediate: true },
)

💥 RpcContext Cleanup: Removing Legacy Attributes

We're cleaning up the RpcContext a bit to simplify its structure.

What's changing?

  • The isCmsPreview attribute has been removed from RpcContext.
  • The storeCampaignKeyword attribute has been removed from RpcContext.
    • The campaignKey is now automatically determined by fetching campaign data through the Storefront API client by retrieving a list of all campaigns from the API. It then narrows down the list by filtering for campaigns that are still active, ensuring any returned campaign is currently running. These active campaigns are then sorted by their start date, ensuring chronological order. Finally, it iterates through the sorted campaigns to find the first one that is currently active, returning its key as an identifier. If no active campaign is found or an error occurs, it returns nothing.
    • The storeCampaignKeyword is also no longer used to determine the active campaign key. The functionality to run only certain campaigns for specific countries is now supported through the SCAYLE Panel out of the box.

💥 Enhanced Key Security: Using SHA256 for Basket and Wishlist Key Generation

We've enhanced security for basket and wishlist keys by switching the default hashing algorithm from MD5 to the more robust SHA256.

What's changing?

  • Basket and wishlist keys are now generated using SHA256 by default.
  • You can customize the hashing algorithm back to MD5 if needed through the storefront.appKeys.hashAlgorithm property within your nuxt.config.ts file.
export default defineNuxtConfig({
  // ...
  runtimeConfig: {
    // ...
    storefront: {
      // ...
      appKeys: {
        // ...
        hashAlgorithm: HashAlgorithm.MD5
      },
      // ...
    },
    // ...
  },
  // ...
})

💥 Simplified Composable Caching: enableDefaultGetCachedDataOverride Replaces disableDefaultGetCachedDataOverride

We've clarified the configuration option for controlling default caching behavior in composables and made a slight adjustment to its logic, especially regarding the handling of shared state with useRpc.

What's changing?

  • Shared State: By default, useRpc utilizes a shared cache. This means all instances called with the same key will share the same data.
  • Configuration Rename: The configuration option disableDefaultGetCachedDataOverride has been renamed to enableDefaultGetCachedDataOverride.
  • Reversed Logic:
    • When enableDefaultGetCachedDataOverride is false (Default):
      • The default caching behavior of Nuxt is used. This differs from v7 which overrode this behavior by default.
      • During SSR, useAsyncData prioritizes data fetching from Nuxt's internal data stores (nuxtApp.payload.data for SSR based on the provided key.
      • If no SSR data is found, it falls back to cached data from previous requests stored in nuxtApp._asyncData[key]?.data.value.
    • When enableDefaultGetCachedDataOverride is true:
      • The default caching mechanism during SSR is overridden. This is identical to the default behavior in v7.
      • Instead of using the built-in hydration data, a Storefront-specific getCachedData function will be used to fetch cached data:
(key, nuxtApp) => {
  const hydrationData = nuxtApp.isHydrating
    ? nuxtApp.payload.data[key]
    : nuxtApp.static.data[key]
  return hydrationData ??
    nuxtApp._asyncData[key]?.data.value as ResponseT
}

To maintain your currently used caching behavior:

Simply change the value of disableDefaultGetCachedDataOverride to its opposite in your nuxt.config.ts file (e.g., from false to true).

export default defineNuxtConfig({
  // ...
  runtimeConfig: {
    // ...
    public: {
      // ...
      storefront: {
        // ...
        disableDefaultGetCachedDataOverride: true,
      },
      // ...
    },
    // ...
  },
  // ...
})

💥 Migrating User Authentication: Removing deprecated loginShopId Attribute

This update removes the reliance on the loginShopId attribute within the ShopUser interface for managing user shop associations. We're transitioning to a more streamlined approach using session cookies.

What's changing?

  • The loginShopId attribute will be removed from the ShopUser interface.
  • User shop association will now be managed through session cookies.

Footnotes

  1. You can now refresh data using the refresh function on the useRpc composable. ↩